Files
railway-client/tools/render_everything_by_label.py
minsung 4c15d5ff5d 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>
2026-06-02 10:11:52 +09:00

147 lines
5.4 KiB
Python

"""everything.json을 레이블별로 분리해 bbox를 원본 이미지에 표시.
사용:
python tools/render_everything_by_label.py \
--image data/역사이미지/slope/DJI_20260306113838_0004_tophalf.jpg \
--json output/raamen/DJI_20260306113838_0004_tophalf_everything.json \
--output output/everything_by_label \
[--scale 0.3] \
[--label small_dark_object_on_ballast ...] \
[--rail-offset 200] # railroad_track/railway_rail bbox + N픽셀 안쪽 필터
"""
import argparse
import json
import re
import cv2
import numpy as np
from pathlib import Path
RAIL_LABELS = {"railroad track", "railway rail"}
def safe_name(label: str) -> str:
return re.sub(r"[^\w]", "_", label)
def build_rail_zone(segments: list, offset: int, img_h: int, img_w: int):
"""railroad_track/railway_rail 세그먼트 합산 bbox + offset → (x1,y1,x2,y2)."""
xs1, ys1, xs2, ys2 = [], [], [], []
for seg in segments:
if seg.get("label", "").strip().lower() in RAIL_LABELS:
bx1, by1, bx2, by2 = seg["bbox"]
xs1.append(bx1); ys1.append(by1)
xs2.append(bx2); ys2.append(by2)
if not xs1:
return None
return (
max(0, int(min(xs1)) - offset),
max(0, int(min(ys1)) - offset),
min(img_w, int(max(xs2)) + offset),
min(img_h, int(max(ys2)) + offset),
)
def in_zone(seg_bbox, zone):
"""세그먼트 bbox 중심이 zone 안에 있으면 True."""
bx1, by1, bx2, by2 = seg_bbox
cx, cy = (bx1 + bx2) / 2, (by1 + by2) / 2
zx1, zy1, zx2, zy2 = zone
return zx1 <= cx <= zx2 and zy1 <= cy <= zy2
def render_label(img_full, segments, label: str, output_path: Path, scale: float,
zone=None):
h, w = img_full.shape[:2]
canvas = cv2.resize(img_full, (int(w * scale), int(h * scale)),
interpolation=cv2.INTER_AREA)
color = (0, 80, 255)
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = max(0.4, scale * 2.0)
thickness = max(1, int(scale * 5))
if zone:
zx1, zy1, zx2, zy2 = [int(v * scale) for v in zone]
cv2.rectangle(canvas, (zx1, zy1), (zx2, zy2), (0, 255, 0), 2)
for seg in segments:
if seg.get("points"):
pts = np.array([[int(x * scale), int(y * scale)] for x, y in seg["points"]], dtype=np.int32)
overlay = canvas.copy()
cv2.fillPoly(overlay, [pts], color)
cv2.addWeighted(overlay, 0.25, canvas, 0.75, 0, canvas)
cv2.polylines(canvas, [pts], True, color, thickness)
else:
x1, y1, x2, y2 = [int(v * scale) for v in seg["bbox"]]
cv2.rectangle(canvas, (x1, y1), (x2, y2), color, thickness)
header = f"{label} [{len(segments)}]"
cv2.putText(canvas, header, (10, 40), font, font_scale * 1.2,
(0, 220, 0), max(1, int(scale * 3)) + 1)
output_path.parent.mkdir(parents=True, exist_ok=True)
ret, buf = cv2.imencode(".jpg", canvas, [cv2.IMWRITE_JPEG_QUALITY, 88])
if not ret:
raise RuntimeError("JPEG 인코딩 실패")
output_path.write_bytes(buf.tobytes())
print(f" {label:40s} {len(segments):5d}개 → {output_path.name}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--image", required=True, type=Path)
ap.add_argument("--json", required=True, type=Path)
ap.add_argument("--output", required=True, type=Path)
ap.add_argument("--scale", type=float, default=0.25)
ap.add_argument("--label", nargs="*", default=None)
ap.add_argument("--rail-offset", type=int, default=0,
help="railroad_track/railway_rail 합산 bbox + N픽셀 zone 필터 (0=비활성)")
ap.add_argument("--zone", type=int, nargs=4, metavar=("X1","Y1","X2","Y2"), default=None,
help="수동 zone 지정 (--rail-offset보다 우선)")
args = ap.parse_args()
buf = np.fromfile(str(args.image), dtype=np.uint8)
img = cv2.imdecode(buf, cv2.IMREAD_COLOR)
if img is None:
raise FileNotFoundError(f"이미지 읽기 실패: {args.image}")
img_h, img_w = img.shape[:2]
with open(args.json, encoding="utf-8") as f:
data = json.load(f)
groups: dict[str, list] = {}
for seg in data["segments"]:
groups.setdefault(seg["label"], []).append(seg)
zone = None
if args.zone:
zone = tuple(args.zone)
print(f"수동 zone: {zone}")
elif args.rail_offset > 0:
zone = build_rail_zone(data["segments"], args.rail_offset, img_h, img_w)
if zone:
print(f"레일 zone (offset={args.rail_offset}px): {zone}")
else:
print("경고: railroad_track/railway_rail 세그먼트 없음 → zone 필터 미적용")
target_labels = args.label if args.label else sorted(groups)
print(f"원본: {img_w}x{img_h} scale={args.scale}")
print(f"레이블 {len(target_labels)}개 렌더링 → {args.output}/")
for lbl in target_labels:
if lbl not in groups:
print(f" [skip] {lbl} - no segments")
continue
segs = sorted(groups[lbl], key=lambda s: s["score"], reverse=True)
if zone:
segs = [s for s in segs if in_zone(s["bbox"], zone)]
fname = f"{safe_name(lbl)}.jpg"
render_label(img, segs, lbl, args.output / fname, args.scale, zone=zone)
print("완료.")
if __name__ == "__main__":
main()