简介:面向目标检测模型训练和计算机视觉入门实践的三轮车专用数据集,包含559张真实场景图片,统一标注为tricycle类别,累计659个矩形标注框。数据由labelImg手工标注,同时输出Pascal VOC格式XML与YOLO格式TXT,每张图片均对应两种标注文件,便于对比格式差异或直接切换训练框架。整个资源压缩包共1679个文件,以jpg原图、xml标注和txt标签为主,大小仅25.22MB,rar解压后即可使用,整体轻量紧凑。标注内容准确合理,适合三轮车目标识别、小样本检测算法验证、数据增强与类别不平衡分析等场景,也可作为单类别检测的入门练习、消融实验或对比实验基准数据,帮助学习者和开发者节省数据整理时间,提升模型迭代效率。目前已有646人学习下载,适合需要专项检测数据或快速验证模型效果的读者参考使用。
1. 拿到手工标注的三轮车559张数据集:先报三本账再谈训练
“三轮车数据集559张VOC+YOLO格式(手工labellmg标注)”里信息量最大的不是559这个数字,而是“手工”两个字。它是在告诉你,这批数据的每个框都是人眼一个一个拖出来的,类别边界干净、来源可追溯,和那种自动标注批量生成的标签完全两个性质。单看数量,559张离大型训练的零头都还差得远;但放到固定机位、单一车型检测这种窄业务场景,它已经够拿出一版能去现场验证的基线模型。下面这篇笔记我先拆这套数据集的构成和标注质量,再走一遍格式转换、训练参数配置,最后把手工小数据集训练里最常踩的几个坑按“现象→原因→解决”的顺序说透。
2. 解析559张三轮车数据集的构成:类别、场景与标注质量的检查边界
2.1 拿到数据集先报三本账:图像分布、目标数量、命名规律
第一步不是训练脚本,而是把数据集的三本账算清楚:图像尺寸分布、单张目标数量分布、文件名规律。这三本账能让你在没看任何一张图的情况下,先判断出这个数据集干不干净、适合怎么拆。
我习惯用一段Python把统计一次性跑完,不人工翻目录。
import glob import xml.etree.ElementTree as ET from collections import Counter xml_files = sorted(glob.glob("VOC/Annotations/*.xml")) img_size_counter = Counter() # 尺寸分布 obj_num_counter = Counter() # 单张目标数分布 for xml_path in xml_files: root = ET.parse(xml_path).getroot() obj_count = len(root.findall("object")) # 尺寸从XML读,不要单独去读图片,保证和标注时一致 w = int(root.findtext("size/width")) h = int(root.findtext("size/height")) img_size_counter[(w, h)] += 1 obj_num_counter[obj_count] += 1 print("尺寸分布:", img_size_counter.most_common(5)) print("目标数量分布:", sorted(obj_num_counter.items()))逻辑说明:obj_num_counter的键是单张图里标注目标个数。如果0占了不小比例,说明存在大量空标签;如果频繁出现8、9、10这类高值,说明多目标和遮挡场景偏多。这两种情况对应的训练策略完全不同——空标签要先排查,多目标则要把更多精力放在小目标和重叠框的处理上。
尺寸分布同样关键。手工用LabelImg标注时,<size>记录的是标注那一瞬间的图像尺寸。如果后来有人对图片统一做过压缩或裁剪,但XML没同步更新,这个统计就会暴露出“XML里写的尺寸实际对不上文件”的问题。这个隐患等到转换坐标时就是灾难,宁可提前发现。
文件名规律也要瞄一眼。IMG_20231205_153021.jpg这种带时间戳的命名,几乎可以断定是手机或相机连拍产物,前后帧内容高度相似。这种数据拆train/val时如果按文件名顺序切分,连续帧会被同时分进两边,验证集分数一路虚高。这个坑具体怎么规避,我留在避坑章展开。
2.2 手工标注的信任边界:从XML结构读出标注风格
标题里的labellmg指的就是LabelImg。它保存VOC格式时输出的XML结构是固定的,但字段内容和填写习惯能反映标注者风格。下面这份XML代表了三轮车数据集中最常见的单目标标注结果:
<annotation> <folder>VOC</folder> <filename>IMG_20231205_153021.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>tricycle</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>102</xmin> <ymin>215</ymin> <xmax>613</xmax> <ymax>680</ymax> </bndbox> </object> </annotation>读这份XML时我通常只关心三个地方。第一是<name>。手工标注最容易出现同一个类别被标成不同字符串的问题:有人全小写tricycle,有人写成three_wheeler,有人输入法没切明白保存了中文标名。这个动作会在YOLO转换时把1个类别静默拆成好几个ID,训练时类别数凭空变多,模型怎么训都混乱。排查命令只有一行:
grep -h "<name>" VOC/Annotations/*.xml | sort | uniq -c第二是<bndbox>。手工标注时如果先点右下角、后点左上角,XML里就会存出xmin > xmax的反框。这种框XML解析不报错,但换算到YOLO时会得到负宽度,训练进程大概率在数据检查阶段崩溃。转换脚本必须带坐标规整逻辑,把反框方向修正回来。
第三是pose、truncated、difficult这些附加字段。对三轮车这类目标,手工标的时这些字段通常保持默认值,不用管。但如果出现一批difficult=1的标注,说明当时的标注者判断过部分目标带遮挡,这类样本在训练时默认是会被忽略还是参与训练,不同框架的处理不一致,需要单独确认。
还有一个看不见但影响巨大的点:标注框的松紧风格。有人喜欢把框拖到贴死目标边缘,有人习惯留一圈余量。同一份数据集里如果混了两种风格,模型在边界框回归时就会左右摇摆,loss下不去。要验证这一点,可以抽几十个XML,计算bndbox面积与图像面积之比,看分布是否出现双峰。双峰基本可以断定是两个人标的,需要统一标准后重标一批或接受掉点的代价。
2.3 559张在目标检测里的容量定位:够起步,不够通用
559张三轮车数据集,在目标检测的容量尺上是明确位置:单类别、固定机位、相近视角,够用;多城市多天气多摄像头型号的通用检测,远远不够。
支撑这个判断的是数据形态。手工标注决定了标签质量是可靠的,但559这个绝对数量决定了场景方差的覆盖上限。通用三轮车检测需要模型学会的不仅是“三轮车长什么样”,还有不同光照、背影、遮挡、镜头畸变下的表现。559张不可能覆盖这些条件组合。它在这个需求下的价值只剩两条:一是把从数据到训练的完整链路跑通,二是作为后续扩展数据的种子模型。决策路径很明确:先跑通验证工程流程,再用现场数据做迭代。
这个时候“VOC+YOLO格式”就有了第二层价值。它不光是省一次格式转换,更重要的是同时接入了两个生态:VOC格式适合对接传统检测框架和数据校验工具,YOLO格式直接进现代YOLO训练管线。手里同时握着两套格式,做格式对比、不同框架复现实验时都不用重新生成数据。
3. 在VOC与YOLO之间切换:坐标换算、批量转换与转换后的三道验证
3.1 两种格式的本质差异:像素坐标与归一化坐标
VOC格式用bndbox里的两个对角点描述框,单位是像素;YOLO格式用中心点和宽高描述框,单位是归一化比例。两者数学上完全等价,只是视角不同。
像素坐标 → 归一化坐标 x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height box_width = (xmax - xmin) / image_width box_height = (ymax - ymin) / image_height 归一化坐标 → 像素坐标 xmin = (x_center - box_width / 2) * image_width ymin = (y_center - box_height / 2) * image_height xmax = (x_center + box_width / 2) * image_width ymax = (y_center + box_height / 2) * image_height这个换算里有三个固定坑位。第一,/必须是浮点除法,任意一处用了整数除法,小目标的中心点精度直接丢失。第二,image_width和image_height必须来自XML里的<size>,不能事后读图猜,否则一旦图像做过缩放就全军覆没。第三,YOLO行格式是class_id x_center y_center width height,width是归一化框宽,不是x方向的偏移量,有人误写成xmax - x_center后模型输出框集体漂移,训练还不会报错。
3.2 VOC转YOLO批量脚本:一次转换带上坐标规整与越界警告
VOC转YOLO是纯算术,真正要防的是数据源头的问题。下面这份脚本包含三件事:批量解析XML、做同义词类别映射、坐标规整和越界警告。
import xml.etree.ElementTree as ET from pathlib import Path # 类别映射表:把可能出现的同义类别名全部归到同一个ID class_map = { "tricycle": 0, "three_wheeler": 0, # 同义词,合并为0 "三轮车": 0, # 若存在中文标签,也合并进0 } def voc2yolo(xml_path, out_txt_path, class_map): root = ET.parse(xml_path).getroot() # 图像尺寸必须取XML里的size字段 img_w = int(root.findtext("size/width")) img_h = int(root.findtext("size/height")) lines = [] for obj in root.findall("object"): name = obj.findtext("name").strip() if name not in class_map: # 打印而不是抛异常,方便一次性收集全部异常标签 print(f"[跳过] 未知类别 {name} in {xml_path}") continue xmin = float(obj.findtext("bndbox/xmin")) ymin = float(obj.findtext("bndbox/ymin")) xmax = float(obj.findtext("bndbox/xmax")) ymax = float(obj.findtext("bndbox/ymax")) # 坐标规整:修复手工标注的反框 xmin, xmax = min(xmin, xmax), max(xmin, xmax) ymin, ymax = min(ymin, ymax), max(ymin, ymax) cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h # 中心点越界警告,坐标有问题在这里暴露 if not (0.0 < cx < 1.0 and 0.0 < cy < 1.0): print(f"[警告] {xml_path} {name} 中心点越界 cx={cx:.3f} cy={cy:.3f}") lines.append(f"{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") # 写出YOLO格式TXT,记住YOLO行序是class_id, x_center, y_center, width, height with open(out_txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) xml_dir = Path("VOC/Annotations") txt_dir = Path("YOLO/labels") txt_dir.mkdir(parents=True, exist_ok=True) for xml_file in sorted(xml_dir.glob("*.xml")): out_txt = txt_dir / (xml_file.stem + ".txt") voc2yolo(str(xml_file), str(out_txt), class_map)参数说明和逻辑要点:
class_map决定了最终TXT里的ID分配,和XML里谁先出现没有关系。没有这张表,tricycle和three_wheeler会被当成两个类,训练时类别数直接变成2,模型学出两个互斥的“三轮车”,预测结果反复横跳。- 坐标规整必须在归一化之前做。顺序弄反了,
min/max作用在归一化值上,修出来的结果也是错的。 - 越界警告出现后脚本继续执行,不会中断整个批量任务。小样本数据集里出现一两条越界记录,记录下来统一排查比中断整个流程更高效。
- 输出精度6位小数足够。一张1920宽的图上,6位小数对应的像素误差不到0.002像素,YOLO训练完全够用,文件体积还小。
3.3 转换后的三道验证:文件名对齐、画框还原、空标签检查
转换完成不要急着开训练,十分钟做三道验证。
第一道,文件名对齐。批量转换循环遍历的是XML目录,理论上TXT必然存在,但图片目录可能被动过,需要确认图像与TXT一一对应:
for xml in VOC/Annotations/*.xml; do base=$(basename "$xml" .xml) [ -f "YOLO/labels/$base.txt" ] || echo "缺TXT: $base" [ -f "VOC/Images/$base.jpg" ] || echo "缺JPG: $base" done第二道,画框还原抽查。随机抽10张图,把TXT归一化坐标还原成像素框叠在原图上,人眼确认框的位置是否贴合目标。这是发现“坐标全部错了一格”这类系统性问题的唯一直接手段:
from PIL import Image, ImageDraw img = Image.open("sample.jpg").convert("RGB") w, h = img.size draw = ImageDraw.Draw(img) with open("sample.txt") as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue _, cx, cy, bw, bh = map(float, parts) # 中心点加减半宽,得到左上角和右下角 x0 = (cx - bw / 2) * w y0 = (cy - bh / 2) * h x1 = (cx + bw / 2) * w y1 = (cy + bh / 2) * h draw.rectangle([x0, y0, x1, y1], outline="red", width=4) img.save("preview.jpg")这个还原公式的配对关系极易写错。x0 = (cx - bw/2) * w必须和x1 = (cx + bw/2) * w成对,如果随手写成x0 = (cx - bw) * w,框直接变成两倍宽且整体偏移,肉眼一眼就能看出来,但训练时模型不会对这个报错。
第三道,空标签检查。XML里没有<object>时,转换脚本会生成0字节TXT:
find YOLO/labels -name "*.txt" -size 0 -print找到后逐张确认:是负样本背景图,还是漏标。漏标的补标,负样本则留着单独管理,后续作为背景输入参与训练。
三道验证跑完,格式转换才算真正收尾。
4. 用559张三轮车数据集微调YOLO:目录划分、参数配置与评估流程
4.1 8:2划分:目录结构、随机种子与data.yaml
559张划分成train/val,我用8:2。训练集447张,验证集112张。验证集在这个阶段只是用来观察收敛和做早停,不是最终测试集;真想评估上线效果,后面还要另留一份现场切片。
目录按YOLO系列的标准约定搭建:
dataset/ ├── images/ │ ├── train/ # 447张 │ └── val/ # 112张 ├── labels/ │ ├── train/ │ └── val/ └── data.yaml划分用脚本做,固定随机种子,避免手工选图带进主观偏差:
python3 - <<'EOF' import random, shutil from pathlib import Path random.seed(42) src_images = Path("VOC/Images") src_labels = Path("YOLO/labels") out = Path("dataset") for p in [out/"images/train", out/"images/val", out/"labels/train", out/"labels/val"]: p.mkdir(parents=True, exist_ok=True) files = sorted(src_images.glob("*.jpg")) random.shuffle(files) val_count = int(len(files) * 0.2) val_set = set(files[:val_count]) for img_path in files: lbl_path = src_labels / (img_path.stem + ".txt") if not lbl_path.exists(): print(f"[跳过] 无标签: {img_path.name}") continue if img_path in val_set: shutil.copy(img_path, out/"images/val") shutil.copy(lbl_path, out/"labels/val") else: shutil.copy(img_path, out/"images/train") shutil.copy(lbl_path, out/"labels/train") EOF划分说明:
random.seed(42)保证任何一次重跑划分结果一致。小数据集训练调参时,如果每次划分都不一样,指标之间的对比就失去意义。- 缺失标签自动跳过,这行在从VOC导入时能挡住一批潜在故障。
- 0.2的验证比例对559张来说验证集有112张,指标波动还在可接受范围。如果某个类别的验证样本少于50个,我会把比例上调到0.25,宁可少训一点,也要保证验证集覆盖住所有场景类型。
接着写data.yaml:
# dataset/data.yaml path: dataset train: images/train val: images/val nc: 1 names: ["tricycle"]注意:path字段加上之后,train和val用相对路径即可,移动整个目录只改一处。nc必须和names列表长度一致,不一致时训练启动直接报断言错误,属于必现故障。
4.2 训练参数怎么设:轻量模型、预训练权重与早停
559张的体量,默认上轻量模型,我用yolov8n,显存充裕时试一档yolov8s。大模型在小数据上只会更早过拟合,不会有任何正向收益。
yolo detect train \ model=yolov8n.pt \ data=dataset/data.yaml \ epochs=120 \ patience=30 \ imgsz=640 \ batch=16 \ lr0=0.01 \ workers=4 \ device=0 \ project=runs/tricycle \ name=base参数说明:
model=yolov8n.pt加载预训练权重,而不是随机初始化。559张从零训练几乎不可能收敛,预训练权重提供的是通用视觉特征,这是小数据微调能成立的关键前提。epochs=120配合patience=30,连续30轮验证指标不改善就早停。这个组合在559张上通常60到90轮就停了,比固定epoch跑完省掉大量无效训练时间。imgsz=640是默认值。三轮车如果在画面里占比很小,我会加跑一档imgsz=896做对比,显存和时间成本翻倍,但小目标AP可能提升,值得花半小时验证。batch=16在显存不够时必须降到8;低于4的话BN统计不稳定,不建议再低。显存宽裕时提到32意义不大,559张喂不满那么大的批。lr0=0.01是微调场景的稳妥起点。如果训练曲线显示loss很早就进入平台期,再试lr0=0.02对比。
训练过程的监控不要盯着一张张loss曲线的毛刺做决策。验证集112张带来的曲线抖动是正常的,重点是看它有没有整体趋势。runs/tricycle/base目录下的results.png里,重点看val/box_loss和mAP50两条曲线的走向,其他指标在小数据集上参考价值有限。
4.3 训练后看什么:best.pt、mAP曲线与现场切片检查
训练结束后,weights/best.pt是验证集上指标最好的权重,后续预测用这个文件。但559张训练出的模型绝不会止步于验证集mAP,还要加一道现场切片检查。
yolo detect predict \ model=runs/tricycle/base/weights/best.pt \ source=field_slices/ \ imgsz=640 \ conf=0.35 \ save=True \ save_txt=Trueconf=0.35这个阈值不是固定值。业务上宁可多报不能漏检,降到0.2;宁可漏检不接受误报,提到0.5。具体定多少,要从现场切片里数出的精确率和召回率来定。
保存预测结果时把save_txt=True开起来,输出的TXT可以直接统计检测数量,量化误报,而不是只靠肉眼看图。这一步是559张训练的最大价值:模型未必能直接上线,但你能拿着这份切片数据判断它离上线还有多远,以及下一步该补哪种数据。
5. 避坑指南:三轮车小数据集从标注到训练的5个高频翻车点
5.1 类别名不统一:一个类别被拆成多个ID
现象:训练启动后日志里类别数变成2甚至3,验证时三轮车一会儿被识别成类别A,一会儿被识别成类别B,预测结果完全没有稳定性。
原因:手工LabelImg标注时,同一个目标在不同时间被标成了不同字符串。tricycle、three_wheeler、中文“三轮车”并存,转换脚本没有做同义词映射,YOLO就把它们当成了不同类别。
解决:转换前的class_map统一映射,转换后用grep -h "<name>" 目录/*.xml | sort | uniq -c复查一遍,确认最终只有一个类别名。这个检查必须放在转换之前,等TXT生成后再发现就晚了。
5.2 空标签文件:训练崩在数据读取阶段
现象:训练进行到一半,日志报某个TXT文件无法解析,或者直接提示All labels empty,进程中断。
原因:某个XML里没有任何<object>块,转换脚本生成了0字节TXT。YOLO的数据加载器不接收空标签文件,遇到就报错。
解决:转换脚本里对空XML写明确日志,转换完成后用find labels -name "*.txt" -size 0 -print扫描一遍。找到后逐张判定是漏标还是背景负样本,漏标补标,背景样本单独管理,不要直接混进训练集。
5.3 EXIF旋转:图像正了,标注框没跟着正
现象:训练时mAP涨到一半就平了,把所有预测框画回原图,发现框集体转了个角度,大概90度的倍数。
原因:手机或相机拍摄的JPG普遍带EXIF方向信息。标注工具按EXIF显示图像时是正的,训练脚本读原始像素时却是横的,两边的坐标系不一致,框就转了。
解决:转换前批量剔除EXIF旋转信息,把图像统一转为RGB并重置方向。PIL里检查Image.getexif()的274字段,非1的全部重新保存一遍。这个预处理做完,再跑转换脚本,问题直接消失。
5.4 mosaic在训练后期拉低小目标精度
现象:前80个epoch的mAP曲线一路上扬,过了某个点开始震荡回落,小目标AP尤其明显,现场表现比验证集差不少。
原因:mosaic是YOLO系列默认开的增强,把四张图缩小拼接成一张,三轮车在这种图上变得更小更密集。模型训练后期为了适配增强后的分布,对小目标的边界预测变得保守,出现精度回退。
解决:小数据训练用两阶段策略——主训练阶段开mosaic,最后20到30轮关掉,用弱增强收尾修正边界回归。参数上利用close_mosaic在训练末尾自动关闭,或者直接单独跑一段mosaic=0.0的续训。如果验证集指标本身已经达标,可以尝试全程关mosaic重跑一次做对比。
5.5 连续帧渗进验证集:mAP虚高,现场漏检
现象:验证集mAP高达98%,拉到现场换一个路口,检测率直接掉到一半以下。
原因:数据集里存在连拍帧。按文件名顺序或简单随机切分时,高度相似的连续帧被同时分进train和val,模型等于背着答案考试,验证分数彻底失真。
解决:划分前按图像相似度分组。用感知哈希或直方图相似度算出帧间重复关系,把相似度超过阈值的图像归到同一组,按组来切分train/val。这样验证集里不会出现“拿着训练集里几乎一模一样的图来考试”的情况。这一步在小样本数据集上成本很低,但能极大提升验证指标的可信度。
这5个坑按排查优先级排,前三个属于数据规范性,后两个属于训练策略。排查时先解决数据源头问题,再回头调训练参数。顺序反了,调参调到头也救不回来。
6. 把559张的价值压榨到底:增强收尾、现场验证与下一批数据怎么补
559张数据集适合当基线,不适合当终点。最有效的三种用法是:格式转换练手、工程链路验证、微调基线。如果想把它的价值榨干,可以按下面这个顺序做。
第一档是增强参数。559张的数量决定了增强不是可选项,而是必需品。一组我常用的基础参数:
yolo detect train model=yolov8n.pt data=dataset/data.yaml \ epochs=120 imgsz=640 batch=16 \ hsv_h=0.02 hsv_s=0.7 hsv_v=0.5 \ degrees=10 translate=0.1 scale=0.5 \ flipud=0.3 mosaic=0.5 close_mosaic=15这组参数的意义:hsv_*模拟光照色偏变化,degrees=10应对轻微倾斜,scale=0.5让模型适应不同距离下的目标大小变化,flipud=0.3是上下翻转增强,对俯拍机位尤其有效。mosaic=0.5把马赛克增强的强度从默认的1.0降下来,因为559张里小目标占比不低,过强的马赛克会让小目标变得不可分辨。close_mosaic=15表示最后15轮自动关闭mosaic。
第二档是现场验证手法。除了验证集mAP,再留20张完全不在训练分布里的现场照片,人工数一遍“框对了几个三轮车、误报了几个行人”。这个数字比任何指标都更能反映上线效果。每次调完参数都把预测错图存一份快照,用时间命名。回头对比不同参数组合的效果时,有硬数据支撑,不用靠印象。
第三档是补数据方向。如果现场表现是漏检为主,补当前场景更恶劣角度的图;如果误报为主,补“行人/其他车辆但不是三轮车”的负样本。补图时每次补充80到120张左右,重新训练后对比现场切片指标,涨了继续补同一方向,不涨则换一个方向。补到现场切片的准确率与验证集mAP不再同步上涨,说明这个方向的样本已经喂够了,该转向下一个数据来源。
我自己做小数据集训练的一个固定习惯是:每换一组参数,就把results.csv和预测错图快照存一份。这些记录攒起来,就是下一次遇到类似小数据集时最可靠的调参依据。559张的第一轮跑通后,真正的价值才开始显现。希望帮到你。
本文还有配套的精品资源,点击获取