""" 세그멘테이션 결과 뷰어 생성 — 라벨별 on/off, 줌/팬. sam3_multi_prompt.py 가 만든 JSON과 원본 이미지를 받아 단독 HTML을 만든다. 라벨은 프롬프트 파일의 "# [그룹명]" 주석 기준으로 묶어서 나열한다. 사용법: python tools/make_viewer.py \ --json "data/everyimage/output/0857_multi.json" \ --image "data/everyimage/DJI_20250805162831_0857.JPG" \ --prompts prompts/discovery_v1.txt 출력: /<이름>_viewer.html + <이름>_view.jpg """ import argparse import json from collections import Counter, OrderedDict from pathlib import Path import cv2 import numpy as np def parse_groups(path: Path) -> "OrderedDict[str, list]": """프롬프트 파일의 '# [그룹명]' 주석으로 라벨을 묶는다.""" groups, current = OrderedDict(), "기타" for line in path.read_text(encoding="utf-8").splitlines(): s = line.strip() if s.startswith("#"): body = s.lstrip("#").strip() if body.startswith("[") and body.endswith("]"): current = body[1:-1].strip() groups.setdefault(current, []) elif s: groups.setdefault(current, []).append(s) return groups HTML = """ __TITLE__

__TITLE__

segment __TOTAL__개 · 라벨 __NLAB__종
""" def color_for(label: str) -> str: """라벨 문자열 해시 → 고정 색상.""" h = 0 for ch in label: h = (h * 131 + ord(ch)) & 0xFFFFFFFF hsv = np.uint8([[[(h % 360) // 2, # OpenCV 색상은 0~179 190 + (h >> 9) % 60, 200 + (h >> 17) % 55]]]) r, g, b = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)[0][0] return "#%02x%02x%02x" % (int(r), int(g), int(b)) def main(): ap = argparse.ArgumentParser() ap.add_argument("--json", required=True) ap.add_argument("--image", required=True) ap.add_argument("--prompts", default="prompts/discovery_v1.txt") ap.add_argument("--output", default=None, help="기본: _viewer.html") ap.add_argument("--max-size", type=int, default=3000, help="뷰어용 이미지 최대 변 길이") args = ap.parse_args() jpath = Path(args.json) data = json.loads(jpath.read_text(encoding="utf-8")) segs = data.get("segments", []) img = cv2.imdecode(np.fromfile(args.image, dtype=np.uint8), cv2.IMREAD_COLOR) if img is None: print(f"이미지 로드 실패: {args.image}") return H, W = img.shape[:2] k = min(1.0, args.max_size / max(H, W)) if k < 1.0: img = cv2.resize(img, (int(W * k), int(H * k)), interpolation=cv2.INTER_AREA) out_html = Path(args.output) if args.output else jpath.with_name(jpath.stem + "_viewer.html") img_name = jpath.stem + "_view.jpg" cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 88])[1].tofile( str(out_html.with_name(img_name))) counts = Counter(s.get("label", "") for s in segs) groups_src = parse_groups(Path(args.prompts)) seen, groups = set(), [] for name, labels in groups_src.items(): rows = [{"label": lb, "color": color_for(lb), "count": counts.get(lb, 0)} for lb in labels if counts.get(lb, 0) > 0] seen.update(r["label"] for r in rows) if rows: groups.append({"name": name, "labels": rows}) extra = [{"label": lb, "color": color_for(lb), "count": c} for lb, c in counts.most_common() if lb not in seen and lb] if extra: groups.append({"name": "그룹 없음", "labels": extra}) payload = { "groups": groups, "colorOf": {lb: color_for(lb) for lb in counts if lb}, "segs": [{"l": s.get("label", ""), "p": [[round(p[0] * k, 1), round(p[1] * k, 1)] for p in s["points"]]} for s in segs if s.get("points")], } html = (HTML.replace("__TITLE__", jpath.stem) .replace("__TOTAL__", str(len(segs))) .replace("__NLAB__", str(len([c for c in counts if c]))) .replace("__IMG__", img_name) .replace("__DATA__", json.dumps(payload, ensure_ascii=False))) out_html.write_text(html, encoding="utf-8") print(f"뷰어: {out_html}") print(f"이미지: {out_html.with_name(img_name)} ({img.shape[1]}×{img.shape[0]})") print(f"segment {len(segs)}개 · 라벨 {len([c for c in counts if c])}종 · 그룹 {len(groups)}개") if __name__ == "__main__": main()