garmentiq.landmark.derivation.mask_intersect

Intersection of a line with the garment mask contour.

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