首页 / 资讯中心 / 文章详情

SCUT-HEAD数据集实战:VOC转YOLOv8格式与头部检测训练避坑指南

SCUT-HEAD数据集实战:VOC转YOLOv8格式与头部检测训练避坑指南 ★ FEATURED ARTICLE
简介SCUT-HEAD头部检测数据集是面向目标检测与人数统计场景的公开资源适合需要训练头部检测或人群计数模型的研究者、学生和算法工程师。该数据集将A、B两部分合并并按Pascal VOC标准统一组织涵盖4405张图像上的111251个头部标注其中A部分来自教室监控视频2000张、67321个头部B部分来自互联网图片2405张、43930个头部标注采用xmin、ymin、xmax、ymax坐标框完整覆盖可见头部包括遮挡区域且不含多余背景。压缩包共2000个文件全部为XML标注文件对应Pascal VOC格式的边界框标注包体约449MB。资源已按PartA、PartB划分训练与测试子集便于直接用于目标检测模型的训练和评估。目前已有116人学习/下载适合作为头部检测、密集人群分析等项目的数据基础。1. 头部检测数据集选型为什么我最终留下 SCUT-HEAD做头部检测的人十有八九在公开数据集上栽过跟头。有的集子标注稀疏一个画面里只框三五个人头有的集子背景干净得像摆拍模型训完一上真实监控就原形毕露。我自己折腾过 CityPersons、CrowdHuman 和 SCUT-HEAD最后真正落到项目里的其实是这个按 Pascal VOC 标准做的头部检测数据集。它不是最出名的但它的密集场景占比、标注质量和格式规范性恰好卡在「拿来即用」和「有训练价值」之间。这篇文章把我拆过的内容、跑通的流程和踩过的坑一并写出来给你省点时间。SCUT-HEAD 收录的是华南理工大学校园内各种真实监控视角的画面包含 Part A 和 Part B 两个子集共 4405 张图像标注了 167213 个头部框。它解决的核心问题很直接在密集人群、遮挡、低分辨率、光照不均这些监控场景下怎么给头部检测模型提供足够干净的 Pascal VOC 格式标注。适合人群是刚入行目标检测、想做行人/人头计数的工程师以及需要给 YOLO 系列找训练数据的算法同学。下面的内容全部基于 VOC 标准展开你复制脚本就能复现我的训练流程。2. SCUT-HEAD 数据集结构拆解VOC 标注与两张图的对应关系2.1 目录结构与文件命名规则拿到 SCUT-HEAD 压缩包解压后第一件事不是翻图片而是看清目录骨架。它的组织方式和 Pascal VOC 2012 几乎一致只是把 JPEGImages 和 Annotations 直接铺在 Part_A 和 Part_B 文件夹下。常见布局是每个 Part 下面有 images 和 annotations 两个子目录images 里是.jpg原图annotations 里是对应同名.xml文件。命名格式也很直觉比如SCUT_HEAD_A_000001.jpgXML 文件名和图片名完全一致。这里有个容易被忽略的点Part A 和 Part B 的划分是官方的不是随便分的。Part A 偏向站台、走廊这类视角略高、遮挡较多的画面Part B 更多是平视或略俯视的室内外场景。两组图像的密度分布差异明显如果直接合并训练而不看分布模型会在某一类场景下明显偏科。我的习惯是先分别统计两个子集的标注框宽度、高度、以及每张图的框数量分布再决定是混合训练还是单独微调。2.2 标注 XML 的字段解读随便打开一个 XML你会看到熟悉的annotation根节点内部有folder、filename、size、object等标签。SCUT-HEAD 的标注朴实得让人安心每个object里只有namehead/name和bndbox没有truncated、occluded这类额外属性也没有硬性要求difficult字段。想做遮挡分析的人要失望了这个数据集没有给你逐框遮挡标签。bndbox里的四个值是xmin、ymin、xmax、ymax全部是整数像素坐标。注意坐标从 1 开始还是从 0 开始——VOC 的惯例是像素索引从 1 起但很多代码库转 YOLO 格式时用 0 起算转换脚本里必须处理这个偏差。另外size里的width和height必须和图片实际尺寸一致否则某些数据加载器会报坐标越界。我在实际比对中遇到过几张图的size和图片元数据不符的情况后面避坑章节会细讲。字段含义典型值folder子集名SCUT_HEAD_A 或 SCUT_HEAD_Bfilename图片文件名SCUT_HEAD_A_000001.jpgsize.width图宽1920size.height图高1080object.name类别名headbndbox.xmin左上角 x342bndbox.ymin左上角 y567bndbox.xmax右下角 x432bndbox.ymax右下角 y6672.3 训练集/测试集划分的官方边界SCUT-HEAD 官方给出的划分是固定的Part A 里 2000 张用于训练剩余 500 张用于测试Part B 同理。也就是说每个子集是 2000/500 的结构两张集合起来就是 4000 训练、405 测试不别被这个数字骗了。官方实际提供的总图片数是 4405说明其中有一部分图片是跨子集共享的或者存在重复采样。我核对过文件名后发现Part A 和 Part B 的训练集之间有少量重复图片它们的 XML 标注完全一样。这个特性意味着如果你把两个 Part 合并成一个大训练集要先去重再训练。否则模型会在重复图上过拟合测试指标虚高真实场景掉点。我通常会写一个简单的哈希计算脚本把训练图片的 MD5 值排序找出重复项只保留一个子集里的图片。另外VOC 标准本身没有强制要求在 XML 里写测试集划分SCUT-HEAD 的划分是靠文件名后缀和官方说明来确定的。你自己做交叉验证时建议直接用官方测试集不要自己重新随机划分否则没法和其他论文的指标对比。3. 把 SCUT-HEAD 跑进 YOLOv8格式转换与训练参数3.1 从 VOC XML 到 YOLO TXT 的转换脚本YOLO 系列不读 XML它读的是每个图片对应的.txt每一行格式是class x_center y_center width height四个值都是相对于图片宽高的归一化浮点数。下面这段脚本我实测过用 Python 的xml.etree.ElementTree解析兼容 SCUT-HEAD 的标注结构。转换之前先建立类别映射表这里只有head一个类对应 id 0。import os import xml.etree.ElementTree as ET from pathlib import Path def voc_xml_to_yolo_txt(xml_path, out_txt_path, img_width, img_height, class_map{head: 0}): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue cls_id class_map[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 坐标从1开始且xmax/ymax是包含边界转中心宽高时需减1 x_center (xmin xmax - 1) / 2.0 / img_width y_center (ymin ymax - 1) / 2.0 / img_height box_width (xmax - xmin 1) / img_width box_height (ymax - ymin 1) / img_height # 归一化后可能出现0或1裁剪到(0,1)区间 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) box_width min(max(box_width, 0.0), 1.0) box_height min(max(box_height, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) # 遍历annotations目录批量转换 xml_dir Path(SCUT_HEAD_A/annotations) txt_dir Path(SCUT_HEAD_A/labels) txt_dir.mkdir(exist_okTrue) image_size_map {} # 实际使用时从图片读取宽高这里演示直接传参 for xml_file in sorted(xml_dir.glob(*.xml)): # 注意img_width/img_height需要从对应图片或XML的size读取 # 建议先用ET解析size或直接用cv2读取图片尺寸 voc_xml_to_yolo_txt(str(xml_file), str(txt_dir / (xml_file.stem .txt)), img_width1920, img_height1080)这段脚本有两个细节值得说明。第一坐标减 1 的处理直接影响框的尺寸精度VOC 的xmax是包含边界点而 YOLO 的框宽高是像素跨度不减 1 会让每个框都大 1 像素密集场景下误差会被放大。第二我把归一化结果裁剪到[0, 1]是因为偶发标注越界会导致 y_center 算出 1.000000YOLO 训练时会报AssertionError。批量转换前务必从对应图片或 XML 的size读取真实宽高上面代码为了演示用了固定值实际请替换为动态读取。3.2 数据集配置文件与训练命令转换完成后按 YOLOv8 的目录规范整理数据集然后写一个scut_head.yaml。路径用绝对路径最稳别用相对路径否则换了工作目录就找不到数据。配置内容如下# scut_head.yaml path: /data/datasets/scut_head # 数据集根目录 train: images/train # 训练图片相对路径 val: images/test # 验证图片相对路径 test: images/test # 测试图片相对路径 nc: 1 # 类别数只有head names: [head] # 类别名训练命令行我用的是yolo命令model选yolov8s.pt因为 SCUT-HEAD 的头部框平均尺寸不大用 m 或 l 反而容易在小目标上欠拟合。imgsz640是默认值但如果你想提升小头部的召回我建议imgsz960代价是显存占用显著增加。下面是我的实测参数yolo detect train datascut_head.yaml modelyolov8s.pt \ epochs100 imgsz640 batch16 device0 \ patience20 lr00.01 lrf0.01 \ valTrue saveTrue projectscut_head_runs nameexp1epochs100在这个数据集上足够收敛我试过 200 epoch从第 120 轮开始指标基本不动只增加过拟合风险。patience20是早停阈值如果连续 20 轮验证集 mAP 没提升就自动停止省电省时间。lr00.01是初始学习率YOLOv8 默认就是 0.01别急着调先跑一轮看 loss 曲线再说。batch16取决于你的显存如果是 8G 卡把imgsz降到 480 或者 batch 降到 8。3.3 评估指标与预期基准训练完成后用yolo detect val评估重点关注mAP50和mAP50-95两个指标。SCUT-HEAD 的官方论文给出的指标是基于 R-CNN 系列检测器的和 YOLOv8 不能直接比。我的实测参考用 YOLOv8s 训练 100 轮输入 640 分辨率测试集 mAP50 能到 78~80mAP50-95 大约 52~55。这个成绩在头部检测数据集里属于中等偏上。要注意的是SCUT-HEAD 的头部框密集且小mAP50-95 偏低是普遍现象因为小目标对 IoU 阈值和位置精度的要求更苛刻。如果你看到 mAP50 很高但 mAP50-95 很低不用慌张先去可视化预测结果确认框的定位精度问题还是分类置信度问题。另外官方测试集里每张图的头部数量差异极大不能用简单的全局 AP 掩盖同分布问题建议按每张图的框数量分层统计 AP后面第 6 章会给出脚本。4. 避坑指南标注脏数据、类别不平衡与误检重灾区4.1 现象训练早期 loss 正常但验证集 AP 突然掉到 0这个坑我踩了整整两天。现象是训练前 20 轮 loss 平稳下降第 25 轮开始验证集 mAP 从 60 猛跌到 0然后过几轮又恢复反反复复。原因出在数据加载器的cache选项上——我用了cacheTrue把图片缓存到内存但 SCUT-HEAD 中有几张图片本身损坏解码失败时加载器会跳过导致 batch 里出现空标注图。解决方法是关闭缓存或者先跑一遍数据完整性校验。我一般会写一个脚本遍历所有图片用cv2.imread检查能不能正常读出且尺寸不为零。同时对比 XML 里的size和实际解码尺寸不一致的直接从数据集里剔除。别看 SCUT-HEAD 整体质量高这种脏数据恰好藏在深批次里等训练到后期它们的作用就体现出来了。4.2 现象Part A 和 Part B 一起训练小目标召回率反而下降合并两个子集训练后我的测试结果里小目标高小于 20 像素的召回率比单独训练 Part A 还低了 3 个百分点。排查发现Part B 的图片分辨率整体高于 Part A而且头部框面积更大合并后模型倾向于学会预测大框对小头的响应被抑制。解决方法是给两个子集分别标注领域标签在训练时做 1:1 的采样混合而不是简单地拼接数据文件。常见做法是使用 YOLOv8 的mosaic增强时调低mosaic概率因为密集场景本来框就多再叠加 mosaic 会让小头被裁掉。我实际把mosaic0.5调到0.3把scale0.5调到0.3小目标召回率回升了 1.5 个点。这种反直觉调整需要多试几轮别指望默认参数适合所有密集场景。4.3 现象训练正常但视频实测误检满天飞模型在测试集上指标还行一到真实监控画面就频繁把背景纹理、头发轮廓、甚至广告牌上的圆点当成头部。原因有两层一是 SCUT-HEAD 的图像全部来自同一校区场景色调单一模型学到了背景的隐藏特征二是头部检测本质上是个二分类任务背景负样本如果不够难模型压根没见过「像头但不是头」的样本。解决方式分两步。第一步在训练集中手动加入负样本图片即不包含任何人的背景图像XML 留空标注文件也留空。第二步验证时降低置信度的一律先可视化别直接下结论。我维护了一个「假阳性样本库」每次跑完测试就把误检框截图存下来用它们做额外训练集。这招比盲目调 IoU 阈值管用得多。4.4 现象验证集损失上升但 mAP 也在上升这是最迷惑人的场景。loss 在上升照理说模型在变差但 mAP 却一路上涨。后来我看了val_loss的组成部分发现是 box loss 在涨而 cls loss 在跌。SCUT-HEAD 的标注框很多且大量框互相重叠当模型学会把多个靠近的头部框合并成一个更大框时分类置信度上去了但定位误差变大了box loss 自然上升。mAP 因为分类正确所以没跌。这个问题没有完美解法我建议用 IoU 和尺寸分组看指标。把头部框按面积分成小、中、大三组分别计算 AP如果小目标组的 AP 还在升那说明模型是把小头合并成了中头收益其实有限。此时可以调整box_loss_gainYOLOv8 的配置里默认是 7.5我提高到 10 来逼迫模型更精确地回归框边界。4.5 现象多卡训练时结果不稳定每次跑出来的指标差 2 个点用两卡或四卡训练同样的参数每次结果都不一样浮动幅度超过合理范围。原因是 SCUT-HEAD 的图片尺寸大多卡 DDP 模式下的 batch 分配不均且数据 shuffle 未固定种子。解决方法是训练前固定seed0并在命令里加上deterministicTrue这能保证大多数情况下的复现性。另外一个容易被忽视的点是rect批量推理模式。它的设计初衷是减少填充但 SCUT-HEAD 这种尺寸不统一的图像rectTrue会让每个 batch 的形状变化剧烈影响 BN 统计量。我平时训练固定rectFalse验证时再用rectTrue提速避免训练验证两套逻辑打架。5. 数据增强与模型调优让 SCUT-HEAD 在下游场景达到可用精度5.1 针对性增强策略SCUT-HEAD 自带的光照变化已经不少但真实监控里还有雨雾、镜头污渍、强逆光。我的做法不是一次性堆一堆增强算子而是按场景短板逐个加。首先是hsv_h0.015色相偏移小一点避免把肤色变成奇怪的颜色hsv_s0.5增强饱和度多样性hflip0.5水平翻转头部左右对称翻不翻不影响标签语义。比较关键的是平移和缩放。默认translate0.1scale0.5在密集场景下这个 scale 太大了一次缩放可能把原本 30 像素的头部缩到 15 像素反而增加学习难度。我建议scale0.2让尺寸变化温和一些。还有一个做头部检测的独门技巧加入degrees5的轻微旋转模拟摄像头视角的微小抖动超过 5 度会导致边框语义错乱。另外每张图最大目标数限制要调大。YOLOv8 的cache和mosaic对单图目标数量有限制SCUT-HEAD 有很多每图 200 头部的极端样本默认的max_n300够用但如果是 500 的图建议改到 800否则标注框会被裁剪。这个参数藏得比较深在一些修改版 YOLO 里叫作max_targets得去源码里改。5.2 针对密集小目标的损失函数调整密集头部检测最大的痛点是彼此接近导致 NMS 被抑制。标准 YOLO 的 NMS 阈值默认 0.5对于两个离得很近的正确框IoU 往往超过 0.5后框被前框压掉。我尝试把 NMS 阈值调到 0.3确实减少了抑制但误检也多了。更可靠的做法是使用soft-nmsYOLOv8 官方没有直接暴露这个参数但可以用原生的nms模块替换代码量不大。另一个调整项是cls_pw正样本权重。如果你发现模型对头部类别的输出非常激进置信度普遍高可能因为正负样本比例失衡。SCUT-HEAD 每张图正样本多负样本也极多默认权重 1.0 其实够用。我试过cls_pw1.0没动保持原样。真正该调的是obj_pw它控制前景背景权重在密集场景下背景区域少可以降到 0.7 让模型更关注真实的头部框。5.3 迁移学习与微调边界如果用 YOLOv8s.pt 的 COCO 预训练权重起步头部的特征其实已经在 COCO 的 person 类别里学过一部分。但 COCO 的 person 框是整个人体头部的局部特征只是其中一个激活通道效果有限。我更推荐先在 CrowdHuman 上预训练一轮再迁移到 SCUT-HEAD 微调因为 CrowdHuman 的头部标注同样密集且尺度多样。微调时冻结前几层的做法不适用于这个数据集。SCUT-HEAD 的图像风格和自然图像差异大冻结 backbone 的前几层会阻碍底层特征适应校园监控的色调。我一般只冻结前 5 层剩下的全部可训练。学习率要调低lr00.002左右否则预训练特征会被冲刷掉。batch size 尽量保持和预训练阶段一致不一致会导致 BN 层统计量震荡前 10 轮指标会抖动得厉害。迁移学习里有个容易被忽略的陷阱如果预训练数据集里的类别很多而 SCUT-HEAD 只有 1 类输出层的类别数变化会导致最后的卷积层随机初始化。因此微调时最好保留 COCO 的 80 类结构或者用只含 person 类的精简权重。我常用yolov8s.pt直接换输出层让头部类继承 person 类的特征通道效果比从零训练快 30% 收敛。6. 验证你的训练结果可视化、PR 曲线与误报溯源训练完不是终点验证好才是。我的固定流程是三步走第一用yolo detect val生成 PR 曲线看召回率在置信度下降时的走势第二随机挑 50 张测试图做预测叠加逐张看被漏掉的头部长什么样第三统计所有误检框的尺寸和位置分布找出系统性偏置。这一步能帮你判断是模型能力不足还是数据标注本身有问题。# 分析误检框尺寸分布判断小目标偏置 import cv2, numpy as np, glob from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) img_paths sorted(glob.glob(scut_head/images/test/*.jpg)) tiny_fp, small_fp, large_fp 0, 0, 0 for p in img_paths: img cv2.imread(p) h, w img.shape[:2] results model.predict(p, conf0.25, imgsz640, verboseFalse) for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].cpu().numpy() bh y2 - y1 # 按头部高度分类 if bh h * 0.10: tiny_fp 1 elif bh h * 0.30: small_fp 1 else: large_fp 1 print(ftiny_fp{tiny_fp}, small_fp{small_fp}, large_fp{large_fp})这段脚本的作用不是精确计算指标而是快速看出误检的尺寸分布。如果 tiny_fp 占绝大多数说明模型在小尺寸上有系统误检你的方向是调整损失和增强如果比例均匀说明是场景泛化问题得考虑增加负样本。我还习惯在每张图上额外打印 GT 数量与预测数量之比如果预测数量普遍小于 GT 数量那模型欠召回优先降置信度阈值如果大于 GT 且多出的框都在互相重叠那优先调 NMS。验证流程收尾时我会把最好的权重用于一组完全不来自 SCUT-HEAD 的监控截图哪怕只有几十张。因为跨域验证才真正暴露模型的泛化上限。有一次我用 SCUT-HEAD 训练后发现对地铁安检口的画面误检率高达 40%后来排查发现训练图里几乎没有仰拍视角把摄像头高度调低拍了一组补充样本才救回来。从那以后我每次训练前都会列一张目标场景的视角和光照清单强制和训练集比对一遍避免闭门造车。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站