File size: 3,657 Bytes
1563787 06a7680 1563787 06a7680 1563787 06a7680 1563787 06a7680 1563787 06a7680 1563787 06a7680 1563787 06a7680 1563787 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | """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}
|