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) orogr2ogr -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 class3and class7into a meaningless5.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 -tapfixes the output pixel size and snaps the grid to round coordinates so tiles align cleanly for later mosaicking.-multi -wo NUM_THREADS=ALL_CPUSparallelises 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.
gdalinfoon 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/-dstnodatawas missing. - Categorical integrity. For thematic rasters,
gdalinfo -hist(or compare unique values) should show no new class codes introduced — proof that-r nearwas used. - Spot-check known points with
gdaltransformto 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_srsused wheregdalwarp -t_srswas needed; data ends up displaced. Transform when the source CRS is correct. - Bilinear on land cover. Invents nonexistent class codes. Use
-r nearfor categorical data. - Missing NoData flags. Resampling smears fill into real cells along edges and voids. Declare
-srcnodataand-dstnodata. - Misaligned tiles after batch. Outputs don't share a grid origin, so a later mosaic shows half-pixel seams. Add
-tapand a fixed-tr. - Silent missing datum grids. PROJ falls back to a less accurate transformation. Check
projinfoand 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
- Reproducible GIS Pipelines
- Command Line GIS for Consultants
- SQL for Spatial Quality Control
- Shapefile vs GeoPackage vs GeoJSON
- Spatial data products