ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

YOLOv8教室人员检测与计数系统实战:从模型选型到部署调优全记录

YOLOv8教室人员检测与计数系统实战:从模型选型到部署调优全记录 第一版跑通的时候我在工位前盯着屏幕大概五秒钟没说话。视频里是教室后排和讲台区域YOLOv8n的框在投影屏边缘跳了两次把老师模糊的影子当成独立目标框住了第三排有两个坐着的人一个框都出不来计数结果一帧40多、下一帧掉到20多的范围内疯狂波动。问了问旁边同事“检测精度多少”我答不上来——因为这个项目的瓶颈根本不在那份精度报告上而在于教室这个固定场景下模型根本不知道“人”该怎么找。这篇博文不是来科普YOLO原理的是一份“基于YOLO系列算法的教室人员检测与计数系统”从零到一落地的完整实战记录。我会围绕目标检测、人员计数这两个核心点从场景分析、模型选型、数据标注、训练排查、部署去重到调优踩坑把每一步的关键判断和理由都讲清楚。适合正在做类似毕业设计、实验室项目、或是第一次把YOLO用到固定摄像头场景里的同学参考。说实话环境配置和跑通demo这类内容网上到处都是真正让一个检测系统从“能跑”变成“能用”的是教室里那些看上去不起眼、却能让你调一周的细节。1. 教学场景的检测困境为什么通用person模型在教室里会翻车1.1 教室画面里的三个“反直觉”难点很多人拿到题目后的第一想法是YOLO目标检测成熟得很拿COCO预训练的person权重直接跑视频再数一下框不就行了。真实情况远没这么简单。教室场景有三个特性刚好都是通用目标检测模型的短板。第一个是遮挡密集。教室里的前后排天然形成大量重叠前一排同学的后脑勺可能会挡住后一排人的大半个身体前排举手、侧身、趴桌这些动作又会让目标变成一堆不规则的局部轮廓。讲台上老师来回走动也有可能在某一瞬间几乎完全挡住某个学生的上半身。通用person模型大多见过街头场景的遮挡但基本没见过“教室后排三个人叠成一条线”这种垂直方向的高度拥挤。第二个是小目标特别多。教室监控摄像头一般挂在黑板正上方或教室前上方离最后一排可能有七八米远。在1080P画面里后排坐着的人头肩区域常常只有十几到二十个像素高稍微一压缩到640分辨率整张脸加肩膀可能就巴掌大。COCO的person类别虽然包含小目标但训练集中占绝对主体的是中等尺度行人照片几十像素高的“人”在预训练模型里并不是强项。第三个是光照和背景噪声太怪。教室里有窗户逆光、窗帘褶皱、投影屏幕的强光反射还有空座椅上颜色像深色衣服的书包、靠在墙边的立式空调、黑板下沿的杂物。这些东西在特定角度下视觉特征和“坐着的学生”确实有相似之处非常容易制造误检。通用模型在开放场景里可能因为样本多样性而对这些干扰不敏感但教室是固定机位固定背景干扰物在每一帧里都在一旦模型学偏错误会被不断强化。1.2 我拿预训练模型直接硬跑的第一版结果我第一版做的就是“最省事方案”YOLOv8n预训练权重直接用model.predict()逐帧跑教室视频不微调、不筛选、不设计后处理。结果分三类问题。漏检集中在后排小目标和严重遮挡目标。后排三排以上模型经常只能框出靠近过道那一半人两个学生前后紧挨着坐的时候模型只输出一个框框的中心落在这两个人的空隙处。误检集中在投影屏幕边缘、黑板下方的暗色区域和靠窗第一排的窗帘褶皱。最离谱的是坐在第一排靠窗位的学生阳光在他身后形成逆光轮廓模型有一阵子把他的轮廓和影子一起框成了一个大号person。计数层面就更灾难了。逐帧数框结果每一帧都可能因为置信度波动而在某个目标上丢失或恢复检测数字像心电图一样跳。后来我统计了30秒测试视频真实人数36人模型逐帧数的平均框数是31.2标准差高达4.5。这个数据直接说明了一件事用预训练模型做教室人员计数不微调不后处理根本没有交付价值。这一步也可以看作对整个项目的定调教室是一个强场景约束、强背景重复的固定机位场景它的“难”不是难在类别多而是难在分布偏移。我们要做的不是“训练一个新模型”而是“把模型的场景域从开放世界挪到教室里来”。2. 版本选型和整体架构为什么选YOLOv8n而不是最新最强2.1 YOLO系列怎么挑算力、生态与场景三者取交集YOLO系列现在版本多到让人选择困难v5、v8、v9、v10、v11各有拥趸。我的原则是先定算力边界再定部署生态最后才是精度参数。教室检测项目通常跑在普通教学楼的边缘设备或实验室的一两张消费级显卡上推理阶段还要可能上CPU或嵌入式的NVIDIA Jetson设备这里就决定了模型参数量不能太大。选型对比我整理了一张表基于实际部署体验不是跑分报告模型参数量级训练友好度部署生态教室场景实测结论YOLOv5s约7M高但需要额外管理repo资料多但分散够用但后续维护不如v8省心YOLOv8n约3.2M极高ultralytics一条龙官方文档清晰导出ONNX/TensorRT/OpenVINO很方便教室精度够速度最好YOLOv8s约11M极高同上精度提升有限速度慢约60%YOLOv9/YOLOv10参数可调中高社区资料逐步完善但部分功能兼容性不够没有必须换的理由YOLOv11系列参数可调中高比较新但实际稳定版本要看发布节奏应用经验沉淀少暂不推荐我最终选的是YOLOv8n核心原因有三个。第一v8在结构上引入了anchor-free和C2f模块检测头是decoupled head对目标尺度变化和拥挤场景的回归更友好而教室场景恰恰需要这种能力。第二ultralytics的生态太省事了数据格式、训练CLI、tracking、导出工具全部内置能把精力集中在数据和工程而不是刷库。第三在教室这种高度受限的场景中模型容量不是精度上限的决定因素。我用同样数据训过v8smAP50只比v8n高不到1个百分点推理速度却慢了一半多。多次试验之后我还是用回了v8n。2.2 从“检测”到“计数”还需要加上跟踪这一环纯检测输出的是“这一帧有几个目标”而人员计数系统要的是“这一段时间里这个教室有多少人”。这两者之间隔着一个跟踪层。为什么必须加跟踪而不是简单数框最直接的原因是帧间抖动。同一个学生这一帧置信度0.42下一帧因为角度稍微变化降到0.33如果阈值是0.35每一帧都会一会儿框一会儿不框。这种情况下计数结果不可能稳定。另一个问题是多人重叠时检测框数量可能小于实际人数但如果只用“帧内最大框数量”来近似人数又会把瞬时误检也计入。我采用的方案是检测加轻量跟踪YOLO先输出检测框再用ByteTrack或IOU跟踪器给每个目标分配一个稳定ID后端的计数逻辑全部基于“当前活跃ID集合”来统计。教室场景下目标运动速度不快、摄像头固定跟踪难度远低于自动驾驶那种高速场景因此不需要DeepSORT级别的重识别复杂度轻量级跟踪就能拿到很好的效果。2.3 系统模块划分与输入输出定义整个系统的模块划分我在动手开发前就固定下来了视频输入模块支持本地视频文件、RTSP摄像头流统一封装成逐帧YUV或BGR数据格式。目标检测模块YOLOv8n完成每帧目标框、置信度、类别输出。跟踪模块给检测框分配ID维护跨帧轨迹。区域计数模块根据业务规则决定哪些ID计入“当前人数”。输出模块叠加绘制检测框、人数统计、帧率信息支持RTMP推送和本地保存。输入是教室监控视频流输出是实时人数统计信息和可视化画面。只要把这条链路想清楚就不会出现“算法跑通了但没法交付”的尴尬。很多初学者一上来只训练模型不做整体设计最后发现检测、跟踪、统计各部分独立都能跑连起来却全是问题。3. 数据准备与标注规范这一阶段决定了模型90%的上限3.1 数据采集的覆盖策略固定场景更要防过拟合模型微调质量的决定性因素不是网络结构而是数据。这一点在教室场景比开放场景更极端。因为教室背景相对固定模型很容易偷懒去记忆“哪个座位有人”而不是学习“人长什么样”。我的数据采集策略是主动覆盖变量。总共在24间不同教室采集了视频覆盖普通多媒体教室、带阶梯的教室和实验室改造教室。每个教室分别在不同时段采集白天自然光满亮度、傍晚混合光、夜间开灯、拉窗帘投影模式。人员分布覆盖了2人自习、十几人小课堂、40人满编课堂等多种状态。最后从视频里每隔2秒抽一帧剔除模糊和严重重叠的帧得到5800多张有效图片标注了约14500个person实例训练集5400张、验证集250张、测试集200张。很多同学在这步会犯的错是只拍自己上课的固定教室、固定时间段、固定人数区间。这样出来的模型在自己的几个测试视频上精度极高换一个教室就明显掉点原因就是模型记住了光照和座位布局没有学到泛化特征。3.2 YOLO格式标注的边界规则标注质量比标注数量更影响最终模型效果。LabelImg这类工具大家都熟真正难的是定一套统一、可执行的标注规范。我的规则如下都是实际踩过坑后总结的类别设置初期只设一个person类。不要一上来就分student和teacher教室场景下老师偶尔坐着、学生也可能站着类别语义不清晰反而让检测头更难学。遮挡规则目标被桌椅或其他人遮挡时按“可见部分推断出的完整身体边界”来标。遮挡导致只有头和肩膀露出来就标一个从肩到头顶的矩形。如果单个目标可见区域小于整体的30%我倾向于不标避免给模型灌输大量残缺信息。边界截断规则目标位于画面边缘且被截断如果可见部分超过一半就保留标注框贴到图片边缘即可截断太多直接放弃。小目标最小尺寸规则目标宽度小于8像素、高度小于15像素的实例不标。太小的人头在640分辨率下已经是纯噪声标了反而干扰训练。空教室负样本必须包含大量无人场景的图片这类图没有任何标注文件空txt。这些负样本用来压制误检作用非常大。标注导出的时候LabelImg选择YOLO格式会在图片同目录生成同名.txt文件每一行是class_id center_x center_y width height坐标全部是0到1的归一化浮点数。有一点要注意如果一张图没有目标txt文件是空的但一定要保留同名文件YOLO训练时会把这张图当负样本用。3.3 增强策略哪些增强对教室场景真正有用Ultralytics默认开启的Mosaic增强、随机翻转、HSV扰动、尺度缩放和随机平移在教室场景里需要做取舍。水平翻转可以在教室数据上放心用因为教室布局基本左右对称翻过来之后语义不变相当于白送一倍数据量。垂直翻转绝对不能开没有人头朝下上课。HSV扰动我调大了饱和度偏移这是因为教室里的投影、窗帘、黑板反光会让同一件衣服在不同位置呈现不同色温增强后模型对颜色变化的鲁棒性明显提升。Mosaic增强默认开启对教室这种目标密集但背景单一的场景有正作用因为它强制模型在前景和背景之间做区分。我还做了一件不少博主会提但少有人实操的“便宜药”从COCO数据集的val中抽取了约1500张带person标注的图混进训练集占比大约20%。公开数据混入的意义在于把模型在开放场景里学到的“人”的广义特征保留住防止微调后只认识教室里的人。我实测混入后换教室场景的mAP50大约提高2到3个百分点代价是训练时间长了约15%。4. 训练细节与loss曲线判读如何知道模型真的在学4.1 环境配置与训练参数环境配置直接用Anaconda解决。创建Python 3.10环境安装PyTorch对应CUDA版本然后pip install ultralytics。YOLOv8的依赖很少没有网上说的那么玄乎。训练数据目录结构按images和labels分开data.yaml里指定train和val路径以及类别数。这里要提醒一句路径尽量不要写中文Windows下中文路径偶尔会让dataloader发疯。我最终采用的训练参数如下这个组合在单张RTX 3060 12GB上大概跑5个多小时model: yolov8n.pt data: classroom.yaml epochs: 120 imgsz: 640 batch: 16 lr0: 0.01 lrf: 0.01 optimizer: SGD seed: 0 workers: 4 augment: true选SGD而不是YOLOv8默认的AdamW是反复对比后的决定。AdamW收敛快但我在教室数据上跑出来val振荡明显最后几个epoch的mAP50上下波动能差3个百分点。SGD收敛慢一点但loss曲线和精度曲线都更平稳最终测试集精度还要高一点。在小数据集微调场景里稳定的优化器比“快速逼近”更重要。首次训练时不要冻结backbone让模型所有层都参与微调。很多人有个误解觉得用预训练模型就只应该train head不动backbone。教室场景的数据分布和ImageNet差距较大backbone提取到的边缘纹理特征需要重新适配冻结反而会限制效果。4.2 训练崩了怎么定位loss、数据、Lr三板斧“目标检测模型微调崩了”是搜索热词我自己也崩过三轮这里把排查链路完整列出来。现象一是loss直接NaN。优先查学习率是不是太高SGD下lr0大于0.05很容易发散其次是数据里有没有异常坐标比如归一化后坐标出现负值或大于1最后查显存是否不够导致精度溢出。按这个顺序排查95%以上的情况能定位。现象二是train_loss一路下降val_loss却翘头mAP50在0.5附近死活上不去。这八成不是超参数问题而是训练集和验证集分布脱节。我第二版就吃了这个亏训练集里的教室全是下午自然光验证集挑了一段夜间开灯的视频结果模型把“下午偏暖色光”当成了“人”的特征之一。解决方法是重新划分数据集确保每个时段、每间教室都能同时出现在训练集和验证集里。现象三是微调后比基础预训练模型还差。这个现象在“微调崩了”的相关搜索里出现频率很高。最常见的原因是标注错乱比如某些图片用了COCO类别号1person在COCO里是0或1取决于版本但data.yaml没对齐。其次是验证集本身就小只有二三十张图mAP波动没有统计意义。最后就是学习率太高导致灾难性遗忘——warmup期间冲坏了已经学习好的低层特征。4.3 收敛判断不能只看loss样本可视化才是照妖镜Loss曲线只能说明模型在“变好”还是“变坏”不能说明模型学到了什么。在每个epoch结束后我习惯把验证集某几张固定图片的预测框叠加结果存下来人工扫一眼。样本可视化会出现很多指标看不出的问题比如模型把“窗帘阴影”学成了person类或者把所有地面附近的物体都框了一遍。另外用混淆矩阵看系统性漏检。YOLOv8在验证阶段会输出归一化的混淆矩阵我关注的是person-background这一格。如果这一格偏高说明相当一部分person实例没有被召回尤其是后排小目标。反过来如果background-person偏高代表误检严重多半需要补负样本和调阈值。同时对比mAP50和mAP50-95这两个指标。mAP50高但mAP50-95明显低说明模型能框出目标中心但框的边界质量差或者对遮挡目标只会给一个很松的框。教室场景里我见过mAP50有0.91但mAP50-95只到0.63的组合这种情况下做人员计数勉强够用做乱序行为识别那种精细任务就不行。5. 推理端到端部署与计数逻辑从模型到真正统计人数5.1 置信度阈值和NMS的调校别再默认conf0.25把训练好的模型部署到实际视频流时置信度阈值和NMS阈值必须重新调不能沿用默认值。YOLOv8默认conf0.25、iou0.7这是针对开放世界检测的保守设定但在教室这类背景固定的场景里0.25的置信度太低投影屏上一块发亮的区域、黑板下沿的一道阴影都可能被模型以0.3左右的得分判成人。我在测试集上做了一组阈值扫描实验记录误检和漏检的变化conf阈值误检数/100帧漏检数(真实36人场景)综合表现0.25183误检偏高计数会虚高0.3094仍偏激进0.3535平衡点我最终采用0.4028后排漏检太多0.50113不可用iou阈值在教室这种拥挤场景反而要调低一点我用0.6。原因很简单后排两个紧挨着的目标两个框的交并比可能很高如果iou阈值过高NMS会把其中一个人吞掉导致漏检。默认的0.7是针对“同一目标被重复框”的抑制在密集人群场景里不合适。推理代码可以这样写批量处理本地视频并直接输出带框画面对比效果from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourceclassroom_test.mp4, conf0.35, iou0.6, imgsz640, classes[0], # 只保留person类 saveTrue, streamTrue, )5.2 计数去重与平滑直接数框会让数字“跳舞”这是整个系统里最容易让交付翻车的环节。直接对每帧检测框计数输出的人数会剧烈抖动原因前面说过置信度波动、遮挡变化、小目标时隐时现。单纯取“多帧最大值”也不对因为误检那一帧会被当成最高值记下来。我的做法是检测结果进跟踪器统计活跃ID。简化逻辑如下# 伪代码用于说明计数逻辑 seen_ids {} for frame in video_stream: dets model(frame, conf0.35, iou0.6)[0] tracks byte_tracker.update(dets, frame_id) for t in tracks: cx, cy t.center if in_classroom_roi(cx, cy): seen_ids[t.id] current_time # 过期ID淘汰 seen_ids {tid: ts for tid, ts in seen_ids.items() if current_time - ts 3.0} current_count len(seen_ids)核心点在于“过期窗口”。学生对一个固定座位大概率连续坐几分钟甚至一节课但如果跟踪ID因为短暂遮挡丢失了同一目标重新出现时会被分配新ID这样计数就会虚高。过期窗口设为3秒ID在3秒内只要再次出现就不会被清掉超过3秒才认为目标真的离开了。这个窗口值根据场景可以调速度快的人流动区域用2秒纯上课场景用5秒也不会差太多。此外如果需求是“统计教室内每个座位区有没有人”还可以给每个座位划定多边形ROI判断检测框底边中点是否落在区域内。区域计数和全局计数并不冲突我实际交付的系统中两者都保留了通过配置切换。5.3 从PyTorch到ONNX/TensorRT加速模型微调完成后通常还要从PyTorch权重导出成可部署格式。教室摄像头如果是多路接入GPU资源和CPU占用都需要控制。我的部署策略分两档CPU服务器导出为ONNX用OpenVINO跑。在T4或i7级别的CPU上YOLOv8n可以跑到20到25 FPS满足不到300FPS的实时需求。嵌入式设备Jetson Orin Nano等导出TensorRT FP16引擎推理速度大约是PyTorch的2到3倍。导出命令很简单from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) model.export(formatonnx, imgsz640, halfFalse, simplifyTrue)导出后要在目标设备上验证一次精度变化。实测中FP16 TensorRT对mAP50的影响基本在0.5个百分点以内视觉上几乎看不出差异。真正影响精度的是分辨率从640提到960后显存占用和推理时间会非线性增加教室后排小目标密集时提升明显但代价不小一般作为“高精度模式”备选。6. 实测效果与踩坑总结后排小目标和讲台遮挡是两大痛点6.1 5个教室盲测的数据结果盲测阶段我特意选了训练集完全没看过的5个教室每个教室录了3段不同时段视频总计15段每段30秒。让三名同学人工数出每帧人数再与系统输出对比。这里说的“人工人数”以画面内清晰可见的人为准标注规范与训练时保持一致性。教室类型实际人数区间系统计数平均误差最大瞬时误差多媒体大教室自然光18-370.382阶梯教室夜间开灯24-400.513小班教室拉帘投影6-120.191实验室改造教室逆光10-200.744窗边逆光小教室4-80.663这个误差水平对于“课中到课率统计”“晚自习人数上报”这类需求已经可以直接用。逆光环境下误差偏大主要原因是逆光侧目标对比度太低detector容易把人和背景融在一起。6.2 系统性问题排查与低成本补救手段盲测里集中暴露了两个系统性问题。一是后排密集小目标。最后一排人肩部高度在640分辨率下只有15到20像素模型出框不稳定尤其当四五个人几乎并排紧坐时经常只输出三四个框。我试过把推理分辨率提高到960这个问题的改善很明显但T4上的推理帧率从28掉到12。最终采用的做法是视频流同时维护两路推理全图用640做快速检测只对画面底部三分之一区域额外做一次960分辨率的检测再把结果合并过滤。这个技巧有点取巧但在固定摄像头场景下非常实用相当于“把算力花在关键区域”。二是讲台前方的遮挡。老师站在讲台边时很容易完全挡住第一排中间某个学生。模型不是检测不到而是只能输出一个很小的头肩框置信度压在0.3附近一旦阈值设到0.35就会漏。这个问题的根治靠标注阶段保留截断目标调优阶段靠把该区域的阈值单独下调我在ROI统计时对讲台前区域覆盖的检测结果使用conf0.25其余区域继续用0.35。这样既不会明显增加误检又能把被遮挡学生的框留住。还有一个问题值得单独说——空座位区域的书包和衣物误检。这个场景在下午最后一节课格外严重学生走了一部分书包留在椅子上颜色和材质看起来像弯腰的人。我的解法和通用做法一样从真实视频里截取一批“有书包但无人”的画面作为负样本混入训练集。这个操作对压制误检非常有效在最终的误检统计表里书包误检从每100帧7次降到2次。踩过这几轮之后我的体会是目标检测项目的天花板往往不在算法更新而在对场景的理解和工程细节的处理。YOLO系列已经把一个检测器的“常规能力”拉到了很高的水位能否在具体场景里真正落地比的是谁更懂自己手里的数据和业务规则。如果你也在做类似的教室检测项目先沉下心把数据拍全、标注规则定清楚后面所有环节都会顺利很多。
返回列表