
简介这份资料面向建筑安装工程的设计、施工与审图人员针对水暖、空调、消防三大专业施工图识读中图例符号繁杂、跨专业对照困难的问题系统汇编常用图符号适合刚入行或需要跨专业看图的初、中级技术人员案头查阅。压缩包内共1个PDF文件约1.36MB为纯图文对照的图例手册方便在电脑或移动端随时翻查。内容按专业分章展开工艺管道部分讲解管道、阀门、法兰、补偿器及弯头、三通、四通等连接件的表示方法通风空调部分涵盖风机、风管、风口、过滤器、热交换器等设备部件符号给排水与采暖部分梳理水管、水表、水箱、水泵及暖气片、锅炉、分水器等内容消防部分则细分基本图形符号、灭火器符号、消防管路及配件、固定灭火系统、灭火设备安装处与自动报警设备等类别。已有65人学习可作为施工图识读与绘制的随身速查参考。1. 施工图图例不是配图它是水暖电三个专业共用的语义层我见过不少现场返工起因不是算错管径而是把截止阀的符号看成了闸阀。施工图里那些短线、圆圈、三角本质上是一套压缩过的语义系统一个图形同时编码了设备类型、连接方式、介质方向和安装位置。《水暖、空调、消防施工图常用图符号.pdf》把这套系统按工艺管道、通风空调、给排水采暖、消防四个板块归类又单独拆出消防辅助符号、灭火器符号、消防管路及配件、固定灭火系统、灭火设备安装处、自动报警设备六类子符号。它适配的人很明确接手机电深化设计的新人、做图纸会审和施工交底的现场工程师以及要把图例资产结构化的数字化交付岗。当字典查没问题更值钱的用法是把它拆成机器可读的符号库。2. 四类图例的区分维度管道、风管、水暖与消防符号怎么编码翻这份 PDF 最容易犯的错是按“长得像不像”去记符号。实际图纸里的符号是分维度编码的形状只承担一部分语义剩下的靠线型、图层、颜色和文字标注补足。把这几个维度理清楚后面做符号库结构设计和匹配算法时才知道该拿什么当特征。2.1 工艺管道线型、阀门、连接件是三套独立编码工艺管道部分最容易被当成一个整体其实是三套并行的编码。线型回答“这是什么管”阀门形状回答“这里能通断还是能调节”连接件回答“管子在这里怎么转向和变径”。这三套可以自由组合所以同一根管线上能出现保温双线管加蝶阀加异径三通的叠加表达。我在做符号分类时会把“管道”和“管件”拆成两个一级类因为它们的匹配逻辑完全不同管道是长条形图元靠端点和线宽判断管件是闭合小图形靠轮廓形状判断。混在一起做模板匹配误检率会明显上升。提示工艺管道的阀门符号里截止阀、节流阀、减压阀外形接近单靠形状识别到 0.8 分左右就顶天了必须结合旁边的文字标注一起判定。2.2 通风空调风管符号主要靠断面比例区分通风空调工程的图例里送风、回风、排风、新风这四种风管在多数图纸上是同一种矩形轮廓区别在于断面尺寸标注和箭头方向。风机符号通常是一个圆形或椭圆主体加斜线叶片过滤器是带网格的矩形热交换器会画出内部折线表示换热流程。这里有个容易忽略的点风管符号在 CAD 里经常是双线画法两条平行线的间距代表风管宽度。做识别时如果只取外轮廓会把不同规格的风管当成同一个符号。比较稳的做法是把包围盒的宽高比一起记录进去作为额外的判断特征。2.3 给排水与采暖线型加颜色是双通道冗余给排水和采暖板块的图例有两条并存的信息通道。线型上实线表示给水或采暖供水虚线常用来表示排水或回水颜色上热水管偏红、冷水管偏蓝。这两条通道是冗余设计为的是应对不同出图习惯。麻烦出在打印环节。彩色图纸转成黑白 PDF 后颜色通道直接归零冷暖水管在视觉上完全一样。我处理过的项目里有相当比例的图纸会审问题就来自这种“彩转黑”后的信息丢失。2.4 消防从基本图形到联动设备的六层符号结构消防部分是这份资料里层级最深的。它至少分了六层基本图形符号消火栓、喷淋头、灭火器、报警按钮、辅助符号报警信号、疏散指示、灭火器类型符号干粉、泡沫、二氧化碳、消防管路及配件符号、固定灭火系统符号自动喷水、气体灭火、泡沫灭火、灭火设备安装处符号最上面一层是自动报警设备符号感烟探测器、感温探测器、手动报警按钮。这六层不是并列关系而是“设备本体 → 系统归属 → 安装位置”的递进。做图例库的时候必须把层级关系存进去否则会出现“喷淋头和感温探测器都是圆形系统却完全不是一回事”的匹配错误。板块主区分维度次区分维度最常见的误读工艺管道线型单线/双线/保温阀门形状截止阀与节流阀外形接近通风空调风管断面比例介质文字与箭头送风口与回风口方向看反给排水采暖线型实线/虚线颜色冷/热彩转黑后冷热管无法区分消防符号层级基本/系统/安装安装位置标注喷淋头与感温探测器都是圆形把上面这些维度固化成一份可执行的结构比记图形有用得多。下面这份字典是我在项目里用的分类骨架键名统一方便后面和 CAD 图层名、材料表名称做对齐# 图例符号分类骨架key 为统一符号 ID 前缀便于与 CAD 图层、材料表对齐 SYMBOL_TAXONOMY { PIPE: { # 工艺管道 line: [PL-1 单线管道, PL-2 双线管道, PL-3 保温管道], valve: [VL-GATE 闸阀, VL-GLOBE 截止阀, VL-BFLY 蝶阀, VL-CHK 止回阀], fitting: [FT-ELB 弯头, FT-TEE 三通, FT-CROSS 四通, FT-RED 异径管], support: [SP-H 吊架, SP-B 支架, SP-F 固定支架], }, HVAC: { # 通风空调 duct: [DU-SUP 送风管, DU-RET 回风管, DU-EXH 排风管, DU-FRE 新风管], device: [DV-FAN 风机, DV-FIL 过滤器, DV-HEX 热交换器, DV-SIL 消声器], port: [PT-SUP 送风口, PT-RET 回风口, PT-EXH 排风口], }, WSD: { # 给排水与采暖 water: [WT-CW 冷水管, WT-HW 热水管, WT-DR 排水管, WT-RW 中水管], device: [DV-PMP 水泵, DV-TNK 水箱, DV-MET 水表, DV-BLR 锅炉], heat: [HT-RAD 散热器, HT-COL 分集水器, HT-CIR 循环泵], }, FIRE: { # 消防 basic: [FB-HYD 消火栓, FB-SPK 喷淋头, FB-EXT 灭火器, FB-BTN 报警按钮], aux: [FA-SIG 报警信号, FA-EVA 疏散指示], ext_type:[EX-POW 干粉, EX-FOAM 泡沫, EX-CO2 二氧化碳], pipe: [FP-PIP 消防管路, FP-FIT 管件, FP-VLV 消防阀门], fixed: [FX-SPR 自动喷水, FX-GAS 气体灭火, FX-FOAM 泡沫灭火], install: [FI-POS 设备安装处], alarm: [AL-SMK 感烟探测器, AL-HEA 感温探测器, AL-MAN 手动报警], }, }这份字典里的每个值都是“ID 中文名”的格式ID 用两位前缀加短横线是为了在后面写 SQL 和写匹配逻辑时能用startswith(FB-)这种前缀匹配快速筛出消防基本符号族。如果把中文名直接当主键跨项目复用时会因为叫法不统一有人写“喷淋头”有人写“喷洒头”而反复改代码。3. 用 PyMuPDF 拆图例从 PDF 到可检索符号库的四步流水线图例集躺在 PDF 里只能靠肉眼翻做成符号库之后就能被程序调用。这条流水线分四步判断 PDF 类型、抽取图元并合并、归一化图像、落库建索引。3.1 先判断这份 PDF 是矢量还是扫描件打开文件后第一件事不是写解析代码而是判断页面里有没有矢量图元。矢量页可以用page.get_drawings()直接拿到每条路径的包围盒精度到点扫描件页只能走图像二值化加轮廓检测。同一份 PDF 里两种页面混排是常态图例集这类文档往往正文是矢量、附页是扫描。import fitz # PyMuPDF doc fitz.open(水暖、空调、消防施工图常用图符号.pdf) for pno in range(doc.page_count): page doc[pno] n_path len(page.get_drawings()) n_text len(page.get_text(blocks)) print(fpage {pno1:02d} paths{n_path:5d} text_blocks{n_text:3d})这段输出是选型依据paths大于 100 的页面基本是矢量图例页可以直接抽路径paths接近 0 而text_blocks也不多的页面多半是扫描图需要转图像处理分支。页码分布还能帮你知道图例集中在哪几页减少无用解析。3.2 用 PyMuPDF 抽图元并把同一符号的路径合并一个阀门符号在 PDF 里常常是三条独立路径外框、内部斜线、引线。如果不合并你会得到三个碎片图模板匹配全废。做法是给包围盒加一个容差用矩形相交判断把邻近路径并成一组。import fitz, os, json doc fitz.open(水暖、空调、消防施工图常用图符号.pdf) os.makedirs(symbols, exist_okTrue) index [] def dist(tb, cx, cy): x0, y0, x1, y1 tb[:4] return ((x0 x1) / 2 - cx) ** 2 ((y0 y1) / 2 - cy) ** 2 for pno in range(doc.page_count): page doc[pno] labels [(b[4].strip(), b[:4]) for b in page.get_text(blocks) if b[4].strip()] paths [d[rect] for d in page.get_drawings()] if not paths: continue # 扫描页走图像分支 boxes [] for r in paths: for b in boxes: if b.intersects(r (-6, -6, 6, 6)): # 6pt 容差吸收引线与标注 b | r break else: boxes.append(fitz.Rect(r)) for i, b in enumerate(boxes): if b.width 8 or b.height 8: # 过滤装饰性短线 continue pix page.get_pixmap(clipb, matrixfitz.Matrix(4, 4), alphaFalse) name fp{pno1:02d}_{i:03d}.png pix.save(fsymbols/{name}) cx, cy (b.x0 b.x1) / 2, (b.y0 b.y1) / 2 label min(labels, keylambda t: dist(t[1], cx, cy))[0] if labels else index.append({file: name, page: pno 1, bbox: list(b), label: label}) json.dump(index, open(symbol_index.json, w, encodingutf-8), ensure_asciiFalse, indent2)关键参数有三个。容差 6pt 是经验值建筑图例的引线通常短于 6pt 就会和符号本体相交最小边长 8pt 用来过滤图框和分隔线阀门这类小符号的实际边长一般在 10pt 以上渲染矩阵Matrix(4, 4)把 72dpi 提到 288dpi保证导出 PNG 的长边不小于 512 像素后续缩放不会糊。label字段取的是距离符号中心最近的文字块。这个策略对图例集这种“左图右名”的排版准确率很高但对引线标注式的图纸会串名所以落库时我一般把它标成“候选名”不当作权威字段。3.3 OpenCV 归一化反相、去白边、等比缩放原始裁剪图尺寸各异、四周留白不等直接拿去做模板匹配分数会被留白面积带偏。归一化做三件事把黑线白底转成白线黑底、按内容外接框裁紧、等比缩放到统一画布并居中。import cv2, numpy as np def normalize(src, dst, size256, pad12): img cv2.imread(src, cv2.IMREAD_GRAYSCALE) if img is None: return False inv 255 - img # 反相成白线黑底 _, bw cv2.threshold(inv, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) ys, xs np.where(bw 0) if len(xs) 0: return False # 空图直接丢弃 crop bw[ys.min():ys.max() 1, xs.min():xs.max() 1] h, w crop.shape scale (size - 2 * pad) / max(h, w) # 等比缩放长边对齐内框 resized cv2.resize(crop, (max(1, int(w * scale)), max(1, int(h * scale))), interpolationcv2.INTER_AREA) canvas np.zeros((size, size), np.uint8) y0 (size - resized.shape[0]) // 2 x0 (size - resized.shape[1]) // 2 canvas[y0:y0 resized.shape[0], x0:x0 resized.shape[1]] resized return cv2.imwrite(dst, canvas)参数取值作用调大/调小的后果渲染矩阵4控制裁剪图分辨率太小线条断裂太大内存翻倍合并容差6pt合并同一符号的多条路径太大会把相邻两个符号粘一起最小边长8pt过滤装饰性短线太大会丢掉小尺寸阀门符号画布尺寸256统一模板输入影响匹配耗时也影响小符号清晰度边距 pad12px防止线条贴边被裁掉太小缩放后笔画发虚注意INTER_AREA是缩小场景的正确选择换成INTER_LINEAR会让细线在缩放后出现灰边二值化时被整条吃掉。3.4 SQLite 落库与关键词检索图例数量通常在几百到一千之间SQLite 完全够用不需要上服务端数据库。表结构要点是把专业板块、中文名、页码、文件名、原始包围盒和感知哈希都存下来页码字段是给人工回查用的——脚本报出一个可疑符号你得能一键跳回 PDF 对应位置。CREATE TABLE symbol ( id INTEGER PRIMARY KEY, sys TEXT NOT NULL, -- PIPE / HVAC / WSD / FIRE name_cn TEXT NOT NULL, -- 图例中文名如“蝶阀” page INTEGER, -- 在 PDF 中的页码便于回查原图 file TEXT NOT NULL, -- 归一化后的 PNG 文件名 bbox TEXT, -- 原 PDF 中的包围盒JSON 字符串 phash TEXT -- 感知哈希用于粗筛重复符号 ); CREATE INDEX idx_symbol_sys ON symbol(sys); CREATE INDEX idx_symbol_name ON symbol(name_cn); -- 按专业板块 关键词检索 SELECT name_cn, page, file FROM symbol WHERE sys FIRE AND name_cn LIKE %探测器% ORDER BY page;有了这张表“消防里所有和探测器有关的图例在第几页”就变成一句 SQL 的事。sys字段是分类骨架和数据库之间的桥专业板块的划分直接沿用上面那份字典的顶层键避免出现两套分类标准。4. ezdxf 读 DXF把图纸里的块定义和图例库对起来符号库建好之后真正的用途是拿它去查一张具体的 DXF。这一步的核心不是算法多高级而是先搞清楚图例在 DXF 里到底以什么形式存在再决定比对策略。4.1 图例在 DXF 里的三种落地形态正规的设计院图纸里图例一般以块BLOCK加块引用INSERT的形式存在画的时候直接插入改的时候改块定义全图同步。第二种形态是散图元设计人直接用直线、圆、多段线拼出一个符号没有做成块这种情况下插入点信息就丢了只能靠几何形状匹配。第三种形态最麻烦——符号来自外部参照XREF主图里只有一个引用路径解析时需要把外部文件也加载进来。判断图纸规范程度有个快办法统计 INSERT 数量和总图元数量的比值。比值低于 0.1说明绝大部分符号是散图元画的符号库匹配的难度会上升一个档次。4.2 用 ezdxf 统计 INSERT 与图层分布不管后面走哪条路先跑一遍统计把图纸的“符号使用画像”摸出来。图层名往往比块名更规范很多单位的图层命名遵循“专业-系统-构件”的三段式比如FIRE-SPK表示消防喷淋。import ezdxf from collections import Counter doc ezdxf.readfile(MEP_plan.dxf) msp doc.modelspace() ins Counter() for e in msp.query(INSERT): ins[e.dxf.name] 1 ins[f{e.dxf.layer}] 1 # 图层同样承载语义前缀 区分 # 只保留出现次数 3 的块名滤掉图框、标题栏之类的单次块 candidates {k: v for k, v in ins.items() if not k.startswith() and v 3} for name, cnt in sorted(candidates.items(), keylambda x: -x[1])[:20]: print(f{cnt:5d} {name}) layer_hits {k[1:]: v for k, v in ins.items() if k.startswith()} print(图层种类数:, len(layer_hits))输出里出现频次最高的前 20 个块名基本就是这张图纸的主力图例。阈值 3 是为了过滤掉标题栏、指北针、图框这类只出现一次或几次的块。图层种类数则反映图纸的组织粒度一个几十万平方米的机电总图图层种类数通常在 80 到 200 之间低于 30 说明画图时没分图层后面做规则校审会很吃力。4.3 模板匹配让查询图比模板大一圈匹配阶段最容易踩的坑是尺寸完全相同。如果待查图和模板都是 256×256matchTemplate的结果矩阵只有 1×1任何几像素的偏移都会让分数断崖式下跌。正确做法是把待查图缩放到 288×288模板保持 256留出 32 像素的平移余量。import cv2, numpy as np, sqlite3 conn sqlite3.connect(symbols.db) rows conn.execute(SELECT name_cn, sys, file FROM symbol).fetchall() lib [] for name, sys, f in rows: tpl cv2.imread(f, cv2.IMREAD_GRAYSCALE) if tpl is not None: lib.append((name, sys, tpl)) def best_match(query_gray, topk3): query_gray 需先归一化到 288x288、白线黑底 hits [] for name, sys, tpl in lib: res cv2.matchTemplate(query_gray, tpl, cv2.TM_CCOEFF_NORMED) hits.append((float(res.max()), name, sys)) hits.sort(reverseTrue) top hits[0] # top1 与 top2 差距过小说明两个符号外形接近标记为待确认 ambiguous len(hits) 1 and (top[0] - hits[1][0]) 0.05 return top, ambiguous, hits[:topk]TM_CCOEFF_NORMED是做灰度模板匹配的默认选择它对整体亮度差异不敏感输出的分数天然落在 0 到 1 之间便于设定阈值。ambiguous标记比分数本身更有用截止阀和节流阀分数都在 0.8 附近、只差 0.02 时脚本不该自作主张地给出结论。4.4 阈值分档与待确认清单分数出来之后要分档处理一刀切会导致要么漏检要么误报。下面这套分档是我在几个机电深化项目里调出来的可以直接作为起点。分数区间判断处理方式≥ 0.90几乎确定自动写入图例清单不打扰人0.75 – 0.90高概率自动写入但打上抽查标记0.60 – 0.75疑似进人工确认队列附带 PDF 页码 0.60未命中大概率是库外符号反向补充进图例库这套分档有个隐藏收益进入“未命中”区间的符号本身就是图例库的增量。每次跑完一批图纸把未命中项归拢一下往往能发现资料里没有覆盖的地方做法或企业自定义符号补进去之后下一批图纸的自动通过率就上去了。5. 阈值调优与误报压制让图例校审脚本真的能上生产前面几章的流程能跑通但能不能上生产取决于阈值定得准不准以及你有没有想清楚哪些判断不该交给程序。5.1 先看分数分布再定阈值凭感觉定 0.75 或 0.8 都不靠谱。正确做法是从待检符号里抽 200 到 300 个人工标一遍对错然后扫一遍阈值看每个阈值下的精度和召回率。import numpy as np def pick_threshold(scores, labels, min_recall0.85): scores: top1 匹配分数; labels: 1 表示匹配正确, 0 表示错误 best, rows None, [] for t in np.arange(0.50, 0.99, 0.01): accept scores t if accept.sum() 0: continue prec labels[accept].mean() # 自动通过的正确率 rec accept[labels 1].mean() # 正确符号被自动通过的比例 rows.append((round(t, 2), round(prec, 3), round(rec, 3))) if rec min_recall and (best is None or prec best[1]): best (round(t, 2), prec, rec) return best, rowsmin_recall是业务约束不是算法参数。图例校审的定位是“减少人工翻图”不是“替代人工判断”所以召回率优先——宁可多推一些到人工队列也不能让一个用错的符号悄悄通过。我一般把min_recall设在 0.85 到 0.90 之间再在满足这个下限的阈值里挑精度最高的那个。5.2 三类必须交回人眼的符号有三类情况无论分数多高都不该自动判定。第一类是旋转和缩放差异大的符号matchTemplate不支持旋转不变性一个转了 90 度的阀门分数可能只有 0.5这类要靠minAreaRect先做主轴对齐再匹配。第二类是带引线文字的符号引线会改变包围盒形状匹配前要用形态学开运算把细长连通域切掉。第三类是库外符号分数低但人工一看就认识这类必须回流进库。注意自动通过率不是越高越好。一个 95% 通过率但漏掉关键消防符号的脚本比一个 70% 通过率、每个疑点都推到人工队列的脚本危险得多。5.3 用感知哈希做重复符号粗筛图例库建到几百条之后会出现大量近似重复项同一家设计院不同项目出的图例集里“感烟探测器”可能存了七八份形状完全一样但文件名不同。用感知哈希做一次粗筛能在进模板匹配之前把候选模板压到原来的三分之一。import cv2, numpy as np def phash(gray, hash_size16): small cv2.resize(gray, (hash_size, hash_size), interpolationcv2.INTER_AREA) dct cv2.dct(np.float32(small)) low dct[:8, :8].flatten()[1:] # 丢掉直流分量 return .join(1 if v np.median(low) else 0 for v in low) def hamming(h1, h2): return sum(a ! b for a, b in zip(h1, h2))哈希长度 63 位同族符号的汉明距离一般小于 6不同族在 15 以上。把这个距离当预筛条件先砍掉明显不相似的模板再对剩下的小集合跑matchTemplate整批图纸的处理时间能压下来一半以上。最后一步是把hamming 6的符号聚成一簇让人工从簇里选一张作为标准模板其余的记为别名——这样图例库里存的才是“符号”而不是“文件的副本”。本文还有配套的精品资源点击获取