ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLO的古建筑病害智能识别与辅助巡检系统

基于YOLO的古建筑病害智能识别与辅助巡检系统 简介基于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_limit0.15, contrast_limit0.15, p0.7), A.HueSaturationValue(hue_shift_limit5, sat_shift_limit15, val_shift_limit10, p0.5), A.GaussNoise(var_limit(5.0, 20.0), p0.3), A.MotionBlur(blur_limit(3, 7), p0.2), A.Resize(640, 640), ], bbox_paramsA.BboxParams(formatyolo, min_visibility0.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(imageimage, bboxesboxes)这段代码里RandomBrightnessContrast模拟早晚和阴影造成的光照变化MotionBlur模拟手持拍摄时的轻微抖动GaussNoise模拟低照度下的传感器噪点。bbox_params中的min_visibility0.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( datadata.yaml, epochs100, imgsz640, batch8, patience15, device0, workers4, optimizerAdamW, lr00.001, valTrue, ampTrue, projectruns/defect_detect, nameyolov8n_heritage_v1, )patience15表示连续 15 个 epoch 在验证集上没有提升就提前结束古建筑病害训练集通常只有几千到几万张很容易遇到瓶颈期这个参数能省下不少时间。lr00.001适合微调场景如果从头训练则可以用 0.01 起步。ampTrue开启混合精度训练显存能够容纳更大的 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( sourceimage_path, conf0.35, iou0.5, imgsz640, device0, )[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/loginPOST用户登录发放令牌/api/buildingsGET获取建筑档案列表/api/detection/uploadPOST上传图像并触发检测/api/detection/{task_id}GET查询检测结果/api/review/updatePOST回写人工复核结果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( sourceimage_path, confself.conf_thresh, iou0.5, imgsz640, device0, verboseFalse, )[0] # 按第 3.3 节的方式解析 result.boxes 并返回 defects 列表 return defectsdevice0表示第一块 GPU如果部署在没有 GPU 的机器上改成devicecpu。verboseFalse关闭预测时控制台日志输出避免接口日志被刷屏。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(titleHeritage 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 fstorage/{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: fBearer {self.token}} resp requests.post(http://127.0.0.1:8000/api/detection/upload, headersheaders, filesfiles, datadata) task_id resp.json()[task_id] detail requests.get(fhttp://127.0.0.1:8000/api/detection/{task_id}, headersheaders).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 阈值分段策略同一个模型在不同业务场景下应该使用不同的置信度阈值。日常巡检快速筛查阶段目标是把潜在病害都找出来conf0.25甚至更低是合理的生成正式档案前把阈值调到conf0.5可以减少把污渍、砖缝误收录进保护档案的情况。建议在 GUI 的检测参数区域开放置信度输入框让现场人员根据任务性质自由切换。使用阶段置信度阈值IOU 阈值目的快速筛查0.25 - 0.350.5减少漏检正式归档0.5 - 0.60.45减少误报人工复核0.35 - 0.50.5兼顾召回和准确率6.2 小目标与细长目标的切图策略裂缝这类目标面积小一张 4000×3000 的照片缩放到 640 后裂缝只有几个像素宽模型几乎无法学习。常见做法是推理阶段对原图做切块处理把大图切成 640×640 的重叠子图分别推理后再把检测框坐标映射回原图坐标系。切图时相邻子图保留 10% 到 20% 的重叠区域否则裂缝正好落在切缝位置就会被截断。这个逻辑同样适用于训练阶段对包含小病害的原图先切块再标注比直接整体缩放的训练效果好很多。6.3 样本回流与回归验证人工复核过程中积累的confirmed和corrected样本是模型迭代最稳定的数据来源。每收集到 300 到 500 张复核过的图像就按 8:2 分成新增训练集和验证集混合原有数据做一轮增量训练。在做回归验证时要关注新模型在旧验证集上的表现有没有下降尤其是裂缝这类容易过拟合的类别。为每一次评估生成一份对比报表记录新模型哪些检测框被保留、哪些被丢弃、哪些是新增确认没有出现「修复了 A 类误检但新增了 B 类误检」的情况后再更新服务端模型权重。这一步不需要频繁执行一个季度做一次就足够保持模型与现场环境同步。本文还有配套的精品资源点击获取
返回列表