1. 从零搭建红绿灯检测系统的整体思路
红绿灯检测这个方向,说简单也简单,说坑多也是真的多。简单在于目标类别少,无非红灯、绿灯、黄灯、不亮或者加个倒计时数字;坑多在于实际场景里光照变化剧烈、目标尺度差异大、遮挡频繁,再加上摄像头角度千差万别,一个在白天训练得很好的模型,到了傍晚逆光或者夜间炫光环境下,召回率可能直接掉一半。我前前后后做过好几个版本的信号灯检测,从最早的YOLOv5一路迭代到现在的YOLOv11,中间踩过的坑足够写一本小册子。
这篇文章要聊的是一套完整的红绿灯目标检测系统,技术栈是YOLOv11做检测核心,PyQt做图形界面,支持图片、视频文件和摄像头实时推理三种输入模式。整套代码可以直接跑起来,适合刚接触目标检测想找个完整项目练手的朋友,也适合需要快速搭建一个信号灯检测demo的开发者。我会把整体设计思路、关键代码细节、实操步骤、参数选择依据以及实际部署中遇到的典型问题都摊开来讲,尽量做到你看完就能复现,复现完就能改。
先说一下为什么选YOLOv11而不是其他版本。YOLOv11是Ultralytics在2024年推出的新版本,相比YOLOv8,它在骨干网络和颈部结构上做了一些调整,引入了C3k2模块和SPPF的改进版本,在COCO数据集上的mAP有提升,同时参数量控制得还不错。对于红绿灯这种小目标检测任务,YOLOv11的检测头设计对中小目标更友好,而且Ultralytics的生态非常成熟,训练、导出、推理一条龙,省去了大量造轮子的时间。PyQt这边选的是PyQt5,虽然PyQt6也出来了,但PyQt5的教程和社区资源更丰富,遇到问题更容易找到答案,对于快速开发来说更稳妥。
整套系统的架构其实不复杂:底层是YOLOv11模型负责推理,中间层做输入源的适配(图片、视频、摄像头统一成帧序列),上层是PyQt界面负责交互和展示。关键在于中间的适配层要做好线程管理,否则摄像头推理很容易把界面卡死。这个后面会详细讲。
注意:本文所有代码基于Ultralytics 8.x版本和PyQt5,Python环境建议3.9到3.11,太高或太低的版本可能在依赖安装上遇到麻烦。
2. 环境配置与依赖安装的实操细节
2.1 Python环境与核心依赖选择
环境配置这一步看起来简单,但实际上是最容易劝退新手的地方。我见过太多人卡在torch安装上,要么是CUDA版本对不上,要么是pip源太慢下不下来。我的建议是,如果你有NVIDIA显卡并且想用GPU推理,先确认显卡驱动支持的CUDA版本,然后去PyTorch官网找到对应的安装命令。如果只是跑着玩玩,CPU推理也完全够用,红绿灯检测模型不大,单帧推理在CPU上大概几十毫秒,实时性勉强能接受。
创建一个独立的虚拟环境是必须的,不要图省事直接装在系统Python里。用conda或者venv都行,我个人习惯用conda,因为管理不同CUDA版本的环境更方便。创建好环境后,按顺序安装依赖:先装torch和torchvision,再装ultralytics,最后装PyQt5和opencv-python。顺序很重要,因为ultralytics在安装时会检查torch是否已存在,如果先装ultralytics它会自动拉一个CPU版本的torch,后面你再想换GPU版本就得先卸载再重装,很折腾。
conda create -n traffic_light python=3.10 conda activate traffic_light pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics pip install PyQt5 opencv-python numpy这里有个细节,opencv-python装完之后,如果你要做视频推理,建议再装一个opencv-python-headless的替代品?不,恰恰相反,PyQt界面里如果要显示视频帧,用opencv-python就够了,headless版本没有GUI功能,反而可能在imshow之类的调用上出问题。不过我们这套系统不用cv2.imshow,而是把帧转成QImage显示在QLabel上,所以headless也能用。但为了省事,直接装完整的opencv-python就行。
2.2 模型权重文件的获取与验证
YOLOv11的预训练权重文件可以从Ultralytics的官方仓库下载,文件名类似yolo11n.pt、yolo11s.pt、yolo11m.pt等,n代表nano,s代表small,m代表medium,越大精度越高但速度越慢。对于红绿灯检测,我建议从yolo11s开始,nano版本虽然快,但在小目标和远距离目标上的召回率明显偏低,s版本在速度和精度之间平衡得比较好。如果你有GPU,可以直接上m甚至l版本,推理速度依然能保持实时。
下载权重文件后,不要急着直接拿来推理红绿灯。COCO数据集里虽然有traffic light这个类别,但它只区分“红绿灯”这一个类,不区分红、绿、黄。所以你需要自己训练一个专门的红绿灯检测模型,或者找现成的开源红绿灯数据集权重。自己训练的话,数据集可以从公开的道路场景数据集中筛选,比如BDD100K、LLVIP等,把红绿灯区域裁剪出来重新标注。标注工具用LabelImg或者Roboflow都行,导出YOLO格式的txt标签。
实操心得:标注红绿灯时,一定要把“红灯”“绿灯”“黄灯”“不亮”分开标注,不要只标一个traffic light。另外,倒计时数字如果也要检测,单独加一个类别。标注框尽量贴紧灯体,不要包含太多背景,否则模型容易学到背景特征而不是灯本身。
2.3 项目目录结构规划
一个清晰的项目结构能让后续开发和维护省很多事。我习惯这样组织:
traffic_light_system/ ├── models/ │ └── best.pt # 训练好的红绿灯检测权重 ├── ui/ │ └── main_window.py # PyQt界面代码 ├── core/ │ ├── detector.py # YOLOv11推理封装 │ └── video_thread.py # 视频/摄像头推理线程 ├── assets/ │ └── icons/ # 界面图标 ├── main.py # 程序入口 └── requirements.txt把推理逻辑和界面逻辑分开,好处是以后想换模型或者换界面框架,只需要改对应的模块,不会牵一发动全身。detector.py里封装一个Detector类,对外只暴露一个detect(frame)方法,返回检测结果列表。video_thread.py里用QThread开一个独立线程跑视频推理,通过信号槽把结果帧发给界面显示。这样界面永远不会卡。
3. YOLOv11推理核心的封装与参数调优
3.1 Detector类的设计与实现
Detector类的职责很单一:接收一帧图像,返回检测框、类别、置信度。内部持有YOLO模型实例,初始化时加载权重文件。这里有个容易忽略的点,YOLO模型在第一次推理时会做一次预热,耗时明显比后续推理长,如果你在摄像头线程里第一次调用detect,界面会卡一下。解决办法是在Detector初始化时用一张空白图跑一次推理,把预热提前做掉。
from ultralytics import YOLO import numpy as np class Detector: def __init__(self, model_path, conf=0.5, iou=0.45, device='cpu'): self.model = YOLO(model_path) self.conf = conf self.iou = iou self.device = device # 预热 dummy = np.zeros((640, 640, 3), dtype=np.uint8) self.model.predict(dummy, device=self.device, verbose=False) def detect(self, frame): results = self.model.predict( frame, conf=self.conf, iou=self.iou, device=self.device, verbose=False ) detections = [] for r in results: boxes = r.boxes for box in boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() cls_id = int(box.cls[0].cpu().numpy()) conf = float(box.conf[0].cpu().numpy()) detections.append({ 'bbox': [int(x1), int(y1), int(x2), int(y2)], 'class_id': cls_id, 'class_name': self.model.names[cls_id], 'confidence': conf }) return detectionsconf和iou这两个参数是调优的重点。conf是置信度阈值,低于这个值的检测框会被丢弃。红绿灯检测里,我一般设0.5起步,如果发现漏检多就降到0.3到0.4,如果误检多就升到0.6。iou是NMS的IoU阈值,控制重叠框的合并程度,红绿灯一般不会密集排列,0.45到0.5都行。device参数根据你的环境填'cpu'或者'cuda:0',有GPU一定要用GPU,速度差距是数量级的。
3.2 输入尺寸与推理速度的权衡
YOLOv11默认推理尺寸是640x640,但你的输入帧可能是1920x1080。Ultralytics会自动做letterbox缩放,保持宽高比,不足的部分用灰色填充。这个缩放过程对红绿灯这种小目标影响很大,如果原始帧里红绿灯只占几十个像素,缩放到640后可能只剩十几个像素,模型很难检测到。解决办法有两个:一是提高推理尺寸,比如设成1280,但速度会下降;二是先对感兴趣区域做裁剪,再送进模型。
我实测下来,对于1080p的道路监控画面,推理尺寸设成960是一个比较好的平衡点,速度比1280快不少,小目标召回率比640有明显提升。你可以在predict里加imgsz参数:
results = self.model.predict(frame, imgsz=960, conf=self.conf, iou=self.iou, device=self.device)另外,如果画面里红绿灯位置相对固定,比如固定在画面上半部分,你可以只把上半部分裁出来送进模型,这样既提高了有效分辨率,又减少了计算量。这个技巧在固定摄像头场景下非常实用。
3.3 类别映射与颜色显示策略
训练好的模型输出的class_id对应的类别名称,取决于你训练时数据集的类别顺序。假设你的数据集类别是['red', 'green', 'yellow', 'off'],那么class_id 0就是红灯,1是绿灯,以此类推。在界面上显示时,我建议给每个类别分配固定的颜色:红灯用红色框,绿灯用绿色框,黄灯用黄色框,不亮用灰色框。这样操作员一眼就能看出检测结果,不用去读文字标签。
COLOR_MAP = { 'red': (0, 0, 255), 'green': (0, 255, 0), 'yellow': (0, 255, 255), 'off': (128, 128, 128) }画框的时候用cv2.rectangle,颜色从COLOR_MAP里取,注意OpenCV用的是BGR顺序,不是RGB。这个坑我踩过,红色和蓝色搞反了,调试了半天才发现。
4. PyQt界面设计与多线程推理实现
4.1 界面布局与控件选型
PyQt界面的设计原则是:操作路径短,信息展示清晰。主界面我一般分成三个区域:左侧是输入源选择和控制按钮,中间是视频显示区域,右侧是检测结果列表和统计信息。输入源用QRadioButton做单选,图片、视频、摄像头三选一。控制按钮包括“开始”“暂停”“停止”“保存结果”。视频显示用QLabel,设置setScaledContents(True)让画面自适应控件大小。
右侧的检测结果列表用QTableWidget,每次推理后更新,显示类别、置信度、边界框坐标。统计信息用QLabel显示当前帧的检测数量和推理耗时。推理耗时这个指标很重要,能帮你判断当前配置是否满足实时性要求。如果单帧耗时超过100毫秒,摄像头推理就会明显卡顿,需要降低推理尺寸或者换更小的模型。
界面布局用QHBoxLayout和QVBoxLayout嵌套,不要用绝对定位,否则窗口缩放时控件会乱。我见过有人用setGeometry硬编码坐标,结果换个分辨率就全乱了。布局管理器虽然有时候不够灵活,但胜在稳定。
4.2 QThread视频推理线程的编写
摄像头推理必须放在独立线程里,这是铁律。主线程负责界面刷新和事件响应,推理线程负责读帧、推理、发信号。两者通过信号槽通信,推理线程每处理完一帧就发一个信号,带上处理后的帧和检测结果,主线程收到后更新界面。
from PyQt5.QtCore import QThread, pyqtSignal import cv2 class VideoThread(QThread): frame_ready = pyqtSignal(np.ndarray, list) finished = pyqtSignal() def __init__(self, source, detector): super().__init__() self.source = source self.detector = detector self.running = False def run(self): self.running = True cap = cv2.VideoCapture(self.source) while self.running and cap.isOpened(): ret, frame = cap.read() if not ret: break detections = self.detector.detect(frame) frame = self.draw_boxes(frame, detections) self.frame_ready.emit(frame, detections) cap.release() self.finished.emit() def stop(self): self.running = False self.wait()这里有几个细节要注意。第一,cap.read()在视频结束后会返回False,循环要能正常退出,否则线程会卡死。第二,stop()方法里先设running为False,再调wait()等待线程结束,不要直接terminate(),那样可能导致资源泄漏。第三,draw_boxes方法里画框和写标签,标签背景用实心矩形填充,文字用白色,这样在复杂背景下也能看清。
4.3 信号槽通信与界面刷新
信号槽是PyQt的核心机制,但用不好也会出问题。frame_ready信号携带numpy数组和检测结果列表,numpy数组在跨线程传递时是引用传递,如果推理线程在emit之后又修改了这个数组,主线程拿到的数据可能已经被改了。所以emit之前要确保帧数据不再被修改,或者干脆copy一份。我一般是在draw_boxes里返回一个新的帧,不在原帧上画,这样更安全。
主线程收到frame_ready后,把numpy数组转成QImage,再转成QPixmap,设置到QLabel上。转换过程:
def update_frame(self, frame, detections): 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)) self.update_table(detections)注意QImage构造时传入rgb.data,这个data是numpy数组的内存视图,如果rgb数组在函数返回后被回收,QImage可能显示异常。稳妥的做法是qimg.copy()一下,或者确保rgb在作用域内不被释放。我一般直接setPixmap之后就不管了,实测大部分情况下没问题,但如果遇到画面闪烁或者花屏,优先检查这里。
注意事项:视频推理线程的帧率不要超过摄像头实际帧率,否则队列会堆积。可以在循环里加一个QThread.msleep(1)让出CPU时间,或者用定时器控制读取频率。另外,如果推理速度跟不上视频帧率,可以考虑跳帧处理,比如每两帧处理一帧,保证界面流畅。
5. 图片、视频、摄像头三种模式的统一适配
5.1 图片推理模式的实现要点
图片模式最简单,用户选择一张图片,程序读取后送进Detector,画框后显示在界面上,同时把结果保存到指定目录。这里的关键是图片路径的处理,要支持中文路径,OpenCV的imread对中文路径支持不好,需要用np.fromfile配合cv2.imdecode来读取。
def read_image_chinese(path): data = np.fromfile(path, dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR) return img保存结果图片时同样要用cv2.imencode配合tofile,避免中文路径写入失败。这个坑在国内环境下几乎必踩,提前处理好能省很多事。图片推理的结果我一般保存两份:一份是画框后的图片,一份是检测结果的txt文件,方便后续分析。
5.2 视频文件推理与进度控制
视频文件推理和摄像头推理的代码结构基本一样,区别在于视频文件有总帧数,可以做进度条。用cv2.VideoCapture的get(cv2.CAP_PROP_FRAME_COUNT)获取总帧数,每处理一帧更新一次进度条。另外视频文件推理可以控制速度,不需要实时,所以可以用更小的推理尺寸和更高的精度设置。
视频保存用cv2.VideoWriter,注意编码器和帧率要和原视频一致,否则保存出来的视频可能无法播放。编码器用mp4v比较通用,帧率从原视频读取。如果原视频是30fps,你保存成25fps,画面会变慢。这个细节很多人不注意,导出的视频看起来怪怪的。
fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(save_path, fourcc, fps, (w, h))5.3 摄像头实时推理的延迟优化
摄像头推理对延迟最敏感。除了前面说的用独立线程,还有几个优化点。第一,设置摄像头的缓冲区大小为1,避免读到旧帧。cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)在部分平台上有效,能明显降低延迟。第二,推理尺寸不要设太大,640或960就够了。第三,如果用的是USB摄像头,尽量插在USB 3.0接口上,带宽更充足。
还有一个容易被忽略的点:摄像头的自动曝光和自动白平衡会导致画面亮度忽明忽暗,影响检测稳定性。如果摄像头支持,可以手动锁定曝光和白平衡。OpenCV里用cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)之类的参数可以调整,但不同摄像头支持程度不一样,需要实测。
我在一个路口场景下做过长时间稳定性测试,连续跑了72小时,发现最大的问题不是模型精度下降,而是摄像头驱动偶尔会断连,导致cap.read()返回False。解决办法是在循环里加一个重连机制,检测到读取失败后释放capture,等待一秒后重新打开。这个在实际部署中非常必要。
6. 常见问题排查与实战避坑指南
6.1 模型加载失败与权重兼容性问题
最常见的问题是模型加载报错,提示找不到权重文件或者权重格式不兼容。首先确认权重文件路径是否正确,相对路径是相对于运行脚本的目录,不是相对于项目根目录。其次确认权重文件是用哪个版本的Ultralytics训练的,YOLOv11的权重用YOLOv8的代码加载可能会报错,反之亦然。如果是从网上下载的权重,先确认它对应的YOLO版本。
还有一个隐蔽的问题:权重文件下载不完整。有时候网络中断导致文件只下了一半,加载时会报各种奇怪的错误。检查文件大小,yolo11s.pt大概18MB左右,如果明显偏小就是没下完。
6.2 检测框偏移与坐标转换错误
检测框画出来位置不对,偏上或者偏左,大概率是坐标转换出了问题。YOLO返回的xyxy坐标是相对于输入图像的,如果你在推理前对图像做了缩放或裁剪,画框时要把坐标映射回原始图像尺寸。Ultralytics的results里其实提供了原始图像的坐标,用box.xyxy[0]拿到的就是映射回原图的坐标,但如果你自己做了预处理,就要自己算映射。
另一个常见错误是BGR和RGB搞混。OpenCV读进来是BGR,PyQt显示需要RGB,画框时cv2.rectangle用的颜色是BGR顺序。如果你发现红色框变成了蓝色,就是这个问题。
6.3 界面卡顿与线程死锁
界面卡顿的原因几乎都是推理跑在主线程里。检查你的代码,detect调用是否在QThread的run方法里。如果是在按钮的clicked槽函数里直接调detect,那界面必卡。另一个原因是信号槽连接方式不对,跨线程的信号槽默认是队列连接,但如果连接方式设成了直连,槽函数会在发射信号的线程里执行,同样会卡界面。
线程死锁一般发生在stop的时候,主线程调wait()等待子线程结束,子线程又在等主线程的某个资源。避免方法是在子线程里不要调用任何界面控件的方法,所有界面更新都通过信号槽发给主线程。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型加载报错 | 路径错误或权重不完整 | 检查文件是否存在及大小 | 重新下载权重,用绝对路径 |
| 检测框位置偏移 | 坐标未映射回原图 | 对比原图和结果图尺寸 | 手动计算缩放比例并映射 |
| 界面卡死 | 推理在主线程执行 | 查看detect调用位置 | 移入QThread的run方法 |
| 摄像头延迟高 | 缓冲区堆积 | 打印每帧时间戳 | 设BUFFERSIZE为1,跳帧处理 |
| 中文路径读取失败 | OpenCV不支持中文 | 换英文路径测试 | 用np.fromfile+imdecode |
| 视频保存无法播放 | 编码器或帧率不对 | 用播放器打开检查 | 用mp4v编码器,帧率与原视频一致 |
| 夜间检测效果差 | 训练数据缺乏夜间样本 | 分时段测试 | 补充夜间数据重新训练 |
| 小目标漏检多 | 推理尺寸太小 | 提高imgsz测试 | 设imgsz=960或1280 |
6.5 长时间运行的稳定性经验
如果你打算把这套系统用于长期运行,有几个经验值得分享。第一,定期重启推理线程,比如每24小时重启一次,释放可能积累的内存碎片。第二,监控GPU显存占用,如果持续增长说明有内存泄漏,检查是否有未释放的tensor。第三,日志要写文件,不要只打印到控制台,出问题的时候可以回溯。第四,给程序加一个看门狗,检测到线程异常退出后自动重启。
我在实际项目里还遇到过一个奇葩问题:夏天机箱温度过高导致GPU降频,推理速度从30fps掉到10fps。后来加了散热风扇才解决。所以如果你在封闭环境部署,散热一定要考虑。
7. 模型训练与自定义数据集的关键步骤
7.1 数据集准备与标注规范
虽然这篇文章主要讲推理系统,但如果你想用自己的数据训练红绿灯模型,数据集这块必须说一下。红绿灯数据集的难点在于类别不平衡,红灯和绿灯样本多,黄灯和不亮样本少。解决办法是过采样少数类别,或者在loss里给少数类别更高的权重。Ultralytics支持在训练时通过class_weights参数调整,但更简单的办法是直接把少数类别的图片复制几份。
标注规范方面,红绿灯的边界框要贴紧灯体,不要包含灯杆和周围背景。如果红绿灯有多个灯珠,只标注当前亮着的那个,不要把所有灯珠都标上。倒计时数字如果也要检测,单独一个类别,标注时只框数字区域,不要框整个倒计时牌。
7.2 训练参数设置与调优
训练YOLOv11红绿灯模型,我一般用这样的参数起步:epochs=100,batch=16,imgsz=640,lr0=0.01,lrf=0.01,momentum=0.937,weight_decay=0.0005。数据增强方面,HSV增强对光照变化很有帮助,hsv_h=0.015,hsv_s=0.7,hsv_v=0.4。马赛克增强mosaic=1.0能提升小目标检测能力,但训练后期建议关掉,避免过度增强导致精度下降。
如果显存不够,batch调小,用梯度累积模拟大batch。Ultralytics支持nbs参数,设nbs=64,batch=16,相当于4步累积。训练过程中看loss曲线和mAP曲线,如果mAP还在涨就不要停,如果连续20个epoch不涨就可以考虑早停。
7.3 模型导出与部署优化
训练完之后,模型可以导出成ONNX或者TensorRT格式,推理速度会更快。ONNX通用性好,TensorRT在NVIDIA GPU上速度最快但兼容性差一些。导出命令:
yolo export model=best.pt format=onnx imgsz=640导出ONNX后,可以用onnxruntime推理,也可以转成TensorRT。如果部署在边缘设备上,比如Jetson系列,TensorRT是首选。导出时注意opset版本,太高或太低都可能导致兼容问题,一般用opset 12或13。
实操心得:导出ONNX后一定要用onnxruntime跑一遍验证,对比PyTorch的输出是否一致。有时候导出过程中某些算子不被支持,会被替换成近似实现,导致精度下降。如果发现精度差异大,检查导出日志里的警告信息。
8. 系统扩展与功能增强方向
8.1 多路视频流并行处理
单路视频推理跑通之后,很容易想到扩展到多路。多路的核心是线程池管理,每路视频一个线程,共享一个Detector实例。但YOLO模型不是线程安全的,多个线程同时调predict会出问题。解决办法是给Detector加锁,或者每个线程独立一个Detector实例。加锁简单但会串行化推理,多路场景下延迟会累积。独立实例占用显存多,但并行度高。具体选哪种看你的硬件资源和延迟要求。
如果路数很多,比如16路以上,建议用批处理推理,把多路帧拼成一个batch送进模型,一次推理出所有结果。Ultralytics支持batch推理,predict传一个列表就行。这样GPU利用率最高,但需要自己管理帧的收集和分发。
8.2 检测结果的结构化输出与告警
红绿灯检测的结果如果只是画框显示,价值有限。真正有用的是结构化输出:当前时刻红灯还是绿灯,持续了多久,有没有异常。可以加一个状态机,跟踪每个红绿灯的状态变化,状态切换时记录时间戳。如果红灯持续时间异常长或者绿灯一直不亮,触发告警。
告警方式可以多种多样,界面上弹提示、写日志、发消息都行。我一般用日志加界面提示,简单可靠。如果要做消息推送,可以对接一些通知服务,但注意不要引入敏感的网络操作。
8.3 模型更新与热切换
系统跑起来之后,模型可能需要更新。如果每次更新都重启程序,体验很差。可以在界面上加一个“加载模型”按钮,点击后弹出文件选择框,选新的权重文件后重新初始化Detector。注意重新初始化时要先停掉推理线程,加载完再重启,否则会出现模型正在推理时被替换的竞态问题。
热切换的另一个场景是切换不同尺寸的模型,比如白天用s模型保证速度,夜间用m模型保证精度。这个可以根据时间自动切换,也可以手动切换。实现上就是维护一个模型路径列表,切换时重新加载。
9. 实际部署中的性能数据与调优记录
9.1 不同硬件平台的推理速度对比
我在几台不同配置的机器上跑过这套系统,记录了一些数据供参考。测试条件:YOLOv11s模型,输入尺寸640,conf=0.5,单帧推理耗时取100帧平均值。
| 硬件平台 | GPU | 推理耗时(ms) | 等效FPS | 备注 |
|---|---|---|---|---|
| 台式机 | RTX 3060 | 8 | 125 | 流畅 |
| 笔记本 | RTX 2060 | 14 | 71 | 流畅 |
| 台式机 | GTX 1060 | 22 | 45 | 够用 |
| 笔记本 | 核显 | 85 | 12 | 勉强 |
| 树莓派5 | 无 | 450 | 2 | 不可用 |
| Jetson Nano | 集成 | 120 | 8 | 需优化 |
从数据可以看出,有独立GPU的情况下,YOLOv11s跑640尺寸完全能满足实时性。核显和边缘设备就需要降尺寸或者换nano模型。Jetson Nano上如果用TensorRT加速,能到15fps左右,勉强可用。
9.2 推理尺寸对精度和速度的影响
同一台机器(RTX 3060),不同推理尺寸的对比:
| 推理尺寸 | 推理耗时(ms) | 小目标召回率 | 适用场景 |
|---|---|---|---|
| 640 | 8 | 72% | 红绿灯占画面比例大 |
| 960 | 15 | 85% | 通用场景 |
| 1280 | 26 | 89% | 远距离小目标 |
| 1600 | 42 | 90% | 精度优先,速度要求低 |
可以看到,从640提升到960,召回率提升明显,耗时增加可接受。从960到1280,召回率提升有限,耗时几乎翻倍。所以960是性价比最高的选择。当然具体场景要具体分析,如果你的摄像头离红绿灯很近,640就够了。
9.3 长时间运行的资源占用记录
连续运行72小时的资源监控数据:
| 指标 | 初始值 | 24小时后 | 72小时后 | 趋势 |
|---|---|---|---|---|
| GPU显存 | 1.2GB | 1.2GB | 1.3GB | 基本稳定 |
| 内存 | 800MB | 850MB | 920MB | 缓慢增长 |
| 推理耗时 | 8ms | 8ms | 9ms | 基本稳定 |
| 帧率 | 30fps | 30fps | 29fps | 基本稳定 |
内存缓慢增长是Python的常见现象,只要不持续暴涨就没事。如果发现内存每小时增长几十MB,那就要查内存泄漏了。常见泄漏点是信号槽连接没有断开,每次重启线程都新建连接,旧连接的对象没释放。解决办法是在线程结束时disconnect所有信号。
10. 代码组织与工程化建议
10.1 配置文件的分离与管理
不要把参数硬编码在代码里,用配置文件管理。我一般用YAML或者JSON,把模型路径、推理尺寸、置信度阈值、IOU阈值、摄像头ID、保存路径这些都放进去。这样换环境的时候只改配置文件,不用动代码。
model: path: "models/best.pt" imgsz: 960 conf: 0.5 iou: 0.45 device: "cuda:0" video: camera_id: 0 save_dir: "outputs/" buffer_size: 1读取配置用pyyaml,几行代码的事。配置文件放在项目根目录,加个config.example.yaml作为模板,实际的config.yaml加到.gitignore里,避免不同机器的配置互相覆盖。
10.2 日志系统的搭建
日志用Python内置的logging模块就够了,不要用print。配置两个handler,一个输出到控制台,一个输出到文件。文件日志按天切割,保留最近7天。日志级别默认INFO,调试的时候开DEBUG。关键操作都要打日志:模型加载、线程启动停止、推理异常、保存文件。出问题的时候,日志是唯一的线索。
import logging from logging.handlers import TimedRotatingFileHandler logger = logging.getLogger("traffic_light") logger.setLevel(logging.INFO) fh = TimedRotatingFileHandler("logs/app.log", when="D", backupCount=7) fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) logger.addHandler(fh)10.3 异常处理与程序健壮性
目标检测系统运行环境复杂,异常处理必须到位。摄像头断连、视频文件损坏、模型文件丢失、显存不足,这些都要有对应的处理逻辑。我的原则是:能恢复的异常就恢复,不能恢复的就优雅退出并提示用户。比如摄像头断连,重试3次,每次间隔1秒,3次都失败就提示用户检查设备。模型加载失败,直接弹窗提示,不要静默失败。
还有一个细节:程序退出时要确保所有线程都已停止,所有文件都已关闭。在closeEvent里做清理工作,不要依赖Python的垃圾回收。我见过程序关了但摄像头灯还亮着的情况,就是capture没释放。
这套红绿灯检测系统从代码量来说不算大,核心逻辑几百行就能写完,但要把稳定性做好,需要考虑的细节非常多。我个人的体会是,目标检测项目里,模型训练只占三成工作量,剩下七成都在工程化和异常处理上。你把这篇里的坑都避开了,系统跑起来基本不会有大问题。后续如果想加功能,比如多路、告警、模型热切换,按照第8节的思路扩展就行,架构上已经留好了口子。