资讯详情

YOLOv3行人检测实战:从VOC2012数据抽取到模型训练全流程

📅 2026/10/14 16:48:06 | 华诺云谱 👁 阅读
YOLOv3行人检测实战:从VOC2012数据抽取到模型训练全流程
1. 项目缘起与整体设计思路行人检测这个方向说它是计算机视觉里的“常青树”一点不为过。无论是安防监控、自动驾驶、客流统计还是机器人避障只要涉及到“画面里有没有人、人在哪”这个基本问题行人检测就是绕不开的第一环。我这次做的项目核心目标很明确用YOLOv3这个经典的单阶段检测器训练一个专门针对行人的检测模型训练数据从VOC2012数据集里抽取。说白了就是把VOC2012里所有包含行人的标注框筛出来重新组织成一份行人专用的数据集然后喂给YOLOv3去训练最终得到一个只检测“person”这一个类别的模型。为什么选YOLOv3而不是更新的版本这个问题我被问过很多次。我的回答通常是这样YOLOv3虽然诞生于2018年但它的结构足够经典、代码实现足够成熟、社区资料足够丰富对于想理解单阶段检测器工作原理的人来说它依然是最好的教材之一。而且在实际工程中很多边缘设备上的部署方案仍然是围绕YOLOv3系列做的优化它的Darknet-53骨干网络在精度和速度之间的平衡做得相当好。你把它跑通了再去迁移到YOLOv5、YOLOv8甚至更新的架构思路是相通的。至于为什么从VOC2012抽取而不是直接用COCO或者CrowdHuman这里有几个现实考量。VOC2012的标注质量很高图像数量适中约1.7万张对于个人开发者或者小团队来说训练成本可控。COCO虽然数据量大但下载和处理的时间成本高而且类别多做单类行人检测反而需要额外的筛选工作。CrowdHuman更专注于拥挤场景标注格式又不一样。VOC2012的XML标注格式简单直接解析起来不费劲抽取行人类别的操作几行代码就能搞定。所以对于“想快速验证行人检测方案”这个需求来说VOC2012是一个性价比很高的起点。整个项目的设计思路可以拆成三个层次来理解。数据层从VOC2012的Annotation目录里解析XML文件找出所有包含person类别的图像提取对应的边界框坐标然后转换成YOLO训练所需的txt格式标注。模型层基于Darknet框架配置YOLOv3的网络结构把最后的分类数从80改成1只有person一类调整filter数量。训练层设置合适的超参数包括学习率、batch size、anchor box等在单卡或者多卡环境下完成训练最后用mAP和实际推理效果来评估模型。这个方案适合谁呢如果你刚接触目标检测想找一个完整的、从数据准备到模型训练再到推理测试的全流程项目来练手这个项目非常合适。如果你已经有一定经验但没做过数据集的定制化抽取和格式转换这里面的处理技巧也能给你一些参考。甚至如果你只是想知道“YOLOv3训练自己的数据到底要改哪些地方”这篇文章也能直接当操作手册用。2. 数据准备从VOC2012到行人专用数据集2.1 VOC2012数据集的结构与标注格式VOC2012的目录结构是固定的下载解压之后你会看到这几个文件夹Annotations存放XML格式的标注文件JPEGImages存放原始图像ImageSets里面是训练集、验证集的划分文件SegmentationClass和SegmentationObject是分割任务用的做检测的话可以忽略。每个XML文件对应一张图像里面记录了图像尺寸、包含的物体类别和边界框坐标。一个典型的XML标注长这样根节点是annotation下面有filename、size包含width、height、depth、然后是若干个object节点。每个object里有name类别名、pose、truncated、difficult以及bndbox包含xmin、ymin、xmax、ymax。行人检测只需要关注name为person的那些object把它们的bndbox坐标提取出来就行。这里有个细节需要注意VOC的坐标是1-based的也就是说左上角第一个像素的坐标是(1,1)而YOLO训练时用的是0-based的归一化坐标。所以在转换的时候需要先把坐标减1再除以图像的宽和高做归一化。这个细节如果搞错了训练出来的框会整体偏移一个像素虽然影响不大但在高精度要求的场景下还是要注意。2.2 筛选行人类别的实操脚本我写了一个Python脚本来完成这个抽取工作逻辑很直接遍历Annotations目录下所有XML文件用xml.etree.ElementTree解析检查是否存在name为person的object。如果存在就把这张图像的文件名记录下来同时把所有person的bndbox坐标存到一个列表里。最后输出两个文件一个是要复制的图像文件名列表一个是每张图像对应的标注信息。import os import xml.etree.ElementTree as ET voc_root /path/to/VOC2012 anno_dir os.path.join(voc_root, Annotations) output_list [] for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() persons [] for obj in root.findall(object): name obj.find(name).text if name person: bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) persons.append((xmin, ymin, xmax, ymax)) if persons: img_name root.find(filename).text output_list.append((img_name, persons)) print(f包含行人的图像数量: {len(output_list)})跑完这个脚本你会发现VOC2012里包含行人的图像大概有几千张具体数字我记不太清了但足够训练一个基础的行人检测模型。如果你觉得数据量不够还可以考虑把VOC2007也加进来格式是一样的合并处理就行。2.3 转换成YOLO格式标注YOLO的标注格式和VOC完全不同。每张图像对应一个txt文件每行表示一个物体格式是类别索引 中心x 中心y 宽度 高度所有值都是归一化到0到1之间的浮点数。因为咱们只检测行人这一类所以类别索引固定为0。转换的核心计算是这样的中心x (xmin xmax) / 2.0 / img_width中心y (ymin ymax) / 2.0 / img_height宽度 (xmax - xmin) / img_width高度 (ymax - ymin) / img_height。注意VOC坐标是1-based所以严谨的做法是先对xmin、ymin减1xmax、ymax减1再做归一化。不过实际测试下来这点偏差对最终模型性能的影响微乎其微追求极致的话可以处理一下。import os from xml.etree import ElementTree as ET def convert_to_yolo(xml_path, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): if obj.find(name).text ! person: continue bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) - 1 ymin float(bndbox.find(ymin).text) - 1 xmax float(bndbox.find(xmax).text) - 1 ymax float(bndbox.find(ymax).text) - 1 cx (xmin xmax) / 2.0 / img_width cy (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height lines.append(f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines图像尺寸可以从XML的size节点里读也可以用OpenCV直接读图像获取。我建议用XML里的尺寸因为有些图像可能被重新保存过实际尺寸和标注时不一致用XML里的数据更保险。2.4 数据集划分与目录组织YOLO训练需要两个文件来指定训练集和验证集train.txt和val.txt每行是一张图像的绝对路径。我一般按照8:2或者9:1的比例来划分用随机种子固定下来保证每次划分结果一致方便复现。目录结构我习惯这样组织custom_person/ ├── images/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── labels/ │ ├── 000001.txt │ ├── 000002.txt │ └── ... ├── train.txt └── val.txt图像和标注文件同名只是扩展名不同这样YOLO在加载的时候会自动去labels目录找对应的txt。这个命名规则是YOLO的默认约定不用改代码就能直接用。注意VOC2012里有些图像的标注中person的bndbox可能存在宽高为0或者超出图像边界的情况。我在处理的时候加了一层过滤宽度或高度小于等于0的框直接丢弃超出边界的坐标裁剪到图像范围内。这种脏数据虽然不多但不处理的话训练时会出现NaN loss排查起来很烦人。3. YOLOv3模型配置与核心参数调整3.1 Darknet框架的编译与环境准备YOLOv3的官方实现是基于Darknet的这是一个用C和CUDA写的轻量级框架。编译过程不复杂但有几个坑需要提前知道。首先确认你的环境里有OpenCV用于图像读写和显示、CUDAGPU训练必备、cuDNN加速卷积运算。然后修改Makefile文件把GPU1、CUDNN1、OPENCV1这几行取消注释。如果你没有GPU那就只能CPU训练速度会慢到让你怀疑人生建议至少用一张显存8G以上的卡。编译命令就是make如果报错说找不到某个头文件大概率是OpenCV的路径没配好。可以在Makefile里手动指定OPENCV_INCLUDE和OPENCV_LIB的路径。编译成功后会在根目录生成darknet可执行文件用./darknet测试一下能看到用法说明就说明编译通过了。3.2 配置文件的关键修改点YOLOv3的配置文件是cfg/yolov3.cfg这个文件定义了整个网络结构。做单类行人检测需要改的地方主要有这几处第一个是filter数量。YOLOv3有三个不同尺度的输出层每个输出层前面有一个卷积层它的filter数量计算公式是3 * (classes 5)。原来COCO是80类所以filter是3 * (80 5) 255。现在只有1类filter就变成3 * (1 5) 18。在cfg文件里搜索filters255会找到三处分别在yolo层前面的卷积层全部改成filters18。第二个是classes数量。每个yolo层里都有classes80这一行同样有三处改成classes1。注意yolo层里的classes和上面卷积层的filters是配套的改了一个不改另一个会直接报错。第三个是anchor box。YOLOv3默认的anchor是基于COCO数据集聚类出来的针对的是80类通用物体。行人这个类别有比较明显的形状特征——通常偏瘦高宽高比一般在0.3到0.5之间。如果直接用默认anchor检测效果可能不是最优的。我建议用自己的行人数据重新跑一遍k-means聚类得到适合行人的anchor尺寸。具体做法是用darknet自带的anchor计算工具或者自己写个k-means脚本输入所有行人框的宽高聚成9类然后按面积从小到大排序分别填入三个yolo层的anchors参数里。我实测下来行人数据的anchor大概在(12,28)、(20,45)、(30,68)、(45,95)、(60,130)、(85,180)、(120,250)、(170,340)、(250,480)这个范围附近具体数值会因数据集不同而略有差异。用自定义anchor比默认anchor的mAP能提升2到3个百分点这个投入是值得的。第四个是输入尺寸。width和height默认是416如果你的图像里行人普遍比较小可以改成608甚至更大但显存占用和推理时间会相应增加。我一般先用416跑通流程再根据实际效果决定要不要调大。3.3 类别文件与数据配置文件在data目录下新建一个person.data文件内容如下classes 1 train /path/to/custom_person/train.txt valid /path/to/custom_person/val.txt names /path/to/custom_person/person.names backup /path/to/backupperson.names文件里只有一行person。这个文件定义了类别名称索引从0开始和标注文件里的类别索引对应。实操心得backup目录一定要提前创建好Darknet在训练过程中会定期把权重文件保存到这个目录。如果不创建训练到第一次保存的时候会直接报错退出前面跑的进度就白费了。我踩过这个坑当时跑了两个小时才发现心态差点崩了。3.4 预训练权重的使用与微调策略YOLOv3官方提供了在COCO上预训练的权重文件yolov3.weights做迁移学习的时候强烈建议加载这个权重。虽然COCO里没有专门的行人类别实际上COCO是有person类的但底层特征提取器学到的边缘、纹理、形状等通用特征对行人检测同样有效。加载预训练权重后模型收敛速度会快很多通常几千次迭代就能看到不错的效果从零开始训练的话可能要几万次。加载预训练权重的命令是在训练命令后面加-clear参数但要注意-clear会清除所有层的学习状态包括卷积层。如果你只想让检测头重新学习可以不加-clear这样Darknet会尝试加载所有匹配的层只有维度不匹配的层比如最后的分类卷积层会重新初始化。我一般先用-clear跑一轮看看loss下降情况如果下降太慢再去掉-clear。4. 训练过程与核心环节实现4.1 训练命令与参数设置训练命令的基本格式是这样的./darknet detector train data/person.data cfg/yolov3-person.cfg yolov3.weights -gpus 0,1-gpus 0,1表示用两张GPU训练如果只有一张就写-gpus 0或者不写。多卡训练的时候Darknet会自动做数据并行batch size会平均分配到每张卡上。比如你设置batch64两张卡各处理32张。学习率的设置很关键。YOLOv3默认的初始学习率是0.001在cfg文件里搜索learning_rate就能找到。如果是加载预训练权重做微调0.001是合适的。如果是从零开始训练可以适当调大一点比如0.01但要注意观察loss如果出现震荡或者发散就赶紧调小。学习率的衰减策略在cfg里也有默认是steps400000,450000scales.1,.1意思是到40万次和45万次迭代的时候学习率乘以0.1。对于行人检测这种相对简单的任务我一般会把steps提前比如改成steps20000,25000让学习率更早降下来帮助模型收敛。4.2 训练过程的监控与日志解读训练启动后终端会不断输出日志每一行代表一次迭代的信息。关键字段有这几个Region 82 Avg IOU表示当前batch在三个尺度上的平均IOU这个值越高说明预测框和真实框的重合度越好一般训练到后期能到0.7以上。Class是分类准确率Obj是目标置信度No Obj是无目标置信度。count是召回率avg是平均损失。我通常关注两个指标一个是avg损失它应该随着迭代次数增加而稳定下降如果出现突然飙升可能是学习率太大或者数据有问题。另一个是Region 82 Avg IOU它应该逐渐上升并趋于稳定。如果IOU一直上不去可能是anchor设置不合理或者标注格式有问题。训练过程中Darknet每隔一定迭代次数默认是1000次会保存一次权重到backup目录文件名类似yolov3-person_1000.weights。同时还会保存一个yolov3-person_last.weights这个是最新的权重。如果训练中断了可以用last.weights恢复训练命令和之前一样只是把预训练权重换成last.weights。4.3 训练时长与硬件资源估算训练时长取决于硬件配置、图像数量和迭代次数。以我自己的经验为例用一张显存11G的卡图像数量大约3000张输入尺寸416batch size设为64大概每1000次迭代需要15到20分钟。要训练到一个比较理想的效果通常需要2万到3万次迭代算下来就是5到10个小时。如果用两张卡时间可以缩短到一半左右。显存占用方面416输入尺寸、batch64的情况下单卡大概需要8到10G显存。如果显存不够可以把batch调小比如改成32或者16但batch太小会影响Batch Normalization的效果可能导致训练不稳定。另一个办法是把输入尺寸降到320显存占用会明显减少但小目标的检测效果会打折扣。注意训练过程中如果看到loss变成nan第一时间检查学习率是不是太大了第二检查标注文件里有没有坐标超出0到1范围的异常值。我遇到过一次是因为某张图像的标注里xmax小于xmin转换之后宽度变成负数导致loss直接爆炸。后来在转换脚本里加了一个abs()和边界裁剪就再也没出现过这个问题。4.4 模型推理与效果验证训练完成后用训练好的权重做推理测试./darknet detector test data/person.data cfg/yolov3-person.cfg backup/yolov3-person_final.weights test.jpgDarknet会在终端输出检测到的行人数量和置信度同时弹出一个窗口显示带框的图像。如果是在没有图形界面的服务器上跑可以加-dont_show参数结果会保存到predictions.jpg文件里。验证模型性能的时候我一般会看两个指标mAP和实际视觉效果。mAP的计算可以用Darknet自带的valid命令./darknet detector valid data/person.data cfg/yolov3-person.cfg backup/yolov3-person_final.weights这个命令会在results目录下生成每个类别的检测结果文件然后用官方提供的reval_voc.py脚本计算mAP。行人检测的mAP在VOC2012验证集上我跑出来的结果大概在0.75到0.82之间具体取决于训练迭代次数和anchor设置。如果mAP低于0.7大概率是训练不够充分或者anchor不合理可以针对性调整。实际视觉效果方面我重点关注三种场景单人清晰场景、多人重叠场景、小目标行人场景。单人场景一般没问题置信度都在0.9以上。多人重叠场景容易出现漏检尤其是人与人遮挡比较严重的时候。小目标行人如果输入尺寸太小也容易漏。针对这两种情况可以考虑用更大的输入尺寸或者在训练数据里增加这类难例。5. 常见问题与排查技巧实录5.1 训练不收敛或loss震荡这是最常见的问题之一。表现是loss在某个值附近上下跳动或者干脆不下降。原因通常有三个学习率太大、batch size太小、数据标注有问题。排查顺序建议从学习率开始把初始学习率调小一个数量级试试比如从0.001降到0.0001。如果还不行检查batch size如果小于16考虑增大或者用梯度累积的方式模拟大batch。最后检查标注文件随机抽几张图像的txt文件看看坐标是不是都在0到1之间有没有负数或者超过1的值。还有一个容易被忽略的原因是预训练权重和当前网络结构不匹配。如果你改了网络结构比如增加了层或者改了filter数量加载预训练权重的时候可能会报错或者静默跳过某些层。这时候可以先用-clear参数从头训练几轮等loss开始下降后再加载预训练权重。5.2 检测框偏移或尺寸异常检测框整体偏移最常见的原因是坐标转换时没有做1-based到0-based的转换。VOC的坐标是从1开始的YOLO是从0开始的差一个像素。虽然一个像素的偏差在大多数情况下看不出来但如果图像尺寸很小比如100x100一个像素的偏差就比较明显了。另一个原因是图像在预处理时被resize了但标注没有同步缩放。YOLO训练时会自动把图像缩放到cfg里指定的尺寸标注也会相应缩放所以这个问题一般不会出现除非你自己在数据增强环节做了额外的裁剪或缩放而没有同步更新标注。检测框尺寸异常比如框特别大或者特别小通常是anchor设置不合理。如果所有检测框都偏大说明anchor的宽高设置得太大可以按比例缩小。反之亦然。用k-means重新聚类anchor是最直接的解决办法。5.3 显存不足的应对方案显存不足的报错信息通常是CUDA out of memory。解决办法有几个按优先级排序第一减小batch size这是最直接有效的。第二减小输入尺寸从416降到320或者288。第三使用subdivisions参数把batch分成更小的子批次每次只加载一部分到显存里。比如batch64、subdivisions16相当于每次只处理4张图像但梯度累积的效果和batch64是一样的。第四如果以上都不行只能换显存更大的卡或者用多卡训练。实操心得subdivisions这个参数很实用但它会稍微降低训练速度因为增加了CPU和GPU之间的数据传输次数。如果显存够用尽量把subdivisions设小一点比如subdivisions8或者subdivisions4训练效率更高。5.4 常见问题速查表问题现象可能原因排查方法解决方案loss变成nan学习率过大、标注异常检查学习率、抽查标注文件调小学习率、修正标注loss不下降学习率过小、预训练权重未加载查看日志中权重加载信息调大学习率、加-clear重训mAP偏低anchor不合理、训练不足用k-means重新聚类anchor替换anchor、增加迭代次数检测框偏移坐标转换错误检查转换脚本做1-based到0-based转换显存不足batch或输入尺寸过大查看nvidia-smi减小batch、减小输入尺寸推理速度慢输入尺寸过大、模型未优化测试不同输入尺寸降低输入尺寸、使用TensorRT5.5 提升检测效果的经验技巧除了上面说的anchor优化和输入尺寸调整还有几个技巧可以提升行人检测的效果。第一个是数据增强。YOLO默认开启了随机缩放、平移、翻转等增强但对于行人检测我建议额外增加随机裁剪和色彩抖动。随机裁剪可以模拟行人被部分遮挡的情况色彩抖动可以提升模型对不同光照条件的鲁棒性。第二个是难例挖掘。训练完一轮后用模型在验证集上跑一遍把漏检和误检比较严重的图像挑出来手动检查标注是否正确然后把这些图像加入到训练集中重新训练。这个迭代过程通常能带来明显的性能提升。第三个是模型集成。如果你有多个训练好的权重可以在推理时把它们的结果做加权融合虽然推理速度会变慢但mAP通常能提升1到2个百分点。6. 从训练到部署的延伸思考训练出一个mAP不错的模型只是第一步真正要落地到实际场景中还有不少工作要做。推理速度是一个硬指标Darknet原生的推理在GPU上跑416尺寸大概能到30到40FPS但如果要部署到边缘设备上就需要用TensorRT或者OpenVINO做推理优化速度可以提升2到3倍。模型量化也是一个方向把FP32量化成INT8模型体积缩小到四分之一推理速度也能提升但精度会有轻微下降需要根据实际场景权衡。另一个值得关注的点是输入分辨率的选择。如果应用场景里行人离摄像头比较远小目标居多那就需要更大的输入尺寸比如608甚至832。但输入尺寸增大带来的计算量是平方级增长的608的计算量是416的2倍多832是416的4倍。所以需要在精度和速度之间找到一个平衡点。我的做法是先用小尺寸快速验证方案可行性再根据实际场景的精度要求逐步调大。数据层面的持续迭代也很重要。VOC2012的行人数据虽然质量不错但场景多样性有限主要是日常生活场景缺少夜间、雨雾、拥挤等复杂条件。如果模型要部署到这些场景就需要补充相应的数据。可以自己采集和标注也可以用一些公开的行人检测数据集做补充训练。持续迭代数据是提升模型实际表现最有效的手段没有之一。最后说一个我踩过的坑训练好的模型在验证集上mAP很高但实际部署后效果差很多。排查了很久才发现验证集的图像和实际场景的图像在色彩分布上有明显差异验证集偏亮实际场景偏暗。后来在训练数据里加入了一些暗光图像并做了针对性的色彩增强问题就解决了。这个经历告诉我验证集的选择一定要贴近实际应用场景否则mAP再高也是自欺欺人。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑