简介:面向目标检测与深度学习研究人员的人车识别VOC数据集,包含约1000张手工精细标注的图像,适合训练YOLO、Faster R-CNN等检测模型,可有效解决高质量数据集获取难、标注成本高的问题。资源包共1994个文件,含997个XML标注文件、729张JPG图片与268张PNG图片,总大小711.07MB,以zip压缩包形式提供。XML标注文件记录目标边界框与类别信息,图像覆盖家庭轿车、SUV及行人等常见交通场景,目录分为dataset图像与Annotations标注两部分,结构清晰便于直接加载训练。目前已有1035人学习下载。所有标注均经作者纯手工完成,并通过实际训练验证检测效果良好,质量有保障,适合作为人车识别模型训练、调参与算法评估的理想基础数据集。
1. 手工标注 1000 张人车识别 VOC 数据集:先算清人力成本再动手
“手工标注高质量人车识别VOC数据集1000张”这个标题,乍看只是把辛苦活说清楚了,实际上是整个检测项目的骨架:图片从哪来、框怎么画、XML 怎么组织、训练时怎么转成 YOLO 能吃的格式,全都要你自己定。我见过不少人直接用公开数据集训人车检测,换到自家园区大门、停车场出入口就失效,最后还得回来补自己的图。手工标注就是解决“场景不对”这件事——用 1000 张自己视角的图,把 person 和 vehicle 两类目标逐张框出来,让 YOLOv8 这类模型在固定机位下跑出能用的精度。它适合预算有限、机位固定、想快速验证效果的从业者,也适合第一次接触 Pascal VOC 结构的人拿来做完整练手。
2. 把 VOC 格式拆开:目录、XML 字段与标注工具选型
很多人的项目死在第一步:不知道 VOC 到底长什么样,标出来的东西训练时加载报错。Pascal VOC 的格式从 2012 年定型后就基本没大改,YOLOv5、YOLOv8、MMDetection 都能直接消费,所以先花半小时把它的目录和字段看明白,后面所有脚本都能一次跑通。
2.1 JPEGImages、Annotations、ImageSets/Main 怎么放
VOC 的根目录下通常有三个关键部分,命名是约定俗成的,最好别乱改:
person_vehicle_voc/ ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── Annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt └── val.txtJPEGImages 放原始图片,Annotations 放每张图对应的 XML 标注,ImageSets/Main 下面是划分文件。train.txt 和 val.txt 里每一行是图片文件名去掉了扩展名的编号,比如第一行是000001,程序拿着它去 JPEGImages 里找000001.jpg、去 Annotations 里找000001.xml。这个“三目录对齐”是整个格式的灵魂。
文件命名我一般用六位数字补零,从 000001 开始递增。VOC 官方就是这么干的,和 COCO 那种000000000139.jpg的十二位命名思路一样,就是为了排序方便、对齐方便。图片和 XML 一一对应,多一张图少一个 XML 训练时都会莫名其妙丢样本,这点在第 5 章还会专门踩到。
2.2 XML 里决定模型上限的五个字段
一张图的标注全在 XML 里躺着,但训练真正读的字段就那么几个。下面是一个最小可用的 VOC 标注文件:
<annotation> <folder>JPEGImages</folder> <filename>000123.jpg</filename> <path>/data/person_vehicle_voc/JPEGImages/000123.jpg</path> <source><database>manual</database></source> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>person</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>104</xmin> <ymin>210</ymin> <xmax>348</xmax> <ymax>689</ymax> </bndbox> </object> </annotation>对检测训练来说,<size>里的宽高决定归一化分母,<object>里的<name>决定类别标签,<bndbox>的四个值决定目标位置。剩下的<pose>、<truncated>、<difficult>都是 VOC 时代留下的元信息,YOLO 转换时基本不读。如果你只有 person 和 vehicle 两个类别,name 字段必须严格统一大小写,Person和person在程序里是两个不同的类,这类低级错误排查起来非常费眼。
2.3 LabelImg 还是 X-AnyLabeling:纯手工 1000 张的选型理由
标注工具我首选 LabelImg,理由很直接:它默认就导出 VOC XML,不需要二次转换,快捷键顺手,单机跑 1000 张图不带卡顿。安装方式很简单:
pip install pyqt5 labelImg labelImg如果遇到 PyQt5 装不上,多半是 Python 版本太新,换到 Python 3.8 或 3.10 的环境再装一次就行。老工具依赖旧,但有得用就不折腾它。
有人会问,现在有 X-AnyLabeling 这类带模型辅助的工具,不是更快吗?问题是它先出一个预测框让你改,遇到机位角度刁钻的场景,改框的时间比自己画还长,而且这个工具默认导出的是 JSON 或自定义格式,还得再转一次 VOC。纯手工 1000 张的项目,老老实实用 LabelImg 逐张画最稳。快捷键是效率命脉,记牢下面这几个就够了:
| 快捷键 | 作用 |
|---|---|
| w | 画一个新框 |
| d / a | 跳到下一张 / 上一张 |
| ctrl+s | 保存当前标注 |
| del | 删除选中框 |
| 滚轮 | 缩放图片,方便标小目标 |
打开 LabelImg 后把「PascalVOC」模式设为默认,再指定打开 JPEGImages 目录和默认保存到 Annotations 目录,剩下就是一张一张地画。别小看这个琐碎动作,1000 张图的肌肉记忆会让你后面转换脚本时少犯很多错。
3. 图片收集与预处理:决定 1000 张上限的隐藏工程
标注本身不产生图片,1000 张从哪来是个被严重低估的环节。直接去网上下载一堆街景图,类别是全了,但机位、光照和你的部署场景不匹配,标注完训练照样翻车。图片来源和筛选决定了这套数据集的真实上限。
3.1 从视频抽帧还是从公开数据挑
最常见、最可靠的做法是从自己的监控视频或手机拍摄视频里抽帧。固定机位拍一段 20 分钟的视频,用 ffmpeg 按固定间隔抽帧,很快就凑够几百张:
ffmpeg -i input.mp4 -vf fps=1 -q:v 2 frames/%04d.jpg这段命令每秒抽 1 帧,-q:v 2是 JPEG 质量控制参数,范围 2 到 5,数字越小画质越高。如果你想要每 2 秒抽一帧,把fps=1改成fps=1/2,具体节奏取决于目标在画面里停留的时长。抽完会发现相似帧多到崩溃,这时候不要整段视频都用,每隔一段删一批,只保留机位、人物姿态有变化的。
公开数据集不是不能用,但要注意 license 和视角匹配,比如找公开监控场景的片段做补充。我的习惯是:固定机位视频为主,公开数据只用来补类别多样性,比如补夜间车灯、补雨天行人打伞,比例控制在两成以内。这样既保住场景一致性,又让模型见得多一点。
3.2 去重、筛选与明暗分布
抽帧出来的图至少有三分之一是高度相似的,直接标注等于浪费人力。我一般用感知哈希快速去重:
from PIL import Image import imagehash import os hash_map = {} for f in sorted(os.listdir("frames")): path = os.path.join("frames", f) h = imagehash.phash(Image.open(path)) dup = [key for key, val in hash_map.items() if h - val < 5] if dup: print("duplicate:", f, dup[0]) else: hash_map[f] = himagehash.phash把图片压缩成 64 位指纹,两个指纹的汉明距离小于 5 就认为是重复帧。这个阈值可以调,想严格一点就改成 3,想宽松一点就改成 8。跑完把标记出来的重复图人工再过一遍,保留帧间差异大的。
筛选的时候要按场景分布计数,而不是按图片张数。我见过一个 1000 张的数据集,全是白天晴天,一到傍晚就全线崩。正确做法是人工看一下明暗分布:白天 600 张、傍晚 200 张、夜间 200 张,再穿插几段雨天,保证每个时段的样本量都过百。这个步骤没有脚本能完全自动化,靠的是对着缩略图快速过一遍的眼睛。
3.3 图片压缩与重命名:把家底盘干净
高清监控原图动辄 4000x3000,直接标注会让框选目标时鼠标移动距离过大,费眼又费手。常规做法是先统一缩到最长边 1280 像素,既保留小目标细节,又让标注效率显着提升。同时把文件名重排成六位数字编号,和 VOC 的习惯对齐:
from PIL import Image import os os.makedirs("raw", exist_ok=True) files = sorted(os.listdir("origin")) for i, f in enumerate(files): if not f.lower().endswith((".jpg", ".jpeg", ".png")): continue img = Image.open(os.path.join("origin", f)).convert("RGB") img.thumbnail((1280, 1280)) out_name = f"{i:06d}.jpg" img.save(os.path.join("raw", out_name), quality=90) print(out_name)thumbnail((1280, 1280))会保持长宽比缩小,不会拉伸变形;quality=90是 JPEG 保存质量,肉眼几乎看不出和原图的差别,但文件体积小很多。重命名的好处是后续所有脚本只用正则匹配六位编号,不需要处理乱七八糟的原始文件名。这一步做完了,你手里就有一套干净的raw/目录,可以正式开始标注了。
4. 手工标注实操:从画框规范到 VOC 转 YOLO 的完整脚本
图片准备完,进入到最耗人的环节。手工标注 1000 张的成败不在手速,在规则。规则不先定清楚,标到第 300 张发现标准不一致,回改的成本高到你想放弃整个项目。
4.1 画框规范:人从头顶到脚底,车要含后视镜
开始标之前,先写一条规则,后面所有图都按它执行。我的惯例是:行人框从头顶到脚底,头发、帽檐都算在框内,宁可四边贴紧不要出血;车辆框包含后视镜和外后视镜,含车顶行李架的也要圈进去。人物被车挡住一半以上的,如果类别可辨就照常标注,框到可见边缘即可;如果只露出一只手或一条腿,那就不标。低于 24 像素的行人和低于 40 像素的车辆,统一不标——这类目标即使标了,在 640x640 的输入尺寸下也学不到什么特征,反而给训练数据引入大量噪声。
这条规则看起来简单,实际执行时每张图都有边界情况,比如隔着玻璃的人影、反光里的车。我的处理是:玻璃内人影算半透明遮挡,可辨就标;反光一律不标。标完一个批次后,抽 10% 复查一遍,发现有人把影子画进框了,立刻回去改。规则一致性直接决定后续模型精度能到多少,这是血泪经验换来的。
4.2 标注节奏与文件名一致性检查
1000 张图一个人标,乐观估计要两到三天。别追求一天标完,疲劳状态下手抖、漏框、框偏的问题会成倍增加。我的节奏是每小时休息 10 分钟,每标完 100 张跑一次文件名一致性检查:
import os jpegs = {os.path.splitext(f)[0] for f in os.listdir("JPEGImages") if f.endswith(".jpg")} xmls = {os.path.splitext(f)[0] for f in os.listdir("Annotations") if f.endswith(".xml")} print("jpg 无 xml:", jpegs - xmls) print("xml 无 jpg:", xmls - jpegs)这个脚本把 JPEGImages 和 Annotations 的文件名分别取集合,再做差集。输出为空说明两边完全对齐;有输出就找出是哪张图在保存时被跳过了,补标或删图。很多人在训练时才暴露这个问题,报错信息又长又绕,其实根因就在文件名没对齐,不如在标注阶段就把它拦住。
4.3 把 VOC XML 转成 YOLO TXT:类别顺序错了全白干
标注完成,下一步是转成 YOLO 要的格式。YOLO 的标签是每张图一个 TXT,每行五个数:类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。VOC 的 XML 是左上角和右下角坐标,转换时必须先算坐标再归一化:
import xml.etree.ElementTree as ET import os CLASS_MAPPING = {"person": 0, "vehicle": 1} ANNO_DIR = "Annotations" OUT_DIR = "labels" os.makedirs(OUT_DIR, exist_ok=True) for xml_name in os.listdir(ANNO_DIR): if not xml_name.endswith(".xml"): continue xml_path = os.path.join(ANNO_DIR, xml_name) root = ET.parse(xml_path).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 CLASS_MAPPING: continue b = obj.find("bndbox") x1 = float(b.find("xmin").text) y1 = float(b.find("ymin").text) x2 = float(b.find("xmax").text) y2 = float(b.find("ymax").text) # 把越界框拉回图像有效范围,过滤误点的幽灵框 x1 = max(0.0, min(x1, W - 1)) y1 = max(0.0, min(y1, H - 1)) x2 = max(0.0, min(x2, W - 1)) y2 = max(0.0, min(y2, H - 1)) if x2 - x1 < 2 or y2 - y1 < 2: continue cx = ((x1 + x2) / 2) / W cy = ((y1 + y2) / 2) / H bw = (x2 - x1) / W bh = (y2 - y1) / H lines.append(f"{CLASS_MAPPING[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(OUT_DIR, xml_name.replace(".xml", ".txt")) with open(out_path, "w") as f: f.write("\n".join(lines))这段脚本有几个细节值得说。宽高一定要从 XML 的<size>读,不要重新去读图片文件——如果图片带 EXIF 旋转信息,PIL 读出来的宽高和标注时看到的不一致,归一化就全错了。clamp 操作把鼠标画框时出界的几个像素拉回有效范围,避免训练时 loss 计算出现负数面积。最后过滤掉宽或高小于 2 像素的框,这类框是鼠标误点产生的,留着只会让模型学到噪声。
类别顺序person: 0, vehicle: 1是写死的,转换后的 TXT 第一行如果出现0,就代表 person,出现1代表 vehicle。这个顺序必须和第 6 章里data.yaml的 names 完全一致,顺序错了,模型训练出来的预测类别就是错位的。如果图片里没有标注任何目标,这个脚本会生成一个 0 字节空 TXT,这是允许的,YOLO 训练时会自动跳过空标签,不要手动删掉它们。
4.4 同步生成 train.txt 与 val.txt
转到 labels 之后还有最后一步:做训练集验证集划分,写进 ImageSets/Main。这里有个 8:2 的随机划分脚本:
import os import random ids = [os.path.splitext(f)[0] for f in os.listdir("JPEGImages") if f.endswith(".jpg")] random.seed(42) random.shuffle(ids) split = int(len(ids) * 0.8) train_ids, val_ids = ids[:split], ids[split:] os.makedirs("ImageSets/Main", exist_ok=True) for name, lst in [("train", train_ids), ("val", val_ids)]: with open(f"ImageSets/Main/{name}.txt", "w") as f: f.write("\n".join(lst))random.seed(42)固定随机种子,这样每次跑脚本划分都完全一样,复现实验时不会因为训练验证集变了而说不清结果。随机划分在图片来源分散的情况下够用,但如果你的图是从视频抽帧来的,强烈建议在划分后人工检查一下 val.txt 里的编号,确保验证集中没有和训练集相邻几帧的画面。这个坑会在第 5 章展开。
最后把目录整理成 YOLO 的 images/labels 组织方式,一条命令的事:
for id in $(cat ImageSets/Main/train.txt); do mkdir -p person_vehicle/images/train person_vehicle/images/val mkdir -p person_vehicle/labels/train person_vehicle/labels/val cp JPEGImages/$id.jpg person_vehicle/images/train/ cp labels/$id.txt person_vehicle/labels/train/ done for id in $(cat ImageSets/Main/val.txt); do cp JPEGImages/$id.jpg person_vehicle/images/val/ cp labels/$id.txt person_vehicle/labels/val/ doneYOLO 训练时要求图片在images/train、标注在labels/train,而且两边的文件名必须一致。这个 bash 循环把 JPEGImages 里的图和 labels 里的 TXT 按划分文件同时拷贝过去,三目录天然对齐。如果你的目标是旋转框检测,比如要喂给 mmrotate 跑 DOTA 格式,那就得走另一套转换流程,上面的 VOC 转水平框只适合普通目标检测。
5. 手工标注避坑指南:人车识别 1000 张里最容易翻车的五个点
这个环节写满五条,每一条都是我或同行真实踩过的,按“现象、原因、解决”列清楚。标 1000 张图本身不难,难的是标完之后训练时那些让你通宵排查的玄学问题。
5.1 验证集 mAP 高得吓人,现场视频却疯狂漏检
这是做监控场景人车识别最典型的翻车现场。现象是训练时验证集 mAP50 到了 0.9 以上,一部署到现场视频,连续几十秒漏掉一个走过去的人。
原因基本是数据泄漏:如果 1000 张图全部来自同一段 20 分钟视频,随机划分后,验证集的某一帧可能只和训练集的某一帧差了 0.5 秒,画面几乎一样,模型等于在背答案,根本学不会泛化。解决方法是按时间段划分而不是按帧随机划分,比如用视频的前 15 分钟做训练,后 5 分钟做验证。如果素材来自多个机位,优先按机位划分,保证验证集的场景和训练集不同。这是处理视频抽帧数据时最不能省的一步。
5.2 训练报错 Label class 1 exceeds nc=1
现象是 YOLOv5 或 YOLOv8 训练到一半直接崩,提示类别 id 超出类别总数。原因几乎都是 VOC 转 YOLO 时类别顺序和 data.yaml 对不上。比如转换脚本里vehicle是 1,但 data.yaml 里只写了names: ['person'],模型一看类别数就 1,读到 1 直接越界。
解决方法是把类别顺序固化成唯一来源。我的做法是建一个predefined_classes.txt,里面按顺序写person、vehicle,转换脚本和 data.yaml 都从这里读顺序,而不是各自维护一份。检查时先看转换出的任一 TXT 里最大的类别 id,再看 data.yaml 里 names 的长度,两个值一对就定位了。
5.3 人和车数量严重失衡,模型只认识人
现象是模型对人的召回率很好,对车的召回率惨不忍睹,尤其近处的大卡车几乎全漏。
原因是 1000 张图里人出现了 4000 次,车只出现了 800 次,模型把大部分容量都用去拟合人了。很多人统计样本只看图片张数,从不看目标个数,这是错误认知。解决方法是先写一段统计脚本,遍历所有 XML 统计两个类别的 bndbox 数量,如果车明显偏少,回去单独补拍车辆的图片,而不是简单重复采样,因为重复采样会造成过拟合。理想比例至少做到人 2500 次、车 1500 次,每类目标数低于 1000 时模型很难学好这个类。
5.4 小目标全部丢失,行人一远就消失
现象是近处的人检测正常,画面中 80 像素高的小人全被忽略,模型似乎对小物体免疫了。
原因大多是标注阶段定的最小尺寸阈值太大,或者缩图时把 4000 宽的图压到了 640,小目标直接被压没了。解决方法是把标注规范里的最小尺寸写明确:行人高度低于 24 像素不标、车辆宽度低于 40 像素不标,但图片最长边缩到 1280 时这个阈值还能保住大多数目标。训练时的 imgsz 也别盲目开大,先按 640 跑一版,如果小目标还是丢,再试 960,同时观察显存占用。小目标检测没有银弹,数据里留得住、训练时输入够大,才能谈得上模型能力。
5.5 bndbox 越界和文件错位,训练时解码莫名其妙崩
现象是训练过程中偶尔报坐标解析错误,或者某张图加载时找不到标签,位置随机、难以复现。原因有两类:一类是鼠标画框出界,xmax 大于图片宽度;另一类是半路手动删过图片,JPEGImages 和 Annotations 数量对不上了,训练集里残留了没有 XML 的图片路径。
解决方法是把转换脚本里的 clamp 逻辑保留,同时每次划分训练集之前跑一遍 4.2 节的文件名一致性检查脚本,保证两边集合完全相等。还有一个容易忽略的细节:如果你用脚本批量重命名过图片,XML 里的<filename>和<path>也要同步更新,LabelImg 保存时会写死原路径,训练框架如果按 XML 里的 path 找图片就会报错,按你自己的目录结构重新组织 JPEGImages 后,最稳妥的做法是让脚本忽略 XML 里的 path,只按文件名匹配。
6. 用 1000 张 VOC 拉起 YOLOv8:最小训练配置与验收方法
转换出来的person_vehicle/目录配上一条 data.yaml 就能训练了。YOLOv8 和 YOLOv5 的配置差别不大,这套流程同样适用 yolov5 训练自己的数据集,只需要把命令里的yolo换成python train.py的对应写法。
path: /data/person_vehicle train: images/train val: images/val names: 0: person 1: vehicle训练命令用最小配置起步:
yolo detect train data=person_vehicle.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 patience=20 device=0model=yolov8n.pt是 nano 版本,1000 张数据量下注意力应该放在数据和标注质量上,而不是模型容量;batch=16在 6GB 显存左右能跑,显存小就降到 8;patience=20表示 20 个 epoch 没有提升就早停,防止过拟合。训练完成后看验证结果:
yolo detect val data=person_vehicle.yaml model=runs/detect/train/weights/best.pt重点看 mAP50 和 per-class AP,而不是 total loss——loss 是黑匣子,mAP 才是能说话的指标。如果 person 的 AP 明显低于 vehicle,回到第 5 章看类别是不是失衡了。
训练跑通之后有一个便宜的进阶技巧:拿 best.pt 对验证集做预测,把置信度在 0.3 到 0.65 之间、但实际有目标的图片挑出来,人工确认后回填进训练集。这部分是模型最有进步空间的难例,补 100 张比随便加 300 张普通图效果更明显。我现在的习惯是先标 300 张快速训一版,照着漏检模式决定后面 700 张补什么图,而不是攒满 1000 张再开工,效率完全不同。希望帮到你。
本文还有配套的精品资源,点击获取