YOLO11分割+PyQt:深度学习实现摄像头积水检测

发布时间:2026/10/11 0:44:31
YOLO11分割+PyQt:深度学习实现摄像头积水检测 简介一套基于Python深度学习的积水图像分割检测方案以YOLOv11为核心模型面向需要训练积水检测与分割模型的开发者、学生和视觉算法从业者。代码基于PyTorch环境编写包含摄像头实时识别与PyQt界面覆盖从数据准备、模型训练到界面演示的完整链路。资源共1108个文件主要涵盖442张jpg标注图像、429份txt标签、214份json标注文件以及pt模型权重、yaml配置、csv训练结果和Python脚本压缩包约424.92MB目录结构清晰便于按需调用。目前已有98人参与学习。使用者可获得可复用的积水检测模型权重、规范化标注数据集、YOLOv11训练与验证脚本以及带PyQt界面的摄像头识别演示为城市内涝监测、户外积水预警等场景提供落地参考。1. 深度学习积水分割检测摄像头图像如何变成积水面积和报警把“基于python深度学习对积水图像分割检测”这句话翻成人话就是对着监控画面或USB摄像头让程序把路面积水像抠图一样逐像素圈出来而不是只画个大框。标题里这包东西的核心价值在于给了一套能自己标注、能重新训练的yolo11分割模型代码和一个PyQt桌面界面落地场景非常具体——智慧水务、停车场巡检、厂区防汛不需要埋水位传感器也可以知道低洼区域积水占了多少面积。难点也不在模型本身而在数据集质量、分割掩膜与界面的工程衔接尤其是阴影、倒影、夜间灯光这些会反复干扰模型的坑。下面这套流程我按“拆解项目结构 → 准备数据训练 → 摄像头界面联调 → 避坑 → 进阶验证”的顺序讲清楚保证拿到类似项目后能直接照着改。2. 项目结构拆解为什么用yolo11做分割而不是单纯的目标检测2.1 从“框”到“像素”积水检测到底需要什么输出结果先得把“图像分割检测”里“分割”两个字掰清楚。普通检测模型输出的是一组矩形框坐标是x1y1x2y2外加一个置信度。对积水这种对象来说矩形框是远远不够的水不是刚性物体轮廓极不规则一道积水横跨多半条车道矩形框会把大量无水区域圈进去。你拿这种框去算“积水占了多少比例”误差可能到一倍以上。分割模型则输出一张和原图尺寸对应的掩膜mask逐像素判断“有水/没水”下游系统可以稳定算出面积比和积水的分布位置这个指标才能用来触发预警或联动排水泵。那为什么选yolo11的分割头而不是直接上Unet、Segformer这类纯语义分割网络我按实战成本说三个理由。第一积水数据量通常只有几百上千张远不够训深层分割网络而yolo11-seg是实例分割思路先出目标框再在框里细化掩膜对中小数据集更友好。第二yolo11的mask和检测框同时输出实际项目里框也很有用可以框出积水区域轮廓用于剔除车辆、判断积水在哪个车道。第三它本身就是工程化封装好的开源推理链路从训练到导出再到摄像头接入代码量比从零搭Unet少一个数量级。这套取舍和“动手深度学习”里反复强调的原则一致不是网络越深越好而是网络结构和你的数据规模、部署环境匹配才是真的好。2.2 一份可训练积水项目的常见目录结构与模块边界一份能正常跑起来的“积水图像分割检测”项目我拿到手后的第一件事永远是先看目录而不是急着装依赖。常见结构长这样flood_seg/ ├── dataset/ │ ├── images/ # 原始图像/视频抽帧 │ │ ├── road_001000.jpg │ │ └── ... │ ├── labels/ # 与图像同名的yolo-seg标注txt │ │ ├── road_001000.txt │ │ └── ... │ ├── data.yaml # 类名、训练/验证路径 │ └── split.py # 随机划分train/val ├── weights/ │ ├── yolo11n-seg.pt # 预训练分割权重 │ ├── best.pt # 自己训出的最优权重 │ └── last.pt ├── train.py # 训练入口 ├── detect_video.py # 视频/摄像头推理入口 ├── ui/ │ ├── main_window.py # PyQt主窗口 │ ├── video_thread.py # 摄像头采集推理线程 │ └── ui_main.ui └── requirements.txt这个结构的核心原则是模块分离train.py只负责读data.yaml并调用ultralytics的YOLO.train完全不碰摄像头detect_video.py负责读摄像头或视频、推理、把掩膜和面积比写日志不导入Qt真正的界面入口在ui/main_window.py。训练代码、推理代码、界面代码彼此独立好处是后续换摄像头或改报警逻辑时不需要动训练流程调试的时候范围也清晰。拿到一个新代码包我一般会先写一个最短推理脚本验证权重和图像通路是否正常不要一上来就开界面# quick_infer.py: 先用单张图确认权重和图像通路 from ultralytics import YOLO model YOLO(weights/best.pt) # 没有best.pt时先放yolo11n-seg.pt results model.predict( dataset/images/road_001000.jpg, conf0.25, iou0.5, saveTrue, projectdebug_out, namequick )这段代码的作用是提前暴露两个最常见问题weights路径不存在或者图像路径和txt标注对不上。conf0.25表示置信度低于0.25的检测直接丢掉iou0.5是NMS去重阈值。第一次验证用单张图就够了因为如果连单图都输出不了掩膜后面接摄像头只会更难排查。跑完后在debug_out/quick目录里看叠加分割效果的图确认mask确实覆盖在积水区域上再往下继续做训练或界面联调。3. 数据准备与模型训练把积水标注变成可用的分割权重3.1 积水数据的获取、筛选和标注哪些镜头不能要积水分割训练的成败八成在数据。模型在测试视频里“翻车”绝大多数不是因为网络结构不够好而是训练集与真实场景的分布差异太大。获取数据的常见做法是雨后到低洼路段用手机或行车记录仪拍视频然后每5到10帧抽一帧存成jpg如果有固定点位监控直接导出监控录像片段抽帧更好因为和部署时的视角最接近。图像里至少要包含三种状态刚下湿的浅积水、能清晰看到倒影的镜面水、车辆驶过时的水花水纹。没有这几种状态模型只能学会识别水面本身却学不会区分水和湿沥青。标注工具我一般用labelme以多边形polygon形式把“积水”区域勾出来。筛选图像时有三类镜头直接弃用一是画面模糊到人眼都分不清边界的二是积水面积小于全图五十分之一的三是积水被车辆大面积遮挡的。少标一点没关系最怕标得脏。一个常见的标注误区是把湿润路面、雨水污渍也顺手标成积水。YOLO分割训练时模型会把这些噪声当成正样本学进去白天看不出问题一到阴雨天就开始疯狂误报。宁可一个标签反复修改也不能把模糊样本推进数据集。3.2 把标注数据转成YOLO分割格式labelme到yolo-seg txt的转换与目录检查yolo11-seg训练需要的数据格式不是labelme的json而是每张图像对应一个同名txt每行表示一个目标第一个数字是类别id后面跟着按顺序排列的多组归一化坐标。转换脚本是整个数据流水线里最容易出错的地方坐标归一化、类别映射、文件命名只要错一处训练时就报数据错或掩膜错位。以一张宽img_w、高img_h的图为例import json def labelme_to_yoloseg(json_path, img_w, img_h, class_ids, out_txt): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: if shape[shape_type] ! polygon: continue label shape[label] if label not in class_ids: continue cls_id class_ids[label] points shape[points] # 归一化到[0,1]x除以图宽y除以图高 norm [] for x, y in points: norm.append(f{x / img_w:.6f}) norm.append(f{y / img_h:.6f}) lines.append(f{cls_id} .join(norm)) with open(out_txt, w, encodingutf-8) as t: t.write(\n.join(lines))逻辑说明labelme的points是像素坐标必须除以对应图像的宽和高得到小数坐标yolo11训练时无法处理像素坐标的标注。class_ids是一个字典比如{water: 0}如果数据里还分了浅水和深水则按实际需求编号。这里最容易踩的是宽高顺序不要用x除以img_h、y除以img_w一旦颠倒掩膜就会沿对角线扭曲训练不会报错但验证时你根本看不出积水在哪里。转换完成后还要做一次“标注自检”我一般写几行代码检查每个txt是否为空、坐标是否都在0到1之间、图像文件名和txt是否一一对应import os label_dir dataset/labels img_dir dataset/images for txt_name in os.listdir(label_dir): stem txt_name[:-4] if not os.path.exists(os.path.join(img_dir, stem .jpg)): print(缺图:, stem) for line in open(os.path.join(label_dir, txt_name)): nums line.strip().split() if len(nums) 7: # 至少1个类别id加3个点 print(点数异常:, stem)这步是给后面训练买的一份“后悔药”。很多源码包里的数据没有做这个检查新手直接跑训练到中途才报Box坐标出错浪费时间。这个自检脚本花五分钟能省半天排查。3.3 调用yolo11开始训练参数设定与恢复训练数据准备好后训练入口其实很轻。先建data.yaml内容指向train和val目录以及类别名path: dataset train: images val: images names: 0: water这里有个细节如果标注数量少train和val可以都指向images然后在代码里按文件列表划分而不是让ultralytics自己按目录找。我习惯写一个split.py先按85%和15%比例随机划分出两个txt清单再传给训练代码。用data.yaml直接指向全量图的缺点是验证集和训练集可能高度重合指标虚高。训练命令用ultralytics的官方接口写就好from ultralytics import YOLO model YOLO(weights/yolo11n-seg.pt) # 用预训练分割权重初始化 model.train( datadataset/data.yaml, epochs80, imgsz640, batch8, lr00.01, weight_decay0.0005, mosaic0.8, device0, )这里几个参数按我跑积水数据的经验解释imgsz640是速度和精度的折中积水边缘细小但不需要看车道牌那样的大纹理640够用batch新手从8开始显存不够再降到4不要为了“看起来专业”硬上16NVIDIA显卡报CUDA out of memory时你会很难受mosaic0.8表示80%训练轮次开启马赛克数据增强积水数据集样本少这种增强能提供更丰富的背景组合但最后10%轮次最好关掉或降下来让模型习惯真实尺寸weight_decay0.0005是L2正则化防止在几百张图上直接过拟合。训练中断恢复是最容易忽视的能力。如果代码包里的train.py没有提供resume逻辑我强烈建议改成model.train( datadataset/data.yaml, epochs80, imgsz640, batch8, resumeweights/last.pt, # 从上次中断的权重继续 )训练跑完weights目录下会生成best.pt和last.pt。best.pt是在验证集上mAP最高的权重后续摄像头识别和PyQt界面都加载它。4. PyQt摄像头识别界面、线程和掩膜叠加的落地写法4.1 界面布局与模型加载把推理和UI分开是底线用PyQt做积水识别界面最大的工程错误只有一个在Qt主线程里直接跑摄像头循环和模型推理。OpenCV的VideoCapture.read是阻塞的YOLO的predict在GPU上没有显著延迟但在CPU上可能跑到一两百毫秒这些时间一过界面就会变成“未响应”画面直接卡死。正确做法是用QThread单独跑一个工作线程。我常用的界面布局是三块顶部一个QLabel用来显示叠加了分割掩膜的实时画面左下角一个QTextEdit记录检测日志底部三个按钮分别是“开始识别”“停止识别”“切换视角”。主窗口代码只负责接收工作线程发来的信号并刷新控件不碰摄像头也不碰模型。一个关键习惯是模型只加载一次。很多人把YOLO(weights/best.pt)写进run的循环里等于每帧重新初始化一次模型轻则速度爆炸重则显存溢出。正确位置是在QThread的初始化方法里加载run里只做推理。4.2 摄像头帧循环、分割掩膜叠加与报警输出摄像头工作线程的核心逻辑可以拆成四步读取一帧、缩放推理、把mask缩回原始尺寸、叠加绘制。写成代码大致是这个形态import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class CameraThread(QThread): frame_ready pyqtSignal(object) # 把叠加后的BGR图像发给界面 water_ratio pyqtSignal(float) # 把当前帧积水面积比发出去 def __init__(self, model_path, cam_id, parentNone): super().__init__(parent) self.model YOLO(model_path) # 模型只初始化一次 self.cam_id cam_id self._running True def run(self): cap cv2.VideoCapture(self.cam_id) # 0为内置摄像头1为USB if not cap.isOpened(): self.frame_ready.emit(None) return while self._running: ok, frame cap.read() if not ok: continue results self.model.predict( frame, imgsz640, conf0.25, verboseFalse ) for r in results: if r.masks is None: continue mask r.masks.data[0].cpu().numpy() # 0/1矩阵 mask cv2.resize( mask, (frame.shape[1], frame.shape[0]), interpolationcv2.INTER_NEAREST ) water_pixels int((mask 0.5).sum()) ratio water_pixels / (mask.shape[0] * mask.shape[1]) # 把mask以半透明方式叠加到原图 overlay frame.copy() overlay[mask 0.5] (0, 180, 255) # BGR下的橙色 frame cv2.addWeighted(overlay, 0.35, frame, 0.65, 0) # 在画面左上角写面积比 cv2.putText(frame, fwater: {ratio*100:.1f}%, (12, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) self.frame_ready.emit(frame) self.water_ratio.emit(ratio if results and results[0].masks else 0.0) cap.release()拆开说几个细节。r.masks.data[0]拿到的mask是高和宽都为640的0/1矩阵它代表输入尺寸下的分割结果不能直接画到原图上必须resize回原图的宽高。这里用INTER_NEAREST而不是默认的双线性插值是因为mask是二值图双线性插值会产生0.3这样的过渡值叠加时会发灰。water_ratio信号单独发出来界面就可以独立处理报警不用关心画面绘制逻辑。这样设计的另一个好处是可以接网络摄像头把cam_id换成RTSP地址即可例如cv2.VideoCapture(rtsp://admin:passwordip:554/stream)。代码里不需要做任何别的修改因为VideoCapture对各种视频源统一了接口。唯一要注意的是RTSP流在网络抖动时会丢帧cap.read可能长时间阻塞后面避坑篇会提缓冲处理。4.3 面积比例、报警阈值与水位换算界面端收到water_ratio信号后要处理的不只是显示一个数字还有消抖和报警。单帧的掩膜往往受水花和反光影响面积比会在一个区间里跳来跳去直接拿一帧数据触发报警会误报不断。我一般的做法是做一个长度为5的滑动窗口取最近5帧的平均值作为当前面积比只有平均值持续超过预警阈值才报警。from collections import deque class AlarmFilter: def __init__(self, window5, warn_threshold0.08, alarm_threshold0.15): self.buf deque(maxlenwindow) self.warn_threshold warn_threshold self.alarm_threshold alarm_threshold def update(self, ratio): self.buf.append(ratio) avg sum(self.buf) / len(self.buf) if avg self.alarm_threshold: return alarm if avg self.warn_threshold: return warn return normalwarn_threshold和alarm_threshold怎么设取决于摄像头安装高度和镜头视场角。我用过一个经验公式假设一条标准车道宽约3.5米摄像头看到的纵向距离约8米那么当画面上积水面积比达到8%时实际积水面积约等于2.2平方米这个量已经能让非机动车道出现明显水洼。如果摄像头视角更俯视比值要适当提高如果画面里只有一小块低洼区则应该把阈值调低。测试时先在现场人为放水标定一两次比拍脑袋准得多。5. 避坑与排查从加载模型到夜间误报的五个常见问题5.1 坑一CUDA没有被PyTorch使用训练和推理都变“龟速”现象代码能跑显卡也在设备列表里但训练一个epoch要十分钟推理一帧要五百毫秒。原因多半是PyTorch装成了CPU版本或者CUDA版本和驱动不匹配ultralytics默认以CPU运行。解决时先做一行诊断python -c import torch; print(torch.__version__, torch.cuda.is_available())如果输出第二项是False说明torch没识别到显卡。常见的正确装法是先到PyTorch官网根据你的CUDA版本复制安装命令不要用pip install torch直接装CPU版。装完后用YOLO(weights/best.pt)加载模型显存足够时它会自动跑到GPU上。踩过这个坑的人通常会记住一句话模型能不能用GPU由torch能不能看到显卡决定和模型代码无关。5.2 坑二分割掩膜画出来整体偏移或边缘错位现象单张测试图很好但叠加到原图后橙色区域像被橡皮擦拖动过边界比实际积水区域大了一圈或挪了半个身位。原因百分之九十是resize时没控制插值方式或者把mask直接当成原图坐标用。yolo11的mask输出是640x640和输入的原图像素坐标不是一个坐标系必须做一次resize。这里有个更隐蔽的坑如果mask是从r.masks.data[0]取的它是单个目标的分割结果如果画面里有多个积水块正确做法是先把所有mask合并成一张全图掩膜再resize。合并可以用np.maximum循环叠加或者直接用results[0].plot()画完再显示。5.3 坑三界面一开就卡死按钮点了没反应现象PyQt窗口能弹出但“开始识别”点下去后窗口立刻转圈最后系统提示无响应。原因是摄像头循环和推理跑在了Qt主线程。如果项目里已经用了QThread还卡那就是线程没启动或者信号连接写错了。排查顺序要先确认子线程真的在跑在run()第一行print(thread start)如果终端没有输出说明线程对象没有start()。另一个容易忽略的问题是quit之后线程没有安全退出导致再次点击时报“thread already started”。我习惯在stop方法里先置_runningFalse再wait(1000)强制等线程结束而不是直接terminate因为terminate很可能让显存和摄像头资源没释放。5.4 坑四白天正常到了夜间和雨天疯狂误报现象白天验证集mAP不错但夜间画面只要出现路面反光、车灯光柱或雨滴反光模型就大面积标成水。原因不是模型坏了而是训练集里几乎没有夜间和雨天样本光照分布和噪声纹理超出模型见过的范围。解决要“对症下药”从监控录像里专门抽夜间两个小时和一个雨天小时的帧加入训练集在数据增强里增加高斯噪声和亮度扰动让模型不要把“亮斑”当成“水”。另外积水本身具有镜面特性夜间灯光照射下水面会形成比沥青路面更高的亮度条纹这个特征如果训练数据里有代表性样本模型反而能学到比白天更强的判别线索。5.5 坑五显示FPS很高但报警延迟好几秒现象界面上fps显示25但水面已经淹过警戒线报警迟迟不弹。原因不是推理慢而是帧循环里积压了太多未处理帧Qt信号把最近一帧和报警数据同时发过去报警队列却还在处理旧帧。解决方法是控制缓存在cap.read之后用queue.Queue(maxsize2)满了就丢弃旧帧只保留最新帧主窗口收到frame_ready信号后不要开QTimer去反复查直接在槽函数里刷新QLabel。这样切走大部分缓存延迟报警延迟能从几秒降到毫秒级。还有一个习惯是把面积比计算放在线程里不要在GUI槽函数里做否则界面刷新又被拖慢。6. 进阶技巧从检测积水面积到估算积水深度和部署验证分割模型的直接输出是像素面积比但业务方真正关心的是“这个坑积水多深、会不会把车淹了”。纯二维图像看不出深度但可以做限位标定。我常用的做法是在摄像头视野里的积水区边缘放一个已知高度的参照物比如路边石高度10厘米让模型在标定模式下也分割它再用标签算出参照物像素高度后续检测中积水在对应位置的像素高度除以参照物像素高度再乘10厘米就得到一个粗糙的深度估计。这个方法精度不高但用于“是否接近路牙、是否会让行人湿鞋”的判断足够。验证阶段不要只盯着mAP。mAP适合衡量模型版本不适合衡量摄像头识别整体系统。我建议跑一段不低于十分钟的现场视频用脚本逐帧记录面积比和时间戳再人工挑出里面三次最关键的报警事件看延迟和漏报。跑完后用这行代码快速看验证集上的边界召回情况from ultralytics import YOLO model YOLO(weights/best.pt) metrics model.val(datadataset/data.yaml, imgsz640, conf0.25) print(metrics.box.map, metrics.seg.map) # 同时输出检测和分割mAP如果检测框mAP明显高于分割mAP说明mask训练不充分回到数据侧补边缘样本如果两者都过了0.7但现场误报多问题往往不在模型在阈值回头调conf和报警阈值。真正的部署阶段如果现场只有CPU可以把权重导出成ONNX并用OpenCV的DNN模块推理参考yolo11的export流程如果现场有摄像头和显示器Win10工控机直接跑这篇的PyQt代码即可。我个人给自己定的死规矩是每次暴雨预警之后抽十张新积水照片放回验证集里重新评测一次如果mAP环比下降超过两个点就要考虑补充训练。模型不是了一次训练就一劳永逸落地的路况变化远比你想象的快。这套项目的价值不在模型有多新而在把摄像头识别、可重训的数据集和界面整合到一条能交付的链路上跑通它你就掌握了积水检测类项目的完整骨架。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询