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