资讯动态

YOLO驾驶员行为检测:22600张数据集的清洗、训练与部署实战

发布时间:2026/10/3 5:17:51 来源:尧图企业网站定制
大家做智能驾驶算法第一反应都是盯路面。但真正让L2、L3落地变难的往往是方向盘后面那个人——打哈欠、看手机、抽烟甚至闭眼。驾驶员行为检测DMS就是专门解决这个问题的而我这套基于YOLO训练的22600张驾驶员行为数据集完整地走通了一条从数据清洗到模型部署的路。文章里不会跟你客气地讲概念直接把我踩过的坑、实测过的参数、能抄作业的脚本全摆出来。适合正在做DMS或智能座舱方向的算法工程师也适合想用YOLO练手但一直缺高质量数据的学生和爱好者。1. 方向盘后面的人才是智能驾驶最该盯的对象1.1 为什么舱内感知长期被人忽视前两年大家做智能驾驶注意力全在外面的路上车道线、前车、行人、红绿灯。车企的宣传片也都是自动变道自动泊车没人愿意拍驾驶员在车里刷手机的样子。但实际跑到开放道路你就会发现L2辅助驾驶的主力场景恰恰是那些人还得盯着的工况——系统随时可能把控制权交还给你而如果驾驶员已经在打瞌睡交还的一瞬间就是事故的一瞬间。这就是DMS从锦上添花变成准标配的原因。国内新车评价体系里对疲劳和分心监测的要求逐年收紧后装商用车市场更是直接把驾驶员状态监摄像头的渗透率拉满。技术上它做的是盯着人但商业上它保的是辅助驾驶系统的下限。1.2 这套数据集的定位与适用边界我用的这套数据是面向DMS场景的监督学习资源22600张舱内图像覆盖正常驾驶和十几种异常行为标注格式可以直接喂给YOLO系列模型。它的定位不是全宇宙最强而是够靠谱的起步数据——类别设计符合真实DMS需求标注规范适合作为baseline训练集。它适合做的事情很明确训练一个能识别手持电话、看手机、打哈欠、闭眼、抽烟、喝水等行为的检测模型精度足以支撑DMS原型和中等复杂度产品。它不适合的事情我也提前说清楚不包含红外图像所以在纯黑环境下做微光疲劳检测需要另补数据没有毫米波雷达的生命体征标注不能用来做呼吸心跳级别的健康监测。搞清楚边界再动手能少走很多弯路。2. 22600张数据到底装了什么东西类别、标注和文件组织2.1 行为类别设计先分清必须救和可以忍拿到数据集第一件事不是训练而是先把类别清单看明白。这套数据一共十几个类别核心可以分成四组正常驾驶normal_driving所有负样本的基石占比最高。分心行为手持打电话、低头看手机、操作中控屏幕、喝饮料、吃东西、回头看后排。疲劳状态闭眼、打哈欠。危险动作抽烟、遮挡面部、双手脱离方向盘区域。这个设计思路很聪明它把行为和风险等级做了隐式绑定。闭眼、哈欠直接关联疲劳是DMS里优先级最高的信号打电话、看手机是分心驾驶里最常见的两类但提醒策略完全不同——打电话可以语音播报低头看手机可能需要的中断干预更强喝水、吃东西虽然也是分心但短促且常见提醒频率太高反而让驾驶员烦躁。这里有个坑要提前讲双手离开方向盘这个类单目RGB摄像头其实很难稳定判断手是否在方向盘上数据里的标注基本上是用手部检测框是否落入方向盘区域来近似。这意味着这个类别的边界天然模糊训练时我建议把它当成弱监督类看待评估时单独看别让它拉低整体mAP。2.2 标注文件与YOLO txt格式的转换细节YOLO训练需要的标注格式很固定每张图片对应一个同名txt文件每一行是class_id x_center y_center width height五个数值全部归一化到0~1之间。我这里分享一个通用的转换思路核心处理是解析原始XML或JSON框坐标再除以图片真实宽高做归一化。import os from PIL import Image import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) objs [] for obj in tree.findall(object): name obj.find(name).text bnd obj.find(bndbox) objs.append((name, int(bnd.find(xmin).text), int(bnd.find(ymin).text), int(bnd.find(xmax).text), int(bnd.find(ymax).text))) return objs def voc_to_yolo(xml_path, img_root, out_root, class_names): os.makedirs(out_root, exist_okTrue) for xml_file in os.listdir(xml_path): if not xml_file.endswith(.xml): continue # 注意归一化必须用原始图片的宽高不能用压缩图 img Image.open(os.path.join(img_root, xml_file.replace(.xml, .jpg))) w, h img.size txt os.path.join(out_root, xml_file.replace(.xml, .txt)) with open(txt, w) as f: for cls, x1, y1, x2, y2 in parse_voc(os.path.join(xml_path, xml_file)): xc ((x1 x2) / 2) / w yc ((y1 y2) / 2) / h bw (x2 - x1) / w bh (y2 - y1) / h f.write(f{class_names.index(cls)} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}\n)三个细节必须注意归一化一律用原图分辨率不要用压缩后的尺寸否则框会系统性偏移几个像素训出来后你会发现检测框总是差一点。类别索引从0开始且必须和训练配置里names列表顺序完全一致。这个错了我真踩过训练了一整天评估结果一团乱麻最后发现是类别顺序对不上。检查有没有重叠框或面积为零的异常框。YOLO框架本身能容忍同类别多框但一个大框完全套住一个小框时回归目标会冲突模型会学得精神分裂。2.3 数据分布真相不均衡是常态而不是意外22600张听着不少摊到十六个类别上平均每类也就一千四。真实分布更难看normal_driving可能占了四分之一低头看手机、打电话这些好采集的行为数量多闭眼、抽烟这类瞬间动作天然稀少。这就是驾驶员行为数据的宿命——你不能要求驾驶员配合你多打几次瞌睡。不均衡训练出来的模型整体mAP会给你安慰。全类别的平均分看着有0.87拆开看闭眼类可能只有0.6。正确做法是先统计类别分布对稀有类过采样或做针对性增强对高频常见类适量降采样。我实际跑下来把闭眼、哈欠这类少量类别重复采样2~3轮再把正常驾驶随机抽取一部分训练效率和最终精度都比硬着头皮全量训好得多。3. 训练之前的数据体检决定模型上限的隐形环节3.1 标签噪声清洗先跑一轮模型反查拿到标注数据我的建议永远是先做一轮人工抽检而不是直接开训练。DMS数据的标签噪声比想象中严重常见三类类别混淆低头看手机和手持打电话在角度刁钻时标注员经常搞混。漏标驾驶员明明在打哈欠但框就是没给出来。框定位不一致有的框只包住手部有的把整个人都包进去检测目标语义完全不对齐。YOLO对标签噪声有一定容忍度因为损失函数对个别异常样本有平均化效果。但如果噪声比例超过10%模型会明显往高频错误方向妥协比如把所有低头动作都预测成打电话。我用的清洗方法是先用初始模型在训练集上做一次预测把预测结果和标注差异大的样本全部挑出来人工复核。当时这一遍清了300多张问题图最终精度提升非常明显比任何调参都管用。3.2 光照、遮挡和镜面干扰舱内环境的特殊问题驾驶员行为检测和常规目标检测有一个本质区别检测目标永远是同一个驾驶位视角固定但光照环境极其恶劣——白天逆光、晚上中控亮仪表暗、进出隧道瞬间过曝。数据里这部分覆盖不够模型部署到实车上傍晚时段就会集体失明。我在这套数据里重点检查了几类样本戴墨镜的、戴口罩的、头发遮半脸的、手扶方向盘挡嘴的。它们会直接影响两个关键类别墨镜毁掉闭眼检测口罩干扰打哈欠识别。这些遮挡情况如果数据里没有模型就只能靠猜猜对了是运气猜错了就是事故。还有一个极其容易忽略的点前挡和眼镜片的反光。太阳斜射时镜片上会拖出长光斑模型会把这些光斑当成异常纹理。我在训练时把这类样本单独统计过一遍确认模型没有因为光斑出现系统性误报才敢往下走。3.3 按视频片段划分数据不然评估全是虚的驾驶员行为数据绝大多数是从视频抽帧来的同一个驾驶员连续几十帧的内容高度相似。如果按帧随机划分训练集和验证集验证集里会混进大量训练集的近亲帧评估结果虚高得离谱——模型可能根本没什么泛化能力只是记住了这个人的脸和动作。正确做法是先按视频时间片段切块保证同一个片段的所有帧只出现在一个集合里再划分。更严格一点训练集和验证集最好不要出现同一个人。DMS系统最终要面对的是真实世界里的各种驾驶员跨人的泛化能力才是硬指标。我在这套数据上的最终划分是85%训练、10%验证、5%测试验证集和测试集全部按完整视频片段独立切出。这样评估出来的mAP虽然比随机划分低几个点但它真实真实才有参考价值。4. YOLO训练从参数到损失的完整配置4.1 模型选型m起步、s落地、n做边缘YOLO系列现在迭代很快v8到v11已经是非常成熟的目标检测方案。驾驶员行为检测的目标是人身上的一堆动作检测框比较集中、尺度变化不大不像自动驾驶要同时处理极远极近的目标所以中小型模型完全够用。我的建议是先用YOLOv8m或YOLO11m把baseline跑通确认数据没有问题再往下压模型体积。不要一上来就训练nano版本——如果数据本身有缺陷nano模型会把它全归咎于模型容量不够你根本分不清是数据问题还是模型问题。baseline的意义就在这一步。训练命令直接用ultralytics的CLI省事yolo detect train datadriver_behavior.yaml modelyolov8m.pt epochs200 batch24 imgsz640 patience30 device0配套的数据配置文件长这样最关键的是类别索引和名称必须和数据集标注严格对应# driver_behavior.yaml path: /data/driver_behavior train: images/train val: images/val test: images/test nc: 16 names: [normal_driving, phone_call_handheld, phone_call_headset, using_phone, smoking, drinking, eating, yawning, eyes_closed, looking_away, hand_off_wheel, adjusting_device, talking, looking_back, face_occluded, touching_face]4.2 损失函数拆解CIoU、DFL和分类BCE到底在干嘛很多人训练YOLO从来不关心损失函数但我建议至少要搞明白一件事YOLO的损失是三大块——分类损失、框回归损失、DFL分布损失。分类损失用BCE每个候选框独立计算类别概率框是否参与分类由正负样本匹配决定。框回归损失在v8及之后用CIoU它同时考虑重叠面积、中心点距离和长宽比收敛速度和精度都比早期的IoU Loss好。DFLDistribution Focal Loss让网络预测离散的坐标分布而不是直接回归一个值它对标注框本身有噪声的场景容错更好。驾驶员行为数据恰恰噪声不小因为人的姿态和肢体边界天然模糊。理解了这三块就能解释一个常见现象为什么你的模型框很准但类别老错或者反过来。框准类别错优先查标签噪声框歪类别对优先看回归损失权重。DMS这种小目标集中场景我在实践里保持默认损失权重不动先把数据清干净效果立竿见影。补充一条圈里常有人用NWD、SIoU这些变体改进yolo它们主要用于小目标和模糊目标。驾驶员行为检测的框基本都不小用CIoUDFL的默认配置就够了强行换损失函数收益不大还可能引入额外的调参负担。4.3 BN崩溃小batch跑大模型为什么必崩训练中如果遇到 val loss 突然变nan或者mAP在某个epoch暴跌后再也起不来先查batch size。YOLO的Backbone里全是BatchNorm层BN的统计量需要在足够大的batch上才算得稳。DMS数据分辨率普遍不低显存不够时很多人把batch压到4甚至2这时候BN的均值方差估计就是拿几张图猜整个分布几个epoch后数值直接崩掉——这就是大家常说的BN崩溃。三个解法按优先级排把imgsz从640降到512或者用梯度累积让等效batch保持16以上冻结Backbone的BN层只训练Head很多迁移学习场景都这么干换更小的模型别让显存卡住训练策略。我自己是在24G显存的卡上跑的batch24、imgsz640全程没遇到BN问题。如果你只有12G显存建议imgsz512、batch16起步先跑通再谈效果。4.4 迁移学习与蒸馏别从零开始YOLO的COCO预训练权重对人这个类别有很强的特征提取能力DMS数据里的目标基本都是人迁移学习的收益非常大。从零训练在小数据集上收敛慢还容易过拟合加载预训练权重微调基本等于白捡5~10个点的mAP。微调的关键是学习率先用较低学习率训练整个网络20~30个epoch让Head适应新的类别数再放开Backbone用正常学习率。ultralytics默认的lr00.01对DMS这种小数据集偏大我一般直接设lr00.005配合CosineAnnealing学习率衰减200个epoch的曲线非常平滑。如果你打算把模型压缩到nano级别部署到低算力设备不要直接训nano而是用蒸馏拿训练好的m模型当teacher把logits和中间层特征蒸馏给nano学生模型。我实测过蒸馏出来的nano比直接训练的nano高出3~5个点这个差距在边缘设备上非常值钱。5. 评估指标要拆开看mAP虚高是真的漏报也是真的5.1 按类别拆解mAP闭眼类0.68是没法上车的整个项目做完验证集上的整体mAP50是0.87看着很漂亮。但拆开按类别看问题就暴露了类别mAP50问题描述normal_driving0.95样本多最容易phone_call_handheld0.90手持姿态相对固定using_phone0.86与打电话混淆较多smoking0.82手部遮挡嘴部时漏检yawning0.78张口幅度小时容易漏eyes_closed0.68墨镜、侧脸导致大量漏检闭眼类0.68在部署层面是不可接受的。疲劳驾驶检测对漏报的容忍度极低——漏一次不是体验不好而是可能出人命。所以我单独对闭眼类做了针对性增强把闭眼样本做水平翻转、小角度旋转、亮度扰动再把这个类的损失权重调高最终拉到0.78左右。这件事让我彻底明白DMS的落地标准从来不是平均分而是最差的那个类也要能用。5.2 单帧检测只是半成品置信度阈值与时序投票目标检测模型对每一帧都是独立判断直接拿来用会非常毛糙。同一个人保持看手机动作三秒钟模型可能一会儿报打电话一会儿报看手机中间还偶尔漏一帧。DMS产品如果这么输出驾驶员会被吓疯。我在检测框架外面加了一层时序逻辑这是项目里投入产出比最高的一部分维护一个行为状态队列对最近N帧的类别预测做投票只有同类行为连续出现超过K帧才确认K根据行为类型设5~10不等对疲劳类行为闭眼、哈欠单独记录帧级频率结合PERCLOS眼睛闭合时间占比做疲劳等级判断。这一层逻辑代码量不大但对抖动和误报的抑制效果远超单纯调模型。实车体验的稳定性主要来自这里而不是mAP。5.3 差异化阈值与混淆对拦截误报控制的工程手段DMS产品有个很现实的要求误报比漏报更让人反感。驾驶员正常开车系统突然弹您已疲劳驾驶连续弹三次这个人大概率直接把系统关了。所以我在置信度阈值上做了分类差异化处理危险等级高的行为闭眼、低头看手机阈值调低保召回常见小动作摸脸、喝水阈值调高避免烦人。另一个实战经验是处理混淆对。我统计过验证集上的错误矩阵发现喝水vs吃东西摸脸vs遮挡面部这两对是最常互相误判的。我的后处理里加了一条规则如果某帧同时检出两个高置信度类别且它们属于已知混淆对只保留风险等级更高的那个。代码就几行但产品侧的决策逻辑瞬间干净很多。6. 部署阶段TensorRT加速与多路视频流算力评估6.1 导出与FP16量化pt到engine的注意事项训练和评估在PyTorch环境落地几乎都要上TensorRT。YOLO的导出链路很成熟pt转onnxonnx再转TensorRT engine。yolo export modelbest.pt formatonnx opset12 imgsz640 # 在TensorRT容器环境里再导出engine yolo export modelbest.pt formatengine device0 halfTrue imgsz640FP16量化对YOLO来说几乎无损实测mAP损失一般不超过0.5个点但推理速度能翻倍。这里有个细节FP16引擎对输入尺寸很敏感训练用640就固定640动态输入虽然灵活但某些版本的TensorRT在动态shape下优化不充分导致延迟明显波动能固定就固定。如果你要压到nano级再部署记得用我在4.4提到的蒸馏方案先压缩模型再走导出流程。蒸馏后的nano在端侧跑的流畅度和直接训练的nano完全不是一个体验。6.2 T4跑1080p 25fps到底能带几路实测估算表被问得最多的问题是一块T41080p 25fps的视频流TensorRT跑YOLO 640分辨率检测到底能撑几路这个我不能凭空说直接给实测估算逻辑。T4的FP16算力约65 TFLOPSTensorRT下YOLOv8s在640x640输入上单帧推理约2.5~4ms。但推理耗时只是其中一环完整管线还包括RTSP拉流解码、缩放归一化和后处理。1080p的H.264解码即使走硬件加速也要占资源从1080p缩到640分辨率用CUDA核做大约0.5~1msNMS和坐标换算在CPU上做每路也要占核心。单路1080p 25fps的帧间隔是40ms整链路单帧处理约6~10ms扣掉调度和I/O开销一块T4同时跑10~16路是现实可行的实测也在这个范围内。模型TRT FP16单帧耗时(640)单卡1080p 25fps路数估算YOLOv8n1.5~2ms16~24路YOLOv8s2.5~4ms10~16路YOLOv8m8~12ms5~8路YOLO11n1.8~2.5ms12~20路这套数据的目标单一、场景固定我实际部署用的是YOLOv8s的FP16引擎单卡挂了12路摄像头CPU后处理占两个核GPU利用率60%左右跑得很稳。如果你的场景要求更高路数建议直接降级到nano模型而不是换更贵的卡。6.3 RTSP拉流、解码与检测的管线拆分真实项目的视频接入没想象中浪漫各种烂流卡顿、花屏、掉帧、分辨率乱跳。检测管线里至少要有一层断线重连和帧率控制。我的处理方式是拉流单独起一个进程用OpenCV或FFmpeg拉RTSP把帧放进环形缓冲区检测线程按固定频率取帧避免烂流拖死整个系统。import cv2 import time from collections import deque frames deque(maxlen8) def rtsp_reader(rtsp_url): cap cv2.VideoCapture(rtsp_url) while True: ok, frame cap.read() if not ok: cap.open(rtsp_url) # 断线自动重连 time.sleep(1) continue frames.append(frame) # 检测线程按需取帧做推理和后处理 while True: if frames: frame frames[-1] # 预处理 模型推理 时序平滑 time.sleep(0.04)这里的工程心得是拉流和解码无论如何不要和模型推理放在同一个线程。一路摄像头卡顿或断流如果和推理耦合会一次性拖垮所有路的检测。7. 数据集的持续进化从一次性资源到主动学习闭环7.1 换个车型就掉点domain gap没有捷径一套数据集再完整也只是某个时刻的切片。驾驶员行为数据尤其如此——车型不同、摄像头安装角度不同、驾驶员体型和习惯不同都会造成明显的domain gap。我把这套22600张数据训出来的模型放到另一个车型的样车上测试mAP直接掉了10个点以上一点都不夸张。这个现象背后的原因很直白模型学到的不仅仅是打电话这个语义还包含了特定视角下的纹理、光照和遮挡模式。换一辆车摄像头从A柱挪到中控台视角变了之前学的很多特征就失效了。靠调阈值解决不了这个问题出在数据分布本身。7.2 难例回传与半自动标注让数据活过来解决domain gap不能只靠再买一套数据而是要把数据集变成活的东西。我的做法是在部署现场接入了主动学习闭环边缘设备每天把低置信度样本、误检样本和漏检样本回传到服务器筛选出有价值的难例人工标注后合入训练集每周做一轮增量训练。坚持跑两个多月新车型上的表现基本追平了原数据集的效果。如果你只是个人或小团队没有完整的回传链路也有简易替代方案自己拿手机对着驾驶位录一段不同光照、不同角度的视频抽帧后用已有的模型做半自动标注——模型先出框人工修正类别和坐标再并回训练集。方法很土但效果实实在在。数据集的价值从来不在数量本身而在你围绕它能持续迭代出多少泛化能力。我做完这个项目最大的体会是数据清洗和迭代机制的设计花的时间一点都不比调模型少但回报也完全成正比。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑