从挑选模型到部署成完整系统,这篇把基于YOLOv12的吸烟识别检测项目拆开揉碎。环境配置、数据集准备、训练参数、PyQt5界面、登录注册、摄像头联调,每个环节都用实际跑过的配置和踩坑记录说话,照着操作能少走很多弯路。
1. 为什么这个项目非要上YOLOv12,换个老模型不行吗
1.1 吸烟检测到底难在哪:目标小、光线乱、姿态多
做吸烟识别,很多人第一反应是把“叼着烟的人”当成一个整体目标去训练。这个思路在固定机位的室内场景勉强能跑通,一旦放到楼道、园区、办公区这类半开放环境,立刻原形毕露——人姿态稍微偏一点,或者手里烟被身体挡住半边,模型就识别不出来了。
真正稳定可靠的做法是检测两个层次的目标:烟支本身(cigarette)和人的头部区域。烟草的火光、烟支轮廓在监控画面里往往只有十几个像素宽,属于典型的小目标检测。这也是为什么这个项目选YOLOv12而不是更老牌的YOLOv5。YOLOv5在小目标上的表现不是不能看,而是在实际监控的远距离画面里,漏检率偏高,尤其当烟雾背景杂乱时,候选框容易被噪声干扰。
我一开始也想着用YOLOv8n直接上,毕竟网上资料多、踩坑少。但对比下来发现,YOLOv12在特征提取阶段做了更细的跨尺度融合,对小尺寸物体的响应更敏感。换成大白话说,v12在“看不清的小东西”上,比v8多了一层注意力补偿,这恰恰是吸烟检测最需要的。
1.2 YOLOv12针对这些痛点做了哪些改进
YOLOv12最核心的变化是把注意力机制重新设计了一遍。以往Transformer结构里的全局注意力计算量太大,放到YOLO这种追求实时性的模型里,帧率根本撑不住。v12引入了区域注意力(area attention)和可变形卷积的组合,在不显著增加推理成本的前提下,让模型能更灵活地聚焦到图像中的关键区域。
从这个项目的实际需求看,这个改动的价值体现在两个地方。第一,烟支目标太小,普通卷积的固定感受野很难在一开始就覆盖到,可变形卷积会根据特征动态调整采样点位置,相当于让模型“主动找烟”。第二,监控画面的光照变化剧烈——白天逆光、晚上霓虹灯、雨天反光,区域注意力让模型对局部亮度变化没那么敏感,训练出来的权重在跨场景迁移时更稳。
我实际对比过同一个数据集上YOLOv8n和YOLOv12n的表现,mAP50提升不算夸张,大概4到6个点,但漏检率下降比较明显。烟支这种连续帧里可能出现又消失的目标,漏检率比mAP更能说明问题。所以这个项目最终锁定了YOLOv12。
1.3 模型选型:v8n、v11n、v12n到底怎么选
这里要给个清楚的选择逻辑。YOLOv8成熟稳定,TensorRT部署资料多,适合机器性能有限、又不想折腾新框架的团队。YOLOv11在v8基础上优化了C3K2模块和训练策略,精度小幅提升,但网络结构相对保守,对小目标的改善有限。YOLOv12在架构层面动了刀,精度上限更高,代价是新的注意力模块在某些老显卡上优化不到位,推理速度可能比v11略慢。
我在项目里最终用的是yolov12n.pt作为预训练起点,然后基于它微调。选择n版而不是s或m,理由非常现实:这套系统要同时开摄像头预览、跑推理、刷新UI,显卡稍有压力的环境下,n版在1080p视频流上能保持20到30帧,s版直接掉到15帧以下。如果你们之后要接多路摄像头,起步一定要留足性能余量。
2. 环境配置:YOLOv12最容易翻车的地方全在这
2.1 CUDA、PyTorch、Ultralytics的版本三角关系
YOLOv12的环境配置比v8时代要敏感得多,原因在于新模型依赖的算子更复杂,PyTorch版本和CUDA版本一旦不匹配,训练时会出现各种奇怪的报错。不少初学者问“yolov12环境怎么配”,其实核心就一句话:让Ultralytics包的依赖需求和PyTorch的CUDA支持版本对齐。
我实测下来比较稳的组合是:
- Python 3.10或3.11(不要用3.13,很多深度学习依赖还没跟上)
- CUDA 11.8 + cuDNN 8.9
- PyTorch 2.1.2或2.2.2
- Ultralytics 8.3.x及以上(v12权重需要这个版本才认)
- opencv-python 4.9以上
如果你用CUDA 12.1,对应的PyTorch要选2.3或2.4那一档。这里最容易出的问题是:先装了Ultralytics最新版,然后装PyTorch时选了不匹配的CUDA轮子,结果训练时直接报CUDA error: no kernel image is available。这个报错基本就是版本错位,重装一套对齐的版本就好,别急着怀疑代码。
2.2 配置建议:用虚拟环境隔离依赖
我习惯为每个视觉项目单独建一个conda环境,吸烟识别这个项目也不例外。命令就这几条:
conda create -n smoke python=3.10 conda activate smoke pip install torch==2.2.2 torchvision==0.17.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python pyqt5有一点要提醒:PyQt5和Ultralytics之间偶尔会撞Qt插件版本,表现出来就是界面启动时报“qt.qpa.plugin: could not load the xcb plugin”。解决办法是检查PyQt5和PyQt5-Qt5的版本一致性,缺了就一起重装。这个坑我在配置参考里给过,后来换到新环境时又踩了一遍,确认是官方源里PyQt5-Qt5版本滞后导致的,直接指定版本安装就能稳定解决。
3. 数据准备与标注:COCO里只有几十张吸烟图,模型靠这个学不会
3.1 数据集从哪里来:公开数据集和自采数据怎么搭配
这是整个项目里最花时间、也最决定效果上限的环节。COCO数据集里有80类,其中确实包含cigarette这个类,但我粗略统计过train2017的标注文件,cigarette类对应的图片数量大概只有六七十张,实例数也刚到一百出头。拿这点数据去训练一个专用吸烟检测模型,纯属杯水车薪。很多YOLO开源项目宣传“下载即用”,其实都是在这个基础上硬扛,效果好不了。
正确做法是三路数据混合:
第一路,从Visual Genome、Open Images这类公开数据集里抽取带有cigarette标签或近似标签(如cigar、lighter)的图片。注意标签不一定完全匹配,下载后要人工过一遍,把标注框明显不准的挑出去。
第二路,自己拍摄。不需要专业设备,手机就行。我找了几个抽烟的同事,在园区吸烟亭、楼道拐角、地下停车场不同光照条件下,拍了大概两千张素材。重点拍远距离、侧身、逆光、遮挡的情况,这些才是监控场景的真实挑战。
第三路,公开的吸烟识别专项数据集。Github上搜smoking detection dataset能翻到一些,质量参差不齐,但可以作为补充样本。使用前务必确认授权协议,毕竟这个项目之后可能会商用。
3.2 标注工具的选型与标注规范
标注工具我推荐X-AnyLabeling。相比老牌的LabelImg,它对YOLO格式的支持更好,直接输出txt格式,不用费劲做格式转换,而且支持长条目标的旋转框标注,烟支这种细长目标很适用。
类别定义不要只标cigarette。我建议按场景定义两类:
- cigarette:烟支、烟头、打火机点火状态
- smoke:明显烟雾区域(可选项,用于辅助判断)
为什么单列一个smoke类?因为很多监控画面上烟支被手指挡住,只有烟雾可见。如果模型能看到烟雾但看不到烟支,后续规则引擎可以给它一个较低置信度的吸烟提示。当然,这两类的数量要控制比例,避免smoke样本太多导致模型主次不分。我最终cigarette类标注了4000多框,smoke类标注了1200框左右,训练时把smoke类权重压低,问题不大。
3.3 数据清洗与目录组织:容易忽略但极其重要的一步
清洗比标注还重要。我第一版模型效果差,后来检查发现训练图里有将近两百张是重复帧——拍视频截帧的时候,相邻帧几乎一样,模型相当于把这些重复样本反复学习,验证集稍微变一下角度就崩。
清洗策略分三步:
- 按感知哈希去重,相似度超过0.9的直接删掉。
- 遍历所有xml或txt标注文件,筛选出宽高小于8像素的框——这种框太极端,会让模型学到无意义的纹理,删掉更干净。
- 检查类别标签是否写错,特别是公开数据集抽出来的,统一脚本核对一遍。
目录结构按YOLO标准格式组织:
dataset/ images/ train/ val/ labels/ train/ val/划分比例8:2。划分时保证同一批连续帧不落在两个集合里,不然模型被“剧透”,验证指标漂亮得离谱,落地上根本没那么强。
4. 训练参数设计与结果判读:别只盯着mAP50
4.1 关键训练参数怎么调
训练过程中我尝试了多组参数组合,最终比较顺的配置是:
| 参数 | 取值 | 说明 |
|---|---|---|
| model | yolov12n.pt | 预训练权重初始化 |
| imgsz | 640 | 兼顾小目标与推理速度 |
| epochs | 120 | 数据量不大,120轮足够收敛 |
| batch | 16 | 根据显存动态调整 |
| optimizer | AdamW | v12默认配AdamW比SGD稳一点 |
| lr0 | 0.001 | 微调场景别太高 |
| lrf | 0.01 | 余弦退火收尾 |
| mosaic | 0.8 | 保留数据增强,提升泛化 |
| close_mosaic | 10 | 最后10轮关闭Mosaic,避免过度增强干扰 |
| cos_lr | True | 配合学习率调度 |
开头几轮不要急着看效果。抽烟目标和人的脸、手重合度高,模型前期会把几乎所有细长目标都当成cigarette,这是正常的。重点看第30轮到60轮之间的收敛曲线,如果mAP50一直不升,优先检查数据清洗,而不是调参。
4.2 结果文件怎么读:混淆矩阵和PR曲线要结合看
训练结束后,Ultralytics会输出results.csv和混淆矩阵。很多人只翻一眼mAP50就下结论,这对吸烟检测项目来说容易误判。原因在于这类项目存在严重的正负样本不均衡——背景区域远比烟支区域多,mAP50高可能只是因为负样本判得准,并不意味着烟支不漏检。
我习惯这样看结果:
- 先看混淆矩阵中cigarette类的漏检率(真实为cigarette但预测为background)。
- 再看PR曲线在低置信度区间是否还有明显拐点。如果模型在Recall=0.8附近置信度就掉得很快,说明训练数据里的难样本不够,需要补充遮挡场景。
- 最后看F1-confidence曲线,找到F1最高的置信度区间。我这个项目最终把置信度阈值定在0.4,既能压住误检,也不至于把真烟滤掉。
4.3 训练过程中最常见的两个坑
第一个坑是Mosaic增强关闭时机不对。有次我把close_mosaic设成0,最后一轮精度掉得厉害,val/box_loss突然抬头。后面改成最后10轮关闭,验证集损失平稳多了。
第二个坑是权重文件路径选错。训练结束后文件夹里有best.pt和last.pt,网上有些教程让直接用last.pt部署,其实last是最后一步的模型状态,不是最优状态。部署务必用best.pt,这个错特别低级,但群里见到的频率不低。
5. 登录注册与用户管理:PyQt5界面里最容易被忽略的模块
5.1 为什么识别系统要带登录注册
很多同类项目demo就是打开界面直接开始识别,我偏要加一个登录注册模块。原因不复杂:识别系统的运行日志里包含时间、截图、统计结果,这些信息属于行为数据,如果人人都能打开软件随意查看,后续的管理责任说不清。另一个原因是多用户场景——不同管理员关心的摄像头分组、统计报表不一样,登录后才能按用户加载对应的配置。
在这一步上我没有做复杂的权限系统,一个轻量的用户表足够。注册、登录、记住密码、退出登录,四项功能齐全,数据落在本地SQLite里,不需要额外部署数据库服务。
5.2 SQLite存储与密码安全策略
用户表结构大概是这样:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, salt TEXT NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );密码绝对不能明文存。我用的方案是每个用户随机生成一个salt,然后对“salt+密码”做SHA-256哈希,库里面存salt和hash两个字段。登录时重新拼接计算,比对哈希结果。这里说明一下,生产级系统建议升级到bcrypt或PBKDF2,这个项目是桌面单机部署,SHA-256加salt已经够用,但读者如果做Web版,不要学这个简化版本。
注册界面的校验也要做完整:用户名非空、长度3到20位、不包含特殊字符、两次密码一致、用户名不重复。每一项用弹窗提示反馈,不能静默失败,否则用户不知道发生了什么。
5.3 登录窗口与主窗口的切换逻辑
PyQt5里最常见的错误是登录窗口关闭了,但主窗口还没初始化,导致程序直接退出。我的做法是在主程序入口先创建登录窗口并exec(),登录成功后把用户信息传给主窗口构造函数,主窗口show(),登录窗口hide()而不是close()。这样退出主程序时才真正释放登录窗口资源。
切换代码骨架大致是这样:
app = QApplication(sys.argv) login = LoginWindow() if login.exec() == QDialog.Accepted: main_win = MainWindow(login.user_info) main_win.show() sys.exit(app.exec_())这套逻辑跑得很顺。记住用户名、密码的“记住我”勾选框,用QSettings存起来,下次启动自动填充,体验会好很多。
6. 识别主界面与多线程推理:UI卡顿的根因和解决办法
6.1 主界面布局:预览区、控制区、日志区三块
主窗体设计不追求花哨,追求信息一目了然。顶部是一块大幅图像预览区,用来显示原始摄像头画面或识别后的标注画面。左侧放控制面板:模式切换按钮(图片/视频/摄像头)、文件选择按钮、开始识别与停止识别按钮、置信度滑块。底部是日志输出区,用来打印识别时间、目标数量、帧率。
识别结果除了画框,还把每一帧的检测框数量做一个累计统计。点击“导出报表”按钮可以生成当天的检测记录CSV文件,包含时间、置信度、框坐标和截图路径。这个功能直接对应落地场景——门卫或者后台管理员不需要盯画面,只看报表就能知道哪个时间段、哪个点位出现过吸烟行为。
6.2 多线程推理:不能在UI线程里跑YOLO
UI卡顿是所有PyQt视觉项目的通病,根因只有一个:把模型推理、图片缩放、格式转换这些耗时操作全部放在主线程里执行。QTimer每30毫秒触发一次刷新,结果上一次推理还没跑完,下一次又来了,消息队列堆满,界面自然僵住。
正确做法是独立推理线程。我在项目里用QThread实现,主线程只负责发信号和接收结果:
class InferenceThread(QThread): frame_ready = pyqtSignal(object) result_ready = pyqtSignal(object) def run(self): while self.running: ret, frame = self.capture.read() if not ret: continue results = self.model.predict(frame, conf=self.conf_thres, verbose=False) annotated = results[0].plot() self.frame_ready.emit(annotated)注意emit的是深拷贝对象或只读数据,不要在子线程里直接修改主线程的界面控件,否则大概率出现段错误。我踩过一次,原因就是在线程里调了QLabel.setText,程序随机崩溃,调了半天才发现是线程安全问题。
6.3 摄像头模式下OpenCV的读取与显示优化
读摄像头用cv2.VideoCapture(0),默认分辨率可能只有640x480,画质太差。手动设置成1280x720:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)如果要追求更高帧率,可以把推理帧率限定在15帧,预览帧率保持30帧。这里不搞复杂方案,做法就是解码后先进一个队列,推理线程按帧间隔取帧,展示的是最近一帧而不是逐帧全部处理,观感上很流畅,CPU和显存占用又低。
7. 部署实测:摄像头、图片、视频三种模式下的真实表现
7.1 三种输入源如何统一推理接口
我把识别逻辑封装成一个SmokeDetector类,内部只暴露process_frame方法,输入是ndarray,输出是标注后的ndarray和检测统计。图片模式下直接整帧调用一次;视频模式下按帧循环调用;摄像头模式下由推理线程循环调用。三个模式共用同一套检测逻辑,代码量少,维护也省心。
图片模式适合单张快速验证,响应时间在最严苛的CPU环境下也就1到2秒。视频模式对历史录像做批量分析,比如楼道摄像头存下来的文件,拖进去就能跑,全程不需要人工干预。摄像头模式则是实时巡检,识别到目标后会在框上标出置信度,同时写入日志。
7.2 实测数据:CPU和GPU的帧率差异
我自己的测试环境有两台,一台是RTX 3060 Laptop(6G显存),另一台是纯CPU核显笔记本。实测数据供参考:
- RTX 3060 + YOLOv12n:640x640输入,推理耗时约25到35毫秒,加上NMS后整体30到45毫秒,界面显示约20帧。
- 纯CPU(i7-12700H):单帧推理约400到600毫秒,界面只能到2到3帧,明显不够实时。
- 摄像头1280x720分辨率下,输入先缩放再推理,帧率损失不大,但预览画面质量保持得很好。
如果你CPU部署还嫌慢,可以考虑把模型导出为ONNX,用onnxruntime的CPU执行提供优化,速度通常比PyTorch原版推理快30%以上。我这把导出命令留给你们参考:
yolo export model=runs/train/exp/weights/best.pt format=onnx imgsz=640 dynamic=False导出后替换加载权重的方式,用onnxruntime跑推理,注意检查一下输出层的格式和NMS前置处理,不然容易出漏框。
7.3 部署过程中碰到的两个隐蔽问题
第一个是中文路径问题。Windows下如果项目目录、视频文件或图片文件名带中文,cv2.imread经常读取失败或返回None。这跟OpenCV的imread内部编码有关,解决方法是先用numpy和cv2.imdecode绕一圈:
data = np.fromfile(file_path, dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR)第二个是置信度阈值与环境光敏感。同一套权重在室内日光灯和傍晚黄昏下,误检数量能差出一倍。我最后做了一层业务侧过滤:检查检测框区域的平均亮度,如果低于某个阈值就降低判决权重,避免把反光、玻璃上的影子当成烟支。这个规则简单但有效,比重新训练一个模型成本低得多。
7.4 安全与合规建议
吸烟识别系统涉及对人员行为的监控,部署前务必跟使用方明确告知政策边界。建议系统只处理检测结果和统计信息,不长期保存原始视频流。日志里不要记录可识别身份的人脸截图,如果确实需要留存,建议对检测区域做马赛克处理再存储。这个建议不仅在法律层面有意义,也是减少后续运维纠纷的必要手段。
8. 一套跑完,说点掏心窝子的经验
这套项目做完,最大的感悟是数据和规则设计决定了最终效果,模型架构和调参只是工具层面的事情。YOLOv12提供了一个更强的骨架,但如果Cigarette类只有几十张公开图,任何新模型都救不回来。真正要花精力的地方在数据采集、清洗和场景规则的设计。
最后分享两个实际心得。第一,界面上的置信度滑块一定要保留,不要写死。不同点位的摄像头,光照条件不同,最佳置信度阈值可能差0.1到0.2,滑块调优比频繁换模型权重省事得多。第二,save_dir建议自动按日期生成,每天一个数据库,按月归档,后续统计分析会非常方便。我一开始把全部识别记录写到一个数据库文件,结果一个月跑了八十万条记录,查询速度肉眼可见地变慢,拆分之后顺畅多了。
如果你现在正准备做类似系统,我建议先拿YOLOv12n跑通端到端流程,再考虑换更大的s或m模型。小模型先把数据闭环跑清楚,发现瓶颈是在数据还是在模型,然后有针对性地优化——这套顺序永远比一上来就追求高精度更有效。