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

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

Closes #1

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

View File

@@ -53,7 +53,12 @@ DISCOVERY_PROMPT = (
"road, asphalt, pavement, "
"slope, embankment, retaining wall, "
"noise barrier, sound wall, "
"signal, sign board"
"signal, sign board, "
"small dark object on ballast, small dark object on railway, "
"small square metal box on ground, control box on ballast, "
"gray square lid on gravel, flat metal cover on ground, "
"small bright object on ballast, small white box on ballast, "
"small gray box on ground, bright square object on gravel"
)
@@ -72,7 +77,7 @@ def sam3_everything(tile_bgr: np.ndarray, conf: float, prompt: str = DISCOVERY_P
},
}
try:
r = requests.post(f"{SAM3_SERVER}/v1/predict", json=payload, timeout=120)
r = requests.post(f"{SAM3_SERVER}/v1/predict", json=payload, timeout=300)
r.raise_for_status()
resp = r.json()
if not resp.get("success"):
@@ -119,14 +124,23 @@ def nms_shapes(shapes: list, iou_thresh: float = 0.4) -> list:
# ── 타일 분할 + 병렬 검출 ─────────────────────────────────────────────────────
def detect_everything_tiled(image_bgr, cols, rows, overlap, conf, workers, prompt):
def detect_everything_tiled(image_bgr, cols, rows, overlap, conf, workers, prompt,
zone=None):
"""zone=(x1,y1,x2,y2) 지정 시 겹치는 타일만 처리."""
H, W = image_bgr.shape[:2]
base_w = W / cols
base_h = H / rows
pad_x = int(base_w * overlap)
pad_y = int(base_h * overlap)
def overlaps_zone(tx0, ty0, tx1, ty1):
if zone is None:
return True
zx1, zy1, zx2, zy2 = zone
return tx0 < zx2 and tx1 > zx1 and ty0 < zy2 and ty1 > zy1
tiles = []
skipped = 0
for r in range(rows):
for c in range(cols):
idx = r * cols + c + 1
@@ -134,9 +148,14 @@ def detect_everything_tiled(image_bgr, cols, rows, overlap, conf, workers, promp
x1 = min(W, int((c + 1) * base_w) + pad_x)
y0 = max(0, int(r * base_h) - pad_y)
y1 = min(H, int((r + 1) * base_h) + pad_y)
tiles.append((idx, x0, y0, x1, y1))
if overlaps_zone(x0, y0, x1, y1):
tiles.append((idx, x0, y0, x1, y1))
else:
skipped += 1
total = len(tiles)
total = len(tiles)
if skipped:
print(f"zone 필터: {skipped}타일 스킵, {total}타일 처리")
done = [0]
all_shapes = []
@@ -227,6 +246,8 @@ def main():
ap.add_argument("--workers", type=int, default=4, help="병렬 스레드 수 (기본 4)")
ap.add_argument("--nms", type=float, default=0.40, help="NMS IoU 임계값 (기본 0.40)")
ap.add_argument("--prompt-extra", default="", help="DISCOVERY_PROMPT 뒤에 추가할 어휘 (콤마 구분)")
ap.add_argument("--zone", type=int, nargs=4, metavar=("X1","Y1","X2","Y2"), default=None,
help="처리 zone 제한 (이 범위와 겹치는 타일만 처리)")
args = ap.parse_args()
prompt = DISCOVERY_PROMPT + (", " + args.prompt_extra.strip(", ") if args.prompt_extra.strip() else "")
@@ -248,10 +269,14 @@ def main():
print(f" · {item.strip()}")
print()
zone = tuple(args.zone) if args.zone else None
if zone:
print(f"zone 제한: x={zone[0]}~{zone[2]} y={zone[1]}~{zone[3]}\n")
t0 = time.time()
shapes = detect_everything_tiled(
image_bgr, args.cols, args.rows, args.overlap,
args.conf, args.workers, prompt
args.conf, args.workers, prompt, zone=zone
)
print(f"검출 {len(shapes)}개 → NMS(iou={args.nms})...")
shapes = nms_shapes(shapes, iou_thresh=args.nms)
@@ -279,7 +304,8 @@ def main():
)),
"segments": [
{"label": s.get("label",""), "score": s.get("score",0),
"bbox": list(_bbox(s["points"]))}
"bbox": list(_bbox(s["points"])),
"points": s["points"]}
for s in shapes
]
}