Owner directive 2026-07-15: extend obstacle recognition to K2 (halls 6-10). - tools/cad/extract_obstacle_seed_k2.py: same 1m-cell raster + row-strip method as K1, plus hall-frame rotation transform (+-11..12deg wings) - 31 measured strips: edge doors, service-room walls, and the full-width midline door strips on H9/H10 = the A/B divider shutter tracks - the 1.0m door buffer keeps the shutter travel line clear (operationally correct) - Layers: ELEVATOR, STAIR, DOOR, gypsum/lightweight-steel/cement-panel walls Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
207 lines
7.5 KiB
Python
207 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""K2(제2전시장 H6~H10) 장애물 실측 시드 생성기 — V64 (hall_obstacle 확장).
|
|
|
|
K1(extract_obstacle_seed.py)과 동일한 1m 셀 래스터·연결성분·행 스트립 분해 방식이되,
|
|
K2 홀 프레임(±11~12° 회전 배치)을 회전 변환해 홀 로컬 좌표로 정규화한다.
|
|
소스: 원본-1층 평면도.dwg → dwg2dxf → k2_slim.py 슬림 추출본(구조 레이어만).
|
|
|
|
사용:
|
|
python tools/cad/extract_obstacle_seed_k2.py <k2_slim.dxf> <out.sql>
|
|
"""
|
|
import io
|
|
import math
|
|
import sys
|
|
from collections import deque
|
|
|
|
import ezdxf
|
|
|
|
MM = 1000.0
|
|
CELL = 1.0
|
|
MIN_AREA = 3.0
|
|
MIN_DIM_DOOR = 0.5
|
|
DOOR_MIN_AREA = 0.5
|
|
|
|
# 홀 프레임(fork 확정, docs/analysis/cad-extraction.md §K2): (회전deg, 중심, u0,u1, v0,v1)
|
|
FRAMES = {
|
|
"H6": (-12, (530, 405), 482, 542, 350.5, 443.5),
|
|
"H7": (0, (0, 0), 439, 529, 215.5, 341.5),
|
|
"H8": (0, (0, 0), 439, 529, 89.5, 215.5),
|
|
"H9": (-11, (245, 155), 196, 295, 95.5, 227.5),
|
|
"H10": (11, (255, 330), 194.5, 293.5, 250, 382),
|
|
}
|
|
|
|
KIND_LAYERS = {
|
|
"stair": ("STAIR", "계단"),
|
|
"elevator": ("ELEVATOR", "ELV", "승강"),
|
|
"door": ("DOOR", "출입구"),
|
|
"wall": ("WALL", "석고보드", "경량철골", "시멘트판넬", "판넬", "조적"),
|
|
}
|
|
|
|
|
|
def kind_of(layer):
|
|
u = layer.upper()
|
|
for kind, keys in KIND_LAYERS.items():
|
|
if any(k.upper() in u for k in keys):
|
|
return kind
|
|
return None
|
|
|
|
|
|
def rot(x, y, deg, c):
|
|
if deg == 0:
|
|
return x, y
|
|
t = math.radians(-deg)
|
|
dx, dy = x - c[0], y - c[1]
|
|
return c[0] + dx * math.cos(t) - dy * math.sin(t), c[1] + dx * math.sin(t) + dy * math.cos(t)
|
|
|
|
|
|
def sample_points(e):
|
|
t = e.dxftype()
|
|
try:
|
|
if t == "LINE":
|
|
a, b = e.dxf.start, e.dxf.end
|
|
ln = math.hypot(b[0] - a[0], b[1] - a[1]) / MM
|
|
n = max(2, int(ln / 0.5))
|
|
return [((a[0] + (b[0] - a[0]) * i / n) / MM, (a[1] + (b[1] - a[1]) * i / n) / MM) for i in range(n + 1)]
|
|
if t == "LWPOLYLINE":
|
|
pts = [(p[0] / MM, p[1] / MM) for p in e.get_points()]
|
|
out = []
|
|
for a, b in zip(pts, pts[1:]):
|
|
ln = math.hypot(b[0] - a[0], b[1] - a[1])
|
|
n = max(1, int(ln / 0.5))
|
|
out += [(a[0] + (b[0] - a[0]) * i / n, a[1] + (b[1] - a[1]) * i / n) for i in range(n + 1)]
|
|
return out
|
|
if t in ("ARC", "CIRCLE"):
|
|
return [(p[0] / MM, p[1] / MM) for p in e.flattening(200)]
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
|
|
def strips_of(cells):
|
|
by_row = {}
|
|
for ci, cj in cells:
|
|
by_row.setdefault(cj, []).append(ci)
|
|
strips = []
|
|
for j in sorted(by_row):
|
|
cols = sorted(by_row[j])
|
|
seg = [cols[0], cols[0]]
|
|
segs = []
|
|
for c in cols[1:]:
|
|
if c == seg[1] + 1:
|
|
seg[1] = c
|
|
else:
|
|
segs.append(tuple(seg))
|
|
seg = [c, c]
|
|
segs.append(tuple(seg))
|
|
for i0, i1 in segs:
|
|
merged = False
|
|
for s in strips:
|
|
if s[1] == j - 1 and s[2] == i0 and s[3] == i1:
|
|
s[1] = j
|
|
merged = True
|
|
break
|
|
if not merged:
|
|
strips.append([j, j, i0, i1])
|
|
return strips
|
|
|
|
|
|
def main():
|
|
src, out_sql = sys.argv[1], sys.argv[2]
|
|
doc = ezdxf.readfile(src)
|
|
msp = doc.modelspace()
|
|
|
|
grids = {}
|
|
dims = {}
|
|
for hall, (deg, c, u0, u1, v0, v1) in FRAMES.items():
|
|
w, h = u1 - u0, v1 - v0
|
|
dims[hall] = (w, h)
|
|
grids[hall] = [[None] * int(w / CELL) for _ in range(int(h / CELL))]
|
|
|
|
for e in msp:
|
|
kind = kind_of(e.dxf.layer)
|
|
if not kind:
|
|
continue
|
|
for x, y in sample_points(e):
|
|
for hall, (deg, c, u0, u1, v0, v1) in FRAMES.items():
|
|
u, v = rot(x, y, deg, c)
|
|
if not (u0 <= u < u1 and v0 <= v < v1):
|
|
continue
|
|
lx, ly = u - u0, v1 - v # y=0 도면 상단
|
|
g = grids[hall]
|
|
ci, cj = int(lx / CELL), min(len(g) - 1, int(ly / CELL))
|
|
cur = g[cj][ci]
|
|
if cur is None or (cur == "wall" and kind != "wall"):
|
|
g[cj][ci] = kind
|
|
|
|
sql = io.StringIO()
|
|
sql.write(
|
|
"-- V64: K2(H6~H10) 벽·계단·엘리베이터·문 장애물 실측 시드 — V63(K1)과 동일 방식 + 홀 프레임 회전 변환.\n"
|
|
"-- 생성기: tools/cad/extract_obstacle_seed_k2.py (결정적 재생성). 근거: docs/analysis/cad-extraction.md §K2.\n\n"
|
|
"DELETE FROM hall_obstacle WHERE hall_id IN ('H6','H7','H8','H9','H10');\n"
|
|
)
|
|
rows = []
|
|
report = []
|
|
for hall, (deg, c, u0, u1, v0, v1) in FRAMES.items():
|
|
g = grids[hall]
|
|
W, H = len(g[0]), len(g)
|
|
hw, hd = dims[hall]
|
|
seen = [[False] * W for _ in range(H)]
|
|
comps = []
|
|
for j in range(H):
|
|
for i in range(W):
|
|
if g[j][i] is None or seen[j][i]:
|
|
continue
|
|
q = deque([(i, j)])
|
|
seen[j][i] = True
|
|
cells = []
|
|
kinds = {}
|
|
while q:
|
|
ci, cj = q.popleft()
|
|
cells.append((ci, cj))
|
|
kinds[g[cj][ci]] = kinds.get(g[cj][ci], 0) + 1
|
|
for ni, nj in ((ci + 1, cj), (ci - 1, cj), (ci, cj + 1), (ci, cj - 1),
|
|
(ci + 1, cj + 1), (ci - 1, cj - 1), (ci + 1, cj - 1), (ci - 1, cj + 1)):
|
|
if 0 <= ni < W and 0 <= nj < H and g[nj][ni] is not None and not seen[nj][ni]:
|
|
seen[nj][ni] = True
|
|
q.append((ni, nj))
|
|
area = len(cells) * CELL * CELL
|
|
kind = ("stair" if kinds.get("stair")
|
|
else "elevator" if kinds.get("elevator")
|
|
else "door" if kinds.get("door")
|
|
else "wall")
|
|
min_area = DOOR_MIN_AREA if kind == "door" else MIN_AREA
|
|
if area < min_area:
|
|
continue
|
|
for j0, j1, i0, i1 in strips_of(cells):
|
|
x0, x1 = i0 * CELL, (i1 + 1) * CELL
|
|
y0, y1 = j0 * CELL, (j1 + 1) * CELL
|
|
w, h = x1 - x0, y1 - y0
|
|
if w * h < min_area and not (kind == "door" and w * h >= DOOR_MIN_AREA):
|
|
continue
|
|
if kind != "door" and min(w, h) < MIN_DIM_DOOR:
|
|
continue
|
|
if (w >= hw * 0.8 and h <= 2.5) or (h >= hd * 0.8 and w <= 2.5):
|
|
continue # 외곽 벽선 관통 박판
|
|
comps.append((kind, x0, y0, x1, y1))
|
|
comps.sort(key=lambda cmp: (cmp[1], cmp[2]))
|
|
report.append(f"{hall}: {len(comps)}개")
|
|
for i, (kind, x0, y0, x1, y1) in enumerate(comps, 1):
|
|
rows.append(f"('{hall}-OB-{i}', '{hall}', '{kind}', {x0:.1f}, {y0:.1f}, {x1:.1f}, {y1:.1f})")
|
|
|
|
if rows:
|
|
sql.write(
|
|
"INSERT INTO hall_obstacle (id, hall_id, kind, x0, y0, x1, y1)\nVALUES\n "
|
|
+ ",\n ".join(rows)
|
|
+ "\nON CONFLICT (id) DO UPDATE SET kind = EXCLUDED.kind,\n"
|
|
" x0 = EXCLUDED.x0, y0 = EXCLUDED.y0, x1 = EXCLUDED.x1, y1 = EXCLUDED.y1;\n"
|
|
)
|
|
with open(out_sql, "w", encoding="utf-8") as f:
|
|
f.write(sql.getvalue())
|
|
for line in report:
|
|
print(line)
|
|
print(f"OK -> {out_sql} (스트립 {len(rows)}개)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|