拓冰建站拓冰建站
首页 / 资讯中心 / 正文

YOLOv5+PyQt5:从detect.py到可交付的可视化目标检测工具

简介面向需要快速构建目标检测可视化应用的开发者这一资源将YOLOv5的检测能力与PyQt5界面相结合实现从视频文件、摄像头采集到实时输出边界框位置与类别标签的完整流程。压缩包共132个文件大小108.25MB涵盖34个Python源文件、25个YAML模型配置、3个PyTorch权重文件、4个UI界面文件同时附有mp4示例视频、jpg测试图片、shell部署脚本与gif效果演示等目录组织清晰。已有1576人学习下载适合希望理解深度学习模型与桌面端GUI集成并基于此进行二次开发的读者。通过阅读源码和运行示例可逐步掌握YOLOv5的模型加载、预处理、推理与结果解析流程以及PyQt5中事件绑定、画面刷新、边界框绘制的实现技巧对于摄像头输入还可学习OpenCV视频流处理与实时显示的方法。这一工程模板对智慧监控、安全巡检等应用场景具有直接参考价值便于在此基础上扩展模型版本或定制检测参数。 做这类型项目的朋友很多都是卡在同一个地方模型跑通了detect.py 也能出结果但拿给别人看的时候总不能让人家去命令行里敲 python detect.py —— 你需要一个界面能选视频、能开摄像头、能实时看到框和类别最好还能把坐标信息导出来。这个需求在毕业设计、车间质检演示、实验室巡检demo里太常见了但网上大部分教程只教到“如何训练yolov5”很少讲“如何把它包成一个像样的可视化工具”。这篇我把自己做这类可视化界面的完整思路、选型理由和踩坑记录写出来照着做你能少走很多弯路。1. 为什么这种可视化客户端会卡在“能跑”和“好用”之间先说个扎心的现实用OpenCV的imshow 命令行参数其实也能实现“打开视频、跑检测、显示结果”的完整流程代码量不超过100行。那你为什么要做可视化界面因为要解决三个命令行解决不了的问题一是操作门槛。命令行调参对开发者自己没问题但项目要交付、要答辩、要给不会敲命令的人演示时一个带按钮、带下拉框的界面说服力完全不一样。二是状态可视化。摄像头是否连接、当前用的是哪个模型、每帧推理耗时多少、检测到几个目标——这些信息需要一个常驻面板来展示而不是打印在终端里滚动消失。三是结果输出的可控性。界面程序里你需要让用户主动触发“保存这一帧的结果”或“把整个视频的结果导出”而不是让程序一股脑把所有日志刷到屏幕。但问题恰恰出在这里很多人的代码能力停留在“能跑通detect.py”的水平一碰到GUI编程就懵了。线程、信号槽、刷新率、资源释放这些概念如果不理解做出来的界面就是“点一下按钮卡死五秒”“窗口拖一下直接无响应”“摄像头关了但进程还在占着”。我当时的选型思路是这样的在Python生态里界面框架无非就是PyQt5/PySide2、Tkinter、Gradio或Streamlit这几种。Tkinter轻量但控件风格老旧做视频帧的实时刷新时性能一般Gradio和Streamlit是Web方案一键就能出一个漂亮的网页但它们在“实时视频流”场景下的延迟控制比较麻烦而且摄像头推流到网页端需要额外的编码转换复杂度和学习成本并不低。所以权衡下来PyQt5是我认为最合适的方案——它原生支持QImage的快速显示能用信号槽机制把推理线程和界面线程解耦控件能力强想扩展一个“实时绘制检测框”的画布也很顺手。这里也提醒一下Win10/Win11上PyQt5和PySide2的API基本通用选哪个看你心情。我习惯用PyQt5但如果你在Ubuntu或者树莓派上跑建议直接上PySide2官方对Linux的兼容维护更积极一些。界面框架确定后接下来的核心就不是“界面”了而是怎么把yolov5的推理逻辑干净地抽出来嵌进一个事件驱动的程序里。2. 先把推理引擎抽出来Detector封装是全局的基石很多人一上来就写界面写到一半发现问题全出在“推理代码和界面代码搅在一起”。所以我强烈建议不管界面做成什么样先把yolov5的检测能力封装成一个独立的Detector类这个类的职责只有一个给一张图返回检测结果。为什么要这么做因为yolov5官方仓库里的detect.py是给命令行用的它的逻辑是把“加载模型、读图、预处理、推理、后处理、画框、保存结果”全部串在一起而且默认会打印一堆日志。直接把它塞进GUI线程你很快会碰到两个问题模型推理是阻塞操作一张640x640的图在GPU上推理大概需要10到30毫秒但如果在界面主线程里执行这几十毫秒的阻塞就会让界面卡顿用户拖窗口时明显掉帧。detect.py里大量print和tqdm进度条的输出逻辑在GUI程序里毫无用处反而会拖慢性能。所以我封装Detector时只保留四个核心方法class Detector: def __init__(self, weights, conf_thres0.25, iou_thres0.45, img_size640, device0): # 加载模型、初始化参数 self.model attempt_load(weights, map_locationdevice) self.conf_thres conf_thres self.iou_thres iou_thres self.img_size img_size self.device device def preprocess(self, frame): # letterbox等比例缩放 padding到640x640 img, ratio, (dw, dh) letterbox(frame, new_shapeself.img_size) img img[:, :, ::-1].transpose(2, 0, 1) # BGR转RGB且HWC转CHW img np.ascontiguousarray(img) img torch.from_numpy(img).to(self.device) img img.float() / 255.0 if img.ndimension() 3: img img.unsqueeze(0) return img, ratio, (dw, dh) def infer(self, frame): img, ratio, (dw, dh) self.preprocess(frame) with torch.no_grad(): pred self.model(img)[0] pred non_max_suppression(pred, self.conf_thres, self.iou_thres) return self.postprocess(pred, frame.shape, ratio, (dw, dh)) def postprocess(self, pred, orig_shape, ratio, pad): # 将letterbox坐标还原到原图坐标 results [] for det in pred: if len(det): det[:, :4] scale_coords(self.img_size, det[:, :4], orig_shape).round() for *xyxy, conf, cls in det: results.append({ bbox: [int(x) for x in xyxy], confidence: float(conf), class_id: int(cls), class_name: self.model.names[int(cls)] }) return results这几个方法是参考yolov5官方实现思路写的如果你要直接抄作业注意几个点letterbox和scale_coords一定要配套用否则坐标会偏移。letterbox把原图等比缩放并填充到640x640检测框的坐标是letterbox后的坐标系必须通过scale_coords反向映射回原图尺寸否则你画框的位置会明显偏掉。non_max_suppression的置信度阈值和NMS阈值建议直接在Detector初始化时通过conf_thres和iou_thres传进去而不是在推理时每次手写。后续如果需要做界面上的“置信度滑块”只需要在调用infer前改这两个属性就行。封装好这个类之后你可以先写个简单的脚本验证一下det Detector(weightsyolov5s.pt) cap cv2.VideoCapture(test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break results det.infer(frame) for r in results: x1, y1, x2, y2 r[bbox] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{r[class_name]} {r[confidence]:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow(test, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这一步验证通过说明你的推理引擎是干净、稳定、可复用的。接下来再动界面你的心态就不一样了——界面代码只负责三件事拿帧、调infer、显示结果。所有的检测逻辑都被隔离在了Detector里出了问题也很容易定位。3. 界面框架搭建视频文件、摄像头和实时预览三合一当你有了独立的Detector之后界面部分就变得很纯粹了。我的界面布局思路是左边一个大的视频预览区右边一个控制面板。控制面板分三个功能区视频文件选择区、摄像头设备区、检测参数调节区。底部再加一个状态栏显示当前的FPS、检测目标数量和耗时。先说视频文件检测。这个功能本质上是“用OpenCV读取视频 循环推理 实时显示”但有个细节很多人忽视视频文件的读取和界面刷新完全不在一个节奏上。视频可能是25帧的但你的推理速度可能只有15帧这意味着你不能简简单单地“读一帧显示一帧”。我当时的设计是视频读取线程只负责往队列里放帧推理线程从队列里取帧去检测检测完把结果帧通过信号发给界面线程显示。如果队列满了就丢帧保证界面永远显示最新的一帧避免延迟累积导致画面越来越慢。摄像头检测的坑更多。首先是摄像头的索引问题笔记本自带摄像头通常是0外接USB摄像头可能是1。界面设计上建议做一个下拉框让用户选设备号而不是写死。其次是摄像头的分辨率设置很多USB摄像头默认是640x480但如果你用的是1920x1080的摄像头直接用cap.read()拿到的帧会非常大检测耗时直接翻倍。所以我在打开摄像头时会主动尝试设置分辨率cap cv2.VideoCapture(camera_index) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) cap.set(cv2.CAP_PROP_FPS, 30)如果设置失败也没关系用摄像头默认参数就行但你要在界面上显示实际分辨率方便用户判断。然后是线程模型。PyQt5里强烈建议用QThread而不是Python的threading.Thread因为QThread配合信号槽可以直接跨线程更新界面class CameraThread(QThread): change_pixmap_signal pyqtSignal(np.ndarray) fps_signal pyqtSignal(float) def __init__(self, camera_index, detector): super().__init__() self.camera_index camera_index self.detector detector self.running True def run(self): cap cv2.VideoCapture(self.camera_index) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) prev_time time.time() while self.running: ret, frame cap.read() if not ret: continue results self.detector.infer(frame) annotated draw_results(frame, results) self.change_pixmap_signal.emit(annotated) curr_time time.time() fps 1.0 / (curr_time - prev_time) self.fps_signal.emit(fps) prev_time curr_time cap.release()这个设计中我在run()里做的是“读帧-推理-绘制-发信号”的完整链路信号发出后界面主线程的槽函数负责把QImage显示到QLabel上。关键点在于绝对不能在工作线程里直接调用任何界面控件的更新方法Python的GIL让线程安全看起来“好像没问题”但PyQt的界面控件并不是线程安全的直接操作轻则闪烁、崩溃重则整个程序无响应。信号槽机制是唯一正确的跨线程通信方式。draw_results函数就是把Detector输出的results列表画到帧上def draw_results(frame, results): for r in results: x1, y1, x2, y2 r[bbox] color get_color(r[class_id]) # 每个类别给一个固定颜色 cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) label f{r[class_name]} {r[confidence]:.2f} cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return frame视频文件检测的线程模型和摄像头基本一样只是来源从摄像头索引变成了文件路径并且在视频读完后要发一个“播放结束”的信号让界面自动把按钮状态复位。摄像头设备还有一类需要特别处理就是RTSP网络摄像头。很多工业现场用的IP摄像头输出的是RTSP流在界面里其实也很简单把VideoCapture的参数从设备索引换成rtsp://user:passwordip:port/stream1即可。但在界面设计上要注意用户输入的RTSP地址可能不对、网络可能不通所以“连接”按钮的点击要放到单独的线程里做超时控制不能在主线程里直接去连否则地址不对的话界面会卡住十几秒。4. 检测参数调节与模型热切换让界面真正“可配置”一个只有“打开文件/打开摄像头”两个按钮的界面其实和命令行工具没什么本质区别。真正让它变得专业的是参数的实时可调性。我在界面右侧放了一组滑块和下拉框效果非常直观置信度阈值滑块范围0.05到0.95默认0.25。这个滑块直接对应Detector的conf_thres属性。NMS阈值滑块范围0.1到0.9默认0.45对应iou_thres。模型选择下拉框支持yolov5s.pt、yolov5m.pt、yolov5l.pt也可以让用户自定义选择其他训练好的权重文件。推理尺寸下拉框320/416/512/640对应img_size参数。滑块滑动的回调里不要重新创建模型只需要更新Detector的现有属性def on_conf_slider_changed(self, value): self.detector.conf_thres value / 100.0 self.conf_value_label.setText(f{value / 100.0:.2f})这个实现极其简单但效果是立竿见影的。比如在户外摄像头场景下默认0.25的置信度会有一堆误检你把滑块调到0.5画面立刻干净很多。而在室内近距离检测场景下0.15反而能召回更多的目标。这就是“可视化界面”相对于命令行的真正价值——你不需要重新运行程序就能实时感受参数对结果的影响。模型热切换稍微复杂一点因为Detector初始化时已经加载了模型切换意味着重新加载权重。我当时的做法是把attempt_load的调用放到一个新的线程里切换完成前先弹一个提示不让用户重复点击class ModelLoadThread(QThread): finished_signal pyqtSignal() def __init__(self, weights, detector): super().__init__() self.weights weights self.detector detector def run(self): self.detector.model attempt_load(self.weights, map_locationself.detector.device) self.detector.model.names self.names # 确保类别名也被更新 self.finished_signal.emit()这里有一个很容易被忽略的坑换模型之后class names一定要同步更新。如果你用官方yolov5s.pt切换到自己的自定义数据集模型类别数量、类别名全部变了如果Detector里的names没有更新画框时就会显示“class 0”“class 1”这样的数字或者直接索引越界。所以我在Detector加了一个update_model方法切换时连带names一起刷新def update_model(self, weights): self.model attempt_load(weights, map_locationself.device) self.names self.model.module.names if hasattr(self.model, module) else self.model.names顺带说一句如果你在切换模型后发现显存占用没有释放记得在加载新模型前把旧模型的输出和缓存全部清掉必要时调用torch.cuda.empty_cache()。训练好的模型多了之后这种显存管理问题会非常突出。5. 结果输出位置信息、类别信息的结构化保存与展示标题里明确要求“输出结果可包含位置信息和类别等信息”这个环节对毕设和项目交付尤其重要。我做了两个层级的输出界面内实时展示和文件导出。界面内实时展示是指在检测帧的下方放一个表格控件QTableWidget每一行显示一个检测目标的信息序号、类别名称、置信度、x1、y1、x2、y2、目标宽度、目标高度。每次处理完一帧清空旧表格并填充新结果。这个表格能让人非常直观地看到“画面里有哪些目标、每个目标的位置在哪”比只画框更符合“输出位置信息”的需求。文件导出我支持两种格式一是TXT文本格式方便直接读取做后续处理frame 12 person 0.92 345 102 456 380 car 0.87 120 90 300 200 frame 13 person 0.91 348 105 460 383二是JSON格式方便和其他系统对接{ frame_id: 12, timestamp: 1634523456.78, detections: [ {class_name: person, class_id: 0, confidence: 0.92, bbox: [345, 102, 456, 380]}, {class_name: car, class_id: 2, confidence: 0.87, bbox: [120, 90, 300, 200]} ] }导出逻辑要注意一点如果你是“检测完一帧就立刻保存”那文件IO会拖慢推理速度。我的设计是加一个“是否记录结果”的开关打开后只在内存里缓存结构化的结果只存检测结果不存帧图像内存占用很低等视频处理完或者用户点击“停止”时一次性写入文件。如果用户需要导出带检测框的视频我再单独开一个分支用VideoWriter把每一帧的绘制结果写入mp4文件。这里有个经验导出的坐标一定要是原图坐标而不是检测时缩放后的坐标。很多人在后处理时没有用scale_coords还原导出的坐标和实际位置对不上后面做分析时一脸懵。你可以在界面里加一个“预览导出坐标”的小功能点击表格中的某一行就自动在预览画面上高亮显示对应的检测框和坐标线这样用户能直观确认坐标是否正确。6. 实测中的性能调优与视频流卡顿问题排查最后专门说一下性能。这个界面项目能不能“用起来舒服”很大程度上取决于你对卡顿问题的处理能力。我实测下来最容易出现的性能问题有三个第一个是“显示分辨率过高”。视频文件如果是4K的直接拿来做推理哪怕只用yolov5s帧率也会掉到个位数。但用户通常只想看看检测效果不需要4K级别的显示精度。解决方案是在读取帧之后先判断宽度如果超过1280就resize到1280再送进Detector显示和保存时按原图尺寸还原坐标。Detector内部反正会做letterbox你送进去的图更小推理更快坐标还原后依然对应原视频位置。第二个是“画框函数太慢”。如果你一帧里有几十个目标每个目标都要画矩形、写文字cv2.putText本身的性能其实还可以但要是在叠加了中文标签的情况下OpenCV原生不支持中文你不得不使用PIL去渲染性能会下降很多。我的建议是实时预览时用类别索引或者英文标签只在实际需要中文输出的界面表格里显示中文类别名不要在每个框上都用PIL渲染中文否则帧率会很难看。第三个是“线程死锁”。如果你同时开了“视频读取线程”和“推理线程”并且用队列传递帧一定要给队列设置最大长度并且读帧线程在队列满时直接丢弃帧而不是阻塞等待。否则视频流一卡读帧线程和推理线程互相等待程序就卡死了。我踩过这个坑当时的现象是“界面显示正常但停止按钮点了没反应”排查半天才发现是队列满了之后生产者在等待消费者取走帧而消费者又在等待界面线程更新全部堵死。GPU和CPU的选择也需要提一下。这个项目如果跑在普通笔记本上用CPU推理yolov5s在640x640分辨率下大概每秒5到8帧能用但不算流畅。如果想流畅可以把推理尺寸降到416或者用yolov5n这种轻量模型帧率能翻一倍。如果一定要在GPU上跑注意batch size始终是1因为视频流是逐帧推理的batch size设大了没有意义反而占显存。优化做完之后我一般会用这样一个简单的方法测试界面性能在视频播放中打开任务管理器观察CPU和GPU的占用情况同时拖拽窗口看是否掉帧。如果窗口拖拽时画面停滞超过0.3秒说明主线程被什么操作阻塞了需要检查是不是有IO操作或者模型加载操作混进了主线程。这个排查思路百试百灵。实际部署时我还遇到过摄像头被其他程序占用的情况比如Zoom、微信视频会议刚好在用摄像头你的界面打开摄像头时就会失败。所以打开摄像头失败时的提示信息一定要写清楚提示用户“摄像头被其他程序占用或设备号错误”而不是直接抛异常崩溃。写在最后的几点感想这个项目做完之后我最深的体会是yolov5本身已经非常成熟真正拉开差距的其实是工程封装能力。一个能打开视频、能接摄像头、能调整参数、能导出结果的界面背后涉及的知识点横跨了模型推理、多线程编程、GUI事件循环、图像绘制和文件IO。你每解决一个问题能力就会实打实地长一截。如果你也是从“只会跑detect.py”开始建议按这个顺序推进先写一个能用的Detector类并用脚本验证再做一个只有打开视频和显示画面的极简界面然后逐步加摄像头、加参数调节、加导出功能。每一步都确认跑通再进下一步别想着一步到位。界面崩溃的时候也别慌把线程模型理清楚九成的问题都能解决。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门