简介:基于Python图像识别技术的古建筑表面病害辅助检测系统项目实例,面向具备Python基础、对计算机视觉与文物保护交叉领域感兴趣的研发人员和技术人员。资源完整展示了从图像采集、数据标注、YOLO模型训练、OpenCV预处理,到FastAPI服务接口、SQLite结果存储,以及GUI设计与人工复核的全流程,覆盖裂缝、剥落、盐析、霉斑、渗水等常见病害的自动识别与定位;内含完整程序代码、数据库设计、GUI界面与代码详解。压缩包内为1个docx文档,大小112KB,文档按项目背景、目标意义、挑战难点、模型架构、代码示例、应用领域等章节组织,配有详细注释和可视化结果,读者可据此复现系统并掌握工程化集成思路。目前已有94人学习浏览,适合用于古建筑日常巡检、修缮前调查、修缮过程质量检查等场景,也可作为数字化病害档案管理与风险评估的辅助工具。
1. 从巡检现场到目标检测:为什么古建筑病害识别需要一套辅助系统
砖木结构的裂缝、灰塑表面的盐析、彩绘层的起甲,这些病害在初期往往面积很小、颜色变化不明显,巡检人员需要在不同光照和角度下反复观察才能确认。靠人眼逐张翻照片,漏检率和工作强度都很高,而且不同人的记录口径不一致,同一处裂缝在不同月份的巡检记录里可能连名称都对不上。基于 Python 的图像识别方案把这项工作拆成了「算法初筛 + 人工复核」:YOLO 模型负责在照片里框出疑似病害区域,输出类别、置信度和位置坐标,保护人员再审一遍结果并修正误判。系统本身不替代专业判断,但能把大规模初筛、区域定位、数据归档和变化追踪这些重复劳动接过去。适合正在做古建筑数字化保护课题的研究生、需要提升巡检效率的文物保护单位技术人员,以及想了解目标检测如何落地到垂直行业的开发者。
2. 病害类别定义与数据准备:先让标注边界清晰,再谈模型
2.1 五类病害的视觉特征与标注边界
系统重点识别裂缝、剥落、盐析、霉斑和渗水五类病害,这五类在古建筑表面经常同时出现,标注规则稍微模糊一点,模型训练出来的特征就会互相串。我把每类病害的典型形态和容易混淆的点整理成一张表,标注前先让所有人按这个口径统一意见。
| 病害类别 | 典型视觉特征 | 易混淆情况 |
|---|---|---|
| 裂缝 | 细长、连续,可能有分叉或断裂,边缘锐利 | 砖石表面的自然纹理、木构件年轮线 |
| 剥落 | 表面材料缺失,边缘破损,颜色突变 | 阴影遮挡造成的局部暗区 |
| 盐析 | 浅色颗粒、条带或片状结晶,常出现在砖缝和墙根 | 白色涂料残留、反光区域 |
| 霉斑 | 黑色、绿色或灰色不规则斑块,边界晕染 | 渗水后留下的水渍 |
| 渗水 | 颜色加深,边界模糊,常伴随水迹和污渍 | 雨后未干区域、材质本身颜色差异 |
标注规则上需要约定主病害和伴生现象的关系。比如渗水区域同时出现盐析时,应该把盐析作为一个独立目标框出来而不是合并进渗水框;霉斑覆盖在剥落表面时,两个类别各标各的框,交叠区域不去强行切割。如果同一张图里病害边界实在模糊,宁可只标确定的部分也不要扩大标注范围,否则模型在后面学到的边界是虚的。
2.2 数据采集规范:控制变量比堆数量更重要
数据质量直接决定模型能学到什么。拍摄距离、角度、分辨率、文件命名这几项必须在采集阶段固定下来,否则训练集里的图像尺度差异会大得离谱。常见的做法是要求巡检人员对同一处病害至少拍两张:一张整体环境照记录病害所在的建筑部位,一张局部特写照用于识别纹理细节。文件命名建议采用「建筑编号_构件类型_拍摄日期_序号」的格式,例如temple03_wall_20250412_001.jpg,这样标注文件和检测结果在溯源时能直接对上原始记录。
光照条件是古建筑巡检里最难控制的变量。同一处盐析在晴天和阴天呈现的颜色完全不同,裂缝在强光下容易被阴影盖掉。采集阶段要有意识地覆盖不同季节、不同时间段、不同天气下的样本,尤其是把那些模型容易误判的困难样本保留下来,而不是只挑拍得干净的照片进数据集。
2.3 图像预处理与数据增强:让模型见过足够多的环境变化
数据增强要做的是模拟真实巡检中的光照变化、轻微模糊和噪声干扰,而不是把图像改得面目全非。以下是基于 OpenCV 和 Albumentations 的增强示例:
import cv2 import albumentations as A def build_augment_pipeline(): return A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=0.7), A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=15, val_shift_limit=10, p=0.5), A.GaussNoise(var_limit=(5.0, 20.0), p=0.3), A.MotionBlur(blur_limit=(3, 7), p=0.2), A.Resize(640, 640), ], bbox_params=A.BboxParams(format="yolo", min_visibility=0.35)) pipeline = build_augment_pipeline() image = cv2.imread("temple03_wall_20250412_001.jpg") boxes = [[0.42, 0.55, 0.08, 0.03]] # YOLO格式: cx, cy, w, h augmented = pipeline(image=image, bboxes=boxes)这段代码里RandomBrightnessContrast模拟早晚和阴影造成的光照变化,MotionBlur模拟手持拍摄时的轻微抖动,GaussNoise模拟低照度下的传感器噪点。bbox_params中的min_visibility=0.35关键,它保证增强后的目标框与图像重叠面积低于 35% 时直接丢弃该样本,避免数据增强把病害区域裁掉一半、模型却按照完整目标去学。把增强后的图像和标注统一缩放到 640×640 输入 YOLO。
3. YOLO 模型训练与推理:从 YAML 配置文件到检测框输出
3.1 数据集组织与 data.yaml 配置
Ultralytics YOLO 直接把数据集路径写在 YAML 文件里,训练和推理都会按这个配置读取。目录结构建议采用标准布局:顶层放images和labels两个目录,各自拆分成train、val、test三个子目录。项目的标注格式为 YOLO 格式,每个图像对应一个同名 txt 文件,每一行是「类别ID cx cy w h」,坐标值都归一化到 0 到 1 之间。
# data.yaml path: ./heritage_defect_dataset train: images/train val: images/val test: images/test nc: 5 names: 0: crack 1: spall 2: efflorescence 3: mold 4: water_penetrationnc代表类别数量,必须和names列表长度一致,否则训练时会直接报 shape mismatch。训练前先检查一遍每张图像的标注框有没有出现cx、cy超出 0 到 1 范围的情况,常见原因是标注工具导出设置错误,这个问题会导致 loss 跑飞或者模型收敛到错误位置。
3.2 训练脚本与超参数
训练过程使用预训练权重做迁移学习,骨干网络参数已经有了通用视觉特征,我们主要让它学习古建筑表面病害的特殊纹理。
from ultralytics import YOLO model = YOLO("yolov8n.pt") # 从预训练权重开始 results = model.train( data="data.yaml", epochs=100, imgsz=640, batch=8, patience=15, device=0, workers=4, optimizer="AdamW", lr0=0.001, val=True, amp=True, project="runs/defect_detect", name="yolov8n_heritage_v1", )patience=15表示连续 15 个 epoch 在验证集上没有提升就提前结束,古建筑病害训练集通常只有几千到几万张,很容易遇到瓶颈期,这个参数能省下不少时间。lr0=0.001适合微调场景,如果从头训练则可以用 0.01 起步。amp=True开启混合精度训练,显存能够容纳更大的 batch。病害形态差异大,小目标集中在裂缝这类细长物体上,推荐先用yolov8n或yolov8s跑通流程,确认 loss 曲线没有异常后再换成yolov8m提升精度。
3.3 推理与结果解析:拿到检测框后还要算什么
模型输出的是归一化坐标和置信度,业务上还需要知道病害面积占比和具体像素位置。
from ultralytics import YOLO model = YOLO("runs/defect_detect/yolov8n_heritage_v1/weights/best.pt") image_path = "sample_images/temple03_wall_20250412_001.jpg" result = model.predict( source=image_path, conf=0.35, iou=0.5, imgsz=640, device=0, )[0] img = cv2.imread(image_path) img_h, img_w = img.shape[:2] total_area = img_h * img_w defects = [] for box in result.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = box.xyxy[0].tolist() x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) area_ratio = (x2 - x1) * (y2 - y1) / total_area defects.append({ "category": result.names[cls_id], "confidence": round(conf, 4), "bbox": [x1, y1, x2, y2], "area_ratio": round(area_ratio, 6), })上面的代码将xyxy转成绝对像素坐标,area_ratio计算检测框面积占整个图像面积的比例。需要注意的是这个值是一个粗略估计,检测框会把病害周围的正常表面也算进去,对于裂缝这类细长目标,框的面积往往远大于实际病害面积。如果要更精确的面积估计,需要在后续升级语义分割模型。快速筛查阶段conf建议设置在 0.3 到 0.4 之间,避免漏掉早期病害;正式归档阶段可以提高 0.5 以上,减少误报进档案。
3.4 类别混淆问题
裂缝和砖石自然纹理在视觉上极易混淆。我在实验中发现,裂缝样本的标注框如果过于贴近砖缝边缘,模型会把砖缝当成裂缝。缓解方式有两种:一是收集大量砖缝、木纹、污渍的负样本单独建一个background类别,让模型学会区分;二是推理阶段对裂缝类别的输出做一个形态学后处理,检测框内的灰度梯度如果连续变化过于平缓,可以降权处理。这属于工程折中方案,经过负样本扩充后,模型的表现提升是比较明显的。
4. 服务接口与数据持久化:FastAPI 把模型包成可调用服务
4.1 接口规划与数据表设计
模型训练完成后需要被前端界面和外部系统调用。我选用 FastAPI 构建接口层,采用 SQLite 保存默认检测记录,用 MySQL 保存用户、角色、建筑档案等业务数据。两者分工明确:推理服务保持轻量,业务管理走完整数据库方案。
核心接口包括登录认证、图像上传与检测任务提交、任务结果查询、人工复核结果回写。下面的表格列出这些接口的路径和职责。
| 接口路径 | 方法 | 职责 |
|---|---|---|
| /api/auth/login | POST | 用户登录,发放令牌 |
| /api/buildings | GET | 获取建筑档案列表 |
| /api/detection/upload | POST | 上传图像并触发检测 |
| /api/detection/{task_id} | GET | 查询检测结果 |
| /api/review/update | POST | 回写人工复核结果 |
SQLite 表结构主要包含三张表:detection_task保存每次检测任务的上传时间、模型版本、原始图像路径和结果图像路径;detection_result保存每个检测框的类别、置信度和坐标;review_record保存人工复核后的状态和修正内容。检测结果表通过task_id关联到任务表,复核记录又通过result_id关联到具体检测框,三层关系把「模型输出」和「人工修正」这两套数据隔离,原始识别结果不会被覆盖。
4.2 模型加载与推理服务封装
模型加载是一个开销较高的操作,不能每来一个请求就重新加载一次。将模型实例化放在服务启动阶段,全局复用。
import cv2 import numpy as np from ultralytics import YOLO class InferenceService: def __init__(self, weights_path: str, conf_thresh: float = 0.35): self.model = YOLO(weights_path) self.conf_thresh = conf_thresh def predict(self, image_path: str): result = self.model.predict( source=image_path, conf=self.conf_thresh, iou=0.5, imgsz=640, device=0, verbose=False, )[0] # 按第 3.3 节的方式解析 result.boxes 并返回 defects 列表 return defectsdevice=0表示第一块 GPU,如果部署在没有 GPU 的机器上改成device="cpu"。verbose=False关闭预测时控制台日志输出,避免接口日志被刷屏。
4.3 FastAPI 路由实现:登录、上传与检测、结果查询
上传检测接口需要同时接收 multipart 表单中的图片文件和任务元数据。保存原始图片后调用InferenceService得到缺陷列表,再写入 SQLite。
import uuid import sqlite3 from fastapi import FastAPI, UploadFile, File, Form, Depends, HTTPException from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials app = FastAPI(title="Heritage Defect Detection API") security = HTTPBearer() def get_db(): conn = sqlite3.connect("defect_detection.db") conn.row_factory = sqlite3.Row return conn @app.post("/api/detection/upload") async def upload_detection( file: UploadFile = File(...), building_id: str = Form(...), component: str = Form(...), cred: HTTPAuthorizationCredentials = Depends(security), ): # 校验用户令牌,无效则抛出 401 task_id = str(uuid.uuid4()) suffix = file.filename.rsplit(".", 1)[-1] raw_path = f"storage/{task_id}.{suffix}" with open(raw_path, "wb") as f: f.write(await file.read()) defects = inference_service.predict(raw_path) conn = get_db() conn.execute( "INSERT INTO detection_task (task_id, building_id, component, raw_path, model_version, created_at) " "VALUES (?, ?, ?, ?, ?, datetime('now'))", (task_id, building_id, component, raw_path, "yolov8n_heritage_v1"), ) for d in defects: conn.execute( "INSERT INTO detection_result (result_id, task_id, category, confidence, bbox_x1, bbox_y1, bbox_x2, bbox_y2, area_ratio, review_status, created_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))", (str(uuid.uuid4()), task_id, d["category"], d["confidence"], *d["bbox"], d["area_ratio"], "pending"), ) conn.commit() return {"task_id": task_id, "defect_count": len(defects)}这段代码的核心意图在数据写入时就把review_status初始化为pending,保证每条检测结果都进入人工复核队列。building_id和component是关联到建筑档案的字段,后续查询「某面墙的裂缝在半年内有没有变大」就依赖这两个字段建立索引。storage/{task_id}.{suffix}用任务 ID 重命名原始图片,避免不同批次上传的同名文件互相覆盖。
4.4 人工复核与结果回写
人工复核本质是对检测结果做状态流转。保护人员看见某个检测框是模型误报,可以修改类别或直接标记为「无效」。
@app.post("/api/review/update") def update_review( result_ids: list[str], review_status: str, comment: str = "", cred: HTTPAuthorizationCredentials = Depends(security), ): conn = get_db() for rid in result_ids: conn.execute( "UPDATE detection_result SET review_status = ?, review_comment = ?, review_at = datetime('now') " "WHERE result_id = ?", (review_status, comment, rid), ) conn.commit() return {"updated": len(result_ids)}review_status建议使用pending、confirmed、rejected、corrected四个枚举值。confirmed是保护人员确认模型正确,corrected是修改了类别或坐标,rejected是删除误报框。把修正过的样本周期性导回训练集重新微调,是提升模型第二轮表现最直接的手段。review_comment字段不要省略,标注人员填写误判原因,后续分析错误样本时这个字段价值很大。
5. 桌面 GUI 与人工复核工作流:让不熟悉命令行的用户也能操作
5.1 GUI 整体结构与登录界面
模型的直接使用人群是文物保护单位的技术人员,不能要求他们敲命令。桌面端我一般选用 PySide6 构建:登录界面、主界面、建筑档案选择、图片选择、结果展示、历史记录查询、人工复核,总共五个页面,通过栈式布局管理页面跳转。登录界面在进入主界面前校验用户名和密码,调用后端/api/auth/login接口,拿到令牌后全局保存。
import sys import requests from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLineEdit, QPushButton, QMessageBox class LoginWindow(QMainWindow): def __init__(self, switch_callback): super().__init__() self.switch_callback = switch_callback self.setWindowTitle("古建筑病害辅助检测系统") self.setFixedSize(320, 180) layout = QVBoxLayout() self.username_input = QLineEdit() self.username_input.setPlaceholderText("用户名") self.password_input = QLineEdit() self.password_input.setPlaceholderText("密码") self.password_input.setEchoMode(QLineEdit.Password) login_btn = QPushButton("登录") login_btn.clicked.connect(self.handle_login) layout.addWidget(self.username_input) layout.addWidget(self.password_input) layout.addWidget(login_btn) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def handle_login(self): resp = requests.post("http://127.0.0.1:8000/api/auth/login", json={"username": self.username_input.text(), "password": self.password_input.text()}) if resp.status_code == 200: self.token = resp.json()["token"] self.switch_callback() else: QMessageBox.warning(self, "登录失败", "用户名或密码错误")登录逻辑的重点在于QLineEdit.EchoMode.Password隐藏用户输入的密码,请求失败时通过QMessageBox给出明确反馈。桌面应用通过 HTTP 调用本地服务时,地址通常写127.0.0.1而不是内网 IP,减少部署时防火墙干扰。
5.2 图片选择与检测结果展示
主界面包含建筑档案下拉框、图片选择按钮和检测结果表格。用户选择图片后,界面把图片文件和建筑 ID、构件类型一起通过/api/detection/upload上传,轮询任务状态拿到检测结果后,在表格里展示类别、置信度和坐标,同时在右侧用 OpenCV 在图像上绘制检测框。
# 主界面上传并展示检测结果 def on_select_and_detect(self): file_path, _ = QFileDialog.getOpenFileName(self, "选择巡检照片", "", "Images (*.jpg *.png)") if not file_path: return with open(file_path, "rb") as f: files = {"file": ("photo.jpg", f, "image/jpeg")} data = {"building_id": self.building_combo.currentData(), "component": self.component_input.text()} headers = {"Authorization": f"Bearer {self.token}"} resp = requests.post("http://127.0.0.1:8000/api/detection/upload", headers=headers, files=files, data=data) task_id = resp.json()["task_id"] detail = requests.get(f"http://127.0.0.1:8000/api/detection/{task_id}", headers=headers).json() self.result_table.setRowCount(len(detail["defects"])) for i, d in enumerate(detail["defects"]): self.result_table.setItem(i, 0, QTableWidgetItem(d["category"])) self.result_table.setItem(i, 1, QTableWidgetItem(str(d["confidence"]))) self.result_table.setItem(i, 2, QTableWidgetItem(str(d["bbox"]))) self.result_table.setItem(i, 3, QTableWidgetItem(str(d["area_ratio"])))表格展示的是检测框的坐标和面积占比,保护人员需要能一眼看到这张照片里最严重的病害。可以在area_ratio一栏按降序排序,把覆盖面最大的病害排在最前面。还要在界面显著位置标注模型版本,当多个模型版本并存时,保护人员反馈的误检才能准确回溯到具体版本,避免把问题归错到已修复的旧模型上。
5.3 人工复核界面的状态流转
复核界面在结果表格上增加「确认」「误报」「修正」三个按钮。选中一行结果后,点击「误报」调用/api/review/update,把该结果标记为rejected,同时表格行背景变成灰色;点击「修正」弹出类别选择框,允许把模型预测的类别改掉。
确认(confirmed) -> 状态变绿,加入已确认列表 误报(rejected) -> 状态变灰,从统计报表中剔除 修正(corrected) -> 弹出类别选择,覆盖原预测类别并记录复核人意见每次复核操作都必须记录操作人账号和操作时间。多个人同时使用同一个账号复核,后续出了问题无法定位是谁改的。在数据库表设计阶段就加入review_by和review_at字段,前端界面显示当前登录用户名,复核提交时一并传给后端。
6. 部署调试与持续优化:阈值策略、切图推理和样本回流
6.1 阈值分段策略
同一个模型在不同业务场景下应该使用不同的置信度阈值。日常巡检快速筛查阶段,目标是把潜在病害都找出来,conf=0.25甚至更低是合理的;生成正式档案前,把阈值调到conf=0.5,可以减少把污渍、砖缝误收录进保护档案的情况。建议在 GUI 的检测参数区域开放置信度输入框,让现场人员根据任务性质自由切换。
| 使用阶段 | 置信度阈值 | IOU 阈值 | 目的 |
|---|---|---|---|
| 快速筛查 | 0.25 - 0.35 | 0.5 | 减少漏检 |
| 正式归档 | 0.5 - 0.6 | 0.45 | 减少误报 |
| 人工复核 | 0.35 - 0.5 | 0.5 | 兼顾召回和准确率 |
6.2 小目标与细长目标的切图策略
裂缝这类目标面积小,一张 4000×3000 的照片缩放到 640 后,裂缝只有几个像素宽,模型几乎无法学习。常见做法是推理阶段对原图做切块处理:把大图切成 640×640 的重叠子图,分别推理后再把检测框坐标映射回原图坐标系。切图时相邻子图保留 10% 到 20% 的重叠区域,否则裂缝正好落在切缝位置就会被截断。这个逻辑同样适用于训练阶段,对包含小病害的原图先切块再标注,比直接整体缩放的训练效果好很多。
6.3 样本回流与回归验证
人工复核过程中积累的confirmed和corrected样本,是模型迭代最稳定的数据来源。每收集到 300 到 500 张复核过的图像,就按 8:2 分成新增训练集和验证集,混合原有数据做一轮增量训练。在做回归验证时,要关注新模型在旧验证集上的表现有没有下降,尤其是裂缝这类容易过拟合的类别。为每一次评估生成一份对比报表,记录新模型哪些检测框被保留、哪些被丢弃、哪些是新增,确认没有出现「修复了 A 类误检但新增了 B 类误检」的情况后,再更新服务端模型权重。这一步不需要频繁执行,一个季度做一次就足够保持模型与现场环境同步。
本文还有配套的精品资源,点击获取