feat: SAM 3.1 검출 커버리지 튜닝 — 무라벨 16% → 3.6%

2D 검출 단계에서 미검출을 없애는 작업. 지표는 "무라벨 화면%" —
마스크 합집합으로 재서 겹침을 뺀 값이다. 다각형 넓이 단순 합(105%)은
겹침 때문에 미검출을 못 잡아낸다.

원인은 프롬프트가 아니라 통짜 패스였다. wide_v1.txt 의 21개 프롬프트는
8192x5460 을 1x1 로 넣어 대상 하나에 인스턴스가 하나만 살아남았다.
도로는 한쪽 차로만, 논은 한 필지만 잡혔다.

BlockYYX 실측 (사진 8장):
  기준선  무라벨 평균 21.6% (69장 전체로는 16.1%, 최악 52.1%)
  튜닝후  무라벨 평균  3.6% (최대 5.0%)

기여도 (0654 기준):
  통짜 -> 타일 패스     -36.7pp
  conf 0.25 -> 0.10      -5.4pp
  타일 4x3 -> 6x4        -4.6pp
  프롬프트 추가 9개      -0.5pp   <- 거의 기여 없음

비용은 140 -> 548초/장 (3.9배).

sam3_multi_prompt.py: --wide-in-tiles 플래그 추가. 통짜 프롬프트를
타일 패스에서도 돌린다. 겹치는 결과는 NMS 가 지운다.

신규 도구:
  coverage_stats.py     마스크 합집합으로 무라벨% 측정
  prompt_stats.py       카테고리-프롬프트별 검출 집계
  make_merge_monitor.py 모니터 HTML 생성 (수치는 집계 JSON 에서만)

vote_labels.py 에 --rescue-pred 추가 (투표에 졌지만 표가 있고 모델도
동의하는 면을 되돌린다). 3D 작업이 다른 담당으로 넘어가 중단된 상태.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
minsung
2026-09-01 20:04:45 +09:00
parent 4e5173522d
commit 28bee39196
12 changed files with 1947 additions and 60 deletions
+147
View File
@@ -0,0 +1,147 @@
# 병합 그룹 정의 — 대괄호 안이 병합 후 대표 라벨.
# 같은 그룹에 속한 라벨끼리 외곽선이 --gap px 이내로 인접하면 하나로 합친다.
# 어느 그룹에도 없는 라벨은 건드리지 않는다.
#
# 여기서 나누지 않는 구분이 있다. SAM은 사진만 보므로 미터도 규모도 모른다.
# 아래는 2D가 아니라 3D 메시에서 갈라야 정확하다:
# 낮은수목 / 수목 CSF 지면 위 높이
# 수목 / 숲 [vegetation] 연결성분 면적
# 지형 / 경사면 면 법선 경사각
# 건물 / 빌딩 [building] 연결성분 면적·높이
# 그래서 tree canopy와 dense forest, building과 residential house는
# 일부러 같은 그룹에 둔다 — 2D에서 갈라봐야 중복 마스크만 나온다.
# [building]
building
building rooftop
building facade
residential house
warehouse
factory structure
storage container
blue roof
red roof
green roof
gray roof
dark gray roof
black roof
white roof
silver metal roof
orange roof
brown roof
yellow roof
greenhouse roof
plastic greenhouse
vinyl greenhouse tunnel
# [road]
asphalt road
concrete pavement
parking lot
pedestrian sidewalk
crosswalk
bridge
# 아래는 비활성 — discovery_v1.txt에서 프롬프트를 통째로 껐다.
# 최종 목표가 지면 대 나머지라 차선은 도로면의 일부지 별도 클래스가 아니다.
# 검출량 1위였지만(583개 중 277개) 전부 [road] 안에서 버려지고,
# 논밭 이랑을 yellow center dividing line으로 오인하기까지 했다.
# [road marking]
# road lane marking
# yellow center dividing line
# white solid lane marking
# white dashed lane marking
# white directional arrow marking
# yellow parking stall line
# white parking stall line
# blue handicap parking space
# pink pedestrian safety marking
# green bike lane marking
# [vegetation]
overgrown weeds
shrub
tree canopy
dense forest
green hedge
grass lawn
# [farmland]
crop field
farmland
plowed field
bare soil field
rice paddy
vegetable field
# [ground]
bare ground
dirt field
earth bank
dirt path
# [stored material]
blue plastic drum
orange plastic barrel
yellow plastic container
white industrial tank
metallic storage tank
blue waterproof tarp
green waterproof tarp
black protective sheet
white canvas canopy
# [retaining wall]
retaining wall
concrete retaining wall
gabion wall
# [slope]
grass covered embankment
road embankment slope
cut slope
riprap slope
# [water]
water surface
pond
reservoir
stream
irrigation canal
drainage ditch
# [structure]
utility pole
streetlight pole
road sign
culvert
# [fence]
gray metal fence
green wire fence
red roadside barrier
guardrail
metal guardrail
# [vehicle]
white vehicle
black vehicle
silver vehicle
gray vehicle
red vehicle
blue vehicle
yellow vehicle
orange vehicle
green vehicle
white cargo truck
blue cargo truck
yellow school bus
commercial bus
tractor
farm tractor
excavator
# 아래는 기본 비활성 — 나란히 놓인 콘이 한 덩어리로 융합된다.
# [cone]
# yellow safety cone
# orange traffic cone
+151
View File
@@ -0,0 +1,151 @@
# 병합 그룹 정의 — 대괄호 안이 병합 후 대표 라벨.
# 같은 그룹에 속한 라벨끼리 외곽선이 --gap px 이내로 인접하면 하나로 합친다.
# 어느 그룹에도 없는 라벨은 건드리지 않는다.
#
# 여기서 나누지 않는 구분이 있다. SAM은 사진만 보므로 미터도 규모도 모른다.
# 아래는 2D가 아니라 3D 메시에서 갈라야 정확하다:
# 낮은수목 / 수목 CSF 지면 위 높이
# 수목 / 숲 [vegetation] 연결성분 면적
# 지형 / 경사면 면 법선 경사각
# 건물 / 빌딩 [building] 연결성분 면적·높이
# 그래서 tree canopy와 dense forest, building과 residential house는
# 일부러 같은 그룹에 둔다 — 2D에서 갈라봐야 중복 마스크만 나온다.
# [building]
building
building rooftop
building facade
residential house
warehouse
factory structure
storage container
blue roof
red roof
green roof
gray roof
dark gray roof
black roof
white roof
silver metal roof
orange roof
brown roof
yellow roof
greenhouse roof
plastic greenhouse
vinyl greenhouse tunnel
# [road]
gravel shoulder
asphalt road
concrete pavement
parking lot
pedestrian sidewalk
crosswalk
bridge
# 아래는 비활성 — discovery_v1.txt에서 프롬프트를 통째로 껐다.
# 최종 목표가 지면 대 나머지라 차선은 도로면의 일부지 별도 클래스가 아니다.
# 검출량 1위였지만(583개 중 277개) 전부 [road] 안에서 버려지고,
# 논밭 이랑을 yellow center dividing line으로 오인하기까지 했다.
# [road marking]
# road lane marking
# yellow center dividing line
# white solid lane marking
# white dashed lane marking
# white directional arrow marking
# yellow parking stall line
# white parking stall line
# blue handicap parking space
# pink pedestrian safety marking
# green bike lane marking
# [vegetation]
yellow grass field
overgrown weeds
shrub
tree canopy
dense forest
green hedge
grass lawn
# [farmland]
ripening rice field
harvested field
crop field
farmland
plowed field
bare soil field
rice paddy
vegetable field
# [ground]
bare ground
dirt field
earth bank
dirt path
# [stored material]
blue plastic drum
orange plastic barrel
yellow plastic container
white industrial tank
metallic storage tank
blue waterproof tarp
green waterproof tarp
black protective sheet
white canvas canopy
# [retaining wall]
retaining wall
concrete retaining wall
gabion wall
# [slope]
grass covered embankment
road embankment slope
cut slope
riprap slope
# [water]
water surface
pond
reservoir
stream
irrigation canal
drainage ditch
# [structure]
utility pole
streetlight pole
road sign
culvert
# [fence]
gray metal fence
green wire fence
red roadside barrier
guardrail
metal guardrail
# [vehicle]
white vehicle
black vehicle
silver vehicle
gray vehicle
red vehicle
blue vehicle
yellow vehicle
orange vehicle
green vehicle
white cargo truck
blue cargo truck
yellow school bus
commercial bus
tractor
farm tractor
excavator
# 아래는 기본 비활성 — 나란히 놓인 콘이 한 덩어리로 융합된다.
# [cone]
# yellow safety cone
# orange traffic cone
+111
View File
@@ -0,0 +1,111 @@
{
"title": "SAM 3.1 후처리 — 검출 커버리지 · 집합 병합",
"note": "1단계는 미검출을 없애는 것이다. 지표는 '무라벨 화면%' — 마스크 합집합으로 재서 겹침을 뺀 값. 목표 5% 이하, 8장 평균 3.6% 로 달성. 2단계는 집합으로 묶고 외곽선이 gap px 이내로 인접한 폴리곤을 합치는 것.",
"params": {
"gap_px": 2,
"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",
"타일": "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"
},
"steps": [
{
"id": "M1",
"name": "프롬프트별 검출 집계",
"state": "done",
"detail": "tools/prompt_stats.py. YYX_run 실측: 폴리곤 13,580 · 라벨 77종."
},
{
"id": "M1b",
"name": "공정 보기(검출 → 병합) 뷰어 연결",
"state": "done",
"detail": "tools/make_viewer.py 로 같은 사진 3장을 단계별로 굽고 모니터에 붙였다. docs/pilot/theater/."
},
{
"id": "M1c",
"name": "무라벨 지표 확립",
"state": "done",
"detail": "tools/coverage_stats.py. YYX_run 69장 실측: 무라벨 평균 16.1% · 중앙 15.0% · 최악 52.1%(0654). 덮음 95% 이상은 2장뿐."
},
{
"id": "M1d",
"name": "통짜 패스 원인 규명",
"state": "done",
"detail": "wide_v1.txt 의 21개(asphalt road, crop field, farmland, tree canopy 등)는 8192×5460 을 1×1 로 넣어 대상당 인스턴스가 1개만 살아남았다. 0006 에서 asphalt road 가 서쪽 차로만 덮음(1개, score 0.910)."
},
{
"id": "M1e",
"name": "--wide-in-tiles 시험",
"state": "done",
"detail": "통짜 프롬프트를 타일 패스에도 돌린다. 0654 무라벨 52.1→15.4%, 0006 19.4→12.2%. 비용 140→231초/장 (+65%)."
},
{
"id": "M1f",
"name": "경계 띠 프롬프트 추가 (v3)",
"state": "done",
"detail": "잔여 무라벨이 논밭 둑·도로 법면·밭 가장자리 잡초였다. overgrown weeds / shrub / grass covered embankment / earth bank / dirt path 5개 추가. 효과 작음 — 0654 15.4→14.9%, 0006 12.2→11.9%."
},
{
"id": "M1g",
"name": "conf 스윕",
"state": "done",
"detail": "conf 0.10 으로 한 번 뽑고 점수 문턱을 오프라인으로 쓸었다. 0006: 0.25에서 9.3% → 0.10에서 5.8%. 0654: 13.1% → 9.5%. conf 0.03 은 마스크 폭증으로 37분에 1장도 못 끝내 중단."
},
{
"id": "M1h",
"name": "v4 프롬프트 (익은 논·갓길)",
"state": "done",
"detail": "ripening rice field / yellow grass field / harvested field / gravel shoulder 추가. 효과 없음 — 0006 5.8→5.7%, 0654 9.5→9.6%. 프롬프트 추가는 포화."
},
{
"id": "M1i",
"name": "타일 4×3 → 6×4 (목표 달성)",
"state": "done",
"detail": "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시간."
},
{
"id": "M1j",
"name": "확정 설정 8장 검증",
"state": "done",
"detail": "8장 실측 무라벨(conf 0.10): 0654 5.0 · 0006 5.0 · 0653 4.7 · 0079 4.4 · 0484 3.0 · 0274 2.9 · 0321 2.5 · 0475 1.5 → 평균 3.6%. 같은 8장 기준선은 평균 21.8%. 소요 466~662초/장(평균 548초). 중간에 PC 다운으로 2장 유실, 재개함."
},
{
"id": "M2",
"name": "집합 정의 조정",
"state": "todo",
"detail": "튜닝 검출 결과(라벨 79종)를 보고 집합을 다시 짠다. 1차는 Claude, 이후 사용자."
},
{
"id": "M3",
"name": "gap 값 조정",
"state": "todo",
"detail": "외곽선 인접 판정 1~3 px. 파일럿 기준 2 px 로 시작."
}
],
"issues": [
{
"state": "closed",
"text": "통짜(1×1) 패스는 대상 하나에 인스턴스가 하나만 남는다 — 도로 한쪽 차로, 논 한 필지만 잡힘. --wide-in-tiles 로 해결(측정치는 M1e)."
},
{
"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) — 유지할지 뺄지 미정."
},
{
"state": "rejected",
"text": "프롬프트 색 특정성 A/B (car/sedan/suv/van/pickup truck) — 폴리곤 +26% 인데 마스크 면적은 +0.002~0.068%p 뿐, 신규 마스크의 90~99%가 기존과 겹침."
},
{
"state": "rejected",
"text": "정사영상으로 차량 검증 — 경사 사진(2025-08-05)과 정사영상(250922)이 다른 비행이라 주차 차량 위치가 다르다."
}
]
}
+296
View File
@@ -0,0 +1,296 @@
<title>SAM 3.1 후처리 — 검출 커버리지 · 집합 병합</title>
<style>
:root{--bg:#fbfbfa;--fg:#1d1f21;--dim:#6b7076;--line:#e2e4e6;--card:#fff;--accent:#2f6f9f}
:root:not([data-theme="light"]){}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
--bg:#16181a;--fg:#e6e8ea;--dim:#9aa1a8;--line:#2c3034;--card:#1d2023;--accent:#79b8e8}}
:root[data-theme="dark"]{--bg:#16181a;--fg:#e6e8ea;--dim:#9aa1a8;--line:#2c3034;--card:#1d2023;--accent:#79b8e8}
body{background:var(--bg);color:var(--fg);font:14px/1.55 -apple-system,"Segoe UI",
"Malgun Gothic",sans-serif;margin:0;padding:28px 22px 60px}
.wrap{max-width:1180px;margin:0 auto}
h1{font-size:21px;margin:0 0 4px}
h2{font-size:15px;margin:30px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--line)}
.sub{color:var(--dim);margin:0 0 2px}
.badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12px;
font-weight:600;white-space:nowrap}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:11px 13px}
.card .k{color:var(--dim);font-size:12px}
.card .v{font-size:19px;font-weight:600;margin-top:2px}
table{border-collapse:collapse;width:100%;font-size:13px}
th,td{padding:5px 9px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}
th:first-child,td:first-child,th:nth-child(2),td:nth-child(2),
th:nth-child(3),td:nth-child(3){text-align:left}
th{background:var(--card);position:sticky;top:0;cursor:pointer;font-weight:600;
color:var(--dim);border-bottom:2px solid var(--line)}
tbody tr:hover{background:var(--card)}
tr.zero td{color:var(--dim)}
tr.nogroup td:nth-child(3){color:#c0392b;font-weight:600}
.scroll{overflow-x:auto;max-height:640px;overflow-y:auto;border:1px solid var(--line);
border-radius:8px}
.bar{height:6px;background:var(--line);border-radius:3px;overflow:hidden;min-width:60px}
.bar i{display:block;height:100%;background:var(--accent)}
.step{display:flex;gap:11px;align-items:flex-start;padding:9px 0;border-bottom:1px solid var(--line)}
.step .id{font-family:ui-monospace,Menlo,Consolas,monospace;color:var(--dim);min-width:30px}
.step .d{color:var(--dim);font-size:13px}
.issue{padding:9px 0;border-bottom:1px solid var(--line);display:flex;gap:11px}
.par{display:grid;grid-template-columns:max-content 1fr;gap:3px 16px;font-size:13px}
.par b{color:var(--dim);font-weight:500}
code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
.controls{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 10px}
.tabs{display:flex;gap:6px;margin:0 0 10px;flex-wrap:wrap}
.tab{background:var(--card);border:1px solid var(--line);border-radius:6px;
padding:6px 13px;cursor:pointer;font:inherit;font-size:13px;color:var(--fg)}
.tab[aria-selected="true"]{background:var(--accent);border-color:var(--accent);color:#fff}
.stage{border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card)}
.stage iframe{display:block;width:100%;height:620px;border:0}
.flow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;
color:var(--dim);font-size:13px}
.flow b{color:var(--fg);font-weight:600}
.flow .ar{color:var(--accent);font-weight:700}
.d-up{color:#1a7f4b}.d-dn{color:#c0392b}
select,input{background:var(--card);color:var(--fg);border:1px solid var(--line);
border-radius:6px;padding:5px 8px;font:inherit;font-size:13px}
</style>
<div class="wrap">
<h1>SAM 3.1 후처리 — 검출 커버리지 · 집합 병합</h1>
<p class="sub">1단계는 미검출을 없애는 것이다. 지표는 &#x27;무라벨 화면%&#x27; — 마스크 합집합으로 재서 겹침을 뺀 값. 목표 5% 이하, 8장 평균 3.6% 로 달성. 2단계는 집합으로 묶고 외곽선이 gap px 이내로 인접한 폴리곤을 합치는 것.</p>
<p class="sub" style="font-size:12px">생성 2026-09-01 18:49 · 집계 <code>tune_t64</code></p>
<h2>실측</h2><div class="cards">
<div class="card"><div class="k">사진</div><div class="v">8</div></div>
<div class="card"><div class="k">폴리곤</div><div class="v">4,586</div></div>
<div class="card"><div class="k">병합 후 폴리곤</div><div class="v">2,636 (-43%)</div></div>
<div class="card"><div class="k">라벨 종류</div><div class="v">79 / 94</div></div>
<div class="card"><div class="k">집합</div><div class="v">12</div></div>
<div class="card"><div class="k">검출 면적 / 화면</div><div class="v">182.1%</div></div>
<div class="card"><div class="k">집합 없는 폴리곤</div><div class="v">50</div></div>
</div>
<h2>미검출 — 무라벨 화면%</h2>
<p class="sub" style="font-size:12px">마스크 합집합 기준(겹침 제거), 축척 1/4. 목표 5% 이하.</p>
<div class="cards">
<div class="card"><div class="k">무라벨 평균</div><div class="v">3.6%</div></div>
<div class="card"><div class="k">무라벨 중앙</div><div class="v">4.4%</div></div>
<div class="card"><div class="k">최악</div><div class="v">5.0%</div></div>
<div class="card"><div class="k">5% 이하 사진</div><div class="v">7 / 8</div></div>
</div>
<div class="scroll" style="max-height:280px;margin-top:10px"><table><thead><tr><th>사진</th><th>폴리곤</th><th>무라벨%</th><th></th></tr></thead><tbody>
<tr><td>DJI_20250805165516_0654</td><td data-v="363">363</td><td data-v="5.038">5.0%</td><td><div class="bar"><i style="width:5.0%;background:#c0392b"></i></div></td></tr>
<tr><td>DJI_20250805163523_0006</td><td data-v="923">923</td><td data-v="4.953">5.0%</td><td><div class="bar"><i style="width:5.0%;background:#1a7f4b"></i></div></td></tr>
<tr><td>DJI_20250805165514_0653</td><td data-v="377">377</td><td data-v="4.719">4.7%</td><td><div class="bar"><i style="width:4.7%;background:#1a7f4b"></i></div></td></tr>
<tr><td>DJI_20250805163720_0079</td><td data-v="443">443</td><td data-v="4.425">4.4%</td><td><div class="bar"><i style="width:4.4%;background:#1a7f4b"></i></div></td></tr>
<tr><td>DJI_20250805164932_0484</td><td data-v="606">606</td><td data-v="3.028">3.0%</td><td><div class="bar"><i style="width:3.0%;background:#1a7f4b"></i></div></td></tr>
<tr><td>DJI_20250805164304_0274</td><td data-v="873">873</td><td data-v="2.917">2.9%</td><td><div class="bar"><i style="width:2.9%;background:#1a7f4b"></i></div></td></tr>
<tr><td>DJI_20250805164457_0321</td><td data-v="714">714</td><td data-v="2.525">2.5%</td><td><div class="bar"><i style="width:2.5%;background:#1a7f4b"></i></div></td></tr>
<tr><td>DJI_20250805164917_0475</td><td data-v="287">287</td><td data-v="1.526">1.5%</td><td><div class="bar"><i style="width:1.5%;background:#1a7f4b"></i></div></td></tr>
</tbody></table></div>
<h2>공정 보기 — 같은 사진, 단계별</h2>
<div class="flow"><b>SAM 검출 4,586 폴리곤</b><span>(79 라벨)</span><span class="ar">→ 집합 병합 →</span><b>2,636 폴리곤</b><span>(14 집합)</span><span>· 사진 8장 기준</span></div>
<div class="tabs">
<button class="tab" role="tab" data-i="0" aria-selected="true">1. SAM 검출 — 확정 설정</button>
<button class="tab" role="tab" data-i="1" aria-selected="false">2. 집합 병합 후 (gap 2px)</button>
<button class="tab" role="tab" data-i="2" aria-selected="false">참고: 기준선 검출 (conf 0.25, 4×3)</button>
</div>
<div class="stage" data-i="0"><iframe src="theater_v4/stage1_detect.html" loading="lazy" title="stage0"></iframe></div>
<div class="stage" data-i="1" hidden><iframe src="theater_v4/stage2_merged.html" loading="lazy" title="stage1"></iframe></div>
<div class="stage" data-i="2" hidden><iframe src="theater/stage1_detect.html" loading="lazy" title="stage2"></iframe></div>
<p class="sub" style="font-size:12px">왼쪽 목록에서 라벨을 켜고 끈다. 휠 확대, 드래그 이동. 새 창: <a href="theater_v4/stage1_detect.html">1. SAM 검출 — 확정 설정</a> · <a href="theater_v4/stage2_merged.html">2. 집합 병합 후 (gap 2px)</a> · <a href="theater/stage1_detect.html">참고: 기준선 검출 (conf 0.25, 4×3)</a></p>
<h2>단계</h2>
<div class="step"><span class="id">M1</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>프롬프트별 검출 집계</div><div class="d">tools/prompt_stats.py. YYX_run 실측: 폴리곤 13,580 · 라벨 77종.</div></div></div>
<div class="step"><span class="id">M1b</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>공정 보기(검출 → 병합) 뷰어 연결</div><div class="d">tools/make_viewer.py 로 같은 사진 3장을 단계별로 굽고 모니터에 붙였다. docs/pilot/theater/.</div></div></div>
<div class="step"><span class="id">M1c</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>무라벨 지표 확립</div><div class="d">tools/coverage_stats.py. YYX_run 69장 실측: 무라벨 평균 16.1% · 중앙 15.0% · 최악 52.1%(0654). 덮음 95% 이상은 2장뿐.</div></div></div>
<div class="step"><span class="id">M1d</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>통짜 패스 원인 규명</div><div class="d">wide_v1.txt 의 21개(asphalt road, crop field, farmland, tree canopy 등)는 8192×5460 을 1×1 로 넣어 대상당 인스턴스가 1개만 살아남았다. 0006 에서 asphalt road 가 서쪽 차로만 덮음(1개, score 0.910).</div></div></div>
<div class="step"><span class="id">M1e</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>--wide-in-tiles 시험</div><div class="d">통짜 프롬프트를 타일 패스에도 돌린다. 0654 무라벨 52.1→15.4%, 0006 19.4→12.2%. 비용 140→231초/장 (+65%).</div></div></div>
<div class="step"><span class="id">M1f</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>경계 띠 프롬프트 추가 (v3)</div><div class="d">잔여 무라벨이 논밭 둑·도로 법면·밭 가장자리 잡초였다. overgrown weeds / shrub / grass covered embankment / earth bank / dirt path 5개 추가. 효과 작음 — 0654 15.4→14.9%, 0006 12.2→11.9%.</div></div></div>
<div class="step"><span class="id">M1g</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>conf 스윕</div><div class="d">conf 0.10 으로 한 번 뽑고 점수 문턱을 오프라인으로 쓸었다. 0006: 0.25에서 9.3% → 0.10에서 5.8%. 0654: 13.1% → 9.5%. conf 0.03 은 마스크 폭증으로 37분에 1장도 못 끝내 중단.</div></div></div>
<div class="step"><span class="id">M1h</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>v4 프롬프트 (익은 논·갓길)</div><div class="d">ripening rice field / yellow grass field / harvested field / gravel shoulder 추가. 효과 없음 — 0006 5.8→5.7%, 0654 9.5→9.6%. 프롬프트 추가는 포화.</div></div></div>
<div class="step"><span class="id">M1i</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>타일 4×3 → 6×4 (목표 달성)</div><div class="d">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시간.</div></div></div>
<div class="step"><span class="id">M1j</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>확정 설정 8장 검증</div><div class="d">8장 실측 무라벨(conf 0.10): 0654 5.0 · 0006 5.0 · 0653 4.7 · 0079 4.4 · 0484 3.0 · 0274 2.9 · 0321 2.5 · 0475 1.5 → 평균 3.6%. 같은 8장 기준선은 평균 21.8%. 소요 466~662초/장(평균 548초). 중간에 PC 다운으로 2장 유실, 재개함.</div></div></div>
<div class="step"><span class="id">M2</span><span class="badge" style="color:#5a5f66;background:#eceef0">대기</span><div><div>집합 정의 조정</div><div class="d">튜닝 검출 결과(라벨 79종)를 보고 집합을 다시 짠다. 1차는 Claude, 이후 사용자.</div></div></div>
<div class="step"><span class="id">M3</span><span class="badge" style="color:#5a5f66;background:#eceef0">대기</span><div><div>gap 값 조정</div><div class="d">외곽선 인접 판정 1~3 px. 파일럿 기준 2 px 로 시작.</div></div></div>
<h2>이슈 · 기각된 시도</h2>
<div class="issue"><span class="badge" style="color:#1a7f4b;background:#e3f5ea">해결</span><div>통짜(1×1) 패스는 대상 하나에 인스턴스가 하나만 남는다 — 도로 한쪽 차로, 논 한 필지만 잡힘. --wide-in-tiles 로 해결(측정치는 M1e).</div></div>
<div class="issue"><span class="badge" style="color:#a12a2a;background:#fbe6e6">미해결</span><div>orange traffic cone / yellow safety cone 은 어느 집합에도 없어 검출 52 폴리곤이 버려진다. [cone] 그룹이 주석 처리돼 있음 — 나란히 놓인 콘이 한 덩어리로 융합된다는 이유.</div></div>
<div class="issue"><span class="badge" style="color:#a12a2a;background:#fbe6e6">미해결</span><div>검출 0 인 프롬프트 8개 (plowed field, rice paddy, vinyl greenhouse tunnel, gabion wall, road embankment slope, riprap slope, water surface, reservoir) — 유지할지 뺄지 미정.</div></div>
<div class="issue"><span class="badge" style="color:#5a5f66;background:#eceef0">기각</span><div>프롬프트 색 특정성 A/B (car/sedan/suv/van/pickup truck) — 폴리곤 +26% 인데 마스크 면적은 +0.002~0.068%p 뿐, 신규 마스크의 90~99%가 기존과 겹침.</div></div>
<div class="issue"><span class="badge" style="color:#5a5f66;background:#eceef0">기각</span><div>정사영상으로 차량 검증 — 경사 사진(2025-08-05)과 정사영상(250922)이 다른 비행이라 주차 차량 위치가 다르다.</div></div>
<h2>집합별 합계</h2><div class="scroll"><table><thead><tr><th>집합</th><th>프롬프트(검출>0/전체)</th><th>폴리곤</th><th>폴리곤%</th><th>면적%</th>
<th>병합 후 폴리곤</th><th>줄어든 비율</th><th>병합 후 면적%</th>
<th></th></tr></thead><tbody>
<tr><td>vegetation</td><td>6 / 7</td><td>1,336</td><td>29.1%</td><td>19.27%</td>
<td>771</td><td class="d-up">42%</td><td>19.04%</td>
<td><div class="bar"><i style="width:19.3%"></i></div></td></tr>
<tr><td>building</td><td>17 / 21</td><td>732</td><td>16.0%</td><td>11.27%</td>
<td>157</td><td class="d-up">79%</td><td>9.58%</td>
<td><div class="bar"><i style="width:11.3%"></i></div></td></tr>
<tr><td>structure</td><td>4 / 4</td><td>589</td><td>12.8%</td><td>0.48%</td>
<td>464</td><td class="d-up">21%</td><td>0.61%</td>
<td><div class="bar"><i style="width:0.5%"></i></div></td></tr>
<tr><td>ground</td><td>4 / 4</td><td>439</td><td>9.6%</td><td>6.62%</td>
<td>257</td><td class="d-up">41%</td><td>7.55%</td>
<td><div class="bar"><i style="width:6.6%"></i></div></td></tr>
<tr><td>road</td><td>6 / 7</td><td>393</td><td>8.6%</td><td>18.45%</td>
<td>106</td><td class="d-up">73%</td><td>18.76%</td>
<td><div class="bar"><i style="width:18.5%"></i></div></td></tr>
<tr><td>fence</td><td>4 / 5</td><td>315</td><td>6.9%</td><td>0.76%</td>
<td>286</td><td class="d-up">9%</td><td>1.02%</td>
<td><div class="bar"><i style="width:0.8%"></i></div></td></tr>
<tr><td>vehicle</td><td>12 / 14</td><td>217</td><td>4.7%</td><td>0.51%</td>
<td>189</td><td class="d-up">13%</td><td>0.70%</td>
<td><div class="bar"><i style="width:0.5%"></i></div></td></tr>
<tr><td>stored material</td><td>8 / 9</td><td>205</td><td>4.5%</td><td>0.82%</td>
<td>168</td><td class="d-up">18%</td><td>1.14%</td>
<td><div class="bar"><i style="width:0.8%"></i></div></td></tr>
<tr><td>farmland</td><td>7 / 8</td><td>196</td><td>4.3%</td><td>38.33%</td>
<td>75</td><td class="d-up">62%</td><td>36.63%</td>
<td><div class="bar"><i style="width:38.3%"></i></div></td></tr>
<tr><td>slope</td><td>2 / 4</td><td>54</td><td>1.2%</td><td>3.25%</td>
<td>54</td><td class="">0%</td><td>4.62%</td>
<td><div class="bar"><i style="width:3.2%"></i></div></td></tr>
<tr><td>없음</td><td>2 / 2</td><td>50</td><td>1.1%</td><td>0.00%</td>
<td>50</td><td class="">0%</td><td>0.01%</td>
<td><div class="bar"><i style="width:0.0%"></i></div></td></tr>
<tr><td>retaining wall</td><td>3 / 3</td><td>42</td><td>0.9%</td><td>0.17%</td>
<td>41</td><td class="d-up">2%</td><td>0.23%</td>
<td><div class="bar"><i style="width:0.2%"></i></div></td></tr>
<tr><td>water</td><td>4 / 6</td><td>18</td><td>0.4%</td><td>0.08%</td>
<td>18</td><td class="">0%</td><td>0.11%</td>
<td><div class="bar"><i style="width:0.1%"></i></div></td></tr>
</tbody></table></div>
<p class="sub" style="font-size:12px">병합 후 면적%는 병합 후 총검출면적 기준. 겹침을 빼지 않은 다각형 넓이 합이다.</p>
<h2>카테고리 · 프롬프트별 검출</h2>
<div class="controls"><select id="fcat"><option value="">카테고리 전체</option><option>Boundary &amp; Filler</option><option>Farmland</option><option>General Categories</option><option>Industrial Materials, Storage &amp; Objects by Color</option><option>Ripening &amp; Bare Field</option><option>Road Infrastructure</option><option>Roofs &amp; Structures by Color</option><option>Vehicles &amp; Transportation by Color</option><option>Water</option></select><select id="fgrp"><option value="">집합 전체</option><option>building</option><option>farmland</option><option>fence</option><option>ground</option><option>retaining wall</option><option>road</option><option>slope</option><option>stored material</option><option>structure</option><option>vegetation</option><option>vehicle</option><option>water</option><option>없음</option></select><input id="fq" placeholder="프롬프트 검색"><label style="color:var(--dim)"><input type="checkbox" id="fzero"> 검출 0 만</label></div>
<div class="scroll"><table id="t"><thead><tr><th>프롬프트</th><th>카테고리</th><th>집합</th><th>폴리곤</th><th>사진</th><th>면적%</th><th>중앙 면적 px</th><th>점수 중앙</th></tr></thead><tbody>
<tr data-cat="General Categories" data-grp="building"><td>building</td><td>General Categories</td><td>building</td><td data-v="50">50</td><td data-v="7">7</td><td data-v="0.05990295">5.99%</td><td data-v="69036.0">69,036</td><td data-v="0.5670">0.567</td></tr>
<tr data-cat="General Categories" data-grp="building"><td>building rooftop</td><td>General Categories</td><td>building</td><td data-v="62">62</td><td data-v="5">5</td><td data-v="0.01545257">1.55%</td><td data-v="19678.8">19,679</td><td data-v="0.1755">0.176</td></tr>
<tr data-cat="General Categories" data-grp="building"><td>building facade</td><td>General Categories</td><td>building</td><td data-v="11">11</td><td data-v="3">3</td><td data-v="0.00224658">0.22%</td><td data-v="71389.5">71,390</td><td data-v="0.1982">0.198</td></tr>
<tr class="zero" data-cat="General Categories" data-grp="building"><td>residential house</td><td>General Categories</td><td>building</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="General Categories" data-grp="building"><td>warehouse</td><td>General Categories</td><td>building</td><td data-v="5">5</td><td data-v="3">3</td><td data-v="0.00041711">0.04%</td><td data-v="58328.5">58,328</td><td data-v="0.1346">0.135</td></tr>
<tr data-cat="General Categories" data-grp="building"><td>storage container</td><td>General Categories</td><td>building</td><td data-v="3">3</td><td data-v="1">1</td><td data-v="0.00016048">0.02%</td><td data-v="16436.0">16,436</td><td data-v="0.1377">0.138</td></tr>
<tr data-cat="General Categories" data-grp="building"><td>factory structure</td><td>General Categories</td><td>building</td><td data-v="8">8</td><td data-v="4">4</td><td data-v="0.00626472">0.63%</td><td data-v="148268.0">148,268</td><td data-v="0.1818">0.182</td></tr>
<tr data-cat="General Categories" data-grp="road"><td>asphalt road</td><td>General Categories</td><td>road</td><td data-v="72">72</td><td data-v="8">8</td><td data-v="0.07113584">7.11%</td><td data-v="48341.8">48,342</td><td data-v="0.3069">0.307</td></tr>
<tr data-cat="General Categories" data-grp="road"><td>concrete pavement</td><td>General Categories</td><td>road</td><td data-v="129">129</td><td data-v="8">8</td><td data-v="0.06126632">6.13%</td><td data-v="73574.0">73,574</td><td data-v="0.1647">0.165</td></tr>
<tr data-cat="General Categories" data-grp="road"><td>parking lot</td><td>General Categories</td><td>road</td><td data-v="32">32</td><td data-v="6">6</td><td data-v="0.03535648">3.54%</td><td data-v="165576.0">165,576</td><td data-v="0.4125">0.412</td></tr>
<tr data-cat="General Categories" data-grp="road"><td>pedestrian sidewalk</td><td>General Categories</td><td>road</td><td data-v="56">56</td><td data-v="8">8</td><td data-v="0.00988794">0.99%</td><td data-v="58755.5">58,756</td><td data-v="0.4101">0.410</td></tr>
<tr data-cat="General Categories" data-grp="road"><td>crosswalk</td><td>General Categories</td><td>road</td><td data-v="95">95</td><td data-v="7">7</td><td data-v="0.00565010">0.57%</td><td data-v="4814.0">4,814</td><td data-v="0.1926">0.193</td></tr>
<tr data-cat="General Categories" data-grp="ground"><td>bare ground</td><td>General Categories</td><td>ground</td><td data-v="156">156</td><td data-v="8">8</td><td data-v="0.01591721">1.59%</td><td data-v="16780.0">16,780</td><td data-v="0.1475">0.147</td></tr>
<tr data-cat="General Categories" data-grp="ground"><td>dirt field</td><td>General Categories</td><td>ground</td><td data-v="78">78</td><td data-v="7">7</td><td data-v="0.02691171">2.69%</td><td data-v="49051.2">49,051</td><td data-v="0.2199">0.220</td></tr>
<tr data-cat="General Categories" data-grp="vegetation"><td>grass lawn</td><td>General Categories</td><td>vegetation</td><td data-v="30">30</td><td data-v="8">8</td><td data-v="0.01612630">1.61%</td><td data-v="56941.8">56,942</td><td data-v="0.4519">0.452</td></tr>
<tr data-cat="General Categories" data-grp="vegetation"><td>tree canopy</td><td>General Categories</td><td>vegetation</td><td data-v="74">74</td><td data-v="8">8</td><td data-v="0.02077963">2.08%</td><td data-v="55689.5">55,690</td><td data-v="0.5534">0.553</td></tr>
<tr data-cat="General Categories" data-grp="vegetation"><td>dense forest</td><td>General Categories</td><td>vegetation</td><td data-v="17">17</td><td data-v="7">7</td><td data-v="0.07295766">7.30%</td><td data-v="2051820.0">2,051,820</td><td data-v="0.8796">0.880</td></tr>
<tr data-cat="General Categories" data-grp="vegetation"><td>green hedge</td><td>General Categories</td><td>vegetation</td><td data-v="97">97</td><td data-v="8">8</td><td data-v="0.02244825">2.24%</td><td data-v="41963.0">41,963</td><td data-v="0.2637">0.264</td></tr>
<tr data-cat="Farmland" data-grp="farmland"><td>crop field</td><td>Farmland</td><td>farmland</td><td data-v="57">57</td><td data-v="7">7</td><td data-v="0.16709837">16.71%</td><td data-v="603950.5">603,950</td><td data-v="0.8435">0.844</td></tr>
<tr data-cat="Farmland" data-grp="farmland"><td>farmland</td><td>Farmland</td><td>farmland</td><td data-v="33">33</td><td data-v="7">7</td><td data-v="0.13175773">13.18%</td><td data-v="92805.0">92,805</td><td data-v="0.1410">0.141</td></tr>
<tr class="zero" data-cat="Farmland" data-grp="farmland"><td>plowed field</td><td>Farmland</td><td>farmland</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Farmland" data-grp="farmland"><td>bare soil field</td><td>Farmland</td><td>farmland</td><td data-v="30">30</td><td data-v="6">6</td><td data-v="0.01316429">1.32%</td><td data-v="56641.0">56,641</td><td data-v="0.1747">0.175</td></tr>
<tr data-cat="Farmland" data-grp="farmland"><td>rice paddy</td><td>Farmland</td><td>farmland</td><td data-v="1">1</td><td data-v="1">1</td><td data-v="0.00014715">0.01%</td><td data-v="95906.5">95,906</td><td data-v="0.1203">0.120</td></tr>
<tr data-cat="Farmland" data-grp="farmland"><td>vegetable field</td><td>Farmland</td><td>farmland</td><td data-v="17">17</td><td data-v="6">6</td><td data-v="0.00765433">0.77%</td><td data-v="93252.5">93,252</td><td data-v="0.3159">0.316</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>blue roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="63">63</td><td data-v="6">6</td><td data-v="0.01027494">1.03%</td><td data-v="18057.5">18,058</td><td data-v="0.6532">0.653</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>red roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="15">15</td><td data-v="2">2</td><td data-v="0.00035583">0.04%</td><td data-v="2623.0">2,623</td><td data-v="0.1682">0.168</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>green roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="17">17</td><td data-v="5">5</td><td data-v="0.00313146">0.31%</td><td data-v="63992.0">63,992</td><td data-v="0.8145">0.814</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>gray roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="47">47</td><td data-v="7">7</td><td data-v="0.00229265">0.23%</td><td data-v="8548.0">8,548</td><td data-v="0.2026">0.203</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>dark gray roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="46">46</td><td data-v="7">7</td><td data-v="0.00229735">0.23%</td><td data-v="10420.8">10,421</td><td data-v="0.2491">0.249</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>black roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="105">105</td><td data-v="6">6</td><td data-v="0.00125461">0.13%</td><td data-v="3830.5">3,830</td><td data-v="0.2251">0.225</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>white roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="209">209</td><td data-v="8">8</td><td data-v="0.00343254">0.34%</td><td data-v="4599.5">4,600</td><td data-v="0.2462">0.246</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>silver metal roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="66">66</td><td data-v="5">5</td><td data-v="0.00373639">0.37%</td><td data-v="16169.5">16,170</td><td data-v="0.1636">0.164</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>orange roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="4">4</td><td data-v="3">3</td><td data-v="0.00004197">0.00%</td><td data-v="5993.2">5,993</td><td data-v="0.1605">0.161</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>brown roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="18">18</td><td data-v="6">6</td><td data-v="0.00140551">0.14%</td><td data-v="20397.0">20,397</td><td data-v="0.4442">0.444</td></tr>
<tr data-cat="Roofs &amp; Structures by Color" data-grp="building"><td>yellow roof</td><td>Roofs &amp; Structures by Color</td><td>building</td><td data-v="3">3</td><td data-v="1">1</td><td data-v="0.00006122">0.01%</td><td data-v="19504.5">19,504</td><td data-v="0.1683">0.168</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>white vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="70">70</td><td data-v="8">8</td><td data-v="0.00203382">0.20%</td><td data-v="16285.2">16,285</td><td data-v="0.8289">0.829</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>black vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="70">70</td><td data-v="7">7</td><td data-v="0.00129219">0.13%</td><td data-v="6947.5">6,948</td><td data-v="0.2245">0.225</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>silver vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="10">10</td><td data-v="6">6</td><td data-v="0.00018943">0.02%</td><td data-v="15045.8">15,046</td><td data-v="0.8922">0.892</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>gray vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="11">11</td><td data-v="6">6</td><td data-v="0.00039751">0.04%</td><td data-v="22787.0">22,787</td><td data-v="0.8217">0.822</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>red vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="11">11</td><td data-v="6">6</td><td data-v="0.00013907">0.01%</td><td data-v="4667.0">4,667</td><td data-v="0.2426">0.243</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>blue vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="9">9</td><td data-v="5">5</td><td data-v="0.00007117">0.01%</td><td data-v="3253.0">3,253</td><td data-v="0.2454">0.245</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>yellow vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="2">2</td><td data-v="1">1</td><td data-v="0.00002377">0.00%</td><td data-v="7746.5">7,746</td><td data-v="0.2087">0.209</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>orange vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="15">15</td><td data-v="5">5</td><td data-v="0.00018191">0.02%</td><td data-v="4692.0">4,692</td><td data-v="0.2296">0.230</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>green vehicle</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="5">5</td><td data-v="2">2</td><td data-v="0.00005272">0.01%</td><td data-v="3681.5">3,682</td><td data-v="0.3302">0.330</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>white cargo truck</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="7">7</td><td data-v="2">2</td><td data-v="0.00041205">0.04%</td><td data-v="32135.0">32,135</td><td data-v="0.8888">0.889</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>blue cargo truck</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="2">2</td><td data-v="2">2</td><td data-v="0.00010174">0.01%</td><td data-v="33156.5">33,156</td><td data-v="0.8211">0.821</td></tr>
<tr class="zero" data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>commercial bus</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr class="zero" data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>tractor</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Vehicles &amp; Transportation by Color" data-grp="vehicle"><td>excavator</td><td>Vehicles &amp; Transportation by Color</td><td>vehicle</td><td data-v="5">5</td><td data-v="4">4</td><td data-v="0.00016362">0.02%</td><td data-v="16793.0">16,793</td><td data-v="0.6529">0.653</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>blue plastic drum</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="2">2</td><td data-v="2">2</td><td data-v="0.00000397">0.00%</td><td data-v="1292.8">1,293</td><td data-v="0.1714">0.171</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>orange plastic barrel</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="23">23</td><td data-v="3">3</td><td data-v="0.00005848">0.01%</td><td data-v="905.5">906</td><td data-v="0.1384">0.138</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>yellow plastic container</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="44">44</td><td data-v="6">6</td><td data-v="0.00016489">0.02%</td><td data-v="1797.5">1,798</td><td data-v="0.2736">0.274</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>white industrial tank</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="28">28</td><td data-v="2">2</td><td data-v="0.00013076">0.01%</td><td data-v="1643.8">1,644</td><td data-v="0.1374">0.137</td></tr>
<tr class="zero" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>metallic storage tank</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>blue waterproof tarp</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="58">58</td><td data-v="5">5</td><td data-v="0.00165482">0.17%</td><td data-v="7392.5">7,392</td><td data-v="0.3191">0.319</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>green waterproof tarp</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="19">19</td><td data-v="4">4</td><td data-v="0.00521047">0.52%</td><td data-v="12532.5">12,532</td><td data-v="0.4539">0.454</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>black protective sheet</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="13">13</td><td data-v="4">4</td><td data-v="0.00032054">0.03%</td><td data-v="12431.0">12,431</td><td data-v="0.1563">0.156</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="stored material"><td>white canvas canopy</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>stored material</td><td data-v="18">18</td><td data-v="5">5</td><td data-v="0.00061365">0.06%</td><td data-v="5492.2">5,492</td><td data-v="0.2553">0.255</td></tr>
<tr class="zero" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="building"><td>plastic greenhouse</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>building</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr class="zero" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="building"><td>vinyl greenhouse tunnel</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>building</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr class="zero" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="building"><td>greenhouse roof</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>building</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="fence"><td>gray metal fence</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>fence</td><td data-v="19">19</td><td data-v="6">6</td><td data-v="0.00050994">0.05%</td><td data-v="8573.5">8,574</td><td data-v="0.2256">0.226</td></tr>
<tr class="zero" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="fence"><td>green wire fence</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>fence</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="fence"><td>red roadside barrier</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>fence</td><td data-v="7">7</td><td data-v="1">1</td><td data-v="0.00001362">0.00%</td><td data-v="1040.0">1,040</td><td data-v="0.1544">0.154</td></tr>
<tr class="nogroup" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="없음"><td>yellow safety cone</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>없음</td><td data-v="5">5</td><td data-v="3">3</td><td data-v="0.00001357">0.00%</td><td data-v="1948.5">1,948</td><td data-v="0.3535">0.354</td></tr>
<tr class="nogroup" data-cat="Industrial Materials, Storage &amp; Objects by Color" data-grp="없음"><td>orange traffic cone</td><td>Industrial Materials, Storage &amp; Objects by Color</td><td>없음</td><td data-v="45">45</td><td data-v="3">3</td><td data-v="0.00002234">0.00%</td><td data-v="294.5">294</td><td data-v="0.1228">0.123</td></tr>
<tr data-cat="Road Infrastructure" data-grp="retaining wall"><td>retaining wall</td><td>Road Infrastructure</td><td>retaining wall</td><td data-v="2">2</td><td data-v="1">1</td><td data-v="0.00006878">0.01%</td><td data-v="22414.2">22,414</td><td data-v="0.2881">0.288</td></tr>
<tr data-cat="Road Infrastructure" data-grp="retaining wall"><td>concrete retaining wall</td><td>Road Infrastructure</td><td>retaining wall</td><td data-v="38">38</td><td data-v="8">8</td><td data-v="0.00147039">0.15%</td><td data-v="15785.8">15,786</td><td data-v="0.1688">0.169</td></tr>
<tr data-cat="Road Infrastructure" data-grp="retaining wall"><td>gabion wall</td><td>Road Infrastructure</td><td>retaining wall</td><td data-v="2">2</td><td data-v="2">2</td><td data-v="0.00014632">0.01%</td><td data-v="47682.5">47,682</td><td data-v="0.6845">0.684</td></tr>
<tr data-cat="Road Infrastructure" data-grp="fence"><td>guardrail</td><td>Road Infrastructure</td><td>fence</td><td data-v="27">27</td><td data-v="8">8</td><td data-v="0.00084786">0.08%</td><td data-v="15065.0">15,065</td><td data-v="0.2854">0.285</td></tr>
<tr data-cat="Road Infrastructure" data-grp="fence"><td>metal guardrail</td><td>Road Infrastructure</td><td>fence</td><td data-v="262">262</td><td data-v="8">8</td><td data-v="0.00619882">0.62%</td><td data-v="5790.2">5,790</td><td data-v="0.1794">0.179</td></tr>
<tr class="zero" data-cat="Road Infrastructure" data-grp="slope"><td>road embankment slope</td><td>Road Infrastructure</td><td>slope</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Road Infrastructure" data-grp="slope"><td>cut slope</td><td>Road Infrastructure</td><td>slope</td><td data-v="19">19</td><td data-v="6">6</td><td data-v="0.00509517">0.51%</td><td data-v="62165.5">62,166</td><td data-v="0.1461">0.146</td></tr>
<tr class="zero" data-cat="Road Infrastructure" data-grp="slope"><td>riprap slope</td><td>Road Infrastructure</td><td>slope</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr class="zero" data-cat="Road Infrastructure" data-grp="road"><td>bridge</td><td>Road Infrastructure</td><td>road</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Road Infrastructure" data-grp="structure"><td>culvert</td><td>Road Infrastructure</td><td>structure</td><td data-v="5">5</td><td data-v="2">2</td><td data-v="0.00000857">0.00%</td><td data-v="1027.0">1,027</td><td data-v="0.1072">0.107</td></tr>
<tr data-cat="Road Infrastructure" data-grp="structure"><td>utility pole</td><td>Road Infrastructure</td><td>structure</td><td data-v="298">298</td><td data-v="8">8</td><td data-v="0.00276138">0.28%</td><td data-v="1992.2">1,992</td><td data-v="0.2121">0.212</td></tr>
<tr data-cat="Road Infrastructure" data-grp="structure"><td>streetlight pole</td><td>Road Infrastructure</td><td>structure</td><td data-v="158">158</td><td data-v="8">8</td><td data-v="0.00067240">0.07%</td><td data-v="1681.0">1,681</td><td data-v="0.2422">0.242</td></tr>
<tr data-cat="Road Infrastructure" data-grp="structure"><td>road sign</td><td>Road Infrastructure</td><td>structure</td><td data-v="128">128</td><td data-v="8">8</td><td data-v="0.00137490">0.14%</td><td data-v="2531.0">2,531</td><td data-v="0.2467">0.247</td></tr>
<tr class="zero" data-cat="Water" data-grp="water"><td>water surface</td><td>Water</td><td>water</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Water" data-grp="water"><td>pond</td><td>Water</td><td>water</td><td data-v="1">1</td><td data-v="1">1</td><td data-v="0.00002042">0.00%</td><td data-v="13309.0">13,309</td><td data-v="0.7263">0.726</td></tr>
<tr class="zero" data-cat="Water" data-grp="water"><td>reservoir</td><td>Water</td><td>water</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Water" data-grp="water"><td>stream</td><td>Water</td><td>water</td><td data-v="8">8</td><td data-v="3">3</td><td data-v="0.00047763">0.05%</td><td data-v="28497.8">28,498</td><td data-v="0.3328">0.333</td></tr>
<tr data-cat="Water" data-grp="water"><td>irrigation canal</td><td>Water</td><td>water</td><td data-v="4">4</td><td data-v="2">2</td><td data-v="0.00016363">0.02%</td><td data-v="27393.0">27,393</td><td data-v="0.3036">0.304</td></tr>
<tr data-cat="Water" data-grp="water"><td>drainage ditch</td><td>Water</td><td>water</td><td data-v="5">5</td><td data-v="4">4</td><td data-v="0.00011772">0.01%</td><td data-v="4654.5">4,654</td><td data-v="0.1441">0.144</td></tr>
<tr data-cat="Boundary &amp; Filler" data-grp="vegetation"><td>overgrown weeds</td><td>Boundary &amp; Filler</td><td>vegetation</td><td data-v="531">531</td><td data-v="8">8</td><td data-v="0.02663274">2.66%</td><td data-v="4321.0">4,321</td><td data-v="0.1667">0.167</td></tr>
<tr data-cat="Boundary &amp; Filler" data-grp="vegetation"><td>shrub</td><td>Boundary &amp; Filler</td><td>vegetation</td><td data-v="587">587</td><td data-v="8">8</td><td data-v="0.03371271">3.37%</td><td data-v="6584.0">6,584</td><td data-v="0.1975">0.198</td></tr>
<tr data-cat="Boundary &amp; Filler" data-grp="slope"><td>grass covered embankment</td><td>Boundary &amp; Filler</td><td>slope</td><td data-v="35">35</td><td data-v="6">6</td><td data-v="0.02739532">2.74%</td><td data-v="166603.0">166,603</td><td data-v="0.4217">0.422</td></tr>
<tr data-cat="Boundary &amp; Filler" data-grp="ground"><td>earth bank</td><td>Boundary &amp; Filler</td><td>ground</td><td data-v="4">4</td><td data-v="2">2</td><td data-v="0.00004401">0.00%</td><td data-v="4505.0">4,505</td><td data-v="0.1454">0.145</td></tr>
<tr data-cat="Boundary &amp; Filler" data-grp="ground"><td>dirt path</td><td>Boundary &amp; Filler</td><td>ground</td><td data-v="201">201</td><td data-v="8">8</td><td data-v="0.02327868">2.33%</td><td data-v="25354.0">25,354</td><td data-v="0.2540">0.254</td></tr>
<tr data-cat="Ripening &amp; Bare Field" data-grp="farmland"><td>ripening rice field</td><td>Ripening &amp; Bare Field</td><td>farmland</td><td data-v="20">20</td><td data-v="5">5</td><td data-v="0.06079511">6.08%</td><td data-v="1491017.8">1,491,018</td><td data-v="0.9570">0.957</td></tr>
<tr class="zero" data-cat="Ripening &amp; Bare Field" data-grp="vegetation"><td>yellow grass field</td><td>Ripening &amp; Bare Field</td><td>vegetation</td><td data-v="0">0</td><td data-v="0">-</td><td data-v="0.00000000">0.00%</td><td data-v="0.0">0</td><td data-v="0.0000">0.000</td></tr>
<tr data-cat="Ripening &amp; Bare Field" data-grp="farmland"><td>harvested field</td><td>Ripening &amp; Bare Field</td><td>farmland</td><td data-v="38">38</td><td data-v="6">6</td><td data-v="0.00271854">0.27%</td><td data-v="20705.0">20,705</td><td data-v="0.1359">0.136</td></tr>
<tr data-cat="Ripening &amp; Bare Field" data-grp="road"><td>gravel shoulder</td><td>Ripening &amp; Bare Field</td><td>road</td><td data-v="9">9</td><td data-v="6">6</td><td data-v="0.00123468">0.12%</td><td data-v="50588.5">50,588</td><td data-v="0.1785">0.179</td></tr>
</tbody></table></div>
<h2>설정</h2><div class="par">
<b>gap_px</b><span><code>2</code></span>
<b>epsilon_px</b><span><code>1.5</code></span>
<b>기준 런</b><span><code>output/sam3/tune_t64 (확정 설정 8장) / 기준선은 output/sam3/YYX_run 69장</code></span>
<b>프롬프트 파일</b><span><code>prompts/discovery_v4.txt (94개). 기준선은 discovery_v1.txt (85개)</code></span>
<b>집합 정의</b><span><code>configs/merge_groups_v4.txt (12집합). 기준선은 merge_groups.txt</code></span>
<b>타일</b><span><code>6×4 = 24개, overlap 10%, 타일 크기 1365×1365. 기준선은 4×3 (2048×1820)</code></span>
<b>검출 설정</b><span><code>conf 0.10 · NMS iou 0.40 · 같은 라벨 병합 gap 8px · --wide-in-tiles. 기준선 conf 0.25, 통짜 분리</code></span>
<b>공정 보기 사진</b><span><code>확정 설정 8장 (기준선 무라벨 3.5~52.1% 분포에서 고름)</code></span>
<b>확정 후보 설정</b><span><code>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</code></span>
</div>
</div>
<script>
const tb=document.querySelector('#t tbody');
document.querySelectorAll('#t th').forEach((th,i)=>{th.onclick=()=>{
const rows=[...tb.rows], asc=th.dataset.asc!=='1';
th.dataset.asc=asc?'1':'0';
rows.sort((a,b)=>{
const x=a.cells[i].dataset.v, y=b.cells[i].dataset.v;
if(x!==undefined&&y!==undefined) return (asc?1:-1)*(parseFloat(x)-parseFloat(y));
return (asc?1:-1)*a.cells[i].textContent.localeCompare(b.cells[i].textContent,'ko');
});
rows.forEach(r=>tb.appendChild(r));
};});
function apply(){
const c=fcat.value,g=fgrp.value,q=fq.value.toLowerCase(),z=fzero.checked;
[...tb.rows].forEach(r=>{
const ok=(!c||r.dataset.cat===c)&&(!g||r.dataset.grp===g)
&&(!q||r.cells[0].textContent.toLowerCase().includes(q))
&&(!z||r.classList.contains('zero'));
r.hidden=!ok;
});
}
[fcat,fgrp,fzero].forEach(e=>e.onchange=apply); fq.oninput=apply;
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{
document.querySelectorAll('.tab').forEach(x=>x.setAttribute('aria-selected',x===b));
document.querySelectorAll('.stage').forEach(s=>s.hidden=s.dataset.i!==b.dataset.i);
});
</script>
+128
View File
@@ -0,0 +1,128 @@
# SAM 3.1 Segmentation Prompts - Color & Object Combinations
# [General Categories]
building
building rooftop
building facade
residential house
warehouse
storage container
factory structure
asphalt road
concrete pavement
parking lot
pedestrian sidewalk
# road lane marking 비활성 — 아래 Road Markings 주석 참고
crosswalk
bare ground
dirt field
grass lawn
tree canopy
dense forest
green hedge
# cast shadow 비활성 — 병합 그룹이 없어 결과가 버려진다 (74개 검출)
# [Farmland]
crop field
farmland
plowed field
bare soil field
rice paddy
vegetable field
# [Roofs & Structures by Color]
blue roof
red roof
green roof
gray roof
dark gray roof
black roof
white roof
silver metal roof
orange roof
brown roof
yellow roof
# [Vehicles & Transportation by Color]
white vehicle
black vehicle
silver vehicle
gray vehicle
red vehicle
blue vehicle
yellow vehicle
orange vehicle
green vehicle
white cargo truck
blue cargo truck
# yellow school bus 비활성 — commercial bus와 같은 [vehicle] 그룹 내 중복
commercial bus
tractor
# farm tractor 비활성 — tractor와 같은 [vehicle] 그룹 내 중복
excavator
# [Industrial Materials, Storage & Objects by Color]
blue plastic drum
orange plastic barrel
yellow plastic container
white industrial tank
metallic storage tank
blue waterproof tarp
green waterproof tarp
black protective sheet
white canvas canopy
plastic greenhouse
vinyl greenhouse tunnel
greenhouse roof
gray metal fence
green wire fence
red roadside barrier
yellow safety cone
orange traffic cone
# [Road Markings & Surface Features]
# 전부 비활성. 최종 목표가 지면 대 나머지라 차선은 도로면의 일부지 별도 클래스가
# 아니다. 검출량은 1위인데(583개 중 277개) 전부 [road] 안에서 버려지고,
# 논밭 이랑을 yellow center dividing line으로 오인하기까지 했다.
# road lane marking 도 같은 이유로 위 General Categories에서 비활성.
# yellow center dividing line
# white solid lane marking
# white dashed lane marking
# white directional arrow marking
# yellow parking stall line
# white parking stall line
# blue handicap parking space
# pink pedestrian safety marking
# green bike lane marking
# [Road Infrastructure]
retaining wall
concrete retaining wall
gabion wall
guardrail
metal guardrail
road embankment slope
cut slope
riprap slope
bridge
culvert
utility pole
streetlight pole
road sign
# [Water]
water surface
pond
reservoir
stream
irrigation canal
drainage ditch
# [Boundary & Filler]
# 0654/0484 잔여 무라벨이 전부 경계 띠였다 — 논밭 사이 둑, 도로 법면·갓길,
# 밭 가장자리 잡초. 타일 패스 85→90개라 배치 청크(6개)가 그대로다 = 추가 비용 없음.
overgrown weeds
shrub
grass covered embankment
earth bank
dirt path
+136
View File
@@ -0,0 +1,136 @@
# SAM 3.1 Segmentation Prompts - Color & Object Combinations
# [General Categories]
building
building rooftop
building facade
residential house
warehouse
storage container
factory structure
asphalt road
concrete pavement
parking lot
pedestrian sidewalk
# road lane marking 비활성 — 아래 Road Markings 주석 참고
crosswalk
bare ground
dirt field
grass lawn
tree canopy
dense forest
green hedge
# cast shadow 비활성 — 병합 그룹이 없어 결과가 버려진다 (74개 검출)
# [Farmland]
crop field
farmland
plowed field
bare soil field
rice paddy
vegetable field
# [Roofs & Structures by Color]
blue roof
red roof
green roof
gray roof
dark gray roof
black roof
white roof
silver metal roof
orange roof
brown roof
yellow roof
# [Vehicles & Transportation by Color]
white vehicle
black vehicle
silver vehicle
gray vehicle
red vehicle
blue vehicle
yellow vehicle
orange vehicle
green vehicle
white cargo truck
blue cargo truck
# yellow school bus 비활성 — commercial bus와 같은 [vehicle] 그룹 내 중복
commercial bus
tractor
# farm tractor 비활성 — tractor와 같은 [vehicle] 그룹 내 중복
excavator
# [Industrial Materials, Storage & Objects by Color]
blue plastic drum
orange plastic barrel
yellow plastic container
white industrial tank
metallic storage tank
blue waterproof tarp
green waterproof tarp
black protective sheet
white canvas canopy
plastic greenhouse
vinyl greenhouse tunnel
greenhouse roof
gray metal fence
green wire fence
red roadside barrier
yellow safety cone
orange traffic cone
# [Road Markings & Surface Features]
# 전부 비활성. 최종 목표가 지면 대 나머지라 차선은 도로면의 일부지 별도 클래스가
# 아니다. 검출량은 1위인데(583개 중 277개) 전부 [road] 안에서 버려지고,
# 논밭 이랑을 yellow center dividing line으로 오인하기까지 했다.
# road lane marking 도 같은 이유로 위 General Categories에서 비활성.
# yellow center dividing line
# white solid lane marking
# white dashed lane marking
# white directional arrow marking
# yellow parking stall line
# white parking stall line
# blue handicap parking space
# pink pedestrian safety marking
# green bike lane marking
# [Road Infrastructure]
retaining wall
concrete retaining wall
gabion wall
guardrail
metal guardrail
road embankment slope
cut slope
riprap slope
bridge
culvert
utility pole
streetlight pole
road sign
# [Water]
water surface
pond
reservoir
stream
irrigation canal
drainage ditch
# [Boundary & Filler]
# 0654/0484 잔여 무라벨이 전부 경계 띠였다 — 논밭 사이 둑, 도로 법면·갓길,
# 밭 가장자리 잡초. 타일 패스 85→90개라 배치 청크(6개)가 그대로다 = 추가 비용 없음.
overgrown weeds
shrub
grass covered embankment
earth bank
dirt path
# [Ripening & Bare Field]
# 0654 잔여 무라벨의 최대 덩어리(화면 4.62%)가 누렇게 익은 논 필지였다.
# crop field / farmland / rice paddy 로 안 잡힌다 (rice paddy 는 69장 검출 0).
ripening rice field
yellow grass field
harvested field
gravel shoulder
+24
View File
@@ -0,0 +1,24 @@
# 타일보다 큰 대상 — 통짜(1×1)로 검출한다.
# 여기 적힌 라벨은 타일 패스에서 제외된다.
building
# asphalt road → 타일 패스로 옮김 (통짜에서 차로 한쪽만 잡힘)
concrete pavement
parking lot
bare ground
dirt field
grass lawn
tree canopy
dense forest
green hedge
crop field
farmland
plowed field
bare soil field
rice paddy
vegetable field
water surface
pond
reservoir
road embankment slope
cut slope
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python
"""검출이 화면을 얼마나 덮는지 잰다 — 미검출을 찾는 단계의 자다.
prompt_stats.py 의 면적은 다각형 넓이의 단순 합이라 겹치면 부풀고 100%를 넘는다.
여기서는 마스크를 굽고 합집합을 세므로 "안 잡힌 곳"이 그대로 나온다.
무라벨% = 100 - (어떤 라벨이든 덮은 화소 / 전체 화소)
집합별 합집합도 같이 낸다. 집합끼리는 겹칠 수 있으므로 합이 100을 넘을 수 있다.
전체 해상도로 구우면 느리므로 --scale 로 줄여서 잰다 (기본 1/4).
"""
import argparse
import glob
import json
import os
import time
import cv2
import numpy as np
def load_groups(path):
mapping, cur = {}, None
for line in open(path, encoding="utf-8"):
s = line.strip()
if s.startswith("#"):
b = s.lstrip("#").strip()
cur = b[1:-1].strip() if b.startswith("[") and b.endswith("]") else None
elif s and cur:
mapping[s] = cur
return mapping
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--run-dir", required=True)
ap.add_argument("--pattern", default="*_multi.json")
ap.add_argument("--groups", required=True)
ap.add_argument("--size", required=True, help="원본 영상 'W,H'")
ap.add_argument("--scale", type=float, default=0.25)
ap.add_argument("--out-json")
args = ap.parse_args()
W, H = (int(x) for x in args.size.split(","))
k = args.scale
w, h = int(W * k), int(H * k)
grp = load_groups(args.groups)
files = sorted(glob.glob(os.path.join(args.run_dir, args.pattern)))
if not files:
raise FileNotFoundError(f"{args.pattern} 없음: {args.run_dir}")
reps = sorted(set(grp.values())) + ["없음"]
per_photo, gsum = [], {g: 0.0 for g in reps}
px = w * h
t0 = time.time()
for i, p in enumerate(files, 1):
with open(p, encoding="utf-8") as fh:
segs = json.load(fh)["segments"]
anym = np.zeros((h, w), np.uint8)
masks = {}
for s in segs:
lb = s["label"]
g = lb if lb in grp.values() else grp.get(lb, "없음")
poly = (np.asarray(s["points"], np.float32) * k).astype(np.int32)
cv2.fillPoly(anym, [poly], 255)
m = masks.get(g)
if m is None:
m = masks[g] = np.zeros((h, w), np.uint8)
cv2.fillPoly(m, [poly], 255)
cov = np.count_nonzero(anym) / px
row = {
"photo": os.path.basename(p),
"segments": len(segs),
"covered": cov,
"groups": {g: np.count_nonzero(m) / px for g, m in masks.items()},
}
for g, v in row["groups"].items():
gsum[g] = gsum.get(g, 0.0) + v
per_photo.append(row)
print(f" [{i}/{len(files)}] {row['photo']:<44} 덮음 {100 * cov:5.1f}% "
f"· 무라벨 {100 * (1 - cov):5.1f}% · 폴리곤 {len(segs):,}")
cov = np.array([r["covered"] for r in per_photo])
print(f"\n사진 {len(files)}장 · {time.time() - t0:.0f}초 · 척도 1/{1 / k:.0f} "
f"({w}x{h})")
print(f"덮음 평균 {100 * cov.mean():.1f}% 중앙 {100 * np.median(cov):.1f}% "
f"최소 {100 * cov.min():.1f}% 최대 {100 * cov.max():.1f}%")
print(f"무라벨 평균 {100 * (1 - cov.mean()):.1f}% "
f"최악 {100 * (1 - cov.min()):.1f}% ({per_photo[int(cov.argmin())]['photo']})")
print(f"\n집합별 평균 합집합 화면%")
for g, v in sorted(gsum.items(), key=lambda t: -t[1]):
if v:
print(f" {g:<20}{100 * v / len(files):>7.2f}%")
print(f"\n무라벨 큰 사진 5장")
for r in sorted(per_photo, key=lambda r: r["covered"])[:5]:
print(f" {r['photo']:<44}{100 * (1 - r['covered']):>6.1f}%")
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),
"pattern": args.pattern,
"scale": k,
"size": [W, H],
"photos": len(files),
"covered_mean": float(cov.mean()),
"covered_min": float(cov.min()),
"group_mean": {g: v / len(files) for g, v in gsum.items() if v},
"per_photo": per_photo,
}, fh, ensure_ascii=False, indent=2)
print(f"\n저장: {args.out_json}")
if __name__ == "__main__":
main()
+331
View File
@@ -0,0 +1,331 @@
#!/usr/bin/env python
"""SAM 후처리 작업 모니터 HTML 을 만든다.
수치는 전부 prompt_stats.py 의 집계 JSON 에서 온다. 이 파일은 숫자를 만들지
않는다. 단계·이슈·설정 같은 사람이 쓰는 것만 상태 JSON 에서 읽는다.
python tools/make_merge_monitor.py \
--stats output/sam3/YYX_run/_prompt_stats.json \
--status configs/sam_merge_status.json \
--out docs/pilot/sam_merge.html
"""
import argparse
import html
import json
import os
import time
from collections import defaultdict
STATE = {
"done": ("완료", "#1a7f4b", "#e3f5ea"),
"running": ("진행", "#8a5a00", "#fdf0d5"),
"blocked": ("막힘", "#a12a2a", "#fbe6e6"),
"todo": ("대기", "#5a5f66", "#eceef0"),
"open": ("미해결", "#a12a2a", "#fbe6e6"),
"closed": ("해결", "#1a7f4b", "#e3f5ea"),
"rejected": ("기각", "#5a5f66", "#eceef0"),
}
def esc(s):
return html.escape(str(s))
def badge(state):
label, fg, bg = STATE.get(state, (state, "#5a5f66", "#eceef0"))
return (f'<span class="badge" style="color:{fg};background:{bg}">'
f'{esc(label)}</span>')
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--stats", required=True, help="병합 전 집계 JSON")
ap.add_argument("--status", required=True, help="단계·이슈 JSON")
ap.add_argument("--stats-after", help="병합 후 집계 JSON (--pattern '*_merged.json')")
ap.add_argument("--coverage", help="coverage_stats.py 집계 JSON (무라벨 지표)")
ap.add_argument("--theater", nargs="*", default=[], metavar="제목=경로",
help="공정을 보여줄 뷰어 HTML. 경로는 --out 기준 상대경로")
ap.add_argument("--out", required=True)
args = ap.parse_args()
with open(args.stats, encoding="utf-8") as fh:
st = json.load(fh)
with open(args.status, encoding="utf-8") as fh:
sv = json.load(fh)
af = None
if args.stats_after:
with open(args.stats_after, encoding="utf-8") as fh:
af = json.load(fh)
cv = None
if args.coverage:
with open(args.coverage, encoding="utf-8") as fh:
cv = json.load(fh)
stages = []
for t in args.theater:
if "=" not in t:
raise SystemExit(f"--theater 형식은 '제목=경로': {t}")
name, path = t.split("=", 1)
if not os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(args.out)), path)):
raise SystemExit(f"뷰어 없음: {path}")
stages.append((name, path))
rows = st["rows"]
gsum = defaultdict(lambda: {"poly": 0, "area": 0.0, "prompts": 0, "used": 0})
for r in rows:
g = gsum[r["group"]]
g["poly"] += r["polygons"]
g["area"] += r["area_px"]
g["prompts"] += 1
g["used"] += 1 if r["polygons"] else 0
tot_area = st["total_area_px"] or 1
tot_poly = st["total_polygons"] or 1
parts = []
A = parts.append
A(f'<title>{esc(sv["title"])}</title>')
A("""<style>
:root{--bg:#fbfbfa;--fg:#1d1f21;--dim:#6b7076;--line:#e2e4e6;--card:#fff;--accent:#2f6f9f}
:root:not([data-theme="light"]){}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
--bg:#16181a;--fg:#e6e8ea;--dim:#9aa1a8;--line:#2c3034;--card:#1d2023;--accent:#79b8e8}}
:root[data-theme="dark"]{--bg:#16181a;--fg:#e6e8ea;--dim:#9aa1a8;--line:#2c3034;--card:#1d2023;--accent:#79b8e8}
body{background:var(--bg);color:var(--fg);font:14px/1.55 -apple-system,"Segoe UI",
"Malgun Gothic",sans-serif;margin:0;padding:28px 22px 60px}
.wrap{max-width:1180px;margin:0 auto}
h1{font-size:21px;margin:0 0 4px}
h2{font-size:15px;margin:30px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--line)}
.sub{color:var(--dim);margin:0 0 2px}
.badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12px;
font-weight:600;white-space:nowrap}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:11px 13px}
.card .k{color:var(--dim);font-size:12px}
.card .v{font-size:19px;font-weight:600;margin-top:2px}
table{border-collapse:collapse;width:100%;font-size:13px}
th,td{padding:5px 9px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}
th:first-child,td:first-child,th:nth-child(2),td:nth-child(2),
th:nth-child(3),td:nth-child(3){text-align:left}
th{background:var(--card);position:sticky;top:0;cursor:pointer;font-weight:600;
color:var(--dim);border-bottom:2px solid var(--line)}
tbody tr:hover{background:var(--card)}
tr.zero td{color:var(--dim)}
tr.nogroup td:nth-child(3){color:#c0392b;font-weight:600}
.scroll{overflow-x:auto;max-height:640px;overflow-y:auto;border:1px solid var(--line);
border-radius:8px}
.bar{height:6px;background:var(--line);border-radius:3px;overflow:hidden;min-width:60px}
.bar i{display:block;height:100%;background:var(--accent)}
.step{display:flex;gap:11px;align-items:flex-start;padding:9px 0;border-bottom:1px solid var(--line)}
.step .id{font-family:ui-monospace,Menlo,Consolas,monospace;color:var(--dim);min-width:30px}
.step .d{color:var(--dim);font-size:13px}
.issue{padding:9px 0;border-bottom:1px solid var(--line);display:flex;gap:11px}
.par{display:grid;grid-template-columns:max-content 1fr;gap:3px 16px;font-size:13px}
.par b{color:var(--dim);font-weight:500}
code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
.controls{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 10px}
.tabs{display:flex;gap:6px;margin:0 0 10px;flex-wrap:wrap}
.tab{background:var(--card);border:1px solid var(--line);border-radius:6px;
padding:6px 13px;cursor:pointer;font:inherit;font-size:13px;color:var(--fg)}
.tab[aria-selected="true"]{background:var(--accent);border-color:var(--accent);color:#fff}
.stage{border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card)}
.stage iframe{display:block;width:100%;height:620px;border:0}
.flow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;
color:var(--dim);font-size:13px}
.flow b{color:var(--fg);font-weight:600}
.flow .ar{color:var(--accent);font-weight:700}
.d-up{color:#1a7f4b}.d-dn{color:#c0392b}
select,input{background:var(--card);color:var(--fg);border:1px solid var(--line);
border-radius:6px;padding:5px 8px;font:inherit;font-size:13px}
</style>""")
A('<div class="wrap">')
A(f'<h1>{esc(sv["title"])}</h1>')
A(f'<p class="sub">{esc(sv.get("note", ""))}</p>')
A(f'<p class="sub" style="font-size:12px">생성 {time.strftime("%Y-%m-%d %H:%M")} · '
f'집계 <code>{esc(os.path.basename(st["run_dir"]))}</code></p>')
A('<h2>실측</h2><div class="cards">')
cards = [
("사진", f'{st["photos"]:,}'),
("폴리곤", f'{st["total_polygons"]:,}'),
("라벨 종류", f'{sum(1 for r in rows if r["polygons"]):,} / {len(rows):,}'),
("집합", f'{sum(1 for g in gsum if g != "없음"):,}'),
("검출 면적 / 화면", f'{100 * tot_area / st["image_area_px"]:.1f}%'
if st.get("image_area_px") else "-"),
("집합 없는 폴리곤",
f'{gsum["없음"]["poly"]:,}' if "없음" in gsum else "0"),
]
if af:
cards.insert(2, ("병합 후 폴리곤",
f'{af["total_polygons"]:,} '
f'(-{100 * (tot_poly - af["total_polygons"]) / tot_poly:.0f}%)'))
for k, v in cards:
A(f'<div class="card"><div class="k">{esc(k)}</div><div class="v">{esc(v)}</div></div>')
A('</div>')
if cv:
per = sorted(cv["per_photo"], key=lambda r: r["covered"])
un = [100 * (1 - r["covered"]) for r in per]
A('<h2>미검출 — 무라벨 화면%</h2>')
A(f'<p class="sub" style="font-size:12px">마스크 합집합 기준(겹침 제거), '
f'축척 1/{1 / cv["scale"]:.0f}. 목표 5% 이하.</p>')
A('<div class="cards">')
for k, v in [("무라벨 평균", f'{100 * (1 - cv["covered_mean"]):.1f}%'),
("무라벨 중앙", f'{sorted(un)[len(un) // 2]:.1f}%'),
("최악", f'{100 * (1 - cv["covered_min"]):.1f}%'),
("5% 이하 사진", f'{sum(1 for u in un if u <= 5)} / {len(un)}')]:
A(f'<div class="card"><div class="k">{esc(k)}</div>'
f'<div class="v">{esc(v)}</div></div>')
A('</div>')
A('<div class="scroll" style="max-height:280px;margin-top:10px"><table><thead><tr>'
'<th>사진</th><th>폴리곤</th><th>무라벨%</th><th></th></tr></thead><tbody>')
for r in per:
u = 100 * (1 - r["covered"])
A(f'<tr><td>{esc(r["photo"].replace("_multi.json", ""))}</td>'
f'<td data-v="{r["segments"]}">{r["segments"]:,}</td>'
f'<td data-v="{u:.3f}">{u:.1f}%</td>'
f'<td><div class="bar"><i style="width:{min(u, 100):.1f}%;'
f'background:{"#c0392b" if u > 5 else "#1a7f4b"}"></i></div></td></tr>')
A('</tbody></table></div>')
if stages:
A('<h2>공정 보기 — 같은 사진, 단계별</h2>')
if af:
A(f'<div class="flow"><b>SAM 검출 {st["total_polygons"]:,} 폴리곤</b>'
f'<span>({sum(1 for r in st["rows"] if r["polygons"])} 라벨)</span>'
f'<span class="ar">→ 집합 병합 →</span>'
f'<b>{af["total_polygons"]:,} 폴리곤</b>'
f'<span>({sum(1 for r in af["rows"] if r["polygons"])} 집합)</span>'
f'<span>· 사진 {st["photos"]}장 기준</span></div>')
A('<div class="tabs">')
for i, (name, _) in enumerate(stages):
A(f'<button class="tab" role="tab" data-i="{i}" '
f'aria-selected="{"true" if i == 0 else "false"}">{esc(name)}</button>')
A('</div>')
for i, (_, path) in enumerate(stages):
A(f'<div class="stage" data-i="{i}"{"" if i == 0 else " hidden"}>'
f'<iframe src="{esc(path)}" loading="lazy" title="stage{i}"></iframe></div>')
A('<p class="sub" style="font-size:12px">왼쪽 목록에서 라벨을 켜고 끈다. '
'휠 확대, 드래그 이동. 새 창: '
+ " · ".join(f'<a href="{esc(p)}">{esc(n)}</a>' for n, p in stages) + '</p>')
A('<h2>단계</h2>')
for s in sv.get("steps", []):
A(f'<div class="step"><span class="id">{esc(s["id"])}</span>{badge(s["state"])}'
f'<div><div>{esc(s["name"])}</div>'
f'<div class="d">{esc(s.get("detail", ""))}</div></div></div>')
A('<h2>이슈 · 기각된 시도</h2>')
for i in sv.get("issues", []):
A(f'<div class="issue">{badge(i["state"])}<div>{esc(i["text"])}</div></div>')
apoly, aarea = defaultdict(int), defaultdict(float)
if af:
for r in af["rows"]:
apoly[r["group"]] += r["polygons"]
aarea[r["group"]] += r["area_px"]
atot = af["total_area_px"] or 1
A('<h2>집합별 합계</h2><div class="scroll"><table><thead><tr>'
'<th>집합</th><th>프롬프트(검출>0/전체)</th><th>폴리곤</th><th>폴리곤%</th>'
'<th>면적%</th>')
if af:
A('<th>병합 후 폴리곤</th><th>줄어든 비율</th><th>병합 후 면적%</th>')
A('<th></th></tr></thead><tbody>')
for g, v in sorted(gsum.items(), key=lambda t: -t[1]["poly"]):
share = 100 * v["area"] / tot_area
A(f'<tr><td>{esc(g)}</td><td>{v["used"]} / {v["prompts"]}</td>'
f'<td>{v["poly"]:,}</td><td>{100 * v["poly"] / tot_poly:.1f}%</td>'
f'<td>{share:.2f}%</td>')
if af:
n = apoly.get(g, 0)
drop = 100 * (v["poly"] - n) / v["poly"] if v["poly"] else 0
cls = "d-up" if n < v["poly"] else ""
A(f'<td>{n:,}</td><td class="{cls}">{drop:.0f}%</td>'
f'<td>{100 * aarea.get(g, 0.0) / atot:.2f}%</td>')
A(f'<td><div class="bar"><i style="width:{min(share, 100):.1f}%"></i></div>'
f'</td></tr>')
A('</tbody></table></div>')
if af:
A('<p class="sub" style="font-size:12px">병합 후 면적%는 병합 후 총검출면적 기준. '
'겹침을 빼지 않은 다각형 넓이 합이다.</p>')
cats = sorted({r["category"] for r in rows})
groups = sorted({r["group"] for r in rows})
A('<h2>카테고리 · 프롬프트별 검출</h2>')
A('<div class="controls">'
'<select id="fcat"><option value="">카테고리 전체</option>'
+ "".join(f'<option>{esc(c)}</option>' for c in cats) + '</select>'
'<select id="fgrp"><option value="">집합 전체</option>'
+ "".join(f'<option>{esc(g)}</option>' for g in groups) + '</select>'
'<input id="fq" placeholder="프롬프트 검색">'
'<label style="color:var(--dim)"><input type="checkbox" id="fzero"> 검출 0 만</label>'
'</div>')
A('<div class="scroll"><table id="t"><thead><tr>'
'<th>프롬프트</th><th>카테고리</th><th>집합</th><th>폴리곤</th><th>사진</th>'
'<th>면적%</th><th>중앙 면적 px</th><th>점수 중앙</th></tr></thead><tbody>')
for r in rows:
cls = []
if not r["polygons"]:
cls.append("zero")
if r["group"] == "없음":
cls.append("nogroup")
c = f' class="{" ".join(cls)}"' if cls else ""
dash = "-"
A(f'<tr{c} data-cat="{esc(r["category"])}" data-grp="{esc(r["group"])}">'
f'<td>{esc(r["prompt"])}</td><td>{esc(r["category"])}</td>'
f'<td>{esc(r["group"])}</td>'
f'<td data-v="{r["polygons"]}">{r["polygons"]:,}</td>'
f'<td data-v="{r["photos"]}">{r["photos"] or dash}</td>'
f'<td data-v="{r["area_share"]:.8f}">'
f'{100 * r["area_share"]:.2f}%</td>'
f'<td data-v="{r["area_median_px"]:.1f}">'
f'{r["area_median_px"]:,.0f}</td>'
f'<td data-v="{r["score_median"]:.4f}">'
f'{r["score_median"]:.3f}</td></tr>')
A('</tbody></table></div>')
if sv.get("params"):
A('<h2>설정</h2><div class="par">')
for k, v in sv["params"].items():
A(f'<b>{esc(k)}</b><span><code>{esc(v)}</code></span>')
A('</div>')
A('</div>')
A("""<script>
const tb=document.querySelector('#t tbody');
document.querySelectorAll('#t th').forEach((th,i)=>{th.onclick=()=>{
const rows=[...tb.rows], asc=th.dataset.asc!=='1';
th.dataset.asc=asc?'1':'0';
rows.sort((a,b)=>{
const x=a.cells[i].dataset.v, y=b.cells[i].dataset.v;
if(x!==undefined&&y!==undefined) return (asc?1:-1)*(parseFloat(x)-parseFloat(y));
return (asc?1:-1)*a.cells[i].textContent.localeCompare(b.cells[i].textContent,'ko');
});
rows.forEach(r=>tb.appendChild(r));
};});
function apply(){
const c=fcat.value,g=fgrp.value,q=fq.value.toLowerCase(),z=fzero.checked;
[...tb.rows].forEach(r=>{
const ok=(!c||r.dataset.cat===c)&&(!g||r.dataset.grp===g)
&&(!q||r.cells[0].textContent.toLowerCase().includes(q))
&&(!z||r.classList.contains('zero'));
r.hidden=!ok;
});
}
[fcat,fgrp,fzero].forEach(e=>e.onchange=apply); fq.oninput=apply;
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{
document.querySelectorAll('.tab').forEach(x=>x.setAttribute('aria-selected',x===b));
document.querySelectorAll('.stage').forEach(s=>s.hidden=s.dataset.i!==b.dataset.i);
});
</script>""")
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
with open(args.out, "w", encoding="utf-8") as fh:
fh.write("\n".join(parts))
print(f"저장: {args.out} ({os.path.getsize(args.out):,} bytes)")
if __name__ == "__main__":
main()
+189
View File
@@ -0,0 +1,189 @@
#!/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()
+108 -60
View File
@@ -151,7 +151,7 @@ def predict_tile(model, processor, find_stage_cls, tile_bgr, text_outs,
def run_pass(model, processor, find_stage_cls, image_bgr, boxes, captions, def run_pass(model, processor, find_stage_cls, image_bgr, boxes, captions,
conf, batch, tag): conf, batch, tag, mask_bytes=6 * 10**8):
"""타일 목록 전체에 프롬프트 집합을 돌린다. 반환: 전역 좌표 shape 리스트.""" """타일 목록 전체에 프롬프트 집합을 돌린다. 반환: 전역 좌표 shape 리스트."""
chunks = [captions[i:i + batch] for i in range(0, len(captions), batch)] chunks = [captions[i:i + batch] for i in range(0, len(captions), batch)]
shapes, t0 = [], time.time() shapes, t0 = [], time.time()
@@ -160,7 +160,8 @@ def run_pass(model, processor, find_stage_cls, image_bgr, boxes, captions,
for c in chunks] for c in chunks]
for i, (x0, y0, x1, y1) in enumerate(boxes, 1): for i, (x0, y0, x1, y1) in enumerate(boxes, 1):
got = predict_tile(model, processor, find_stage_cls, got = predict_tile(model, processor, find_stage_cls,
image_bgr[y0:y1, x0:x1], text_outs, chunks, conf) image_bgr[y0:y1, x0:x1], text_outs, chunks, conf,
mask_bytes)
for s in got: # 전역 좌표로 이동 for s in got: # 전역 좌표로 이동
s["points"] = [[p[0] + x0, p[1] + y0] for p in s["points"]] s["points"] = [[p[0] + x0, p[1] + y0] for p in s["points"]]
shapes.extend(got) shapes.extend(got)
@@ -174,20 +175,29 @@ def run_pass(model, processor, find_stage_cls, image_bgr, boxes, captions,
def main(): def main():
ap = argparse.ArgumentParser(description=__doc__, ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter) formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--input", required=True) ap.add_argument("--input", default=None)
ap.add_argument("--output", default=None, help="기본: 입력명_multi.jpg") ap.add_argument("--output", default=None, help="단일 이미지용 출력 경로")
ap.add_argument("--outdir", default=None,
help="결과 폴더. --input 이 폴더면 필수 — <이름>_multi.jpg/.json 로 저장")
ap.add_argument("--prompts", default="prompts/discovery_v1.txt") ap.add_argument("--prompts", default="prompts/discovery_v1.txt")
ap.add_argument("--wide-prompts", default=None, ap.add_argument("--wide-prompts", default=None,
help="타일보다 큰 대상 목록. 여기 적힌 라벨은 타일 패스에서 빼고 " help="타일보다 큰 대상 목록. 여기 적힌 라벨은 타일 패스에서 빼고 "
"이미지 전체를 한 장으로 검출한다 (예: prompts/wide_v1.txt)") "이미지 전체를 한 장으로 검출한다 (예: prompts/wide_v1.txt)")
ap.add_argument("--wide-in-tiles", action="store_true",
help="통짜 프롬프트를 타일 패스에서도 돌린다. 통짜는 대상 하나에 "
"인스턴스가 하나만 살아남아 도로 한쪽 차로·논 한 필지만 "
"잡히는 일이 있다. 겹치는 결과는 NMS 가 지운다. 느려진다")
ap.add_argument("--cols", type=int, default=9) ap.add_argument("--cols", type=int, default=9)
ap.add_argument("--rows", type=int, default=6) ap.add_argument("--rows", type=int, default=6)
ap.add_argument("--overlap", type=float, default=0.10) ap.add_argument("--overlap", type=float, default=0.10)
ap.add_argument("--conf", type=float, default=0.25) ap.add_argument("--conf", type=float, default=0.25)
ap.add_argument("--nms", type=float, default=0.40) ap.add_argument("--nms", type=float, default=0.40)
ap.add_argument("--batch", type=int, default=16, ap.add_argument("--device", default="cuda:0",
help="forward 1회에 넣을 최대 프롬프트 수 (기본 16). " help="사용할 GPU (예: cuda:0, cuda:1). --list-gpus 로 확인")
"RTX 3060 12GB 기준 16이 최적 — 32 이상은 VRAM 압박으로 4배 이상 느려짐") ap.add_argument("--list-gpus", action="store_true", help="GPU 목록만 출력하고 종료")
ap.add_argument("--batch", type=int, default=0,
help="forward 1회에 넣을 최대 프롬프트 수. 0이면 VRAM에서 자동 결정 "
"(12GB→16, 24GB→32). 3060 12GB에서 32는 4배 이상 느려짐")
ap.add_argument("--merge", action="store_true", help="같은 라벨 인접 폴리곤 병합") ap.add_argument("--merge", action="store_true", help="같은 라벨 인접 폴리곤 병합")
ap.add_argument("--merge-gap", type=int, default=8) ap.add_argument("--merge-gap", type=int, default=8)
ap.add_argument("--checkpoint", default=None, ap.add_argument("--checkpoint", default=None,
@@ -198,44 +208,67 @@ def main():
help="VRAM 사용 상한 비율. 넘으면 느려지는 대신 OOM 에러 (기본 0.92)") help="VRAM 사용 상한 비율. 넘으면 느려지는 대신 OOM 에러 (기본 0.92)")
args = ap.parse_args() args = ap.parse_args()
if not torch.cuda.is_available():
raise SystemExit("CUDA 사용 불가. CPU 폴백하지 않는다 — GPU 환경을 확인하라.")
if args.list_gpus:
for i in range(torch.cuda.device_count()):
p = torch.cuda.get_device_properties(i)
print(f" cuda:{i} {p.name} {p.total_memory/2**30:.0f}GB")
return
if not args.input:
raise SystemExit("--input 필요")
idx = torch.device(args.device).index or 0
if idx >= torch.cuda.device_count():
raise SystemExit(f"{args.device} 없음 — 장착 GPU {torch.cuda.device_count()}개. "
f"--list-gpus 로 확인하라.")
torch.cuda.set_device(idx)
# model_builder 가 device == "cuda" 문자열을 정확 비교하므로 "cuda:N" 을 주면
# 모델이 CPU에 남는다. 장치 선택은 set_device 로 하고 문자열은 "cuda" 고정.
device = "cuda"
vram_gb = torch.cuda.get_device_properties(idx).total_memory / 2**30
if args.batch <= 0: # VRAM에 맞춰 자동 결정
args.batch = 32 if vram_gb >= 20 else 16
print(f"GPU cuda:{idx} {torch.cuda.get_device_name(idx)} "
f"{vram_gb:.0f}GB batch={args.batch}")
captions = load_prompts(Path(args.prompts)) captions = load_prompts(Path(args.prompts))
if not captions: if not captions:
print(f"프롬프트 없음: {args.prompts}") print(f"프롬프트 없음: {args.prompts}")
return return
img_path = Path(args.input) src = Path(args.input)
image_bgr = cv2.imdecode(np.fromfile(str(img_path), dtype=np.uint8), if src.is_dir():
cv2.IMREAD_COLOR) images = sorted(p for p in src.iterdir()
if image_bgr is None: if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".tif"))
print(f"이미지 로드 실패: {img_path}") if not images:
return raise SystemExit(f"이미지 없음: {src}")
H, W = image_bgr.shape[:2] if not args.outdir:
raise SystemExit("폴더 입력에는 --outdir 필요")
else:
images = [src]
wide = load_prompts(Path(args.wide_prompts)) if args.wide_prompts else [] wide = load_prompts(Path(args.wide_prompts)) if args.wide_prompts else []
fine = [c for c in captions if c not in set(wide)] fine = captions if args.wide_in_tiles else [c for c in captions if c not in set(wide)]
boxes = tile_boxes(W, H, args.cols, args.rows, args.overlap)
def nchunk(n): def nchunk(n):
return (n + args.batch - 1) // args.batch return (n + args.batch - 1) // args.batch
print(f"이미지 : {W}×{H}") print(f"입력 : {len(images)}")
print(f"타일 : {args.cols}×{args.rows}={len(boxes)}개 overlap={args.overlap*100:.0f}%") print(f"타일 : {args.cols}×{args.rows}={args.cols*args.rows}"
print(f"타일 패스 : 프롬프트 {len(fine)}개 → forward {nchunk(len(fine))*len(boxes)}") f"overlap={args.overlap*100:.0f}%")
print(f"타일 패스 : 프롬프트 {len(fine)}개 → forward "
f"{nchunk(len(fine))*args.cols*args.rows}회/장")
if wide: if wide:
print(f"통짜 패스 : 프롬프트 {len(wide)}개 → forward {nchunk(len(wide))} " print(f"통짜 패스 : 프롬프트 {len(wide)}개 → forward {nchunk(len(wide))}/장")
f"(타일보다 큰 대상)")
print(f"conf={args.conf} nms={args.nms}\n") print(f"conf={args.conf} nms={args.nms}\n")
from sam3.model_builder import build_sam3_image_model from sam3.model_builder import build_sam3_image_model
from sam3.model.sam3_image_processor import Sam3Processor from sam3.model.sam3_image_processor import Sam3Processor
from sam3.model.data_misc import FindStage from sam3.model.data_misc import FindStage
if not torch.cuda.is_available():
raise SystemExit("CUDA 사용 불가. CPU 폴백하지 않는다 — GPU 환경을 확인하라.")
device = "cuda"
# VRAM 상한을 걸어 드라이버가 시스템 메모리로 폴백(10배 이상 느려짐)하기 전에 # VRAM 상한을 걸어 드라이버가 시스템 메모리로 폴백(10배 이상 느려짐)하기 전에
# OOM으로 실패하게 만든다 # OOM으로 실패하게 만든다
torch.cuda.set_per_process_memory_fraction(args.vram_fraction) torch.cuda.set_per_process_memory_fraction(args.vram_fraction, idx)
torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True
@@ -246,47 +279,62 @@ def main():
bpe_path=bpe_path, device=device, checkpoint_path=ckpt_path) bpe_path=bpe_path, device=device, checkpoint_path=ckpt_path)
processor = Sam3Processor(model, confidence_threshold=args.conf, device=device) processor = Sam3Processor(model, confidence_threshold=args.conf, device=device)
t0 = time.time() mask_bytes = int(6 * 10**8 * vram_gb / 12) # VRAM에 비례해 업샘플 청크 조정
all_shapes = run_pass(model, processor, FindStage, image_bgr, boxes, t_all = time.time()
fine, args.conf, args.batch, "타일") for n, img_path in enumerate(images, 1):
if wide: print(f"\n===== [{n}/{len(images)}] {img_path.name} =====")
# 타일보다 큰 대상은 이미지 전체를 한 장으로 보고 검출 image_bgr = cv2.imdecode(np.fromfile(str(img_path), dtype=np.uint8),
all_shapes += run_pass(model, processor, FindStage, image_bgr, cv2.IMREAD_COLOR)
[(0, 0, W, H)], wide, args.conf, args.batch, "통짜") if image_bgr is None:
raise SystemExit(f"이미지 로드 실패: {img_path}")
H, W = image_bgr.shape[:2]
boxes = tile_boxes(W, H, args.cols, args.rows, args.overlap)
print(f"\n검출 {len(all_shapes)}개 → NMS(iou={args.nms})...") t0 = time.time()
all_shapes = nms_shapes(all_shapes, iou_thresh=args.nms) all_shapes = run_pass(model, processor, FindStage, image_bgr, boxes,
print(f"NMS 후 {len(all_shapes)}") fine, args.conf, args.batch, "타일", mask_bytes)
if wide:
# 타일보다 큰 대상은 이미지 전체를 한 장으로 보고 검출
all_shapes += run_pass(model, processor, FindStage, image_bgr,
[(0, 0, W, H)], wide, args.conf, args.batch,
"통짜", mask_bytes)
if args.merge: print(f" 검출 {len(all_shapes)}개 → NMS(iou={args.nms})...")
before = len(all_shapes) all_shapes = nms_shapes(all_shapes, iou_thresh=args.nms)
all_shapes = merge_adjacent(all_shapes, gap=args.merge_gap) print(f" NMS 후 {len(all_shapes)}")
print(f"병합(gap={args.merge_gap}px) {before}{len(all_shapes)}") if args.merge:
print(f"{time.time()-t0:.0f}\n") before = len(all_shapes)
all_shapes = merge_adjacent(all_shapes, gap=args.merge_gap)
print(f" 병합(gap={args.merge_gap}px) {before}{len(all_shapes)}")
print(f" {time.time()-t0:.0f}")
analyze_labels(all_shapes) vis = draw_everything(image_bgr, all_shapes, args.cols, args.rows)
h, w = vis.shape[:2]
if max(h, w) > 4096:
s = 4096 / max(h, w)
vis = cv2.resize(vis, (int(w * s), int(h * s)))
vis = draw_everything(image_bgr, all_shapes, args.cols, args.rows) if args.outdir:
h, w = vis.shape[:2] out_path = Path(args.outdir) / (img_path.stem + "_multi.jpg")
if max(h, w) > 4096: elif args.output:
s = 4096 / max(h, w) out_path = Path(args.output)
vis = cv2.resize(vis, (int(w * s), int(h * s))) else:
out_path = img_path.parent / (img_path.stem + "_multi.jpg")
out_path.parent.mkdir(parents=True, exist_ok=True)
cv2.imencode(".jpg", vis, [cv2.IMWRITE_JPEG_QUALITY, 93])[1].tofile(str(out_path))
out_path = (Path(args.output) if args.output json_path = out_path.with_suffix(".json")
else img_path.parent / (img_path.stem + "_multi.jpg")) json_path.write_text(json.dumps({
out_path.parent.mkdir(parents=True, exist_ok=True) "source_image": str(img_path),
cv2.imencode(".jpg", vis, [cv2.IMWRITE_JPEG_QUALITY, 93])[1].tofile(str(out_path)) "total_segments": len(all_shapes),
print(f"\n저장: {out_path}") "label_counts": dict(Counter(s.get("label", "") for s in all_shapes)),
"segments": [{"label": s.get("label", ""), "score": s.get("score", 0),
"bbox": list(_bbox(s["points"])), "points": s["points"]}
for s in all_shapes],
}, ensure_ascii=False, indent=2), encoding="utf-8")
print(f" 저장: {out_path.name} / {json_path.name}")
json_path = out_path.with_suffix(".json") print(f"\n전체 {len(images)}장 완료 — {time.time()-t_all:.0f}")
json_path.write_text(json.dumps({
"total_segments": len(all_shapes),
"label_counts": dict(Counter(s.get("label", "") for s in all_shapes)),
"segments": [{"label": s.get("label", ""), "score": s.get("score", 0),
"bbox": list(_bbox(s["points"])), "points": s["points"]}
for s in all_shapes],
}, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"라벨 데이터: {json_path}")
if sys.platform == "win32": # 완료 알림음 if sys.platform == "win32": # 완료 알림음
import winsound import winsound
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python
"""여러 사진의 SAM 마스크를 3D 면에 투표해 면별 라벨을 정한다.
면 하나가 여러 사진에 보인다 (가림을 뺀 실제 가시 뷰가 중앙 8장). 각 사진에서
그 면의 중심 픽셀에 어떤 라벨이 있는지 읽어 모으고 다수결한다.
득표가 갈리거나 뷰가 모자란 면은 강제로 정하지 않고 0(미결정)으로 남긴다.
틀린 라벨보다 없는 라벨이 낫다 — 학습 데이터에서 0은 ignore_index로 빠진다.
폴리곤을 라벨맵으로 구울 때 순서가 중요하다. 면적 큰 것부터 그려서 작은 것이
위에 오게 한다. 안 그러면 도로 폴리곤이 그 위의 차량을 덮어버린다.
한 면이 보이는데 어느 폴리곤에도 안 걸리는 경우가 있다 (SAM 커버리지가
장당 87%). 그건 기권으로 세지 어느 클래스 표도 아니다.
작은 것은 이 다수결에서 진다. 도로/건물 폴리곤이 크니 차량 위를 스치는 뷰가
몇 장만 있어도 득표율이 0.5 아래로 내려간다. 그래서 --rescue-pred 를 주면
"투표에는 졌지만 그 클래스 표가 있고 학습된 모델도 같은 클래스라고 하는 면"
되돌린다. 두 증거가 독립이므로 정밀도를 거의 안 깎고 재현을 올린다.
"""
import argparse
import glob
import json
import os
import time
from collections import OrderedDict
import cv2
import numpy as np
def log(msg):
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def poly_area(pts):
x, y = pts[:, 0], pts[:, 1]
return abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) / 2
def build_label_map(seg_path, W, H, cls_id):
"""병합 JSON -> 라벨맵 (int16, 0=없음). 면적 내림차순으로 그린다."""
with open(seg_path, encoding="utf-8") as fh:
segs = json.load(fh)["segments"]
items = []
for s in segs:
pts = np.asarray(s["points"], dtype=np.float64).reshape(-1, 2)
if len(pts) < 3:
continue
lab = s["label"]
if lab not in cls_id:
continue
items.append((poly_area(pts), cls_id[lab], pts))
items.sort(key=lambda t: -t[0])
lm = np.zeros((H, W), dtype=np.int16)
for _, cid, pts in items:
cv2.fillPoly(lm, [np.round(pts).astype(np.int32)], int(cid))
return lm, len(items)
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--vis-dir", required=True, help="rasterize_visibility.py 출력")
ap.add_argument("--seg-dir", required=True, help="*_merged.json 폴더")
ap.add_argument("--out-dir", required=True)
ap.add_argument("--min-votes", type=int, default=2,
help="이보다 표가 적으면 미결정")
ap.add_argument("--min-ratio", type=float, default=0.5,
help="1위 득표율이 이보다 낮으면 미결정")
ap.add_argument("--rescue-pred",
help="PointVector 예측 npy (면 순서 동일). 주면 구제 규칙을 켠다")
ap.add_argument("--rescue-pred-classes",
help="--rescue-pred 의 클래스 목록 JSON (classes 키 또는 배열)")
ap.add_argument("--rescue-class", default="vehicle", help="구제할 클래스 이름")
ap.add_argument("--rescue-min-votes", type=int, default=1,
help="구제하려면 그 클래스 표가 최소 몇 개여야 하는지")
args = ap.parse_args()
if args.rescue_pred and not args.rescue_pred_classes:
ap.error("--rescue-pred 를 주면 --rescue-pred-classes 도 필요하다")
os.makedirs(args.out_dir, exist_ok=True)
with open(os.path.join(args.vis_dir, "visibility.json"), encoding="utf-8") as fh:
vmeta = json.load(fh)
n_face = vmeta["tile_faces"]
W, H = vmeta["image_size"]
names = [r["image"] for r in vmeta["per_camera"]]
vis = np.unpackbits(np.load(os.path.join(args.vis_dir, "visible.npy")),
axis=1, count=n_face).astype(bool)
pu = np.load(os.path.join(args.vis_dir, "pix_u.npy"))
pv = np.load(os.path.join(args.vis_dir, "pix_v.npy"))
log(f"{n_face:,} · 카메라 {len(names)} · 영상 {W}x{H}")
# 클래스 목록은 실제로 등장한 라벨에서 만든다. 지어내지 않는다.
labels = OrderedDict()
seg_files = {}
for n in names:
p = os.path.join(args.seg_dir, os.path.splitext(n)[0] + "_multi_merged.json")
if not os.path.isfile(p):
raise FileNotFoundError(f"병합 JSON 없음: {p}")
seg_files[n] = p
with open(p, encoding="utf-8") as fh:
for s in json.load(fh)["segments"]:
labels.setdefault(s["label"], 0)
labels[s["label"]] += 1
cls_names = ["미결정"] + sorted(labels, key=lambda k: -labels[k])
cls_id = {c: i for i, c in enumerate(cls_names) if i > 0}
n_cls = len(cls_names)
log(f"클래스 {n_cls} (0=미결정 포함): {', '.join(cls_names[1:])}")
votes = np.zeros((n_face, n_cls), dtype=np.int16) # 0열은 안 쓴다
seen = np.zeros(n_face, dtype=np.int16)
t0 = time.time()
for i, n in enumerate(names):
lm, npoly = build_label_map(seg_files[n], W, H, cls_id)
m = vis[i]
idx = np.flatnonzero(m)
lab = lm[pv[i][idx], pu[i][idx]]
seen[idx] += 1
hit = lab > 0
np.add.at(votes, (idx[hit], lab[hit]), 1)
log(f" [{i + 1}/{len(names)}] {n} 폴리곤 {npoly} · 가시면 {len(idx):,} "
f"· 라벨 적중 {int(hit.sum()):,} ({hit.mean():.1%})")
del lm
votes[:, 0] = 0
total = votes.sum(axis=1)
best = votes.argmax(axis=1).astype(np.int16)
top = votes.max(axis=1)
with np.errstate(divide="ignore", invalid="ignore"):
ratio = np.where(total > 0, top / np.maximum(total, 1), 0.0)
pred = np.where((total >= args.min_votes) & (ratio >= args.min_ratio),
best, 0).astype(np.int16)
rescue = None
if args.rescue_pred:
if args.rescue_class not in cls_id:
raise ValueError(f"투표 클래스에 없다: {args.rescue_class}")
with open(args.rescue_pred_classes, encoding="utf-8") as fh:
j = json.load(fh)
pcls = j["classes"] if isinstance(j, dict) else j
if args.rescue_class not in pcls:
raise ValueError(f"예측 클래스에 없다: {args.rescue_class}")
mp = np.load(args.rescue_pred)
if len(mp) != n_face:
raise ValueError(f"예측 {len(mp):,} != 면 {n_face:,}")
rid = cls_id[args.rescue_class]
take = ((pred != rid)
& (votes[:, rid] >= args.rescue_min_votes)
& (mp == pcls.index(args.rescue_class)))
was = pred[take].copy()
pred[take] = rid
u, c = np.unique(was, return_counts=True)
rescue = {cls_names[k]: int(v) for k, v in sorted(zip(u, c), key=lambda t: -t[1])}
log("")
log(f"구제({args.rescue_class}, 표>={args.rescue_min_votes} & 모델 동의): "
f"{int(take.sum()):,}")
for k, v in rescue.items():
log(f" {k} 에서 {v:,}")
log("")
log(f"투표 {time.time() - t0:.0f}")
log(f"가시 뷰 0인 면 {int((seen == 0).sum()):,} ({(seen == 0).mean():.2%})")
log(f"보이지만 라벨 표가 0인 면 {int(((seen > 0) & (total == 0)).sum()):,} "
f"({((seen > 0) & (total == 0)).mean():.2%})")
log(f"표는 있으나 기준 미달 {int(((total > 0) & (pred == 0)).sum()):,} "
f"({((total > 0) & (pred == 0)).mean():.2%})")
log("")
cnt = np.bincount(pred, minlength=n_cls)
log(f"{'id':>3} {'클래스':<20} {'':>10} {'비율':>7} {'평균득표율':>9}")
for c in np.argsort(-cnt):
if cnt[c] == 0:
continue
r = ratio[pred == c].mean() if c > 0 and cnt[c] else float("nan")
log(f"{c:>3} {cls_names[c]:<20} {cnt[c]:>10,} {cnt[c] / n_face:>6.2%} "
f"{'' if c == 0 else f'{r:>8.1%}'}")
np.save(os.path.join(args.out_dir, "face_label.npy"), pred)
np.save(os.path.join(args.out_dir, "face_votes.npy"), votes)
with open(os.path.join(args.out_dir, "vote.json"), "w", encoding="utf-8") as fh:
json.dump({
"vis_dir": os.path.abspath(args.vis_dir),
"seg_dir": os.path.abspath(args.seg_dir),
"faces": int(n_face),
"cameras": len(names),
"classes": cls_names,
"min_votes": args.min_votes,
"min_ratio": args.min_ratio,
"faces_no_view": int((seen == 0).sum()),
"faces_no_label_vote": int(((seen > 0) & (total == 0)).sum()),
"faces_below_threshold": int(((total > 0) & (pred == 0)).sum()),
"counts": {cls_names[c]: int(cnt[c]) for c in range(n_cls) if cnt[c]},
"rescue": None if rescue is None else {
"pred": os.path.abspath(args.rescue_pred),
"cls": args.rescue_class,
"min_votes": args.rescue_min_votes,
"faces": int(sum(rescue.values())),
"from": rescue,
},
"note": "face_label.npy는 타일 면 인덱스 순서. 0 = 미결정 = ignore_index",
}, fh, indent=2, ensure_ascii=False)
log(f"기록: {args.out_dir}")
if __name__ == "__main__":
main()