muhammadhamza-stack commited on
Commit
fd5039f
·
1 Parent(s): 9a2f47d

refine the gradio ui

Browse files
app.py CHANGED
@@ -58,7 +58,7 @@ class FingerCutOverlapError(Exception):
58
  """There was an overlap with fingercuts... Please try again to generate dxf."""
59
  pass
60
  # ---------------------
61
- # Global Model Initialization with caching and print statements
62
  # ---------------------
63
  print("Loading YOLOWorld model...")
64
  start_time = time.time()
@@ -110,7 +110,7 @@ transform_image_global = transforms.Compose([
110
  ])
111
 
112
  # ---------------------
113
- # Model Reload Function (if needed)
114
  # ---------------------
115
  def unload_and_reload_models():
116
  global drawer_detector_global, reference_detector_global, birefnet_global, u2net_global
@@ -708,198 +708,6 @@ def draw_single_polygon(poly, image_rgb, scaling_factor, image_height, color=(0,
708
  pts_px = np.array(pts_px, dtype=np.int32)
709
  cv2.polylines(image_rgb, [pts_px], isClosed=True, color=color, thickness=thickness, lineType=cv2.LINE_AA)
710
 
711
- # def draw_and_pad(polygons_inch, scaling_factor,boundary_polygon, padding=50,
712
- # color=(0,0,255), thickness=2):
713
- # """
714
- # - polygons_inch: list of Shapely Polygons in inch units (already including boundary).
715
- # - scaling_factor: inches per pixel.
716
- # - padding: padding in pixels.
717
- # """
718
- # all_x = []
719
- # all_y = []
720
- # pixel_polys = []
721
-
722
- # # 1) Convert to pixel coords and collect bounds
723
- # for poly in polygons_inch:
724
- # coords = list(poly.exterior.coords)
725
- # pts = []
726
- # for x_in, y_in in coords:
727
- # px = int(round(x_in / scaling_factor))
728
- # py = int(round(y_in / scaling_factor))
729
- # pts.append([px, py])
730
- # all_x.append(px)
731
- # all_y.append(py)
732
- # pixel_polys.append(np.array(pts, dtype=np.int32))
733
-
734
- # # 2) Compute canvas size
735
-
736
- # min_x, max_x = min(all_x), max(all_x)
737
- # min_y, max_y = min(all_y), max(all_y)
738
- # width = max_x - min_x + 1
739
- # height = max_y - min_y + 1
740
-
741
- # # 3) Create blank white canvas
742
- # canvas = 255 * np.ones((height, width, 3), dtype=np.uint8)
743
-
744
- # # 4) Draw each polygon, flipping y within the local box
745
- # for pts in pixel_polys:
746
- # # Offset so min corner is (0,0)
747
- # pts_off = pts - np.array([[min_x, min_y]])
748
- # # Flip y: new_y = height-1 - old_y
749
- # pts_off[:,1] = (height - 1) - pts_off[:,1]
750
- # cv2.polylines(canvas, [pts_off], isClosed=True,
751
- # color=color, thickness=thickness, lineType=cv2.LINE_AA)
752
-
753
- # # 5) Pad the canvas
754
-
755
- # padded = cv2.copyMakeBorder(
756
- # canvas,
757
- # top=padding, bottom=padding,
758
- # left=padding, right=padding,
759
- # borderType=cv2.BORDER_CONSTANT,
760
- # value=[255,255,255]
761
- # )
762
- # return padded
763
-
764
- # import numpy as np
765
- # import cv2
766
-
767
- # def draw_and_pad(polygons_inch, scaling_factor, boundary_polygon, padding=50,
768
- # color=(0,0,255), thickness=2):
769
- # """
770
- # - polygons_inch: list of Shapely Polygons in inch units.
771
- # - scaling_factor: inches per pixel.
772
- # - boundary_polygon: the Shapely boundary polygon, or None.
773
- # - padding: base padding in pixels.
774
- # """
775
- # all_x, all_y = [], []
776
- # pixel_polys = []
777
-
778
- # # 1) Convert to pixel coords and collect bounds
779
- # for poly in polygons_inch:
780
- # coords = list(poly.exterior.coords)
781
- # pts = []
782
- # for x_in, y_in in coords:
783
- # px = int(round(x_in / scaling_factor))
784
- # py = int(round(y_in / scaling_factor))
785
- # pts.append([px, py])
786
- # all_x.append(px)
787
- # all_y.append(py)
788
- # pixel_polys.append(np.array(pts, dtype=np.int32))
789
-
790
- # # 2) Compute canvas size
791
- # min_x, max_x = min(all_x), max(all_x)
792
- # min_y, max_y = min(all_y), max(all_y)
793
- # width = max_x - min_x + 1
794
- # height = max_y - min_y + 1
795
-
796
- # # 3) Create blank white canvas
797
- # canvas = 255 * np.ones((height, width, 3), dtype=np.uint8)
798
-
799
- # # 4) Draw each polygon, flipping y within the local box
800
- # for pts in pixel_polys:
801
- # pts_off = pts - np.array([[min_x, min_y]])
802
- # pts_off[:,1] = (height - 1) - pts_off[:,1]
803
- # cv2.polylines(canvas, [pts_off], isClosed=True,
804
- # color=color, thickness=thickness, lineType=cv2.LINE_AA)
805
-
806
- # # 5) Decide padding amounts
807
- # if boundary_polygon is not None:
808
- # top = bottom = left = right = padding
809
- # else:
810
- # # Double the padding if there's no boundary, to avoid clipping
811
- # top = bottom = left = right = padding * 2
812
-
813
- # # 6) Pad the canvas
814
- # padded = cv2.copyMakeBorder(
815
- # canvas,
816
- # top=top, bottom=bottom,
817
- # left=left, right=right,
818
- # borderType=cv2.BORDER_CONSTANT,
819
- # value=[255,255,255]
820
- # )
821
- # return padded
822
-
823
- import numpy as np
824
- import cv2
825
-
826
- # def draw_and_pad(polygons_inch, scaling_factor, boundary_polygon, padding=50,
827
- # color=(0, 0, 255), thickness=2):
828
- # """
829
- # Draws Shapely Polygons (in inch units) on a white canvas.
830
-
831
- # When boundary_polygon is None, the computed bounds are expanded by the padding value
832
- # so that the drawn contours are not clipped at the edges after adding the final padding.
833
-
834
- # Arguments:
835
- # polygons_inch: list of Shapely Polygons in inch units (already including boundary).
836
- # scaling_factor: inches per pixel.
837
- # boundary_polygon: the Shapely boundary polygon, or None.
838
- # padding: padding in pixels.
839
- # color: color of the drawn polylines (in BGR format).
840
- # thickness: line thickness.
841
-
842
- # Returns:
843
- # padded: an image (numpy array) of the drawn polygons with an external white border.
844
- # """
845
- # all_x = []
846
- # all_y = []
847
- # pixel_polys = []
848
-
849
- # # 1) Convert each polygon to pixel coordinates and compute overall bounds.
850
- # for poly in polygons_inch:
851
- # coords = list(poly.exterior.coords)
852
- # pts = []
853
- # for x_in, y_in in coords:
854
- # px = int(round(x_in / scaling_factor))
855
- # py = int(round(y_in / scaling_factor))
856
- # pts.append([px, py])
857
- # all_x.append(px)
858
- # all_y.append(py)
859
- # pixel_polys.append(np.array(pts, dtype=np.int32))
860
-
861
- # # 2) Compute the basic canvas size from the polygon bounds.
862
- # min_x, max_x = min(all_x), max(all_x)
863
- # min_y, max_y = min(all_y), max(all_y)
864
-
865
- # # If no boundary polygon is provided, expand the bounds to add margin
866
- # # so that later when we pad externally, the contours do not get clipped.
867
- # if boundary_polygon is None:
868
- # min_x -= padding
869
- # max_x += padding
870
- # min_y -= padding
871
- # max_y += padding
872
-
873
- # width = max_x - min_x + 1
874
- # height = max_y - min_y + 1
875
-
876
- # # 3) Create a blank white canvas.
877
- # canvas = 255 * np.ones((height, width, 3), dtype=np.uint8)
878
-
879
- # # 4) Draw each polygon, flipping the y-coordinates to match image coordinates.
880
- # for pts in pixel_polys:
881
- # # Offset so the minimum corner becomes (0,0) on canvas.
882
- # pts_off = pts - np.array([[min_x, min_y]])
883
- # # Flip y: image coordinates have (0,0) at the top-left.
884
- # pts_off[:, 1] = (height - 1) - pts_off[:, 1]
885
- # cv2.polylines(canvas, [pts_off], isClosed=True,
886
- # color=color, thickness=thickness, lineType=cv2.LINE_AA)
887
-
888
- # # 5) Finally, add external padding on all sides.
889
- # padded = cv2.copyMakeBorder(
890
- # canvas,
891
- # top=padding, bottom=padding,
892
- # left=padding, right=padding,
893
- # borderType=cv2.BORDER_CONSTANT,
894
- # value=[255, 255, 255]
895
- # )
896
-
897
- # return padded
898
-
899
- import numpy as np
900
- import cv2
901
- from shapely.geometry import Polygon
902
-
903
  import numpy as np
904
  import cv2
905
  from shapely.geometry import Polygon
@@ -1274,39 +1082,139 @@ def predict(
1274
  str(scaling_factor)
1275
  )
1276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1277
  # ---------------------
1278
  # Gradio Interface
1279
  # ---------------------
1280
  if __name__ == "__main__":
1281
  os.makedirs("./outputs", exist_ok=True)
 
1282
  def gradio_predict(img, offset, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text):
1283
  try:
1284
  return predict(img, offset, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text)
1285
  except Exception as e:
 
 
 
 
 
 
 
 
1286
  return None, None, None, None, f"Error: {str(e)}"
1287
- iface = gr.Interface(
1288
- fn=gradio_predict,
1289
- inputs=[
1290
- gr.Image(label="Input Image"),
1291
- gr.Number(label="Offset value for Mask", value=0.075),
1292
- gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches"),
1293
- gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="Yes"),
1294
- gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="Yes"),
1295
- gr.Number(label="Boundary Length", value=50, precision=2),
1296
- gr.Number(label="Boundary Width", value=50, precision=2),
1297
- gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
1298
- ],
1299
- outputs=[
1300
- gr.Image(format="png",label="Output Image"),
1301
- gr.Image(format="png",label="Outlines of Objects"),
1302
- gr.File(label="DXF file"),
1303
- gr.Image(label="Mask"),
1304
- gr.Textbox(label="Scaling Factor (inches/pixel)")
1305
- ],
1306
- examples=[
1307
- ["./Test20.jpg", 0.075, "inches", "Yes", "No", 300.0, 200.0, "MyTool"],
1308
- ["./Test21.jpg", 0.075, "inches", "Yes", "Yes", 300.0, 200.0, "Tool2"]
1309
- ]
1310
- )
1311
- iface.launch(share=True)
1312
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  """There was an overlap with fingercuts... Please try again to generate dxf."""
59
  pass
60
  # ---------------------
61
+ # Global Model Initialization with caching and print statements (Original code preserved)
62
  # ---------------------
63
  print("Loading YOLOWorld model...")
64
  start_time = time.time()
 
110
  ])
111
 
112
  # ---------------------
113
+ # Helper Functions (Preserved as is)
114
  # ---------------------
115
  def unload_and_reload_models():
116
  global drawer_detector_global, reference_detector_global, birefnet_global, u2net_global
 
708
  pts_px = np.array(pts_px, dtype=np.int32)
709
  cv2.polylines(image_rgb, [pts_px], isClosed=True, color=color, thickness=thickness, lineType=cv2.LINE_AA)
710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  import numpy as np
712
  import cv2
713
  from shapely.geometry import Polygon
 
1082
  str(scaling_factor)
1083
  )
1084
 
1085
+
1086
+ # ---------------------
1087
+ # Documentation Strings for Gradio
1088
+ # ---------------------
1089
+
1090
+ QUICK_START = """
1091
+ ## 1. Quick Start Guide: From Photo to DXF Cut File
1092
+ This application converts a single photograph of tools in a drawer/tray into a ready-to-use DXF file for CNC cutting foam inserts.
1093
+
1094
+ 1. **Preparation**: Place your tools in a drawer and include a **reference coin** (assumed to be 0.955 inches / 24.25mm wide, e.g., a US Quarter). Ensure clear, even lighting.
1095
+ 2. **Upload**: Upload the image.
1096
+ 3. **Configure**: Adjust the `Offset Value` (clearance around the tool) and select options like `Finger Clearance` and `Boundary`.
1097
+ 4. **Run**: Click the **"Submit"** button (automatically generated by Gradio).
1098
+ 5. **Download**: Review the `Outlines` and `Output Image`, then download the final **DXF file** for your cutting machine.
1099
+ """
1100
+
1101
+ INPUT_EXPLANATION = """
1102
+ ## 2. Expected Inputs
1103
+
1104
+ ### Image Requirements
1105
+ * **Reference Object is Mandatory:** The system *must* detect a specific reference object (default: coin roughly 0.955 inches wide) to correctly calculate the real-world scale (inches/pixel).
1106
+ * **Placement:** The image should be taken from directly above the drawer (minimal perspective distortion).
1107
+ * **Lighting:** Clear, shadow-free lighting is crucial for object detection and masking.
1108
+
1109
+ ### Processing Parameters
1110
+
1111
+ | Parameter | Purpose | Default Value | Guidance for Non-Tech Users |
1112
+ | :--- | :--- | :--- | :--- |
1113
+ | **Offset Value** | The amount of space (clearance) added around the tool profile. This is the buffer to ensure the tool fits easily into the foam cutout. | 0.075 | Increase this value for thicker tools or if you need looser cutouts. |
1114
+ | **Offset Unit** | Specifies whether the offset value is in millimeters or inches. | inches | Match this to your preferred measurement system. |
1115
+ | **Add Finger Clearance?** | Adds a small circular cutout to the side of the tool profile to allow easy removal using a finger. | Yes | Recommended for most tools. |
1116
+ | **Add Rectangular Boundary?**| Defines the exterior rectangular shape of the entire foam insert (the boundary of the drawer). | Yes | Required if you need a rectangular outer boundary for cutting. |
1117
+ | **Boundary Length/Width** | The actual dimensions of the drawer/tray area for the foam insert. | 50.0 (in the selected unit) | Must be larger than the combined area of all tools plus margins. |
1118
+ | **Annotation** | Text to engrave/cut into the foam, typically placed at the top of the boundary box. (Max 20 chars) | (Empty) | Use this to label the tray (e.g., "Wrench Set"). |
1119
+ """
1120
+
1121
+ OUTPUT_EXPLANATION = """
1122
+ ## 3. Expected Outputs
1123
+
1124
+ | Output Field | Description | Purpose |
1125
+ | :--- | :--- | :--- |
1126
+ | **Output Image** | The original image overlaid with the detected tool outlines (including offset and finger clearance). | Visual confirmation that the detection and sizing are correct relative to the original photo. |
1127
+ | **Outlines of Objects**| A clean, scaled, white-background image showing only the outlines (blue lines). This visually represents the final geometry exported to the DXF. | Final quality check for shape and spacing before CNC cutting. |
1128
+ | **DXF file** | The final vector file containing the tool outlines, boundary box, and text annotation (if included). | Ready to upload to CNC machine software (AutoCAD DXF R2000 format). |
1129
+ | **Mask** | The binary image used internally to generate the tool contours after applying the offset value. | Technical output for debugging segmentation issues. |
1130
+ | **Scaling Factor**| The calculated real-world scale (inches per pixel) determined by the reference coin. | Confirmation of measurement accuracy. Values close to 0.7 are common for phone photos. |
1131
+ """
1132
+
1133
+
1134
  # ---------------------
1135
  # Gradio Interface
1136
  # ---------------------
1137
  if __name__ == "__main__":
1138
  os.makedirs("./outputs", exist_ok=True)
1139
+
1140
  def gradio_predict(img, offset, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text):
1141
  try:
1142
  return predict(img, offset, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text)
1143
  except Exception as e:
1144
+ # Handle specific known errors for clearer user feedback
1145
+ if isinstance(e, (DrawerNotDetectedError, ReferenceBoxNotDetectedError)):
1146
+ gr.Warning(f"Processing Error: {str(e)} Please check image quality and coin placement.")
1147
+ elif isinstance(e, (BoundaryOverlapError, TextOverlapError)):
1148
+ gr.Error(f"Boundary/Text Placement Error: {str(e)}")
1149
+ else:
1150
+ gr.Error(f"An unexpected error occurred: {str(e)}")
1151
+
1152
  return None, None, None, None, f"Error: {str(e)}"
1153
+
1154
+ with gr.Blocks(title="Tool Cutout DXF Generator") as demo:
1155
+ gr.Markdown("<h1 style='text-align: center;'> AI-Powered Tool Cutout DXF Generator </h1>")
1156
+ gr.Markdown("Convert a photo of your tool drawer into precise CNC-ready vector files.")
1157
+
1158
+ # 1. Guidelines & Instructions
1159
+ with gr.Accordion(" Tips & User Guide", open=False):
1160
+ gr.Markdown(QUICK_START)
1161
+ gr.Markdown("---")
1162
+ gr.Markdown(INPUT_EXPLANATION)
1163
+ gr.Markdown("---")
1164
+ gr.Markdown(OUTPUT_EXPLANATION)
1165
+
1166
+ # 2. Main Interface Setup
1167
+
1168
+ with gr.Row():
1169
+ with gr.Column():
1170
+ gr.Markdown("## Step 1: Upload Image and Configure Parameters")
1171
+ input_image = gr.Image(label="Input Image", type="numpy")
1172
+
1173
+ with gr.Column():
1174
+ gr.Markdown("## Step 2: Configure Parameters")
1175
+ offset_value = gr.Number(label="Offset value for Mask", value=0.075)
1176
+ offset_unit = gr.Dropdown(label="Offset Unit", choices=["mm", "inches"], value="inches")
1177
+ finger_clearance = gr.Dropdown(label="Add Finger Clearance?", choices=["Yes", "No"], value="Yes")
1178
+ add_boundary = gr.Dropdown(label="Add Rectangular Boundary?", choices=["Yes", "No"], value="Yes")
1179
+ boundary_length = gr.Number(label="Boundary Length", value=50.0, precision=2)
1180
+ boundary_width = gr.Number(label="Boundary Width", value=50.0, precision=2)
1181
+ annotation_text = gr.Textbox(label="Annotation (max 20 chars)", max_length=20, placeholder="Type up to 20 characters")
1182
+
1183
+ gr.Markdown("## Step 3: Click Generate DXF & Previews")
1184
+ submit_button = gr.Button("Generate DXF & Previews", variant="primary")
1185
+
1186
+ # 3. Outputs
1187
+ gr.Markdown("## Outputs")
1188
+
1189
+ with gr.Row():
1190
+ output_img = gr.Image(format="png", label="Output Image (Tools overlaid with outlines)")
1191
+ outlines_color = gr.Image(format="png", label="Outlines of Objects (Final DXF Geometry Preview)")
1192
+
1193
+ with gr.Row():
1194
+ dxf_file = gr.File(label="DXF file")
1195
+ with gr.Column():
1196
+ scaling_factor_display = gr.Textbox(label="Scaling Factor (inches/pixel)")
1197
+ dilated_mask = gr.Image(label="Mask (Technical Preview)")
1198
+
1199
+ # 4. Examples
1200
+ gr.Markdown("---")
1201
+ gr.Markdown("## Example Data (Click a row to load the image and parameters)")
1202
+ gr.Examples(
1203
+ examples=[
1204
+ ["data/Test20.jpg", 0.075, "inches", "Yes", "No", 300.0, 200.0, "MyTool"],
1205
+ ["data/Test21.jpg", 0.075, "inches", "Yes", "Yes", 300.0, 200.0, "Tool2"]
1206
+ ],
1207
+ inputs=[input_image, offset_value, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text],
1208
+ outputs=[output_img, outlines_color, dxf_file, dilated_mask, scaling_factor_display],
1209
+ fn=gradio_predict,
1210
+ cache_examples=False,
1211
+ )
1212
+
1213
+ # Event Handler
1214
+ submit_button.click(
1215
+ fn=gradio_predict,
1216
+ inputs=[input_image, offset_value, offset_unit, finger_clearance, add_boundary, boundary_length, boundary_width, annotation_text],
1217
+ outputs=[output_img, outlines_color, dxf_file, dilated_mask, scaling_factor_display]
1218
+ )
1219
+
1220
+ demo.launch(share=True)
Test20.jpg → data/Test20.jpg RENAMED
File without changes
Test21.jpg → data/Test21.jpg RENAMED
File without changes
requirements.txt CHANGED
@@ -1,11 +1,13 @@
1
  transformers
2
  ultralytics==8.3.9
3
  ezdxf
4
- gradio
5
  pydantic==2.10.6
6
  kornia
7
  timm
8
  einops
9
  shapely
10
  gevent==22.10.2
11
- matplotlib
 
 
 
 
1
  transformers
2
  ultralytics==8.3.9
3
  ezdxf
 
4
  pydantic==2.10.6
5
  kornia
6
  timm
7
  einops
8
  shapely
9
  gevent==22.10.2
10
+ matplotlib
11
+ numpy<2
12
+ gradio==3.50.2
13
+ gradio-client==0.6.1