ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepLabV3+语义分割实战:水面漂浮物检测与报警全流程

DeepLabV3+语义分割实战:水面漂浮物检测与报警全流程 简介这是一份基于开源语义分割模型DeepLabV3完成的小组结课项目同时入选极市开发者平台打榜实践适合模式识别与机器学习课程的学生、需要完成课程设计或准备算法赛题的开发者参考。项目以水体及水面漂浮物为检测对象利用像素级分割结果计算目标面积并按阈值自动输出告警信息展示了从数据集构建、模型训练到业务逻辑判断的完整链路。压缩包共258个文件、约29.77MB核心内容包括120张png与100张jpg水面场景图像样本、26个py模型与训练脚本、7个txt标注或说明文件另有shell脚本、Markdown说明等配套材料目录划分清晰便于按模块复现。目前已有223人学习下载尤其适合希望快速上手语义分割开源模型、并借鉴极市平台项目组织方式的学习者。借助该项目的代码和样本既能理解DeepLabV3在真实场景中的应用也能掌握面积阈值报警的落地思路。1. 水面漂浮物检测这个 DeepLabV3 课程项目从分割到报警做了什么汛期河道监控画面里水面上漂着的塑料瓶、树枝和白色泡沫靠人眼盯十几路视频根本盯不过来。这个项目要解决的就是把这部分巡检自动化。团队用开源模型 DeepLabV3 做主网对画面里的「水体」和「水上漂浮物」做像素级语义分割——不是画检测框而是把每一个属于漂浮物的像素找出来再按面积阈值判断该不该输出报警消息。项目同时也是极市开发者平台的打榜题数据来自真实河道与近海监控场景。适合两类人一类是模式识别与机器学习课设还没定题想找一个能讲清楚原理又能完整跑通的方案另一类是刚接触语义分割想从数据准备一路走到报警输出的工程师。下面按数据、模型、训练、避坑、后处理的顺序完整拆一遍。2. 先从文件名读懂数据集从扁平目录到可训练的分割数据整个交付里最容易被忽略的是这批 ZDSfloating_objects 图片几十张看起来差不多的 JPG 平铺在根目录没有标注目录、没有子文件夹。直接拿去训练连 train/val 怎么切都不知道。我拿到资源后第一件事不是写模型而是把文件名解码、目录重构、数据体检这三步走完。2.1 解读 ZDSfloating_objects 文件名这些字段决定你怎么切分数据文件名按固定规律编码比如ZDSfloating_objects20230206_V3_train_rivers_27_000044.jpg拆开看字段示例值含义对训练的影响项目前缀ZDSfloating_objects数据集标识无日期20230206采集日期数据版本与时间跨度版本V3数据版本筛选版本集别train训练/测试归属需要重新按场景划分场景rivers / sea河道 / 近海场景差异是分布差异点位编号27 / 1 / 36拍摄点 ID同点位相邻帧强相关帧号000044帧序号时间排序这里值得细说的是场景和点位两个字段。rivers 和 sea 是差异很大的两种场景河道水面窄、背景有岸堤海面开阔、有浪花纹理和反光。如果你随机把文件切成 train/val同一个点位 27 的相邻帧会同时出现在训练集和验证集里验证 mIoU 会被抬高好几个点部署到没见过的点位直接崩。所以后面切分一定要按「点位」分组不能按文件随机切。2.2 目录重构把平铺的 JPG 归位原始目录下只有图没有 labels 子目录也没有 annotation 文件。分割任务训练时需要两张图一张三通道 RGB 原图一张单通道 maskPNGmask 的每个像素值就是类别编号。类别编号按课程设计常见约定0 是背景岸堤、天空等1 是水体2 是漂浮物。标注文件在项目交付包里单独存在后缀是 .png与原图同名对应。先把图片按场景字段归位mkdir -p data/rivers/images data/rivers/labels mkdir -p data/sea/images data/sea/labels find /path/to/original -name *_rivers_* -exec mv {} data/rivers/images/ \; find /path/to/original -name *_sea_* -exec mv {} data/sea/images/ \;这段脚本只做一件事按文件名里的场景字段把图挪进对应目录。find 配合 -exec 在文件量几百张时足够直接量大时建议改用 Python 加 shutil 批量处理。注意如果同时挪标注要找同名 .png 一起处理别只挪原图、标注留在原地后面 Dataset 类加载时图找得到、mask 找不到一跑就崩。然后做数据体检。语义分割训练过程中损坏图片是最容易翻车的地方from PIL import Image from pathlib import Path import collections for root in [Path(data/rivers/images), Path(data/sea/images)]: sizes collections.Counter() broken [] for img_path in sorted(root.glob(*.jpg)): try: img Image.open(img_path).load() sizes[img.size] 1 except Exception: broken.append(str(img_path)) print(root, 分辨率分布:, sizes.most_common(3)) print(root, 损坏图片:, broken[:5])脚本做两件事算出每个场景的分辨率分布列出打不开的坏图。分辨率分布决定后面 RandomCrop 的窗口大小和要不要做统一缩放坏图必须提前清掉不然训练到一半 DataLoader 抛异常整个跑批白等。常见做法是顺手把分辨率悬殊的图先缩放到同一档位比如最长边 1024避免 batch 内图尺寸差异过大。血泪经验体检这步别省打榜数据里混进一两张损坏图是常态。2.3 看一眼标签分布漂浮物究竟占多少像素分割项目里最容易被跳过的是统计标签分布。跑一段脚本确认三类像素的全局占比import numpy as np from PIL import Image from pathlib import Path label_dir Path(data/rivers/labels) overall np.zeros(3, dtypenp.float64) for p in label_dir.glob(*.png): mask np.array(Image.open(p)) counts np.bincount(mask.ravel(), minlength3)[:3] overall counts print(全局像素分布:, overall / overall.sum())如果漂浮物的全局占比低于 1%第 4 章的损失函数就不能用裸的 CrossEntropy。这个数值还直接决定要不要做小目标过采样。把统计结果写进课设 PPT评审老师基本都会认可数据准备工作是扎实的。3. DeepLabV3 选型与改造ASPP、Decoder 和三处关键代码数据就位后选模型。课程项目为什么选 DeepLabV3 而不是 U-Net 或 PSPNet评审几乎必问。先把原理讲透再落到代码改动。3.1 空洞卷积与 ASPP不降分辨率地扩大感受野分割任务和分类任务有个本质差别分类可以一直池化到 1×1分割要求输出和输入同尺寸所以特征图分辨率不能丢太多。标准 CNN 层层下采样后高层特征感受野够大但分辨率低直接上采样预测的结果边缘很糊。DeepLab 系列的核心思路是不靠下采样扩大感受野而是用空洞卷积dilated convolution。空洞卷积在卷积核里插入空洞。rate2 时一个 3×3 的核覆盖的实际区域相当于 5×5rate6 时覆盖到 13×13。模型不损失分辨率还能让每个输出像素看到更大的上下文。这对本项目意义很直接水面是超大类漂浮物是超小类模型既要能「看到」整片水面判断环境又不能因为降采样把漂浮物的细节丢掉。DeepLabV3 在此基础上提出 ASPPAtrous Spatial Pyramid Pooling把四种尺度的分支并行叠起来一个 1×1 卷积、三个空洞卷积rate 6 / 12 / 18、一个全局平均池化最后拼接。多尺度的价值在于漂浮物的尺寸在画面里变化非常大——近处垃圾可能占 200×200 像素远处可能只有 8×8单个固定感受野很难兼顾。3.2 Encoder-Decoder把低层细节接回高层语义DeepLabV3 有个明显短板ASPP 输出的特征图经过 16 倍下采样后直接上采样边界细节恢复不了。漂浮物的边缘恰恰是关键报警面积就差在边缘像素的归属上。DeepLabV3 加了一段轻量 Decoder把主干浅层输出用 1×1 卷积降通道再和上采样后的深层特征拼接最后卷积融合。这个结构和 U-Net 的 skip connection 相似但 V3 的 Decoder 更克制只在最后一级做融合参数量增加不多。在漂浮物这种边界模糊的目标上Decoder 带来的边界改善肉眼可见气泡、细碎树枝这类小目标在 V3 上容易断成几截在 V3 上能连成完整连通域后续面积统计才有意义。3.3 从开源模型到课设代码三处必改点极市这类打榜项目模型通常要求复现论文。课设项目更推荐直接用 segmentation_models_pytorch省掉从零搭 Encoder 的时间把精力留给数据处理和报警逻辑import segmentation_models_pytorch as smp model smp.DeepLabV3Plus( encoder_nameresnet101, # 主干网络 encoder_weightsimagenet, # ImageNet 预训练权重 in_channels3, # RGB 输入 classes3, # 背景 / 水体 / 漂浮物 )三个参数要解释清楚。encoder_name 控制主干网络三档选择直接对应显存和精度encoder_name显存占用推理速度mIoU 潜力课设适用度mobilenet_v2低快中高笔记本可跑resnet50中中较高高平衡之选resnet101高慢最高低需要好卡encoder_weights 用 ImageNet 预训练课程数据集只有几万张时效果差距明显随机初始化要多花大量 epoch 才能追上。classes 必须从默认的 21Pascal VOC 类别数改成 3。如果不用这个库手工改 torchvision 里的 DeepLabV3 时要改对地方一是分类头类别数二是加载预训练权重时分类器形状对不上需要用 strictFalse 加载 backbone 部分import torchvision.models.segmentation as seg model seg.deeplabv3_resnet101(pretrainedFalse, num_classes3) state torch.load(你的预训练权重路径.pth) backbone_state {k: v for k, v in state.items() if classifier not in k} model.load_state_dict(backbone_state, strictFalse)因为 strictFalse 会放过主干网络里可能存在的键名不匹配加载后要打印 missing keys 确认缺失的全是分类头别拿一个权重完全没加载进去的模型跑训练。这一点我们课设 review 时栽过——加载完没检查训练十分钟 loss 不降查了半天才发现预训练权重根本没进去。4. 训练配置损失函数、增强策略和按点位的验证集划分模型定义好之后训练策略是决定项目成绩的关键。这里讲三件事损失函数怎么调、数据增强怎么配、验证集怎么切每一件都直接影响最终报警是否靠谱。4.1 类别不均衡与混合损失函数从第 2 章的标签统计能看出漂浮物类别在全局像素里占比大概率低于 1%甚至低于 0.1%。这种数据直接用 CrossEntropy模型会把所有像素都学成水体或背景验证集整体准确率看着很高漂浮物类别 IoU 却是 0。常见做法是把 CrossEntropy 和 Dice Loss 混在一起import torch.nn.functional as F def mixed_loss(pred, mask, dice_weight0.5): ce F.cross_entropy(pred, mask) prob F.softmax(pred, dim1) # (B, 3, H, W) onehot F.one_hot(mask, num_classes3).permute(0, 3, 1, 2).float() inter (prob * onehot).sum(dim(2, 3)) union prob.sum(dim(2, 3)) onehot.sum(dim(2, 3)) dice_per_class 1 - (2 * inter 1) / (union 1) # 每类分别算 dice dice_per_class.mean() return ce dice_weight * diceCE 负责整体稳定Dice 直接对类别重叠度求梯度对小类敏感。dice_weight 从 0.5 起调太大时早期训练震荡明显。如果漂浮物占比特别极端还可以把第 2 类单独加权重到 23 倍。这个混合损失写进组会讲稿评委一般会追问 CE 和 Dice 的互补性提前准备好回答。4.2 数据增强随机裁剪比 resize 更适合小目标分割增强套路和分类不太一样。初学者容易直接套分类那套Resize 到固定尺寸再随机裁剪。在漂浮物项目里先 Resize 会把小目标的像素信息提前压缩掉损失函数再强也救不回来。我一般用随机裁剪保留原始分辨率信息import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.RandomCrop(513, 513), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.3), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ])crop 尺寸 513 是 DeepLab 系列论文里的常见值显存允许可换 769更大视野对小目标上下文有增益。RandomBrightnessContrast 模拟一天不同时段光照尤其水面反光差异大。验证集只做 Normalize不做随机增强保证评估稳定。4.3 验证集划分按点位分组不按文件随机切第 2 章提过文件名里的点位字段直接影响验证集可信度。同一个点位连续拍的帧之间非常相似随机切分相当于「泄题」。正确做法是用 sklearn 的 GroupShuffleSplitgroup 是文件名解析出的点位 IDfrom sklearn.model_selection import GroupShuffleSplit def parse_group(name): parts name.split(_) return f{parts[4]}_{parts[5]} # rivers_27 / sea_1 imgs sorted((data_dir / images).glob(*.jpg)) groups [parse_group(p.name) for p in imgs] gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(imgs, groupsgroups))parse_group 按第 2 章解码的字段位置取名把 rivers_27、sea_1 作为同一组。验证集点位完全没在训练集出现过mIoU 才反映真实泛化能力。不做这一步验证 mIoU 可能虚高 10 个点以上报警阈值的标定也会跟着偏。4.4 训练超参的起点从头调超参太费时间我一般用一套分割任务普适的初始配置然后只看验证集曲线微调参数推荐初始值说明优化器SGDmomentum0.9分割任务比 Adam 稳定初始学习率0.007配合 poly 衰减到 0批大小48取决于显存与 crop 尺寸训练轮数4080观察验证 mIoU 是否进入平台期学习率策略poly按 (1 - iter/total)^0.9 衰减batch size 建议先从 2 跑通显存再往上加直接上 8 翻车概率不低。训练中每 500 步保存一次 checkpoint同时记录每类的 IoU 而不是只记平均 mIoU——这个习惯在下一章会看到几乎是一个项目表象与真实效果不符时的第一道防线。5. 避坑记录训练和报警里最容易翻车的五个点这个项目做下来踩坑的时间比写代码的时间多。下面五条血泪经验按「现象 → 原因 → 解决」写基本都是我们组里真实遇到过、并且回看时能对上根因的。5.1 验证集 mIoU 很高预测图却一塌糊涂现象训练时验证集 mIoU 到了 0.9 以上导出模型跑出来的预测图几乎整张是背景色漂浮物区域一片黑。原因类别极度不均衡。背景和水体占绝大多数像素平均 mIoU 被大类别拉高漂浮物类别 IoU 可能只有 0.05。平均指标好看不代表小类别被学会。解决训练日志里同时输出每类的 IoU专门盯漂浮物那一行的变化损失函数换成第 4 章的混合损失如果漂浮物占比低于 0.1%给类别 2 单独加权重。切换损失后漂浮物 IoU 从接近 0 提升到能用的水平是常见结果。5.2 水面波纹和反光被当成漂浮物现象推理时水面波纹、阳光反射区域被预测成漂浮物类别报警消息里全是误报。原因波纹和反光在局部纹理上和漂浮物边缘很像加上数据多来自白天监控光照差异大。增强里只做了亮度对比度没有针对性强化反光负样本。解决训练中保留充足的纯水面反光样本让模型见过反光的多样性后处理里对漂浮物连通域做最小面积过滤低于阈值直接丢弃。这一层后处理通常能压掉一半以上误报。5.3 报警量忽高忽低面积阈值不可跨图比较现象面积阈值定在 5000 像素有些图大量报警有些图一个不报看起来毫无规律。原因图片分辨率不一致或模型输入做了固定 resize。相同面积的漂浮物在不同原始分辨率下换算成像素数完全不同固定像素阈值等于拍脑袋。解决统一推理流程计算「漂浮物像素 / 水体像素」的面积占比而不是绝对像素数阈值先拿验证集统计面积占比分布再定别凭感觉填。具体代码在第 6 章。5.4 训练 OOM模型和数据一起压爆显存现象batch size 设 8crop 尺寸 769ResNet101 主干一启动直接 CUDA OutOfMemory。换 batch 2 能跑但太慢。原因三个因素叠加ResNet101 中间特征图大、769×769 输入大、batch 8 放大太多。另外还要确认机器上有没有其他进程占显存——显存占用是门玄学。先看占用再改代码nvidia-smi --query-gpumemory.used,memory.free --formatcsv解决先跑 batch1 确认基线用梯度累积模拟大 batch或把 crop 降回 513观察验证曲线再平衡。主力卡 12G 的话ResNet50 主干加 513 crop 加 batch 4 是性价比很高的组合。5.5 河道效果好、近海效果崩现象训练集里 rivers 图多、sea 图少模型在 rivers 点位验证 mIoU 很高换到 sea 场景漂浮物分割几乎失效。原因两个场景数据分布差距大——水面颜色、光照角度、漂浮物形态都不同模型只拟合了数量多的那个场景。解决按场景分层采样或把 sea 图片做水平翻转、亮度增强后复制进训练集把场景比例拉平。课设时间紧也可以接受「分场景出两个模型」推理时按监控点位路由到对应模型——这在工业落地反而是常见的省力方案。6. 报警消息输出面积阈值判断与验证训练完成后模型输出的是每个像素的类别概率。报警模块要做的是把概率图转成可用的业务消息。import numpy as np from scipy import ndimage logits model(tensor).squeeze(0) # (3, H, W) pred logits.argmax(0).cpu().numpy() # 0 背景, 1 水体, 2 漂浮物 floating (pred 2).astype(np.uint8) labels, n ndimage.label(floating) areas ndimage.sum(floating, labels, range(1, n 1)) ratio floating.sum() / max((pred 1).sum(), 1) for i, area in enumerate(areas, 1): if area min_area: ys, xs np.where(labels i) msg { image: name, area_ratio: round(float(ratio), 4), blob_area: int(area), center: [int(xs.mean()), int(ys.mean())], } # 推送到业务端print 或通过 HTTP / MQTT 转发这个后处理里两个东西要标定。min_area 是单个连通域的最小像素数低于它的噪点不报警area_ratio 是最终报警判断指标按第 5 章 5.3 的方法在验证集上统计分布找到误报与漏报的平衡点。验证时不要把验证集当测试集最好留一段完全没参与训练的点位序列模拟新点位接入后的效果。课设演示时把这段后处理串成一个小脚本从图片输入到报警消息一条命令跑完展示效果会完整很多。这个项目要交的核心是数据、模型配置和报警逻辑这三件套拿到资源后按第 2 章的目录重构和数据体检顺序走一遍训练、推理、报警这条链路就能完整复现。从那以后我每次做分割项目都会强制走一遍这个流程先统计每类 IoU再跑一段未参与训练的数据看报警消息确认面积阈值不是拍脑袋定的才敢把模型交出去。希望帮到你。本文还有配套的精品资源点击获取
返回列表