@
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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)이 다른 비행이라 주차 차량 위치가 다르다."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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단계는 미검출을 없애는 것이다. 지표는 '무라벨 화면%' — 마스크 합집합으로 재서 겹침을 뺀 값. 목표 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 & Filler</option><option>Farmland</option><option>General Categories</option><option>Industrial Materials, Storage & Objects by Color</option><option>Ripening & Bare Field</option><option>Road Infrastructure</option><option>Roofs & Structures by Color</option><option>Vehicles & 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 & Structures by Color" data-grp="building"><td>blue roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>red roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>green roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>gray roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>dark gray roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>black roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>white roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>silver metal roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>orange roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>brown roof</td><td>Roofs & 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 & Structures by Color" data-grp="building"><td>yellow roof</td><td>Roofs & 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 & Transportation by Color" data-grp="vehicle"><td>white vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>black vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>silver vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>gray vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>red vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>blue vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>yellow vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>orange vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>green vehicle</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>white cargo truck</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>blue cargo truck</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>commercial bus</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>tractor</td><td>Vehicles & 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 & Transportation by Color" data-grp="vehicle"><td>excavator</td><td>Vehicles & 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 & Objects by Color" data-grp="stored material"><td>blue plastic drum</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>orange plastic barrel</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>yellow plastic container</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>white industrial tank</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>metallic storage tank</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>blue waterproof tarp</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>green waterproof tarp</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>black protective sheet</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="stored material"><td>white canvas canopy</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="building"><td>plastic greenhouse</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="building"><td>vinyl greenhouse tunnel</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="building"><td>greenhouse roof</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="fence"><td>gray metal fence</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="fence"><td>green wire fence</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="fence"><td>red roadside barrier</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="없음"><td>yellow safety cone</td><td>Industrial Materials, Storage & 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 & Objects by Color" data-grp="없음"><td>orange traffic cone</td><td>Industrial Materials, Storage & 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 & Filler" data-grp="vegetation"><td>overgrown weeds</td><td>Boundary & 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 & Filler" data-grp="vegetation"><td>shrub</td><td>Boundary & 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 & Filler" data-grp="slope"><td>grass covered embankment</td><td>Boundary & 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 & Filler" data-grp="ground"><td>earth bank</td><td>Boundary & 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 & Filler" data-grp="ground"><td>dirt path</td><td>Boundary & 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 & Bare Field" data-grp="farmland"><td>ripening rice field</td><td>Ripening & 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 & Bare Field" data-grp="vegetation"><td>yellow grass field</td><td>Ripening & 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 & Bare Field" data-grp="farmland"><td>harvested field</td><td>Ripening & 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 & Bare Field" data-grp="road"><td>gravel shoulder</td><td>Ripening & 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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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,
|
||||
conf, batch, tag):
|
||||
conf, batch, tag, mask_bytes=6 * 10**8):
|
||||
"""타일 목록 전체에 프롬프트 집합을 돌린다. 반환: 전역 좌표 shape 리스트."""
|
||||
chunks = [captions[i:i + batch] for i in range(0, len(captions), batch)]
|
||||
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 i, (x0, y0, x1, y1) in enumerate(boxes, 1):
|
||||
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: # 전역 좌표로 이동
|
||||
s["points"] = [[p[0] + x0, p[1] + y0] for p in s["points"]]
|
||||
shapes.extend(got)
|
||||
@@ -174,20 +175,29 @@ def run_pass(model, processor, find_stage_cls, image_bgr, boxes, captions,
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--input", required=True)
|
||||
ap.add_argument("--output", default=None, help="기본: 입력명_multi.jpg")
|
||||
ap.add_argument("--input", default=None)
|
||||
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("--wide-prompts", default=None,
|
||||
help="타일보다 큰 대상 목록. 여기 적힌 라벨은 타일 패스에서 빼고 "
|
||||
"이미지 전체를 한 장으로 검출한다 (예: 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("--rows", type=int, default=6)
|
||||
ap.add_argument("--overlap", type=float, default=0.10)
|
||||
ap.add_argument("--conf", type=float, default=0.25)
|
||||
ap.add_argument("--nms", type=float, default=0.40)
|
||||
ap.add_argument("--batch", type=int, default=16,
|
||||
help="forward 1회에 넣을 최대 프롬프트 수 (기본 16). "
|
||||
"RTX 3060 12GB 기준 16이 최적 — 32 이상은 VRAM 압박으로 4배 이상 느려짐")
|
||||
ap.add_argument("--device", default="cuda:0",
|
||||
help="사용할 GPU (예: cuda:0, cuda:1). --list-gpus 로 확인")
|
||||
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-gap", type=int, default=8)
|
||||
ap.add_argument("--checkpoint", default=None,
|
||||
@@ -198,44 +208,67 @@ def main():
|
||||
help="VRAM 사용 상한 비율. 넘으면 느려지는 대신 OOM 에러 (기본 0.92)")
|
||||
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))
|
||||
if not captions:
|
||||
print(f"프롬프트 없음: {args.prompts}")
|
||||
return
|
||||
|
||||
img_path = Path(args.input)
|
||||
image_bgr = cv2.imdecode(np.fromfile(str(img_path), dtype=np.uint8),
|
||||
cv2.IMREAD_COLOR)
|
||||
if image_bgr is None:
|
||||
print(f"이미지 로드 실패: {img_path}")
|
||||
return
|
||||
H, W = image_bgr.shape[:2]
|
||||
src = Path(args.input)
|
||||
if src.is_dir():
|
||||
images = sorted(p for p in src.iterdir()
|
||||
if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".tif"))
|
||||
if not images:
|
||||
raise SystemExit(f"이미지 없음: {src}")
|
||||
if not args.outdir:
|
||||
raise SystemExit("폴더 입력에는 --outdir 필요")
|
||||
else:
|
||||
images = [src]
|
||||
|
||||
wide = load_prompts(Path(args.wide_prompts)) if args.wide_prompts else []
|
||||
fine = [c for c in captions if c not in set(wide)]
|
||||
boxes = tile_boxes(W, H, args.cols, args.rows, args.overlap)
|
||||
fine = captions if args.wide_in_tiles else [c for c in captions if c not in set(wide)]
|
||||
|
||||
def nchunk(n):
|
||||
return (n + args.batch - 1) // args.batch
|
||||
|
||||
print(f"이미지 : {W}×{H}")
|
||||
print(f"타일 : {args.cols}×{args.rows}={len(boxes)}개 overlap={args.overlap*100:.0f}%")
|
||||
print(f"타일 패스 : 프롬프트 {len(fine)}개 → forward {nchunk(len(fine))*len(boxes)}회")
|
||||
print(f"입력 : {len(images)}장")
|
||||
print(f"타일 : {args.cols}×{args.rows}={args.cols*args.rows}개 "
|
||||
f"overlap={args.overlap*100:.0f}%")
|
||||
print(f"타일 패스 : 프롬프트 {len(fine)}개 → forward "
|
||||
f"{nchunk(len(fine))*args.cols*args.rows}회/장")
|
||||
if wide:
|
||||
print(f"통짜 패스 : 프롬프트 {len(wide)}개 → forward {nchunk(len(wide))}회 "
|
||||
f"(타일보다 큰 대상)")
|
||||
print(f"통짜 패스 : 프롬프트 {len(wide)}개 → forward {nchunk(len(wide))}회/장")
|
||||
print(f"conf={args.conf} nms={args.nms}\n")
|
||||
|
||||
from sam3.model_builder import build_sam3_image_model
|
||||
from sam3.model.sam3_image_processor import Sam3Processor
|
||||
from sam3.model.data_misc import FindStage
|
||||
|
||||
if not torch.cuda.is_available():
|
||||
raise SystemExit("CUDA 사용 불가. CPU 폴백하지 않는다 — GPU 환경을 확인하라.")
|
||||
device = "cuda"
|
||||
# VRAM 상한을 걸어 드라이버가 시스템 메모리로 폴백(10배 이상 느려짐)하기 전에
|
||||
# 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.cudnn.allow_tf32 = True
|
||||
|
||||
@@ -246,47 +279,62 @@ def main():
|
||||
bpe_path=bpe_path, device=device, checkpoint_path=ckpt_path)
|
||||
processor = Sam3Processor(model, confidence_threshold=args.conf, device=device)
|
||||
|
||||
t0 = time.time()
|
||||
all_shapes = run_pass(model, processor, FindStage, image_bgr, boxes,
|
||||
fine, args.conf, args.batch, "타일")
|
||||
if wide:
|
||||
# 타일보다 큰 대상은 이미지 전체를 한 장으로 보고 검출
|
||||
all_shapes += run_pass(model, processor, FindStage, image_bgr,
|
||||
[(0, 0, W, H)], wide, args.conf, args.batch, "통짜")
|
||||
mask_bytes = int(6 * 10**8 * vram_gb / 12) # VRAM에 비례해 업샘플 청크 조정
|
||||
t_all = time.time()
|
||||
for n, img_path in enumerate(images, 1):
|
||||
print(f"\n===== [{n}/{len(images)}] {img_path.name} =====")
|
||||
image_bgr = cv2.imdecode(np.fromfile(str(img_path), dtype=np.uint8),
|
||||
cv2.IMREAD_COLOR)
|
||||
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})...")
|
||||
all_shapes = nms_shapes(all_shapes, iou_thresh=args.nms)
|
||||
print(f"NMS 후 {len(all_shapes)}개")
|
||||
t0 = time.time()
|
||||
all_shapes = run_pass(model, processor, FindStage, image_bgr, boxes,
|
||||
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:
|
||||
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}초\n")
|
||||
print(f" 검출 {len(all_shapes)}개 → NMS(iou={args.nms})...")
|
||||
all_shapes = nms_shapes(all_shapes, iou_thresh=args.nms)
|
||||
print(f" NMS 후 {len(all_shapes)}개")
|
||||
if args.merge:
|
||||
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)
|
||||
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)))
|
||||
if args.outdir:
|
||||
out_path = Path(args.outdir) / (img_path.stem + "_multi.jpg")
|
||||
elif args.output:
|
||||
out_path = Path(args.output)
|
||||
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
|
||||
else 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))
|
||||
print(f"\n저장: {out_path}")
|
||||
json_path = out_path.with_suffix(".json")
|
||||
json_path.write_text(json.dumps({
|
||||
"source_image": str(img_path),
|
||||
"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" 저장: {out_path.name} / {json_path.name}")
|
||||
|
||||
json_path = out_path.with_suffix(".json")
|
||||
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}")
|
||||
print(f"\n전체 {len(images)}장 완료 — {time.time()-t_all:.0f}초")
|
||||
|
||||
if sys.platform == "win32": # 완료 알림음
|
||||
import winsound
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user