简介本资源为城市道路井盖破损与丢失场景的目标检测数据集面向从事智慧城市、道路巡检、市政设施安全监测方向的算法工程师与深度学习研究者可用于训练和验证井盖异常状态识别模型。数据集采用Pascal VOC格式仅包含jpg图片与对应xml标注文件共1377张图像、1377个标注文件压缩包内文件总数2000个整体约211.93MB另附1个说明txt。标注由labelImg完成以矩形框方式覆盖4类目标井盖、井盖破损、井盖丢失、井盖挪动对应框数分别为872、623、177和50主要针对圆形与方形井盖。目前已有1637人学习下载适合直接用于目标检测模型的训练、微调与效果对比也可作为道路安全隐患自动识别项目的训练素材帮助读者快速搭建井盖状态检测流程并验证算法在真实道路场景下的表现。1. 井盖数据集为什么值得单独做一遍1377 张 VOC 标注能解决什么城市道路巡检里井盖破损和丢失是高频但极难自动识别的一类目标。它不像车辆、行人那样有稳定外观破损井盖边缘碎裂、丢失井盖只剩一个黑洞和路面阴影、水渍、坑洼在低分辨率下几乎同色。很多团队拿 COCO 或 BDD100 的预训练权重直接推理结果就是漏检一片——因为这些通用数据集里根本没有「井盖破损」这个类别。这份 VOC-1377 张的数据集价值就在于它把「井盖」和「井盖破损/丢失」拆成了可训练的标注类别用 Pascal VOC 的 XML 格式给出边界框能直接喂给 YOLO 系列、Faster R-CNN、SSD 这些主流检测器。它适合做市政巡检算法验证的工程师、想跑通自定义数据集训练的学生以及需要快速搭一个 demo 验证可行性的人。1377 张不算大但足够把一条训练流水线从头到尾走通也足够暴露小目标检测里那些玄学问题。2. VOC 格式拆解与训练框架选型为什么先转 YOLO 再谈精度拿到一份 VOC 数据集第一件事不是急着训练而是把目录结构和标注内容摸清楚。VOC 的约定很固定但不同来源的包经常在细节上翻车比如 JPEGImages 里混了非 jpg 文件、Annotations 里有空 XML、ImageSets/Main 下的 txt 划分和实际文件对不上。这一章先把结构讲透再决定用哪套框架。2.1 VOC 目录结构与 XML 字段含义标准 Pascal VOC 的目录长这样VOCdevkit/ └── VOC2007/ ├── Annotations/ # 每张图对应一个 XML ├── JPEGImages/ # 原始图片 ├── ImageSets/ │ └── Main/ # train.txt / val.txt / trainval.txt └── SegmentationClass/ (本数据集用不到)一个 XML 里真正影响训练的就几个字段filename必须和 JPEGImages 里的文件名完全一致含扩展名size下的 width/height 要和图片真实尺寸对得上对不上会导致坐标缩放错位每个object里的name是类别名bndbox的 xmin/ymin/xmax/ymax 是左上角和右下角坐标。常见做法是写个脚本先做一遍一致性校验别等训练报错了才回头查。import os import xml.etree.ElementTree as ET from PIL import Image def check_voc(root): ann_dir os.path.join(root, Annotations) img_dir os.path.join(root, JPEGImages) bad [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_file)) r tree.getroot() fname r.find(filename).text img_path os.path.join(img_dir, fname) # 1. 图片是否存在 if not os.path.exists(img_path): bad.append((xml_file, missing image)) continue # 2. 尺寸是否一致 w int(r.find(size/width).text) h int(r.find(size/height).text) iw, ih Image.open(img_path).size if (w, h) ! (iw, ih): bad.append((xml_file, fsize mismatch {w}x{h} vs {iw}x{ih})) # 3. 坐标是否越界 for obj in r.findall(object): b obj.find(bndbox) xmin int(float(b.find(xmin).text)) ymin int(float(b.find(ymin).text)) xmax int(float(b.find(xmax).text)) ymax int(float(b.find(ymax).text)) if xmin 0 or ymin 0 or xmax iw or ymax ih or xmax xmin or ymax ymin: bad.append((xml_file, fbad box {xmin},{ymin},{xmax},{ymax})) return bad if __name__ __main__: issues check_voc(./VOCdevkit/VOC2007) print(f发现 {len(issues)} 处问题) for f, msg in issues[:20]: print(f, msg)这段脚本做三件事核对图片存在性、核对 XML 记录尺寸和真实图片尺寸、检查边界框是否越界或退化。参数上root指向 VOC2007 那一层不要指到 VOCdevkit。跑完如果问题数不为零先修数据再训练否则后面 loss 不降你会怀疑人生。2.2 转 YOLO 格式坐标归一化与类别映射YOLO 系列v5/v8/v11吃的是 txt 格式每行class_id cx cy w h全部归一化到 0~1。转换时最容易错的是 cx/cy 用中心点而不是左上角以及归一化除以的是图片自身宽高。下面这个脚本把 VOC 转成 YOLO同时生成 data.yaml。import os import xml.etree.ElementTree as ET from PIL import Image CLASSES [manhole, manhole_broken] # 按你数据集实际类别改 def voc_to_yolo(voc_root, out_root): ann_dir os.path.join(voc_root, Annotations) img_dir os.path.join(voc_root, JPEGImages) for split in [train, val]: os.makedirs(os.path.join(out_root, images, split), exist_okTrue) os.makedirs(os.path.join(out_root, labels, split), exist_okTrue) # 读取划分文件 split_map {} for split in [train, val]: txt os.path.join(voc_root, ImageSets/Main, f{split}.txt) with open(txt) as f: for line in f: split_map[line.strip()] split for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] split split_map.get(stem) if split is None: continue tree ET.parse(os.path.join(ann_dir, xml_file)) r tree.getroot() fname r.find(filename).text img_path os.path.join(img_dir, fname) iw, ih Image.open(img_path).size lines [] for obj in r.findall(object): name obj.find(name).text if name not in CLASSES: continue cid CLASSES.index(name) b obj.find(bndbox) xmin float(b.find(xmin).text) ymin float(b.find(ymin).text) xmax float(b.find(xmax).text) ymax float(b.find(ymax).text) cx (xmin xmax) / 2 / iw cy (ymin ymax) / 2 / ih w (xmax - xmin) / iw h (ymax - ymin) / ih lines.append(f{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) # 拷贝图片 写 label import shutil shutil.copy(img_path, os.path.join(out_root, images, split, fname)) with open(os.path.join(out_root, labels, split, stem .txt), w) as f: f.write(\n.join(lines)) # 生成 data.yaml with open(os.path.join(out_root, data.yaml), w) as f: f.write(fpath: {os.path.abspath(out_root)}\n) f.write(train: images/train\n) f.write(val: images/val\n) f.write(fnc: {len(CLASSES)}\n) f.write(fnames: {CLASSES}\n) if __name__ __main__: voc_to_yolo(./VOCdevkit/VOC2007, ./yolo_dataset)关键参数说明CLASSES必须和 XML 里name字段完全一致大小写敏感split_map依赖 ImageSets/Main 下的 train.txt 和 val.txt如果数据集没提供划分常见做法是按 8:2 自己随机切归一化用.6f保留六位YOLO 官方推荐至少这个精度位数太少小目标会漂。转换完务必抽查几张 label用可视化脚本画框确认别直接开训。2.3 框架选型YOLOv8/v11 还是 Faster R-CNN1377 张属于小数据集选型逻辑很直接。YOLOv8n 或 YOLOv11n 这种 nano 级别在单卡 8G 显存上 batch16、imgsz640 能跑起来训练 100 epoch 大概一两个小时适合快速验证。Faster R-CNN 精度上限可能略高但小数据集上更容易过拟合且推理速度慢做巡检 demo 不划算。常见做法是先用 YOLOv8n 跑一版 baseline看 mAP50 能不能过 0.6能过再考虑换 s/m 模型或加数据增强。如果类别只有「井盖」和「破损井盖」两类nc2别把背景也算一类。3. 训练配置与参数调优小目标井盖的 batch、imgsz 与增强策略数据转好了接下来是训练。这一章把配置文件、启动命令、关键参数逐个拆开重点讲井盖这类小目标为什么 imgsz 不能太小、增强怎么开才不帮倒忙。3.1 data.yaml 与训练启动命令YOLOv8/v11 的训练入口是yolo detect train核心参数都在命令行或 yaml 里。先确认 data.yaml 路径正确path: /abs/path/to/yolo_dataset train: images/train val: images/val nc: 2 names: [manhole, manhole_broken]启动命令yolo detect train \ data./yolo_dataset/data.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience30 \ augmentTrue \ mosaic1.0 \ degrees10.0 \ translate0.1 \ scale0.5 \ fliplr0.5 \ projectruns/manhole \ nameexp1参数逐个说imgsz640是底线井盖在整图里占比小320 会让它缩到十几个像素直接漏检batch16是 8G 显存的稳妥值爆显存就降到 8lr00.01是 SGD 的常见起点用 AdamW 的话降到 0.001patience30表示 30 轮没提升就早停小数据集别设太大mosaic1.0默认开但井盖这种目标 mosaic 拼接后可能出现不合理的空间关系如果发现验证集掉点可以降到 0.5 或关掉degrees10旋转增强对俯拍井盖有用但如果是固定视角的巡检图旋转反而引入噪声按数据来源决定。3.2 小目标检测的 imgsz 与 anchor 适配井盖破损区域往往只占整图 5% 以下属于典型小目标。YOLOv8 是 anchor-free 的省去了调 anchor 的麻烦但 imgsz 和特征金字塔的 stride 仍然决定小目标能不能被检出。P3 层 stride8640 输入下每个格子对应 8 像素井盖如果只有 30 像素宽落在 P3 上还有 3~4 个格子勉强够用如果井盖更小就得考虑imgsz960或 1280代价是显存和速度。另一个技巧是检查数据里边界框的宽高分布import os def box_stats(label_dir): ws, hs [], [] for f in os.listdir(label_dir): with open(os.path.join(label_dir, f)) as fp: for line in fp: _, _, _, w, h line.split() ws.append(float(w)) hs.append(float(h)) ws.sort(); hs.sort() n len(ws) print(f样本数 {n}) print(f宽度 中位数 {ws[n//2]:.4f} 最小 {ws[0]:.4f} 最大 {ws[-1]:.4f}) print(f高度 中位数 {hs[n//2]:.4f} 最小 {hs[0]:.4f} 最大 {hs[-1]:.4f}) box_stats(./yolo_dataset/labels/train)如果中位宽度低于 0.05即 640 下不到 32 像素就该考虑放大 imgsz 或对图片做裁剪分块。这个统计比拍脑袋调参靠谱得多。3.3 数据增强的取舍哪些增强会害了小目标增强不是越多越好。井盖数据集里mosaic和mixup能增加背景多样性但 mixup 对小目标不友好两张图叠加后井盖可能被遮挡得只剩一半标注框却还是完整的等于给模型喂了错标签。copy-paste增强把井盖抠出来贴到别的路面理论上适合小目标但需要额外 maskVOC 只有框做不了精细抠图。稳妥组合是mosaic0.5~1.0、fliplr0.5、scale0.5、translate0.1关掉mixup。如果验证集 mAP 波动大先把 mosaic 关掉跑一版对照确认是不是增强引入的方差。4. 避坑与排查训练不收敛、漏检、类别混淆的常见原因这一章是血泪经验集中区。小数据集训练翻车的方式就那么几种按「现象 → 原因 → 解决」列出来遇到问题直接对号入座。4.1 现象loss 一直不降或震荡剧烈原因通常是三类学习率太大、标注里有大量退化框、类别名映射错位。先查标注用第 2 章的校验脚本跑一遍确认没有 xmaxxmin 的框。再查 data.yaml 的names顺序和转换脚本里的CLASSES是否一致顺序错了模型学的是错类别。最后降 lr0 到 0.001 试一轮如果 loss 开始降说明就是学习率问题。4.2 现象验证集 mAP 还行但实际图片漏检严重这是最典型的分布不一致。训练集如果是晴天正午拍的实际巡检有阴影、逆光、夜间补光模型没见过就漏。解决不是调模型是补数据。另一个原因是 imgsz 太小训练时 640推理时如果原图 4000 像素直接 resize 到 640井盖只剩几像素。正确做法是推理时用滑动窗口或先裁剪再检测保持井盖在输入里的像素量。4.3 现象井盖和破损井盖互相误判两类目标外观高度相似破损井盖本质就是井盖加了碎裂特征。如果破损样本里有些只是轻微裂纹标注边界模糊模型就会混淆。解决检查标注一致性把「轻微裂纹算不算破损」统一标准如果两类样本量悬殊比如井盖 1200、破损 177对破损类做过采样或调高其 loss 权重。YOLO 里可以用cls损失权重间接调节或者干脆先做单类「井盖」检测再单独训一个破损分类器。4.4 现象训练报 CUDA out of memorybatch 太大或 imgsz 太大。8G 显存下 imgsz640、batch16 是边界换 960 就得把 batch 降到 4~8。另一个隐藏原因是 dataloader 的workers开太多每个 worker 都占显存做增强设成 4 或 8 就够。还有cacheTrue会把整个数据集缓存到内存1377 张不大可以开但如果图片是 4K 原图缓存会吃满内存改成cachedisk或关掉。4.5 现象转完 YOLO 格式后图片和 label 对不上最常见的是文件名 stem 不一致。VOC 的 XML 里filename可能是IMG_001.jpg但 JPEGImages 里实际是img_001.JPG大小写或扩展名不同转换脚本按 stem 找 label 就错位。解决转换时统一用 XML 里的filename作为基准label 文件名用os.path.splitext(filename)[0]别用 XML 文件名去推。转完随机抽 10 张用可视化脚本画框确认框和井盖对齐。5. 推理验证与精度提升从 mAP 到实际巡检可用训练完拿到 best.pt别只看 mAP 数字就收工。这一章讲怎么验证模型在真实场景下能不能用以及几个不换模型就能提点的技巧。5.1 用验证集和单图推理做双重检查先跑官方验证yolo detect val \ modelruns/manhole/exp1/weights/best.pt \ data./yolo_dataset/data.yaml \ imgsz640 \ batch16 \ conf0.25 \ iou0.5看输出的 mAP50、mAP50-95、precision、recall。井盖这种两类任务mAP50 能到 0.7 以上算可用0.5~0.7 需要补数据低于 0.5 先查标注。然后拿几张训练集外的实拍图做单图推理yolo detect predict \ modelruns/manhole/exp1/weights/best.pt \ source./test_imgs \ imgsz640 \ conf0.3 \ saveTrueconf0.3比验证时的 0.25 略高是为了减少误报巡检场景误报比漏报更烦人。看runs/detect/predict下的可视化结果重点看阴影处、水渍处有没有误检破损井盖有没有被框成普通井盖。5.2 置信度阈值与 NMS 的调参边界conf和iou两个阈值直接决定输出框数量。conf调高减少误报但增加漏报调低反之。井盖巡检里漏掉一个丢失井盖可能意味着安全事故所以宁可误报也别漏conf可以压到 0.2。iou控制 NMS 合并重叠框的力度井盖之间通常不重叠iou0.5够用如果同一井盖被检出多个框降到 0.3 加强合并。这两个参数没有万能值拿一批实拍图跑网格搜索画 precision-recall 曲线选拐点。5.3 不换模型提点的三个实操技巧第一测试时增强TTA。推理时对图片做翻转、多尺度把结果融合mAP 通常能涨 1~3 个点代价是速度慢几倍。YOLO 里加augmentTrue即可。第二用训练集验证集重新训一版。1377 张本来就少如果验证集只占 10%最终交付模型时可以把全部数据拿来训但这样就没了验证集得提前留一批实拍图做最终测试。第三模型融合。训 YOLOv8n 和 YOLOv11n 两个模型推理时对框做加权融合小数据集上比单模型稳。这三个技巧按投入产出排序TTA 最省事重训最有效融合最麻烦。5.4 导出与部署前的最后一道检查部署到边缘设备前先导出 ONNX 或 TensorRTyolo export modelruns/manhole/exp1/weights/best.pt formatonnx imgsz640 opset12导出后务必用同一张图对比 PyTorch 和 ONNX 的输出框坐标偏差超过 1 像素就说明导出有问题常见原因是 opset 版本和动态轴设置。我一般会在导出后写个脚本把两个后端的检测结果画在同一张图上叠着看确认对齐了再上设备。从那以后我每次交付前都强制走一遍「校验标注 → 转格式 → 抽检可视化 → 导出对齐」这条链少一步都可能在上线后翻车。希望帮到你。本文还有配套的精品资源点击获取