
简介这份资源面向具备Python基础、对计算机视觉与深度学习有一定了解的研发人员、文物保护技术人员及研究生提供一套古建筑表面病害辅助检测系统的完整工程实例。系统以YOLO目标检测模型为核心结合OpenCV图像处理、FastAPI服务接口与SQLite结果存储可自动识别并定位裂缝、剥落、盐析、霉斑、渗水等常见病害覆盖图像采集、数据标注、模型训练、推理检测到结果可视化与人工复核的全流程强调“模型识别人工复核”的协同模式。资源包为1个docx文档约112KB内含完整程序代码、数据库设计与GUI界面设计及代码详解目录结构清晰便于按模块查阅。目前已有94人学习。读者可据此动手搭建可复核、可追踪的数字化病害档案用于日常巡检、修缮前调查与修缮过程质量检查提升古建筑保护工作的数字化与标准化水平。1. 古建筑表面病害检测为什么传统人工巡检越来越不够用做古建筑保护的人都有一个共同感受病害不是突然出现的而是慢慢长出来的。裂缝从发丝宽扩展到几毫米苔藓从墙角蔓延到整面墙体风化让砖雕的纹样一点点模糊。问题是人工巡检的频次远远跟不上病害发展的速度。一个中等规模的古建筑群两三个技术人员带着相机和记录本走一圈少则半天多则两三天回到办公室还要逐张照片比对、标注、归档。等报告出来现场情况可能又变了。基于 Python 的古建筑表面病害辅助检测系统核心思路就是用图像识别把「人眼找病害」这件事部分自动化。拍回来的照片经过预处理、分割、分类自动框出裂缝、剥落、泛碱、苔藓等典型病害区域再叠加到建筑立面图上生成病害分布图。它不替代文保专家的判断但能把「找」和「量」这两步的效率提升一个量级。适合谁用一是做文保数字化记录的技术团队二是古建修缮工程的前期勘察人员三是想用计算机视觉做垂直场景落地但不想碰医疗、工业那些红海方向的开发者。这个方向的数据集相对好获取病害类型定义清晰GUI 展示也直观作为完整项目练手或实际部署都有空间。2. 从一张照片到病害标注图像识别流水线怎么搭2.1 为什么选 OpenCV scikit-image 做预处理而不是直接上深度学习很多人一上来就想用 YOLO 或 Mask R-CNN 端到端解决问题。实际做过就知道古建筑表面病害检测的第一个瓶颈不是模型不够强而是图像质量太差。古建筑拍摄环境复杂光线不均匀、墙面纹理干扰、拍摄角度倾斜、尺度变化大。直接拿原始照片训模型标注成本高不说模型很容易学到「墙面颜色」而不是「病害特征」。我一般会先用传统图像处理做一轮预处理和候选区域提取把明显不是病害的区域排除掉再把候选区域送进分类网络。这样标注量能减少 60% 以上模型收敛也快得多。OpenCV 负责几何校正和光照均衡scikit-image 负责纹理分析和边缘检测两者配合足够覆盖大部分预处理需求。import cv2 import numpy as np from skimage import exposure, filters def preprocess_wall_image(img_path, target_size(1024, 1024)): 古建筑墙面图像预处理几何校正 光照均衡 纹理增强 img_path: 原始照片路径 target_size: 统一缩放尺寸便于后续批量处理 img cv2.imread(img_path) if img is None: raise FileNotFoundError(f无法读取图像: {img_path}) # 1. 尺寸归一化保持长宽比短边补齐 h, w img.shape[:2] scale max(target_size[0] / w, target_size[1] / h) new_w, new_h int(w * scale), int(h * scale) img cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_AREA) # 2. 限制对比度自适应直方图均衡解决墙面光照不均 lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) lab[:, :, 0] clahe.apply(lab[:, :, 0]) img cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) # 3. 非局部均值去噪保留边缘的同时抑制砖石表面颗粒噪声 img cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21) # 4. 自适应伽马校正暗部提亮但不冲淡裂缝对比度 img exposure.adjust_gamma(img, gamma0.8) return img这段代码里几个参数值得展开说。clipLimit2.0是 CLAHE 的对比度限制阈值设太高会把墙面正常纹理也放大成假边缘设太低则暗部裂缝提不出来2.0 是我在砖石和木构表面都试过比较稳的值。fastNlMeansDenoisingColored的h10控制去噪强度古建筑表面本身有颗粒感这个值再大会把细小裂缝一起抹掉。gamma0.8是提亮暗部如果拍摄时曝光正常可以改成 1.0 跳过这步。2.2 裂缝、剥落、泛碱怎么分特征工程与分类器选型预处理之后下一步是把候选区域找出来。不同病害的图像特征差异其实很大用同一套特征去描述效果不好。我的做法是分病害类型设计特征再统一送进分类器。裂缝的核心特征是线状结构用 Frangi 滤波器或者基于 Hessian 矩阵的脊线检测最直接。剥落表现为区域性的颜色突变和纹理粗糙度变化用局部二值模式LBP加颜色矩描述。泛碱是白色粉末状覆盖在 HSV 空间里饱和度和明度都有明显特征用颜色阈值加形态学操作就能圈出大部分候选。from skimage.filters import frangi, threshold_otsu from skimage.feature import local_binary_pattern from sklearn.ensemble import RandomForestClassifier import joblib def extract_crack_features(gray_img): 提取裂缝候选区域特征 # Frangi 滤波增强线状结构sigmas 覆盖不同宽度裂缝 vesselness frangi(gray_img, sigmasrange(1, 6), black_ridgesTrue) # Otsu 自适应阈值二值化 thresh threshold_otsu(vesselness) binary vesselness thresh # 形态学闭运算连接断裂的裂缝段 kernel np.ones((3, 3), np.uint8) binary cv2.morphologyEx(binary.astype(np.uint8), cv2.MORPH_CLOSE, kernel) return binary, vesselness def extract_lbp_features(gray_img, region_mask): 提取剥落区域的 LBP 纹理特征 radius 3 n_points 8 * radius lbp local_binary_pattern(gray_img, n_points, radius, methoduniform) # 只统计候选区域内的 LBP 直方图 hist, _ np.histogram(lbp[region_mask], binsnp.arange(0, n_points 3), densityTrue) return hist # 分类器用随机森林特征维度不高时比 SVM 更稳 clf RandomForestClassifier(n_estimators200, max_depth12, random_state42) # 训练时特征向量拼接Frangi 响应统计量 LBP 直方图 颜色矩Frangi 的sigmasrange(1, 6)对应检测 1 到 5 像素宽的裂缝实际对应到原图大概 2 到 10 毫米宽的裂缝覆盖了大部分早期病害。black_ridgesTrue是因为裂缝在图像里是暗线。LBP 的radius3和n_points24是纹理分析的常用配置再大计算量上去了但区分度提升有限。随机森林的n_estimators200和max_depth12是在几千张标注样本上交叉验证调出来的样本量小的时候可以降到 100 棵树、深度 8避免过拟合。2.3 把检测结果落到建筑立面图上坐标映射与可视化检测出来的病害区域是在照片坐标系里的但文保人员需要的是建筑立面图上的病害分布。这中间要做一次透视变换和坐标映射。如果拍摄时放了标定板或者已知尺寸的参照物映射精度会高很多。没有参照物的话至少要把照片里建筑的关键角点手动标出来做单应性变换。def map_to_elevation(detections, src_points, dst_points, elevation_size): 将照片中的检测框映射到建筑立面图坐标系 detections: [(x, y, w, h, label), ...] 照片坐标系 src_points: 照片中建筑四个角点坐标 dst_points: 立面图中对应四个角点坐标 elevation_size: 立面图尺寸 (width, height) H, _ cv2.findHomography(np.array(src_points), np.array(dst_points)) mapped [] for (x, y, w, h, label) in detections: # 检测框中心点和四角映射 corners np.array([ [x, y], [x w, y], [x w, y h], [x, y h] ], dtypenp.float32).reshape(-1, 1, 2) transformed cv2.perspectiveTransform(corners, H).reshape(-1, 2) # 取映射后的外接矩形 tx, ty transformed[:, 0].min(), transformed[:, 1].min() tw transformed[:, 0].max() - tx th transformed[:, 1].max() - ty mapped.append((int(tx), int(ty), int(tw), int(th), label)) # 绘制到立面图上 canvas np.ones((elevation_size[1], elevation_size[0], 3), dtypenp.uint8) * 255 color_map {crack: (0, 0, 255), spall: (0, 165, 255), efflorescence: (255, 0, 0), moss: (0, 255, 0)} for (x, y, w, h, label) in mapped: cv2.rectangle(canvas, (x, y), (x w, y h), color_map.get(label, (128, 128, 128)), 2) cv2.putText(canvas, label, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_map.get(label, (0, 0, 0)), 1) return canvas, mappedfindHomography默认用 RANSAC 找最优单应矩阵四个点对就够解出来。实际用的时候建议标 6 到 8 个点多出来的点可以用来验证映射误差。立面图尺寸按实际建筑比例设比如一面 10 米宽 6 米高的墙用 2000×1200 像素的立面图映射精度大概在 2 到 3 厘米对病害定位来说够用了。3. 数据库与 GUI让检测结果能查、能看、能导出3.1 SQLite 表结构设计病害记录、建筑信息、检测批次怎么关联检测结果不能只存成图片得结构化入库才能做统计和追溯。我用 SQLite 是因为它零配置、单文件、Python 内置支持对于单机部署的辅助检测系统完全够用。表结构设计上分三层建筑基本信息、检测批次、病害记录。-- 建筑信息表 CREATE TABLE buildings ( building_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, location TEXT, dynasty TEXT, structure_type TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 检测批次表每次巡检算一个批次 CREATE TABLE inspection_batches ( batch_id INTEGER PRIMARY KEY AUTOINCREMENT, building_id INTEGER NOT NULL, inspect_date DATE NOT NULL, operator TEXT, weather TEXT, image_count INTEGER DEFAULT 0, FOREIGN KEY (building_id) REFERENCES buildings(building_id) ); -- 病害记录表 CREATE TABLE defects ( defect_id INTEGER PRIMARY KEY AUTOINCREMENT, batch_id INTEGER NOT NULL, defect_type TEXT NOT NULL CHECK(defect_type IN (crack,spall,efflorescence,moss,other)), severity INTEGER CHECK(severity BETWEEN 1 AND 5), bbox_x INTEGER, bbox_y INTEGER, bbox_w INTEGER, bbox_h INTEGER, area_ratio REAL, image_path TEXT, elevation_x INTEGER, elevation_y INTEGER, note TEXT, FOREIGN KEY (batch_id) REFERENCES inspection_batches(batch_id) ); -- 索引按建筑和病害类型查是最常用的 CREATE INDEX idx_defects_batch ON defects(batch_id); CREATE INDEX idx_defects_type ON defects(defect_type);severity字段用 1 到 5 表示病害严重程度这个值可以人工复核时调整也可以根据面积占比自动初判。area_ratio存病害面积占墙面面积的比例做趋势分析时直接按时间序列画出来就能看到病害发展速度。elevation_x和elevation_y存映射到立面图上的坐标前端展示时直接读这两个字段。3.2 Tkinter 界面布局从选图到出报告的完整操作流GUI 用 Tkinter 是因为它跟 Python 集成最顺打包成 exe 也方便。界面不用做太花哨核心是让操作人员能快速完成「选建筑 → 选照片 → 跑检测 → 看结果 → 导出报告」这个流程。import tkinter as tk from tkinter import ttk, filedialog, messagebox from PIL import Image, ImageTk import threading class DefectDetectionGUI: def __init__(self, root): self.root root self.root.title(古建筑表面病害辅助检测系统) self.root.geometry(1280x800) # 左侧控制面板 control_frame ttk.Frame(root, width300) control_frame.pack(sidetk.LEFT, filltk.Y, padx5, pady5) ttk.Label(control_frame, text建筑名称:).pack(anchortk.W, pady(10, 0)) self.building_var tk.StringVar() ttk.Entry(control_frame, textvariableself.building_var).pack(filltk.X) ttk.Label(control_frame, text检测日期:).pack(anchortk.W, pady(10, 0)) self.date_var tk.StringVar() ttk.Entry(control_frame, textvariableself.date_var).pack(filltk.X) ttk.Button(control_frame, text选择照片, commandself.select_images).pack(filltk.X, pady10) ttk.Button(control_frame, text开始检测, commandself.start_detection).pack(filltk.X, pady5) ttk.Button(control_frame, text导出报告, commandself.export_report).pack(filltk.X, pady5) # 进度条 self.progress ttk.Progressbar(control_frame, modedeterminate) self.progress.pack(filltk.X, pady10) # 右侧图像显示区 self.image_frame ttk.Frame(root) self.image_frame.pack(sidetk.RIGHT, filltk.BOTH, expandTrue) self.canvas tk.Canvas(self.image_frame, bggray) self.canvas.pack(filltk.BOTH, expandTrue) self.image_paths [] self.detection_results [] def select_images(self): files filedialog.askopenfilenames( title选择巡检照片, filetypes[(图像文件, *.jpg *.jpeg *.png *.bmp)] ) self.image_paths list(files) if self.image_paths: self.show_image(self.image_paths[0]) def start_detection(self): if not self.image_paths: messagebox.showwarning(提示, 请先选择照片) return # 检测放后台线程避免界面卡死 thread threading.Thread(targetself._run_detection) thread.daemon True thread.start() def _run_detection(self): total len(self.image_paths) for i, path in enumerate(self.image_paths): # 这里调用前面定义的预处理和检测函数 result self.detect_single(path) self.detection_results.append(result) self.progress[value] (i 1) / total * 100 self.root.update_idletasks() self.root.after(0, lambda: messagebox.showinfo(完成, f检测完成共处理 {total} 张照片)) def detect_single(self, img_path): 单张照片检测流程实际项目里会调用完整的检测管线 img preprocess_wall_image(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) binary, _ extract_crack_features(gray) # 后续分类和记录入库逻辑 return {path: img_path, defects: []} def show_image(self, path): img Image.open(path) img.thumbnail((900, 700)) self.photo ImageTk.PhotoImage(img) self.canvas.delete(all) self.canvas.create_image(450, 350, imageself.photo) def export_report(self): if not self.detection_results: messagebox.showwarning(提示, 没有可导出的检测结果) return # 生成 HTML 报告包含原图、标注图、病害统计表 report_path filedialog.asksaveasfilename(defaultextension.html) if report_path: self._generate_html_report(report_path)界面逻辑里有个关键点检测过程必须放后台线程。Tkinter 的主循环是单线程的如果在主线程里跑图像处理界面会直接卡死操作人员以为程序崩了。用threading.Thread加root.after回主线程更新 UI 是标准做法。进度条更新用update_idletasks强制刷新不然进度条不会动。3.3 检测结果导出HTML 报告与 Excel 统计表导出这块我一般做两个格式。HTML 报告给文保人员看包含每张照片的原图、标注图、病害列表和严重程度。Excel 统计表给管理人员做汇总按病害类型和严重程度分组计数。import pandas as pd from datetime import datetime def export_to_excel(detection_results, output_path): 将检测结果导出为 Excel 统计表 rows [] for result in detection_results: for defect in result[defects]: rows.append({ 照片文件: result[path], 病害类型: defect[type], 严重程度: defect[severity], 位置X: defect[x], 位置Y: defect[y], 宽度: defect[w], 高度: defect[h], 面积占比: defect.get(area_ratio, 0), 备注: defect.get(note, ) }) df pd.DataFrame(rows) # 按病害类型和严重程度做透视统计 pivot df.pivot_table( index病害类型, columns严重程度, values照片文件, aggfunccount, fill_value0 ) with pd.ExcelWriter(output_path, engineopenpyxl) as writer: df.to_excel(writer, sheet_name病害明细, indexFalse) pivot.to_excel(writer, sheet_name统计汇总) return output_pathpivot_table出来的统计汇总表是文保报告里最常被引用的数据。按病害类型分行、严重程度分列一眼就能看出哪类病害最严重、集中在哪个等级。Excel 用 openpyxl 引擎写支持多 sheet不用额外装 xlsxwriter。4. 避坑与排查古建筑病害检测里最容易翻车的五个地方4.1 裂缝检测把砖缝当裂缝现象、原因与解决现象检测结果里出现大量平行短线人工复核发现是砖墙的灰缝不是裂缝。原因Frangi 滤波器对任何线状暗纹都响应砖缝在图像里跟裂缝的形态特征高度相似。解决加一个方向一致性约束。裂缝走向通常不规则而砖缝方向一致且间距均匀。用霍夫变换检测直线如果候选区域内的线段方向方差小于阈值且间距规律就判定为砖缝排除。另外可以结合颜色特征灰缝颜色通常比裂缝浅且均匀。4.2 光照不均导致大面积误检现象、原因与解决现象墙面一侧被阳光直射、另一侧在阴影里阴影边缘被检测成剥落区域。原因CLAHE 虽然能均衡光照但强阴影边界处的梯度仍然很大被误判为区域突变。解决预处理阶段加一步 Retinex 或者同态滤波把光照分量和反射分量分离只对反射分量做检测。另外在分类器特征里加入局部对比度统计量阴影区域的对比度通常低于真实剥落。4.3 小样本下模型过拟合现象、原因与解决现象训练集准确率 95%验证集只有 60% 多。原因古建筑病害标注样本本身少而且不同建筑、不同病害类型的样本分布极不均匀。解决一是用数据增强但要注意古建筑图像的增强不能做水平翻转砖石纹理有方向性适合做亮度扰动、小角度旋转、局部裁剪。二是用预训练模型做特征提取只在后面接一个轻量分类头。三是分层采样保证每个 batch 里各类病害都有。4.4 立面图映射坐标对不上现象、原因与解决现象检测框在照片里位置准确映射到立面图后偏移明显。原因拍摄时镜头畸变没校正或者标定的角点不在同一平面上。解决先用cv2.calibrateCamera做镜头畸变校正再找角点。角点必须选在建筑同一立面上的实际角点不能选装饰构件上的点。如果建筑立面不是平面比如有凹凸单应变换只能近似这时候要分区域做局部映射。4.5 大批量照片处理内存溢出现象、原因与解决现象处理到第几十张照片时程序崩溃报 MemoryError。原因每张照片预处理后都保留在内存里加上中间特征图几百张 1024×1024 的图很快就把内存吃满。解决用生成器逐张读取和处理处理完立即释放。数据库写入用批量提交每 50 条 commit 一次。GUI 里只保留当前显示的一张图历史结果只存路径和检测数据不存图像数组。5. 进阶技巧用迁移学习把裂缝检测准确率再提一截前面讲的传统图像处理加浅层分类器在病害类型明确、拍摄条件可控的情况下够用。但如果想进一步提升准确率尤其是面对不同建筑、不同年代、不同拍摄设备带来的域偏移迁移学习是绕不过去的。我的做法是用一个在大型自然图像数据集上预训练过的卷积网络做特征提取器把中间层特征拿出来接一个针对古建筑病害微调的分类头。具体操作上不需要重新训练整个网络。把预训练模型的前面若干层冻结只训练最后几个卷积块和全连接层。学习率设小一点比如 1e-4用 Adam 优化器。数据量少的时候甚至可以把卷积层全部冻结只训练一个 SVM 或者逻辑回归分类器效果也比手工特征好不少。import torch import torchvision.models as models import torch.nn as nn def build_defect_classifier(num_classes5, freeze_backboneTrue): 基于预训练 ResNet 的病害分类器 num_classes: 裂缝/剥落/泛碱/苔藓/正常 共 5 类 freeze_backbone: 是否冻结卷积层 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) if freeze_backbone: for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 128), nn.ReLU(), nn.Linear(128, num_classes) ) return model # 训练配置 model build_defect_classifier(num_classes5, freeze_backboneTrue) criterion nn.CrossEntropyLoss() # 只优化 fc 层参数 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)freeze_backboneTrue的时候只训练最后两层全连接参数量从一千多万降到十几万小样本下几乎不会过拟合。如果数据量能到几千张以上可以把freeze_backbone设成 False学习率降到 1e-4做全网络微调。Dropout(0.3)是防止全连接层过拟合数据越少这个值可以越大但超过 0.5 会欠拟合。验证的时候不要只看准确率。病害检测是个不平衡分类问题正常区域远多于病害区域。要看每类的召回率和精确率尤其是裂缝这一类漏检比误检代价大得多。我一般会画混淆矩阵看裂缝被误分成什么如果大量裂缝被分成正常说明分类阈值需要调或者裂缝样本在训练集里权重不够。还有一个实际部署时的技巧把传统方法的高召回率和深度模型的高精确率结合起来。传统方法先圈出所有候选区域宁可多圈不要漏然后用深度模型对候选区域做精细分类。这样既控制了计算量又保证了不漏检。我试过在几百张测试图上这种组合策略比单独用深度模型端到端检测的 F1 分数高 8 到 10 个百分点。最后说一个我踩过的坑模型在实验室数据上表现很好拿到现场新拍的照片上准确率掉一大截。后来发现是白平衡差异。不同相机、不同光线条件下拍出来的墙面颜色偏差很大模型学到了颜色而不是纹理。解决办法是在训练时做颜色抖动增强或者预处理阶段加一步颜色恒常性校正。这个坑让我养成了一个习惯任何图像模型上线前一定拿不同设备、不同时间拍的数据做一次交叉验证不然就是自欺欺人。希望帮到你。本文还有配套的精品资源点击获取