#!/usr/bin/env python """SAM 검출 원본(_multi.json)을 프롬프트별로 집계한다. 병합 전 상태를 본다. 집합(병합 그룹)을 다시 짜려면 먼저 각 프롬프트가 실제로 무엇을 얼마나 집어냈는지 봐야 한다. 이 도구는 판정하지 않는다 — 센다. 프롬프트 파일의 `# [카테고리]` 머리글로 프롬프트를 묶고, 병합 그룹 파일에서 그 프롬프트가 지금 어느 집합에 들어가는지 붙인다. 어느 집합에도 없으면 '없음'으로 나온다 — 그 검출은 지금 버려지고 있다는 뜻이다. 면적은 다각형 넓이(shoelace)다. 겹침을 빼지 않으므로 합이 화면보다 클 수 있다. """ import argparse import glob import json import os from collections import OrderedDict, defaultdict import numpy as np def load_categories(path): """프롬프트 파일 -> {프롬프트: 카테고리}, 그리고 비활성(주석) 프롬프트 집합.""" cat, off, cur = OrderedDict(), OrderedDict(), None for line in open(path, encoding="utf-8"): s = line.strip() if not s: continue if s.startswith("#"): body = s.lstrip("#").strip() if body.startswith("[") and body.endswith("]"): cur = body[1:-1].strip() continue # '# white roof 비활성 — 이유' 형태의 꺼둔 프롬프트. # 한글이 섞였으면 프롬프트가 아니라 설명문이다. name = body.split("비활성")[0].strip().rstrip("—").strip() if (cur and name and name[0].islower() and len(name.split()) <= 5 and not any("가" <= ch <= "힣" for ch in name)): off[name] = cur continue if cur: cat[s] = cur return cat, off def load_groups(path): """병합 그룹 파일 -> {라벨: 대표}. 주석 처리된 그룹은 비활성이므로 뺀다.""" mapping, cur = OrderedDict(), None for line in open(path, encoding="utf-8"): s = line.strip() if not s: continue if s.startswith("#"): body = s.lstrip("#").strip() cur = body[1:-1].strip() if body.startswith("[") and body.endswith("]") else None continue if cur: mapping[s] = cur return mapping def poly_area(pts): a = np.asarray(pts, dtype=np.float64).reshape(-1, 2) if len(a) < 3: return 0.0 x, y = a[:, 0], a[:, 1] return float(abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) / 2) def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--run-dir", required=True, help="검출 JSON 폴더") ap.add_argument("--pattern", default="*_multi.json", help="병합 후를 보려면 '*_multi_merged.json'") ap.add_argument("--prompts", required=True, help="프롬프트 목록 txt") ap.add_argument("--groups", required=True, help="병합 그룹 txt") ap.add_argument("--image-size", help="'W,H' — 주면 화면 대비 면적 비율을 낸다") ap.add_argument("--out-json", help="집계 결과 저장 경로") args = ap.parse_args() cat, off = load_categories(args.prompts) grp = load_groups(args.groups) reps = set(grp.values()) files = sorted(glob.glob(os.path.join(args.run_dir, args.pattern))) if not files: raise FileNotFoundError(f"{args.pattern} 없음: {args.run_dir}") npoly = defaultdict(int) nphoto = defaultdict(int) area = defaultdict(float) scores = defaultdict(list) areas = defaultdict(list) for p in files: with open(p, encoding="utf-8") as fh: segs = json.load(fh)["segments"] here = set() for s in segs: lb = s["label"] a = poly_area(s["points"]) npoly[lb] += 1 area[lb] += a areas[lb].append(a) scores[lb].append(float(s.get("score", 0))) here.add(lb) for lb in here: nphoto[lb] += 1 img_area = None if args.image_size: w, h = (float(x) for x in args.image_size.split(",")) img_area = w * h * len(files) total_poly = sum(npoly.values()) total_area = sum(area.values()) print(f"사진 {len(files)} · 폴리곤 {total_poly:,} · 라벨 {len(npoly)}종") if img_area: print(f"검출 면적 합 {total_area:,.0f} px = 전체 화면의 {100 * total_area / img_area:.1f}% " f"(겹침 미보정)") seen = set(npoly) rows = [] order = list(dict.fromkeys(list(cat) + sorted(seen - set(cat)))) cur_cat = None print(f"\n{'프롬프트':<28}{'집합':<17}{'폴리곤':>8}{'사진':>7}" f"{'면적%':>8}{'중앙면적px':>11}{'점수중앙':>9}") for lb in order: c = cat.get(lb) or ("(병합 결과)" if lb in reps else "(프롬프트 파일 밖)") if c != cur_cat: print(f"-- [{c}]") cur_cat = c n = npoly.get(lb, 0) # 병합 후 파일은 라벨이 곧 집합 대표 이름이다. g = lb if lb in reps else grp.get(lb, "없음") row = { "prompt": lb, "category": c, "group": g, "polygons": n, "photos": nphoto.get(lb, 0), "area_px": area.get(lb, 0.0), "area_share": (area.get(lb, 0.0) / total_area) if total_area else 0.0, "area_median_px": float(np.median(areas[lb])) if n else 0.0, "score_median": float(np.median(scores[lb])) if n else 0.0, } rows.append(row) if n == 0: print(f"{lb:<28}{g:<17}{'0':>8}{'-':>7}{'-':>8}{'-':>11}{'-':>9}") continue print(f"{lb:<28}{g:<17}{n:>8,}{row['photos']:>7}" f"{100 * row['area_share']:>7.2f}%{row['area_median_px']:>11,.0f}" f"{row['score_median']:>9.3f}") if off: print(f"\n비활성 프롬프트 {len(off)}개: {', '.join(off)}") nogroup = [r for r in rows if r["group"] == "없음" and r["polygons"]] if nogroup: s = sum(r["polygons"] for r in nogroup) print(f"\n집합 없는 검출 {s:,} 폴리곤 " f"({100 * s / total_poly:.1f}%) — 지금 버려진다") for r in sorted(nogroup, key=lambda r: -r["polygons"]): print(f" {r['prompt']:<28}{r['polygons']:>8,}") print(f"\n집합별 합계") gsum = defaultdict(lambda: [0, 0.0, 0]) for r in rows: v = gsum[r["group"]] v[0] += r["polygons"] v[1] += r["area_px"] v[2] += 1 if r["polygons"] else 0 print(f"{'집합':<20}{'프롬프트(검출>0)':>16}{'폴리곤':>10}{'면적%':>9}") for g, (n, a, k) in sorted(gsum.items(), key=lambda t: -t[1][0]): print(f"{g:<20}{k:>16}{n:>10,}{100 * a / total_area if total_area else 0:>8.2f}%") if args.out_json: with open(args.out_json, "w", encoding="utf-8") as fh: json.dump({ "run_dir": os.path.abspath(args.run_dir), "prompts_file": os.path.abspath(args.prompts), "groups_file": os.path.abspath(args.groups), "photos": len(files), "total_polygons": total_poly, "total_area_px": total_area, "image_area_px": img_area, "rows": rows, }, fh, ensure_ascii=False, indent=2) print(f"\n저장: {args.out_json}") if __name__ == "__main__": main()