简介:这份文档面向遥感影像处理、语义分割方向的研究者与工程人员,聚焦复杂背景下海陆边界分割不准确这一难题,给出了一套可参考的网络设计方案。内容围绕基于Res2Net的多尺度海陆分割网络MSRNet展开,涵盖多尺度特征提取、压缩与注意力模块特征增强、上采样融合及深度监督损失增强等关键环节,并对比传统阈值分割、活动轮廓模型等方法,说明深度学习在海岸线提取、海岛礁识别与近岸目标检测中的优势。资源包共1个docx文件,约345KB,为完整论文文档,含摘要、引言、网络结构、实验与结论等章节,便于系统研读方法细节与实验设置。目前已有128人学习,适合希望借鉴多尺度编码解码结构、弱边界增强思路及边缘评价指标的读者参考。
1. 遥感影像海陆分割:为什么Res2Net加多尺度是当前最稳的组合
拿到一幅高分遥感影像,想把陆地和海洋干净地切开,听起来像个二分类问题,但真正动过手的人都知道这事没那么简单。海岸线不是一条平滑的曲线,它有潮间带、滩涂、礁石、港口人工岸线、养殖区,还有云层阴影和船只尾迹来捣乱。传统方法靠NDWI或者阈值分割,在干净的海岸线上勉强能用,一旦遇到泥沙含量高的河口或者薄云覆盖的区域,结果就惨不忍睹。这几年做遥感影像下载和预处理的人越来越多,数据不缺了,缺的是能把海陆边界稳定切出来的方法。Res2Net这个结构最初是为图像分类设计的,它把残差连接改成了层级化的多尺度残差,在细粒度识别上表现很突出。把它搬到遥感海陆分割任务里,配合多尺度特征融合,恰好能对付海岸线那种“大尺度轮廓要准、小尺度细节要清”的双重需求。这篇内容面向的是已经有一定深度学习基础、想把这个方案跑通并落到实际影像上的工程师,从网络结构选型讲到训练参数配置,再到推理阶段的踩坑排查,尽量把可复现的细节写清楚。
2. Res2Net与多尺度融合:海陆分割的网络结构怎么搭
2.1 为什么选Res2Net而不是ResNet做主干
ResNet的瓶颈结构是3×3卷积堆叠,感受野增长是线性的。海陆分割里有个很实际的问题:一条宽度只有几个像素的防波堤,和一片几十公里宽的近岸浅滩,网络得同时照顾到。ResNet靠加深层数来扩大感受野,但浅层的小目标特征在深层容易被稀释掉。Res2Net的做法是在一个残差块内部把通道分成若干组,每组经过3×3卷积后与下一组拼接再卷积,形成层级化的残差连接。这样同一个块内就产生了多个尺度的感受野,等效于在不增加太多参数的情况下拿到了多尺度特征。
具体到海陆分割,海岸线附近的过渡区域像素类别模糊,潮间带在有些影像里跟浅海的光谱差异很小。Res2Net的层级残差让网络在同一层就能捕捉到不同宽度的边缘响应,这对区分“湿沙滩”和“浅水”这种细粒度类别特别有用。我一般会把Res2Net的scale参数设为4,也就是每个block内分4组,width设为26,这个配置在ImageNet上验证过,迁移到遥感分割任务时显存占用和精度比较平衡。
2.2 多尺度融合模块的设计与代码实现
光有Res2Net主干还不够,遥感影像的尺度差异比自然图像大得多。同一幅影像里,港口区域和远海区域的纹理复杂度完全不是一个量级。常见做法是在主干后面接一个多尺度融合模块,把不同stage的特征图拿来做跨尺度注意力或者金字塔池化。
下面是一个基于PyTorch的多尺度融合模块实现,接在Res2Net的四个stage输出之后:
import torch import torch.nn as nn import torch.nn.functional as F class MultiScaleFusion(nn.Module): def __init__(self, in_channels_list, out_channels=256): super().__init__() # 对每个stage的输出做1x1卷积统一通道数 self.lateral_convs = nn.ModuleList([ nn.Conv2d(in_ch, out_channels, 1) for in_ch in in_channels_list ]) # 多尺度空洞卷积金字塔,dilation分别为1,3,5 self.aspp = nn.ModuleList([ nn.Conv2d(out_channels, out_channels, 3, padding=d, dilation=d, groups=out_channels) for d in [1, 3, 5] ]) self.fusion_conv = nn.Conv2d(out_channels * 3, out_channels, 1) self.bn = nn.BatchNorm2d(out_channels) def forward(self, features): # features: list of [B, C_i, H_i, W_i],从浅到深 laterals = [conv(f) for conv, f in zip(self.lateral_convs, features)] # 统一到最深层的空间尺寸 target_size = laterals[-1].shape[2:] laterals = [ F.interpolate(l, size=target_size, mode='bilinear', align_corners=False) for l in laterals ] # 逐元素相加做跨尺度融合 fused = sum(laterals) # 多尺度空洞卷积 ms_feats = [conv(fused) for conv in self.aspp] out = self.fusion_conv(torch.cat(ms_feats, dim=1)) return self.bn(out)这段代码的逻辑是:先把Res2Net四个stage的输出用1×1卷积压到统一通道数,再双线性插值到同一空间分辨率,逐元素相加得到融合特征。然后走三个不同空洞率的深度可分离卷积,分别捕捉近距离、中距离、远距离的上下文。最后拼接后1×1卷积降维。参数上,out_channels设256比较通用,显存紧张可以降到128;空洞率选1、3、5是因为遥感影像里海岸线的上下文范围大概在几十到几百像素,再大的空洞率收益递减。
2.3 解码器与损失函数的选择
解码器部分不需要太复杂,用两次上采样加卷积就能恢复到原图尺寸。关键是损失函数。海陆分割有个天然的类别不平衡问题:海洋面积通常远大于陆地,但陆地上的细节更丰富。纯交叉熵会让网络倾向于把模糊区域判成海洋。我一般用Dice Loss加Focal Loss的组合,权重比3:1。Dice管区域重叠度,Focal管难分样本。代码上直接在训练循环里加权求和就行,不需要改网络结构。
注意:如果影像里包含大量云层,建议在损失里加一个忽略掩码,把云像素的loss置零,否则网络会浪费容量去拟合云的光谱特征。
3. 从数据到训练:海陆分割的完整落地流程
3.1 数据准备与标签制作的实际操作
遥感海陆分割的数据来源主要有两个:公开数据集和自标注。公开数据集里,像LoveDA、DeepGlobe这些偏土地利用,海陆二分类的标签需要自己从多类标签里合并出来。更常见的做法是下载高分影像后自己标。这里有个血泪经验:不要试图标全图,先裁成512×512的图块,只标海岸线附近的区域,纯海和纯陆的图块可以直接用NDWI生成伪标签,人工只复核边界区域。
标签格式用PNG单通道,0代表海洋,1代表陆地,255代表忽略区域。裁图的时候注意重叠率设20%到30%,避免海岸线正好落在图块边缘被切断。下面是一个裁图脚本的核心逻辑:
import numpy as np from PIL import Image def crop_with_overlap(img_path, label_path, crop_size=512, overlap=0.25): img = np.array(Image.open(img_path)) label = np.array(Image.open(label_path)) h, w = img.shape[:2] stride = int(crop_size * (1 - overlap)) patches = [] for y in range(0, h - crop_size + 1, stride): for x in range(0, w - crop_size + 1, stride): img_patch = img[y:y+crop_size, x:x+crop_size] label_patch = label[y:y+crop_size, x:x+crop_size] # 跳过纯海或纯陆且无边界信息的图块 if len(np.unique(label_patch)) < 2: continue patches.append((img_patch, label_patch)) return patchescrop_size设512是因为Res2Net主干下采样32倍后特征图是16×16,再小就丢细节了。overlap设0.25是经验值,再高冗余太多,再低边界容易被切。跳过纯海纯陆图块能减少30%到40%的训练量,对精度几乎没影响。
3.2 训练参数配置与显存优化
训练配置直接给一套我常用的参数。优化器用AdamW,初始学习率1e-4,weight_decay设1e-4。学习率调度用CosineAnnealing,最低降到1e-6。Batch size在单卡2080Ti上设8,如果显存不够就设4,同时把学习率降到5e-5。训练轮数一般60到80轮就收敛了,看验证集Dice曲线,连续10轮不升就可以停。
数据增强方面,随机水平翻转和垂直翻转必开,颜色抖动幅度不要太大,亮度对比度各0.2就够了,遥感影像的光谱信息比自然图像敏感。随机旋转90度的倍数可以用,任意角度旋转会引入黑边,反而干扰训练。
import torch from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model = Res2NetMultiScale(num_classes=2).cuda() optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=80, eta_min=1e-6) # 混合精度训练,省显存约40% scaler = torch.cuda.amp.GradScaler() for epoch in range(80): model.train() for imgs, labels in train_loader: imgs, labels = imgs.cuda(), labels.cuda() with torch.cuda.amp.autocast(): preds = model(imgs) loss = dice_loss(preds, labels) * 0.75 + focal_loss(preds, labels) * 0.25 optimizer.zero_grad() scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()混合精度训练在这个任务上基本没有精度损失,显存能省将近一半。T_max设80跟总轮数一致,eta_min设1e-6是防止后期学习率太小导致loss震荡。
3.3 推理与后处理的工程细节
推理阶段最容易翻车的地方是滑窗拼接。大图不能直接resize进网络,会丢分辨率。正确做法是滑窗推理,窗口大小512,重叠128,每个像素的预测概率取重叠区域的平均值。拼接完再取argmax得到二值图。
后处理做两件事:一是去掉面积小于500像素的孤立陆地或海洋区域,用连通域分析;二是对海岸线做一次形态学平滑,闭运算核大小3×3,消除锯齿。这两步在OpenCV里几行代码就能搞定,但对最终视觉效果提升很明显。
提示:如果推理影像的获取时间跟训练集季节差异大,比如训练用夏季影像推理用冬季影像,建议先做一次直方图匹配,把推理影像的色调对齐到训练集的均值方差,能减少域偏移带来的精度下降。
4. 避坑与排查:海陆分割训练中最容易翻车的五个地方
4.1 损失震荡不收敛,Dice曲线来回跳
现象:训练前20轮loss下降正常,之后开始剧烈震荡,验证集Dice在0.7到0.85之间反复横跳。
原因:最常见的是学习率太大加上batch size太小,梯度噪声被放大。另一个可能是数据里混入了标注质量差的样本,比如把云阴影标成了陆地。
解决:先把学习率降到5e-5试10轮,如果还震荡就检查数据。写个脚本把训练集里loss最高的前20个样本可视化出来,大概率能看到标注错误或者极端场景。把这些样本修正或剔除后重新训练。
4.2 海岸线附近出现规律性锯齿
现象:推理结果的海岸线不是平滑的,而是每隔几个像素就有一个台阶状的锯齿。
原因:滑窗推理时窗口重叠不够,或者上采样用了最近邻插值。Res2Net主干下采样32倍,如果解码器用最近邻上采样,边界就会出锯齿。
解决:解码器统一用双线性插值,滑窗重叠从64提高到128。如果还有轻微锯齿,后处理加一次3×3的形态学闭运算。
4.3 小岛屿和狭窄水道被整个吞掉
现象:面积小于200像素的岛屿在预测结果里消失了,宽度小于10像素的水道被填成了陆地。
原因:Dice Loss对小目标不敏感,加上后处理的连通域过滤阈值设太高。
解决:损失函数里把Focal Loss的权重从0.25提到0.4,让网络更关注难分的小目标。后处理的面积阈值从500降到100,同时把形态学操作的核从3×3降到2×2。如果还不行,在训练时对包含小岛屿的图块做oversampling,重复采样因子设3。
4.4 不同季节影像的精度断崖式下跌
现象:夏季影像上Dice能到0.92,换冬季影像直接掉到0.75。
原因:冬季影像里海水颜色偏暗,和湿沙滩的光谱差异变小,加上太阳高度角低导致阴影方向不同。
解决:训练集里必须混入不同季节的影像,哪怕每个季节只有几十张。如果实在没有冬季数据,推理前做直方图匹配,把推理影像的RGB均值方差对齐到训练集。更彻底的做法是在网络输入前加一个颜色恒常性模块,但这个改动比较大,适合有充足数据时再做。
4.5 显存溢出但batch size已经降到1
现象:batch size设1还是OOM,报错显示Res2Net的某个block激活值太大。
原因:输入影像尺寸太大,比如直接喂了1024×1024的图。Res2Net的层级残差在浅层stage的激活值随分辨率平方增长。
解决:训练时强制裁到512×512,推理时用滑窗。如果必须训练大图,把Res2Net的scale从4降到2,width从26降到18,参数量减少约40%,精度损失在1到2个点。另一个办法是用梯度检查点,把每个block的中间激活值不保存,反向时重算,显存能省60%但训练速度慢30%。
5. 进阶技巧:用局部聚焦策略提升海岸线精度
训练完基础模型后,如果发现海岸线附近的误分类还是偏多,可以试一个我常用的技巧:局部聚焦推理。思路很简单,先用基础模型跑一遍全图,得到粗略的海陆概率图,然后把概率在0.3到0.7之间的区域抠出来,这些就是海岸线的模糊地带。对这些区域做一次更高分辨率的裁剪,比如把512窗口缩到256但保持原图分辨率,再送进网络推理一次,用第二次的结果覆盖第一次的模糊区域。
这个策略的代价是推理时间增加约50%,但海岸线附近的Dice能提升3到5个点。代码实现上就是在滑窗推理外面套一层判断:
def local_focus_inference(model, image, base_window=512, focus_window=256): # 第一遍:全图滑窗推理 prob_map = sliding_window_inference(model, image, base_window, overlap=128) # 找出模糊区域 uncertain_mask = (prob_map > 0.3) & (prob_map < 0.7) # 对模糊区域做局部高分辨率推理 coords = np.argwhere(uncertain_mask) if len(coords) > 0: y_min, x_min = coords.min(axis=0) y_max, x_max = coords.max(axis=0) # 扩展边界并裁剪 pad = focus_window // 2 y_min = max(0, y_min - pad) x_min = max(0, x_min - pad) y_max = min(image.shape[0], y_max + pad) x_max = min(image.shape[1], x_max + pad) crop = image[y_min:y_max, x_min:x_max] # 对裁剪区域用更小的窗口推理 focus_prob = sliding_window_inference(model, crop, focus_window, overlap=64) prob_map[y_min:y_max, x_min:x_max] = focus_prob return (prob_map > 0.5).astype(np.uint8)base_window和focus_window的比值一般设2:1,再小收益不明显。overlap在聚焦推理时设64就够了,因为区域本身不大。这个技巧对港口、养殖区、河口这些海岸线复杂的场景特别有效,纯自然海岸线提升有限。
验证方法上,不要只看整体Dice,要单独算海岸线缓冲区的Dice。具体做法是把标签做一次形态学膨胀和腐蚀,相减得到海岸线带,只在这个带上算指标。这个指标比整体Dice更能反映模型的实际可用性。我一般会要求海岸线带Dice至少比整体Dice低不超过5个点,否则说明边界处理还有问题。
最后说个习惯:每次训练完模型,我都会拿几张训练集里没有的、不同地区的影像跑一遍,肉眼检查海岸线。指标好看但边界毛糙的情况太常见了,尤其是河口和潮间带。宁可多花半小时看图,也别等上线了才发现问题。希望帮到你。
本文还有配套的精品资源,点击获取