
简介这是一份面向计算机视觉目标检测任务的鼠类标注数据集同时提供VOC与YOLO两种通用格式适合需要训练鼠类检测模型或进行数据集格式转换练习的开发者使用。数据集中587张左右的jpg图片均配套对应的xml与txt标注文件标注类别统一为rat覆盖多种拍摄场景与角度可直接用于检测模型的训练、验证与微调。压缩包共1762个文件包含jpg图片、xml标注和txt标注三类整体大小149.16MB解压后按图片、xml、txt三个文件夹分类存放目录清晰便于读取。目前已有146人学习下载。资源由labelImg工具完成标注并遵循目标边界准确、目标全量标注、一致性检查等规范可帮助初学者直观理解VOC与YOLO标注格式的差异也可作为目标检测数据集构建流程的参考样例。1. 鼠数据集 587 张双格式标注VOC 和 YOLO 到底值不值得都做像“鼠数据集 VOC和yolo格式目标标注587张左右”这样的项目本质上是给实验室里拍摄的小鼠图像补齐一套可用的目标检测标注每张图框出鼠的位置同时保留 VOC 的 XML 和 YOLO 的 txt 两份文件让同一份数据在训练、评估、部署时不打架。587 张不算多但鼠的目标形态相对一致、背景可控足够启动一个能用的检测模型真正耗时间的地方是标注格式的组织和转换对齐。适合做动物行为分析、药理实验、实验室监控的工程师也适合刚接触数据集建设、想弄清 VOC 和 YOLO 标注差异的算法新人。下文按标注制作、格式转换、训练调参、排错、增量优化这条链路逐个展开。2. 鼠数据标注的完整制作链路从原始视频帧到 VOC 格式 XML先强调一个顺序问题要做双格式标注一定是先做 VOC 再转 YOLO别反着来。LabelImg 这类主流标注工具原生保存的就是 PascalVOC 的 XML很多自动标注平台也只对外提供 VOC 和 COCO 格式YOLO 的纯文本标签反而要靠自己生成。更重要的是VOC 里同时保留了图像的像素坐标和尺寸信息而 YOLO 的归一化坐标一旦原始图被重新缩放就全对不上。先把 VOC 这份底稿做扎实后面转 YOLO 只是跑一段脚本的事等于给自己留了后悔药。2.1 原始图像采集帧采样、去重和类别定义鼠数据集的图像来源如果是视频而不是现成照片第一步是把视频切成图片。直接顺序读帧会产生大量相似帧train 和 val 里混入同一只鼠的同一姿态会让验证指标虚高到真实场景立刻翻车。常见做法是按事件抽帧而不是均匀抽帧比如鼠理毛、饮水、跑轮分别取样实在没有事件标注时按固定时间间隔抽取也能用关键是间隔要合适。抽帧用 OpenCV 的 VideoCapture 就能完成import cv2 cap cv2.VideoCapture(mouse_cage.mp4) fps max(cap.get(cv2.CAP_PROP_FPS), 1) total int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) step int(fps * 2.5) # 每 2.5 秒抽一帧 for i in range(0, total, step): cap.set(cv2.CAP_PROP_POS_FRAMES, i) ok, frame cap.read() if ok: cv2.imwrite(fimages/frame_{i:06d}.jpg, frame) cap.release()逻辑说明CAP_PROP_POS_FRAMES 把读取指针直接跳到目标帧号避免从头逐帧读587 张图在普通笔记本上几十秒就能抽完。关键参数是 step鼠频繁活动时调到 1 倍 fps、即每秒一帧鼠趴在角落不动时 step 可以放到 5 秒以上。这里有个容易被忽略的环节文件名要在抽帧阶段就定死后面标注生成的 XML 里的 filename 来自图片文件名一旦改名就要批量同步。图像收齐后统一后缀和尺寸。我一般用 Python 批量把 png、bmp 都转成 jpg最长边压缩到 1280 以内再做标注。这样做的原因是后续 YOLO 训练默认把输入缩放到 640原始高分辨率并不会带来精度收益反而让标注时眼睛更容易漏。对 1280 的短边做标注屏幕上一屏能看全整张图效率高不少。类别定义在标注前就要冻结。鼠检测最常见的做法是只建一个 mouse 类但如果实验里有白色和黑色两种毛色或者需要区分野生型和转基因鼠就拆成 white_mouse、black_mouse 两个类。同一批数据里被遮挡超过一半的鼠要不要标我的约定是能看出完整轮廓就标遮住 60% 以上不标这个规则必须在动鼠标之前写下来否则 587 张标到一半改规则前面全返工。2.2 用 LabelImg 标注 VOC 格式目录骨架、快捷键和 XML 必查字段安装 LabelImg 比较简单pip 直接装即可pip install labelImg labelImg首次打开后把标注模式切到 PascalVOC勾上 Auto Save再设置 JPEGImages 和 Annotations 两个目录。标注时的快捷键只用记三个w 画框、d 下一张、a 上一张。对鼠这种相对紧凑的目标画框时尽量贴合身体尾巴不要框进去——尾巴会明显拉宽框的宽高比转成 YOLO 的归一化坐标后中心点也会跟着偏移后续训练时损失函数会在这些离群框上浪费梯度。每张图保存后生成的同名 XML需要关注的核心字段如下字段含义标注时的坑filename图像文件名与 JPEGImages 不一致训练直接报找不到图片size.width图像宽度如果图片被二次压缩而 XML 没更新坐标失真size.height图像高度同上尤其注意换名和压缩同时发生时object.name类别名拼写不一致会被框架当成两个类bndbox.xmin框左边界绝对值必须小于 xmax且落在图像宽度范围bndbox.ymin框上边界绝对值必须小于 ymax且落在图像高度范围为了避免标到一半发现脏数据我每标 50 张就自动扫一遍全部 XML检查框是否越界或顺序颠倒import xml.etree.ElementTree as ET from pathlib import Path for xml_path in Path(Annotations).glob(*.xml): root ET.parse(str(xml_path)).getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) for obj in root.findall(object): 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) if not (0 x1 x2 w and 0 y1 y2 h): print(f脏数据: {xml_path} - {x1},{y1},{x2},{y2})这段脚本在 587 张图的规模下运行不到一秒。逻辑不复杂逐个 XML 读取图像尺寸再遍历每个 object 的 bndbox校验坐标是否满足 x1 x2、y1 y2 且都在画幅内。输出为空就进入下一步有输出就立即修正不要拖到转换后再排查。2.3 划分 train/val/test按视频分组切分而不是随机切分VOC 的目录规范是三个文件夹并排JPEGImages、Annotations、ImageSets/Main。前两个存图和 XMLImageSets/Main 里放划分索引每行一个不带扩展名的文件名。很多初学朋友把划分名单直接放在数据集根目录YOLO 训练时找不到索引文件白折腾一轮。对鼠行为视频我推荐的划分比例是 train:val:test 8:1:1。数据量小测试集 10% 已经足够暴露问题。关键点在于按视频来源分组切而不是按文件名随机切。同一个视频抽出的相邻帧背景一致随机切会让背景信息泄漏到验证集指标看着好换到新视频就原形毕露。下面这段脚本按视频 ID 分组切分import random from pathlib import Path random.seed(42) names [p.stem for p in Path(JPEGImages).glob(*.jpg)] # 假设文件名格式是 video01_000123.jpg取前 7 位作为视频 ID video_ids sorted({n.split(_)[0] for n in names}) random.shuffle(video_ids) n_test max(1, int(len(video_ids) * 0.1)) n_val max(1, int(len(video_ids) * 0.1)) test_videos set(video_ids[:n_test]) val_videos set(video_ids[n_test:n_test n_val]) test [n for n in names if n.split(_)[0] in test_videos] val [n for n in names if n.split(_)[0] in val_videos] train [n for n in names if n.split(_)[0] not in test_videos | val_videos] for subset, ids in [(train, train), (val, val), (test, test)]: Path(ImageSets/Main).mkdir(parentsTrue, exist_okTrue) with open(fImageSets/Main/{subset}.txt, w) as f: f.write(\n.join(ids))参数说明video_ids 先去重再打乱按视频维度切分保证同一视频的所有帧只进入一个子集union 操作避免 train 里混入 val 视频。这里 random.seed(42) 固定随机种子是为了每次实验的划分一致方便后续对模型做对比。如果你的文件名没有视频前缀需要在生成图片时就按 视频编号_帧号 的规则命名这是很小的习惯但对划分的帮助非常大。3. 从 VOC 转到 YOLOXML 到 TXT 的转换脚本与必调参数VOC 和 YOLO 的标注格式差异是刚接触目标标注的人绕不开的一道坎。VOC 的 bndbox 记录的是左上角和右下角的像素绝对值直观但冗余YOLO 的 txt 每行只存五个数类别编号、归一化中心点 x、中心点 y、归一化宽、归一化高。模型在训练时会把输入图缩放到任意尺寸只有归一化坐标才能在缩放后依然表示同一个框。这就是 yolo 训练自己的数据集时几乎都要先经过一次格式转换的原因。3.1 坐标转换的核心公式转换本身不复杂公式如下cx (xmin xmax) / 2 / width cy (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height注意这里的 width 和 height 必须来自 XML 的 size 字段而不是转换时用 cv2 重新读图获得。原因前面提过XML 的 size 可能与图像实际尺寸不一致如果换用 cv2 读图转出来的坐标和 VOC 标注就互相矛盾排错时很难定位到是转换问题还是标注问题。以 XML 的 size 为准这个约定要贯彻到底。3.2 转换脚本实现目录结构、类别映射和精度控制下面这个脚本把 Annotations 下所有 XML 转成 labels 目录下的同名 txtimport xml.etree.ElementTree as ET from pathlib import Path class_names [mouse] # 与后续 data.yaml 的 names 顺序完全一致 def voc_to_yolo(xml_path, out_dir, class_names): root ET.parse(str(xml_path)).getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(f未知类别跳过: {name}) 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) x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_dir.mkdir(parentsTrue, exist_okTrue) (out_dir / f{xml_path.stem}.txt).write_text(\n.join(lines)) for xml in Path(Annotations).glob(*.xml): voc_to_yolo(xml, Path(labels), class_names)参数说明class_names 列表的索引顺序就是 YOLO 的类别编号之后 data.yaml 里的 names 必须与它严格一致顺序错一个模型就把两个类学反了。x_center 和 y_center 的保留精度我习惯用 6 位小数对 1280 以内的图6 位小数已经远小于一个像素再多的位数没有意义反而让文件变大。脚本逐行写入 txt不写多余的空行或注释YOLO 解析器对空行容忍度不同干净输出是最稳的。3.3 转换后的可视化校验一图发现坐标偏移转换完后先别急着训练做一次可视化把 YOLO 标签画回原图看位置有没有偏。这一步是血泪经验换来的曾经有一次漏改 XML 的 size转换后所有框都朝右上角偏了一个身位训练了两轮才发现白白浪费半天。import cv2 img cv2.imread(images/frame_000000.jpg) h, w img.shape[:2] with open(labels/frame_000000.txt) as f: for line in f: cls_id, 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(check.jpg, img)逻辑说明反变换把归一化坐标还原成像素坐标再画矩形框。如果 check.jpg 里的绿框与标注时完全重合说明转换链路没有断裂如果框整体偏移或拉宽优先怀疑 XML 的 size 和图片实际尺寸不一致回到 2.2 的修复脚本重新同步。对 587 张图我一般抽 10 张左右做校验覆盖不同光照和不同鼠姿态确认无误再进入训练。可以做一个简单的检查对照表检查项正常表现异常表现框位置与鼠身体贴合整体偏移半个身位框宽高比与毛色区域接近被压扁或拉长框数量与 XML 一致多框、少框4. 用 YOLOv8 训练鼠检测模型小数据集的配置和超参数调整在讲配置前先把道理说透587 张图不可能从零训练一个深度检测网络必须加载预训练权重做迁移学习。YOLOv8 从随机初始化开始训练需要几十万张图片支撑但加载 COCO 预训练过的 yolov8n.pt模型早期特征边缘、纹理、色彩块已经具备鼠的毛发纹理和环境光与自然图像有大量共享信息只需要在高层特征上做微调。这也是“yolov8训练自己的数据集”能在一个小时左右跑通的关键。提示YOLOv8 不会帮你把 VOC 的 XML 转成 labels必须先完成第 3 章的转换否则训练会在初始化阶段直接报错。4.1 data.yaml 的数据组织labels 目录结构必须和 images 对齐YOLOv8 对数据集目录有自己的约定和 VOC 的平铺结构不同。常见做法是mouse_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml 内容path: /data/mouse_dataset train: images/train val: images/val test: images/test names: 0: mouse注意YOLOv8 只按 data.yaml 的 train 路径去同级目录找 labels。如果 train 写的是 images/train框架就去找 labels/train目录层级错一级就会提示找不到标签。这一步是初学者最常见的翻车点。转换完成后最好先对比一次图片和标签数量find images/train -name *.jpg | wc -l find labels/train -name *.txt | wc -l两个数字必须一致。不一致时先看 labels 目录层级再看是否有图片缺少对应 txt。4.2 训练命令和关键超参数训练命令如下yolo train data/data/mouse_dataset/data.yaml \ modelyolov8n.pt \ imgsz640 epochs150 batch16 lr00.005 \ patience15 projectmouse_exp namemouse_v1关键参数说明表参数建议值说明imgsz640鼠这类小目标场景640 是性价比最高的输入尺寸epochs150数据量小epochs 大一点靠 patience 提前停止batch168G 显存可用显存小则降到 8lr00.005比默认的 0.01 低小样本高学习率很容易震荡patience15连续 15 个 epoch 验证损失不降就停batch 和显存的关系是入门者问得最多的“玄学”之一。实际上 batch 影响的是 BN 层的统计稳定性batch 太小在 587 张数据上会让 loss 曲线剧烈抖动。我在 8G 显存的卡上开 batch 16 很稳4G 卡就降 imgsz 到 512 或 batch 8不要硬顶分辨率。4.3 训练日志、损失函数和结果判断训练结束后终端输出 Precision、Recall、mAP50、mAP50-95。对我来说单类鼠检测的判定标准是指标阈值含义mAP50 0.85可直接用日常视频检测基本不漏检mAP50 0.7 - 0.85可调优边缘帧会漏检需增强或补标mAP50 0.6先查数据标签路径或标注质量有问题YOLOv8 的损失函数包含三块分类损失、边界框回归损失CIoU和 DFL 损失。如果你在训练日志里看到 box_loss 降得很慢大概率是标注框的宽高比差异太大。鼠卧着时是扁长条站起来是竖直条这种形状变化大的单类目标模型需要更多 epoch 去拟合不同姿态所以不要因为前 30 轮 loss 好看就过早手动停掉让 patience 机制自己决定。val 目录里的 results.png 和 confusion_matrix.png 值得认真看。confusion_matrix 里 background 列的值如果偏高说明模型把大量背景切块当成了鼠部署时就要提高置信度阈值来压假阳性。这是从训练日志判断数据质量最直接的方法几乎不需要额外代码。4.4 迭代过程补标签或调增强如果第一次训练 mAP50 在 0.85 以下我通常不调模型结构而是回到数据层面做两件事。第一用模型预测训练集把所有 IoU 0.3 的预测视为潜在漏标逐一检查原始图像鼠这一目标体积小标注时容易漏掉贴边个体。第二调整数据增强组合关闭默认的 mosaic1.0改为 0.3因为 mosaic 会把 4 张图拼接鼠在拼接图上变得更小对本来就不大的目标反而有害。这一条经验在小目标检测场景里反复验证过是这套流程里最值得调的一处。5. 鼠数据标注训练中的五个常见坑和排查方法这里的每一条我都是用实际项目踩出来的。下面按“现象—原因—解决”的结构写方便直接对照。5.1 现象转换后 bbox 坐标异常、框飞到图片外现象可视化校验时部分绿色框落在图片外或者框的宽高变成 0.0训练时还会报 invalid box。原因XML 里的 size 字段与实际图像尺寸不一致。常见于标注完成后用 Pillow 批量压缩过图片但 XML 未同步更新转换脚本以 XML 的 size 为准得到的就是错误归一化坐标。解决统一用一段脚本把每张图片的真实宽高回写到 XMLfrom PIL import Image import xml.etree.ElementTree as ET from pathlib import Path for xml in Path(Annotations).glob(*.xml): img_path Path(JPEGImages) / f{xml.stem}.jpg w, h Image.open(img_path).size root ET.parse(str(xml)).getroot() root.find(size/width).text str(w) root.find(size/height).text str(h) ET.ElementTree(root).write(str(xml), encodingutf-8, xml_declarationTrue)逻辑很简单读图取真实宽高覆盖写回 XML 的 size 节点。跑完后再执行一次 2.2 的脏数据检查脚本确认没有越界框才继续转换。5.2 现象训练时类别编号对不上或类别数量超出 names现象YOLOv8 训练到一半打出 “class 2 is not in names” 之类的警告甚至直接退出还有的标签 txt 里出现了 2而 data.yaml 只定义了 mouse 一个类。原因转换脚本里的 class_names 顺序和 data.yaml 的 names 顺序不一致。例如转换时用 [mouse, black_mouse]data.yaml 里却写了 [black_mouse, mouse]所有编号就会错位。解决统一以 class_names 为唯一真源在转换脚本和 data.yaml 里使用同一份文件作为类别表。转换完成后可以用一条命令扫描所有标签查看出现的最大类别编号cat labels/*.txt | awk {print $1} | sort -un如果输出的最大编号大于 names 长度减 1说明标签里有脏编号需要回到转换脚本查 class_names 映射表。这种错误在 587 张的规模下肉眼根本发现不了只能靠命令核查。5.3 现象验证指标高但新视频上检测效果差现象train 和 val 的 mAP50 都超过 0.9放到一段没见过的视频上鼠稍微换个姿势就漏检。原因划分数据集时用了随机切帧没有按视频分组。同一个视频相邻帧背景几乎相同模型学到的可能只是背景特征而不是鼠本身。这就是 2.3 节强调按视频 ID 分组切分的原因。解决回到 2.3 的划分脚本把随机切帧的逻辑改为按视频维度划分如果已经有模型权重可以用换组后的划分重新训练对比 mAP。不要试图把所有帧混在一起随机切那样只会重复这个问题。5.4 现象mosaic 开启后小目标检测效果反而更差现象默认开启的 mosaic 增强让鼠检测的召回率下降尤其是镜头远端的小个体更明显。原因mosaic 把 4 张图拼接成一张相当于每张图中的鼠被缩到原来的 1/2 甚至 1/4目标尺寸在训练中被人为变小。YOLOv8 对多尺度训练有自适应但小鼠数据本身的目标框就小mosaic 放大了这个问题。解决在 data.yaml 里自定义增强参数来调低或关闭 mosaic。示例augment: mosaic: 0.3 mixup: 0.0 scale: 0.5 fliplr: 0.5参数说明mosaic 从默认的 1.0 降为 0.3即只有 30% 的 batch 做拼接保留部分多样性同时避免小目标被过度缩小。mixup 对单类鼠数据帮助不大关闭后训练更稳定。5.5 现象训练一开始就报找不到标签现象YOLOv8 初始化时提示 “No labels found in .../labels/train”。原因labels 的目录层级与 images 不对齐。图片在 images/train 下标签却被转换脚本放在了 labels 根目录或者 labels/train 存在但 train 里没有 txt 文件。解决用以下命令对比数量ls images/train/*.jpg | wc -l ls labels/train/*.txt | wc -l两个数字必须一致。不一致时先看目录结构data.yaml 的 train 写 images/train则标签必须在 labels/train 中且文件名与图片文件名完全相同。逐个排查完再用 4.2 的训练命令重新跑。6. 增量优化数据增强组合、模型选型和置信度阈值调整587 张数据做好双格式标注只是起点。我的一般做法是在这个基础上做三轮迭代每轮只改一个变量方便定位是哪个改动带来了收益。第一轮用默认参数跑一个基线记录 mAP50 和推理延迟第二轮把增强从保守组换成激进组保守组只有 fliplr0.5 和轻微 HSV 扰动激进组加上 mosaic0.3、scale0.5对比两组指标决定是否保留增强第三轮在模型选型上做实验同一批数据分别用 yolov8n 和 yolov8s 各训一次。对鼠这种单类场景n 和 s 的 mAP 差距通常不到 0.03但 n 的推理速度快一倍所以大部分情况下我会优先选 n。如果图像里鼠目标很小可以考虑 imgsz1280 重训一次看 mAP50 是否明显上涨没有上涨就不值这个训练成本。置信度阈值也是一个容易被忽略的环节。默认 conf0.25 在鼠笼场景下容易出现大量假阳性饮水嘴、笼子缝隙都会触发检测。部署时我习惯把 conf 调到 0.4 左右具体数值看 val/PR_curve.png 里 Precision 和 Recall 的交点附近。最后说一个我一直坚持的标签自查技巧用训练好的模型回测训练集找出预测框与 ground truth 的 IoU 小于 0.3 的图逐张检查。其中绝大多数是漏标——鼠贴在笼子边缘人眼当时没看到。把这些漏标框补上再训练一轮mAP 的涨幅往往比调增强参数大得多。对小数据集补标不是玄学是收益率最高的投入。这套流程我从 587 张的鼠数据一路做到后续扩充到上千张过程里最深刻的教训是格式一致性和目录组织远比调参重要问题发生时先怀疑标注和路径再怀疑模型。希望帮到你。本文还有配套的精品资源点击获取