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

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -11
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 original image pixel space.
 
 
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
- def add_crop_point(preview_pil, points, scale, evt: gr.SelectData):
 
 
 
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
- x, y = int(evt.index[0] / scale), int(evt.index[1] / scale)
 
 
 
 
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
- "Click/tap up to **4 points** on the image below to define the region "
1320
- "to segment. The polygon will be drawn as you click. "
 
 
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, evt)
 
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):