
简介一套基于YOLOv8的行人闯红灯抓拍检测系统面向计算机视觉、人工智能相关专业的毕设与课设场景功能完善且部署门槛低适合学生、教师或企业开发者直接用于项目演示与二次开发。项目完整包含Python可视化界面、模型训练与视频检测脚本、预训练权重以及完整数据集说明文档清晰简单配置即可运行。压缩包共8个文件其中3个py脚本分别承担界面交互、训练推理和视频抓拍功能3个pt权重文件覆盖不同模型规模2个txt文档提供使用说明与项目备注整体大小仅15.91MB。资源已产生核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果和标签分布图可作为答辩评审的有力支撑也便于修改扩展。目前已有73人学习适合快速完成毕设演示或课程设计落地。1. 行人闯红灯抓拍检测系统毕设演示要过的真正的技术关把YOLOv8接进行人闯红灯抓拍检测系统最容易低估的不是训练模型而是“判定闯红灯”这件事本身。你从摄像头里拿到一帧画面模型输出一个框这只是“有一个人”。至于这个人是不是闯了红灯、证据怎么留、界面会不会卡死这些才是让整个系统在答辩现场站得住的关键。很多毕设做到一半才发现检测精度能调到九十多但红灯一亮、人走过停止线程序就是不抓拍或者同一人一秒弹十几次违规记录。这套系统适合用YOLOv8做算法核心、又有课程设计或毕设交付压力的读者。下面按我落地这类项目的顺序把模型选型、红灯判定逻辑、PyQt5界面组织和部署踩坑讲透。2. 把检测底座立住YOLOv8模型选型与行人数据集的预处理行人闯红灯抓拍检测系统的第一层是检测也就是从画面里找到行人。YOLOv8这一步很容易让人误判工作量coco预训练模型的80个类别里本来就带person直接用yolov8n.pt跑行人就能被框出来不需要为了“识别行人”重新训练。真正要你动手处理数据集的是另外两件事一是让模型在固定视角下对负样本更可控二是想清楚信号灯状态从哪来。这两个问题理解透了后边的闯红灯判定才有地基。2.1 选n还是sCPU部署下的帧率、精度和误检权衡选哪个YOLOv8版本取决于你的推理环境。用CPU跑的话n参数量在3M左右640输入分辨率下我的实测能到10到15帧界面上算流畅s参数约11M精度高一些但CPU推理掉到5帧上下预览画面会明显一卡一卡。要是机器有RTX3060级别以上的独立显卡s更稳误检和漏检都比n好。l和x在这类单类别任务里没有性价比直接不考虑。yolov8网络结构图里改动最大的在Head部分anchor-free的回归方式对行人这种中等尺寸目标帮助不算明显真正影响远距离行人检出的是输入分辨率。640下镜头远端30米外的行人只有几十像素高检测器容易漏。把imgsz调到960召回会涨推理时间接近翻倍CPU环境下得不偿失。我的习惯是先按640跑通全流程再回头用评估脚本确认要不要提分辨率先把管线搭稳再抠精度。2.2 行人和信号灯的数据从哪来预训练权重、识别红绿灯机制这里有一个关键设计决策信号灯状态不一定要用目标检测识别。真实道路上的闯红灯抓拍系统信号灯状态是从信号机读I/O信号过来的不是拿摄像头去看灯。毕设没有真实信号机常见做法是三种UI里手动切换红绿灯、给视频片段打时间轴标签、把信号灯也做成检测或分类模型。我不建议把红绿灯塞进YOLOv8检测——路口监控画面里信号灯在远端只有几十像素低照度下黄灯和红灯颜色接近检测器经常漏。如果压缩包里给了完整数据集先别急着训练。第一步是检查目录结构是不是YOLO格式的标准布局datasets下要有images/train和labels/train、images/val和labels/val再加一个data.yaml。标注类别里如果只有person一个class说明信号灯状态走的是模拟信号源路线如果还有red_light这种类别说明作者做的是视觉识别路线。这个判断决定了你后边判定逻辑的写法一定先看再动手。2.3 要用自己的数据微调怎么办LabelMe标注后转YOLO格式如果你的场景是固定视角的实验室门口或校园路口继续用coco预训练权重直接跑通常也行行人检测的泛化能力摆在那。但想让模型对人行横道附近的典型负样本垃圾桶、树影、抱着箱子的人更稳定就得自己标注微调。常用流程是LabelMe框出目标导出json再转成YOLO的txt格式核心转换脚本如下import json import os label_map {person: 0, red_light: 1, green_light: 2} def convert_labelme_to_yolo(json_path, out_txt): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in label_map: continue points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x1, y1, x2, y2 min(xs), min(ys), max(xs), max(ys) xc (x1 x2) / 2 / img_w yc (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{label_map[label]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines))转换逻辑不复杂但有四个边界坑。LabelMe的points是任意四边形取min/max变外接矩形没问题但你在标注时尽量画正矩形斜的框会把背景包进来。YOLO格式的中心点坐标用图像宽高做归一化分母写反是这类转换脚本最常见的错误。类别索引必须和data.yaml里names顺序一致从0开始。最后一个坑是数据集分辨率如果混着1080p和720p转换脚本必须在循环里逐图读尺寸不能在外面取固定值否则标注全错。2.4 训练命令与必调参数从跑通到收敛的知识点数据就绪后训练命令我直接给一份最小可用版本yolo detect train \ datadatasets/redlight/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/train \ nameredlight_v1model用yolov8n.pt而不是custom.yaml目的是加载coco预训练权重做迁移学习。batch16在8G显存上能跑OOM就降到8这两个参数都不用纠结。patience20表示20个epoch指标不涨就早停不用干等100轮。训练跑完看runs/train/redlight_v1/results.png主loss、验证集box_loss和cls_loss都在一张图里验证集最后一轮的PR曲线才是决定置信度阈值的关键找曲线拐点对应的confidence值把检测阈值设到那个点附近比默认的0.25好用得多。如果mAP50不到0.8先检查标注有没有类别错位再考虑把n换成s不要一上来换大模型。3. 闯红灯判定逻辑从“识别到人”到“确认违规”的中间层检测模型输出一堆行人框但闯红灯是一个时空事件红灯状态下某个行人越过了停止线。这层逻辑不在模型里在你自己写的代码里也是答辩时老师最可能深挖的地方。做不好会出现两种尴尬人走了好远才弹抓拍或者同一个人被连续抓十几帧。下边把这层拆成信号源、越线判定、轨迹ID、证据落盘四部分讲。3.1 三种信号灯状态来源硬件读取、视觉识别、模拟信号信号灯状态是闯红灯判定的前提选错了方案后面全是坑。真实工程里信号灯状态从信号机的RS485或网络接口读取延时稳定可靠交管系统不会拿摄像头去看红绿灯。毕设没有这个条件常见是这三种替代方案方案实现成本可靠性适用场景硬件I/O读取信号机状态高需要真实信号机或单片机模拟高真实路口工程视觉识别信号灯区域中单独训练一个小分类器中受逆光和夜景影响想加分的毕设模拟信号源UI手动切换/视频时间轴低代码里一个状态变量可控毕设演示首选推荐第三种。现场演示时手动切红绿评审老师一眼就能看懂信号源和抓拍的对应关系这本身就是逻辑展示。跑录好的视频回放时把红灯时间段做成时间轴按播放进度自动切换状态千万别边播边手点因为人工切换的时机漂移会让抓拍效果看起来像抽风。3.2 虚拟线圈判定脚底点、归一化坐标和平滑判定几何的核心是虚拟线圈也就是在画面里固定一条停止线行人脚底越过它且信号灯为红判定违规。三个细节必须处理。第一比位置用检测框的什么点。框中心点不行摄像头斜着俯视路口时行人在近处框中心在胸口高度人还没过线就算越线了误报高。正确做法是框底边中点也就是脚底点它跟地面接触关系最稳定。第二坐标全部用归一化值不要用像素。模型输出的xywhn本身是归一化的停止线也存成0到1之间的小数摄像头分辨率变了不用重新标定。第三脚底点在跟踪过程中会跳帧与帧之间上下波动十几个像素直接比会带来边界抖动做一层指数移动平均能明显减少误触发平滑系数选0.3到0.5。判定类的实现我直接给出来项目里就是这么用的class CrossingJudge: def __init__(self, stop_line_y0.80, red_hold_frames5, smooth0.4): self.stop_line_y stop_line_y # 停止线归一化坐标0~1 self.red_hold_frames red_hold_frames # 红灯稳定多少帧后才允许判定 self.smooth smooth # 脚底点平滑系数 self.red_counter 0 self.violated_ids set() self.foot_pos {} def update(self, tracks, signal_state): # tracks: {track_id: (xc, yc, w, h)}全部是归一化坐标 if signal_state ! red: self.red_counter 0 self.violated_ids.clear() return [] self.red_counter 1 if self.red_counter self.red_hold_frames: return [] violations [] for track_id, box in tracks.items(): xc, yc, w, h box foot_y yc h / 2 if track_id not in self.foot_pos: self.foot_pos[track_id] foot_y else: self.foot_pos[track_id] ( self.smooth * foot_y (1 - self.smooth) * self.foot_pos[track_id] ) if self.foot_pos[track_id] self.stop_line_y and track_id not in self.violated_ids: violations.append((track_id, box, self.foot_pos[track_id])) self.violated_ids.add(track_id) return violations说明一下两个关键参数。red_hold_frames5是为了防止红灯刚切换那几帧误判黄灯转红的瞬间画面里常有人走到一半严格说算闯了但演示系统里容易引发争议等红灯稳定几帧再判定更稳妥。信号灯是UI手动切换时这个值可以设成3信号源来自视频时间轴时设5到8给画面里的灯色变化留余量。stop_line_y0.80表示停止线在画面从上往下80%的位置实际值要根据摄像头安装角度标定不固定。3.3 track_id怎么分配一个不依赖外部跟踪器的轻量方案track_id的意义在于让同一个行人连续多帧的轨迹归属同一个ID否则他走过停止线的瞬间每一帧都会被视为“新的人”一秒弹出十几张违规记录。成熟方案是ByteTrack但要多装依赖和ultralytics版本偶尔打架毕设直接上学习成本偏高。我一般先用一个基于IoU的贪心匹配迷你跟踪器def iou_xyxy(box1, box2): x1 max(box1[0], box2[0]) y1 max(box1[1], box2[1]) x2 min(box1[2], box2[2]) y2 min(box1[3], box2[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area1 (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter / (area1 area2 - inter 1e-6) class LightTracker: def __init__(self, iou_thresh0.3, max_lost20): self.tracks {} self.next_id 0 self.iou_thresh iou_thresh self.max_lost max_lost def update(self, detections): unconsumed list(detections) for tid, (old_box, lost) in self.tracks.items(): best_idx -1 best_score 0 for i, det in enumerate(unconsumed): v iou_xyxy(old_box, det) if v best_score: best_score v best_idx i if best_idx 0 and best_score self.iou_thresh: self.tracks[tid] (unconsumed.pop(best_idx), 0) else: self.tracks[tid] (old_box, lost 1) for tid in list(self.tracks): if self.tracks[tid][1] self.max_lost: del self.tracks[tid] for det in unconsumed: self.tracks[self.next_id] (det, 0) self.next_id 1 return self.tracksiou_thresh0.3是调试过的值人快速走动时相邻两帧的框重叠度本来就不高尤其CPU推理帧率低的时候0.3比主流默认值更稳。max_lost20表示轨迹连续20帧没匹配上就删除大约相当于消失半秒人从树后走出来还能接回同一个ID。这套方案零额外依赖抠出来就能用毕设完全够。3.4 抓拍证据链违规瞬间前后帧与日志落盘判定违规后只保存一张当前帧不够证据容易被质疑。我习惯保存违规瞬间前后各3帧共7帧原图不带框的存一份画了检测框和违规信息的存一份。文件名用时间戳加track_id例如2024-05-20_14-33-21_id07.jpg。同时把日志写进CSV记录时间、track_id、脚底点坐标、信号灯状态、置信度这几列在答辩时按顺序讲非常有说服力。抓拍落盘逻辑接在判定返回之后import csv import datetime import cv2 def save_violation(frame_queue, judge_result, det_conf, log_path): ts datetime.datetime.now().strftime(%Y%m%d_%H%M%S) for tid, box, foot_y in judge_result: base fcaptures/{ts}_id{tid:02d} cv2.imwrite(base _raw.jpg, frame_queue[-1]) with open(log_path, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([ts, tid, round(foot_y, 4), round(det_conf, 3), red])frame_queue用collections.deque(maxlen7)实现就是一个环形缓冲始终保留最近7帧。保存视频片段相比截图麻烦不少回放演示用图就够了。这层逻辑做到位整个系统的证据链才算闭合。4. 可视化界面怎么组织PyQt5下的采集、推理和绘制三线程标题里写了带可视化界面这一步的分量经常被低估。答辩时老师不会去跑命令行他只看界面上能不能出实时画面、红色警告能不能弹出来、抓拍记录能不能查看。界面翻车基本都卡在一个问题上一跑起来就卡死。一个能看的界面背后是摄像头采集、模型推理、UI刷新三件事的并发关系。4.1 界面方案对比PyQt5、Tkinter与纯OpenCV窗口监控类系统的界面方案实际就三个。Tkinter上手最快但图像列表、视频播放、表格控件做出来很粗糙展示效果吃亏。纯OpenCV窗口就是cv2.imshow加imwrite没有真正的控件能力功能完善谈不上。PyQt5学习曲线陡一点但QThread线程支持成熟、表格列表组件齐全、QSS能美化是这类系统的合适架子。拿到毕设压缩包后先看源码组织一般会有main.py入口、detector、ui、utils这几个目录重点看main.py里模型和界面是串行还是分离串行写法基本就是卡死的伏笔。4.2 线程分工QThread采集、推理线程与信号槽数据流卡顿的核心原因是分不清“该在哪个线程做什么”。PyQt的UI线程只能跑绘图和事件循环摄像头read是阻塞的模型predict更阻塞两个堆在UI线程里界面必然卡死。我的分工是两条QThread加一条主线程采集线程只管VideoCapture.read读出一帧就放进队列推理线程从队列取帧跑predict输出结果信号UI线程只负责把帧画到QLabel上、把违规信息写进表格。摄像头采集可能是20fps的设备资源CPU推理可能只有5fps中间必须用队列缓冲不能让慢的拖死快的。另一个细节是模型加载只能做一次放在推理线程的run开头千万别在predict循环里传路径让ultralytics重新解析模型文件。4.3 主界面最小骨架显示画面、违规列表和手动信号切换界面布局按功能拆三块顶部视频显示区一个QLabel左侧违规记录列表QTableWidget显示时间、行人ID、置信度右侧信号灯状态区两个按钮切红灯绿灯加一个当前状态指示灯。这个布局最容易被评审理解。核心骨架如下from PyQt5.QtWidgets import QMainWindow, QLabel, QTableWidget, QPushButton from PyQt5.QtGui import QImage, QPixmap import cv2 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(行人闯红灯抓拍检测系统) self.label_view QLabel() self.table QTableWidget(0, 3) self.table.setHorizontalHeaderLabels([时间, 行人ID, 置信度]) self.btn_red QPushButton(红灯) self.btn_green QPushButton(绿灯) self.signal_state green self.capture_thread CaptureThread(0) self.detect_thread DetectThread() self.capture_thread.frame_signal.connect(self.detect_thread.queue.put) self.detect_thread.result_signal.connect(self.update_frame) def set_signal(self, state): self.signal_state state def update_frame(self, payload): frame, results payload rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) qimage QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format_RGB888) self.label_view.setPixmap(QPixmap.fromImage(qimage)) # 在这里把results里的框画到frame上再调用judge.update完成判定set_signal只是改一个字符串状态判定逻辑每帧从主窗口读这个变量信号源和检测就解耦了。以后换成视频时间轴驱动只需要把set_signal改成按播放进度自动调用。update_frame里务必在转换QImage之前对frame做一次copy否则下一次推理会改写正在显示的numpy数组画面会出现撕裂闪烁。4.4 记录导出截图、视频片段和CSV日志抓拍截图和CSV在3.4已经落盘界面里还要有一个“导出记录”动作把当天的违规汇总成一个CSV方便展示统计数字。做法不复杂内存维护一个list每条违规记录是一个dict点导出时循环写文件。另一个演示效果很好的功能是“回放抓拍”点击表格某一行在弹窗里显示该ID保存的那一帧画面。这功能实现简单但评审看到你点一下就能重现违规瞬间会认可系统的证据逻辑。注意视频片段回放要处理编码器兼容性图片轮播则没有这个问题演示场景优先用图片。5. 部署与运行的避坑检查五个常见的翻车现场这类项目最占时间的往往不是写代码是部署环境和调通流程。下面五个场景是从我的血泪经验里筛出来的高频问题按现象、原因、解决三段写可以逐条对号入座。5.1 环境装完import就崩numpy和opencv的版本冲突现象pip install ultralytics很顺利python里import ultralytics直接报错常见的是AttributeError: module numpy has no attribute bool8或者cv2版本不对导致找不到函数。在Ubuntu 20.04上搭建yolov8环境cpu版本时尤其容易中招。原因ultralytics对numpy版本有约束老环境里的numpy还是1.19新装进来的torch或opencv会把版本关系踩乱。系统python里已经有大量包残留pip的依赖解析不会主动帮你做兼容降级。解决永远用独立conda环境装别碰系统python。Python选3.9先装CPU版torch再装ultralytics让它自己解析依赖装完先跑python -c import ultralytics, torch, cv2验证。CPU环境不要装CUDA版torch白白增加几百MB还不对推理提速。注意照着网上教程裸装最容易在这个环节卡住先建干净环境再动手能省一整天的排查时间。5.2 视频打不开路径、编码和摄像头后端问题现象VideoCapture(0)能返回对象read却一直是False或者打开回放视频画面是灰的。Windows上最常见的是摄像头被占用或者笔记本默认走了MSMF后端而驱动只支持DSHOW。原因OpenCV在不同平台默认后端不一样Windows上不指定后端就会自己猜猜错了就打不开。rtsp流打不开则经常是地址里带了中文或特殊字符或者路径拼接少了反斜杠。解决USB摄像头固定写cv2.VideoCapture(0, cv2.CAP_DSHOW)别省第二个参数。rtsp地址统一用rtsp://user:passip:554/stream1标准格式。回放视频先确认路径全英文无空格仍然不行就用ffmpeg转成mp4或avi再让系统读取。5.3 红灯判定不触发别急着怀疑检测模型现象视频里行人明显在红灯状态走过停止线程序就是不抓拍或者慢了一两秒才弹。很多人第一反应是模型漏检跑去看检测框才发现框是有的问题出在判定层。原因常见三个。停止线坐标用像素写死而模型输出的坐标经过letterbox是缩放过的两套坐标系没对上信号灯时间轴和行人过线的时机对不齐红灯状态在关键时刻根本没置位脚底点正好在线边缘抖动一帧过线一帧不过判定逻辑没有连续帧确认。解决先用鼠标事件在画面上点一条可见线把归一化坐标打印出来确认和斑马线边缘对齐信号源用时间轴驱动而不是手点判定类加red_hold_frames连续帧确认。三个都做了还不触发就把每帧的foot_y、stop_line_y、red_counter用print打出来跑一遍回放一眼看出卡在哪。这种逐层打印排除比乱调参数快得多。5.4 界面卡死模型加载和线程复用的问题现象界面打开后前两秒正常之后窗口拖不动风扇狂转弹“无响应”。或者关掉界面后进程退不干净命令行还挂着python进程。原因两个。一是模型加载写在UI线程里YOLO(yolov8n.pt)加载就要一两秒期间UI线程被占二是更常见的模型加载写在predict循环里每帧都重新解析权重文件read和predict一起堆在UI线程。解决模型加载只做一次放在推理线程的run开头predict循环只做推理不回UI。关闭窗口时在closeEvent里置runningFalse并调用thread.wait()防止线程挂在阻塞读取上导致进程不退。5.5 抓拍框位置偏移letterbox坐标还原的坑现象检测框在实时预览里位置正确但抓拍保存的图片里框偏了一大截画面边缘的目标偏移更明显。原因ultralytics的predict内部会对输入图像做letterboxpad到640再推理输出的框坐标会自动映射回原图所以results[0].boxes.xyxy是原图坐标直接画就正确。但很多毕设代码习惯自己先cv2.resize把帧改成正方形再喂模型输出的是resize后的坐标画回原图时必须反向缩放漏掉这一步框就会偏。解决要么直接喂原帧让ultralytics内部处理letterbox要么自己还原还原公式是原图坐标等于letterbox坐标减去pad偏移再除以缩放比例。另外别用results.plot()生成的图做保存它画在letterbox后的图上要转回原图尺寸或者自己用cv2.rectangle在原始帧上画框后者更可控。6. 验证系统的检测效果一套能量化漏报和误报的评估流程系统能跑不等于系统可靠。我建议准备一段1分钟左右的路口视频人工标出红灯时间段和行人越线事件作为真值然后让系统跑同一段视频三次记录每次抓拍的时间戳。这套流程花不了太多时间但比“看起来能抓”有说服力得多。真值和抓拍日志对齐后定义三个指标TP是红灯期间有人越线、系统抓到且track_id对得上FP是系统抓到但真值里没有违规FN是真值里有违规但系统没抓到。同一个行人连续被系统抓两次只算一次TP这里track_id就派上用场了。精确率PrecisionTP/(TPFP)召回率RecallTP/(TPFN)毕设做到召回大于0.9、精确率大于0.8已经能站住脚。指标不达标时调参顺序我固定不变先动置信度阈值再动停止线位置最后动red_hold_frames一次只动一个改完跑一遍回放记录指标。置信度阈值降低会提召回但误报增多停止线往画面远处移会让判定更严格red_hold_frames增大能滤掉信号灯切换瞬间的抖动但会漏掉红灯刚亮时已经越线一半的行人。经验起点是conf0.45、stop_line_y0.80、hold5然后按评估结果微调。我现在的习惯是拿到这类抓拍项目先把信号源和停止线坐标对时再碰模型参数。信号源对不齐后面全是白调。希望这些写法帮你在做行人闯红灯抓拍检测系统时少走弯路部署和调试顺利。本文还有配套的精品资源点击获取