简介:这份资源面向具备Python基础、对计算机视觉与深度学习有一定了解的研发人员、文物保护技术人员及研究生,提供一套古建筑表面病害辅助检测系统的完整工程实例。系统以YOLO目标检测模型为核心,结合OpenCV图像处理、FastAPI服务接口与SQLite结果存储,可自动识别并定位裂缝、剥落、盐析、霉斑、渗水等常见病害,覆盖图像采集、数据标注、模型训练、推理检测到结果可视化与人工复核的全流程,强调“模型识别+人工复核”的协同模式。资源包为1个docx文档,约112KB,内含项目背景、模型架构、数据集配置、训练与推理代码示例、检测服务接口实现及SQLite历史查询等章节,并附数据库与GUI设计说明。已有94人学习。读者可据此动手搭建可复核、可追踪的数字化病害档案,用于日常巡检、修缮前调查与过程质量检查,提升古建筑保护工作的智能化与标准化水平。
1. 古建筑表面病害检测:从人工锤敲到 Python 图像识别的落地路径
做古建筑修缮的同行大多有过这样的经历:一面青砖墙,老师傅拿着小锤逐块敲,靠声音判断空鼓,一天下来胳膊抬不起来,记录还全靠手绘草图。更麻烦的是,同一面墙不同人敲,结论可能对不上。这几年我一直在琢磨,能不能用 Python 加图像识别,把这件事做成一套辅助检测系统——不是替代老师傅,而是先把可疑区域圈出来,让人带着重点去复核。这套古建筑表面病害辅助检测系统,核心就是三件事:采集墙面图像、识别裂缝与剥落等病害、把结果落到数据库和界面上供人查看。它适合做文物数字化、建筑巡检、图像识别落地的人,也适合想拿一个完整项目练手的开发者。下面我按实际做下来的顺序,把选型、代码、参数和踩过的坑讲清楚。
2. 病害识别方案怎么选:传统图像处理还是深度学习
2.1 两类病害的成像特征决定了技术路线
古建筑表面病害常见的有裂缝、剥落、空鼓、泛碱、风化几类。其中裂缝和剥落在二维图像上特征最明显,也最适合做视觉检测。裂缝表现为细长的暗色线条,宽度从零点几毫米到几厘米不等,方向随机;剥落表现为成片的颜色突变区域,边界不规则,内部纹理和周围砖石差异大。
这两类目标的成像特征差异,直接决定了不能只用一套方法。裂缝是线状结构,对边缘和梯度敏感;剥落是面状区域,对颜色和纹理敏感。我一开始想用一套阈值分割全搞定,结果裂缝被当成噪声滤掉,剥落又和阴影混在一起,翻车得很彻底。后来改成两条支路:裂缝走边缘增强加形态学连接,剥落走颜色空间转换加区域生长。
传统方法的好处是不需要标注数据,参数调好就能跑,适合样本少的场景。缺点是泛化差,换一面墙、换一种光照,阈值就得重调。深度学习的好处是泛化强,但需要标注。实际项目里我一般这么分:如果病害类型固定、拍摄条件可控,传统方法够用;如果要覆盖多种建筑、多种光照,就上深度学习,用传统方法做预处理和数据增强。
2.2 最小可跑通的裂缝检测代码
先给一段能直接跑的裂缝检测,基于 OpenCV,思路是灰度化、CLAHE 增强对比度、Canny 边缘、形态学闭运算连接断点、轮廓筛选。
import cv2 import numpy as np def detect_cracks(img_path, min_area=80, max_width=40): # 读取图像并转灰度 img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # CLAHE 限制对比度自适应直方图均衡,clipLimit 控制增强强度 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) # 高斯滤波去噪,核大小必须是奇数 blurred = cv2.GaussianBlur(enhanced, (5, 5), 0) # Canny 双阈值,低阈值:高阈值约 1:2 到 1:3 edges = cv2.Canny(blurred, 40, 120) # 闭运算连接断裂的裂缝边缘,核越大连接越强但可能粘连 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=2) # 轮廓筛选:面积过滤噪声,外接矩形长宽比过滤块状区域 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result = img.copy() cracks = [] for cnt in contours: area = cv2.contourArea(cnt) if area < min_area: continue x, y, w, h = cv2.boundingRect(cnt) # 裂缝通常是细长形,短边过大的排除 if min(w, h) > max_width: continue cracks.append((x, y, w, h, area)) cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2) return result, cracks if __name__ == "__main__": out, boxes = detect_cracks("wall_sample.jpg") print(f"检出候选裂缝 {len(boxes)} 处") cv2.imwrite("crack_result.jpg", out)逻辑说明:CLAHE 解决古建筑墙面光照不均的问题,比全局直方图均衡更稳;Canny 的双阈值决定哪些梯度算边缘,低阈值抓弱边缘但会引入噪声,高阈值干净但会断线;闭运算把断开的裂缝边缘连起来,迭代次数越多连接越强,但两次以上容易把相邻裂缝粘成一片。参数说明:min_area是面积下限,拍近景时裂缝像素多可以调大,拍远景要调小;max_width是短边上限,用来排除块状区域,如果墙面本身有方形砖缝干扰,这个值要设得比砖缝宽度小。
2.3 剥落检测走颜色空间这条路
剥落区域和周围砖石的颜色差异,在 HSV 空间比 RGB 空间更稳定,因为 HSV 把色调和亮度分开了,光照变化主要影响 V 通道。做法是把图像转 HSV,对 H 和 S 通道做阈值分割,再对 V 通道做局部方差判断纹理突变。
def detect_spalling(img_path, h_range=(0, 30), s_min=40, v_var_thresh=120): img = cv2.imread(img_path) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h, s, v = cv2.split(hsv) # 色调和饱和度阈值,圈出偏色区域 mask_h = cv2.inRange(h, h_range[0], h_range[1]) mask_s = cv2.inRange(s, s_min, 255) mask = cv2.bitwise_and(mask_h, mask_s) # 局部方差反映纹理突变,剥落区纹理通常比砖石平滑或更杂乱 v_f = v.astype(np.float32) mean = cv2.blur(v_f, (15, 15)) sq_mean = cv2.blur(v_f * v_f, (15, 15)) variance = sq_mean - mean * mean mask_var = (variance > v_var_thresh).astype(np.uint8) * 255 final_mask = cv2.bitwise_and(mask, mask_var) # 开运算去小噪点 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) final_mask = cv2.morphologyEx(final_mask, cv2.MORPH_OPEN, kernel) return final_mask逻辑说明:H 通道阈值范围要根据砖石颜色定,青砖偏青蓝,红砖偏红黄,范围设错会把整面墙都圈进去。局部方差用积分图思路算,blur的核大小决定感受野,15 像素适合中近距离拍摄。参数说明:s_min是饱和度下限,泛碱区域饱和度低,这个值要调低才能抓到;v_var_thresh是方差阈值,纹理越复杂的墙面要设得越高。
3. 把检测结果落到数据库和 GUI:一套能交付的系统长什么样
3.1 数据库表结构设计
检测结果不能只存一张标注图,得能按建筑、按时间、按病害类型查。我用 SQLite 做本地存储,轻量、免安装,适合单机部署。核心三张表:建筑信息表、检测记录表、病害明细表。
-- 建筑信息表 CREATE TABLE building ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 建筑名称 location TEXT, -- 位置描述 build_year TEXT, -- 年代 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 检测记录表,一次检测对应一条 CREATE TABLE inspection ( id INTEGER PRIMARY KEY AUTOINCREMENT, building_id INTEGER, image_path TEXT, -- 原图路径 result_path TEXT, -- 标注图路径 inspect_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, operator TEXT, FOREIGN KEY (building_id) REFERENCES building(id) ); -- 病害明细表,一处病害一条 CREATE TABLE defect ( id INTEGER PRIMARY KEY AUTOINCREMENT, inspection_id INTEGER, defect_type TEXT, -- crack / spalling / efflorescence bbox_x INTEGER, bbox_y INTEGER, bbox_w INTEGER, bbox_h INTEGER, area REAL, -- 像素面积 severity TEXT, -- low / medium / high FOREIGN KEY (inspection_id) REFERENCES inspection(id) );逻辑说明:把建筑、检测、病害拆成三层,是为了支持「同一建筑多次检测对比」这个需求。病害明细里存 bbox 坐标而不是只存图,方便后续做面积统计和变化趋势分析。severity字段用规则算:裂缝按长度和宽度分级,剥落按面积占比分级。参数说明:area存像素面积,如果要换算实际面积,需要在采集时记录拍摄距离和相机焦距,用标定系数转换。
3.2 用 Tkinter 搭一个能用的界面
GUI 不需要花哨,能选图、能跑检测、能看结果、能存库就行。Tkinter 是 Python 自带的,不用额外装依赖,适合交付给非技术用户。
import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTk import sqlite3 import os class DefectApp: def __init__(self, root): self.root = root self.root.title("古建筑表面病害辅助检测") self.root.geometry("900x600") self.img_path = None self.conn = sqlite3.connect("inspection.db") self._build_ui() def _build_ui(self): # 顶部按钮区 top = tk.Frame(self.root) top.pack(fill=tk.X, padx=10, pady=8) tk.Button(top, text="选择图像", command=self.load_image).pack(side=tk.LEFT) tk.Button(top, text="裂缝检测", command=self.run_crack).pack(side=tk.LEFT, padx=6) tk.Button(top, text="剥落检测", command=self.run_spalling).pack(side=tk.LEFT, padx=6) tk.Button(top, text="保存记录", command=self.save_record).pack(side=tk.LEFT, padx=6) # 图像显示区 self.canvas = tk.Label(self.root, bg="#f0f0f0") self.canvas.pack(expand=True, fill=tk.BOTH, padx=10, pady=6) # 状态栏 self.status = tk.StringVar(value="就绪") tk.Label(self.root, textvariable=self.status, anchor=tk.W).pack(fill=tk.X, padx=10) def load_image(self): path = filedialog.askopenfilename(filetypes=[("图像", "*.jpg *.png *.bmp")]) if not path: return self.img_path = path img = Image.open(path) img.thumbnail((860, 480)) self.tk_img = ImageTk.PhotoImage(img) self.canvas.config(image=self.tk_img) self.status.set(f"已加载:{os.path.basename(path)}") def run_crack(self): if not self.img_path: messagebox.showwarning("提示", "请先选择图像") return # 调用第 2 章的检测函数,此处省略重复定义 out, boxes = detect_cracks(self.img_path) self._show_result(out) self.status.set(f"裂缝检测完成,候选 {len(boxes)} 处") def _show_result(self, bgr_img): rgb = bgr_img[:, :, ::-1] img = Image.fromarray(rgb) img.thumbnail((860, 480)) self.tk_img = ImageTk.PhotoImage(img) self.canvas.config(image=self.tk_img) def save_record(self): if not self.img_path: return cur = self.conn.cursor() cur.execute("INSERT INTO inspection (building_id, image_path) VALUES (?, ?)", (1, self.img_path)) self.conn.commit() self.status.set("记录已保存") if __name__ == "__main__": root = tk.Tk() app = DefectApp(root) root.mainloop()逻辑说明:界面分三层——按钮区触发动作,画布区显示图像,状态栏反馈进度。_show_result里做了 BGR 到 RGB 的转换,因为 OpenCV 读进来是 BGR,PIL 显示要 RGB,这一步漏了图会变色,是新手最常见的翻车点。参数说明:thumbnail的尺寸要小于画布,否则显示不全;数据库连接在__init__里建立,整个应用生命周期复用,不要每次操作都开关连接。
3.3 检测流程串起来
实际使用流程是:新建建筑记录 → 选择该建筑 → 导入图像 → 跑检测 → 人工复核修正 → 保存。人工复核这一步不能省,算法给的是候选,老师傅的判断才是最终结论。系统里要留一个「手动增删标注框」的入口,让复核结果能回写数据库。
4. 避坑与排查:那些让检测结果不可信的细节
4.1 现象:同一面墙两次拍摄,检测结果差很多
原因:光照变化导致 CLAHE 增强后的对比度不一致,Canny 阈值固定时,强光下边缘多、弱光下边缘少。解决:采集时用固定光源或加遮光罩,或者在检测前做一次亮度归一化,把图像平均亮度拉到固定值再处理。我一般会在预处理里加一步cv2.normalize,把灰度均值归到 128 附近。
4.2 现象:砖缝被大量识别成裂缝
原因:砖缝本身是线状暗纹,和裂缝的梯度特征高度相似。解决:两个方向。一是提高min_area,砖缝通常比裂缝短;二是加方向过滤,裂缝走向随机,砖缝走向规则,用霍夫变换检测主方向后排除规则排列的线段。如果墙面是规则砌筑,这个方法很有效。
4.3 现象:剥落检测把阴影圈进去
原因:阴影区域 V 通道低,局部方差也低,和剥落的特征重叠。解决:在 HSV 分割时加 V 通道下限,阴影的 V 值通常低于 40,剥落区不会那么暗。另外可以用形态学判断区域边界,阴影边界平滑,剥落边界破碎。
4.4 现象:数据库里病害坐标和原图对不上
原因:GUI 显示时做了缩放,但保存的是缩放后的坐标。解决:检测在原图上做,坐标存原图坐标系,显示时只缩放显示不缩放数据。这个坑我在第一个版本里踩过,存进去的框全偏了,后来统一规定「数据永远用原图坐标」才解决。
4.5 现象:程序跑一段时间内存涨
原因:OpenCV 的 Mat 对象和 PIL 的 Image 对象没及时释放,尤其是循环处理多张图时。解决:处理完一张图后显式del掉中间变量,或者把检测逻辑放到独立函数里,让局部变量随函数返回自动回收。Tkinter 里反复设置PhotoImage也要注意旧对象释放。
5. 进阶技巧:让辅助检测真正省人力的三个做法
第一个做法是批量处理和报告生成。单张检测只是演示,实际巡检一次几百张图,要能批量跑完并输出一份带缩略图和统计的 HTML 报告。用jinja2模板渲染,把每张图的标注结果和病害数量填进去,修缮队拿到报告就能直接排工期。批量处理时用concurrent.futures开多进程,图像检测是 CPU 密集型,多进程比多线程有效,进程数设成 CPU 核心数就行。
第二个做法是病害变化对比。同一建筑不同时期拍的照片,把两次检测的病害框对齐,算面积变化率。裂缝面积增长超过阈值就标红,提示需要优先处理。对齐用简单的 IoU 匹配,重叠度超过 0.3 认为是同一处病害。这个功能对长期监测特别有用,能看出哪面墙在恶化。
第三个做法是模型微调。传统方法跑一段时间后,把人工复核修正过的样本攒起来,标注成数据集,用轻量分割模型训练一版。我一般用 MobileNet 做 backbone 的 U-Net,输入 512×512,训练 50 轮左右,在自建数据上 IoU 能到 0.6 以上。推理时和传统方法做融合,两个都检出的区域置信度最高,只有一个检出的交给人工重点看。
# 批量处理示例,多进程加速 from concurrent.futures import ProcessPoolExecutor import glob def process_one(path): out, boxes = detect_cracks(path) save_path = path.replace(".jpg", "_result.jpg") cv2.imwrite(save_path, out) return path, len(boxes) if __name__ == "__main__": files = glob.glob("images/*.jpg") with ProcessPoolExecutor(max_workers=4) as ex: results = list(ex.map(process_one, files)) for path, n in results: print(f"{path}: {n} 处")逻辑说明:ProcessPoolExecutor把每张图的检测分发到独立进程,绕开 GIL 限制。max_workers设成 CPU 核心数,设太大反而因为进程切换开销变慢。参数说明:glob的模式按实际目录结构调整;如果图像很大,可以在process_one里先缩放再检测,速度会快很多,但坐标要按缩放比例还原。
这套系统我做下来最大的体会是:算法精度不是第一位的,流程闭环才是。检测、复核、存档、对比,每一步都打通,修缮队才愿意用。我自己的习惯是每做完一个版本,拿同一批图跑三遍,看结果稳不稳定,不稳定就先查预处理,别急着调模型。希望帮到你。
本文还有配套的精品资源,点击获取