123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
"""演示5: 几何分析法 — 平面线框图的拓扑图构建与房间识别。
合成一张类似建筑平面图的 2x2 网格墙体线框图(含一个门口缺口与噪点),
流程: 霍夫提取线段 -> 求交点(顶点) -> 构建属性邻接图 -> networkx 最小环基(房间识别)。
"""
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from figure_style import figures_dir, configure
configure()
OUT = figures_dir()
os.makedirs(OUT, exist_ok=True)
H, W = 420, 420
img = np.zeros((H, W), dtype=np.uint8)
# ---- 合成"平面图": 外部墙 + 内部十字隔墙 (完整封闭, 形成 4 个房间) ----
def wall(p1, p2, thickness=4):
cv2.line(img, p1, p2, 255, thickness)
wall((40, 40), (380, 40)) # 上墙
wall((380, 40), (380, 380)) # 右墙
wall((380, 380), (40, 380)) # 下墙
wall((40, 380), (40, 40)) # 左墙
wall((210, 40), (210, 380)) # 内部竖隔墙
wall((40, 210), (380, 210)) # 内部横隔墙
# 椒盐噪声
rng = np.random.default_rng(7)
noise = rng.random((H, W)) > 0.999
img[noise] = 255
# ---- 1. 图元提取: 霍夫线段 ----
edges = cv2.Canny(img, 50, 150, apertureSize=3)
segments = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=60,
minLineLength=50, maxLineGap=30)
segs = segments.reshape(-1, 4).astype(float) if segments is not None else np.zeros((0, 4))
# ---- 2. 拓扑图构建: 求交点并聚类成顶点 ----
def line_intersection(p1, p2, p3, p4):
"""两线段交点(在延长线上), 返回 None 或 (x, y)。"""
x1, y1 = p1; x2, y2 = p2
x3, y3 = p3; x4, y4 = p4
denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
if abs(denom) < 1e-9:
return None
t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom
return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
candidates = []
for a, b in ((s[:2], s[2:]) for s in segs):
for c, d in ((s[:2], s[2:]) for s in segs):
ip = line_intersection(a, b, c, d)
if ip is not None and 0 <= ip[0] <= W and 0 <= ip[1] <= H:
candidates.append(ip)
# 聚类: 距离 < 8 的交点归为同一顶点
vertices = []
for ip in candidates:
if all(np.hypot(ip[0] - vx, ip[1] - vy) > 8 for vx, vy in vertices):
vertices.append(ip)
vertices = np.array(vertices)
# 顶点 -> 索引
def nearest_vertex(p):
d = np.hypot(vertices[:, 0] - p[0], vertices[:, 1] - p[1])
return int(np.argmin(d))
# 构建平面图: 每条线段在其途经的所有顶点处被打断, 相邻顶点之间形成一条边
G = nx.Graph()
for s in segs:
p1, p2 = s[:2], s[2:]
# 计算每个顶点在该线段直线上的投影参数 t
dx, dy = p2 - p1
L2 = dx * dx + dy * dy
hits = []
for vi, v in enumerate(vertices):
t = ((v[0] - p1[0]) * dx + (v[1] - p1[1]) * dy) / L2
proj = p1 + t * np.array([dx, dy])
dist = np.hypot(v[0] - proj[0], v[1] - proj[1])
if t > -0.05 and t < 1.05 and dist < 10: # 顶点落在线段上
hits.append((t, vi))
hits.sort()
for (_, vi), (_, vj) in zip(hits, hits[1:]):
G.add_edge(vi, vj)
# ---- 3. 结构分析: 最小环基 = 每个"房间" ----
rooms = list(nx.minimum_cycle_basis(G))
print(f"提取线段: {len(segs)} 条, 顶点: {len(vertices)} 个, 最小环(房间): {len(rooms)} 个")
for i, cyc in enumerate(rooms):
cx = np.mean(vertices[cyc, 0]); cy = np.mean(vertices[cyc, 1])
print(f" 房间 {i}: 顶点数 {len(cyc)}, 中心 ({cx:.0f}, {cy:.0f})")
# ---- 可视化 ----
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
ax = axes[0]; ax.imshow(img, cmap="gray"); ax.set_title("原图(墙体线框图)"); ax.axis("off")
ax = axes[1]
ax.imshow(img, cmap="gray")
for s in segs:
ax.plot([s[0], s[2]], [s[1], s[3]], c="red", lw=1.5)
ax.plot(vertices[:, 0], vertices[:, 1], "bo", ms=4)
ax.set_title(f"图元提取与交点(顶点): {len(segs)} 线 / {len(vertices)} 点"); ax.axis("off")
ax = axes[2]
ax.imshow(img, cmap="gray")
colors = plt.cm.tab10(np.linspace(0, 1, max(len(rooms), 1)))
for i, cyc in enumerate(rooms):
poly = vertices[cyc]
ax.fill(poly[:, 0], poly[:, 1], color=colors[i], alpha=0.45)
ax.plot(poly[:, 0], poly[:, 1], color=colors[i], lw=2)
ax.set_title(f"最小环基识别: {len(rooms)} 个房间/区域"); ax.axis("off")
fig.suptitle("流派一: 几何分析法 — 平面线框图 → 拓扑图 → 房间识别", fontsize=14)
fig.tight_layout(rect=(0, 0, 1, 0.95))
out_path = os.path.join(OUT, "fig_wireframe_rooms.png")
fig.savefig(out_path, dpi=150)
print(f"-> {out_path}")
|