Upload app.py
Browse files
app.py
CHANGED
|
@@ -1052,12 +1052,10 @@ 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 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,15 +1078,6 @@ def draw_polygon_overlay(image_pil, points, stem_px=0):
|
|
| 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,7 +1100,6 @@ def make_preview(image_pil):
|
|
| 1111 |
return preview, (preview.width / float(image_pil.width)) if image_pil.width else 1.0
|
| 1112 |
|
| 1113 |
|
| 1114 |
-
OFFSET_PCT_DEFAULT = 0.0 # % of picker height; 0 = marker under the finger
|
| 1115 |
ZOOM_FACTOR = 6 # zoom window is 1/6 of the image width
|
| 1116 |
|
| 1117 |
|
|
@@ -1146,47 +1134,6 @@ def make_zoom_view(full_img, cx_full, cy_full):
|
|
| 1146 |
return view, {"x0": x0, "y0": y0, "zscale": zscale}
|
| 1147 |
|
| 1148 |
|
| 1149 |
-
def add_crop_point(preview_pil, points, scale, offset_pct, evt: gr.SelectData):
|
| 1150 |
-
"""
|
| 1151 |
-
Called by gr.Image .select(). Appends the clicked coordinate,
|
| 1152 |
-
redraws the overlay on the PREVIEW, returns (updated_preview, updated_points).
|
| 1153 |
-
|
| 1154 |
-
`points` are kept in ORIGINAL image pixels so run_segmentation's warp is
|
| 1155 |
-
unaffected; only the drawing happens in preview space.
|
| 1156 |
-
Ignores clicks once 4 points are set.
|
| 1157 |
-
"""
|
| 1158 |
-
if points is None:
|
| 1159 |
-
points = []
|
| 1160 |
-
scale = scale or 1.0
|
| 1161 |
-
|
| 1162 |
-
def to_preview(pts):
|
| 1163 |
-
return [(int(px * scale), int(py * scale)) for px, py in pts]
|
| 1164 |
-
|
| 1165 |
-
if preview_pil is None:
|
| 1166 |
-
return preview_pil, points
|
| 1167 |
-
|
| 1168 |
-
# Vertical offset so the marker is not hidden under the fingertip.
|
| 1169 |
-
# Expressed as a share of picker height, which keeps it consistent across
|
| 1170 |
-
# phone sizes instead of being tied to the image's pixel dimensions.
|
| 1171 |
-
stem = int(preview_pil.height * (float(offset_pct or 0.0) / 100.0))
|
| 1172 |
-
|
| 1173 |
-
if len(points) >= 4:
|
| 1174 |
-
return draw_polygon_overlay(preview_pil, to_preview(points), stem), points
|
| 1175 |
-
|
| 1176 |
-
# Gradio can deliver an empty selection (see gradio-app/gradio#5945);
|
| 1177 |
-
# dropping it beats raising a red error box the user cannot interpret.
|
| 1178 |
-
if evt is None or evt.index is None or evt.index[0] is None:
|
| 1179 |
-
return draw_polygon_overlay(preview_pil, to_preview(points)), points
|
| 1180 |
-
|
| 1181 |
-
tx, ty = int(evt.index[0]), int(evt.index[1])
|
| 1182 |
-
# Lift the marker above the touch, clamped so a tap near the top edge
|
| 1183 |
-
# cannot place a vertex off the image.
|
| 1184 |
-
my = max(0, min(preview_pil.height - 1, ty - stem))
|
| 1185 |
-
x, y = int(tx / scale), int(my / scale)
|
| 1186 |
-
new_points = points + [(x, y)]
|
| 1187 |
-
return draw_polygon_overlay(preview_pil, to_preview(new_points), stem), new_points
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
def clear_crop_points(image_pil):
|
| 1191 |
"""Reset polygon — return original image with no overlay and empty points."""
|
| 1192 |
return image_pil, []
|
|
@@ -1397,13 +1344,6 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1397 |
cancel_zoom_btn = gr.Button("↩ Back to full view", size="sm",
|
| 1398 |
visible=False)
|
| 1399 |
|
| 1400 |
-
touch_offset = gr.Slider(
|
| 1401 |
-
minimum=0, maximum=25, value=OFFSET_PCT_DEFAULT, step=1,
|
| 1402 |
-
label="Fingertip offset (optional, % of picker height)",
|
| 1403 |
-
info="Lifts the marker above your finger. Leave at 0 — "
|
| 1404 |
-
"the zoom step already solves the occlusion problem.",
|
| 1405 |
-
)
|
| 1406 |
-
|
| 1407 |
model_dropdown = gr.Dropdown(
|
| 1408 |
choices=list(MODEL_OPTIONS.keys()),
|
| 1409 |
label="Select Model",
|
|
@@ -1594,7 +1534,7 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1594 |
pv = [(int(x * scale), int(y * scale)) for x, y in points]
|
| 1595 |
return draw_polygon_overlay(preview, pv)
|
| 1596 |
|
| 1597 |
-
def on_crop_click(full_img, preview, points, scale, zoom,
|
| 1598 |
evt: gr.SelectData):
|
| 1599 |
"""Two stages per corner: rough tap -> zoomed view -> precise tap."""
|
| 1600 |
points = list(points or [])
|
|
@@ -1615,18 +1555,15 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1615 |
return (_overview(preview, points, scale), points, None,
|
| 1616 |
"*4 points set ✓ — **✕ Clear** to redo, or run segmentation*",
|
| 1617 |
gr.update(visible=False))
|
| 1618 |
-
|
| 1619 |
-
my = max(0, min(preview.height - 1, ty - stem))
|
| 1620 |
-
view, z = make_zoom_view(full_img, tx / scale, my / scale)
|
| 1621 |
return (view, points, z,
|
| 1622 |
f"*Zoomed in — now tap corner {len(points) + 1} precisely "
|
| 1623 |
f"(the red cross is your rough tap)*",
|
| 1624 |
gr.update(visible=True))
|
| 1625 |
|
| 1626 |
# ---- stage 2: precise tap inside the zoomed view -----------------
|
| 1627 |
-
stem = int((PREVIEW_MAX * 0.75) * (float(offset_pct or 0) / 100.0))
|
| 1628 |
x_full = zoom["x0"] + tx / zoom["zscale"]
|
| 1629 |
-
y_full = zoom["y0"] +
|
| 1630 |
W, H = full_img.size
|
| 1631 |
new_points = points + [(int(min(max(0, x_full), W - 1)),
|
| 1632 |
int(min(max(0, y_full), H - 1)))]
|
|
@@ -1639,7 +1576,7 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1639 |
|
| 1640 |
crop_display.select(fn=on_crop_click,
|
| 1641 |
inputs=[img_input, base_image_state, crop_points_state,
|
| 1642 |
-
preview_scale_state, zoom_state
|
| 1643 |
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1644 |
cancel_zoom_btn])
|
| 1645 |
|
|
|
|
| 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 preview pixel space.
|
|
|
|
|
|
|
| 1059 |
Returns a new PIL image.
|
| 1060 |
"""
|
| 1061 |
img = image_pil.copy().convert("RGBA")
|
|
|
|
| 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 |
return preview, (preview.width / float(image_pil.width)) if image_pil.width else 1.0
|
| 1101 |
|
| 1102 |
|
|
|
|
| 1103 |
ZOOM_FACTOR = 6 # zoom window is 1/6 of the image width
|
| 1104 |
|
| 1105 |
|
|
|
|
| 1134 |
return view, {"x0": x0, "y0": y0, "zscale": zscale}
|
| 1135 |
|
| 1136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1137 |
def clear_crop_points(image_pil):
|
| 1138 |
"""Reset polygon — return original image with no overlay and empty points."""
|
| 1139 |
return image_pil, []
|
|
|
|
| 1344 |
cancel_zoom_btn = gr.Button("↩ Back to full view", size="sm",
|
| 1345 |
visible=False)
|
| 1346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1347 |
model_dropdown = gr.Dropdown(
|
| 1348 |
choices=list(MODEL_OPTIONS.keys()),
|
| 1349 |
label="Select Model",
|
|
|
|
| 1534 |
pv = [(int(x * scale), int(y * scale)) for x, y in points]
|
| 1535 |
return draw_polygon_overlay(preview, pv)
|
| 1536 |
|
| 1537 |
+
def on_crop_click(full_img, preview, points, scale, zoom,
|
| 1538 |
evt: gr.SelectData):
|
| 1539 |
"""Two stages per corner: rough tap -> zoomed view -> precise tap."""
|
| 1540 |
points = list(points or [])
|
|
|
|
| 1555 |
return (_overview(preview, points, scale), points, None,
|
| 1556 |
"*4 points set ✓ — **✕ Clear** to redo, or run segmentation*",
|
| 1557 |
gr.update(visible=False))
|
| 1558 |
+
view, z = make_zoom_view(full_img, tx / scale, ty / scale)
|
|
|
|
|
|
|
| 1559 |
return (view, points, z,
|
| 1560 |
f"*Zoomed in — now tap corner {len(points) + 1} precisely "
|
| 1561 |
f"(the red cross is your rough tap)*",
|
| 1562 |
gr.update(visible=True))
|
| 1563 |
|
| 1564 |
# ---- stage 2: precise tap inside the zoomed view -----------------
|
|
|
|
| 1565 |
x_full = zoom["x0"] + tx / zoom["zscale"]
|
| 1566 |
+
y_full = zoom["y0"] + ty / zoom["zscale"]
|
| 1567 |
W, H = full_img.size
|
| 1568 |
new_points = points + [(int(min(max(0, x_full), W - 1)),
|
| 1569 |
int(min(max(0, y_full), H - 1)))]
|
|
|
|
| 1576 |
|
| 1577 |
crop_display.select(fn=on_crop_click,
|
| 1578 |
inputs=[img_input, base_image_state, crop_points_state,
|
| 1579 |
+
preview_scale_state, zoom_state],
|
| 1580 |
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1581 |
cancel_zoom_btn])
|
| 1582 |
|