Skip to content

sudapy.raster.ops

Raster geoprocessing operations: clip, reproject, resample, mosaic, hillshade, slope.

sudapy.raster.ops

Raster geoprocessing operations.

Functions wrap :mod:rasterio to provide high-level raster processing capabilities: clip, reproject, resample, mosaic, hillshade, and slope.

clip

clip(src: PathLike, clip_vector: PathLike, out: PathLike, *, crop: bool = True, nodata: float | None = None) -> Path

Clip a raster by vector geometries.

Parameters:

Name Type Description Default
src PathLike

Input raster path.

required
clip_vector PathLike

Vector file whose geometries define the clip extent.

required
out PathLike

Output raster path.

required
crop bool

Whether to crop the raster extent to the vector bounds.

True
nodata float | None

NoData value for masked pixels. Defaults to the source nodata.

None

Returns:

Type Description
Path

Path to the output raster.

Source code in src\sudapy\raster\ops.py
def clip(
    src: PathLike,
    clip_vector: PathLike,
    out: PathLike,
    *,
    crop: bool = True,
    nodata: float | None = None,
) -> Path:
    """Clip a raster by vector geometries.

    Args:
        src: Input raster path.
        clip_vector: Vector file whose geometries define the clip extent.
        out: Output raster path.
        crop: Whether to crop the raster extent to the vector bounds.
        nodata: NoData value for masked pixels. Defaults to the source nodata.

    Returns:
        Path to the output raster.
    """
    src = Path(src)
    out = Path(out)
    if not src.exists():
        raise FileFormatError(f"Raster not found: {src}")

    mask_gdf = read_vector(clip_vector)

    with rasterio.open(src) as ds:
        if mask_gdf.crs and mask_gdf.crs != ds.crs:
            logger.info("Reprojecting clip vector to raster CRS (%s)", ds.crs)
            mask_gdf = mask_gdf.to_crs(ds.crs)

        geometries = mask_gdf.geometry.values
        nd = nodata if nodata is not None else ds.nodata

        out_image, out_transform = rasterio.mask.mask(
            ds, geometries, crop=crop, nodata=nd,
        )
        out_meta = ds.meta.copy()
        out_meta.update({
            "driver": "GTiff",
            "height": out_image.shape[1],
            "width": out_image.shape[2],
            "transform": out_transform,
            "nodata": nd,
        })

    out.parent.mkdir(parents=True, exist_ok=True)
    with rasterio.open(out, "w", **out_meta) as dst:
        dst.write(out_image)

    logger.info("Clipped raster written to %s", out)
    return out

reproject_raster

reproject_raster(src: PathLike, out: PathLike, to_epsg: int, *, resampling: Resampling = Resampling.nearest) -> Path

Reproject a raster to a new CRS.

Parameters:

Name Type Description Default
src PathLike

Input raster path.

required
out PathLike

Output raster path.

required
to_epsg int

Target EPSG code.

required
resampling Resampling

Resampling method.

nearest

Returns:

Type Description
Path

Path to the output raster.

Source code in src\sudapy\raster\ops.py
def reproject_raster(
    src: PathLike,
    out: PathLike,
    to_epsg: int,
    *,
    resampling: Resampling = Resampling.nearest,
) -> Path:
    """Reproject a raster to a new CRS.

    Args:
        src: Input raster path.
        out: Output raster path.
        to_epsg: Target EPSG code.
        resampling: Resampling method.

    Returns:
        Path to the output raster.
    """
    src = Path(src)
    out = Path(out)
    if not src.exists():
        raise FileFormatError(f"Raster not found: {src}")

    dst_crs = validate_epsg(to_epsg)

    with rasterio.open(src) as ds:
        transform, width, height = calculate_default_transform(
            ds.crs, dst_crs, ds.width, ds.height, *ds.bounds
        )
        kwargs = ds.meta.copy()
        kwargs.update({
            "crs": dst_crs,
            "transform": transform,
            "width": width,
            "height": height,
            "driver": "GTiff",
        })

        out.parent.mkdir(parents=True, exist_ok=True)
        with rasterio.open(out, "w", **kwargs) as dst:
            for i in range(1, ds.count + 1):
                reproject(
                    source=rasterio.band(ds, i),
                    destination=rasterio.band(dst, i),
                    src_transform=ds.transform,
                    src_crs=ds.crs,
                    dst_transform=transform,
                    dst_crs=dst_crs,
                    resampling=resampling,
                )

    logger.info("Reprojected raster written to %s", out)
    return out

resample

resample(src: PathLike, out: PathLike, scale_factor: float, method: str = 'bilinear') -> Path

Resample a raster by a scale factor.

Parameters:

Name Type Description Default
src PathLike

Input raster path.

required
out PathLike

Output raster path.

required
scale_factor float

Factor to scale resolution (2.0 = double resolution).

required
method str

Resampling method name: nearest, bilinear, cubic.

'bilinear'

Returns:

Type Description
Path

Path to the output raster.

Source code in src\sudapy\raster\ops.py
def resample(
    src: PathLike,
    out: PathLike,
    scale_factor: float,
    method: str = "bilinear",
) -> Path:
    """Resample a raster by a scale factor.

    Args:
        src: Input raster path.
        out: Output raster path.
        scale_factor: Factor to scale resolution (2.0 = double resolution).
        method: Resampling method name: ``nearest``, ``bilinear``, ``cubic``.

    Returns:
        Path to the output raster.
    """
    src = Path(src)
    out = Path(out)
    if not src.exists():
        raise FileFormatError(f"Raster not found: {src}")

    resampling_method = _RESAMPLING_MAP.get(method)
    if resampling_method is None:
        raise SudaPyError(
            f"Unknown resampling method '{method}'",
            hint=f"Supported: {', '.join(_RESAMPLING_MAP.keys())}",
        )

    with rasterio.open(src) as ds:
        new_height = int(ds.height * scale_factor)
        new_width = int(ds.width * scale_factor)

        data = ds.read(
            out_shape=(ds.count, new_height, new_width),
            resampling=resampling_method,
        )

        transform = ds.transform * ds.transform.scale(
            ds.width / new_width,
            ds.height / new_height,
        )

        kwargs = ds.meta.copy()
        kwargs.update({
            "driver": "GTiff",
            "height": new_height,
            "width": new_width,
            "transform": transform,
        })

    out.parent.mkdir(parents=True, exist_ok=True)
    with rasterio.open(out, "w", **kwargs) as dst:
        dst.write(data)

    logger.info("Resampled raster (x%.1f, %s) written to %s", scale_factor, method, out)
    return out

mosaic

mosaic(src_dir: PathLike, out: PathLike) -> Path

Merge multiple raster tiles from a directory into one.

Parameters:

Name Type Description Default
src_dir PathLike

Directory containing raster tiles.

required
out PathLike

Output merged raster path.

required

Returns:

Type Description
Path

Path to the merged raster.

Source code in src\sudapy\raster\ops.py
def mosaic(
    src_dir: PathLike,
    out: PathLike,
) -> Path:
    """Merge multiple raster tiles from a directory into one.

    Args:
        src_dir: Directory containing raster tiles.
        out: Output merged raster path.

    Returns:
        Path to the merged raster.
    """
    src_dir = Path(src_dir)
    out = Path(out)
    if not src_dir.is_dir():
        raise FileFormatError(f"Not a directory: {src_dir}")

    raster_files = sorted(
        f for f in src_dir.iterdir() if f.suffix.lower() in _RASTER_EXTS
    )
    if not raster_files:
        raise FileFormatError(
            f"No raster files found in {src_dir}",
            hint=f"Supported extensions: {', '.join(_RASTER_EXTS)}",
        )

    datasets = [rasterio.open(f) for f in raster_files]
    try:
        mosaic_data, mosaic_transform = merge(datasets)
    finally:
        for ds in datasets:
            ds.close()

    kwargs = datasets[0].meta.copy()
    kwargs.update({
        "driver": "GTiff",
        "height": mosaic_data.shape[1],
        "width": mosaic_data.shape[2],
        "transform": mosaic_transform,
    })

    out.parent.mkdir(parents=True, exist_ok=True)
    with rasterio.open(out, "w", **kwargs) as dst:
        dst.write(mosaic_data)

    logger.info("Mosaic of %d tiles written to %s", len(raster_files), out)
    return out

hillshade

hillshade(src: PathLike, out: PathLike, *, azimuth: float = 315.0, altitude: float = 45.0) -> Path

Generate a hillshade from a DEM raster.

Parameters:

Name Type Description Default
src PathLike

Input DEM raster path (single band, elevation values).

required
out PathLike

Output hillshade raster path.

required
azimuth float

Sun azimuth in degrees (default 315 = northwest).

315.0
altitude float

Sun altitude in degrees above horizon (default 45).

45.0

Returns:

Type Description
Path

Path to the hillshade raster.

Source code in src\sudapy\raster\ops.py
def hillshade(
    src: PathLike,
    out: PathLike,
    *,
    azimuth: float = 315.0,
    altitude: float = 45.0,
) -> Path:
    """Generate a hillshade from a DEM raster.

    Args:
        src: Input DEM raster path (single band, elevation values).
        out: Output hillshade raster path.
        azimuth: Sun azimuth in degrees (default 315 = northwest).
        altitude: Sun altitude in degrees above horizon (default 45).

    Returns:
        Path to the hillshade raster.
    """
    src = Path(src)
    out = Path(out)
    if not src.exists():
        raise FileFormatError(f"Raster not found: {src}")

    with rasterio.open(src) as ds:
        dem = ds.read(1).astype(np.float64)
        cellsize_x = abs(ds.transform.a)
        cellsize_y = abs(ds.transform.e)

        hs = _compute_hillshade(dem, cellsize_x, cellsize_y, azimuth, altitude)

        kwargs = ds.meta.copy()
        kwargs.update({
            "driver": "GTiff",
            "dtype": "float32",
            "count": 1,
        })

    out.parent.mkdir(parents=True, exist_ok=True)
    with rasterio.open(out, "w", **kwargs) as dst:
        dst.write(hs.astype(np.float32), 1)

    logger.info("Hillshade written to %s", out)
    return out

slope

slope(src: PathLike, out: PathLike) -> Path

Calculate slope in degrees from a DEM raster.

Parameters:

Name Type Description Default
src PathLike

Input DEM raster path (single band).

required
out PathLike

Output slope raster path (values in degrees).

required

Returns:

Type Description
Path

Path to the slope raster.

Source code in src\sudapy\raster\ops.py
def slope(
    src: PathLike,
    out: PathLike,
) -> Path:
    """Calculate slope in degrees from a DEM raster.

    Args:
        src: Input DEM raster path (single band).
        out: Output slope raster path (values in degrees).

    Returns:
        Path to the slope raster.
    """
    src = Path(src)
    out = Path(out)
    if not src.exists():
        raise FileFormatError(f"Raster not found: {src}")

    with rasterio.open(src) as ds:
        dem = ds.read(1).astype(np.float64)
        cellsize_x = abs(ds.transform.a)
        cellsize_y = abs(ds.transform.e)

        slope_deg = _compute_slope(dem, cellsize_x, cellsize_y)

        kwargs = ds.meta.copy()
        kwargs.update({
            "driver": "GTiff",
            "dtype": "float32",
            "count": 1,
        })

    out.parent.mkdir(parents=True, exist_ok=True)
    with rasterio.open(out, "w", **kwargs) as dst:
        dst.write(slope_deg.astype(np.float32), 1)

    logger.info("Slope raster written to %s", out)
    return out