Spaces:
Sleeping
Sleeping
import math | |
import numpy as np | |
import cv2 | |
import uuid | |
def is_number(n): | |
is_number = True | |
try: | |
num = float(n) | |
# check for "nan" floats | |
is_number = num == num # or use `math.isnan(num)` | |
except ValueError: | |
is_number = False | |
return is_number | |
class Point: | |
def __init__(self, x, y): | |
self.x = int(x) # Ensure integer coordinates if they represent pixels | |
self.y = int(y) | |
self.proximity_node = None # Placeholder for proximity node assignment | |
self.is_arrow = False # Placeholder for entry point assignment | |
def get_distance_between_points(self, other_point): | |
"""Calculate Euclidean distance between this point and another point.""" | |
return math.sqrt((self.x - other_point.x) ** 2 + (self.y - other_point.y) ** 2) | |
def is_inside_contour(self, contour): | |
"""Check if this point is inside a given contour using cv2.pointPolygonTest""" | |
# Note: This requires cv2, which might be better placed in a different module | |
point_tuple = (float(self.x), float(self.y)) # pointPolygonTest needs float tuple | |
# Ensure contour is in the correct format (e.g., Nx1x2 or Nx2) | |
try: | |
# >= 0 means inside or on the boundary | |
return cv2.pointPolygonTest(contour, point_tuple, False) >= 0 | |
except Exception as e: | |
print(f"Error during pointPolygonTest: {e}") | |
return False | |
def get_numpy_array(self): | |
"""Returns the point as a numpy array.""" | |
return np.array([self.x, self.y], dtype=np.int32) | |
def __repr__(self): | |
return f"Point({self.x}, {self.y})" | |
def __eq__(self, other): | |
if not isinstance(other, Point): | |
return NotImplemented | |
return self.x == other.x and self.y == other.y | |
def __hash__(self): | |
"""Allows Point objects to be added to sets or used as dictionary keys.""" | |
return hash((self.x, self.y)) | |
class Line: | |
def __init__(self, start_point: Point, end_point: Point, angle=None, length=None): | |
""" | |
Initializes a Line object. | |
If angle and length are not provided, they are calculated. | |
""" | |
self.point1 = start_point | |
self.point2 = end_point | |
# Assign self to the points for back-reference if needed later | |
# self.point1.part_of = self | |
# self.point2.part_of = self | |
if angle is None or length is None: | |
dx = self.point2.x - self.point1.x | |
dy = self.point2.y - self.point1.y | |
# Calculate angle in degrees | |
self.angle = math.degrees(math.atan2(dy, dx)) if not (dx == 0 and dy == 0) else 0.0 | |
# Calculate length | |
self.length = self.point1.get_distance_between_points(self.point2) | |
else: | |
self.angle = angle | |
self.length = length | |
def get_other_point(self, point: Point) -> Point: | |
"""Given one point of the line, returns the other point.""" | |
if point == self.point1: | |
return self.point2 | |
elif point == self.point2: | |
return self.point1 | |
else: | |
# This case should ideally not be reached if logic is correct | |
raise ValueError("Point is not part of this line.") | |
def get_vector(self, start_point: Point = None, end_point: Point = None) -> np.ndarray: | |
""" | |
Returns the vector of the line. | |
If start_point and end_point are provided, computes vector from start to end. | |
Otherwise, defaults to point1 -> point2. | |
""" | |
if start_point and end_point: | |
return np.array([end_point.x - start_point.x, end_point.y - start_point.y]) | |
return np.array([self.point2.x - self.point1.x, self.point2.y - self.point1.y]) | |
def get_normalized_vector(self, start_point: Point = None, end_point: Point = None) -> np.ndarray: | |
"""Returns the normalized (unit) vector of the line.""" | |
vec = self.get_vector(start_point, end_point) | |
norm = np.linalg.norm(vec) | |
if norm == 0: | |
return np.array([0, 0]) # Represents a zero-length line segment | |
return vec / norm | |
def distance_point_to_infinite_line(self, point: Point) -> float: | |
""" | |
Calculates the perpendicular distance from a point to the infinite line | |
defined by this line segment. | |
""" | |
p1_np = np.array([self.point1.x, self.point1.y]) | |
p2_np = np.array([self.point2.x, self.point2.y]) | |
p3_np = np.array([point.x, point.y]) | |
if np.array_equal(p1_np, p2_np): # If the line is just a point | |
return np.linalg.norm(p3_np - p1_np) | |
numerator = np.abs(np.cross(p2_np - p1_np, p1_np - p3_np)) | |
denominator = np.linalg.norm(p2_np - p1_np) | |
if denominator == 0: | |
return np.linalg.norm(p3_np - p1_np) # Distance to the single point | |
return numerator / denominator | |
def distance_point_to_segment(self, point: Point) -> float: | |
""" | |
Calculates the shortest distance from a query point to this line segment. | |
""" | |
# Convert query point and segment endpoints to numpy arrays | |
p_np = point.get_numpy_array().astype(float) | |
a_np = self.point1.get_numpy_array().astype(float) # Segment start (self.point1) | |
b_np = self.point2.get_numpy_array().astype(float) # Segment end (self.point2) | |
# If the segment is essentially a point (point1 and point2 are the same) | |
if self.point1 == self.point2: # Relies on Point.__eq__ | |
return point.get_distance_between_points(self.point1) | |
# Vector from A to B (segment vector) | |
vec_ab = b_np - a_np | |
# Vector from A to P (point relative to segment start) | |
vec_ap = p_np - a_np | |
t = np.dot(vec_ap, vec_ab) / np.dot(vec_ab, vec_ab) | |
if 0.0 <= t <= 1.0: | |
# The projection falls on the segment AB. | |
# The shortest distance is the perpendicular distance from P to the line AB. | |
# This can be calculated by self.distance_point_to_infinite_line(point). | |
return self.distance_point_to_infinite_line(point) | |
elif t < 0.0: | |
# The projection falls outside the segment, on the side of A. | |
# The closest point on the segment to P is A (self.point1). | |
return point.get_distance_between_points(self.point1) | |
else: # t > 1.0 | |
return point.get_distance_between_points(self.point2) | |
def __repr__(self): | |
return f"Line(start={self.point1}, end={self.point2}, angle={self.angle:.2f}, length={self.length:.2f})" | |
def __eq__(self, other): | |
if not isinstance(other, Line): | |
return NotImplemented | |
# A line is considered equal if its endpoints are the same, regardless of order. | |
return (self.point1 == other.point1 and self.point2 == other.point2) or \ | |
(self.point1 == other.point2 and self.point2 == other.point1) | |
def __hash__(self): | |
"""Allows Line objects to be added to sets. The hash is order-invariant for points.""" | |
# Hash the tuple of sorted point hashes | |
return hash(tuple(sorted((hash(self.point1), hash(self.point2))))) | |
##################################################################### | |
##################################################################### | |
class Place: | |
def __init__( | |
self, | |
circle: tuple[int, int, int], # (x, y, radius) | |
original_detection_data=None, # Placeholder for any original detection data | |
): | |
self.id = str(uuid.uuid4()) | |
self.center = Point(circle[0], circle[1]) | |
self.radius = circle[2] | |
self.center.part_of = self # Link back to the Place object | |
self.text = [] # Placeholder for any text associated with this place | |
self.original_detection_data = original_detection_data | |
self.markers = 0 # Placeholder for markers associated with this place | |
def from_contour(cls, contour: np.ndarray): | |
(x, y), radius = cv2.minEnclosingCircle(contour) | |
return cls((x, y, radius), original_detection_data= contour) | |
def update_markers_from_text(self): | |
""" | |
Recalculates and updates self.markers by summing numeric values | |
from associated Text objects in self.text. | |
Only text values that consist purely of digits after stripping whitespace | |
are considered numeric. | |
""" | |
current_sum_of_markers = 0 | |
for text_obj in self.text: # self.text is a list of Text objects | |
value_str = text_obj.value.strip() | |
if is_number(value_str): | |
try: | |
num_val = float(value_str) | |
if num_val != float('inf') and num_val != float('-inf'): | |
current_sum_of_markers += int(num_val) | |
self.text.remove(text_obj) ## Remove the text object from the list | |
except ValueError: | |
pass | |
self.markers = current_sum_of_markers | |
def get_name(self): | |
if len(self.text) > 0: | |
return " ".join(text.value for text in self.text if text.value.strip() != "") | |
else: | |
return "" | |
def __repr__(self): | |
return f"Place(center={self.center}, radius={self.radius})" | |
def __eq__(self, other): | |
if not isinstance(other, Place): | |
return NotImplemented | |
return (self.center == other.center and self.radius == other.radius) | |
def __hash__(self): | |
return hash((self.center, self.radius)) | |
class Transition: | |
def __init__( | |
self, | |
center_coords: tuple[int, int], # (x, y) | |
height: int, | |
width: int, | |
angle: float = 0.0, # Default angle | |
original_detection_data=None, | |
): | |
self.id = str(uuid.uuid4()) | |
self.center = Point(center_coords[0], center_coords[1]) | |
self.center.part_of = self | |
self.height = height | |
self.width = width | |
self.angle = angle # Angle in degrees | |
self.box_points = cv2.boxPoints(((self.center.x, self.center.y), (self.height, self.width), angle)) | |
self.points = [Point(int(pt[0]), int(pt[1])) for pt in self.box_points] | |
for point in self.points: | |
point.part_of = self | |
self.text = [] | |
self.original_detection_data = original_detection_data | |
def from_contour(cls, contour: np.ndarray): | |
min_area_rect = cv2.minAreaRect(contour) | |
return cls(min_area_rect[0], min_area_rect[1][0], min_area_rect[1][1], min_area_rect[2], original_detection_data=contour) | |
def __repr__(self): | |
return f"Transition(center={self.center}, height={self.height}, width={self.width}, angle={self.angle})" | |
def get_name(self): | |
if len(self.text) > 0: | |
return " ".join(text.value for text in self.text if text.value.strip() != "") | |
else: | |
return "" | |
def __eq__(self, other): | |
if not isinstance(other, Transition): | |
return NotImplemented | |
return (self.center == other.center and self.height == other.height and self.width == other.width and self.angle == other.angle) | |
def __hash__(self): | |
return hash((self.center, self.height, self.width, self.angle)) | |
### Potentially add an Arc class later if needed to represent the final connections | |
class Arc: | |
def __init__(self, source, target, start_point, end_point, points=None, lines=None): | |
self.id = str(uuid.uuid4()) | |
self.source = source # Place or Transition object | |
self.target = target # Place or Transition object | |
self.start_point = start_point # Point object | |
self.end_point = end_point # Point object | |
self.points = points # Optional: Ordered list of points forming the arc geometry | |
self.lines = lines # Optional: List of Line segments forming the arc geometry | |
self.text = [] # Placeholder for any text associated with this place | |
self.weight = 1 | |
def update_weight_from_text(self): | |
""" | |
Recalculates and updates self.weight by summing numeric values | |
from associated Text objects in self.text. | |
Only text values that consist purely of digits after stripping whitespace | |
are considered numeric. | |
""" | |
current_sum_of_weight = 1 | |
for text_obj in self.text: # self.text is a list of Text objects | |
value_str = text_obj.value.strip() | |
if is_number(value_str): | |
try: | |
num_val = float(value_str) | |
if num_val != float('inf') and num_val != float('-inf'): | |
current_sum_of_weight = int(num_val) | |
self.text.remove(text_obj) ## Remove the text object from the list | |
except ValueError: | |
pass | |
self.weight = current_sum_of_weight | |
def get_name(self): | |
if len(self.text) > 0: | |
return " ".join(text.value for text in self.text if text.value.strip() != "") | |
else: | |
return "" | |
def __repr__(self): | |
return f"Arc(source={self.source}, target={self.target})" | |
def __eq__(self, other): | |
if not isinstance(other, Arc): | |
return NotImplemented | |
return (self.source == other.source and self.target == other.target) | |
def __hash__(self): | |
return hash((self.source, self.target)) | |
class Text: | |
"""Represents a detected text element with its content and bounding box.""" | |
# Store geometry as absolute integer coordinates | |
def __init__(self, value: str, geometry_abs: tuple[tuple[int, int], tuple[int, int]], confidence: float): | |
""" | |
Args: | |
value: The recognized text string. | |
geometry_abs: Bounding box absolute coordinates ((xmin, ymin), (xmax, ymax)). | |
confidence: The recognition confidence score. | |
""" | |
self.value = value | |
self.pt1 = Point(geometry_abs[0][0], geometry_abs[0][1]) | |
self.pt2 = Point(geometry_abs[1][0], geometry_abs[1][1]) | |
self.center = Point( | |
(self.pt1.x + self.pt2.x) // 2, | |
(self.pt1.y + self.pt2.y) // 2 | |
) | |
self.confidence = confidence | |
def __repr__(self): | |
return f"Text(value='{self.value}', box=({self.pt1.x},{self.pt1.y})-({self.pt2.x},{self.pt2.y}), conf={self.confidence:.2f})" | |