Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
988049e832 | ||
|
|
833816061b | ||
|
|
b754694ef8 | ||
|
|
28bee39196 |
@@ -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,155 @@
|
||||
# 병합 그룹 정의 — 대괄호 안이 병합 후 대표 라벨.
|
||||
# 같은 그룹에 속한 라벨끼리 외곽선이 --gap px 이내로 인접하면 하나로 합친다.
|
||||
# 어느 그룹에도 없는 라벨은 건드리지 않는다.
|
||||
#
|
||||
# 여기서 나누지 않는 구분이 있다. SAM은 사진만 보므로 미터도 규모도 모른다.
|
||||
# 아래는 2D가 아니라 3D 메시에서 갈라야 정확하다:
|
||||
# 낮은수목 / 수목 CSF 지면 위 높이
|
||||
# 수목 / 숲 [vegetation] 연결성분 면적
|
||||
# 지형 / 경사면 면 법선 경사각
|
||||
# 건물 / 빌딩 [building] 연결성분 면적·높이
|
||||
# 그래서 tree canopy와 dense forest, building과 residential house는
|
||||
# 일부러 같은 그룹에 둔다 — 2D에서 갈라봐야 중복 마스크만 나온다.
|
||||
|
||||
# factory structure 는 일부러 어느 집합에도 넣지 않는다 (2026-09-02).
|
||||
# merge_labels.py 는 집합 밖 라벨을 그대로 통과시키므로 독립 클래스로 남는다.
|
||||
# [building] 에 두면 공장 구조물이 일반 건물에 흡수돼 병합 후 되돌릴 수 없다.
|
||||
# 16장 실측 86개.
|
||||
|
||||
# [building]
|
||||
building
|
||||
building rooftop
|
||||
building facade
|
||||
residential house
|
||||
warehouse
|
||||
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,180 @@
|
||||
# ==========================================================
|
||||
# 보류 (2026-09-02). 세부 세그멘테이션은 후순위로 미뤘다.
|
||||
# 현재 쓰는 정의는 configs/merge_groups_v4.txt 다.
|
||||
#
|
||||
# 이 파일은 교통시설물을 뭉치지 않고 가른 안이다. 16장 실측으로
|
||||
# structure 1,079 -> utility pole 608 / streetlight pole 372 /
|
||||
# road sign 358 / culvert 9, fence 702 -> guardrail 668 /
|
||||
# fence 46 / roadside barrier 17 로 갈라지는 것까지 확인했다.
|
||||
# 라벨 14종 -> 19종, 폴리곤 6,370 -> 6,667.
|
||||
#
|
||||
# 다시 쓸 때 주의: culvert 9개, roadside barrier 17개는 16장 기준
|
||||
# 표본이 너무 적다. 69장 전체를 돌린 뒤 클래스로 쓸지 판단해라.
|
||||
# ==========================================================
|
||||
|
||||
# 병합 그룹 정의 — 대괄호 안이 병합 후 대표 라벨.
|
||||
# 같은 그룹에 속한 라벨끼리 외곽선이 --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]
|
||||
utility pole
|
||||
|
||||
# [streetlight pole]
|
||||
streetlight pole
|
||||
|
||||
# [road sign]
|
||||
road sign
|
||||
|
||||
# [structure]
|
||||
culvert
|
||||
|
||||
# [fence]
|
||||
gray metal fence
|
||||
green wire fence
|
||||
|
||||
# [guardrail]
|
||||
guardrail
|
||||
metal guardrail
|
||||
|
||||
# [roadside barrier]
|
||||
red roadside barrier
|
||||
|
||||
# [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
|
||||
|
||||
# 콘은 일부러 어느 집합에도 넣지 않는다.
|
||||
# merge_labels.py 는 집합에 없는 라벨을 건드리지 않고 그대로 통과시킨다
|
||||
# (merge_by_group 의 passthrough). 그래서 콘은 버려지지 않고 독립 클래스로 남는다.
|
||||
# [cone] 으로 묶으면 나란히 선 콘이 gap 2px 에서 한 덩어리가 되어 개수를 잃는다.
|
||||
# 16장 실측: orange traffic cone 114 · yellow safety cone 6 이 그대로 살아 있다.
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"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 (11집합 + 집합 밖 3종: factory structure, 콘 2종)",
|
||||
"타일": "6×4 = 24개, overlap 10%, 타일 크기 1365×1365. 기준선은 4×3 (2048×1820)",
|
||||
"검출 설정": "conf 0.10 · NMS iou 0.40 · 같은 라벨 병합 gap 8px · --wide-in-tiles. 기준선 conf 0.25, 통짜 분리",
|
||||
"공정 보기 사진": "확정 설정 8장 (기준선 무라벨 3.5~52.1% 분포에서 고름)",
|
||||
"확정 후보 설정": "prompts/discovery_v4.txt (94개) · configs/merge_groups_v4.txt · --wide-in-tiles · --cols 6 --rows 4 · --conf 0.10 · NMS 0.40 · --merge (gap 8px) · 병합 gap 2px",
|
||||
"claim 규칙": "configs/claim_rules.txt — vehicle : 0.5 : 지붕 11종 + 방수포/보호시트"
|
||||
},
|
||||
"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": "확정 설정 16장 검증",
|
||||
"state": "done",
|
||||
"detail": "16장 실측 무라벨(conf 0.10): 최소 0.5%(0423) ~ 최대 6.4%(0581), 평균 3.44%. 15/16 장이 5% 이하. 같은 16장 기준선 평균 19.9%. 소요 466~907초/장(평균 585초). 예상이 틀린 곳: 물 많은 사진이 위험할 것으로 봤으나 0423 이 0.5% 로 최저였다 — water surface/reservoir 검출이 0이어도 다른 프롬프트가 그 면적을 덮는다."
|
||||
},
|
||||
{
|
||||
"id": "M2",
|
||||
"name": "집합 정의 조정 — 보류",
|
||||
"state": "blocked",
|
||||
"detail": "교통시설물을 가르는 안(configs/merge_groups_v5.txt)을 만들고 16장으로 확인했다: structure 1,079 → utility pole 608 / streetlight pole 372 / road sign 358 / culvert 9, fence 702 → guardrail 668 / fence 46 / roadside barrier 17. 라벨 14 → 19종. 사용자 지시로 보류하고 v4 로 되돌렸다. 세부 세그멘테이션은 후순위."
|
||||
},
|
||||
{
|
||||
"id": "M3",
|
||||
"name": "gap 값 조정 — 2px 유지",
|
||||
"state": "done",
|
||||
"detail": "사진 4장(폴리곤 3,280)으로 gap 0~32px 스윕. 꺾이는 지점이 없는 매끄러운 곡선이다. 폴리곤 0px 2,081 · 2px 2,032 · 3px 2,014 · 8px 1,954 · 16px 1,801 · 32px 1,574. 무라벨은 4.60% → 3.75% 로 거의 안 움직인다. 1~3px 구간 차이는 폴리곤 2.4%, 무라벨 0.13pp 로 노이즈 수준이라 2px 를 유지한다. 과다 병합은 16px 부터 보인다(vehicle 240→229, 32px 에서 209). 콘은 gap 32px 에서도 65개 그대로 — 집합 밖이라 병합을 안 탄다. 소요는 사진당 약 5초(gap 2px)."
|
||||
},
|
||||
{
|
||||
"id": "M4",
|
||||
"name": "단계 비교 뷰어",
|
||||
"state": "done",
|
||||
"detail": "tools/make_stage_viewer.py 신규. 같은 사진에서 검출 → claim → 병합을 갈아끼우고 바뀐 폴리곤만 빨갛게 강조한다. claim 은 563개 재배정 (white roof→white vehicle 194 등 지붕 오검을 차량으로), 병합은 1,033개가 둘 이상 합쳐진 것. 폴리곤 11,169 → 11,169 → 6,370."
|
||||
},
|
||||
{
|
||||
"id": "M5",
|
||||
"name": "factory structure 를 [building] 에서 분리",
|
||||
"state": "done",
|
||||
"detail": "사용자 지적. 공장 구조물이 일반 건물에 흡수되면 병합 후 되돌릴 수 없다. 집합 밖으로 빼면 passthrough 로 독립 클래스가 된다. 16장 실측 24개. 부작용으로 building 이 466 → 490 으로 늘었다 — 전에는 factory structure 폴리곤이 건물 사이 다리 노릇을 해 여러 건물을 한 덩어리로 잇고 있었다. 라벨 14 → 15종, 폴리곤 6,370 → 6,418."
|
||||
}
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"state": "closed",
|
||||
"text": "'집합 없는 검출은 버려진다'는 앞선 판단은 틀렸다. merge_labels.py 의 merge_by_group 은 집합에 없는 라벨을 passthrough 로 그대로 통과시킨다. 16장 병합 결과에 orange traffic cone 114 · yellow safety cone 6 이 독립 클래스로 살아 있다. 콘을 [cone] 으로 묶으면 오히려 나란한 콘이 gap 2px 에서 한 덩어리가 되어 개수를 잃는다 — 지금처럼 집합 밖에 두는 게 맞다."
|
||||
},
|
||||
{
|
||||
"state": "closed",
|
||||
"text": "통짜(1×1) 패스는 대상 하나에 인스턴스가 하나만 남는다 — 도로 한쪽 차로, 논 한 필지만 잡힘. --wide-in-tiles 로 해결(측정치는 M1e)."
|
||||
},
|
||||
{
|
||||
"state": "open",
|
||||
"text": "0581 무라벨 6.4% — 16장 중 유일한 5% 초과. 원인 확인함: 큰 구멍이 없다. 구멍 1,554개 중 최대가 0.83%, 상위 6개 합이 2.15% 뿐이고 나머지 4.25%가 작은 조각 1,548개로 흩어져 있다. 자재 야적장·건물 밀집 구역이라 폴리곤이 1,079개 나왔는데도 객체 사이 얇은 틈이 누적된 것. 0654 유형(큰 필지 통째 누락)과 다르다. 프롬프트로는 못 줄인다 — 후처리(폴리곤 팽창·구멍 메우기) 영역."
|
||||
},
|
||||
{
|
||||
"state": "open",
|
||||
"text": "확정 설정 16장에서 검출 0 인 프롬프트 12개: residential house, plowed field, commercial bus, plastic greenhouse, vinyl greenhouse tunnel, greenhouse roof, green wire fence, road embankment slope, riprap slope, bridge, water surface, yellow grass field. 16장은 69장의 23% 라 대상이 없었을 뿐일 수 있다 (bridge 는 다리 있는 사진에서만 나온다). 지워도 94→82개라 배치 청크가 6으로 그대로여서 속도 이득이 없다 — 78개 이하로 줄여야 5청크가 된다. 69장 전체를 돌린 뒤 판단할 것."
|
||||
},
|
||||
{
|
||||
"state": "rejected",
|
||||
"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,243 @@
|
||||
# 다른 PC(RTX 4090)에 SAM 3.1 검출 환경 세우기 — 작업 지시 프롬프트
|
||||
|
||||
이 문서는 그대로 복사해서 새 PC 의 Claude Code(또는 사람)에게 주는 **작업 지시서**다.
|
||||
아래 `---` 사이를 통째로 붙여넣으면 된다.
|
||||
|
||||
측정 근거: RTX 3060 12GB 에서 사진 1장 548초. 4090 24GB 추정 80~180초.
|
||||
자세한 내역은 `docs/pilot/sam_merge.html` 의 단계 기록을 봐라.
|
||||
|
||||
---
|
||||
|
||||
## 지시
|
||||
|
||||
RTX 4090 이 달린 이 PC 에 드론 사진 SAM 3.1 검출 환경을 세워라.
|
||||
목적은 사진 1장을 튜닝된 설정으로 검출해 3060 대비 실제 배속을 재는 것이다.
|
||||
전체 블록 실행은 그 측정 뒤에 결정한다.
|
||||
|
||||
### 전제
|
||||
|
||||
- OS: Windows (원래 환경과 같음). Linux 면 경로 구분자만 바꿔라.
|
||||
- GPU: RTX 4090 24GB. `nvidia-smi` 로 드라이버가 올라와 있는지 먼저 확인해라.
|
||||
|
||||
### 반입 목록 (실측 크기)
|
||||
|
||||
**git 으로 오는 것 — 복사하지 마라**
|
||||
|
||||
| 경로 | 리포 |
|
||||
|---|---|
|
||||
| `prompts/discovery_v4.txt` (94개 프롬프트) | railway-client, `sam31-coverage-tuning` 브랜치 |
|
||||
| `prompts/wide_v1.txt` | 〃 |
|
||||
| `configs/merge_groups_v4.txt` (현재 활성 집합 정의) | 〃 |
|
||||
| `tools/*.py` 전부 | 〃 |
|
||||
| `sam3/` 패키지 | sam31server |
|
||||
| `configs/auto_labeling/segment_anything_3.yaml` | 〃 |
|
||||
|
||||
`merge_groups_v5.txt` 도 리포에 있지만 **보류 상태다. 쓰지 마라.** 활성은 v4 다.
|
||||
|
||||
**손으로 복사해야 하는 것 — git 에 없다**
|
||||
|
||||
| 항목 | 실측 크기 | 원본 위치 | 왜 git 에 없나 |
|
||||
|---|---|---|---|
|
||||
| SAM3.1 체크포인트 | **3.3 GB** | `C:\Users\<사용자>\.cache\huggingface\hub\models--facebook--sam3.1\` | `.gitignore` 가 `*.pt` 제외 |
|
||||
| BPE 사전 `bpe_simple_vocab_16e6.txt.gz` | 1.4 MB | `sam31server\bpe_simple_vocab_16e6.txt.gz` | `.gitignore` 가 `*.gz` 제외 |
|
||||
| 원본 JPEG `stage_BlockYYX\` 69장 | **1.2 GB** | `railway-client\output\sam3\stage_BlockYYX\` | `.gitignore:42` 가 `output/` 제외 |
|
||||
|
||||
체크포인트는 복사 대신 HuggingFace 에서 새로 받아도 된다
|
||||
(`huggingface-cli download facebook/sam3.1`). 회선이 느리면 복사가 빠르다.
|
||||
|
||||
**복사하면 안 되는 것**
|
||||
|
||||
| 항목 | 이유 |
|
||||
|---|---|
|
||||
| `.venv\` | 절대경로가 박혀 있어 다른 PC 에서 깨진다. 새로 만들어라 |
|
||||
| `output\` 의 나머지 (`tune_t64` 등) | 이쪽 PC 의 산출물이다. 4090 은 새로 만든다 |
|
||||
| `docs\pilot\theater*\` | 뷰어 JPEG 55MB. `make_viewer.py` 로 다시 굽는다 |
|
||||
|
||||
### 1. 리포 2개 clone
|
||||
|
||||
두 리포를 **형제 폴더로** 두어라. `sam3_multi_prompt.py` 가 기본값으로
|
||||
`../sam31server` 를 찾는다 (환경변수 `SAM31SERVER_DIR` 로 바꿀 수 있다).
|
||||
|
||||
```
|
||||
D:\MYCLAUDE_PROJECT\
|
||||
├── railway-client\
|
||||
└── sam31server\
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://gitea.hmac.kr/kimminsung/railway-client.git
|
||||
git clone https://gitea.hmac.kr/kimminsung/sam31server.git
|
||||
|
||||
cd railway-client
|
||||
git checkout sam31-coverage-tuning
|
||||
```
|
||||
|
||||
**브랜치 체크아웃을 빠뜨리지 마라.** `main` 에는 튜닝된 프롬프트
|
||||
(`prompts/discovery_v4.txt`)와 집합 정의(`configs/merge_groups_v4.txt`),
|
||||
그리고 `--wide-in-tiles` 플래그가 없다.
|
||||
|
||||
`sam31server` 는 FastAPI 서버지만 **서버를 띄울 필요는 없다.**
|
||||
검출 스크립트는 그 안의 `sam3` 패키지를 `sys.path` 로 직접 import 한다
|
||||
(`tools/sam3_multi_prompt.py:28-33`). `app/` 은 안 쓴다.
|
||||
|
||||
### 2. Python 환경
|
||||
|
||||
venv 를 **복사하지 마라.** 절대경로가 박혀 있어 깨진다. 새로 만들어라.
|
||||
|
||||
원본 환경 (참고용, 실측):
|
||||
- Python 3.12.8
|
||||
- torch 2.10.0+cu126 (CUDA 12.6)
|
||||
|
||||
```bash
|
||||
cd sam31server
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
|
||||
# torch 는 CUDA 버전에 맞춰 먼저 설치해라 (requirements.txt 의 torch>=2.4.0 보다 우선)
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu126
|
||||
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
`sam3` 는 pip 설치 대상이 아니다 (site-packages 에 없고 .pth 도 없다).
|
||||
폴더가 있으면 된다. 의존성만 깔면 끝이다.
|
||||
|
||||
설치 확인:
|
||||
```bash
|
||||
.venv\Scripts\python.exe -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))"
|
||||
```
|
||||
`True` 와 `NVIDIA GeForce RTX 4090` 이 나와야 한다.
|
||||
|
||||
### 3. 체크포인트 + BPE 사전 (git 에 없음, 실측 3.3GB + 1.4MB)
|
||||
|
||||
`.gitignore` 가 `*.pt`, `*.gz` 를 뺀다.
|
||||
|
||||
필요한 것:
|
||||
- `sam3.1_multiplex.pt` — HuggingFace `facebook/sam3.1` (캐시 폴더 전체 3.3GB)
|
||||
- `bpe_simple_vocab_16e6.txt.gz` (1.4MB)
|
||||
|
||||
받는 방법 둘 중 하나:
|
||||
1. HuggingFace 에서 새로 받는다 (`huggingface-cli download facebook/sam3.1`)
|
||||
2. 소스 PC 의 HF 캐시를 복사한다
|
||||
(`C:\Users\<사용자>\.cache\huggingface\hub\models--facebook--sam3.1\...`)
|
||||
|
||||
경로 지정은 셋 중 하나 (앞이 우선):
|
||||
1. CLI 인자 `--checkpoint` / `--bpe`
|
||||
2. 환경변수 `SAM3_CHECKPOINT` / `SAM3_BPE`
|
||||
3. `sam31server/configs/auto_labeling/segment_anything_3.yaml` 의
|
||||
`params.model_path` / `params.bpe_path`
|
||||
|
||||
3번 파일에는 **원래 PC 의 절대경로가 적혀 있다.** 그대로 쓰면 파일을 못 찾는다.
|
||||
환경변수로 덮거나 이 파일을 고쳐라.
|
||||
|
||||
### 4. 원본 사진 (git 에 없음, 실측 1.2GB)
|
||||
|
||||
`.gitignore:42` 가 `output/` 을 통째로 뺀다.
|
||||
|
||||
소스 PC 에서 복사해라:
|
||||
```
|
||||
railway-client/output/sam3/stage_BlockYYX/ JPEG 69장, 장당 14~23MB
|
||||
```
|
||||
SMB 공유, rsync, 외장 디스크 아무거나. 방법은 상관없다.
|
||||
|
||||
### 5. 시험 실행 — 사진 1장
|
||||
|
||||
```bash
|
||||
set SAM31SERVER_DIR=D:\MYCLAUDE_PROJECT\sam31server
|
||||
set SAM3_CHECKPOINT=<체크포인트 절대경로>
|
||||
set SAM3_BPE=<BPE 절대경로>
|
||||
|
||||
cd D:\MYCLAUDE_PROJECT\railway-client
|
||||
|
||||
..\sam31server\.venv\Scripts\python.exe tools\sam3_multi_prompt.py ^
|
||||
--input output\sam3\stage_BlockYYX\DJI_20250805165516_0654.JPG ^
|
||||
--output output\sam3\gpu4090_test\DJI_20250805165516_0654_multi.json ^
|
||||
--prompts prompts\discovery_v4.txt ^
|
||||
--wide-prompts prompts\wide_v1.txt --wide-in-tiles ^
|
||||
--cols 6 --rows 4 --conf 0.10 --merge ^
|
||||
--batch 32
|
||||
```
|
||||
|
||||
`--batch 32` 는 24GB 라서 가능하다. 3060 은 VRAM 때문에 16 이 한계였다.
|
||||
OOM 이 나면 24 → 16 으로 낮춰라.
|
||||
|
||||
**비교 기준 (3060 실측, 같은 사진 0654):**
|
||||
|
||||
| 구간 | 3060 |
|
||||
|---|---|
|
||||
| 타일 패스 (24타일 × 6청크 = 144 forward) | 410초 |
|
||||
| 통짜 패스 | 43초 |
|
||||
| 모델 로딩 + NMS + 병합 + 저장 | 약 105초 |
|
||||
| **합계** | **466~662초 (10장 평균 548초)** |
|
||||
|
||||
로그에 찍히는 `전체 1장 완료 — N초` 와 타일별 시간을 그대로 보고해라.
|
||||
|
||||
### 6. 결과 검증
|
||||
|
||||
무라벨(미검출) 지표를 재라. GPU 불필요:
|
||||
|
||||
```bash
|
||||
python tools\coverage_stats.py ^
|
||||
--run-dir output\sam3\gpu4090_test ^
|
||||
--groups configs\merge_groups_v4.txt ^
|
||||
--size 8192,5460
|
||||
```
|
||||
|
||||
0654 는 3060 에서 **무라벨 5.0%** 가 나왔다. 4090 에서도 5% 근처여야 한다.
|
||||
GPU 아키텍처가 달라 비트 단위로 같지는 않지만 소수점 아래에서만 달라야 한다.
|
||||
**1%p 넘게 벌어지면 설정이 다른 것이니 멈추고 보고해라.**
|
||||
|
||||
### 7. 전체 블록 실행 (측정 뒤 지시 받고 할 것)
|
||||
|
||||
`--input` 을 **폴더**로, `--outdir` 로 주면 모델을 한 번만 로딩한다.
|
||||
장별로 프로세스를 새로 띄우면 사진마다 60~90초를 로딩에 버린다.
|
||||
|
||||
```bash
|
||||
..\sam31server\.venv\Scripts\python.exe tools\sam3_multi_prompt.py ^
|
||||
--input output\sam3\stage_BlockYYX --outdir output\sam3\YYX_v4 ^
|
||||
--prompts prompts\discovery_v4.txt ^
|
||||
--wide-prompts prompts\wide_v1.txt --wide-in-tiles ^
|
||||
--cols 6 --rows 4 --conf 0.10 --merge --batch 32
|
||||
```
|
||||
|
||||
끝나면 집합 병합:
|
||||
```bash
|
||||
python tools\merge_labels.py --json <각 _multi.json> --groups configs\merge_groups_v4.txt --gap 2
|
||||
```
|
||||
|
||||
### 8. 결과 회수
|
||||
|
||||
산출 JSON 만 가져오면 된다 — 장당 2~5MB, 69장 약 300MB.
|
||||
집계·병합·뷰어 생성은 GPU 없이 원래 PC 에서 돌린다.
|
||||
|
||||
### 하지 말 것
|
||||
|
||||
- venv 복사 (절대경로 깨짐)
|
||||
- `main` 브랜치로 실행 (튜닝 설정 없음)
|
||||
- FastAPI 서버 기동 (이 작업에 불필요)
|
||||
- 설정값 임의 변경. `--cols 6 --rows 4 --conf 0.10 --wide-in-tiles` 넷이
|
||||
무라벨 16% → 3.6% 를 만든 조합이다. 하나라도 바꾸면 품질이 달라진다.
|
||||
- 측정 안 한 수치 보고. 추정이면 추정이라고 써라.
|
||||
|
||||
---
|
||||
|
||||
## 부록: 이 설정이 나온 근거
|
||||
|
||||
BlockYYX 사진 8~16장 실측. 지표는 "무라벨 화면%" — 마스크 합집합으로 재서
|
||||
겹침을 뺀 값이다.
|
||||
|
||||
| 단계 | 0654 | 0006 |
|
||||
|---|---|---|
|
||||
| 기준선 (conf 0.25, 타일 4×3, 통짜 분리) | 52.1% | 19.4% |
|
||||
| `--wide-in-tiles` | 15.4% | 12.2% |
|
||||
| 경계 띠 프롬프트 +5 (v3) | 14.9% | 11.9% |
|
||||
| conf 0.25 → 0.10 | 9.5% | 5.8% |
|
||||
| 프롬프트 +4 (v4) | 9.6% | 5.7% |
|
||||
| 타일 4×3 → 6×4 | **5.0%** | **5.0%** |
|
||||
|
||||
기여도: 통짜→타일 −36.7pp · conf −5.4pp · 타일 세분화 −4.6pp ·
|
||||
프롬프트 추가 −0.5pp. **프롬프트 추가는 거의 기여하지 않았다.**
|
||||
|
||||
원인은 통짜(1×1) 패스였다. `wide_v1.txt` 의 21개 프롬프트는 8192×5460 을
|
||||
한 장으로 넣어 대상 하나에 인스턴스가 하나만 살아남았다 — 도로는 한쪽 차로만,
|
||||
논은 한 필지만 잡혔다.
|
||||
@@ -0,0 +1,308 @@
|
||||
<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-03 13:19 · 집계 <code>tune_t64</code></p>
|
||||
<h2>실측</h2><div class="cards">
|
||||
<div class="card"><div class="k">사진</div><div class="v">16</div></div>
|
||||
<div class="card"><div class="k">폴리곤</div><div class="v">11,169</div></div>
|
||||
<div class="card"><div class="k">병합 후 폴리곤</div><div class="v">6,418 (-43%)</div></div>
|
||||
<div class="card"><div class="k">라벨 종류</div><div class="v">82 / 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">184.7%</div></div>
|
||||
<div class="card"><div class="k">집합 없는 폴리곤</div><div class="v">120</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.5%</div></div>
|
||||
<div class="card"><div class="k">무라벨 중앙</div><div class="v">3.3%</div></div>
|
||||
<div class="card"><div class="k">최악</div><div class="v">6.4%</div></div>
|
||||
<div class="card"><div class="k">5% 이하 사진</div><div class="v">13 / 16</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_20250805165320_0581</td><td data-v="1079">1,079</td><td data-v="6.435">6.4%</td><td><div class="bar"><i style="width:6.4%;background:#c0392b"></i></div></td></tr>
|
||||
<tr><td>DJI_20250805163524_0007</td><td data-v="970">970</td><td data-v="5.134">5.1%</td><td><div class="bar"><i style="width:5.1%;background:#c0392b"></i></div></td></tr>
|
||||
<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_20250805164537_0346</td><td data-v="1020">1,020</td><td data-v="3.582">3.6%</td><td><div class="bar"><i style="width:3.6%;background:#1a7f4b"></i></div></td></tr>
|
||||
<tr><td>DJI_20250805164555_0357</td><td data-v="915">915</td><td data-v="3.331">3.3%</td><td><div class="bar"><i style="width:3.3%;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_20250805164750_0421</td><td data-v="826">826</td><td data-v="3.018">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_20250805164931_0483</td><td data-v="489">489</td><td data-v="2.488">2.5%</td><td><div class="bar"><i style="width:2.5%;background:#1a7f4b"></i></div></td></tr>
|
||||
<tr><td>DJI_20250805164739_0414</td><td data-v="522">522</td><td data-v="1.713">1.7%</td><td><div class="bar"><i style="width:1.7%;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>
|
||||
<tr><td>DJI_20250805164753_0423</td><td data-v="762">762</td><td data-v="0.521">0.5%</td><td><div class="bar"><i style="width:0.5%;background:#1a7f4b"></i></div></td></tr>
|
||||
</tbody></table></div>
|
||||
<h2>공정 보기 — 같은 사진, 단계별</h2>
|
||||
<div class="flow"><b>SAM 검출 11,169 폴리곤</b><span>(82 라벨)</span><span class="ar">→ 집합 병합 →</span><b>6,418 폴리곤</b><span>(15 집합)</span><span>· 사진 16장 기준</span></div>
|
||||
<div class="tabs">
|
||||
<button class="tab" role="tab" data-i="0" aria-selected="true">단계 비교 (검출 → claim → 병합)</button>
|
||||
<button class="tab" role="tab" data-i="1" aria-selected="false">검출만 보기 (라벨 82종)</button>
|
||||
<button class="tab" role="tab" data-i="2" aria-selected="false">병합만 보기</button>
|
||||
</div>
|
||||
<div class="stage" data-i="0"><iframe src="theater_v4/stages.html" loading="lazy" title="stage0"></iframe></div>
|
||||
<div class="stage" data-i="1" hidden><iframe src="theater_v4/stage1_detect.html" loading="lazy" title="stage1"></iframe></div>
|
||||
<div class="stage" data-i="2" hidden><iframe src="theater_v4/stage2_merged.html" loading="lazy" title="stage2"></iframe></div>
|
||||
<p class="sub" style="font-size:12px">왼쪽 목록에서 라벨을 켜고 끈다. 휠 확대, 드래그 이동. 새 창: <a href="theater_v4/stages.html">단계 비교 (검출 → claim → 병합)</a> · <a href="theater_v4/stage1_detect.html">검출만 보기 (라벨 82종)</a> · <a href="theater_v4/stage2_merged.html">병합만 보기</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>확정 설정 16장 검증</div><div class="d">16장 실측 무라벨(conf 0.10): 최소 0.5%(0423) ~ 최대 6.4%(0581), 평균 3.44%. 15/16 장이 5% 이하. 같은 16장 기준선 평균 19.9%. 소요 466~907초/장(평균 585초). 예상이 틀린 곳: 물 많은 사진이 위험할 것으로 봤으나 0423 이 0.5% 로 최저였다 — water surface/reservoir 검출이 0이어도 다른 프롬프트가 그 면적을 덮는다.</div></div></div>
|
||||
<div class="step"><span class="id">M2</span><span class="badge" style="color:#a12a2a;background:#fbe6e6">막힘</span><div><div>집합 정의 조정 — 보류</div><div class="d">교통시설물을 가르는 안(configs/merge_groups_v5.txt)을 만들고 16장으로 확인했다: structure 1,079 → utility pole 608 / streetlight pole 372 / road sign 358 / culvert 9, fence 702 → guardrail 668 / fence 46 / roadside barrier 17. 라벨 14 → 19종. 사용자 지시로 보류하고 v4 로 되돌렸다. 세부 세그멘테이션은 후순위.</div></div></div>
|
||||
<div class="step"><span class="id">M3</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>gap 값 조정 — 2px 유지</div><div class="d">사진 4장(폴리곤 3,280)으로 gap 0~32px 스윕. 꺾이는 지점이 없는 매끄러운 곡선이다. 폴리곤 0px 2,081 · 2px 2,032 · 3px 2,014 · 8px 1,954 · 16px 1,801 · 32px 1,574. 무라벨은 4.60% → 3.75% 로 거의 안 움직인다. 1~3px 구간 차이는 폴리곤 2.4%, 무라벨 0.13pp 로 노이즈 수준이라 2px 를 유지한다. 과다 병합은 16px 부터 보인다(vehicle 240→229, 32px 에서 209). 콘은 gap 32px 에서도 65개 그대로 — 집합 밖이라 병합을 안 탄다. 소요는 사진당 약 5초(gap 2px).</div></div></div>
|
||||
<div class="step"><span class="id">M4</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>단계 비교 뷰어</div><div class="d">tools/make_stage_viewer.py 신규. 같은 사진에서 검출 → claim → 병합을 갈아끼우고 바뀐 폴리곤만 빨갛게 강조한다. claim 은 563개 재배정 (white roof→white vehicle 194 등 지붕 오검을 차량으로), 병합은 1,033개가 둘 이상 합쳐진 것. 폴리곤 11,169 → 11,169 → 6,370.</div></div></div>
|
||||
<div class="step"><span class="id">M5</span><span class="badge" style="color:#1a7f4b;background:#e3f5ea">완료</span><div><div>factory structure 를 [building] 에서 분리</div><div class="d">사용자 지적. 공장 구조물이 일반 건물에 흡수되면 병합 후 되돌릴 수 없다. 집합 밖으로 빼면 passthrough 로 독립 클래스가 된다. 16장 실측 24개. 부작용으로 building 이 466 → 490 으로 늘었다 — 전에는 factory structure 폴리곤이 건물 사이 다리 노릇을 해 여러 건물을 한 덩어리로 잇고 있었다. 라벨 14 → 15종, 폴리곤 6,370 → 6,418.</div></div></div>
|
||||
<h2>이슈 · 기각된 시도</h2>
|
||||
<div class="issue"><span class="badge" style="color:#1a7f4b;background:#e3f5ea">해결</span><div>'집합 없는 검출은 버려진다'는 앞선 판단은 틀렸다. merge_labels.py 의 merge_by_group 은 집합에 없는 라벨을 passthrough 로 그대로 통과시킨다. 16장 병합 결과에 orange traffic cone 114 · yellow safety cone 6 이 독립 클래스로 살아 있다. 콘을 [cone] 으로 묶으면 오히려 나란한 콘이 gap 2px 에서 한 덩어리가 되어 개수를 잃는다 — 지금처럼 집합 밖에 두는 게 맞다.</div></div>
|
||||
<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>0581 무라벨 6.4% — 16장 중 유일한 5% 초과. 원인 확인함: 큰 구멍이 없다. 구멍 1,554개 중 최대가 0.83%, 상위 6개 합이 2.15% 뿐이고 나머지 4.25%가 작은 조각 1,548개로 흩어져 있다. 자재 야적장·건물 밀집 구역이라 폴리곤이 1,079개 나왔는데도 객체 사이 얇은 틈이 누적된 것. 0654 유형(큰 필지 통째 누락)과 다르다. 프롬프트로는 못 줄인다 — 후처리(폴리곤 팽창·구멍 메우기) 영역.</div></div>
|
||||
<div class="issue"><span class="badge" style="color:#a12a2a;background:#fbe6e6">미해결</span><div>확정 설정 16장에서 검출 0 인 프롬프트 12개: residential house, plowed field, commercial bus, plastic greenhouse, vinyl greenhouse tunnel, greenhouse roof, green wire fence, road embankment slope, riprap slope, bridge, water surface, yellow grass field. 16장은 69장의 23% 라 대상이 없었을 뿐일 수 있다 (bridge 는 다리 있는 사진에서만 나온다). 지워도 94→82개라 배치 청크가 6으로 그대로여서 속도 이득이 없다 — 78개 이하로 줄여야 5청크가 된다. 69장 전체를 돌린 뒤 판단할 것.</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>3,035</td><td>27.2%</td><td>16.63%</td>
|
||||
<td>1,766</td><td class="d-up">42%</td><td>16.09%</td>
|
||||
<td><div class="bar"><i style="width:16.6%"></i></div></td></tr>
|
||||
<tr><td>building</td><td>17 / 21</td><td>2,218</td><td>19.9%</td><td>15.18%</td>
|
||||
<td>490</td><td class="d-up">78%</td><td>12.57%</td>
|
||||
<td><div class="bar"><i style="width:15.2%"></i></div></td></tr>
|
||||
<tr><td>structure</td><td>4 / 4</td><td>1,347</td><td>12.1%</td><td>0.46%</td>
|
||||
<td>1,079</td><td class="d-up">20%</td><td>0.57%</td>
|
||||
<td><div class="bar"><i style="width:0.5%"></i></div></td></tr>
|
||||
<tr><td>ground</td><td>4 / 4</td><td>939</td><td>8.4%</td><td>7.71%</td>
|
||||
<td>573</td><td class="d-up">39%</td><td>8.99%</td>
|
||||
<td><div class="bar"><i style="width:7.7%"></i></div></td></tr>
|
||||
<tr><td>road</td><td>6 / 7</td><td>839</td><td>7.5%</td><td>25.09%</td>
|
||||
<td>247</td><td class="d-up">71%</td><td>25.41%</td>
|
||||
<td><div class="bar"><i style="width:25.1%"></i></div></td></tr>
|
||||
<tr><td>fence</td><td>4 / 5</td><td>768</td><td>6.9%</td><td>0.90%</td>
|
||||
<td>702</td><td class="d-up">9%</td><td>1.18%</td>
|
||||
<td><div class="bar"><i style="width:0.9%"></i></div></td></tr>
|
||||
<tr><td>vehicle</td><td>13 / 14</td><td>663</td><td>5.9%</td><td>0.80%</td>
|
||||
<td>576</td><td class="d-up">13%</td><td>1.06%</td>
|
||||
<td><div class="bar"><i style="width:0.8%"></i></div></td></tr>
|
||||
<tr><td>stored material</td><td>9 / 9</td><td>536</td><td>4.8%</td><td>0.75%</td>
|
||||
<td>450</td><td class="d-up">16%</td><td>1.02%</td>
|
||||
<td><div class="bar"><i style="width:0.7%"></i></div></td></tr>
|
||||
<tr><td>farmland</td><td>7 / 8</td><td>460</td><td>4.1%</td><td>29.24%</td>
|
||||
<td>156</td><td class="d-up">66%</td><td>27.27%</td>
|
||||
<td><div class="bar"><i style="width:29.2%"></i></div></td></tr>
|
||||
<tr><td>없음</td><td>2 / 2</td><td>120</td><td>1.1%</td><td>0.00%</td>
|
||||
<td>144</td><td class="">-20%</td><td>1.43%</td>
|
||||
<td><div class="bar"><i style="width:0.0%"></i></div></td></tr>
|
||||
<tr><td>retaining wall</td><td>3 / 3</td><td>100</td><td>0.9%</td><td>0.28%</td>
|
||||
<td>99</td><td class="d-up">1%</td><td>0.38%</td>
|
||||
<td><div class="bar"><i style="width:0.3%"></i></div></td></tr>
|
||||
<tr><td>slope</td><td>2 / 4</td><td>86</td><td>0.8%</td><td>2.73%</td>
|
||||
<td>85</td><td class="d-up">1%</td><td>3.75%</td>
|
||||
<td><div class="bar"><i style="width:2.7%"></i></div></td></tr>
|
||||
<tr><td>water</td><td>5 / 6</td><td>58</td><td>0.5%</td><td>0.23%</td>
|
||||
<td>51</td><td class="d-up">12%</td><td>0.28%</td>
|
||||
<td><div class="bar"><i style="width:0.2%"></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="160">160</td><td data-v="15">15</td><td data-v="0.07781232">7.78%</td><td data-v="77476.8">77,477</td><td data-v="0.4456">0.446</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="building"><td>building rooftop</td><td>General Categories</td><td>building</td><td data-v="159">159</td><td data-v="12">12</td><td data-v="0.02030119">2.03%</td><td data-v="27486.0">27,486</td><td data-v="0.1983">0.198</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="building"><td>building facade</td><td>General Categories</td><td>building</td><td data-v="36">36</td><td data-v="8">8</td><td data-v="0.00321796">0.32%</td><td data-v="67333.5">67,334</td><td data-v="0.1627">0.163</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="15">15</td><td data-v="8">8</td><td data-v="0.00131459">0.13%</td><td data-v="91419.0">91,419</td><td data-v="0.1356">0.136</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="building"><td>storage container</td><td>General Categories</td><td>building</td><td data-v="86">86</td><td data-v="5">5</td><td data-v="0.00105981">0.11%</td><td data-v="5593.2">5,593</td><td data-v="0.1590">0.159</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="building"><td>factory structure</td><td>General Categories</td><td>building</td><td data-v="24">24</td><td data-v="9">9</td><td data-v="0.01036028">1.04%</td><td data-v="232985.2">232,985</td><td data-v="0.2278">0.228</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="road"><td>asphalt road</td><td>General Categories</td><td>road</td><td data-v="148">148</td><td data-v="16">16</td><td data-v="0.07837162">7.84%</td><td data-v="51795.8">51,796</td><td data-v="0.2607">0.261</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="road"><td>concrete pavement</td><td>General Categories</td><td>road</td><td data-v="279">279</td><td data-v="16">16</td><td data-v="0.07276611">7.28%</td><td data-v="50809.5">50,810</td><td data-v="0.1657">0.166</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="road"><td>parking lot</td><td>General Categories</td><td>road</td><td data-v="79">79</td><td data-v="13">13</td><td data-v="0.08050929">8.05%</td><td data-v="168694.5">168,694</td><td data-v="0.4431">0.443</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="road"><td>pedestrian sidewalk</td><td>General Categories</td><td>road</td><td data-v="118">118</td><td data-v="16">16</td><td data-v="0.01098084">1.10%</td><td data-v="61354.2">61,354</td><td data-v="0.4539">0.454</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="road"><td>crosswalk</td><td>General Categories</td><td>road</td><td data-v="193">193</td><td data-v="12">12</td><td data-v="0.00667974">0.67%</td><td data-v="7348.5">7,348</td><td data-v="0.2230">0.223</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="ground"><td>bare ground</td><td>General Categories</td><td>ground</td><td data-v="379">379</td><td data-v="16">16</td><td data-v="0.01435555">1.44%</td><td data-v="11594.5">11,594</td><td data-v="0.1424">0.142</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="ground"><td>dirt field</td><td>General Categories</td><td>ground</td><td data-v="165">165</td><td data-v="15">15</td><td data-v="0.04128724">4.13%</td><td data-v="58397.5">58,398</td><td data-v="0.2319">0.232</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="vegetation"><td>grass lawn</td><td>General Categories</td><td>vegetation</td><td data-v="83">83</td><td data-v="16">16</td><td data-v="0.02137669">2.14%</td><td data-v="52884.0">52,884</td><td data-v="0.3523">0.352</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="vegetation"><td>tree canopy</td><td>General Categories</td><td>vegetation</td><td data-v="128">128</td><td data-v="16">16</td><td data-v="0.01665810">1.67%</td><td data-v="55470.5">55,470</td><td data-v="0.5031">0.503</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="vegetation"><td>dense forest</td><td>General Categories</td><td>vegetation</td><td data-v="29">29</td><td data-v="12">12</td><td data-v="0.04650714">4.65%</td><td data-v="1293221.5">1,293,222</td><td data-v="0.7821">0.782</td></tr>
|
||||
<tr data-cat="General Categories" data-grp="vegetation"><td>green hedge</td><td>General Categories</td><td>vegetation</td><td data-v="252">252</td><td data-v="16">16</td><td data-v="0.02497443">2.50%</td><td data-v="37935.0">37,935</td><td data-v="0.2556">0.256</td></tr>
|
||||
<tr data-cat="Farmland" data-grp="farmland"><td>crop field</td><td>Farmland</td><td>farmland</td><td data-v="112">112</td><td data-v="15">15</td><td data-v="0.12624180">12.62%</td><td data-v="361167.2">361,167</td><td data-v="0.7590">0.759</td></tr>
|
||||
<tr data-cat="Farmland" data-grp="farmland"><td>farmland</td><td>Farmland</td><td>farmland</td><td data-v="77">77</td><td data-v="15">15</td><td data-v="0.09702145">9.70%</td><td data-v="73523.0">73,523</td><td data-v="0.1343">0.134</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="71">71</td><td data-v="12">12</td><td data-v="0.01008127">1.01%</td><td data-v="21693.0">21,693</td><td data-v="0.1709">0.171</td></tr>
|
||||
<tr data-cat="Farmland" data-grp="farmland"><td>rice paddy</td><td>Farmland</td><td>farmland</td><td data-v="2">2</td><td data-v="2">2</td><td data-v="0.00022162">0.02%</td><td data-v="146495.0">146,495</td><td data-v="0.1150">0.115</td></tr>
|
||||
<tr data-cat="Farmland" data-grp="farmland"><td>vegetable field</td><td>Farmland</td><td>farmland</td><td data-v="53">53</td><td data-v="13">13</td><td data-v="0.01650198">1.65%</td><td data-v="108924.0">108,924</td><td data-v="0.2973">0.297</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="194">194</td><td data-v="12">12</td><td data-v="0.01329944">1.33%</td><td data-v="17519.8">17,520</td><td data-v="0.5919">0.592</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="73">73</td><td data-v="7">7</td><td data-v="0.00046298">0.05%</td><td data-v="2929.0">2,929</td><td data-v="0.2086">0.209</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="51">51</td><td data-v="11">11</td><td data-v="0.00414770">0.41%</td><td data-v="47352.5">47,352</td><td data-v="0.7417">0.742</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="130">130</td><td data-v="14">14</td><td data-v="0.00284384">0.28%</td><td data-v="8339.8">8,340</td><td data-v="0.2075">0.207</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="106">106</td><td data-v="14">14</td><td data-v="0.00223156">0.22%</td><td data-v="9017.2">9,017</td><td data-v="0.2273">0.227</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="285">285</td><td data-v="14">14</td><td data-v="0.00240389">0.24%</td><td data-v="4436.0">4,436</td><td data-v="0.2084">0.208</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="590">590</td><td data-v="16">16</td><td data-v="0.00552140">0.55%</td><td data-v="4830.8">4,831</td><td data-v="0.2437">0.244</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="212">212</td><td data-v="12">12</td><td data-v="0.00485095">0.49%</td><td data-v="10739.8">10,740</td><td data-v="0.1825">0.182</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="33">33</td><td data-v="7">7</td><td data-v="0.00069414">0.07%</td><td data-v="3407.0">3,407</td><td data-v="0.1886">0.189</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="60">60</td><td data-v="13">13</td><td data-v="0.00120870">0.12%</td><td data-v="10086.0">10,086</td><td data-v="0.2119">0.212</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="4">4</td><td data-v="2">2</td><td data-v="0.00003148">0.00%</td><td data-v="10609.8">10,610</td><td data-v="0.1536">0.154</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="237">237</td><td data-v="16">16</td><td data-v="0.00338687">0.34%</td><td data-v="16916.0">16,916</td><td data-v="0.7617">0.762</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="203">203</td><td data-v="15">15</td><td data-v="0.00180003">0.18%</td><td data-v="5644.0">5,644</td><td data-v="0.1868">0.187</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="25">25</td><td data-v="14">14</td><td data-v="0.00026839">0.03%</td><td data-v="14451.5">14,452</td><td data-v="0.8810">0.881</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="33">33</td><td data-v="13">13</td><td data-v="0.00048906">0.05%</td><td data-v="20154.0">20,154</td><td data-v="0.8044">0.804</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="48">48</td><td data-v="11">11</td><td data-v="0.00040837">0.04%</td><td data-v="6041.2">6,041</td><td data-v="0.2329">0.233</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="28">28</td><td data-v="11">11</td><td data-v="0.00020255">0.02%</td><td data-v="3788.5">3,788</td><td data-v="0.2381">0.238</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="17">17</td><td data-v="9">9</td><td data-v="0.00010850">0.01%</td><td data-v="6313.5">6,314</td><td data-v="0.3195">0.319</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="25">25</td><td data-v="10">10</td><td data-v="0.00019264">0.02%</td><td data-v="6282.0">6,282</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="8">8</td><td data-v="5">5</td><td data-v="0.00005021">0.01%</td><td data-v="7088.8">7,089</td><td data-v="0.3677">0.368</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="27">27</td><td data-v="6">6</td><td data-v="0.00091902">0.09%</td><td data-v="41477.5">41,478</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.00005016">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 data-cat="Vehicles & Transportation by Color" data-grp="vehicle"><td>tractor</td><td>Vehicles & Transportation by Color</td><td>vehicle</td><td data-v="3">3</td><td data-v="2">2</td><td data-v="0.00004634">0.00%</td><td data-v="21901.0">21,901</td><td data-v="0.1648">0.165</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="7">7</td><td data-v="6">6</td><td data-v="0.00009973">0.01%</td><td data-v="14172.0">14,172</td><td data-v="0.4863">0.486</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="22">22</td><td data-v="6">6</td><td data-v="0.00019449">0.02%</td><td data-v="1395.5">1,396</td><td data-v="0.1420">0.142</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="53">53</td><td data-v="9">9</td><td data-v="0.00008698">0.01%</td><td data-v="1405.0">1,405</td><td data-v="0.1450">0.145</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="123">123</td><td data-v="12">12</td><td data-v="0.00027882">0.03%</td><td data-v="1620.0">1,620</td><td data-v="0.2151">0.215</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="42">42</td><td data-v="6">6</td><td data-v="0.00018354">0.02%</td><td data-v="2405.5">2,406</td><td data-v="0.1566">0.157</td></tr>
|
||||
<tr 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="1">1</td><td data-v="1">1</td><td data-v="0.00002943">0.00%</td><td data-v="38908.5">38,908</td><td data-v="0.2450">0.245</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="111">111</td><td data-v="9">9</td><td data-v="0.00151746">0.15%</td><td data-v="7970.5">7,970</td><td data-v="0.3540">0.354</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="65">65</td><td data-v="8">8</td><td data-v="0.00296029">0.30%</td><td data-v="7204.5">7,204</td><td data-v="0.3242">0.324</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="55">55</td><td data-v="9">9</td><td data-v="0.00133438">0.13%</td><td data-v="11098.0">11,098</td><td data-v="0.1801">0.180</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="64">64</td><td data-v="11">11</td><td data-v="0.00091031">0.09%</td><td data-v="4894.0">4,894</td><td data-v="0.2169">0.217</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="46">46</td><td data-v="14">14</td><td data-v="0.00061087">0.06%</td><td data-v="9764.0">9,764</td><td data-v="0.1678">0.168</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="17">17</td><td data-v="2">2</td><td data-v="0.00001570">0.00%</td><td data-v="742.0">742</td><td data-v="0.1420">0.142</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="6">6</td><td data-v="4">4</td><td data-v="0.00000716">0.00%</td><td data-v="1676.5">1,676</td><td data-v="0.2962">0.296</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="114">114</td><td data-v="8">8</td><td data-v="0.00003171">0.00%</td><td data-v="303.5">304</td><td data-v="0.1416">0.142</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="6">6</td><td data-v="3">3</td><td data-v="0.00080077">0.08%</td><td data-v="147961.2">147,961</td><td data-v="0.3340">0.334</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="92">92</td><td data-v="14">14</td><td data-v="0.00194630">0.19%</td><td data-v="14316.0">14,316</td><td data-v="0.1693">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.00007213">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="47">47</td><td data-v="13">13</td><td data-v="0.00086195">0.09%</td><td data-v="18291.5">18,292</td><td data-v="0.2035">0.204</td></tr>
|
||||
<tr data-cat="Road Infrastructure" data-grp="fence"><td>metal guardrail</td><td>Road Infrastructure</td><td>fence</td><td data-v="658">658</td><td data-v="16">16</td><td data-v="0.00747726">0.75%</td><td data-v="5725.8">5,726</td><td data-v="0.1700">0.170</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="25">25</td><td data-v="9">9</td><td data-v="0.00294042">0.29%</td><td data-v="59922.0">59,922</td><td data-v="0.1410">0.141</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="9">9</td><td data-v="3">3</td><td data-v="0.00000804">0.00%</td><td data-v="1195.5">1,196</td><td data-v="0.1054">0.105</td></tr>
|
||||
<tr data-cat="Road Infrastructure" data-grp="structure"><td>utility pole</td><td>Road Infrastructure</td><td>structure</td><td data-v="608">608</td><td data-v="16">16</td><td data-v="0.00234281">0.23%</td><td data-v="1668.5">1,668</td><td data-v="0.2006">0.201</td></tr>
|
||||
<tr data-cat="Road Infrastructure" data-grp="structure"><td>streetlight pole</td><td>Road Infrastructure</td><td>structure</td><td data-v="372">372</td><td data-v="16">16</td><td data-v="0.00089454">0.09%</td><td data-v="1817.8">1,818</td><td data-v="0.2200">0.220</td></tr>
|
||||
<tr data-cat="Road Infrastructure" data-grp="structure"><td>road sign</td><td>Road Infrastructure</td><td>structure</td><td data-v="358">358</td><td data-v="16">16</td><td data-v="0.00135648">0.14%</td><td data-v="2304.8">2,305</td><td data-v="0.2210">0.221</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="9">9</td><td data-v="4">4</td><td data-v="0.00056034">0.06%</td><td data-v="49909.0">49,909</td><td data-v="0.4575">0.458</td></tr>
|
||||
<tr data-cat="Water" data-grp="water"><td>reservoir</td><td>Water</td><td>water</td><td data-v="1">1</td><td data-v="1">1</td><td data-v="0.00032781">0.03%</td><td data-v="433374.0">433,374</td><td data-v="0.3949">0.395</td></tr>
|
||||
<tr data-cat="Water" data-grp="water"><td>stream</td><td>Water</td><td>water</td><td data-v="20">20</td><td data-v="6">6</td><td data-v="0.00088044">0.09%</td><td data-v="38496.2">38,496</td><td data-v="0.3507">0.351</td></tr>
|
||||
<tr data-cat="Water" data-grp="water"><td>irrigation canal</td><td>Water</td><td>water</td><td data-v="22">22</td><td data-v="6">6</td><td data-v="0.00045042">0.05%</td><td data-v="18064.8">18,065</td><td data-v="0.2014">0.201</td></tr>
|
||||
<tr data-cat="Water" data-grp="water"><td>drainage ditch</td><td>Water</td><td>water</td><td data-v="6">6</td><td data-v="5">5</td><td data-v="0.00006412">0.01%</td><td data-v="6352.5">6,352</td><td data-v="0.1434">0.143</td></tr>
|
||||
<tr data-cat="Boundary & Filler" data-grp="vegetation"><td>overgrown weeds</td><td>Boundary & Filler</td><td>vegetation</td><td data-v="1190">1,190</td><td data-v="16">16</td><td data-v="0.02730425">2.73%</td><td data-v="4305.5">4,306</td><td data-v="0.1722">0.172</td></tr>
|
||||
<tr data-cat="Boundary & Filler" data-grp="vegetation"><td>shrub</td><td>Boundary & Filler</td><td>vegetation</td><td data-v="1353">1,353</td><td data-v="16">16</td><td data-v="0.02944237">2.94%</td><td data-v="6433.0">6,433</td><td data-v="0.2057">0.206</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="61">61</td><td data-v="11">11</td><td data-v="0.02437004">2.44%</td><td data-v="156737.0">156,737</td><td data-v="0.4462">0.446</td></tr>
|
||||
<tr data-cat="Boundary & Filler" data-grp="ground"><td>earth bank</td><td>Boundary & Filler</td><td>ground</td><td data-v="6">6</td><td data-v="3">3</td><td data-v="0.00010854">0.01%</td><td data-v="4643.0">4,643</td><td data-v="0.1147">0.115</td></tr>
|
||||
<tr data-cat="Boundary & Filler" data-grp="ground"><td>dirt path</td><td>Boundary & Filler</td><td>ground</td><td data-v="389">389</td><td data-v="16">16</td><td data-v="0.02134826">2.13%</td><td data-v="23083.5">23,084</td><td data-v="0.2458">0.246</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="27">27</td><td data-v="10">10</td><td data-v="0.03871443">3.87%</td><td data-v="1393140.0">1,393,140</td><td data-v="0.9576">0.958</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="118">118</td><td data-v="14">14</td><td data-v="0.00360974">0.36%</td><td data-v="15127.0">15,127</td><td data-v="0.1392">0.139</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="22">22</td><td data-v="12">12</td><td data-v="0.00163846">0.16%</td><td data-v="55625.5">55,626</td><td data-v="0.1586">0.159</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 (11집합 + 집합 밖 3종: factory structure, 콘 2종)</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>
|
||||
<b>claim 규칙</b><span><code>configs/claim_rules.txt — vehicle : 0.5 : 지붕 11종 + 방수포/보호시트</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,352 @@
|
||||
#!/usr/bin/env python
|
||||
"""후처리 단계를 같은 사진 위에서 갈아끼워 비교하는 뷰어.
|
||||
|
||||
make_viewer.py 는 한 벌의 결과를 본다. 이건 여러 단계(검출 → claim → 병합)를
|
||||
한 화면에서 전환한다. 사진은 그대로 두고 폴리곤만 바뀌므로 무엇이 달라졌는지
|
||||
눈으로 바로 잡힌다.
|
||||
|
||||
바뀐 것만 보기: claim 단계는 라벨이 재배정된 폴리곤(claimed_from), 병합 단계는
|
||||
둘 이상이 합쳐진 폴리곤(merged_from 길이 2 이상)을 강조한다.
|
||||
|
||||
python tools/make_stage_viewer.py \
|
||||
--stage "1. 검출=output/sam3/tune_t64/*_multi.json" \
|
||||
--stage "2. claim=output/sam3/tune_t64/*_multi_claimed.json" \
|
||||
--stage "3. 병합=output/sam3/tune_t64/*_multi_merged.json" \
|
||||
--output docs/pilot/theater_v4/stages.html
|
||||
"""
|
||||
import argparse
|
||||
import colorsys
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def stem_of(path):
|
||||
"""DJI_..._multi_merged.json → DJI_... (단계 접미사를 벗긴다)"""
|
||||
s = os.path.basename(path)
|
||||
return re.sub(r"_multi(_[a-z0-9]+)?\.json$", "", s)
|
||||
|
||||
|
||||
def color_for(label):
|
||||
h = int(hashlib.md5(label.encode("utf-8")).hexdigest()[:8], 16)
|
||||
r, g, b = colorsys.hsv_to_rgb((h % 360) / 360, 0.62, 0.98)
|
||||
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
|
||||
|
||||
|
||||
HTML = """<title>__TITLE__</title>
|
||||
<style>
|
||||
:root{--bg:#0d0f10;--fg:#e6e9ea;--dim:#8b969b;--line:#2c3336;--card:#171c1e;--accent:#4da3ff}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.5 -apple-system,
|
||||
"Segoe UI","Malgun Gothic",sans-serif;height:100vh;display:flex;overflow:hidden}
|
||||
#side{width:250px;flex:none;background:var(--card);border-right:1px solid var(--line);
|
||||
display:flex;flex-direction:column;overflow:hidden}
|
||||
#side h1{font-size:13px;margin:0;padding:11px 13px;border-bottom:1px solid var(--line)}
|
||||
.pager{display:flex;gap:5px;padding:9px 11px;border-bottom:1px solid var(--line)}
|
||||
.pager select{flex:1;background:#0f1416;color:var(--fg);border:1px solid var(--line);
|
||||
border-radius:4px;padding:4px;font-size:12px}
|
||||
button{background:#232a2d;color:var(--fg);border:1px solid var(--line);border-radius:4px;
|
||||
padding:4px 9px;cursor:pointer;font:inherit;font-size:12px}
|
||||
button:hover{background:#2e3639}
|
||||
.stages{padding:10px 11px;border-bottom:1px solid var(--line);display:flex;
|
||||
flex-direction:column;gap:5px}
|
||||
.stages button{text-align:left;padding:7px 10px;font-size:13px}
|
||||
.stages button[aria-selected="true"]{background:var(--accent);border-color:var(--accent);
|
||||
color:#04121f;font-weight:700}
|
||||
.stages .n{float:right;opacity:.75;font-variant-numeric:tabular-nums}
|
||||
.opts{padding:9px 11px;border-bottom:1px solid var(--line);display:flex;
|
||||
flex-direction:column;gap:5px;color:var(--dim)}
|
||||
.opts label{display:flex;gap:7px;align-items:center;cursor:pointer}
|
||||
#legend{overflow-y:auto;padding:7px 11px;flex:1}
|
||||
#legend .row{display:flex;gap:7px;align-items:center;padding:2px 0}
|
||||
#legend .sw{width:11px;height:11px;border-radius:2px;flex:none}
|
||||
#legend .lb{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
#legend .ct{color:var(--dim);font-variant-numeric:tabular-nums}
|
||||
#view{flex:1;position:relative;overflow:hidden;background:#000}
|
||||
canvas{position:absolute;top:0;left:0}
|
||||
#hud{position:absolute;left:12px;top:12px;background:#000b;padding:6px 11px;
|
||||
border-radius:5px;font-size:12px;line-height:1.7;pointer-events:none}
|
||||
#hud b{color:var(--accent)}
|
||||
#hint{position:absolute;right:12px;bottom:12px;background:#000b;padding:5px 9px;
|
||||
border-radius:5px;font-size:11px;color:var(--dim)}
|
||||
#pick{position:absolute;left:12px;bottom:12px;background:#000c;padding:6px 11px;
|
||||
border-radius:5px;font-size:12px;line-height:1.6;pointer-events:none;max-width:60%}
|
||||
#pick .dim{color:var(--dim)}
|
||||
</style>
|
||||
<div id="side">
|
||||
<h1>__TITLE__</h1>
|
||||
<div class="pager">
|
||||
<button onclick="go(-1)">◀</button>
|
||||
<select id="sel" onchange="setPage(+this.value)"></select>
|
||||
<button onclick="go(1)">▶</button>
|
||||
</div>
|
||||
<div class="stages" id="stages"></div>
|
||||
<div class="opts">
|
||||
<label><input type="checkbox" id="fill" checked> 면 채우기</label>
|
||||
<label><input type="checkbox" id="only"> 바뀐 것만</label>
|
||||
<label><input type="checkbox" id="dim"> 사진 어둡게</label>
|
||||
<button onclick="fit()">화면 맞춤</button>
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
<div id="view"><canvas id="cv"></canvas>
|
||||
<div id="hud"></div>
|
||||
<div id="pick"></div>
|
||||
<div id="hint">1~9 단계 · ←→ 사진 · 휠 확대 · 드래그 이동 · 폴리곤 위에 커서를 올리면 정보</div>
|
||||
</div>
|
||||
<script>
|
||||
const DATA = __DATA__;
|
||||
const img = new Image();
|
||||
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
|
||||
const view = document.getElementById('view');
|
||||
let page = 0, stage = 0, scale = 1, ox = 0, oy = 0;
|
||||
|
||||
function curr() { return DATA.pages[page].stages[stage]; }
|
||||
function changed(s) { return s.c === 1; }
|
||||
|
||||
function buildStages() {
|
||||
const box = document.getElementById('stages');
|
||||
box.innerHTML = '';
|
||||
DATA.stages.forEach((name, i) => {
|
||||
const b = document.createElement('button');
|
||||
const st = DATA.pages[page].stages[i];
|
||||
b.innerHTML = name + `<span class="n">${st ? st.p.length.toLocaleString() : '-'}</span>`;
|
||||
b.setAttribute('aria-selected', i === stage);
|
||||
b.onclick = () => { stage = i; buildStages(); draw(); };
|
||||
box.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function buildLegend() {
|
||||
const st = curr(), c = {};
|
||||
for (const s of st.p) c[s.l] = (c[s.l] || 0) + 1;
|
||||
const el = document.getElementById('legend');
|
||||
el.innerHTML = '';
|
||||
Object.entries(c).sort((a, b) => b[1] - a[1]).forEach(([lb, n]) => {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'row';
|
||||
d.innerHTML = `<span class="sw" style="background:${DATA.colors[lb] || '#888'}"></span>`
|
||||
+ `<span class="lb">${lb}</span><span class="ct">${n.toLocaleString()}</span>`;
|
||||
el.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const st = curr();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
ctx.drawImage(img, ox, oy, img.width * scale, img.height * scale);
|
||||
if (document.getElementById('dim').checked) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,.55)';
|
||||
ctx.fillRect(ox, oy, img.width * scale, img.height * scale);
|
||||
}
|
||||
const fill = document.getElementById('fill').checked;
|
||||
const only = document.getElementById('only').checked;
|
||||
let nch = 0;
|
||||
for (const s of st.p) {
|
||||
const ch = changed(s);
|
||||
if (ch) nch++;
|
||||
if (only && !ch) continue;
|
||||
const col = DATA.colors[s.l] || '#888';
|
||||
ctx.beginPath();
|
||||
s.p.forEach((pt, i) => {
|
||||
const x = ox + pt[0] * scale, y = oy + pt[1] * scale;
|
||||
i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
|
||||
});
|
||||
ctx.closePath();
|
||||
if (fill) { ctx.fillStyle = col + (ch ? 'aa' : '55'); ctx.fill(); }
|
||||
ctx.strokeStyle = ch ? '#ff3b30' : col;
|
||||
ctx.lineWidth = ch ? 2.5 : 1.2;
|
||||
ctx.stroke();
|
||||
}
|
||||
document.getElementById('hud').innerHTML =
|
||||
`${DATA.pages[page].name}<br>단계 <b>${DATA.stages[stage]}</b> · 폴리곤 <b>${st.p.length.toLocaleString()}</b>`
|
||||
+ (nch ? ` · 바뀐 것 <b>${nch.toLocaleString()}</b>` : '');
|
||||
}
|
||||
|
||||
function fit() {
|
||||
scale = Math.min(view.clientWidth / img.width, view.clientHeight / img.height);
|
||||
ox = (view.clientWidth - img.width * scale) / 2;
|
||||
oy = (view.clientHeight - img.height * scale) / 2;
|
||||
draw();
|
||||
}
|
||||
|
||||
function setPage(i) {
|
||||
page = (i + DATA.pages.length) % DATA.pages.length;
|
||||
document.getElementById('sel').value = page;
|
||||
img.onload = () => { buildStages(); buildLegend(); fit(); };
|
||||
img.src = DATA.pages[page].img;
|
||||
}
|
||||
function go(d) { setPage(page + d); }
|
||||
|
||||
function resize() { cv.width = view.clientWidth; cv.height = view.clientHeight; draw(); }
|
||||
window.addEventListener('resize', () => { resize(); fit(); });
|
||||
view.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
const f = e.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||||
ox = e.offsetX - (e.offsetX - ox) * f;
|
||||
oy = e.offsetY - (e.offsetY - oy) * f;
|
||||
scale *= f; draw();
|
||||
}, { passive: false });
|
||||
let drag = null;
|
||||
view.addEventListener('mousedown', e => drag = [e.clientX - ox, e.clientY - oy]);
|
||||
window.addEventListener('mouseup', () => drag = null);
|
||||
window.addEventListener('mousemove', e => {
|
||||
if (drag) { ox = e.clientX - drag[0]; oy = e.clientY - drag[1]; draw(); return; }
|
||||
hover(e);
|
||||
});
|
||||
|
||||
function inside(pt, poly) {
|
||||
let hit = false;
|
||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||
const [xi, yi] = poly[i], [xj, yj] = poly[j];
|
||||
if ((yi > pt[1]) !== (yj > pt[1])
|
||||
&& pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi) hit = !hit;
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
function area(poly) {
|
||||
let a = 0;
|
||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++)
|
||||
a += poly[j][0] * poly[i][1] - poly[i][0] * poly[j][1];
|
||||
return Math.abs(a) / 2;
|
||||
}
|
||||
function hover(e) {
|
||||
const r = view.getBoundingClientRect();
|
||||
const x = (e.clientX - r.left - ox) / scale, y = (e.clientY - r.top - oy) / scale;
|
||||
const st = curr();
|
||||
let best = null, bestA = Infinity;
|
||||
for (const s of st.p) {
|
||||
if (!inside([x, y], s.p)) continue;
|
||||
const a = area(s.p);
|
||||
if (a < bestA) { best = s; bestA = a; } // 가장 작은 것 = 가장 위
|
||||
}
|
||||
const el = document.getElementById('pick');
|
||||
if (!best) { el.innerHTML = `<span class="dim">커서 (${Math.round(x)}, ${Math.round(y)})</span>`; return; }
|
||||
let why = '';
|
||||
if (best.f) why = `<br><span class="dim">원래</span> ${best.f} <span class="dim">→ 뺏김</span>`;
|
||||
else if (best.m.length) why = `<br><span class="dim">합쳐진 것</span> ${best.m.join(' + ')}`;
|
||||
el.innerHTML = `<b style="color:${DATA.colors[best.l] || '#888'}">${best.l}</b>`
|
||||
+ ` <span class="dim">(${Math.round(x)}, ${Math.round(y)})</span>${why}`;
|
||||
}
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key === 'ArrowLeft') go(-1);
|
||||
else if (e.key === 'ArrowRight') go(1);
|
||||
else if (/^[1-9]$/.test(e.key)) {
|
||||
const i = +e.key - 1;
|
||||
if (i < DATA.stages.length) { stage = i; buildStages(); buildLegend(); draw(); }
|
||||
}
|
||||
});
|
||||
['fill', 'only', 'dim'].forEach(id =>
|
||||
document.getElementById(id).onchange = draw);
|
||||
const sel = document.getElementById('sel');
|
||||
DATA.pages.forEach((p, i) => {
|
||||
const o = document.createElement('option');
|
||||
o.value = i; o.textContent = `${i + 1}/${DATA.pages.length} ${p.name}`;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
resize();
|
||||
setPage(0);
|
||||
</script>"""
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--stage", action="append", required=True, metavar="이름=글롭",
|
||||
help="단계 하나. 순서대로 여러 번 준다")
|
||||
ap.add_argument("--output", required=True)
|
||||
ap.add_argument("--images", help="원본 이미지 폴더 (JSON 의 source_image 가 없을 때)")
|
||||
ap.add_argument("--max-size", type=int, default=2000)
|
||||
ap.add_argument("--title", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
stages = []
|
||||
for spec in args.stage:
|
||||
if "=" not in spec:
|
||||
raise SystemExit(f"--stage 형식은 '이름=글롭': {spec}")
|
||||
name, pat = spec.split("=", 1)
|
||||
files = {stem_of(p): p for p in sorted(glob.glob(pat))}
|
||||
if not files:
|
||||
raise SystemExit(f"단계 '{name}' 에 해당하는 JSON 없음: {pat}")
|
||||
stages.append((name, files))
|
||||
|
||||
common = set(stages[0][1])
|
||||
for _, f in stages[1:]:
|
||||
common &= set(f)
|
||||
if not common:
|
||||
raise SystemExit("모든 단계에 공통으로 있는 사진이 없다")
|
||||
stems = sorted(common)
|
||||
print(f"단계 {len(stages)}개 · 공통 사진 {len(stems)}장")
|
||||
|
||||
out_html = os.path.abspath(args.output)
|
||||
os.makedirs(os.path.dirname(out_html), exist_ok=True)
|
||||
|
||||
pages, colors, counts = [], {}, Counter()
|
||||
for stem in stems:
|
||||
img_name = stem + "_view.jpg"
|
||||
img_path = os.path.join(os.path.dirname(out_html), img_name)
|
||||
if not os.path.isfile(img_path):
|
||||
with open(stages[0][1][stem], encoding="utf-8") as fh:
|
||||
src = json.load(fh).get("source_image")
|
||||
if (not src or not os.path.isfile(src)) and args.images:
|
||||
for ext in (".JPG", ".jpg", ".png"):
|
||||
cand = os.path.join(args.images, stem + ext)
|
||||
if os.path.isfile(cand):
|
||||
src = cand
|
||||
break
|
||||
if not src or not os.path.isfile(src):
|
||||
raise SystemExit(f"원본 이미지를 못 찾았다: {stem} (--images 로 지정)")
|
||||
im = cv2.imdecode(np.fromfile(src, dtype=np.uint8), cv2.IMREAD_COLOR)
|
||||
hh, ww = im.shape[:2]
|
||||
k = min(1.0, args.max_size / max(hh, ww))
|
||||
if k < 1.0:
|
||||
im = cv2.resize(im, (int(ww * k), int(hh * k)), interpolation=cv2.INTER_AREA)
|
||||
cv2.imencode(".jpg", im, [cv2.IMWRITE_JPEG_QUALITY, 86])[1].tofile(img_path)
|
||||
print(f" 이미지 생성 {img_name}")
|
||||
# 좌표 축척은 원본 기준이므로 이미지 크기에서 되계산한다
|
||||
im = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), cv2.IMREAD_COLOR)
|
||||
vh, vw = im.shape[:2]
|
||||
|
||||
page_stages = []
|
||||
for name, files in stages:
|
||||
with open(files[stem], encoding="utf-8") as fh:
|
||||
segs = json.load(fh)["segments"]
|
||||
if not page_stages: # 첫 단계에서 원본 해상도를 잡는다
|
||||
mx = max((p[0] for s in segs for p in s["points"]), default=vw)
|
||||
my = max((p[1] for s in segs for p in s["points"]), default=vh)
|
||||
k = min(vw / max(mx, 1), vh / max(my, 1))
|
||||
items = []
|
||||
for s in segs:
|
||||
lb = s.get("label", "")
|
||||
colors.setdefault(lb, color_for(lb))
|
||||
counts[lb] += 1
|
||||
ch = 1 if (s.get("claimed_from")
|
||||
or len(s.get("merged_from", []) or []) > 1) else 0
|
||||
src = s.get("merged_from") or []
|
||||
items.append({"l": lb, "c": ch,
|
||||
"f": s.get("claimed_from") or "",
|
||||
"m": src if len(src) > 1 else [],
|
||||
"p": [[round(p[0] * k, 1), round(p[1] * k, 1)]
|
||||
for p in s["points"]]})
|
||||
page_stages.append({"p": items})
|
||||
pages.append({"name": stem, "img": img_name, "stages": page_stages})
|
||||
|
||||
title = args.title or os.path.splitext(os.path.basename(out_html))[0]
|
||||
payload = {"stages": [n for n, _ in stages], "pages": pages, "colors": colors}
|
||||
with open(out_html, "w", encoding="utf-8") as fh:
|
||||
fh.write(HTML.replace("__TITLE__", title)
|
||||
.replace("__DATA__", json.dumps(payload, ensure_ascii=False)))
|
||||
tot = [sum(len(p["stages"][i]["p"]) for p in pages) for i in range(len(stages))]
|
||||
print(f"뷰어: {args.output}")
|
||||
for (name, _), n in zip(stages, tot):
|
||||
print(f" {name:<16}{n:>8,} 폴리곤")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,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()
|
||||
+86
-38
@@ -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,25 +279,34 @@ def main():
|
||||
bpe_path=bpe_path, device=device, checkpoint_path=ckpt_path)
|
||||
processor = Sam3Processor(model, confidence_threshold=args.conf, device=device)
|
||||
|
||||
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)
|
||||
|
||||
t0 = time.time()
|
||||
all_shapes = run_pass(model, processor, FindStage, image_bgr, boxes,
|
||||
fine, args.conf, args.batch, "타일")
|
||||
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, "통짜")
|
||||
[(0, 0, W, H)], wide, args.conf, args.batch,
|
||||
"통짜", mask_bytes)
|
||||
|
||||
print(f"\n검출 {len(all_shapes)}개 → NMS(iou={args.nms})...")
|
||||
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)}개")
|
||||
|
||||
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}초\n")
|
||||
|
||||
analyze_labels(all_shapes)
|
||||
print(f" 병합(gap={args.merge_gap}px) {before} → {len(all_shapes)}개")
|
||||
print(f" {time.time()-t0:.0f}초")
|
||||
|
||||
vis = draw_everything(image_bgr, all_shapes, args.cols, args.rows)
|
||||
h, w = vis.shape[:2]
|
||||
@@ -272,21 +314,27 @@ def main():
|
||||
s = 4096 / max(h, w)
|
||||
vis = cv2.resize(vis, (int(w * s), int(h * s)))
|
||||
|
||||
out_path = (Path(args.output) if args.output
|
||||
else img_path.parent / (img_path.stem + "_multi.jpg"))
|
||||
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))
|
||||
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"라벨 데이터: {json_path}")
|
||||
print(f" 저장: {out_path.name} / {json_path.name}")
|
||||
|
||||
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