做水体类型识别这个方向断断续续有小半年了,这阶段完整跑通了一套基于RetinaNet的湖泊、河流、海洋遥感图像检测流程。以前一说“识别水体”,很多人下意识会以为是图像分割,把水像油漆一样涂出来;但实际需求往往不是这样——我需要知道哪个位置有一片湖、哪一段是河、海岸线推进到了哪里,这本质上就是目标检测:既要判断类别,也要输出位置框。这篇记录围绕RetinaNet展开,重点放在模型训练和推理工程上,把数据准备、锚框调整、踩坑排查、patch拼接这类实操环节摊开讲清楚,适合正在做遥感目标检测、或者准备用单阶段检测器处理地理空间影像的开发者参考。
1. 为什么水体检测要单开一个模型:任务本质与RetinaNet的选择逻辑
1.1 从“分类”到“定位”:识别的输出不是打标签
最开始我接到这个需求时,团队里有人提议直接用分类网络做“这块是水体/不是水体”,但稍微推敲就会发现不对:分类网络回答的是“有没有”,回答不了“在哪里”。业务上真正关心的往往是具体位置,比如某座水库今年枯水期的水域范围、某条河流改道后在影像上往哪个方向偏移、某片滩涂的高潮线与低潮线差了多少像素。要回答这些,必须把“位置”作为模型的直接输出,所以才选了目标检测路线。
这里要强调一个容易被忽略的点:水体检测和常规目标检测的“定位”语义不完全一样。检测车辆,框住车身边缘就行;检测水体,用户最后要的是水陆边界。但RetinaNet这类检测器输出的是水平包围框,框住一个不规则湖泊时天然会带进陆地,所以模型学到的其实是“框住水体的主要范围”,而不是“精确分割水体”。理解这一点很重要,它决定了你怎么设计标签、怎么评估结果,也决定了后续要不要再挂一个分割头做边缘细化。
1.2 大面积水体与稀疏目标的失衡:Focal Loss的用武之地
遥感影像里有个很极端的样本特点:背景占比极高。一张包含河流的影像,可能水只占几个百分点,其余全是陆地、植被、建筑、云。普通单阶段检测器在训练时会密集生成成千上万个候选位置,绝大部分是易分类的负样本,它们产生的梯度会淹没真正有用的正样本。
RetinaNet的Focal Loss就是专门解决这个问题的。它给每个样本的损失乘上一个调制因子,简单样本的梯度贡献被压下去,困难样本和正样本的权重自然凸现出来。公式不复杂:
Focal Loss = -α * (1 - p_t)^γ * log(p_t)
γ通常取2,α按类别比例加权。当模型已经很有把握预测某个候选框是背景时,(1 - p_t)趋近于0,损失被压得很小;当模型拿不准时,这个样本的损失反而被保留。实验下来,比直接用标准交叉熵稳定很多,尤其在海洋这类“一眼望去全是大片水”的场景里,不会出现模型学到“看到蓝绿色就输出一个巨大包围框”的偷懒行为。
1.3 对比Faster R-CNN与YOLO:单阶段家族里的折中
选RetinaNet之前,我把几个常见检测器都过了一遍,做了个简单对比:
| 检测器 | 优势 | 对水体检测的潜在问题 |
|---|---|---|
| Faster R-CNN | 两阶段精度高,RPN筛掉大量背景 | 推理慢,大尺寸遥感影像上成本高,正负样本处理太重 |
| YOLO系列 | 速度快,部署方便 | 对极端长宽比目标适配一般,小目标召回不稳定 |
| RetinaNet | 单阶段+FPN多尺度+Focal Loss,精度接近两阶段,速度可控 | 锚框配置需要针对目标形态调整 |
水体目标有两个特点:一是尺度跨度极大,一个水库和一段小溪在1024像素的patch里面积能差两个数量级;二是河流这种目标经常是长条形,长宽比能到1:8甚至更夸张。RetinaNet自带FPN特征金字塔,从P3到P7每层负责不同尺度的目标,天然适配这个跨度。相比之下,当时试过YOLO的anchor-free版本,对小河流的召回并不理想,后来还是回到RetinaNet做锚框定制。
2. 数据质量决定上限:遥感水体样本的切块、标注与增强
2.1 切块规则:重叠滑窗与分辨率取舍
遥感影像跟自然图像最大的不同是尺寸。一张原始影像动辄上万乘上万像素,不可能整图直接进模型。我的做法是切成patch训练,常见的方案是512或1024见方,我最终选了1024×1024。为什么不是512?因为水体目标往往很大,一个湖泊在512的patch里可能已经占据了整个画面,模型学不到“水体有边界”这个信息,反而容易把整块都判定成正样本;1024能让湖泊边缘、岸线、周围地物同时出现在上下文里。
切块要注意重叠。我设置相邻patch之间重叠128像素,这样做主要是为了推理时避免目标被切断在patch边缘,模型对切断的目标特别容易漏检。训练阶段可以不用重叠,随机采样即可,但推理阶段必须重叠滑窗,这个后面再展开。
还有一个很容易踩的细节:切块前要做一次影像的辐射质量检查。如果原始影像里有坏线、全零像素或者云遮挡区域,这些patch直接扔给模型训练,会让损失曲线出现莫名其妙的跳变。我习惯先做一遍异常像素统计,把有效像元覆盖率低于99%的patch直接过滤掉。
2.2 湖泊、河流、海洋三类的标注尺度差异
标注是这次项目里最耗时的环节。三类水体看着都是“水”,标注策略差别很大:
湖泊和水库相对规整,可以整体画一个框。但遇到形状极不规则的细长水库,我建议拆成2到3段分别标注,而不是硬套一个大框。因为水平框越大,框内非水体像素比例越高,模型学到的边界回归信号就越混乱,它会不知道到底该贴合哪条边。
河流是最大的坑。一条完整的河可能跨越几个patch甚至几十个patch,但RetinaNet预测的是局部目标,不可能也没必要把整条河框住。我的做法是把河流按直线段拆开,每段单独一个框,框的长度控制在目标区域主方向长度的1/2到1/3,宽度尽量贴合河道宽度。拆完以后你会发现明显好训很多,模型只需要学会“这里有一段水面”。
海洋和前面两类完全不同。海域往往是整张patch的背景级目标,直接画一个覆盖全图的框没有意义。我采用的方法是沿着海岸线向内缩几个像素画一个大范围框,只框真正的海面,不包含滩涂和陆地。注意避开近岸白色泡沫带,那片区域光谱特征和陆地的浅色地物很像,标进去等于主动给模型添乱。
2.3 针对水体特征的增强:哪些能用,哪些最好别碰
数据增强我试过一整套,最后保留下来的只有少数几种。对遥感影像来说,旋转90度、180度、270度是完全合理的,因为卫星拍摄没有固定的“上下方向”;水平翻转和垂直翻转也都能用。这些几何增强对水体的形态学特征没有破坏,还能有效扩充细长河流的样本方向分布。
颜色增强要极其克制。尤其是随机色相扰动,默认参数经常把水体从蓝色偏移成绿色甚至紫色,这在自然图像里问题不大,但在遥感里会严重破坏水体的物理特征。水体在可见光和近红外波段的反射特性是识别它的重要依据,你把颜色搅乱了,模型等于在降低噪声下学习一个不存在的分布。我最后把hue增强直接关掉,亮度对比度也只做很小的幅度调整。
另外建议对含河流的patch做上采样。河流样本数量天然少,如果按普通自然图像的经验做随机采样,训练集里河流patch占比可能只有百分之几,模型对这类目标的拟合会非常差。我会统计每个patch的类别占比,对含河流标注的patch提高采样权重,实测能把河流类别的AP拉高6到8个百分点。
2.4 按空间与时相划分数据集,防止验证指标虚高
数据集划分看着是小问题,但在遥感场景里特别容易出错。如果随机划分patch,同一片区域的相邻patch会被同时分到训练集和验证集,它们之间高度相似,验证指标会虚高得离谱,模型真正遇到新区域时表现会大打折扣。
正确的做法是按地理位置划分:先把整个研究区网格化,确保同一网格内的所有patch要么全进训练集,要么全进验证集,不允许跨集合。更进一步,我建议按影像时相划分。比如同一个地方有春季和秋季两期影像,让训练集和验证集各占一个时相,这样评估的是模型对水位变化、光照条件、植被状态变化的泛化能力,而不是对同一时相下相似像素的记忆能力。
3. 训练RetinaNet的实测配置:锚框、损失与调参记录
3.1 锚框面积与长宽比:为了细长河流做的调整
RetinaNet默认的锚框设计对遥感水体并不友好。它预设的面积尺度和长宽比主要为通用目标设计,包含大量正方形和接近正方形的框,但河流这种长条形目标在默认锚框下匹配到的正样本非常少。我的做法是直接改锚框配置。
由于是1024输入,FPN最高层P7的感受野已经覆盖很大范围。针对湖泊,我把锚框面积整体乘了一个1.5的系数,让更大的目标能匹配到更高层的特征;针对河流,长宽比从默认的{1:2, 1:1, 2:1}扩到{1:3, 1:5, 3:1, 5:1},并且额外加了一组细长比例。这里务必要做实验对比,不要想当然。我第一版把长宽比拉得过猛,结果小湖泊被错误匹配成河流,AP反而掉了。后来锚框分布跟训练集标注的真实长宽比分布对齐之后,模型表现才稳定下来。
3.2 损失函数与优化器设置
RetinaNet的损失分两部分:分类用Focal Loss,回归用Smooth L1。训练时我采用SGD而不是Adam,动量设为0.9,初始学习率0.005,使用余弦退火调度,前500步做了warmup。batch size只开到2,因为1024×1024输入在单卡上显存压力不小。
α参数我设成0.25,γ设成2,这是单阶段检测器里被验证过很多次的组合。但你不需要盲从,如果你的数据里河流目标特别稀少,可以适当把正样本对应的α调高,比如让Focal Loss里α_t在正样本上取0.4左右,让模型对稀缺类别的误判付出更高代价。我试过一次把α调到0.5,湖泊和海洋的AP几乎不变,河流AP微涨,但背景误报开始增多,最后还是回到了0.25。
需要注意的是,遥感影像和自然图像的分布差异很大,如果用了在通用目标检测数据集上预训练的权重,不要冻结骨干网络。很多人习惯只训练检测头,但在遥感场景里,底层卷积对光谱纹理的响应模式和自然图像完全不同,冻结骨干会让模型永远无法适应遥感影像的分布。我全程保持骨干可训练,只降低骨干部分的学习率倍数,比如设为整体学习率的0.1倍,既能稳住预训练特征,又能逐步做领域迁移。
3.3 训练日志里那些值得警惕的曲线
前几个epoch里,分类损失会快速下降,这是模型在学会“拒绝大部分背景”的阶段。等分类损失降到一定程度,回归损失开始主导训练,这是正常的。但我遇到过一次诡异的情况:前10个epoch损失下降很顺畅,到了第12个epoch,回归损失突然小幅回升。排查了半天,最后发现是余弦退火的学习率在这个位置衰减得太快,模型进入了某种局部震荡。把初始学习率从0.01降回0.005之后,曲线重新变得平滑。
还有一个经验:每一轮epoch结束,不要只看总损失,要看类别的分布。有些框架只打印整体mAP,这个指标在类别不均衡时很容易骗人。我在训练脚本里加了一个按类别分别统计AP的逻辑,发现湖泊和海洋的AP已经到0.85以上时,河流AP可能还不到0.4。如果不分拆打印,你可能完全察觉不到模型实际上只学会了“找大片水体”。
4. 训练阶段高频踩坑的完整排查链路
4.1 损失突变成NaN:先查数据,再查学习率
这是每个训练检测模型的人都躲不过的坑。第一次碰见时我很自然地调低学习率,重新训练,结果还是NaN。后来我养成了一个固定的排查顺序,推荐你也按这个来:
第一步查数据。打印所有参与训练的patch和标注框,看有没有全零像素、是否存在面积为0的边界框、坐标是否超出patch范围。水体标注尤其容易出这种问题,因为河流拆段标注时,操作人员可能会拖出一个坐标重叠的退化框。
第二步查损失组成。单独输出分类损失和回归损失,看NaN是从哪一项开始的。如果回归损失先炸,大概率是标注框出了问题;如果分类损失先炸,再考虑学习率。
第三步查正样本匹配。RetinaNet依赖IoU分配正负样本,锚框和标注框如果尺度严重不匹配,部分层可能完全没有正样本,梯度异常也会导致训练崩溃。
最后才考虑学习率和优化器。我在项目里只遇到一次真正由学习率引起的NaN,其余全是数据问题。
4.2 山体阴影被当成湖泊:换通道组合是最快解法
这个坑一度让我怀疑RetinaNet对水体的区分能力。某块区域的山体阴影在真彩色影像里看起来就是一片深色调,和湖泊的光谱特征在RGB空间里极其接近,模型怎么调参都会把阴影误判成水。
后来分析发现,问题出在输入通道。真彩色RGB只用了可见光波段,而水体在近红外波段的吸收特性非常明显,山体阴影在近红外却不会表现出这种吸收。解决方法是把输入通道从RGB换成近红外、红、绿三个波段的组合,模型立刻就能区分阴影和水体,误报率几乎降了一个数量级。
如果你用的是多光谱数据,强烈建议给RetinaNet输入做一次波段选择实验。不要迷信“模型能从RGB里自己学出来”,遥感物理特征在输入阶段直接给到模型,比让网络自己硬拟合要高效得多。
4.3 河流被切成虚线:正样本数量决定的漏检
模型对河流的预测经常呈现断断续续的“虚线”状态,一段河流中间漏一截,或者只检出不连续的几小段。这是长条形目标最常见的失败模式。
根因有两个。第一,锚框长宽比不够时,河流段落在某些角度下匹配不到足够的正样本;第二,河流在1024×1024的patch里如果太细,只有不到几十个像素宽,对应到FPN高层特征图时可能已经退化到几个像素点,特征表达几乎消失。
我的解法是组合拳:调整长宽比、提高输入分辨率到1280、增加河流patch上采样权重、把非常细的河流段标注拆成更短的框以便匹配。这四件事同时做之后,河流的连续性肉眼可见地变好。
5. 从单张patch走向整片区域:推理、后处理与提速
5.1 patch结果拼回大图的去重策略
训练是patch级的,推理也要回到patch级,但最终用户需要的是整片区域的完整结果。我的流程是先按1024×1024加128像素重叠滑窗推理,再把所有detection映射回原图坐标。
这里最关键的逻辑是重叠区域的去重。同一个湖泊在两个相邻patch的重叠区域内可能被预测两次,直接合并会导致输出框有大量重复。我采用的方法是先按置信度阈值(0.3到0.35)过滤低质量框,然后做跨patch的NMS。但NMS的IoU阈值不能设太高,设成0.5比较合适,太低了会合并两条被预测框覆盖的相邻河道。
还有一个细节容易被忽略:patch边缘的预测框质量普遍比中央差。因为边缘处目标可能被截断,模型的上下文不完整。我在推理脚本里加了一个惩罚策略,对靠近patch边缘的预测框按距离降低置信度,效果比单纯加NMS更明显。
5.2 多时相投票:把误报再降一截
如果你手里有同一区域多期影像,强烈建议做时相维度的后处理。单期影像里的云遮挡、云阴影、季节性浅滩都会造成误报,但真实水体的位置在多期影像里应当是稳定出现的。
我的做法是:把同一地理区域的多期影像分别推理,然后将预测框投影到同一地理坐标下,统计每个位置的确定性。简单来说就是投票表决,只有超过半数期次都检出的目标才保留,只有一两期出现的目标直接丢弃。这个操作看起来有点暴力,但实测能把云阴影导致的误报率降低四成左右,代价仅仅是推理时间增加。如果你的算力允许,这是性价比非常高的后处理手段。
5.3 半精度与批处理加速
推理阶段我全程用半精度浮点。水体检测的输入是遥感影像,噪声本身就不小,半精度降低的那一点数值精度完全不影响输出框质量,却能把单卡推理速度提升近一倍。显存允许的情况下尽量开批推理,多个patch同时进网络,比单张循环快得多。
如果还需要进一步提速,可以试试把输入分辨率从1024降到768甚至512。水体目标普遍偏大,降分辨率对湖泊和海洋的影响很小,但细河流会开始漏检,所以要按场景权衡。我最终的方案是:大范围初筛用512分辨率,找到可疑水体区域后再用1024分辨率精检测,兼顾速度与召回。
6. 评估水体检测模型:只看mAP容易迷惑
6.1 分面积AP:湖泊和河流的指标要分开看
通用目标检测用mAP一个数概括所有情况,但水体检测里一个mAP会掩盖太多问题。湖泊是大目标,河流是细长目标,海洋则是超大区域目标,三者本来就该用不同的尺度标准来评价。
我建议至少按目标面积分三组统计AP:小型目标(比如面积小于32×32像素的河段)、中型目标(湖泊、较宽的河段)、大型目标(大湖、近海区域)。只看总体AP,大型湖泊的表现会把河流的缺陷完全掩盖。我第一版模型总体AP到了0.82,看着已经不错了,但分面积一看,小型目标AP只有0.3,这才发现河流漏检问题远比想象中严重。
6.2 沿边界的误差:用边缘距离补充IoU
目标检测的标准评估指标是IoU,但水体检测用户关心的往往是边界误差——模型画的框离真实水陆边界差多少像素。尤其是海岸带和湖岸线场景,10个像素的偏移可能对应实际地面数十米的距离。
我的做法是额外计算一个边界距离指标:取标注框边界和预测框边界,计算两者之间的平均距离。这个指标不用复杂工具,找几个验证patch手工对比即可。如果IoU很高但边界距离也很大,说明模型框的方向和位置虽然大致正确,但存在系统性偏移,通常在标注阶段就埋下了隐患,比如河流框拆段时习惯性偏向上游一侧。
6.3 错误类型归因:一次复盘实例
最后建议每次迭代都保留一组错误检出的截图,按错误类型归类。我整理过一张表,记录了同一模型的主要错误类型和占比:
| 错误类型 | 典型表现 | 主要原因 | 解决方向 |
|---|---|---|---|
| 阴影误报 | 山体阴影被当湖泊 | 输入波段缺少近红外 | 改通道组合 |
| 河流断裂 | 同一河道预测不连续 | 锚框长宽比不足、正样本太少 | 调锚框、拆段标注、过采样 |
| 海洋外扩 | 海面框侵入陆地或滩涂 | 近岸泡沫带干扰 | 标注时避开泡沫带 |
| 云遮挡漏检 | 云下水体完全未检出 | 信息缺失不可恢复 | 多时相投票过滤 |
做这个归因表看起来费时间,但它比看十遍mAP曲线更有用。每次迭代都从表里占比最高的错误类型入手,你会发现模型能力提升的路径特别清晰。比如有一次我把阴影误报降到接近零之后,表里占比最大的就变成了海洋外扩,于是接下来几天专门优化近岸标注和边界回归,一版一版往下走。
如果你也在做类似的水体检测项目,建议从第一版评估表开始保留原始错误截图,每次跑完新模型回头翻一翻,这比单看指标变化更能发现模型到底在什么地方变强了。我自己就是靠这套“类型归因”的方法,把河流AP从0.31一路抬到了0.68,中间没有换过模型结构,全是数据和后处理层面一点一点抠出来的。