做了几年计算机视觉相关的工作我接过不少“看着简单、做起来抓狂”的项目墙面缺陷检测就是其中一个。前段时间参与一个旧房翻新前评估的项目需要统计室内墙面的剥落、污渍和滴痕分布我一开始天真地想拿通用目标检测模型直接跑结果模型把墙角的插座、瓷砖缝、甚至窗帘褶皱都当成目标对真正的剥落区域反而无动于衷。后来才意识到室内墙面建筑物墙壁油漆剥落、滴痕、污渍、表面不平这类问题的检测最缺的根本不是算法技巧而是高质量数据——尤其是一份同时给了VOC和YOLO格式的标注数据集。我最近拿到一份3011张图、6个类别的墙面缺陷数据集花了不少时间吃透它的目录结构、标注口径和训练细节一路踩了不少坑。这篇就完整记录下来给准备做房屋验收、物业巡检、建筑外观检测这些方向的朋友一个可以直接参考的路线。1. 室内墙面缺陷检测的尴尬现状为什么这类数据集这么稀缺1.1 通用标注数据集帮不上忙墙面病害和猫、狗、汽车这些常规目标完全不是一回事。一张通用目标检测数据集里“墙体”往往只是背景偶尔有几类窗户、门、标语之类的“墙体附属物”真正关心墙面本身有没有剥落、有没有流挂痕迹、有没有表面不平整的数据非常少。原因也简单通用数据集主要服务安防、自动驾驶、电商等大场景墙面缺陷这种细分方向商业价值确实存在但数据采集和标注成本高愿意系统去整理的人不多。再加上墙面缺陷本身的形态很特殊——剥落区域的边界不是一个锐利的轮廓而是一片模糊的过渡带滴痕和污渍在视觉上经常连人都分不清。这就要求标注人员既懂图像标注又对装修和建筑病害有一些基本认知不然标出来的框根本没法用。市面上很多公开数据集的墙面类图片要么标签粒度太粗只有“室内”和“室外”这种场景级标签要么标注框画得很随意把整面墙都框进去训练出来的模型自然没法定位到具体病害位置。1.2 真实场景采集的难点比想象中大采集也是个大问题。室内墙面照片不像公开的街景图片可以随便抓取要在住宅、办公室、地下车库、楼道这些不同环境里拍摄涉及场地协调和隐私问题再加上不同时段的光照差异、墙面本身的纹理和颜色干扰真正能用于训练的“干净”样本其实很少。数据少导致的问题也很直接模型没见过足够多“长得像但实际不是”的负例推理时就容易把阴影当污渍、把墙纸接缝当裂缝。这一点可能外行体会不深做过实际项目的人都知道墙面病害检测的错误主要不是漏检而是误报。一面正常的墙在傍晚会有斜射光产生的阴影在顶角会有施工残留的波浪纹在踢脚线附近会有灰尘堆积——这些在模型眼里都可能是“病害”。没有足够丰富且标注准确的实拍数据模型很快就把这些环境噪声学进去了。这也是我拿到这份3011张的数据集时比较兴奋的原因它至少把采集和清洗这两个最耗时的工作砍掉了大半让我能直接把精力放到模型和业务逻辑上。2. 数据全貌拆解3011张图与6类墙面病害2.1 图像覆盖的场景与拍摄条件先说整体感受。3011张全部是室内墙面实拍图不是渲染图也不是网上随便抓的杂图。场景覆盖比较广从普通住宅的卧室、客厅到办公楼的走廊、会议室再到地下车库、楼梯间这类公共区域都有涉及。光照条件也分了好几种自然光直射、日光灯均匀照明、昏暗走廊的局部照明甚至还有几组晚上开闪光灯拍的。这个多样性对我来说很关键因为真实业务里你就是会遇到各种乱七八糟的光线如果数据集全是白天顺光拍的模型一上现场就会现原形。拍摄距离和角度也不是单一的。有离墙很近的特写能看清剥落边缘的漆皮翘起细节也有站在房间中间拍的单面墙全景能看出一片区域内的病害分布还有些是斜着拍的带有透视变形。一开始我担心斜拍角度会影响检测效果实际跑下来发现影响不大因为目标检测框对轻微透视是能容忍的反倒是这些“不够正”的角度让模型泛化更好没那么挑剔拍摄姿势。2.2 六类病害的标注口径这份数据集一共6个类别索引顺序是固定的。按常规的建筑病害分类习惯结合标题中明确提到的剥落、滴痕、污渍、表面不平再加上墙面检测里常见的裂缝和霉斑整理下来是这样一组类别标签名典型形态标注难点剥落peeling漆皮翘起、成片掉落露出腻子或水泥基层边界模糊多个小片区域是否合并裂缝crack线状或树枝状开裂粗细不一细长目标长宽比夸张滴痕drip_mark液体流挂痕迹常见于顶角、窗边、管道附近容易和污渍混淆污渍stain水渍、脚印、油污等不规则色斑和阴影、墙面纹理难区分表面不平uneven鼓包、凹陷、抹灰面不平整弱特征依赖光影表现霉斑mold黑灰色或灰绿色斑点常成片分布密集小目标边界呈毛刺状注意类别索引顺序和标签名的一致性。不同框架读取类别名称的方式不太一样有的按配置文件里的顺序有的直接读txt第一列的数字顺序一旦对不上训练出来的结果就是类别错乱。我拿到数据后第一件事就是统计所有标注文件里出现的类别ID和配置里的names列表逐一对齐确认没有错位。2.3 标注框的几个细节从标注文件来看框大体上是水平矩形紧贴目标边缘没有出现整面墙一个大框这种偷懒做法。剥落区域如果呈现“这里一小片、那里一小片”的分散状态标注员会拆成多个独立框而不是用一个大框笼统包住这对训练小目标检测很有利。你也别指望所有框都完美无缺我抽样检查时发现有少量霉斑目标标得偏大把周边正常的墙面也框进去了还有个别滴痕漏标的情况。这类噪声在真实数据集里正常后面我会专门说怎么处理。样本分布也不是均衡的。剥落和污渍这类常见病害样本量明显大滴痕、表面不平相对少裂缝虽然数量不少但很多图里只有一条很细的线单张图的目标框很少。建议拿到数据后先做一次统计分析把每类目标总数、每张图的平均框数列出来心里有个底再决定后续的训练策略。3. VOC和YOLO双格式的实际含义与转换逻辑3.1 两种格式的本质差异很多新手看到“VOCYOLO格式”会以为是两份一模一样的数据只是套了两层皮。严格说不是。VOC格式源于Pascal VOC的标注规范每个图片对应一个XML文件里面用xmin、ymin、xmax、ymax记录目标框的像素坐标同时还能带目标的名称、姿态、截断程度等额外信息结构完整但冗长。YOLO格式则是为训练效率做了极大简化每个图片对应一个txt文本每一行是“类别ID、中心点x、中心点y、框宽、框高”全部归一化到0到1之间文件小、读取快检测框架直接解析。两者各有各的适用场景。VOC格式适合人工检查、二次编辑、跑传统的Faster R-CNN系列流程YOLO格式适合直接喂给现在主流的YOLO系检测框架做训练。数据给双格式本质上就是替你把两种使用场景都覆盖了省得自己写转换脚本。3.2 拿到数据先看目录结构我拿到的这份数据目录大概是这个风格wall_defect_dataset/ ├── VOC/ │ ├── JPEGImages/ # 原始图片 │ ├── Annotations/ # 每个图片对应的XML标注 │ └── ImageSets/ │ └── Main/ # train.txt / val.txt / test.txt └── YOLO/ ├── images/ # 图片按train/val/test分好 └── labels/ # 与图片同名的txt标注先检查的关键点有两个一是YOLO目录里的图片和标注txt是否同名一一对应二是ImageSets/Main里的划分列表是否和实际文件数量对得上。我有一次拿到其他项目的数据train.txt里列了800张图但JPEGImages里实际只有790张训练跑到一半突然报图片读取失败白白浪费了几个小时。所以无论谁给的数据第一件事永远是做文件一致性校验。3.3 一份可靠的格式转换脚本如果你想从VOC转到YOLO格式或者反过来校验建议直接写个脚本处理。转换的关键在于坐标换算XML里给的是像素坐标YOLO需要的是中心点坐标加宽高的归一化值需要读取原始图片的真实宽高不能自己假设。import os import xml.etree.ElementTree as ET from PIL import Image class_names [peeling, crack, drip_mark, stain, uneven, mold] def voc2yolo(xml_path, img_path, out_dir): tree ET.parse(xml_path) root tree.getroot() # 用真实图片尺寸做归一化不能写死 with Image.open(img_path) as img: img_w, img_h img.size lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_names: print(f未知类别: {name}, 文件: {xml_path}) continue cls_id class_names.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) cx ((x1 x2) / 2.0) / img_w cy ((y1 y2) / 2.0) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines))脚本里抛出的“未知类别”日志很重要。我曾经遇到过XML里写的是crack但配置里叫crack_line的情况如果不打印日志这类错误会静默吞掉最后模型少学一类。另外还要注意有些标注框会出现x2小于x1或者坐标越界的脏数据转换前最好加一层合法性检查把异常框打印出来人工确认。3.4 双格式同时给的原因与常见误解有人觉得既然YOLO格式能直接训练为什么还要保留VOC我的理解是YOLO格式是给训练脚本看的VOC格式是给人看的。调试的时候打开XML能清晰看到目标名称、坐标、还有图片尺寸等元信息打开txt只能看到一堆数字。另外如果后续想转成COCO的json格式去做Faster R-CNN或者Mask R-CNN从VOC转比从txt反推要直接得多。双格式不是冗余是给使用场景多留了一条路。4. 用这份数据集跑通一次目标检测训练4.1 数据划分建议数据集虽然自带了划分文件但我建议你重新按自己的需求划分。原因在于原始划分多数是纯随机切分而墙面图片有个特性同一面墙常常会连续拍好几张角度略微变化。如果随机切分这些高度相似的照片很可能一半在训练集、一半在验证集验证集指标会虚高看起来效果很好一到现场就拉胯。我的做法是把图片按场景或拍摄批次分组然后按组来划分训练、验证、测试比例取8:1:1。这样验证集里的图和你训练时见过的图在场景上是分离的更能反映真实泛化能力。如果数据集没有提供场景ID之类的信息可以根据文件名前缀或者拍摄时间推断实在不行就按文件名哈希分桶尽量减少同场景图片泄露。4.2 配置文件与训练参数以YOLO系列框架为例训练前先写一个数据集配置文件把路径和类别对齐。这里强调一个细节配置里的names顺序必须和标注文件里的类别ID一致很多训出来结果完全混乱的问题就出在这一步。# wall_defect.yaml names: 0: peeling 1: crack 2: drip_mark 3: stain 4: uneven 5: mold nc: 6 train: ./YOLO/images/train val: ./YOLO/images/val训练参数上我试过的组合里比较稳的是输入640x640预训练权重从公开的COCO模型初始化batch size根据显存定16或32都行epochs给150配合早停。墙面缺陷的整体目标尺寸不大剥落和污渍勉强算中等大小裂缝和霉斑则偏小。所以我没有用默认的anchor设置而是先跑了50个epoch统计验证集上预测框的尺寸分布然后基于这个分布重新聚类anchor对小目标有明显改善。数据增强方面Mosaic和随机仿射变换可以开但要注意别开太猛。墙面病害的判别依赖颜色和纹理细节过度色彩抖动容易把墙面本来就不明显的特征抹掉。我最后只保留了一定程度的亮度、对比度扰动和轻微旋转高斯模糊也开了模拟不同距离拍摄的清晰度差异。4.3 训练结果怎么评判训练过程要看损失曲线和验证集指标我主要关注mAP0.5和mAP0.5:0.95这两个数。在这个数据量级上mAP0.5能到0.7以上就算比较理想了mAP0.5:0.95会低不少通常在0.4到0.55之间别被这个数字吓到。墙面病害本身边界模糊标注框的主观性也强定位精度要求不用像自动驾驶那样苛刻业务上真正重要的是“有没有把病害区域找出来、大概在哪个位置”。还有个更贴近实际的做法不看平均指标分开看每个类别的PR曲线。裂缝这个类别如果PR曲线掉得厉害说明模型在细长目标上还没学会滴痕如果召回率低可能是训练样本太少。逐类分析之后再做针对性调整比只盯着一个mAP数字瞎调参效率高得多。4.4 推理阶段的两个实用设置训练完的模型做推理时我习惯把置信度阈值从默认的0.25往下调到0.15左右。原因很简单污渍和滴痕这类目标的特征本身弱模型给出的分数普遍偏低阈值太高会把不少正确检测直接滤掉。适当放宽阈值会导致误报增加但墙面检测场景里误报的代价比漏报小——宁可多标几个疑似区域让人工确认也不能漏掉一处真正的病害。NMS的IoU阈值可以略微放松到0.6避免同一片剥落区域被重复打框导致面积统计失真。5. 墙面数据最典型的几个坑与避坑方法5.1 滴痕、污渍、霉斑的类别混淆这三个类别的语义边界本身就模糊。一道水渍沿着墙面流下来既有“污渍”属性也有“滴痕”属性霉斑初始阶段就是几个黑点和污渍很难用肉眼区分。标注员在判类的时候经常各标各的导致训练数据里同一类现象被分到不同类别。这不能全怪数据质量墙面病害本身就不是互斥的。我的处理方式是训练阶段接受这种噪声同时强制每个类别的样本都带上一点“像其他类”的负样本推理阶段用规则兜底。比如检测到滴痕时结合目标位置判断——如果出现在顶角附近且呈垂直延伸大概率是滴痕如果出现在踢脚线附近且形态不规则更可能是污渍。模型负责找规则负责收口比单纯靠模型硬扛效果好很多。5.2 裂缝这种细长小目标裂缝应该是这6类里最让训练头疼的。长宽比经常超过10比1目标面积占比却连百分之一都不到。默认的anchor对这类目标非常不友好聚类时也容易被中等大小的剥落框带偏。如果框架支持我会单独给裂缝一类设定更小的anchor尺度或者将输入分辨率从640提到768。代价是显存和训练时间增加但对裂缝这类细长目标来说提升是肉眼可见的。还有一个偏工程的做法数据增强阶段加一项“复制粘贴增强”——把标注好的裂缝区域随机裁剪下来贴到没有裂缝的墙面图上同时写入对应的新标注框。这样做既扩大了裂缝样本量又让模型适应不同位置的裂缝形态。我用这个方法把裂缝类别的召回率提升了5个点左右代价是偶尔产生不自然的粘贴痕迹实测下来利大于弊。5.3 光照、阴影与反光干扰墙面检测里误报的最大来源不是病害本身而是光影。傍晚斜射阳光形成的条状阴影在模型眼里和裂缝非常像灯光在墙面上形成的高光区域又容易被判断成表面不平。数据集里虽然已经有不同光照的照片但推理场景的光线组合永远比你见过的多。我是这样平衡的训练时加入随机亮度、对比度、色相扰动让模型学会抓住病害的“纹理突变”而不是“明暗变化”推理时对置信度处于0.2到0.4之间的暧昧框做一个二次校验如果这个区域和周边墙面的颜色方差特别小说明它大概率是光影造成的假阳性直接过滤掉。这个方法不复杂但能砍掉将近三成的误报。5.4 类别不均衡的处理剥落和污渍的样本量可能是滴痕的三到四倍直接训练会让少数类被多数类“带偏”。我试过两种办法都有效但适用场景稍微不同。一种是损失函数加权给滴痕、表面不平等少数类更高的分类权重改动最小训练还是端到端的。另一种是结合过采样训练时提高少数类样本的采样概率等效于让模型每个epoch多看几遍这些图片。加权的方法在多数类足够多的时候够用过采样对极端不均衡更稳。我一般是两者结合先在配置里给少数类加一点权重再看验证集效果决定要不要加强过采样。5.5 标注噪声要不要清洗前面提到这份数据存在少量漏标和偏大的框要不要清洗取决于你的使用场景。如果只是做Demo、验证模型效果可以直接用噪声影响不大如果要部署到实际项目里给客户出检测报告建议至少清洗一轮。清洗的优先级是先查漏标再查错类最后才查框大小。漏标会让模型把“病害区域”当成背景误报率上升错类会让分类边界混乱框偏大偏小属于标注精度问题影响的是定位质量而不影响分类逻辑。我清洗时不是一张张看而是先用预训练模型跑一遍把高置信度检测框和标注框做比对找出标注里明显漏掉的区域再人工确认补充。6. 从模型到产品这批数据能支撑哪些应用场景6.1 房屋验收与装修质检墙面缺陷检测最直接的应用就是房屋验收。开发商交付前、装修公司完工后都需要对墙面质量做检查。传统流程是人工拿手电筒边照边看发现问题拿粉笔圈出来效率低、标准不统一。有了检测模型之后验收人员用手机或者平板对着墙面扫一圈系统自动框出剥落、裂缝、污渍的位置还能按框的面积估算每类问题的占比生成一份带坐标的质检报告。落地时需要注意验收场景拍的通常是整面墙的全景模型要能定位到具体位置而不是只给一个“这面墙有问题”的结论。6.2 物业巡检与维保工单物业公司定期巡查地下车库、楼梯间、公共走廊时墙面问题是最容易被忽视的。光照暗、角落里又容易堆积污渍和霉斑人工巡检很难客观记录。手机拍照自动检测配合地理位置信息和时间戳可以直接生成工单推送给维修班组形成“发现—派单—维修—复检”的闭环。这类场景对误报的容忍度更高模型检测结果可以作为辅助证据最终由维修人员确认。6.3 爬壁机器人与楼宇外观检测室内做得好下一步通常是往室外迁移。爬壁机器人沿着建筑外墙采集图像模型实时识别外墙的剥落、裂缝和空鼓迹象。室内外的差异主要在光照、距离和材料上室外还有窗户、装饰线条等更多干扰。我的建议是把这份室内数据当作基础再补充一部分外墙实拍数据做微调迁移成本比从零开始低很多。空鼓这类需要敲击检测的问题纯视觉只能做初步筛查最终确认还是得靠其他手段配合。6.4 模型再扩展的思路如果业务需要新增类别比如空鼓、起粉、盐析不需要重新标注上千张图。可以把现有模型作为特征提取器用几十张新类别的样本做少样本学习或者增量训练。另一个更省事的办法是半监督自举先让现有模型在大量无标注图片上跑一遍把高置信度检测结果当成伪标签人工抽检修正后回灌训练集。我在这个数据集上试过一轮把滴痕的样本量扩大了一倍模型在验证集上的召回率又涨了两个点。数据积累就是这样滚雪球先用起来再慢慢变好。最后再分享一个个人经验墙面缺陷检测这类项目别太执迷于刷指标。我在这份数据集上花了最多时间的部分其实是把“mAP不错”的模型放到真实场景里验证看它在不同光线、不同手机、不同墙面材质下的表现。很多时候模型在测试集上已经很好但一上现场就被墙角阴影、踢脚线反光打得满地找牙。所以建议你想办法留出几十张真实环境里拍的照片作为“压箱底”的验证集永远不参与训练和调参只在每次模型迭代后过一遍。这个习惯帮我避开了不少自以为“已经搞定”的坑。毕竟墙体缺陷这东西一栋楼里常常有成百上千处漏掉一处后面就是业主投诉和返工成本。