
简介这份资源是面向计算机相关专业学生与项目实战学习者的高速公路车辆违规停车检测系统完整源码包基于OpenMMLab生态中的MMDetection、MMRotate、MMSegmentation等框架实现无人机视角下的车辆目标检测、多目标跟踪、禁停区语义分割、速度估计与违规停车报警适用于高速公路监控、智能交通与安全管理等场景可作为毕业设计、课程设计或期末大作业的参考方案。压缩包共27个文件以20个Python脚本为核心涵盖检测、分割、跟踪与工具模块另含2个pth模型权重、1个mp4演示视频、1个md说明文档及txt依赖清单整体约53.53MB目录按configs、projects、tools、utils等模块划分结构清晰便于二次开发。目前已有65人学习下载。项目代码完整可运行配套文档说明与预训练权重能帮助读者快速理解检测、分割与跟踪的串联逻辑掌握违规停车判定的实现思路并在此基础上完成功能扩展与调试排错。1. 高速应急车道违停检测从一段固定机位视频说起很多做计算机毕业设计的同学选题时最头疼的不是写不出代码而是选了一个「看起来能做、实际上跑不起来」的题目。高速公路车辆违规停车检测系统就是这样一个典型听起来有场景、有痛点、有落地价值但真正动手时才发现公开数据集难找、标注成本高、模型调完不知道对不对。我当初选这个方向是因为手头正好有一段固定机位的高速监控视频画面里应急车道上时不时有车停下来有的停几十秒就走有的停了好几分钟。这段视频成了整个项目的起点。这个系统要解决的核心问题很具体给定一段高速公路监控视频自动判断画面中是否有车辆在应急车道或行车道内违规停车并输出违规发生的时间段和位置。它适合计算机、电子信息、软件工程等专业的毕业设计也适合想入门视频目标检测与轨迹分析的 Python 学习者。整套方案用 Python 实现依赖 OpenCV、YOLO 系列检测器和简单的轨迹跟踪逻辑不需要 GPU 集群一台带独显的笔记本就能跑通。下面我从技术选型、数据准备、检测实现、违规判定到避坑排查把这条链路完整走一遍。2. 技术选型与整体架构为什么不用纯深度学习端到端方案2.1 检测跟踪规则判定的三段式架构常见做法是把违规停车检测拆成三个模块目标检测负责找出画面中的车辆多目标跟踪负责给每辆车分配稳定 ID 并记录轨迹规则判定负责根据轨迹判断是否违停。为什么不直接训一个端到端模型输出「违停/正常」原因很实际违停的本质是「同一辆车在同一位置停留超过阈值时间」这是一个时序逻辑问题不是单帧分类问题。端到端模型需要大量标注了违停时间段的视频片段标注成本极高而且换一个摄像头角度就要重新标。三段式架构的好处是每一段都可以独立调试和替换检测器可以用现成的预训练权重跟踪器可以用轻量级算法规则判定部分完全可解释。我一般会推荐 YOLOv8n 或 YOLOv5s 作为检测器因为这两个模型在 COCO 数据集上对 car、truck、bus 的检测精度已经足够而且推理速度快640×640 输入下在 GTX 1650 上能跑到 40 FPS 以上。跟踪器用 ByteTrack 或简单的 IOU 匹配加卡尔曼滤波就够不需要上 ReID 模型。规则判定部分用一个字典维护每辆车的状态车辆 ID、最近 N 帧的位置、首次进入应急车道区域的帧号、累计停留帧数。2.2 环境搭建与依赖安装先确认 Python 版本建议 3.8 到 3.10太新的版本有些库轮子还没跟上。用 conda 或 venv 建一个干净环境然后装依赖。# 创建虚拟环境 python -m venv highway_env source highway_env/bin/activate # Windows 用 highway_env\Scripts\activate # 安装核心依赖 pip install opencv-python4.8.1.78 pip install ultralytics8.0.200 pip install numpy1.24.3 pip install filterpy1.4.5 pip install scipy1.11.3ultralytics这个包自带 YOLOv8 的推理和训练接口装完就能直接用预训练权重。filterpy用来做卡尔曼滤波如果不想引入额外依赖也可以自己写一个简化的位置预测。opencv-python负责视频读写和画框显示。注意不要装opencv-contrib-python和opencv-python两个包会冲突。2.3 项目目录结构highway_parking_detection/ ├── configs/ │ └── config.yaml # 阈值、区域坐标、模型路径 ├── data/ │ ├── videos/ # 测试视频 │ └── outputs/ # 结果视频和日志 ├── src/ │ ├── detector.py # YOLO 检测封装 │ ├── tracker.py # 跟踪与轨迹管理 │ ├── violation.py # 违停判定逻辑 │ └── visualizer.py # 画框、画区域、写日志 ├── main.py # 主入口 └── requirements.txt这个结构的好处是每个模块职责清晰写毕业设计论文时也方便按模块画流程图。config.yaml里放所有可调参数换视频或换摄像头角度时只改配置不改代码。# configs/config.yaml model: weights: yolov8n.pt conf_threshold: 0.45 iou_threshold: 0.5 classes: [2, 5, 7] # car, bus, truck tracker: max_lost_frames: 30 iou_match_threshold: 0.3 violation: emergency_lane_polygon: [[820, 300], [1180, 300], [1200, 680], [780, 680]] stationary_pixel_threshold: 15 stationary_frame_threshold: 150 # 约 5 秒 30fps min_track_frames: 20 video: input_path: data/videos/test_highway.mp4 output_path: data/outputs/result.mp4 display: trueemergency_lane_polygon是应急车道在画面中的多边形区域需要根据具体视频手动标定。stationary_pixel_threshold是判断车辆是否静止的像素位移阈值太小会把抖动当成移动太大会漏掉缓慢移动的违停。stationary_frame_threshold是连续静止帧数阈值30fps 下 150 帧约等于 5 秒实际可根据交规和场景调整。3. 车辆检测与多目标跟踪把 YOLO 和 ByteTrack 串起来3.1 YOLOv8 推理封装与类别过滤YOLOv8 默认在 COCO 上训练类别 2 是 car5 是 bus7 是 truck。高速场景主要关心这三类其他类别如 person、bicycle 可以过滤掉减少误检。# src/detector.py import numpy as np from ultralytics import YOLO class VehicleDetector: def __init__(self, weights, conf_thres0.45, iou_thres0.5, classesNone): self.model YOLO(weights) self.conf_thres conf_thres self.iou_thres iou_thres self.classes classes if classes else [2, 5, 7] def detect(self, frame): results self.model.predict( sourceframe, confself.conf_thres, iouself.iou_thres, classesself.classes, verboseFalse ) detections [] for r in results: boxes r.boxes if boxes is None: continue for box in boxes: x1, y1, x2, y2 box.xyxy[0].cpu().numpy() conf float(box.conf[0].cpu().numpy()) cls_id int(box.cls[0].cpu().numpy()) detections.append({ bbox: [x1, y1, x2, y2], confidence: conf, class_id: cls_id }) return detectionspredict方法返回的结果里boxes.xyxy是左上角和右下角坐标boxes.conf是置信度。classes参数直接传给 YOLO让模型只输出指定类别比检测完再过滤效率高。verboseFalse关掉每帧的日志输出不然控制台会刷屏。如果显存不够可以把imgsz设成 480 或 320但小目标检测精度会下降高速场景车辆通常比较大480 一般够用。3.2 基于 IOU 和卡尔曼滤波的轻量跟踪器ByteTrack 的核心思想是「高分检测框先匹配低分检测框再补匹配」但毕业设计里自己实现一个简化版更能体现工作量。下面这个跟踪器用 IOU 做帧间匹配用卡尔曼滤波预测下一帧位置维护轨迹的丢失帧数。# src/tracker.py import numpy as np from filterpy.kalman import KalmanFilter from scipy.optimize import linear_sum_assignment def iou_batch(bb_test, bb_gt): 计算两组框之间的 IOU 矩阵 bb_gt np.expand_dims(bb_gt, 0) bb_test np.expand_dims(bb_test, 1) xx1 np.maximum(bb_test[..., 0], bb_gt[..., 0]) yy1 np.maximum(bb_test[..., 1], bb_gt[..., 1]) xx2 np.minimum(bb_test[..., 2], bb_gt[..., 2]) yy2 np.minimum(bb_test[..., 3], bb_gt[..., 3]) w np.maximum(0., xx2 - xx1) h np.maximum(0., yy2 - yy1) wh w * h o wh / ((bb_test[..., 2] - bb_test[..., 0]) * (bb_test[..., 3] - bb_test[..., 1]) (bb_gt[..., 2] - bb_gt[..., 0]) * (bb_gt[..., 3] - bb_gt[..., 1]) - wh) return o class Track: def __init__(self, bbox, track_id): self.id track_id self.bbox bbox # [x1, y1, x2, y2] self.history [bbox] self.lost_frames 0 self.age 1 self.kf self._init_kalman(bbox) def _init_kalman(self, bbox): kf KalmanFilter(dim_x7, dim_z4) kf.F np.array([[1,0,0,0,1,0,0], [0,1,0,0,0,1,0], [0,0,1,0,0,0,1], [0,0,0,1,0,0,0], [0,0,0,0,1,0,0], [0,0,0,0,0,1,0], [0,0,0,0,0,0,1]], dtypefloat) kf.H np.array([[1,0,0,0,0,0,0], [0,1,0,0,0,0,0], [0,0,1,0,0,0,0], [0,0,0,1,0,0,0]], dtypefloat) kf.R[2:,2:] * 10. kf.P[4:,4:] * 1000. kf.P * 10. kf.Q[-1,-1] * 0.01 kf.Q[4:,4:] * 0.01 cx (bbox[0] bbox[2]) / 2 cy (bbox[1] bbox[3]) / 2 w bbox[2] - bbox[0] h bbox[3] - bbox[1] kf.x[:4] np.array([[cx, cy, w, h]]).T return kf def predict(self): self.kf.predict() return self.kf.x[:4].flatten() def update(self, bbox): cx (bbox[0] bbox[2]) / 2 cy (bbox[1] bbox[3]) / 2 w bbox[2] - bbox[0] h bbox[3] - bbox[1] self.kf.update(np.array([[cx, cy, w, h]]).T) self.bbox bbox self.history.append(bbox) self.lost_frames 0 self.age 1卡尔曼滤波的状态向量是[cx, cy, w, h, vx, vy, vw]前四个是中心点坐标和宽高后三个是速度分量。F是状态转移矩阵H是观测矩阵。R和Q是噪声协方差调大R表示更相信预测调大Q表示更相信观测。实际调参时如果发现框抖动厉害把R调大如果跟踪跟不上快速移动的车把Q调大。3.3 跟踪主循环与 ID 管理# src/tracker.py 续 class MultiObjectTracker: def __init__(self, max_lost30, iou_threshold0.3): self.tracks [] self.next_id 1 self.max_lost max_lost self.iou_threshold iou_threshold def update(self, detections): # 预测所有现有轨迹 for t in self.tracks: t.predict() if len(self.tracks) 0: for det in detections: self.tracks.append(Track(det[bbox], self.next_id)) self.next_id 1 return self.tracks track_boxes np.array([t.bbox for t in self.tracks]) det_boxes np.array([d[bbox] for d in detections]) if detections else np.empty((0,4)) if len(det_boxes) 0: iou_matrix iou_batch(det_boxes, track_boxes) row_ind, col_ind linear_sum_assignment(-iou_matrix) matched_track set() matched_det set() for r, c in zip(row_ind, col_ind): if iou_matrix[r, c] self.iou_threshold: continue self.tracks[c].update(det_boxes[r]) matched_track.add(c) matched_det.add(r) # 未匹配的检测新建轨迹 for i in range(len(det_boxes)): if i not in matched_det: self.tracks.append(Track(det_boxes[i], self.next_id)) self.next_id 1 # 未匹配的轨迹增加丢失计数 for j in range(len(self.tracks)): if j not in matched_track: self.tracks[j].lost_frames 1 else: for t in self.tracks: t.lost_frames 1 # 清除丢失太久的轨迹 self.tracks [t for t in self.tracks if t.lost_frames self.max_lost] return self.trackslinear_sum_assignment是匈牙利算法在 IOU 矩阵上找最优匹配。-iou_matrix是因为这个函数求最小代价取负号就变成求最大 IOU。max_lost控制轨迹保留时间30 帧约 1 秒车辆被短暂遮挡后还能接上。如果设太小车辆过隧道或大车遮挡时会断轨设太大同一辆车可能被分配多个 ID。4. 违停判定逻辑区域判断、静止检测与时间累积4.1 应急车道区域标定与点在多边形内判断应急车道区域用多边形表示判断车辆是否在区域内可以用车辆底部中心点或者检测框与多边形的重叠面积。底部中心点更符合实际因为车辆压线时上半部分可能在行车道但车轮已经在应急车道。# src/violation.py import cv2 import numpy as np class ViolationJudge: def __init__(self, config): self.polygon np.array(config[emergency_lane_polygon], dtypenp.int32) self.pixel_threshold config[stationary_pixel_threshold] self.frame_threshold config[stationary_frame_threshold] self.min_track_frames config[min_track_frames] self.vehicle_states {} # track_id - state dict def _bottom_center(self, bbox): x1, y1, x2, y2 bbox return (int((x1 x2) / 2), int(y2)) def _in_polygon(self, point): return cv2.pointPolygonTest(self.polygon, point, False) 0 def update(self, tracks, frame_idx): violations [] for t in tracks: if t.age self.min_track_frames: continue tid t.id bc self._bottom_center(t.bbox) in_lane self._in_polygon(bc) if tid not in self.vehicle_states: self.vehicle_states[tid] { first_in_lane_frame: None, stationary_count: 0, last_center: bc, violation_reported: False } state self.vehicle_states[tid] if in_lane: if state[first_in_lane_frame] is None: state[first_in_lane_frame] frame_idx dx abs(bc[0] - state[last_center][0]) dy abs(bc[1] - state[last_center][1]) if dx self.pixel_threshold and dy self.pixel_threshold: state[stationary_count] 1 else: state[stationary_count] 0 state[last_center] bc if (state[stationary_count] self.frame_threshold and not state[violation_reported]): violations.append({ track_id: tid, frame_idx: frame_idx, bbox: t.bbox, duration_frames: state[stationary_count] }) state[violation_reported] True else: state[first_in_lane_frame] None state[stationary_count] 0 state[violation_reported] False state[last_center] bc return violationspointPolygonTest返回正值表示点在多边形内零表示在边上负值表示在外。stationary_count是连续静止帧计数一旦位移超过阈值就清零。violation_reported防止同一辆车在同一段停留中重复报警。min_track_frames过滤掉刚出现几帧的误检避免新车 ID 还没稳定就触发判定。4.2 静止判定的像素阈值怎么定stationary_pixel_threshold这个参数最玄学。设成 5 像素摄像头轻微抖动就会把静止车判成移动设成 30 像素缓慢溜车的违停会被漏掉。我的血泪经验是先取 10 到 15 像素然后用一段已知违停的视频跑一遍看日志里stationary_count的增长曲线。如果曲线锯齿状上下跳说明阈值太小如果曲线一直不涨说明阈值太大。另一个技巧是用检测框中心点的移动而不是底部中心点因为底部中心点受车辆姿态变化影响更大。4.3 时间阈值与帧率换算stationary_frame_threshold不能直接写死 150因为不同视频帧率不同。正确做法是在配置里写秒数运行时根据cv2.CAP_PROP_FPS换算成帧数。# main.py 片段 import cv2 from src.detector import VehicleDetector from src.tracker import MultiObjectTracker from src.violation import ViolationJudge import yaml with open(configs/config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) cap cv2.VideoCapture(config[video][input_path]) fps cap.get(cv2.CAP_PROP_FPS) if fps 0: fps 30.0 config[violation][stationary_frame_threshold] int( config[violation].get(stationary_seconds, 5) * fps ) detector VehicleDetector( config[model][weights], config[model][conf_threshold], config[model][iou_threshold], config[model][classes] ) tracker MultiObjectTracker( config[tracker][max_lost_frames], config[tracker][iou_match_threshold] ) judge ViolationJudge(config[violation]) frame_idx 0 while True: ret, frame cap.read() if not ret: break detections detector.detect(frame) tracks tracker.update(detections) violations judge.update(tracks, frame_idx) for v in violations: print(f[违停] 帧 {v[frame_idx]} 车辆ID {v[track_id]} f持续 {v[duration_frames]} 帧) frame_idx 1 cap.release()把秒数写进配置代码里换算换视频时不用改逻辑。stationary_seconds默认 5 秒实际项目里可以根据交规调整有些地方应急车道停车超过 3 分钟才算违停但毕业设计演示用 5 到 10 秒就够了不然视频要跑很久才出结果。5. 避坑与排查那些让我熬夜的翻车现场5.1 检测框抖动导致静止计数反复清零现象车辆明明停着stationary_count却涨到十几就归零永远达不到阈值。原因YOLO 检测框在相邻帧之间有 2 到 5 像素的随机抖动底部中心点跟着抖位移超过pixel_threshold。解决对检测框做指数移动平均或者把pixel_threshold调到 15 以上。我一般会在Track类里加一个smooth_bbox方法用 0.7 的旧框加 0.3 的新框。5.2 车辆 ID 切换导致同一辆车被当成两辆现象一辆车停了几秒ID 从 5 变成 12违停计数重新开始。原因跟踪器在车辆被遮挡或检测置信度下降时丢失轨迹重新匹配时分配了新 ID。解决把max_lost_frames从 30 调到 60同时降低iou_match_threshold到 0.2。如果还不行可以在违停判定里用位置哈希做二次关联但毕业设计里调参就够了。5.3 应急车道多边形坐标写死导致换视频就失效现象换了一段摄像头角度不同的视频所有车都不在应急车道区域内。原因emergency_lane_polygon是针对第一段视频手动标定的换视频后画面布局变了。解决写一个简单的标定脚本用cv2.setMouseCallback让用户点击四个角点把坐标存回配置文件。这个脚本不到 50 行但能省下大量手动改坐标的时间。5.4 置信度阈值太低导致路牌、护栏被当成车现象画面里出现了不存在的车辆框违停日志里多出莫名其妙的记录。原因conf_threshold设成了 0.25YOLO 把一些纹理类似车辆的物体误检了。解决把conf_threshold提到 0.45 到 0.5同时确保classes只保留 car、bus、truck。如果还有误检可以在检测后加一个宽高比过滤正常车辆宽高比在 0.8 到 3.0 之间太扁或太方的框直接丢掉。5.5 视频读写帧率不一致导致输出视频快放或慢放现象处理后的结果视频比原视频快很多违停时间段对不上。原因cv2.VideoWriter的帧率参数没有用原视频的fps默认写了 30但原视频可能是 25 或 15。解决读视频时先取fps写视频时用同一个值。如果原视频fps是 0 或异常值回退到 25。另外注意VideoWriter的尺寸要和原视频一致不然写出来的视频打不开。6. 进阶技巧用轨迹热力图和日志回放验证检测效果跑通基础流程后怎么证明你的系统真的有效答辩时老师不会只看你演示一段视频他们会问「你怎么知道检测是对的」。我一般会做两件事生成轨迹热力图和写结构化日志。轨迹热力图是把所有车辆的底部中心点按帧累积到一张图上用高斯模糊和伪彩色渲染。应急车道区域如果出现明显的亮斑说明有车在那里长时间停留和违停日志能对上。# src/visualizer.py 片段 import cv2 import numpy as np class HeatmapGenerator: def __init__(self, frame_shape): self.heatmap np.zeros(frame_shape[:2], dtypenp.float32) def add_point(self, point): x, y int(point[0]), int(point[1]) if 0 x self.heatmap.shape[1] and 0 y self.heatmap.shape[0]: self.heatmap[y, x] 1 def render(self, frame, alpha0.5): heat cv2.GaussianBlur(self.heatmap, (0, 0), sigmaX15) heat cv2.normalize(heat, None, 0, 255, cv2.NORM_MINMAX) heat heat.astype(np.uint8) heat_color cv2.applyColorMap(heat, cv2.COLORMAP_JET) return cv2.addWeighted(frame, 1 - alpha, heat_color, alpha, 0)add_point每帧对每辆车的底部中心点累加一次GaussianBlur的sigmaX控制热力扩散范围15 左右比较平滑。COLORMAP_JET是蓝到红的伪彩色红色区域就是停留最密集的地方。把这个热力图叠加到原视频第一帧上一眼就能看出应急车道有没有违停热点。结构化日志用 JSON Lines 格式每行一条违停记录包含时间戳、车辆 ID、持续帧数、持续秒数、检测框坐标。import json def log_violation(log_path, frame_idx, fps, track_id, bbox, duration_frames): record { frame_idx: frame_idx, timestamp_sec: round(frame_idx / fps, 2), track_id: track_id, bbox: [round(v, 1) for v in bbox], duration_frames: duration_frames, duration_sec: round(duration_frames / fps, 2) } with open(log_path, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n)日志回放的意思是拿日志里的frame_idx去原视频里跳帧截图人工核对那个位置是不是真的有车停着。我一般会抽 10 条记录做人工验证如果准确率低于 8 条就回去调conf_threshold和stationary_pixel_threshold。这个验证过程写进毕业设计论文的「实验结果与分析」章节比只放几张截图有说服力得多。最后一个习惯每次调完参数把配置文件和对应的结果日志一起存档命名带上日期和参数摘要。不然过两天老师问「你上次那个阈值是多少」你翻聊天记录都找不到。这个方案从检测到判定再到验证整条链路在普通笔记本上跑 1080p 视频大概 15 到 25 FPS毕业设计演示完全够用。希望帮到你。本文还有配套的精品资源点击获取