简介一套面向目标检测任务的高质量数据集包含1073张真实场景图片涵盖婴儿车、行人、自行车、行李箱、轮椅等5个类别共1606个矩形框标注适合用于商场、机场、医院、车站等公共场所的婴儿车检测与行人辅助识别研究。压缩包中共2000个文件以jpg图像、xml标注文件与txt标签文件为主整体大小约58.43MB同时提供VOC与YOLO两种主流格式可直接接入YOLOv5、Faster R-CNN、SSD等常见检测框架省去格式转换环节。所有标注由LabelImg工具人工绘制框选准确、类别规范数据集只包含可靠标注其中婴儿车类别框数最多1169框适合作为专项检测或迁移学习的关键数据也可联合其他公开数据扩充训练集。目前已有263人学习/下载附有使用前必读文档便于快速了解目录结构与加载方式上手门槛较低可有效降低自行标注的时间成本。1. 婴儿车检测数据集VOCYOLO先搞清楚你拿到的到底是什么做母婴智能硬件的同行大概都经历过跑遍全网找“婴儿车”数据集的尴尬通用数据集里婴儿车要么被归到“椅子”要么干脆没有想自己标1073张图从采集到框完够忙大半个月。所以当“婴儿车检测数据集VOCYOLO格式1073张5类别.7z”这个压缩包出现时第一反应都是先解压看看里面到底有什么。这个数据集的卖点很直白——图片数量、类别数、标注格式全写在标题里VOC和YOLO两套标注同时给到意味着你既可以用LabelImg打开看框也可以直接喂给YOLO做训练。它不追求大样本追求的是开箱即用。我下面不评价数据质量只说怎么把它用好目录怎么组织、训练参数怎么配、哪些坑值得提前知道、以及后续要扩容时怎么保持两种标注同步。2. 数据集格式拆解VOC和YOLO标注如何互相转换、目录怎么用2.1 VOC和YOLO格式到底差在哪目录结构与标注对比标题里既然写了“VOCYOLO格式”那解压后你一定会看到两种标注。常见的组织方式有两种一种是分两套目录比如VOC2007/和yolo/各自放一套另一种是同一个images目录Annotations/放XMLlabels/放TXT。先判断你拿到的是哪种别一上来就搜路径。如果你在Linux下解压记住用7z x保留目录结构而不是7z e后者会把所有文件压平到一个目录里XML和TXT的同名关系虽然还在但images与标签的层级全乱了后面写data.yaml会很痛苦。VOC格式是Pascal VOC标准每张图片对应一个XML根节点里有size存图片宽高object列表里每个对象带name和bndboxbndbox里是xmin、ymin、xmax、ymax四个绝对像素坐标。YOLO格式是后来为深度学习训练设计的每张图片对应一个TXT每一行是class_id x_center y_center width height全部归一化到0到1之间。注意YOLO的坐标是中心点加宽高不是左上角加右下角这一点很多人第一次转格式时容易把x_center写成xmin/w。为什么同时给两份因为工具链不同。LabelImg之类的标注工具原生保存为VOC XML而Ultralytics的YOLO训练流程只认TXT。数据集作者把两套都放进来就是让你“看框用XML训练用TXT”不用自己写转换脚本。但代价是两份标注容易不一致比如你之后用LabelImg补标了一张图只存了XMLTXT还是旧的模型就会学到错误标签。所以拿到数据的第一步不是开训练而是先校验两套标注是否对齐。2.2 用脚本检查两份标注是否对齐并完成数据划分我一般会写一个小脚本遍历所有图片同名文件把XML里的绝对坐标转成归一化中心坐标再和TXT逐行比对。先放核心转换函数def voc2yolo(w, h, xmin, ymin, xmax, ymax): x_center ((xmin xmax) / 2) / w y_center ((ymin ymax) / 2) / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h return x_center, y_center, box_w, box_h注意这里w和h是从XML的size节点读出来的整数如果图片被缩放过但XML里的size没更新转出来的坐标就会偏移后面训练时YOLO内部再按实际图片尺寸还原框就会错位。所以先用实际图片尺寸替换size节点再转换。然后是对比脚本import xml.etree.ElementTree as ET def load_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) objects [] for obj in root.findall(object): name obj.find(name).text bnd obj.find(bndbox) xmin int(float(bnd.find(xmin).text)) ymin int(float(bnd.find(ymin).text)) xmax int(float(bnd.find(xmax).text)) ymax int(float(bnd.find(ymax).text)) objects.append((name, xmin, ymin, xmax, ymax)) return w, h, objects def load_yolo(txt_path): boxes [] with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls, xc, yc, bw, bh int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) boxes.append((cls, xc, yc, bw, bh)) return boxes由于VOC里类别名是字符串而YOLO里是整数对比前必须先建立一个类别名到ID的映射比如{stroller: 0, person: 1, ...}。这里的具体类别名以你解压后的XML里的name为准我习惯先跑一个统计脚本把所有name枚举出来再去定义映射。接着比较def compare(xml_path, txt_path, thresh0.01): w, h, voc_boxes load_voc(xml_path) yolo_boxes load_yolo(txt_path) if len(voc_boxes) ! len(yolo_boxes): return False, fbox数量不一致: VOC{len(voc_boxes)} YOLO{len(yolo_boxes)} for (name, xmin, ymin, xmax, ymax), (cls, xc, yc, bw, bh) in zip(voc_boxes, yolo_boxes): v_xc, v_yc, v_bw, v_bh voc2yolo(w, h, xmin, ymin, xmax, ymax) if abs(v_xc - xc) thresh or abs(v_yc - yc) thresh: return False, f中心点偏移过大: {name} {v_xc},{v_yc} vs {xc},{yc} if abs(v_bw - bw) thresh or abs(v_bh - bh) thresh: return False, f宽高偏移过大: {name} {v_bw},{v_bh} vs {bw},{bh} return True, OKthresh我一般取0.01也就是归一化后最多1%的像素误差。如果误报太多先看是不是XML里size和实际图片尺寸不符。跑完对比后再处理一下数据划分。很多YOLO仓库支持按目录划分所以我通常会建四个目录images/train、images/val、labels/train、labels/val把图片和标签按9:1或8:2随机分进去。如果数据集已经提供了train.txt和val.txt那就直接照着划分如果没有可以用下面这段import os import random random.seed(42) img_dir images # 改成你的实际目录 train_ratio 0.9 imgs [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) split_idx int(len(imgs) * train_ratio) train_imgs imgs[:split_idx] val_imgs imgs[split_idx:] with open(train.txt, w) as f: for img in train_imgs: f.write(os.path.join(os.path.abspath(img_dir), img) \n) with open(val.txt, w) as f: for img in val_imgs: f.write(os.path.join(os.path.abspath(img_dir), img) \n)注意这里的train.txt/val.txt只是图片列表YOLOv5会默认把图片路径中images替换成labels来找TXT。如果你不按这个约定就得在data.yaml里显式设置train_labels之类反而麻烦。我建议直接按YOLO约定目录组织后面训练配置省心。3. 用YOLOv5训练婴儿车检测模型从解压到跑通的最小命令3.1 一分钟搭好YOLOv5环境环境搭建其实不复杂但有不少人卡在torch和CUDA版本上。我一般用conda创建干净环境再单独装PyTorch不直接用requirements里带的那一版因为它的torch依赖经常跟已有环境冲突。步骤如下conda create -n yolo python3.9 -y conda activate yolo git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt如果网络较慢pip可以加国内镜像源比如-i https://pypi.tuna.tsinghua.edu.cn/simple。克隆完成后先验证GPU可用python -c import torch; print(torch.cuda.is_available())输出True再继续否则后面训练会直接掉回CPU速度慢到怀疑人生。第一次训练时YOLOv5会自动下载COCO预训练模型yolov5s.pt也就是热搜里经常提到的“yolo预训练模型下载”。如果下载失败就手动把pt文件放到yolov5目录下或者干脆用--weights 从头训练。不过只有1073张图从头训练效果大概率不理想还是建议把预训练权重先搞定。3.2 把数据集写成YOLO训练配置data.yaml与目录映射按照上一章整理好的目录结构data.yaml可以这样写# data.yaml path: /home/user/babycar_dataset # 改成数据集根目录绝对路径 train: images/train val: images/val nc: 5 names: [class0, class1, class2, class3, class4]关键是names列表的顺序必须和labels/txt里的class_id一一对应。很多人在这里翻车看到VOC XML里的类别名是“stroller”“baby”就直接按字母序写names但YOLO标注生成时可能用的是另外一套顺序。正确做法是先解压数据集然后随机打开一个labels目录下的txt文件看第一列的整数范围是不是0到4再打开对应的XML看name文本手动建立一个映射表。你也可以用脚本提取XML里的所有类别名按出现的顺序写进names但注意训练脚本内部不会帮你重排TXT里的class_id所以names顺序必须完全对齐。如果你拿到的数据是VOC和YOLO两套标注分开存放建议训练前只依赖其中一套我一般直接从labels目录读取TXT不再管XML。这样至少保证训练时用的标签和验证时用的一致。不要一会儿用XML转的TXT一会儿用原始TXT很容易乱。3.3 训练命令与关键参数调整训练命令很简单核心就一行python train.py --data data/babycar.yaml --weights yolov5s.pt --batch-size 16 --epochs 100 --img 640跑起来后你会看到每个epoch打印三行损失分别对应box loss、cls loss、obj loss。这就是“yolo损失函数”的日常监控入口。box loss用CIoU衡量框的位置和大小误差cls loss用BCEWithLogits衡量类别错分obj loss衡量是否存在目标。三个值加起来就是总loss。观察它们的下降趋势比只看总loss更重要如果box loss降但obj loss不动说明很多框虽然能框住物体但模型对“这里有没有目标”没信心常见于背景干扰大、标注框有遗漏的情况。参数怎么调我列了几个最值得动的参数作用建议--batch-size单次喂入GPU的图片数8G显存用16显存不足用8或4但过小容易bn崩溃--img输入分辨率640起步小目标多可试896但显存和时间代价翻倍--epochs训练轮数100起步看val loss平台期再决定是否延长或早停--lr0初始学习率默认0.01小数据集降到0.005或0.001更稳--patience早停耐心轮数默认100如果显存不够想早停可以设50--cache是否缓存数据到内存/硬盘数据只有一千多张建议--cache ram提速举个例子如果你的GPU只有6G显存batch-size设16可能直接OOM我一般降到8并把img降到512。小数据集不建议把batch设太低因为BN在batch太小时统计不稳定容易复现“yolo训练中bn崩溃”——后面避坑章节会专门讲。另外要留意训练过程中YOLOv5会生成runs/train/exp目录里面存了每个epoch的权重和训练曲线。训练结束后取best.pt或last.pt即可不用纠结哪个best.pt是验证集mAP最高的轮次一般用它做后续验证和部署。4. 婴儿车检测训练避坑清单5个常见问题与排查4.1 标注和压缩包层面的三个坑7z解压、XML与TXT失同步第一个坑出现在解压阶段。很多人拿到.7z文件直接用Windows右键解压结果打开后发现Annotations和labels目录都不见了只有一堆图片散落在某个深层目录。这通常是因为压缩包里顶层目录嵌套太深解压工具没有保留完整路径或者文件名里包含特殊字符。我的习惯是在Linux上用命令解压7z x会保留目录结构7z e则会压平目录。如果你必须用图形界面也建议安装7-Zip而不是用系统自带的带压缩文件夹功能。坑的实质是解压后的目录层级变了而不是文件损坏。所以现象是“解压后XML和TXT数量对不上”原因大概率是某个子目录没有被解压出来。解决方法是解压后先统计一下find . -name *.xml | wc -l find . -name *.txt -path */labels/* | wc -l两个数字应该一致如果差了就要检查是不是有XML文件没有对应TXT还是相反。跑一遍前面的compare脚本也能定位到具体文件。第二个坑是VOC标注和YOLO标注存在偏移。常见现象是训练后验证集mAP不低但打印出来的预测框总比ground truth偏大或偏小一圈。原因是有人在转换标注时把width / w写成width / h或者把中心坐标和左上角坐标搞混了。我见过最离谱的一次是有人把xmin和ymin当成归一化坐标直接写进TXT导致所有框都挤在图片左上角。解决方法是不要信任转换工具拿起两个文件手动核对一个框用LabelImg打开图看XML里的坐标再通过换算验证TXT里的值至少抽10张图检查。第三个坑是类别id对不上。现象是训练启动正常但到了验证时某些类别AP恒为0。原因往往是XML里的类别名和TXT里的class_id映射错了比如stroller在XML里出现但TXT中它的id写成了5而nc5意味着id只能0到4。解决方法是写一个统计脚本读取所有TXT中的class_id最大值再读取所有XML中的name集合两边交叉验证。4.2 训练和评估层面的两个坑bn崩溃、混淆矩阵不明白第四个坑是“yolo训练中bn崩溃”。现象很直接训练到某一步loss突然变成nan显卡显存占用也掉下来训练中断。常见原因是BatchNorm在小batch下统计量不稳定加上初始学习率太高梯度爆炸另外如果数据里有全黑的图片或全白的图片底层卷积输出方差过大也可能诱发。解决方法是分三步走先把lr0降到0.001再把batch-size提高到16或32如果还崩就修改模型的BN momentum默认0.03改成0.01。改完之后从头重新训不要指望从崩溃点续训因为BN的running stats已经污染了。第五个坑是关于“yolo混淆矩阵总合不唯一”的。评估时你会看到confusion_matrix.png里面每一行的数字加起来可能不是100%有些人以为代码bug。其实混淆矩阵的归一化是按行真实框数量来的每一行代表一个真实类别的分布被分对的比例、被分到其他类别的比例、被漏检成背景的比例。如果某一行总和远小于100%说明该真实类别有很多框没有被模型任何预测框匹配到也就是漏检严重。这比看总mAP更能暴露问题。我一般会先看每个类的对角线值如果某个类别明显低于其他类别就先给它补数据而不是盲目调超参。5. 验证与调优用混淆矩阵和mAP判断模型是否真的可用5.1 用val.py评估模型mAP与损失曲线的读法训练完不是直接拿去用先用验证集跑一遍正式评估。YOLOv5的val.py可以单独调用python val.py --weights runs/train/exp/weights/best.pt --data data/babycar.yaml --task val输出里最关键的三个数字是mAP0.5、mAP0.5:0.95和每个类别的AP。mAP0.5是IoU阈值为0.5时的平均AP比较宽容mAP0.5:0.95则把阈值从0.5到0.95每0.05算一次再平均对框的精确位置更敏感。如果你训练模型是为了部署到移动端做实时检测通常只需要mAP0.5达到0.85以上但如果用于计数或轨迹跟踪框的位置必须稳我更看重mAP0.5:0.95。当两个指标差距明显时说明你的框“能框住但不精准”下一步应该调bbox回归损失权重或提高输入分辨率。除了AP我还会看results.png里的四条曲线train_box_loss、train_obj_loss、val_box_loss、val_obj_loss。如果val loss在训练中期掉头上升而train loss还在降这就是过拟合信号。对于只有1073张图的数据集过拟合非常常见。解决方式是增加Mosaic增强、扩大--cache数据池或者在data.yaml里加mixup: 0.5。但注意增强太强也会让box坐标更难回归要盯着val loss曲线做小步实验。5.2 婴儿车检测的调优方向小目标与遮挡婴儿车检测具体难在哪两种典型场景一是摄像头架得高婴儿车在画面里只有几十个像素属于小目标二是婴儿车常被大人身体或购物袋遮挡属于密集遮挡类。针对小目标最直接的方法是提高输入尺寸。把--img 640改成--img 896mAP0.5:0.95通常能涨两三个点但训练速度和显存都要翻倍。如果算力瓶颈明显更推荐在YOLOv5里开启P2检测层也就是在模型neck的浅层加一个输出头对小目标更友好。具体做法是在models/yolov5s.yaml里修改from和anchors这个操作比较绕新手很容易把yaml改坏我一般是直接换用更现代的YOLOv8n-P2配置。针对遮挡可以用更激进的数据增强。YOLOv5默认开启了Mosaic它把四张图拼在一起天然产生很多截断框有助于学习遮挡目标。但Mosaic在最后10个epoch如果还开着可能会让模型对真实尺度分布把握不稳。我一般会在训练命令里加--hyp指定一个自定义hyps文件把其中mosaic: 1.0保留但把copy_paste: 0.5打开这个属于YOLOv5较新的增强对小样本场景提升比较明显。注意不要同时把mixup和copy_paste都调得很高否则标注框会被增强得太碎反而拉低定位精度。另外不管之后是不是用TensorRT部署调优阶段都要把“最终推理帧率”记在心里。婴儿车检测多半跑在边缘设备上实时性压过一切。一个输入尺寸从640升到896的模型在Jetson这类平台上帧率可能直接掉一半。所以我习惯先定部署平台再返回来选模型尺寸和精度目标而不是先把模型训得又大又准最后发现部署不了。6. 补标扩容时保持VOC与YOLO同步一个脚本搞定模型在真实场景里漏检了几台特定颜色的婴儿车这时候最自然的做法是补标几十张图。问题在于LabelImg保存的是VOC XML而你的YOLO训练流程认的是TXT。若只把新图片塞进images/train却忘了把对应的XML转成TXT放进labels/train训练时会静默跳过没有标签的图片模型学不到新知识更隐蔽的是如果你以前用XML转TXT的脚本手动转过一次这次新标注的XML没有转训练数据的标注就是新旧两套格式混着用模型会越训越乱。我吃过这个亏有一次只改了XML没同步TXT损失曲线一直降验证集recall却几乎不变排错半天才发现是标签错位了。从那以后我养成了习惯——新增或修改任何XML都跑一遍下面这个转换脚本把整个Annotations目录重新扫一次覆盖生成所有TXTimport os import glob import xml.etree.ElementTree as ET # 这个映射必须和训练时 data.yaml 的 names 顺序严格一致 names_map { stroller: 0, baby: 1, person: 2, umbrella: 3, pet: 4, } def convert_annotation(xml_path, out_txt_path, names_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in names_map: print(f跳过未知类别 {name}在 {xml_path}) continue cls_id names_map[name] bnd obj.find(bndbox) xmin int(float(bnd.find(xmin).text)) ymin int(float(bnd.find(ymin).text)) xmax int(float(bnd.find(xmax).text)) ymax int(float(bnd.find(ymax).text)) if xmax xmin or ymax ymin: print(f框异常{xml_path} {name} {xmin},{ymin},{xmax},{ymax}) continue xc ((xmin xmax) / 2) / w yc ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) # 批量转换假设XML在Annotations/输出到labels/ xml_files glob.glob(Annotations/*.xml) for xml_file in xml_files: stem os.path.splitext(os.path.basename(xml_file))[0] out_txt os.path.join(labels, stem .txt) convert_annotation(xml_file, out_txt, names_map) print(f已生成 {out_txt})这个脚本的关键是每次都全量重写TXT而不是只处理新增文件。全量重写能避免旧标签残留——比如你用LabelImg删掉了一个框但旧TXT里那行还在训练时模型就会学到一个不存在的目标。脚本里还做了两件事未知类别直接跳过并打印警告这样你一眼就能发现XML里有没有新类别没加进names_map框坐标异常xmax xmin也直接跳过避免生成负数宽高的标签。跑完转换后我还会顺手把第2章的compare脚本再跑一遍确保XML和TXT完全一致。这个动作很简单但能省下后面几天排查标注问题的精力。补标一次、转换一次、校验一次这个流程重复几次之后你会觉得所谓“标注维护”也就是顺手的事。希望帮到你。本文还有配套的精品资源点击获取