Nighty3912 commited on
Commit
dcf3b0f
·
verified ·
1 Parent(s): ec896ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +448 -50
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,9 +155,9 @@ 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),
@@ -286,42 +286,441 @@ def polygon_to_exterior_coords(poly: Polygon):
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
  # ---------------------
@@ -344,7 +743,7 @@ def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=
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)
@@ -411,13 +810,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 +898,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 +908,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 +927,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
 
@@ -564,7 +962,7 @@ def predict(
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()
 
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 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),
 
286
  return []
287
  return list(poly.exterior.coords)
288
 
289
+ # def place_finger_cut_adjusted(
290
+ # tool_polygon,
291
+ # points_inch,
292
+ # existing_centers,
293
+ # all_polygons,
294
+ # circle_diameter=1, # Finger cut circle diameter in inches.
295
+ # min_gap=0.25, # Minimum clearance (in inches) between the finger cut and adjacent contours.
296
+ # max_attempts=50 # Maximum candidate attempts.
297
+ # ):
298
+ # """
299
+ # Attempts to place a finger-cut circle along the tool polygon's contour, biased toward one side
300
+ # (the boundary side) by shifting the candidate center away from the polygon centroid.
301
+ # The candidate circle is merged with the tool polygon via union_tool_and_circle().
302
+ # Debug information is printed to help trace candidate evaluation.
303
+
304
+ # :param tool_polygon: Shapely Polygon representing the tool contour.
305
+ # :param points_inch: List of (x, y) points (in inches) along the contour.
306
+ # :param existing_centers: List of already accepted finger cut centers.
307
+ # :param all_polygons: List of all polygons (for overlap checking).
308
+ # :param circle_diameter: Diameter of the finger cut (in inches).
309
+ # :param min_gap: Clearance (in inches) required between the finger cut and any adjacent contour.
310
+ # :param max_attempts: Maximum number of candidate attempts.
311
+ # :return: (updated_polygon, candidate_center) if successful; otherwise, (None, None).
312
+ # """
313
+ # import random
314
+ # from shapely.geometry import Point
315
+ # import numpy as np
316
+
317
+ # needed_center_distance = circle_diameter + min_gap
318
+ # radius = circle_diameter / 2.0
319
+ # attempts = 0
320
+
321
+ # # Parameter: how far to push the candidate center outward.
322
+ # # Here we set it to half the circle radius, but you can adjust this as needed.
323
+ # outward_offset = radius * 0.1
324
+
325
+ # # Compute the centroid of the tool polygon once.
326
+ # polygon_centroid = tool_polygon.centroid
327
+
328
+ # # Create a safe version of the polygon via an inward buffer.
329
+ # safe_tool_polygon = tool_polygon.buffer(-min_gap)
330
+ # if safe_tool_polygon.is_empty:
331
+ # safe_tool_polygon = tool_polygon
332
+
333
+ # # Shuffle the contour indices for randomness.
334
+ # indices = list(range(len(points_inch)))
335
+ # random.shuffle(indices)
336
+
337
+ # for i in indices:
338
+ # if attempts >= max_attempts:
339
+ # break
340
+
341
+ # # Base candidate point from the resampled contour:
342
+ # base_x, base_y = points_inch[i]
343
+
344
+ # # Try a grid of offsets from this candidate base point.
345
+ # for dx in np.linspace(-0.3, 0.3, 15):
346
+ # for dy in np.linspace(-0.3, 0.3, 15):
347
+ # # Compute an initial candidate point.
348
+ # candidate_unshifted = (base_x + dx, base_y + dy)
349
+
350
+ # # Compute the outward direction based on the vector from the centroid to candidate.
351
+ # vec_x = candidate_unshifted[0] - polygon_centroid.x
352
+ # vec_y = candidate_unshifted[1] - polygon_centroid.y
353
+ # norm = np.hypot(vec_x, vec_y)
354
+ # if norm == 0:
355
+ # continue # Skip degenerate case.
356
+ # # Normalize the outward vector.
357
+ # unit_vec = (vec_x / norm, vec_y / norm)
358
+ # # Push the candidate center further out so it lies along the boundary.
359
+ # candidate_center = (candidate_unshifted[0] + unit_vec[0] * outward_offset,
360
+ # candidate_unshifted[1] + unit_vec[1] * outward_offset)
361
+
362
+ # # Check that candidate center is not too close to previously accepted centers.
363
+ # if any(np.hypot(candidate_center[0] - ex, candidate_center[1] - ey) < needed_center_distance
364
+ # for ex, ey in existing_centers):
365
+ # continue
366
+
367
+ # # Create the candidate circle.
368
+ # candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
369
+
370
+ # # Check if the candidate circle is mostly on the boundary:
371
+ # area_ratio = candidate_circle.intersection(tool_polygon).area / candidate_circle.area
372
+ # # Debug: Show candidate center and its area ratio.
373
+ # #print(f"Candidate center: {candidate_center}, area_ratio: {area_ratio:.2f}")
374
+ # # Accept if at least 70% of the circle's area lies within the tool polygon.
375
+ # if area_ratio < 0.6:
376
+ # continue
377
+
378
+ # # Merge candidate circle with tool polygon using the existing union function.
379
+ # candidate_union = union_tool_and_circle(tool_polygon, candidate_center, circle_diameter)
380
+
381
+ # # Overlap check: ensure this candidate does not intrude on any other object.
382
+ # overlap = False
383
+ # for other_poly in all_polygons:
384
+ # if other_poly.equals(tool_polygon):
385
+ # continue
386
+ # if candidate_union.intersects(other_poly) or candidate_circle.buffer(min_gap).intersects(other_poly):
387
+ # overlap = True
388
+ # #print(f"Candidate at {candidate_center} rejected due to overlap.")
389
+ # break
390
+ # if overlap:
391
+ # continue
392
+
393
+ # # Candidate accepted.
394
+ # print(f"Accepted candidate center: {candidate_center} with area_ratio: {area_ratio:.2f}")
395
+ # existing_centers.append(candidate_center)
396
+ # return candidate_union, candidate_center
397
+
398
+ # attempts += 1
399
+
400
+ # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
401
+ # return None, None
402
+
403
+
404
+ # def place_finger_cut_adjusted(
405
+ # tool_polygon,
406
+ # points_inch,
407
+ # existing_centers,
408
+ # all_polygons,
409
+ # circle_diameter=1.0, # Finger cut circle diameter in inches.
410
+ # min_gap=0.25, # Minimum clearance (in inches) between the finger cut and adjacent contours.
411
+ # max_attempts=50 # Maximum candidate attempts.
412
+ # ):
413
+ # """
414
+ # Attempts to place a finger-cut circle along the tool polygon's contour, biased toward one side
415
+ # (the boundary side) by shifting the candidate center away from the polygon centroid.
416
+ # Simplified version that maintains core functionality while using a more streamlined approach.
417
+
418
+ # :param tool_polygon: Shapely Polygon representing the tool contour.
419
+ # :param points_inch: List of (x, y) points (in inches) along the contour.
420
+ # :param existing_centers: List of already accepted finger cut centers.
421
+ # :param all_polygons: List of all polygons (for overlap checking).
422
+ # :param circle_diameter: Diameter of the finger cut (in inches).
423
+ # :param min_gap: Clearance (in inches) required between the finger cut and any adjacent contour.
424
+ # :param max_attempts: Maximum number of candidate attempts.
425
+ # :return: (updated_polygon, candidate_center) if successful; otherwise, (None, None).
426
+ # """
427
+ # import random
428
+ # import numpy as np
429
+ # from shapely.geometry import Point
430
+
431
+ # needed_center_distance = circle_diameter + min_gap
432
+ # radius = circle_diameter / 2.0
433
+
434
+ # # Compute the centroid of the tool polygon once.
435
+ # polygon_centroid = tool_polygon.centroid
436
+
437
+ # # Parameter: how far to push the candidate center outward.
438
+ # outward_offset = radius * 0.5
439
+
440
+ # # Try random points along the contour
441
+ # indices = list(range(len(points_inch)))
442
+ # random.shuffle(indices)
443
+
444
+ # for idx in indices[:max_attempts]:
445
+ # # Get base point from contour
446
+ # base_x, base_y = points_inch[idx]
447
+
448
+ # # Calculate the outward vector from centroid to point
449
+ # vec_x = base_x - polygon_centroid.x
450
+ # vec_y = base_y - polygon_centroid.y
451
+ # norm = np.hypot(vec_x, vec_y)
452
+
453
+ # if norm == 0:
454
+ # continue # Skip if point is at centroid
455
+
456
+ # # Normalize and calculate shifted candidate center
457
+ # unit_vec = (vec_x / norm, vec_y / norm)
458
+ # candidate_center = (base_x + unit_vec[0] * outward_offset,
459
+ # base_y + unit_vec[1] * outward_offset)
460
+
461
+ # # Check distance from existing centers
462
+ # too_close = False
463
+ # for (ex_x, ex_y) in existing_centers:
464
+ # if np.hypot(candidate_center[0] - ex_x, candidate_center[1] - ex_y) < needed_center_distance:
465
+ # too_close = True
466
+ # break
467
+
468
+ # if too_close:
469
+ # continue
470
+
471
+ # # Create the candidate circle
472
+ # candidate_circle = Point(candidate_center).buffer(radius, resolution=64)
473
+
474
+ # # Check if circle is mostly on the boundary (at least 60% inside)
475
+ # area_ratio = candidate_circle.intersection(tool_polygon).area / candidate_circle.area
476
+ # if area_ratio < 0.6:
477
+ # continue
478
+
479
+ # # Merge candidate with tool polygon
480
+ # union_poly = union_tool_and_circle(tool_polygon, candidate_center, circle_diameter)
481
+
482
+ # # Check for overlaps with other polygons
483
+ # overlap = False
484
+ # for other_poly in all_polygons:
485
+ # if other_poly.equals(tool_polygon):
486
+ # continue
487
+ # if union_poly.intersects(other_poly) or candidate_circle.buffer(min_gap).intersects(other_poly):
488
+ # overlap = True
489
+ # break
490
+
491
+ # if overlap:
492
+ # continue
493
+
494
+ # # Candidate accepted
495
+ # print(f"Accepted candidate center: {candidate_center} with area_ratio: {area_ratio:.2f}")
496
+ # existing_centers.append(candidate_center)
497
+ # return union_poly, candidate_center
498
+
499
+ # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
500
+ # return None, None
501
+
502
+
503
+ # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=100):
504
+ # import random
505
+ # needed_center_distance = circle_diameter + min_gap
506
+ # radius = circle_diameter / 2.0
507
+ # for _ in range(max_attempts):
508
+ # idx = random.randint(0, len(points_inch) - 1)
509
+ # cx, cy = points_inch[idx]
510
+ # too_close = False
511
+ # for (ex_x, ex_y) in existing_centers:
512
+ # if np.hypot(cx - ex_x, cy - ex_y) < needed_center_distance:
513
+ # too_close = True
514
+ # break
515
+ # if too_close:
516
+ # continue
517
+ # circle_poly = Point((cx, cy)).buffer(radius, resolution=64)
518
+ # union_poly = tool_polygon.union(circle_poly)
519
+ # overlap_with_others = False
520
+ # too_close_to_others = False
521
+ # for poly in all_polygons:
522
+ # if union_poly.intersects(poly):
523
+ # overlap_with_others = True
524
+ # break
525
+ # if circle_poly.buffer(min_gap).intersects(poly):
526
+ # too_close_to_others = True
527
+ # break
528
+ # if overlap_with_others or too_close_to_others:
529
+ # continue
530
+ # existing_centers.append((cx, cy))
531
+ # return union_poly, (cx, cy)
532
+ # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
533
+ # return None, None
534
+
535
+ # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, 2nd best
536
+ # circle_diameter=1.0, min_gap=0.25, max_attempts=30):
537
+ # import random
538
+ # from shapely.geometry import Point
539
+
540
+ # # Analyze bounding box
541
+ # bounds = tool_polygon.bounds # (minx, miny, maxx, maxy)
542
+ # width = bounds[2] - bounds[0]
543
+ # height = bounds[3] - bounds[1]
544
+ # min_dim = min(width, height)
545
+
546
+ # # Adjust circle diameter based on tool size
547
+ # scale_factor = min(1.0, min_dim / 2.0) # Adjust this factor to control size sensitivity
548
+ # adjusted_diameter = circle_diameter * scale_factor
549
+ # radius = adjusted_diameter / 2.0
550
+ # needed_center_distance = adjusted_diameter + min_gap
551
+
552
+ # for _ in range(max_attempts):
553
+ # idx = random.randint(0, len(points_inch) - 1)
554
+ # cx, cy = points_inch[idx]
555
+
556
+ # # Check distance from existing centers
557
+ # if any(np.hypot(cx - ex_x, cy - ex_y) < needed_center_distance for ex_x, ex_y in existing_centers):
558
+ # continue
559
+
560
+ # circle_poly = Point((cx, cy)).buffer(radius, resolution=64)
561
+ # union_poly = tool_polygon.union(circle_poly)
562
+
563
+ # overlap_with_others = any(union_poly.intersects(poly) for poly in all_polygons)
564
+ # too_close_to_others = any(circle_poly.buffer(min_gap).intersects(poly) for poly in all_polygons)
565
+
566
+ # if overlap_with_others or too_close_to_others:
567
+ # continue
568
+
569
+ # existing_centers.append((cx, cy))
570
+ # return union_poly, (cx, cy)
571
+
572
+ # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
573
+ # return None, None
574
+
575
+
576
+ def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=30): #1st best
577
  needed_center_distance = circle_diameter + min_gap
578
  radius = circle_diameter / 2.0
579
+ import random
580
+ for _ in range(max_attempts):
581
+ idx = random.randint(0, len(points_inch) - 1)
582
+ cx, cy = points_inch[idx]
583
+
584
+ # Check if this point is too close to an existing center
585
+ too_close = any(np.hypot(cx - ex_x, cy - ex_y) < needed_center_distance for ex_x, ex_y in existing_centers)
586
+ if too_close:
587
+ continue
588
+
589
+ # Create the finger cut circle and try adding it to the tool
590
+ circle_poly = Point((cx, cy)).buffer(radius, resolution=64)
591
+ union_poly = tool_polygon.union(circle_poly)
592
+
593
+ # Check for overlap and spacing with other tools
594
+ overlap_with_others = False
595
+ too_close_to_others = False
596
+
597
+ for poly in all_polygons:
598
+ if poly.equals(tool_polygon):
599
+ continue # Skip comparing the tool to itself
600
+
601
+ if union_poly.intersects(poly):
602
+ overlap_with_others = True
603
+ break
604
+
605
+ if circle_poly.buffer(min_gap).intersects(poly):
606
+ too_close_to_others = True
607
+ break
608
+
609
+ if overlap_with_others or too_close_to_others:
610
+ continue
611
+
612
+ existing_centers.append((cx, cy))
613
+ return union_poly, (cx, cy)
614
+
615
  print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
616
  return None, None
617
 
618
+
619
+ # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons,
620
+ # circle_diameter=1.0, min_gap=0.25, max_attempts=30):
621
+ # fallback_diameters = [circle_diameter, 0.75, 0.5, 0.4] # Try these in order
622
+
623
+ # for fallback_d in fallback_diameters:
624
+ # radius = fallback_d / 2.0
625
+ # needed_center_distance = fallback_d + min_gap
626
+
627
+ # for _ in range(max_attempts):
628
+ # idx = random.randint(0, len(points_inch) - 1)
629
+ # cx, cy = points_inch[idx]
630
+
631
+ # # Check distance from existing centers
632
+ # too_close = any(np.hypot(cx - ex_x, cy - ex_y) < needed_center_distance
633
+ # for ex_x, ex_y in existing_centers)
634
+ # if too_close:
635
+ # continue
636
+
637
+ # # Create and check the finger cut circle
638
+ # circle_poly = Point((cx, cy)).buffer(radius, resolution=64)
639
+ # union_poly = tool_polygon.union(circle_poly)
640
+
641
+ # if not union_poly.is_valid:
642
+ # continue # Skip invalid geometry
643
+
644
+ # # Check against all other polygons strictly
645
+ # overlap = False
646
+ # for poly in all_polygons:
647
+ # if poly.equals(tool_polygon):
648
+ # continue
649
+
650
+ # # Shrink slightly to avoid even edge contact
651
+ # if union_poly.intersects(poly.buffer(-1e-3)):
652
+ # overlap = True
653
+ # break
654
+ # if circle_poly.buffer(min_gap).intersects(poly):
655
+ # overlap = True
656
+ # break
657
+
658
+ # if overlap:
659
+ # continue
660
+
661
+ # # Final sanity check
662
+ # for poly in all_polygons:
663
+ # if poly.equals(tool_polygon):
664
+ # continue
665
+ # if union_poly.intersects(poly):
666
+ # print("Overlap slipped through. Rejecting.")
667
+ # overlap = True
668
+ # break
669
+
670
+ # if overlap:
671
+ # continue
672
+
673
+ # existing_centers.append((cx, cy))
674
+ # return union_poly, (cx, cy)
675
+
676
+ # # If we get here, all attempts failed — still try placing smallest fallback
677
+ # print("Warning: Could not place cleanly. Forcing smallest fallback.")
678
+ # smallest_radius = fallback_diameters[-1] / 2.0
679
+ # for _ in range(100):
680
+ # idx = random.randint(0, len(points_inch) - 1)
681
+ # cx, cy = points_inch[idx]
682
+ # circle_poly = Point((cx, cy)).buffer(smallest_radius, resolution=64)
683
+ # union_poly = tool_polygon.union(circle_poly)
684
+ # if union_poly.is_valid:
685
+ # existing_centers.append((cx, cy))
686
+ # return union_poly, (cx, cy)
687
+
688
+ # # Absolute fallback — return original tool without finger cut
689
+ # print("Failed to place even smallest fallback. No cutout added.")
690
+ # return tool_polygon, None
691
+
692
+
693
+ # def place_finger_cut_adjusted(tool_polygon, points_inch, existing_centers, all_polygons, circle_diameter=1.0, min_gap=0.25, max_attempts=30):
694
+ # import random
695
+ # import numpy as np
696
+ # from shapely.geometry import Point, Polygon
697
+
698
+ # needed_center_distance = circle_diameter + min_gap
699
+ # radius = circle_diameter / 2.0
700
+ # for _ in range(max_attempts):
701
+ # idx = random.randint(0, len(points_inch) - 1)
702
+ # cx, cy = points_inch[idx]
703
+ # # Check against existing centers
704
+ # too_close = any(np.hypot(cx - ex_x, cy - ex_y) < needed_center_distance for (ex_x, ex_y) in existing_centers)
705
+ # if too_close:
706
+ # continue
707
+ # circle_poly = Point((cx, cy)).buffer(radius, resolution=64)
708
+ # # Ensure circle intersects the tool to form a valid cut
709
+ # if not circle_poly.intersects(tool_polygon):
710
+ # continue
711
+ # # Check proximity to other polygons
712
+ # if any(circle_poly.buffer(min_gap).intersects(poly) for poly in all_polygons):
713
+ # continue
714
+ # # Subtract circle from tool and check for overlaps
715
+ # difference_poly = tool_polygon.difference(circle_poly)
716
+ # if any(difference_poly.intersects(poly) for poly in all_polygons):
717
+ # continue
718
+ # existing_centers.append((cx, cy))
719
+ # return difference_poly, (cx, cy)
720
+ # print("Warning: Could not place a finger cut circle meeting all spacing requirements.")
721
+ # return None, None
722
+
723
+
724
  # ---------------------
725
  # DXF Spline and Boundary Functions
726
  # ---------------------
 
743
  points_inch.append(points_inch[0])
744
  tool_polygon = build_tool_polygon(points_inch)
745
  if finger_clearance:
746
+ 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)
747
  if union_poly is not None:
748
  tool_polygon = union_poly
749
  exterior_coords = polygon_to_exterior_coords(tool_polygon)
 
810
  msp.add_lwpolyline(rect_coords, close=True, dxfattribs={"layer": "BOUNDARY"})
811
 
812
  text_top = boundary_polygon.bounds[1] + 1
813
+ too_small = boundary_width_in < inner_width + 2 * clearance_side or boundary_length_in < inner_length + 2 * clearance_tb
814
+ if too_small:
815
+ raise BoundaryOverlapError("Error: The specified boundary dimensions are too small and overlap with the inner contours. Please provide larger values.")
816
+ if annotation_text.strip() and text_top > min_y - 0.75:
817
+ raise TextOverlapError("Error: The text is too close to the inner contours. Please increase boundary length.")
 
 
818
  return boundary_polygon
819
 
820
  def draw_polygons_inch(polygons_inch, image_rgb, scaling_factor, image_height, color=(0,0,255), thickness=2):
 
898
  try:
899
  t = time.time()
900
  reference_obj_img, scaling_box_coords = detect_reference_square(shrunked_img)
901
+ print("Reference coin detection completed in {:.2f} seconds".format(time.time() - t))
902
  except ReferenceBoxNotDetectedError as e:
903
  return None, None, None, None, f"Error: {str(e)}"
904
 
 
908
  t = time.time()
909
  reference_obj_img = make_square(reference_obj_img)
910
  reference_square_mask = remove_bg_u2netp(reference_obj_img)
911
+ reference_square_mask= resize_img(reference_square_mask,(reference_obj_img.shape[1],reference_obj_img.shape[0]))
912
  print("Reference image processing completed in {:.2f} seconds".format(time.time() - t))
913
 
914
  t = time.time()
915
  try:
916
  cv2.imwrite("mask.jpg", cv2.cvtColor(reference_obj_img, cv2.COLOR_RGB2GRAY))
917
  scaling_factor = calculate_scaling_factor(
 
918
  target_image=reference_square_mask,
919
+ reference_obj_size_mm=0.955,
920
  feature_detector="ORB",
921
  )
922
  except ZeroDivisionError:
 
927
  print(f"Error calculating scaling factor: {e}")
928
 
929
  if scaling_factor is None or scaling_factor == 0:
930
+ scaling_factor = 0.7
931
+ print("Using default scaling factor of 0.7 due to calculation error")
932
  gc.collect()
933
  print("Scaling factor determined: {}".format(scaling_factor))
934
 
 
962
  objects_mask = remove_bg(shrunked_img)
963
  processed_size = objects_mask.shape[:2]
964
 
965
+ objects_mask = exclude_scaling_box(objects_mask, scaling_box_coords, orig_size, processed_size, expansion_factor=1.2)
966
  objects_mask = resize_img(objects_mask, (shrunked_img.shape[1], shrunked_img.shape[0]))
967
  del scaling_box_coords
968
  gc.collect()