简介:面向毕业设计的Python违规驾驶行为识别系统源码,融合计算机视觉与深度学习技术,可对驾驶过程中的常见违规行为进行检测分析,适合人工智能、软件工程等专业学生完成毕业设计或课程实践。压缩包共128个文件,大小约64.93MB,以Python源码为主,包含81个py脚本,覆盖数据处理、模型训练、推理检测等核心环节;另有8个shell辅助脚本用于环境配置或自动运行,模型权重(pth/pkl/npy)可供直接加载使用,图片样本和Markdown/TXT说明文档则辅助理解项目结构与操作方式。已有1061人学习下载。读者可获得一整套可运行的违规驾驶行为识别项目,既能参照源码和文档掌握算法设计、模型部署的思路,也能基于现有模块二次开发,快速完成实验或毕设功能演示,实用性和学习价值都比较高。
1. 违规驾驶行为识别源码:Python 工程怎么落地,而不是只交一份 Demo
交通事故统计里,分心驾驶造成的伤亡往往被酒驾、超速掩盖,但对车队管理来说它才是最难抓的。靠人盯视频监控,100 辆车一天产生的画面就足以让安全员看吐。这套 Python 违规驾驶行为识别系统源码的定位,不是算法比赛的玩具,而是一条能跑通「摄像画面输入 → 行为检测 → 分类判定 → 结果输出」的工程链路。它用开源视觉方案处理逐帧图像,识别驾驶员是否手持接打电话、吸烟等违规动作,并输出可视化标注和日志记录,毕设、课程设计、算法预研都能直接用。新手可以拿它当模板照着跑,熟手能直接替换模型和参数继续迭代。下面我按拆过的工程实操流程来写,尽量把每个环节的取舍和坑都说清楚。
2. 架构选型与数据准备:两级识别方案背后的三个理由
2.1 为什么不用端到端单模型,而是“检测 + 分类”两级串联
我拆过不少驾驶行为识别的毕设代码,最常见的翻车点不是模型不收敛,而是架构选错了。有人拿视频分类网络直接硬切整段视频,输入是连续几帧,输出直接是「打电话/抽烟/正常」——这种端到端方案看起来简单,实际上有三个问题:第一,它需要海量视频级标注,公开的驾驶行为数据集基本没有足够量;第二,它把「人在哪」和「人在干什么」绑死在同一个特征空间里,换一个车内摄像头角度就废;第三,推理时要攒够一个 clip 的帧数,延迟高,没法做实时的违规告警。
这套源码的选型是两级架构:先用目标检测模型定位驾驶员的手部区域和关键物体(比如手机),再把裁剪后的区域送进行为分类网络判断动作类别。检测管「在哪里」,分类管「是什么」,各干各的,哪个环节效果不好就单独替换。我一般会把这个结构理解成两个接口清晰的模块,后面不管是换 YOLO 版本还是换更轻量的分类网络,都不需要动整条链路。对于毕设场景,这个架构还额外有个好处:检测和分类的错误能分开归因,答辩时讲技术方案也容易说清楚。
源码根目录里的grid.npy就属于这类辅助模块的产物,它通常保存了预计算好的网格划分信息。这类文件在运行时用np.load()直接读进来就行,训练阶段不需要重新生成。如果你换摄像头安装位置,才需要重新跑一遍网格生成脚本——这一点不少初学者会忽略,后文避坑章节我会单独展开。
2.2 视频样本怎么切:帧采样频率与标注策略
数据集环节决定了这个系统的上限。驾驶行为识别难在两类样本太少:一是违规动作的时长往往只有 3 到 5 秒,二是不同司机的手部姿态差异极大。如果直接把原始视频逐帧存下来,一张 1080p 图片约 1.5 MB,10 分钟视频就是近万帧,硬盘和标注时间都吃不消。常见做法是按时间步长抽帧,我用的是每 0.2 秒取一帧的节奏:
import cv2 import os video_path = "data/train_video.mp4" output_dir = "data/frames" os.makedirs(output_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) frame_id = 0 step = int(fps / 5) # 固定每 0.2 秒取一帧 while True: ret, frame = cap.read() if not ret: break if frame_id % step == 0: cv2.imwrite(os.path.join(output_dir, f"{frame_id:06d}.jpg"), frame) frame_id += 1 cap.release()步长为什么要取fps / 5而不是逐帧?两个原因。第一,邻近帧之间画面差异很小,连续帧只会让训练集里充满高度相似的样本,模型容易过拟合到重复模式上;第二,驾驶行为的动作周期通常以秒计,0.2 秒一帧足够捕获到接电话、抽烟这类动作的关键姿态,还能把数据量压缩到原来的二十分之一。如果你想抓更快的动作,比如擦眼睛、低头看手机,可以把分母加大到 10,也就是每 0.1 秒一帧,但要注意后期标注成本会成倍增长。
标注策略上,这个场景不建议做像素级分割,用检测框加行为标签就够了。对每一帧,框出驾驶员手部区域,标签写死成三类:normal、phone、smoke。如果某个动作发生在模糊或遮挡严重的帧里,直接跳过这一帧而不是硬标一个框,这样能避免给模型喂脏数据。另有一点:训练集里「正常驾驶」帧不要占比超过 70%,否则模型会学成偏科生——上来不管看到什么都倾向输出normal。
2.3 配置文件与参数体系:先读懂再动手
拿到源码后先别急着跑训练,把配置文件读一遍能省下半天排错时间。这套系统的配置集中在config.py里,核心参数如下:
# config.py # 输入来源与运行设备 INPUT_VIDEO = "data/sample.mp4" SOURCE_TYPE = "video" # video / camera DEVICE = "cpu" # cpu / cuda # 模型权重路径 DET_MODEL = "weights/yolo_det.pt" CLS_MODEL = "weights/behavior_cls.onnx" # 推理阈值 CONF_THRESH = 0.45 # 检测框置信度阈值,低于它的框直接丢弃 IOU_THRESH = 0.5 # NMS 交并比阈值,控制重叠框的合并 # 性能控制 FRAME_SKIP = 2 # 每隔 2 帧做一次推理,其余帧直接复用上一次结果 # 驾驶位裁剪区域,格式为 [x, y, w, h] ROI = [560, 40, 720, 720]这套配置里最容易被忽略的是ROI。车内摄像头角度固定时,驾驶员基本只出现在画面的某个子区域,副驾和后座会白白消耗检测算力,还会引入误检。把ROI划出来,只在驾驶位区域做检测,精度和速度都能提升。FRAME_SKIP则是帧率换精度的开关,设成 2 表示每 3 帧只推理一次,中间两帧直接沿用上一次的框位置,后面第五章我会专门把这几组数的组合规律讲透。
3. 环境搭建与工程入口:从裸机到跑通识别只需要四步
3.1 Python 环境与 CUDA 检查
这套源码对硬件的要求比想象中低,CPU 机器也能跑,只是推理速度在每秒几帧左右。有独立显卡的话先确认驱动和 CUDA 状态,避免后面 PyTorch 装错版本导致torch.cuda.is_available()始终返回False:
python --version nvidia-sminvidia-smi正常输出会显示驱动版本和 CUDA 版本,比如 CUDA 11.8。建议用 conda 新建一个独立环境,不要直接装在系统 Python 里,因为后续装 PyTorch、OpenCV 时经常出现版本回退或冲突,独立环境能让你随时删掉重来。我一般习惯命名为driving_behavior,Python 版本固定 3.8 或 3.9 都可以,太新的 Python 版本部分预编译 wheel 可能还没跟上。
3.2 依赖清单与安装命令
工程依赖以视觉和深度学习库为主,典型的requirements.txt大概长这样:
opencv-python==4.8.0.74 numpy==1.24.3 torch==2.0.0 torchvision==0.15.0 onnxruntime==1.15.1 PyYAML==6.0 tqdm==4.65.0 Pillow==10.0.0安装时有个值得注意的细节:先把torch和torchvision通过官方源装好,再装opencv-python,最后装小依赖。如果直接把requirements.txt一把梭,有可能会因为依赖解析顺序问题,装出一个 OpenCV 找不到libGL.so.1的环境。装完跑一句验证:
python -c "import cv2, numpy, torch; print(cv2.__version__, numpy.__version__, torch.__version__)"能正常打印版本号就说明环境就绪。如果在这一步报libGL.so.1缺失,在 Ubuntu 上执行apt install libgl1 -y就能解决,这个坑几乎每个做视觉的同学都踩过。
3.3 源码目录结构与启动入口
拆完已知源码文件,目录结构大致如下:
├── src/ # 核心代码 │ ├── detect.py # 主入口:视频读取、推理、可视化 │ ├── config.py # 全局配置参数 │ ├── models/ │ │ ├── det_model.py # 目标检测模型封装 │ │ └── cls_model.py # 行为分类模型封装 │ ├── utils/ │ │ ├── nms.py # 非极大值抑制后处理 │ │ └── logger.py # 日志与告警记录 │ └── weights/ # 训练好的权重文件 ├── data/ # 视频样本与帧数据 ├── grid.npy # 预计算网格数据,runtime 加载 ├── requirements.txt └── README.md启动入口是detect.py,支持从视频文件和摄像头两种来源取流。视频文件模式适合验证效果,摄像头模式适合现场演示。命令行参数设计得比较直接:
python src/detect.py --source data/sample.mp4 --weights weights/best.pt --conf 0.45--source指定输入路径,--weights指定权重文件,--conf覆盖配置文件里的置信度阈值。加--camera 0则换成调用本机默认摄像头。第一次跑建议用视频文件模式,一是方便观察每帧输出,二是避免摄像头初始化失败干扰判断。
4. 源码核心逻辑拆解:检测、分类、可视化怎么串成一条流水线
4.1 检测模型推理:把画面里的人和手持物品先找出来
主流程是一个标准的视频循环:读帧 → 裁剪 ROI → 检测 → 分类 → 画框 → 输出。第一级检测模型负责任务是把手部区域和手机、烟这类手持物品框出来。这里有一段典型的封装代码:
import torch def detect_objects(model, image, conf_thresh): """对一帧图像做目标检测,返回过滤后的检测框列表。 返回值的每个框是 (x1, y1, x2, y2, score, class_id) """ results = model(image) boxes = [] for pred in results.xyxy[0]: x1, y1, x2, y2, score, cls = pred.tolist() if score < conf_thresh: continue # 低置信度框直接丢弃,减少分类阶段压力 boxes.append((x1, y1, x2, y2, score, int(cls))) return boxesresults.xyxy[0]是检测模型输出的所有候选框,每一项是[左上角x, 左上角y, 右下角x, 右下角y, 置信度, 类别ID]。这里的过滤逻辑是纯粹基于置信度的粗筛,精确的框合并交给utils/nms.py里的 NMS。注意一点:不要把conf_thresh调得太低,否则后续分类网络要处理的裁剪区域会暴增。做过真机测试的人都清楚,每帧多十几个候选框,累积下来 fps 掉得非常明显。
4.2 行为分类推理:从手部区域判定违规动作
检测模型输出的是带坐标的框,但它只告诉你「这里有东西」,不告诉你「这个动作违规没有」。第二级分类网络接手做这件事:对每个检测框裁剪出对应区域,缩放到分类网络要求的输入尺寸,推理得到行为类别。代码结构大致是这样:
import cv2 import torch CLASS_NAMES = ["normal", "phone", "smoke"] def classify_behavior(cls_model, frame, box): """把一个检测框对应的图像区域送进分类网络。""" x1, y1, x2, y2, score, cls_id = box crop = frame[int(y1):int(y2), int(x1):int(x2)] # 分类模型输入尺寸固定为 224x224,缩放会损失细节,尽量保持长宽比 crop_resized = cv2.resize(crop, (224, 224)) tensor = transform(crop_resized).unsqueeze(0) with torch.no_grad(): output = cls_model(tensor) label_id = int(output.argmax(dim=1)) return label_id, CLASS_NAMES[label_id]两个细节值得注意。第一,transform里必须包含归一化,且均值和标准差要与训练时一致,不然分类精度会突然崩掉;这也是很多复制来的源码换了机器就不好使的隐性原因。第二,argmax是最简单的判定方式,工程上更稳妥的做法是再加一个阈值判断——如果最高的类别概率都不到 0.6,直接归类为「不确定」,而不是硬判一个结果。实时监控系统里,「不确定」比「错判」要好处理得多,后者会引发大量无效告警。
4.3 可视化与日志输出:让每一次告警可追责
最后一步是把结果画到画面上,同时写入本地日志。这一环看似简单,但在毕设答辩或真实部署里很关键,因为它是唯一能被外界感知的输出物:
import cv2 import time log_file = open("alarm_log.txt", "a", encoding="utf-8") for box in current_boxes: x1, y1, x2, y2, score, cls_id = box label = CLASS_NAMES[cls_id] # 违规行为用红色框,正常行为用绿色框 color = (0, 0, 255) if label != "normal" else (0, 255, 0) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText(frame, f"{label} {score:.2f}", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 仅在检测到违规时写日志,避免正常帧刷屏 if label != "normal": timestamp = time.strftime("%Y-%m-%d %H:%M:%S") log_file.write(f"{timestamp}|{label}|{score:.2f}\n") log_file.flush()日志格式用|分隔而不是纯文本描述,目的是方便后续用脚本统计:比如按小时统计违规频次、按行为类型分析高发时段。flush()每次写入立即落盘,防止程序异常退出时丢失最后几条告警记录。这个习惯是我吃过亏后养成的——有一次演示到一半进程崩了,日志文件里恰好丢了最关键的一条违规记录,现场非常尴尬。
5. 训练与推理参数调优:置信度、IoU、帧率这三组数怎么配
5.1 超参数配置表:哪些参数影响精度,哪些影响速度
参数乱调是新手最容易犯的错,默认值不是给你随便改的。我把核心参数按「精度向」和「速度向」分成了两类:
| 参数 | 默认值 | 主要影响 | 调参方向建议 |
|---|---|---|---|
CONF_THRESH | 0.45 | 检测框数量与误检率 | 调高减少误报,调低减少漏报 |
IOU_THRESH | 0.5 | NMS 后留下的重复框 | 调低更容易合并重叠框,调高保留更多细节 |
FRAME_SKIP | 2 | 推理频率,直接影响 fps | 调高速度快,但短动作可能漏掉 |
CLS_THRESH | 0.6 | 行为分类判定的置信度门槛 | 调高减少违规误报,调低更敏感 |
| 输入分辨率 | 640 | 小目标(烟头)能否被检出 | 调高精度好但推理时间翻倍 |
这套组合的底层逻辑是:精度和速度不可能两头都占,必须根据你的部署场景压一边。毕设演示通常不需要真实时,可以偏精度;真要做车队告警,速度和漏报率才是第一位的。
5.2 置信度与 IoU 的取舍:漏报和误报挑一个
CONF_THRESH和IOU_THRESH是两兄弟,但管的不是一回事。CONF_THRESH过滤的是「模型有多大把握认为这是个目标」,IOU_THRESH处理的是「两个重叠框是不是同一个目标」。实际操作中有一个血泪经验:抽烟行为的检出率经常上不去,因为烟头在图像里小到只有几个像素,检测模型给它的置信度天然偏低。如果你把全局CONF_THRESH从 0.45 降到 0.3,抽烟能检出来一些,但误检也会明显变多——画面里的方向盘 logo、杯架上的银色杯子都会被框出来。
常见的做法是给不同类别设不同的置信度阈值。检测模型的输出里带了class_id,你完全可以在解析结果时写一个分支判断。比如手机类用 0.45,而烟这个类别单独降到 0.3,代价是多处理几个假框,但关键的违规样本保住了。这是我认为这套源码最值得改的一处逻辑。
IOU_THRESH的坑则藏在另一个方向:设得太高,NMS 可能合并不掉同一只手和手机的重叠框,一个违规行为被画成两个框,日志里就会重复记录;设得太低,框的裁剪区域不完整,分类网络拿到的可能只是一只手的局部,分类结果自然不准。默认 0.5 是平衡值,除非你明显看到重复框问题,否则不用动。
5.3 帧率与抽帧策略:实时系统的流量控制
FRAME_SKIP = 2的真实含义是每 3 帧里只推理一次,另外两帧直接用上一次的检测结果。这样系统 fps 可以提升三分之一到一半,但也会让动作检测产生「颗粒感」。做一个简单换算:视频源是 30fps,取FRAME_SKIP = 2后实际每秒只分析 10 帧;一个持续 2 秒的接电话动作,在这 2 秒里最多只能抓到 20 个关键帧,减去动作起止的模糊帧,真正有效的可能只有 10 帧左右。
如果你接的是摄像头实时流,还有一个额外因素:摄像头读取帧的速度有可能比推理速度慢,导致cap.read()阻塞,这时加再多的FRAME_SKIP也没有用。我之前的处理方式是把摄像头帧读取和推理分开在两个线程,读帧线程只管往队列里塞,推理线程按自己的节奏消费。毕设源码里不一定要做到这个程度,但你要知道瓶颈在哪里——先用time.time()分别统计读帧耗时和推理耗时,哪个大就优化哪个,而不是盲目调参。
另外有一点容易被忽略:离线视频处理和实时摄像头处理的参数组合应该不同。离线视频可以从头到尾每帧都推理,因为出错可以倒回去重看;但实时告警系统一旦漏掉就再也没有补救机会。我的做法是在config.py里写死两套预设,离线用PRESET_OFFLINE,实时用PRESET_REALTIME,切换的时候直接改SOURCE_TYPE就行,不用每次手动调三个参数。
6. 常见问题避坑与部署进阶:五个高频坑和一个交付技巧
6.1 五个高频坑的现场表现与解决方式
坑一:程序启动报错module 'cv2' has no attribute 'imshow'。
现象:明明pip install opencv-python成功了,代码跑到cv2.imshow就抛异常。原因:Linux 服务器上装的是opencv-python-headless,或者 OpenCV 版本太旧,图形接口被裁剪了。解决:在虚拟环境里重装pip uninstall opencv-python-headless后再装opencv-python。如果你不需要弹窗显示,只做离线批处理,直接用 headless 版反而更轻。
坑二:驾驶员位置换了,系统把副驾的持手机动作也算违规。
现象:换个摄像头角度,检测框频繁落在副驾位,日志里全是无效告警。原因:ROI参数还是原来的位置,没有跟着摄像头安装位置调整。解决:把当前画面的截图打印出来,手动量一下驾驶员所在区域的像素坐标,更新config.py里的ROI。grid.npy如果保存的是区域划分信息,也需要重新生成,这一点很多毕设源码的 README 里都没写。
坑三:换了自己的测试视频,结果全是normal,一条违规都检不出来。
现象:视频能正常读,框也能画出来,就是永远输出正常。原因:训练数据里正常样本比例过高,或者测试视频与训练集的亮度、色彩差异太大。解决:先看检测阶段是否框到了手部区域——如果检测框就没找到手,问题在检测模型;如果检测框正常而分类全偏,问题在分类模型。按这两条路径排查,比瞎调参数有效。数据层面可以做亮度扰动和直方图均衡化,让模型对光照变化更鲁棒。
坑四:告警有延迟,违规动作已经结束 2 秒后才弹出来。
现象:实际违规发生在第 5 秒,日志里记录的时间是第 7 秒。原因:FRAME_SKIP设置过大或推理本身耗时过长,系统根本来不及实时跟上画面。解决:分别统计单帧推理耗时和目标检测耗时,如果在 CPU 上单帧推理超过 150ms,就不要强行追求实时,把SOURCE_TYPE改成离线视频,等结果延后几秒不影响判断;如果有 GPU,把DEVICE改成cuda,通常能直接快 5 倍以上。
坑五:模型训练时 loss 降不下去,验证集上频繁震荡。
现象:训练前 20 个 epoch 里 loss 像心电图一样上下跳动,最优值始终徘徊在同一个区间。原因:学习率设置过高或 batch size 太小,优化器在损失曲面边缘反复横跳;另一个可能是数据集的标注存在大量噪声,比如一张手部被方向盘遮挡的图被标成了phone。解决:先把学习率降到当前的十分之一观察一轮,如果 loss 平滑了,说明是优化问题;如果还是震荡,检查标注。我一般会在训练脚本里加一个每 5 个 epoch 保存一次 checkpoint 逻辑,这样即使后面训练崩了,也能回退到之前的稳定权重。
6.2 部署进阶:导出 ONNX 并集成到告警系统
毕设跑通之后,如果还想往前走一步,最值得做的就是把 PyTorch 权重导出成 ONNX。PyTorch 模型依赖训练框架,导出成 ONNX 后就能脱离 PyTorch 用onnxruntime推理,体积更小,推理速度更快,部署到服务器不需要装笨重的torch全家桶。导出脚本很简单:
import torch model = torch.load("weights/best.pt", map_location="cpu") model.eval() dummy_input = torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, "weights/best.onnx", opset_version=11, input_names=["input"], output_names=["output"] ) print("export done")导出后验证一下输出是否一致,用onnxruntime跑一次试试:
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("weights/best.onnx") input_name = sess.get_inputs()[0].name # 假设这里有一张预处理后的图片,shape 为 (1,3,640,640) result = sess.run(None, {input_name: preprocessed_image}) print(result[0].shape)这里的关键是dummy_input的 shape 必须和训练时一致,如果你的检测模型输入是 640x640,导出时的dummy_input就不能用别的分辨率。导完之后,整个系统在部署机上只依赖onnxruntime和opencv-python,依赖链条短了一半,现场演示时翻车的概率也小一半。
我自己的习惯是:每次拿到一份毕设源码,先把配置文件和模型导出脚本走一遍,确认「没跑通前不入参调优」——省得参数改了,环境没搭建好,根本判断不了是哪个环节出了问题。从那以后,我每次给团队演示前都强制先走一遍「环境重建 → 离线视频 → 模型导出」三个步骤,确认整条链路干净了才动配置。这套方法帮我避开过很多次现场事故,希望帮到你。
本文还有配套的精品资源,点击获取