城市道路井盖缺陷检测数据集:1377张VOC+YOLO双格式,4类标注与训练避坑指南
简介本资源为城市道路井盖破损与丢失检测数据集面向计算机视觉算法工程师、智慧城市巡检项目开发者及高校相关课题研究者可用于目标检测模型的训练、验证与算法对比实验。数据集共1377张jpg图片每张均配有对应的VOC格式xml标注与YOLO格式txt标注标注工具为labelImg涵盖jg、jg_ds、jg_nd、jg_ps四个类别总标注框数达1722个其中jg类872框、jg_ps类623框、jg_ds类177框、jg_nd类50框类别分布清晰便于按需筛选与均衡处理。压缩包内文件总数2000个以1377个xml与623个txt标注文件为主整体约212.35MB目录结构规整可直接接入主流检测框架。目前已有426人学习下载适合需要快速构建井盖异常检测基线、开展多类别目标检测实验的读者参考使用。1. 城市道路井盖数据集1377 张 VOCYOLO 双格式4 类缺陷怎么落地井盖破损检测这个场景做市政巡检、智慧城管、道路病害识别的团队几乎都绕不开。真实痛点很直接公开数据集里专门针对井盖的少带精细类别标注的更少能同时给你 Pascal VOC 和 YOLO 两套格式、拿来就能训的基本靠碰。这份资源就是冲着这个缺口来的——1377 张 jpg 图片配套 1377 个 VOC 格式 xml 和 1377 个 YOLO 格式 txt4 个类别总框数 1722标注工具是 labelImg。它解决的不是有没有数据的问题而是拿到手能不能直接进 YOLO 训练管线、要不要自己再转格式的问题。适合两类人一类是想快速验证井盖检测模型可行性、不想在数据清洗上耗一周的算法工程师另一类是做课程设计或毕设、需要一份结构规整、类别定义清晰的数据集来跑通全流程的学生。下面按这数据长什么样 → 怎么读怎么转 → 怎么训 → 坑在哪的顺序拆开讲。2. 数据集结构与类别定义4 个标签到底在标什么2.1 目录组织与文件对应关系拿到压缩包解压后第一件事不是急着写训练脚本而是先把目录结构和文件命名规则摸清楚。这份数据集的核心特征是三件套对齐每张 jpg 图片对应一个同名 xmlVOC 格式对应一个同名 txtYOLO 格式。也就是说 1377 张图理论上应该有 1377 个 xml 1377 个 txt缺一个都说明解压或传输出了问题。常见的目录组织有两种。一种是平铺式所有 jpg 在一个文件夹所有 xml 在一个文件夹所有 txt 在一个文件夹靠文件名关联。另一种是按 images / annotations / labels 分目录。这份资源从正文描述看是jpg 图片 VOC xml YOLO txt三类文件并存实际使用时建议先跑一遍对齐检查别默认它一定整齐。# 先统计三类文件数量确认是否都是 1377 find . -name *.jpg | wc -l find . -name *.xml | wc -l find . -name *.txt | wc -l # 检查是否有图片缺少对应的 xml 或 txt for f in *.jpg; do base${f%.jpg} [ -f ${base}.xml ] || echo 缺 xml: $base [ -f ${base}.txt ] || echo 缺 txt: $base done这段脚本的逻辑很直白先数数量再逐个图片反查配套文件。参数上没什么可调的*.jpg换成你实际的扩展名即可。如果输出里出现大量缺 xml或缺 txt先别怀疑数据集本身八成是解压时文件名编码出了问题——中文路径或特殊字符在跨系统解压时经常翻车这是血泪经验。2.2 四个类别的语义与分布类别名称是jg、jg_ds、jg_nd、jg_ps从命名习惯推断jg是井盖的拼音首字母后面三个后缀大概率对应不同缺陷或状态类型。数据集没有给出每个后缀的官方中文释义这点必须说清楚——你拿到手后最稳妥的做法是抽几十张图把每个类别的框可视化出来自己确认语义边界别硬猜。从框数分布看数据是明显不均衡的类别框数占比jg87250.6%jg_ps62336.2%jg_ds17710.3%jg_nd502.9%合计1722100%jg_nd只有 50 个框是典型的少数类。这个分布直接决定了你训练时的策略如果直接按默认配置训模型大概率会把jg_nd学废召回率低得难看。常见做法是在损失函数里给少数类加权或者在数据加载阶段对含jg_nd的样本做过采样。具体怎么加权后面第 4 章会展开。提示类别语义没官方说明时先做可视化确认再动手训。标错语义的代价远大于多花半小时看图。2.3 VOC 与 YOLO 格式的差异VOC 格式的 xml 存的是绝对像素坐标结构是bndbox下的xmin/ymin/xmax/ymax。YOLO 格式的 txt 存的是归一化后的中心点加宽高每行class_id cx cy w h值都在 0 到 1 之间。这份数据集两套都给省了你写转换脚本的功夫但要注意一个细节YOLO 的class_id是从 0 开始的整数而类别名到 id 的映射关系数据集里不一定给了单独的classes.txt或data.yaml。这是最容易踩的坑之一——id 和名字对不上训出来的模型类别全乱。# 从 VOC xml 反推类别名到 id 的映射确认 YOLO txt 里的 id 含义 import os import xml.etree.ElementTree as ET names set() for f in os.listdir(annotations): if not f.endswith(.xml): continue tree ET.parse(os.path.join(annotations, f)) for obj in tree.findall(object): names.add(obj.find(name).text) # 排序后打印这个顺序通常就是 YOLO id 的映射依据 for i, n in enumerate(sorted(names)): print(i, n)这段代码遍历所有 xml收集出现过的类别名排序后打印。逻辑说明labelImg 导出 YOLO 格式时class_id 一般按类别名在classes.txt里的顺序或字母序生成。参数上sorted(names)是假设按字母序如果你的 txt 里 id 对不上就得手动核对。跑完这一步你手里就有了 id 到名字的确定映射后面写data.yaml才不会错。3. 从 VOC 到 YOLO 训练管线转换、划分与配置3.1 格式转换与校验虽然数据集已经给了 YOLO txt但实际项目里你经常需要重新生成或校验原因有两个一是原始 txt 可能和 xml 存在细微不一致比如某张图漏标二是你要按自己的划分重新组织目录。所以转换脚本还是得会写。import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射必须和数据集实际 id 一致 classes [jg, jg_ds, jg_nd, jg_ps] class_to_id {c: i for i, c in enumerate(classes)} def voc_to_yolo(xml_path, img_path, out_path): tree ET.parse(xml_path) root tree.getroot() # 读图片真实尺寸不要用 xml 里的 size偶尔会不一致 w, h Image.open(img_path).size lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_to_id: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化中心点与宽高 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{class_to_id[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))逻辑说明核心是把绝对坐标转成归一化中心点格式。参数上classes列表的顺序必须和数据集 YOLO txt 里的 id 一致这是整个转换的命门。用Image.open读真实尺寸而不是 xml 里的size是因为 labelImg 偶尔会记录错误尺寸用真实尺寸更稳。:.6f保留六位小数是 YOLO 生态的常见精度够用且不冗余。转换完必须校验不能转完就训。校验方法是把 YOLO txt 反画回图片上看框对不对import cv2 def draw_yolo(img_path, txt_path, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: cid, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(out_path, img)抽 20 到 30 张画出来看重点看少数类jg_nd的框有没有偏移或漏标。这一步花十分钟能省掉后面训完发现 mAP 异常再回头查的两小时。3.2 训练集验证集划分1377 张图按 8:1:1 或 7:2:1 划分都行。但井盖数据集有个特殊性同一路段、同一井盖可能被拍了多张如果随机划分训练集和验证集里出现同一井盖的不同角度验证指标会虚高。常见做法是按拍摄批次或文件名前缀分组划分让同一来源的图只出现在一个集合里。import random import os import shutil random.seed(42) imgs [f for f in os.listdir(images) if f.endswith(.jpg)] random.shuffle(imgs) n len(imgs) train_end int(n * 0.8) val_end int(n * 0.9) splits { train: imgs[:train_end], val: imgs[train_end:val_end], test: imgs[val_end:] } for split, files in splits.items(): os.makedirs(fdataset/{split}/images, exist_okTrue) os.makedirs(fdataset/{split}/labels, exist_okTrue) for f in files: base f[:-4] shutil.copy(fimages/{f}, fdataset/{split}/images/{f}) shutil.copy(flabels/{base}.txt, fdataset/{split}/labels/{base}.txt)逻辑说明random.seed(42)固定随机种子保证划分可复现这在团队协作里很重要。参数上0.8 和 0.9 是划分比例你可以按需调。如果文件名里带路段或批次信息把random.shuffle换成按前缀分组再分配能避免数据泄漏。3.3 data.yaml 与训练配置YOLO 训练需要一个data.yaml描述数据路径和类别。这份数据集 4 类写起来不复杂但路径和类别顺序必须和实际一致。path: ./dataset train: train/images val: val/images test: test/images nc: 4 names: 0: jg 1: jg_ds 2: jg_nd 3: jg_ps参数说明nc是类别数必须等于 4names的 id 顺序必须和 YOLO txt 里的 class_id 完全对应错一个整个训练就废了。path用相对路径方便整个项目打包迁移。训练命令以 YOLOv8 为例yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/jinggai \ nameexp1参数说明model选yolov8n.pt是因为数据集规模不大n 版本够用且训得快imgsz640是默认输入尺寸井盖目标通常不小640 够batch16看显存显存小就降到 8patience20是早停耐心值20 轮没提升就停省时间。lr00.01是初始学习率如果训到一半 loss 震荡可以降到 0.001 再试。4. 类别不均衡与训练调参少数类怎么救4.1 类别不均衡的影响前面表格已经暴露了问题jg_nd只有 50 个框占比不到 3%。这种量级的少数类在标准交叉熵损失下梯度贡献几乎被多数类淹没。典型现象是训练日志里jg_nd的 mAP 长期在 0 附近徘徊或者模型干脆不预测这个类。这不是模型不行是数据分布逼的。常见应对有三条路损失加权、过采样、以及调整数据增强策略。三条可以叠加用但别一次全上否则变量太多出问题不好定位。4.2 损失加权与过采样YOLOv8 本身不直接暴露类别权重参数但可以通过自定义损失或在数据层做文章。最省事的做法是过采样把含jg_nd的图片在训练集里复制多份让它的出现频率接近其他类。import os import shutil # 找出所有含 jg_nd 的标注文件 nd_files [] for f in os.listdir(dataset/train/labels): with open(fdataset/train/labels/{f}) as fp: for line in fp: if line.startswith(2 ): # jg_nd 的 id 是 2 nd_files.append(f[:-4]) break # 过采样 3 倍 for base in nd_files: for i in range(3): shutil.copy(fdataset/train/images/{base}.jpg, fdataset/train/images/{base}_dup{i}.jpg) shutil.copy(fdataset/train/labels/{base}.txt, fdataset/train/labels/{base}_dup{i}.txt)逻辑说明先扫描标注文件找出含jg_ndid2的样本然后复制 3 份并加后缀避免重名。参数上startswith(2 )里的 2 必须和你的类别 id 一致写错就找错类。复制倍数 3 是经验值让jg_nd的框数从 50 提到 200 左右接近jg_ds的量级。注意过采样只在训练集做验证集和测试集保持原始分布否则评估指标失真。4.3 数据增强的取舍井盖检测的增强策略和通用目标检测略有不同。翻转、缩放、色彩抖动都可以用但要注意井盖的破损形态和拍摄角度相关垂直翻转可能生成不真实的样本井盖不会倒挂所以垂直翻转要慎用或禁用。Mosaic 增强对少数类有帮助因为它能把多张图拼在一起增加少数类和其他类共现的机会。# YOLOv8 增强配置片段写在训练命令或配置里 flipud: 0.0 # 禁用垂直翻转 fliplr: 0.5 # 水平翻转保留 mosaic: 1.0 # 开启 Mosaic scale: 0.5 # 缩放幅度 hsv_h: 0.015 # 色调抖动 hsv_s: 0.7 # 饱和度抖动 hsv_v: 0.4 # 明度抖动参数说明flipud: 0.0是关键井盖场景垂直翻转不合理mosaic: 1.0表示始终启用对少数类友好scale: 0.5控制缩放范围井盖大小差异大适度缩放有帮助。这些值不是绝对的训完看混淆矩阵再微调。5. 避坑与排查这份数据集最容易翻车的五个点5.1 类别 id 与名字对不上现象训练能跑loss 也降但验证时所有预测框的类别都是错的或者某个类永远不出现。原因YOLO txt 里的 class_id 和data.yaml里names的顺序不一致。labelImg 生成 id 的顺序取决于classes.txt而这份数据集没明确给映射。解决用第 2.3 节的脚本从 xml 反推类别名顺序和 txt 里的 id 逐一核对确认后再写data.yaml。核对时抽 10 张图把 txt 第一列的数字和图上框的类别对照最直接。5.2 图片尺寸与标注尺寸不一致现象画框校验时发现框整体偏移或大小不对。原因xml 里记录的size和图片真实尺寸不符labelImg 在某些情况下会写错。解决转换时一律用Image.open读真实尺寸别信 xml 里的width/height。如果已经用 xml 尺寸转了一批重新用真实尺寸转一遍覆盖即可。5.3 少数类被模型完全忽略现象训练日志里jg_nd的 mAP 始终为 0混淆矩阵里该类全被预测成背景或其他类。原因样本太少梯度贡献不足。解决按第 4.2 节做过采样同时检查增强配置里 Mosaic 是否开启。如果过采样后仍无改善考虑把jg_nd和其他相似类合并或者单独训一个二分类模型先验证可行性。5.4 验证集指标虚高现象验证集 mAP 很高但实际部署时效果差。原因随机划分导致同一井盖的不同照片同时出现在训练集和验证集数据泄漏。解决按文件名前缀或拍摄批次分组划分确保同一来源只进一个集合。如果文件名没有批次信息至少用图片相似度做一次去重把高度相似的图分到同一侧。5.5 训练中途 loss 变 NaN现象训到几十轮loss 突然变成 NaN训练崩溃。原因学习率过大、某批数据标注异常比如宽高为 0、或者混合精度训练数值溢出。解决先把lr0降到 0.001 重跑再检查标注文件里有没有0 0 0 0 0这种空行或宽高为 0 的框写个脚本扫一遍过滤掉如果用了 AMP关掉试试。这类问题排查顺序是先降学习率再查数据最后查精度设置。6. 进阶用混淆矩阵和 PR 曲线定位真实短板训完一个模型看总 mAP 只是第一步真正能指导下一步优化的是混淆矩阵和各类的 PR 曲线。YOLOv8 训练完会在runs/下生成confusion_matrix.png和PR_curve.png这两个图比任何日志都直观。混淆矩阵看的是谁被错认成谁。井盖数据集里重点看jg_nd那一行如果它大量被预测成jg或背景说明模型没学到jg_nd的判别特征要么加样本要么检查这个类的标注是否一致比如有的标了破损有的标了完整。PR 曲线看的是每个类的召回和精度权衡jg_nd的曲线如果整体偏低且平说明这个类难分考虑单独调整它的置信度阈值。# 用验证集跑一次预测导出每个类的指标 from ultralytics import YOLO model YOLO(runs/jinggai/exp1/weights/best.pt) metrics model.val(datadata.yaml, splitval) # 打印每个类的 mAP50 和 mAP50-95 for i, name in enumerate(metrics.names.values()): print(f{name}: mAP50{metrics.box.ap50[i]:.4f}, fmAP50-95{metrics.box.ap[i]:.4f})逻辑说明model.val跑验证集返回的metrics.box.ap50是每个类在 IoU0.5 时的 APap是 0.5 到 0.95 的平均。参数上splitval指定用验证集别用 testtest 留到最后一次评估。跑完对比四个类的数值jg_nd如果明显低于其他类就回到第 4 章做针对性处理。还有一个实用技巧把验证集里预测错的样本单独挑出来看。YOLOv8 的val结果里可以拿到每张图的预测和真值对比写个脚本把 FP 和 FN 最多的图筛出来人工看一遍往往能发现标注问题或场景盲区。比如某些夜间或逆光图片模型全漏那就得考虑补这类场景的样本或者加亮度增强。我自己的习惯是每次训完新模型先不看总 mAP先把混淆矩阵和jg_nd的 PR 曲线拉出来看一遍确认少数类没被牺牲再决定要不要调参重训。这个顺序帮我省过很多次总指标好看但实际不能用的返工。从那以后我每次拿到新数据集都强制先跑一遍类别分布统计和标注可视化再动手写训练脚本。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →