sketch2pnml / pipeline /commons.py
sam0ed
Initial commit with Git LFS support
8eb0b3e
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)