Short answer

Batch reprojection is reliable when each file is warped with the same explicit recipe: a correct source and target CRS, a resampling method matched to the data type, and NoData declared on both sides so edges are not contaminated. gdalwarp handles rasters, ogr2ogr handles vectors, and a short shell or PowerShell loop applies the recipe across hundreds of files. The two errors that ruin a batch are using nearest-neighbour on continuous data (or bilinear on categorical data), and forgetting NoData so resampling smears fill values into real cells.

Reproject vs relabel — they are not the same operation

There are two distinct things people call "changing the projection":

  • Reprojection (transformation): recompute coordinates from the source CRS into the target CRS, applying any datum shift. This is gdalwarp -t_srs / ogr2ogr -t_srs. The data physically moves.
  • Relabel (assign): change the CRS tag without touching coordinates, appropriate only when the file's CRS metadata is missing or wrong but the coordinates are already correct. This is gdal_edit.py -a_srs (raster) or ogr2ogr -a_srs (vector).

Mixing them up is the most damaging reprojection mistake: relabelling data that needed transforming leaves it geographically displaced while reporting a valid CRS. If the source CRS is correct, always transform. Relabel only to repair missing/incorrect metadata on data you have confirmed is already in that CRS.

Resampling: match the method to the data

When you reproject a raster, output cells rarely line up with input cells, so values must be resampled. The method must respect the data's nature:

  • near (nearest neighbour) — for categorical/thematic rasters: land cover, geology classes, soil types, masks. It copies the closest source value, so no new (invalid) class codes are invented. Using bilinear here would average class 3 and class 7 into a meaningless 5.
  • bilinear — for continuous data: DEMs, temperature, smooth imagery. Weighted average of the 4 nearest cells; smooth and fast.
  • cubic / cubicspline — smoother continuous results, good for elevation and imagery where you want clean derivatives downstream.
  • average — when downsampling continuous data to coarser resolution, it represents the source better than picking one cell.

This single choice is the difference between a usable layer and an aliased or blurred one.

NoData: declare it on both sides

During resampling, GDAL blends neighbouring cells. If NoData is not declared, the fill value (often a large number or 0) gets averaged with real data along every void and the outer edge, producing a contaminated ring of bogus values. Always pass both:

gdalwarp -srcnodata -9999 -dstnodata -9999 ...

-srcnodata tells GDAL which input values to ignore during resampling; -dstnodata flags them in the output so the next tool also respects them.

A single-file recipe, then the batch

Start from a recipe that works for one raster:

gdalwarp \
  -s_srs EPSG:4326 -t_srs EPSG:32633 \
  -r bilinear \
  -tr 30 30 -tap \
  -srcnodata -9999 -dstnodata -9999 \
  -of GTiff -co COMPRESS=DEFLATE -co PREDICTOR=2 -co TILED=YES \
  -multi -wo NUM_THREADS=ALL_CPUS \
  -overwrite \
  input.tif output.tif
  • -tr 30 30 -tap fixes the output pixel size and snaps the grid to round coordinates so tiles align cleanly for later mosaicking.
  • -multi -wo NUM_THREADS=ALL_CPUS parallelises the warp.
  • Compression and tiling keep outputs small and cloud-friendly.

Batch over rasters (bash)

mkdir -p out
for f in in/*.tif; do
  name=$(basename "$f")
  gdalwarp -t_srs EPSG:32633 -r bilinear -tr 30 30 -tap \
           -srcnodata -9999 -dstnodata -9999 \
           -co COMPRESS=DEFLATE -co TILED=YES -overwrite \
           "$f" "out/$name"
done

Batch on Windows (PowerShell)

New-Item -ItemType Directory -Force -Path out | Out-Null
Get-ChildItem in\*.tif | ForEach-Object {
  gdalwarp -t_srs EPSG:32633 -r bilinear -tr 30 30 -tap `
           -srcnodata -9999 -dstnodata -9999 `
           -co COMPRESS=DEFLATE -co TILED=YES -overwrite `
           $_.FullName ("out\" + $_.Name)
}

Batch vectors with ogr2ogr

for f in in/*.shp; do
  name=$(basename "${f%.shp}")
  ogr2ogr -f GPKG "out/${name}.gpkg" "$f" -t_srs EPSG:32633 -nln "$name" -makevalid
done

-makevalid repairs invalid geometries on the way through, and writing to GeoPackage avoids the shapefile field-name and sidecar problems.

Datum shifts: confirm the coordinates actually moved

When source and target use different datums (e.g. NAD27 → NAD83, or a local datum → WGS84), the transformation includes a datum shift that can be tens to hundreds of metres. GDAL/PROJ applies it automatically when -s_srs and -t_srs are correct, choosing a transformation pipeline (sometimes downloading grid-shift files). To prove a shift happened rather than a silent relabel:

echo "500000 4649776" | gdaltransform -s_srs EPSG:4267 -t_srs EPSG:4326

If input and output coordinates are identical for a cross-datum pair, the transformation was not applied — check that -s_srs is the true source CRS and that PROJ has the needed transformation grids (projinfo -s EPSG:4267 -t EPSG:4326 lists available pipelines and any missing grids). Set PROJ_DEBUG=2 to see which pipeline PROJ selected.

Validation

  • Compare extents and pixel size. gdalinfo on a few outputs: the CRS should be the target, the pixel size should be your -tr, and the extent should be the reprojected footprint.
  • Check the edges and voids. Confirm NoData is preserved and there is no contaminated ring — a sign -srcnodata/-dstnodata was missing.
  • Categorical integrity. For thematic rasters, gdalinfo -hist (or compare unique values) should show no new class codes introduced — proof that -r near was used.
  • Spot-check known points with gdaltransform to confirm the datum shift magnitude is plausible.
  • Log the recipe. Keep the script and the source-data versions so the whole batch is reproducible.

Common pitfalls and why they happen

  • Relabelling instead of transforming. gdal_edit -a_srs used where gdalwarp -t_srs was needed; data ends up displaced. Transform when the source CRS is correct.
  • Bilinear on land cover. Invents nonexistent class codes. Use -r near for categorical data.
  • Missing NoData flags. Resampling smears fill into real cells along edges and voids. Declare -srcnodata and -dstnodata.
  • Misaligned tiles after batch. Outputs don't share a grid origin, so a later mosaic shows half-pixel seams. Add -tap and a fixed -tr.
  • Silent missing datum grids. PROJ falls back to a less accurate transformation. Check projinfo and install grids if accuracy matters.

Bathyl perspective

We treat reprojection as a recorded transformation, not a quiet metadata edit. A single warp recipe — correct source and target CRS, data-appropriate resampling, explicit NoData, a snapped grid — is applied across the whole batch by a script that doubles as documentation. The reprojected product can then be regenerated and its datum handling audited, rather than trusted because the files opened.

Related reading

Sources