LiangLabUMB commited on
Commit
edfb9e4
Β·
verified Β·
1 Parent(s): 4c75271

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -27
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 = 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):
@@ -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
- "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
 
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="How far above your finger the marker is placed. "
1367
- "Set to 0 when using a mouse.",
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 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*"
1563
- elif n < 4:
1564
- status = f"*{n} / 4 points set β€” keep tapping*"
1565
- else:
1566
- status = "*4 points set βœ“ β€” **βœ• Clear** to redo, or run segmentation*"
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):
1575
  img, pts = clear_crop_points(base_img)
1576
- return img, pts, "*Points cleared β€” click to set new vertices*"
 
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,