sam31server 전환, 라멘 파이프라인 정리, 문서 추가

- sam31server를 SAM3.1 서버로 전환 (x-anylabeling01 대체)
- detect_raamen.py: B/C 분류 기반 라멘형 전철주 검출 파이프라인 정비
- sam3_everything_explore.py: Discovery Sweep 탐색 모드 정리
- detect_all_objects.py: 타일 검출 개선
- docs/railway-client-guide.html: 서버·도구·파이프라인 전체 가이드 추가
- tools 추가: detect_control_box, group_ramen_poles, render_everything_by_label, render_label_polygons, debug_vh

Closes #1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-06-02 10:11:52 +09:00
parent ccba1266b5
commit 4c15d5ff5d
10 changed files with 2125 additions and 290 deletions

View File

@@ -0,0 +1,664 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Railway Client — 사용 가이드</title>
<style>
:root {
--bg: #0f1117; --bg2: #1a1d27; --bg3: #22263a;
--border: #2e3354; --accent: #4f8ef7; --accent2: #7c3aed;
--green: #22c55e; --yellow: #f59e0b; --red: #ef4444;
--text: #e2e8f0; --muted: #8892a4; --code-bg: #12151f;
--orange: #f97316;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; line-height: 1.6; }
a { color: var(--accent); text-decoration: none; }
nav { background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 2rem; display: flex; align-items: center; gap: 2rem; height: 52px; position: sticky; top: 0; z-index: 100; }
nav .brand { font-weight: 700; font-size: 1rem; color: var(--accent); letter-spacing: .05em; }
nav a { color: var(--muted); font-size: .85rem; }
nav a:hover { color: var(--text); }
.layout { display: flex; min-height: calc(100vh - 52px); }
aside { width: 250px; flex-shrink: 0; background: var(--bg2); border-right: 1px solid var(--border); padding: 1.5rem 1rem; position: sticky; top: 52px; height: calc(100vh - 52px); overflow-y: auto; }
aside h4 { font-size: .68rem; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); margin-bottom: .6rem; margin-top: 1.2rem; }
aside h4:first-child { margin-top: 0; }
aside ul { list-style: none; }
aside ul li a { display: block; padding: .28rem .6rem; border-radius: 5px; font-size: .82rem; color: var(--muted); }
aside ul li a:hover { background: var(--bg3); color: var(--text); }
aside ul li a.priority { color: var(--orange); font-weight: 600; }
main { flex: 1; padding: 2.5rem 3rem; max-width: 980px; }
h1 { font-size: 1.9rem; font-weight: 700; margin-bottom: .4rem; }
h2 { font-size: 1.3rem; font-weight: 600; margin: 2.5rem 0 1rem; padding-bottom: .4rem; border-bottom: 1px solid var(--border); color: var(--accent); }
h2.priority-h2 { color: var(--orange); border-color: #7c3310; }
h3 { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 .6rem; color: #c4cfe8; }
p { margin-bottom: .8rem; color: #c4cfe8; font-size: .93rem; }
.badge { display: inline-block; padding: .15rem .55rem; border-radius: 4px; font-size: .72rem; font-weight: 600; letter-spacing: .04em; margin-right: .3rem; }
.badge-orange { background: #431407; color: #fed7aa; }
.badge-blue { background: #1e3a6e; color: #93c5fd; }
.badge-green { background: #14532d; color: #86efac; }
pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; overflow-x: auto; margin: .8rem 0 1.2rem; font-size: .82rem; line-height: 1.7; }
code { font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace; }
p code, li code, td code { background: var(--code-bg); border: 1px solid var(--border); border-radius: 3px; padding: .1em .4em; font-size: .82em; color: #a5b4fc; }
table { width: 100%; border-collapse: collapse; font-size: .85rem; margin: .8rem 0 1.4rem; }
th { background: var(--bg3); color: var(--muted); text-align: left; padding: .55rem .8rem; font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; border-bottom: 1px solid var(--border); }
td { padding: .5rem .8rem; border-bottom: 1px solid #1e2235; vertical-align: top; }
tr:hover td { background: #161929; }
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; margin: 1rem 0; }
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 1.1rem 1.2rem; }
.card.highlight { border-color: var(--orange); }
.card h4 { font-size: .9rem; font-weight: 600; margin-bottom: .4rem; }
.card p { font-size: .82rem; color: var(--muted); margin: 0; }
.step { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
.step-num { flex-shrink: 0; width: 28px; height: 28px; background: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: .8rem; font-weight: 700; color: #fff; margin-top: .15rem; }
.step-num.orange { background: var(--orange); }
.step-body { flex: 1; }
.step-body h4 { font-size: .92rem; font-weight: 600; margin-bottom: .3rem; }
.step-body p { font-size: .85rem; color: var(--muted); margin: 0 0 .4rem; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .4rem; }
.dot-green { background: var(--green); }
.swatch { display: inline-block; width: 14px; height: 14px; border-radius: 3px; vertical-align: middle; margin-right: .4rem; border: 1px solid rgba(255,255,255,.15); }
.alert { border-radius: 7px; padding: .8rem 1rem; margin: .8rem 0; font-size: .85rem; border-left: 3px solid; }
.alert-info { background: #0c1a3a; border-color: var(--accent); color: #93c5fd; }
.alert-warn { background: #2a1a00; border-color: var(--yellow); color: #fde68a; }
.alert-ok { background: #0a2e1a; border-color: var(--green); color: #86efac; }
.alert-pri { background: #2a1000; border-color: var(--orange); color: #fed7aa; }
.pipeline-row { display: flex; align-items: center; gap: .6rem; margin: 1rem 0; flex-wrap: wrap; }
.pipe-box { background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; padding: .5rem .9rem; font-size: .85rem; font-weight: 600; }
.pipe-box.highlight { border-color: var(--orange); color: var(--orange); }
.pipe-arrow { color: var(--muted); font-size: 1.1rem; }
.subtitle { color: var(--muted); font-size: .9rem; margin-bottom: 1.5rem; }
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
ul.body-list { padding-left: 1.4rem; margin-bottom: .8rem; }
ul.body-list li { font-size: .87rem; color: #c4cfe8; margin-bottom: .25rem; }
.cb-legend { display: flex; gap: 1.5rem; margin: .8rem 0; }
.cb-item { display: flex; align-items: center; gap: .5rem; font-size: .87rem; }
.cb-box { width: 32px; height: 32px; border-radius: 5px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1rem; }
</style>
</head>
<body>
<nav>
<span class="brand">⚡ Railway Client</span>
<a href="#raamen">🔴 라멘 검출</a>
<a href="#everything">탐색 모드</a>
<a href="#server">서버</a>
<a href="#tools">전체 도구</a>
<a href="#categories">카테고리</a>
</nav>
<div class="layout">
<aside>
<h4>🔴 최우선 파이프라인</h4>
<ul>
<li><a href="#raamen" class="priority">라멘형 전철주 검출</a></li>
<li><a href="#raamen-pipeline" class="priority">2단계 파이프라인</a></li>
<li><a href="#raamen-cb" class="priority">B/C 분류 설명</a></li>
<li><a href="#raamen-args">파라미터 상세</a></li>
</ul>
<h4>탐색 모드</h4>
<ul>
<li><a href="#everything">Everything 모드 개요</a></li>
<li><a href="#everything-usage">사용법</a></li>
<li><a href="#everything-prompt">Discovery Prompt</a></li>
<li><a href="#everything-output">출력 해석</a></li>
</ul>
<h4>서버 (sam31server)</h4>
<ul>
<li><a href="#server-setup">설치 요구사항</a></li>
<li><a href="#server-start">서버 시작</a></li>
<li><a href="#server-api">API 엔드포인트</a></li>
</ul>
<h4>클라이언트 도구</h4>
<ul>
<li><a href="#tool-detect-all">detect_all_objects</a></li>
<li><a href="#tool-autolabel">sam3_autolabel</a></li>
<li><a href="#tool-batch">sam3_batch_label</a></li>
<li><a href="#tool-yoloworld">yoloworld_sam3_pipeline</a></li>
<li><a href="#tool-video">video_sam3_segment</a></li>
<li><a href="#tool-render">렌더링 도구</a></li>
</ul>
<h4>설정</h4>
<ul>
<li><a href="#categories">카테고리 (railway_zone.json)</a></li>
<li><a href="#prompts">프롬프트 파일</a></li>
</ul>
<h4>시스템</h4>
<ul>
<li><a href="#overview">아키텍처 개요</a></li>
<li><a href="#quickstart">빠른 시작</a></li>
<li><a href="#troubleshoot">트러블슈팅</a></li>
</ul>
</aside>
<main>
<!-- ══════════════════════════════════════════════
1. 라멘형 전철주 검출 — 최우선
══════════════════════════════════════════════ -->
<section id="raamen">
<h1>Railway Client 사용 가이드</h1>
<p class="subtitle">드론 항공 이미지 기반 철도 시설물 자동 검출 파이프라인</p>
<div class="alert alert-ok">
<strong>서버 상태:</strong> <span class="dot dot-green"></span>sam31server SAM 3.1 → localhost:8000 구동 중
</div>
<h2 class="priority-h2" id="raamen">🔴 라멘형(門) 전철주 검출 파이프라인</h2>
<span class="badge badge-orange">최우선 기능</span>
<p>드론 사선 촬영 이미지에서 門자형 라멘 구조 전철주를 검출한다. SAM3.1로 폴리곤을 추출한 후 소실점(Vanishing Point) 기반 기하학 분석으로 C(기둥)/B(빔)를 분류, 라멘 그룹을 판정한다.</p>
<div id="raamen-cb">
<h3>B / C 접두어 — 분류 기준</h3>
<div class="cb-legend">
<div class="cb-item">
<div class="cb-box" style="background:#1a3a6e; color:#93c5fd; border:2px solid #3b82f6;">C</div>
<div>
<strong>Column (기둥)</strong><br>
<span style="color:var(--muted); font-size:.82rem;">소실점 방향으로 향하는 수직·사선 폴리곤.<br>각도 임계값(<code>--c-thresh</code>, 기본 20°) 이내.</span>
</div>
</div>
<div class="cb-item">
<div class="cb-box" style="background:#3a1a00; color:#fed7aa; border:2px solid #f97316;">B</div>
<div>
<strong>Beam (빔)</strong><br>
<span style="color:var(--muted); font-size:.82rem;">수평으로 뻗은 폴리곤.<br>소실점 방향에서 크게 벗어난 것.</span>
</div>
</div>
</div>
<div class="alert alert-info">
라멘 구조 판정: <strong>B 1개 + 그 아래 C 2개 이상</strong> → 門자형 라멘으로 분류. 출력 이미지에 B/C 접두어 + 그룹 번호로 표시.
</div>
</div>
<div id="raamen-pipeline">
<h3>2단계 파이프라인</h3>
<div class="pipeline-row">
<div class="pipe-box">① detect_all_objects.py</div>
<span class="pipe-arrow"></span>
<div class="pipe-box" style="color:var(--muted)">JSON annotation<br><small>(catenary_pole polygons)</small></div>
<span class="pipe-arrow"></span>
<div class="pipe-box highlight">② detect_raamen.py</div>
<span class="pipe-arrow"></span>
<div class="pipe-box" style="color:var(--muted)">B/C 분류<br>라멘 그룹 판정</div>
</div>
</div>
<h3>Step 1 — 전철주 폴리곤 추출</h3>
<pre><code>cd D:\MYCLAUDE_PROJECT\railway-client
.venv\Scripts\python.exe tools/detect_all_objects.py `
--input "data/역사구간/1.회덕역/DJI_20260306100900_0034.JPG" `
--categories configs/railway_zone.json `
--tiles all `
--cols 4 --rows 3 --overlap 0.10 --workers 2</code></pre>
<p>출력: <code>output/detect/DJI_20260306100900_0034.json</code> — X-AnyLabeling 호환 annotation</p>
<h3>Step 2 — 라멘 구조 분석</h3>
<pre><code>.venv\Scripts\python.exe tools/detect_raamen.py `
--image "data/역사구간/1.회덕역/DJI_20260306100900_0034.JPG" `
--label "output/detect/DJI_20260306100900_0034.json" `
--output "output/raamen/DJI_20260306100900_0034_raamen.png"</code></pre>
<p>출력: 이미지 위에 C/B 라벨 + 라멘 그룹 번호 오버레이</p>
<div id="raamen-args">
<h3>detect_raamen.py 파라미터</h3>
<table>
<tr><th>파라미터</th><th>기본값</th><th>설명</th></tr>
<tr><td><code>--image</code></td><td>필수</td><td>원본 이미지 경로</td></tr>
<tr><td><code>--label</code></td><td>필수</td><td>JSON annotation 파일 (detect_all_objects 출력)</td></tr>
<tr><td><code>--output</code></td><td>자동 생성</td><td>결과 이미지 저장 경로</td></tr>
<tr><td><code>--class-names</code></td><td><code>catenary_pole</code></td><td>분석할 클래스 이름 (쉼표 구분)</td></tr>
<tr><td><code>--class-ids</code></td><td>없음</td><td>클래스 ID로 필터링 (대안)</td></tr>
<tr><td><code>--epsilon</code></td><td>4.0</td><td>폴리곤 단순화 강도 (높을수록 단순)</td></tr>
<tr><td><code>--c-thresh</code></td><td>20.0°</td><td>C(기둥) 판정 각도 임계값</td></tr>
<tr><td><code>--b-max-diff</code></td><td>75.0°</td><td>B(빔) 최대 각도 편차</td></tr>
<tr><td><code>--margin</code></td><td>30px</td><td>근접성 그룹핑 거리</td></tr>
</table>
</div>
<h3>4단계 내부 처리 흐름</h3>
<table>
<tr><th>Phase</th><th>처리 내용</th></tr>
<tr><td>Phase 1</td><td>폴리곤 단순화(approxPolyDP) + 소실점(Vanishing Point) 계산</td></tr>
<tr><td>Phase 2</td><td>동적 C/B 분류 — 소실점 기반 기대 각도와 비교</td></tr>
<tr><td>Phase 3</td><td>근접성 기반 그룹핑 — B 앵커 기준으로 아래 C 탐색</td></tr>
<tr><td>Phase 4</td><td>라멘 구조 판정 + 가림(occlusion) 예외 처리</td></tr>
</table>
</section>
<!-- ══════════════════════════════════════════════
2. Everything 탐색 모드
══════════════════════════════════════════════ -->
<section id="everything">
<h2>Everything 탐색 모드 (Discovery Sweep)</h2>
<p>새 지역·새 구간을 처음 분석할 때 사용. SAM3.1 텍스트 grounding 방식이므로 완전 무프롬프트는 불가 — 대신 광범위한 <strong>Discovery Prompt</strong>로 이미지에 존재하는 모든 객체를 일괄 검출하고 라벨 빈도를 집계한다.</p>
<div class="alert alert-info">
<strong>용도:</strong> 어떤 객체가 이 이미지에 얼마나 나오는지 파악 → 이후 <code>detect_all_objects.py</code> 의 카테고리·conf 튜닝에 활용.
</div>
<div id="everything-usage">
<h3>사용법</h3>
<pre><code>cd D:\MYCLAUDE_PROJECT\railway-client
# 기본 (8×6 타일)
.venv\Scripts\python.exe tools/sam3_everything_explore.py `
--input "data/역사구간/1.회덕역/DJI_20260306100900_0034.JPG"
# 타일 수 조정 (고해상도 드론 이미지)
.venv\Scripts\python.exe tools/sam3_everything_explore.py `
--input "data/역사구간/..." `
--cols 8 --rows 6 --conf 0.10 --workers 4
# 특정 영역(ROI)만 탐색
.venv\Scripts\python.exe tools/sam3_everything_explore.py `
--input "..." `
--zone 1000 500 3000 2500
# Discovery Prompt에 추가 키워드 삽입
.venv\Scripts\python.exe tools/sam3_everything_explore.py `
--input "..." `
--prompt-extra "signal box, relay cabinet"</code></pre>
</div>
<h3>파라미터</h3>
<table>
<tr><th>파라미터</th><th>기본값</th><th>설명</th></tr>
<tr><td><code>--input</code></td><td>필수</td><td>이미지 파일 경로</td></tr>
<tr><td><code>--cols</code></td><td>8</td><td>가로 타일 분할 수</td></tr>
<tr><td><code>--rows</code></td><td>6</td><td>세로 타일 분할 수</td></tr>
<tr><td><code>--overlap</code></td><td>0.10</td><td>타일 겹침 비율</td></tr>
<tr><td><code>--conf</code></td><td>0.10</td><td>신뢰도 임계값 (탐색 모드라 낮게 설정)</td></tr>
<tr><td><code>--workers</code></td><td>4</td><td>병렬 처리 스레드 수</td></tr>
<tr><td><code>--nms</code></td><td>0.40</td><td>NMS IoU 임계값</td></tr>
<tr><td><code>--zone</code></td><td>없음</td><td>ROI 좌표 (X1 Y1 X2 Y2) — 관심 영역만 처리</td></tr>
<tr><td><code>--prompt-extra</code></td><td>없음</td><td>Discovery Prompt에 추가할 키워드</td></tr>
</table>
<div id="everything-prompt">
<h3>Discovery Prompt (내장)</h3>
<p>다음 키워드 묶음이 자동으로 SAM3.1에 전달된다:</p>
<pre><code>railroad track, railway rail,
catenary pole, overhead line pole, electric pole,
overhead wire, catenary wire, power line cable,
railway sleeper, concrete tie,
guardrail, highway barrier, road fence,
bridge, viaduct, overpass,
vegetation, tree, bush, grass,
building, structure, roof, wall,
vehicle, car, truck,
road, asphalt, pavement,
slope, embankment, retaining wall,
noise barrier, sound wall,
signal, sign board,
small dark object on ballast, small square metal box on ground,
control box on ballast, gray square lid on gravel,
flat metal cover on ground, bright square object on gravel</code></pre>
<p><code>--prompt-extra</code>로 추가 키워드를 붙일 수 있다.</p>
</div>
<div id="everything-output">
<h3>출력 해석</h3>
<pre><code>output/everything/
└── DJI_20260306100900_0034_everything.jpg ← 모든 segment 오버레이
콘솔 출력 (라벨 빈도 집계):
catenary pole : 47
railway rail : 31
overhead wire : 28
concrete tie : 19
control box on ballast: 8
...</code></pre>
<div class="alert alert-info">
<strong>활용:</strong> 빈도 상위 라벨 → <code>railway_zone.json</code>의 프롬프트에 추가하거나 conf 조정. 빈도 0인 카테고리 → 해당 이미지에 없는 객체.
</div>
<h3>탐색 후 타겟 검출로 전환</h3>
<div class="pipeline-row">
<div class="pipe-box highlight">sam3_everything_explore</div>
<span class="pipe-arrow"></span>
<div class="pipe-box" style="color:var(--muted)">라벨 빈도 확인<br>프롬프트 튜닝</div>
<span class="pipe-arrow"></span>
<div class="pipe-box">detect_all_objects</div>
<span class="pipe-arrow"></span>
<div class="pipe-box">detect_raamen (선택)</div>
</div>
</div>
</section>
<!-- ══════════════════════════════════════════════
3. 서버
══════════════════════════════════════════════ -->
<section id="server-setup">
<h2>서버 설정 (sam31server)</h2>
<h3 id="overview">시스템 아키텍처</h3>
<pre><code>┌─────────────────────────────────┐
│ sam31server │
│ FastAPI app/main.py :8000 │
│ GET /health │
│ POST /v1/predict │
│ Model: SAM 3.1 Multiplex (CUDA)│
└──────────────┬──────────────────┘
│ HTTP localhost:8000
┌──────────────▼──────────────────┐
│ railway-client tools │
│ detect_all_objects.py │ ─┐
│ detect_raamen.py ◀────────┘ │ 라멘 파이프라인
│ sam3_everything_explore.py │
│ sam3_autolabel.py ... │
└─────────────────────────────────┘</code></pre>
<h3>요구사항</h3>
<table>
<tr><th>항목</th><th>버전</th></tr>
<tr><td>Python</td><td>3.12</td></tr>
<tr><td>PyTorch</td><td>2.10.0+cu126 (CUDA 필수)</td></tr>
<tr><td>triton-windows</td><td>3.6.0.post25</td></tr>
<tr><td>FastAPI</td><td>≥ 0.115.0</td></tr>
<tr><td>SAM 3.1 모델</td><td>HuggingFace 자동 다운로드 (최초 1회)</td></tr>
</table>
<h3>디렉토리 구조</h3>
<pre><code>sam31server/
├── sam3/ ← SAM 3.1 모델 라이브러리
├── app/
│ ├── main.py ← FastAPI 진입점
│ ├── api/predict.py ← POST /v1/predict
│ ├── api/health.py ← GET /health
│ ├── models/segment_anything_3.py
│ ├── core/registry.py
│ └── tasks/inference.py
├── configs/
│ ├── server.yaml ← 포트·동시성
│ ├── models.yaml ← enabled: segment_anything_3
│ └── auto_labeling/segment_anything_3.yaml
├── bpe_simple_vocab_16e6.txt.gz
└── start_server.bat</code></pre>
</section>
<section id="server-start">
<h2>서버 시작</h2>
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<h4>sam31server 디렉토리에서 실행</h4>
<pre><code>cd D:\MYCLAUDE_PROJECT\sam31server
# 배치파일
start_server.bat
# 또는 직접
.venv\Scripts\python.exe -m app.main
# 또는 uvicorn 직접 (Windows 이벤트루프 이슈 우회)
.venv\Scripts\python.exe -m uvicorn app.main:app --host 0.0.0.0 --port 8000</code></pre>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<h4>정상 구동 확인</h4>
<pre><code>INFO | Loading [segment_anything_3] (Segment Anything 3.1)...
INFO | SAM3 model loaded successfully
INFO | Uvicorn running on http://0.0.0.0:8000</code></pre>
</div>
</div>
</section>
<section id="server-api">
<h2>API 엔드포인트</h2>
<h3>POST /v1/predict</h3>
<pre><code>{
"model": "segment_anything_3",
"image": "&lt;base64 JPEG&gt;",
"params": {
"text_prompt": "catenary pole, overhead line pole",
"conf_threshold": 0.25,
"show_masks": true,
"show_boxes": false
}
}</code></pre>
<p>응답:</p>
<pre><code>{
"data": {
"shapes": [
{ "label": "catenary pole", "shape_type": "polygon",
"points": [[x,y], ...], "score": 0.87 }
]
}
}</code></pre>
<table>
<tr><th>params 키</th><th>설명</th></tr>
<tr><td><code>text_prompt</code></td><td>텍스트 grounding (쉼표로 다중 객체)</td></tr>
<tr><td><code>marks</code></td><td>rectangle / point 시각 프롬프트</td></tr>
<tr><td><code>conf_threshold</code></td><td>신뢰도 임계값</td></tr>
<tr><td><code>show_masks</code></td><td>polygon 반환 (true 권장)</td></tr>
<tr><td><code>show_boxes</code></td><td>bounding box 반환</td></tr>
</table>
</section>
<!-- ══════════════════════════════════════════════
4. 전체 도구
══════════════════════════════════════════════ -->
<section id="tools">
<h2>클라이언트 도구 전체 목록</h2>
<table>
<tr><th>스크립트</th><th>기능</th><th>출력</th><th>우선순위</th></tr>
<tr style="background:#1a0e00;">
<td><code>detect_all_objects.py</code></td>
<td>타일 분할 + 다중 카테고리 검출 + NMS</td>
<td>PNG + JSON</td>
<td><span class="badge badge-orange">핵심</span></td>
</tr>
<tr style="background:#1a0e00;">
<td><code>detect_raamen.py</code></td>
<td>門형 라멘 전철주 B/C 분류 + 그룹 판정</td>
<td>PNG</td>
<td><span class="badge badge-orange">핵심</span></td>
</tr>
<tr style="background:#0c1a0c;">
<td><code>sam3_everything_explore.py</code></td>
<td>Discovery Sweep — 전체 객체 탐색 + 빈도 집계</td>
<td>PNG + 통계</td>
<td><span class="badge badge-green">탐색</span></td>
</tr>
<tr><td><code>sam3_autolabel.py</code></td><td>전철주 + 레일 zone 기반 자동 라벨링</td><td>JSON</td><td></td></tr>
<tr><td><code>sam3_batch_label.py</code></td><td>폴더 배치 → X-AnyLabeling JSON</td><td>JSON</td><td></td></tr>
<tr><td><code>yoloworld_sam3_pipeline.py</code></td><td>YOLO-World bbox → SAM3 polygon</td><td>JSON+PNG</td><td></td></tr>
<tr><td><code>video_sam3_segment.py</code></td><td>영상 프레임 추출 → 세그멘테이션</td><td>JSON/frame</td><td></td></tr>
<tr><td><code>render_everything_by_label.py</code></td><td>라벨별 색상 렌더링</td><td>PNG</td><td></td></tr>
<tr><td><code>render_label_polygons.py</code></td><td>polygon 오버레이</td><td>PNG</td><td></td></tr>
<tr><td><code>sam3_segment_everything.py</code></td><td>이미지 전체 세그멘테이션</td><td>PNG</td><td></td></tr>
<tr><td><code>railway_pipeline.py</code></td><td>전체 파이프라인 통합</td><td>종합</td><td></td></tr>
</table>
</section>
<section id="tool-detect-all">
<h2>detect_all_objects.py</h2>
<pre><code>.venv\Scripts\python.exe tools/detect_all_objects.py `
--input "data/역사구간/..." `
--categories configs/railway_zone.json `
--tiles all `
--cols 4 --rows 3 --overlap 0.10 --workers 2</code></pre>
<table>
<tr><th>파라미터</th><th>기본값</th><th>설명</th></tr>
<tr><td><code>--input</code></td><td>필수</td><td>이미지 또는 폴더</td></tr>
<tr><td><code>--categories</code></td><td>railway_zone.json</td><td>카테고리 설정 파일</td></tr>
<tr><td><code>--tiles</code></td><td>all</td><td><code>9-24</code>, <code>1,5,9</code>, <code>all</code></td></tr>
<tr><td><code>--cols / --rows</code></td><td>8 / 6</td><td>타일 분할 수</td></tr>
<tr><td><code>--overlap</code></td><td>0.10</td><td>타일 겹침 비율</td></tr>
<tr><td><code>--workers</code></td><td>4</td><td>병렬 스레드</td></tr>
<tr><td><code>--conf</code></td><td>카테고리별</td><td>전역 신뢰도 오버라이드</td></tr>
</table>
<p>출력: <code>output/detect/이미지명_detected.png</code> + <code>.json</code></p>
</section>
<section id="tool-autolabel">
<h2>sam3_autolabel.py</h2>
<pre><code>.venv\Scripts\python.exe tools/sam3_autolabel.py `
--input data/역사구간/1.회덕역/ `
--output output/labels/</code></pre>
</section>
<section id="tool-batch">
<h2>sam3_batch_label.py</h2>
<pre><code>.venv\Scripts\python.exe tools/sam3_batch_label.py `
--input data/역사구간/ `
--prompt "railway catenary pole" `
--output output/batch_labels/ `
--workers 8</code></pre>
</section>
<section id="tool-yoloworld">
<h2>yoloworld_sam3_pipeline.py</h2>
<pre><code>.venv\Scripts\python.exe tools/yoloworld_sam3_pipeline.py `
--input data/역사구간/ `
--output output/labeled/</code></pre>
</section>
<section id="tool-video">
<h2>video_sam3_segment.py</h2>
<pre><code>.venv\Scripts\python.exe tools/video_sam3_segment.py</code></pre>
<p>스크립트 상단에서 영상 경로·프롬프트·프레임 추출 간격 직접 수정.</p>
</section>
<section id="tool-render">
<h2>렌더링 도구</h2>
<pre><code># 라벨별 색상 렌더링
.venv\Scripts\python.exe tools/render_everything_by_label.py `
--input output/labels/image.json --image data/.../image.JPG
# polygon 오버레이
.venv\Scripts\python.exe tools/render_label_polygons.py `
--input output/labels/ --image data/.../</code></pre>
</section>
<!-- ══════════════════════════════════════════════
5. 카테고리
══════════════════════════════════════════════ -->
<section id="categories">
<h2>카테고리 설정 — railway_zone.json</h2>
<p>경로: <code>configs/railway_zone.json</code></p>
<table>
<tr><th>이름</th><th>한국어</th><th>색상</th><th>conf</th><th>우선순위</th></tr>
<tr><td><code>control_box</code></td><td>컨트롤박스</td><td><span class="swatch" style="background:rgb(255,255,0)"></span>노랑</td><td>0.15</td><td>2</td></tr>
<tr><td><code>vehicle</code></td><td>차량</td><td><span class="swatch" style="background:#fff"></span>흰색</td><td>0.25</td><td>2</td></tr>
<tr><td><code>catenary_pole</code></td><td>전철주</td><td><span class="swatch" style="background:rgb(255,0,255)"></span>마젠타</td><td>0.25</td><td>3</td></tr>
<tr><td><code>railway</code></td><td>철도 레일</td><td><span class="swatch" style="background:rgb(0,0,255)"></span>파랑</td><td>0.25</td><td>4</td></tr>
<tr><td><code>fence</code></td><td>팬스/울타리</td><td><span class="swatch" style="background:rgb(0,255,0)"></span>초록</td><td>0.50</td><td>5</td></tr>
<tr><td><code>sleeper</code></td><td>침목</td><td><span class="swatch" style="background:rgb(0,128,255)"></span>하늘</td><td>0.20</td><td>6</td></tr>
<tr><td><code>ballast</code></td><td>자갈도상</td><td><span class="swatch" style="background:rgb(30,50,100)"></span>남색</td><td>0.20</td><td>7</td></tr>
<tr><td><code>bracket</code></td><td>브라켓/암</td><td><span class="swatch" style="background:rgb(0,80,200)"></span>청색</td><td>0.20</td><td>8</td></tr>
<tr><td><code>bridge</code></td><td>교량/교각</td><td><span class="swatch" style="background:rgb(0,165,255)"></span>주황하늘</td><td>0.25</td><td>8</td></tr>
<tr><td><code>building</code></td><td>건물</td><td><span class="swatch" style="background:rgb(50,50,255)"></span>청보라</td><td>0.25</td><td>8</td></tr>
<tr><td><code>retaining_wall</code></td><td>방음벽/옹벽</td><td><span class="swatch" style="background:rgb(60,180,60)"></span>연두</td><td>0.25</td><td>9</td></tr>
<tr><td><code>culvert</code></td><td>암거/소교량</td><td><span class="swatch" style="background:rgb(180,60,180)"></span>보라</td><td>0.20</td><td>9</td></tr>
<tr><td><code>service_road</code></td><td>유지보수 도로</td><td><span class="swatch" style="background:rgb(80,120,160)"></span>회청</td><td>0.30</td><td>10</td></tr>
<tr><td><code>farmland</code></td><td>농지</td><td><span class="swatch" style="background:rgb(50,200,50)"></span>밝은초록</td><td>0.25</td><td>11</td></tr>
<tr><td><code>vegetation</code></td><td>식생</td><td><span class="swatch" style="background:rgb(0,120,0)"></span>짙은초록</td><td>0.25</td><td>12</td></tr>
</table>
<div class="alert alert-info">
<strong>우선순위(priority):</strong> 낮을수록 cross-class NMS에서 우선 보존. 우선순위 2 = 최우선(컨트롤박스·차량), 12 = 최하위(식생).
</div>
</section>
<section id="prompts">
<h2>프롬프트 파일</h2>
<p>일부 도구는 <code>prompts/</code> 폴더의 텍스트 파일을 사용:</p>
<pre><code>prompts/
├── pole.txt ← 전철주 검출용 프롬프트
├── rail.txt ← 레일 검출용 프롬프트
├── bracket.txt ← 브라켓 검출용
└── sleeper.txt ← 침목 검출용</code></pre>
</section>
<!-- ══════════════════════════════════════════════
6. 빠른 시작 / 트러블슈팅
══════════════════════════════════════════════ -->
<section id="quickstart">
<h2>빠른 시작 — 단일 이미지 전체 파이프라인</h2>
<div class="step">
<div class="step-num orange">1</div>
<div class="step-body">
<h4>SAM3.1 서버 시작 (터미널 A)</h4>
<pre><code>cd D:\MYCLAUDE_PROJECT\sam31server
.venv\Scripts\python.exe -m app.main</code></pre>
</div>
</div>
<div class="step">
<div class="step-num orange">2</div>
<div class="step-body">
<h4>[선택] 탐색 모드로 객체 파악 (터미널 B)</h4>
<pre><code>cd D:\MYCLAUDE_PROJECT\railway-client
.venv\Scripts\python.exe tools/sam3_everything_explore.py `
--input "data/역사구간/1.회덕역/.../DJI_20260306100900_0034.JPG" `
--cols 4 --rows 3</code></pre>
</div>
</div>
<div class="step">
<div class="step-num orange">3</div>
<div class="step-body">
<h4>전철주 검출 + JSON annotation 생성</h4>
<pre><code>.venv\Scripts\python.exe tools/detect_all_objects.py `
--input "data/역사구간/1.회덕역/.../DJI_20260306100900_0034.JPG" `
--categories configs/railway_zone.json `
--tiles all --cols 4 --rows 3 --overlap 0.10 --workers 2</code></pre>
</div>
</div>
<div class="step">
<div class="step-num orange">4</div>
<div class="step-body">
<h4>라멘 구조 분석</h4>
<pre><code>.venv\Scripts\python.exe tools/detect_raamen.py `
--image "data/역사구간/1.회덕역/.../DJI_20260306100900_0034.JPG" `
--label "output/detect/DJI_20260306100900_0034.json" `
--output "output/raamen/DJI_20260306100900_0034_raamen.png"</code></pre>
</div>
</div>
</section>
<section id="troubleshoot">
<h2>트러블슈팅</h2>
<table>
<tr><th>오류</th><th>원인</th><th>해결</th></tr>
<tr><td><code>Connection refused :8000</code></td><td>서버 미실행</td><td>sam31server에서 서버 시작</td></tr>
<tr><td><code>Torch not compiled with CUDA</code></td><td>CPU 버전 torch</td><td><code>pip install torch==2.10.0+cu126 --index-url .../cu126</code></td></tr>
<tr><td><code>No module named 'triton'</code></td><td>triton-windows 미설치</td><td><code>pip install triton-windows==3.6.0.post25</code></td></tr>
<tr><td><code>No module named 'decord'</code></td><td>decord 미설치</td><td><code>pip install decord</code></td></tr>
<tr><td>라멘 검출 없음</td><td>catenary_pole conf 너무 높음</td><td>Step 1에서 <code>--conf 0.10</code> 로 낮춤</td></tr>
<tr><td>C/B 분류 오류</td><td>소실점 계산 불안정</td><td><code>--c-thresh</code> 조정 (기본 20°, 높이면 C 기준 완화)</td></tr>
<tr><td>Windows 이벤트루프 경고</td><td>Python 3.12 + uvicorn</td><td><code>python -m uvicorn app.main:app --host 0.0.0.0 --port 8000</code></td></tr>
</table>
<hr>
<p style="color:var(--muted); font-size:.8rem; text-align:center;">Railway Client Guide · SAM 3.1 + FastAPI · 2026-06</p>
</section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,280 @@
# 철도 디지털 트윈 — AI 라벨링 파이프라인 진행 현황
> 기준일: 2026-05-22
> 프로젝트: `d:\MYCLAUDE_PROJECT\x-anylabeling01`
> 목표: 드론 부감 이미지에서 철도 지장물 자동 탐지 → YOLOv26 학습 데이터 구축 → 디지털 트윈
---
## 1. 탐지 대상 (지장물 카테고리)
| 카테고리 | 한글 | 상태 |
|---|---|---|
| raamen (ラーメン) | 라멘형 전철주 | ✅ 완료 |
| pole | 전철주 일반 | ✅ 완료 |
| rail | 레일 | ✅ 완료 |
| sleeper | 침목 | ✅ 완료 |
| control_box | 컨트롤박스 (소형 금속 박스) | 🔄 진행 중 |
---
## 2. 완료된 작업
### 2-1. 라멘형 전철주 검출 (`tools/detect_raamen.py`)
- **방식**: SAM3.1 텍스트 grounding + VP(소실점) 기반 H/V 분류
- **입력**: 드론 고해상도 이미지 (.JPG), 선택적으로 `--json` (detect_all_objects.py 결과 재사용)
- **출력**: AnyLabeling JSON (폴리곤 + H/V 라벨)
- **핵심 알고리즘**:
- 이미지를 타일 분할 → SAM3.1 병렬 호출
- VP(소실점) 다중 시드 + 반복 정제로 수직(V) / 수평(H) 분류
- H-max-diff 필터로 수평 빔과 수직 기둥 구분
- Cross-NMS로 중복 제거
- **최근 커밋**: `923c396` (VP 다중 시드 + 반복 정제로 V/H 분류 정확도 향상)
### 2-2. 전체 객체 검출 UI (`tools/web_ui.py`)
- FastAPI + uvicorn 기반 웹 UI
- 이미지 선택 → 타일/카테고리 설정 → `detect_all_objects.py` 실행 → 결과 프리뷰
- **개선 사항** (이번 세션):
- 마우스 휠 줌 + 드래그 팬 추가
- `--save-labels``--save-json` 수정 (txt 금지, JSON만)
- `--debug` 플래그 제거 (존재하지 않는 옵션)
- 프리뷰 해상도 1200 → 2000px
### 2-3. 전체 객체 검출 (`tools/detect_all_objects.py`)
- SAM3.1 텍스트 grounding, 카테고리별 타일 검출
- `configs/railway_zone.json` 카테고리 설정 사용
- **출력 경로 개선**: `output/detect/{이미지명}/tiles{N}_{카테고리}_{번호}.jpg`
- `--save-json` 옵션으로 AnyLabeling JSON 저장
### 2-4. SAM3 Everything 탐색 (`tools/sam3_everything_explore.py`)
- 38개 항목 광역 프롬프트로 이미지 전체 탐색
- 타일 병렬 처리 + NMS
- JSON 라벨 통계 출력 (text_prompt 후보 발굴용)
- `--prompt-extra` 옵션으로 추가 어휘 주입 가능
- **테스트 결과** (DJI_20260306113838_0004.JPG, 8×6 타일):
- 총 7,111개 세그먼트 검출
- control_box 관련 라벨 1,740개 매칭
- **문제**: 정밀도 ~1-2%, false positive 폭증 → SAM3 텍스트 grounding 한계 확인
---
## 3. 핵심 결정 사항
### 3-1. YOLOv26 단독 전환 (2026-05-19)
**배경**: SAM3.1 텍스트 grounding으로 control_box 탐지 시도 → 완전 실패
| 시도 | 결과 |
|---|---|
| 고립 프롬프트 ("railway control box, electrical cabinet...") | 0 → 0 검출 |
| conf 0.20 → 0.10 낮춤 | 0 → 0 검출 |
| 16×12 세밀한 타일 그리드 | 0 → 0 검출 |
| 인접 4개 타일 수동 실험 | 0 → 0 검출 |
| 38개 광역 프롬프트 | 1,740개 매칭, 정밀도 ~1-2% |
**결론**: SAM3.1은 부감 드론 시점 소형 객체 텍스트 grounding에 부적합
**전환**: **YOLOv26 학습 데이터셋 구축 → fine-tune → 추론** 경로
**준비 완료**:
- `yolo26n.pt`, `yolo26n-seg.pt` 사전학습 모델 (프로젝트 루트)
- ultralytics 26.x 설치 완료
- X-AnyLabeling-Server YOLO endpoint 연동 완료
### 3-2. SAM3 활용 방식 변경
- **폐기**: SAM3 텍스트 grounding → 단독 검출기
- **유지**: SAM3 everything 모드 → 후보 bbox 생성기 (인간 라벨링 보조)
- **신규**: YOLOv26 fine-tune → production 검출기
---
## 4. 현재 진행 중: control_box 라벨링 파이프라인
### 4-1. 전략 (부트스트랩)
```
SAM3 everything → 1,740개 후보 bbox
labeling_server.py (인간 투표)
MIN_VOTES=3, TRUE_RATIO=0.6 필터
YOLO 학습용 txt 라벨
YOLOv26 fine-tune (yolo26n.pt)
production 검출기
```
### 4-2. 라벨링 서버 (`tools/labeling_server.py`) — v2
**실행 명령:**
```bash
python tools/labeling_server.py \
--json "data/역사이미지/slope/DJI_20260306113838_0004_everything.json" \
--reset
```
**브라우저**: `http://서버IP:7001`
**UI 방식** (v2 — 전체 이미지 오버레이):
- 전체 드론 이미지 표시 (최대 3000px)
- SAM3 후보 1,740개를 색상 bbox로 오버레이
- 🟡 노랑: 미투표
- 🟢 초록: 컨트롤박스 YES
- 🔴 빨강: 아님 NO
- ⬜ 회색: 타인 투표 완료
- 마우스 휠 줌 / 드래그 이동 / F키 맞춤
- bbox 클릭 → YES/NO 즉시 저장 (SQLite)
- 호버 → 라벨명 + 점수 툴팁
**집계 기준**:
- `MIN_VOTES = 3` (3인 이상 투표)
- `TRUE_RATIO = 0.6` (60% 이상 YES)
**YOLO 내보내기**: `POST /api/export``labels/yolo_export/*.txt`
**DB**: `labels/labeling.db` (SQLite, 로컬)
- `candidates` 테이블: json_idx, label, score, bbox, image_path
- `votes` 테이블: candidate_id, user, vote, ts (UNIQUE 제약 — 1인 1표)
### 4-3. 현재 데이터
| 항목 | 값 |
|---|---|
| 입력 이미지 | `data/역사이미지/slope/DJI_20260306113838_0004.JPG` |
| everything JSON | `DJI_20260306113838_0004_everything.json` |
| 전체 세그먼트 | 7,111개 |
| control_box 후보 | 1,740개 |
| 라벨링 진행 | 미시작 (서버 실행 대기) |
---
## 5. 다음 할 일 (TODO)
### 즉시
- [ ] `labeling_server.py` 배포 → 직원 20명 라벨링 시작
- [ ] 추가 이미지 `_everything.json` 생성 (현재 1장 → 더 많이 필요)
### SAM3 Everything 프롬프트 재설계 (⚠️ 중요)
- **문제**: 현재 DISCOVERY_PROMPT 38개 항목이 control_box를 제대로 분리 못함
- **방향**: 더 구체적인 시각적 특징 기반 프롬프트 필요
- 기존: `"small square box", "compact trackside junction box"` 등 → FP 폭증
- 새 방향 검토 필요:
- 크기/형태 명시: `"small gray square metal lid on ground"`, `"square dark gray lid flush with ballast"`
- 재질/색상 특징: `"weathered gray metal surface"`, `"flat square cover"`
- 부감 시점 특화: `"top-down view of small electrical enclosure"`
### 학습 후
- [ ] 50-100개 이상 확정 라벨 → `yolo train` 실행
```bash
yolo train model=yolo26n.pt data=configs/control_box.yaml epochs=100 imgsz=640
```
- [ ] 검출 성능 검증 → 미달 시 데이터 추가 수집
---
## 6. 설정 파일
### `configs/railway_zone.json` (control_box 항목)
```json
{
"name": "control_box",
"name_kr": "컨트롤박스",
"prompt": "small square gray metal box beside rail, compact trackside junction box, small near-square electrical enclosure on the ground, small cube-shaped equipment box next to track",
"conf": 0.15,
"priority": 2
}
```
### SAM3 Everything DISCOVERY_PROMPT 현재 상태 (재설계 필요)
```
"railroad track, railway rail,
catenary pole, overhead line pole, electric pole,
overhead wire, catenary wire, power line cable,
railway sleeper, concrete tie,
guardrail, highway barrier, road fence,
bridge, viaduct, overpass,
vegetation, tree, bush, grass,
building, structure, roof, wall,
vehicle, car, truck,
road, asphalt, pavement,
slope, embankment, retaining wall,
noise barrier, sound wall,
signal, sign board"
```
→ control_box 관련 항목 부재 → everything 탐색에서 누락됨
---
## 7. 기술 제약 / 주의사항
- **CLI 명령어**: bash 한 종류만 사용 (PowerShell 혼용 금지)
- **라벨 저장**: `--save-json` 옵션만 사용 (txt 금지)
- **detect_all_objects.py**: `--workers 8` 항상 명시
- **SAM3 서버**: `python.exe -m app.main` (X-AnyLabeling-Server 디렉토리에서 실행, localhost:8000)
- **GDINO**: 폐기됨. SAM3.1 텍스트 프롬프트 (`params.text_prompt`) 직접 사용
- **YOLOv26**: ultralytics 26.x, 모델 파일 `yolo26n.pt` / `yolo26n-seg.pt` (프로젝트 루트)
---
## 8. 주요 파일 구조
```
x-anylabeling01/
├── tools/
│ ├── detect_all_objects.py # 메인 검출 도구 (SAM3 타일 검출)
│ ├── detect_raamen.py # 라멘형 전철주 전용 검출
│ ├── web_ui.py # 웹 UI (FastAPI, port:8001)
│ ├── labeling_server.py # CAPTCHA 라벨링 서버 (port:7001) ← 신규 v2
│ ├── sam3_everything_explore.py # SAM3 전체 탐색 (프롬프트 발굴용) ← 개선 필요
│ ├── sam3_segment_everything.py # SAM3 포인트 그리드 세그멘테이션
│ └── post_merge_poles.py # 전철주 병합 후처리
├── configs/
│ └── railway_zone.json # 카테고리별 프롬프트/conf/priority
├── labels/
│ ├── labeling.db # 라벨링 투표 SQLite DB
│ └── yolo_export/ # YOLO txt 내보내기 결과
├── output/
│ └── detect/{이미지명}/ # 검출 결과 이미지 (타일/카테고리별)
├── yolo26n.pt # YOLOv26 nano 사전학습 모델
└── yolo26n-seg.pt # YOLOv26 nano segmentation 모델
```
---
## 9. 다음 세션 시작 프롬프트
```
철도 디지털 트윈 프로젝트 (x-anylabeling01) 이어서 진행합니다.
[현재 상태]
- detect_raamen.py (라멘형 전철주) 검출 완료
- detect_all_objects.py (다중 카테고리 SAM3 검출) 완료
- web_ui.py 개선 완료 (줌/팬, --save-json, 폴더 구조)
- labeling_server.py v2 완료 (전체 이미지 + bbox 오버레이, 클릭 투표)
[오늘 할 일: SAM3 Everything 프롬프트 재설계]
tools/sam3_everything_explore.py의 DISCOVERY_PROMPT를 재설계해야 합니다.
현재 문제:
- 기존 38개 항목 광역 프롬프트로 DJI_20260306113838_0004.JPG 처리 시
- 7,111개 세그먼트, control_box 관련 1,740개 매칭 → 정밀도 ~1-2%
- control_box (소형 정사각형 금속 박스, 자갈도상 옆 지면에 놓인 것)가 제대로 분리 안 됨
목표:
- control_box를 효과적으로 탐지할 수 있는 SAM3 텍스트 프롬프트 발굴
- 혹은 SAM3 everything → 후보 bbox 1,740개에서 더 정밀하게 필터링하는 방법 개선
- 최종적으로는 YOLOv26 학습 데이터 50-100개 확보
[기술 제약]
- CLI는 bash만 사용 (PowerShell 혼용 금지)
- 라벨 저장은 --save-json만 (txt 금지)
- SAM3 서버: python.exe -m app.main (X-AnyLabeling-Server에서, localhost:8000)
- detect_all_objects.py 실행 시 --workers 8 항상 명시
```