프로젝트 분리 이동

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-05-20 14:28:27 +09:00
commit ccba1266b5
24 changed files with 7900 additions and 0 deletions

234
tools/rail_to_dxf.py Normal file
View File

@@ -0,0 +1,234 @@
"""
rail_to_dxf.py
==============
X-AnyLabeling JSON 어노테이션에서 레일 중심선을 추출하여 Rhino용 DXF로 저장.
사용법:
python tools/rail_to_dxf.py <annotation.json> [output.dxf]
예시:
python tools/rail_to_dxf.py images/rail.json
python tools/rail_to_dxf.py images/rail.json output/rail_centerline.dxf
라벨 이름 (X-AnyLabeling에서 Finish 후 입력한 이름):
rail, railway_track, track, AUTOLABEL_OBJECT 자동 인식
다른 이름이면 스크립트 하단 TARGET_LABELS 수정
Rhino에서 사용:
1. DXF Import
2. RAIL_CENTERLINE 레이어 선택
3. Sweep2 또는 Rail Sweep으로 레일 단면 적용
"""
import json
import sys
import numpy as np
import cv2
from pathlib import Path
from skimage.morphology import skeletonize
# ─── 설정 ─────────────────────────────────────────────────────────────────────
TARGET_LABELS = [
"rail",
"railline",
"railway_track",
"track",
"레일",
"철로",
"AUTOLABEL_OBJECT",
]
# line/linestrip 타입은 스켈레톤 불필요 — 직접 DXF 출력
LINE_SHAPE_TYPES = {"line", "linestrip", "lines"}
SMOOTH_WINDOW = 15 # 중심선 스무딩 강도 (클수록 부드러움, 0=비활성)
DOWNSAMPLE_STEP = 8 # Rhino 폴리라인 포인트 간격 (클수록 포인트 수 감소)
DILATION_ITER = 3 # 마스크 팽창 반복 (얇은 레일 마스크 연결 보완)
# ──────────────────────────────────────────────────────────────────────────────
def polygon_to_mask(points, h, w):
mask = np.zeros((h, w), dtype=np.uint8)
pts = np.array([[int(p[0]), int(p[1])] for p in points], dtype=np.int32)
cv2.fillPoly(mask, [pts], 1)
return mask
def extract_skeleton(mask):
kernel = np.ones((3, 3), np.uint8)
dilated = cv2.dilate(mask, kernel, iterations=DILATION_ITER)
return skeletonize(dilated > 0).astype(np.uint8)
def order_skeleton(skeleton):
"""스켈레톤 픽셀을 끝점에서 시작해 순서대로 연결."""
ys, xs = np.where(skeleton > 0)
if len(xs) == 0:
return []
pt_set = set(zip(xs.tolist(), ys.tolist()))
def neighbors(pt):
x, y = pt
return [(x+dx, y+dy)
for dx in (-1,0,1) for dy in (-1,0,1)
if not (dx==0 and dy==0) and (x+dx, y+dy) in pt_set]
# 끝점(이웃 1개) 찾기 → 없으면 임의 시작
endpoints = [p for p in pt_set if len(neighbors(p)) == 1]
start = endpoints[0] if endpoints else next(iter(pt_set))
ordered, visited = [start], {start}
current = start
while True:
nbs = [n for n in neighbors(current) if n not in visited]
if not nbs:
break
# 가장 직선에 가까운 방향 우선 선택
if len(ordered) >= 2:
dx = current[0] - ordered[-2][0]
dy = current[1] - ordered[-2][1]
nbs.sort(key=lambda n: -((n[0]-current[0])*dx + (n[1]-current[1])*dy))
current = nbs[0]
visited.add(current)
ordered.append(current)
return ordered
def smooth_polyline(points, window):
if window < 3 or len(points) < window:
return points
pts = np.array(points, dtype=float)
half = window // 2
out = pts.copy()
for i in range(half, len(pts) - half):
out[i] = pts[i-half:i+half+1].mean(axis=0)
return out.tolist()
def downsample(points, step):
if step <= 1 or len(points) <= step:
return points
sampled = points[::step]
if list(sampled[-1]) != list(points[-1]):
sampled = list(sampled) + [points[-1]]
return sampled
def to_dxf_coords(points, flip_y=True):
"""이미지 좌표(Y↓) → DXF 좌표(Y↑)"""
if flip_y:
return [(float(p[0]), float(-p[1])) for p in points]
return [(float(p[0]), float(p[1])) for p in points]
def process(json_path: str, dxf_path: str):
import ezdxf
with open(json_path, encoding="utf-8") as f:
data = json.load(f)
H = data.get("imageHeight", 1000)
W = data.get("imageWidth", 1000)
print(f"[이미지] {W} × {H} px")
shapes = data.get("shapes", [])
targets = [s for s in shapes if s.get("label") in TARGET_LABELS]
if not targets:
print("⚠ TARGET_LABELS 일치 없음 → 모든 shape 사용")
targets = shapes
type_counts = {}
for s in targets:
t = s.get("shape_type", "unknown")
type_counts[t] = type_counts.get(t, 0) + 1
print(f"[처리] {len(targets)}개: {type_counts}")
doc = ezdxf.new("R2010")
msp = doc.modelspace()
doc.layers.add("RAIL_CENTERLINE", color=1) # 빨강 — Sweep 경로
doc.layers.add("RAIL_POLYGON", color=3) # 초록 — 원본 마스크 윤곽
for i, shape in enumerate(targets):
label = shape.get("label", f"shape_{i}")
pts = shape.get("points", [])
shape_type = shape.get("shape_type", "polygon")
print(f"\n [{i+1}] label={label!r} type={shape_type!r} pts={len(pts)}")
if len(pts) < 2:
print(" → 포인트 부족, 건너뜀")
continue
# 중복 끝점 제거 (Polygon 도구로 그린 선: 마지막 점이 앞 점과 거의 동일)
if len(pts) >= 3:
dedup = [pts[0]]
for p in pts[1:]:
if abs(p[0]-dedup[-1][0]) > 2 or abs(p[1]-dedup[-1][1]) > 2:
dedup.append(p)
if len(dedup) < len(pts):
print(f" 중복점 제거: {len(pts)}pt → {len(dedup)}pt")
pts = dedup
# ── LINE 타입: 스켈레톤 없이 직접 DXF 출력 ──────
if shape_type in LINE_SHAPE_TYPES or len(pts) == 2:
cl_dxf = to_dxf_coords(pts)
msp.add_lwpolyline(cl_dxf, dxfattribs={"layer": "RAIL_CENTERLINE"})
print(f" → 라인 직접 출력 ({len(pts)}pt)")
continue
# ── POLYGON 타입: 마스크 → 스켈레톤 → 중심선 ────
if len(pts) < 3:
print(" → 폴리곤 포인트 부족, 건너뜀")
continue
poly_dxf = to_dxf_coords(pts)
poly_dxf.append(poly_dxf[0])
msp.add_lwpolyline(poly_dxf, dxfattribs={"layer": "RAIL_POLYGON"})
mask = polygon_to_mask(pts, H, W)
area = int(mask.sum())
print(f" 마스크 면적: {area} px²")
if area < 20:
print(" → 면적 너무 작음, 건너뜀")
continue
skel = extract_skeleton(mask)
skel_n = int(skel.sum())
print(f" 스켈레톤 픽셀: {skel_n}")
if skel_n < 2:
print(" → 스켈레톤 생성 실패, 건너뜀")
continue
ordered = order_skeleton(skel)
smoothed = smooth_polyline(ordered, SMOOTH_WINDOW)
final_pts = downsample(smoothed, DOWNSAMPLE_STEP)
print(f" 중심선 포인트: {len(final_pts)}")
if len(final_pts) < 2:
continue
cl_dxf = to_dxf_coords(final_pts)
msp.add_lwpolyline(cl_dxf, dxfattribs={"layer": "RAIL_CENTERLINE"})
Path(dxf_path).parent.mkdir(parents=True, exist_ok=True)
doc.saveas(dxf_path)
print(f"\n[완료] DXF 저장: {dxf_path}")
print(" 레이어: RAIL_CENTERLINE(빨강) = Sweep 경로")
print(" RAIL_POLYGON(초록) = 원본 마스크")
print("\nRhino 사용법:")
print(" 1. File → Import → DXF 선택")
print(" 2. RAIL_CENTERLINE 레이어 선택")
print(" 3. Rail 단면 커브 그리기 (레일 단면 약 60x60mm)")
print(" 4. Sweep1 또는 Sweep2 명령으로 단면 × 중심선 → 3D 레일 생성")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("사용법: python tools/rail_to_dxf.py <annotation.json> [output.dxf]")
sys.exit(1)
json_file = sys.argv[1]
dxf_file = sys.argv[2] if len(sys.argv) >= 3 else str(Path(json_file).with_suffix(".dxf"))
process(json_file, dxf_file)