Spaces:
Sleeping
Sleeping
import numpy as np | |
import cv2 | |
from .models import Line, Point | |
import inspect | |
import os | |
def here(resource: str): | |
"""Utils that given a relative path returns the corresponding absolute path, independently from the environment | |
Parameters | |
---------- | |
resource: str | |
The relative path of the given resource | |
Returns | |
------- | |
str | |
The absolute path of the give resource | |
""" | |
stack = inspect.stack() | |
caller_frame = stack[1][0] | |
caller_module = inspect.getmodule(caller_frame) | |
return os.path.abspath( | |
os.path.join(os.path.dirname(caller_module.__file__), resource) | |
) | |
def minmaxToContours(xyxy): | |
x1, y1, x2, y2 = xyxy | |
return np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype=np.int32) # Convert to contour format | |
def fill_contours(preprocessed_img, contours): | |
"""Fills the contours in the preprocessed image.""" | |
img_filled = preprocessed_img.copy() | |
cv2.drawContours(img_filled, contours, -1, (255), thickness=cv2.FILLED) | |
return img_filled | |
def remove_contours(preprocessed_img, contours): | |
"""Removes the detected node shapes (circles and rectangles) from the image.""" | |
contours_mask = np.zeros_like(preprocessed_img) | |
contours_mask = fill_contours(contours_mask, contours) | |
contours_mask = cv2.bitwise_not(contours_mask) | |
img_no_contours = cv2.bitwise_and(preprocessed_img, contours_mask) | |
return img_no_contours | |
def filter_enclosed_contours( | |
contours_to_filter: list[np.ndarray], | |
enclosing_contours_input: list[np.ndarray], | |
include_border: bool = True | |
) -> list[np.ndarray]: | |
""" | |
Filters contours from contours_to_filter that are entirely enclosed by ANY contour | |
in enclosing_contours_input. | |
Args: | |
contours_to_filter: List of contours (np.ndarray) to be filtered. | |
Each contour is typically an array of shape (N, 1, 2). | |
enclosing_contours_input: List of contours (np.ndarray) that act as enclosing shapes. | |
Each contour is typically an array of shape (M, 1, 2). | |
include_border: Whether points on the border of an enclosing_contour | |
count as inside. | |
Returns: | |
A list of contours from contours_to_filter that are fully enclosed. | |
Each contour in the returned list is a reference to an object in | |
the input contours_to_filter list. | |
""" | |
if not contours_to_filter or not enclosing_contours_input: | |
return [] | |
# Filter out empty enclosing contours and precompute their bounding rects | |
enclosing_contours = [] | |
brects_enclosing = [] | |
for c_enclosing in enclosing_contours_input: | |
# Ensure contour has points (shape[0] is the number of points) | |
if c_enclosing.shape[0] > 0: | |
enclosing_contours.append(c_enclosing) | |
brects_enclosing.append(cv2.boundingRect(c_enclosing)) | |
if not enclosing_contours: # No valid enclosing contours | |
return [] | |
result_contours = [] | |
# Keep track of indices of contours from contours_to_filter that have been added | |
added_contour_indices = set() | |
test_threshold = 0 if include_border else 1 | |
# Precompute bounding rects for contours_to_filter | |
# (x, y, w, h) | |
brects_to_filter = [] | |
for c in contours_to_filter: | |
if c.shape[0] > 0: | |
brects_to_filter.append(cv2.boundingRect(c)) | |
else: | |
# Placeholder for empty contours, they will be skipped later | |
brects_to_filter.append((0,0,0,0)) | |
for idx1, contour1 in enumerate(contours_to_filter): | |
if idx1 in added_contour_indices: | |
continue | |
# An empty contour (no points) cannot be considered enclosed | |
if contour1.shape[0] == 0: | |
continue | |
x1, y1, w1, h1 = brects_to_filter[idx1] | |
is_contour1_enclosed_by_any = False | |
for idx2, contour2 in enumerate(enclosing_contours): | |
x2, y2, w2, h2 = brects_enclosing[idx2] | |
# AABB Pruning: For contour1 to be enclosed by contour2, | |
# contour1's bounding box must be within contour2's bounding box. | |
if not (x1 >= x2 and \ | |
y1 >= y2 and \ | |
(x1 + w1) <= (x2 + w2) and \ | |
(y1 + h1) <= (y2 + h2)): | |
continue # Bounding box of contour1 is not contained in bounding box of contour2 | |
# Precise point-in-polygon test: | |
# All points of contour1 must be inside (or on border of) contour2 | |
all_points_inside = True | |
# Reshape contour1 from (N,1,2) to (N,2) for easier iteration | |
points_contour1 = contour1.reshape(-1, 2) | |
for pt_x, pt_y in points_contour1: | |
# cv2.pointPolygonTest expects point as (float, float) | |
dist = cv2.pointPolygonTest(contour2, (float(pt_x), float(pt_y)), False) | |
if dist < test_threshold: | |
all_points_inside = False | |
break # This point of contour1 is outside contour2 | |
if all_points_inside: | |
is_contour1_enclosed_by_any = True | |
break # contour1 is enclosed by contour2; no need to check other enclosing_contours | |
if is_contour1_enclosed_by_any: | |
result_contours.append(contour1) | |
added_contour_indices.add(idx1) | |
return result_contours | |
def dilate_contour(contour, image_shape, config): | |
if contour is None or len(contour) == 0: | |
raise ValueError("Invalid contour provided for dilation.") | |
dilation_kernel_size = config.get('shape_detection', {}).get('remove_nodes_dilation_kernel_size', [3, 3]) | |
dilation_iterations = config.get('shape_detection', {}).get('remove_nodes_dilation_iterations', 3) | |
height, width = image_shape | |
mask = np.zeros((height, width), dtype=np.uint8) | |
cv2.drawContours(mask, [contour], -1, (255), thickness=cv2.FILLED) | |
kernel = np.ones((dilation_kernel_size[0], dilation_kernel_size[1]), np.uint8) | |
dilated_mask = cv2.dilate(mask, kernel, iterations=dilation_iterations) | |
dilated_contours, _ = cv2.findContours(dilated_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
if dilated_contours: | |
largest_dilated_contour = max(dilated_contours, key=cv2.contourArea) | |
return largest_dilated_contour | |
else: | |
raise ValueError("No contours found after dilation.") | |
def find_closest_distance_to_contour(point_obj: Point, contour_np: np.ndarray) -> float: | |
""" | |
Calculates the shortest distance from a Point object to a contour. | |
The contour is a numpy array of points. | |
- If contour has 1 point, it's point-to-point distance. | |
- If contour has 2 points, it's distance to the line segment defined by these points. | |
- If contour has >2 points, it's distance to the boundary of the polygon defined by these points. | |
Args: | |
point_obj: The Point object from which to measure the distance. | |
contour_np: A NumPy array representing the contour, shape (N, 1, 2) or (N, 2). | |
Coordinates are typically integers. | |
Returns: | |
The shortest distance as a float. | |
""" | |
# Validate and standardize contour_np shape for point extraction | |
if contour_np.ndim == 3 and contour_np.shape[1] == 1 and contour_np.shape[2] == 2: | |
# Shape (N, 1, 2), reshape to (N, 2) for easier iteration | |
processed_contour_points = contour_np.reshape(-1, 2) | |
elif contour_np.ndim == 2 and contour_np.shape[1] == 2: | |
# Shape (N, 2), use as is | |
processed_contour_points = contour_np | |
else: | |
raise ValueError(f"Contour numpy array has an unsupported shape: {contour_np.shape}. " | |
"Expected (N, 1, 2) or (N, 2).") | |
num_contour_points = processed_contour_points.shape[0] | |
if num_contour_points == 0: | |
return float('inf') # No points in contour, distance is infinite | |
if num_contour_points == 1: | |
contour_pt_coords = processed_contour_points[0] | |
contour_pt_obj = Point(contour_pt_coords[0], contour_pt_coords[1]) | |
return point_obj.get_distance_between_points(contour_pt_obj) | |
if num_contour_points == 2: | |
pt_a_coords = processed_contour_points[0] | |
pt_b_coords = processed_contour_points[1] | |
# Create Point objects for the segment endpoints | |
segment_pt_a = Point(pt_a_coords[0], pt_a_coords[1]) | |
segment_pt_b = Point(pt_b_coords[0], pt_b_coords[1]) | |
line_segment = Line(segment_pt_a, segment_pt_b) | |
return line_segment.distance_point_to_segment(point_obj) | |
# num_contour_points > 2 (Polygon case) | |
else: | |
# cv2.pointPolygonTest requires contour in (N, 1, 2) format and float32 type. | |
# We use the original contour_np for this, as it might already be (N,1,2). | |
if contour_np.ndim == 2: # Original was (N,2) | |
contour_for_cv2 = contour_np.reshape((-1, 1, 2)).astype(np.float32) | |
else: # Original was (N,1,2) | |
contour_for_cv2 = contour_np.astype(np.float32) | |
# The query point for pointPolygonTest needs to be a float tuple | |
query_point_tuple = (float(point_obj.x), float(point_obj.y)) | |
# measureDist=True returns signed distance: | |
# The absolute value is the shortest distance to any edge of the contour. | |
distance = cv2.pointPolygonTest(contour_for_cv2, query_point_tuple, True) | |
return abs(distance) | |