curryer.correction.pairing ========================== .. py:module:: curryer.correction.pairing .. autoapi-nested-parse:: 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 :func:`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 MATLAB .mat files on disk. Attributes ---------- .. autoapisummary:: curryer.correction.pairing.logger Classes ------- .. autoapisummary:: curryer.correction.pairing.GCPPairingFunc curryer.correction.pairing.ImageMetadata curryer.correction.pairing.GCPMetadata curryer.correction.pairing.PairMatch curryer.correction.pairing.PairingResult Functions --------- .. autoapisummary:: curryer.correction.pairing.validate_pairing_output curryer.correction.pairing.enu_rotation_matrix curryer.correction.pairing.geodetic_to_enu curryer.correction.pairing._image_corners curryer.correction.pairing._image_center curryer.correction.pairing._image_bbox curryer.correction.pairing._point_in_polygon curryer.correction.pairing._distance_point_to_segment curryer.correction.pairing._distance_point_to_polygon_m curryer.correction.pairing._build_image_metadata curryer.correction.pairing._build_gcp_metadata curryer.correction.pairing.find_l1a_gcp_pairs curryer.correction.pairing.discover_gcp_files curryer.correction.pairing.pair_files Module Contents --------------- .. py:data:: logger .. py:class:: GCPPairingFunc Bases: :py:obj:`Protocol` Protocol for GCP pairing functions in Correction pipeline. Pairing functions determine which science observations (L1A images) overlap with which ground control points (GCP reference images). Standard Signature: def pair_gcps(science_keys: List[str]) -> List[Tuple[str, str]] :returns: List of (science_key, gcp_reference_path) tuples, one per valid pair .. note:: This is a simplified interface for Correction compatibility. Real implementations (like find_l1a_gcp_pairs below) may use more sophisticated spatial algorithms internally, but must return results in this simple tuple format. .. rubric:: Examples # Real spatial pairing def spatial_gcp_pairing(science_keys): l1a_images = load_images(science_keys) gcp_images = discover_gcps() pairs = find_spatial_overlaps(l1a_images, gcp_images) return [(l1a.name, gcp.path) for l1a, gcp in pairs] # Test/synthetic pairing def synthetic_gcp_pairing(science_keys): return [(key, f"synthetic_gcp_{i}.tif") for i, key in enumerate(science_keys)] .. py:method:: __call__(science_keys: list[str]) -> list[tuple[str, str]] Find GCP pairs for given science observations. .. py:function:: validate_pairing_output(pairs: list[tuple[str, str]]) -> None Validate that GCP pairing output conforms to expected format. :param pairs: List of (science_key, gcp_path) tuples :raises TypeError: If structure is invalid :raises ValueError: If tuple elements have wrong types .. rubric:: Example >>> pairs = gcp_pairing_func(["sci_001", "sci_002"]) >>> validate_pairing_output(pairs) .. py:function:: 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). :param lat_deg: Geodetic latitude of the origin in degrees. :type lat_deg: float :param lon_deg: Geodetic longitude of the origin in degrees. :type lon_deg: float :returns: Matrix that converts an ECEF delta vector into east, north, and up components with respect to the specified origin. :rtype: ndarray, shape (3, 3) .. py:function:: 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. :param lat_deg: Geodetic latitude and longitude (degrees) of the points to convert. :type lat_deg: array_like :param lon_deg: Geodetic latitude and longitude (degrees) of the points to convert. :type lon_deg: array_like :param h_m: Heights above the WGS-84 ellipsoid (meters) for each point. :type h_m: array_like :param origin_lat_deg: Geodetic latitude/longitude of the ENU frame origin in degrees. :type origin_lat_deg: float :param origin_lon_deg: Geodetic latitude/longitude of the ENU frame origin in degrees. :type origin_lon_deg: float :param origin_h_m: Height of the origin point in meters. Defaults to ``0``. :type origin_h_m: float, optional :returns: East, north, and up coordinates (meters) of the input points relative to the specified origin. :rtype: ndarray, shape (..., 3) .. py:class:: ImageMetadata Metadata describing an image footprint. :param index: Position of the image inside the original input list. :param name: Identifier associated with the image (e.g., filename). :param corners: Four corner latitude/longitude tuples ordered clockwise. :param center: Latitude/longitude of the image center pixel. :param bbox: Bounding box expressed as ``(lat_min, lat_max, lon_min, lon_max)``. .. py:attribute:: index :type: int .. py:attribute:: name :type: str .. py:attribute:: corners :type: list[tuple[float, float]] .. py:attribute:: center :type: tuple[float, float] .. py:attribute:: bbox :type: tuple[float, float, float, float] .. py:method:: corner_array() -> numpy.ndarray Return the corner coordinates as a ``(4, 2)`` NumPy array. .. py:class:: GCPMetadata Bases: :py:obj:`ImageMetadata` Metadata describing a GCP image footprint. Extends :class:`ImageMetadata` with the ECEF coordinates of the GCP center point to simplify subsequent distance calculations. .. py:attribute:: center_point_ecef :type: numpy.ndarray .. py:class:: 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. .. py:attribute:: l1a_index :type: int .. py:attribute:: gcp_index :type: int .. py:attribute:: distance_m :type: float .. py:class:: PairingResult Container for the output of :func:`find_l1a_gcp_pairs`. .. py:attribute:: l1a_images :type: list[ImageMetadata] .. py:attribute:: gcp_images :type: list[GCPMetadata] .. py:attribute:: matches :type: list[PairMatch] .. py:function:: _image_corners(image: curryer.correction.data_structures.ImageGrid) -> list[tuple[float, float]] Return the four corner latitude/longitude pairs of ``image``. .. py:function:: _image_center(image: curryer.correction.data_structures.ImageGrid) -> tuple[float, float, float] Return the latitude, longitude, and height of the center pixel. .. py:function:: _image_bbox(image: curryer.correction.data_structures.ImageGrid) -> tuple[float, float, float, float] Return the latitude/longitude bounding box of ``image``. .. py:function:: _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. .. py:function:: _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``. .. py:function:: _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. :param point_lat: Geodetic coordinates of the query location. :param point_lon: Geodetic coordinates of the query location. :param point_h: Geodetic coordinates of the query location. :param 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. :rtype: float .. py:function:: _build_image_metadata(index: int, image: curryer.correction.data_structures.NamedImageGrid) -> ImageMetadata Construct :class:`ImageMetadata` for ``image``. .. py:function:: _build_gcp_metadata(index: int, image: curryer.correction.data_structures.NamedImageGrid) -> GCPMetadata Construct :class:`GCPMetadata` for ``image``. .. py:function:: find_l1a_gcp_pairs(l1a_images: collections.abc.Iterable[curryer.correction.data_structures.NamedImageGrid], gcp_images: collections.abc.Iterable[curryer.correction.data_structures.NamedImageGrid], max_distance_m: float) -> PairingResult Find all L1A/GCP pairs within a distance threshold. :param l1a_images: Iterable of :class:`NamedImageGrid` instances representing L1A imagery. :param gcp_images: Iterable of :class:`NamedImageGrid` instances representing GCP chips. :param 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``. :rtype: PairingResult .. py:function:: discover_gcp_files(gcp_directory: pathlib.Path, pattern: str = '*_resampled.mat') -> list[pathlib.Path] Find all GCP files in a directory matching a pattern. :param gcp_directory: Directory to search :param pattern: Glob pattern for GCP files (default: "*_resampled.mat") :returns: Sorted list of Path objects for GCP files .. rubric:: Example >>> gcp_files = discover_gcp_files(Path("tests/data/clarreo/image_match")) >>> print(f"Found {len(gcp_files)} GCP files") .. py:function:: 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. This is the production replacement for placeholder_gcp_pairing(). Uses the find_l1a_gcp_pairs() algorithm for spatial matching. :param l1a_files: List of L1A file paths to pair :param gcp_directory: Directory containing GCP reference files :param 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) :param l1a_key: MATLAB struct key for L1A data (default: "subimage") :param gcp_key: MATLAB struct key for GCP data (default: "GCP") :param gcp_pattern: File pattern for GCP discovery (default: "*_resampled.mat") :returns: List of (l1a_file, gcp_file) tuples for all valid spatial pairs Note: One L1A can pair with multiple GCPs (many-to-many) :raises FileNotFoundError: If gcp_directory doesn't exist :raises ValueError: If no valid pairs found .. rubric:: Example >>> l1a_files = [Path("test1.mat"), Path("test2.mat")] >>> pairs = pair_files(l1a_files, Path("gcp_chips"), max_distance_m=0.0) >>> print(f"Found {len(pairs)} valid L1A-GCP pairs") >>> for l1a, gcp in pairs: ... print(f" {l1a.name} → {gcp.name}")