首页/在线代码库/demo6_symbol_isomorphism.py

demo6_symbol_isomorphism.py

场景: 一张线框图中包含多个图元(矩形+双对角线、三角形、圆形)。 先将图元转换成"顶点-边"属性邻接图, 再用 networkx GraphMatcher 在大图中 搜索与"矩形+双对角线"模板图同构(且边长比例一致)的子图。

Python 159 行 系列:识别系列 🔒 源码仅供在线学习,不提供文件下载
完整源码 · 在线查看demo6_symbol_isomorphism.py
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
"""演示6: 几何分析法 — 属性邻接图构建 + 子图同构识别图元符号。

场景: 一张线框图中包含多个图元(矩形+双对角线、三角形、圆形)。
先将图元转换成"顶点-边"属性邻接图, 再用 networkx GraphMatcher 在大图中
搜索与"矩形+双对角线"模板图同构(且边长比例一致)的子图。
"""
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import networkx as nx
from figure_style import figures_dir, configure

configure()

OUT = figures_dir()
os.makedirs(OUT, exist_ok=True)

# ============ 1. 图元库(标准符号模板) ============
# "矩形+双对角线"符号: 4 个角点(外框环) + 1 个中心交叉点
# 角点 0-3 围成外框(边长 1.0), 中心点 4 经 4 条半对角线(长度 1/sqrt2)连到各角点
def make_symbol_template():
    T = nx.Graph()
    T.add_nodes_from(range(5))
    for i in range(4):
        T.add_edge(i, (i + 1) % 4, len=1.0)
    for i in range(4):
        T.add_edge(i, 4, len=1.0 / np.sqrt(2))
    return T


# ============ 2. 合成线框图: 目标符号 + 干扰图元 ============
# 目标: 矩形+双对角线 (角点/中心/边长给定)
def rect_symbol_edges(x0, y0, w, h):
    """返回矩形的边列表, 以及对角线交叉中心点。"""
    c = [(x0, y0), (x0 + w, y0), (x0 + w, y0 + h), (x0, y0 + h)]
    cx, cy = x0 + w / 2, y0 + h / 2
    segs = [
        (c[0], c[1]), (c[1], c[2]), (c[2], c[3]), (c[3], c[0]),  # 外框
        (c[0], (cx, cy)), (c[1], (cx, cy)),                      # 半对角线
        (c[2], (cx, cy)), (c[3], (cx, cy)),
    ]
    return segs


def triangle_edges(p0, p1, p2):
    return [(p0, p1), (p1, p2), (p2, p0)]


rect_segs = rect_symbol_edges(60, 60, 120, 120)     # 目标符号(正方形+双对角线)
tri_segs = triangle_edges((250, 60), (380, 60), (315, 170))   # 干扰: 三角形
# 干扰: 一条与目标符号角点接触的短线段(增加匹配难度)
stray_segs = [((315, 170), (380, 240))]

all_segs = rect_segs + tri_segs + stray_segs


def build_graph(seg_list):
    """由线段集合构建属性邻接图: 交点成顶点, 线段在顶点处断开成边, 边带长度属性。"""
    # 收集所有交点(端点与交点都视为顶点候选)
    cand = []
    for a, b in seg_list:
        cand.extend([a, b])
    for a, b in seg_list:
        for c, d in seg_list:
            x1, y1 = a; x2, y2 = b
            x3, y3 = c; x4, y4 = d
            denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
            if abs(denom) < 1e-9:
                continue
            t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom
            ip = (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
            # 交点需同时落在两线段上
            ok = True
            for (p, q) in ((a, b), (c, d)):
                tt = ((ip[0] - p[0]) * (q[0] - p[0]) + (ip[1] - p[1]) * (q[1] - p[1])) / (
                    (q[0] - p[0]) ** 2 + (q[1] - p[1]) ** 2 + 1e-12)
                if not (-1e-6 <= tt <= 1 + 1e-6):
                    ok = False
                    break
            if ok:
                cand.append(ip)
    # 去重聚类
    verts = []
    for ip in cand:
        if all(np.hypot(ip[0] - v[0], ip[1] - v[1]) > 1e-3 for v in verts):
            verts.append(ip)
    verts = np.array(verts)

    G = nx.Graph()
    for a, b in seg_list:
        # 找出落在此线段上的所有顶点, 按投影排序, 相邻成边
        dx, dy = b[0] - a[0], b[1] - a[1]
        L2 = dx * dx + dy * dy
        hits = []
        for vi, v in enumerate(verts):
            t = ((v[0] - a[0]) * dx + (v[1] - a[1]) * dy) / L2
            proj = a + t * np.array([dx, dy])
            if -1e-3 <= t <= 1 + 1e-3 and np.hypot(*(v - proj)) < 1e-3:
                hits.append((t, vi))
        hits.sort()
        for (_, vi), (_, vj) in zip(hits, hits[1:]):
            G.add_edge(vi, vj)
    # 边长度属性: 用整体最大边长归一化, 使图像图与模板图处于同一尺度
    lens = [np.hypot(*(verts[u] - verts[v])) for u, v in G.edges()]
    scale = max(lens)
    for u, v in G.edges():
        G[u][v]["len"] = np.hypot(*(verts[u] - verts[v])) / scale
    return G, verts


G_img, verts = build_graph(all_segs)
template = make_symbol_template()


def edge_match(e1, e2):
    """边长比例一致性校验(容差 10%)。"""
    return abs(e1["len"] - e2["len"]) / max(e1["len"], e2["len"]) < 0.10


matcher = nx.algorithms.isomorphism.GraphMatcher(G_img, template, edge_match=edge_match)
matches = list(matcher.subgraph_isomorphisms_iter())
print(f"图像顶点: {len(verts)}, 子图同构匹配数: {len(matches)}")

# ============ 3. 可视化 ============
fig, axes = plt.subplots(1, 2, figsize=(12, 5.5))

# 左: 原始线框图(图元级)
ax = axes[0]
for a, b in all_segs:
    ax.plot([a[0], b[0]], [a[1], b[1]], "k-", lw=2)
ax.plot(verts[:, 0], verts[:, 1], "o", c="royalblue", ms=5)
ax.set_title(f"线框图(图元+交点)  顶点数 {len(verts)}")
ax.set_xlim(0, 420); ax.set_ylim(300, 0)
ax.set_aspect("equal"); ax.axis("off")

# 右: 子图同构匹配结果
ax = axes[1]
for a, b in all_segs:
    ax.plot([a[0], b[0]], [a[1], b[1]], "k-", lw=1.2, alpha=0.35)
if matches:
    m = matches[0]
    hit = verts[list(m.keys())]
    # 用模板结构绘制命中符号(带长度缩放)
    ax.plot(hit[:, 0], hit[:, 1], "r*", ms=16, zorder=5)
    ax.add_patch(mpatches.FancyArrowPatch((hit[0]), (hit[2]), color="red", lw=2,
                                          arrowstyle="-", zorder=6))
    ax.set_title(f"子图同构: 识别出 {len(matches)} 个\"矩形+双对角线\"符号")
else:
    ax.set_title("子图同构: 未匹配到目标符号")
ax.set_xlim(0, 420); ax.set_ylim(300, 0)
ax.set_aspect("equal"); ax.axis("off")

fig.suptitle("流派一: 几何分析法 — 属性邻接图 + 子图同构识别图元符号", fontsize=14)
fig.tight_layout(rect=(0, 0, 1, 0.95))
out_path = os.path.join(OUT, "fig_wireframe_symbol.png")
fig.savefig(out_path, dpi=150)
print(f"-> {out_path}")

输出图

fig_wireframe_symbol.png
输出图 · fig_wireframe_symbol.png