์
์งธ ๋ถ๋ฅ none(๋น ์คยท๊ฐ๋ฆผ)์ ๋ํ 3๋ถ๋ฅ ํ์ ๊ธฐ. judge()๊ฐ kind 'none'์ ๋ธ๋ค (H200, 2026-10-04)
1563787 verified Download leaf_kind.py from leapshared/leaf_kind_dinov2_logreg: direct link, hf CLI and curl.
- Browser
- Download file 3.66 kB
-
https://huggingface.co/leapshared/leaf_kind_dinov2_logreg/resolve/main/leaf_kind.py
- Command line
-
hf download hf://leapshared/leaf_kind_dinov2_logreg/leaf_kind.py
-
curl -L -o leaf_kind.py https://huggingface.co/leapshared/leaf_kind_dinov2_logreg/resolve/main/leaf_kind.py
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} | |