遮挡行人检测实战:CUHK数据集标注转换与YOLO训练优化
2026/9/15 5:22:44 网站建设 项目流程

简介:基于CUHK Occlusion Dataset整理的行人检测数据集,面向目标检测学习者与研究者,已完成YOLO格式的训练集与验证集划分,并同时提供YOLO和VOC两种标注格式,可直接用于YOLO系列模型训练、验证及效果对比。压缩包共2000个文件,主要包括jpg图像、txt标注、xml标注等类型,包体约315.63MB;其中jpg为遮挡场景下的行人图像,txt为YOLO格式的标签文件,xml为VOC格式的标签文件,满足不同检测框架或脚本的数据读取要求,也便于用户根据自己的项目需要灵活选用。目前已有593人学习下载。内容上,除完整数据集和双格式标注外,还附带了参数化编程思路清晰、注释详尽的辅助说明与代码,方便调整批次大小、类别数量、输入尺寸等训练参数,并可参考作者对数据划分、路径配置和格式转换的处理经验;适用于计算机、电子信息工程、数学等专业学生的课程设计、期末大作业与毕业设计,对有遮挡行人检测、模型微调或数据格式转换需求的读者尤其有参考价值。

1. 遮挡场景行人检测:2125张标注图像里的边界情况

在目标检测任务里,密集人群中的相互遮挡一直是漏检率最高的场景——两个重叠的行人经常被模型合并成一个框,甚至置信度低的那一个直接被NMS阈值干掉。CUHK Occlusion Dataset这个行人数据集,收集的就是这类遮挡样本,2125张图像全部带yolo和VOC两种格式的标注文件,train/val划分也已经做好,解压之后可以直接喂给yolo跑训练。这个数据集的量不算大,但它的价值在于专门炮制“难例”:适合做yolo目标检测课程设计、期末大作业,也适合想验证自己的yolo改进算法(比如改进NMS、换注意力模块)能不能扛住遮挡场景的工程师。下面直接拆文件结构、标签格式和训练配置。

2. 从VOC的XML到YOLO的txt:标注格式的血缘关系

2.1 两种格式的字段映射

解压压缩包后,Annotations目录里是VOC格式的XML文件,labels目录里是yolo格式的txt文件。先打开一个XML看一下核心结构,文件名和压缩包里的资源描述一致,比如set00_set06-occ_309.xml

<annotation> <folder>JPEGImages</folder> <filename>set00_set06-occ_309.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>person</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>310</xmax> <ymax>350</ymax> </bndbox> </object> </annotation>

这里只有person一个类别,所以name字段始终是person;bndbox里存的是绝对像素坐标,左上角是(xmin, ymin),右下角是(xmax, ymax)。每一对<object>标签对应图像里的一个行人目标,遮挡样本里同一张图会有多个object,并且它们的框互相重叠。

对应的yolo格式txt内容长这样:

0 0.335938 0.447917 0.296875 0.562500

五个字段含义分别是:class_idx_centery_centerwidthheight。第一个0是类别索引,因为只有person一类,所以所有行的第一个字段都是0;后面四个值全部是归一化坐标,范围在0到1之间。

2.2 为什么yolo要做归一化而不是直接用绝对坐标

VOC里存绝对像素坐标,yolo里存归一化坐标,这不是风格差异,而是训练机制决定的。yolo在训练时会把输入图像resize到一个固定尺寸(比如640x640),不同原始分辨率的图像被拉伸或填充后,如果用绝对坐标,所有标注框的位置和大小都要跟着重新计算;而归一化坐标是相对图像宽高的比例值,resize前后保持不变,省掉了这一层换算。

提示:归一化的另一个好处是让不同分辨率的图像在标注层面可比。CUHK Occlusion Dataset里图像分辨率不完全一致,存绝对坐标的话,标注数值的分布会被图像尺寸干扰,模型的回归头学起来会更吃力。

2.3 写一个VOC转YOLO的Python脚本

格式转换的逻辑和边界情况可以通过一段脚本说清楚:

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) yolo_lines = [] for obj in root.iter('object'): cls_id = 0 # 本数据集只有person一类,索引固定为0 xmin = float(obj.find('bndbox/xmin').text) ymin = float(obj.find('bndbox/ymin').text) xmax = float(obj.find('bndbox/xmax').text) ymax = float(obj.find('bndbox/ymax').text) x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h yolo_lines.append( f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}" ) base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(output_dir, base + '.txt'), 'w') as f: f.write('\n'.join(yolo_lines))

逻辑说明:root.find('size/width')取的是原始图像宽度,必须用原图的宽高,不能用resize后的值;x_center先取像素中心的平均值,再除以图宽得到归一化坐标,box_w是框宽的像素值除以图宽。四个坐标值保留6位小数,yolo训练脚本是读字符串后转float的,保留位数太少会导致框回归的目标值被截断,训练时box_loss降不下去。

提示:如果转换后txt里的坐标出现负数或大于1的值,说明XML里的标注框越界了,或者原图宽高读的是resize后的值。yolo训练遇到这种标签会直接跳过对应图像,表现就是loss正常但mAP始终很低。

2.4 YOLO转VOC:反向转换的一个关键细节

反过来把txt转成XML时,最容易踩的坑是坐标越界。yolo的归一化值乘回图像宽高后,x_center + width/2计算出的xmax可能超过图像宽度,贴近图像边缘的目标尤其容易出现。转换时要加一个clamp操作:

def clamp_to_bounds(value, low, high): return max(low, min(value, high))

这个压缩包自带的标签本身是配对的,一般不需要自己转格式。但如果后续用LabelImg增补了样本,导出的XML和原有yolo标签混在一起,这个转换函数就能派上用场。

2.5 标签字段对照速查

字段VOC格式YOLO格式说明
类别<name>person</name>第一个值0索引从0开始计数
框中心X(xmin+xmax)/2第二个值归一化到 [0,1]
框中心Y(ymin+ymax)/2第三个值归一化到 [0,1]
框宽xmax-xmin第四个值除以图像宽度
框高ymax-ymin第五个值除以图像高度

3. 数据集划分与yolo训练配置

3.1 解压后的目录结构整理

压缩包里的图像和标签目录结构需要整理成yolo训练框架能够识别的标准布局。以yolov5/yolov8工程为例,建议整理成如下结构:

dataset/ ├── images/ │ ├── train/ # 训练集图像 │ └── val/ # 验证集图像 ├── labels/ │ ├── train/ # 训练集标签(txt) │ └── val/ # 验证集标签(txt) └── data.yaml

资源描述里提到“做好了yolo格式数据集的训练和划分”,说明train/val的划分结果已包含在压缩包中。需要注意一个硬性约束:图像文件与标签文件必须同名,set00_set06-occ_309.jpg对应的标签必须是set00_set06-occ_309.txt,扩展名不同,前面的文件名主体要完全一致,多一个字符或少一个字符都会导致训练脚本跳过该样本。

3.2 data.yaml的写法与参数坑

在数据集根目录下创建data.yaml

train: ./images/train val: ./images/val nc: 1 names: ['person']
  • trainval的路径建议写成绝对路径。训练脚本的工作目录经常与数据集不在同一级,相对路径会触发AssertionError: train: ... does not exist,排查这个问题比训练本身更浪费时间。
  • nc是类别数,本数据集只有person一类,必须写1。写大了不会立刻报错,但训练时如果遇到txt中不存在的类别索引,会在dataloader阶段崩溃。
  • names列表的索引顺序必须和标注文件里的第一个字段对应。这里names[0]是person,而所有txt里第一个字段都是0,两者匹配才正确。

3.3 用yolov8启动训练

这里以yolov8为例,因为它的命令行接口最简洁,数据格式兼容yolo格式的标签:

yolo train data=data.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640

参数在这个场景下的取舍逻辑:

  • model=yolov8n.pt表示加载nano版本的预训练权重。2100多张的样本量对参数量较大的模型来说很容易过拟合,用nano做迁移学习,收敛速度和最终精度更均衡;等验证集mAP涨不动了,再换yolov8s.pt对比一次即可。
  • imgsz=640是该数据集的合理选择。图像源分辨率不高,640的输入尺寸既能保留遮挡区域的细节,又不至于因letterbox填充过多空白区域。
  • batch=16依赖单卡显存,8GB显存建议降到8,否则会在第一个epoch因OOM中断。

3.4 训练日志中的关键指标

训练过程中的标准输出:

Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 99 3.1G 0.7812 0 0.6255 4 640

box_loss是定位损失,cls_loss几乎为0,因为只有一类且大部分图像的标签数量不多。dfl_loss是yolov8的分布焦点损失,控制框边界预测的质量。每个epoch结束后还会输出mAP50mAP50-95两项指标,其中mAP50是对遮挡目标最敏感的指标——IoU阈值0.5在重度遮挡下已经算宽松,这个值上不去,说明模型根本没学到遮挡目标的特征。

提示:如果训练到20个epoch时box_loss仍在0.9以上不降,优先检查标签坐标而不是调超参数。从val集里随机抽三张图,把txt坐标还原成像素框画在原图上,看看框是否贴合行人轮廓。

4. 遮挡行人场景的训练难点与调优策略

4.1 标签框重叠带来的训练信号冲突

CUHK Occlusion Dataset的特点在名字里就写了:occlusion。两张标签框的面积交并比明显高于普通街景数据集,这意味着:

  • 分类分支没问题,模型需要识别出“这里有人”。
  • 回归分支的挑战在于两个重叠框的中心点可能非常接近,特征图上同一个位置要输出两个差异很大的框尺寸。
  • NMS阶段,两个置信度都比较高的重叠框,会在后处理时被合并掉一个,导致漏检。

训练这个数据集,本质是在调教模型处理“目标不完整但必须识别”的边界情况。

4.2 推理阶段NMS参数针对性调整

训练完成后做预测,默认的iou=0.45对遮挡场景过于激进。两个行人的框IoU超过0.45时,后处理会认为它们是同一个目标,直接丢弃置信度较低的那个。调整方式:

yolo predict model=runs/detect/train/weights/best.pt source=val/images iou=0.3 conf=0.2

把NMS的IoU阈值降到0.3,两个重叠框被合并的门槛变高,同时保留它们的机会变大。代价是误检会增加,需要再通过conf阈值来平衡。conf=0.2属于较低的置信度门槛,风格是“宁可多检不可漏检”,适合行人检测这种漏检代价高的场景。

4.3 数据增强的参数选择

yolov8默认开启mosaic、scale、flip等增强策略,针对遮挡数据集需要做两处调整:

增强参数默认值建议值理由
scale0.50.3过大的随机缩放会把本来就遮挡严重的目标缩小到难以辨识
translate0.10.2轻微平移可以模拟更多遮挡位置分布
fliplr0.50.3左右翻转在密集场景会生成过度反常的重叠形态
mosaic1.00.8保持开启,但关小概率,预留更多真实遮挡样本参与训练

还有一个容易被忽略的增强手段是copy-paste:把一张图中的行人裁剪后随机粘贴到另一张图上,人为制造更多遮挡组合。这个功能在yolov8里默认关闭,开启方式是在训练参数里加copy_paste=0.3

4.4 预训练模型 vs 从头训练

COCO预训练权重里已经有person类别的特征响应,用它做初始权重比随机初始化收敛快得多,而且val的mAP上限更高。COCO里包含大量完整行人的样本,预训练权重对行人整体的纹理、形状先验是有积累的;这个数据集的微调目标是在“被挡住一半甚至只露出头肩”的条件下唤醒这些特征。

如果换用model=''从头训练,在2125张图上通常要跑到250个epoch以上才能接近预训练权重的效果,且结果不稳定,不同随机种子下的mAP波动可能超过5个点。这里的数据量决定了迁移学习是更稳的选择。

4.5 验证效果时用什么指标判断调优生效

model.val()拿到mAP50mAP50-95precision/recall之后,额外关注一个容易忽略的数字:recall。遮挡场景下,模型宁可多框出来几个候选也不要漏人,所以调优的目标不应该是“mAP最大”,而应该是在mAP50下降不超过2个点的前提下,把recall拉上去。每次改动NMS阈值、增强策略或损失权重后,都跑一遍val(),记录recall数值。

5. 推理验证与常见报错排查

5.1 对验证集做逐张推理

训练阶段看到的mAP是批量统计的结果,遮挡样本的表现还要落在具体图像上看。写一个推理脚本,对验证集里的所有图像逐张输出检测结果,同时打印置信度分布:

from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict( source='dataset/images/val', conf=0.2, iou=0.3, save=True, save_txt=True ) conf_values = [] for r in results: for b in r.boxes: conf_values.append(float(b.conf[0])) print(f"检测框数量: {len(conf_values)}") print(f"置信度均值: {sum(conf_values)/len(conf_values):.3f}") print(f"置信度最小值: {min(conf_values):.3f}")

这段代码的作用是量化检测结果的分布特征。如果置信度均值在0.5以下,说明大部分预测框都处于模棱两可的状态,模型对遮挡目标的特征学习还不够;如果小于0.2的预测框数量占比超过20%,说明背景误检偏多,需要回调conf或者加数据清洗。

5.2 标签格式引发的两类典型报错

第一类是图像与标签数量对不上。解压后的压缩包里如果混入了未标注的图像,yolo训练时不会主动跳过,而会在loss计算阶段报一条指向空列表的索引错误。处理方式是用脚本核对:images目录的jpeg数量应当等于labels目录的txt数量。

第二类是标签内容与yaml配置不匹配。比如某个txt文件里出现了1开头的行,而nc: 1,训练会在数据加载阶段直接崩溃,报错信息里会带上label ... is out of bounds的字样。遇到这种问题,不要急着改yaml的nc值,先定位是哪个文件的行首数字超过了本数据集的唯一类别0。

5.3 框太小训练不收敛的补救

CUHK Occlusion Dataset中的部分样本,行人距离远导致目标框面积占整张图的比例极小。yolo对这类小目标的回归能力天然偏弱,补救方案是调低输入图像resize时的降采样倍数,或者将imgsz从640提到960。框在特征图上占据的像素变多,回归头能学到的东西才会变多。显存不够时,优先降低batch而不是降imgsz。

5.4 保存推理结果到本地

save=True会把带预测框的图像输出到runs/detect/predict目录,但遮挡场景的对比还需要原始标注框叠加的效果,这时利用压缩包里的VOC格式XML:

python draw_gt.py --xml Annotations --img JPEGImages --out gt_vis

把XML还原成方框画在原图上,再与yolo预测输出对比,能直观看出哪些遮挡目标被漏检,是NMS阈值压制的问题,还是模型在训练时就没有拟合到该样本。这个环节在翻车排查时最省时间,不用盲改参数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询