ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

1876张鼠标图搞定YOLO训练:VOC与YOLO格式转换校验全指南

2026/9/23 13:01:57 拓冰建站 浏览量
1876张鼠标图搞定YOLO训练:VOC与YOLO格式转换校验全指南 简介面向目标检测与计算机视觉入门学习者一套包含1876张鼠标图片的检测数据集提供Pascal VOC与YOLO两种主流标注格式免去格式转换成本可直接用于YOLO系列、SSD等模型的训练与验证。资源包共2000个文件以XML标注与TXT标注为主压缩后大小约219.91MB类别统一为mouse共包含2261个矩形标注框由labelImg工具绘制框体准确合理。目前已有296人学习下载适合作为目标检测实战练习或算法对比的起步数据集。使用者可借助该数据集熟悉VOC与YOLO标注结构测试数据加载流程并在此基础上扩充自有样本。1. 1876张鼠标图能做多少事先把格式这笔账算清提到“鼠标数据集1876张VOCYOLO格式”很多人的第一反应是把JPEG一股脑丢进训练脚本。以我处理中小型目标检测数据集的经验真正卡住进度的往往不是模型而是标注本身VOC的XML记录的是绝对像素坐标YOLO的txt是归一化小数两个目录只要有一处对不上训练前就得先烧掉一整晚。这个数据集的定位很清楚——单类别目标检测检测目标就是图片里的鼠标。1876张图作为训练集不算充裕但配合预训练权重做迁移学习在桌面物件识别、智能办公场景、实验室行为采集回放这些方向都足够跑通完整闭环。适合三类人第一次给YOLOv5/v8做自建数据的入门者需要写校验脚本的算法工程师以及想在边缘设备上快速出模型的团队。下面按“看懂格式 → 校验数据 → 常见问题 → 训练配置 → 验证效果”的顺序讲完。2. 看懂 VOC 和 YOLO 的双格式坐标系换算关系先踩实2.1 VOC 的 annotation 文件里记录了什么VOCPASCAL VOC格式的标注文件是 XML习惯放在 Annotations 目录里每张图片对应一个同名 XML。打开之后核心字段就四块文件名、图片尺寸、object 列表、每个 object 的 bndbox 坐标。以鼠标检测为例一个简化后的 XML 长这样annotation folderJPEGImages/folder filenamemouse_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namemouse/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin386/xmin ymin244/ymin xmax621/xmax ymax431/ymax /bndbox /object /annotation注意这里xmin、ymin、xmax、ymax是像素坐标单位是“个像素点”不是百分比。bndbox 四个值直接标出了鼠标左上角和右下角的位置。name就是类别名像这份鼠标数据集里大概率叫 mouse但具体叫什么不能靠猜应该用脚本扫一遍确认。size里的宽高是图片的真实像素尺寸这一点很重要因为 YOLO 的坐标还原全靠它来换算后面我会专门讲这个坑。拿到一份 VOC 格式数据集我一般先做三件事统计 object 类别种类、确认 XML 数量和图片数量一致、抽查几份 XML 的尺寸与真实图片是否吻合。很多下载的数据集是从不同渠道拼出来的XML 里的 width 和 height 跟 JPEG 实际尺寸对不上并不罕见。2.2 YOLO txt 的一行五个数字是怎么来的YOLO 格式的标注是纯文本每行对应一个目标格式是“类别ID 中心点x 中心点y 宽度 高度”五个数字用空格分隔。中心点和宽高全部做了归一化即除以图片宽和高。下面这一行0 0.262239 0.312500 0.122396 0.086806拆开看0 表示类别ID中心点 x 0.262239乘回图片宽度 1920 得到像素坐标 503中心点 y 0.3125乘回高度 1080 得到 337.5宽度 0.122396 乘 1920 得到大约 235 像素高度 0.086806 乘 1080 得到大约 94 像素。也就是说真实框的左上角是(503 - 235/2, 337.5 - 94/2)右下角是(503 235/2, 337.5 94/2)也就是(385.5, 290.5)到(621, 384.5)和 VOC 里那组 bndbox 坐标能对上。换算公式我习惯写成这样像素框(x1, y1, x2, y2)转 YOLO 归一化坐标cx (x1 x2) / 2 / width cy (y1 y2) / 2 / height bw (x2 - x1) / width bh (y2 - y1) / height反向还原就乘回去注意宽高不能为负否则说明画框时坐标写反了。这个双向换算关系是判断“两个格式是不是同一份标注”的基准。拿到一个数据集先用脚本把 XML 转成 YOLO 坐标再和自带的 txt 对比如果偏差在千分之一以内说明两份文件是对应的如果差很多说明其中一套标注不是从同一份框导出的训练前得统一。2.3 一份鼠标数据集常见的目录组织方式VOC YOLO 双格式的数据集目录组织一般有两种风格。一种是兼容训练框架的官方规范常见结构如下mouse_dataset/ ├── images/ │ ├── train/ │ │ ├── mouse_0001.jpg │ │ └── ... │ └── val/ │ ├── mouse_0002.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── mouse_0001.txt │ │ └── ... │ └── val/ │ ├── mouse_0002.txt │ └── ... └── data.yaml另一种是保留 VOC 原汁原味的结构JPEGImages 放图片、Annotations 放 XML、labels 放 YOLO txt。两种结构没有优劣之分但训练脚本对路径的要求不一样。如果拿到的是第二种需要自己写一个划分脚本把图片和标注按比例分到 train/val 目录里。我见过不少下载即用的数据集train 和 val 目录里图片数量跟标签数量不一致。比如 images/train 有 1500 张但 labels/train 只有 1490 个 txt——少的那些图要么没目标要么标注文件丢了。正因如此我从不跳过校验直接训练。下面这张表可以快速对照两种格式在表示方式上的差异对比项VOC 格式YOLO 格式文件扩展名.xml.txt坐标类型绝对像素坐标归一化相对坐标类别表示类别名字符串类别ID整数一行描述多个 object 块每个目标一行是否需要图片尺寸标注里自带需要通过图片读取坐标类型的差异决定了坑的位置一个在绝对值的边界一个在归一化的比例。把这两点头脑里立住后面检查脚本跑起来就有数了。3. 训练前的数据校验三个脚本把双格式底细摸清3.1 用脚本扫描 XML统计类别、数量与尺寸拿到数据集第一步我建议先写个通用脚本扫描所有 XML输出三类信息XML 文件总数、类别分布、每个 XML 的图片尺寸分布。这样能快速发现“标注里混入了意外类别”或者“不同图片尺寸交错”这类基础问题。代码如下import os import xml.etree.ElementTree as ET def scan_voc(annotation_dir): xml_files [f for f in os.listdir(annotation_dir) if f.lower().endswith(.xml)] print(XML 文件总数:, len(xml_files)) cls_stats {} size_stats {} for xml_file in xml_files: path os.path.join(annotation_dir, xml_file) tree ET.parse(path) root tree.getroot() # 读取图片尺寸 size root.find(size) w int(size.find(width).text) h int(size.find(height).text) size_stats[(w, h)] size_stats.get((w, h), 0) 1 # 统计每个 object 的种类 for obj in root.findall(object): name obj.find(name).text.strip() cls_stats[name] cls_stats.get(name, 0) 1 print(类别统计类别名: 目标数量:) for name, cnt in cls_stats.items(): print(f {name}: {cnt}) print(尺寸分布宽, 高: XML数量:) for size_key, cnt in sorted(size_stats.items()): print(f {size_key}: {cnt}) return cls_stats, size_stats if __name__ __main__: scan_voc(Annotations)这段逻辑不复杂但有两个细节容易漏。一是用root.findall(object)而不是root.find(object)因为一张图可能同时出现多个鼠标findall 才能遍历全部。二是读取name后必须 strip 掉首尾空格标注工具导出的 XML 偶尔会在字符串周围带换行或空格不清理的话类别统计会出现两个长得一样实际不同的键。跑完这个脚本如果 size_stats 里出现了多种分辨率后续训练时图像缩放会导致不同来源图片的相对缩放比例不同但 YOLO 的归一化坐标天然兼容这个情况只要标注本身与图片一致就没大问题。真正要担心的是类别统计里出现了你没预料到的名字——比如 mouse、mouse_black、mouse_white 同时存在这就不是单类别问题了需要决定是合并还是保留多类。3.2 检查 YOLO txt 坐标边界越界、负值、空文件一网打尽YOLO 标注最常见的三类问题坐标越界、宽高为负、文件为空。出现在 txt 里的归一化坐标应该都在 0 到 1 之间宽高都大于 0。下面这个脚本专门用来抓这些问题import os def check_yolo_labels(label_dir): txt_files [f for f in os.listdir(label_dir) if f.lower().endswith(.txt)] empty_files [] bad_lines [] for txt_file in txt_files: path os.path.join(label_dir, txt_file) with open(path, r) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_files.append(txt_file) continue for line_no, line in enumerate(lines, 1): parts line.split() if len(parts) ! 5: bad_lines.append((txt_file, line_no, 字段数不为5)) continue try: cid int(parts[0]) cx, cy, bw, bh map(float, parts[1:5]) except ValueError: bad_lines.append((txt_file, line_no, 存在非数字字符)) continue if not (0 cx 1 and 0 cy 1): bad_lines.append((txt_file, line_no, 中心点坐标越界)) if not (0 bw 1 and 0 bh 1): bad_lines.append((txt_file, line_no, 宽高不在(0,1]区间)) if cid 0: bad_lines.append((txt_file, line_no, 类别ID为负数)) print(txt 文件总数:, len(txt_files)) print(空标注文件数:, len(empty_files), empty_files[:10]) print(异常标注行数:, len(bad_lines)) for item in bad_lines[:10]: print( , item) if __name__ __main__: check_yolo_labels(labels)空标注文件值得单独说一句。YOLO 训练框架遇到空 txt 通常会打印警告然后把对应图片跳过并不会中断但代价是模型少看了这张图。如果空文件多训练集的有效样本就悄悄缩水了。你要做的是确认这些图是真没目标还是导出时漏了标注。更隐蔽的问题是bw、bh为 0。有些打标工在截取非常小的目标时会出现左上角和右下角坐标相同的情况转换后宽度就是 0。训练时这类框会被当作无效目标丢弃而且 loss 有可能会写入一个异常值表现在训练曲线上就是某个 step 的 loss 突然跳一下。所以检查的阈值不是 0而是 1允许一定缓冲。3.3 双格式互转验证XML 转出的 txt 和原 txt 对比两份标注文件如果不一致训练时用的是哪份取决于框架的读取路径。为了避免“校验了半天、模型用的却是另一份”最好的办法是把两份格式搬到同一个坐标系下对比。常见的做法是用脚本把 XML 转成 YOLO 格式再和原有的 txt 逐字段比对import os import xml.etree.ElementTree as ET CLASSES [mouse] # 以扫描XML得到的类别顺序为准 def voc_to_yolo(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue cid CLASSES.index(name) bnd obj.find(bndbox) x1 float(bnd.find(xmin).text) y1 float(bnd.find(ymin).text) x2 float(bnd.find(xmax).text) y2 float(bnd.find(ymax).text) # 防止坐标越界先做裁剪再转换 x1 max(0, min(x1, img_w)) x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) if x2 x1 or y2 y1: continue cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) return lines def compare_formats(xml_dir, label_dir): mismatch [] for fn in os.listdir(xml_dir): if not fn.lower().endswith(.xml): continue base os.path.splitext(fn)[0] txt_path os.path.join(label_dir, base .txt) if not os.path.exists(txt_path): mismatch.append((base, XML存在但txt缺失)) continue converted voc_to_yolo(os.path.join(xml_dir, fn)) with open(txt_path, r) as f: original [line.strip() for line in f if line.strip()] if len(converted) ! len(original): mismatch.append((base, f目标数量不一致: xml{len(converted)}, txt{len(original)})) continue if mismatch: print(不一致样本最多展示20条:) for item in mismatch[:20]: print( , item) else: print(所有文件双格式校验通过) if __name__ __main__: compare_formats(Annotations, labels)这个脚本里有两个容易被忽略的坑。一是坐标裁剪标注工具偶尔会画出超出图片边界的框直接用原始值转换的话归一化坐标会出现负数或大于 1 的情况训练框架会报警告。我在转换前用min/max把坐标裁回图片范围内这样即使原标注有偏差也不会在训练时触发坐标过滤逻辑。二是“目标数量不一致”这条比对规则。我不比对具体数值是否一致因为浮点数经过格式化输出后末尾几个小数位可能有舍入差异逐位相等太苛刻。但目标数量不一致就是实质性差异了——说明某个 XML 里的 object 没有被正确转换成 txt这是最常见的数据缺陷。如果这个脚本跑出大面积 mismatch我的建议是别急着训练回到标注源头检查一下是哪一步丢失了目标。4. 使用双格式鼠标数据集的五个常见问题排查4.1 类别索引错位训练出来的框全偏了现象训练一个单类鼠标检测模型loss 一直在降但验证集的 mAP 低得离谱可视化检测结果时框的中心点偏在目标边缘之外或者某个固定位置出现大量小框。原因数据集里如果混了多个类别VOC 格式用的是字符串类别名YOLO 用的是类别 IDID 和类名的对应关系完全由读者自定义。当你把 VOC 里的 label 顺序设定成[mouse, keyboard]但 txt 里 mouse 的 ID 是 1 而不是 0训练框架就会把鼠标当键盘去学导致模型永远找不准目标。解决参数里把data.yaml的names列表和 txt 首列数字严格一一对应。对单类数据集来说最简单的是先扫一遍 txt确认所有首列都是同一个值然后让names: [mouse]。如果出现不同 ID那就必须重写 txt 的所有首列。4.2 XML 记录的尺寸和真实图片不一致归一化坐标全错位现象训练数据加载正常loss 也能下降但用val.py跑出来每个框都带着一个斜向偏移即框的上边缘贴着目标、下边缘多出一大截或者左右方向来回偏。原因XML 里的width和height与 JPEG 实际像素尺寸不一致。比如原图是 1920X1080某张图被缩放过但 XML 没同步更新YOLO 转换时按错误的 1280x720 做了归一化于是坐标比例整体失真。解决用 PIL 读取每张图片的真实尺寸替换并重写 XML 里的 size 节点。你可以复用前面 3.1 节的脚本把输出的尺寸分布和真实图片尺寸做对照。遇到不一致就按真实尺寸重新生成 YOLO txt。4.3 文件名后缀大小写不一致图片和标注配对失败现象训练脚本加载到约一半时提示找不到某张图片或某个标签检查后发现文件都在只是后缀大小写不同——mouse_0185.JPG写着大写而 XML 的filename里是mouse_0185.jpg。原因Windows 下导出标注时生成的引用名是.JPGLinux 的 YOLO 训练环境里文件系统区分大小写glob(*.jpg)匹配不到.JPG文件图片被静默跳过。解决写个重命名脚本把所有图片后缀统一成小写.jpg同时把 XML 里的filename强制洗一遍成小写。文件名统一有多值得等你踩完这个坑就明白了。4.4 训练集和验证集划分重复验证指标虚高现象模型训练时验证集 mAP 稳定在 0.9x自己另找一批图片测试真实 mAP 只有 0.6 左右上线效果缩水明显。原因划分 train/val 时随机数种子没有固定或者脚本里先打乱了全部图片列表再切分导致 val 里包含了 train 里的同款图片。模型相当于提前背了答案验证过程形同虚设。解决划分脚本里显式传入random.seed(42)或np.random.seed(0)并且以“同一张图片只能出现在一个集合”为原则做数据切分。划分完成后输出一份 train 和 val 文件名清单检查两边有没有交集。import os import random from sklearn.model_selection import train_test_split random.seed(42) image_dir images label_dir labels all_images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] train_files, val_files train_test_split( all_images, test_size0.2, random_state42, shuffleTrue ) print(train 图片数:, len(train_files)) print(val 图片数:, len(val_files))这里用了 sklearn 的train_test_split好处是random_state固定后每次运行划分结果完全一致同时自动完成 shuffle。注意test_size0.2是验证集比例如果之后还想留一套最终测试集应先把测试集拆出去再对剩余部分做 train/val 划分避免三条数据互相重叠。4.5 空标签文件引发训练中断或静默跳过现象训练进行到某个 epoch 时报错提示AssertionError: Label class xx exceeds nc1 in data或者一张图的标签文件是空文件训练进程直接退出。原因空 txt 文件本身不致命但有些数据集导出时会在 txt 里写一个换行符框架读取后解析出一行空字符串转成数组时字段数不足于是报错。解决跑一遍 3.2 节的检查脚本把所有空文件和“只有换行符”的文件揪出来统一清空成 0 字节。如果确认这些图确实没有目标可以保留空文件但建议在训练配置里开启skip_empty相关参数如果这些图应该有目标才是真正需要补标注的。5. 把 1876 张装进 YOLO 训练流程配置与参数选择5.1 准备工作目录和 data.yaml假设数据集的目录组织已经按第 2.3 节整理成 images/labels/train/val 结构接下来需要写一个 data.yaml 告诉训练框架“去哪找图、有几类、类名是什么”。内容如下train: mouse_dataset/images/train val: mouse_dataset/images/val nc: 1 names: 0: mouse这里的核心是nc和names必须一致都是 1 和 mouse。train和val路径写相对路径时以你运行训练命令的工作目录为基准写绝对路径最稳妥但换机器时要记得改。我习惯把 data.yaml 放在鼠标数据集根目录下和 images、labels 平级训练时直接传相对路径。还要注意一个细节YOLOv5 和 YOLOv8 对 labels 目录的默认查找规则不同。YOLOv5 默认在images同级找labels且需要images/train对应labels/trainYOLOv8 的加载逻辑更自动化但同样要求每个图片路径替换掉images为labels后能找到对应 txt。所以目录结构一定不能自己发挥按框架约定来。5.2 跑通最小训练命令与三个必调参数YOLOv5 训练命令很直接python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data mouse_dataset/data.yaml \ --weights yolov5s.pt \ --cacheYOLOv8 的 CLI 等价写法是yolo train \ datamouse_dataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ cacheTrue拿这个标题提到的 1876 张图来说三个参数我会这样调。imgsz入口尺寸640 是性价比最高的起点图上鼠标如果是中小目标640 足够保留细节显存有限可以降到 416但小目标召回率会掉。batch以不爆显存为底线8GB 显存跑 YOLOv5s 用 16 没问题实际不够就减半训练速度慢一点总好过 OOM。epochs初次跑 100配合早停机制观察 loss 是否连续 30 个 epoch 不再下降。另外--cache把图片缓存在内存里对小数据集很划算1876 张的加载速度会明显加快。如果你的硬盘是机械盘这个参数能省下大量等待时间SSD 上差异就没那么大了。5.3 迁移学习思路冻结主干从第几个 epoch 开始1876 张的规模直接从头训练一个 YOLOv5s 很容易欠拟合或者收敛到局部最优。常规做法是加载 COCO 预训练权重先冻结 backbone 训练几个 epoch让模型专注学鼠标的语义特征再解冻全部层微调。YOLOv5 可以通过以下方式实现python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data mouse_dataset/data.yaml \ --weights yolov5s.pt \ --freeze 10 \ --cache--freeze 10表示冻结前 10 层对应 YOLOv5s 的 backbone 部分。一般我按“前 50 个 epoch 冻结、后 50 个 epoch 解冻”的节奏跑前 50 个 epoch 用较小学习率 0.001让模型适应鼠标的目标空间分布后 50 个 epoch 恢复默认学习率 0.01让检测头充分拟合。如果数据集里鼠标的拍摄角度非常单一比如全是俯拍冻结时间可以延长到 80 个 epoch如果角度杂乱冻结太久反而学不到细节可以缩短到 30 个。训练中要留意 loss 曲线的形态box_loss和cls_loss稳定下降是正常信号如果box_loss降不动但cls_loss还在降说明模型已经分辨出鼠标这个类别了只是定位精度不够这时可以适当增加--iou阈值或者调高输入分辨率再试。6. 交差前最后一件事用可视化脚本验证标注框训练完成之后无论 val mAP 看起来多漂亮我最后一步永远是做可视化验证。把图片和标注框画在同一张图上人工肉眼过一遍这一步能挡住绝大多数“指标很好、实际不可用”的翻车现场。import cv2 def draw_yolo_boxes(image_path, label_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cid, cx, cy, bw, bh map(float, parts) 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.putText(img, class_names[int(cid)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(vis_ image_path.split(/)[-1], img) # 单张验证 draw_yolo_boxes(images/train/mouse_0001.jpg, labels/train/mouse_0001.txt, [mouse])这个脚本顺手画出了类别名字符串长度不同时字体大小和位置可能需要微调。多跑几张图后留意两类问题一是“框包住了鼠标但紧贴边缘裁掉一截”说明标注框画得偏紧不影响训练但影响 AP 分数二是“框明显偏大或偏移鼠标只占框的一半”如果出现这种图要返回检查是不是 XML 尺寸和真实图片尺寸不一致这对应第 4.2 节的坑。我的习惯是把 train 和 val 各随机抽 20 张做可视化全部过一遍才敢把模型交出去。毕竟训练指标可以美化但可视化结果骗不了人——这也是我用数据这么多年最实在的检查习惯。每次拿到新数据集都重复这套流程扫描 XML、查 YOLO 坐标、双格式对比、可视化抽查。过程机械但能挡住九成以上的数据脏坑。希望帮到你。本文还有配套的精品资源点击获取