Source code for shapelib.tools.converters.transform_utils

"""Utilities for affine transform adjustments in raster-to-vector conversions."""

import numpy as np
import rasterio
from rasterio.io import DatasetReader
from rasterio.windows import Window


[docs] def get_transform_with_resampling( dataset_reader: DatasetReader, window: Window | None = None, target_resolution_cm_per_pixel: float | None = None, ) -> rasterio.Affine: """ Get affine transform from dataset, optionally adjusted for resampling. This function retrieves the affine transform from a rasterio dataset reader, with optional adjustment for window regions and resolution resampling. When masks are generated from resampled imagery, the transform must be adjusted to maintain correct spatial positioning. Parameters ---------- dataset_reader : rasterio.io.DatasetReader Rasterio dataset reader containing the original transform and CRS. window : rasterio.windows.Window, optional If provided, the transform will be adjusted for the window region. target_resolution_cm_per_pixel : float, optional Target resolution in centimeters per pixel if the mask was resampled. If provided and different from the dataset's original resolution, the transform will be automatically adjusted to account for resampling. Returns ------- rasterio.Affine Affine transform, potentially adjusted for window and/or resampling. Examples -------- >>> import rasterio >>> with rasterio.open('image.tif') as src: ... # Get transform without adjustments ... transform1 = get_transform_with_resampling(src) ... ... # Get transform adjusted for resampling ... transform2 = get_transform_with_resampling( ... src, ... target_resolution_cm_per_pixel=3.0 ... ) ... ... # Get transform for window with resampling ... window = rasterio.windows.Window(0, 0, 100, 100) ... transform3 = get_transform_with_resampling( ... src, ... window=window, ... target_resolution_cm_per_pixel=3.0 ... ) """ transform = dataset_reader.window_transform(window) if window is not None else dataset_reader.transform if target_resolution_cm_per_pixel is not None: transform = adjust_transform_for_resampling( transform=transform, target_resolution_cm_per_pixel=target_resolution_cm_per_pixel, ) return transform
[docs] def adjust_transform_for_resampling( transform: rasterio.Affine, target_resolution_cm_per_pixel: float, ) -> rasterio.Affine: """ Adjust an affine transform to account for mask resampling. When a mask is resampled to a different resolution, its pixel coordinates no longer align with the original dataset's transform. This function computes a corrected transform where pixel sizes are scaled appropriately while maintaining the geographic origin. Parameters ---------- transform : rasterio.Affine Original affine transform from the dataset. target_resolution_cm_per_pixel : float Target resolution in centimeters per pixel that the mask was resampled to. Returns ------- rasterio.Affine Adjusted transform with corrected pixel sizes for the resampled mask. Raises ------ ValueError If the calculated scale factor is invalid (<=0, infinite, or NaN). Notes ----- The transform adjustment formula: scale_factor = original_resolution / target_resolution adjusted_pixel_size = original_pixel_size / scale_factor This works for both: - Downsampling (scale_factor > 1): finer resolution, smaller pixels - Upsampling (scale_factor < 1): coarser resolution, larger pixels If the original resolution matches the target resolution (within 0.001% tolerance), the transform is returned unchanged to avoid unnecessary computation and potential floating-point errors. Examples -------- >>> import rasterio >>> # Original: 6 cm/pixel, downsampled to 3 cm/pixel (scale = 2.0) >>> original_transform = rasterio.Affine(0.06, 0, 100, 0, -0.06, 200) >>> adjusted = adjust_transform_for_resampling(original_transform, 3.0) >>> # adjusted pixel size will be 0.03m (half of original) """ original_resolution_x_cm = abs(transform.a) * 100.0 original_resolution_y_cm = abs(transform.e) * 100.0 if not np.isclose(original_resolution_x_cm, target_resolution_cm_per_pixel, rtol=1e-5): scale_factor_x = original_resolution_x_cm / target_resolution_cm_per_pixel scale_factor_y = original_resolution_y_cm / target_resolution_cm_per_pixel if ( scale_factor_x <= 0 or scale_factor_y <= 0 or not np.isfinite(scale_factor_x) or not np.isfinite(scale_factor_y) ): raise ValueError( f"Invalid scale factors: x={scale_factor_x:.3f}, y={scale_factor_y:.3f}. " f"Original resolution: ({original_resolution_x_cm:.2f}, " f"{original_resolution_y_cm:.2f}) cm/pixel, " f"Target resolution: {target_resolution_cm_per_pixel:.2f} cm/pixel" ) transform = rasterio.Affine( transform.a / scale_factor_x, transform.b, transform.c, transform.d, transform.e / scale_factor_y, transform.f, ) return transform
[docs] def pad_mask_to_min_size(mask: np.ndarray, min_size: tuple[int, int], fill_value: int) -> np.ndarray: """ Pad a mask to match a minimum size. If the mask is already equal to or larger than min_size in both dimensions, returns the mask unchanged. Otherwise, pads with fill_value to match the exact minimum size. Padding is applied to the right and bottom to preserve spatial alignment. Parameters ---------- mask : np.ndarray The input mask to pad (2D array). min_size : tuple[int, int] Minimum size (height, width) for the output mask. fill_value : int Value to use for padding. Returns ------- np.ndarray The padded mask with shape matching min_size, or the original mask if it already meets or exceeds the minimum size. Examples -------- >>> import numpy as np >>> mask = np.ones((100, 80), dtype=np.uint8) >>> padded = pad_mask_to_min_size(mask, (256, 256), fill_value=0) >>> print(padded.shape) (256, 256) >>> # No padding when already large enough >>> large_mask = np.ones((300, 300), dtype=np.uint8) >>> result = pad_mask_to_min_size(large_mask, (256, 256), fill_value=0) >>> print(result.shape) (300, 300) Notes ----- Padding is applied to the right and bottom only, preserving the spatial position of the original mask content in the top-left corner. """ current_height, current_width = mask.shape min_height, min_width = min_size # Check if padding is needed if current_height >= min_height and current_width >= min_width: return mask # Calculate padding amounts (pad only to the right and bottom) pad_height = max(0, min_height - current_height) pad_width = max(0, min_width - current_width) # Apply padding: ((top, bottom), (left, right)) padded_mask = np.pad( mask, pad_width=((0, pad_height), (0, pad_width)), mode="constant", constant_values=fill_value, ) return padded_mask