thenukegun10x commited on
Commit
253e323
·
verified ·
1 Parent(s): b799883

feat: vis heatmap in --vis (no new arg)

Browse files

Add get_vit_heatmap() per-patch logits 21x21->336 jet overlay where ViT looks (MoE 5 prefix tokens). --vis now 1x3: image | heatmap overlay | bar (was 1x2). Uses core.head on patch tokens, top class mean. Tested test.png 1.2MB with heatmap, noise 62K. Autocast bf16, fallback to 1x2 if heatmap fails. No new CLI arg, enhanced existing --vis.

Files changed (1) hide show
  1. plant_cli.py +85 -16
plant_cli.py CHANGED
@@ -73,6 +73,42 @@ def log(msg): print(msg, flush=True)
73
  def get_transform(img_size=336):
74
  return T.Compose([T.Resize(int(img_size*1.14)), T.CenterCrop(img_size), T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  def load_species_map(manifest=MANIFEST):
77
  # Public release: species_labels.json (999 WA species) - preferred
78
  if SPECIES_JSON.exists():
@@ -438,25 +474,58 @@ def cmd_identify(args):
438
  pd.DataFrame(meta["topk"]).to_csv(args.output, index=False)
439
  print(f"wrote {args.output}")
440
  if args.vis:
441
- # visualize topk bar + image
442
  try:
443
  import matplotlib
444
  matplotlib.use("Agg")
445
  import matplotlib.pyplot as plt
446
  n=len(image_paths)
447
- fig, axes = plt.subplots(1, 2, figsize=(14,5), gridspec_kw={"width_ratios":[1,1.2]})
448
- ax_img, ax_bar = axes
449
- # show first image
450
- im=open_image_pil(image_paths[0]).resize((336,336))
451
- ax_img.imshow(im)
452
- title=f"{meta['topk'][0]['species']}\n{meta['topk'][0]['prob']:.1%} conf {meta['confidence']:.1%} {dt*1000:.0f}ms"
453
- if meta["is_unknown"]:
454
- title="UNKNOWN\n"+title
455
- if len(image_paths)>1: title+=f" {n}-view"
456
- ax_img.set_title(title, fontsize=9, color="red" if meta["is_unknown"] else "black")
457
- ax_img.axis("off")
458
- probs_bar=[r["prob"] for r in meta["topk"]]
459
- species=[r["species"] for r in meta["topk"]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  colors=["red" if meta["is_unknown"] and i==0 else "green" if i==0 else "steelblue" for i in range(len(probs_bar))]
461
  bars=ax_bar.barh(range(len(probs_bar))[::-1], probs_bar[::-1], color=colors[::-1])
462
  ax_bar.set_yticks(range(len(probs_bar))[::-1])
@@ -465,13 +534,13 @@ def cmd_identify(args):
465
  ax_bar.set_title(f"WA 999 spp Top-{args.topk} {'UNKNOWN' if meta['is_unknown'] else ''}", fontsize=10, color="red" if meta["is_unknown"] else "black")
466
  for p, bar in zip(probs_bar[::-1], bars):
467
  ax_bar.text(p+0.01, bar.get_y()+bar.get_height()/2, f"{p:.1%}", va="center", fontsize=7)
468
- # add unknown text if unknown
469
  if meta["is_unknown"]:
470
  ax_bar.text(0.5, -1, f"sim {meta['unknown_info']['sim_max']:.2f} < {meta['thr_cos']:.2f}", ha="center", fontsize=8, color="red", transform=ax_bar.transAxes)
471
  plt.tight_layout()
472
  plt.savefig(args.vis, dpi=150)
473
- print(f"wrote {args.vis}")
474
  except Exception as e:
 
475
  log(f"vis failed {e}")
476
 
477
  def cmd_batch(args):
 
73
  def get_transform(img_size=336):
74
  return T.Compose([T.Resize(int(img_size*1.14)), T.CenterCrop(img_size), T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])
75
 
76
+ def get_vit_heatmap(visual_model, image_path, device="cuda", img_size=336):
77
+ """Feature heatmap where the ViT looks - per-patch logits for top class, 21x21 -> 336."""
78
+ try:
79
+ from pathlib import Path as _P
80
+ im = open_image_pil(image_path)
81
+ tf = get_transform(img_size)
82
+ x = tf(im).unsqueeze(0).to(device)
83
+ core = visual_model.core
84
+ with torch.no_grad():
85
+ with torch.autocast("cuda", dtype=torch.bfloat16, enabled=device=="cuda"):
86
+ tokens = core.stem.forward_features(x) # [1, T, D]
87
+ if core.input_proj is not None:
88
+ tokens = core.input_proj(tokens.float())
89
+ else:
90
+ tokens = tokens.float()
91
+ for b in core.blocks:
92
+ tokens, _, _, _ = b(tokens)
93
+ tokens = core.exit_norm(tokens) # [1, T, D]
94
+ n_prefix = int(getattr(core, "n_prefix", 1))
95
+ patch_tokens = tokens[:, n_prefix:, :] # [1, P, D]
96
+ P = patch_tokens.shape[1]
97
+ h = w = int(round(P ** 0.5))
98
+ pt = patch_tokens.squeeze(0) # [P, D]
99
+ logits_patch = core.head(pt) # [P, 999]
100
+ top_idx = int(logits_patch.mean(0).argmax())
101
+ heat = logits_patch[:, top_idx]
102
+ heat = heat - heat.min()
103
+ heat = heat / (heat.max() - heat.min() + 1e-8)
104
+ heat_np = heat.float().cpu().numpy().reshape(h, w)
105
+ heat_img = Image.fromarray((heat_np * 255).astype(np.uint8)).resize((img_size, img_size), Image.BILINEAR)
106
+ heat_arr = np.array(heat_img).astype(np.float32) / 255.0
107
+ return heat_arr, top_idx, heat_np
108
+ except Exception as e:
109
+ log(f"heatmap failed {e}")
110
+ return None, None, None
111
+
112
  def load_species_map(manifest=MANIFEST):
113
  # Public release: species_labels.json (999 WA species) - preferred
114
  if SPECIES_JSON.exists():
 
474
  pd.DataFrame(meta["topk"]).to_csv(args.output, index=False)
475
  print(f"wrote {args.output}")
476
  if args.vis:
477
+ # visualize topk bar + image + feature heatmap (where ViT looks)
478
  try:
479
  import matplotlib
480
  matplotlib.use("Agg")
481
  import matplotlib.pyplot as plt
482
  n=len(image_paths)
483
+ # --- generate heatmap for first image (where feature extractor finds features) ---
484
+ heat_arr = None
485
+ try:
486
+ vm, _, _ = load_visual_model(device=device)
487
+ heat_arr, hm_idx, _ = get_vit_heatmap(vm, image_paths[0], device=device, img_size=args.size)
488
+ except Exception as e:
489
+ log(f"heatmap gen failed {e}")
490
+ heat_arr = None
491
+ # 1x3 layout: image | heatmap overlay | bar (if heatmap available, else 1x2)
492
+ if heat_arr is not None:
493
+ fig, axes = plt.subplots(1, 3, figsize=(18,5), gridspec_kw={"width_ratios":[1,1,1.2]})
494
+ ax_img, ax_heat, ax_bar = axes
495
+ # original image
496
+ im_raw=open_image_pil(image_paths[0]).resize((args.size,args.size))
497
+ ax_img.imshow(im_raw)
498
+ title=f"{meta['topk'][0]['species']}\n{meta['topk'][0]['prob']:.1%} conf {meta['confidence']:.1%} {dt*1000:.0f}ms"
499
+ if meta["is_unknown"]:
500
+ title="UNKNOWN\n"+title
501
+ if len(image_paths)>1: title+=f" {n}-view"
502
+ ax_img.set_title(title, fontsize=9, color="red" if meta["is_unknown"] else "black")
503
+ ax_img.axis("off")
504
+ # heatmap overlay (jet)
505
+ ax_heat.imshow(im_raw)
506
+ ax_heat.imshow(heat_arr, cmap="jet", alpha=0.55, vmin=0, vmax=1)
507
+ ax_heat.set_title(f"Feature heatmap\nTop patch {hm_idx} {heat_arr.max():.2f}", fontsize=9, color="red" if meta["is_unknown"] else "black")
508
+ ax_heat.axis("off")
509
+ else:
510
+ fig, axes = plt.subplots(1, 2, figsize=(14,5), gridspec_kw={"width_ratios":[1,1.2]})
511
+ ax_img, ax_bar = axes
512
+ im=open_image_pil(image_paths[0]).resize((336,336))
513
+ ax_img.imshow(im)
514
+ title=f"{meta['topk'][0]['species']}\n{meta['topk'][0]['prob']:.1%} conf {meta['confidence']:.1%} {dt*1000:.0f}ms"
515
+ if meta["is_unknown"]:
516
+ title="UNKNOWN\n"+title
517
+ if len(image_paths)>1: title+=f" {n}-view"
518
+ ax_img.set_title(title, fontsize=9, color="red" if meta["is_unknown"] else "black")
519
+ ax_img.axis("off")
520
+ # heat axis not present, will reuse ax_bar below
521
+ # bar chart (common)
522
+ if heat_arr is not None:
523
+ probs_bar=[r["prob"] for r in meta["topk"]]
524
+ species=[r["species"] for r in meta["topk"]]
525
+ else:
526
+ probs_bar=[r["prob"] for r in meta["topk"]]
527
+ species=[r["species"] for r in meta["topk"]]
528
+ # ax_bar is defined in both branches (for heat case it's third axis)
529
  colors=["red" if meta["is_unknown"] and i==0 else "green" if i==0 else "steelblue" for i in range(len(probs_bar))]
530
  bars=ax_bar.barh(range(len(probs_bar))[::-1], probs_bar[::-1], color=colors[::-1])
531
  ax_bar.set_yticks(range(len(probs_bar))[::-1])
 
534
  ax_bar.set_title(f"WA 999 spp Top-{args.topk} {'UNKNOWN' if meta['is_unknown'] else ''}", fontsize=10, color="red" if meta["is_unknown"] else "black")
535
  for p, bar in zip(probs_bar[::-1], bars):
536
  ax_bar.text(p+0.01, bar.get_y()+bar.get_height()/2, f"{p:.1%}", va="center", fontsize=7)
 
537
  if meta["is_unknown"]:
538
  ax_bar.text(0.5, -1, f"sim {meta['unknown_info']['sim_max']:.2f} < {meta['thr_cos']:.2f}", ha="center", fontsize=8, color="red", transform=ax_bar.transAxes)
539
  plt.tight_layout()
540
  plt.savefig(args.vis, dpi=150)
541
+ print(f"wrote {args.vis} {'with heatmap' if heat_arr is not None else ''}")
542
  except Exception as e:
543
+ import traceback; traceback.print_exc()
544
  log(f"vis failed {e}")
545
 
546
  def cmd_batch(args):