首页 / 资讯中心 / 文章详情

基于PyQt+YOLOv5+dlib的驾驶员行为监控系统实战

基于PyQt+YOLOv5+dlib的驾驶员行为监控系统实战 ★ FEATURED ARTICLE
简介这份课程设计资源面向计算机视觉与深度学习方向的本科生及自学者提供一套基于PyQt5、YOLOv5与Dlib的驾驶员行为监控系统完整实现可用于课程设计、毕业设计或视觉项目练手。系统通过摄像头实时采集视频流结合YOLOv5完成目标检测、Dlib进行68点人脸关键点定位进而识别闭眼、张嘴、低头等疲劳与分心行为并触发声音或界面警报同时支持行为数据记录与GUI可视化展示。资源包共105个文件约224.11MB以66个Python源码为主体另含4个ui界面文件、6个mp3提示音、2个yaml与2个json配置、hdf5与pt模型权重、dat关键点模型及bat启动脚本等覆盖界面、推理、模型与资源各层。已有273人学习下载适合希望打通检测、关键点、GUI与实时推理链路的读者参考复用。1. 驾驶员行为监控系统从课程设计到能跑起来的桌面端方案课程设计里做驾驶员行为监控最怕的不是模型精度不够而是演示时摄像头一开、界面一卡、检测框乱跳答辩老师问一句“你这个疲劳判定阈值怎么来的”就答不上来。基于 PyQt YOLOv5 dlib 这套组合本质上是用 YOLOv5 做目标检测手、手机、烟、水杯等用 dlib 做面部关键点定位眼睛、嘴巴开合度再用 PyQt 把视频流、检测结果和告警逻辑串成一个能交互的桌面程序。它解决的是“单机、离线、可演示”的驾驶员状态监控需求适合做课程设计、毕业设计或小型原型验证的开发者。这套方案不追求车规级鲁棒性但胜在链路完整、每一环都能自己调参、自己排错跑通之后你对检测关键点GUI 的协同会有很具体的体感。2. 环境搭建与三件套的选型理由为什么不是别的组合2.1 PyQt、YOLOv5、dlib 各自负责什么先把职责边界划清楚后面排错才不会互相甩锅。YOLOv5 负责“画面里有没有手机、有没有抽烟的手、有没有水杯”输出的是类别和框dlib 负责“人的眼睛闭没闭、嘴巴张没张、头有没有低下去”输出的是 68 个关键点坐标PyQt 负责“把摄像头帧、检测框、关键点、告警文字画到一个窗口里并且让按钮和定时器正常工作”。三者通过一个主循环耦合读帧 → YOLO 推理 → dlib 关键点 → 计算疲劳/分心指标 → 更新界面。常见做法是 YOLOv5 用 PyTorch 权重dlib 用 shape_predictor_68_face_landmarks.dat。选 dlib 而不是 MediaPipe主要是因为课程设计里 dlib 的关键点索引稳定、资料多、和 OpenCV 配合直接缺点是速度偏慢但桌面端单路视频够用。选 YOLOv5 而不是 YOLOv8是因为课程设计场景下 v5 的工程资料和改配置的容错率更高且权重文件小方便打包。2.2 用 conda 建环境并锁定版本这一步翻车最多的是 dlib 编译和 PyTorch 版本冲突。我一般会单独建一个环境不跟其他项目混。# 创建独立环境python 版本不要追新3.8/3.9 对 dlib 和 torch 最友好 conda create -n driver_monitor python3.9 -y conda activate driver_monitor # 先装 pytorch按自己机器选 cpu 或 cu 版本课程设计用 cpu 也能跑 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cpu # 装 opencv、pyqt、dlib pip install opencv-python4.8.1.78 pip install PyQt55.15.9 pip install dlib19.24.2 # 验证 dlib 是否装好 python -c import dlib; print(dlib.__version__)逻辑说明先装 torch 再装 dlib是因为 dlib 安装时会调用 cmake 编译如果环境里已经有 numpy 和 torch编译过程不容易被依赖顺序打断。参数上python 3.9 是实测 dlib 19.24 和 PyQt5 5.15 都能稳定共存的版本opencv 4.8 对 VideoCapture 的兼容性比 4.5 好尤其是 Windows 下摄像头索引读取。如果 dlib 编译报 cmake 错误先conda install cmake再重试如果报 boost 相关错误直接换pip install dlib-bin用预编译包省时间。2.3 目录结构与权重文件放置课程设计最后要交代码目录乱会导致答辩时找不到文件。我一般固定成下面这样driver_monitor/ ├── main.py # PyQt 主入口 ├── detector.py # YOLOv5 封装 ├── fatigue.py # dlib 关键点与疲劳计算 ├── ui_main.py # 界面布局 ├── weights/ │ └── yolov5s.pt # 检测权重 ├── models/ │ └── shape_predictor_68_face_landmarks.dat └── utils/ └── video_thread.py # 视频读取线程权重文件不要放在代码根目录否则打包时容易漏。dlib 的 dat 文件约 99MB属于必须随项目走的资源提交前确认路径写的是相对路径不要写自己电脑的绝对路径。3. 用 YOLOv5 做行为检测从加载权重到画框3.1 加载模型与推理封装YOLOv5 的推理不要每次读帧都重新加载模型那样帧率会掉到个位数。正确做法是在类初始化时加载一次之后只调推理。import torch import cv2 import numpy as np class BehaviorDetector: def __init__(self, weights_path, conf_thres0.45, iou_thres0.5): # 只加载一次模型避免每帧重复初始化 self.model torch.hub.load(ultralytics/yolov5, custom, pathweights_path, sourcelocal) self.model.conf conf_thres # 置信度阈值 self.model.iou iou_thres # NMS 的 IoU 阈值 self.model.classes None # 不过滤类别全部输出 def detect(self, frame): # YOLOv5 接受 RGBOpenCV 读进来是 BGR必须转 img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results self.model(img_rgb, size640) # pandas 格式方便按列取 df results.pandas().xyxy[0] boxes [] for _, row in df.iterrows(): boxes.append({ label: row[name], conf: float(row[confidence]), x1: int(row[xmin]), y1: int(row[ymin]), x2: int(row[xmax]), y2: int(row[ymax]) }) return boxes逻辑说明torch.hub.load里sourcelocal是为了避免每次联网拉代码课程设计环境经常断网。size640是推理尺寸调小到 416 能提速但小目标手机容易漏调大到 1280 精度上去但帧率掉得厉害桌面端建议 640。conf_thres设 0.45 是经验值低于 0.3 会出现大量误检高于 0.6 会漏掉部分遮挡的手部动作。3.2 画框与告警文字叠加检测结果要画回原帧并且根据类别触发不同告警。这里注意颜色和文字位置否则界面会很乱。def draw_boxes(frame, boxes, alert_list): for b in boxes: color (0, 255, 0) # 默认绿色 if b[label] in [phone, cigarette]: color (0, 0, 255) # 危险行为用红色 alert_list.append(b[label]) cv2.rectangle(frame, (b[x1], b[y1]), (b[x2], b[y2]), color, 2) text f{b[label]} {b[conf]:.2f} cv2.putText(frame, text, (b[x1], b[y1] - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return frame, alert_list参数说明cv2.rectangle的线宽 2 在 640 宽度下清晰但不遮目标putText的 y 坐标减 8 是为了文字不压框线。告警列表每帧清空再累积避免上一帧的告警残留导致误报。如果类别名和你训练的不一致去data.yaml里核对names顺序YOLOv5 输出的name直接来自那里。3.3 检测帧率与跳帧策略桌面端如果每帧都跑 YOLOCPU 上大概 5 到 8 FPS界面会明显卡顿。常见做法是隔帧检测中间帧复用上一次的框。frame_count 0 last_boxes [] while True: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % 3 0: # 每 3 帧检测一次 last_boxes detector.detect(frame) frame, alerts draw_boxes(frame, last_boxes, []) # 后续交给 PyQt 显示跳帧参数 3 是平衡值跳到 5 以上框会明显滞后手都放下去了框还在不跳帧则界面卡到没法演示。如果机器有 GPU可以改成每帧检测但课程设计通常没这个必要。4. 用 dlib 算疲劳指标EAR 和 MAR 怎么设才不误报4.1 68 关键点里眼睛和嘴巴的索引dlib 的 68 点模型里左眼是 36 到 41右眼是 42 到 47嘴巴外圈是 48 到 59。算 EAR眼睛纵横比和 MAR嘴巴纵横比只需要这几组点。import dlib import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) def eye_aspect_ratio(eye_points): # 垂直距离两组水平距离一组 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]) return (A B) / (2.0 * C) def mouth_aspect_ratio(mouth_points): # 上下唇内侧距离 / 嘴角宽度 A np.linalg.norm(mouth_points[2] - mouth_points[10]) B np.linalg.norm(mouth_points[4] - mouth_points[8]) C np.linalg.norm(mouth_points[0] - mouth_points[6]) return (A B) / (2.0 * C)逻辑说明EAR 的分子是眼睛上下距离分母是左右距离闭眼时分子变小比值下降。MAR 同理张嘴时上下距离变大比值上升。索引不要写错36 到 41 是左眼图像视角42 到 47 是右眼写反了会导致左右眼数据对调但 EAR 值本身不受影响只是调试时看着别扭。4.2 阈值标定别用网上抄来的 0.2网上很多代码直接写 EAR 0.2 算闭眼实际用下来误报率很高因为不同人眼型、不同摄像头角度下 EAR 基线差别很大。我一般会先跑一段“正常睁眼”的视频统计 EAR 均值再取均值的 70% 作为闭眼阈值。# 标定阶段采集 100 帧正常状态的 EAR ear_samples [] for _ in range(100): ret, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) pts np.array([[shape.part(i).x, shape.part(i).y] for i in range(68)]) left_eye pts[36:42] right_eye pts[42:48] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 ear_samples.append(ear) ear_baseline np.mean(ear_samples) ear_threshold ear_baseline * 0.7 print(fEAR 基线 {ear_baseline:.3f}闭眼阈值 {ear_threshold:.3f})参数说明采样 100 帧是为了避开眨眼瞬间取均值更稳。系数 0.7 是经验值眼型偏小的人可以调到 0.65眼型偏大的人调到 0.75。MAR 的张口阈值同理正常闭嘴时 MAR 约 0.2 到 0.3打哈欠时能到 0.6 以上阈值设 0.5 比较稳。4.3 疲劳判定连续帧数比单帧阈值更重要单帧 EAR 低于阈值不能说明疲劳可能只是正常眨眼。必须加连续帧计数。EAR_CONSEC_FRAMES 20 # 连续 20 帧闭眼才告警 MOUTH_CONSEC_FRAMES 15 eye_counter 0 mouth_counter 0 # 在主循环里 if ear ear_threshold: eye_counter 1 else: eye_counter 0 if mar mar_threshold: mouth_counter 1 else: mouth_counter 0 if eye_counter EAR_CONSEC_FRAMES: fatigue_alerts.append(闭眼疲劳) if mouth_counter MOUTH_CONSEC_FRAMES: fatigue_alerts.append(打哈欠)逻辑说明20 帧在 25 FPS 下约 0.8 秒正常眨眼通常 0.1 到 0.4 秒不会触发真正疲劳闭眼会持续 1 秒以上。打哈欠 15 帧约 0.6 秒也高于正常说话时的张嘴时长。这两个参数是整套系统里最需要按实际视频调的调小了误报调大了漏报。5. PyQt 界面与视频线程别让 GUI 卡死主循环5.1 用 QThread 把视频读取和推理挪出主线程PyQt 的主线程负责界面刷新如果把cap.read()和模型推理直接写在主线程里窗口会无响应。正确做法是开一个 QThread在run里循环读帧、推理通过信号把结果发回主线程显示。from PyQt5.QtCore import QThread, pyqtSignal import cv2 class VideoThread(QThread): change_pixmap pyqtSignal(np.ndarray) # 发帧给界面 change_alert pyqtSignal(str) # 发告警文字 def __init__(self, detector, fatigue): super().__init__() self.detector detector self.fatigue fatigue self.running True def run(self): cap cv2.VideoCapture(0) frame_count 0 last_boxes [] while self.running: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % 3 0: last_boxes self.detector.detect(frame) frame, alerts draw_boxes(frame, last_boxes, []) # 疲劳检测每帧都做因为 dlib 比 YOLO 快 fatigue_result self.fatigue.check(frame) if fatigue_result: self.change_alert.emit(fatigue_result) self.change_pixmap.emit(frame) cap.release() def stop(self): self.running False self.wait()逻辑说明pyqtSignal的参数类型要写对传 numpy 数组用np.ndarray传字符串用str。stop里先置标志再wait避免线程还在读摄像头时窗口已经销毁。frame_count % 3和前面跳帧策略一致保证检测和显示节奏对齐。5.2 界面布局与告警显示界面不需要花哨但要有视频区、告警区、开始/停止按钮。用 QLabel 显示帧QTextEdit 显示告警历史。from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(驾驶员行为监控) self.video_label QLabel() self.alert_text QTextEdit() self.alert_text.setReadOnly(True) self.btn_start QPushButton(开始) self.btn_stop QPushButton(停止) layout QVBoxLayout() layout.addWidget(self.video_label) layout.addWidget(self.alert_text) layout.addWidget(self.btn_start) layout.addWidget(self.btn_stop) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def update_frame(self, frame): # BGR 转 RGB再转 QImage rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.width(), self.video_label.height()))参数说明QImage的bytesPerLine必须传ch * w否则图像会错位。scaled保持比例时加Qt.KeepAspectRatio但课程设计里直接拉伸也能接受看个人要求。告警文字用append而不是setText保留历史记录方便答辩时回看。5.3 信号连接与资源释放按钮点击要连到线程的启动和停止窗口关闭时要确保摄像头释放。def start_monitor(self): self.thread VideoThread(self.detector, self.fatigue) self.thread.change_pixmap.connect(self.update_frame) self.thread.change_alert.connect(self.append_alert) self.thread.start() def append_alert(self, text): self.alert_text.append(text) def closeEvent(self, event): if hasattr(self, thread) and self.thread.isRunning(): self.thread.stop() event.accept()逻辑说明closeEvent里必须停线程否则程序关了摄像头还占着下次运行会报“无法打开摄像头”。hasattr判断是为了防止没点开始就关窗口时报错。6. 避坑与排查课程设计里最容易翻车的 5 个点6.1 摄像头打不开或读出来全黑现象cap.read()返回 False或者画面全黑。原因通常是摄像头索引不对或者被其他程序占用。解决先用cap cv2.VideoCapture(0)单独测试不行就换 1 或 2Windows 下检查隐私设置里摄像头权限是否打开如果笔记本有多个摄像头索引可能不是 0。6.2 dlib 检测不到人脸导致疲劳指标全空现象YOLO 框正常但 EAR/MAR 一直是 0 或报错。原因dlib 的get_frontal_face_detector对侧脸和暗光很敏感检测不到人脸就没有关键点。解决先cv2.equalizeHist做直方图均衡再把detector(gray, 1)的第二个参数从 0 改成 1 做上采样能提升小脸检出率代价是速度慢一点。6.3 PyQt 界面卡死或闪退现象点开始后窗口无响应或者几秒后直接退出。原因视频循环写在了主线程或者信号参数类型不匹配。解决确认VideoThread继承的是QThread且run里没有直接操作界面控件信号参数类型和emit的数据类型必须一致传 numpy 数组不要写成pyqtSignal(list)。6.4 告警频繁误报现象正常眨眼、说话也触发疲劳告警。原因连续帧阈值设太小或者 EAR 阈值直接抄了网上的 0.2。解决按 4.2 节做基线标定把EAR_CONSEC_FRAMES从 20 往上调同时确认摄像头帧率如果帧率只有 10 FPS20 帧只有 2 秒阈值要相应放大。6.5 打包或换机器后路径报错现象在自己电脑上跑得好好的换台机器就找不到权重文件。原因代码里写了绝对路径。解决所有路径用os.path.join(os.path.dirname(__file__), weights, yolov5s.pt)拼接dlib 的 dat 文件同理。提交前把整个项目文件夹拷到另一台机器上跑一遍这是最省事的验证方法。7. 进阶技巧把检测结果落盘做回放验证课程设计答辩时老师经常会问“你怎么证明这个告警是准的”。现场演示受光线和摄像头角度影响很大我一般会加一个录制和回放功能把带检测框和告警文字的帧写成视频文件答辩前先跑几段典型场景正常驾驶、看手机、打哈欠把结果视频存下来现场直接播放比实时演示稳得多。# 在 VideoThread 的 run 里加一个 writer fourcc cv2.VideoWriter_fourcc(*mp4v) out cv2.VideoWriter(output/result.mp4, fourcc, 20.0, (frame.shape[1], frame.shape[0])) # 每帧画完框和告警后写入 out.write(frame) # 循环结束后释放 out.release()参数说明fourcc用mp4v兼容性最好20.0是写入帧率和实际读取帧率接近即可差太多会导致回放速度不对。frame.shape要在循环内取因为第一帧可能还没读到。输出目录要提前os.makedirs(output, exist_okTrue)否则 writer 会静默失败。另一个实用技巧是把每帧的 EAR、MAR、检测类别和置信度写进 CSV回放时对照视频看哪一帧触发了告警调阈值的时候不用反复肉眼盯屏幕。import csv with open(output/log.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame, ear, mar, labels, alert]) # 循环内 writer.writerow([frame_count, f{ear:.3f}, f{mar:.3f}, |.join([b[label] for b in last_boxes]), |.join(alerts)])这个 CSV 在调EAR_CONSEC_FRAMES和mar_threshold的时候特别有用你能直接看到误报那一帧的 EAR 到底是多少而不是靠猜。我自己的习惯是每改一次阈值就跑一段固定视频对比 CSV 里告警帧的数量变化通常调三轮就能找到适合当前摄像头和光照的参数。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站