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)
|