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

@@ -161,104 +161,6 @@ def nms_shapes(shapes: list, iou_thresh: float = 0.4) -> list:
return _nms_core(shapes, iou_thresh)
def _poly_orient(points: list, H: int, W: int) -> str: # post_merge_poles.py에서도 사용
"""폴리곤 장축 방향 판별 (render_skeleton_overlay.py 동일 로직).
V: 장축이 이미지 중심에서 방사형 방향과 정렬 (cos_sim > 0.7) → 세로 기둥
H: 장축이 radial 직교 방향 → 수평 빔
?: aspect ratio < 1.3 으로 판별 불가
"""
pts = np.array(points, dtype=np.float32)
rect = cv2.minAreaRect(pts)
(rx, ry), (rw, rh), angle = rect
if min(rw, rh) < 1:
return '?'
ar = max(rw, rh) / min(rw, rh)
if ar < 1.3:
return '?'
long_angle_deg = angle if rw >= rh else angle + 90
lx = float(np.cos(np.radians(long_angle_deg)))
ly = float(np.sin(np.radians(long_angle_deg)))
img_cx, img_cy = W / 2.0, H / 2.0
rdx, rdy = rx - img_cx, ry - img_cy
radial_norm = (rdx ** 2 + rdy ** 2) ** 0.5
if radial_norm < 1:
return '?'
rdx, rdy = rdx / radial_norm, rdy / radial_norm
cos_sim = abs(lx * rdx + ly * rdy)
return 'V' if cos_sim > 0.7 else 'H'
def merge_nonramen_poles(shapes: list, H: int, W: int,
x_overlap_thresh: float = 0.30,
y_gap_thresh: int = 150) -> list:
"""타일 경계 분할된 전철주 병합 — V+V 조합만 허용.
_poly_orient로 각 폴리곤 V/H 분류.
두 폴리곤 모두 V(세로 기둥)이고 공간 기준 충족 시만 병합.
H(수평 빔) 포함 쌍 = 라멘 관련 조각 → 병합 건너뜀.
"""
if len(shapes) <= 1:
return shapes
orients = [_poly_orient(s["points"], H, W) for s in shapes]
v_count = sum(1 for o in orients if o == 'V')
h_count = sum(1 for o in orients if o == 'H')
print(f" [orient] V={v_count}, H={h_count}, ?={len(orients)-v_count-h_count}")
def get_bbox(s):
xs = [p[0] for p in s["points"]]; ys = [p[1] for p in s["points"]]
return min(xs), min(ys), max(xs), max(ys)
def x_overlap_ratio(b1, b2):
ox = min(b1[2], b2[2]) - max(b1[0], b2[0])
ux = max(b1[2], b2[2]) - min(b1[0], b2[0])
return ox / ux if ux > 0 else 0.0
def y_gap(b1, b2):
return max(0.0, max(b1[1], b2[1]) - min(b1[3], b2[3]))
def merge_two(s1, s2):
mask = np.zeros((H, W), dtype=np.uint8)
for s in (s1, s2):
cv2.fillPoly(mask, [np.array(s["points"], dtype=np.int32)], 255)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return s1
c = max(contours, key=cv2.contourArea)
eps = 0.002 * cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, eps, True)
merged = dict(s1)
merged["points"] = [[float(p[0][0]), float(p[0][1])] for p in approx]
merged["score"] = max(float(s1.get("score", 0)), float(s2.get("score", 0)))
return merged
merged_flags = [False] * len(shapes)
result = []
merged_count = 0
for i in range(len(shapes)):
if merged_flags[i]:
continue
cur = shapes[i]
cur_ori = orients[i]
cb = get_bbox(cur)
for j in range(i + 1, len(shapes)):
if merged_flags[j]:
continue
if cur_ori != 'V' or orients[j] != 'V':
continue # 둘 다 V가 아니면 병합 안 함
jb = get_bbox(shapes[j])
if x_overlap_ratio(cb, jb) >= x_overlap_thresh and y_gap(cb, jb) <= y_gap_thresh:
cur = merge_two(cur, shapes[j])
cur_ori = 'V'
cb = get_bbox(cur)
merged_flags[j] = True
merged_count += 1
result.append(cur)
print(f" [merge] 병합={merged_count}")
return result
def cross_class_nms(buckets: list, categories: list, iou_thresh: float) -> list:
"""클래스 간 NMS: 동일 영역에 다른 클래스가 중복 검출될 때 우선순위 높은 쪽 보존.
@@ -508,9 +410,9 @@ def main():
else:
tile_tag = args.tiles.replace(",", "_").replace("-", "to")
cat_tag = Path(args.categories).stem if args.categories else "default"
out_dir = Path("output") / "detect" / img_path.stem
out_dir = Path("output") / "detect"
out_dir.mkdir(parents=True, exist_ok=True)
base_name = f"tiles{tile_tag}_{cat_tag}"
base_name = f"{img_path.stem}_tiles{tile_tag}_{cat_tag}"
n = 1
while True:
out_path = out_dir / f"{base_name}_{n:03d}.jpg"