diff --git a/configs/merge_groups_v4.txt b/configs/merge_groups_v4.txt index aad5e71..9a429db 100644 --- a/configs/merge_groups_v4.txt +++ b/configs/merge_groups_v4.txt @@ -11,13 +11,17 @@ # 그래서 tree canopy와 dense forest, building과 residential house는 # 일부러 같은 그룹에 둔다 — 2D에서 갈라봐야 중복 마스크만 나온다. +# factory structure 는 일부러 어느 집합에도 넣지 않는다 (2026-09-02). +# merge_labels.py 는 집합 밖 라벨을 그대로 통과시키므로 독립 클래스로 남는다. +# [building] 에 두면 공장 구조물이 일반 건물에 흡수돼 병합 후 되돌릴 수 없다. +# 16장 실측 86개. + # [building] building building rooftop building facade residential house warehouse -factory structure storage container blue roof red roof diff --git a/configs/sam_merge_status.json b/configs/sam_merge_status.json index 018ca14..395b71f 100644 --- a/configs/sam_merge_status.json +++ b/configs/sam_merge_status.json @@ -6,11 +6,12 @@ "epsilon_px": 1.5, "기준 런": "output/sam3/tune_t64 (확정 설정 8장) / 기준선은 output/sam3/YYX_run 69장", "프롬프트 파일": "prompts/discovery_v4.txt (94개). 기준선은 discovery_v1.txt (85개)", - "집합 정의": "configs/merge_groups_v4.txt (12집합). 기준선은 merge_groups.txt", + "집합 정의": "configs/merge_groups_v4.txt (11집합 + 집합 밖 3종: factory structure, 콘 2종)", "타일": "6×4 = 24개, overlap 10%, 타일 크기 1365×1365. 기준선은 4×3 (2048×1820)", "검출 설정": "conf 0.10 · NMS iou 0.40 · 같은 라벨 병합 gap 8px · --wide-in-tiles. 기준선 conf 0.25, 통짜 분리", "공정 보기 사진": "확정 설정 8장 (기준선 무라벨 3.5~52.1% 분포에서 고름)", - "확정 후보 설정": "prompts/discovery_v4.txt (94개) · configs/merge_groups_v4.txt · --wide-in-tiles · --cols 6 --rows 4 · --conf 0.10 · NMS 0.40 · --merge (gap 8px) · 병합 gap 2px" + "확정 후보 설정": "prompts/discovery_v4.txt (94개) · configs/merge_groups_v4.txt · --wide-in-tiles · --cols 6 --rows 4 · --conf 0.10 · NMS 0.40 · --merge (gap 8px) · 병합 gap 2px", + "claim 규칙": "configs/claim_rules.txt — vehicle : 0.5 : 지붕 11종 + 방수포/보호시트" }, "steps": [ { @@ -81,9 +82,21 @@ }, { "id": "M3", - "name": "gap 값 조정", - "state": "todo", - "detail": "외곽선 인접 판정 1~3 px. 파일럿 기준 2 px 로 시작." + "name": "gap 값 조정 — 2px 유지", + "state": "done", + "detail": "사진 4장(폴리곤 3,280)으로 gap 0~32px 스윕. 꺾이는 지점이 없는 매끄러운 곡선이다. 폴리곤 0px 2,081 · 2px 2,032 · 3px 2,014 · 8px 1,954 · 16px 1,801 · 32px 1,574. 무라벨은 4.60% → 3.75% 로 거의 안 움직인다. 1~3px 구간 차이는 폴리곤 2.4%, 무라벨 0.13pp 로 노이즈 수준이라 2px 를 유지한다. 과다 병합은 16px 부터 보인다(vehicle 240→229, 32px 에서 209). 콘은 gap 32px 에서도 65개 그대로 — 집합 밖이라 병합을 안 탄다. 소요는 사진당 약 5초(gap 2px)." + }, + { + "id": "M4", + "name": "단계 비교 뷰어", + "state": "done", + "detail": "tools/make_stage_viewer.py 신규. 같은 사진에서 검출 → claim → 병합을 갈아끼우고 바뀐 폴리곤만 빨갛게 강조한다. claim 은 563개 재배정 (white roof→white vehicle 194 등 지붕 오검을 차량으로), 병합은 1,033개가 둘 이상 합쳐진 것. 폴리곤 11,169 → 11,169 → 6,370." + }, + { + "id": "M5", + "name": "factory structure 를 [building] 에서 분리", + "state": "done", + "detail": "사용자 지적. 공장 구조물이 일반 건물에 흡수되면 병합 후 되돌릴 수 없다. 집합 밖으로 빼면 passthrough 로 독립 클래스가 된다. 16장 실측 24개. 부작용으로 building 이 466 → 490 으로 늘었다 — 전에는 factory structure 폴리곤이 건물 사이 다리 노릇을 해 여러 건물을 한 덩어리로 잇고 있었다. 라벨 14 → 15종, 폴리곤 6,370 → 6,418." } ], "issues": [ @@ -101,11 +114,7 @@ }, { "state": "open", - "text": "orange traffic cone / yellow safety cone 은 어느 집합에도 없어 검출 52 폴리곤이 버려진다. [cone] 그룹이 주석 처리돼 있음 — 나란히 놓인 콘이 한 덩어리로 융합된다는 이유." - }, - { - "state": "open", - "text": "검출 0 인 프롬프트 8개 (plowed field, rice paddy, vinyl greenhouse tunnel, gabion wall, road embankment slope, riprap slope, water surface, reservoir) — 유지할지 뺄지 미정." + "text": "확정 설정 16장에서 검출 0 인 프롬프트 12개: residential house, plowed field, commercial bus, plastic greenhouse, vinyl greenhouse tunnel, greenhouse roof, green wire fence, road embankment slope, riprap slope, bridge, water surface, yellow grass field. 16장은 69장의 23% 라 대상이 없었을 뿐일 수 있다 (bridge 는 다리 있는 사진에서만 나온다). 지워도 94→82개라 배치 청크가 6으로 그대로여서 속도 이득이 없다 — 78개 이하로 줄여야 5청크가 된다. 69장 전체를 돌린 뒤 판단할 것." }, { "state": "rejected", diff --git a/docs/pilot/sam_merge.html b/docs/pilot/sam_merge.html index 3aa0ecd..52e6bde 100644 --- a/docs/pilot/sam_merge.html +++ b/docs/pilot/sam_merge.html @@ -55,11 +55,11 @@ border-radius:6px;padding:5px 8px;font:inherit;font-size:13px}

SAM 3.1 후처리 — 검출 커버리지 · 집합 병합

1단계는 미검출을 없애는 것이다. 지표는 '무라벨 화면%' — 마스크 합집합으로 재서 겹침을 뺀 값. 목표 5% 이하, 8장 평균 3.6% 로 달성. 2단계는 집합으로 묶고 외곽선이 gap px 이내로 인접한 폴리곤을 합치는 것.

-

생성 2026-09-02 09:19 · 집계 tune_t64

+

생성 2026-09-03 13:19 · 집계 tune_t64

실측

사진
16
폴리곤
11,169
-
병합 후 폴리곤
6,370 (-43%)
+
병합 후 폴리곤
6,418 (-43%)
라벨 종류
82 / 94
집합
12
검출 면적 / 화면
184.7%
@@ -92,16 +92,16 @@ border-radius:6px;padding:5px 8px;font:inherit;font-size:13px} DJI_20250805164753_04237620.5%

공정 보기 — 같은 사진, 단계별

-
SAM 검출 11,169 폴리곤(82 라벨)→ 집합 병합 →6,370 폴리곤(14 집합)· 사진 16장 기준
+
SAM 검출 11,169 폴리곤(82 라벨)→ 집합 병합 →6,418 폴리곤(15 집합)· 사진 16장 기준
- - - + + +
-
- - -

왼쪽 목록에서 라벨을 켜고 끈다. 휠 확대, 드래그 이동. 새 창: 1. SAM 검출 — 확정 설정 (16장) · 2. 집합 병합 후 (gap 2px) · 참고: 기준선 검출 (conf 0.25, 4×3)

+
+ + +

왼쪽 목록에서 라벨을 켜고 끈다. 휠 확대, 드래그 이동. 새 창: 단계 비교 (검출 → claim → 병합) · 검출만 보기 (라벨 82종) · 병합만 보기

단계

M1완료
프롬프트별 검출 집계
tools/prompt_stats.py. YYX_run 실측: 폴리곤 13,580 · 라벨 77종.
M1b완료
공정 보기(검출 → 병합) 뷰어 연결
tools/make_viewer.py 로 같은 사진 3장을 단계별로 굽고 모니터에 붙였다. docs/pilot/theater/.
@@ -114,53 +114,54 @@ border-radius:6px;padding:5px 8px;font:inherit;font-size:13px}
M1i완료
타일 4×3 → 6×4 (목표 달성)
conf 0.10 · v4 프롬프트 · --wide-in-tiles 와 함께. 4장 실측 무라벨: 0654 5.0% (기준선 52.1) · 0484 3.0% (38.7) · 0079 4.4% (27.1) · 0006 5.0% (19.4). 평균 34.3% → 4.35%. 비용 466~616초/장, 평균 523초 = 기준선 140초의 3.7배. 69장 재검출 시 약 10시간.
M1j완료
확정 설정 16장 검증
16장 실측 무라벨(conf 0.10): 최소 0.5%(0423) ~ 최대 6.4%(0581), 평균 3.44%. 15/16 장이 5% 이하. 같은 16장 기준선 평균 19.9%. 소요 466~907초/장(평균 585초). 예상이 틀린 곳: 물 많은 사진이 위험할 것으로 봤으나 0423 이 0.5% 로 최저였다 — water surface/reservoir 검출이 0이어도 다른 프롬프트가 그 면적을 덮는다.
M2막힘
집합 정의 조정 — 보류
교통시설물을 가르는 안(configs/merge_groups_v5.txt)을 만들고 16장으로 확인했다: structure 1,079 → utility pole 608 / streetlight pole 372 / road sign 358 / culvert 9, fence 702 → guardrail 668 / fence 46 / roadside barrier 17. 라벨 14 → 19종. 사용자 지시로 보류하고 v4 로 되돌렸다. 세부 세그멘테이션은 후순위.
-
M3대기
gap 값 조정
외곽선 인접 판정 1~3 px. 파일럿 기준 2 px 로 시작.
+
M3완료
gap 값 조정 — 2px 유지
사진 4장(폴리곤 3,280)으로 gap 0~32px 스윕. 꺾이는 지점이 없는 매끄러운 곡선이다. 폴리곤 0px 2,081 · 2px 2,032 · 3px 2,014 · 8px 1,954 · 16px 1,801 · 32px 1,574. 무라벨은 4.60% → 3.75% 로 거의 안 움직인다. 1~3px 구간 차이는 폴리곤 2.4%, 무라벨 0.13pp 로 노이즈 수준이라 2px 를 유지한다. 과다 병합은 16px 부터 보인다(vehicle 240→229, 32px 에서 209). 콘은 gap 32px 에서도 65개 그대로 — 집합 밖이라 병합을 안 탄다. 소요는 사진당 약 5초(gap 2px).
+
M4완료
단계 비교 뷰어
tools/make_stage_viewer.py 신규. 같은 사진에서 검출 → claim → 병합을 갈아끼우고 바뀐 폴리곤만 빨갛게 강조한다. claim 은 563개 재배정 (white roof→white vehicle 194 등 지붕 오검을 차량으로), 병합은 1,033개가 둘 이상 합쳐진 것. 폴리곤 11,169 → 11,169 → 6,370.
+
M5완료
factory structure 를 [building] 에서 분리
사용자 지적. 공장 구조물이 일반 건물에 흡수되면 병합 후 되돌릴 수 없다. 집합 밖으로 빼면 passthrough 로 독립 클래스가 된다. 16장 실측 24개. 부작용으로 building 이 466 → 490 으로 늘었다 — 전에는 factory structure 폴리곤이 건물 사이 다리 노릇을 해 여러 건물을 한 덩어리로 잇고 있었다. 라벨 14 → 15종, 폴리곤 6,370 → 6,418.

이슈 · 기각된 시도

해결
'집합 없는 검출은 버려진다'는 앞선 판단은 틀렸다. merge_labels.py 의 merge_by_group 은 집합에 없는 라벨을 passthrough 로 그대로 통과시킨다. 16장 병합 결과에 orange traffic cone 114 · yellow safety cone 6 이 독립 클래스로 살아 있다. 콘을 [cone] 으로 묶으면 오히려 나란한 콘이 gap 2px 에서 한 덩어리가 되어 개수를 잃는다 — 지금처럼 집합 밖에 두는 게 맞다.
해결
통짜(1×1) 패스는 대상 하나에 인스턴스가 하나만 남는다 — 도로 한쪽 차로, 논 한 필지만 잡힘. --wide-in-tiles 로 해결(측정치는 M1e).
미해결
0581 무라벨 6.4% — 16장 중 유일한 5% 초과. 원인 확인함: 큰 구멍이 없다. 구멍 1,554개 중 최대가 0.83%, 상위 6개 합이 2.15% 뿐이고 나머지 4.25%가 작은 조각 1,548개로 흩어져 있다. 자재 야적장·건물 밀집 구역이라 폴리곤이 1,079개 나왔는데도 객체 사이 얇은 틈이 누적된 것. 0654 유형(큰 필지 통째 누락)과 다르다. 프롬프트로는 못 줄인다 — 후처리(폴리곤 팽창·구멍 메우기) 영역.
-
미해결
orange traffic cone / yellow safety cone 은 어느 집합에도 없어 검출 52 폴리곤이 버려진다. [cone] 그룹이 주석 처리돼 있음 — 나란히 놓인 콘이 한 덩어리로 융합된다는 이유.
-
미해결
검출 0 인 프롬프트 8개 (plowed field, rice paddy, vinyl greenhouse tunnel, gabion wall, road embankment slope, riprap slope, water surface, reservoir) — 유지할지 뺄지 미정.
+
미해결
확정 설정 16장에서 검출 0 인 프롬프트 12개: residential house, plowed field, commercial bus, plastic greenhouse, vinyl greenhouse tunnel, greenhouse roof, green wire fence, road embankment slope, riprap slope, bridge, water surface, yellow grass field. 16장은 69장의 23% 라 대상이 없었을 뿐일 수 있다 (bridge 는 다리 있는 사진에서만 나온다). 지워도 94→82개라 배치 청크가 6으로 그대로여서 속도 이득이 없다 — 78개 이하로 줄여야 5청크가 된다. 69장 전체를 돌린 뒤 판단할 것.
기각
프롬프트 색 특정성 A/B (car/sedan/suv/van/pickup truck) — 폴리곤 +26% 인데 마스크 면적은 +0.002~0.068%p 뿐, 신규 마스크의 90~99%가 기존과 겹침.
기각
정사영상으로 차량 검증 — 경사 사진(2025-08-05)과 정사영상(250922)이 다른 비행이라 주차 차량 위치가 다르다.

집합별 합계

- + - + - + - + - + - + - + - + - + - + - + @@ -270,11 +271,12 @@ border-radius:6px;padding:5px 8px;font:inherit;font-size:13px} epsilon_px1.5기준 런output/sam3/tune_t64 (확정 설정 8장) / 기준선은 output/sam3/YYX_run 69장프롬프트 파일prompts/discovery_v4.txt (94개). 기준선은 discovery_v1.txt (85개) -집합 정의configs/merge_groups_v4.txt (12집합). 기준선은 merge_groups.txt +집합 정의configs/merge_groups_v4.txt (11집합 + 집합 밖 3종: factory structure, 콘 2종)타일6×4 = 24개, overlap 10%, 타일 크기 1365×1365. 기준선은 4×3 (2048×1820)검출 설정conf 0.10 · NMS iou 0.40 · 같은 라벨 병합 gap 8px · --wide-in-tiles. 기준선 conf 0.25, 통짜 분리공정 보기 사진확정 설정 8장 (기준선 무라벨 3.5~52.1% 분포에서 고름)확정 후보 설정prompts/discovery_v4.txt (94개) · configs/merge_groups_v4.txt · --wide-in-tiles · --cols 6 --rows 4 · --conf 0.10 · NMS 0.40 · --merge (gap 8px) · 병합 gap 2px +claim 규칙configs/claim_rules.txt — vehicle : 0.5 : 지붕 11종 + 방수포/보호시트""" + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--stage", action="append", required=True, metavar="이름=글롭", + help="단계 하나. 순서대로 여러 번 준다") + ap.add_argument("--output", required=True) + ap.add_argument("--images", help="원본 이미지 폴더 (JSON 의 source_image 가 없을 때)") + ap.add_argument("--max-size", type=int, default=2000) + ap.add_argument("--title", default=None) + args = ap.parse_args() + + stages = [] + for spec in args.stage: + if "=" not in spec: + raise SystemExit(f"--stage 형식은 '이름=글롭': {spec}") + name, pat = spec.split("=", 1) + files = {stem_of(p): p for p in sorted(glob.glob(pat))} + if not files: + raise SystemExit(f"단계 '{name}' 에 해당하는 JSON 없음: {pat}") + stages.append((name, files)) + + common = set(stages[0][1]) + for _, f in stages[1:]: + common &= set(f) + if not common: + raise SystemExit("모든 단계에 공통으로 있는 사진이 없다") + stems = sorted(common) + print(f"단계 {len(stages)}개 · 공통 사진 {len(stems)}장") + + out_html = os.path.abspath(args.output) + os.makedirs(os.path.dirname(out_html), exist_ok=True) + + pages, colors, counts = [], {}, Counter() + for stem in stems: + img_name = stem + "_view.jpg" + img_path = os.path.join(os.path.dirname(out_html), img_name) + if not os.path.isfile(img_path): + with open(stages[0][1][stem], encoding="utf-8") as fh: + src = json.load(fh).get("source_image") + if (not src or not os.path.isfile(src)) and args.images: + for ext in (".JPG", ".jpg", ".png"): + cand = os.path.join(args.images, stem + ext) + if os.path.isfile(cand): + src = cand + break + if not src or not os.path.isfile(src): + raise SystemExit(f"원본 이미지를 못 찾았다: {stem} (--images 로 지정)") + im = cv2.imdecode(np.fromfile(src, dtype=np.uint8), cv2.IMREAD_COLOR) + hh, ww = im.shape[:2] + k = min(1.0, args.max_size / max(hh, ww)) + if k < 1.0: + im = cv2.resize(im, (int(ww * k), int(hh * k)), interpolation=cv2.INTER_AREA) + cv2.imencode(".jpg", im, [cv2.IMWRITE_JPEG_QUALITY, 86])[1].tofile(img_path) + print(f" 이미지 생성 {img_name}") + # 좌표 축척은 원본 기준이므로 이미지 크기에서 되계산한다 + im = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), cv2.IMREAD_COLOR) + vh, vw = im.shape[:2] + + page_stages = [] + for name, files in stages: + with open(files[stem], encoding="utf-8") as fh: + segs = json.load(fh)["segments"] + if not page_stages: # 첫 단계에서 원본 해상도를 잡는다 + mx = max((p[0] for s in segs for p in s["points"]), default=vw) + my = max((p[1] for s in segs for p in s["points"]), default=vh) + k = min(vw / max(mx, 1), vh / max(my, 1)) + items = [] + for s in segs: + lb = s.get("label", "") + colors.setdefault(lb, color_for(lb)) + counts[lb] += 1 + ch = 1 if (s.get("claimed_from") + or len(s.get("merged_from", []) or []) > 1) else 0 + src = s.get("merged_from") or [] + items.append({"l": lb, "c": ch, + "f": s.get("claimed_from") or "", + "m": src if len(src) > 1 else [], + "p": [[round(p[0] * k, 1), round(p[1] * k, 1)] + for p in s["points"]]}) + page_stages.append({"p": items}) + pages.append({"name": stem, "img": img_name, "stages": page_stages}) + + title = args.title or os.path.splitext(os.path.basename(out_html))[0] + payload = {"stages": [n for n, _ in stages], "pages": pages, "colors": colors} + with open(out_html, "w", encoding="utf-8") as fh: + fh.write(HTML.replace("__TITLE__", title) + .replace("__DATA__", json.dumps(payload, ensure_ascii=False))) + tot = [sum(len(p["stages"][i]["p"]) for p in pages) for i in range(len(stages))] + print(f"뷰어: {args.output}") + for (name, _), n in zip(stages, tot): + print(f" {name:<16}{n:>8,} 폴리곤") + + +if __name__ == "__main__": + main()
집합프롬프트(검출>0/전체)폴리곤폴리곤%면적% 병합 후 폴리곤줄어든 비율병합 후 면적%
vegetation6 / 73,03527.2%16.63%1,76642%16.28%1,76642%16.09%
building17 / 212,21819.9%15.18%46679%12.97%49078%12.57%
structure4 / 41,34712.1%0.46% 1,07920%0.57%
ground4 / 49398.4%7.71%57339%9.09%57339%8.99%
road6 / 78397.5%25.09%24771%25.72%24771%25.41%
fence4 / 57686.9%0.90%7029%1.20%7029%1.18%
vehicle13 / 146635.9%0.80%57613%1.08%57613%1.06%
stored material9 / 95364.8%0.75%45016%1.03%45016%1.02%
farmland7 / 84604.1%29.24%15666%27.59%15666%27.27%
없음2 / 21201.1%0.00%1200%0.01%144-20%1.43%
retaining wall3 / 31000.9%0.28%991%0.39%991%0.38%
slope2 / 4860.8%2.73%851%3.79%851%3.75%
water5 / 6580.5%0.23% 5112%0.28%