
简介面向城市道路巡检与目标检测应用场景该压缩包提供一套完整的井盖破损/丢失数据集含1377张真实道路井盖图像覆盖4个类别共1722个标注框并同时提供Pascal VOC xml与YOLO txt两种标注格式方便直接用于YOLO、SSD等常见检测模型的训练与验证。压缩包共2000个文件以xml标注文件与txt标签文件为主整体大小约212.35MB适合有一定深度学习基础的研究者或算法工程师快速开展模型微调与效果评估。整个数据集使用labelImg工具人工标注类别划分清晰已吸引427人学习/下载。借助该数据集可省去自行采集与标注的耗时直接获得与主流框架兼容的训练样本便于复现井盖破损、丢失等视觉检测任务并验证算法鲁棒性。1. 城市道路井盖破损丢失数据集VOCYOLO格式1377张4类别这个 zip 到底能帮你省多少事“城市道路井盖破损丢失数据集VOCYOLO格式1377张4类别.zip”这个词看上去像个普通压缩包但在市政巡检项目里它代表一套能直接进入训练流程的双格式标注样本。1377 张道路影像按 VOC 和 YOLO 两套规范标好四类井盖状态拆开就能喂给检测网络。适合接市政养护、智慧路灯杆联动、道路病害巡检这类项目的人先拿它做预训练和方案验证。做过井盖检测的都知道标注是最大成本市面很少有成套的双格式井盖数据能复现。这篇文章我按自己的落地流程写先拆目录、再做 VOC 转 YOLO、然后训一个 YOLOv8 基线最后把那些只在真实街道数据上出现过的坑列出来。2. 先拆数据集4 类标注逻辑与 VOC/YOLO 双格式目录解析拿到这套 zip 后我不会急着解压就训练。先做两件事定类别、对文件名。之所以先定类别是因为 YOLO 的 txt 标注写的是类别编号不是类别名如果类别顺序和实际标注对不上后面所有指标都是假的我在这上面栽过不止一次。2.1 四类标什么为什么把“完好”也放进来市政井盖检测常见的四类分法是完好、破损、丢失、异动/位移。“完好”是负样本类。训练时它能让模型学会“这里有个井盖但不用报修”避免把所有井盖都判成破损。市政项目真正上线后漏报破损的代价远大于误报完好所以负样本类必须留足占比最好占三到四成。只标三类的方案我不推荐那种模型会把路基接缝、沥青修补块全当成破损巡检画面框满红框完全没法用。“破损”指井盖表面裂缝、缺角、塌陷变形。这个类别的难点在拍摄角度侧视角下远处破损和近处普通裂纹纹理高度相似要靠局部亮度差异和边缘连续性去分。“丢失”代表井盖不在井口位置露出井洞责任级别最高它和“破损”的边界常常是同一个井盖在不同时刻的状态。盖子还在但有裂缝叫破损盖子被冲走叫丢失标注口径如果在两份数据里不一致模型会学得相当痛苦。“异动/位移”指井盖整体错位但未形成洞口比如被碾压翘起、平移。这类样本数量通常最少也最容易和破损混淆。如果这套 zip 里这一类比预想少我的习惯是把它并入“破损”先用二分类把“要不要派人修”判断干净后续专项检测再细分。这里还有一条反面经验有些数据为了凑类别数把完好井盖标成“其他”把树坑和污水格栅也加进来类别数虚高真实信息量反而下降。拿到 zip 后如果发现标签里出现不在四类语义内的名称第一反应不是改脚本兼容它而是向来源确认这到底是噪声还是新增需求。2.2 VOC 与 YOLO 目录怎么对应文件命名是第一个容易被坑的地方双格式 zip 解压后通常能看到两类目录一类是 VOC 的 annotations/images 布局一类是 YOLO 的 images/labels 布局。VOC 目录里每个标注是一个 XML 文件文件名和图像同名YOLO 目录里每个标注是 txt 文件也要求与图像同名。常见结构是这样manhole_1377/ ├── VOC │ ├── JPEGImages/ │ │ ├── 00001.jpg │ │ └── ... │ ├── Annotations/ │ │ ├── 00001.xml │ │ └── ... │ └── ImageSets/Main/ │ ├── train.txt │ ├── val.txt │ └── test.txt └── YOLO ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/重点不在目录名是 JPEGImages 还是 images而在于 YOLO 的 labels 与 images 文件名前缀完全一致。有的 zip 为了压缩省空间图像用短文件名、标签用长文件名比如 image00123.jpg 对应 image00123.xml但 YOLO 侧却把“.jpg”去掉直接存 .txt训练器扫描时找不到匹配就会一直卡在 Scanning labels。我还会顺手看一眼 ImageSets/Main 里的划分。VOC 侧的 train.txt/val.txt 是训练集和验证集划分的依据YOLO 侧如果自己又随机分了一次两边最好对齐用同一批 id。如果 zip 里没给划分文件我一般按 8:2 随机分但 seed 必须写死保证复现。提示zip 文件先在本地完整解压一次确认没有长路径和全半角符号混用否则某些解压工具会在长路径处静默跳过文件训练时才发现图像总数差了 20 张。2.3 开箱后的五道校验进入训练前先跑一遍我默认做五道校验不需要逐张看只跑脚本扫一遍。校验一类别表。grep 所有 xml 里的name把不重复的类别名列出来确认就是 4 个目标类没有错标类名。类别名大小写不一致也会炸掉数据准备过程。grep -h name VOC/Annotations/*.xml | sed s/[^]*//g | sort | uniq -c校验二文件名一致。用 diff 或 for 循环扫描两份目录。ZIP 在 Windows 解压时偶尔把 XML 文件名里的字母转了大小写Linux 下训练器区分大小写结果一半标注读不上。find VOC/Annotations -name *.xml | wc -l find VOC/JPEGImages -name *.jpg | wc -l # 两行结果如果不相等先确认是不是有子目录和隐藏文件校验三空标签。一个 txt 内容为空表示该图没有任何目标。YOLO 允许背景图但如果空标签有几百张就要想想真实场景是否真存在这么多无井盖路段。校验四图像宽高。查看图片实际尺寸和 xml 里size是否一致。有些数据集从视频抽帧后 resize 过但 xml 没跟着改VOC 转 YOLO 时这一步必错。校验五坐标越界。遍历 xml 的 bndbox把 xmin 小于 0、xmax 大于图像宽的情况列出来。高空正视角图像里尤其常见因为标注员把屏幕外的目标也标了。注意不要只看文件名里的 1377 就默认数量和划分正确解压后 image 目录的实际 jpg 数和 xml 数要专门数一遍。如果 zip 打包时把 xml 塞在VOC/Annotations/extra/这种子目录里光数根目录也会出错要用 find 扫全盘。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑VOC 标注是像素坐标的 xmin/ymin/xmax/ymaxYOLO 是归一化的中心点加宽高。转换逻辑不难难在核对边界坐标是否越界、类别顺序是否错位、空标签是否被漏掉。3.1 最小可用的 voc_to_yolo 转换脚本我写一个 Python 最小版本不依赖第三方库xml.etree.ElementTree就够import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) if img_w 0 or img_h 0: print(f[skip] {xml_path.stem}: bad image size) return lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_names: print(f[warn] {xml_path.stem}: unknown class {name}) continue class_id class_names.index(name) b obj.find(bndbox) xmin float(b.findtext(xmin)) ymin float(b.findtext(ymin)) xmax float(b.findtext(xmax)) ymax float(b.findtext(ymax)) # 越界坐标先裁回图像范围避免归一化后出现负数或大于 1 xmin max(0.0, min(img_w, xmin)) xmax max(0.0, min(img_w, xmax)) ymin max(0.0, min(img_h, ymin)) ymax max(0.0, min(img_h, ymax)) # 有的标注工具会写出 xmin xmax 的脏数据这里统一规整一次 if xmin xmax: xmin, xmax xmax, xmin if ymin ymax: ymin, ymax ymax, ymin w xmax - xmin h ymax - ymin if w 0 or h 0: print(f[skip] {xml_path.stem}: empty bbox after clip) continue cx xmin w / 2.0 cy ymin h / 2.0 lines.append( f{class_id} {cx / img_w:.6f} {cy / img_h:.6f} f{w / img_w:.6f} {h / img_h:.6f} ) if not lines: return out_path out_dir / (xml_path.stem .txt) out_path.write_text(\n.join(lines)) # 类名顺序就是 YOLO 的类别 id顺序一旦确定不要再改 CLASS_NAMES [intact, broken, missing, displaced] XML_DIR Path(VOC/Annotations) OUT_DIR Path(YOLO/labels/val) OUT_DIR.mkdir(parentsTrue, exist_okTrue) for xml_file in sorted(XML_DIR.glob(*.xml)): voc_to_yolo(xml_file, OUT_DIR, CLASS_NAMES)核心逻辑是先读size拿原图宽高再遍历object取类别名和 bndbox把坐标转成归一化中心点格式一条标注写一行最后写入与 xml 同名的 txt。代码里做了两层防御一是越界坐标裁回[0, img_w]二是把 xmin 大于 xmax 的脏数据规整回来。不做这两步训练时你会看到 loss 正常下降但验证集 mAP 飘忽不定因为大量归一化结果在 0 到 1 之外的框也进了损失计算。参数上class_names列表顺序就是 YOLO 的类别编号xml 里的name必须能对得上否则脚本会跳 warn。.6f保留 6 位小数是 YOLO 惯例不推荐改成.2f小目标框在 1280 长边上的小数误差会被放大成实际像素偏移。注意如果同一张图在 xml 和生成的 txt 里目标数量对不上第一个要查的是空标签而不是转换脚本。3.2 归一化坐标的边界坑0 到 1 之外和 float 精度第一个坑是越界坐标。很多网上的转换脚本只做“除以图片宽高”完全不检查坐标是否已经越过图片边界。井盖数据集中低空俯拍图经常出现标注框的 xmax 比图片实际宽度大几十像素归一化后变成 1.01。YOLO 训练本身能容忍一点越界但在 NMS 阶段中心点落在图像外的框会被当成无效框合并造成漏检。我的处理原则是宁可把越界框裁回边界也不保留越界值。井盖破损标注的主要区域在画面内框边缘多出来的部分通常是标注员拖拽失误裁掉不影响语义。第二个坑是宽高比极端框。井盖在路面影像里通常是近圆形或方形但侧视角下会变成扁椭圆。如果某个标注框的宽高比超过 15大概率是误标了旁边排水沟栅栏或管线井。这类框在归一化后拉伸成窄条会把检测器对目标尺寸的先验分布带偏训练出的模型更容易把长条形的路面物体误检成目标。我在转换脚本外再加一道过滤宽高比超过 15 的框直接丢弃按 xml 文件名记入日志留给人确认。第三个坑是 float 精度。归一化时用.6f1280 像素宽的照片理论误差在 0.0005 像素量级可接受但千万别用round(x, 2)或写成整数百分比。训练器读 txt 时把字符串转 float精度不足的标注会被 yolo 损失函数放大因为框回归用的是 IoU 联合优化坐标量化到小数点后两位会让梯度反复震荡表现为 loss 曲线像锯齿。第四个坑是 XML 里同时存在多个size节点的情况很罕见但确实遇到过。标注链路里有人用工具重写过 xml把原图 size 和缩略图 size 都写进了同一文件解析脚本只取第一个坐标全错。转换时加一个断言遍历到的size节点必须唯一否则直接报错不要静默用第一个。3.3 最难查的坑类别顺序错位这个坑最隐蔽。VOC 转 YOLO 后txt 每行第一个数字是 class id不是类名。如果转换脚本里 CLASS_NAMES 的顺序与训练配置 data.yaml 里的 names 顺序不一致比如转换时“破损”在 0、“完好”在 1而 data.yaml 写成“完好”在 0、“破损”在 1模型训练和验证时不会有任何报错只有最后看预测结果发现完好井盖被标成破损上报才会意识到类别错位。怎么防我的习惯是不在转换脚本里手工写顺序而是先从 VOC 的 xml 文件里扫描全量name按类别出现顺序生成一份 classes 映射再以此为准写转换脚本和 data.yaml。扫描命令就是 2.3 节里那条 grep跑完再看一眼每个类别的样本量分布确认没有漏类或混入无关类。扫描完再做一次冒烟验证随机抽三张转换后的 txt对照原图确认坐标框中心点落在井盖上并人工核对类别编号和业务语义一致。这个动作最多半小时但能拦住后面好几天的无效训练。我自己的血泪经验是类别错位往往在训练跑到第 40 轮、准备验收时才暴露回头清洗标签要花的力气远比一开始核对多得多。4. 用 YOLOv8 训练这套井盖数据data.yaml 与关键参数格式转换完之后就进入“yolov8训练自己的数据集”的常规流程。我用 YOLOv8 举例因为工程落地里它最通用这些参数可以平移到后续版本。4.1 目录重组与 data.yaml 写法如果 zip 已经给了 YOLO 侧的 images/labels 目录那就不需要重组只需确认 train/val 划分。如果只有 VOC 侧按上一章转换脚本生成 labels 后建议目录固定成这个形态manhole_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── manhole.yaml再写训练配置文件# manhole.yaml path: /home/user/manhole_dataset # 改成你解压后的绝对路径 train: images/train val: images/val # 类别顺序要和转换脚本里的 CLASS_NAMES 完全一致 names: 0: intact 1: broken 2: missing 3: displaced这里有几个常见写法差异一是train项写相对路径时YOLO 会和path拼接如果写了绝对路径就别再让path参与拼接。二是 val 目录必须存在且非空否则训练会直接报val: not found。三是如果想固定验证集可以给 train/val 写 txt 文件列表文件里每行是图片绝对路径这种写法在多机训练时更好用但单机随手拷贝数据集时反而容易踩路径坑我一般只在需要固定划分时才用。4.2 训练命令与 4 个不要乱调的参数推荐先用官方预训练权重不要从零随机初始化。井盖是刚性目标迁移学习比随机初始化收敛快很多1377 张的小数据量上尤其明显。yolo detect train \ datamanhole.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ seed42 \ device0100 epochs、640 输入、16 batch 是基线值。真正要盯的是下面四个参数。imgsz640井盖在巡检图中占比差异很大。无人机正射影像里一个 1280 宽的图井盖可能只有 60 像素宽缩到 640 输入后只剩 30 像素小目标几乎学不到东西。这时要么直接imgsz1280要么先做切图把原图切成 640 或 896 的小块再训练。我的经验是 imgsz 从 640 提到 1280mAP.5:.95 通常能涨 3 到 5 个点但显存占用接近翻倍要按卡量力而行。batch16显存允许范围内尽量大。1377 张训练集batch 太小会让 BN 统计不稳定loss 在 30 epoch 前后反复震荡。8G 显存建议batch8, imgsz640起步不要硬上 batch16 爆显存。lr00.01用预训练权重时我通常不动。lr 太大会把预训练特征冲掉太小则 100 轮学不完。如果前 10 轮 loss 完全不动优先检查标注是不是为空或坐标异常而不是去调学习率。seed42一定要写。训练里有 mosaic、随机 HSV 增强这些随机因素不锁 seed同一份数据两次训练 mAP 能差 2% 以上做参数对比时根本分不清是调参收益还是随机波动。再说一个很多人爱动但我不建议初学者动的东西yolo 损失函数里三个权重box、cls、dfl的配比。在 1377 张这种量级的数据集上调损失权重带来的收益远远小于标注噪音带来的干扰我见过的大多数反复调参最后都不如把训练集里几十张错标图抽出来重标一遍有效。其余参数像 workers、cache、patience 属于运行环境参数。cacheTrue把图片预加载到内存能明显加快小数据集训练但内存不足会直接卡死patience30配合早停避免无效长训。4.3 验证与推理best.pt 到手后先看置信度分布训练结束我一般不会直接看 train 曲线拍板而是立刻跑一次评估和批量推理yolo val modelruns/detect/train/weights/best.pt \ datamanhole.yaml \ imgsz640 \ batch8 yolo predict modelruns/detect/train/weights/best.pt \ sourcetest_images/ \ save_txtTrue \ save_confTrueval 会输出每类的 precision、recall、mAP50、mAP50-95。对井盖四分类重点看missing类的 recall。丢失类在数据里往往样本少模型学习时偏向把目标判成频度更高的破损如果 recall 低于 0.8说明漏了不少井洞这类漏报在实际巡检里是要出事的。predict 时我会开save_confTrue把置信度一并存进 txt再写个小脚本统计“各类别在不同置信度阈值下的检出数”画一条置信度-召回曲线。这样做不是为了严谨评估而是为了判断上线阈值定在 0.25、0.45 还是 0.6 合适。井盖检测的误报代价和漏报代价不对称阈值不能只看 mAP 最大的位置。5. 井盖数据集训练和推理中的 5 条避坑记录现象、原因、解决以下五条是真实街道影像上反复踩过的账按“现象、原因、解决”的顺序写。5.1 训练卡在 Scanning labels或起步后 Samples 为 0现象输入训练命令后日志停在Scanning labels很久或者显示 Sample 数是 0一个 epoch 都走不完。原因标签文件和图片文件没有同名对应或者标签内容是空文件。常见于 zip 解压后路径被截断比如labels/train/001.jpg.txt这类嵌套怪名另一个常见原因是 VOC 转 YOLO 时把00002.xml转成了00002_jpg.txt而图片叫00002.jpg。解决写一个两分钟的自查脚本。cd manhole_dataset for f in labels/train/*.txt; do base${f%.txt} imgimages/train/$(basename $base).jpg if [ ! -f $img ]; then echo MISSING IMG: $img fi if [ ! -s $f ]; then echo EMPTY LABEL: $f fi done先处理 MISSING IMG说明命名对不上再处理 EMPTY LABEL。背景图保留本身没问题但如果是大量文件为空说明转换脚本在类别过滤时把目标全部滤掉了要回头查类别名大小写。还有一种情况是图像侧是 HEIC 或 png 后缀标签侧按 jpg 生成同样会报 MISSING IMG我的处理是先把图像统一转成 jpg再做文件名对齐不让后缀问题混进训练链路。5.2 完好和破损互相误判混淆矩阵主对角线外面那两条最刺眼现象val 的 mAP 不低打开混淆矩阵发现intact有 20% 被预测成brokenbroken也有 15% 被预测成intact。原因这大概率不是模型能力问题而是标注口径不一致。不同标注员对“破损”的定义有偏差一条浅裂缝算不算破损井盖边角略微变形算不算同一个人在不同批次标也会漂移。解决把训练集中所有broken和intact的样本各抽 50 张出来看通常能明显看出两派标注风格。我的处理是挑出模棱两可的样本做一次标签复核或者更省事先合并成二分类“需维修 / 不需维修”把问题压成单决策边界等后续有更精细的标注补充再升级四分类。井盖检测落到真实运维第一优先级是“要不要派人去现场”类别分得太细但噪声大反而没有实用价值。5.3 丢失类在夜间和积水路面疯狂误报现象白天晴天验证集 mAP 好看一旦换到夜间路灯场景或雨后潮湿路面大量井盖周围的深色区域被误判成“丢失”巡检视频里出现一连串假报警。原因丢失类的本质特征是“井口露出深色空洞”但夜间井盖周围阴影、路面积水倒影、沥青修补块局部纹理同样是深色区域单帧局部特征区分不了。这属于场景和类别语义的冲突不是简单增强能根治的。解决两个层面。训练层面把夜间、雨天、逆光的井盖图像并入数据集并对丢失类做针对性增强随机降低亮度、加高斯噪声、轻微运动模糊让模型不再依赖“很黑”这一条线索。推理层面不要单帧定论。巡检车或无人机经过同一井盖能拍到多帧做连续帧投票——同一位置连续 3 帧以上检出丢失且中心点相对位移稳定才触发报警。这个逻辑能把误报率直接砍掉一半以上。5.4 loss 正常下降mAP50 也不错但 mAP50-95 一直上不去现象训练 loss 曲线平滑下降mAP50 到 0.85可 mAP50-95 卡在 0.45 左右怎么调都涨不动。原因mAP50-95 对框的位置精度和 IoU 重叠非常敏感。井盖小目标在原图 1280 里可能只有 40 像素输入缩到 640 后只剩 20 像素框哪怕偏 3 个像素IoU 就从 0.7 掉到 0.5。这不是模型收敛问题是“输入分辨率把小目标细节丢了”。解决优先把imgsz提到 1280 重新训练显存不够就做切图训练。切图做法是把原图切成 640×640 patch同时把落在 patch 边缘被截断的目标过滤或延边补齐。这一步对井盖这种分布较均匀的路面目标特别有效。如果显存实在吃紧换 yolov8s 而不是更小的 n小模型在小目标上反而更容易欠拟合。5.5 同一份数据两次训练结果差两个点以上复现不了现象完全相同的数据和命令昨天跑 mAP50 0.87今天重跑只有 0.84换个机器差别更大。初看像玄学其实是随机性没锁住。原因YOLO 训练默认开启 mosaic、随机旋转、HSV 增强这些变换的随机种子没有固定验证集划分也可能重新随机两个扰动叠加几点的波动很正常。解决命令里固定seed42同时把验证集固定下来。如果 data.yaml 只给了 train/val 目录而每次启动内部重新划分就不如自己生成稳定的 train.txt 和 val.txt 两个清单让每次训练面对同一批验证图。再配合随机增强里的 seed基本能把两次训练的 mAP 差压到 1 个点以内。做完这一步再谈调参对比才有意义。6. 进阶用连续帧与 F1 把模型从“能跑”变成“能验收”井盖检测最终不是看一张图出几个框而是看能不能形成一条可验收的巡检结论。我提两个进阶用法。第一个是把评估指标从 mAP 切到 F1 和漏报率。市政项目里业主更关心“丢失类有没有漏”“完好类误报了几次”。我习惯在 val 之后对missing类单独算 recall在某个置信度阈值下跑完整条巡检片段统计每公里的误报数与漏报数。这几年很多项目都要求巡检系统输出精确率、召回率报告只交一个 mAP 数字不够验收。第二个是连续帧的时序确认。用一个很轻的“位置网格加计数”逻辑就能做不需要上跟踪算法from collections import defaultdict # 简化的网格计时逻辑同一网格连续命中才算一次告警 counter defaultdict(int) threshold 3 for frame_id, dets in enumerate(frame_sorted_detections): for det in dets: if det.conf 0.45: continue # 按 100 像素网格粗分坐标先聚合再计数 grid (det.cls_id, round(det.cx / 100), round(det.cy / 100)) counter[grid] 1 if len(dets) 0: # 该帧没有检测时对所有计数减半避免长时间累积误报 for k in list(counter.keys()): counter[k] // 2 if counter[k] 0: del counter[k] alarms {k: v for k, v in counter.items() if v threshold}逻辑不依赖复杂跟踪只要求帧间井盖位置相对稳定很适合巡检车匀速直线行驶的场景。参数上conf 阈值 0.45 需要根据第 4 章那条置信度-召回曲线回调threshold3 对应“连续三帧确认”。至于“yolo实例分割”方向的扩展如果项目要计算破损面积或井盖尺寸bbox 检测只能给位置面积测量必须换分割模型。做法是把 VOC 的 bndbox 坐标转成 polygon再喂给 yolo-seg 训练注意原本只有 bbox 的数据集不能直接训分割。另一个可考虑方向是把bdd100这类道路场景数据作为补充负样本用来压掉夜间误报但补充时注意别把井盖数据本身的场景分布冲散。最后说一个我的习惯拿到任何 zip 数据集先花固定半小时跑完第 2 章的五个校验再转换、再训练每一批误报坏例都丢进一个 bad case 文件夹下个版本训练时抽出来复盘。类别错位白跑一周末的事我不想再来第二次这套工序不炫技但能把井盖检测做成每天能交活的系统。希望帮到你。本文还有配套的精品资源点击获取