ARTICLE DETAIL

资讯详情

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

SCUT-HEAD头部检测数据集详解:VOC转YOLO训练与小目标调参实战

SCUT-HEAD头部检测数据集详解:VOC转YOLO训练与小目标调参实战 简介SCUT-HEAD是面向行人检测、人群计数与监控场景分析的大规模头部检测数据集适合计算机视觉学习者和科研人员用于训练、验证目标检测模型。资源将原PartA与PartB合并并统一整理为Pascal VOC标注格式便于直接接入常见检测框架。压缩包内共2000个xml注释文件对应2000张图像的头部边界框标注每个可见头部均以xmin、ymin、xmax、ymax坐标标记同时覆盖被遮挡部分整体包体449.03MB。完整数据包含4405张图像与111251个头像标注其中PartA来自教室监控视频抽帧PartB来自互联网图片两部分均含训练与测试划分。当前已有116人学习下载适合用于数据预处理、模型微调以及检测精度对比实验。1. SCUT-HEAD 头部检测数据集Pascal VOC 标准为什么值得下载复现SCUT-HEAD 头部检测数据集Pascal VOC 标准是我见过最适合快速验证小目标检测流程的资源之一。它把头部检测做成了单类 VOC 任务图片来自真实监控视角标注只有 head 一类目录结构和评估口径全部对齐 Pascal VOC意味着你可以直接用现成的 Faster R-CNN、YOLO、SSD 工具链跑通数据解析、训练、评估的完整闭环不需要像处理 COCO 那样先折腾 json。适合两类人一类是正在做行人检测、人群计数、安防巡检的工程师另一类是刚开始接触目标检测、想找一个干净数据集练手的学生。这份资源真正解决的是两件事——让你有一个能立刻开训的头部检测训练集同时提前暴露小目标检测里那些让人头疼的坑。2. 数据集结构与 VOC 标准解析从目录到标注全看懂2.1 目录结构JPEGImages、Annotations 与 ImageSetsPascal VOC 标准的数据集解压之后目录结构是固定的。SCUT-HEAD 既然按这个标准组织那你拿到压缩包后的第一件事就是确认这套目录是否完整。典型结构如下SCUT-HEAD/ ├── Annotations/ # 每个 .xml 对应一张图片的标注信息 ├── JPEGImages/ # 图片文件jpg 格式文件名与 xml 严格对应 └── ImageSets/ └── Main/ ├── train.txt # 训练图片 id 列表 ├── val.txt # 验证图片 id 列表 └── test.txt # 测试图片 id 列表我一般会先跑一句tree或者用 Python 统计三件事jpg 数量、xml 数量、txt 里的行数。这三者一旦对不上后面训练时就会出现“图找不到标注”或者“标注落空”的怪问题。最常见的翻车是 JPEGImages 里多了几张没标注的图或者 ImageSets 里写了一个并不存在的文件名这类脏数据不提前清掉训练到一半才爆出来就很被动。这个结构带来的直接好处是工具链成熟。torchvision 自带VOCDetectionDatasetdetectron2 也提供了 VOC 格式的注册接口连数据增强的 pipeline 都是现成的。你不需要为这个数据集单独写一套数据加载逻辑把路径指过去就能跑。这也是为什么我推荐新手第一次做小目标检测时优先选 Pascal VOC 标准的数据集而不是 COCO——少处理一个标注格式精力就能全部放在模型和参数上。2.2 解析 XML 标注size、object 与 bndbox 字段VOC 的标注文件是 XML核心字段就那么几个。SCUT-HEAD 里每个 object 对应一个头name 固定是 headbndbox 给出左上角和右下角的像素坐标。典型的一条标注长这样annotation filenameIMG_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namehead/name bndbox xmin340/xmin ymin210/ymin xmax372/xmax ymax238/ymax /bndbox /object /annotation一张图里有多少个人头就会有多少个object。理解这个 XML 结构是后面所有工作的基础尤其是size的宽高它决定了你从像素坐标换算到归一化坐标时的分母填错一位整个训练标签就全偏了。写一个快速解析脚本把每张图的标注信息打印出来是最有效的熟悉方式import xml.etree.ElementTree as ET def inspect_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text width int(root.find(size/width).text) height int(root.find(size/height).text) print(f图片: {filename} 尺寸: {width}x{height}) for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) print(f 类别: {name} 框: ({xmin:.0f}, {ymin:.0f}) - ({xmax:.0f}, {ymax:.0f})) inspect_voc_xml(Annotations/IMG_001.xml)解析逻辑很简单先定位 filename 和 size再循环所有 object 取出 bndbox。参数说明里有一个细节值得注意——bndbox的坐标是像素值且是浮点数字符串直接float()转才不会丢精度。如果某张图的框数量明显异常比如一张教室全景图只有 1 个头而另一张只有一个学生的图有 10 个头这种不均衡就是后面训练时 mAP 波动的根源提前做到心里有数。2.3 只有 head 一类为什么反而更难训练单类检测听起来比多类简单但头部检测是个例外。原因是头部目标天然具有三个让模型头疼的特征小、密、多遮挡。监控画面里一个正常身高的人头部在 1080p 画幅下通常只有 20 到 40 个像素宽属于典型的小目标人群聚集时头部密集排列相互遮挡严重很多标注框里其实只有半张侧脸或一顶帽子的轮廓再加上俯视、逆光、暗光这类监控场景常有的变量头部检测的难度并不比行人检测低。这带来一个直接结果通用检测模型在 COCO 上跑得好不代表在 SCUT-HEAD 上也能直接拿到理想效果。ResNet 骨干的下采样倍率、特征金字塔的层级、anchor 的尺寸设置每一样都要针对“小目标”这个特点重新校准。所以这个数据集最好的用法不是用它来证明某个新模型的 mAP 有多高而是把它当作一面镜子照出你对小目标检测的处理是否真的到位。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑3.1 写一个转换脚本XML 解析与坐标归一化虽然 VOC 格式本身成熟但 YOLO 系列训练时用的是自定义的 txt 标签格式每一行是class x_center y_center width height坐标全部归一化到 0 到 1。SCUT-HEAD 只有 head 一类转换逻辑相对简单直接遍历 Annotations 目录逐个处理即可import xml.etree.ElementTree as ET import os # 类别映射只有 head 一类id 从 0 开始 CATEGORIES {head: 0} def convert_voc_to_yolo(xml_path, out_txt): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CATEGORIES: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 过滤掉宽高为 0 或反向框 if xmax xmin or ymax ymin: continue x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 防止归一化越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(w, 1.0) h min(h, 1.0) lines.append(f{CATEGORIES[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines)) xml_dir Annotations out_dir labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): stem os.path.splitext(xml_file)[0] convert_voc_to_yolo( os.path.join(xml_dir, xml_file), os.path.join(out_dir, stem .txt) )这段脚本的核心逻辑就三步读size拿到宽高做分母、读bndbox取出像素坐标、把左上右下格式换算成中心点加宽高。换算公式是x_center (xmin xmax) / 2 / img_w宽度用(xmax - xmin) / img_w两个分母千万别搞混——一个是图片宽度一个是标注框本身的宽。3.2 边界坑一类别 id 从 0 开始还是从 1 开始这是所有 VOC 转 YOLO 的人最容易踩的第一个坑。VOC 官方评估工具的类别 id 通常从 1 开始因为 0 预留给背景但 YOLO 系列训练时的 class id 一定从 0 开始。很多网上的迁移脚本把 head 写成了 1结果训练时nc1图片里唯一的类别却是“1”轻则标签越界报错重则模型把背景也当一个类去学loss 直接震荡。我一般会在转换脚本开头写死一张映射表比如CATEGORIES {head: 0}并且转换完成后抽查几个 txt确认第一行第一个数字只有 0 没有 1。如果你后续要接 Detectron2 或者 MMDetection还要注意它们内部对 VOC 类别的 id 偏移各不相同每换一个框架就重查一次映射不要想当然。3.3 边界坑二脏标注框过滤与画框目检校园监控场景的标注里偶尔会出现一些非常规的框xmin 比 xmax 还大、宽高为 0、或者框的中心点落在了图片范围之外。这些脏数据在 VOC 格式下可能不影响画图但转成中心点宽高后轻则产生负值坐标重则越界导致训练程序崩溃。所以我建议转换时直接过滤掉宽高不大于 0 的框这一步不要省。过滤完只是第一步更重要的是一眼可见的校验。画框目检是成本最低、效果最好的验证方式import cv2 def draw_yolo_boxes(img_path, txt_path): img cv2.imread(img_path) h_img, w_img img.shape[:2] with open(txt_path) as f: for line in f: parts line.split() if len(parts) 5: continue cls, x_c, y_c, w, h parts x_c, y_c, w, h map(float, (x_c, y_c, w, h)) x1 int((x_c - w / 2) * w_img) y1 int((y_c - h / 2) * h_img) x2 int((x_c w / 2) * w_img) y2 int((y_c h / 2) * h_img) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(check_ txt_path.split(.)[0] .jpg, img) draw_yolo_boxes(JPEGImages/IMG_001.jpg, labels/IMG_001.txt)画框脚本的逻辑是把归一化坐标反算回像素坐标然后直接叠加到原图上。我会随机抽 30 到 50 张图目检重点看两种表现框是否紧贴头部轮廓以及有没有框明显错位。框比头大一圈是正常的因为标注本身就有裕量但框整体平移了十几个像素那就要回去查归一化时分母是不是写错了。3.4 边界坑三空 txt 该删还是该留转换过程中有一类文件很容易被忽略XML 里确实存在该图但里面没有有效 object转出来的 txt 就是空的。有些训练框架遇到空标签文件会直接报错有些则会跳过这张图行为不一致。我的习惯是保留空 txt但单独写一个清单把这类图列出来训练时用不到它们可排查时这些“没有标注的图”反而是检查原始数据遗漏的好线索。如果你追求训练速度也可以直接把对应的图片一起过滤掉但那样会影响你对原始数据分布的判断。另一个容易被忽略的小坑是路径分隔符。Windows 下os.listdir出来的路径是反斜杠直接拼接到训练配置里有时会转义出错我习惯在拼接后用os.path.normpath统一成 Windows 风格配置文件里再写死正斜杠避免训练时读到错误的训练路径。4. 训练配置与模型选型小目标头部检测的调参思路4.1 模型选型YOLO 系与两阶段检测器怎么选头部检测不是越新的模型越好而是要看目标尺寸和算力预算。SCUT-HEAD 这种场景下主流的做法是优先试试 YOLO 系尤其是 YOLOv8 和 YOLO11因为它们对新手最友好训练、验证、导出一条龙且自带数据增强。两阶段的 Faster R-CNN 在小目标召回上通常略好但训练和推理都慢不少适合对精度要求高于实时性的场景。我自己常用的选型权衡如下模型小目标召回训练成本推理速度适用场景YOLOv8 / YOLO11中等偏上配 P2 层后更好低高监控实时分析、边缘部署Faster R-CNN好尤其 20px 以下的小头高低离线分析、精度优先不要求实时RT-DETR / DETR 系好但收敛依赖数据量中高中有较多训练数据、想省掉 anchor 的阶段这张表不是绝对结论但能帮你快速定方向。我的习惯是小头占比高、又要求实时就选 YOLO 系并打开浅层特征融合如果只是离线评估一个算法上界Faster R-CNN 更稳妥。SCUT-HEAD 这类监控数据集里小头比例通常不低所以选 YOLO 系时要重点关注下采样倍数。4.2 关键参数imgsz、mosaic、anchor 与学习率训练参数是最值得折腾的部分也是翻车高发区。几个关键参数我逐个说imgsz直接决定小目标能不能被看到。我建议训练分辨率从 640 起步如果显存够用直接上 768 或 1024。头部只有二三十像素时640 的分辨率下采样 32 倍后只剩不到 1 个像素特征都没了谈何检测。分辨率调高后漏检明显减少但训练时间和显存占用同步上涨需要平衡。mosaic增强在小目标场景下要特别小心。YOLO 默认开启 mosaic把四张图拼在一起缩放到训练尺寸这个操作对大目标无所谓但会把原本就小的头部进一步缩小甚至缩到几个像素标签噪点被放大训练效果反而变差。常见做法是前 50 个 epoch 打开 mosaic 增加样本多样性后 20 个 epoch 关掉 mosaic 做精调让模型在小目标细节上收敛得更干净。anchor的设置取决于框架。YOLOv8 之后 anchor 是模型自适应学习的不需要手动算但如果你用 YOLOv5 或两阶段模型就需要用 k-means 在训练集的真实框上重新聚类算 anchor。头部框的比例相对固定宽高比集中在 0.8 到 1.2 之间聚类结果通常就是几个接近正方形的框和 COCO 的默认 anchor 差异很大硬用 COCO 预训练默认值收敛会又慢又差。学习率这边我一般从lr0.01起步配合 5 个 epoch 的 warmupbatch size 翻倍时学习率也跟着翻倍。到了训练后期把学习率降到 0.0001 左右做精调可以让头部边缘的置信度预测更准确。4.3 数据划分固定随机种子按场景隔离训练集和验证集的划分质量直接决定你看到的 mAP 是不是真实水平。如果 SCUT-HEAD 资源里带了官方的 ImageSets 划分那就直接用官方划分别自己折腾。但如果没有自己划分时要格外小心一个隐蔽问题数据泄露。同一个摄像头同一时间段拍出的画面如果同时出现在训练集和验证集里模型相当于“见过”验证图mAP 虚高是必然的。我的做法是固定随机种子并且尽可能按场景分组划分。比如把所有图片先按来源场景分桶再从每个桶里抽取同样比例进验证集保证验证集能看到训练集没见过的场景。示例脚本from sklearn.model_selection import train_test_split # image_ids 是图片文件名列表scenes 是场景标签列表 train_ids, val_ids train_test_split( image_ids, test_size0.15, random_state42, stratifyscenes # 按场景分层采样 ) with open(ImageSets/Main/train.txt, w) as f: f.write(\n.join(train_ids)) with open(ImageSets/Main/val.txt, w) as f: f.write(\n.join(val_ids))这里stratifyscenes是关键它让验证集里各个场景的占比和训练集保持一致不会出现验证集全是教室、训练集全是走廊这种结构性偏差。random_state42保证你每次划分的结果可复现换台机器重新跑也一样。划分完再统计一次两个集合里的目标数量分布确保不是训练集有 5 万个头、验证集只有 200 个头的失衡局面。5. 避坑排查训练与评估阶段的五个高频翻车点5.1 loss 不降或直接 NaN现象训练 loss 在前几个 epoch 正常下降到某个 batch 突然变成 NaN或者从第一个 epoch 开始就纹丝不动。原因大概率在两个地方一是标签文件的类别 id 越界类别数设为 1 但标签里出现了 1二是学习率过高batch 内梯度爆炸。解决先检查 txt 标签每行的第一个数字是否小于nc再查看 loss 变化曲线找到崩掉的 epoch把学习率从 0.01 降到 0.001 重新跑。这也是我每次换数据集必做的两项检查。5.2 小头漏检严重现象验证集里几十上百个头的图片上模型只检测出五六个且全是大头小头集体漏检。原因是特征金字塔底层没有保留足够的小目标语义信息或是训练分辨率太低。解决办法按优先级排列先加大imgsz到 768 以上再看模型是否输出了 P2 层的检测头YOLO 的 P2 层专门服务 8x8 特征图上的小目标很多默认配置是关掉的。如果显存不允许大分辨率退一步用切图推理——把一张大图切成四块分别检测再把结果合并坐标实测能明显找回小头。5.3 mAP 很高但现场效果差现象验证集 mAP 跑到 0.85拿到真实监控视频里一测漏检和误检都让人看不下去。原因几乎都是数据划分出了问题训练集和验证集内容高度同源验证集的漏检率被人为压低。解决回看 4.3 节强制按场景隔离划分。另一个隐蔽原因是验证时置信度阈值设得太低mAP 计算时会把低置信度的框也算进去但实际应用时你不可能容忍满屏的误检框所以训练时同样要关注mAP50对应的高置信度区间表现。5.4 画框偏移resize 与 letterbox 的坐标错位现象训练时用可视化工具查看验证集预测结果框整体向右下角偏移了十几像素或者被拉伸变形。原因是在数据增强阶段用了强制 resize 而不是 letterbox——原图被拉伸之后真实框的坐标没有做同步变换。解决训练配置里把强制 resize 关掉改用 letterbox 等比缩放并填充灰边。YOLO 系框架默认处理了 letterbox 的 padding 补偿但如果你自己写数据加载一定要把pad_w、pad_h加回坐标换算里。贴心提示验证脚本里用模型原始的letterbox参数去还原坐标别手写一遍容易漏掉奇数像素的 padding。5.5 显存溢出与训练中断现象batch size 一加大显卡直接 OOM训练中断。原因就是显存不够但显存不够不一定要降低 batch size那样会影响收敛稳定性。我的做法是保持 batch size 不变改用梯度累积把有效 batch size 拆成多个小 batch梯度累加若干步后再更新权重。同时开启半精度训练显存占用直接砍半多一点速度还能提上去。还有一个容易被忽略的点如果框架开了 cache 或 RAM 缓存把图片预加载进内存显存也会跟着涨内存吃紧时关掉 cache 改回读盘问题立刻缓解。6. 从检测框到人群计数把模型接到实际问题上的两个技巧6.1 用检测结果直接做区域计数拿到训练好的模型最简单的落地方式就是把检测框数量当作人数。监控场景下模型输出的一帧结果里有多少个 head 框基本就等于这一帧画面里有多少人。关键是要设置一个合理的置信度阈值并在同场景里保证阈值一致不然不同时段的结果没法横向对比。示例如下results model(frame, verboseFalse)[0] boxes results.boxes.xyxy.cpu().numpy() conf results.boxes.conf.cpu().numpy() valid boxes[conf 0.4] # 阈值按实际场景调 0.3~0.5 count len(valid) print(f当前画面人数: {count})这段代码的逻辑是先过滤低置信度框再统计剩余框数量。实际使用中如果你要统计的是某个排队窗口或闸机区域的人数把每张图的检测框中心点坐标取出来判断它是否落在目标区域内分区域计数即可。这个阈值 0.4 不是固定值建议在有标注的测试集上扫一遍 0.3 到 0.5 区间找到精度和召回平衡的那个点。6.2 按测试图分组看 PR 曲线找出模型短板我养成的习惯是训练完不只看总 mAP而是把测试集按图片特点分组分别计算精度。分组维度通常有两个单头图和多头图以及大头图和小头图。你会发现多头的密集人群图和小头图往往是拉低整体 mAP 的主力而这个问题在总 mAP 上是看不出来的。具体做法是把测试集的推理结果按图片保存成 JSON然后用 Python 脚本按人头数量或平均框尺寸分桶每个桶单独画 PR 曲线。如果你的模型在“小于 20 像素的头”这一桶上召回率特别低就该回去调分辨率或特征融合策略而不是盲目改全局参数。之前我差点被总 mAP 的 0.86 骗过去以为模型已经够好结果去支撑一个教室场景的实时拥挤度统计漏检率直接导致计数结果失真。从那以后我每次跑头部检测都会强制走一遍“分组看 PR 曲线再谈参数”的流程希望帮到你。本文还有配套的精品资源点击获取
返回列表