Image Classification
LiteRT
LiteRT
ONNX
English
vision
botany
western-australia
dinov3
mixture-of-experts
adaround
fp8
int8
android
biodiversity
flora
Instructions to use thenukegun10x/PLantDetect-WA with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use thenukegun10x/PLantDetect-WA with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
feat: vis heatmap in --vis (no new arg)
Browse filesAdd 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.
- 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 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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):
|