feat: SAM3.1 다중 프롬프트 배치 검출 및 라벨 병합 후처리

sam3_multi_prompt.py: 프롬프트 N개를 forward 1회에 배치 처리한다.
서버(프롬프트당 forward 1회) 대비 forward 횟수가 1/N로 줄고, 이미지
임베딩과 텍스트 인코딩을 타일 전체에서 재사용한다. RTX 3060 12GB
기준 배치 16이 최적(32 이상은 VRAM 압박으로 4배 이상 느려짐).

타일보다 큰 대상(숲, 도로, 주차장)은 타일 경계에서 잘려 사각형
마스크가 되므로 --wide-prompts 로 지정해 이미지 전체를 한 장으로
처리한다.

merge_labels.py: 검출 결과 후처리. 같은 병합 그룹에 속한 라벨끼리
외곽선이 gap px 이내로 인접하면 하나로 합치고 대표 라벨을 붙인다
(building + building rooftop + blue roof -> building). 병합 전
claim 규칙으로 소유권을 재배정해, 차량 위에 잡힌 *roof 폴리곤이
건물이 아니라 차량에 합쳐지도록 한다.

make_viewer.py: 라벨별 on/off 가능한 단독 HTML 뷰어 생성.
프롬프트 파일의 그룹 주석 기준으로 라벨을 묶어 나열하고, 줌/팬과
선택 상태 저장을 지원한다.

cut_tiles.py: R{행}C{열} 격자 오버레이 + 원본 배율 타일 저장.

전역 규칙 반영:
- CUDA 사용 불가 시 CPU로 폴백하지 않고 즉시 에러
- set_per_process_memory_fraction 으로 드라이버의 시스템 메모리
  폴백(10배 이상 느려짐) 전에 OOM 발생
- 체크포인트/BPE 경로 하드코딩 제거. CLI 인자 -> 환경변수 ->
  sam31server 설정파일 순으로 해결

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
minsung
2026-08-20 10:05:23 +09:00
parent 4c15d5ff5d
commit 4e5173522d
11 changed files with 1287 additions and 46 deletions
+75
View File
@@ -0,0 +1,75 @@
"""
이미지를 cols×rows 타일로 잘라 파일로 저장하고, R{행}C{열} 라벨을 찍은 격자 오버레이도 생성.
사용법:
python tools/cut_tiles.py --input <이미지> --cols 9 --rows 6
출력:
output/tiles/<이미지명>/R1C1.jpg ... (원본 배율)
output/tiles/<이미지명>_grid.jpg (격자 오버레이, 최대 4096px)
"""
import argparse
from pathlib import Path
import cv2
import numpy as np
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--input", required=True)
ap.add_argument("--outdir", default="output/tiles")
ap.add_argument("--cols", type=int, default=9)
ap.add_argument("--rows", type=int, default=6)
args = ap.parse_args()
img_path = Path(args.input)
buf = np.fromfile(str(img_path), dtype=np.uint8)
img = cv2.imdecode(buf, cv2.IMREAD_COLOR)
if img is None:
print(f"이미지 로드 실패: {img_path}")
return
H, W = img.shape[:2]
base_w = W / args.cols
base_h = H / args.rows
print(f"이미지 {W}×{H}{args.cols}×{args.rows} 타일 {base_w:.0f}×{base_h:.0f}")
tile_dir = Path(args.outdir) / img_path.stem
tile_dir.mkdir(parents=True, exist_ok=True)
vis = img.copy()
font_scale = base_h / 200.0
thickness = max(2, int(font_scale * 2))
for r in range(args.rows):
for c in range(args.cols):
x0, x1 = int(c * base_w), int((c + 1) * base_w)
y0, y1 = int(r * base_h), int((r + 1) * base_h)
name = f"R{r+1}C{c+1}"
cv2.imencode(".jpg", img[y0:y1, x0:x1],
[cv2.IMWRITE_JPEG_QUALITY, 95])[1].tofile(
str(tile_dir / f"{name}.jpg"))
cv2.rectangle(vis, (x0, y0), (x1, y1), (0, 200, 255), 4)
(tw, th), _ = cv2.getTextSize(name, cv2.FONT_HERSHEY_SIMPLEX,
font_scale, thickness)
tx, ty = x0 + 12, y0 + th + 12
cv2.rectangle(vis, (tx - 6, ty - th - 6), (tx + tw + 6, ty + 8),
(0, 0, 0), -1)
cv2.putText(vis, name, (tx, ty), cv2.FONT_HERSHEY_SIMPLEX,
font_scale, (0, 200, 255), thickness, cv2.LINE_AA)
h, w = vis.shape[:2]
if max(h, w) > 4096:
s = 4096 / max(h, w)
vis = cv2.resize(vis, (int(w * s), int(h * s)))
grid_path = Path(args.outdir) / f"{img_path.stem}_grid.jpg"
cv2.imencode(".jpg", vis, [cv2.IMWRITE_JPEG_QUALITY, 92])[1].tofile(str(grid_path))
print(f"타일 {args.cols * args.rows}개 → {tile_dir}")
print(f"격자 오버레이 → {grid_path}")
if __name__ == "__main__":
main()