leaf_kind_dinov2_logreg / leaf_kind.py
leapshared's picture
์…‹์งธ ๋ถ€๋ฅ˜ none(๋นˆ ์ค„ยท๊ฐ€๋ฆผ)์„ ๋”ํ•œ 3๋ถ€๋ฅ˜ ํŒ์ •๊ธฐ. judge()๊ฐ€ kind 'none'์„ ๋‚ธ๋‹ค (H200, 2026-10-04)
1563787 verified
Raw History Blame Contribute Delete
3.66 kB
"""OpenArm SciEdu ๋‚˜๋ญ‡์žŽ ์นด๋“œ ํŒ์ •๊ธฐ: ๋ณต์—ฝ(compound) / ๋‹จ์—ฝ(simple) / ์—†์Œ(none: ๋นˆ ์ค„์ด๊ฑฐ๋‚˜ ์นด๋“œ๊ฐ€ ๊ฐ€๋ ค์ง).
DINOv2-base ํŠน์ง• + 3๋ถ€๋ฅ˜ ์†Œํ”„ํŠธ๋งฅ์Šค ํšŒ๊ท€. CPU๋กœ ๋ˆ๋‹ค.
from leaf_kind import LeafKind
clf = LeafKind() # classifier.json๊ณผ facebook/dinov2-base๋ฅผ ๋ฐ›๋Š”๋‹ค
r = clf.judge([frame1, frame2, frame3]) # ๊ฐ™์€ ๋•Œ ์ฐ์€ d455f 640x480 RGB uint8 ํ”„๋ ˆ์ž„ ๋ช‡ ์žฅ
# r["kind"]: "compound" | "simple" โ†’ ์˜ฎ๊ธด๋‹ค
# "none" โ†’ ์นด๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๊ฐ€๋ ค์กŒ๋‹ค. ํŒ์ •ํ•˜์ง€ ์•Š๊ณ  1์ดˆ ๋’ค ๋‹ค์‹œ ๋ณธ๋‹ค
# None โ†’ ์นด๋“œ๋Š” ์žˆ๋Š”๋ฐ ํ™•์‹ ์ด ๋‚ฎ๋‹ค(ํ”„๋ ˆ์ž„ ๋‹ต์ด ๊ฐˆ๋ฆฌ๊ฑฐ๋‚˜ ํ‰๊ท  ํ™•์‹  < ๋ฌธํ„ฑ). ์„ ์ƒ๋‹˜์—๊ฒŒ ๋ฌป๋Š”๋‹ค
- ์‚ฌ์ง„์€ ์นด๋“œ๊ฐ€ ์ค„ ์œ„์— ํ˜ผ์ž ์„œ ์žˆ๊ณ  ๋‘ ํŒ”์ด ์‰ฌ๊ณ  ์žˆ์„ ๋•Œ ์ฐ๋Š”๋‹ค. ํ•™์Šตํ•œ ์นด๋“œ ์‚ฌ์ง„์€ ์นด๋“œ๋ฅผ ์˜ฎ๊ธด ํŒ”์ด ์ถœ๋ฐœํ•˜๊ธฐ 5ํ”„๋ ˆ์ž„ ์ „์ด๋‹ค.
- ํ•œ ํ”„๋ ˆ์ž„์ด๋ผ๋„ none์ด๋ฉด kind๋Š” "none"์ด๋‹ค. ์—†๋Š” ์นด๋“œ๋ฅผ ์ง‘์œผ๋Ÿฌ ๊ฐ€์ง€ ์•Š๋Š” ์ชฝ์œผ๋กœ ์ •ํ–ˆ๋‹ค.
H200 ์„ธ์…˜(leap-06)์ด 2026-10-04์— ๋งŒ๋“ค์—ˆ๋‹ค. ๊ฒ€์ฆ๊ณผ ํ•™์Šต ์ž๋ฃŒ๋Š” README.md์— ์žˆ๋‹ค.
"""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import torch
from transformers import AutoImageProcessor, AutoModel
HERE = Path(__file__).resolve().parent
class LeafKind:
def __init__(self, classifier: str | Path = HERE / "classifier.json", device: str = "cpu", threshold: float = 0.9):
cfg = json.loads(Path(classifier).read_text(encoding="utf-8"))
self.classes = cfg["classes"] # ["compound", "simple", "none"]
self.crop = cfg["crop_xyxy"]
self.mean, self.std = np.asarray(cfg["mean"]), np.asarray(cfg["std"])
self.W, self.b = np.asarray(cfg["W"]), np.asarray(cfg["b"])
self.threshold = threshold
self.device = device
self.processor = AutoImageProcessor.from_pretrained(cfg["feature_model"])
self.model = AutoModel.from_pretrained(cfg["feature_model"]).to(device).eval()
def features(self, frames: list[np.ndarray]) -> np.ndarray:
"""d455f ํ”„๋ ˆ์ž„(H x W x 3, RGB uint8, 640x480)๋“ค์„ ์ž˜๋ผ DINOv2 CLS(pooler_output)๋ฅผ ๋‚ธ๋‹ค."""
x0, y0, x1, y1 = self.crop
crops = [np.ascontiguousarray(f[y0:y1, x0:x1]) for f in frames]
with torch.no_grad():
inputs = self.processor(images=crops, return_tensors="pt").to(self.device)
return self.model(**inputs).pooler_output.float().cpu().numpy()
def proba(self, frames: list[np.ndarray]) -> np.ndarray:
"""ํ”„๋ ˆ์ž„๋งˆ๋‹ค [compound, simple, none] ํ™•๋ฅ ."""
z = ((self.features(frames) - self.mean) / self.std) @ self.W.T + self.b
z = z - z.max(axis=1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=1, keepdims=True)
def judge(self, frames: list[np.ndarray]) -> dict:
p = self.proba(frames)
votes = [self.classes[i] for i in p.argmax(axis=1)]
mean = p.mean(axis=0)
out = {"probs": dict(zip(self.classes, mean.round(4).tolist())), "votes": votes}
if "none" in votes:
return {"kind": "none", "confident": True, "p_compound": None, **out}
# ์นด๋“œ๊ฐ€ ์žˆ๋‹ค: ๋ณต์—ฝ/๋‹จ์—ฝ ๋‘˜ ์‚ฌ์ด์—์„œ๋งŒ ๋‹ค์‹œ ๋‚˜๋ˆˆ๋‹ค
pc = float((p[:, 0] / (p[:, 0] + p[:, 1])).mean())
agree = len(set(votes)) == 1
confident = agree and max(pc, 1 - pc) >= self.threshold
kind = ("compound" if pc > 0.5 else "simple") if confident else None
return {"kind": kind, "confident": confident, "p_compound": pc, **out}