Spaces:
Sleeping
Sleeping
File size: 14,499 Bytes
8eb0b3e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
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
@classmethod
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
@classmethod
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})"
|