Nighty3912 commited on
Commit
cbe7186
·
verified ·
1 Parent(s): e10b0ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +766 -507
app.py CHANGED
@@ -53,8 +53,7 @@ class BoundaryOverlapError(Exception):
53
  class TextOverlapError(Exception):
54
  """Raised when the text overlaps with the inner contours (with a margin of 0.75)."""
55
  pass
56
- class boundary_issue(Exception):
57
- """Raised when bounds are given but rectangular boundary is no."""
58
  # ---------------------
59
  # Global Model Initialization with caching and print statements
60
  # ---------------------
@@ -159,7 +158,7 @@ def detect_reference_square(img: np.ndarray):
159
  res = reference_detector_global.predict(img, conf=0.3)
160
  if not res or len(res) == 0 or len(res[0].boxes) == 0:
161
  raise ReferenceBoxNotDetectedError("Reference Coin not detected in the image.")
162
- print("Reference detection completed in {:.2f} seconds".format(time.time() - t))
163
  return (
164
  save_one_box(res[0].cpu().boxes.xyxy, res[0].orig_img, save=False),
165
  res[0].cpu().boxes.xyxy[0]
@@ -244,88 +243,238 @@ def exclude_scaling_box(image: np.ndarray, bbox: np.ndarray, orig_size: tuple, p
244
  image[expanded_y_min:expanded_y_max, expanded_x_min:expanded_x_max] = 0
245
  return image
246
 
247
- def resample_contour(contour):
248
- num_points = 1000
249
- smoothing_factor = 5
250
- spline_degree = 3
251
- if len(contour) < spline_degree + 1:
252
- raise ValueError(f"Contour must have at least {spline_degree + 1} points, but has {len(contour)} points.")
253
- contour = contour[:, 0, :]
254
- tck, _ = splprep([contour[:, 0], contour[:, 1]], s=smoothing_factor)
255
- u = np.linspace(0, 1, num_points)
256
- resampled_points = splev(u, tck)
257
- smoothed_x = gaussian_filter1d(resampled_points[0], sigma=1)
258
- smoothed_y = gaussian_filter1d(resampled_points[1], sigma=1)
259
- return np.array([smoothed_x, smoothed_y]).T
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- # ---------------------
262
- # Add the missing extract_outlines function
263
- # ---------------------
264
- def extract_outlines(binary_image: np.ndarray) -> (np.ndarray, list):
265
- contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
266
- outline_image = np.zeros_like(binary_image)
267
- cv2.drawContours(outline_image, contours, -1, (255), thickness=2)
268
- return cv2.bitwise_not(outline_image), contours
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
- # ---------------------
271
- # Functions for Finger Cut Clearance
272
- # ---------------------
273
- # def union_tool_and_circle(tool_polygon: Polygon, center_inch, circle_diameter=1.0): #original
 
 
 
 
 
274
  # radius = circle_diameter / 2.0
275
  # circle_poly = Point(center_inch).buffer(radius, resolution=64)
276
  # union_poly = tool_polygon.union(circle_poly)
277
  # return union_poly
278
 
279
- def union_tool_and_circle(tool_polygon, center_inch, circle_diameter=1.0):
280
- """Union a tool polygon with a circle with extensive validation."""
281
- if tool_polygon is None or not isinstance(tool_polygon, Polygon):
282
- print("Invalid tool polygon provided")
283
- return None
284
-
285
- radius = circle_diameter / 2.0
286
- try:
287
- # Create the circle
288
- circle_poly = Point(center_inch).buffer(radius, resolution=64)
289
-
290
- # Make sure both geometries are valid
291
- if not tool_polygon.is_valid:
292
- tool_polygon = tool_polygon.buffer(0)
293
- if not circle_poly.is_valid:
294
- print("Invalid circle geometry")
295
- return tool_polygon
296
-
297
- # Perform union
298
- result = tool_polygon.union(circle_poly)
299
-
300
- # Validate result
301
- if result.is_empty:
302
- print(f"Union resulted in empty geometry at {center_inch}")
303
- return tool_polygon
304
-
305
- # Handle multi-polygon results
306
- if result.geom_type == "MultiPolygon":
307
- # Take the largest piece to avoid fragmentation
308
- result = max(result.geoms, key=lambda g: g.area)
309
-
310
- # Final validation
311
- if not result.is_valid:
312
- print("Union produced invalid geometry, returning original polygon")
313
- return tool_polygon
314
-
315
- # Check exterior points
316
- if not result.exterior or len(list(result.exterior.coords)) < 4:
317
- print(f"Union resulted in degenerate polygon with insufficient points")
318
- return tool_polygon
319
-
320
- return result
321
- except Exception as e:
322
- print(f"Exception during union operation: {e}")
323
- return tool_polygon
324
 
325
- def build_tool_polygon(points_inch):
326
- return Polygon(points_inch)
327
-
328
- # def polygon_to_exterior_coords(poly: Polygon): # works fine original
329
  # if poly.geom_type == "MultiPolygon":
330
  # biggest = max(poly.geoms, key=lambda g: g.area)
331
  # poly = biggest
@@ -333,456 +482,533 @@ def build_tool_polygon(points_inch):
333
  # return []
334
  # return list(poly.exterior.coords)
335
 
336
- def polygon_to_exterior_coords(poly):
337
- """Extract exterior coordinates with robust error handling."""
338
- if poly is None:
339
- print("Warning: Null polygon provided")
340
- return []
341
-
342
- try:
343
- if poly.geom_type == "MultiPolygon":
344
- if len(poly.geoms) == 0:
345
- print("Warning: Empty MultiPolygon")
346
- return []
347
- biggest = max(poly.geoms, key=lambda g: g.area)
348
- poly = biggest
349
-
350
- if not poly.exterior:
351
- print("Warning: Polygon has no exterior")
352
- return []
353
-
354
- coords = list(poly.exterior.coords)
355
- if len(coords) < 4:
356
- print(f"Warning: Polygon has insufficient coordinates: {len(coords)}")
357
-
358
- return coords
359
- except Exception as e:
360
- print(f"Error extracting polygon coordinates: {e}")
361
- return []
362
-
363
-
364
-
365
- # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1, min_gap=1, max_attempts=500): #1st best
366
  # needed_center_distance = circle_diameter + min_gap
367
  # radius = circle_diameter / 2.0
368
- # import random
369
- # for _ in range(max_attempts):
370
- # idx = random.randint(0, len(points_inch) - 1)
371
- # cx, cy = points_inch[idx]
372
-
373
- # # Check if this point is too close to an existing center
374
- # too_close = any(np.hypot(cx - ex_x, cy - ex_y) < needed_center_distance for ex_x, ex_y in existing_centers)
375
- # if too_close:
376
- # continue
377
-
378
- # # Create the finger cut circle and try adding it to the tool
379
- # circle_poly = Point((cx, cy)).buffer(radius, resolution=64)
380
- # union_poly = tool_polygon.union(circle_poly)
381
-
382
- # # Check for overlap and spacing with other tools
383
- # overlap_with_others = False
384
- # too_close_to_others = False
385
-
386
- # for poly in all_polygons:
387
- # if poly.equals(tool_polygon):
388
- # continue # Skip comparing the tool to itself
389
-
390
- # if union_poly.buffer(min_gap).intersects(poly) > 1e-6:
391
- # overlap_with_others = True
392
- # break
393
 
394
- # if circle_poly.buffer(min_gap).intersects(poly) > 1e-6:
395
- # too_close_to_others = True
 
 
 
 
 
 
396
  # break
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- # if overlap_with_others or too_close_to_others:
399
- # continue
 
 
 
 
 
 
 
 
 
 
 
400
 
401
- # existing_centers.append((cx, cy))
402
- # return union_poly, (cx, cy)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
- # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
405
- # return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
- import numpy as np
408
- from shapely.geometry import Point
 
 
 
 
 
 
 
 
 
409
 
 
 
 
410
 
411
- # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.35, max_attempts=2000): #Best best
412
- # import random
413
- # import numpy as np
414
- # from shapely.geometry import Point
415
-
416
- # needed_center_distance = circle_diameter + min_gap
417
- # radius = circle_diameter / 2.0
418
- # attempts = 0
419
- # indices = list(range(len(points_inch)))
420
- # random.shuffle(indices) # Shuffle indices for randomness
421
-
422
- # # Try a grid of adjustments around each candidate point
423
- # adjustments = list(np.linspace(-0.15, 0.10, 7)) # More adjustment options
424
-
425
- # for i in indices:
426
- # if attempts >= max_attempts:
427
- # break
428
 
429
- # cx, cy = points_inch[i]
430
-
431
- # # Try small adjustments around the chosen candidate
432
- # for dx in adjustments:
433
- # for dy in adjustments:
434
- # attempts += 1
435
- # if attempts >= max_attempts:
436
- # break
437
-
438
- # candidate_center = (cx + dx, cy + dy)
439
-
440
- # # Check distance from already placed centers
441
- # too_close_to_existing = False
442
- # for ex, ey in existing_centers:
443
- # if np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance:
444
- # too_close_to_existing = True
445
- # break
446
-
447
- # if too_close_to_existing:
448
- # continue
449
-
450
- # # Create circle polygon for this candidate
451
- # circle_poly = Point(candidate_center).buffer(radius, resolution=64)
452
-
453
- # # Create the union with the tool polygon
454
- # union_poly = tool_polygon.union(circle_poly)
455
-
456
- # # Buffer the circle to check minimum gap requirements
457
- # circle_buffer = circle_poly.buffer(min_gap, resolution=32)
458
- # coords = polygon_to_exterior_coords(union_poly)
459
-
460
- # # Check against all other polygons for overlap or proximity issues
461
- # overlap = False
462
- # for poly in all_polygons:
463
- # if poly == tool_polygon:
464
- # continue # Skip comparing to self
465
- # if len(coords) < 4:
466
- # # It's degenerate or not a valid polygon for your purposes; skip
467
- # break
468
-
469
- # # Check if the union overlaps with any other polygon
470
- # if union_poly.intersects(poly):
471
- # overlap = True
472
- # break
473
-
474
- # # Check if the buffered circle (circle + min_gap) intersects with any other polygon
475
- # if circle_buffer.intersects(poly):
476
- # overlap = True
477
- # break
478
-
479
- # if not overlap:
480
- # # If candidate passes all checks, accept it
481
- # existing_centers.append(candidate_center)
482
- # return union_poly, candidate_center
483
 
484
- # print(f"Warning: Could not place a finger cut circle after {attempts} attempts. Consider adjusting parameters.")
485
- # return None, None
486
 
 
 
 
 
 
 
 
 
 
 
 
487
 
488
- # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=100): #working best
489
- # import random
490
- # needed_center_distance = circle_diameter + min_gap
491
- # radius = circle_diameter / 2.0
492
- # attempts = 0
493
- # indices = list(range(len(points_inch)))
494
- # random.shuffle(indices) # Shuffle indices for randomness
495
-
496
- # for i in indices:
497
- # if attempts >= max_attempts:
498
- # break
499
- # cx, cy = points_inch[i]
500
- # # Try small adjustments around the chosen candidate
501
- # for dx in np.linspace(-0.1, 0.1, 10):
502
- # for dy in np.linspace(-0.1, 0.1, 10):
503
- # candidate_center = (cx + dx, cy + dy)
504
- # # Check distance from already placed centers
505
- # if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance for ex, ey in existing_centers):
506
- # continue
507
-
508
- # union_poly= union_tool_and_circle(tool_polygon,candidate_center)
509
- # overlap = False
510
- # # Check against other tool polygons for overlap or proximity issues
511
- # for poly in all_polygons:
512
- # if poly == tool_polygon:
513
- # continue
514
- # if union_poly.intersects(poly) or union_poly.buffer(min_gap).intersects(poly):
515
- # overlap = True
516
- # break
517
- # if overlap:
518
- # continue
519
- # # If candidate passes, accept it
520
- # existing_centers.append(candidate_center)
521
- # return union_poly, candidate_center
522
- # attempts += 1
523
- # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
524
- # return None, None
525
 
 
 
 
526
 
527
- # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=20):
528
- # """Place a finger cut with strategic positioning based on tool shape."""
529
- # if tool_polygon is None or len(points_inch) < 4:
530
- # return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
 
532
- # import random
533
- # needed_center_distance = circle_diameter + min_gap
534
- # radius = circle_diameter / 2.0
535
 
536
- # # Calculate the tool's bounding box and find its center
537
- # minx, miny, maxx, maxy = tool_polygon.bounds
538
- # bbox_center = ((minx + maxx) / 2, (miny + maxy) / 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
 
540
- # # Strategy 1: Try handle areas first (typically thinner parts)
541
- # # Find narrow regions by looking at distance to boundary
542
- # narrow_candidates = []
543
- # for i, pt in enumerate(points_inch):
544
- # # Calculate distance to the opposite side of the polygon
545
- # point = Point(pt)
546
- # if tool_polygon.boundary.distance(point) < radius * 1.5: # Points near boundary
547
- # narrow_candidates.append(i)
 
 
 
 
 
 
 
 
548
 
549
- # # Strategy 2: Try areas far from existing cuts
550
- # far_candidates = []
551
- # if existing_centers:
552
- # for i, pt in enumerate(points_inch):
553
- # min_dist = min(np.hypot(pt[0] - cx, pt[1] - cy) for cx, cy in existing_centers)
554
- # if min_dist > needed_center_distance * 1.5: # Points far from existing cuts
555
- # far_candidates.append(i)
 
 
 
 
 
 
 
556
 
557
- # # Strategy 3: Try concave regions (often good for finger grips)
558
- # # This is more complex but could identify good grip points
 
 
 
 
 
 
 
 
559
 
560
- # # Combine strategies, prioritizing certain candidates
561
- # candidate_indices = (narrow_candidates + far_candidates +
562
- # list(set(range(len(points_inch))) -
563
- # set(narrow_candidates) -
564
- # set(far_candidates)))
565
- # random.shuffle(candidate_indices) # Add randomness
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
- # # Try candidates
568
- # for idx in candidate_indices:
569
- # base_pt = points_inch[idx]
 
 
 
570
 
571
- # # Try at different distances from the point
572
- # for dist_factor in [0.0, 0.1, 0.2, 0.3, 0.4]:
573
- # # Try different angles
574
- # for angle in np.linspace(0, 2*np.pi, 12):
575
- # dx = dist_factor * radius * np.cos(angle)
576
- # dy = dist_factor * radius * np.sin(angle)
577
- # candidate = (base_pt[0] + dx, base_pt[1] + dy)
578
-
579
- # # Skip if point isn't inside the polygon
580
- # if not tool_polygon.contains(Point(candidate)):
581
- # continue
582
-
583
- # # Skip if too close to existing centers
584
- # if any(np.hypot(candidate[0] - cx, candidate[1] - cy) < needed_center_distance
585
- # for cx, cy in existing_centers):
586
- # continue
587
-
588
- # # Try creating the union
589
- # new_polygon = union_tool_and_circle(tool_polygon, candidate, circle_diameter)
590
- # if new_polygon is None or new_polygon == tool_polygon:
591
- # continue # Union failed
592
-
593
- # # Check for overlaps with other tools
594
- # overlap = False
595
- # for poly in all_polygons:
596
-
597
- # if poly == tool_polygon:
598
- # continue
599
- # # if new_polygon.intersects(poly):
600
- # # overlap = True
601
- # # break
602
- # if new_polygon.buffer(0.25).intersects(poly):
603
- # overlap = True
604
- # break
605
-
606
- # if not overlap:
607
- # # Success! This is a good spot for a finger cut
608
- # existing_centers.append(candidate)
609
- # return new_polygon, candidate
610
-
611
- # # If we get here, we couldn't find a good spot
612
- # print("Could not find suitable location for finger cut")
613
- # return None, None
614
-
615
- def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=50):
616
- """Place a finger cut with strategic positioning based on tool shape."""
617
- if tool_polygon is None or len(points_inch) < 4:
618
- return None, None
619
-
620
- import random
621
  needed_center_distance = circle_diameter + min_gap
622
  radius = circle_diameter / 2.0
623
 
624
- # Calculate the tool's bounding box and find its center
625
- minx, miny, maxx, maxy = tool_polygon.bounds
626
- bbox_center = ((minx + maxx) / 2, (miny + maxy) / 2)
627
-
628
- # Strategy 1: Try handle areas first (typically thinner parts)
629
- # Find narrow regions by looking at distance to boundary
630
- narrow_candidates = []
631
- for i, pt in enumerate(points_inch):
632
- # Calculate distance to the opposite side of the polygon
633
- point = Point(pt)
634
- if tool_polygon.boundary.distance(point) < radius * 1.5: # Points near boundary
635
- narrow_candidates.append(i)
636
-
637
- # Strategy 2: Try areas far from existing cuts
638
- far_candidates = []
639
- if existing_centers:
640
- for i, pt in enumerate(points_inch):
641
- min_dist = min(np.hypot(pt[0] - cx, pt[1] - cy) for cx, cy in existing_centers)
642
- if min_dist > needed_center_distance * 1.5: # Points far from existing cuts
643
- far_candidates.append(i)
644
-
645
- # Strategy 3: Try concave regions (often good for finger grips)
646
- # This is more complex but could identify good grip points
647
 
648
- # Combine strategies, prioritizing certain candidates
649
- candidate_indices = (narrow_candidates + far_candidates +
650
- list(set(range(len(points_inch))) -
651
- set(narrow_candidates) -
652
- set(far_candidates)))
653
- random.shuffle(candidate_indices) # Add randomness
 
 
 
654
 
655
- # Try candidates
656
- for idx in candidate_indices:
657
- base_pt = points_inch[idx]
658
-
659
- # Try at different distances from the point
660
- for dist_factor in [0.0, 0.1, 0.2, 0.3, 0.4]:
661
- # Try different angles
662
- for angle in np.linspace(0, 2*np.pi, 12):
663
- dx = dist_factor * radius * np.cos(angle)
664
- dy = dist_factor * radius * np.sin(angle)
665
- candidate = (base_pt[0] + dx, base_pt[1] + dy)
666
 
667
- # Skip if point isn't inside the polygon
668
- if not tool_polygon.contains(Point(candidate)):
669
- continue
 
 
 
670
 
671
- # Skip if too close to existing centers
672
- if any(np.hypot(candidate[0] - cx, candidate[1] - cy) < needed_center_distance
673
- for cx, cy in existing_centers):
674
- continue
675
-
676
- # Try creating the union
677
- new_polygon = union_tool_and_circle(tool_polygon, candidate, circle_diameter)
678
- if new_polygon is None or new_polygon == tool_polygon:
679
- continue # Union failed
680
-
681
- # Check for overlaps with other tools
682
- overlap = False
683
- for poly in all_polygons:
684
- if poly == tool_polygon:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  continue
686
- if new_polygon.buffer(0.1).intersects(poly):
687
- overlap = True
688
- break
 
 
 
 
 
 
 
 
689
 
690
- if not overlap:
691
- # Success! This is a good spot for a finger cut
692
- existing_centers.append(candidate)
693
- return new_polygon, candidate
 
 
 
 
 
 
 
 
694
 
695
- # If we get here, we couldn't find a good spot
696
- print("Could not find suitable location for finger cut")
697
- return None, None
698
- # ---------------------
699
- # DXF Spline and Boundary Functions
700
- # ---------------------
701
- def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=False): # works
702
  degree = 3
703
  closed = True
704
- doc = ezdxf.new(units=0)
705
- doc.units = ezdxf.units.IN
706
- doc.header["$INSUNITS"] = ezdxf.units.IN
707
- msp = doc.modelspace()
708
- finger_cut_centers = []
709
- final_polygons_inch = []
710
- for contour in inflated_contours:
711
- try:
712
- resampled_contour = resample_contour(contour)
713
- points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
714
- if len(points_inch) < 3:
715
- continue
716
- if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
717
- points_inch.append(points_inch[0])
718
- tool_polygon = build_tool_polygon(points_inch)
719
- if finger_clearance:
720
- union_poly, center = place_finger_cut_adjusted(tool_polygon, points_inch, finger_cut_centers, final_polygons_inch)
721
- if union_poly is not None:
722
- tool_polygon = union_poly
723
- exterior_coords = polygon_to_exterior_coords(tool_polygon)
724
- if len(exterior_coords) < 3:
725
- continue
726
- msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
727
- final_polygons_inch.append(tool_polygon)
728
- except ValueError as e:
729
- print(f"Skipping contour: {e}")
730
- return doc, final_polygons_inch
731
-
732
- # def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=True):
733
- # degree = 3
734
- # closed = True
735
- # doc = ezdxf.new(units=0)
736
- # doc.units = ezdxf.units.IN
737
- # doc.header["$INSUNITS"] = ezdxf.units.IN
738
- # msp = doc.modelspace()
739
- # finger_cut_centers = []
740
- # final_polygons_inch = []
741
 
742
- # for contour in inflated_contours:
743
- # try:
744
- # resampled_contour = resample_contour(contour)
745
- # points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
 
 
 
 
 
 
 
746
 
747
- # # Ensure minimum number of points
748
- # if len(points_inch) < 4: # Need at least 4 for a valid polygon and spline
749
- # print(f"Skipping contour with only {len(points_inch)} points")
750
- # continue
751
 
752
- # # Ensure first and last points match for closed polygon
753
- # if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
754
- # points_inch.append(points_inch[0])
 
 
 
 
 
 
755
 
756
- # # Create tool polygon with validation
757
- # tool_polygon = build_tool_polygon(points_inch)
758
- # if not tool_polygon.is_valid:
759
- # tool_polygon = tool_polygon.buffer(0) # Fix invalid geometries
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
 
761
- # # Add finger clearance if requested
762
- # if finger_clearance:
763
- # for attempt in range(3): # Try multiple finger cuts per tool
764
- # union_poly, center = place_finger_cut_adjusted(
765
- # tool_polygon, points_inch, finger_cut_centers, final_polygons_inch)
766
- # if union_poly is not None:
767
- # tool_polygon = union_poly
768
- # else:
769
- # break # Stop if we can't place more cuts
770
-
771
- # # Get exterior coordinates with validation
772
- # exterior_coords = polygon_to_exterior_coords(tool_polygon)
773
- # if len(exterior_coords) < 4:
774
- # print(f"Warning: Insufficient exterior points ({len(exterior_coords)})")
775
- # continue
776
 
777
- # # Add to DXF
778
- # msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
779
- # final_polygons_inch.append(tool_polygon)
780
-
781
- # except Exception as e:
782
- # print(f"Error processing contour: {e}")
783
-
784
- # return doc, final_polygons_inch
785
-
786
 
787
  def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width, offset_unit, annotation_text="", image_height_in=None, image_width_in=None):
788
  msp = doc.modelspace()
@@ -956,8 +1182,8 @@ def predict(
956
  print(f"Error calculating scaling factor: {e}")
957
 
958
  if scaling_factor is None or scaling_factor == 0:
959
- scaling_factor = 0.05
960
- print("Using default scaling factor of 0.05 due to calculation error")
961
  gc.collect()
962
  print("Scaling factor determined: {}".format(scaling_factor))
963
 
@@ -975,7 +1201,7 @@ def predict(
975
  else:
976
  boundary_length_in = boundary_length
977
  boundary_width_in = boundary_width
978
-
979
  # ---------------------
980
  # 5) Remove background from the shrunked drawer image (main objects)
981
  # ---------------------
@@ -983,8 +1209,14 @@ def predict(
983
  if offset_value < 1:
984
  offset_value = offset_value * 25.4
985
  offset_inches = offset_value / 25.4
 
 
 
 
986
  else:
987
  offset_inches = offset_value
 
 
988
 
989
  t = time.time()
990
  orig_size = shrunked_img.shape[:2]
@@ -1058,8 +1290,7 @@ def predict(
1058
  )
1059
  if boundary_polygon is not None:
1060
  final_polygons_inch.append(boundary_polygon)
1061
- # else:
1062
- # raise boundary_issue("Raised when bounds are given but rectangular boundary is no.")
1063
  # ---------------------
1064
  # 8) Add annotation text (if provided) in the DXF
1065
  # ---------------------
@@ -1089,7 +1320,7 @@ def predict(
1089
  translated_paths,
1090
  dxfattribs={"layer": "ANNOTATION", "color": 7}
1091
  )
1092
-
1093
  # Save the DXF
1094
  dxf_filepath = os.path.join("./outputs", "out.dxf")
1095
  doc.saveas(dxf_filepath)
@@ -1161,17 +1392,44 @@ def predict(
1161
  2, # Thinner inner part
1162
  cv2.LINE_AA
1163
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
 
1165
- outlines_color = cv2.cvtColor(new_outlines, cv2.COLOR_BGR2RGB)
1166
- print("Total prediction time: {:.2f} seconds".format(time.time() - overall_start))
1167
 
1168
- return (
1169
- cv2.cvtColor(output_img, cv2.COLOR_BGR2RGB),
1170
- outlines_color,
1171
- dxf_filepath,
1172
- dilated_mask,
1173
- str(scaling_factor)
1174
- )
1175
 
1176
  # ---------------------
1177
  # Gradio Interface
@@ -1189,10 +1447,10 @@ if __name__ == "__main__":
1189
  gr.Image(label="Input Image"),
1190
  gr.Number(label="Offset value for Mask", value=0.075),
1191
  gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches"),
1192
- gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="Yes"),
1193
- gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="Yes"),
1194
- gr.Number(label="Boundary Length", value=30.0, precision=2),
1195
- gr.Number(label="Boundary Width", value=30.0, precision=2),
1196
  gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
1197
  ],
1198
  outputs=[
@@ -1203,8 +1461,9 @@ if __name__ == "__main__":
1203
  gr.Textbox(label="Scaling Factor (inches/pixel)")
1204
  ],
1205
  examples=[
1206
- ["./Test20.jpg", 0.075, "inches", "Yes", "No", 30.0, 30.0, "MyTool"],
1207
- ["./Test21.jpg", 0.075, "inches", "Yes", "Yes", 30.0, 30.0, "Tool2"]
1208
  ]
1209
  )
1210
- iface.launch(share=True)
 
 
53
  class TextOverlapError(Exception):
54
  """Raised when the text overlaps with the inner contours (with a margin of 0.75)."""
55
  pass
56
+
 
57
  # ---------------------
58
  # Global Model Initialization with caching and print statements
59
  # ---------------------
 
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
 
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 place_finger_cut_adjusted(
806
+ tool_polygon: Polygon,
807
+ points_inch: list,
808
+ existing_centers: list,
809
+ all_polygons: list,
810
+ circle_diameter: float = 1.0,
811
+ min_gap: float = 0.5,
812
+ max_attempts: int = 100
813
+ ) -> (Polygon, tuple):
814
+ logger.info(f"Starting place_finger_cut_adjusted with {len(points_inch)} points")
815
 
816
+ # Define fallback function for timeout case
817
+ def fallback_solution():
818
+ logger.warning("Using fallback approach for finger cut placement")
819
+ candidate_center = points_inch[len(points_inch) // 2]
820
+ radius = circle_diameter / 2.0
821
+ candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
822
 
823
+ try:
824
+ union_poly = tool_polygon.union(candidate_circle)
825
+ except Exception as e:
826
+ logger.warning(f"Fallback union failed, using buffer trick: {e}")
827
+ union_poly = tool_polygon.buffer(0).union(candidate_circle.buffer(0))
828
+
829
+ existing_centers.append(candidate_center)
830
+ logger.info(f"Used fallback finger cut at center {candidate_center}")
831
+ return union_poly, candidate_center
832
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
  needed_center_distance = circle_diameter + min_gap
834
  radius = circle_diameter / 2.0
835
 
836
+ # Limit points to prevent timeout - use a subset for efficient processing
837
+ if len(points_inch) > 100:
838
+ logger.info(f"Limiting points from {len(points_inch)} to 100 for efficiency")
839
+ step = len(points_inch) // 100
840
+ points_inch = points_inch[::step]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
841
 
842
+ # Randomize candidate points order
843
+ indices = list(range(len(points_inch)))
844
+ random.shuffle(indices)
845
+ logger.debug(f"Shuffled {len(indices)} point indices")
846
+
847
+ # Use a non-blocking timeout approach with explicit time checks
848
+ start_time = time.time()
849
+ timeout_seconds = 5
850
+ attempts = 0
851
 
852
+ try:
853
+ while attempts < max_attempts:
854
+ # Check if we're approaching the timeout
855
+ current_time = time.time()
856
+ if current_time - start_time > timeout_seconds - 0.1: # Leave 0.1s margin
857
+ logger.warning(f"Approaching timeout after {attempts} attempts")
858
+ return fallback_solution()
 
 
 
 
859
 
860
+ # Process a batch of points to improve efficiency
861
+ for i in indices:
862
+ # Check timeout frequently
863
+ if time.time() - start_time > timeout_seconds - 0.05:
864
+ logger.warning("Timeout during point processing")
865
+ return fallback_solution()
866
 
867
+ cx, cy = points_inch[i]
868
+ # Reduce the number of adjustments to speed up processing
869
+ for dx, dy in [(0,0), (-0.2,0), (0.2,0), (0,0.2), (0,-0.2)]:
870
+ candidate_center = (cx + dx, cy + dy)
871
+
872
+ # Quick check for existing centers distance
873
+ if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance
874
+ for ex, ey in existing_centers):
875
+ continue
876
+
877
+ # Create candidate circle
878
+ candidate_circle = Point(candidate_center).buffer(radius, resolution=32) # Reduced resolution
879
+
880
+ # Quick geometric checks
881
+ if tool_polygon.contains(candidate_circle) or not candidate_circle.intersects(tool_polygon):
882
+ continue
883
+
884
+ # Check intersection area - use simplified geometry for speed
885
+ try:
886
+ inter_area = candidate_circle.intersection(tool_polygon).area
887
+ if inter_area <= 0 or inter_area >= candidate_circle.area:
888
+ continue
889
+ except Exception:
890
+ continue
891
+
892
+ # Quick distance check to other polygons
893
+ too_close = False
894
+ for other_poly in all_polygons:
895
+ if other_poly.equals(tool_polygon):
896
+ continue
897
+ if other_poly.distance(candidate_circle) < min_gap:
898
+ too_close = True
899
+ break
900
+ if too_close:
901
+ continue
902
+
903
+ # Attempt the union
904
+ try:
905
+ union_poly = tool_polygon.union(candidate_circle)
906
+ # Check if we got a multi-polygon when we don't want one
907
+ if union_poly.geom_type == "MultiPolygon" and len(union_poly.geoms) > 1:
908
+ continue
909
+ # Check if the union actually changed anything
910
+ if union_poly.equals(tool_polygon):
911
+ continue
912
+ except Exception:
913
  continue
914
+
915
+ # We found a valid candidate
916
+ existing_centers.append(candidate_center)
917
+ logger.info(f"Completed place_finger_cut_adjusted successfully at center {candidate_center}")
918
+ return union_poly, candidate_center
919
+
920
+ attempts += 1
921
+ # If we've made several attempts and are running out of time, use fallback
922
+ if attempts >= max_attempts // 2 and (time.time() - start_time) > timeout_seconds * 0.8:
923
+ logger.warning(f"Approaching timeout after {attempts} attempts")
924
+ return fallback_solution()
925
 
926
+ logger.debug(f"Completed attempt {attempts}/{max_attempts}")
927
+
928
+ # If we reached max attempts without finding a solution
929
+ logger.warning(f"No suitable finger cut found after {max_attempts} attempts, using fallback")
930
+ return fallback_solution()
931
+
932
+ except Exception as e:
933
+ logger.error(f"Error in place_finger_cut_adjusted: {e}")
934
+ return fallback_solution()
935
+
936
+ def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=False):
937
+ logger.info(f"Starting save_dxf_spline with {len(inflated_contours)} contours")
938
 
 
 
 
 
 
 
 
939
  degree = 3
940
  closed = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
941
 
942
+ try:
943
+ doc = ezdxf.new(units=0)
944
+ doc.units = ezdxf.units.IN
945
+ doc.header["$INSUNITS"] = ezdxf.units.IN
946
+ msp = doc.modelspace()
947
+
948
+ finger_cut_centers = []
949
+ final_polygons_inch = []
950
+
951
+ for idx, contour in enumerate(inflated_contours):
952
+ logger.debug(f"Processing contour {idx+1}/{len(inflated_contours)}")
953
 
954
+ try:
955
+ resampled_contour = resample_contour(contour)
956
+ points_inch = [(x * scaling_factor, (height - y) * scaling_factor) for x, y in resampled_contour]
 
957
 
958
+ if len(points_inch) < 3:
959
+ logger.warning(f"Skipping contour {idx}: insufficient points ({len(points_inch)})")
960
+ continue
961
+
962
+ if np.linalg.norm(np.array(points_inch[0]) - np.array(points_inch[-1])) > 1e-2:
963
+ logger.debug("Closing contour by adding first point to end")
964
+ points_inch.append(points_inch[0])
965
+
966
+ tool_polygon = build_tool_polygon(points_inch)
967
 
968
+ if finger_clearance:
969
+ logger.debug("Applying finger clearance")
970
+ try:
971
+ # Use a hard 5-second timeout for the entire finger cut operation
972
+ start_time = time.time()
973
+ union_poly, center = place_finger_cut_adjusted(
974
+ tool_polygon,
975
+ points_inch,
976
+ finger_cut_centers,
977
+ final_polygons_inch,
978
+ circle_diameter=1.0,
979
+ min_gap=0.25,
980
+ max_attempts=100
981
+ )
982
+
983
+ # Check if we exceeded the timeout anyway
984
+ if time.time() - start_time > 5:
985
+ logger.warning(f"Finger cut took too long for contour {idx} ({time.time() - start_time:.2f}s)")
986
+
987
+ if union_poly is not None:
988
+ tool_polygon = union_poly
989
+ logger.debug(f"Applied finger cut at {center}")
990
+ except Exception as e:
991
+ logger.warning(f"Finger cut failed for contour {idx}: {e}, using original polygon")
992
+
993
+ exterior_coords = polygon_to_exterior_coords(tool_polygon)
994
 
995
+ if len(exterior_coords) < 3:
996
+ logger.warning(f"Skipping contour {idx}: insufficient exterior points ({len(exterior_coords)})")
997
+ continue
998
+
999
+ msp.add_spline(exterior_coords, degree=degree, dxfattribs={"layer": "TOOLS"})
1000
+ final_polygons_inch.append(tool_polygon)
1001
+ logger.debug(f"Added spline for contour {idx}")
 
 
 
 
 
 
 
 
1002
 
1003
+ except ValueError as e:
1004
+ logger.warning(f"Skipping contour {idx}: {e}")
1005
+
1006
+ logger.info(f"Completed save_dxf_spline with {len(final_polygons_inch)} successful polygons")
1007
+ return doc, final_polygons_inch
1008
+
1009
+ except Exception as e:
1010
+ logger.error(f"Error in save_dxf_spline: {e}")
1011
+ raise
1012
 
1013
  def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width, offset_unit, annotation_text="", image_height_in=None, image_width_in=None):
1014
  msp = doc.modelspace()
 
1182
  print(f"Error calculating scaling factor: {e}")
1183
 
1184
  if scaling_factor is None or scaling_factor == 0:
1185
+ scaling_factor = 0.7
1186
+ print("Using default scaling factor of 0.7 due to calculation error")
1187
  gc.collect()
1188
  print("Scaling factor determined: {}".format(scaling_factor))
1189
 
 
1201
  else:
1202
  boundary_length_in = boundary_length
1203
  boundary_width_in = boundary_width
1204
+
1205
  # ---------------------
1206
  # 5) Remove background from the shrunked drawer image (main objects)
1207
  # ---------------------
 
1209
  if offset_value < 1:
1210
  offset_value = offset_value * 25.4
1211
  offset_inches = offset_value / 25.4
1212
+ if offset_value==0:
1213
+ offset_value = offset_value * 25.4
1214
+ offset_inches = offset_value / 25.4
1215
+ offset_inches+=0.005
1216
  else:
1217
  offset_inches = offset_value
1218
+ if offset_inches==0:
1219
+ offset_inches+=0.005
1220
 
1221
  t = time.time()
1222
  orig_size = shrunked_img.shape[:2]
 
1290
  )
1291
  if boundary_polygon is not None:
1292
  final_polygons_inch.append(boundary_polygon)
1293
+
 
1294
  # ---------------------
1295
  # 8) Add annotation text (if provided) in the DXF
1296
  # ---------------------
 
1320
  translated_paths,
1321
  dxfattribs={"layer": "ANNOTATION", "color": 7}
1322
  )
1323
+
1324
  # Save the DXF
1325
  dxf_filepath = os.path.join("./outputs", "out.dxf")
1326
  doc.saveas(dxf_filepath)
 
1392
  2, # Thinner inner part
1393
  cv2.LINE_AA
1394
  )
1395
+ else:
1396
+ text_height_cv = 0.75
1397
+ text_x_img = int(((inner_min_x + inner_max_x) / 2.0) / scaling_factor)
1398
+ text_y_in = inner_min_y - 0.125 - text_height_cv
1399
+ text_y_img = int(processed_size[0] - (text_y_in / scaling_factor))
1400
+ org = (text_x_img - int(len(annotation_text.strip()) * 6), text_y_img)
1401
+
1402
+ cv2.putText(
1403
+ output_img,
1404
+ annotation_text.strip(),
1405
+ org,
1406
+ cv2.FONT_HERSHEY_SIMPLEX,
1407
+ 1.2,
1408
+ (0, 0, 255),
1409
+ 2,
1410
+ cv2.LINE_AA
1411
+ )
1412
+ cv2.putText(
1413
+ new_outlines,
1414
+ annotation_text.strip(),
1415
+ org,
1416
+ cv2.FONT_HERSHEY_SIMPLEX,
1417
+ 1.2,
1418
+ (0, 0, 255),
1419
+ 2,
1420
+ cv2.LINE_AA
1421
+ )
1422
 
1423
+ outlines_color = cv2.cvtColor(new_outlines, cv2.COLOR_BGR2RGB)
1424
+ print("Total prediction time: {:.2f} seconds".format(time.time() - overall_start))
1425
 
1426
+ return (
1427
+ cv2.cvtColor(output_img, cv2.COLOR_BGR2RGB),
1428
+ outlines_color,
1429
+ dxf_filepath,
1430
+ dilated_mask,
1431
+ str(scaling_factor)
1432
+ )
1433
 
1434
  # ---------------------
1435
  # Gradio Interface
 
1447
  gr.Image(label="Input Image"),
1448
  gr.Number(label="Offset value for Mask", value=0.075),
1449
  gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches"),
1450
+ gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="No"),
1451
+ gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="No"),
1452
+ gr.Number(label="Boundary Length", value=300.0, precision=2),
1453
+ gr.Number(label="Boundary Width", value=200.0, precision=2),
1454
  gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
1455
  ],
1456
  outputs=[
 
1461
  gr.Textbox(label="Scaling Factor (inches/pixel)")
1462
  ],
1463
  examples=[
1464
+ ["./Test20.jpg", 0.075, "inches", "No", "No", 300.0, 200.0, "MyTool"],
1465
+ ["./Test21.jpg", 0.075, "inches", "Yes", "Yes", 300.0, 200.0, "Tool2"]
1466
  ]
1467
  )
1468
+ iface.launch(share=True)
1469
+