
墙面病害检测这个方向我前前后后折腾了大半年最头疼的其实不是模型选型而是数据。模型现在开源的一大把随便挑一个都能跑但真正决定上限的永远是喂进去的数据长什么样。这次我想认真聊一聊我们做的一个建筑物墙面混凝土缺陷检测数据集——4024张图、5个类别、VOC和YOLO两种标注格式双份输出整个从采集、清洗、标注到格式转换、训练验证的过程。如果你是刚接触这个领域或者正准备自己攒数据训练缺陷检测模型这篇文章应该能帮你少走不少弯路。先说下这次数据集解决的痛点混凝土墙面上的裂纹、空洞、腐蚀这类缺陷不像目标检测通用数据集里那些猫猫狗狗它们往往颜色对比度低、边界模糊、光照变化强而且缺陷尺寸跨度极大——一条细裂纹可能只有几个像素宽一个腐蚀区域可能占据大半面墙。早期我们试用过几个公开数据集不是类别太少就是标注质量参差不齐更麻烦的是格式混乱训练前光转格式就折腾了一周。所以这次我们干脆自己动手从现场采集到标注规范全部重新定义最终产出了这么一套东西希望能给同样被数据折磨的人一点参考。1. 这5个类别是怎么定下来的裂纹、空洞、腐蚀之外的现实考量很多人拿到数据第一反应是标注但我想先说类别定义。类别定义错了后面全白干返工成本极高。我们在项目早期其实走过一段弯路。最初需求方给的清单是“裂缝、蜂窝麻面、露筋、空鼓、剥落”这几个土木工程术语听着很专业但真到标注的时候就发现完蛋了——很多缺陷在照片里是重叠伴生的比如钢筋锈蚀会膨胀膨胀把混凝土撑裂裂缝边上一定范围又有锈迹污染这时候你让标注员判断这张图是“露筋”还是“裂缝”还是“锈蚀”三个人能标出三个版本。后来我们做了一轮类别合并和边界定义把5类定为裂纹线状开裂粗细不限但必须有连续性。空洞表面可见的孔洞、凹陷包括蜂窝麻面中较集中的孔洞区域。腐蚀混凝土表面或钢筋区域的锈蚀、颜色发黄发褐、酥松脱落等。剥落表层混凝土成片脱落露出内部骨料或钢筋。渗析墙面上的白色析出物、水渍痕迹这类往往预示着内部渗水或化学侵蚀。注意我把很多传统分类里的“露筋”合并到“腐蚀”里了因为露筋通常是伴随剥落和腐蚀出现的单独设类会导致同一个目标被重复框选。而“渗析”是我们额外加的因为在南方潮湿地区这类现象太常见了而且它确实能反映结构病害的早期信号。给标注员的边界规则是这样的裂纹框只框主裂纹分支裂缝如果与主裂缝间隔小于5个像素并入主框如果分支较长且独立单独标注。空洞区域的框必须完全包裹孔洞边缘允许包含少量周围完好区域但不得超过孔洞直径的10%。腐蚀和剥落同时出现时按“剥落优先”原则只标剥落框。渗析区域如果有明显分界线按渗析区域标如果水渍边缘模糊框取颜色最深的核心区域。这个规则表我们打印出来贴在标注工位上每次QA争议都对着表看三个月下来标注一致性从最初的62%提升到了88%算是我觉得这次项目里最值的一笔投资。2. 采集与初筛不是拍了照片就能用数据来源我们分为三个渠道委托现场拍摄、公开建筑图片补充、以及我们自己拿手机在几个改造工地的随手记录。三个渠道比例大概是6:3:1。现场拍摄用的是普通工程相机加手机没上什么专业设备。但有一个关键细节必须离墙面足够近。很多人拍建筑外观喜欢拍全景那种图对检测没什么用。我们要求的拍摄距离是0.5米到2米之间保证缺陷区域在画面中占比不低于10%。加上光线条件阴天和晴天反差很大所以我们刻意在不同时段、不同天气下补拍让模型见过足够多的光照变化。采集回来的原始照片一共7000多张但真正进入标注流程的只有4024张。筛掉的是什么过曝或严重欠曝的细节完全丢失标注出来模型也学不到东西。视角过于倾斜的墙体透视形变严重框出来的缺陷位置会偏移。缺陷占比太小的那种远远拍一整面楼裂缝只有一条线这种图标了等于没标。同一场景重复度过高的我们保留最多3张不同角度防止模型过拟合单一场景。另外做了一件很多数据集没做的事困难样本的定向补充。我们发现第一轮初筛后裂纹类别占比超过60%渗析不到5%类别极度不均衡。于是又专门去拍了一批墙面渗析和空洞的近景照片最后把渗析类别补到了接近800张。最终五个类别的数量分布大概是类别图片数约实例数约占比裂纹1120186027.8%空洞69098017.2%腐蚀880145021.9%剥落64082015.9%渗析69476017.2%总数4024张这个数量级不算大但对缺陷检测来说够用了尤其是配合数据增强之后。实例数比图片数多是因为很多图里不止一处缺陷。3. VOC和YOLO标注格式互转你以为的简单其实暗坑不少网上很多教程说VOC转YOLO就是解析XML然后除以宽高真的只写两行代码就能跑但跑完你会发现问题一堆。先说明这两个格式的核心区别VOC格式用XML存信息每个目标由bndbox下的xmin, ymin, xmax, ymax表示是绝对像素坐标。YOLO格式用txt存信息每行是类别id 中心x 中心y 宽 高这四个值全部是相对图像的归一化坐标0到1之间。从VOC转YOLO的公式看起来确实简单x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height但我实际转的时候踩了三个坑坑一PASCAL VOC的坐标是1-indexedYOLO是0-indexed。严谨的VOC标注工具xmin最小是1而不是0转换时最好先把所有坐标减1再做归一化否则框会整体偏移一个像素。对目标检测来说单个像素偏移问题不大但强迫症和数据洁癖患者会很难受。坑二图片的实际宽高和XML里记录的宽高可能不一致。我们从采集设备导出的部分照片被自动旋转过EXIF里有方向信息但XML里写的是旋转前或旋转后的值一旦对不上转换后的框全部错位。解决方案简单粗暴转格式前先统一重新保存一遍所有图片把EXIF剥掉确保实际像素尺寸和XML里的一致。坑三YOLO格式的类别id必须是从0开始的连续整数。如果你在VOC里用的是类似crack1, hole2这种带数字的名字解析时会得到乱七八糟的id。我建议不管你是从哪个工具导出的标注都写一段校验脚本转换后把YOLO标注画回到图片上人工抽查。下面这个是我用的核验代码读取txt框并画出来保存为预览图import cv2 import numpy as np # 读取YOLO标注并可视化 def visualize_yolo(img_path, txt_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_center float(parts[1]) * w y_center float(parts[2]) * h box_w float(parts[3]) * w box_h float(parts[4]) * h x1 int(x_center - box_w / 2) y1 int(y_center - box_h / 2) x2 int(x_center box_w / 2) y2 int(y_center box_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, class_names[cls_id], (x1, max(y1 - 5, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1) cv2.imwrite(check_ txt_path.replace(.txt, .jpg), img) if __name__ __main__: # 示例检查单张 visualize_yolo(data/images/001.jpg, data/labels/001.txt, [crack, hole, corrosion, spall, efflorescence])这段代码不需要装额外库有opencv就行。跑完后到目录里随机拉几十张图肉眼扫一遍有没有框偏、框错、漏框基本就能发现格式问题。4. 训练YOLO模型的实操记录数据集划分与参数选择数据准备好了接下来是训练。我们首选YOLOv8实话说是因为生态成熟、文档全、社区踩坑案例多做工程的人没时间天天去看新版论文。数据集目录结构建议这样组织dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml其中data.yaml内容如下path: ./dataset train: images/train val: images/val test: images/test nc: 5 names: [crack, hole, corrosion, spall, efflorescence]有人问为什么不用全部4024张都训练当然要留出验证集和测试集。我们按6:2:2划分并且划分的时候做了分层采样——保证每个类别在训练、验证、测试里的比例和总体比例基本一致。随机划分的坑在于如果你某种缺陷只集中在某个拍摄地点随机划分很容易让验证集里全是那个地点的图造成验证分数虚高或虚低。我们按拍摄地点做了分组同一个地点的图片尽量只出现在同一个集合里避免模型“认场地”不“认缺陷”。训练命令我用的是yolo train modelyolov8m.pt datadata.yaml epochs200 imgsz640 batch16 optimizerAdamW lr00.001模型选的是yolov8m而不是s理由是缺陷目标小、长宽比极端裂纹就是细长条大一点的模型特征提取能力更强。实测下来m比s的mAP50高大概4个点推理速度快那几十毫秒对墙面检测场景完全无所谓。训练过程要关注两条曲线的差距train_loss和val_loss。如果train_loss一直降、val_loss先降后升那就是过拟合信号赶紧调低epoch或者加正则。我们第一轮跑了400个epoch到260轮已经明显过拟合了后来收敛到200轮。还有个容易忽略的点数据增强参数。YOLO自带的增强会自动做旋转、缩放、翻转但墙面缺陷检测里一个上下翻转是没问题的因为天花板和地面都有可能出现但90度旋转要慎重——方向性太强比如竖向裂纹旋转后变成横向语义没变但如果你未来部署时的图片方向是固定的没必要引入太多无谓旋转。我在yolo的augment参数里把degrees从默认的0.0调到了5.0只允许小角度旋转。5. 评估结果与模型陷阱mAP很高但现场一测就露馅训练完常规指标是这样的类别mAP50mAP50-95裂纹0.7820.468空洞0.8430.546腐蚀0.7910.503剥落0.8230.517渗析0.8630.561总体0.8200.519数字看着还行但放到现场一个遮阳棚下的阴影区域裂纹的召回直接掉到0.4。为什么因为测试集里阴影占比很少模型没充分见过这类样本。另外我们还发现一个特别典型的问题对模糊图像模型的预测框会抖。给同一张图做5次推理开TTA每次框的位置都可能偏移几个像素置信度也有波动。这是因为缺陷边缘本身是渐变过渡的不像人、车有清晰轮廓。这个问题目前没有特别好的根治方案我们能做的是在部署端做多帧融合——对连续视频帧的检测结果做非极大值抑制同一位置连续出现3帧以上才输出误报率能下降一截。评估还有一个坑别只看mAP0.5。我在项目后期发现裂纹这种细长目标框和真实框的IoU经常在0.4到0.6之间晃荡对mAP50而言算是正确检测但mAP50-95上不去意味着框的定位精度很差。如果要定位就得上一些后处理比如对裂纹做形态学细化把矩形框转成polygon。如果你只做“有没有问题”的定性判断那mAP50够用。6. 实际部署从离线检测到实时上报模型训练完最终要落到使用场景。我们把检测服务封装成一个HTTP接口用Flask起服务模型用YOLO的export导出为ONNX再用onnxruntime推理单帧处理时间在CPU上大概300msGPU上30ms左右。墙面巡检是慢慢扫描的不需要特别高的帧率所以CPU完全够用。当时设计的时候考虑过一个路线直接把YOLO的PyTorch模型部署。但生产环境装一套PyTorch太重而且版本管理麻烦。ONNX导出很简单yolo export modelbest.pt formatonnx opset12但我导出后踩了一个小坑YOLOv8默认导出是动态batch还是固定batch1都行但onnxruntime下如果输入尺寸不是640x640推理时间会明显变长。我们的照片都是1920x1080检测前要resize这个resize也消耗时间。后来我把输入分辨率改成1280x1280裂纹检出的召回提升了大约3.2个百分点代价是单帧推理时间翻倍。权衡后在实际巡检时我们先用640x640快速初筛发现疑似缺陷后对缺陷区域裁剪放大再做一次细检这样速度和精度都保住了。关于部署环境我强烈建议用Docker固定环境。我们吃过一次亏某台机器上onnxruntime安装的是CPU版跑起来慢得离谱排查半天才发现是环境变量问题。7. 关于这个数据集和后续扩展的一些想法4024张图说多不多说少不少够用来做原型验证但如果想要模型在工业场景里更耐用这个数据量还是要继续涨。我个人的规划是下一步用半监督学习的方式用当前模型去给未标注的现场视频自动打伪标签再由人工修正把数据量扩到1万张级别。已经有几篇工作证实这种方式在缺陷检测上的收益比单纯加数据增强更大。另外这套数据的扩展方向有几个增加缺陷的分割标注检测框只能告诉你“这里有缺陷”但修复工程量需要的是缺陷面积这时候要segmentation模型。我们正在选一部分图像做像素级标注做成半分割数据集。增加多尺度检测无人机拍摄的高度变化导致同一缺陷在不同图片里尺度可能差10倍以上。专门拍一批远景图训练多尺度鲁棒性。增加视频时序信息静态图片检测容易漏掉细微裂纹视频连续帧可以提供更多线索。我们在测试用SlowFast加2D检测器的组合但目前效果尚未超过简单多帧投票。最后再说点实在的体会。做缺陷检测最难的其实不是模型而是对“缺陷到底是什么”的认知统一。不同的人看同一张墙面照片裂纹和划痕的边界、腐蚀和污渍的边界永远存在争议。我们最后能达成88%的一致性靠的是那页标注规则。所以如果你也想做一个类似的数据集我真心建议先花两周时间把标注规范写透、标50张试行版、再全员评审这比后面反复返工省力得多。数据格式方面VOC和YOLO双格式看起来只是两种存储方式但在实际工程里VOC适合做人工检查和转换脚本YOLO适合直接喂给训练框架两边各留一份团队协作会舒服很多。我们这次就是把VOC作为“主格式”所有人工修改都改XML然后通过脚本同步生成YOLO版本再辅以上面那个可视化核验脚本到现在还没有出现过训练时框错位的低级事故。这也算是我这次项目最想分享的实操经验了。