Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -41,6 +41,14 @@ class ReferenceBoxNotDetectedError(Exception):
|
|
41 |
"""Raised when the reference box cannot be detected in the image"""
|
42 |
pass
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
# ---------------------
|
45 |
# Global Model Initialization with caching and print statements
|
46 |
# ---------------------
|
@@ -339,14 +347,21 @@ def save_dxf_spline(inflated_contours, scaling_factor, height, finger_clearance=
|
|
339 |
print(f"Skipping contour: {e}")
|
340 |
return doc, final_polygons_inch
|
341 |
|
342 |
-
def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width,
|
343 |
msp = doc.modelspace()
|
344 |
-
if
|
|
|
|
|
|
|
|
|
|
|
345 |
boundary_length_in = boundary_length / 25.4
|
346 |
boundary_width_in = boundary_width / 25.4
|
347 |
else:
|
348 |
boundary_length_in = boundary_length
|
349 |
boundary_width_in = boundary_width
|
|
|
|
|
350 |
min_x = float("inf")
|
351 |
min_y = float("inf")
|
352 |
max_x = -float("inf")
|
@@ -360,6 +375,19 @@ def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width
|
|
360 |
if min_x == float("inf"):
|
361 |
print("No tool polygons found, skipping boundary.")
|
362 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
363 |
shape_cx = (min_x + max_x) / 2
|
364 |
shape_cy = (min_y + max_y) / 2
|
365 |
half_w = boundary_width_in / 2.0
|
@@ -369,6 +397,7 @@ def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width
|
|
369 |
bottom = shape_cy - half_l
|
370 |
top = shape_cy + half_l
|
371 |
rect_coords = [(left, bottom), (right, bottom), (right, top), (left, top), (left, bottom)]
|
|
|
372 |
from shapely.geometry import Polygon as ShapelyPolygon
|
373 |
boundary_polygon = ShapelyPolygon(rect_coords)
|
374 |
msp.add_lwpolyline(rect_coords, close=True, dxfattribs={"layer": "BOUNDARY"})
|
@@ -399,12 +428,12 @@ def draw_single_polygon(poly, image_rgb, scaling_factor, image_height, color=(0,
|
|
399 |
# ---------------------
|
400 |
def predict(
|
401 |
image: Union[str, bytes, np.ndarray],
|
402 |
-
|
|
|
403 |
finger_clearance: str, # "Yes" or "No"
|
404 |
add_boundary: str, # "Yes" or "No"
|
405 |
boundary_length: float,
|
406 |
boundary_width: float,
|
407 |
-
boundary_unit: str,
|
408 |
annotation_text: str
|
409 |
):
|
410 |
overall_start = time.time()
|
@@ -417,7 +446,7 @@ def predict(
|
|
417 |
image = np.array(Image.open(io.BytesIO(base64.b64decode(image))).convert("RGB"))
|
418 |
except Exception:
|
419 |
raise ValueError("Invalid base64 image data")
|
420 |
-
# Apply sharpness enhancement
|
421 |
if isinstance(image, np.ndarray):
|
422 |
pil_image = Image.fromarray(image)
|
423 |
enhanced_image = ImageEnhance.Sharpness(pil_image).enhance(1.5)
|
@@ -462,6 +491,35 @@ def predict(
|
|
462 |
print("Using default scaling factor of 1.0 due to calculation error")
|
463 |
gc.collect()
|
464 |
print("Scaling factor determined: {}".format(scaling_factor))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
465 |
t = time.time()
|
466 |
orig_size = shrunked_img.shape[:2]
|
467 |
objects_mask = remove_bg(shrunked_img)
|
@@ -477,17 +535,13 @@ def predict(
|
|
477 |
del objects_mask
|
478 |
gc.collect()
|
479 |
print("Mask dilation completed in {:.2f} seconds".format(time.time() - t))
|
480 |
-
# Save the dilated mask for debugging if needed.
|
481 |
Image.fromarray(dilated_mask).save("./outputs/scaled_mask_new.jpg")
|
482 |
-
# --- Extract outlines (only used for DXF generation) ---
|
483 |
t = time.time()
|
484 |
outlines, contours = extract_outlines(dilated_mask)
|
485 |
print("Outline extraction completed in {:.2f} seconds".format(time.time() - t))
|
486 |
-
# Instead of drawing the original contours, we now prepare a clean copy of the shrunk image for drawing new contours.
|
487 |
output_img = shrunked_img.copy()
|
488 |
del shrunked_img
|
489 |
gc.collect()
|
490 |
-
# --- Generate DXF using the extracted contours and apply finger clearance ---
|
491 |
t = time.time()
|
492 |
use_finger_clearance = True if finger_clearance.lower() == "yes" else False
|
493 |
doc, final_polygons_inch = save_dxf_spline(contours, scaling_factor, processed_size[0], finger_clearance=use_finger_clearance)
|
@@ -496,10 +550,9 @@ def predict(
|
|
496 |
print("DXF generation completed in {:.2f} seconds".format(time.time() - t))
|
497 |
boundary_polygon = None
|
498 |
if add_boundary.lower() == "yes":
|
499 |
-
boundary_polygon = add_rectangular_boundary(doc, final_polygons_inch, boundary_length, boundary_width,
|
500 |
if boundary_polygon is not None:
|
501 |
final_polygons_inch.append(boundary_polygon)
|
502 |
-
# --- Annotation Text Placement (Centered horizontally) ---
|
503 |
min_x = float("inf")
|
504 |
min_y = float("inf")
|
505 |
max_x = -float("inf")
|
@@ -514,31 +567,34 @@ def predict(
|
|
514 |
max_x = b[2]
|
515 |
if b[3] > max_y:
|
516 |
max_y = b[3]
|
517 |
-
margin = 0.
|
518 |
text_x = (min_x + max_x) / 2
|
519 |
-
|
|
|
|
|
|
|
520 |
msp = doc.modelspace()
|
521 |
if annotation_text.strip():
|
522 |
text_entity = msp.add_text(
|
523 |
annotation_text.strip(),
|
524 |
dxfattribs={
|
525 |
-
"height": 0.
|
526 |
-
"layer": "ANNOTATION"
|
|
|
527 |
}
|
528 |
)
|
529 |
text_entity.dxf.insert = (text_x, text_y)
|
530 |
dxf_filepath = os.path.join("./outputs", "out.dxf")
|
531 |
doc.saveas(dxf_filepath)
|
532 |
-
# --- Draw only the new contours (final_polygons_inch) on the clean output image ---
|
533 |
draw_polygons_inch(final_polygons_inch, output_img, scaling_factor, processed_size[0], color=(0,0,255), thickness=2)
|
534 |
-
# Also prepare an "Outlines" image based on a blank canvas for clarity.
|
535 |
new_outlines = np.ones_like(output_img) * 255
|
536 |
draw_polygons_inch(final_polygons_inch, new_outlines, scaling_factor, processed_size[0], color=(0,0,255), thickness=2)
|
537 |
if annotation_text.strip():
|
538 |
text_px = int(text_x / scaling_factor)
|
539 |
text_py = int(processed_size[0] - (text_y / scaling_factor))
|
540 |
-
|
541 |
-
cv2.putText(
|
|
|
542 |
outlines_color = cv2.cvtColor(new_outlines, cv2.COLOR_BGR2RGB)
|
543 |
print("Total prediction time: {:.2f} seconds".format(time.time() - overall_start))
|
544 |
return (
|
@@ -554,18 +610,21 @@ def predict(
|
|
554 |
# ---------------------
|
555 |
if __name__ == "__main__":
|
556 |
os.makedirs("./outputs", exist_ok=True)
|
557 |
-
def gradio_predict(img, offset, finger_clearance, add_boundary, boundary_length, boundary_width,
|
558 |
-
|
|
|
|
|
|
|
559 |
iface = gr.Interface(
|
560 |
fn=gradio_predict,
|
561 |
inputs=[
|
562 |
gr.Image(label="Input Image"),
|
563 |
-
gr.Number(label="Offset value for Mask
|
|
|
564 |
gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="No"),
|
565 |
gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="No"),
|
566 |
gr.Number(label="Boundary Length", value=300.0, precision=2),
|
567 |
gr.Number(label="Boundary Width", value=200.0, precision=2),
|
568 |
-
gr.Dropdown(label="Boundary Unit", choices=["mm", "inches"], value="mm"),
|
569 |
gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
|
570 |
],
|
571 |
outputs=[
|
@@ -576,8 +635,8 @@ if __name__ == "__main__":
|
|
576 |
gr.Textbox(label="Scaling Factor (inches/pixel)")
|
577 |
],
|
578 |
examples=[
|
579 |
-
["./Test20.jpg", 0.075, "No", "No", 300.0, 200.0, "
|
580 |
-
["./Test21.jpg", 0.075, "Yes", "Yes", 300.0, 200.0, "
|
581 |
]
|
582 |
)
|
583 |
iface.launch(share=True)
|
|
|
41 |
"""Raised when the reference box cannot be detected in the image"""
|
42 |
pass
|
43 |
|
44 |
+
class BoundaryExceedsError(Exception):
|
45 |
+
"""Raised when the optional boundary dimensions exceed allowed image dimensions."""
|
46 |
+
pass
|
47 |
+
|
48 |
+
class BoundaryOverlapError(Exception):
|
49 |
+
"""Raised when the optional boundary dimensions are too small and overlap with the inner contours."""
|
50 |
+
pass
|
51 |
+
|
52 |
# ---------------------
|
53 |
# Global Model Initialization with caching and print statements
|
54 |
# ---------------------
|
|
|
347 |
print(f"Skipping contour: {e}")
|
348 |
return doc, final_polygons_inch
|
349 |
|
350 |
+
def add_rectangular_boundary(doc, polygons_inch, boundary_length, boundary_width, offset_unit):
|
351 |
msp = doc.modelspace()
|
352 |
+
# First, if unit is mm, check if values seem too low (accidental inches) and convert them.
|
353 |
+
if offset_unit.lower() == "mm":
|
354 |
+
if boundary_length < 50:
|
355 |
+
boundary_length = boundary_length * 25.4
|
356 |
+
if boundary_width < 50:
|
357 |
+
boundary_width = boundary_width * 25.4
|
358 |
boundary_length_in = boundary_length / 25.4
|
359 |
boundary_width_in = boundary_width / 25.4
|
360 |
else:
|
361 |
boundary_length_in = boundary_length
|
362 |
boundary_width_in = boundary_width
|
363 |
+
|
364 |
+
# Compute bounding box from inner contours.
|
365 |
min_x = float("inf")
|
366 |
min_y = float("inf")
|
367 |
max_x = -float("inf")
|
|
|
375 |
if min_x == float("inf"):
|
376 |
print("No tool polygons found, skipping boundary.")
|
377 |
return None
|
378 |
+
|
379 |
+
# Calculate inner bounding box dimensions.
|
380 |
+
inner_width = max_x - min_x
|
381 |
+
inner_length = max_y - min_y
|
382 |
+
|
383 |
+
# Define a clearance margin (in inches) required between the inner contours and the boundary.
|
384 |
+
clearance_margin = 0.2 # Adjust this value as needed
|
385 |
+
|
386 |
+
# New check: if the provided boundary dimensions are too small relative to the inner contours, raise an error.
|
387 |
+
if boundary_width_in <= inner_width + clearance_margin or boundary_length_in <= inner_length + clearance_margin:
|
388 |
+
raise BoundaryOverlapError("Error: The specified boundary dimensions are too small and overlap with the inner contours. Please provide larger values.")
|
389 |
+
|
390 |
+
# Compute the boundary rectangle centered on the inner contours.
|
391 |
shape_cx = (min_x + max_x) / 2
|
392 |
shape_cy = (min_y + max_y) / 2
|
393 |
half_w = boundary_width_in / 2.0
|
|
|
397 |
bottom = shape_cy - half_l
|
398 |
top = shape_cy + half_l
|
399 |
rect_coords = [(left, bottom), (right, bottom), (right, top), (left, top), (left, bottom)]
|
400 |
+
|
401 |
from shapely.geometry import Polygon as ShapelyPolygon
|
402 |
boundary_polygon = ShapelyPolygon(rect_coords)
|
403 |
msp.add_lwpolyline(rect_coords, close=True, dxfattribs={"layer": "BOUNDARY"})
|
|
|
428 |
# ---------------------
|
429 |
def predict(
|
430 |
image: Union[str, bytes, np.ndarray],
|
431 |
+
offset_value: float,
|
432 |
+
offset_unit: str, # "mm" or "inches"
|
433 |
finger_clearance: str, # "Yes" or "No"
|
434 |
add_boundary: str, # "Yes" or "No"
|
435 |
boundary_length: float,
|
436 |
boundary_width: float,
|
|
|
437 |
annotation_text: str
|
438 |
):
|
439 |
overall_start = time.time()
|
|
|
446 |
image = np.array(Image.open(io.BytesIO(base64.b64decode(image))).convert("RGB"))
|
447 |
except Exception:
|
448 |
raise ValueError("Invalid base64 image data")
|
449 |
+
# Apply sharpness enhancement.
|
450 |
if isinstance(image, np.ndarray):
|
451 |
pil_image = Image.fromarray(image)
|
452 |
enhanced_image = ImageEnhance.Sharpness(pil_image).enhance(1.5)
|
|
|
491 |
print("Using default scaling factor of 1.0 due to calculation error")
|
492 |
gc.collect()
|
493 |
print("Scaling factor determined: {}".format(scaling_factor))
|
494 |
+
|
495 |
+
# ---------------------
|
496 |
+
# Process boundary dimensions if boundary is enabled.
|
497 |
+
# ---------------------
|
498 |
+
if add_boundary.lower() == "yes":
|
499 |
+
image_height_px, image_width_px = shrunked_img.shape[:2]
|
500 |
+
image_height_in = image_height_px * scaling_factor
|
501 |
+
image_width_in = image_width_px * scaling_factor
|
502 |
+
# First, if units are mm, check if boundary dimensions seem too low and convert.
|
503 |
+
if offset_unit.lower() == "mm":
|
504 |
+
if boundary_length < 50:
|
505 |
+
boundary_length = boundary_length * 25.4
|
506 |
+
if boundary_width < 50:
|
507 |
+
boundary_width = boundary_width * 25.4
|
508 |
+
boundary_length_in = boundary_length / 25.4
|
509 |
+
boundary_width_in = boundary_width / 25.4
|
510 |
+
else:
|
511 |
+
boundary_length_in = boundary_length
|
512 |
+
boundary_width_in = boundary_width
|
513 |
+
if boundary_length_in > (image_height_in - 2) or boundary_width_in > (image_width_in - 2):
|
514 |
+
raise BoundaryExceedsError("Error: The specified boundary dimensions exceed the allowed image dimensions (image size minus 2 inches). Please enter smaller values.")
|
515 |
+
|
516 |
+
# Convert offset value.
|
517 |
+
if offset_unit.lower() == "mm":
|
518 |
+
if offset_value < 1:
|
519 |
+
offset_value = offset_value * 25.4
|
520 |
+
offset_inches = offset_value / 25.4
|
521 |
+
else:
|
522 |
+
offset_inches = offset_value
|
523 |
t = time.time()
|
524 |
orig_size = shrunked_img.shape[:2]
|
525 |
objects_mask = remove_bg(shrunked_img)
|
|
|
535 |
del objects_mask
|
536 |
gc.collect()
|
537 |
print("Mask dilation completed in {:.2f} seconds".format(time.time() - t))
|
|
|
538 |
Image.fromarray(dilated_mask).save("./outputs/scaled_mask_new.jpg")
|
|
|
539 |
t = time.time()
|
540 |
outlines, contours = extract_outlines(dilated_mask)
|
541 |
print("Outline extraction completed in {:.2f} seconds".format(time.time() - t))
|
|
|
542 |
output_img = shrunked_img.copy()
|
543 |
del shrunked_img
|
544 |
gc.collect()
|
|
|
545 |
t = time.time()
|
546 |
use_finger_clearance = True if finger_clearance.lower() == "yes" else False
|
547 |
doc, final_polygons_inch = save_dxf_spline(contours, scaling_factor, processed_size[0], finger_clearance=use_finger_clearance)
|
|
|
550 |
print("DXF generation completed in {:.2f} seconds".format(time.time() - t))
|
551 |
boundary_polygon = None
|
552 |
if add_boundary.lower() == "yes":
|
553 |
+
boundary_polygon = add_rectangular_boundary(doc, final_polygons_inch, boundary_length, boundary_width, offset_unit)
|
554 |
if boundary_polygon is not None:
|
555 |
final_polygons_inch.append(boundary_polygon)
|
|
|
556 |
min_x = float("inf")
|
557 |
min_y = float("inf")
|
558 |
max_x = -float("inf")
|
|
|
567 |
max_x = b[2]
|
568 |
if b[3] > max_y:
|
569 |
max_y = b[3]
|
570 |
+
margin = 0.40
|
571 |
text_x = (min_x + max_x) / 2
|
572 |
+
if add_boundary.lower() == "yes":
|
573 |
+
text_y = min_y + margin
|
574 |
+
else:
|
575 |
+
text_y = min_y - margin
|
576 |
msp = doc.modelspace()
|
577 |
if annotation_text.strip():
|
578 |
text_entity = msp.add_text(
|
579 |
annotation_text.strip(),
|
580 |
dxfattribs={
|
581 |
+
"height": 0.50,
|
582 |
+
"layer": "ANNOTATION",
|
583 |
+
"style": "Bold"
|
584 |
}
|
585 |
)
|
586 |
text_entity.dxf.insert = (text_x, text_y)
|
587 |
dxf_filepath = os.path.join("./outputs", "out.dxf")
|
588 |
doc.saveas(dxf_filepath)
|
|
|
589 |
draw_polygons_inch(final_polygons_inch, output_img, scaling_factor, processed_size[0], color=(0,0,255), thickness=2)
|
|
|
590 |
new_outlines = np.ones_like(output_img) * 255
|
591 |
draw_polygons_inch(final_polygons_inch, new_outlines, scaling_factor, processed_size[0], color=(0,0,255), thickness=2)
|
592 |
if annotation_text.strip():
|
593 |
text_px = int(text_x / scaling_factor)
|
594 |
text_py = int(processed_size[0] - (text_y / scaling_factor))
|
595 |
+
org = (int(text_px) - int(len(annotation_text.strip()) / 2), int(text_py))
|
596 |
+
cv2.putText(output_img, annotation_text.strip(), org, cv2.FONT_HERSHEY_SIMPLEX, 1.25, (0,0,255), 3, cv2.LINE_AA)
|
597 |
+
cv2.putText(new_outlines, annotation_text.strip(), org, cv2.FONT_HERSHEY_SIMPLEX, 1.25, (0,0,255), 3, cv2.LINE_AA)
|
598 |
outlines_color = cv2.cvtColor(new_outlines, cv2.COLOR_BGR2RGB)
|
599 |
print("Total prediction time: {:.2f} seconds".format(time.time() - overall_start))
|
600 |
return (
|
|
|
610 |
# ---------------------
|
611 |
if __name__ == "__main__":
|
612 |
os.makedirs("./outputs", exist_ok=True)
|
613 |
+
def gradio_predict(img, offset, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text):
|
614 |
+
try:
|
615 |
+
return predict(img, offset, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text)
|
616 |
+
except Exception as e:
|
617 |
+
return None, None, None, None, f"Error: {str(e)}"
|
618 |
iface = gr.Interface(
|
619 |
fn=gradio_predict,
|
620 |
inputs=[
|
621 |
gr.Image(label="Input Image"),
|
622 |
+
gr.Number(label="Offset value for Mask", value=0.075),
|
623 |
+
gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches"),
|
624 |
gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="No"),
|
625 |
gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="No"),
|
626 |
gr.Number(label="Boundary Length", value=300.0, precision=2),
|
627 |
gr.Number(label="Boundary Width", value=200.0, precision=2),
|
|
|
628 |
gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
|
629 |
],
|
630 |
outputs=[
|
|
|
635 |
gr.Textbox(label="Scaling Factor (inches/pixel)")
|
636 |
],
|
637 |
examples=[
|
638 |
+
["./Test20.jpg", 0.075, "inches", "No", "No", 300.0, 200.0, "MyTool"],
|
639 |
+
["./Test21.jpg", 0.075, "inches", "Yes", "Yes", 300.0, 200.0, "Tool2"]
|
640 |
]
|
641 |
)
|
642 |
iface.launch(share=True)
|