Upload app.py
Browse files
app.py
CHANGED
|
@@ -1111,7 +1111,39 @@ 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
|
| 1116 |
|
| 1117 |
def add_crop_point(preview_pil, points, scale, offset_pct, evt: gr.SelectData):
|
|
@@ -1323,6 +1355,8 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1323 |
# preview->original scale, so taps on the small picker map back to
|
| 1324 |
# full-resolution coordinates for the segmentation warp
|
| 1325 |
preview_scale_state = gr.State(value=1.0)
|
|
|
|
|
|
|
| 1326 |
#raw image state
|
| 1327 |
raw_image_state = gr.State(value=None)
|
| 1328 |
# Masks as segmented, before stereological exclusion β lets the zones
|
|
@@ -1340,11 +1374,11 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1340 |
|
| 1341 |
gr.Markdown(
|
| 1342 |
"### Crop region (optional)\n"
|
| 1343 |
-
"
|
| 1344 |
-
"
|
| 1345 |
-
"
|
| 1346 |
-
"
|
| 1347 |
-
"
|
| 1348 |
)
|
| 1349 |
|
| 1350 |
crop_display = gr.Image(
|
|
@@ -1360,11 +1394,14 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 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="
|
| 1367 |
-
"
|
| 1368 |
)
|
| 1369 |
|
| 1370 |
model_dropdown = gr.Dropdown(
|
|
@@ -1511,7 +1548,7 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1511 |
fn=on_image_upload,
|
| 1512 |
inputs=[img_input],
|
| 1513 |
outputs=[crop_display, base_image_state, preview_scale_state, crop_status]
|
| 1514 |
-
).then(fn=lambda: [], outputs=[crop_points_state])
|
| 1515 |
|
| 1516 |
# Grey the size sliders out while their filter is disabled, so the
|
| 1517 |
# checkbox state is visible rather than silently inferred.
|
|
@@ -1553,31 +1590,78 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1553 |
_component.change(fn=on_exclusion_change,
|
| 1554 |
inputs=exclusion_inputs, outputs=exclusion_outputs)
|
| 1555 |
|
| 1556 |
-
def
|
| 1557 |
-
|
| 1558 |
-
|
| 1559 |
-
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
| 1565 |
-
|
| 1566 |
-
|
| 1567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1568 |
|
| 1569 |
crop_display.select(fn=on_crop_click,
|
| 1570 |
-
inputs=[base_image_state, crop_points_state,
|
| 1571 |
-
touch_offset],
|
| 1572 |
-
outputs=[crop_display, crop_points_state, crop_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1573 |
|
| 1574 |
def on_clear_crop(base_img):
|
| 1575 |
img, pts = clear_crop_points(base_img)
|
| 1576 |
-
return img, pts, "*Points cleared β
|
|
|
|
| 1577 |
|
| 1578 |
clear_crop_btn.click(fn=on_clear_crop,
|
| 1579 |
inputs=[base_image_state],
|
| 1580 |
-
outputs=[crop_display, crop_points_state, crop_status
|
|
|
|
| 1581 |
|
| 1582 |
segment_btn.click(
|
| 1583 |
fn=run_segmentation,
|
|
|
|
| 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 |
+
|
| 1118 |
+
def make_zoom_view(full_img, cx_full, cy_full):
|
| 1119 |
+
"""Zoomed window of the ORIGINAL pixels, centred on a rough tap.
|
| 1120 |
+
|
| 1121 |
+
Corner placement fails on a phone because the fingertip covers the target.
|
| 1122 |
+
Zooming makes the target ~6x larger than the finger, so the second tap
|
| 1123 |
+
needs no precision at all -- and cropping from the original rather than the
|
| 1124 |
+
preview means the second tap also gains real detail, not interpolation.
|
| 1125 |
+
"""
|
| 1126 |
+
W, H = full_img.size
|
| 1127 |
+
win_w = max(32, W // ZOOM_FACTOR)
|
| 1128 |
+
win_h = max(24, int(win_w * 0.75))
|
| 1129 |
+
x0 = int(min(max(0, cx_full - win_w // 2), max(0, W - win_w)))
|
| 1130 |
+
y0 = int(min(max(0, cy_full - win_h // 2), max(0, H - win_h)))
|
| 1131 |
+
win_w, win_h = min(win_w, W - x0), min(win_h, H - y0)
|
| 1132 |
+
|
| 1133 |
+
view = full_img.crop((x0, y0, x0 + win_w, y0 + win_h))
|
| 1134 |
+
out_w = PREVIEW_MAX
|
| 1135 |
+
view = view.resize((out_w, max(1, int(out_w * win_h / win_w))), Image.LANCZOS)
|
| 1136 |
+
zscale = view.width / float(win_w) # view px per original px
|
| 1137 |
+
|
| 1138 |
+
d = ImageDraw.Draw(view)
|
| 1139 |
+
cx_v = (cx_full - x0) * zscale
|
| 1140 |
+
cy_v = (cy_full - y0) * zscale
|
| 1141 |
+
arm = max(20, view.width // 25)
|
| 1142 |
+
for dx, dy in ((1, 0), (0, 1)):
|
| 1143 |
+
d.line([(cx_v - arm * dx, cy_v - arm * dy), (cx_v + arm * dx, cy_v + arm * dy)],
|
| 1144 |
+
fill=(255, 90, 90, 255), width=2)
|
| 1145 |
+
d.ellipse([cx_v - 4, cy_v - 4, cx_v + 4, cy_v + 4], outline=(255, 90, 90), width=2)
|
| 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):
|
|
|
|
| 1355 |
# preview->original scale, so taps on the small picker map back to
|
| 1356 |
# full-resolution coordinates for the segmentation warp
|
| 1357 |
preview_scale_state = gr.State(value=1.0)
|
| 1358 |
+
# None = showing the overview; dict = showing a zoomed window
|
| 1359 |
+
zoom_state = gr.State(value=None)
|
| 1360 |
#raw image state
|
| 1361 |
raw_image_state = gr.State(value=None)
|
| 1362 |
# Masks as segmented, before stereological exclusion β lets the zones
|
|
|
|
| 1374 |
|
| 1375 |
gr.Markdown(
|
| 1376 |
"### Crop region (optional)\n"
|
| 1377 |
+
"For each corner, tap **twice**: once roughly near it, then "
|
| 1378 |
+
"again precisely in the zoomed view that appears. The rough "
|
| 1379 |
+
"tap needs no accuracy β your finger can cover it entirely. "
|
| 1380 |
+
"Four corners define the region to segment; leave empty to "
|
| 1381 |
+
"use the whole image."
|
| 1382 |
)
|
| 1383 |
|
| 1384 |
crop_display = gr.Image(
|
|
|
|
| 1394 |
|
| 1395 |
clear_crop_btn = gr.Button("β Clear crop points", size="sm")
|
| 1396 |
|
| 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(
|
|
|
|
| 1548 |
fn=on_image_upload,
|
| 1549 |
inputs=[img_input],
|
| 1550 |
outputs=[crop_display, base_image_state, preview_scale_state, crop_status]
|
| 1551 |
+
).then(fn=lambda: ([], None), outputs=[crop_points_state, zoom_state])
|
| 1552 |
|
| 1553 |
# Grey the size sliders out while their filter is disabled, so the
|
| 1554 |
# checkbox state is visible rather than silently inferred.
|
|
|
|
| 1590 |
_component.change(fn=on_exclusion_change,
|
| 1591 |
inputs=exclusion_inputs, outputs=exclusion_outputs)
|
| 1592 |
|
| 1593 |
+
def _overview(preview, points, scale):
|
| 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, offset_pct,
|
| 1598 |
+
evt: gr.SelectData):
|
| 1599 |
+
"""Two stages per corner: rough tap -> zoomed view -> precise tap."""
|
| 1600 |
+
points = list(points or [])
|
| 1601 |
+
if preview is None or full_img is None:
|
| 1602 |
+
return (gr.update(), points, zoom, gr.update(), gr.update())
|
| 1603 |
+
|
| 1604 |
+
# Gradio can deliver an empty selection (gradio-app/gradio#5945)
|
| 1605 |
+
if evt is None or evt.index is None or evt.index[0] is None:
|
| 1606 |
+
return (gr.update(), points, zoom,
|
| 1607 |
+
"*Tap not registered β try again inside the image*",
|
| 1608 |
+
gr.update())
|
| 1609 |
+
|
| 1610 |
+
tx, ty = int(evt.index[0]), int(evt.index[1])
|
| 1611 |
+
|
| 1612 |
+
if zoom is None:
|
| 1613 |
+
# ---- stage 1: rough tap on the overview ----------------------
|
| 1614 |
+
if len(points) >= 4:
|
| 1615 |
+
return (_overview(preview, points, scale), points, None,
|
| 1616 |
+
"*4 points set β β **β Clear** to redo, or run segmentation*",
|
| 1617 |
+
gr.update(visible=False))
|
| 1618 |
+
stem = int(preview.height * (float(offset_pct or 0) / 100.0))
|
| 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"] + max(0, ty - stem) / zoom["zscale"]
|
| 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)))]
|
| 1633 |
+
n = len(new_points)
|
| 1634 |
+
status = (f"*{n} / 4 corners set β tap roughly near corner {n + 1}*"
|
| 1635 |
+
if n < 4 else
|
| 1636 |
+
"*4 points set β β **β Clear** to redo, or run segmentation*")
|
| 1637 |
+
return (_overview(preview, new_points, scale), new_points, None,
|
| 1638 |
+
status, gr.update(visible=False))
|
| 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, touch_offset],
|
| 1643 |
+
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1644 |
+
cancel_zoom_btn])
|
| 1645 |
+
|
| 1646 |
+
def on_cancel_zoom(preview, points, scale):
|
| 1647 |
+
n = len(points or [])
|
| 1648 |
+
return (_overview(preview, points or [], scale), None,
|
| 1649 |
+
f"*Back to full view β {n} / 4 corners set*",
|
| 1650 |
+
gr.update(visible=False))
|
| 1651 |
+
|
| 1652 |
+
cancel_zoom_btn.click(fn=on_cancel_zoom,
|
| 1653 |
+
inputs=[base_image_state, crop_points_state, preview_scale_state],
|
| 1654 |
+
outputs=[crop_display, zoom_state, crop_status, cancel_zoom_btn])
|
| 1655 |
|
| 1656 |
def on_clear_crop(base_img):
|
| 1657 |
img, pts = clear_crop_points(base_img)
|
| 1658 |
+
return (img, pts, None, "*Points cleared β tap roughly near corner 1*",
|
| 1659 |
+
gr.update(visible=False))
|
| 1660 |
|
| 1661 |
clear_crop_btn.click(fn=on_clear_crop,
|
| 1662 |
inputs=[base_image_state],
|
| 1663 |
+
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1664 |
+
cancel_zoom_btn])
|
| 1665 |
|
| 1666 |
segment_btn.click(
|
| 1667 |
fn=run_segmentation,
|