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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user