Initial publish: SamGeo3 multi-prompt segmentation lab.

Scripts, prompt JSON tiers, usage docs, and README. Input images
(data/) and segmentation outputs (output/) are gitignored.
This commit is contained in:
minsung
2026-07-15 16:11:59 +09:00
commit 34a885bcf8
16 changed files with 4586 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
.venv/
__pycache__/
*.py[cod]
*.egg-info/
.ipynb_checkpoints/
.DS_Store
Thumbs.db
# Local secrets / HF
.env
.hf_token
# ---- Input / output data (publish code only) ----
# Keep folder structure via .gitkeep; ignore all payloads.
data/*
!data/.gitkeep
output/*
!output/.gitkeep
# Large model / geospatial / mask artifacts (anywhere)
*.pt
*.pth
*.ckpt
*.npy
*.tif
*.tiff
*.jp2
*.gpkg
*.shp
*.dbf
*.shx
*.prj
*.las
*.laz
# Accidental root-level media dumps
/*.jpg
/*.jpeg
/*.JPG
/*.JPEG
/*.png
/*.PNG
/*.webp
/*.mp4
/*.mov
+180
View File
@@ -0,0 +1,180 @@
# samgeo3-lab
독립 SamGeo3 (SAM 3 / SAM 3.1) 세그멘테이션 실험 프로젝트.
- **패키지**: [segment-geospatial](https://github.com/opengeos/segment-geospatial) (`SamGeo3`)
- **문서**: https://samgeo.gishub.org
- **범위**: 설치 검증 + 단일 이미지 텍스트 프롬프트 세그멘테이션
- **비범위**: railway-client / sam31server 통합, 철도 후처리, REST API
기존 SAM 서버 env/코드와 **공유하지 않습니다**. 이 폴더의 `.venv`만 사용하세요.
## 가정 (이 머신 기준)
| 항목 | 상태 |
|------|------|
| GPU | NVIDIA RTX 3060 (드라이버 CUDA 13.x 가능) |
| conda / pixi | 미설치 → **`uv` + Python 3.12 venv** 사용 |
| HF 토큰 | `~/.cache/huggingface/token` 존재 |
| 체크포인트 | `facebook/sam3.1` HF 캐시 이미 존재 (~3.3GB) |
## 폴더 구조
```
samgeo3-lab/
README.md
requirements.txt
pyproject.toml
scripts/
check_install.py # 설치/GPU/import 확인 (+ 옵션: 모델 로드)
text_segment.py # 단일 이미지 텍스트 세그멘테이션
multi_prompt_segment.py # 멀티 프롬프트 일괄 세그 (모델 1회 로드)
prompts/
dji_20260306_0016.json # Grok+Gemini 병합 multi-object 프롬프트
README.md
docs/usage.html
data/ # 입력 이미지
output/ # 마스크/시각화 결과
.venv/ # 로컬 가상환경 (gitignore)
```
## 설치 (Windows + GPU)
PowerShell, 프로젝트 루트에서:
```powershell
cd D:\MYCLAUDE_PROJECT\samgeo3-lab
# 1) 독립 venv (Python 3.12)
uv venv --python 3.12 .venv
.\.venv\Scripts\Activate.ps1
# 2) CUDA PyTorch 먼저 (CPU wheel 덮어쓰기 방지)
uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
# 3) SamGeo3 extras only (api / all 설치 금지)
uv pip install "segment-geospatial[samgeo3]>=1.4.1" matplotlib
# 4) Windows: sam3 import에 필요 (pip extra가 빠질 수 있음)
uv pip install "triton-windows>=3.3.0.post19"
# 5) (선택) 노트북
uv pip install ipykernel jupyterlab
```
### 대안: pixi (공식 권장)
conda/pixi를 쓸 수 있으면 공식 문서 권장 경로가 더 안정적입니다.
https://samgeo.gishub.org/installation/#install-with-pixi-recommended
### Hugging Face (SAM 3 / 3.1)
1. https://huggingface.co/facebook/sam3.1 접근 승인
2. 인증: `hf auth login` (또는 기존 토큰 파일 유지)
3. (선택) 로컬 체크포인트만 쓰려면:
```powershell
$env:SAM3_CHECKPOINT_PATH = "C:\path\to\sam3.1_multiplex.pt"
```
## 실행
```powershell
cd D:\MYCLAUDE_PROJECT\samgeo3-lab
.\.venv\Scripts\Activate.ps1
# A) 패키지/GPU 확인
python scripts/check_install.py
# B) 모델 로드까지 확인 (캐시/다운로드)
python scripts/check_install.py --load-model
# C) 샘플 이미지 텍스트 세그멘테이션
python scripts/text_segment.py
# 동의어 (기본 confidence=0.3 — sam3.1 score가 0.5 미만인 경우 많음):
python scripts/text_segment.py --prompt person --confidence 0.3
# D) 자체 이미지
python scripts/text_segment.py --image data\my.jpg --prompt building --min-size 100
# E) 멀티 프롬프트 (Grok+Gemini 병합 세트, 모델 1회 로드)
python scripts/multi_prompt_segment.py --list-only --tier compact
python scripts/multi_prompt_segment.py --tier compact --confidence 0.3
python scripts/multi_prompt_segment.py --tier A_high
python scripts/multi_prompt_segment.py --tier all --confidence 0.25
# DJI 드론 정사 기본 이미지: data/DJI_20260306100802_0016.JPG
# 프롬프트 JSON: prompts/dji_20260306_0016.json
# F) 누락 보강 (Gemini gap) — 같은 output 폴더에 추가
python scripts\multi_prompt_segment.py `
--tier gap --confidence 0.25 `
--output-dir output\dji_0016_compact
# G) 멀티 결과 한 장으로 합치기 (overlay + grid + HTML)
python scripts\merge_multi_results.py `
--result-dir output\dji_0016_compact `
--image data\DJI_20260306100802_0016.JPG
# → output\dji_0016_compact\merged\index.html (디스크 마스크 전부 병합)
# H) 예시: DJI_0100 전체 파이프라인 (0044 프롬프트 → compact → gap → gap2_core → merge)
$img = "data\DJI_20260306101434_0100.JPG"
$out = "output\dji_0100_compact"
$json = "prompts\dji_20260306_0044.json"
python scripts\multi_prompt_segment.py --prompts-json $json --list-only --tier compact
python scripts\multi_prompt_segment.py `
--prompts-json $json --image $img --tier compact --confidence 0.3 --output-dir $out
# (선택) gap / gap2_core 보강 — 같은 $out 에 추가
python scripts\multi_prompt_segment.py `
--prompts-json $json --image $img --tier gap --confidence 0.25 --output-dir $out
python scripts\multi_prompt_segment.py `
--prompts-json $json --image $img --tier gap2_core --confidence 0.22 --output-dir $out
python scripts\merge_multi_results.py --result-dir $out --image $img --max-side 2048
start "$out\merged\index.html"
```
상세 주석 버전은 `prompts/README.md` §「DJI_0100 전체 파이프라인」을 보세요.
결과 파일은 `output/` 아래에 저장됩니다.
| 파일 | 내용 |
|------|------|
| `*_mask.png` | 인스턴스별 unique mask |
| `*_scores.npy` | 객체별 confidence 점수 |
| `*_ann.png` | 오버레이 시각화 |
**참고**: 모델 로드 시 `missing_keys` 경고가 날 수 있음 (`sam3` 패키지 vs `sam3.1` 체크포인트 일부 키 불일치). 추론은 동작하나 score가 낮을 수 있어 `--confidence` 튜닝이 필요합니다.
## SamGeo3 빠른 참고
```python
from samgeo import SamGeo3
sam = SamGeo3(
backend="meta", # sam3.1 은 meta 전용
model_id="facebook/sam3.1",
confidence_threshold=0.5,
resolution=1008,
)
sam.set_image("data/test_image.jpg")
sam.generate_masks("person")
sam.save_masks("output/mask.png")
```
- `backend="meta"`: `facebook/sam3`, `facebook/sam3.1`, 배치·인터랙티브 지원
- `backend="transformers"`: `facebook/sam3`
## 검증 순서
1. `check_install.py` — CUDA + imports
2. `check_install.py --load-model` — 가중치 로드
3. `text_segment.py` — 마스크 저장 확인
4. `confidence` / `min_size` 튜닝 후 실데이터 실험
## 참고
- 업스트림: https://github.com/opengeos/segment-geospatial
- API 문서: https://samgeo.gishub.org/samgeo3/
- 라이선스(업스트림): MIT
View File
+251
View File
@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DJI_0044 = 0016 확장 — 철로+도로</title>
<style>
:root {
--panel: #1a2332; --panel2: #243044; --text: #e7ecf3; --muted: #9aa8bc;
--accent: #3d9cf0; --accent2: #5eead4; --ok: #34d399; --warn: #fbbf24;
--border: #2d3a4f; --code-bg: #0b1220;
--font: "Segoe UI", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
--mono: "Cascadia Code", Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0; font-family: var(--font); color: var(--text); line-height: 1.65;
background: linear-gradient(160deg, #0b1020, #121a28 40%, #0f1419);
}
.wrap { max-width: 960px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
header.hero {
background: linear-gradient(135deg, #1e3a5f, #0f766e);
border-radius: 16px; padding: 2rem 1.75rem; margin-bottom: 1.5rem;
box-shadow: 0 8px 28px rgba(0,0,0,.35);
}
header.hero h1 { margin: 0 0 .4rem; font-size: 1.65rem; }
header.hero p { margin: 0; color: rgba(255,255,255,.92); }
.badges { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: 1rem; }
.badge {
font-size: .72rem; font-weight: 600; padding: .22rem .55rem; border-radius: 999px;
background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.2);
}
section, nav.toc {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 1.2rem 1.35rem; margin-bottom: 1.15rem;
}
section h2 {
margin: 0 0 .7rem; font-size: 1.15rem; color: #fff;
border-bottom: 1px solid var(--border); padding-bottom: .45rem;
}
nav.toc h2 { margin: 0 0 .5rem; font-size: .88rem; color: var(--accent2); text-transform: uppercase; }
nav.toc ol { margin: 0; padding-left: 1.2rem; }
nav.toc a { color: var(--text); text-decoration: none; }
nav.toc a:hover { color: var(--accent); }
h3 { margin: 1rem 0 .4rem; color: var(--accent2); font-size: 1rem; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; margin: .6rem 0 1rem; }
th, td { border: 1px solid var(--border); padding: .48rem .6rem; text-align: left; vertical-align: top; }
th { background: var(--panel2); color: var(--accent2); }
code { font-family: var(--mono); font-size: .84em; background: var(--code-bg);
padding: .1em .35em; border-radius: 4px; border: 1px solid var(--border); }
pre {
background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px;
padding: .95rem 1rem; overflow-x: auto; font-family: var(--mono); font-size: .8rem;
color: #d1e0f5; line-height: 1.55;
}
.cmd { color: #7dd3fc; } .c { color: #6b8a9e; }
.callout {
border-left: 4px solid var(--ok); background: rgba(52,211,153,.1);
padding: .8rem 1rem; border-radius: 8px; margin: .8rem 0;
}
.callout.warn { border-left-color: var(--warn); background: rgba(251,191,36,.1); }
.path { color: var(--accent2); font-family: var(--mono); font-size: .88em; }
.flow {
display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: .8rem 0;
}
.flow span {
background: var(--panel2); border: 1px solid var(--border); padding: .4rem .7rem;
border-radius: 8px; font-size: .88rem;
}
.flow .arr { background: transparent; border: 0; color: var(--accent2); font-weight: 700; }
footer { text-align: center; color: var(--muted); font-size: .84rem; margin-top: 1.5rem; }
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<h1>DJI_0044 = 이미지 1번(0016) 확장</h1>
<p>
별도 체계가 아닙니다. <strong>0016 프롬프트·tier·gap/gap2 워크플로를 상속</strong>하고,
이 장면(철로+도로+열차+야적)에만 필요한 객체를 <code>scene_delta</code> 로 덧붙입니다.
</p>
<div class="badges">
<span class="badge">extends 0016</span>
<span class="badge">same pipeline</span>
<span class="badge">scene_delta only</span>
<span class="badge">SAM 3.1 meta</span>
</div>
</header>
<nav class="toc">
<h2>목차</h2>
<ol>
<li><a href="#model">확장 모델</a></li>
<li><a href="#delta">scene_delta (이 장면 추가분)</a></li>
<li><a href="#workflow">실행 순서 (0016과 동일)</a></li>
<li><a href="#run">커맨드</a></li>
<li><a href="#files">파일</a></li>
</ol>
</nav>
<section id="model">
<h2>1. 확장 모델</h2>
<div class="flow">
<span>① DJI_0016 base<br/><small>compact / gap / gap2</small></span>
<span class="arr"></span>
<span>② scene_delta<br/><small>열차·도로·야적</small></span>
<span class="arr"></span>
<span>③ DJI_0044 compact<br/><small>base + delta</small></span>
<span class="arr"></span>
<span>④ merge</span>
</div>
<table>
<tr><th>개념</th><th>설명</th></tr>
<tr>
<td>베이스</td>
<td><code>prompts/dji_20260306_0016.json</code> — 철로·산업 orthophoto에서 검증한 전체 세트</td>
</tr>
<tr>
<td>확장 파일</td>
<td><code>prompts/dji_20260306_0044.json</code><code>"extends": "…0016.json"</code></td>
</tr>
<tr>
<td>상속</td>
<td>A~E tier, compact, gap, improved, gap2, gap2_core 전부 사용 가능</td>
</tr>
<tr>
<td>추가</td>
<td><code>scene_delta.prompts</code> + 필요 시 tier별 덧붙임</td>
</tr>
<tr>
<td>스크립트</td>
<td><code>multi_prompt_segment.py</code> 가 extends를 자동 병합</td>
</tr>
</table>
<div class="callout">
<strong>원칙</strong>
새 이미지가 들어와도 “새 체계”를 만들지 않고,
<strong>1번에서 쌓은 프롬프트 자산 + 장면 delta</strong> 로만 확장합니다.
</div>
</section>
<section id="delta">
<h2>2. scene_delta (0044에서 추가되는 객체)</h2>
<p class="muted">Gemini 분석 + 비전 확인. 0016에 없거나 이 장면에서 핵심인 항목.</p>
<table>
<thead><tr><th>그룹</th><th>프롬프트</th></tr></thead>
<tbody>
<tr>
<td>열차</td>
<td><code>train</code>, <code>freight car</code>, <code>container car</code>, locomotive, passenger train…</td>
</tr>
<tr>
<td>도로</td>
<td><code>highway</code>, <code>pedestrian crossing</code>, crosswalk, road marking</td>
</tr>
<tr>
<td>야적</td>
<td><code>pipe pile</code>, pipes, bag pile, wooden pile, wood stack, tarp, gravel…</td>
</tr>
<tr>
<td>기타</td>
<td>chain-link fence, sleeper, trackbed, catenary, red roof…</td>
</tr>
</tbody>
</table>
<p>
베이스에 이미 있는 것(<code>railway track</code>, <code>building</code>, <code>road</code>,
<code>car</code>, <code>blue tarp</code>, <code>material pile</code> 등)은
<strong>다시 정의하지 않고 상속</strong>합니다.
</p>
</section>
<section id="workflow">
<h2>3. 실행 순서 (0016과 동일 파이프라인)</h2>
<ol>
<li><code>--tier compact</code> — 0016 compact + scene_delta</li>
<li><code>--tier gap</code> — 0016 누락 보강 세트 (상속)</li>
<li><code>--tier gap2</code> 또는 <code>gap2_core</code> — 합본 빈 영역 보강 (상속)</li>
<li><code>--tier delta</code> — 0044 추가분만 (빠른 재실험)</li>
<li><code>merge_multi_results.py</code></li>
</ol>
<div class="callout warn">
<strong>tier 의미</strong>
<code>gap</code> / <code>gap2</code> 는 0044 전용이 아니라
<strong>0016에서 만든 보강 세트 그대로</strong>입니다. 같은 철로 orthophoto 계열에 재사용.
</div>
</section>
<section id="run">
<h2>4. 커맨드</h2>
<pre><span class="cmd">cd D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
<span class="cmd">.\.venv\Scripts\Activate.ps1</span>
<span class="c"># 상속 결과 확인 (compact = 0016 + delta)</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json prompts\dji_20260306_0044.json `
--list-only --tier compact</span>
<span class="c"># 이 장면 추가분만</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json prompts\dji_20260306_0044.json `
--list-only --tier delta</span>
<span class="c"># 본 실행 (1번과 동일 패턴)</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json prompts\dji_20260306_0044.json `
--image data\DJI_20260306100928_0044.JPG `
--tier compact --confidence 0.3 `
--output-dir output\dji_0044_compact</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json prompts\dji_20260306_0044.json `
--tier gap --confidence 0.25 `
--output-dir output\dji_0044_compact</span>
<span class="cmd">python scripts\merge_multi_results.py `
--result-dir output\dji_0044_compact `
--image data\DJI_20260306100928_0044.JPG</span></pre>
</section>
<section id="files">
<h2>5. 파일</h2>
<table>
<tr><th>경로</th><th>역할</th></tr>
<tr>
<td><span class="path">prompts/dji_20260306_0016.json</span></td>
<td><strong>베이스 (이미지 1)</strong></td>
</tr>
<tr>
<td><span class="path">prompts/dji_20260306_0044.json</span></td>
<td>extends 0016 + scene_delta</td>
</tr>
<tr>
<td><span class="path">docs/dji_gap_fill.html</span></td>
<td>0016 gap 리포트</td>
</tr>
<tr>
<td><span class="path">output/dji_0044_compact/merged/</span></td>
<td>0044 합본</td>
</tr>
</table>
<p><a href="dji_gap_fill.html">← 0016 갭 필 리포트</a> · <a href="usage.html">일반 사용법</a></p>
</section>
<footer>
samgeo3-lab · 0044 extends 0016 · 별도 체계 아님<br />
<span class="path">docs/dji_0044_prompts.html</span>
</footer>
</div>
</body>
</html>
+534
View File
@@ -0,0 +1,534 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DJI 정사 멀티 프롬프트 · 갭 필 리포트 — samgeo3-lab</title>
<style>
:root {
--bg: #0f1419;
--panel: #1a2332;
--panel2: #243044;
--text: #e7ecf3;
--muted: #9aa8bc;
--accent: #3d9cf0;
--accent2: #5eead4;
--warn: #fbbf24;
--ok: #34d399;
--bad: #f87171;
--border: #2d3a4f;
--code-bg: #0b1220;
--radius: 10px;
--shadow: 0 8px 28px rgba(0,0,0,.35);
--font: "Segoe UI", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
--mono: "Cascadia Code", "Consolas", "Courier New", monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font);
background: linear-gradient(160deg, #0b1020 0%, #121a28 40%, #0f1419 100%);
color: var(--text);
line-height: 1.65;
min-height: 100vh;
}
.wrap { max-width: 960px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
header.hero {
background: linear-gradient(135deg, #1e3a5f 0%, #0f766e 55%, #134e4a 100%);
border-radius: 16px;
padding: 2rem 1.75rem;
box-shadow: var(--shadow);
margin-bottom: 1.75rem;
}
header.hero h1 { margin: 0 0 .4rem; font-size: 1.75rem; letter-spacing: -.02em; }
header.hero p { margin: 0; color: rgba(255,255,255,.92); max-width: 42rem; }
.badges { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.badge {
font-size: .75rem; font-weight: 600; padding: .25rem .65rem;
border-radius: 999px; background: rgba(255,255,255,.14);
border: 1px solid rgba(255,255,255,.22);
}
nav.toc {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1rem 1.25rem; margin-bottom: 1.75rem;
}
nav.toc h2 {
margin: 0 0 .6rem; font-size: .9rem; color: var(--accent2);
text-transform: uppercase; letter-spacing: .06em;
}
nav.toc ol { margin: 0; padding-left: 1.25rem; }
nav.toc a { color: var(--text); text-decoration: none; }
nav.toc a:hover { color: var(--accent); text-decoration: underline; }
section {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1.35rem 1.4rem 1.5rem;
margin-bottom: 1.25rem; box-shadow: 0 2px 12px rgba(0,0,0,.2);
}
section h2 {
margin: 0 0 .85rem; font-size: 1.22rem; color: #fff;
border-bottom: 1px solid var(--border); padding-bottom: .55rem;
}
section h3 { margin: 1.2rem 0 .5rem; font-size: 1.02rem; color: var(--accent2); }
p { margin: .55rem 0; }
.muted { color: var(--muted); }
ul, ol { margin: .4rem 0 .8rem; padding-left: 1.3rem; }
li { margin: .28rem 0; }
a { color: var(--accent); }
table {
width: 100%; border-collapse: collapse; font-size: .9rem; margin: .75rem 0 1rem;
}
th, td {
border: 1px solid var(--border); padding: .5rem .65rem;
text-align: left; vertical-align: top;
}
th { background: var(--panel2); color: var(--accent2); font-weight: 600; }
tr:nth-child(even) td { background: rgba(255,255,255,.02); }
code, kbd {
font-family: var(--mono); font-size: .85em;
background: var(--code-bg); padding: .1em .38em;
border-radius: 4px; border: 1px solid var(--border);
}
pre {
background: var(--code-bg); border: 1px solid var(--border);
border-radius: 8px; padding: 1rem 1.1rem; overflow-x: auto;
font-family: var(--mono); font-size: .8rem; line-height: 1.55;
margin: .7rem 0 1rem; color: #d1e0f5;
}
pre .c { color: #6b8a9e; }
pre .cmd { color: #7dd3fc; }
.callout {
border-radius: 8px; padding: .85rem 1rem; margin: .9rem 0;
border-left: 4px solid var(--accent); background: rgba(61,156,240,.1);
}
.callout.warn { border-left-color: var(--warn); background: rgba(251,191,36,.1); }
.callout.ok { border-left-color: var(--ok); background: rgba(52,211,153,.1); }
.callout.bad { border-left-color: var(--bad); background: rgba(248,113,113,.1); }
.callout strong { display: block; margin-bottom: .25rem; }
.tag-ok { color: var(--ok); font-weight: 600; }
.tag-fail { color: var(--bad); font-weight: 600; }
.path { color: var(--accent2); font-family: var(--mono); font-size: .88em; }
.grid-2 {
display: grid; grid-template-columns: 1fr 1fr; gap: .75rem;
}
@media (max-width: 720px) { .grid-2 { grid-template-columns: 1fr; } }
.stat {
background: var(--panel2); border: 1px solid var(--border);
border-radius: 8px; padding: .85rem 1rem;
}
.stat .n { font-size: 1.6rem; font-weight: 700; color: var(--accent2); }
.stat .l { font-size: .82rem; color: var(--muted); }
.chips { display: flex; flex-wrap: wrap; gap: .35rem; margin: .5rem 0; }
.chip {
font-size: .78rem; padding: .2rem .5rem; border-radius: 6px;
background: #0b1220; border: 1px solid var(--border); font-family: var(--mono);
}
.chip.ok { border-color: rgba(52,211,153,.45); color: #a7f3d0; }
.chip.fail { border-color: rgba(248,113,113,.4); color: #fecaca; }
footer {
text-align: center; color: var(--muted); font-size: .85rem; margin-top: 2rem;
}
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<h1>DJI 정사 · 멀티 프롬프트 갭 필 리포트</h1>
<p>
SamGeo3 / SAM 3.1 텍스트 세그 실험 —
compact → gap → gap2 단계로 빈 영역을 메운 과정과 결과 요약.
</p>
<div class="badges">
<span class="badge">facebook/sam3.1</span>
<span class="badge">backend=meta</span>
<span class="badge">DJI_20260306100802_0016</span>
<span class="badge">56 classes merged</span>
<span class="badge">samgeo3-lab</span>
</div>
</header>
<nav class="toc" aria-label="목차">
<h2>목차</h2>
<ol>
<li><a href="#overview">개요 · 파이프라인</a></li>
<li><a href="#image">대상 이미지</a></li>
<li><a href="#empty">합본에서 보인 빈 영역</a></li>
<li><a href="#tiers">프롬프트 tier</a></li>
<li><a href="#gap">gap (1차 누락 보강)</a></li>
<li><a href="#gap2">gap2 (합본 재검토 보강)</a></li>
<li><a href="#results">실측 결과</a></li>
<li><a href="#run">실행 커맨드</a></li>
<li><a href="#outputs">결과 파일</a></li>
<li><a href="#limits">한계 · 다음 단계</a></li>
</ol>
</nav>
<section id="overview">
<h2>1. 개요 · 파이프라인</h2>
<div class="grid-2">
<div class="stat"><div class="n">compact</div><div class="l">1차 19 프롬프트 → ~18 마스크</div></div>
<div class="stat"><div class="n">gap</div><div class="l">Gemini 누락 보강 → road, forest…</div></div>
<div class="stat"><div class="n">gap2</div><div class="l">합본 빈 영역 44 프롬프트 → 28 성공</div></div>
<div class="stat"><div class="n">56</div><div class="l">최종 merge 클래스 수</div></div>
</div>
<pre><span class="c"># 전체 흐름</span>
<span class="cmd">multi_prompt_segment.py --tier compact</span> <span class="c"># 기본 객체</span>
<span class="cmd">multi_prompt_segment.py --tier gap</span> <span class="c"># 1차 누락</span>
<span class="cmd">multi_prompt_segment.py --tier gap2</span> <span class="c"># 합본 빈 영역</span>
<span class="cmd">merge_multi_results.py</span> <span class="c"># overlay + legend + HTML</span></pre>
<p class="muted">
설정: <span class="path">prompts/dji_20260306_0016.json</span> ·
사용법 일반: <a href="usage.html">docs/usage.html</a>
</p>
</section>
<section id="image">
<h2>2. 대상 이미지</h2>
<table>
<tr><th>항목</th><th></th></tr>
<tr>
<td>파일</td>
<td><code>data/DJI_20260306100802_0016.JPG</code></td>
</tr>
<tr>
<td>해상도</td>
<td>8192 × 5460 (드론 정사에 가까운 수직뷰)</td>
</tr>
<tr>
<td>장면</td>
<td>복선 철로 · 창고/주차장 · 태양광 지붕 · 수목 · 야적장</td>
</tr>
<tr>
<td>모델</td>
<td><code>facebook/sam3.1</code> · <code>backend=meta</code></td>
</tr>
<tr>
<td>confidence</td>
<td>compact 0.3 · gap 0.25 · gap2 0.22</td>
</tr>
</table>
</section>
<section id="empty">
<h2>3. 합본에서 보인 빈 영역</h2>
<p>
1차 merge(<code>combined_with_legend.png</code>) 기준으로
마스크 색이 없고 원본 톤이 남는 구간을 재검토했습니다.
</p>
<table>
<thead>
<tr><th>구역</th><th>추정 내용</th><th>1차에서 부족한 이유</th></tr>
</thead>
<tbody>
<tr>
<td>선로 상·하 갈색 띠</td>
<td>성토·제방·마른 초지</td>
<td><code>railway track</code> 밴드만, 바깥 지면 미채움</td>
</tr>
<tr>
<td>검은 타프 주변</td>
<td>흙더미·절토</td>
<td><code>black tarp</code>만, 주변 토사 약함</td>
</tr>
<tr>
<td>시설 사이 공터</td>
<td>콘크리트·야적 바닥</td>
<td><code>parking lot</code> / <code>road</code>와 다른 표면</td>
</tr>
<tr>
<td>하측 야적</td>
<td>녹생 덮개·시트</td>
<td>색 지붕 프롬프트와 다른 tarp/cover</td>
</tr>
<tr>
<td>소형 구조</td>
<td>shed · 부스 · 차양</td>
<td><code>building</code>에 안 묶임</td>
</tr>
<tr>
<td>지붕 잔여</td>
<td>금속 / 골판 / 회색</td>
<td>white·green·blue roof만으로는 부족</td>
</tr>
<tr>
<td>선형 세부</td>
<td>도상 · 전선</td>
<td>정사 + 텍스트 그라운딩 한계</td>
</tr>
</tbody>
</table>
</section>
<section id="tiers">
<h2>4. 프롬프트 tier</h2>
<table>
<thead>
<tr><th>tier</th><th>설명</th><th>권장 conf</th></tr>
</thead>
<tbody>
<tr>
<td><code>compact</code></td>
<td>1차 실험 19개 (철로·건물·차량·타프…)</td>
<td>0.30</td>
</tr>
<tr>
<td><code>gap</code></td>
<td>Gemini 누락 보강 (road, dense forest, covered truck…)</td>
<td>0.25</td>
</tr>
<tr>
<td><code>gap2</code></td>
<td>합본 빈 영역 전체 44개</td>
<td>0.22</td>
</tr>
<tr>
<td><code>gap2_core</code></td>
<td>gap2 중 실측 성공만 (~18개, 재실행용)</td>
<td>0.22</td>
</tr>
<tr>
<td><code>improved</code></td>
<td>compact + gap 병합</td>
<td>0.28</td>
</tr>
<tr>
<td><code>all</code></td>
<td>A~E 전체 (중복 제거)</td>
<td>0.25~0.30</td>
</tr>
</tbody>
</table>
</section>
<section id="gap">
<h2>5. gap (1차 누락 보강)</h2>
<p class="muted">Gemini 재검토 + 실측 성공 동의어</p>
<div class="chips">
<span class="chip ok">road</span>
<span class="chip ok">asphalt pavement</span>
<span class="chip ok">gravel path</span>
<span class="chip ok">dense forest</span>
<span class="chip ok">covered truck</span>
<span class="chip ok">cargo vehicle</span>
<span class="chip ok">debris</span>
<span class="chip ok">rubbish</span>
<span class="chip ok">light pole</span>
<span class="chip ok">scrap pile</span>
<span class="chip fail">street light</span>
<span class="chip fail">lamp post</span>
<span class="chip fail">drainage ditch</span>
<span class="chip fail">industrial waste</span>
</div>
<div class="callout">
<strong></strong>
가로등: <code>light pole</code> @ 0.2 가 유효.
배수로·전선은 정사에서 거의 실패.
<code>rubbish</code> / <code>scrap pile</code> 은 노이즈 과다 가능.
</div>
</section>
<section id="gap2">
<h2>6. gap2 (합본 재검토 보강)</h2>
<h3>타겟 갭 (JSON notes)</h3>
<ul>
<li>선로 상·하 갈색 성토/제방·마른 초지</li>
<li>검은 타프 주변 흙더미·절토면</li>
<li>주차장·창고 사이 콘크리트·공터</li>
<li>하측 야적 녹생 방수시트/덮개</li>
<li>소형 shed · 담장 · 차양</li>
<li>금속/골판/회색 지붕 잔여</li>
<li>건물 벽면 · 장벽</li>
</ul>
<h3>성공 프롬프트 <span class="tag-ok">28/44</span> (conf=0.22)</h3>
<div class="chips">
<span class="chip ok">dirt embankment</span>
<span class="chip ok">dirt pile</span>
<span class="chip ok">dry grass</span>
<span class="chip ok">bushes</span>
<span class="chip ok">grass</span>
<span class="chip ok">vegetation</span>
<span class="chip ok">brown field</span>
<span class="chip ok">open ground</span>
<span class="chip ok">concrete</span>
<span class="chip ok">concrete pad</span>
<span class="chip ok">concrete pavement</span>
<span class="chip ok">industrial yard</span>
<span class="chip ok">paved yard</span>
<span class="chip ok">green tarp</span>
<span class="chip ok">green plastic sheet</span>
<span class="chip ok">plastic cover</span>
<span class="chip ok">shed</span>
<span class="chip ok">small shed</span>
<span class="chip ok">metal roof</span>
<span class="chip ok">corrugated roof</span>
<span class="chip ok">gray roof</span>
<span class="chip ok">wall</span>
<span class="chip ok">building wall</span>
<span class="chip ok">fence wall</span>
<span class="chip ok">barrier</span>
<span class="chip ok">canopy</span>
<span class="chip ok">awning</span>
<span class="chip ok">sidewalk</span>
</div>
<h3>실패 <span class="tag-fail">16</span></h3>
<div class="chips">
<span class="chip fail">embankment</span>
<span class="chip fail">railway embankment</span>
<span class="chip fail">soil pile</span>
<span class="chip fail">earth mound</span>
<span class="chip fail">scrub</span>
<span class="chip fail">dormant vegetation</span>
<span class="chip fail">booth</span>
<span class="chip fail">kiosk</span>
<span class="chip fail">retaining wall</span>
<span class="chip fail">ballast</span>
<span class="chip fail">track bed</span>
<span class="chip fail">railway ballast</span>
<span class="chip fail">power line</span>
<span class="chip fail">overhead wire</span>
<span class="chip fail">shadow</span>
<span class="chip fail">walkway</span>
</div>
<h3>gap2_core (재실행 권장)</h3>
<pre>"dirt embankment", "dirt pile", "dry grass", "bushes", "vegetation",
"brown field", "open ground", "concrete", "industrial yard",
"green tarp", "plastic cover", "shed", "metal roof", "corrugated roof",
"gray roof", "building wall", "barrier", "awning"</pre>
</section>
<section id="results">
<h2>7. 실측 결과 요약</h2>
<table>
<thead>
<tr><th>단계</th><th>merge 클래스</th><th>비고</th></tr>
</thead>
<tbody>
<tr>
<td>compact + gap</td>
<td>~28</td>
<td>road / dense forest / covered truck 등 추가</td>
</tr>
<tr>
<td>+ gap2</td>
<td><strong>56</strong></td>
<td>성토·초지·콘크리트·shed·금속지붕 등</td>
</tr>
</tbody>
</table>
<div class="callout warn">
<strong>과대 마스크 주의</strong>
<code>industrial yard</code>, <code>open ground</code>, <code>dirt embankment</code>
픽셀 면적이 커서 다른 클래스를 덮을 수 있습니다.
필요 시 해당 <code>*_mask.png</code> 를 제외하고 다시 merge 하세요.
</div>
<div class="callout ok">
<strong>빈 영역 개선에 특히 유효했던 프롬프트</strong>
dirt embankment · dry grass · vegetation · concrete · green tarp ·
shed · metal/corrugated roof · building wall · awning
</div>
</section>
<section id="run">
<h2>8. 실행 커맨드</h2>
<pre><span class="cmd">cd D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
<span class="cmd">.\.venv\Scripts\Activate.ps1</span>
<span class="c"># 1차</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--image "data\DJI_20260306100802_0016.JPG" `
--tier compact --confidence 0.3 `
--output-dir "output\dji_0016_compact"</span>
<span class="c"># Gemini 누락 보강 (같은 폴더에 마스크 추가)</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--tier gap --confidence 0.25 `
--output-dir "output\dji_0016_compact"</span>
<span class="c"># 합본 빈 영역 보강</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--tier gap2 --confidence 0.22 `
--output-dir "output\dji_0016_compact"</span>
<span class="c"># 또는 성공 프롬프트만</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--tier gap2_core --confidence 0.22 `
--output-dir "output\dji_0016_compact"</span>
<span class="c"># 한 장으로 합치기</span>
<span class="cmd">python scripts\merge_multi_results.py `
--result-dir "output\dji_0016_compact" `
--image "data\DJI_20260306100802_0016.JPG" `
--max-side 2560</span></pre>
</section>
<section id="outputs">
<h2>9. 결과 파일</h2>
<table>
<thead>
<tr><th>경로</th><th>내용</th></tr>
</thead>
<tbody>
<tr>
<td><span class="path">output/dji_0016_compact/*_mask.png</span></td>
<td>프롬프트별 인스턴스 마스크</td>
</tr>
<tr>
<td><span class="path">output/dji_0016_compact/*_ann.png</span></td>
<td>개별 오버레이 (대용량 가능)</td>
</tr>
<tr>
<td><span class="path">output/dji_0016_compact/merged/combined_with_legend.png</span></td>
<td>전 클래스 합본 + 범례</td>
</tr>
<tr>
<td><span class="path">output/dji_0016_compact/merged/per_class_grid.png</span></td>
<td>클래스별 썸네일 격자</td>
</tr>
<tr>
<td><span class="path">output/dji_0016_compact/merged/index.html</span></td>
<td>브라우저 뷰 (결과 이미지)</td>
</tr>
<tr>
<td><span class="path">output/dji_0016_compact/merged/merge_summary.json</span></td>
<td>클래스·픽셀 통계</td>
</tr>
<tr>
<td><span class="path">prompts/dji_20260306_0016.json</span></td>
<td>tier · gap · gap2 · gap2_core 정의</td>
</tr>
</tbody>
</table>
<p>
결과 미리보기 HTML:
<a href="../output/dji_0016_compact/merged/index.html">merged/index.html</a>
(상대 경로; 로컬에서 파일 열기)
</p>
</section>
<section id="limits">
<h2>10. 한계 · 다음 단계</h2>
<ul>
<li>SAM 3.1 텍스트 세그는 <strong>개념 단위 인스턴스</strong>이지 픽셀 토지피복 분류기가 아님.</li>
<li>도상·전선·그림자 등 가늘거나 추상적인 개념은 텍스트만으로 약함.</li>
<li>클래스 간 겹침이 큼 → 우선순위 합성 / 배타 마스크 후처리 권장.</li>
<li>작은 시설물은 점·박스 인터랙티브(<code>enable_inst_interactivity</code>)가 유리.</li>
<li>초대형 정사는 <code>generate_masks_tiled</code> 로 VRAM·경계 품질 개선 가능.</li>
</ul>
<div class="callout">
<strong>권장 운영</strong>
compact → gap → gap2_core → merge → 과대 클래스 수동 제외 → 재 merge.
confidence는 0.22~0.35 구간에서 장면별로 조정.
</div>
</section>
<footer>
samgeo3-lab · DJI gap-fill report · 2026-07-15<br />
파일: <span class="path">docs/dji_gap_fill.html</span>
· 일반 사용법: <a href="usage.html">usage.html</a>
</footer>
</div>
</body>
</html>
+763
View File
@@ -0,0 +1,763 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>samgeo3-lab 사용법 — SamGeo3 / SAM 3.1</title>
<style>
:root {
--bg: #0f1419;
--panel: #1a2332;
--panel2: #243044;
--text: #e7ecf3;
--muted: #9aa8bc;
--accent: #3d9cf0;
--accent2: #5eead4;
--warn: #fbbf24;
--ok: #34d399;
--border: #2d3a4f;
--code-bg: #0b1220;
--radius: 10px;
--shadow: 0 8px 28px rgba(0,0,0,.35);
--font: "Segoe UI", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
--mono: "Cascadia Code", "Consolas", "Courier New", monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font);
background: linear-gradient(160deg, #0b1020 0%, #121a28 40%, #0f1419 100%);
color: var(--text);
line-height: 1.65;
min-height: 100vh;
}
.wrap {
max-width: 920px;
margin: 0 auto;
padding: 2rem 1.25rem 4rem;
}
header.hero {
background: linear-gradient(135deg, #1e3a5f 0%, #0f766e 100%);
border-radius: 16px;
padding: 2rem 1.75rem;
box-shadow: var(--shadow);
margin-bottom: 1.75rem;
}
header.hero h1 {
margin: 0 0 .4rem;
font-size: 1.85rem;
letter-spacing: -.02em;
}
header.hero p {
margin: 0;
color: rgba(255,255,255,.9);
max-width: 40rem;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: .5rem;
margin-top: 1rem;
}
.badge {
display: inline-block;
font-size: .75rem;
font-weight: 600;
padding: .25rem .6rem;
border-radius: 999px;
background: rgba(255,255,255,.15);
border: 1px solid rgba(255,255,255,.2);
}
nav.toc {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
margin-bottom: 1.75rem;
}
nav.toc h2 {
margin: 0 0 .6rem;
font-size: .95rem;
color: var(--accent2);
text-transform: uppercase;
letter-spacing: .06em;
}
nav.toc ol {
margin: 0;
padding-left: 1.25rem;
}
nav.toc a {
color: var(--text);
text-decoration: none;
}
nav.toc a:hover { color: var(--accent); text-decoration: underline; }
section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.35rem 1.4rem 1.5rem;
margin-bottom: 1.25rem;
box-shadow: 0 2px 12px rgba(0,0,0,.2);
}
section h2 {
margin: 0 0 .85rem;
font-size: 1.25rem;
border-bottom: 1px solid var(--border);
padding-bottom: .55rem;
color: #fff;
}
section h3 {
margin: 1.25rem 0 .5rem;
font-size: 1.05rem;
color: var(--accent2);
}
p { margin: .55rem 0; color: var(--text); }
.muted { color: var(--muted); }
ul, ol { margin: .4rem 0 .8rem; padding-left: 1.3rem; }
li { margin: .3rem 0; }
a { color: var(--accent); }
table {
width: 100%;
border-collapse: collapse;
font-size: .92rem;
margin: .75rem 0 1rem;
}
th, td {
border: 1px solid var(--border);
padding: .55rem .7rem;
text-align: left;
vertical-align: top;
}
th {
background: var(--panel2);
color: var(--accent2);
font-weight: 600;
}
tr:nth-child(even) td { background: rgba(255,255,255,.02); }
code, kbd {
font-family: var(--mono);
font-size: .86em;
background: var(--code-bg);
padding: .12em .4em;
border-radius: 4px;
border: 1px solid var(--border);
}
pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.1rem;
overflow-x: auto;
font-family: var(--mono);
font-size: .82rem;
line-height: 1.55;
margin: .7rem 0 1rem;
color: #d1e0f5;
}
pre .c { color: #6b8a9e; } /* comment */
pre .cmd { color: #7dd3fc; }
.callout {
border-radius: 8px;
padding: .85rem 1rem;
margin: .9rem 0;
border-left: 4px solid var(--accent);
background: rgba(61,156,240,.1);
}
.callout.warn {
border-left-color: var(--warn);
background: rgba(251,191,36,.1);
}
.callout.ok {
border-left-color: var(--ok);
background: rgba(52,211,153,.1);
}
.callout strong { display: block; margin-bottom: .25rem; }
.steps {
counter-reset: step;
list-style: none;
padding: 0;
}
.steps > li {
counter-increment: step;
position: relative;
padding: .75rem 0 .75rem 3rem;
border-bottom: 1px solid var(--border);
}
.steps > li:last-child { border-bottom: none; }
.steps > li::before {
content: counter(step);
position: absolute;
left: 0;
top: .85rem;
width: 1.85rem;
height: 1.85rem;
border-radius: 50%;
background: var(--accent);
color: #041018;
font-weight: 700;
font-size: .85rem;
display: flex;
align-items: center;
justify-content: center;
}
footer {
text-align: center;
color: var(--muted);
font-size: .85rem;
margin-top: 2rem;
}
.path { color: var(--accent2); font-family: var(--mono); font-size: .9em; }
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<h1>samgeo3-lab 사용법</h1>
<p>
segment-geospatial의 <strong>SamGeo3</strong> 백엔드로
SAM 3 / SAM 3.1 텍스트 프롬프트 세그멘테이션을 실험하는
<strong>독립 프로젝트</strong> 가이드입니다.
</p>
<div class="badges">
<span class="badge">Python 3.12</span>
<span class="badge">CUDA / RTX GPU</span>
<span class="badge">backend=meta</span>
<span class="badge">facebook/sam3.1</span>
<span class="badge">Windows</span>
</div>
</header>
<nav class="toc" aria-label="목차">
<h2>목차</h2>
<ol>
<li><a href="#overview">개요 · 범위</a></li>
<li><a href="#structure">폴더 구조</a></li>
<li><a href="#install">설치</a></li>
<li><a href="#hf">Hugging Face · 체크포인트</a></li>
<li><a href="#run">실행 커맨드</a></li>
<li><a href="#cli">text_segment.py 옵션</a></li>
<li><a href="#multi">멀티 프롬프트 (Grok+Gemini)</a></li>
<li><a href="#example-0100">예시: DJI_0100 전체 파이프라인</a></li>
<li><a href="#output">출력 파일</a></li>
<li><a href="#api">Python API 빠른 참고</a></li>
<li><a href="#tips">튜닝 · 트러블슈팅</a></li>
<li><a href="#links">참고 링크</a></li>
<li><a href="dji_gap_fill.html">DJI_0016 갭 필 리포트</a></li>
<li><a href="dji_0044_prompts.html">DJI_0044 철로+도로 프롬프트</a></li>
</ol>
</nav>
<section id="overview">
<h2>1. 개요 · 범위</h2>
<p>
프로젝트 경로: <span class="path">D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
</p>
<table>
<thead>
<tr><th>항목</th><th>내용</th></tr>
</thead>
<tbody>
<tr>
<td>패키지</td>
<td><code>segment-geospatial</code> (PyPI) — 클래스 <code>SamGeo3</code></td>
</tr>
<tr>
<td>하는 일</td>
<td>설치 검증, 단일 이미지 텍스트 세그멘테이션, 마스크/시각화 저장</td>
</tr>
<tr>
<td>하지 않는 일</td>
<td>railway-client / sam31server 통합, 철도 후처리, REST API, AnyLabeling/YOLO 파이프라인</td>
</tr>
<tr>
<td>환경 원칙</td>
<td>이 폴더의 <code>.venv</code>만 사용. 다른 SAM 서버 env와 <strong>섞지 않음</strong></td>
</tr>
</tbody>
</table>
<div class="callout ok">
<strong>이미 구성된 경우</strong>
설치 단계는 건너뛰고 <a href="#run">§5 실행 커맨드</a>부터 진행하면 됩니다.
</div>
</section>
<section id="structure">
<h2>2. 폴더 구조</h2>
<pre>samgeo3-lab/
README.md
docs/usage.html ← 이 문서
requirements.txt
pyproject.toml
scripts/
check_install.py # GPU / import / (옵션) 모델 로드 확인
text_segment.py # 단일 이미지 텍스트 세그멘테이션
data/ # 입력 이미지
output/ # 마스크 · 점수 · 오버레이 결과
.venv/ # 독립 가상환경</pre>
</section>
<section id="install">
<h2>3. 설치 (Windows + GPU)</h2>
<p class="muted">PowerShell에서 프로젝트 루트로 이동한 뒤 순서대로 실행합니다.</p>
<pre><span class="cmd">cd D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
<span class="c"># 1) 독립 venv (Python 3.12)</span>
<span class="cmd">uv venv --python 3.12 .venv</span>
<span class="cmd">.\.venv\Scripts\Activate.ps1</span>
<span class="c"># 2) CUDA PyTorch 먼저 (CPU wheel 덮어쓰기 방지)</span>
<span class="cmd">uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121</span>
<span class="c"># 3) SamGeo3 extras only (api / all 설치 금지)</span>
<span class="cmd">uv pip install "segment-geospatial[samgeo3]&gt;=1.4.1" matplotlib</span>
<span class="c"># 4) Windows: sam3 import에 필요</span>
<span class="cmd">uv pip install "triton-windows&gt;=3.3.0.post19"</span>
<span class="c"># 5) (선택) 노트북</span>
<span class="cmd">uv pip install ipykernel jupyterlab</span></pre>
<div class="callout warn">
<strong>주의</strong>
<code>triton-windows</code>가 없으면 <code>import sam3</code> 가 실패합니다.
<code>segment-geospatial[samgeo3]</code> 설치 후에도 반드시 확인하세요.
</div>
<h3>대안: pixi (공식 권장)</h3>
<p>
conda/pixi 환경이 가능하면 PyTorch·CUDA·SAM3 의존성 해결에 유리합니다.<br />
<a href="https://samgeo.gishub.org/installation/#install-with-pixi-recommended" target="_blank" rel="noopener">
samgeo.gishub.org — Install with pixi
</a>
</p>
</section>
<section id="hf">
<h2>4. Hugging Face · 체크포인트</h2>
<ol class="steps">
<li>
<strong>모델 접근 승인</strong><br />
<a href="https://huggingface.co/facebook/sam3.1" target="_blank" rel="noopener">facebook/sam3.1</a>
(및 필요 시 <a href="https://huggingface.co/facebook/sam3" target="_blank" rel="noopener">sam3</a>) 폼 제출 후 승인
</li>
<li>
<strong>인증</strong><br />
<code>hf auth login</code> 또는 기존 토큰 파일
<code>~/.cache/huggingface/token</code> 유지
</li>
<li>
<strong>(선택) 로컬 체크포인트</strong>
<pre><span class="cmd">$env:SAM3_CHECKPOINT_PATH = "C:\path\to\sam3.1_multiplex.pt"</span></pre>
스크립트에서는 <code>--checkpoint</code> 로도 지정 가능합니다.
</li>
</ol>
<div class="callout">
<strong>백엔드 제약</strong>
<ul>
<li><code>backend="meta"</code><code>facebook/sam3</code>, <code>facebook/sam3.1</code> (배치·인터랙티브 풍부)</li>
<li><code>backend="transformers"</code><code>facebook/sam3</code> 만. <strong>sam3.1은 meta 전용</strong></li>
</ul>
</div>
</section>
<section id="run">
<h2>5. 실행 커맨드</h2>
<p>매번 가상환경을 활성화한 뒤 실행합니다.</p>
<pre><span class="cmd">cd D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
<span class="cmd">.\.venv\Scripts\Activate.ps1</span>
<span class="c"># A) 패키지 / GPU / import 확인</span>
<span class="cmd">python scripts\check_install.py</span>
<span class="c"># B) 모델 로드까지 확인 (HF 캐시 또는 다운로드)</span>
<span class="cmd">python scripts\check_install.py --load-model</span>
<span class="c"># C) 샘플 이미지 텍스트 세그멘테이션 (기본 prompt=person, confidence=0.3)</span>
<span class="cmd">python scripts\text_segment.py</span>
<span class="cmd">python scripts\text_segment.py --prompt person --confidence 0.3</span>
<span class="c"># D) 자체 이미지</span>
<span class="cmd">python scripts\text_segment.py --image data\my.jpg --prompt building --min-size 100</span></pre>
<h3>권장 검증 순서</h3>
<ol>
<li><code>check_install.py</code> — CUDA + imports</li>
<li><code>check_install.py --load-model</code> — 가중치 로드</li>
<li><code>text_segment.py</code> — 마스크 저장 확인</li>
<li><code>--confidence</code> / <code>--min-size</code> 튜닝 후 실데이터 실험</li>
</ol>
</section>
<section id="cli">
<h2>6. text_segment.py 옵션</h2>
<table>
<thead>
<tr><th>옵션</th><th>기본값</th><th>설명</th></tr>
</thead>
<tbody>
<tr>
<td><code>--image</code></td>
<td><code>data/test_image.jpg</code></td>
<td>입력 이미지 경로 (없으면 샘플 자동 다운로드)</td>
</tr>
<tr>
<td><code>--prompt</code></td>
<td><code>person</code></td>
<td>텍스트 프롬프트 (예: building, tree, car)</td>
</tr>
<tr>
<td><code>--model-id</code></td>
<td><code>facebook/sam3.1</code></td>
<td>HF 모델 ID</td>
</tr>
<tr>
<td><code>--backend</code></td>
<td><code>meta</code></td>
<td><code>meta</code> | <code>transformers</code></td>
</tr>
<tr>
<td><code>--confidence</code></td>
<td><code>0.3</code></td>
<td>confidence threshold. 0.5면 마스크 0개일 수 있음</td>
</tr>
<tr>
<td><code>--mask-threshold</code></td>
<td><code>0.5</code></td>
<td>transformers 후처리용 마스크 threshold</td>
</tr>
<tr>
<td><code>--resolution</code></td>
<td><code>1008</code></td>
<td>meta 입력 해상도</td>
</tr>
<tr>
<td><code>--min-size</code> / <code>--max-size</code></td>
<td><code>0</code> / 없음</td>
<td>마스크 픽셀 면적 필터</td>
</tr>
<tr>
<td><code>--device</code></td>
<td>자동</td>
<td><code>cuda</code> | <code>cpu</code></td>
</tr>
<tr>
<td><code>--checkpoint</code></td>
<td>환경변수 또는 없음</td>
<td>로컬 <code>.pt</code> 경로</td>
</tr>
<tr>
<td><code>--output-dir</code></td>
<td><code>output/</code></td>
<td>결과 저장 디렉터리</td>
</tr>
<tr>
<td><code>--no-viz</code></td>
<td>off</td>
<td>오버레이 PNG 생략 (마스크는 저장)</td>
</tr>
</tbody>
</table>
<h3>check_install.py 옵션</h3>
<pre><span class="cmd">python scripts\check_install.py</span>
<span class="cmd">python scripts\check_install.py --load-model</span>
<span class="cmd">python scripts\check_install.py --load-model --model-id facebook/sam3.1 --device cuda</span></pre>
</section>
<section id="multi">
<h2>6b. 멀티 프롬프트 (Grok + Gemini 병합)</h2>
<p>
드론 정사 장면 분석 결과를 합친 프롬프트 JSON과 일괄 실행 스크립트입니다.
모델·이미지를 <strong>1회만</strong> 로드한 뒤 프롬프트를 순차
<code>generate_masks</code> 합니다.
</p>
<table>
<thead>
<tr><th>경로</th><th>설명</th></tr>
</thead>
<tbody>
<tr>
<td><code>prompts/dji_20260306_0016.json</code></td>
<td>tier A~D + compact, 장면 노트(KO), 권장 confidence</td>
</tr>
<tr>
<td><code>scripts/multi_prompt_segment.py</code></td>
<td>멀티 프롬프트 러너 + <code>summary.json</code></td>
</tr>
<tr>
<td><code>data/DJI_20260306100802_0016.JPG</code></td>
<td>기본 입력 (없으면 sample 경로 fallback)</td>
</tr>
</tbody>
</table>
<pre><span class="cmd">python scripts\multi_prompt_segment.py --list-only --tier compact</span>
<span class="cmd">python scripts\multi_prompt_segment.py --tier compact --confidence 0.3</span>
<span class="cmd">python scripts\multi_prompt_segment.py --tier A_high</span>
<span class="cmd">python scripts\multi_prompt_segment.py --tier all --confidence 0.25</span>
<span class="cmd">python scripts\multi_prompt_segment.py --prompts "railway track,building,solar panel"</span></pre>
<p class="muted">
tier: <code>compact</code> | <code>gap</code> | <code>gap2</code> | <code>gap2_core</code> |
<code>improved</code> | <code>all</code> | <code>A_high</code> |
<code>B_facility</code> | <code>C_detail</code> | <code>D_rail_domain</code>
</p>
<p>결과 디렉터리: <code>output/&lt;이미지stem&gt;_&lt;tier&gt;/</code> — 프롬프트별 mask/ann + <code>summary.json</code></p>
<h3>DJI 정사 — 단일 프롬프트 예시 (실제 경로)</h3>
<pre><span class="cmd">cd D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
<span class="cmd">.\.venv\Scripts\Activate.ps1</span>
<span class="cmd">$img = "data\DJI_20260306100802_0016.JPG"</span>
<span class="cmd">$out = "output\dji_0016_analysis"</span>
<span class="cmd">python scripts\text_segment.py --image $img --prompt "solar panel" --confidence 0.3 --output-dir $out</span>
<span class="cmd">python scripts\text_segment.py --image $img --prompt "building" --confidence 0.3 --output-dir $out</span>
<span class="cmd">python scripts\text_segment.py --image $img --prompt "railway track" --confidence 0.3 --output-dir $out</span></pre>
<h3>기대 난이도 (정사 드론)</h3>
<table>
<thead>
<tr><th>프롬프트</th><th>기대</th><th>튜닝 팁</th></tr>
</thead>
<tbody>
<tr>
<td><code>building</code>, <code>solar panel</code>, <code>tree</code>, <code>car</code>, <code>truck</code></td>
<td>높음</td>
<td>기본 객체. 낮은 confidence에서도 비교적 잘 잡힘</td>
</tr>
<tr>
<td><code>railway track</code>, <code>fence</code>, <code>parking lot</code>, <code>blue tarp</code></td>
<td>중~높음</td>
<td>선형·구획. context에 따라 변동</td>
</tr>
<tr>
<td><code>utility pole</code>, <code>container</code>, <code>material pile</code></td>
<td></td>
<td>작거나 밀집. <code>--min-size</code> 권장</td>
</tr>
<tr>
<td><code>rail</code>, <code>ballast</code>, <code>railroad sleeper</code>, <code>catenary</code></td>
<td>낮~중</td>
<td>도메인·가는 객체. confidence 튜닝 필수</td>
</tr>
</tbody>
</table>
<div class="callout">
<strong>권장</strong>
confidence <strong>0.25~0.35</strong> 에서 시작. 작은 폴·미세 자재는 <code>--min-size</code>로 노이즈 필터.
</div>
<h3>결과 합치기 (merge)</h3>
<p>멀티 프롬프트 실행 후 클래스별 마스크를 <strong>한 장 오버레이 + 격자 + HTML</strong>로 합칩니다.</p>
<pre><span class="cmd">python scripts\merge_multi_results.py `
--result-dir "output\dji_0016_compact" `
--image "data\DJI_20260306100802_0016.JPG" `
--max-side 2560</span></pre>
<p>출력: <code>output\dji_0016_compact\merged\</code></p>
<ul>
<li><code>combined_with_legend.png</code> — 전 클래스 오버레이 + 범례</li>
<li><code>per_class_grid.png</code> — 클래스별 썸네일</li>
<li><code>index.html</code> — 브라우저 뷰</li>
</ul>
</section>
<section id="example-0100">
<h2>6c. 예시: DJI_0100 전체 파이프라인</h2>
<p>
0044 프롬프트 JSON(<strong>0016 상속 + 도로/열차 delta</strong>)을
<code>DJI_…_0100.JPG</code>에 적용하는 end-to-end 예입니다.
권장 순서: <code>compact</code> → (선택) <code>gap</code> → (선택) <code>gap2_core</code> → merge → HTML.
</p>
<p class="muted">
confidence: compact <code>0.3</code> · gap <code>0.25</code> · gap2_core <code>0.22</code>.
같은 출력 폴더에 단계별로 마스크를 쌓은 뒤 merge하면 한 HTML에서 전부 볼 수 있습니다.
</p>
<pre><span class="cmd">cd D:\MYCLAUDE_PROJECT\samgeo3-lab</span>
<span class="cmd">.\.venv\Scripts\Activate.ps1</span>
<span class="cmd">$img = "data\DJI_20260306101434_0100.JPG"</span>
<span class="cmd">$out = "output\dji_0100_compact"</span>
<span class="cmd">$json = "prompts\dji_20260306_0044.json"</span>
<span class="c"># (선택) 프롬프트 목록 확인</span>
<span class="cmd">python scripts\multi_prompt_segment.py --prompts-json $json --list-only --tier compact</span>
<span class="c"># 1) 세그 — compact (0016 상속 + 도로/열차 delta)</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json $json `
--image $img `
--tier compact `
--confidence 0.3 `
--output-dir $out</span>
<span class="c"># 2) (선택) gap 보강</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json $json `
--image $img `
--tier gap `
--confidence 0.25 `
--output-dir $out</span>
<span class="c"># 3) (선택) gap2_core 보강</span>
<span class="cmd">python scripts\multi_prompt_segment.py `
--prompts-json $json `
--image $img `
--tier gap2_core `
--confidence 0.22 `
--output-dir $out</span>
<span class="c"># 4) 합치기 + interactive index.html</span>
<span class="cmd">python scripts\merge_multi_results.py `
--result-dir $out `
--image $img `
--max-side 2048</span>
<span class="c"># 5) 결과 열기</span>
<span class="cmd">start "$out\merged\index.html"</span></pre>
<div class="callout ok">
<strong>결과</strong>
브라우저에서 <code>output\dji_0100_compact\merged\index.html</code> 을 열면
오버레이·클래스별 격자·범례를 한눈에 볼 수 있습니다.
</div>
</section>
<section id="output">
<h2>7. 출력 파일</h2>
<p>결과는 <code>output/</code> 아래에 저장됩니다. 파일명 패턴: <code>{이미지stem}_{prompt}_*</code></p>
<table>
<thead>
<tr><th>파일</th><th>내용</th></tr>
</thead>
<tbody>
<tr>
<td><code>*_mask.png</code></td>
<td>인스턴스별 unique mask (객체마다 다른 픽셀 값)</td>
</tr>
<tr>
<td><code>*_scores.npy</code></td>
<td>객체별 confidence 점수 (NumPy 배열). float PNG는 저장 불가</td>
</tr>
<tr>
<td><code>*_ann.png</code></td>
<td>원본 + 마스크/박스/점수 오버레이 시각화</td>
</tr>
</tbody>
</table>
<p class="muted">예: <code>test_image_person_mask.png</code>, <code>test_image_person_ann.png</code></p>
</section>
<section id="api">
<h2>8. Python API 빠른 참고</h2>
<pre><span class="cmd">from samgeo import SamGeo3</span>
sam = SamGeo3(
backend="meta", <span class="c"># sam3.1 은 meta 전용</span>
model_id="facebook/sam3.1",
confidence_threshold=0.3, <span class="c"># 환경에 맞게 튜닝</span>
resolution=1008,
enable_segmentation=True,
enable_inst_interactivity=False,
)
sam.set_image("data/test_image.jpg")
sam.generate_masks("person") <span class="c"># 텍스트 프롬프트</span>
sam.save_masks("output/mask.png")
sam.show_anns(output="output/ann.png")</pre>
<h3>포인트 / 박스 인터랙티브 (참고)</h3>
<pre>import numpy as np
from samgeo import SamGeo3
sam = SamGeo3(backend="meta", enable_inst_interactivity=True)
sam.set_image("image.jpg")
masks, scores, logits = sam.predict_inst(
point_coords=np.array([[520, 375]]),
point_labels=np.array([1]), <span class="c"># 1=foreground, 0=background</span>
)</pre>
<h3>배치 (meta만)</h3>
<pre>sam = SamGeo3(backend="meta")
sam.set_image_batch(["a.jpg", "b.jpg"])
sam.generate_masks_batch("tree")
sam.save_masks_batch("output/", prefix="tree_mask")</pre>
</section>
<section id="tips">
<h2>9. 튜닝 · 트러블슈팅</h2>
<table>
<thead>
<tr><th>증상</th><th>원인 / 대응</th></tr>
</thead>
<tbody>
<tr>
<td><code>No module named 'triton'</code></td>
<td><code>uv pip install "triton-windows&gt;=3.3.0.post19"</code></td>
</tr>
<tr>
<td><code>No objects found</code></td>
<td>
<code>--confidence</code> 낮추기 (예: 0.3 → 0.2), 프롬프트 변경,
명확한 객체가 보이는 이미지 사용
</td>
</tr>
<tr>
<td>로드 시 <code>missing_keys</code> 경고</td>
<td>
sam3 패키지와 sam3.1 체크포인트 일부 키 불일치.
추론은 동작할 수 있으나 score가 낮을 수 있음 → confidence 튜닝
</td>
</tr>
<tr>
<td><code>CUDA not available</code></td>
<td>
CUDA torch 재설치, <code>nvidia-smi</code> 확인.
SAM3 meta는 GPU 권장 (CPU만으로는 제한)
</td>
</tr>
<tr>
<td>HF 접근 거부 / 다운로드 실패</td>
<td>모델 게이트 승인 + <code>hf auth login</code></td>
</tr>
<tr>
<td>작은 객체 과다 검출</td>
<td><code>--min-size</code> 로 픽셀 면적 필터</td>
</tr>
<tr>
<td>GeoTIFF 멀티밴드</td>
<td>API: <code>set_image(path, bands=[4,3,2])</code> (1-based band index)</td>
</tr>
</tbody>
</table>
<div class="callout warn">
<strong>confidence 기본값 0.3 이유</strong>
이 환경에서 sam3.1 meta score가 대략 0.330.35 부근으로 나오는 경우가 있어,
기본 0.5를 쓰면 마스크가 0개일 수 있습니다. 실데이터에서 점수 분포를 보고 조정하세요.
</div>
</section>
<section id="links">
<h2>10. 참고 링크</h2>
<ul>
<li>업스트림: <a href="https://github.com/opengeos/segment-geospatial" target="_blank" rel="noopener">opengeos/segment-geospatial</a></li>
<li>문서: <a href="https://samgeo.gishub.org" target="_blank" rel="noopener">samgeo.gishub.org</a></li>
<li>SamGeo3 API: <a href="https://samgeo.gishub.org/samgeo3/" target="_blank" rel="noopener">samgeo3 문서</a></li>
<li>설치(pixi): <a href="https://samgeo.gishub.org/installation/" target="_blank" rel="noopener">Installation</a></li>
<li>라이선스(업스트림): MIT</li>
</ul>
</section>
<footer>
samgeo3-lab · 독립 SamGeo3 실험 환경 · 생성일 2026-07-15<br />
파일 위치: <span class="path">docs/usage.html</span>
</footer>
</div>
</body>
</html>
View File
+212
View File
@@ -0,0 +1,212 @@
# Multi-object prompts (Grok + Gemini)
비전 분석 결과를 합친 SAM 3.1 / SamGeo3 텍스트 프롬프트 세트입니다.
| 파일 | 설명 |
|------|------|
| `dji_20260306_0016.json` | DJI 드론 정사 철도·산업 장면 (Grok+Gemini 병합) |
| `dji_20260306_0044.json` | **0016 확장** (`extends`) + scene_delta (철로+도로+열차) |
## tier
| tier | 내용 |
|------|------|
| `compact` | 1차 실험용 19개 (기본) |
| `gap` | **누락 보강 12개** (Gemini 재검토: road, dense forest, covered truck 등) |
| `gap2` | 합본 빈 영역 보강 (다수 프롬프트) |
| `gap2_core` | gap2 중 실측 성공 프롬프트만 (재실행 권장) |
| `improved` | compact + gap 병합 (~31개) |
| `A_high` | 1차 고확률 |
| `B_facility` | 시설·구조 |
| `C_detail` | 야적·지면·지붕색 |
| `D_rail_domain` | 철도 특화 (편차 큼) |
| `E_missed` | gap 확장판 (동의어 포함) |
| `all` | A→E 전부 (중복 제거) |
## 기대 난이도 (정사 드론 기준)
| 프롬프트 | 기대 | 튜닝 팁 |
|----------|------|---------|
| `building`, `solar panel`, `tree`, `car`, `truck` | 높음 | 기본 객체. 낮은 confidence에서도 비교적 잘 잡힘 |
| `railway track`, `fence`, `parking lot`, `blue tarp` | 중~높음 | 선형·구획. context에 따라 변동 |
| `utility pole`, `container`, `material pile` | 중 | 작거나 밀집. `--min-size`로 노이즈 제거 |
| `rail`, `ballast`, `railroad sleeper`, `catenary` | 낮~중 | 도메인 용어·가는 객체. confidence 튜닝 필수 |
권장: **confidence 0.25~0.35** 로 시작.
## 단일 프롬프트 (`text_segment.py`)
```powershell
cd D:\MYCLAUDE_PROJECT\samgeo3-lab
.\.venv\Scripts\Activate.ps1
$img = "data\DJI_20260306100802_0016.JPG"
# 또는: $img = "D:\MYCLAUDE_PROJECT\segment-geospatial\sample\DJI_20260306100802_0016.JPG"
$out = "output\dji_0016_analysis"
python scripts\text_segment.py `
--image $img `
--prompt "solar panel" `
--confidence 0.3 `
--output-dir $out
python scripts\text_segment.py `
--image $img `
--prompt "building" `
--confidence 0.3 `
--output-dir $out
python scripts\text_segment.py `
--image $img `
--prompt "railway track" `
--confidence 0.3 `
--output-dir $out
```
결과 예: `output\dji_0016_analysis\DJI_..._solar_panel_mask.png`
## 멀티 프롬프트 일괄 (`multi_prompt_segment.py`) — 이미 포함됨
모델·이미지를 **1회 로드** 후 프롬프트 루프. 결과는
`output\<stem>_<tier>\{stem}_{prompt}_mask.png` 등 + `summary.json`.
```powershell
cd D:\MYCLAUDE_PROJECT\samgeo3-lab
.\.venv\Scripts\Activate.ps1
# 프롬프트 목록만
python scripts\multi_prompt_segment.py --list-only --tier compact
# compact 19개 일괄 (권장 1차)
python scripts\multi_prompt_segment.py `
--image "data\DJI_20260306100802_0016.JPG" `
--tier compact `
--confidence 0.3 `
--output-dir "output\dji_0016_compact"
# 누락 보강 (Gemini gap) — confidence 약간 낮게, 같은 폴더에 추가하면 merge에 포함
python scripts\multi_prompt_segment.py `
--image "data\DJI_20260306100802_0016.JPG" `
--tier gap `
--confidence 0.25 `
--output-dir "output\dji_0016_compact"
# 1차+보강 한 번에
python scripts\multi_prompt_segment.py --tier improved --confidence 0.28
# 1차 고확률만
python scripts\multi_prompt_segment.py --tier A_high --confidence 0.3
# 전체 tier
python scripts\multi_prompt_segment.py --tier all --confidence 0.25
# 직접 지정
python scripts\multi_prompt_segment.py `
--prompts "solar panel,building,railway track,tree,car,truck" `
--confidence 0.3 `
--output-dir "output\dji_0016_analysis"
```
## DJI_0044 = 이미지 1번(0016) 확장 (별도 체계 아님)
```text
0016 base (compact/gap/gap2) + scene_delta(열차·도로·야적) → 0044
```
```powershell
# 상속 확인
python scripts\multi_prompt_segment.py --prompts-json prompts\dji_20260306_0044.json --list-only --tier compact
python scripts\multi_prompt_segment.py --prompts-json prompts\dji_20260306_0044.json --list-only --tier delta
# 0016과 동일한 파이프라인
python scripts\multi_prompt_segment.py `
--prompts-json prompts\dji_20260306_0044.json `
--image data\DJI_20260306100928_0044.JPG `
--tier compact --confidence 0.3 `
--output-dir output\dji_0044_compact
python scripts\multi_prompt_segment.py `
--prompts-json prompts\dji_20260306_0044.json `
--tier gap --confidence 0.25 `
--output-dir output\dji_0044_compact
python scripts\merge_multi_results.py `
--result-dir output\dji_0044_compact `
--image data\DJI_20260306100928_0044.JPG
```
HTML: `docs/dji_0044_prompts.html`
## 예시: DJI_0100 전체 파이프라인 (compact → gap → gap2_core → merge)
0044 프롬프트 JSON(0016 상속 + 도로/열차 delta)을 **0100 이미지**에 적용하는
권장 end-to-end 예입니다. gap / gap2_core 단계는 선택입니다.
```powershell
cd D:\MYCLAUDE_PROJECT\samgeo3-lab
.\.venv\Scripts\Activate.ps1
$img = "data\DJI_20260306101434_0100.JPG"
$out = "output\dji_0100_compact"
$json = "prompts\dji_20260306_0044.json"
# (선택) 프롬프트 목록 확인
python scripts\multi_prompt_segment.py --prompts-json $json --list-only --tier compact
# 1) 세그 — compact (0016 상속 + 도로/열차 delta)
python scripts\multi_prompt_segment.py `
--prompts-json $json `
--image $img `
--tier compact `
--confidence 0.3 `
--output-dir $out
# 2) (선택) gap 보강
python scripts\multi_prompt_segment.py `
--prompts-json $json `
--image $img `
--tier gap `
--confidence 0.25 `
--output-dir $out
# 3) (선택) gap2_core 보강
python scripts\multi_prompt_segment.py `
--prompts-json $json `
--image $img `
--tier gap2_core `
--confidence 0.22 `
--output-dir $out
# 4) 합치기 + interactive index.html
python scripts\merge_multi_results.py `
--result-dir $out `
--image $img `
--max-side 2048
# 5) 결과 열기
start "$out\merged\index.html"
```
권장 confidence: compact `0.3` → gap `0.25` → gap2_core `0.22`.
같은 `$out` 폴더에 단계별로 마스크를 쌓은 뒤 merge하면 한 HTML에서 전부 볼 수 있습니다.
## 결과 합치기 (overlay + grid + HTML)
`multi_prompt_segment` 실행 후:
```powershell
python scripts\merge_multi_results.py `
--result-dir "output\dji_0016_compact" `
--image "data\DJI_20260306100802_0016.JPG" `
--max-side 2560
```
출력 (`output\dji_0016_compact\merged\`):
| 파일 | 내용 |
|------|------|
| `combined_overlay.png` | 전 클래스 색상 오버레이 |
| `combined_with_legend.png` | 오버레이 + 범례 |
| `per_class_grid.png` | 클래스별 썸네일 격자 |
| `index.html` | 브라우저로 한눈에 보기 |
| `merge_summary.json` | 클래스·픽셀 통계 |
+385
View File
@@ -0,0 +1,385 @@
{
"id": "dji_20260306_0016",
"image": "data/DJI_20260306100802_0016.JPG",
"image_abs_hint": "D:/MYCLAUDE_PROJECT/segment-geospatial/sample/DJI_20260306100802_0016.JPG",
"sources": ["grok-vision", "gemini-vision", "gemini-missed-review"],
"scene_notes_ko": {
"center": "복선 철로, 침목, 자갈 도상(발라스트), 전차선 전주(폴)",
"above_track": "나대지/낙엽 경사면, 검은 덮개막, 파란 타프",
"below_track": "녹색 방음벽/펜스 라인",
"top_left": "낙엽 수목 지역",
"top_right": "대형 창고·차양, 주차장, 흰 승용차, 덮개 화물차, 녹색 지붕 소형 건물, 파란 컨테이너, 노란 안전구역, 가로등/전주",
"bottom": "태양광 패널 지붕, 흰/회색 지붕, 파란/녹색 지붕, 자재 야적(철재·파이프), 녹색 방수시트, 드럼통 더미",
"missed_review_ko": "범례에 가려진 우측 숲·철로 연속부; parking lot만 되고 road 누락; covered truck; street light; drainage; debris"
},
"tiers": {
"A_high": {
"description": "잘 잡힐 가능성 높음 — 강력한 기본 객체",
"prompts": [
"railway track",
"train track",
"railroad",
"building",
"roof",
"solar panel",
"tree",
"car",
"truck",
"fence"
]
},
"B_facility": {
"description": "구체적 시설·구조",
"prompts": [
"warehouse",
"factory building",
"parking lot",
"container",
"utility pole",
"electric pole",
"catenary pole",
"sound barrier",
"retaining wall",
"drainage ditch",
"canopy",
"awning"
]
},
"C_detail": {
"description": "세부·야적·지면·지붕 색",
"prompts": [
"solar farm",
"photovoltaic panel",
"cargo truck",
"covered truck",
"material pile",
"pipe stack",
"steel pile",
"steel pipe",
"bare ground",
"dirt road",
"paved road",
"shed",
"small building",
"blue tarp",
"black tarp",
"green tarp",
"construction tarp",
"green roof",
"blue roof",
"white roof",
"parking space"
]
},
"D_rail_domain": {
"description": "철도 특화 — 성공 편차 큼, confidence/min_size 튜닝 필요",
"prompts": [
"rail",
"ballast",
"railroad sleeper",
"overhead line",
"railway catenary",
"railway embankment",
"noise barrier wall"
]
},
"E_missed": {
"description": "Gemini 재검토: 1차 compact에서 놓치거나 가려진 영역 보강",
"review_notes_ko": [
"범례로 가려진 우측: dense forest / tree 연속부",
"철로·도상 연속: railway 보강은 D와 중복 가능, gravel path 추가",
"parking lot 연결 아스팔트 road 누락",
"covered truck / cargo vehicle 누락",
"street light / lamp post, drainage ditch",
"debris / rubbish / industrial waste"
],
"prompts": [
"road",
"asphalt pavement",
"asphalt road",
"paved road",
"gravel path",
"dense forest",
"forest",
"covered truck",
"cargo vehicle",
"blue covered truck",
"street light",
"lamp post",
"street lamp",
"light pole",
"drainage ditch",
"drainage channel",
"debris",
"rubbish",
"industrial waste",
"scrap metal",
"scrap pile"
],
"recommended_confidence": 0.25
}
},
"compact": {
"description": "1차 실험용 압축 세트",
"prompts": [
"railway track",
"railroad",
"fence",
"utility pole",
"building",
"warehouse",
"solar panel",
"green roof",
"blue roof",
"white roof",
"car",
"truck",
"blue tarp",
"black tarp",
"container",
"material pile",
"tree",
"bare ground",
"parking lot"
]
},
"gap": {
"description": "누락 보강 전용 — Gemini 추천 + 실측 성공 동의어",
"prompts": [
"road",
"asphalt pavement",
"gravel path",
"dense forest",
"covered truck",
"cargo vehicle",
"street light",
"lamp post",
"light pole",
"drainage ditch",
"debris",
"rubbish",
"industrial waste",
"scrap pile"
],
"recommended_confidence": 0.25,
"notes_ko": {
"worked": ["road", "asphalt pavement", "gravel path", "dense forest", "covered truck", "cargo vehicle", "debris", "rubbish", "light pole", "scrap pile"],
"hard_at_0.25": ["street light", "lamp post", "drainage ditch", "industrial waste"],
"tip": "가로등은 light pole@0.2, 배수로는 정사에서 거의 안 잡힘, rubbish/scrap pile은 노이즈 많을 수 있음"
}
},
"improved": {
"description": "compact + gap 병합 (1차+누락 보강, 중복 제거)",
"prompts": [
"railway track",
"railroad",
"fence",
"utility pole",
"building",
"warehouse",
"solar panel",
"green roof",
"blue roof",
"white roof",
"car",
"truck",
"blue tarp",
"black tarp",
"container",
"material pile",
"tree",
"bare ground",
"parking lot",
"road",
"asphalt pavement",
"gravel path",
"dense forest",
"covered truck",
"cargo vehicle",
"street light",
"lamp post",
"light pole",
"drainage ditch",
"debris",
"rubbish",
"industrial waste",
"scrap pile"
],
"recommended_confidence": 0.28
},
"gap2": {
"description": "combined_with_legend 재검토: 아직 원본이 비어 보이는 영역 보강",
"source": "visual review of final merged overlay (Downloads/combined_with_legend.png)",
"gaps_ko": [
"선로 상·하 갈색 성토/제방·마른 초지(scrub) — railway 노란 밴드 바깥",
"검은 타프 주변 갈색 흙더미·절토면",
"주차장·창고 사이 미포장 공터·콘크리트 슬라브",
"하측 야적장 녹생 방수시트/덮개(green tarp) 일부 미채움",
"소형 부스·shed·담장·차양",
"금속/골판 지붕 중 색 라벨에 안 걸린 부분",
"전차선·가느다란 전선/폴 잔여",
"건물 벽면·그림자 밴드",
"선로 침목·도상(ballast) 세부"
],
"prompts": [
"embankment",
"railway embankment",
"dirt embankment",
"soil pile",
"dirt pile",
"earth mound",
"dry grass",
"scrub",
"bushes",
"grass",
"vegetation",
"dormant vegetation",
"brown field",
"open ground",
"concrete",
"concrete pad",
"concrete pavement",
"industrial yard",
"paved yard",
"green tarp",
"green plastic sheet",
"plastic cover",
"shed",
"small shed",
"booth",
"kiosk",
"metal roof",
"corrugated roof",
"gray roof",
"wall",
"building wall",
"fence wall",
"barrier",
"retaining wall",
"canopy",
"awning",
"ballast",
"track bed",
"railway ballast",
"power line",
"overhead wire",
"shadow",
"sidewalk",
"walkway"
],
"recommended_confidence": 0.22,
"worked_at_0.22": [
"dirt embankment",
"dirt pile",
"dry grass",
"bushes",
"grass",
"vegetation",
"brown field",
"open ground",
"concrete",
"concrete pad",
"concrete pavement",
"industrial yard",
"paved yard",
"green tarp",
"green plastic sheet",
"plastic cover",
"shed",
"small shed",
"metal roof",
"corrugated roof",
"gray roof",
"wall",
"building wall",
"fence wall",
"barrier",
"canopy",
"awning",
"sidewalk"
],
"failed_at_0.22": [
"embankment",
"railway embankment",
"soil pile",
"earth mound",
"scrub",
"dormant vegetation",
"booth",
"kiosk",
"retaining wall",
"ballast",
"track bed",
"railway ballast",
"power line",
"overhead wire",
"shadow",
"walkway"
]
},
"gap2_core": {
"description": "gap2 중 실측 성공 프롬프트만 (재실행 권장 세트)",
"prompts": [
"dirt embankment",
"dirt pile",
"dry grass",
"bushes",
"vegetation",
"brown field",
"open ground",
"concrete",
"industrial yard",
"green tarp",
"plastic cover",
"shed",
"metal roof",
"corrugated roof",
"gray roof",
"building wall",
"barrier",
"awning"
],
"recommended_confidence": 0.22
},
"recommended": {
"model_id": "facebook/sam3.1",
"backend": "meta",
"confidence": 0.3,
"confidence_range": [0.25, 0.35],
"gap_confidence": 0.25,
"min_size": 0,
"note": "1차는 compact@0.3, 누락 보강은 gap@0.25 후 merge. 작은 폴/자재는 --min-size."
},
"expected_difficulty": [
{
"prompts": ["building", "solar panel", "tree", "car", "truck"],
"level": "high",
"level_ko": "높음",
"tip_ko": "기본 객체. 낮은 confidence에서도 비교적 잘 잡힘."
},
{
"prompts": ["railway track", "fence", "parking lot", "blue tarp"],
"level": "medium_high",
"level_ko": "중~높음",
"tip_ko": "선형·구획 객체. 장면 context에 따라 성능 변동."
},
{
"prompts": ["road", "asphalt pavement", "dense forest", "covered truck"],
"level": "medium",
"level_ko": "중 (gap 보강)",
"tip_ko": "1차에서 자주 누락. gap tier로 재실행. confidence 0.25 권장."
},
{
"prompts": ["utility pole", "container", "material pile", "street light", "lamp post"],
"level": "medium",
"level_ko": "중",
"tip_ko": "작거나 밀집. --min-size로 노이즈 제거 권장."
},
{
"prompts": ["rail", "ballast", "railroad sleeper", "railway catenary", "debris", "rubbish"],
"level": "low_medium",
"level_ko": "낮~중",
"tip_ko": "도메인·가는 객체·잔해. confidence 튜닝 필수."
}
]
}
+157
View File
@@ -0,0 +1,157 @@
{
"id": "dji_20260306_0044",
"extends": "prompts/dji_20260306_0016.json",
"extends_note_ko": "1번 이미지(DJI_0016) 프롬프트·tier·gap/gap2 워크플로를 그대로 상속. 이 파일은 장면 전용 객체만 추가 확장. 별도 체계 아님.",
"image": "data/DJI_20260306100928_0044.JPG",
"image_abs_hint": "D:/MYCLAUDE_PROJECT/segment-geospatial/sample/DJI_20260306100928_0044.JPG",
"sources": ["extends:dji_20260306_0016", "gemini-vision", "grok-vision"],
"scene_notes_ko": {
"extends": "0016 = 철로+산업단지 베이스. 0044 = 동일 파이프라인 + 도로·열차·야적 강화",
"top": "다차로 도로·교차로·횡단보도·주행 차량·가로수·밀집 건물·태양광",
"middle": "야적장·파이프·백색 자루·목재·철재·파란 타프·철조망 펜스",
"bottom": "복선 철로·열차(객차/화차)·전차선 폴·나대지"
},
"scene_delta": {
"description": "0016 베이스에 없는(또는 이 장면에서 핵심인) 추가 객체 — Gemini+Grok",
"prompts": [
"train",
"locomotive",
"passenger train",
"freight car",
"container car",
"highway",
"pedestrian crossing",
"crosswalk",
"road marking",
"chain-link fence",
"pipe stack",
"pipe pile",
"pipes",
"industrial pipes",
"bag pile",
"bulk bag",
"white sack pile",
"wooden pile",
"lumber pile",
"wood stack",
"tarp",
"gravel",
"gravel ground",
"paved ground",
"dirt ground",
"shrub",
"sleeper",
"trackbed",
"catenary",
"boxcar",
"construction material",
"industrial material",
"debris pile",
"red roof"
],
"recommended_confidence": 0.28
},
"tiers": {
"A_high": {
"description": "0016 A_high + 도로/열차 핵심 추가",
"prompts": [
"road",
"highway",
"train",
"freight car",
"container car",
"car",
"truck"
]
},
"B_facility": {
"description": "0016 B + 횡단보도·도로시설",
"prompts": [
"pedestrian crossing",
"crosswalk",
"road marking",
"chain-link fence",
"street light",
"light pole"
]
},
"C_detail": {
"description": "0016 C + 야적 자재 세분",
"prompts": [
"pipe pile",
"pipes",
"bag pile",
"wooden pile",
"wood stack",
"tarp",
"gravel",
"dirt ground"
]
},
"D_rail_domain": {
"description": "0016 D + 열차 관련",
"prompts": [
"train",
"freight car",
"container car",
"sleeper",
"trackbed",
"catenary"
]
}
},
"compact": {
"description": "0016 compact 상속 + scene_delta(핵심만 필터하지 않고 전체 병합; 실행 시 길면 --tier delta)",
"include_scene_delta": true,
"prompts": [
"train",
"freight car",
"container car",
"highway",
"pedestrian crossing",
"pipe pile",
"wooden pile",
"wood stack"
],
"recommended_confidence": 0.3
},
"recommended": {
"model_id": "facebook/sam3.1",
"backend": "meta",
"confidence": 0.3,
"confidence_range": [0.25, 0.35],
"workflow_ko": [
"1) 0016과 동일: compact → gap → gap2_core (상속된 tier)",
"2) 0044 전용만: --tier delta",
"3) 합본: base compact+delta = --tier compact",
"4) merge_multi_results.py"
],
"note": "별도 체계 아님. 1번 이미지 확장."
},
"expected_difficulty": [
{
"prompts": ["building", "train", "car", "truck", "tree", "road"],
"level": "high",
"level_ko": "높음",
"tip_ko": "0016과 동일 orthophoto 기본 객체 + 열차"
},
{
"prompts": ["railway track", "highway", "fence", "material pile", "pedestrian crossing"],
"level": "medium_high",
"level_ko": "중~높음",
"tip_ko": "선형·구획"
},
{
"prompts": ["pipe pile", "bag pile", "wooden pile", "street light"],
"level": "medium",
"level_ko": "중",
"tip_ko": "밀집 자재 — min_size / conf 0.22~0.28"
},
{
"prompts": ["rail", "ballast", "sleeper", "catenary", "locomotive"],
"level": "low_medium",
"level_ko": "낮~중",
"tip_ko": "도메인·세분 용어 — 0016 gap2와 동일 한계"
}
]
}
+19
View File
@@ -0,0 +1,19 @@
[project]
name = "samgeo3-lab"
version = "0.1.0"
description = "Independent SamGeo3 (SAM 3 / SAM 3.1) segmentation experiments"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
dependencies = [
"segment-geospatial[samgeo3]>=1.4.1",
"triton-windows>=3.3.0.post19; sys_platform == 'windows'",
"matplotlib>=3.8",
]
[project.optional-dependencies]
notebook = ["ipykernel>=6.29", "jupyterlab>=4.0"]
[tool.uv]
# Torch CUDA wheels are installed separately from the pytorch index.
# See README install section.
+7
View File
@@ -0,0 +1,7 @@
# Pin loosely for experiments. Install torch CUDA wheels FIRST (see README).
# uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
segment-geospatial[samgeo3]>=1.4.1
triton-windows>=3.3.0.post19; sys_platform == "windows"
matplotlib>=3.8
ipykernel>=6.29
jupyterlab>=4.0
+174
View File
@@ -0,0 +1,174 @@
"""Verify SamGeo3 / PyTorch / CUDA install for this isolated lab env.
Usage (from project root, with venv active):
python scripts/check_install.py
python scripts/check_install.py --load-model
"""
from __future__ import annotations
import argparse
import os
import sys
import traceback
def _ok(msg: str) -> None:
print(f"[OK] {msg}")
def _warn(msg: str) -> None:
print(f"[WARN] {msg}")
def _fail(msg: str) -> None:
print(f"[FAIL] {msg}")
def check_torch() -> bool:
try:
import torch
except ImportError as e:
_fail(f"torch import failed: {e}")
return False
_ok(f"torch {torch.__version__}")
cuda = torch.cuda.is_available()
if cuda:
name = torch.cuda.get_device_name(0)
cap = torch.cuda.get_device_capability(0)
_ok(f"CUDA available | GPU={name} | capability={cap}")
_ok(f"cuda runtime reported by torch: {torch.version.cuda}")
else:
_warn("CUDA not available — SAM3 meta backend needs NVIDIA GPU + CUDA torch")
return True
def check_samgeo3_imports() -> bool:
try:
import samgeo
from samgeo import SamGeo3
except ImportError as e:
_fail(f"samgeo / SamGeo3 import failed: {e}")
return False
_ok(f"samgeo {getattr(samgeo, '__version__', 'unknown')}")
_ok("SamGeo3 class importable")
try:
import sam3 # noqa: F401
_ok(f"sam3 package present ({getattr(sam3, '__version__', 'no __version__')})")
except ImportError as e:
_fail(f"sam3 package missing (required for backend='meta'): {e}")
return False
return True
def check_hf_access() -> None:
token_env = bool(os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN"))
token_file = os.path.expanduser("~/.cache/huggingface/token")
has_file = os.path.isfile(token_file)
if token_env:
_ok("HF token found in environment")
elif has_file:
_ok(f"HF token file present: {token_file}")
else:
_warn(
"No HF token found. SAM3/3.1 gated models need: "
"hf auth login (after HF access approval)"
)
ckpt = os.environ.get("SAM3_CHECKPOINT_PATH")
if ckpt:
exists = os.path.isfile(ckpt)
( _ok if exists else _fail)(f"SAM3_CHECKPOINT_PATH={ckpt} exists={exists}")
else:
_warn("SAM3_CHECKPOINT_PATH not set (optional; HF download/cache used otherwise)")
def try_load_model(model_id: str, device: str | None) -> bool:
try:
from samgeo import SamGeo3
import torch
except ImportError as e:
_fail(f"imports for model load failed: {e}")
return False
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda" and not torch.cuda.is_available():
_fail("requested CUDA but torch.cuda.is_available() is False")
return False
print(f"\nLoading SamGeo3(backend='meta', model_id='{model_id}', device='{device}') ...")
try:
kwargs = {
"backend": "meta",
"model_id": model_id,
"device": device,
"confidence_threshold": 0.5,
"enable_segmentation": True,
"enable_inst_interactivity": False,
}
ckpt = os.environ.get("SAM3_CHECKPOINT_PATH")
if ckpt and os.path.isfile(ckpt):
kwargs["checkpoint_path"] = ckpt
kwargs["load_from_HF"] = False
_ok(f"using local checkpoint: {ckpt}")
sam = SamGeo3(**kwargs)
_ok(f"model loaded | backend={sam.backend} | device={getattr(sam, 'device', device)}")
del sam
if torch.cuda.is_available():
torch.cuda.empty_cache()
return True
except Exception as e:
_fail(f"model load failed: {e}")
traceback.print_exc()
return False
def main() -> int:
parser = argparse.ArgumentParser(description="SamGeo3 install verification")
parser.add_argument(
"--load-model",
action="store_true",
help="Also instantiate SamGeo3 (downloads weights if needed)",
)
parser.add_argument(
"--model-id",
default="facebook/sam3.1",
help="HF model id for --load-model (default: facebook/sam3.1)",
)
parser.add_argument(
"--device",
default=None,
help="cuda | cpu (default: auto)",
)
args = parser.parse_args()
print("=== SamGeo3 lab: install check ===\n")
print(f"python: {sys.version}")
print(f"exe: {sys.executable}\n")
ok = True
ok = check_torch() and ok
ok = check_samgeo3_imports() and ok
check_hf_access()
if args.load_model:
ok = try_load_model(args.model_id, args.device) and ok
print()
if ok:
_ok("all required checks passed")
return 0
_fail("one or more required checks failed")
return 1
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
+570
View File
@@ -0,0 +1,570 @@
"""Run SamGeo3 text segmentation for multiple prompts (one model load).
Prompt sets are Grok + Gemini vision merged lists (see prompts/*.json).
Usage (from project root, venv active):
python scripts/multi_prompt_segment.py
python scripts/multi_prompt_segment.py --tier compact
python scripts/multi_prompt_segment.py --tier A_high
python scripts/multi_prompt_segment.py --tier all
python scripts/multi_prompt_segment.py --prompts "building,solar panel,tree"
python scripts/multi_prompt_segment.py --image data/DJI_20260306100802_0016.JPG --tier compact
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_PROMPTS_JSON = ROOT / "prompts" / "dji_20260306_0016.json"
DEFAULT_IMAGE = ROOT / "data" / "DJI_20260306100802_0016.JPG"
# fallback to sample folder outside lab if data copy missing
SAMPLE_FALLBACK = Path(
r"D:\MYCLAUDE_PROJECT\segment-geospatial\sample\DJI_20260306100802_0016.JPG"
)
def safe_name(prompt: str) -> str:
return "".join(c if c.isalnum() or c in "-_" else "_" for c in prompt.strip())
def _dedupe(prompts: list[str]) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for p in prompts:
p = p.strip()
if p and p not in seen:
seen.add(p)
out.append(p)
return out
def _merge_prompt_lists(*lists: list[str]) -> list[str]:
merged: list[str] = []
for lst in lists:
merged.extend(lst or [])
return _dedupe(merged)
def load_prompt_config(path: Path, _stack: list[Path] | None = None) -> dict[str, Any]:
"""Load JSON; if 'extends' is set, inherit base (image-1) and apply scene delta.
Extension model (image N expands image 1, not a separate system):
- base tiers / compact / gap / improved / gap2* are kept
- child may add scene_delta.prompts and optional tier/set additions
- child image path overrides base image
"""
path = path.resolve()
stack = _stack or []
if path in stack:
raise ValueError(f"Circular extends: {path}")
stack = stack + [path]
with open(path, encoding="utf-8") as f:
cfg = json.load(f)
extends = cfg.get("extends")
if not extends:
return cfg
base_path = Path(extends)
if not base_path.is_absolute():
# relative to project root first, then to this file's dir
cand = (ROOT / extends).resolve()
if not cand.is_file():
cand = (path.parent / extends).resolve()
base_path = cand
if not base_path.is_file():
raise FileNotFoundError(f"extends not found: {extends} (from {path})")
base = load_prompt_config(base_path, stack)
# Start from base, then overlay child metadata / image
merged: dict[str, Any] = json.loads(json.dumps(base)) # deep copy
merged["id"] = cfg.get("id", base.get("id"))
merged["extends"] = str(extends)
merged["extends_resolved"] = str(base_path)
for key in (
"image",
"image_abs_hint",
"sources",
"scene_notes_ko",
"recommended",
"expected_difficulty",
):
if key in cfg:
merged[key] = cfg[key]
# scene-only additions (this image expands base)
delta = list(cfg.get("scene_delta", {}).get("prompts", []))
merged["scene_delta"] = cfg.get("scene_delta", {"prompts": delta})
# Merge tiers: base list + child additions (if child redefines tier fully, use add only
# unless replace_tiers=true)
child_tiers = cfg.get("tiers", {})
if cfg.get("replace_tiers"):
merged["tiers"] = child_tiers
else:
base_tiers = dict(merged.get("tiers", {}))
for tname, tblock in child_tiers.items():
add = list(tblock.get("prompts", []))
if tname in base_tiers:
base_tiers[tname] = {
**base_tiers[tname],
"description": tblock.get(
"description", base_tiers[tname].get("description", "")
),
"prompts": _merge_prompt_lists(
base_tiers[tname].get("prompts", []), add
),
}
else:
base_tiers[tname] = tblock
# also append scene_delta into A_high by default for visibility
if delta and "A_high" in base_tiers:
base_tiers["A_high"] = {
**base_tiers["A_high"],
"prompts": _merge_prompt_lists(
base_tiers["A_high"].get("prompts", []), delta
),
}
merged["tiers"] = base_tiers
# Named sets: inherit base, extend with delta / child prompts
for set_name in (
"compact",
"gap",
"improved",
"gap2",
"gap2_core",
"compact_plus",
):
base_set = merged.get(set_name, {})
base_prompts = list(base_set.get("prompts", [])) if isinstance(base_set, dict) else []
child_set = cfg.get(set_name)
if child_set is None:
# default: base set + scene_delta for compact / improved
if set_name in ("compact", "improved", "compact_plus") and delta:
merged[set_name] = {
**(base_set if isinstance(base_set, dict) else {}),
"description": (
f"extends base {set_name} + scene_delta "
f"({cfg.get('id', 'child')})"
),
"prompts": _merge_prompt_lists(base_prompts, delta),
}
continue
if child_set.get("mode") == "replace":
merged[set_name] = child_set
else:
# default mode: append (base + child + optional delta)
extra = list(child_set.get("prompts", []))
use_delta = child_set.get("include_scene_delta", True)
parts = [base_prompts, extra]
if use_delta:
parts.append(delta)
merged[set_name] = {
**(base_set if isinstance(base_set, dict) else {}),
**{k: v for k, v in child_set.items() if k != "prompts"},
"description": child_set.get(
"description",
f"extends base {set_name} + additions",
),
"prompts": _merge_prompt_lists(*parts),
}
# If child has no compact at all, still attach delta
if "compact" not in cfg and delta:
bp = list(base.get("compact", {}).get("prompts", []))
merged["compact"] = {
**base.get("compact", {}),
"description": "base compact + scene_delta",
"prompts": _merge_prompt_lists(bp, delta),
}
merged["inheritance_ko"] = (
"1번 이미지 프롬프트 체계를 상속하고, 이 장면 전용 객체만 추가 확장."
)
return merged
def resolve_prompts(
cfg: dict[str, Any],
tier: str,
extra_prompts: list[str] | None,
) -> list[str]:
if extra_prompts:
return [p.strip() for p in extra_prompts if p.strip()]
# scene_delta only (this image's new objects vs base)
if tier in ("delta", "scene_delta", "new"):
return list(cfg.get("scene_delta", {}).get("prompts", []))
# top-level named sets (compact / gap / improved / gap2...)
if tier in cfg and isinstance(cfg[tier], dict) and "prompts" in cfg[tier]:
return list(cfg[tier]["prompts"])
if tier == "all":
seen: set[str] = set()
out: list[str] = []
for key in (
"A_high",
"B_facility",
"C_detail",
"D_rail_domain",
"E_missed",
):
block = cfg.get("tiers", {}).get(key, {})
for p in block.get("prompts", []):
if p not in seen:
seen.add(p)
out.append(p)
# include scene_delta
for p in cfg.get("scene_delta", {}).get("prompts", []):
if p not in seen:
seen.add(p)
out.append(p)
return out
if tier in cfg.get("tiers", {}):
return list(cfg["tiers"][tier]["prompts"])
# allow short aliases
aliases = {
"a": "A_high",
"A": "A_high",
"b": "B_facility",
"B": "B_facility",
"c": "C_detail",
"C": "C_detail",
"d": "D_rail_domain",
"D": "D_rail_domain",
"e": "E_missed",
"E": "E_missed",
"missed": "E_missed",
"gap_fill": "gap",
}
if tier in aliases:
key = aliases[tier]
if key in cfg.get("tiers", {}):
return list(cfg["tiers"][key]["prompts"])
if key in cfg and "prompts" in cfg[key]:
return list(cfg[key]["prompts"])
raise ValueError(
f"Unknown tier '{tier}'. Use compact | gap | gap2 | improved | all | "
f"delta | A_high | B_facility | C_detail | D_rail_domain | E_missed | or --prompts"
)
def resolve_image(path: Path | None, cfg: dict[str, Any]) -> Path:
candidates: list[Path] = []
if path is not None:
candidates.append(path)
candidates.append(ROOT / cfg.get("image", "data/DJI_20260306100802_0016.JPG"))
candidates.append(DEFAULT_IMAGE)
candidates.append(SAMPLE_FALLBACK)
abs_hint = cfg.get("image_abs_hint")
if abs_hint:
candidates.append(Path(abs_hint))
for c in candidates:
if c and Path(c).is_file():
return Path(c).resolve()
raise FileNotFoundError(
"Image not found. Place DJI JPG under data/ or pass --image. Tried:\n "
+ "\n ".join(str(c) for c in candidates)
)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Multi-prompt SamGeo3 segmentation (Grok+Gemini prompt sets)"
)
p.add_argument(
"--prompts-json",
type=Path,
default=DEFAULT_PROMPTS_JSON,
help="JSON with tiers/compact prompts",
)
p.add_argument(
"--tier",
default="compact",
help=(
"compact | gap | gap2 | improved | all | "
"A_high | B_facility | C_detail | D_rail_domain | E_missed"
),
)
p.add_argument(
"--prompts",
default=None,
help='Comma-separated overrides, e.g. "building,tree,car"',
)
p.add_argument("--image", type=Path, default=None)
p.add_argument("--model-id", default="facebook/sam3.1")
p.add_argument("--backend", default="meta", choices=["meta", "transformers"])
p.add_argument("--confidence", type=float, default=0.3)
p.add_argument("--mask-threshold", type=float, default=0.5)
p.add_argument("--resolution", type=int, default=1008)
p.add_argument("--min-size", type=int, default=0)
p.add_argument("--max-size", type=int, default=None)
p.add_argument("--device", default=None)
p.add_argument(
"--checkpoint",
default=os.environ.get("SAM3_CHECKPOINT_PATH"),
)
p.add_argument(
"--output-dir",
type=Path,
default=None,
help="Default: output/<image_stem>_<tier>/",
)
p.add_argument("--no-viz", action="store_true")
p.add_argument(
"--skip-empty",
action="store_true",
default=True,
help="Do not write mask files when 0 objects (default true)",
)
p.add_argument(
"--keep-empty",
action="store_true",
help="Opposite of --skip-empty",
)
p.add_argument(
"--list-only",
action="store_true",
help="Print resolved prompts and exit (no model load)",
)
return p.parse_args()
def main() -> int:
args = parse_args()
cfg_path = args.prompts_json.resolve()
if not cfg_path.is_file():
print(f"ERROR: prompts json not found: {cfg_path}", file=sys.stderr)
return 2
cfg = load_prompt_config(cfg_path)
extra = None
if args.prompts:
extra = [x.strip() for x in args.prompts.split(",") if x.strip()]
try:
prompts = resolve_prompts(cfg, args.tier, extra)
except ValueError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 2
# de-dupe preserve order
seen: set[str] = set()
prompts = [p for p in prompts if not (p in seen or seen.add(p))]
if args.list_only:
print(f"prompts_json: {cfg_path}")
print(f"tier: {args.tier if not extra else 'custom'}")
print(f"count: {len(prompts)}")
for i, pr in enumerate(prompts, 1):
print(f" {i:02d}. {pr}")
return 0
try:
image_path = resolve_image(args.image, cfg)
except FileNotFoundError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 2
if args.backend == "transformers" and "sam3.1" in args.model_id:
print(
"ERROR: facebook/sam3.1 requires backend='meta'.",
file=sys.stderr,
)
return 2
tier_tag = "custom" if extra else args.tier
out_dir = (
args.output_dir.resolve()
if args.output_dir
else (ROOT / "output" / f"{image_path.stem}_{tier_tag}").resolve()
)
out_dir.mkdir(parents=True, exist_ok=True)
skip_empty = not args.keep_empty
print("=== SamGeo3 multi-prompt segmentation ===")
print(f"image: {image_path}")
print(f"prompts_json:{cfg_path}")
print(f"tier: {tier_tag}")
print(f"n_prompts: {len(prompts)}")
print(f"model_id: {args.model_id}")
print(f"backend: {args.backend}")
print(f"confidence: {args.confidence}")
print(f"output_dir: {out_dir}")
print("prompts:")
for i, pr in enumerate(prompts, 1):
print(f" {i:02d}. {pr}")
import numpy as np
import torch
from samgeo import SamGeo3
if args.device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
else:
device = args.device
print(f"\ndevice: {device} | cuda={torch.cuda.is_available()}")
if device == "cuda" and not torch.cuda.is_available():
print("ERROR: --device cuda but CUDA is not available", file=sys.stderr)
return 1
init_kwargs: dict[str, Any] = dict(
backend=args.backend,
model_id=args.model_id,
device=device,
confidence_threshold=args.confidence,
mask_threshold=args.mask_threshold,
resolution=args.resolution,
enable_segmentation=True,
enable_inst_interactivity=False,
)
if args.checkpoint and os.path.isfile(args.checkpoint):
init_kwargs["checkpoint_path"] = args.checkpoint
init_kwargs["load_from_HF"] = False
print(f"checkpoint: {args.checkpoint}")
t0 = time.perf_counter()
print("\nLoading model once...")
sam = SamGeo3(**init_kwargs)
print("set_image once...")
sam.set_image(str(image_path))
t_load = time.perf_counter() - t0
print(f"load+set_image: {t_load:.1f}s\n")
results: list[dict[str, Any]] = []
stem = image_path.stem
for i, prompt in enumerate(prompts, 1):
t1 = time.perf_counter()
print(f"[{i}/{len(prompts)}] generate_masks({prompt!r})...")
gen_kwargs: dict[str, Any] = {"min_size": args.min_size, "quiet": True}
if args.max_size is not None:
gen_kwargs["max_size"] = args.max_size
try:
sam.generate_masks(prompt, **gen_kwargs)
except Exception as e:
print(f" FAIL: {e}")
results.append(
{
"prompt": prompt,
"n_objects": 0,
"error": str(e),
"elapsed_s": round(time.perf_counter() - t1, 3),
}
)
continue
n = len(sam.masks) if getattr(sam, "masks", None) is not None else 0
score_vals: list[float] = []
scores = getattr(sam, "scores", None)
if scores is not None and len(scores):
try:
score_vals = [
float(s.item() if hasattr(s, "item") else s) for s in scores
]
except Exception:
pass
rec: dict[str, Any] = {
"prompt": prompt,
"n_objects": n,
"scores_min": min(score_vals) if score_vals else None,
"scores_max": max(score_vals) if score_vals else None,
"scores_mean": float(np.mean(score_vals)) if score_vals else None,
"elapsed_s": round(time.perf_counter() - t1, 3),
"mask": None,
"ann": None,
"scores_npy": None,
}
if n == 0:
print(f" -> 0 objects ({rec['elapsed_s']}s)")
results.append(rec)
continue
print(
f" -> {n} objects | score "
f"{rec['scores_min']:.3f}~{rec['scores_max']:.3f} "
f"({rec['elapsed_s']}s)"
)
if skip_empty and n == 0:
results.append(rec)
continue
tag = safe_name(prompt)
mask_path = out_dir / f"{stem}_{tag}_mask.png"
ann_path = out_dir / f"{stem}_{tag}_ann.png"
scores_path = out_dir / f"{stem}_{tag}_scores.npy"
try:
sam.save_masks(str(mask_path), unique=True)
rec["mask"] = str(mask_path)
except Exception as e:
print(f" [WARN] save_masks: {e}")
if score_vals:
np.save(str(scores_path), np.array(score_vals, dtype=np.float32))
rec["scores_npy"] = str(scores_path)
if not args.no_viz:
try:
sam.show_anns(output=str(ann_path))
rec["ann"] = str(ann_path)
except Exception as e:
print(f" [WARN] show_anns: {e}")
results.append(rec)
summary = {
"image": str(image_path),
"prompts_json": str(cfg_path),
"tier": tier_tag,
"model_id": args.model_id,
"backend": args.backend,
"confidence": args.confidence,
"min_size": args.min_size,
"device": device,
"load_set_image_s": round(t_load, 3),
"total_s": round(time.perf_counter() - t0, 3),
"n_prompts": len(prompts),
"n_with_objects": sum(1 for r in results if r.get("n_objects", 0) > 0),
"results": results,
"sources": cfg.get("sources"),
"scene_notes_ko": cfg.get("scene_notes_ko"),
}
summary_path = out_dir / "summary.json"
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print("\n=== summary ===")
print(f"with objects: {summary['n_with_objects']}/{summary['n_prompts']}")
print(f"total time: {summary['total_s']}s")
print(f"summary: {summary_path}")
for r in results:
n = r.get("n_objects", 0)
mark = "OK" if n else "--"
print(f" [{mark}] {r['prompt']!r:30s} n={n}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+189
View File
@@ -0,0 +1,189 @@
"""Single-image text-prompt segmentation with SamGeo3 (meta backend).
Usage (from project root, with venv active):
python scripts/text_segment.py
python scripts/text_segment.py --image data/test_image.jpg --prompt person
python scripts/text_segment.py --model-id facebook/sam3.1 --confidence 0.4
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
# Project root = parent of scripts/
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DATA = ROOT / "data"
DEFAULT_OUTPUT = ROOT / "output"
SAMPLE_URL = (
"https://raw.githubusercontent.com/facebookresearch/sam3/"
"refs/heads/main/assets/images/test_image.jpg"
)
def ensure_sample_image(path: Path) -> Path:
if path.is_file():
return path
path.parent.mkdir(parents=True, exist_ok=True)
print(f"Downloading sample image -> {path}")
try:
from samgeo import download_file
download_file(SAMPLE_URL, str(path))
except Exception:
import urllib.request
urllib.request.urlretrieve(SAMPLE_URL, str(path))
if not path.is_file():
raise FileNotFoundError(f"failed to obtain image: {path}")
return path
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="SamGeo3 text segmentation smoke test")
p.add_argument("--image", type=Path, default=DEFAULT_DATA / "test_image.jpg")
p.add_argument("--prompt", default="person", help="Text prompt for grounding")
p.add_argument("--model-id", default="facebook/sam3.1")
p.add_argument("--backend", default="meta", choices=["meta", "transformers"])
# sam3.1 meta scores on this sample often peak ~0.30.4; 0.5 can yield zero masks
p.add_argument("--confidence", type=float, default=0.3)
p.add_argument("--mask-threshold", type=float, default=0.5)
p.add_argument("--resolution", type=int, default=1008)
p.add_argument("--min-size", type=int, default=0, help="Filter tiny masks (pixels)")
p.add_argument("--max-size", type=int, default=None)
p.add_argument("--device", default=None, help="cuda | cpu (default: auto)")
p.add_argument(
"--checkpoint",
default=os.environ.get("SAM3_CHECKPOINT_PATH"),
help="Local .pt path (or set SAM3_CHECKPOINT_PATH)",
)
p.add_argument(
"--output-dir",
type=Path,
default=DEFAULT_OUTPUT,
help="Directory for mask / annotation outputs",
)
p.add_argument(
"--no-viz",
action="store_true",
help="Skip matplotlib annotation PNG (mask file still saved)",
)
return p.parse_args()
def main() -> int:
args = parse_args()
if args.backend == "transformers" and "sam3.1" in args.model_id:
print(
"ERROR: facebook/sam3.1 requires backend='meta'. "
"Use model-id facebook/sam3 for transformers.",
file=sys.stderr,
)
return 2
image_path = ensure_sample_image(args.image.resolve())
out_dir = args.output_dir.resolve()
out_dir.mkdir(parents=True, exist_ok=True)
stem = image_path.stem
safe_prompt = "".join(c if c.isalnum() or c in "-_" else "_" for c in args.prompt)
mask_path = out_dir / f"{stem}_{safe_prompt}_mask.png"
ann_path = out_dir / f"{stem}_{safe_prompt}_ann.png"
scores_path = out_dir / f"{stem}_{safe_prompt}_scores.npy"
print("=== SamGeo3 text segmentation ===")
print(f"image: {image_path}")
print(f"prompt: {args.prompt}")
print(f"model_id: {args.model_id}")
print(f"backend: {args.backend}")
print(f"output_dir: {out_dir}")
import torch
from samgeo import SamGeo3
if args.device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
else:
device = args.device
print(f"device: {device} | cuda={torch.cuda.is_available()}")
if device == "cuda" and not torch.cuda.is_available():
print("ERROR: --device cuda but CUDA is not available", file=sys.stderr)
return 1
init_kwargs = dict(
backend=args.backend,
model_id=args.model_id,
device=device,
confidence_threshold=args.confidence,
mask_threshold=args.mask_threshold,
resolution=args.resolution,
enable_segmentation=True,
enable_inst_interactivity=False,
)
if args.checkpoint and os.path.isfile(args.checkpoint):
init_kwargs["checkpoint_path"] = args.checkpoint
init_kwargs["load_from_HF"] = False
print(f"checkpoint: {args.checkpoint}")
print("\nLoading model...")
sam = SamGeo3(**init_kwargs)
print("set_image...")
sam.set_image(str(image_path))
print(f'generate_masks("{args.prompt}")...')
gen_kwargs = {"min_size": args.min_size}
if args.max_size is not None:
gen_kwargs["max_size"] = args.max_size
sam.generate_masks(args.prompt, **gen_kwargs)
n = len(sam.masks) if getattr(sam, "masks", None) is not None else 0
if n == 0:
print("No masks found. Try another prompt or lower --confidence.")
return 0
scores = getattr(sam, "scores", None)
if scores is not None and len(scores):
try:
vals = [float(s.item() if hasattr(s, "item") else s) for s in scores]
print(f"scores (n={len(vals)}): min={min(vals):.3f} max={max(vals):.3f}")
except Exception:
pass
print(f"Saving masks -> {mask_path}")
# PNG cannot store float score maps; save mask first, scores as .npy
sam.save_masks(str(mask_path), unique=True)
scores = getattr(sam, "scores", None)
if scores is not None and len(scores):
import numpy as np
score_vals = np.array(
[float(s.item() if hasattr(s, "item") else s) for s in scores],
dtype=np.float32,
)
np.save(str(scores_path), score_vals)
print(f"Saved per-object scores -> {scores_path}")
if not args.no_viz:
try:
print(f"Saving annotations -> {ann_path}")
sam.show_anns(output=str(ann_path))
except Exception as e:
print(f"[WARN] show_anns failed: {e}")
print(f"\nDone. Found {n} object(s).")
print(f" mask: {mask_path}")
if scores_path.is_file():
print(f" scores: {scores_path}")
if ann_path.is_file():
print(f" ann: {ann_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())