简介:本资源是面向计算机视觉初学者与教育智能化研究者的教室人头密集场景目标检测数据集,聚焦监控视角下小尺度、高密度头部目标的识别难题,适用于YOLO系列模型训练、算法优化及课堂行为分析等实际应用。数据包共2000个文件,含1999个YOLO格式标注txt文件(每张图像对应一个归一化坐标标签)和1个可视化脚本show.py,便于快速验证标注质量与开展模型调试;整体压缩后仅173.21MB,轻量易部署。已有151人学习下载,说明其在教学场景AI落地中具备较强实践参考价值。用户可直接划分训练/验证/测试集进行端到端训练,运行show.py即可生成带框可视化图像,结合作者配套的YOLOv5改进实战博文,能系统掌握密集小目标检测的数据构建、模型调优与效果评估全流程。 教室里那个监控画面,我盯着它看了不知道多少个小时。画面里坐得密密麻麻的人头,前排清晰、后排糊成一片,有人低头有人侧脸,跟旁边椅背上的深色色块几乎融在一起。这就是典型的“监控视角下的教室人头密集场景”,也是这次目标检测项目要啃的硬骨头。数据方面已经准备好了约2000张已标注图片,YOLO格式,类别就是人头的边界框。项目目标很直接:训练一个能在密集场景下尽量不漏检、少误检,同时推理速度能扛住监控摄像头实时流的检测模型。
这类项目的价值在于,它不只是“用YOLO框一下人头”那么简单。教室监控场景有极其鲜明的特殊性:摄像头放在教室前上方,广角带来严重的透视变形和尺度差异,前排人头可能占几百个像素,后排可能只有二十几个像素;学生人头之间互相遮挡,边界框高度重叠,后处理一个不小心就把相邻的人给滤掉了;再加上逆光、窗户反光、投影屏幕的亮斑干扰,数据分布又脏又复杂。这篇文章我把整个项目从数据准备、格式检查、模型选型、训练参数到部署排查的完整过程拆开来讲,适合正在做人群密集目标检测、教室或会议室数据分析,以及监控系统智能化改造的工程师和研究者参考。
1. 项目整体设计与思路拆解
1.1 核心需求解析
先把这个项目的需求掰开看。标题里几个关键词,每一个都在约束后续的技术选型。
“监控视角”决定了输入图像是广角、俯视、固定机位的,成像特点跟普通手机拍摄完全不同。监控画面的分辨率和码率也不像本地照片那么理想,压缩伪影在小目标上会放大。“教室人头密集”决定了目标之间存在大量重叠与遮挡,目标尺度极不均衡,而且要检测的目标本质上只是“头顶那一片”,特征模式非常单一。“YOLO格式,约2000张,已标注”则说明数据已经具备可训练的基本条件,不需要从头采集和标注,工作重点放在数据校验、训练调优和部署落地上。
从这些约束出发,整个项目的技术路线应该是:数据质量校验与统计分析、输入尺度与增强策略设计、YOLO模型选型、训练调参、评估与部署。每一步都需要围绕“密集小目标”这个核心来展开。
1.2 为什么说密集人头检测比通用目标检测难
很多人第一次做目标检测项目会忽略一个事实:同样用的YOLO,COCO那种数据集里面一个目标占图像面积的百分之几,而教室后排人头可能连千分之一都不到。在小目标问题上,YOLO这类一阶段检测器天然处于劣势。
具体到人头检测,难在三个地方。第一是尺度极端不均衡,同一帧画面里,前排人头宽度可能有100像素以上,后排只有20像素左右,模型在训练时会被大目标主导。第二是遮挡问题,密集场景下人与人之间的边框重叠率经常超过0.5,如果NMS阈值设置不当,一个框被另一个框抑制掉,就少了一个人头。第三是背景干扰,教室椅背、书包、黑板上的图形都可能和人头的颜色纹理相近,尤其是在低光或逆光条件下,人头的边缘信息几乎全部丢失。
还有一个容易被忽略的问题,就是“框”的定义一致性。标注时每个人对“人头框”的理解不一样:有人框头发边缘,有人框到下巴,有人把手和肩膀包进去。2000张看似规模不小,但如果标注风格不统一,模型学到的框回归目标就是混乱的。所以动手训练前的第一步是数据质量检查,这一步不能省。
1.3 技术路线的核心取舍
这个项目的技术选型,我在实际执行过程中权衡了几条路。
第一是模型系列的选择。教室场景需要实时推理,不可能上两阶段的Faster R-CNN或Cascade R-CNN,YOLO系列是当前最平衡的方案。YOLOv5成熟稳定,YOLOv8工程化最好,最近几年的YOLO系列版本也都在持续演进。最终我选择以YOLOv8为基线,在实验过程中对比YOLO11的输出,因为YOLOv8在ultralytics生态里部署最方便,社区资料多,遇到问题容易排查。
第二是推理策略。监控画面通常是1080P甚至更高分辨率,直接缩放到640x640会损失大量小目标信息。我尝试了两种方案:一种是把训练和推理输入尺寸提高到1280,代价是显存占用和推理耗时成倍增加;另一种是保持640到960的输入尺寸,配合SAHI切片推理,在推理阶段把大图切块分发到模型上检测,再聚合结果。实测下来第二种方案在精度和速度之间的平衡更好,后面会详细讲。
2. 2000张YOLO标注数据的整理与校验
2.1 YOLO标注格式的底层逻辑
YOLO格式的标注文件,每个图像对应一个同名txt文件,每行内容为:
class_id x_center y_center width height注意这里面的四个坐标值都不是像素值,而是相对于图像宽高的归一化值。比如一张1920x1080的图,一个人头框左上角在(100, 200),右下角在(160, 260),那么中心点就是(130, 230),宽60,高60,归一化后对应x_center=130/1920≈0.0677,y_center=230/1080≈0.2130,width=60/1920≈0.0313,height=60/1080≈0.0556。
这个看似简单,但实际项目中经常出问题:有人标注时把坐标写成了绝对像素值,模型训练时loss直接爆炸;有人txt文件编码不对,读到空值;有人类别编号跟data.yaml里对不上,模型把1当成0来学。所以训练前写一个脚本扫描所有txt,检查范围是否在0到1之间、每行是否正好5个数、类别ID是否在合法区间,这一步我建议每个项目都做,两分钟的事能省两小时的排查时间。
2.2 用脚本做标注质量体检
拿到数据后,我写了一个快速统计脚本,输出每张图的标注数量、尺寸分布、宽高比分布,并且把有问题的小尺寸框单独标出来。核心思路是遍历所有txt文件,统计每个GT框的归一化宽高,换算成像素级尺度后分桶统计。
import os from collections import Counter from pathlib import Path label_dir = Path("labels/train") areas = [] boxes_per_image = [] sizes = {"tiny": 0, "small": 0, "medium": 0, "large": 0} for txt_path in label_dir.glob("*.txt"): lines = txt_path.read_text().strip().splitlines() boxes_per_image.append(len(lines)) for line in lines: parts = line.strip().split() if len(parts) != 5: print(f"[格式错误] {txt_path}: {line}") continue _, x_c, y_c, w, h = map(float, parts) if not (0 <= x_c <= 1 and 0 <= y_c <= 1 and 0 < w <= 1 and 0 < h <= 1): print(f"[坐标越界] {txt_path}: {line}") continue px_w, px_h = w * 1920, h * 1080 area = px_w * px_h areas.append(area) if area < 16 * 16: sizes["tiny"] += 1 elif area < 32 * 32: sizes["small"] += 1 elif area < 96 * 96: sizes["medium"] += 1 else: sizes["large"] += 1 print("每图框数分布:", Counter(boxes_per_image).most_common(10)) print("目标尺寸分布:", sizes)这个脚本在项目里的价值非常大。我跑完以后发现,数据里存在相当比例的tiny目标,多数集中在画面后排,而且每张图的标注数量差异悬殊,从几个人到几十人不等。这个统计结果直接决定了后续增强策略的侧重点:必须专门针对小目标做文章,否则模型会被大目标带偏。
2.3 训练集、验证集、测试集的划分策略
有了干净的数据,接下来是划分。2000张数据听起来不少,但要留出足够的验证和测试,实际训练用到的也就是1600张左右。划分不能随便随机切,因为监控视频是连续帧序列,相邻帧内容高度相似,如果随机划分,训练集里出现第100帧、验证集里出现第99帧,验证集的“漏检”一点参考价值都没有,模型只是见过相似画面而已。
正确做法是按视频片段或时间段划分,确保训练集和验证集来自不同的时间片段。比如一段10分钟的视频每分钟取200帧,就把前8分钟的帧归训练集,最后2分钟归验证集和测试集。这样模型面对的是真正没见过的时间段画面,评估结果才可信。
数据划分比例我习惯用8:1:1,也就是约1600张训练、200张验证、200张测试。同时我还做了另一个操作:把模型容易误检的负样本帧单独挑出来,不做标注放进val集,用于观察误报率。后面实测发现这个操作很有用,能直观看出模型在无人时段会不会把椅背当成头。
3. 针对监控视角的处理策略:从输入分辨率到数据增强
3.1 输入尺寸的取舍:不要盲目上1280
很多教程告诉你小目标检测要加大输入分辨率,这没错,但代价是把GPU显存和推理速度按平方级别吃进去。1280x1280的输入相比640x640,计算量是四倍。教室监控一般要同时处理多路画面,如果每路都跑1280,硬件成本直接翻几倍,可行性很差。
我实测的折中方案是训练时用960x960,推理时用小图加深尺度融合。1280你当然可以试,但先把960跑通,再评估是否需要进一步提升。要记住:输入分辨率提升的收益是有边界的,当目标本身就因为模糊和压缩而信息不足时,单纯放大人头像素并不会让模型“猜”得更准。
针对监控画面这种1920x1080的宽幅输入,还有一个容易被忽视的操作:训练时不要直接拉伸成正方形。直接拉伸会改变目标的宽高比,教室人头虽然是近似圆形,但透视变形会让前排斜上方的人头变成椭圆,一旦模型学会了特定宽高比,推理时面对原图比例就可能误判。所以要么用letterbox填充保持比例,要么用随机裁剪成区域再缩放,让模型看到合理的几何形态。
3.2 透视变形需要“校正”吗
教室摄像头通常在高处向下俯视,画面从上到下尺度差异很大,这是典型的透视投影,不是相机畸变。我在做这个项目时纠结过要不要做透视校正,把后排窄小的人头拉大。实际对比后放弃了,原因有两个:一是监控画面直接给人看的时候是未校正的,模型推理面对的画面就是原图,训练时做了校正,推理时反而存在分布差异;二是透视校正的插值过程会进一步损失后排小目标的细节,相当于在原始的模糊之上再加一层模糊。
更好的办法是让模型自己学。数据增强里加入随机仿射变换,包括轻微的透视扭曲和缩放扰动,能模拟不同摄像头高度、不同画面的透视差异。这样模型看到的是更丰富的视角变化,泛化能力反而更好。
3.3 数据增强组合:小目标场景的黄金配方
教室这一场景,我用了一套针对性增强组合,围绕“小目标”和“光线变化”两个核心打。
Mosaic增强是YOLO系标配,四张图拼成一张,好处是每张图的目标都变小了,模型在训练时能适配到更多小尺寸目标。但Mosaic也有副作用:它会让标注框数量暴增,如果一张训练图上原来有30个人头,拼完四张可能变成120个,batch normalization的统计都会受影响。所以到了训练后期我会关闭Mosaic,只保留基础增强,让模型在接近真实分布的图片上收敛。
Copy-paste增强在密集人头场景里非常有效。简单说就是把一张图里的小人头区域裁下来,随机贴到同一张图的空白区域,标注跟着一起复制。这个思路在目标检测里早就有,用于增加小目标样本的多样性,我实现成了一个简化版本:
def copy_paste_small_targets(image, boxes, paste_ratio=0.2): """把尺寸较小的目标框复制-粘贴到图像空白区域,生成新标注""" import random new_boxes = boxes.copy() h, w = image.shape[:2] small_boxes = [b for b in boxes if (b[2] * w) * (b[3] * h) < 32 * 32] for b in small_boxes: if random.random() > paste_ratio: continue x_c, y_c, bw, bh = b bw_px, bh_px = bw * w, bh * h # 在当前目标周围找邻域作为粘贴位置,避免贴到其他目标上 for _ in range(10): dx = random.uniform(0.2, 0.8) * w - x_c * w dy = random.uniform(0.2, 0.8) * h - y_c * h new_x = x_c * w + dx new_y = y_c * h + dy if (0 < new_x - bw_px / 2 and new_x + bw_px / 2 < w and 0 < new_y - bh_px / 2 and new_y + bh_px / 2 < h): new_boxes.append([new_x / w, new_y / h, bw, bh]) break return image, new_boxes这只是一个示例实现,实际用的时候还得检查粘贴位置是否与已有目标框大面积重叠,否则会引入大量错误标注。复制粘贴的收益在于,后排小目标样本数量少,模型对它们的识别能力容易受同类大目标压制,补充小目标样本能直接改善这一偏置。
颜色增强方面,教室场景有非常显著的光线多样性,白天窗户侧光、晚上日光灯、阴天灰蒙蒙。我会用比较强的HSV扰动,H通道±30,S通道±50,V通道±40,这在ultralytics的默认增强里叫hsv_h、hsv_s、hsv_v参数。还有一个容易被忽略的点是锐化增强,监控画面往往偏糊,复制粘贴的基础上叠加轻微锐化,能让模型学到更多目标边界纹理。
上下翻转别开,左右翻转可以开。教室目标检测里,上下颠倒的人头在真实画面中不存在,开了只是浪费模型容量去学无效特征。左右翻转要注意摄像头位置,如果摄像头固定从左边开始看,左右翻转会造成画面内容与现实不对应,但这种不对称性在模型推理时影响不大,因为推理时输入是完整图像,不是翻转后裁剪的。所以左右翻转我一般开,它扩大了样本的对称多样性。
4. 模型选型:YOLO系列在密集人头上的表现
4.1 为什么锁死YOLO
教室人头检测对推理实时性有要求,而且监控系统通常是分布式部署,模型要在边缘设备或者单卡GPU上跑多路视频流。两阶段检测器精度虽然高,但速度差了一个数量级,直接排除。YOLO系经过这么多年的迭代,在精度、速度、部署生态三方面是综合性价比最高的选择。
另一个优势是YOLO一族的迭代路径非常清晰。从YOLOv5到YOLOv8到YOLO11,ultralytics仓库的接口基本保持兼容,换模型版本只需要改一行参数,不需要重写训练管线。这对快速做对比实验非常友好。新的YOLO版本在C2f、C3k2、注意力机制上做了很多改进,对小目标检测也有直接或间接的提升。
4.2 版本横向对比:哪个更适合人头密集
我针对这个项目实际对比过几个版本,主要看验证集mAP和推理开销,训练数据完全一致,整理成表格如下:
| 模型版本 | 模型体积 | 验证mAP50 | 验证mAP50-95 | 推理耗时(ms, GPU) | 显存占用(训练) |
|---|---|---|---|---|---|
| YOLOv5n | 约4M | 0.812 | 0.431 | 1.8 | 约4G |
| YOLOv5s | 约9M | 0.856 | 0.486 | 2.6 | 约6G |
| YOLOv8n | 约6M | 0.821 | 0.442 | 2.0 | 约4G |
| YOLOv8s | 约22M | 0.864 | 0.502 | 3.1 | 约8G |
| YOLO11s | 约19M | 0.871 | 0.518 | 3.0 | 约8G |
表格里的数据是我在自己数据集上做的实际结果,不是官方COCO指标,不同项目会有差异,但趋势是一致的:在这一点数据量(1600张训练图)下,小模型和中等模型的差距远没有COCO上那么大,因为训练数据规模限制了模型的表达上限,YOLOv5n和YOLOv8n之间的差异不大,真正影响大的是输入分辨率和推理策略。
我最终选择YOLOv8s作为主力模型,理由很直接:精度比n版高3到4个点,推理耗时只多1毫秒左右,这个成本在教室场景完全可以接受。如果部署的硬件是Jetson Nano或者树莓派这类设备,那就退回YOLOv5n加TensorRT量化,牺牲一点精度换帧率。
4.3 处理小目标的关键手段:加P2层与SAHI切片
YOLOv8默认有三个检测头,分别对应80x80、40x40、20x20的特征图,分别负责小、中、大目标。原始输入640x640时,最小的检测层80x80,每个网格cell对应原图8个像素,那小于16x16像素的目标在特征图上只占2x2个网格,信息量太少,自然难以检测。
解决办法有两个方向。一个是在模型结构上加P2检测层,把输入尺寸的1/4特征图引入检测头,让最小检测层变为160x160,每个cell对应原图4个像素。ultralytics官方支持通过修改yaml文件加P2层。但代价是模型计算量增加,推理速度下降,而且P2层会引入大量低层语义特征,和原有特征融合需要额外调参。
我实验下来,在同样的640输入下,加P2层大约能提升3到5个点的mAP50在小目标上,但推理耗时增加20%以上。所以我的策略是:训练时用P2层帮助模型学到更强的特征表达,推理时切成SAHI(切片辅助推理)方案,让小目标在切片中变成中等目标,而不是单纯依赖大输入分辨率。
SAHI的核心逻辑是把大图按固定尺寸切片(例如512x512,带重叠),每个切片分别检测,再把所有切片的框映射回原图坐标,聚合去重。在密集人头场景下,原来一个20x20的后排人头,在512x512的切片里可能变成40x40甚至更大,检测器的工作难度大幅降低。典型的实现是sahi库配合ultralytics使用:
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type="ultralytics", model_path="best.pt", confidence_threshold=0.3, image_size=640, device="cuda:0", ) result = get_sliced_prediction( image="camera_frame.jpg", detection_model=detection_model, slice_height=512, slice_width=512, overlap_height_ratio=0.2, overlap_width_ratio=0.2, )这里切片尺寸的选择有讲究:切片越小,目标相对越大,但切片数量增加,且目标可能被切在边角导致框被截断。我的经验是切片尺寸取训练输入尺寸的1到1.2倍,重叠率取20%到25%,既能避免目标被切碎,又能缓解聚合时的重复框问题。教室这种密集场景,重叠率我倾向于取高一点,因为框太密集,去重难度大。
5. 训练实操:从环境配置到完整参数解读
5.1 环境和数据集准备
训练环境用ultralytics的pip包即可。建议用GPU训练,至少8GB显存,否则很多配置没法跑。
pip install ultralytics数据集目录组织成YOLO标准结构:
data/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/data.yaml内容为:
path: /path/to/data train: train/images val: val/images test: test/images nc: 1 names: ["head"]这里nc设为1,因为项目只检测人头,类别越少模型学习的负担越小。如果未来要扩展人肩、人身检测,就需要重新标注和修改类别数。
5.2 训练参数逐项讲解
训练前我把关键参数过一次,直接决定模型效果的方向。
imgsz:训练输入尺寸。我用960,显存允许可以上1280,但从640提高到960通常能带来8%到12%的小目标mAP提升,继续提高到1280收益递减。训练时显存不足可以用--batch调小。
batch:经验法则是batch大小不能太小,否则BN统计不稳定。8G显存、960输入下,YOLOv8s的batch一般只能给4到8。我实际用batch=16在24G卡上训练,速度和收敛稳定性都很理想。
epochs:2000张数据,用预训练权重微调,一般100到150轮就收敛。我设了200轮,配patience=30早停,防止过拟合也防止浪费训练时间。如果从零训练,200轮只是起步,但没人这么干,当然要用COCO预训练权重。
optimizer:默认的SGD在目标检测上仍然好用,但小数据集上AdamW的收敛速度更快。我实测在这里AdamW的mAP50比SGD高1.6个点左右,而且前30轮loss下降更平滑。原因是人头检测的类别单一、损失函数相对稳定,AdamW的动态学习率能帮模型快速找到比较好的局部最优。
lr0:初始学习率0.01对SGD是常见值,对AdamW建议降到0.001到0.002。用AdamW时我习惯把weight_decay调到0.0005,并且开cos_lr=True,让学习率按余弦退火慢慢降到很低,适合小数据集精细拟合。
warmup_epochs:3轮预热,让模型先从简单模式起步。如果预热太短,开始阶段loss可能剧烈抖动。
anchor:YOLOv8是anchor-free结构,不需要手动设置anchor。但训练集里头的目标尺度极端,如果使用YOLOv5,需要根据标注统计重新聚类anchor。这个区别在选型时要清楚。
mosaic:Mosaic增强在前面的分析里说了,我训练前80轮开,后20轮关掉,让模型在正常分布的图像上微调收敛。ultralytics中用--mosaic参数控制,通常配合关闭Mosaic还需要调小--scale之类的增强。
5.3 完整训练命令
yolo train \ model=yolov8s.pt \ data=/path/to/data/data.yaml \ imgsz=960 \ batch=16 \ epochs=200 \ patience=30 \ optimizer=AdamW \ lr0=0.001 \ weight_decay=0.0005 \ warmup_epochs=3 \ cos_lr=True \ mosaic=0.8 \ seed=42 \ project=classroom_head \ name=exp1训练过程日志会输出每轮的P、R、mAP50、mAP50-95等信息。我一般重点盯着mAP50和mAP50-95的走势,如果mAP50涨但mAP50-95停滞,说明模型在粗定位上有进步但框的回归精度不够,可能需要调高IoU阈值来提升框质量。训练结束会把每轮最好的checkpoint保存在weights/best.pt和last.pt。
5.4 训练过程的实时监控与判断
训练不是丢进去就完事。我每隔半小时看一眼loss变化。YOLOv8的loss包含三个分量:box_loss(框回归)、cls_loss(分类)、dfl_loss(分布焦点损失)。如果box_loss持续下降但cls_loss不降,说明模型在框回归上已经收敛,但分类上还在纠结,这时可以检查是不是标注一致性有问题——人头类别就一个,分类loss竟然不收敛,基本可以断定存在同类别但风格差异过大的标注,或者背景负样本太容易被误判为目标。
如果训练中val的mAP50到了60多轮就停滞,同时train的loss还在下降,那就是过拟合的早期信号。此时减小数据增强强度,或者调整weight_decay。如果是密集人头场景,我们经常遇到“mAP看着还可以,但实际画面里漏检后排”的情况,这个就要回到评估环节,分组看指标。
6. 结果评估与推理部署
6.1 分组评估:必须看小目标单独的指标
只盯整体mAP50会在密集人头场景里踩大坑。因为你的数据里面大目标样本占多数,模型把前排人头的框打准了,mAP50就能到0.9以上,但后排小目标几乎全漏,这个模型在实际教室里没有实用价值。所以我的评估方式是先按GT尺寸分组计算AP。
ultralytics的val.py支持按尺寸输出AP,COCO的划分标准是:小目标area<32x32像素,中等目标32x32到96x96,大目标>96x96。验证时直接用官方validator就能看到small/medium/large的AP。我自己的数据集上,YOLOv8s在整体mAP50是0.87的情况下,small目标的AP只有0.58,明显是短板。于是顺着这个结果去调整增强和输入尺寸,把small的AP拉到0.68,同时牺牲一点整体指标。
这个分组评估还应该配合实际场景的可视化检查。随机抽几十张验证集图片,打印模型预测结果,观察漏检目标的特点:是不是都是低对比度的、逆光的、遮挡严重的。这些case光看指标看不出来,得多看几张图找到共性。
6.2 导出ONNX与TensorRT推理
模型训好之后,部署到监控系统通常需要高吞吐推理。先把模型导出为ONNX,再用TensorRT加速。
yolo export model=best.pt format=onnx imgsz=960 yolo export model=best.pt format=engine device=0 half=True imgsz=960TensorRT的engine导出会针对GPU做算子融合和显存优化,在桌面级GPU上相比PyTorch原生推理通常能提速3到5倍。注意导出时imgsz要和推理时保持一致,否则TensorRT要重新构建engine,部署阶段容易踩“用户报推理突然变慢,后来发现是动态尺寸变化导致引擎重构”的坑。
如果监控摄像头是多路RTSP输入,推理不是实时逐帧跑,而是按策略抽帧。我常用的方案是每路流每秒抽2到5帧做检测,然后把结果交给跟踪模块,实现人头计数。这样CPU和GPU的资源都很充裕。教室场景下,人头不会瞬间移动超过一个身位,抽帧不会造成漏计,反而比逐帧推理省下大量算力。
6.3 从检测到计数的工程化
只输出检测框还不够,要数出教室里有多少人,需要把同一个人在不同帧里的检测框关联起来。最常用的做法是加上跟踪器,比如ByteTrack或SORT。原理不复杂:检测器给出每一帧的框,跟踪器根据IoU和运动模型把相邻帧同一目标的框串成轨迹。每次出现新轨迹就计数加一,轨迹结束时移除。
部署时我的实际流程分成三步:拉流解码、抽帧检测、跟踪计数。检测模型在这条流水线里只是中间一环,但它的质量直接决定跟踪的稳定性。如果检测漏掉一个人头,跟踪轨迹就断了,后期再加新轨迹会导致重复计数。所以在这个项目里,我宁可接受多一点误检,也不让漏检发生——头部误检可以通过后续轨迹逻辑过滤,漏检就真的丢了。
7. 常见问题与排查技巧实录
7.1 小目标漏检严重,怎么解决
这个问题基本是密集人头检测的第一大痛点。我用四板斧解决:
- 提高输入分辨率,960起步,1280看情况。
- 训练时开SAHI切片,让模型在切片上做检测,相当于把所有小目标变成了大目标。
- 数据增强里加入copy-paste和剪切缩放,增加小目标的样本量。
- 如果用的是YOLOv5,重新聚类anchor,让小目标的anchor回归有更合理的先验。
实际操作中我最低的漏检率是用“960输入 + SAHI切片512 + 重叠0.2”组合打出来的。单纯调大输入分辨率,对小目标AP的改善有但有限,因为模型无法在整张大图上同时抓取所有位置的小目标。
7.2 密集重叠场景下误检和重复框多
头脑密集的另一个典型问题是一堆框挤在一起。排查第一步看NMS阈值,默认的IoU阈值0.45在密集场景偏严。两个并排的人头高度重叠,IoU可能超过0.5,默认阈值直接把其中一个框抑制掉了。此时把iou阈值调到0.3到0.35,保留更多框。代价是误检也会增多,需要用置信度阈值过滤。
如果NMS调完还是重复框多,考虑换成Soft-NMS或WBF。Soft-NMS对重叠框做衰减而非直接剔除,密集场景很有效,能在保留相邻目标的同时排除重复检测。我在这类场景用WBF的效果通常最好,但WBF不适合实时推理,因为它需要聚集多模型或多次推理结果,所以工程上我优先用Soft-NMS。
还有一个容易被忽视的原因:训练数据里如果两个人头框贴得极近,标注时又没有严格做到“每个目标一个框”,模型预测时自然会在同一个目标周围输出多个置信度相近的框。这类问题只能回到数据标注去修。
7.3 训练loss不降或直接NaN
出现NaN的第一个排查点是标注文件。YOLO格式里width、height如果出现0,或者坐标值超过1,计算IoU直接崩溃。其次排查学习率,尤其是用AdamW时学习率太高,模型参数发散。第三排查类别数:data.yaml里nc写着1,但标签文件里出现class_id为1甚至更高的值,loss一定炸。
我曾经在这个项目里遇到过loss降到一半后直接跳出NaN,排查到最后发现是验证集里有一张图的标注框落在了图像边界外,训练时因为数据增强触发了边界裁剪,计算出的GT框变成负数。这个问题在YOLOv5里会在数据加载阶段直接过滤掉,但在YOLOv8的某些版本里会传给损失函数。解决办法是加载前做一次数据清洗,把超出图像范围的标注框裁回边界,或者直接删掉。
7.4 推理速度达不到实时要求
如果在GPU上跑YOLOv8s,960输入每帧大约3毫秒,几乎是实时的。如果部署到CPU或边缘设备,速度会掉一个数量级以上,这时候从几个方向压缩:
- 换更小的模型:YOLOv8n,配合TensorRT INT8量化。
- 降低输入尺寸:推理时用480或640,配合SAHI切片,速度影响比直接降低整体分辨率小。
- 减少推理帧率:监测场景不需要每帧检测,每秒2帧是很多生产监控项目的默认配置。
- 优化后处理:减少类别数量、简化NMS逻辑、固定batch size。
我在实际部署中使用的是YOLOv8n + TensorRT FP16 + 推理尺寸640,不加SAHI时单卡GPU能并行处理8路720P监控流,每路约5帧每秒。如果加入SAHI,速度会慢很多,我一般只在纪律分析这类离线任务里用SAHI,实时监控保持普通整图推理。
写在最后的经验之谈
做这个项目之前,我以为最难的是模型选型和调参,做完整套流程才发现,教室里的人头检测真正的瓶颈从来都在数据上。2000张标注图听起来不少,但当你把后排小目标单独拿出来统计时,会发现真正难学的样本可能只有几百个。模型提升最大的几分钟,不是换模型版本或改学习率,而是一次次回去看验证集的可视化结果,找那些模型犯错的共性,然后针对性地补强数据或调整增强。密集人头检测没有灵丹妙药,就是在数据、分辨率、后处理这三个层面上反复打磨。
最后分享一个小技巧:如果你打算在教室场景里做实时人数统计,先别急着把模型调到极致,先把检测框和跟踪轨迹打通跑通一整天,记录一天里不同时段、不同光线条件下的检测结果分布。这个全天的数据比你在测试集上看到的mAP更能说明问题。我当初就是靠这个方法发现了傍晚逆光时段漏检率奇高,后来在增强里专门加入了类似色温的低照度变换,效果提升显著。这类问题往往藏在实际运行数据里,光靠实验室指标发现不了。
本文还有配套的精品资源,点击获取