curryer.correction.regrid ========================= .. py:module:: curryer.correction.regrid .. autoapi-nested-parse:: GCP chip regridding algorithms. This module provides functionality to transform GCP chips from irregular geodetic grids (derived from ECEF coordinates) to regular latitude/longitude grids. The regridding process is mission-agnostic and configurable via RegridConfig. The main workflow is: 1. Load raw GCP chip with ECEF coordinates (from HDF file) 2. Convert ECEF → geodetic (lon, lat, h) 3. Determine output grid bounds and spacing 4. Interpolate data onto regular grid using bilinear interpolation 5. Return ImageGrid with regular lat/lon coordinates Attributes ---------- .. autoapisummary:: curryer.correction.regrid.logger Functions --------- .. autoapisummary:: curryer.correction.regrid.compute_regular_grid_bounds curryer.correction.regrid.create_regular_grid curryer.correction.regrid.point_in_triangle curryer.correction.regrid.bilinear_interpolate_quad curryer.correction.regrid.find_containing_cell curryer.correction.regrid.regrid_irregular_to_regular curryer.correction.regrid.regrid_gcp_chip Module Contents --------------- .. py:data:: logger .. py:function:: compute_regular_grid_bounds(lon_irregular: numpy.ndarray, lat_irregular: numpy.ndarray, conservative: bool = True) -> tuple[float, float, float, float] Compute bounds for regular output grid from irregular input. :param lon_irregular: 2D arrays of irregular grid coordinates (degrees). :type lon_irregular: np.ndarray :param lat_irregular: 2D arrays of irregular grid coordinates (degrees). :type lat_irregular: np.ndarray :param conservative: If True, shrink bounds to ensure all output points are within input. Conservative bounds avoid extrapolation at edges by taking the maximum of left/bottom edges and minimum of right/top edges. :type conservative: bool, default=True :returns: **minlon, maxlon, minlat, maxlat** -- Bounding box for regular grid (degrees). :rtype: float .. rubric:: Notes Conservative bounds (default) follow MATLAB behavior: - minlon = max(bottom_left_lon, top_left_lon) - maxlon = min(bottom_right_lon, top_right_lon) - minlat = max(bottom_left_lat, bottom_right_lat) - maxlat = min(top_left_lat, top_right_lat) This ensures the regular grid lies entirely within the irregular grid. .. py:function:: create_regular_grid(bounds: tuple[float, float, float, float], grid_size: tuple[int, int] | None = None, resolution: tuple[float, float] | None = None) -> tuple[numpy.ndarray, numpy.ndarray] Create regular lat/lon grid. :param bounds: (minlon, maxlon, minlat, maxlat) in degrees. :type bounds: tuple[float, float, float, float] :param grid_size: (nrows, ncols). If None, derive from resolution. :type grid_size: tuple[int, int], optional :param resolution: (dlat, dlon) in degrees. If None, use grid_size. :type resolution: tuple[float, float], optional :returns: **lon_regular, lat_regular** -- 2D arrays of regular grid coordinates (degrees), shape (nrows, ncols). :rtype: np.ndarray .. rubric:: Notes Exactly one of grid_size or resolution must be provided. Grid follows MATLAB convention: - Row index increases going south (latitude decreases) - Column index increases going east (longitude increases) .. py:function:: point_in_triangle(point: numpy.ndarray, triangle: numpy.ndarray) -> tuple[bool, numpy.ndarray] Check if point is inside triangle using barycentric coordinates. :param point: Point [x, y] to test. :type point: np.ndarray :param triangle: Triangle vertices, shape (3, 2): [[x1, y1], [x2, y2], [x3, y3]]. :type triangle: np.ndarray :returns: * **inside** (*bool*) -- True if point is inside triangle (barycentric coords all in (0, 1)). * **barycentric_coords** (*np.ndarray*) -- Barycentric coordinates [w1, w2, w3]. .. rubric:: Notes Uses the cross product method from MATLAB bandval function. Point P is inside triangle ABC if all barycentric weights are in (0, 1). .. py:function:: bilinear_interpolate_quad(point: numpy.ndarray, corners_lon: numpy.ndarray, corners_lat: numpy.ndarray, corner_values: numpy.ndarray) -> float Bilinear interpolation within an irregular quadrilateral. :param point: Target point [lon, lat] (degrees). :type point: np.ndarray :param corners_lon: Coordinates of 4 corners, ordered clockwise from top-left: [top-left, top-right, bottom-right, bottom-left]. :type corners_lon: np.ndarray :param corners_lat: Coordinates of 4 corners, ordered clockwise from top-left: [top-left, top-right, bottom-right, bottom-left]. :type corners_lat: np.ndarray :param corner_values: Values at the 4 corners. :type corner_values: np.ndarray :returns: **interpolated_value** -- Interpolated value at target point. :rtype: float .. rubric:: Notes Uses matrix inversion method from MATLAB code: Solves [1, lon, lat, lon*lat]^T = M * [w1, w2, w3, w4]^T where M is constructed from corner coordinates. .. py:function:: find_containing_cell(point: numpy.ndarray, lon_grid: numpy.ndarray, lat_grid: numpy.ndarray, start_cell: tuple[int, int] | None = None) -> tuple[int, int] | None Find which cell in irregular grid contains the target point. :param point: Target point [lon, lat] (degrees). :type point: np.ndarray :param lon_grid: 2D arrays of irregular grid coordinates (degrees). :type lon_grid: np.ndarray :param lat_grid: 2D arrays of irregular grid coordinates (degrees). :type lat_grid: np.ndarray :param start_cell: Starting cell (i, j) for search (optimization hint). :type start_cell: tuple[int, int], optional :returns: **cell_indices** -- (i, j) of cell containing point, or None if not found. :rtype: tuple[int, int] or None .. rubric:: Notes Uses barycentric coordinate test to check if point is inside quadrilateral. For each cell, tests two triangles (upper-left and lower-right) that together form the quadrilateral. Search strategy follows MATLAB optimization: - Start from hint if provided - Check cells near last found cell (spatial locality) - If not found, search all cells Optimization: Inline triangle test to avoid array allocations. .. py:function:: regrid_irregular_to_regular(data_irregular: numpy.ndarray, lon_irregular: numpy.ndarray, lat_irregular: numpy.ndarray, lon_regular: numpy.ndarray, lat_regular: numpy.ndarray, method: str = 'bilinear', fill_value: float = np.nan, use_spatial_index: bool = True) -> numpy.ndarray Regrid data from irregular geodetic grid to regular lat/lon grid. This is the core algorithm: for each point in the regular output grid, find the corresponding quadrilateral cell in the irregular input grid and interpolate the value. :param data_irregular: 2D array of values on irregular grid. :type data_irregular: np.ndarray :param lon_irregular: 2D arrays of irregular grid coordinates (degrees). :type lon_irregular: np.ndarray :param lat_irregular: 2D arrays of irregular grid coordinates (degrees). :type lat_irregular: np.ndarray :param lon_regular: 2D arrays of regular grid coordinates (degrees). :type lon_regular: np.ndarray :param lat_regular: 2D arrays of regular grid coordinates (degrees). :type lat_regular: np.ndarray :param method: Interpolation method: "bilinear" or "nearest". :type method: str, default="bilinear" :param fill_value: Value for output points that fall outside input grid. :type fill_value: float, default=np.nan :param use_spatial_index: If True, build a spatial index (KD-tree) for faster cell finding. Recommended for large grids (>100×100). Adds ~0.1s overhead. :type use_spatial_index: bool, default=True :returns: **data_regular** -- 2D array of interpolated values on regular grid. :rtype: np.ndarray .. rubric:: Notes Algorithm (follows MATLAB Chip_regrid2.m): 1. For each point P in regular grid: a. Search for containing quadrilateral in irregular grid b. Perform bilinear interpolation using 4 corner values 2. Optimization: Use spatial locality (start search near last found cell) 3. Points outside irregular grid are filled with fill_value Performance: O(n²) worst case, O(n²/k) typical with spatial locality. Optimizations applied: - Minimize array allocations in inner loop - Extract corner data once per cell - Use scalar operations where possible - Optional spatial index for O(log n) nearest neighbor queries .. py:function:: regrid_gcp_chip(band_data: numpy.ndarray, ecef_coords: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray], config: curryer.correction.config.RegridConfig, output_file: str | None = None, output_metadata: dict[str, str] | None = None) -> curryer.correction.grid_types.ImageGrid High-level function: Regrid GCP chip from ECEF to regular lat/lon grid. This is the main entry point for GCP chip regridding. It handles the complete workflow from ECEF coordinates to a regular geodetic grid. :param band_data: 2D array of radiometric values. :type band_data: np.ndarray :param ecef_coords: (X, Y, Z) ECEF coordinate arrays (meters), each shape (nrows, ncols). :type ecef_coords: tuple[np.ndarray, np.ndarray, np.ndarray] :param config: Regridding configuration. :type config: RegridConfig :param output_file: If provided, save the regridded chip to this NetCDF file path. File will be created with CF-compliant metadata. :type output_file: str, optional :param output_metadata: Additional metadata to include in the NetCDF file (only used if output_file is specified). Common keys: 'source_file', 'mission', 'sensor', 'band'. :type output_metadata: dict[str, str], optional :returns: * **regridded_chip** (*ImageGrid*) -- Regridded data on regular lat/lon grid. * *Workflow* * *--------* * *1. Convert ECEF → geodetic (lon, lat, h) using curryer.compute.spatial* * *2. Compute regular grid bounds (conservative or full extent)* * *3. Create regular output grid (from resolution or size)* * *4. Regrid data using bilinear interpolation* * *5. Return ImageGrid with (data, lat, lon, h)* * *6. Optionally save to NetCDF file* .. rubric:: Examples Basic usage (return in-memory): >>> from curryer.correction.image_io import load_gcp_chip_from_hdf >>> from curryer.correction.regrid import regrid_gcp_chip, RegridConfig >>> band, x, y, z = load_gcp_chip_from_hdf("chip.hdf") >>> config = RegridConfig(output_resolution_deg=(0.001, 0.001)) >>> regridded = regrid_gcp_chip(band, (x, y, z), config) With NetCDF output: >>> regridded = regrid_gcp_chip( ... band, (x, y, z), config, ... output_file="regridded_chip.nc", ... output_metadata={ ... 'source_file': 'LT08CHP.20140803.p002r071.c01.v001.hdf', ... 'mission': 'CLARREO Pathfinder', ... 'band': 'red', ... } ... )