Reprojection and CRS
This chapter covers raster, vector, and lidar reprojection in WbW-Py.
CRS handling is a semantic correctness issue, not just a metadata preference. When coordinate assumptions are wrong, analyses may still run but yield invalid spatial conclusions. The patterns here emphasize explicit source/destination CRS checks, controlled reprojection settings, and immediate post-transform verification before any downstream computation.
CRS Inspection
Run this first to verify assumptions before any reprojection call.
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
dem = wbe.read_raster('dem.tif')
roads = wbe.read_vector('roads.gpkg')
las = wbe.read_lidar('survey.las')
print('raster epsg:', dem.metadata().epsg_code)
print('vector epsg:', roads.metadata().crs_epsg)
print('lidar epsg:', las.metadata().crs_epsg)
Assigning Projection Metadata
Use CRS assignment only when the coordinates are already in the correct coordinate system but the file metadata is missing or wrong. Assignment does not move coordinates; it only changes the declared CRS. If you need to change the coordinate values themselves, use the reprojection methods shown later in this chapter.
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
dem = wbe.read_raster('dem_without_crs.tif')
roads = wbe.read_vector('roads_without_crs.gpkg')
las = wbe.read_lidar('survey_without_crs.las')
# Assign by EPSG when you know the coordinates already use that CRS.
dem.set_crs_epsg(26917)
roads.set_crs_epsg(26917)
# Assign by WKT when that is the CRS information you have available.
utm_wkt = wbe.projection.to_ogc_wkt(26917)
las.set_crs_wkt(utm_wkt)
print('raster epsg after assignment:', dem.crs_epsg())
print('vector epsg after assignment:', roads.crs_epsg())
print('lidar epsg after assignment:', las.crs_epsg())
# If metadata is wrong rather than missing, clear it before reassigning.
roads.clear_crs()
roads.set_crs_epsg(26917)
This pattern is especially useful for legacy rasters, sidecar-free vector exports, and lidar files that have correct coordinates but incomplete CRS metadata.
Raster Reprojection
WbW-Py exposes six raster-object reprojection methods. Use the one that matches your grid-control needs:
raster.reproject(...): General method with full control over extent, rows/cols, resolution, snap origin, nodata policy, antimeridian policy, grid-size policy, and destination footprint.raster.reproject_nearest(dst_epsg, ...): Convenience wrapper for nearest-neighbour reprojection.raster.reproject_bilinear(dst_epsg, ...): Convenience wrapper for bilinear reprojection.raster.reproject_to_match_grid(target_grid, ...): Reprojects and snaps exactly to another raster's grid geometry (extent, rows, cols, resolution).raster.reproject_to_match_resolution(reference_grid, ...): Reprojects while matching the reference raster's resolution and snap behavior.raster.reproject_to_match_resolution_in_epsg(dst_epsg, reference_grid, ...): Reprojects to a specified EPSG while deriving output resolution controls from a reference raster.
Available resampling methods (wbraster)
Use these values anywhere a reprojection method accepts resample:
nearest: category-safe nearest-neighbour.bilinear: smooth linear interpolation.cubic: bicubic interpolation.lanczos: high-quality sinc-window interpolation.average: 3x3 mean statistic.min: 3x3 minimum statistic.max: 3x3 maximum statistic.mode: 3x3 modal statistic.median: 3x3 median statistic.stddev: 3x3 standard deviation statistic.
Practical defaults:
- Categorical/class rasters:
nearest(ormodefor smoothing by majority). - Continuous surfaces (DEM, reflectance, temperature):
bilinear,cubic, orlanczos. - Thematic/statistical resamples:
average,min,max,median,stddev.
Example: full-control reprojection
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
dem = wbe.read_raster('dem.tif')
dem_utm = dem.reproject(
dst_epsg=32618,
resample='bilinear',
x_res=10.0,
y_res=10.0,
nodata_policy='partial_kernel',
antimeridian_policy='auto',
grid_size_policy='expand',
destination_footprint='none',
)
wbe.write_raster(dem_utm, 'dem_utm_10m.tif')
Example: grid-matching reprojection
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
src = wbe.read_raster('landcover_4326.tif')
target = wbe.read_raster('dem_utm_10m.tif')
# Categorical raster: nearest is typically required.
aligned = src.reproject_to_match_grid(target, resample='nearest')
wbe.write_raster(aligned, 'landcover_aligned.tif')
Automatic reprojection in raster-stack tools
Several stack-based tools now support automatic stack alignment with explicit controls:
auto_reproject(defaulttrue)auto_reproject_method(optional override)
Current behavior:
inputs[0]is treated as the reference raster.- Any stack raster with mismatched CRS is auto-reprojected to match the
reference grid when
auto_reproject=true. - If
auto_reproject_methodis unset:- categorical rasters infer
nearest - continuous rasters infer
bilinear
- categorical rasters infer
- If extents do not overlap after alignment, tools raise a hard error.
This is especially important for stack workflows (input_rasters/inputs) such
as overlay operations, weighted sums, PCA, inverse PCA, raster calculator,
image segmentation, and position-based stack selection.
Example: stack tool with automatic reprojection
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
result = wbe.run_tool(
'weighted_sum',
{
'input_rasters': ['slope_utm.tif', 'landcover_4326.tif', 'distance_utm.tif'],
'weights': [0.4, 0.35, 0.25],
'auto_reproject': True,
'auto_reproject_method': '', # empty -> infer nearest/bilinear per raster
'output': 'weighted_sum.tif',
},
)
print(result)
Vector Reprojection
This pattern is appropriate when geometry validity and failure policy need to be explicit.
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
roads = wbe.read_vector('roads.gpkg')
roads_utm = wbe.projection_georeferencing.general.reproject_vector(
roads,
dst_epsg=32618,
failure_policy='error',
topology_policy='none',
)
wbe.write_vector(roads_utm, 'roads_utm.gpkg')
Lidar Reprojection
Use explicit lidar reprojection settings to avoid silent dimensional or policy defaults.
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
las = wbe.read_lidar('survey.las')
las_utm = wbe.projection_georeferencing.general.reproject_lidar(
las,
dst_epsg=32618,
use_3d_transform=False,
failure_policy='error',
)
wbe.write_lidar(las_utm, 'survey_utm.copc.laz')
Georeference Raster from Control Points
Use this tool when an image/raster has no reliable georeferencing and you have ground-control points (GCPs) linking image pixel coordinates to map coordinates.
Required CSV fields:
source_colsource_rowtarget_xtarget_y
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
result = wbe.projection_georeferencing.general.georeference_raster_from_control_points(
input_raster='historical_scan.tif',
control_points_csv='historical_scan_gcps.csv',
epsg=32618,
resample='bilinear',
output='historical_scan_georef.tif',
report='historical_scan_georef_report.json', # optional diagnostics JSON
)
print(result)
If you need raw runtime invocation style, the equivalent tool ID is
georeference_raster_from_control_points.
Projection Utility Namespace
This namespace is useful for CRS diagnostics and point-level coordinate transform tasks outside full dataset reprojection.
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
wkt_3857 = wbe.projection.to_ogc_wkt(3857)
print('epsg from wkt:', wbe.projection.identify_epsg(wkt_3857))
pts = [{'x': -79.3832, 'y': 43.6532}]
pts_utm = wbe.projection.reproject_points(pts, src_epsg=4326, dst_epsg=32618)
print(pts_utm)
Parse a PROJ string
Use projection.from_proj_string when you have a PROJ4-style string (e.g.,
read from a legacy file header or third-party metadata) and need to identify
the corresponding EPSG code or obtain an OGC WKT representation.
The method returns a dict with exactly one of these keys:
{'epsg': int}— EPSG code identified{'wkt': str}— no EPSG match, WKT representation available{'unknown': True}— PROJ string parsed but CRS could not be identified further
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
proj_str = '+proj=utm +zone=17 +datum=NAD83 +units=m +no_defs'
result = wbe.projection.from_proj_string(proj_str)
if 'epsg' in result:
print('identified EPSG:', result['epsg']) # e.g. 26917
elif 'wkt' in result:
print('WKT:', result['wkt'])
else:
print('CRS unknown')
This is the recommended fallback for legacy data sources that carry only a PROJ4 metadata string. WbW-Py itself uses this path internally when reprojecting rasters whose CRS metadata does not include an EPSG code.
Area-of-use bounding box
Use projection.area_of_use to retrieve the geographic bounding box of valid
use for an EPSG code. This is useful for validating that your data actually
falls within the intended CRS domain before or after reprojection.
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
bbox = wbe.projection.area_of_use(32618) # UTM Zone 18N
if bbox is not None:
print(f"valid lon: {bbox['lon_min']} to {bbox['lon_max']}")
print(f"valid lat: {bbox['lat_min']} to {bbox['lat_max']}")
# Returns None for codes with no registered bounding box.
print(wbe.projection.area_of_use(9999)) # None
You can also pass the bounding box check as a pre-reprojection guard:
import whitebox_workflows as wb
wbe = wb.WbEnvironment()
dem = wbe.read_raster('dem.tif')
dst_epsg = 32618
bbox = wbe.projection.area_of_use(dst_epsg)
if bbox is not None:
ext = dem.metadata().extent
# Quick geographic sanity check before committing to full reprojection.
in_range = (
ext.min_x >= bbox['lon_min'] and ext.max_x <= bbox['lon_max'] and
ext.min_y >= bbox['lat_min'] and ext.max_y <= bbox['lat_max']
)
if not in_range:
print('WARNING: DEM extent may fall outside area of use for EPSG:', dst_epsg)
dem_utm = dem.reproject(dst_epsg=dst_epsg, resample='bilinear')
wbe.write_raster(dem_utm, 'dem_utm.tif')
Best Practices
- Confirm source CRS before any reprojection.
- Use
nearestfor categorical raster data,bilinear/cubicfor continuous data. - Re-open outputs and verify CRS metadata post-transform.
- Keep transform options explicit for reproducible pipelines.