1. 这套系统到底解决什么问题?
先说你为什么需要它。在化工厂、油站、实验室或者大型设备房,管道接头、阀门、法兰盘、泵体密封处,一旦出现渗漏,初期可能就是一小摊积水、一点油渍,肉眼巡查很难及时发现。等漏到肉眼一眼看穿的地步,往往已经造成了不小的浪费,甚至带来安全隐患。传统方案要么靠人工定时巡检,要么靠压力传感器这类间接手段,前者漏检率高、强度大,后者只能判断"有没有异常",说不清楚"哪里在漏"。
这套基于 YOLOv12 的设备泄漏检测系统,本质上是把"老师傅的眼睛"塞进摄像头里:用深度学习模型对监控画面做实时分析,框出正在渗漏的位置,标出置信度,并且通过前端界面把检测结果可视化。整体交付物很完整:Python 写的核心检测工程、训练好的权重文件、带登录注册的用户界面、前后端联调的完整流程,拿来就能跑,也方便二次开发。
它适合谁来参考?如果你是在校学生,想找一份能写进简历的 CV 实战项目;如果你是做工业智能巡检的开发者,想快速搭一版检测原型;甚至你只是对 YOLO 系列迭代好奇、想看看从 YOLOv5 到 YOLOv12 在数据标注和部署上到底有哪些变化——这篇内容都能给你一个相对完整的参照。
我自己在做类似项目时最大的感受是:检测模型本身不是最耗时间的,真正磨人的是数据采集、标注规范和部署时那一堆"边界情况"。下面的内容我会把从数据集制作、模型训练到桌面 UI 集成的全过程拆开讲,很多细节是文档里不会写的,属于实操之后才会踩到的坑。
2. 方案选型:为什么是 YOLOv12?
2.1 从算法演进看 YOLOv12 的位置
YOLO 系列发展到今天,v5 已经工业落地最广,v8 在分类和分割上做得更顺手,而 YOLOv12 这个版本最让人关注的点,是它在骨干网络里引入了对注意力机制的进一步重构。
简单类比一下:以前 YOLO 的骨干网络就像一个经验丰富的保安,负责在整张图上"扫视",哪里都要看一眼;但注意力机制让网络学会了"重点盯防"——它会根据任务需要,动态决定把更多计算资源放在哪些区域。YOLOv12 在保持实时性这个 YOLO 家族传统优势的前提下,把注意力机制做得更适合小目标检测,而泄漏初期恰恰就是"小目标"工况:渗漏点小、对比度低、容易被环境背景淹没。
选它而不是选 v5/v8,对我来说核心原因有三个:
- 小目标漏检率更低。泄漏点在画面中通常只占几十个像素,v12 在特征融合阶段的改进对这类目标友好。
- 推理速度依旧能打。实测在普通独立显卡上处理 1080P 视频,帧率能稳定在 30+,完全满足实时监控。
- 工程生态成熟。Ultralytics 框架的封装让训练、导出、部署的流程基本不变,从 v8 迁到 v12 的成本很低。
当然,如果你手头的硬件非常老旧,或者业务场景对帧率要求极高,退回 YOLOv8s 也完全可行,算法选型本来就不是"越新越好",而是"够用且稳"。
2.2 整体技术路线和目录结构
系统的整体链路是这样的:
摄像头/视频文件 → 抽帧 → YOLOv12 模型推理 → 检测到泄漏目标 → 标注框 + 置信度 → UI 界面实时显示 → 用户可登录、查看历史记录、设置报警参数 → 报警信息入库
从代码工程的角度,核心模块包括:
- 数据层:存放标注好的图像数据集、划分训练/验证集;
- 训练层:YOLOv12 的配置文件、训练脚本、权重输出目录;
- 业务层:负责加载模型、做推理、过滤低置信度结果、判断是否触发报警;
- 界面层:基于 PySide6(或 Tkinter + 自绘组件)实现的桌面应用,含登录注册页、监控主页面、报警记录页;
- 数据持久化:SQLite 存用户账号、检测历史、报警日志。
这种分层的价值在于,模型可以独立迭代,界面不用动;界面哪怕整个换掉,只要业务层暴露的接口不变,系统依然能跑。做项目时千万不要图省事把推理逻辑和 UI 逻辑揉在一个文件里,后面改一个参数都要提心吊胆。
3. 数据准备:成也标注,败也标注
3.1 建一个能用的泄漏检测数据集
做泄漏检测,最大的现实障碍是"泄漏样本难得"。工厂里不可能为了让你拍数据集故意去漏液漏气。我的做法是分成三路并行:
- 公共数据集:网上搜索管道泄漏、液体渗漏、油渍检测类的公开图像数据集,虽然和你的现场环境差异大,但用于预训练非常合适。
- 现场实拍:找一台实验台架,人为控制少量液体渗出,用手机或监控摄像头从多角度、多光照条件下拍摄。
- 数据增强兜底:对已有样本做亮度扰动、对比度扰动、仿射变换、模拟雨雾效果,尽量把样本分布撑开。
数据量方面,我的经验是:单类别泄漏检测,起步 1500~3000 张标注图是能训练出一个像样模型的量级,这里面要保证至少 70% 是真实拍摄,不要全靠增强。类别标签我建议按场景拆,比如liquid_leak(液体渗漏)、gas_leak(气体泄漏,通常表现为背景畸变)、wet_stain(渗漏后留下的湿渍)。宁可类别多一点,也不要一个背景图硬塞进数据里,模型会学到一堆噪声。
3.2 YOLO 格式标注的核心要点
YOLOv12 训练需要的数据格式是 YOLOv11/v8 一脉相承的 txt 标注文件,每张图片对应一个同名 .txt,放在同目录下。txt 中每一行表示一个目标,格式为:
类别ID 中心点x坐标 中心点y坐标 目标宽度 目标高度注意,这里的坐标不是像素值,而是归一化到 [0,1] 的浮点数。比如图片宽 1280、高 720,目标框中心在像素 (640, 360),宽 200,高 100,那么这一行就是:
0 0.5 0.5 0.15625 0.13889用标注工具生成这类文件时,有的工具默认输出 VOC 格式的 XML,有的输出 COCO 格式的 JSON,记得转换。我常用的是开源标注工具,标注完直接导出 YOLO 格式,省去自己写转换脚本的麻烦。标注过程中的几个血泪经验:
- 框的边界要紧贴目标,宁可略紧不要偏松。泄漏物的边缘往往模糊,标注人员容易下意识把边缘也框进去,结果模型学到的目标特征里混入大量背景。
- 小目标不要漏标。画面上一个只有 20×20 像素的渗漏点,如果漏标了,模型会在训练时把这块区域视为"负样本",等于在主动教模型"这里没问题",后果很严重。
- 遮挡物边界情况必须标。比如管道挡住了泄漏物的一半,要按可见部分标注,不要凭"感觉"补全。
- 做一次一致性抽检。标注完成后,随机抽 50 张图,把标注框可视化叠加到原图上快速浏览,发现框明显偏移马上修正。这一步很多人偷懒,但模型训练效果不好时,回查数据基本都是标歪了。
3.3 数据集划分和目录组织
数据集目录我建议固定成这个结构:
datasets/leak/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/训练集和验证集的比例按 8:2 或者 9:1 都行,但要确保验证集里的场景能代表真实部署环境,比如至少包含几个不同光照、不同拍摄角度的样本。如果所有验证集图片都是同一台相机同一个角度拍的,模型过拟合了你都不知道,一换摄像头就原形毕露。
此外还要写一个data.yaml,YOLOv12 训练脚本要靠它知道类别数量和类别名:
path: datasets/leak train: images/train val: images/val nc: 3 names: ["liquid_leak", "gas_leak", "wet_stain"]里面的路径建议全部写相对路径,之后项目目录换了位置,配置还能直接用,不然每次移动都要改 yaml,烦得很。
4. 模型训练:把参数调明白再动手
4.1 训练环境与依赖
在开始前,先把环境装对。建议用虚拟环境隔离,避免和系统 Python 环境起冲突。我用这套组合实测最省心:
- Python 3.9 或 3.10
- PyTorch 2.x + CUDA 11.8 或 12.1(看显卡驱动版本)
- Ultralytics 最新版即可(v12 模型直接支持)
- 标注、数据处理需要的 opencv-python、numpy、pandas
装好后验证 GPU 可用:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果打印的是 False,说明你的 PyTorch 装成了 CPU 版,或者 CUDA 版本和驱动不匹配。这一步装错后面全白跑,建议多花十分钟确认一下。
4.2 训练命令和关键参数解读
我通常直接在命令行启动训练,这样参数的修改和记录都清晰:
yolo detect train \ --model yolov12s.pt \ --data datasets/leak/data.yaml \ --epochs 200 \ --imgsz 1280 \ --batch 16 \ --device 0 \ --patience 30 \ --project runs/train \ --name leak_v12参数的意思拆开讲:
yolov12s.pt:s 是 small 版本,速度和精度的平衡点,适合大多数场景。如果显存小就换 yolov12n,精度需求高就上 yolov12m。imgsz 1280:这是最关键的参数之一。泄漏目标小,太低的分辨率会直接让小目标淹没在降采样过程中。我用 640 对比过,mAP 掉了将近 20 个百分点,所以能用 1280 就别迁就显存。patience 30:30 轮验证集指标没有提升就提前停止训练,防止过度拟合。batch 16:显存不够就把 batch 降到 8,或者用--device 0,1开多卡。
训练过程中要盯着两个曲线:train/loss和val/box_loss。如果 val 的 loss 先降后升,而 train 的 loss 还在降,那就是过拟合了;如果两个 loss 都在震荡,通常是学习率设置问题,试试把默认学习率调低到 0.001。
4.3 训练中常见的问题与处理
我在训练这个泄漏检测模型时连续踩了几次坑,记录几个典型的:
- 显存溢出(OOM):图像尺寸 1280 加上大 batch 很容易爆显存。解决办法是减小 batch,而不是盲目降低图像尺寸。batch=8、图像尺寸 1280 在 12GB 显存上是可以跑的。
- 验证集 mAP 特别低但训练集很高:典型过拟合。把
--patience调低,增加数据增强强度,或者干脆增加验证集的样本多样性。 - 泄漏点检测不全:同一张图里明明有三个渗漏点,模型只给出两个框。回查发现,有两个渗漏点非常小,且边界模糊,在标注阶段被轻视了。修复方案是把这些小目标单独提出来,复制粘贴到不同背景上做合成数据。
4.4 导出与模型压缩
训练完成后,权重会保存在runs/train/leak_v12/weights/目录下,里面有best.pt和last.pt,用best.pt就好。如果系统部署在无独立的显卡环境,需要把模型导出成 CPU 友好的格式:
yolo export --model runs/train/leak_v12/weights/best.pt --format onnx --dynamic导出 ONNX 后再配合推理框架部署,速度会提升。但如果你的运行环境就是普通 Windows 电脑带独立显卡,直接用.pt文件加载最省事,千万不要为了"显得专业"强行导出又导回,带来不必要的精度损失。
5. 核心推理代码与业务逻辑实现
5.1 模型加载和推理封装
实际项目里不可能每次检测都去裸调命令行,要封装成可复用的推理模块。下面这段代码是我用的骨架,直接放在detector.py里:
import cv2 from ultralytics import YOLO class LeakDetector: def __init__(self, model_path="best.pt", conf_thres=0.35, device="0"): self.model = YOLO(model_path) self.conf_thres = conf_thres self.device = device def detect_frame(self, frame): results = self.model.predict( source=frame, conf=self.conf_thres, device=self.device, verbose=False ) boxes = [] labels = [] scores = [] for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls_id = int(box.cls[0]) conf = float(box.conf[0]) boxes.append([x1, y1, x2, y2]) labels.append(self.model.names[cls_id]) scores.append(conf) return boxes, labels, scores关键点在于conf_thres的取值。泄漏检测场景宁可误报也不该漏报,所以置信度阈值我一般设在 0.25~0.4 之间,而不是默认的 0.5。后续可以通过 UI 里的滑条让用户自己调节,这个参数的弹性正好说明了为什么系统需要界面——不同用户、不同光照条件,最优阈值是不同的。
5.2 从视频流到实时检测循环
如果是摄像头实时监控,检测主循环长这样:
cap = cv2.VideoCapture(0) # 读取本地摄像头 if not cap.isOpened(): raise RuntimeError("无法打开摄像头") detector = LeakDetector("best.pt", conf_thres=0.35) while True: ret, frame = cap.read() if not ret: break boxes, labels, scores = detector.detect_frame(frame) for (x1, y1, x2, y2), label, score in zip(boxes, labels, scores): cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(frame, f"{label} {score:.2f}", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow("Leak Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()注意一个非常实际的性能问题:predict每次调用都有初始化开销,不要在一个循环里反复创建 YOLO 对象,已经写在上面的代码中,只实例化一次,重复调用。帧率优化方面,如果画面中泄漏检测不需要每一帧都做,可以每 2~3 帧跑一次检测然后插入逻辑,能省不少算力。
5.3 数据持久化与报警判定
检测到泄漏之后,要把结果记录下来。我在系统里用一个简单的 SQLite 数据库存三张表:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL ); CREATE TABLE detect_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, frame_path TEXT, leak_type TEXT, confidence REAL ); CREATE TABLE alarm_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, leak_type TEXT, confidence REAL, handle_status TEXT DEFAULT 'pending' );为什么报警判定要做单独的逻辑而不是“检出即报警”?因为摄像头画面里偶尔会出现误检——比如水渍确实存在,但它可能是上次维修后留下的,不是正在发生的新泄漏。更合理的策略是:连续 N 帧(比如 5 帧)都检测到同类泄漏目标,且置信度都在阈值之上,才触发报警。这个"迟滞"判定能大幅降低误报率,实际测试下来很稳。
6. UI 界面与登录注册模块集成
6.1 用 PySide6 搭建主界面
桌面 UI 我用的是 PySide6(Qt 的 Python 绑定),对比 Tkinter,最大的优势是控件丰富,做监控画面这种需要实时刷新视频流的界面更顺手。主界面拆成几个区域:
- 左侧:视频实时检测画面;
- 右上:检测信息面板(当前类别、置信度、FPS);
- 右下:报警历史列表,双击可以查看当时保存的帧截图;
- 顶部:摄像头源选择(本地摄像头/视频文件/RTSP 流)、灵敏度滑条、停止/开始按钮。
视频流刷新到界面上的思路是:在工作线程里跑检测循环,把结果通过信号机制发回主线程更新画面。在 Qt 里直接在主线程做耗时操作会导致界面卡死,这是新手最容易踩的坑。用QThread加信号的方式,画面刷新能保持流畅。
6.2 登录注册界面和状态保持
登录注册用QDialog实现,注册信息写入 SQLite 的users表,密码存储不能用明文,直接用哈希处理。
import hashlib import secrets def hash_password(password: str, salt: str = None): if salt is None: salt = secrets.token_hex(16) digest = hashlib.sha256((salt + password).encode()).hexdigest() return f"{salt}${digest}" def verify_password(password: str, stored: str): salt, digest = stored.split("$") return hash_password(password, salt) == stored登录成功的状态用一个简单的单例Session对象保存,主界面构造时读取当前用户名,显示在界面上。登出时清空状态回到登录页。
这套流程对外看起来很简单,但涉及的核心逻辑是登录态和检测进程的生命周期管理。登出时,除了关闭界面,还要保证后台检测线程安全停止、释放摄像头资源,否则下次登录时会发现摄像头被占用打不开。我在这里踩过坑:最开始没做线程退出标记,登出后摄像头释放失败,然后再次登录就直接报错,必须重启程序。解决办法是在检测线程的循环里检查一个self._running标志,登出时置为 False 并等待线程结束,再关闭摄像头。
6.3 历史记录与截图回放
报警触发时,程序会保存当前帧的截图到本地目录,路径记录到alarm_log表的frame_path字段。这个功能非常重要,因为在真实场景中,运维人员需要回到报警现场查看情况,判断是真正的泄漏还是环境干扰。界面上双击报警记录,能直接打开保存的帧图,并显示模型检测到的标注框。
截图保存格式建议 JPEG 而非 PNG,因为泄漏检测画面大多是监控视频的连续帧,JPEG 压缩率更高,长时间运行下来能节省大量磁盘空间。
7. 遇到过的真实问题与排查办法
7.1 摄像头源不兼容问题
用 OpenCV 读取 RTSP 摄像头流时,常见的报错是无法打开视频流。这个问题的原因多半是底层 FFmpeg 库不支持某些压缩格式。我实验下来最稳定的解法是在打开摄像头时添加参数:
cap = cv2.VideoCapture("rtsp://user:password@192.168.1.100:554/stream1", cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)设置缓冲区大小为 1 的作用是降低延迟,不设的话默认缓冲会堆积好几帧,实时性大打折扣。
7.2 检测准确率够了但 UI 刷新掉帧
模型推理耗时 30ms,按理说界面不该卡,但实际 UI 刷新就是卡。分析后发现问题出在 OpenCV 的imshow和 Qt 的刷新机制冲突:两套窗口系统在同一时刻抢占主线程。解决办法是不在检测循环里用cv2.imshow,改为把帧转成 QImage 发给 Qt 界面控件绘制,彻底移除 OpenCV 的窗口操作。
def convert_cv_to_qimage(cv_img): rgb_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_img.shape bytes_per_line = ch * w return QImage(rgb_img.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()这个转换函数几乎是 PySide6 视频界面必写的工具函数。
7.3 多类别的漏检不平衡
如果数据集中wet_stain样本特别多,gas_leak很少,模型会严重偏向前者。对策有两个:一是做类别重采样,让每个类别的样本数量尽量接近;二是在训练时给少数类提高损失权重。Ultralytics 框架中,类别权重可以通过修改数据集配置或者在损失函数层面调整来实现,如果你不熟悉这一层,最简单方法还是把各类别的样本数量拉齐,别偷懒。
7.4 部署到另一台电脑,模型加载不了
模型在 A 机器上训练正常,复制到 B 机器上加载时报版本不兼容错误。原因是训练时用的 Ultralytics 版本和部署机的版本不一致。稳妥做法是训练机上导出一次 ONNX 格式,部署机上用 ONNX Runtime 加载,这样模型就和 PyTorch 版本解耦了。或者更简单:部署机上安装和训练机完全一致的依赖版本,直接把整个虚拟环境打包迁移。
8. 这套系统的扩展方向
几个我很推荐你继续做的扩展:
- 报警消息推送:检测到泄漏后通过消息通道推送给运维群,比只在界面上弹窗实用得多。代码改动不大,在报警判定通过后加一个消息接口就行。
- 热力图叠加:把一段时间内泄漏出现的坐标渲染成热力图,投到大屏上,能直观看出哪个区域泄漏多发,给设备维护提供数据支撑。
- 多路视频并行检测:现在的系统是单路摄像头,如果你需要同时看 8 路画面,可以考虑把模型推理放到独立的推理服务进程,界面端只负责拉取结果,互不阻塞。
- 泄漏量趋势估计:根据检测框的面积变化估算泄漏速度的变化,虽然做不到真实流量计量,但作为趋势参考完全够用。
我个人在实际项目里最看重的一个细节是:系统做出来后,一定要花时间收集几段"非泄漏"的正常监控视频,专门用来测试误报率。很多时候模型在自己采集的验证集上表现不错,一到现场就疯狂误报,就是因为没有把负样本视频单独跑一遍。你把正常工况下的误报率压到足够低,模型才真正算"可用",而不是"能跑"。
最后再分享一个小技巧:YOLOv12 的检测头对小目标本身就比较友好,但如果你发现某个具体场景下泄漏点实在太小,也可以考虑做"切图推理"——把画面切成四个象限,分别送进模型,再把结果合并,小目标的检测效果会有肉眼可见的提升。代价是推理耗时翻倍,但如果你分析的是离线视频或者帧率要求不高,这招非常值。做完这套系统,你会发现真正给你成就感的不是"模型能检测了",而是从数据、训练、部署到界面的整条链路都能自己掌握,任何一个环节出问题,你都知道去哪里排查,这种掌控感才是这个项目最大的收获。