ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手工标注1000张人车识别VOC数据集:从标注规范到训练验证的完整指南

手工标注1000张人车识别VOC数据集:从标注规范到训练验证的完整指南 简介这是一份面向深度学习目标检测任务的高质量人车识别VOC数据集共包含1000张图像及对应标注适合需要训练行人/车辆检测模型的开发者与研究人员。压缩包共1994个文件以XML标注文件、JPG和PNG图像为主其中Annotations目录存放每张图片的目标边界框与类别信息可直接用于YOLO、Faster R-CNN等模型的训练与验证图像涵盖轿车、SUV、家庭轿车等多种场景标注均为纯手工完成作者亲测训练后检测效果良好能有效支撑实际落地项目的精度需求。资源包整体约711MB目前已有1035人学习使用数据规模与标注质量兼备可作为构建车辆、行人检测系统的理想训练数据起点。1. 手工标注1000张人车识别VOC数据集这件事到底难在哪想训练一个人车检测模型最省事的做法是下载公开的VOC数据集但你会发现里面的人车类别和自己的场景对不上要么夜间和遮挡样本太少要么车的类型和监控视角完全不像模型在测试集上看着还行一到现场就翻车。自己手工标注1000张人车识别VOC数据集不是简单把框画出来而是把“你的场景里的人车标准”定清楚。VOC数据集的好处是格式通用一个JPEG配一个同名XML后续转YOLO、COCO都很容易适合公开数据集不够、想快速验证模型能力的小团队和个人。这篇按我实际做过的流程从格式、标注、质检到训练验证把每一步和坑讲明白新手能照着做熟手也能看边界。2. 先定标准再动手VOC人车检测数据集的格式与标注规范手工标注最怕的就是边标边改标准。你标到第500张突然发现前面把公交车都标成car要么全部返工要么忍着字符号不一致的模型结果。所以动手之前先把VOC格式和标注规范钉死。2.1 VOC数据集的目录结构与XML文件到底长什么样VOC数据集格式起源于PASCAL VOC挑战赛标准的目录结构长这样VOCdevkit/ └── VOC2007/ ├── Annotations/ # 存放XML标注文件 ├── JPEGImages/ # 存放JPEG原图 ├── ImageSets/ │ └── Main/ # 存放train.txt、val.txt等划分文件 └── ...每个XML文件对应一张图片记录图片文件名、尺寸和每个目标的类别与边界框。一个典型的人车XML如下annotation folderVOC2007/folder filenamestreet_day_001.jpg/filename size width1920/width height1080/height depth3/depth /size object nameperson/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin100/xmin ymin80/ymin xmax300/xmax ymax480/ymax /bndbox /object object namecar/name truncated1/truncated difficult0/difficult bndbox xmin500/xmin ymin200/ymin xmax900/xmax ymax600/ymax /bndbox /object /annotation这里每个字段都有用filename必须和JPEG文件名一致width和height必须是图片实际像素name是类别名bndbox是像素坐标。很多新手忽略difficult字段导致后面转YOLO时把被严重遮挡的目标也当成正常目标参与训练模型越学越糊涂。提示XML里的坐标必须满足 xmin xmax、ymin ymax并且不能超出图像范围。LabelImg 一般不会画反但手动改文件时很容易改坏。2.2 人车识别类别定义person、car、bus、truck 的判定边界标题说的是人车识别但“车”这个字很含糊。我建议按道路场景常见类别拆成四类person、car、bus、truck。不要把所有带轮子的都标成car否则模型无法区分公交车和货车部署后误报率极高。可以先用一张表固定判定边界类别判定标准易混淆项person站、坐、行走的人包含骑摩托车/自行车上的人体雕像、海报上的人不标car乘用车、SUV、面包车长度明显小于bus皮卡归truck不归carbus车身长度超过8米厢式结构多个车窗长轴面包车容易误判看轴距和车门布局truck载货货车含货斗和厢式货车皮卡按货斗结构归truck这个表定完标注时就不再纠结同一辆皮卡今天标car明天标truck。类别不要贪多1000张图如果标8类平均每类只有几百个框训练出来的模型基本只看得到第一二类。人车识别场景4类已经很吃力能压缩到2类就优先压缩。2.3 高质量标准遮挡、截断、小目标怎么标标注质量的差别不在框画得准不准而在“特殊目标”的处理是否一致。我的规则是遮挡超过50%但人眼能分辨正常标遮挡超过80%或者只剩一条腿建议把difficult设为1让模型在训练时忽略这个框。目标被图片边缘截断按可见部分画完整外接框同时truncated设为1。小目标像素小于24×24且能分辨才标太小的一律设difficult1。边界框要贴合目标外轮廓不要留出大块背景也不要切到人的头或者车的后视镜。经验是框与目标表面留2到3个像素间隙稍微紧一点比松一点好。同一个目标在连续多张图里出现不要每次按感觉重新画要用上一帧的框微调否则训练时同一目标的框尺寸抖动会干扰回归分支。2.4 为什么手工标注公开数据集与自动标注的局限公开的VOC数据集总量不小但它不是为你的场景准备的。我做过一个项目用的是城市道路监控画面公开数据集里大量是户外自然场景和搞笑图片人和车的姿态、相机高度、光照都不同迁移过去效果很差。自动标注工具比如用现有检测模型给图片预打框速度很快但漏标和类别错乱很严重尤其夜间小目标和遮挡目标后期清洗不如一开始就手工标。1000张的手工标注熟练的人一小时能标20到30张大概是两三天的工作量。这个代价换来的是每一张图都经过人眼确认框和类别是可信的后面转格式、训模型出现问题时能快速定位不会甩锅给标注。我的判断是1000张量级的专用数据集手工标永远比半自动标划算。3. 手工标注全流程选图、LabelImg配置与逐张质检标准定好之后就可以开始动手了。这一章是完整的可复现流程包含选图策略、工具配置、标注步骤和检查脚本。3.1 选图策略1000张图如何覆盖场景与光线不要从视频里每隔几帧抽图就完事视频相邻帧高度相似1000张可能只有150个独立场景。训练出来的模型在验证集上很漂亮换个路口就崩。我的做法是先按场景分桶白天、夜间、黄昏、雨天、逆光至少各占一部分比例根据你的实际使用场景定。如果主要做夜间检测夜间图就要超过一半。同一个场景连续抽帧不要超过20张。抽完图后用缩略图目测去重感知哈希可以辅助但最终以人眼为准。落在同一条路上的两个不同路口的画面看着相似但背景不同可以保留同一摄像头同一时段连续两分钟的帧只能留一张。选图还注意别只选大目标清晰图。远距离的小目标、被栏杆遮挡的行人、公交车后面探出头的车这些难例要刻意混进去。否则模型在简单样本上精度很高一到复杂场景就出现离谱漏检。3.2 LabelImg安装与PascalVOC模式配置标注工具我常用 LabelImg原生支持 PascalVOC 格式操作简单。安装方式pip install labelImg labelImg如果 pip 安装失败从 GitHub 拉取源码运行git clone https://github.com/tzutalin/labelImg cd labelImg pip install -r requirements.txt python labelImg.py打开后做三件事第一确认左侧按钮显示PascalVOC不是YOLO第二勾选 Auto Save mode切换图片时自动保存第三设置好默认的图片目录和XML保存目录。在Linux高分辨率屏幕上如果界面按钮挤在一起先执行export QT_AUTO_SCREEN_SCALE_FACTOR1 labelImg注意Auto Save mode 只在切换图片时触发直接关闭窗口可能丢最后一张图的改动。我后面会讲这个坑。3.3 标注实操一张图从打开到保存的9个动作很多人用 LabelImg 只会按 W 画框画完就赶下一张结果漏标率很高。我一般按固定顺序操作打开图片后先整体看5秒确定有几个目标心里数一遍按 W 进入创建框模式从目标左上角拖到右下角松手在弹出的框里输入类别名输入完后按 Enter 确认如果类别名已经存在直接按快捷键我习惯把 person 设为 1car 设为 2bus 设为 3truck 设为 4按 Ctrl鼠标拖动微调边界框的边线把图片缩放到25%显示按“四角加中心”的顺序扫一遍专门找漏掉的小目标确认所有目标都标完按 D 切换到下一张开启 Auto Save 时文件名会同步生成否则手动按 CtrlS 保存每天标完后用脚本对比 JPEGImages 和 Annotations 两个目录确保图片和XML一一对应。这里最关键的是第六步。人眼有盲区在100%缩放时只盯着某个目标很容易漏掉边缘的小车和行人。缩到25%后整张图一览无余漏标率会明显下降。3.4 质检脚本批量检查漏标、错标与非法框手工标注结束后先别急着训练。我通常跑一个Python脚本做基础检查import os import xml.etree.ElementTree as ET img_dir JPEGImages xml_dir Annotations allowed_classes [person, car, bus, truck] errors [] for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) img_name xml_name.replace(.xml, .jpg) img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): errors.append(f缺失图片: {img_name}) continue tree ET.parse(xml_path) root tree.getroot() w int(root.findtext(size/width)) h int(root.findtext(size/height)) for obj in root.findall(object): name obj.findtext(name) xmin int(float(obj.findtext(bndbox/xmin))) ymin int(float(obj.findtext(bndbox/ymin))) xmax int(float(obj.findtext(bndbox/xmax))) ymax int(float(obj.findtext(bndbox/ymax))) if name not in allowed_classes: errors.append(f{xml_name}: 未知类别 {name}) if xmin 0 or ymin 0 or xmax xmin or ymax ymin: errors.append(f{xml_name}: 非法框 {name} {xmin},{ymin},{xmax},{ymax}) if xmax w or ymax h: errors.append(f{xml_name}: 框越界 {name}) if xmax - xmin 5 or ymax - ymin 5: errors.append(f{xml_name}: 框过小 {name}) if errors: for e in errors: print(e) print(f共发现 {len(errors)} 个问题) else: print(基础检查通过)逻辑说明脚本遍历 Annotations 下的每个XML先确认对应图片存在再检查类别名合法、坐标有效、不越界、尺寸大于5像素。这里的findtext路径要写对object/bndbox/xmin必须放在for obj循环内部取直接写root.find(object/bndbox/xmin)只会拿到第一个目标的值。参数说明最小框大于5像素是我个人经验再小的目标基本没有训练价值如果人员距离特别远应该把这类目标在XML里设difficult1让脚本放行而不是放宽全局阈值。4. 从VOC到训练格式XML转YOLO与数据划分标注完成只是第一步。要训练模型得把VOC格式转成目标检测框架需要的格式并做好数据划分。4.1 为什么训练框架通常不直接用VOC XMLVOC XML 是表达能力最强的标注格式但训练时逐帧解析XML太慢。YOLO、mmdetection 这类框架也提供 VOC 解析器但底层最终还是要转成张量。实际操作中我更倾向于转成 YOLO 格式每张图片对应一个同名的txt文件每行是类别 中心x 中心y 宽度 高度坐标全部归一化到 0~1。这样训练读图时快数据增强也方便操作。但这里有个原则原始XML必须保留YOLO txt是派生物。转出来的txt丢了可以随时再生成XML丢了数据资产就没了只能重新标。4.2 XML转YOLO解析、归一化与脚本实现我通常用下面这个脚本转换import os import xml.etree.ElementTree as ET # 类别映射必须和训练配置文件的顺序一致 class_map {person: 0, car: 1, bus: 2, truck: 3} def convert(xml_path, txt_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.findtext(name) difficult int(obj.findtext(difficult, 0)) if difficult 1: continue # YOLO没有difficult概念跳过难例 if name not in class_map: continue bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) for xml_name in os.listdir(Annotations): xml_path os.path.join(Annotations, xml_name) tree ET.parse(xml_path) root tree.getroot() # 优先取XML里的size避免图片EXIF旋转导致宽高错位 img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) txt_path os.path.join(labels, xml_name.replace(.xml, .txt)) convert(xml_path, txt_path, img_w, img_h)逻辑说明这段代码把difficult1的框直接丢弃因为 YOLO 格式没有“难例”标记。如果你不想丢就不要设 difficult而是在标注阶段就不标那些太模糊的目标。坐标归一化时用的是XML里的size/width和size/height不是直接用PIL.Image.size因为部分手机和相机图片存在 EXIF 旋转实际像素宽高和XML不一致用XML能保证框的位置不偏。参数说明class_map的顺序就是模型预测输出的顺序。我按下标0、1、2、3排列训练配置里的类别文件也要保持同样顺序否则预测结果出现“person识别成car”的错位。4.3 数据划分train、val、test比例与场景平衡1000张图常见划分是 train:val:test 8:1:1。如果对验证集稳定性有要求可以换成 7:2:1。但不要用随机洗牌直接切而是先按场景分组同一场景的图片尽量放到同一个集合里避免训练集和验证集出现连续帧的相似图导致验证分数虚高。划分脚本import os import random random.seed(42) images [f for f in os.listdir(JPEGImages) if f.endswith(.jpg)] random.shuffle(images) total len(images) val_split int(total * 0.1) test_split int(total * 0.2) val images[:val_split] test images[val_split:test_split] train images[test_split:] with open(train.txt, w) as f: for img in train: f.write(os.path.abspath(os.path.join(JPEGImages, img)) \n) with open(val.txt, w) as f: for img in val: f.write(os.path.abspath(os.path.join(JPEGImages, img)) \n) with open(test.txt, w) as f: for img in test: f.write(os.path.abspath(os.path.join(JPEGImages, img)) \n)逻辑说明脚本按顺序把前10%划为验证、10%到20%划为测试、剩下80%为训练。random.seed(42)保证每次运行划分结果一致方便复现。文件里写的是JPEG的绝对路径YOLO训练时会直接按这个路径读图。参数说明如果你已经做了场景分组建议在洗牌前先给同场景图片一个共同前缀然后按前缀分组在组级别划分而不是在图片级别随机切。1000张规模不大不做场景分组也可以但模型部署到新现场时需要格外小心。4.4 训练前最后一次检查类别映射与空标签转换完成后还要花两分钟做一次终检。先统计每类目标数量for f in labels/*.txt; do awk {print $1} $f; done | sort | uniq -c正常情况person 和 car 的数量级应该在同一个范围内如果 person 只有几百个car 有几千个需要补标或用增强手段平衡。另外检查 labels 目录下有没有空 txt 文件。一张图如果没有标注在YOLO训练中它会作为纯背景图参与负样本学习不是完全不能用但如果你图里明明有目标却没标那就是漏标。我还会写一行命令对比文件数量ls JPEGImages/*.jpg | wc -l ls labels/*.txt | wc -l两个数字不一致说明有空标注或缺标注需要回头查。5. 避坑与常见问题人车标注数据集最容易翻车的五个地方手工标注人车识别数据集很多坑不是画框本身而是画完之后训练阶段才暴露。这一章是我反复踩过的血泪经验按现象、原因、解决三步写。5.1 类别不平衡车多person少模型只学到了车现象训练时 loss 降得很快但验证集上 person 的 AP 只有 0.3car 却有 0.85看起来模型“偏科”严重。原因道路上自然出现的车往往比行人多而且行人目标小、容易被漏标。标注时如果你没做目标数量统计person 和 car 的框数差距会越来越大。模型在训练时对小类别样本过拟合严重遇到新人脸就误检。解决标注中期就随手跑到第4.4节的统计命令哪类少就定向补图。比如 person 太少就去商场门口、公交站、人行道附近补标。如果图已经标完优先用 Mosaic 增强把一张大图里的小行人通过拼接放大。也可以在损失函数里给 person 类提高权重但这不是根本解法。5.2 看到同一人连续出现在多帧框的大小却像心跳现象模型在视频测试时同一行人从远到近走过去框的宽度不是平稳变化而是忽大忽小检测框像在呼吸。原因标注员在放大视图下画框每张图都是新画的凭感觉取边界。不同帧放大比例不同框的贴合程度就不同训练数据里同一个目标的框尺寸存在抖动回归分支学不到稳定边界。解决连续帧标注时不要每张重新画先在上一张图上复制现有框再按 Ctrl微调。质检脚本可以加一条规则如果同一场景连续10帧里某个框的宽高变化超过20%标记出来人工复核。这个属于高级质检但能明显提升训练效果。5.3 XML解析报错UTF-8 BOM和多出来的空格现象训练时解析 VOC 数据集报ParseError或出现“XML or text declaration not at start of entity”的提示。原因有人用文本编辑器打开XML手动改过坐标保存时带了 UTF-8 BOM 头或者标注工具在文件开头写过不可见字符。另一个常见原因是XML里出现了未转义的符号比如把某个路名写进了备注字段。解决所有 XML 修改都用脚本和标注工具不要手动编辑。已经污染的文件先用下面命令清掉 BOMsed -i s/^\xEF\xBB\xBF// Annotations/*.xml再用 Python 的ElementTree批量解析一次能过就是干净了。5.4 图像改名后XML对应关系全乱了现象从视频抽帧得到的文件名是一长串时间戳某些场景想按语义排序把IMG_0001.jpg改成street_day_001.jpg结果 XML 里的filename还是旧名字模型训练时报找不到图片。原因XML 里的filename字段只在标注工具保存那一刻写入之后你在文件管理器里改图片名工具不会知道更不会同步XML。解决命名规范必须在标注前定好。我用场景_光照_三位编号格式比如street_day_001.jpg。如果已经改名乱了写脚本按新文件名批量改写XML里的filename字段import os import xml.etree.ElementTree as ET for xml_name in os.listdir(Annotations): xml_path os.path.join(Annotations, xml_name) tree ET.parse(xml_path) root tree.getroot() old root.findtext(filename) new xml_name.replace(.xml, .jpg) if old ! new: root.find(filename).text new tree.write(xml_path, encodingutf-8, xml_declarationTrue)注意这个脚本只改了XML里的filename字段XML文件名本身以及JPEG文件名都要保持一致否则文件列表还是对不上。5.5 LabelImg自动保存的坑旧数据覆盖新数据现象标注完100张后检查发现最后3张的XML里根本没有框或者框停留在上一次保存的状态。原因LabelImg 的 Auto Save mode 只在切换图片时触发如果你画完最后一张直接关窗口改动没写入文件。还有一种情况你编辑了一个框但没按 CtrlS 就双击切到别的文件自动保存写入的是切换前后的状态刚编辑的内容可能丢。解决养成关窗前手动按一次 CtrlS 的习惯。每天标完用脚本对比XML里的filename和JPEG文件名的差异以及XML文件最后修改时间。如果发现某张图的XML比图片时间还旧说明这张图片可能是后来新加的需要重新标注。6. 用1000张做出可用的检测模型验证方法与增量标注标完、转完、检查完最后一步是让数据说话然后带着这套数据继续滚下去。6.1 快速跑一个baseline不要上来就用大模型先用 YOLOv5s 这种轻量模型训练50轮看基本AP。我一般这样做python train.py --data data.yaml --weights yolov5s.pt --epochs 50 --imgsz 640如果 val 集上 person 的 AP 超过 0.6car 超过 0.75说明数据质量基本可用。如果某类只有 0.2回去查标注规范是不是出现了同类目标两种标法或者样本数量太少。这一步只验证数据不调参调参放到数据稳定之后。6.2 数据增强与难例挖掘1000张图确实不够训练一个鲁棒模型但可以通过难例挖掘把1000张用出3000张的效果。我的做法是训练完第一版后用模型去预测验证集把置信度在0.3到0.7之间的预测框全部找出来人工复查。这中间往往有两种结果一个是漏标补充到XML里另一些是模型误检比如把路牌看成person说明训练数据里缺少类似负样本我会去截几张路牌和电线杆的图放到训练集里作为背景。这种难例挖掘比再多标100张新图更高效因为它直接攻击模型最不确定的区域。我习惯跑两轮难例挖掘每轮只改错的标注不修改已经通过验证的XML。6.3 版本管理与增量标注最后说一个长期习惯。原始XML是唯一真相YOLO txt、划分txt、增强后的图片都是生成物不要手工编辑。增量标注时只往 Annotations 目录里加新的XML不要回头改旧文件如果确实发现某个框标错了写一个corrections.py脚本统一修正并记录修改日志。我之前吃过亏直接改了XML后一个月忘了改过哪里训练结果对不上来回排查浪费了两天。后来所有改动都走脚本参数留档反而效率最高。这是手工标数据集最后一道后悔药希望对你也管用。本文还有配套的精品资源点击获取
返回列表