curryer.correction.pairing

Utilities for pairing L1A images with nearby GCP chips.

The routines in this module describe each image footprint using the NamedImageGrid metadata, convert the corners to a local East-North-Up frame, and compute the distance between a GCP center point and the nearest edge of each L1A footprint. The core entry point is find_l1a_gcp_pairs(), which returns a many-to-many mapping between the supplied L1A and GCP collections.

File-based utilities (discover_gcp_files, pair_files) provide higher-level wrappers for working with .mat or NetCDF .nc files on disk.

Attributes

Classes

ImageMetadata

Metadata describing an image footprint.

GCPMetadata

Metadata describing a GCP image footprint.

PairMatch

Relationship between an L1A image and a GCP chip.

PairingResult

Container for the output of find_l1a_gcp_pairs().

Functions

enu_rotation_matrix(→ numpy.ndarray)

Return the rotation matrix from ECEF deltas to local ENU

geodetic_to_enu(→ numpy.ndarray)

Convert geodetic coordinates to local ENU (East–North–Up) coordinates.

_image_corners(→ list[tuple[float, float]])

Return the four corner latitude/longitude pairs of image.

_image_center(→ tuple[float, float, float])

Return the latitude, longitude, and height of the center pixel.

_image_bbox(→ tuple[float, float, float, float])

Return the latitude/longitude bounding box of image.

_point_in_polygon(→ bool)

Return True if point_xy lies inside polygon_xy.

_distance_point_to_segment(→ float)

Return the minimum distance from point_xy to segment ab.

_distance_point_to_polygon_m(→ float)

Return the distance from a point to a polygon in meters.

_build_image_metadata(→ ImageMetadata)

Construct ImageMetadata for image.

_build_gcp_metadata(→ GCPMetadata)

Construct GCPMetadata for image.

find_l1a_gcp_pairs(→ PairingResult)

Find all L1A/GCP pairs within a distance threshold.

discover_gcp_files(→ list[pathlib.Path])

Find all GCP files in a directory matching a pattern.

pair_files(→ list[tuple[pathlib.Path, pathlib.Path]])

Find L1A-GCP pairs based on spatial overlap and return as file path tuples.

pair_geolocated_dataset_with_gcp_files(...)

Find GCP chip files that spatially overlap an in-memory geolocated dataset.

Module Contents

curryer.correction.pairing.logger
curryer.correction.pairing.enu_rotation_matrix(lat_deg: float, lon_deg: float) numpy.ndarray

Return the rotation matrix from ECEF deltas to local ENU (East–North–Up, local tangent-coordinate frame).

Parameters:
  • lat_deg (float) – Geodetic latitude of the origin in degrees.

  • lon_deg (float) – Geodetic longitude of the origin in degrees.

Returns:

Matrix that converts an ECEF delta vector into east, north, and up components with respect to the specified origin.

Return type:

ndarray, shape (3, 3)

curryer.correction.pairing.geodetic_to_enu(lat_deg: numpy.ndarray, lon_deg: numpy.ndarray, h_m: numpy.ndarray, origin_lat_deg: float, origin_lon_deg: float, origin_h_m: float = 0.0) numpy.ndarray

Convert geodetic coordinates to local ENU (East–North–Up) coordinates.

Parameters:
  • lat_deg (array_like) – Geodetic latitude and longitude (degrees) of the points to convert.

  • lon_deg (array_like) – Geodetic latitude and longitude (degrees) of the points to convert.

  • h_m (array_like) – Heights above the WGS-84 ellipsoid (meters) for each point.

  • origin_lat_deg (float) – Geodetic latitude/longitude of the ENU frame origin in degrees.

  • origin_lon_deg (float) – Geodetic latitude/longitude of the ENU frame origin in degrees.

  • origin_h_m (float, optional) – Height of the origin point in meters. Defaults to 0.

Returns:

East, north, and up coordinates (meters) of the input points relative to the specified origin.

Return type:

ndarray, shape (…, 3)

class curryer.correction.pairing.ImageMetadata

Metadata describing an image footprint.

Parameters:
  • index – Position of the image inside the original input list.

  • name – Identifier associated with the image (e.g., filename).

  • corners – Four corner latitude/longitude tuples ordered clockwise.

  • center – Latitude/longitude of the image center pixel.

  • bbox – Bounding box expressed as (lat_min, lat_max, lon_min, lon_max).

index: int
name: str
corners: list[tuple[float, float]]
center: tuple[float, float]
bbox: tuple[float, float, float, float]
corner_array() numpy.ndarray

Return the corner coordinates as a (4, 2) NumPy array.

class curryer.correction.pairing.GCPMetadata

Bases: ImageMetadata

Metadata describing a GCP image footprint.

Extends ImageMetadata with the ECEF coordinates of the GCP center point to simplify subsequent distance calculations.

center_point_ecef: numpy.ndarray
class curryer.correction.pairing.PairMatch

Relationship between an L1A image and a GCP chip.

The distance_m field stores the signed margin between the GCP center and the closest edge of the L1A footprint in meters. Positive values indicate the center lies inside the footprint, while negative values mean it lies outside.

l1a_index: int
gcp_index: int
distance_m: float
class curryer.correction.pairing.PairingResult

Container for the output of find_l1a_gcp_pairs().

l1a_images: list[ImageMetadata]
gcp_images: list[GCPMetadata]
matches: list[PairMatch]
curryer.correction.pairing._image_corners(image: curryer.correction.grid_types.ImageGrid) list[tuple[float, float]]

Return the four corner latitude/longitude pairs of image.

curryer.correction.pairing._image_center(image: curryer.correction.grid_types.ImageGrid) tuple[float, float, float]

Return the latitude, longitude, and height of the center pixel.

curryer.correction.pairing._image_bbox(image: curryer.correction.grid_types.ImageGrid) tuple[float, float, float, float]

Return the latitude/longitude bounding box of image.

curryer.correction.pairing._point_in_polygon(point_xy: numpy.ndarray, polygon_xy: numpy.ndarray) bool

Return True if point_xy lies inside polygon_xy.

Uses the winding-number algorithm with a ray cast along the positive x-axis.

curryer.correction.pairing._distance_point_to_segment(point_xy: numpy.ndarray, a_xy: numpy.ndarray, b_xy: numpy.ndarray) float

Return the minimum distance from point_xy to segment ab.

curryer.correction.pairing._distance_point_to_polygon_m(point_lat: float, point_lon: float, point_h: float, polygon_latlon: collections.abc.Sequence[tuple[float, float]]) float

Return the distance from a point to a polygon in meters.

Parameters:
  • point_lat – Geodetic coordinates of the query location.

  • point_lon – Geodetic coordinates of the query location.

  • point_h – Geodetic coordinates of the query location.

  • polygon_latlon – Sequence of latitude/longitude tuples describing polygon corners.

Returns:

Signed distance between the point and the polygon boundary. Positive values indicate the point is inside the polygon and represent the margin to the nearest edge. Negative values indicate the point lies outside the polygon and represent the distance to the closest edge.

Return type:

float

curryer.correction.pairing._build_image_metadata(index: int, image: curryer.correction.grid_types.NamedImageGrid) ImageMetadata

Construct ImageMetadata for image.

curryer.correction.pairing._build_gcp_metadata(index: int, image: curryer.correction.grid_types.NamedImageGrid) GCPMetadata

Construct GCPMetadata for image.

curryer.correction.pairing.find_l1a_gcp_pairs(l1a_images: collections.abc.Iterable[curryer.correction.grid_types.NamedImageGrid], gcp_images: collections.abc.Iterable[curryer.correction.grid_types.NamedImageGrid], max_distance_m: float) PairingResult

Find all L1A/GCP pairs within a distance threshold.

Parameters:
  • l1a_images – Iterable of NamedImageGrid instances representing L1A imagery.

  • gcp_images – Iterable of NamedImageGrid instances representing GCP chips.

  • max_distance_m – Minimum margin (meters) required between the GCP center and the nearest L1A edge. Only pairs with margin >= max_distance_m are returned.

Returns:

Metadata for the supplied images together with any pairs that fall within max_distance_m.

Return type:

PairingResult

curryer.correction.pairing.discover_gcp_files(gcp_directory: pathlib.Path, pattern: str = '*_resampled.mat') list[pathlib.Path]

Find all GCP files in a directory matching a pattern.

Parameters:
  • gcp_directory – Directory to search

  • pattern – Glob pattern for GCP files (default: “*_resampled.mat”)

Returns:

Sorted list of Path objects for GCP files

Example

>>> gcp_files = discover_gcp_files(Path("tests/data/clarreo/image_match"))
>>> print(f"Found {len(gcp_files)} GCP files")
curryer.correction.pairing.pair_files(l1a_files: list[pathlib.Path], gcp_directory: pathlib.Path, max_distance_m: float = 0.0, l1a_key: str = 'subimage', gcp_key: str = 'GCP', gcp_pattern: str = '*_resampled.mat') list[tuple[pathlib.Path, pathlib.Path]]

Find L1A-GCP pairs based on spatial overlap and return as file path tuples.

Both .mat and NetCDF (.nc) files are supported for L1A observations and GCP chips. The format is detected automatically from the file extension by load_named_image_grid().

Parameters:
  • l1a_files – List of L1A (or observation) file paths to pair. Supports .mat and .nc files.

  • gcp_directory – Directory containing GCP reference files.

  • max_distance_m – Minimum margin for valid pairing (default: 0.0) - 0.0: Requires GCP center inside L1A footprint (strict) - >0: Allows GCP center up to this distance inside footprint - <0: Allows GCP center outside footprint (loose)

  • l1a_key – MATLAB struct key for L1A data (default: “subimage”). Ignored for NetCDF files.

  • gcp_key – MATLAB struct key for GCP data (default: “GCP”). Ignored for NetCDF files.

  • gcp_pattern – File pattern for GCP discovery (default: “*_resampled.mat”; use “*_regridded.nc” for NetCDF chips).

Returns:

List of (l1a_file, gcp_file) tuples for all valid spatial pairs. One L1A can pair with multiple GCPs (many-to-many).

Raises:
  • FileNotFoundError – If gcp_directory doesn’t exist.

  • ValueError – If no valid pairs found.

Example

>>> # .mat subimages paired with .mat GCPs (original behaviour)
>>> pairs = pair_files(mat_files, Path("gcp_chips"))
>>> # NetCDF observations paired with regridded NetCDF GCP chips
>>> pairs = pair_files(
...     nc_obs_files, Path("gcp_chips"),
...     gcp_pattern="*_regridded.nc",
... )
curryer.correction.pairing.pair_geolocated_dataset_with_gcp_files(geolocated_data: xarray.Dataset, gcp_files: list[pathlib.Path], max_distance_m: float = 0.0, gcp_key: str = 'GCP') list[pathlib.Path]

Find GCP chip files that spatially overlap an in-memory geolocated dataset.

This is the in-memory-observation counterpart of pair_files(). Both delegate to the same core spatial-pairing algorithm (find_l1a_gcp_pairs()) with proper ENU-distance polygon containment, ensuring a single implementation for all GCP pairing.

Parameters:
  • geolocated_data (xr.Dataset) – Already-geolocated observation dataset with latitude and longitude variables.

  • gcp_files (list[Path]) – GCP chip file paths to test for spatial overlap.

  • max_distance_m (float, optional) – Minimum signed margin (meters) required between the GCP chip center and the nearest observation footprint edge. 0.0 (default) means the GCP center must fall strictly inside the footprint; negative values allow the center to lie outside the footprint by up to abs(max_distance_m) meters.

  • gcp_key (str, optional) – MATLAB struct key used when loading .mat GCP chips. Defaults to "GCP". Ignored for NetCDF files.

Returns:

Subset of gcp_files whose center-points overlap the observation footprint, ordered by their original position in gcp_files. Empty when no overlap is found or when gcp_files is empty.

Return type:

list[Path]

Notes

Files that cannot be loaded are silently skipped with a DEBUG log entry. This mirrors the behaviour of pair_files() for individual files.

Examples

>>> gcp_dir = Path("data/gcp_chips")
>>> all_chips = sorted(gcp_dir.glob("*_regridded.nc"))
>>> matched = pair_geolocated_dataset_with_gcp_files(geo_ds, all_chips, max_distance_m=-500.0)
>>> print(f"{len(matched)}/{len(all_chips)} chips overlap the observation footprint")