YOLOV5专注性检测:疲劳与分心行为识别实战

YOLOV5专注性检测:疲劳与分心行为识别实战 简介目标检测是计算机视觉的基础任务之一而如何从单纯的“检测到人”升级为“判断人的状态”是诸多实际应用中的难点。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类别名说明0person人体完整框用于追踪和状态关联1face人脸框用于触发关键点分析2cellphone手机用于分心判定3book书本/资料可视为非电子设备专注干扰项4cup水杯/饮料辅助判断短暂离席这份清单不是拍脑袋定的。我核对了专注性检测相关的行为学资料发现常见的分心行为集中在三类电子设备干扰玩手机、注意力转移转头、起身、明显的疲劳信号闭眼、打哈欠。手机这类小目标在监控视角下很容易漏检所以单独拉出来作为类别训练不要并进person里。采集场景要注意一个很容易被忽视的点视角多样性。同一个低头玩手机动作摄像头装在讲台上方俯视约30度和装在教室侧面平视在画面里的特征差异非常大。如果你只在一个固定视角采集数据训练换一个安装位置模型精度会直线下降。我的做法是至少采集3个不同视角每个视角下覆盖不同程度的光线白天、傍晚、灯光不足、不同距离近景、中景、远景。这是最花时间但最值得的工程投入。2.2 标注格式与质量控制的坑标注工具我用的是labelimg格式直接输出YOLO的txt格式。这里有一个特别容易翻车的点很多人把标注框做得非常贴合目标边缘但对于监控小目标过于贴合的框反倒会让模型学得不好。原因在于YOLOV5的锚框回归训练时会根据GT框的宽高比做聚类初始化如果你标注的框过小且长宽比极端要么聚类出来的锚框分布很偏要么训练时回归不稳定。我的经验是留出约3%的边距既保证IOU计算准确又给回归留一点弹性空间。标注质量控制上我采用了双人交叉抽检的方式。第一个标注员完成100张图后第二个标注员随机抽20%重新标注计算mAP0.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, 47EAR的计算公式如下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时长统计打哈欠是比闭眼更早出现的疲劳信号。我使用嘴巴区域的MARMouth 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), # 右眼外角 ], dtypenp.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), # 右眼外角 ], dtypenp.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] ], dtypenp.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个照搬默认参数很难发挥最佳效果。我的建议是先跑一组对照实验再确定超参数。记录一下我实测的结果同一份数据mAP0.5对比配置训练轮数mAP0.5备注默认超参数1000.781收敛慢小目标漏检调大mosaic和mixup概率1000.824小目标检测提升明显增加hsv_h和hsv_s增强1000.806光线鲁棒性提升调大mosaic 降低hsv_h1000.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只是一个工具它输出的是目标是什么、在哪里真正的价值在于你怎么利用这些信息设计出贴合实际场景的状态判定逻辑。每次在现场测试觉得误报太多的时候别急着换模型调参先回头看看是数据问题、标注问题还是状态机的判定策略不合理。解决这些工程细节比换一个大模型带来的收益要实在得多。本文还有配套的精品资源点击获取