简介:目标检测是计算机视觉的基础任务之一,而如何从单纯的“检测到人”升级为“判断人的状态”,是诸多实际应用中的难点。YOLOV5作为高效的目标检测算法,常被用于实时识别画面中的目标;但要实现疲劳检测与分心行为识别,还需结合人脸关键点分析、头部姿态估计以及时序状态机。通过计算眼睛纵横比(EAR)和嘴巴纵横比(MAR),配合PERCLOS疲劳指标,系统可以判定闭眼、打哈欠等疲劳信号;结合头部俯仰角与偏航角,还能识别低头玩手机、转头侧视等分心动作。这类专注性检测技术可广泛应用于课堂听课分析、驾驶员疲劳预警、机房值班监测等场景。这套系统从数据准备、模型训练到推理部署的完整实践,展示了如何通过阈值调优与低算力部署实现可靠落地,并分享了大量工程实战经验。 开年接了个挺有意思的活儿:一套基于YOLOV5的人物专注性检测系统,要求同时跑疲劳检测和分心行为检测。很多人一听这题目,觉得不就是在视频流里框出个人嘛,但真正做起来才会发现,专注性检测压根不是"人在镜头前"这么简单——你得让系统实时判断出这个人到底是在认真干活,还是在打瞌睡、玩手机、东张西望。这套系统里我负责的是YOLOV5目标检测主干加人脸关键点分析、头部姿态解算、行为状态判定整条链路。这篇文章把我从数据准备到训练调参再到推理部署的完整过程写出来,包括踩过的坑和最后采用的妥协方案,给大家做个参考。
整个系统适合这些场景:课堂听课状态分析、驾驶员的疲劳预警、机房/工位值班监测、在线考试防作弊辅助。如果你是做视觉算法落地、或者正在纠结"YOLOV5训练自己的数据集该怎么搞",这篇应该能帮上忙。内容不涉及复杂数学推导,但会把每个模块为什么这么设计讲的比较透。
1. 专注性检测的本质:从"人在镜头前"到"人在状态里"
1.1 为什么单纯目标检测远远不够
如果只靠YOLOV5的目标检测,你能得到什么?就是一堆框:person、cell phone、book、cup,每一帧都有坐标和置信度。你可以统计画面里有没有人、人有没有拿手机,但判断不出这个人是不是低着头睡着了,也判断不出他是不是盯着屏幕发呆、但心思早就飞了。
专注性检测真正要回答的问题不是"谁在画面里",而是"这个人当前在做什么状态"。这就意味着系统必须由三层构成:
- 第一层是存在性检测,YOLOV5负责,告诉你画面里有什么目标、在什么位置。
- 第二层是细粒度分析,对人脸区域做关键点检测和头部姿态解算,得到眼睛开合程度、嘴巴张合状态、头部的俯仰角和偏转角。
- 第三层是行为状态机,把前面两层的结果按时间窗口组合,判断当前是"专注""疲劳""分心"还是"离岗"。
很多从零开始做这个系统的朋友,往往在第二层或第三层卡住。原因很简单:第一层用现成权重就能跑通,但第二层需要自己处理关键点数据,第三层则需要设计一套合理的状态判定逻辑,这跟"跑通一个Demo"完全是两码事。
1.2 整体链路设计:检测端、分析端、判定端各司其职
我搭的系统结构是这样的(最简化版,按模块拆分方便后续换模型):
# 伪代码,表达模块关系 video_frame = capture.read() # 第一层:YOLOV5 目标检测 detections = yolo_model.detect(video_frame) # person, cellphone, book... # 第二层:对人脸框做关键点分析 for face_box in detections.get("person_face"): landmarks = face_keypoint_model(face_crop) # 68点或468点 ear_value = calc_EAR(landmarks) # 眼睛纵横比 mar_value = calc_MAR(landmarks) # 嘴巴纵横比 euler_angles = solve_head_pose(landmarks) # pitch / yaw / roll # 第三层:时间窗口状态判定 state = behavior_state_machine(ear_value, mar_value, euler_angles, detections) log_state(state, timestamp)这里有个很关键的设计决策:为什么不在YOLOV5里直接加一个"疲劳"或者"分心"类别?因为疲劳、分心本质上是状态,不是目标。一辆车静止停在路边和飞驰在高速上,在目标检测里都是"car",但状态完全不同。你可以在模型里加一个"closed_eye"类别,但单帧的闭眼说明不了任何问题,连续10秒闭眼才是疲劳。状态判断天然需要时序信息,而YOLOV5是单帧检测网络,所以我的选择是:YOLOV5只负责输出稳定可靠的目标框和类别,状态分析全部交给上层逻辑来做。这样分工清晰,也方便后续把YOLOV5换成其他检测器而不影响状态判定模块。
2. 数据与标注:让模型真正理解"专注"的关键一步
2.1 类别清单与采集场景设计
很多人训练YOLOV5自己的数据集时,第一反应是找公开数据集。但专注性检测这个场景非常"碎"——同样是"分心",低头看手机和转头看窗外是两种完全不同的姿态,目标特征差异很大;同样是"疲劳",打哈欠和闭眼小憩的表现形式也不一样。公开数据集往往覆盖不全,所以我最终采用了"公开数据集预训练 + 自采数据微调"的策略。
我的类别清单是这样的:
| 类别ID | 类别名 | 说明 |
|---|---|---|
| 0 | person | 人体完整框,用于追踪和状态关联 |
| 1 | face | 人脸框,用于触发关键点分析 |
| 2 | cellphone | 手机,用于分心判定 |
| 3 | book | 书本/资料,可视为"非电子设备专注"干扰项 |
| 4 | cup | 水杯/饮料,辅助判断短暂离席 |
这份清单不是拍脑袋定的。我核对了专注性检测相关的行为学资料,发现常见的分心行为集中在三类:电子设备干扰(玩手机)、注意力转移(转头、起身)、明显的疲劳信号(闭眼、打哈欠)。手机这类小目标在监控视角下很容易漏检,所以单独拉出来作为类别训练,不要并进person里。
采集场景要注意一个很容易被忽视的点:视角多样性。同一个"低头玩手机"动作,摄像头装在讲台上方(俯视约30度)和装在教室侧面(平视),在画面里的特征差异非常大。如果你只在一个固定视角采集数据训练,换一个安装位置,模型精度会直线下降。我的做法是至少采集3个不同视角,每个视角下覆盖不同程度的光线(白天、傍晚、灯光不足)、不同距离(近景、中景、远景)。这是最花时间但最值得的工程投入。
2.2 标注格式与质量控制的坑
标注工具我用的是labelimg,格式直接输出YOLO的txt格式。这里有一个特别容易翻车的点:很多人把标注框做得非常贴合目标边缘,但对于监控小目标,过于贴合的框反倒会让模型学得不好。原因在于YOLOV5的锚框回归,训练时会根据GT框的宽高比做聚类初始化,如果你标注的框过小且长宽比极端,要么聚类出来的锚框分布很偏,要么训练时回归不稳定。我的经验是留出约3%的边距,既保证IOU计算准确,又给回归留一点弹性空间。
标注质量控制上,我采用了双人交叉抽检的方式。第一个标注员完成100张图后,第二个标注员随机抽20%重新标注,计算mAP@0.5(用已训练好的初版模型评估),如果低于0.85就退回返工。这个阈值不是随便定的,我踩过用低质量标注训练导致模型到了现场疯狂误检的坑,后来把标注质量门槛提上来,模型收敛速度和最终精度都有明显改善。
另外说一句,如果你只想快速验证整套系统流程,可以先直接用YOLOV5官方COCO预训练权重,不训练自己的数据集。COCO里本身有person类别,虽然不能精确识别"cellphone"这种小目标,但跑通检测到关键点到状态判定的链路是够用的。等流程验证没问题了,再回头训练自己的数据。
3. 人脸关键点与EAR算法:疲劳检测的工程实现
3.1 眼睛纵横比的计算逻辑
疲劳检测最经典、也最稳定的信号就是眼睛闭合程度。我用的是人脸关键点中眼睛区域的6个特征点,计算眼睛纵横比(Eye Aspect Ratio, EAR)。
以68点人脸关键点为例,每只眼睛由6个点描述:
- 左眼关键点索引:36, 37, 38, 39, 40, 41
- 右眼关键点索引:42, 43, 44, 45, 46, 47
EAR的计算公式如下:
EAR = (||p2 - p6|| + ||p3 - p5||) / (2 * ||p1 - p4||)对应到左眼就是:
import numpy as np def eye_aspect_ratio(eye_points): # eye_points 是6个坐标点,顺序按照 dlib 68点索引 A = np.linalg.norm(eye_points[1] - eye_points[5]) B = np.linalg.norm(eye_points[2] - eye_points[4]) C = np.linalg.norm(eye_points[0] - eye_points[3]) ear = (A + B) / (2.0 * C) return ear人眼正常睁开时,EAR大约在0.25到0.35之间,因人略有差异;闭眼时EAR会迅速掉到0.1以下。这个指标的妙处在于它是一个比值,对检测尺度不敏感——不管人脸在画面里是大还是小,只要关键点定位准确,EAR的数值范围都比较稳定。所以阈值设定可以跨场景复用,不用每个摄像头单独标定。
但在真实监控画面里,人脸往往不超过100x100像素,关键点定位误差很可能导致EAR抖动。我处理的办法有两个:一是对EAR做滑窗均值滤波,窗口大小取5帧,既平滑噪声又不至于延迟太大;二是不要只凭一帧的EAR低于阈值就判定闭眼,而是用一个短暂的时间窗口去确认。
3.2 打哈欠检测与PERCLOS时长统计
打哈欠是比闭眼更早出现的疲劳信号。我使用嘴巴区域的MAR(Mouth Aspect Ratio)来判断张嘴程度,原理和EAR一致,只是换成嘴部6个特征点:
def mouth_aspect_ratio(mouth_points): # 上下嘴唇点的垂直距离 / 左右嘴角水平距离 A = np.linalg.norm(mouth_points[1] - mouth_points[7]) # 不同关键点模型点序不同 B = np.linalg.norm(mouth_points[2] - mouth_points[6]) C = np.linalg.norm(mouth_points[3] - mouth_points[5]) mar = (A + B) / (2.0 * C) return mar但嘴巴动作有个干扰因素:说话。一个人正常说话时嘴巴也在反复张合,如果单纯看MAR超阈值就判定打哈欠,系统会疯狂误报。我的做法是同时看两个条件:MAR超过阈值(比如0.6)并且持续超过1秒。正常说话时嘴巴单次张开的时长通常在0.3到0.5秒,而哈欠的持续张开动作一般超过1秒。用时长作为第二重判据,误报率能下降一大截。
疲劳判定的行业标准里有个PERCLOS指标(Percentage of Eyelid Closure over the Pupil over Time),指的是单位时间内眼睛闭合时间所占的百分比。我的实现方式是维护一个60秒的滚动窗口,统计闭眼帧数除以总帧数,如果超过40%,就判定为疲劳状态。这里有一个经验值:PERCLOS阈值设在0.4左右比较合适,设低了容易误报,设高了疲劳已经比较明显才触发。
3.3 阈值怎么定才不容易误报
阈值是整个疲劳检测模块里最玄学的部分。我在不同光线条件的现场测试中发现,同样的EAR阈值,在亮光下检测正常,到了黄昏光线不足时,关键点检测精度下降,EAR波动幅度会变大,容易产生间歇性误报。
我最后用的方案是动态阈值 + 分级预警:
# 初始化时采集前100帧,计算正常状态下的EAR均值 baseline_ear = np.mean(initial_ear_values) # 根据基线做动态判定 fatigue_threshold = baseline_ear * 0.7 # 低于基线70%判定为闭眼 yawning_threshold = 0.6 # 嘴部MAR固定阈值动态阈值的思路很简单:每个人睁眼时的EAR基线不同,与其用固定值0.2去卡所有人,不如在系统启动时用一小段时间采集基线,再按比例设定闭眼判据。这个方案在现场测试中把误报率从每10分钟4次降到了每小时不足1次,代价是启动时需要用户配合保持正常状态5秒钟左右。
分级预警也比较关键。轻度疲劳(打哈欠频率高)只做记录,中度疲劳(持续闭眼3到5秒)弹提示,重度疲劳(PERCLOS超标或闭眼超过5秒)才触发声音报警。如果你一上来就用声音报警,90%的情况都是在误报中把人吵烦了。
4. 头部姿态估计与分心行为识别:YOLOV5之外的第二层分析
4.1 用2D关键点解算3D头部姿态的原理
分心行为的核心信号之一就是头部朝向。一个专注射屏幕的人和一个频繁东张西望的人,最明显的区别就是头部转角分布。我通过2D人脸关键点和标准3D人脸模型做PnP求解(solvePnP),得到人头的俯仰角Pitch、偏航角Yaw和滚转角Roll。
原理不复杂:我们知道标准3D人脸模型上一批关键点的空间坐标(比如鼻尖、下巴、左眼外角、右眼外角等),也检测到了这些关键点在2D图像上的像素坐标,然后通过相机投影关系反推旋转矩阵和平移向量。
import cv2 import numpy as np # 2D关键点(从关键点检测模型获得) image_points = np.array([ (nose_x, nose_y), # 鼻尖 (chin_x, chin_y), # 下巴 (left_eye_x, left_eye_y), # 左眼外角 (right_eye_x, right_eye_y), # 右眼外角 ], dtype=np.float64) # 3D标准人脸模型坐标(单位:毫米) model_points = np.array([ (0.0, 0.0, 0.0), # 鼻尖 (0.0, -70.0, -50.0), # 下巴 (-60.0, 20.0, -30.0), # 左眼外角 (60.0, 20.0, -30.0), # 右眼外角 ], dtype=np.float64) # 相机内参,使用近似值,实际需要按摄像头标定 focal_length = frame_width center = (frame_width / 2, frame_height / 2) camera_matrix = np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtype=np.float64) success, rotation_vector, translation_vector = cv2.solvePnP( model_points, image_points, camera_matrix, np.zeros((4, 1)) )得到旋转向量后,转成欧拉角就是头部姿态。如果pitch大于25度(低头超过25度),基本可以判定是在看桌面或者手机;如果yaw绝对值大于40度,说明头转向侧面,可能是分心交流或者看窗外。
有个点必须提醒:solvePnP解算姿态对2D关键点的精度非常敏感,特别是当人脸很小(比如80像素以下)时,2个像素的抖动就可能带来10度以上的姿态误差。所以我在把头部分类交给状态机之前,同样做了平滑处理——将欧拉角做指数滑动平均,系数取0.4左右,让姿态变化更稳定。
4.2 转头、低头和玩手机行为的判定策略
头部姿态只是分心行为的一个证据,必须和YOLOV5检测到的目标类别结合,才能做更靠谱的判定。
我设计的判定策略是这样的:
低头玩手机:头部pitch > 25度,且同时在手部区域附近检测到cellphone目标。如果只有低头没有手机,可能是在看书或写字,不能武断判定为分心。
转头侧视:yaw绝对值 > 40度,且持续超过2秒。短促的转头可能是正常扫视环境,持续性的侧头大概率是在与身边的人交流或看侧面屏幕。
离席/长时间离开:person目标消失超过30秒。这里要配合追踪模块,确认是同一个人消失而不是追踪丢失。
用手撑脸:这个比较难,我是在某次现场测试时发现一个人长时间用手托腮,上半身姿态看着像是在认真听,但眼睛早就闭上了。这类情形单靠目标检测和姿态解算都不够,需要额外检测手部关键点或手脸遮挡关系。我的取舍是第一版不专门做,因为它的行为学意义比较模糊,容易误伤。
这些策略我写成了独立的判定函数:
def classify_behavior(head_angles, detections, face_landmarks): pitch, yaw, roll = head_angles has_phone = any(d.cls == 'cellphone' for d in detections) ear = eye_aspect_ratio(face_landmarks) if ear < 0.18: return "eyes_closed" if pitch > 25 and has_phone: return "looking_down_phone" if abs(yaw) > 40: return "head_turning" if pitch < -15: return "head_up_distracted" return "focusing"这个策略本身不复杂,难在怎么把多个单帧判断串联成状态。比如"eyes_closed"单帧偶尔出现可能是因为眨眼,3帧内连续出现才算是闭眼;"head_turning"短时间出现可能是转头看时间,持续5帧以上才算是分心。所以我在状态机里加了一个计数器,每一类行为都有独立的持续时间记录。
4.3 多目标场景下的追踪与状态关联
教室或者机房场景下,画面里通常不只有一个人。如果只是对每个人脸做独立分析,输出就是一堆孤立的"人脸1疲劳""人脸2分心",这些信息没有意义。必须把同一人在帧与帧之间的检测结果关联起来,才能得到一个"谁从什么时候开始分心"的连续记录。
我用的追踪方案是IOU追踪加外观特征辅助,一个比较轻量级的组合策略。先按YOLOV5检测框做IOU匹配,IOU大于0.3认为可能是同一个人;如果IOU匹配失败(比如人转身后检测框变化太大),再用ReID特征向量做二次匹配。这个方案比纯IOU追踪鲁棒很多,又比DeepSORT轻量,单路视频CPU上测试大概增加5到8毫秒的处理时间。
追踪ID和状态记录我用了一个简单的数据结构:
person_states = {} # track_id -> {ear_buffer, head_pose_buffer, behavior, start_time} # 每帧更新 for track_id in current_tracks: state = person_states.setdefault(track_id, init_state()) state.ear_buffer.append(ear_value) state.head_pose_buffer.append(pitch_yaw_roll) # 判定行为并记录 state.behavior = classify_behavior(...) if state.behavior != "focusing": state.start_time = state.start_time or frame_time else: state.start_time = None多目标场景还有一个现实问题:目标互相遮挡。前排的人略微移动就可能挡住后排的人脸,如果这时直接判定"人脸丢失并终止分析",状态记录就断开了。我的处理是保留一个短暂的tolerant窗口,允许追踪对象在3到5帧内丢失,超过才真正判定离席。窗口太短容易被遮挡干扰,太长又会把真正的离开误判为短暂遮挡,实测5帧是一个比较平衡的值。
5. 训练与部署复盘:从自建数据集到本地实时推理
5.1 数据增强与超参数调整的实际效果
YOLOV5训练自己的数据集,最容易犯的错是拿默认超参数直接开训。默认的超参数是针对COCO这种大规模、多类别、高分辨率数据设计的,你的场景如果是单摄像头固定视角、类别数只有5个,照搬默认参数很难发挥最佳效果。
我的建议是先跑一组对照实验再确定超参数。记录一下我实测的结果(同一份数据,mAP@0.5对比):
| 配置 | 训练轮数 | mAP@0.5 | 备注 |
|---|---|---|---|
| 默认超参数 | 100 | 0.781 | 收敛慢,小目标漏检 |
| 调大mosaic和mixup概率 | 100 | 0.824 | 小目标检测提升明显 |
| 增加hsv_h和hsv_s增强 | 100 | 0.806 | 光线鲁棒性提升 |
| 调大mosaic + 降低hsv_h | 100 | 0.838 | 最终采用 |
这个结果表明,数据增强组合对结果的影响非常明显。mosaic增强能显著提升模型对遮挡、小目标的检测能力,因为mosaic天然把多张图拼接在一起,小目标出现频率更高。而hsv_h(色相扰动)不要调太大,否则对手机这种颜色特征比较敏感的小目标会产生负面干扰。
训练命令我用的是官方标准方式,加了一点自定义参数:
python train.py \ --img 640 \ --batch 16 \ --epochs 150 \ --data data/focus.yaml \ --weights yolov5s.pt \ --patience 20 \ --cache ram输入分辨率我特意选了640而不是1280。监控场景下人脸目标本身很小,理论上高分辨率能提升小目标检测精度,但实测640在检测效果和推理速度间最平衡。如果你用嵌入式设备,甚至可以考虑480或416,精度会有下降但能换回接近翻倍的帧率。
5.2 单通道与低算力平台的部署取舍
热搜词里有提到"yolov5训练单通道",这应该是想搞灰度图或者单通道红外摄像头。YOLOV5默认输入是三通道RGB,想用单通道图像训练,有个简单的办法:在数据加载阶段把单通道图复制成三通道,这样不需要改模型结构,也能正常训练。但说实话,如果摄像头本身只能输出单通道,我更建议直接保留单通道数据,训练时复制成3通道,因为模型结构不需要动。
更重要的是低算力平台的部署。这套系统如果跑在带GPU的服务器上,YOLOV5s的640分辨率推理大概能有30到60 FPS,完全够用。但如果要部署到边缘设备(比如有网友提到rv1106这种平台),就必须要做剪枝和量化:
- 我用的方案是通道剪枝 + INT8量化,剪枝比例设置在30%左右,量化后模型体积从14MB压缩到4.5MB,推理速度提升2到3倍,代价是mAP下降约2个百分点。
- 量化最容易踩的坑是校准数据集太小。我一开始只用200张图做校准,结果量化后小目标(人脸、手机)几乎全丢。后来把校准集扩到2000张,覆盖不同光线和时间段,量化后的精度才回到可接受范围。
如果边缘设备的算力实在不够,还有一个取舍方案:降低检测频率。比如每2帧跑一次YOLOV5检测,中间帧只做追踪和关键点分析,这样整体的处理帧率能提高不少,对"专注性检测"这种秒级敏感的应用,延迟完全可接受。
5.3 我在训练和落地过程中踩过的几个坑
写到最后一部分,分享一些实操中真正掉进去过的坑,这些在官方文档里几乎不会提到。
第一个坑:公开预训练权重里的类别和你的数据集类别冲突。YOLOV5在训练自己的数据集时,如果你的类别数和COCO不同,它会自动调整输出层。但如果你用yolov5s.pt作为预训练权重,并且设置了自己的data.yaml,一定要确认nc字段正确。我见过有人nc写错了还在奇怪为什么训练完模型输出的类别数对不上,其实改一下data.yaml重新训就行。
第二个坑:训练集里的"干净样本"导致现场误检爆炸。我第一版训练数据只采集了清晰的正面画面,结果拿到现场测试,模型把所有反光的白墙、电脑屏幕上的白色界面都当成了人脸,误检率惨不忍睹。后来我专门采了一批包含大量反光、模糊、遮挡的边缘样本,加到训练集里做负样本增强,才算把误检压下来。记住:训练集一定要包含"没有目标但很像目标"的难负样本,这比堆正样本数量更重要。
第三个坑:EAR和MAR在低分辨率下的失真问题。当人脸检测框小于50像素时,关键点定位精度已经不足以支撑EAR/MAR做可靠判断。我的做法是为这类小脸单独设置置信度权重——如果关键点置信度低于阈值,就把这一帧的疲劳判定结果降级为"不确定",而不是直接沿用上一帧的状态或者给出一个错误的状态。这个"宁可不判断,不要乱判断"的原则,在落地项目中特别重要。
第四个坑:源码来源的安全问题。网上确实有很多"源码+笔记"的资源,但用之前一定要检查代码里有没有恶意内容。我看到有的"YOLOV5专注性检测源码"下载链接,捆绑了一堆来历不明的脚本,这种是绝对不能用的。自己从官方仓库拉代码、自己改,虽然费时间,但至少能保证代码干净。
最后说一个我自己迭代到现在得出的体会:专注性检测这类系统,技术上最难的从来不是模型选型,而是如何把检测结果变成真正可靠的行为判断。YOLOV5只是一个工具,它输出的是"目标是什么、在哪里",真正的价值在于你怎么利用这些信息,设计出贴合实际场景的状态判定逻辑。每次在现场测试觉得误报太多的时候,别急着换模型调参,先回头看看是数据问题、标注问题,还是状态机的判定策略不合理。解决这些工程细节,比换一个大模型带来的收益要实在得多。
本文还有配套的精品资源,点击获取