123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
"""原理图解: 生成入门文档所需的概念示意图。
1. fig_pipeline.png 总流程图: 拆线 -> 分类 -> 识别 -> 拓扑 -> 3D
2. fig_hough_vote.png 霍夫参数空间投票原理
3. fig_ransac_idea.png RANSAC 采样-拟合-内点计数思想
4. fig_face_from_edges 面识别: 边 -> 环 -> 平面拟合(长方体 6 面)
"""
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
from figure_style import figures_dir, configure
configure()
OUT = figures_dir()
os.makedirs(OUT, exist_ok=True)
# ============ 1. 总流程图 ============
fig, ax = plt.subplots(figsize=(13, 4))
ax.set_xlim(0, 13); ax.set_ylim(0, 4); ax.axis("off")
boxes = [
(0.6, 1.4, "二值图像\n(含噪/断裂)", "tab:gray"),
(2.9, 1.4, "拆线\nDouglas-Peucker", "tab:blue"),
(5.2, 1.4, "分类\n边数/夹角/曲率", "tab:green"),
(7.5, 1.4, "识别\n霍夫/RANSAC", "tab:orange"),
(9.8, 1.4, "拓扑结构\n最小环/子图同构", "tab:red"),
(12.1, 0.6, "3D 重建\n点线面体", "tab:purple"),
]
for x, y, text, color in boxes:
ax.add_patch(FancyBboxPatch((x, y), 0.95, 1.2, boxstyle="round,pad=0.05",
fc=color, ec="k", alpha=0.85))
ax.text(x + 0.475, y + 0.6, text, ha="center", va="center",
fontsize=9, color="white", fontweight="bold")
for i in range(len(boxes) - 1):
x0 = boxes[i][0] + 0.95
x1 = boxes[i + 1][0]
y0 = boxes[i][1] + 0.6
y1 = boxes[i + 1][1] + 0.6
ax.add_patch(FancyArrowPatch((x0, y0), (x1, y1), arrowstyle="-|>",
mutation_scale=16, lw=1.8, color="k"))
# 向下指向 3D 的箭头
ax.add_patch(FancyArrowPatch((9.8 + 0.475, 1.4), (12.1 + 0.475, 1.8),
arrowstyle="-|>", mutation_scale=16, lw=1.8, color="k"))
ax.set_title("从 2D 到 3D 的图形理解流水线", fontsize=14, pad=10)
fig.tight_layout()
fig.savefig(os.path.join(OUT, "fig_pipeline.png"), dpi=150)
plt.close(fig)
# ============ 2. 霍夫参数空间投票 ============
rng = np.random.default_rng(0)
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
# 左: 断裂直线(像素空间)
ax = axes[0]
ax.set_xlim(0, 10); ax.set_ylim(0, 10); ax.axis("off")
ax.set_title("图像空间: 一条断裂的直线")
for i, (x0, x1) in enumerate([(1, 3.2), (3.8, 6), (6.6, 9)]):
y = -0.9 * (np.array([x0, x1]) - 5)
ax.plot([x0, x1], [y[0] + 5.6, y[1] + 5.6], "tab:blue", lw=2)
# 断裂段上的采样点
xs = np.linspace(x0, x1, 8)
ys = -0.9 * (xs - 5) + 5.6
ax.plot(xs, ys, ".", c="tab:red", ms=5)
ax.annotate("断成几段", xy=(2.1, 3.9), xytext=(0.5, 1.2), fontsize=9,
arrowprops=dict(arrowstyle="->"))
# 右: 参数空间投票
ax = axes[1]
ax.set_xlim(0, 10); ax.set_ylim(0, 10); ax.axis("off")
ax.set_title("参数空间(θ-ρ): 同一条线的投票汇聚成峰")
theta = np.linspace(0, np.pi, 120)
for i, (x0, x1) in enumerate([(1, 3.2), (3.8, 6), (6.6, 9)]):
xs = np.linspace(x0, x1, 15)
ys = -0.9 * (xs - 5) + 5.6
for (px, py) in zip(xs, ys):
rho = px * np.cos(theta) + py * np.sin(theta)
t = theta + (1 - theta / np.pi) * 0 # 占位
# 映射到 (theta', rho') 画正弦曲线(示意)
th = theta * (9 / np.pi) * 0.8 + 1
rh = (rho + 12) / 24 * 9
ax.plot(th, rh, ".", ms=1, color=f"C{i}")
# 峰值标注
ax.add_patch(mpatches.Circle((4.6, 5.6), 0.9, fc="red", ec="k", alpha=0.35))
ax.annotate("投票峰值\n= 最强直线", xy=(4.6, 5.6), xytext=(6.5, 2.5),
fontsize=9, arrowprops=dict(arrowstyle="->"))
fig.suptitle("霍夫变换原理: 从断裂碎片到参数空间峰值", fontsize=14)
fig.tight_layout(rect=(0, 0, 1, 0.93))
fig.savefig(os.path.join(OUT, "fig_hough_vote.png"), dpi=150)
plt.close(fig)
# ============ 3. RANSAC 思想 ============
fig, axes = plt.subplots(1, 3, figsize=(13, 4.2))
np.random.seed(1)
xin = np.linspace(0, 10, 40) + np.random.normal(0, 0.3, 40)
yin = 1.2 * xin + 2 + np.random.normal(0, 0.4, 40)
xout = np.random.uniform(0, 10, 30); yout = np.random.uniform(0, 15, 30)
scenarios = [
("① 原始点集(含离群点)", "tab:gray"),
("② 随机采样 2 点拟合候选线", "tab:orange"),
("③ 保留内点最多的模型", "tab:green"),
]
for ax, (title, color) in zip(axes, scenarios):
ax.plot(np.concatenate([xin, xout]), np.concatenate([yin, yout]),
".", c=color, ms=4)
ax.set_title(title, fontsize=10)
ax.set_xlim(0, 10); ax.set_ylim(0, 15)
if title.startswith("②"):
i1, i2 = np.random.choice(len(xin), 2, replace=False)
p1, p2 = np.array([xin[i1], yin[i1]]), np.array([xin[i2], yin[i2]])
m = (p2[1] - p1[1]) / (p2[0] - p1[0])
b = p1[1] - m * p1[0]
ax.plot([0, 10], [b, m * 10 + b], "r-", lw=2, label="候选直线")
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], "o", c="r", ms=8)
ax.legend(fontsize=8)
if title.startswith("③"):
x = np.concatenate([xin, xout]); y = np.concatenate([yin, yout])
ax.plot(x, y, ".", c="lightgray", ms=4, alpha=0.4)
ax.plot(xin, yin, ".", c="green", ms=5)
ax.plot([0, 10], [2, 1.2 * 10 + 2], "r-", lw=2)
fig.suptitle("RANSAC 思想: 反复随机采样 + 一致性检验, 无视离群点", fontsize=14)
fig.tight_layout(rect=(0, 0, 1, 0.92))
fig.savefig(os.path.join(OUT, "fig_ransac_idea.png"), dpi=150)
plt.close(fig)
# ============ 4. 面识别: 边 -> 环 -> 面 ============
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
w, h, d = 2.0, 1.5, 1.0
verts3d = np.array([
[0, 0, 0], [w, 0, 0], [w, h, 0], [0, h, 0],
[0, 0, d], [w, 0, d], [w, h, d], [0, h, d],
])
edges = [(0,1),(1,2),(2,3),(3,0), (4,5),(5,6),(6,7),(7,4), (0,4),(1,5),(2,6),(3,7)]
fig, axes = plt.subplots(1, 3, figsize=(13, 4.5), subplot_kw={"projection": "3d"})
# ① 3D 线框(边)
ax = axes[0]
for u, v in edges:
ax.plot(*zip(verts3d[u], verts3d[v]), "k-", lw=1.6)
ax.scatter(*verts3d.T, c="tab:blue", s=25)
ax.set_title("① 3D 骨架(顶点+边)", fontsize=10)
# ② 找环(最小环/四边形环)
ax = axes[1]
for u, v in edges:
ax.plot(*zip(verts3d[u], verts3d[v]), "k-", lw=0.8, alpha=0.3)
cyc = [0, 1, 2, 3]
poly = verts3d[cyc]
ax.add_collection3d(Poly3DCollection([poly], alpha=0.35, fc="tab:green", ec="k"))
for i in range(4):
u, v = cyc[i], cyc[(i + 1) % 4]
ax.plot(*zip(verts3d[u], verts3d[v]), "tab:green", lw=2.5)
ax.set_title("② 搜索闭合环", fontsize=10)
# ③ 全部 6 个面
ax = axes[2]
faces = [[0,1,2,3], [4,5,6,7], [0,1,5,4], [1,2,6,5], [2,3,7,6], [3,0,4,7]]
cmap = plt.cm.tab10
for i, f in enumerate(faces):
ax.add_collection3d(Poly3DCollection([verts3d[f]], alpha=0.4,
fc=cmap(i % 10), ec="k", lw=0.8))
ax.set_title("③ 6 个面 = 封闭实体(B-rep)", fontsize=10)
for ax in axes:
ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z")
ax.set_xlim(-0.3, 2.3); ax.set_ylim(-0.3, 1.8); ax.set_zlim(-0.3, 1.3)
ax.view_init(elev=25, azim=-60)
fig.suptitle("面识别原理: 边 → 环 → 平面拟合 → 封闭体", fontsize=14)
fig.tight_layout(rect=(0, 0, 1, 0.92))
fig.savefig(os.path.join(OUT, "fig_face_from_edges.png"), dpi=150)
plt.close(fig)
print("生成 4 张原理图:")
for f in ["fig_pipeline.png", "fig_hough_vote.png", "fig_ransac_idea.png", "fig_face_from_edges.png"]:
p = os.path.join(OUT, f)
print(f" {p} ({os.path.getsize(p)} bytes)")
|