YOLOv11+PyQt5红绿灯检测系统:从训练到多线程部署实战
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版本和PyQt5Python环境建议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 python3.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代表nanos代表smallm代表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, conf0.5, iou0.45, devicecpu): self.model YOLO(model_path) self.conf conf self.iou iou self.device device # 预热 dummy np.zeros((640, 640, 3), dtypenp.uint8) self.model.predict(dummy, deviceself.device, verboseFalse) def detect(self, frame): results self.model.predict( frame, confself.conf, iouself.iou, deviceself.device, verboseFalse ) 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, imgsz960, confself.conf, iouself.iou, deviceself.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, dtypenp.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读进来是BGRPyQt显示需要RGB画框时cv2.rectangle用的颜色是BGR顺序。如果你发现红色框变成了蓝色就是这个问题。6.3 界面卡顿与线程死锁界面卡顿的原因几乎都是推理跑在主线程里。检查你的代码detect调用是否在QThread的run方法里。如果是在按钮的clicked槽函数里直接调detect那界面必卡。另一个原因是信号槽连接方式不对跨线程的信号槽默认是队列连接但如果连接方式设成了直连槽函数会在发射信号的线程里执行同样会卡界面。线程死锁一般发生在stop的时候主线程调wait()等待子线程结束子线程又在等主线程的某个资源。避免方法是在子线程里不要调用任何界面控件的方法所有界面更新都通过信号槽发给主线程。6.4 常见问题速查表问题现象可能原因排查方法解决方案模型加载报错路径错误或权重不完整检查文件是否存在及大小重新下载权重用绝对路径检测框位置偏移坐标未映射回原图对比原图和结果图尺寸手动计算缩放比例并映射界面卡死推理在主线程执行查看detect调用位置移入QThread的run方法摄像头延迟高缓冲区堆积打印每帧时间戳设BUFFERSIZE为1跳帧处理中文路径读取失败OpenCV不支持中文换英文路径测试用np.fromfileimdecode视频保存无法播放编码器或帧率不对用播放器打开检查用mp4v编码器帧率与原视频一致夜间检测效果差训练数据缺乏夜间样本分时段测试补充夜间数据重新训练小目标漏检多推理尺寸太小提高imgsz测试设imgsz960或12806.5 长时间运行的稳定性经验如果你打算把这套系统用于长期运行有几个经验值得分享。第一定期重启推理线程比如每24小时重启一次释放可能积累的内存碎片。第二监控GPU显存占用如果持续增长说明有内存泄漏检查是否有未释放的tensor。第三日志要写文件不要只打印到控制台出问题的时候可以回溯。第四给程序加一个看门狗检测到线程异常退出后自动重启。我在实际项目里还遇到过一个奇葩问题夏天机箱温度过高导致GPU降频推理速度从30fps掉到10fps。后来加了散热风扇才解决。所以如果你在封闭环境部署散热一定要考虑。7. 模型训练与自定义数据集的关键步骤7.1 数据集准备与标注规范虽然这篇文章主要讲推理系统但如果你想用自己的数据训练红绿灯模型数据集这块必须说一下。红绿灯数据集的难点在于类别不平衡红灯和绿灯样本多黄灯和不亮样本少。解决办法是过采样少数类别或者在loss里给少数类别更高的权重。Ultralytics支持在训练时通过class_weights参数调整但更简单的办法是直接把少数类别的图片复制几份。标注规范方面红绿灯的边界框要贴紧灯体不要包含灯杆和周围背景。如果红绿灯有多个灯珠只标注当前亮着的那个不要把所有灯珠都标上。倒计时数字如果也要检测单独一个类别标注时只框数字区域不要框整个倒计时牌。7.2 训练参数设置与调优训练YOLOv11红绿灯模型我一般用这样的参数起步epochs100batch16imgsz640lr00.01lrf0.01momentum0.937weight_decay0.0005。数据增强方面HSV增强对光照变化很有帮助hsv_h0.015hsv_s0.7hsv_v0.4。马赛克增强mosaic1.0能提升小目标检测能力但训练后期建议关掉避免过度增强导致精度下降。如果显存不够batch调小用梯度累积模拟大batch。Ultralytics支持nbs参数设nbs64batch16相当于4步累积。训练过程中看loss曲线和mAP曲线如果mAP还在涨就不要停如果连续20个epoch不涨就可以考虑早停。7.3 模型导出与部署优化训练完之后模型可以导出成ONNX或者TensorRT格式推理速度会更快。ONNX通用性好TensorRT在NVIDIA GPU上速度最快但兼容性差一些。导出命令yolo export modelbest.pt formatonnx imgsz640导出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模型输入尺寸640conf0.5单帧推理耗时取100帧平均值。硬件平台GPU推理耗时(ms)等效FPS备注台式机RTX 30608125流畅笔记本RTX 20601471流畅台式机GTX 10602245够用笔记本核显8512勉强树莓派5无4502不可用Jetson Nano集成1208需优化从数据可以看出有独立GPU的情况下YOLOv11s跑640尺寸完全能满足实时性。核显和边缘设备就需要降尺寸或者换nano模型。Jetson Nano上如果用TensorRT加速能到15fps左右勉强可用。9.2 推理尺寸对精度和速度的影响同一台机器RTX 3060不同推理尺寸的对比推理尺寸推理耗时(ms)小目标召回率适用场景640872%红绿灯占画面比例大9601585%通用场景12802689%远距离小目标16004290%精度优先速度要求低可以看到从640提升到960召回率提升明显耗时增加可接受。从960到1280召回率提升有限耗时几乎翻倍。所以960是性价比最高的选择。当然具体场景要具体分析如果你的摄像头离红绿灯很近640就够了。9.3 长时间运行的资源占用记录连续运行72小时的资源监控数据指标初始值24小时后72小时后趋势GPU显存1.2GB1.2GB1.3GB基本稳定内存800MB850MB920MB缓慢增长推理耗时8ms8ms9ms基本稳定帧率30fps30fps29fps基本稳定内存缓慢增长是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, whenD, backupCount7) fh.setFormatter(logging.Formatter(%(asctime)s - %(levelname)s - %(message)s)) logger.addHandler(fh)10.3 异常处理与程序健壮性目标检测系统运行环境复杂异常处理必须到位。摄像头断连、视频文件损坏、模型文件丢失、显存不足这些都要有对应的处理逻辑。我的原则是能恢复的异常就恢复不能恢复的就优雅退出并提示用户。比如摄像头断连重试3次每次间隔1秒3次都失败就提示用户检查设备。模型加载失败直接弹窗提示不要静默失败。还有一个细节程序退出时要确保所有线程都已停止所有文件都已关闭。在closeEvent里做清理工作不要依赖Python的垃圾回收。我见过程序关了但摄像头灯还亮着的情况就是capture没释放。这套红绿灯检测系统从代码量来说不算大核心逻辑几百行就能写完但要把稳定性做好需要考虑的细节非常多。我个人的体会是目标检测项目里模型训练只占三成工作量剩下七成都在工程化和异常处理上。你把这篇里的坑都避开了系统跑起来基本不会有大问题。后续如果想加功能比如多路、告警、模型热切换按照第8节的思路扩展就行架构上已经留好了口子。