Nighty3912 commited on
Commit
7137ab0
·
verified ·
1 Parent(s): f6b2b0a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +925 -197
app.py CHANGED
@@ -43,7 +43,7 @@ class DrawerNotDetectedError(Exception):
43
  pass
44
 
45
  class ReferenceBoxNotDetectedError(Exception):
46
- """Raised when the reference box cannot be detected in the image"""
47
  pass
48
 
49
  class BoundaryOverlapError(Exception):
@@ -69,10 +69,10 @@ print("YOLOWorld model loaded in {:.2f} seconds".format(time.time() - start_time
69
 
70
  print("Loading YOLO reference model...")
71
  start_time = time.time()
72
- reference_model_path = os.path.join(CACHE_DIR, "best.pt")
73
  if not os.path.exists(reference_model_path):
74
  print("Caching YOLO reference model to", reference_model_path)
75
- shutil.copy("best.pt", reference_model_path)
76
  reference_detector_global = YOLO(reference_model_path)
77
  print("YOLO reference model loaded in {:.2f} seconds".format(time.time() - start_time))
78
 
@@ -120,7 +120,7 @@ def unload_and_reload_models():
120
  gc.collect()
121
  new_drawer_detector = YOLOWorld(os.path.join(CACHE_DIR, "yolov8x-worldv2.pt"))
122
  new_drawer_detector.set_classes(["box"])
123
- new_reference_detector = YOLO(os.path.join(CACHE_DIR, "best.pt"))
124
  new_birefnet = AutoModelForImageSegmentation.from_pretrained(
125
  "zhengpeng7/BiRefNet", trust_remote_code=True, cache_dir=CACHE_DIR
126
  )
@@ -155,10 +155,10 @@ def yolo_detect(image: Union[str, Path, int, Image.Image, list, tuple, np.ndarra
155
 
156
  def detect_reference_square(img: np.ndarray):
157
  t = time.time()
158
- res = reference_detector_global.predict(img, conf=0.15)
159
  if not res or len(res) == 0 or len(res[0].boxes) == 0:
160
- raise ReferenceBoxNotDetectedError("Reference box not detected in the image.")
161
- print("Reference detection completed in {:.2f} seconds".format(time.time() - t))
162
  return (
163
  save_one_box(res[0].cpu().boxes.xyxy, res[0].orig_img, save=False),
164
  res[0].cpu().boxes.xyxy[0]
@@ -243,118 +243,811 @@ def exclude_scaling_box(image: np.ndarray, bbox: np.ndarray, orig_size: tuple, p
243
  image[expanded_y_min:expanded_y_max, expanded_x_min:expanded_x_max] = 0
244
  return image
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  def resample_contour(contour):
 
 
247
  num_points = 1000
248
  smoothing_factor = 5
249
  spline_degree = 3
 
250
  if len(contour) < spline_degree + 1:
251
- raise ValueError(f"Contour must have at least {spline_degree + 1} points, but has {len(contour)} points.")
252
- contour = contour[:, 0, :]
253
- tck, _ = splprep([contour[:, 0], contour[:, 1]], s=smoothing_factor)
254
- u = np.linspace(0, 1, num_points)
255
- resampled_points = splev(u, tck)
256
- smoothed_x = gaussian_filter1d(resampled_points[0], sigma=1)
257
- smoothed_y = gaussian_filter1d(resampled_points[1], sigma=1)
258
- return np.array([smoothed_x, smoothed_y]).T
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- # ---------------------
261
- # Add the missing extract_outlines function
262
- # ---------------------
263
  def extract_outlines(binary_image: np.ndarray) -> (np.ndarray, list):
264
- contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
265
- outline_image = np.zeros_like(binary_image)
266
- cv2.drawContours(outline_image, contours, -1, (255), thickness=2)
267
- return cv2.bitwise_not(outline_image), contours
 
 
 
 
 
 
 
 
 
 
 
268
 
269
- # ---------------------
270
- # Functions for Finger Cut Clearance
271
- # ---------------------
272
  def union_tool_and_circle(tool_polygon: Polygon, center_inch, circle_diameter=1.0):
273
- radius = circle_diameter / 2.0
274
- circle_poly = Point(center_inch).buffer(radius, resolution=64)
275
- union_poly = tool_polygon.union(circle_poly)
276
- return union_poly
 
 
 
 
 
 
 
 
 
277
 
278
  def build_tool_polygon(points_inch):
279
- return Polygon(points_inch)
 
 
 
 
 
 
 
 
280
 
281
- def polygon_to_exterior_coords(poly: Polygon):
282
- if poly.geom_type == "MultiPolygon":
283
- biggest = max(poly.geoms, key=lambda g: g.area)
284
- poly = biggest
285
- if not poly.exterior:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  return []
287
- return list(poly.exterior.coords)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=30):
290
- import random
291
  needed_center_distance = circle_diameter + min_gap
292
  radius = circle_diameter / 2.0
293
- attempts = 0
 
 
 
 
 
 
 
294
  indices = list(range(len(points_inch)))
295
- random.shuffle(indices) # Shuffle indices for randomness
296
-
297
- for i in indices:
298
- if attempts >= max_attempts:
299
- break
300
- cx, cy = points_inch[i]
301
- # Try small adjustments around the chosen candidate
302
- for dx in np.linspace(-0.1, 0.1, 5):
303
- for dy in np.linspace(-0.1, 0.1, 5):
304
- candidate_center = (cx + dx, cy + dy)
305
- # Check distance from already placed centers
306
- if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance for ex, ey in existing_centers):
307
- continue
308
- circle_poly = Point(candidate_center).buffer(radius, resolution=64)
309
- union_poly = tool_polygon.union(circle_poly)
310
- overlap = False
311
- # Check against other tool polygons for overlap or proximity issues
312
- for poly in all_polygons:
313
- if union_poly.intersects(poly) or circle_poly.buffer(min_gap).intersects(poly):
314
- overlap = True
315
- break
316
- if overlap:
317
- continue
318
- # If candidate passes, accept it
319
- existing_centers.append(candidate_center)
320
- return union_poly, candidate_center
321
- attempts += 1
322
- print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
323
- return None, None
324
 
325
- # ---------------------
326
- # DXF Spline and Boundary Functions
327
- # ---------------------
328
- def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  degree = 3
330
  closed = True
331
- doc = ezdxf.new(units=0)
332
- doc.units = ezdxf.units.IN
333
- doc.header["$INSUNITS"] = ezdxf.units.IN
334
- msp = doc.modelspace()
335
- finger_cut_centers = []
336
- final_polygons_inch = []
337
- for contour in inflated_contours:
338
- try:
339
- resampled_contour = resample_contour(contour)
340
- points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
341
- if len(points_inch) < 3:
342
- continue
343
- if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
344
- points_inch.append(points_inch[0])
345
- tool_polygon = build_tool_polygon(points_inch)
346
- if finger_clearance:
347
- union_poly, center = place_finger_cut_adjusted(tool_polygon, points_inch, finger_cut_centers, final_polygons_inch, circle_diameter=1.0, min_gap=0.25, max_attempts=30)
348
- if union_poly is not None:
349
- tool_polygon = union_poly
350
- exterior_coords = polygon_to_exterior_coords(tool_polygon)
351
- if len(exterior_coords) < 3:
352
- continue
353
- msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
354
- final_polygons_inch.append(tool_polygon)
355
- except ValueError as e:
356
- print(f"Skipping contour: {e}")
357
- return doc, final_polygons_inch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
  def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width, offset_unit, annotation_text="", image_height_in=None, image_width_in=None):
360
  msp = doc.modelspace()
@@ -411,13 +1104,11 @@ def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width
411
  msp.add_lwpolyline(rect_coords, close=True, dxfattribs={"layer": "BOUNDARY"})
412
 
413
  text_top = boundary_polygon.bounds[1] + 1
414
- if (annotation_text.strip()==0):
415
- if boundary_width_in <= inner_width + 2 * clearance_side or boundary_length_in <= inner_length + 2 * clearance_tb:
416
- raise BoundaryOverlapError("Error: The specified boundary dimensions are too small and overlap with the inner contours. Please provide larger values.")
417
- else:
418
- if text_top > (min_y - 0.75):
419
- raise TextOverlapError("Error: The Text is overlapping the inner contours of the object.")
420
-
421
  return boundary_polygon
422
 
423
  def draw_polygons_inch(polygons_inch, image_rgb, scaling_factor, image_height, color=(0,0,255), thickness=2):
@@ -501,7 +1192,7 @@ def predict(
501
  try:
502
  t = time.time()
503
  reference_obj_img, scaling_box_coords = detect_reference_square(shrunked_img)
504
- print("Reference square detection completed in {:.2f} seconds".format(time.time() - t))
505
  except ReferenceBoxNotDetectedError as e:
506
  return None, None, None, None, f"Error: {str(e)}"
507
 
@@ -511,14 +1202,15 @@ def predict(
511
  t = time.time()
512
  reference_obj_img = make_square(reference_obj_img)
513
  reference_square_mask = remove_bg_u2netp(reference_obj_img)
 
514
  print("Reference image processing completed in {:.2f} seconds".format(time.time() - t))
515
 
516
  t = time.time()
517
  try:
518
  cv2.imwrite("mask.jpg", cv2.cvtColor(reference_obj_img, cv2.COLOR_RGB2GRAY))
519
  scaling_factor = calculate_scaling_factor(
520
- reference_image_path="./Reference_ScalingBox.jpg",
521
  target_image=reference_square_mask,
 
522
  feature_detector="ORB",
523
  )
524
  except ZeroDivisionError:
@@ -529,8 +1221,8 @@ def predict(
529
  print(f"Error calculating scaling factor: {e}")
530
 
531
  if scaling_factor is None or scaling_factor == 0:
532
- scaling_factor = 1.0
533
- print("Using default scaling factor of 1.0 due to calculation error")
534
  gc.collect()
535
  print("Scaling factor determined: {}".format(scaling_factor))
536
 
@@ -556,15 +1248,21 @@ def predict(
556
  if offset_value < 1:
557
  offset_value = offset_value * 25.4
558
  offset_inches = offset_value / 25.4
 
 
 
 
559
  else:
560
  offset_inches = offset_value
 
 
561
 
562
  t = time.time()
563
  orig_size = shrunked_img.shape[:2]
564
  objects_mask = remove_bg(shrunked_img)
565
  processed_size = objects_mask.shape[:2]
566
 
567
- objects_mask = exclude_scaling_box(objects_mask, scaling_box_coords, orig_size, processed_size, expansion_factor=2)
568
  objects_mask = resize_img(objects_mask, (shrunked_img.shape[1], shrunked_img.shape[0]))
569
  del scaling_box_coords
570
  gc.collect()
@@ -594,7 +1292,7 @@ def predict(
594
  t = time.time()
595
  use_finger_clearance = True if finger_clearance.lower() == "yes" else False
596
  doc, final_polygons_inch = save_dxf_spline(
597
- contours, scaling_factor, processed_size[0], finger_clearance=use_finger_clearance
598
  )
599
  del contours
600
  gc.collect()
@@ -638,28 +1336,29 @@ def predict(
638
  msp = doc.modelspace()
639
 
640
  if annotation_text.strip():
641
- text_x = ((inner_min_x + inner_max_x) / 2.0) - (int(len(annotation_text.strip()) / 2.0))
642
- text_height_dxf = 0.75
643
- text_y_dxf = boundary_polygon.bounds[1] + 0.25
644
- font = get_font_face("Arial")
645
- paths = text2path.make_paths_from_str(
646
- annotation_text.strip().upper(),
647
- font=font, # Use default font
648
- size=text_height_dxf,
649
- align=TextEntityAlignment.LEFT
650
- )
 
 
 
 
 
 
651
 
652
- # Create a translation matrix
653
- translation = ezdxf.math.Matrix44.translate(text_x, text_y_dxf, 0)
654
- # Apply the translation to each path
655
- translated_paths = [p.transform(translation) for p in paths]
656
-
657
- # Render the paths as splines and polylines
658
- path.render_splines_and_polylines(
659
- msp,
660
- translated_paths,
661
- dxfattribs={"layer": "ANNOTATION", "color": 7}
662
- )
663
 
664
  # Save the DXF
665
  dxf_filepath = os.path.join("./outputs", "out.dxf")
@@ -673,64 +1372,92 @@ def predict(
673
  draw_polygons_inch(final_polygons_inch, new_outlines, scaling_factor, processed_size[0], color=(0, 0, 255), thickness=2)
674
 
675
  if annotation_text.strip():
676
- text_height_cv = 0.75
677
- text_x_img = int(((inner_min_x + inner_max_x) / 2.0) / scaling_factor)
678
- text_y_in = boundary_polygon.bounds[1] + 0.25
679
- text_y_img = int(processed_size[0] - (text_y_in / scaling_factor))
680
- org = (text_x_img - int(len(annotation_text.strip()) * 6), text_y_img)
681
-
682
- # Method 2: Use two different thicknesses
683
- # Draw thicker outline
684
- temp_img = np.zeros_like(output_img)
685
-
686
- cv2.putText(
687
- temp_img,
688
- annotation_text.strip().upper(),
689
- org,
690
- cv2.FONT_HERSHEY_SIMPLEX,
691
- 2,
692
- (0, 0, 255), # Red color
693
- 4, # Thicker outline
694
- cv2.LINE_AA
695
- )
696
-
697
- cv2.putText(
698
- temp_img,
699
- annotation_text.strip().upper(),
700
- org,
701
- cv2.FONT_HERSHEY_SIMPLEX,
702
- 2,
703
- (0, 0, 0), # Black to create hole
704
- 2, # Thinner inner part
705
- cv2.LINE_AA
706
- )
707
-
708
- outline_mask = cv2.cvtColor(temp_img, cv2.COLOR_BGR2GRAY)
709
- _, outline_mask = cv2.threshold(outline_mask, 1, 255, cv2.THRESH_BINARY)
710
-
711
- output_img[outline_mask > 0] = temp_img[outline_mask > 0]
712
-
713
- cv2.putText(
714
- new_outlines,
715
- annotation_text.strip().upper(),
716
- org,
717
- cv2.FONT_HERSHEY_SIMPLEX,
718
- 2,
719
- (0, 0, 255), # Red color
720
- 4, # Thicker outline
721
- cv2.LINE_AA
722
- )
723
-
724
- cv2.putText(
725
- new_outlines,
726
- annotation_text.strip().upper(),
727
- org,
728
- cv2.FONT_HERSHEY_SIMPLEX,
729
- 2,
730
- (255, 255, 255), # Inner text in white
731
- 2, # Thinner inner part
732
- cv2.LINE_AA
733
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
 
735
  outlines_color = cv2.cvtColor(new_outlines, cv2.COLOR_BGR2RGB)
736
  print("Total prediction time: {:.2f} seconds".format(time.time() - overall_start))
@@ -759,10 +1486,10 @@ if __name__ == "__main__":
759
  gr.Image(label="Input Image"),
760
  gr.Number(label="Offset value for Mask", value=0.075),
761
  gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches"),
762
- gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="No"),
763
- gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="No"),
764
- gr.Number(label="Boundary Length", value=300.0, precision=2),
765
- gr.Number(label="Boundary Width", value=200.0, precision=2),
766
  gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
767
  ],
768
  outputs=[
@@ -773,8 +1500,9 @@ if __name__ == "__main__":
773
  gr.Textbox(label="Scaling Factor (inches/pixel)")
774
  ],
775
  examples=[
776
- ["./Test20.jpg", 0.075, "inches", "No", "No", 300.0, 200.0, "MyTool"],
777
  ["./Test21.jpg", 0.075, "inches", "Yes", "Yes", 300.0, 200.0, "Tool2"]
778
  ]
779
  )
780
- iface.launch(share=True)
 
 
43
  pass
44
 
45
  class ReferenceBoxNotDetectedError(Exception):
46
+ """Raised when the Reference coin cannot be detected in the image"""
47
  pass
48
 
49
  class BoundaryOverlapError(Exception):
 
69
 
70
  print("Loading YOLO reference model...")
71
  start_time = time.time()
72
+ reference_model_path = os.path.join(CACHE_DIR, "coin_det.pt")
73
  if not os.path.exists(reference_model_path):
74
  print("Caching YOLO reference model to", reference_model_path)
75
+ shutil.copy("coin_det.pt", reference_model_path)
76
  reference_detector_global = YOLO(reference_model_path)
77
  print("YOLO reference model loaded in {:.2f} seconds".format(time.time() - start_time))
78
 
 
120
  gc.collect()
121
  new_drawer_detector = YOLOWorld(os.path.join(CACHE_DIR, "yolov8x-worldv2.pt"))
122
  new_drawer_detector.set_classes(["box"])
123
+ new_reference_detector = YOLO(os.path.join(CACHE_DIR, "coin_det.pt"))
124
  new_birefnet = AutoModelForImageSegmentation.from_pretrained(
125
  "zhengpeng7/BiRefNet", trust_remote_code=True, cache_dir=CACHE_DIR
126
  )
 
155
 
156
  def detect_reference_square(img: np.ndarray):
157
  t = time.time()
158
+ res = reference_detector_global.predict(img, conf=0.3)
159
  if not res or len(res) == 0 or len(res[0].boxes) == 0:
160
+ raise ReferenceBoxNotDetectedError("Reference Coin not detected in the image.")
161
+ print("Reference coin detection completed in {:.2f} seconds".format(time.time() - t))
162
  return (
163
  save_one_box(res[0].cpu().boxes.xyxy, res[0].orig_img, save=False),
164
  res[0].cpu().boxes.xyxy[0]
 
243
  image[expanded_y_min:expanded_y_max, expanded_x_min:expanded_x_max] = 0
244
  return image
245
 
246
+ # def resample_contour(contour):
247
+ # num_points = 1000
248
+ # smoothing_factor = 5
249
+ # spline_degree = 3
250
+ # if len(contour) < spline_degree + 1:
251
+ # raise ValueError(f"Contour must have at least {spline_degree + 1} points, but has {len(contour)} points.")
252
+ # contour = contour[:, 0, :]
253
+ # tck, _ = splprep([contour[:, 0], contour[:, 1]], s=smoothing_factor)
254
+ # u = np.linspace(0, 1, num_points)
255
+ # resampled_points = splev(u, tck)
256
+ # smoothed_x = gaussian_filter1d(resampled_points[0], sigma=1)
257
+ # smoothed_y = gaussian_filter1d(resampled_points[1], sigma=1)
258
+ # return np.array([smoothed_x, smoothed_y]).T
259
+
260
+ # # ---------------------
261
+ # # Add the missing extract_outlines function
262
+ # # ---------------------
263
+ # def extract_outlines(binary_image: np.ndarray) -> (np.ndarray, list):
264
+ # contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
265
+ # outline_image = np.zeros_like(binary_image)
266
+ # cv2.drawContours(outline_image, contours, -1, (255), thickness=2)
267
+ # return cv2.bitwise_not(outline_image), contours
268
+
269
+ # # # ---------------------
270
+ # # # Functions for Finger Cut Clearance
271
+ # # # ---------------------
272
+ # # def union_tool_and_circle(tool_polygon: Polygon, center_inch, circle_diameter=1.0):
273
+ # # radius = circle_diameter / 2.0
274
+ # # circle_poly = Point(center_inch).buffer(radius, resolution=64)
275
+ # # union_poly = tool_polygon.union(circle_poly)
276
+ # # return union_poly
277
+
278
+
279
+
280
+
281
+ # # def build_tool_polygon(points_inch):
282
+ # # return Polygon(points_inch)
283
+
284
+ # # def polygon_to_exterior_coords(poly: Polygon):
285
+ # # if poly.geom_type == "MultiPolygon":
286
+ # # biggest = max(poly.geoms, key=lambda g: g.area)
287
+ # # poly = biggest
288
+ # # if not poly.exterior:
289
+ # # return []
290
+ # # return list(poly.exterior.coords)
291
+
292
+
293
+ # # from shapely.geometry import Point, Polygon
294
+ # # import numpy as np
295
+ # # import random
296
+
297
+ # # from shapely.geometry import Point, Polygon
298
+ # # import numpy as np
299
+ # # import random
300
+
301
+ # # def place_finger_cut_adjusted(
302
+ # # tool_polygon: Polygon,
303
+ # # points_inch: list,
304
+ # # existing_centers: list,
305
+ # # all_polygons: list,
306
+ # # circle_diameter: float = 1.0,
307
+ # # min_gap: float = 0.5,
308
+ # # max_attempts: int = 100
309
+ # # ) -> (Polygon, tuple):
310
+
311
+ # # needed_center_distance = circle_diameter + min_gap
312
+ # # radius = circle_diameter / 2.0
313
+ # # attempts = 0
314
+ # # timeout_secs = 10 # 100s timeout
315
+ # # start_time = time.perf_counter()
316
+ # # fallback_triggered = False
317
+
318
+ # # # Randomize candidate points order.
319
+ # # indices = list(range(len(points_inch)))
320
+ # # random.shuffle(indices)
321
+
322
+ # # while attempts < max_attempts and not fallback_triggered:
323
+ # # for i in indices:
324
+ # # if time.perf_counter() - start_time >= timeout_secs:
325
+ # # fallback_triggered = True
326
+ # # break
327
+ # # cx, cy = points_inch[i]
328
+ # # # Try small adjustments around the candidate point.
329
+ # # for dx in np.linspace(-0.3, 0.3, 7): # adjust by ±0.3 inches in 7 steps
330
+ # # for dy in np.linspace(-0.3, 0.3, 7):
331
+ # # if time.perf_counter() - start_time >= timeout_secs:
332
+ # # fallback_triggered = True
333
+ # # break
334
+ # # candidate_center = (cx + dx, cy + dy)
335
+
336
+ # # # Ensure candidate center is not too close to any already placed centers.
337
+ # # if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance
338
+ # # for ex, ey in existing_centers):
339
+ # # continue
340
+
341
+ # # # Create candidate circle with a high resolution.
342
+ # # candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
343
+
344
+ # # # Reject candidate if circle is completely inside the tool polygon.
345
+ # # if tool_polygon.contains(candidate_circle):
346
+ # # continue
347
+
348
+ # # # Also reject candidate if circle does not intersect the tool at all.
349
+ # # if not candidate_circle.intersects(tool_polygon):
350
+ # # continue
351
+
352
+
353
+ # # # Ensure that the candidate circle crosses the tool boundary.
354
+ # # inter_area = candidate_circle.intersection(tool_polygon).area
355
+ # # if inter_area <= 0 or inter_area >= candidate_circle.area:
356
+ # # continue
357
+
358
+ # # # Verify candidate circle is not too close to any neighboring tool polygons.
359
+ # # too_close = False
360
+ # # for other_poly in all_polygons:
361
+ # # if other_poly.equals(tool_polygon):
362
+ # # continue
363
+ # # if candidate_circle.buffer(0.1).intersects(other_poly):
364
+ # # too_close = True
365
+ # # if other_poly.distance(candidate_circle) < min_gap:
366
+ # # too_close = True
367
+ # # break
368
+ # # if too_close:
369
+ # # continue
370
+
371
+ # # # Attempt the union, using a buffering trick to fix potential geometry problems.
372
+ # # try:
373
+ # # union_poly = tool_polygon.union(candidate_circle)
374
+ # # except Exception:
375
+ # # union_poly = tool_polygon.buffer(0).union(candidate_circle.buffer(0))
376
+
377
+ # # # Verify that the union is a single contiguous polygon.
378
+ # # if union_poly.geom_type == "MultiPolygon" and len(union_poly.geoms) > 1:
379
+ # # continue
380
+
381
+ # # # If the union did not change the polygon (no effective union), skip candidate.
382
+ # # if union_poly.equals(tool_polygon):
383
+ # # continue
384
+
385
+ # # # We have found a valid candidate.
386
+ # # existing_centers.append(candidate_center)
387
+ # # return union_poly, candidate_center
388
+ # # if fallback_triggered:
389
+ # # break
390
+ # # attempts += 1
391
+ # # if fallback_triggered:
392
+ # # print("In fallback block")
393
+
394
+ # # # Fallback: If no candidate was found after max_attempts, force a candidate from median of points.
395
+ # # candidate_center = points_inch[len(points_inch) // 2]
396
+ # # candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
397
+ # # try:
398
+ # # too_close= False
399
+ # # for other_poly in all_polygons:
400
+
401
+ # # if other_poly.equals(tool_polygon):
402
+ # # continue
403
+ # # if candidate_circle.buffer(0.1).intersects(other_poly):
404
+ # # too_close = True
405
+ # # if other_poly.distance(candidate_circle) < min_gap:
406
+ # # too_close = True
407
+ # # if too_close:
408
+ # # continue
409
+ # # union_poly = tool_polygon.union(candidate_circle)
410
+ # # except Exception:
411
+ # # too_close= False
412
+ # # for other_poly in all_polygons:
413
+
414
+ # # if other_poly.equals(tool_polygon):
415
+ # # continue
416
+ # # if candidate_circle.buffer(0.1).intersects(other_poly):
417
+ # # too_close = True
418
+ # # if other_poly.distance(candidate_circle) < min_gap:
419
+ # # too_close = True
420
+ # # if too_close:
421
+ # # continue
422
+ # # union_poly = tool_polygon.buffer(0).union(candidate_circle.buffer(0))
423
+ # # existing_centers.append(candidate_center)
424
+ # # return union_poly, candidate_center
425
+
426
+ # # # ---------------------
427
+ # # # DXF Spline and Boundary Functions
428
+ # # # ---------------------
429
+ # # def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=False):
430
+ # degree = 3
431
+ # closed = True
432
+ # doc = ezdxf.new(units=0)
433
+ # doc.units = ezdxf.units.IN
434
+ # doc.header["$INSUNITS"] = ezdxf.units.IN
435
+ # msp = doc.modelspace()
436
+ # finger_cut_centers = []
437
+ # final_polygons_inch = []
438
+ # for contour in inflated_contours:
439
+ # try:
440
+ # resampled_contour = resample_contour(contour)
441
+ # points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
442
+ # if len(points_inch) < 3:
443
+ # continue
444
+ # if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
445
+ # points_inch.append(points_inch[0])
446
+ # tool_polygon = build_tool_polygon(points_inch)
447
+ # if finger_clearance:
448
+ # union_poly, center = place_finger_cut_adjusted(tool_polygon, points_inch, finger_cut_centers, final_polygons_inch, circle_diameter=1.0, min_gap=0.25, max_attempts=100)
449
+ # if union_poly is not None:
450
+ # tool_polygon = union_poly
451
+ # exterior_coords = polygon_to_exterior_coords(tool_polygon)
452
+ # if len(exterior_coords) < 3:
453
+ # continue
454
+ # msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
455
+ # final_polygons_inch.append(tool_polygon)
456
+ # except ValueError as e:
457
+ # print(f"Skipping contour: {e}")
458
+ # return doc, final_polygons_inch
459
+
460
+ # import random
461
+ # import time
462
+ # import numpy as np
463
+ # from shapely.geometry import Point, Polygon
464
+
465
+ # # ---------------------
466
+ # # Utility functions
467
+ # # ---------------------
468
+ # def union_tool_and_circle(tool_polygon: Polygon, center_inch, circle_diameter=1.0):
469
+ # radius = circle_diameter / 2.0
470
+ # circle_poly = Point(center_inch).buffer(radius, resolution=64)
471
+ # union_poly = tool_polygon.union(circle_poly)
472
+ # return union_poly
473
+
474
+ # def build_tool_polygon(points_inch):
475
+ # return Polygon(points_inch)
476
+
477
+ # def polygon_to_exterior_coords(poly: Polygon):
478
+ # if poly.geom_type == "MultiPolygon":
479
+ # biggest = max(poly.geoms, key=lambda g: g.area)
480
+ # poly = biggest
481
+ # if not poly.exterior:
482
+ # return []
483
+ # return list(poly.exterior.coords)
484
+
485
+ # ---------------------
486
+ # Main candidate placement function
487
+ # ---------------------
488
+ # def place_finger_cut_adjusted(
489
+ # tool_polygon1: Polygon,
490
+ # points_inch: list,
491
+ # existing_centers: list,
492
+ # all_polygons: list,
493
+ # circle_diameter: float = 1.0,
494
+ # min_gap: float = 0.5,
495
+ # max_attempts: int = 100
496
+ # ) -> (Polygon, tuple):
497
+ # """
498
+ # Adjust and union a candidate circle (finger cut) with the tool_polygon.
499
+ # If a candidate meeting all conditions is found, update existing_centers
500
+ # and return the union and candidate_center.
501
+
502
+ # If no candidate is found after max_attempts (or if a timeout is reached),
503
+ # use a fallback candidate (the median point from points_inch).
504
+ # """
505
+ # needed_center_distance = circle_diameter + min_gap
506
+ # radius = circle_diameter / 2.0
507
+ # attempts = 0
508
+ # timeout_secs = 0.1 # 100ms timeout
509
+ # start_time = time.perf_counter()
510
+ # fallback_triggered = False
511
+ # tool_polygon= tool_polygon1
512
+
513
+ # # Randomize candidate points order.
514
+ # indices = list(range(len(points_inch)))
515
+ # random.shuffle(indices)
516
+
517
+ # while attempts < max_attempts and not fallback_triggered:
518
+ # for i in indices:
519
+ # if time.perf_counter() - start_time >= timeout_secs:
520
+ # fallback_triggered = True
521
+ # break
522
+ # cx, cy = points_inch[i]
523
+ # # Try small adjustments around the candidate point.
524
+ # for dx in np.linspace(-0.3, 0.3, 7):
525
+ # for dy in np.linspace(-0.3, 0.3, 7):
526
+ # if time.perf_counter() - start_time >= timeout_secs:
527
+ # fallback_triggered = True
528
+ # break
529
+ # candidate_center = (cx + dx, cy + dy)
530
+
531
+ # # Ensure candidate center is not too close to any already placed centers.
532
+ # if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance
533
+ # for ex, ey in existing_centers):
534
+ # continue
535
+
536
+ # # Create candidate circle with high resolution.
537
+ # candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
538
+
539
+ # # Reject candidate if circle is completely inside the tool polygon.
540
+ # if tool_polygon.contains(candidate_circle):
541
+ # continue
542
+ # # Reject candidate if circle does not intersect the tool at all.
543
+ # if not candidate_circle.intersects(tool_polygon):
544
+ # continue
545
+ # # Ensure that the candidate circle crosses the tool boundary.
546
+ # inter_area = candidate_circle.intersection(tool_polygon).area
547
+ # if inter_area <= 0 or inter_area >= candidate_circle.area:
548
+ # continue
549
+
550
+ # # Verify candidate circle is not too close to any neighboring tool polygons.
551
+ # too_close = False
552
+ # for other_poly in all_polygons:
553
+ # if other_poly.equals(tool_polygon):
554
+ # continue
555
+ # # Use a small buffer around the circle for safety.
556
+ # if candidate_circle.buffer(0.1).intersects(other_poly):
557
+ # too_close = True
558
+ # if other_poly.distance(candidate_circle) < min_gap:
559
+ # too_close = True
560
+ # break
561
+ # if too_close:
562
+ # continue
563
+
564
+ # # Attempt the union, using buffering to fix any potential geometry issues.
565
+ # try:
566
+ # union_poly = tool_polygon.union(candidate_circle)
567
+ # except Exception:
568
+ # union_poly = tool_polygon.buffer(0).union(candidate_circle.buffer(0))
569
+
570
+ # # Clean the unioned polygon.
571
+ # union_poly = union_poly.buffer(0)
572
+
573
+ # # Verify that the union is a single contiguous polygon.
574
+ # if union_poly.geom_type == "MultiPolygon" and len(union_poly.geoms) > 1:
575
+ # continue
576
+
577
+ # # If the union did not change the tool polygon (no effective union), skip candidate.
578
+ # if union_poly.equals(tool_polygon):
579
+ # continue
580
+
581
+ # # We have found a valid candidate. Update the centers list.
582
+ # existing_centers.append(candidate_center)
583
+ # return tool_polygon1, existing_centers[-1]
584
+ # if fallback_triggered:
585
+ # break
586
+ # attempts += 1
587
+
588
+ # # Fallback: If no candidate is found (or timeout reached), use a fallback candidate.
589
+ # if fallback_triggered:
590
+ # print("Fallback triggered")
591
+ # # Use a fallback candidate – here the median point is used.
592
+ # xs = [p[0] for p in points_inch]
593
+ # ys = [p[1] for p in points_inch]
594
+ # candidate_center = (np.median(xs), np.median(ys))
595
+ # candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
596
+ # try:
597
+ # union_poly = tool_polygon.union(candidate_circle)
598
+ # except Exception:
599
+ # union_poly = tool_polygon.buffer(0).union(candidate_circle.buffer(0))
600
+ # union_poly = union_poly.buffer(0)
601
+ # # Add the fallback center to avoid duplicate placements later.
602
+ # existing_centers.append(candidate_center)
603
+ # return tool_polygon1, existing_centers[-1]
604
+
605
+ # ---------------------
606
+ # DXF Spline and Boundary Functions
607
+ # ---------------------
608
+ # def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=False):
609
+ # import ezdxf # assuming ezdxf is installed
610
+ # degree = 3
611
+ # closed = True
612
+ # doc = ezdxf.new(units=0)
613
+ # doc.units = ezdxf.units.IN
614
+ # doc.header["$INSUNITS"] = ezdxf.units.IN
615
+ # msp = doc.modelspace()
616
+
617
+ # # Global shared lists for finger cut centers and final tool polygons.
618
+ # finger_cut_centers = []
619
+ # final_polygons_inch = []
620
+
621
+ # for contour in inflated_contours:
622
+ # try:
623
+ # # resample_contour should be defined elsewhere;
624
+ # # here it returns a list of (x, y) points.
625
+ # resampled_contour = resample_contour(contour)
626
+ # # Scale and flip Y coordinate according to height.
627
+ # points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
628
+
629
+ # if len(points_inch) < 3:
630
+ # continue
631
+
632
+ # # Ensure the polygon is closed.
633
+ # if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
634
+ # points_inch.append(points_inch[0])
635
+
636
+ # tool_polygon = build_tool_polygon(points_inch)
637
+
638
+ # # Add finger clearance cuts if needed.
639
+ # if finger_clearance:
640
+ # tool, center = place_finger_cut_adjusted(
641
+ # tool_polygon, points_inch, finger_cut_centers, final_polygons_inch,
642
+ # circle_diameter=1.0, min_gap=0.25, max_attempts=100
643
+ # )
644
+ # union_poly=union_tool_and_circle(tool,center)
645
+ # if union_poly is not None:
646
+ # tool_polygon = union_poly
647
+
648
+ # exterior_coords = polygon_to_exterior_coords(tool_polygon)
649
+ # if len(exterior_coords) < 3:
650
+ # continue
651
+
652
+ # # Add the tool geometry to the DXF document as a spline.
653
+ # msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
654
+ # final_polygons_inch.append(tool_polygon)
655
+ # except ValueError as e:
656
+ # print(f"Skipping contour: {e}")
657
+
658
+ # return doc, final_polygons_inch
659
+
660
+ import logging
661
+ import time
662
+ import signal
663
+ import numpy as np
664
+ import cv2
665
+ from scipy.interpolate import splprep, splev
666
+ from scipy.ndimage import gaussian_filter1d
667
+ from shapely.geometry import Point, Polygon
668
+ import random
669
+ import ezdxf
670
+ import functools
671
+
672
+ # Set up logging
673
+ logging.basicConfig(
674
+ level=logging.INFO,
675
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
676
+ )
677
+ logger = logging.getLogger(__name__)
678
+
679
+ # Custom TimeoutError class
680
+ class TimeoutReachedError(Exception):
681
+ pass
682
+
683
+ # Timeout context manager
684
+ class TimeoutContext:
685
+ def __init__(self, seconds):
686
+ self.seconds = seconds
687
+ self.original_handler = None
688
+
689
+ def timeout_handler(self, signum, frame):
690
+ raise TimeoutReachedError(f"Function timed out after {self.seconds} seconds")
691
+
692
+ def __enter__(self):
693
+ if hasattr(signal, 'SIGALRM'): # Unix-like systems
694
+ self.original_handler = signal.getsignal(signal.SIGALRM)
695
+ signal.signal(signal.SIGALRM, self.timeout_handler)
696
+ signal.alarm(self.seconds)
697
+ self.start_time = time.time()
698
+ return self
699
+
700
+ def __exit__(self, exc_type, exc_val, exc_tb):
701
+ if hasattr(signal, 'SIGALRM'): # Unix-like systems
702
+ signal.alarm(0)
703
+ signal.signal(signal.SIGALRM, self.original_handler)
704
+ if exc_type is TimeoutReachedError:
705
+ logger.warning(f"Timeout reached: {exc_val}")
706
+ return True # Suppress the exception
707
+ return False
708
+
709
  def resample_contour(contour):
710
+ logger.info(f"Starting resample_contour with contour of shape {contour.shape}")
711
+
712
  num_points = 1000
713
  smoothing_factor = 5
714
  spline_degree = 3
715
+
716
  if len(contour) < spline_degree + 1:
717
+ error_msg = f"Contour must have at least {spline_degree + 1} points, but has {len(contour)} points."
718
+ logger.error(error_msg)
719
+ raise ValueError(error_msg)
720
+
721
+ try:
722
+ contour = contour[:, 0, :]
723
+ logger.debug(f"Reshaped contour to shape {contour.shape}")
724
+
725
+ tck, _ = splprep([contour[:, 0], contour[:, 1]], s=smoothing_factor)
726
+ logger.debug("Generated spline parameters")
727
+
728
+ u = np.linspace(0, 1, num_points)
729
+ resampled_points = splev(u, tck)
730
+ logger.debug(f"Resampled to {num_points} points")
731
+
732
+ smoothed_x = gaussian_filter1d(resampled_points[0], sigma=1)
733
+ smoothed_y = gaussian_filter1d(resampled_points[1], sigma=1)
734
+
735
+ result = np.array([smoothed_x, smoothed_y]).T
736
+ logger.info(f"Completed resample_contour with result shape {result.shape}")
737
+ return result
738
+ except Exception as e:
739
+ logger.error(f"Error in resample_contour: {e}")
740
+ raise
741
 
 
 
 
742
  def extract_outlines(binary_image: np.ndarray) -> (np.ndarray, list):
743
+ logger.info(f"Starting extract_outlines with image shape {binary_image.shape}")
744
+
745
+ try:
746
+ contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
747
+ logger.debug(f"Found {len(contours)} contours")
748
+
749
+ outline_image = np.zeros_like(binary_image)
750
+ cv2.drawContours(outline_image, contours, -1, (255), thickness=2)
751
+
752
+ result_image = cv2.bitwise_not(outline_image)
753
+ logger.info(f"Completed extract_outlines with {len(contours)} contours")
754
+ return result_image, contours
755
+ except Exception as e:
756
+ logger.error(f"Error in extract_outlines: {e}")
757
+ raise
758
 
 
 
 
759
  def union_tool_and_circle(tool_polygon: Polygon, center_inch, circle_diameter=1.0):
760
+ logger.info(f"Starting union_tool_and_circle with center at {center_inch}")
761
+
762
+ try:
763
+ radius = circle_diameter / 2.0
764
+ circle_poly = Point(center_inch).buffer(radius, resolution=64)
765
+ logger.debug(f"Created circle with radius {radius} at {center_inch}")
766
+
767
+ union_poly = tool_polygon.union(circle_poly)
768
+ logger.info(f"Completed union_tool_and_circle, result area: {union_poly.area}")
769
+ return union_poly
770
+ except Exception as e:
771
+ logger.error(f"Error in union_tool_and_circle: {e}")
772
+ raise
773
 
774
  def build_tool_polygon(points_inch):
775
+ logger.info(f"Starting build_tool_polygon with {len(points_inch)} points")
776
+
777
+ try:
778
+ polygon = Polygon(points_inch)
779
+ logger.info(f"Completed build_tool_polygon, polygon area: {polygon.area}")
780
+ return polygon
781
+ except Exception as e:
782
+ logger.error(f"Error in build_tool_polygon: {e}")
783
+ raise
784
 
785
+ # def polygon_to_exterior_coords(poly: Polygon):
786
+ # logger.info(f"Starting polygon_to_exterior_coords with polygon type {poly.geom_type}")
787
+
788
+ # try:
789
+ # if poly.geom_type == "MultiPolygon":
790
+ # logger.debug("Converting MultiPolygon to single Polygon")
791
+ # biggest = max(poly.geoms, key=lambda g: g.area)
792
+ # poly = biggest
793
+
794
+ # if not poly.exterior:
795
+ # logger.warning("Polygon has no exterior")
796
+ # return []
797
+
798
+ # coords = list(poly.exterior.coords)
799
+ # logger.info(f"Completed polygon_to_exterior_coords with {len(coords)} coordinates")
800
+ # return coords
801
+ # except Exception as e:
802
+ # logger.error(f"Error in polygon_to_exterior_coords: {e}")
803
+ # raise
804
+
805
+ def polygon_to_exterior_coords(poly):
806
+ logger.info(f"Starting polygon_to_exterior_coords with polygon type {poly.geom_type}")
807
+
808
+ try:
809
+ # Handle GeometryCollection case specifically
810
+ if poly.geom_type == "GeometryCollection":
811
+ logger.warning("Converting GeometryCollection to Polygon")
812
+ # Find the largest geometry in the collection that has an exterior
813
+ largest_area = 0
814
+ largest_geom = None
815
+ for geom in poly.geoms:
816
+ if hasattr(geom, 'area') and geom.area > largest_area:
817
+ if hasattr(geom, 'exterior') or geom.geom_type == "MultiPolygon":
818
+ largest_area = geom.area
819
+ largest_geom = geom
820
+
821
+ if largest_geom is None:
822
+ logger.warning("No valid geometry found in GeometryCollection")
823
+ return []
824
+
825
+ poly = largest_geom
826
+
827
+ if poly.geom_type == "MultiPolygon":
828
+ logger.debug("Converting MultiPolygon to single Polygon")
829
+ biggest = max(poly.geoms, key=lambda g: g.area)
830
+ poly = biggest
831
+
832
+ if not hasattr(poly, 'exterior') or poly.exterior is None:
833
+ logger.warning("Polygon has no exterior")
834
+ return []
835
+
836
+ coords = list(poly.exterior.coords)
837
+ logger.info(f"Completed polygon_to_exterior_coords with {len(coords)} coordinates")
838
+ return coords
839
+ except Exception as e:
840
+ logger.error(f"Error in polygon_to_exterior_coords: {e}")
841
+ # Return empty list as fallback
842
  return []
843
+
844
+ def place_finger_cut_adjusted(
845
+ tool_polygon: Polygon,
846
+ points_inch: list,
847
+ existing_centers: list,
848
+ all_polygons: list,
849
+ circle_diameter: float = 1.0,
850
+ min_gap: float = 0.5,
851
+ max_attempts: int = 100
852
+ ) -> (Polygon, tuple):
853
+ logger.info(f"Starting place_finger_cut_adjusted with {len(points_inch)} points")
854
+
855
+ # Define fallback function for timeout case
856
+ def fallback_solution():
857
+ logger.warning("Using fallback approach for finger cut placement")
858
+ candidate_center = points_inch[len(points_inch) // 2]
859
+ radius = circle_diameter / 2.0
860
+ candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
861
+
862
+ try:
863
+ union_poly = tool_polygon.union(candidate_circle)
864
+ except Exception as e:
865
+ logger.warning(f"Fallback union failed, using buffer trick: {e}")
866
+ union_poly = tool_polygon.buffer(0).union(candidate_circle.buffer(0))
867
+
868
+ existing_centers.append(candidate_center)
869
+ logger.info(f"Used fallback finger cut at center {candidate_center}")
870
+ return union_poly, candidate_center
871
 
 
 
872
  needed_center_distance = circle_diameter + min_gap
873
  radius = circle_diameter / 2.0
874
+
875
+ # Limit points to prevent timeout - use a subset for efficient processing
876
+ if len(points_inch) > 100:
877
+ logger.info(f"Limiting points from {len(points_inch)} to 100 for efficiency")
878
+ step = len(points_inch) // 100
879
+ points_inch = points_inch[::step]
880
+
881
+ # Randomize candidate points order
882
  indices = list(range(len(points_inch)))
883
+ random.shuffle(indices)
884
+ logger.debug(f"Shuffled {len(indices)} point indices")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885
 
886
+ # Use a non-blocking timeout approach with explicit time checks
887
+ start_time = time.time()
888
+ timeout_seconds = 5
889
+ attempts = 0
890
+
891
+ try:
892
+ while attempts < max_attempts:
893
+ # Check if we're approaching the timeout
894
+ current_time = time.time()
895
+ if current_time - start_time > timeout_seconds - 0.1: # Leave 0.1s margin
896
+ logger.warning(f"Approaching timeout after {attempts} attempts")
897
+ return fallback_solution()
898
+
899
+ # Process a batch of points to improve efficiency
900
+ for i in indices:
901
+ # Check timeout frequently
902
+ if time.time() - start_time > timeout_seconds - 0.05:
903
+ logger.warning("Timeout during point processing")
904
+ return fallback_solution()
905
+
906
+ cx, cy = points_inch[i]
907
+ # Reduce the number of adjustments to speed up processing
908
+ for dx, dy in [(0,0), (-0.2,0), (0.2,0), (0,0.2), (0,-0.2)]:
909
+ candidate_center = (cx + dx, cy + dy)
910
+
911
+ # Quick check for existing centers distance
912
+ if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance
913
+ for ex, ey in existing_centers):
914
+ continue
915
+
916
+ # Create candidate circle
917
+ candidate_circle = Point(candidate_center).buffer(radius, resolution=32) # Reduced resolution
918
+
919
+ # Quick geometric checks
920
+ if tool_polygon.contains(candidate_circle) or not candidate_circle.intersects(tool_polygon):
921
+ continue
922
+
923
+ # Check intersection area - use simplified geometry for speed
924
+ try:
925
+ inter_area = candidate_circle.intersection(tool_polygon).area
926
+ if inter_area <= 0 or inter_area >= candidate_circle.area:
927
+ continue
928
+ except Exception:
929
+ continue
930
+
931
+ # Quick distance check to other polygons
932
+ too_close = False
933
+ for other_poly in all_polygons:
934
+ if other_poly.equals(tool_polygon):
935
+ continue
936
+ if other_poly.distance(candidate_circle) < min_gap:
937
+ too_close = True
938
+ break
939
+ if too_close:
940
+ continue
941
+
942
+ # Attempt the union
943
+ try:
944
+ union_poly = tool_polygon.union(candidate_circle)
945
+ # Check if we got a multi-polygon when we don't want one
946
+ if union_poly.geom_type == "MultiPolygon" and len(union_poly.geoms) > 1:
947
+ continue
948
+ # Check if the union actually changed anything
949
+ if union_poly.equals(tool_polygon):
950
+ continue
951
+ except Exception:
952
+ continue
953
+
954
+ # We found a valid candidate
955
+ existing_centers.append(candidate_center)
956
+ logger.info(f"Completed place_finger_cut_adjusted successfully at center {candidate_center}")
957
+ return union_poly, candidate_center
958
+
959
+ attempts += 1
960
+ # If we've made several attempts and are running out of time, use fallback
961
+ if attempts >= max_attempts // 2 and (time.time() - start_time) > timeout_seconds * 0.8:
962
+ logger.warning(f"Approaching timeout after {attempts} attempts")
963
+ return fallback_solution()
964
+
965
+ logger.debug(f"Completed attempt {attempts}/{max_attempts}")
966
+
967
+ # If we reached max attempts without finding a solution
968
+ logger.warning(f"No suitable finger cut found after {max_attempts} attempts, using fallback")
969
+ return fallback_solution()
970
+
971
+ except Exception as e:
972
+ logger.error(f"Error in place_finger_cut_adjusted: {e}")
973
+ return fallback_solution()
974
+
975
+ def save_dxf_spline(offset_value,inflated_contours, scaling_factor, height, finger_clearance=False):
976
+ logger.info(f"Starting save_dxf_spline with {len(inflated_contours)} contours")
977
+
978
  degree = 3
979
  closed = True
980
+
981
+ try:
982
+ doc = ezdxf.new(units=0)
983
+ doc.units = ezdxf.units.IN
984
+ doc.header["$INSUNITS"] = ezdxf.units.IN
985
+ msp = doc.modelspace()
986
+
987
+ finger_cut_centers = []
988
+ final_polygons_inch = []
989
+
990
+ for idx, contour in enumerate(inflated_contours):
991
+ logger.debug(f"Processing contour {idx+1}/{len(inflated_contours)}")
992
+
993
+ try:
994
+ resampled_contour = resample_contour(contour)
995
+ points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
996
+
997
+ if len(points_inch) < 3:
998
+ logger.warning(f"Skipping contour {idx}: insufficient points ({len(points_inch)})")
999
+ continue
1000
+
1001
+ if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
1002
+ logger.debug("Closing contour by adding first point to end")
1003
+ points_inch.append(points_inch[0])
1004
+
1005
+ tool_polygon = build_tool_polygon(points_inch)
1006
+
1007
+ if finger_clearance:
1008
+ logger.debug("Applying finger clearance")
1009
+ try:
1010
+ # Use a hard 5-second timeout for the entire finger cut operation
1011
+ start_time = time.time()
1012
+ union_poly, center = place_finger_cut_adjusted(
1013
+ tool_polygon,
1014
+ points_inch,
1015
+ finger_cut_centers,
1016
+ final_polygons_inch,
1017
+ circle_diameter=1.0,
1018
+ min_gap=(0.25+offset_value),
1019
+ max_attempts=100
1020
+ )
1021
+
1022
+ # Check if we exceeded the timeout anyway
1023
+ if time.time() - start_time > 5:
1024
+ logger.warning(f"Finger cut took too long for contour {idx} ({time.time() - start_time:.2f}s)")
1025
+
1026
+ if union_poly is not None:
1027
+ tool_polygon = union_poly
1028
+ logger.debug(f"Applied finger cut at {center}")
1029
+ except Exception as e:
1030
+ logger.warning(f"Finger cut failed for contour {idx}: {e}, using original polygon")
1031
+
1032
+ exterior_coords = polygon_to_exterior_coords(tool_polygon)
1033
+
1034
+ if len(exterior_coords) < 3:
1035
+ logger.warning(f"Skipping contour {idx}: insufficient exterior points ({len(exterior_coords)})")
1036
+ continue
1037
+
1038
+ msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
1039
+ final_polygons_inch.append(tool_polygon)
1040
+ logger.debug(f"Added spline for contour {idx}")
1041
+
1042
+ except ValueError as e:
1043
+ logger.warning(f"Skipping contour {idx}: {e}")
1044
+
1045
+ logger.info(f"Completed save_dxf_spline with {len(final_polygons_inch)} successful polygons")
1046
+ return doc, final_polygons_inch
1047
+
1048
+ except Exception as e:
1049
+ logger.error(f"Error in save_dxf_spline: {e}")
1050
+ raise
1051
 
1052
  def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width, offset_unit, annotation_text="", image_height_in=None, image_width_in=None):
1053
  msp = doc.modelspace()
 
1104
  msp.add_lwpolyline(rect_coords, close=True, dxfattribs={"layer": "BOUNDARY"})
1105
 
1106
  text_top = boundary_polygon.bounds[1] + 1
1107
+ too_small = boundary_width_in < inner_width + 2 * clearance_side or boundary_length_in < inner_length + 2 * clearance_tb
1108
+ if too_small:
1109
+ raise BoundaryOverlapError("Error: The specified boundary dimensions are too small and overlap with the inner contours. Please provide larger values.")
1110
+ if annotation_text.strip() and text_top > min_y - 0.75:
1111
+ raise TextOverlapError("Error: The text is too close to the inner contours. Please increase boundary length.")
 
 
1112
  return boundary_polygon
1113
 
1114
  def draw_polygons_inch(polygons_inch, image_rgb, scaling_factor, image_height, color=(0,0,255), thickness=2):
 
1192
  try:
1193
  t = time.time()
1194
  reference_obj_img, scaling_box_coords = detect_reference_square(shrunked_img)
1195
+ print("Reference coin detection completed in {:.2f} seconds".format(time.time() - t))
1196
  except ReferenceBoxNotDetectedError as e:
1197
  return None, None, None, None, f"Error: {str(e)}"
1198
 
 
1202
  t = time.time()
1203
  reference_obj_img = make_square(reference_obj_img)
1204
  reference_square_mask = remove_bg_u2netp(reference_obj_img)
1205
+ reference_square_mask= resize_img(reference_square_mask,(reference_obj_img.shape[1],reference_obj_img.shape[0]))
1206
  print("Reference image processing completed in {:.2f} seconds".format(time.time() - t))
1207
 
1208
  t = time.time()
1209
  try:
1210
  cv2.imwrite("mask.jpg", cv2.cvtColor(reference_obj_img, cv2.COLOR_RGB2GRAY))
1211
  scaling_factor = calculate_scaling_factor(
 
1212
  target_image=reference_square_mask,
1213
+ reference_obj_size_mm=0.955,
1214
  feature_detector="ORB",
1215
  )
1216
  except ZeroDivisionError:
 
1221
  print(f"Error calculating scaling factor: {e}")
1222
 
1223
  if scaling_factor is None or scaling_factor == 0:
1224
+ scaling_factor = 0.7
1225
+ print("Using default scaling factor of 0.7 due to calculation error")
1226
  gc.collect()
1227
  print("Scaling factor determined: {}".format(scaling_factor))
1228
 
 
1248
  if offset_value < 1:
1249
  offset_value = offset_value * 25.4
1250
  offset_inches = offset_value / 25.4
1251
+ if offset_value==0:
1252
+ offset_value = offset_value * 25.4
1253
+ offset_inches = offset_value / 25.4
1254
+ offset_inches+=0.005
1255
  else:
1256
  offset_inches = offset_value
1257
+ if offset_inches==0:
1258
+ offset_inches+=0.005
1259
 
1260
  t = time.time()
1261
  orig_size = shrunked_img.shape[:2]
1262
  objects_mask = remove_bg(shrunked_img)
1263
  processed_size = objects_mask.shape[:2]
1264
 
1265
+ objects_mask = exclude_scaling_box(objects_mask, scaling_box_coords, orig_size, processed_size, expansion_factor=1.2)
1266
  objects_mask = resize_img(objects_mask, (shrunked_img.shape[1], shrunked_img.shape[0]))
1267
  del scaling_box_coords
1268
  gc.collect()
 
1292
  t = time.time()
1293
  use_finger_clearance = True if finger_clearance.lower() == "yes" else False
1294
  doc, final_polygons_inch = save_dxf_spline(
1295
+ offset_inches,contours, scaling_factor, processed_size[0], finger_clearance=use_finger_clearance
1296
  )
1297
  del contours
1298
  gc.collect()
 
1336
  msp = doc.modelspace()
1337
 
1338
  if annotation_text.strip():
1339
+ if boundary_polygon is not None:
1340
+ text_x = ((inner_min_x + inner_max_x) / 2.0) - (int(len(annotation_text.strip()) / 2.0))
1341
+ text_height_dxf = 0.75
1342
+ text_y_dxf = boundary_polygon.bounds[1] + 0.25
1343
+ font = get_font_face("Arial")
1344
+ paths = text2path.make_paths_from_str(
1345
+ annotation_text.strip().upper(),
1346
+ font=font, # Use default font
1347
+ size=text_height_dxf,
1348
+ align=TextEntityAlignment.LEFT
1349
+ )
1350
+
1351
+ # Create a translation matrix
1352
+ translation = ezdxf.math.Matrix44.translate(text_x, text_y_dxf, 0)
1353
+ # Apply the translation to each path
1354
+ translated_paths = [p.transform(translation) for p in paths]
1355
 
1356
+ # Render the paths as splines and polylines
1357
+ path.render_splines_and_polylines(
1358
+ msp,
1359
+ translated_paths,
1360
+ dxfattribs={"layer": "ANNOTATION", "color": 7}
1361
+ )
 
 
 
 
 
1362
 
1363
  # Save the DXF
1364
  dxf_filepath = os.path.join("./outputs", "out.dxf")
 
1372
  draw_polygons_inch(final_polygons_inch, new_outlines, scaling_factor, processed_size[0], color=(0, 0, 255), thickness=2)
1373
 
1374
  if annotation_text.strip():
1375
+ if boundary_polygon is not None:
1376
+ text_height_cv = 0.75
1377
+ text_x_img = int(((inner_min_x + inner_max_x) / 2.0) / scaling_factor)
1378
+ text_y_in = boundary_polygon.bounds[1] + 0.25
1379
+ text_y_img = int(processed_size[0] - (text_y_in / scaling_factor))
1380
+ org = (text_x_img - int(len(annotation_text.strip()) * 6), text_y_img)
1381
+
1382
+ # Method 2: Use two different thicknesses
1383
+ # Draw thicker outline
1384
+ temp_img = np.zeros_like(output_img)
1385
+
1386
+ cv2.putText(
1387
+ temp_img,
1388
+ annotation_text.strip().upper(),
1389
+ org,
1390
+ cv2.FONT_HERSHEY_SIMPLEX,
1391
+ 2,
1392
+ (0, 0, 255), # Red color
1393
+ 4, # Thicker outline
1394
+ cv2.LINE_AA
1395
+ )
1396
+
1397
+ cv2.putText(
1398
+ temp_img,
1399
+ annotation_text.strip().upper(),
1400
+ org,
1401
+ cv2.FONT_HERSHEY_SIMPLEX,
1402
+ 2,
1403
+ (0, 0, 0), # Black to create hole
1404
+ 2, # Thinner inner part
1405
+ cv2.LINE_AA
1406
+ )
1407
+
1408
+ outline_mask = cv2.cvtColor(temp_img, cv2.COLOR_BGR2GRAY)
1409
+ _, outline_mask = cv2.threshold(outline_mask, 1, 255, cv2.THRESH_BINARY)
1410
+
1411
+ output_img[outline_mask > 0] = temp_img[outline_mask > 0]
1412
+
1413
+ cv2.putText(
1414
+ new_outlines,
1415
+ annotation_text.strip().upper(),
1416
+ org,
1417
+ cv2.FONT_HERSHEY_SIMPLEX,
1418
+ 2,
1419
+ (0, 0, 255), # Red color
1420
+ 4, # Thicker outline
1421
+ cv2.LINE_AA
1422
+ )
1423
+
1424
+ cv2.putText(
1425
+ new_outlines,
1426
+ annotation_text.strip().upper(),
1427
+ org,
1428
+ cv2.FONT_HERSHEY_SIMPLEX,
1429
+ 2,
1430
+ (255, 255, 255), # Inner text in white
1431
+ 2, # Thinner inner part
1432
+ cv2.LINE_AA
1433
+ )
1434
+ else:
1435
+ text_height_cv = 0.75
1436
+ text_x_img = int(((inner_min_x + inner_max_x) / 2.0) / scaling_factor)
1437
+ text_y_in = inner_min_y - 0.125 - text_height_cv
1438
+ text_y_img = int(processed_size[0] - (text_y_in / scaling_factor))
1439
+ org = (text_x_img - int(len(annotation_text.strip()) * 6), text_y_img)
1440
+
1441
+ cv2.putText(
1442
+ output_img,
1443
+ annotation_text.strip(),
1444
+ org,
1445
+ cv2.FONT_HERSHEY_SIMPLEX,
1446
+ 1.2,
1447
+ (0, 0, 255),
1448
+ 2,
1449
+ cv2.LINE_AA
1450
+ )
1451
+ cv2.putText(
1452
+ new_outlines,
1453
+ annotation_text.strip(),
1454
+ org,
1455
+ cv2.FONT_HERSHEY_SIMPLEX,
1456
+ 1.2,
1457
+ (0, 0, 255),
1458
+ 2,
1459
+ cv2.LINE_AA
1460
+ )
1461
 
1462
  outlines_color = cv2.cvtColor(new_outlines, cv2.COLOR_BGR2RGB)
1463
  print("Total prediction time: {:.2f} seconds".format(time.time() - overall_start))
 
1486
  gr.Image(label="Input Image"),
1487
  gr.Number(label="Offset value for Mask", value=0.075),
1488
  gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches"),
1489
+ gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="Yes"),
1490
+ gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="Yes"),
1491
+ gr.Number(label="Boundary Length", value=50, precision=2),
1492
+ gr.Number(label="Boundary Width", value=50, precision=2),
1493
  gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
1494
  ],
1495
  outputs=[
 
1500
  gr.Textbox(label="Scaling Factor (inches/pixel)")
1501
  ],
1502
  examples=[
1503
+ ["./Test20.jpg", 0.075, "inches", "Yes", "No", 300.0, 200.0, "MyTool"],
1504
  ["./Test21.jpg", 0.075, "inches", "Yes", "Yes", 300.0, 200.0, "Tool2"]
1505
  ]
1506
  )
1507
+ iface.launch(share=True)
1508
+