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:
146
tools/render_everything_by_label.py
Normal file
146
tools/render_everything_by_label.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user