garmentiq.landmark.derivation.mask_intersect

  1import numpy as np
  2import cv2
  3from typing import Optional, List, Tuple, Any
  4from shapely.geometry import Point, LineString, Polygon, MultiPoint, MultiLineString
  5from shapely.ops import unary_union
  6
  7
  8def _get_mask_boundary(mask: np.ndarray):
  9    """
 10    Processes a binary or grayscale mask array and returns the primary boundary as Shapely geometry.
 11
 12    This function finds contours in the mask, converts them into Shapely Polygon boundaries
 13    or LineStrings, and then unions them into a single (potentially Multi-) geometry.
 14
 15    Args:
 16        mask (np.ndarray): The input binary or grayscale mask array.
 17
 18    Returns:
 19        Optional[Any]: A Shapely geometry (Polygon.boundary, LineString, MultiPoint, or MultiLineString)
 20                       representing the mask boundary, or None if the mask is invalid or no contours are found.
 21    """
 22    try:
 23        if mask is None or not isinstance(mask, np.ndarray):
 24            print("Error: Provided mask is not a valid NumPy array.")
 25            return None
 26
 27        # Ensure binary mask (values 0 or 1)
 28        binary_mask = (mask > 0).astype(np.uint8)
 29
 30        contours, _ = cv2.findContours(
 31            binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
 32        )
 33
 34        if not contours:
 35            print("Warning: No contours found in the mask array.")
 36            return None
 37
 38        geometries = []
 39        for contour in contours:
 40            points = [tuple(p[0]) for p in contour]
 41            if len(points) >= 3:
 42                geometries.append(Polygon(points).boundary)
 43            elif len(points) == 2:
 44                geometries.append(LineString(points))
 45
 46        if not geometries:
 47            print("Warning: No valid boundary geometries found in the mask.")
 48            return None
 49
 50        return unary_union(geometries)
 51
 52    except Exception as e:
 53        print(f"Error processing mask array: {str(e)}")
 54        return None
 55
 56
 57def _find_line_mask_intersections(
 58    line_point: Tuple[float, float],
 59    line_vector: Tuple[float, float],
 60    mask_boundary: Any,  # Shapely Geometry
 61    line_length_factor: float,
 62) -> Optional[List[Tuple[float, float]]]:
 63    """
 64    Finds intersection points between a line (defined by a point and vector) and a Shapely mask boundary.
 65
 66    Constructs a long Shapely LineString from the input line definition and
 67    computes its intersection with the provided `mask_boundary` geometry.
 68
 69    Args:
 70        line_point (Tuple[float, float]): A point (x, y) on the line.
 71        line_vector (Tuple[float, float]): The direction vector (dx, dy) of the line.
 72        mask_boundary (Any): A Shapely Geometry object representing the mask boundary (e.g., from `_get_mask_boundary`).
 73        line_length_factor (float): A factor to extend the line segment for intersection testing,
 74                                    ensuring it crosses the entire mask if needed.
 75
 76    Returns:
 77        Optional[List[Tuple[float, float]]]: A list of (x, y) tuples for all unique intersection points,
 78                                            an empty list if no intersections, or None if an error occurs.
 79    """
 80    try:
 81        # Create a long Shapely line representing the mathematical line
 82        norm_v = np.linalg.norm(line_vector)
 83        if np.isclose(norm_v, 0):
 84            print("Error: Line vector is zero during Shapely line creation.")
 85            return None  # Cannot create line
 86
 87        unit_v = (line_vector[0] / norm_v, line_vector[1] / norm_v)
 88
 89        pt_a = (
 90            line_point[0] - line_length_factor * unit_v[0],
 91            line_point[1] - line_length_factor * unit_v[1],
 92        )
 93        pt_b = (
 94            line_point[0] + line_length_factor * unit_v[0],
 95            line_point[1] + line_length_factor * unit_v[1],
 96        )
 97        shapely_line = LineString([pt_a, pt_b])
 98
 99        # Calculate intersection
100        intersection = mask_boundary.intersection(shapely_line)
101
102        # Process intersection results
103        if intersection.is_empty:
104            return []  # Return empty list for no intersection
105
106        intersection_points = []
107        geoms_to_process = []
108
109        if isinstance(intersection, Point):
110            geoms_to_process.append(intersection)
111        elif isinstance(intersection, (MultiPoint, LineString, MultiLineString)):
112            # Use .geoms for MultiPoint/MultiLineString, .coords for LineString
113            if hasattr(intersection, "geoms"):
114                geoms_to_process.extend(list(intersection.geoms))
115            elif hasattr(intersection, "coords"):  # LineString (boundary coincidence)
116                # Extract points from the LineString coords
117                coords = list(intersection.coords)
118                for coord in coords:
119                    intersection_points.append(coord)  # Add individual vertices
120                # Avoid processing LineString further as Point below
121
122        # Extract coordinates from Point geometries
123        for geom in geoms_to_process:
124            if isinstance(geom, Point):
125                intersection_points.append((geom.x, geom.y))
126
127        # Remove duplicates if necessary (e.g., from LineString endpoints)
128        # Using list(dict.fromkeys(intersection_points)) preserves order unlike set
129        unique_intersection_points = list(dict.fromkeys(intersection_points))
130
131        return unique_intersection_points
132
133    except Exception as e:
134        print(f"Error during Shapely intersection calculation: {str(e)}")
135        return None  # Indicate an error occurred