Spaces:
Running on Zero
Running on Zero
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1052,10 +1052,12 @@ def prepare_export(stored_masks, stored_image, threshold_bias):
|
|
| 1052 |
# Tab builder
|
| 1053 |
# ---------------------------------------------------------------------------
|
| 1054 |
|
| 1055 |
-
def draw_polygon_overlay(image_pil, points):
|
| 1056 |
"""
|
| 1057 |
Draw numbered vertex dots and polygon edges onto a copy of image_pil.
|
| 1058 |
-
points: list of (x, y) tuples in
|
|
|
|
|
|
|
| 1059 |
Returns a new PIL image.
|
| 1060 |
"""
|
| 1061 |
img = image_pil.copy().convert("RGBA")
|
|
@@ -1078,6 +1080,15 @@ def draw_polygon_overlay(image_pil, points):
|
|
| 1078 |
fill=(74, 170, 255, 255), outline=(255, 255, 255, 255))
|
| 1079 |
draw.text((x, y), str(i + 1), fill=(255, 255, 255, 255), anchor="mm")
|
| 1080 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
combined = Image.alpha_composite(img, overlay)
|
| 1082 |
return combined.convert("RGB")
|
| 1083 |
|
|
@@ -1100,7 +1111,10 @@ def make_preview(image_pil):
|
|
| 1100 |
return preview, (preview.width / float(image_pil.width)) if image_pil.width else 1.0
|
| 1101 |
|
| 1102 |
|
| 1103 |
-
|
|
|
|
|
|
|
|
|
|
| 1104 |
"""
|
| 1105 |
Called by gr.Image .select(). Appends the clicked coordinate,
|
| 1106 |
redraws the overlay on the PREVIEW, returns (updated_preview, updated_points).
|
|
@@ -1118,17 +1132,27 @@ def add_crop_point(preview_pil, points, scale, evt: gr.SelectData):
|
|
| 1118 |
|
| 1119 |
if preview_pil is None:
|
| 1120 |
return preview_pil, points
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1121 |
if len(points) >= 4:
|
| 1122 |
-
return draw_polygon_overlay(preview_pil, to_preview(points)), points
|
| 1123 |
|
| 1124 |
# Gradio can deliver an empty selection (see gradio-app/gradio#5945);
|
| 1125 |
# dropping it beats raising a red error box the user cannot interpret.
|
| 1126 |
if evt is None or evt.index is None or evt.index[0] is None:
|
| 1127 |
return draw_polygon_overlay(preview_pil, to_preview(points)), points
|
| 1128 |
|
| 1129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
new_points = points + [(x, y)]
|
| 1131 |
-
return draw_polygon_overlay(preview_pil, to_preview(new_points)), new_points
|
| 1132 |
|
| 1133 |
|
| 1134 |
def clear_crop_points(image_pil):
|
|
@@ -1316,8 +1340,10 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1316 |
|
| 1317 |
gr.Markdown(
|
| 1318 |
"### Crop region (optional)\n"
|
| 1319 |
-
"
|
| 1320 |
-
"to segment.
|
|
|
|
|
|
|
| 1321 |
"Leave empty to segment the full image."
|
| 1322 |
)
|
| 1323 |
|
|
@@ -1334,6 +1360,13 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1334 |
|
| 1335 |
clear_crop_btn = gr.Button("✕ Clear crop points", size="sm")
|
| 1336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
model_dropdown = gr.Dropdown(
|
| 1338 |
choices=list(MODEL_OPTIONS.keys()),
|
| 1339 |
label="Select Model",
|
|
@@ -1520,9 +1553,10 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1520 |
_component.change(fn=on_exclusion_change,
|
| 1521 |
inputs=exclusion_inputs, outputs=exclusion_outputs)
|
| 1522 |
|
| 1523 |
-
def on_crop_click(base_img, points, scale, evt: gr.SelectData):
|
| 1524 |
before = len(points or [])
|
| 1525 |
-
updated_img, updated_pts = add_crop_point(base_img, points, scale,
|
|
|
|
| 1526 |
n = len(updated_pts)
|
| 1527 |
if n == before and n < 4:
|
| 1528 |
status = "*Tap not registered — try again slightly inside the image*"
|
|
@@ -1533,7 +1567,8 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1533 |
return updated_img, updated_pts, status
|
| 1534 |
|
| 1535 |
crop_display.select(fn=on_crop_click,
|
| 1536 |
-
inputs=[base_image_state, crop_points_state, preview_scale_state
|
|
|
|
| 1537 |
outputs=[crop_display, crop_points_state, crop_status])
|
| 1538 |
|
| 1539 |
def on_clear_crop(base_img):
|
|
|
|
| 1052 |
# Tab builder
|
| 1053 |
# ---------------------------------------------------------------------------
|
| 1054 |
|
| 1055 |
+
def draw_polygon_overlay(image_pil, points, stem_px=0):
|
| 1056 |
"""
|
| 1057 |
Draw numbered vertex dots and polygon edges onto a copy of image_pil.
|
| 1058 |
+
points: list of (x, y) tuples in preview pixel space.
|
| 1059 |
+
stem_px: if non-zero, draw a tail from the last vertex down to the touch
|
| 1060 |
+
point it was offset from (see add_crop_point).
|
| 1061 |
Returns a new PIL image.
|
| 1062 |
"""
|
| 1063 |
img = image_pil.copy().convert("RGBA")
|
|
|
|
| 1080 |
fill=(74, 170, 255, 255), outline=(255, 255, 255, 255))
|
| 1081 |
draw.text((x, y), str(i + 1), fill=(255, 255, 255, 255), anchor="mm")
|
| 1082 |
|
| 1083 |
+
# In offset mode the marker sits above the fingertip. Without something
|
| 1084 |
+
# connecting the two it reads as "the app put my point in the wrong place",
|
| 1085 |
+
# so draw the stem down to where the touch actually landed.
|
| 1086 |
+
if stem_px and points:
|
| 1087 |
+
x, y = points[-1]
|
| 1088 |
+
draw.line([(x, y + r), (x, y + stem_px)], fill=(74, 170, 255, 140), width=2)
|
| 1089 |
+
draw.ellipse([x - 5, y + stem_px - 5, x + 5, y + stem_px + 5],
|
| 1090 |
+
outline=(255, 255, 255, 170), width=2)
|
| 1091 |
+
|
| 1092 |
combined = Image.alpha_composite(img, overlay)
|
| 1093 |
return combined.convert("RGB")
|
| 1094 |
|
|
|
|
| 1111 |
return preview, (preview.width / float(image_pil.width)) if image_pil.width else 1.0
|
| 1112 |
|
| 1113 |
|
| 1114 |
+
OFFSET_PCT_DEFAULT = 10.0 # % of picker height; ~a fingertip on a phone
|
| 1115 |
+
|
| 1116 |
+
|
| 1117 |
+
def add_crop_point(preview_pil, points, scale, offset_pct, evt: gr.SelectData):
|
| 1118 |
"""
|
| 1119 |
Called by gr.Image .select(). Appends the clicked coordinate,
|
| 1120 |
redraws the overlay on the PREVIEW, returns (updated_preview, updated_points).
|
|
|
|
| 1132 |
|
| 1133 |
if preview_pil is None:
|
| 1134 |
return preview_pil, points
|
| 1135 |
+
|
| 1136 |
+
# Vertical offset so the marker is not hidden under the fingertip.
|
| 1137 |
+
# Expressed as a share of picker height, which keeps it consistent across
|
| 1138 |
+
# phone sizes instead of being tied to the image's pixel dimensions.
|
| 1139 |
+
stem = int(preview_pil.height * (float(offset_pct or 0.0) / 100.0))
|
| 1140 |
+
|
| 1141 |
if len(points) >= 4:
|
| 1142 |
+
return draw_polygon_overlay(preview_pil, to_preview(points), stem), points
|
| 1143 |
|
| 1144 |
# Gradio can deliver an empty selection (see gradio-app/gradio#5945);
|
| 1145 |
# dropping it beats raising a red error box the user cannot interpret.
|
| 1146 |
if evt is None or evt.index is None or evt.index[0] is None:
|
| 1147 |
return draw_polygon_overlay(preview_pil, to_preview(points)), points
|
| 1148 |
|
| 1149 |
+
tx, ty = int(evt.index[0]), int(evt.index[1])
|
| 1150 |
+
# Lift the marker above the touch, clamped so a tap near the top edge
|
| 1151 |
+
# cannot place a vertex off the image.
|
| 1152 |
+
my = max(0, min(preview_pil.height - 1, ty - stem))
|
| 1153 |
+
x, y = int(tx / scale), int(my / scale)
|
| 1154 |
new_points = points + [(x, y)]
|
| 1155 |
+
return draw_polygon_overlay(preview_pil, to_preview(new_points), stem), new_points
|
| 1156 |
|
| 1157 |
|
| 1158 |
def clear_crop_points(image_pil):
|
|
|
|
| 1340 |
|
| 1341 |
gr.Markdown(
|
| 1342 |
"### Crop region (optional)\n"
|
| 1343 |
+
"Tap up to **4 points** on the image below to define the region "
|
| 1344 |
+
"to segment. On a touch screen the marker is placed **above "
|
| 1345 |
+
"your fingertip** so it is not hidden — a faint tail shows "
|
| 1346 |
+
"where you actually touched. "
|
| 1347 |
"Leave empty to segment the full image."
|
| 1348 |
)
|
| 1349 |
|
|
|
|
| 1360 |
|
| 1361 |
clear_crop_btn = gr.Button("✕ Clear crop points", size="sm")
|
| 1362 |
|
| 1363 |
+
touch_offset = gr.Slider(
|
| 1364 |
+
minimum=0, maximum=25, value=OFFSET_PCT_DEFAULT, step=1,
|
| 1365 |
+
label="Fingertip offset (% of picker height)",
|
| 1366 |
+
info="How far above your finger the marker is placed. "
|
| 1367 |
+
"Set to 0 when using a mouse.",
|
| 1368 |
+
)
|
| 1369 |
+
|
| 1370 |
model_dropdown = gr.Dropdown(
|
| 1371 |
choices=list(MODEL_OPTIONS.keys()),
|
| 1372 |
label="Select Model",
|
|
|
|
| 1553 |
_component.change(fn=on_exclusion_change,
|
| 1554 |
inputs=exclusion_inputs, outputs=exclusion_outputs)
|
| 1555 |
|
| 1556 |
+
def on_crop_click(base_img, points, scale, offset_pct, evt: gr.SelectData):
|
| 1557 |
before = len(points or [])
|
| 1558 |
+
updated_img, updated_pts = add_crop_point(base_img, points, scale,
|
| 1559 |
+
offset_pct, evt)
|
| 1560 |
n = len(updated_pts)
|
| 1561 |
if n == before and n < 4:
|
| 1562 |
status = "*Tap not registered — try again slightly inside the image*"
|
|
|
|
| 1567 |
return updated_img, updated_pts, status
|
| 1568 |
|
| 1569 |
crop_display.select(fn=on_crop_click,
|
| 1570 |
+
inputs=[base_image_state, crop_points_state, preview_scale_state,
|
| 1571 |
+
touch_offset],
|
| 1572 |
outputs=[crop_display, crop_points_state, crop_status])
|
| 1573 |
|
| 1574 |
def on_clear_crop(base_img):
|