anonymous-IA commited on
Commit
3f86f08
Β·
verified Β·
1 Parent(s): 938fad3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -27
app.py CHANGED
@@ -234,6 +234,44 @@ def _resolve_path(file_obj):
234
  return ""
235
 
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  # ─────────────────────────────────────────────────────────────────────────────
238
  # Event handlers β€” image
239
  # ─────────────────────────────────────────────────────────────────────────────
@@ -241,7 +279,7 @@ def _resolve_path(file_obj):
241
 
242
  def on_file_upload(file_obj):
243
  """Load any image or DICOM and switch the panel to fixation-click mode."""
244
- _no_change = (None, [], "", gr.update(), gr.update(), gr.update())
245
  if file_obj is None:
246
  return _no_change
247
 
@@ -265,6 +303,7 @@ def on_file_upload(file_obj):
265
  gr.update(visible=False), # upload_zone β†’ hide
266
  gr.update(value=pil, visible=True, label=_FIXATION_LABEL), # image_panel β†’ show
267
  gr.update(visible=True), # delete_btn β†’ show
 
268
  )
269
 
270
 
@@ -272,17 +311,17 @@ def on_select(orig_image: Image.Image, points: list, weight: float, evt: gr.Sele
272
  """Record a fixation click in original-image pixel coords."""
273
  if orig_image is None:
274
  gr.Warning("Upload an image first.")
275
- return points, gr.update()
276
  x_px, y_px = float(evt.index[0]), float(evt.index[1])
277
  new_points = points + [(x_px, y_px, float(weight))]
278
- return new_points, draw_points(orig_image, new_points)
279
 
280
 
281
  def on_clear(orig_image):
282
  """Remove all fixations but keep the current image."""
283
  if orig_image is None:
284
- return [], gr.update()
285
- return [], gr.update(value=orig_image)
286
 
287
 
288
  def on_delete():
@@ -294,6 +333,7 @@ def on_delete():
294
  gr.update(value=None, visible=True), # upload_zone β†’ show (reset)
295
  gr.update(value=None, visible=False), # image_panel β†’ hide
296
  gr.update(visible=False), # delete_btn β†’ hide
 
297
  )
298
 
299
 
@@ -376,13 +416,13 @@ def on_apply_fixfile(fixfile_json, id_col, x_col, y_col, time_col, orig_image, i
376
  ID column doesn't match the uploaded filename)."""
377
  if orig_image is None:
378
  gr.Warning("Load an image first, then apply the fixation file.")
379
- return gr.update(), gr.update()
380
  if not fixfile_json:
381
  gr.Warning("Upload a fixation file first.")
382
- return gr.update(), gr.update()
383
  if not x_col or not y_col:
384
  gr.Warning("Pick the X and Y columns first.")
385
- return gr.update(), gr.update()
386
 
387
  df = pd.read_json(fixfile_json)
388
 
@@ -402,7 +442,7 @@ def on_apply_fixfile(fixfile_json, id_col, x_col, y_col, time_col, orig_image, i
402
 
403
  if sub.empty:
404
  gr.Warning("No usable fixation rows found.")
405
- return gr.update(), gr.update()
406
 
407
  w, h = orig_image.size
408
  x_vals = sub[x_col].astype(float).to_numpy()
@@ -413,13 +453,20 @@ def on_apply_fixfile(fixfile_json, id_col, x_col, y_col, time_col, orig_image, i
413
  t_raw = sub[time_col].astype(float).to_numpy()
414
  order = np.argsort(t_raw) # chronological order
415
  x_px, y_px, t_raw = x_px[order], y_px[order], t_raw[order]
416
- tmin, tmax = float(np.nanmin(t_raw)), float(np.nanmax(t_raw))
417
- weight = (t_raw - tmin) / (tmax - tmin + 1e-8)
 
 
 
 
 
 
 
418
  else:
419
- weight = np.linspace(0.0, 1.0, len(sub))
420
 
421
  new_points = [(float(xp), float(yp), float(wt)) for xp, yp, wt in zip(x_px, y_px, weight)]
422
- return new_points, draw_points(orig_image, new_points)
423
 
424
 
425
  # ─────────────────────────────────────────────────────────────────────────────
@@ -487,19 +534,20 @@ def run(orig_image: Image.Image, points: list, preset_name: str):
487
  class_probs = {c: float(p) for c, p in zip(predictor.classes, probs)}
488
  predicted_class = max(class_probs, key=class_probs.get)
489
 
490
- # Learned gaze-conditioned attention mask, upsampled + overlaid.
 
491
  img_size = predictor.img_size
492
- mask_full = patch_to_image(patch_mask[0].cpu().numpy(), img_size, img_size)
493
  display_img = np.array(orig_image.resize((img_size, img_size)))
494
- overlay = make_overlay(display_img, mask_full)
495
- mask_img = heatmap_to_image(mask_full)
 
496
 
497
  prob_lines = "\n".join(
498
  f"- **{c}**: {p:.3f}" for c, p in sorted(class_probs.items(), key=lambda kv: -kv[1])
499
  )
500
  summary = f"### Predicted: **{predicted_class}**\n\n{prob_lines}"
501
 
502
- return class_probs, summary, overlay, mask_img
503
 
504
 
505
  # ─────────────────────────────────────────────────────────────────────────────
@@ -540,13 +588,20 @@ with gr.Blocks(title="GazeAlign", css=_CSS) as demo:
540
  file_types=[".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp", ".dcm"],
541
  type="filepath",
542
  )
 
 
 
543
  image_panel = gr.Image(
544
- label=_FIXATION_LABEL, type="pil", interactive=True, visible=False
 
 
 
 
545
  )
546
 
547
  weight_slider = gr.Slider(
548
  0.0, 1.0, value=0.5, step=0.05,
549
- label="Fixation weight (dwell) for clicks",
550
  )
551
  with gr.Row():
552
  clear_btn = gr.Button("Clear fixations")
@@ -568,8 +623,8 @@ with gr.Blocks(title="GazeAlign", css=_CSS) as demo:
568
  run_btn = gr.Button("Run GazeAlign", variant="primary", elem_id="run-btn")
569
  label_output = gr.Label(label="Predicted class (probabilities)", num_top_classes=5)
570
  summary_output = gr.Markdown()
571
- overlay_output = gr.Image(label="Gaze-conditioned attention overlay")
572
- mask_output = gr.Image(label="Learned attention mask")
573
 
574
  gr.Markdown(
575
  "<div class='footer-note'>Fixation tables use raw pixel or normalised "
@@ -582,18 +637,18 @@ with gr.Blocks(title="GazeAlign", css=_CSS) as demo:
582
  upload_zone.upload(
583
  on_file_upload,
584
  [upload_zone],
585
- [orig_image_state, points_state, image_name_state, upload_zone, image_panel, delete_btn],
586
  )
587
  image_panel.select(
588
  on_select,
589
  [orig_image_state, points_state, weight_slider],
590
- [points_state, image_panel],
591
  )
592
- clear_btn.click(on_clear, [orig_image_state], [points_state, image_panel])
593
  delete_btn.click(
594
  on_delete,
595
  None,
596
- [orig_image_state, points_state, image_name_state, upload_zone, image_panel, delete_btn],
597
  )
598
 
599
  fixfile.upload(
@@ -604,7 +659,7 @@ with gr.Blocks(title="GazeAlign", css=_CSS) as demo:
604
  apply_fix_btn.click(
605
  on_apply_fixfile,
606
  [fixfile_df_state, id_col_dd, x_col_dd, y_col_dd, time_col_dd, orig_image_state, image_name_state],
607
- [points_state, image_panel],
608
  )
609
 
610
  run_btn.click(
 
234
  return ""
235
 
236
 
237
+ def _status(points: list) -> str:
238
+ """Small feedback line so it's obvious when fixations register."""
239
+ n = len(points) if points else 0
240
+ if not n:
241
+ return "_No fixations yet β€” click the image, or load a fixation file below._"
242
+ return f"**{n}** fixation(s) placed."
243
+
244
+
245
+ def gaze_duration_heatmap(points, height, width, img_w, img_h, sigma=None):
246
+ """Gaussian-splat heatmap of the fixations, each blob weighted by its
247
+ dwell / duration (the 3rd component of each point), rendered at
248
+ (height, width). This is the *observed* gaze heatmap β€” not the model's
249
+ learned attention.
250
+
251
+ points: list of (x_px, y_px, weight) in original-image pixels.
252
+ """
253
+ hm = np.zeros((height, width), dtype=np.float32)
254
+ if not points:
255
+ return hm
256
+ if sigma is None:
257
+ sigma = max(height, width) / 22.0
258
+ sx, sy = width / max(img_w, 1), height / max(img_h, 1)
259
+ rad = max(int(sigma * 3), 1)
260
+ for x_px, y_px, wgt in points:
261
+ cx, cy = int(round(x_px * sx)), int(round(y_px * sy))
262
+ if not (0 <= cx < width and 0 <= cy < height):
263
+ continue
264
+ x0, x1 = max(cx - rad, 0), min(cx + rad + 1, width)
265
+ y0, y1 = max(cy - rad, 0), min(cy + rad + 1, height)
266
+ xv, yv = np.meshgrid(np.arange(x0, x1), np.arange(y0, y1))
267
+ g = np.exp(-((xv - cx) ** 2 + (yv - cy) ** 2) / (2.0 * sigma ** 2))
268
+ # +0.15 floor so short-dwell fixations still register a little.
269
+ hm[y0:y1, x0:x1] += g.astype(np.float32) * (0.15 + float(wgt))
270
+ if hm.max() > 0:
271
+ hm /= hm.max()
272
+ return hm
273
+
274
+
275
  # ─────────────────────────────────────────────────────────────────────────────
276
  # Event handlers β€” image
277
  # ─────────────────────────────────────────────────────────────────────────────
 
279
 
280
  def on_file_upload(file_obj):
281
  """Load any image or DICOM and switch the panel to fixation-click mode."""
282
+ _no_change = (None, [], "", gr.update(), gr.update(), gr.update(), gr.update())
283
  if file_obj is None:
284
  return _no_change
285
 
 
303
  gr.update(visible=False), # upload_zone β†’ hide
304
  gr.update(value=pil, visible=True, label=_FIXATION_LABEL), # image_panel β†’ show
305
  gr.update(visible=True), # delete_btn β†’ show
306
+ _status([]), # fix_status
307
  )
308
 
309
 
 
311
  """Record a fixation click in original-image pixel coords."""
312
  if orig_image is None:
313
  gr.Warning("Upload an image first.")
314
+ return points, gr.update(), _status(points)
315
  x_px, y_px = float(evt.index[0]), float(evt.index[1])
316
  new_points = points + [(x_px, y_px, float(weight))]
317
+ return new_points, draw_points(orig_image, new_points), _status(new_points)
318
 
319
 
320
  def on_clear(orig_image):
321
  """Remove all fixations but keep the current image."""
322
  if orig_image is None:
323
+ return [], gr.update(), _status([])
324
+ return [], gr.update(value=orig_image), _status([])
325
 
326
 
327
  def on_delete():
 
333
  gr.update(value=None, visible=True), # upload_zone β†’ show (reset)
334
  gr.update(value=None, visible=False), # image_panel β†’ hide
335
  gr.update(visible=False), # delete_btn β†’ hide
336
+ _status([]), # fix_status
337
  )
338
 
339
 
 
416
  ID column doesn't match the uploaded filename)."""
417
  if orig_image is None:
418
  gr.Warning("Load an image first, then apply the fixation file.")
419
+ return gr.update(), gr.update(), gr.update()
420
  if not fixfile_json:
421
  gr.Warning("Upload a fixation file first.")
422
+ return gr.update(), gr.update(), gr.update()
423
  if not x_col or not y_col:
424
  gr.Warning("Pick the X and Y columns first.")
425
+ return gr.update(), gr.update(), gr.update()
426
 
427
  df = pd.read_json(fixfile_json)
428
 
 
442
 
443
  if sub.empty:
444
  gr.Warning("No usable fixation rows found.")
445
+ return gr.update(), gr.update(), gr.update()
446
 
447
  w, h = orig_image.size
448
  x_vals = sub[x_col].astype(float).to_numpy()
 
453
  t_raw = sub[time_col].astype(float).to_numpy()
454
  order = np.argsort(t_raw) # chronological order
455
  x_px, y_px, t_raw = x_px[order], y_px[order], t_raw[order]
456
+ # Per-fixation dwell = gap to the next fixation (last one gets the
457
+ # median gap); normalised to [0,1] so it weights the duration heatmap.
458
+ if len(t_raw) > 1:
459
+ dwell = np.diff(t_raw, append=t_raw[-1] + np.median(np.diff(t_raw)))
460
+ dwell = np.clip(dwell, 0, None)
461
+ dmax = float(dwell.max())
462
+ weight = dwell / dmax if dmax > 0 else np.ones_like(dwell)
463
+ else:
464
+ weight = np.ones(1)
465
  else:
466
+ weight = np.ones(len(sub))
467
 
468
  new_points = [(float(xp), float(yp), float(wt)) for xp, yp, wt in zip(x_px, y_px, weight)]
469
+ return new_points, draw_points(orig_image, new_points), _status(new_points)
470
 
471
 
472
  # ─────────────────────────────────────────────────────────────────────────────
 
534
  class_probs = {c: float(p) for c, p in zip(predictor.classes, probs)}
535
  predicted_class = max(class_probs, key=class_probs.get)
536
 
537
+ # Output visual: the *observed* gaze-fixation heatmap, each fixation
538
+ # weighted by its dwell/duration β€” overlaid on the image and shown raw.
539
  img_size = predictor.img_size
 
540
  display_img = np.array(orig_image.resize((img_size, img_size)))
541
+ gaze_hm = gaze_duration_heatmap(points, img_size, img_size, w, h)
542
+ overlay = make_overlay(display_img, gaze_hm)
543
+ heatmap_img = heatmap_to_image(gaze_hm)
544
 
545
  prob_lines = "\n".join(
546
  f"- **{c}**: {p:.3f}" for c, p in sorted(class_probs.items(), key=lambda kv: -kv[1])
547
  )
548
  summary = f"### Predicted: **{predicted_class}**\n\n{prob_lines}"
549
 
550
+ return class_probs, summary, overlay, heatmap_img
551
 
552
 
553
  # ─────────────────────────────────────────────────────────────────────────────
 
588
  file_types=[".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp", ".dcm"],
589
  type="filepath",
590
  )
591
+ # interactive=False β†’ a pure display surface that reports click
592
+ # coordinates via .select (an interactive Image opens an editor
593
+ # instead and never fires reliable pixel coords).
594
  image_panel = gr.Image(
595
+ label=_FIXATION_LABEL, type="pil", interactive=False,
596
+ visible=False, height=440,
597
+ )
598
+ fix_status = gr.Markdown(
599
+ "_No fixations yet β€” click the image, or load a fixation file below._"
600
  )
601
 
602
  weight_slider = gr.Slider(
603
  0.0, 1.0, value=0.5, step=0.05,
604
+ label="Dwell / duration weight for the next click",
605
  )
606
  with gr.Row():
607
  clear_btn = gr.Button("Clear fixations")
 
623
  run_btn = gr.Button("Run GazeAlign", variant="primary", elem_id="run-btn")
624
  label_output = gr.Label(label="Predicted class (probabilities)", num_top_classes=5)
625
  summary_output = gr.Markdown()
626
+ overlay_output = gr.Image(label="Gaze-fixation heatmap (dwell-weighted) β€” overlay")
627
+ mask_output = gr.Image(label="Gaze-fixation heatmap (weighted by duration)")
628
 
629
  gr.Markdown(
630
  "<div class='footer-note'>Fixation tables use raw pixel or normalised "
 
637
  upload_zone.upload(
638
  on_file_upload,
639
  [upload_zone],
640
+ [orig_image_state, points_state, image_name_state, upload_zone, image_panel, delete_btn, fix_status],
641
  )
642
  image_panel.select(
643
  on_select,
644
  [orig_image_state, points_state, weight_slider],
645
+ [points_state, image_panel, fix_status],
646
  )
647
+ clear_btn.click(on_clear, [orig_image_state], [points_state, image_panel, fix_status])
648
  delete_btn.click(
649
  on_delete,
650
  None,
651
+ [orig_image_state, points_state, image_name_state, upload_zone, image_panel, delete_btn, fix_status],
652
  )
653
 
654
  fixfile.upload(
 
659
  apply_fix_btn.click(
660
  on_apply_fixfile,
661
  [fixfile_df_state, id_col_dd, x_col_dd, y_col_dd, time_col_dd, orig_image_state, image_name_state],
662
+ [points_state, image_panel, fix_status],
663
  )
664
 
665
  run_btn.click(