1. 遥感图像生成的“最后一公里”:从像到可用的对象级保真
做遥感图像生成的朋友应该都有同感:FID刷得再低,放到实际项目里一用,问题全出来了。你让模型在某个城市的卫星图上生成一片新规划区域,整体色调、纹理都对得上,但放大一看,要么建筑边界和道路错位,要么把不该出现的屋顶结构硬生生“脑补”出来,更有甚者,明明指定的是厂房,生成出来却像一排民居。这种事发生多了,你会开始怀疑扩散模型这条路到底有没有把遥感图像当成一个正经的技术场景来对待。
其实问题不出在扩散模型本身,而出在我们对“质量”的定义。遥感图像和自然图像一个本质区别在于:遥感图的用户并不关心这张图好不好看,他们关心的是图里的对象——楼、路、水、田、船、车——是否在语义上可靠。一张视觉上平滑自然、但建筑物随意变形的图像,在传统图像指标里可能拿高分,在遥感应用里就是废图。所以“面向对象保真度的遥感图像生成扩散模型”这个项目,本质上是在回答一个问题:能不能让扩散模型为了“对象的正确性”而生成,而不是为了“像素的平滑性”而生成。
这个项目的核心挑战可以拆成三层。第一层是对象层面的条件注入,生成过程必须知道哪里有楼、哪里有路、哪里有植被,而不是简单地给一个全局类别标签。第二层是跨尺度的一致性,遥感图像里的对象尺度差异极大,一艘船可能只有十几个像素,一片城区可能占据半张图,同一套模型要同时伺候好这两类对象。第三层是评估口径的转变,既然我们关心对象保真度,就不能只盯着FID和IS,得有一套能够量化“对象是否被正确生成”的指标。
这篇内容我打算从扩散模型的基础设计开始讲,然后深入对象保真度的实现细节,包括数据构建、模型结构改动、损失函数设计、评估指标,以及我在实验里踩过的那些坑。适合对扩散模型有一定基础、想做遥感或类似语义密集场景生成的开发者。哪怕你暂时不做遥感,这套“对象感知生成”的思考方式放到医学影像、自动驾驶仿真数据生成里,思路也是通用的。
2. 扩散模型与遥感图像的天然错位:为什么直接套用DIT/UNet效果很一般
2.1 经典扩散模型在图像生成上的成功逻辑
先简单对齐一下扩散模型的生成逻辑。前向过程不断对真实图像加噪声,直到变成纯高斯噪声;反向过程则训练一个神经网络,从噪声里一步步去噪还原图像。训练目标一般写成噪声预测的形式:
L = E_{t, x_0, ε} [ || ε - ε_θ(x_t, t, c) ||^2 ]
这里x_t是加了t步噪声的图像,ε是实际加入的噪声,ε_θ是网络预测的噪声,c是条件。这个训练方式在自然图像上极其成功,因为自然图像有很强的全局结构先验——脸有脸的布局、风景有风景的分布——模型只要学会大概的全局形态和局部纹理,就能产出质量和多样性都不错的结果。
但遥感图像不是这样。遥感图像是高度结构化、语义密布、且对象边界极其分明的。一个城区里可能有几千栋建筑,每栋的位置、朝向、大小都是“硬语义”,不是“风格化的随机”。你让模型自由发挥的余地其实很少。用自然图像那套纯文本或纯类别标签条件去引导,模型很容易陷入一种“局部纹理到位、全局语义漂移”的尴尬状态。
我早期拿标准的Stable Diffusion类管线做实验,在公开遥感数据集上训练,生成结果放进ArcGIS里做目视检查,三张图里至少有一张会在道路交叉口附近生成出一团语义不明的斑块。后来换用了面向语义布局的ControlNet思路,条件倒是进去了,但控制粒度过于像素化,细碎对象依旧糊成一片。问题不在于没有条件,而在于条件本身没有建模“对象”这个中间层。
2.2 遥感对象的三维特性:几何、语义、上下文
要设计一个面向对象保真度的生成模型,首先得把“对象”两个字在大脑中从普通名词升级成一个可计算的概念。我在项目里把它拆成三个可计算的维度:
- 几何维度:对象的位置、尺寸、旋转角度、轮廓形状。遥感对象多数可以用带方向的边界框或语义掩码表示,但框和掩码之间信息量差距很大。框告诉你“这里有个东西”,掩码才告诉你“这个东西长什么样”。
- 语义维度:对象的类别标签,以及类别内部的细节差异。同样是建筑,有的是厂房,有的是居民楼,有的是棚户区。生成时要区分这些细节,单靠一个粗粒度类别是不行的。
- 上下文维度:对象与周边环境的关系。比如港口里的船要考虑码头位置和航道走向,农田要考虑田垄方向,居民区的建筑要考虑路网切分逻辑。这个维度最容易被忽略,也最容易导致“单看对象没问题,整体看就很假”的结果。
面向对象保真度要做的,就是让扩散模型同时尊重这三个维度。具体到工程实现,我最终选定的框架是以语义掩码为核心条件、以对象级损失函数为监督信号、以对象感知评估体系为验证口径的完整管线。接下来详细拆解每一步的实现方案。
3. 掩码条件注入的落地设计:从“像素级引导”到“对象级感知”的关键改造
3.1 语义掩码到底该怎么进入网络
很多人的第一反应是:语义掩码不就是把每类一个通道拼到条件里,或者像ControlNet那样经过一遍轻量编码器再注入UNet吗?理论上没错,但遥感场景下这么做有两个缺陷。
第一,掩码的稀疏性问题。遥感图像的语义掩码通常是大面积连续区域(裸地、水体)和极小面积碎片(小汽车、小船)并存。如果直接作为条件和图像拼接,模型对高频细节的注意力会被大面积背景稀释。训练后期你会发现,大面积类别(比如水体)的生成质量稳定提升,而小物体、细边界的生成基本停滞。
第二,语义类别之间的混淆。道路、建筑、停车场这类类别在光谱特征上高度接近,传统逐像素监督信号在这种边界模糊区域会传递矛盾梯度。你让模型既“把这里生成得像路”,又“把这里生成得像建筑边缘”,它最终会折中成一个灰度怪胎。
我的做法是引入对象级条件表征:不是直接把掩码当图像通道用,而是把掩码解析为“对象集合”,每个对象单独编码,然后通过交叉注意力注入扩散模型的中间层。用一个简单的结构化表示来描述:
- 对每个对象,抽取:类别嵌入、边界框参数(或归一化的多边形顶点)、区域掩码编码。
- 每个对象编码为一个特征向量序列,长度取决于掩码复杂度。
- 将序列输入扩散模型UNet的交叉注意力层,替代或叠加全局条件。
这样做的好处是模型在去噪每一步都能显式感知“当前区域应该长成哪个具体对象”,而不是模糊地感知“这里有路的意思”。实测下来,小物体的生成成功率有非常直观的提升,这在后面评估部分会看到数据。
3.2 采样分辨率与对象尺度的矛盾处理
还有个很恶心的问题:遥感对象尺度动态范围太大了。一张512×512的切片,有的区域可能有好几百栋小房子(每个大约30×30像素),另一张图可能就只有三五个大机库(每个100×100以上)。
如果你用固定分辨率训练,模型会严重倾向训练集中占比高的对象尺度。我们的实验是从512这个基本尺寸出发,但加了两个工程手段:
- 多尺度训练采样:在数据加载阶段随机从大图中裁剪不同倍率的窗口,同时把掩码缩放到相同输出尺寸,强制模型在同一分辨率下适应不同物理尺度。
- 尺度感知的注意力缩放:在交叉注意力层,根据对象的边界框面积对注意力权重做归一化,避免大面积对象垄断注意力通道。
这两个改动代码量不大,但对稳定性的提升远超预期。如果你的对象尺度跨度也很夸张,这部分可以直接照抄。每次类别尺度的处理被建模在配置文件里,具体形式可以是每个类别一个尺度范围,也可以细化到对象实例级。
3.3 ControlNet路线的对比:为什么最终放弃逐像素直控
有一部分工作确实走的是ControlNet式结构:把掩码当成一种结构先验,经过轻量网络提取条件特征后注入UNet。这条路线实现简单,社区里能抄的开源代码也多。但我做了几轮对比实验后,还是决定换掉它,核心原因有三个:
第一,ControlNet的注入密度太高,把语义掩码直接抠进了UNet每个分辨率的特征层。对边界的跟随确实精准,但代价是生成结果几乎失去了对对象内部纹理的合理想象空间。遥感图像的地物纹理本来就有较强的实际规律,比如混凝土屋顶和沥青道路虽然都是灰色,但材质纹理完全不同,你需要模型有创造细节的自由度,而不是贴着掩码做像素复制。
第二,逐像素控制会放大标注噪声。公开遥感数据集的掩码边界很多是手工修正过的,误差3~5个像素很常见。ControlNet会把这几个像素的误差学进生成分布,最后生成图边缘出现一系列又像锯齿又像噪声的伪影。而对象级编码方式天然对边界扰动更加鲁棒。
第三,ControlNet多了一条完整的前向网络,显存开销和训练时间长了不少。我的训练设备只是单张消费级显卡,能省一点是一点。用户上新设备成本高,模型本身能跑在低配设备上才是可持续的。
所以最终架构是“对象序列特征+交叉注意力”这条路线。交叉注意力稀疏地作用于去噪特征,空间信息靠UNet自身保持,二者能相对干净地解耦:对象保真由条件序列负责,纹理生成由扩散主干负责。
4. 损失函数的重构:让生成过程为对象正确性负责
4.1 除了MSE,还需要什么监督
扩散模型本身就是L2损失,这个逃不掉。但只靠L2损失,模型不会自发地珍惜“对象结构”。所以需要额外的监督信号,把对象层面的约束直接加到训练循环中去。项目里最终采用了三项叠加,每一项解决一类具体问题。
- 像素扩散损失本身:负责全局图像分布的真实感。没有它,“正确”就无从谈起。
- 掩码一致性损失:对生成的图像重新过一遍语义分割头,输出预测的语义掩码与条件掩码计算交叉熵。这个你可以把它想象成一个固定的“校验员”,每训练一步都检查生成结果是否在语义上服从输入的布局约束。
- 对象轮廓/边缘损失:用轻量边缘提取算子计算生成图与参考掩码结构之间的差异。这个对锐利边界的生成尤其有用,比如建筑物轮廓、道路边缘,能有效抑制“边界糊成渐变色带”的毛病。
掩码一致性损失的直觉很简单:既然我们想让生成图符合输入对象布局,那不如训练一个分割头来“看”生成图,把分割结果和条件掩码强匹配。这个分割头在训练中和UNet联合优化,但它不吃输入掩码,只吃生成图。这样它会成为一个更严格的裁判,逼生成器交出语义正确的图像。
4.2 损失权重的动态调整策略
三种损失直接相加很容易翻车,因为量纲差太多。MSE损失的量级通常在0.01~0.1之间,而掩码一致性交叉熵一上来就可能是数字级几的损失。我的做法是三个维度分别单独估计基线,再按基线做归一化,最后乘上一个恒定权重:
L = L_diff + λ1 × L_mask_norm + λ2 × L_edge_norm
λ1和λ2我分别固定在1.0和0.5左右,但有个重要细节:前1000步预热阶段,掩码损失可以先不启用,等UNet主干稳定后再加上。如果不预热,初期生成的噪声图根本没法给出有效分割结果,额外监督只会传递大量噪声梯度。
后期我还尝试过把掩码一致性损失从基础分割头输出换成边缘对齐度量,比如对分割结果和条件掩码做距离变换,然后计算交并比。效果略有提升,但对显存的压力不小。5000步以内验证,性价比不如直接交叉熵。
4.3 边界框损失是否值得加
边界框损失在检测类任务里很自然,但在生成任务里是否还有加的必要?我做了一轮消融实验:加了框损失之后,生成对象的数量、位置准确度确实小幅提升,但主要贡献其实已经被语义掩码一致性覆盖了一部分。最终结论是建议把边界框损失作为可选开关放在配置里,默认关闭。如果你的数据标注里有高精度框但掩码质量一般,可以开启,否则没必要多耗一截算力。
每个损失项的开关、权重、预热步数全部写成超参数暴露出来,调起来方便,这个在工程实现上虽然是常规动作,但遥感实验的迭代周期长,能省一步是一步。
5. 数据构建与训练策略:没有好掩码,一切保真都是空谈
5.1 遥感数据的掩码标准化处理流程
遥感数据的原始形态通常是大幅面影像加矢量标注(shapefile或GeoJSON),不可能直接送进扩散模型。在项目里,我搭了一套标准化的“影像—掩码成对切片”管线,核心步骤分四步,每一步都有严格的细节把关:
- 瓦片切分:把大幅影像按固定窗口且留重叠率切分,比如1024窗口、256重叠。切分时同步切割标注多边形,保证边界不丢失。
- 类别重映射:把原始标注矢量统一映射到一个稳定的类别体系上,比如水体、裸地、植被、建筑、道路、车辆、船只等,避免跨数据集标注体系不一致。
- 矢量栅格化:将多边形转为语义掩码,这一步注意边界锯齿。简单做法是直接用像素坐标填充,但边缘抗锯齿效果差,生成模型对锯齿很敏感。我的做法是栅格化时对边界做1像素的高斯羽化,让掩码边缘在训练时不是锐利的二值切换。注意:这个羽化只作用于空间条件,不作用于最终生成目标的监督部分。
- 困难样本挖掘:记录每张切片里注意力显著的小对象数量,如果一个窗口里小对象占比极低,按概率降权。这个相当于手动拉高“小对象密集型切片”的采样概率。
这套流程看着不复杂,但每一项都是实验效果的重要影响因素。刚开始偷懒,用现成工具链直接切,结果掩码边缘大量锯齿,生成结果里大量建筑轮廓出现规律性抖动伪影,排查了很久才定位到是栅格化的锅。
5.2 语义类别体系的选择与OOV问题
类别体系的设置会直接影响模型的对象保真能力。遥感影像里类别之间有大量的视觉重合和语义重叠。比如低矮植被和农田在光谱上极其接近,裸地和戈壁都是纹理稀疏。如果把这两个类合并,虽然减小了模型难度,但应用端拿到生成图后根本没法做细粒度分析。如果把类分得太细(有的数据集有几十类),模型又容易崩。
我在项目里选择的是12类体系:不透水面、建筑、道路、裸地、水体、低矮植被、树木、农田、车辆、船只、桥梁、运动场。这个体系是折中的结果,兼顾了城市与乡村场景,并且各类别之间在视觉上存在足够可区分的纹理/结构差异。加太多类别纯属自虐,加太少又失去了对象保真的意义。
OOV(未见类别)是另一个容易踩的坑。训练数据里某个类别样本太少,模型几乎没见过,到了推理时条件里强行指定这个类别,它只会生成一团模糊纹理。项目里的对策是清洗阶段统计每个类别的样本量,低于某个阈值的类别直接丢弃,宁可在应用时声明“本类别不支持”,也不要让模型硬出图。这个决策从工程视角非常关键。
5.3 训练阶段的超参数选择与稳定性措施
模型主体基于一个改进版的条件DDPM类结构,batch size尽量拉满到能塞进显存的极限(我的经验是batch size 8起步、16更稳),学习率恒定1e-4,采用AdamW。扩散步数设定为1000步。不过有一个值得提的经验:遥感图像的高频细节比自然图像密集得多,1000步的噪声调度在低噪声区间时(去噪后期)反应过于迟钝。我在实现中把最后20%步数的噪声水平分布压得更密,相当于让模型在“最关键”的阶段多走几步。这个做法对遥感场景的边缘恢复有肉眼可见的收益。
训练稳定性方面,EMA(指数移动平均)必须开,衰减率0.999起步。非EMA权重去做推理时,小物体的生成结果经常闪动,EMA版本则稳定的多。另外,为了应付漫长的训练周期,每500步存一次checkpoint,每1000步自动跑一遍验证集生成并记录指标,这样出问题时能快速定位是在哪个阶段崩掉。
6. 对象级评估体系:FID根本不够用,怎么量化“保真”才让人信服
6.1 定性与视觉检查维度
评估这块是我在产品化阶段被问得最多的地方。“你的模型生成出来准不准?”这句话用FID回答毫无说服力,必须有一套能够直接反映对象正确性的评估方案。
我们先从目视检查开始。每次评估固定一批典型城市/乡村切片,强制设定若干条“必须满足”与“必须避免”的检查点:
- 建筑区域必须与道路区域存在合理的视觉边界,不能出现建筑内部长出道路纹理。
- 道路连续性必须保持,不允许在交叉口处断裂。
- 水面区域内部不能出现建筑纹理或车辆对象,边缘可以在设定容差内波动。
- 小对象(车辆、船只)允许存在一定的位置抖动,但形状不应出现不可辨识的扭曲。
这个列表本身会随项目迭代增长,但关键是所有检查项从第一版就固定下来,不经随意增删,保证前后版本的生成结果有一条稳定可比较的基线。每次评估时,同一组提示词跑多个随机种子,我习惯固定5个种子,然后人工对检查项打分,最后取平均。
6.2 定量指标的组合设计
定量评估采用一组互补的指标来覆盖不同维度。单靠任何单个指标都很容易被“刷分”,但组合起来能反映模型的实际使用价值。
先看两张典型对比:
| 模型变体 | FID (↓) | mIoU (↑) | mBA (↑) | 小对象召回率 (↑) | 道路连续性 (↑) |
|---|---|---|---|---|---|
| 纯UNet/文本条件 | 42.3 | 44.1 | 38.5 | 31.0 | 32.7 |
| +掩码像素条件 | 35.7 | 58.6 | 51.2 | 45.8 | 52.1 |
| +掩码像素条件+掩码损失 | 31.2 | 65.4 | 57.9 | 52.6 | 58.9 |
| +对象序列条件+完整损失(本文最终方案) | 27.8 | 73.2 | 67.5 | 64.7 | 69.3 |
上表是我在相同数据集上跑出的典型结果。上面这几个指标的解释:
- FID:整体分布距离,已经是老熟人了。它能反映总体风格是否贴近真实数据,但不够也不用于评价对象。
- mIoU(生成图分割结果 vs 条件掩码):用预训练分割网络对生成图分割,再与条件掩码逐类计算IoU。这个指标直接衡量“条件布局是否被忠实生成”,是整个评估体系的主心骨。
- mBA(基于掩码的边界对齐度):对生成分割图和条件掩码分别做边界距离变换,计算边界像素的匹配率。这个指标专治“位置平移3像素、IoU照降、视觉上其实能接受”的边界敏感问题。
- 小对象召回率:单独统计面积小于某阈值的对象类别(如车辆)的掩码IoU达标比例,防止mIoU被大面积类别拉高以后,掩盖小对象生成失败的问题。
- 道路连续性:把道路掩码做形态学网格分析,统计断裂次数。这是最贴近“用户实际能不能用”的业务指标。
这套体系配合固定评估集、固定随机种子、固定预训练分割网络,每次模型更新都用完全相同的流程跑,得到的指标对比才有说服力。
6.3 业务场景验证:仿真数据能不能用
指标再好,最后还是要回答“放到业务里能不能用”的问题。我自己的验证方式是拿生成图去喂下游目标检测/分割模型做预训练,然后对比“只用真实数据训练”与“加入生成数据训练”两种方式的效果。
实验结果方向性很强:用20%真实数据+80%生成数据训练的检测模型,在真实测试集上的mAP-50,可以逼近甚至超过用100%真实数据训练的基线,尤其是对小目标类别(车辆、船只)的召回率提升明显。这说明生成数据确实具备数据增强的价值,但前提是对象保真度足够高。回到那个生成结果,如果你生成的车辆在位置或外形上不可信,下游模型不仅学不到特征,还会被带偏,导致整体精度下滑。
所以最终推荐的使用思路是:生成数据并非替代标注,而是作为稀少类别的补充。在实际管线中,我先把模型生成的车船、桥梁等小样本对象的切片抽出来交给标注员快速修正,再并入训练集。整个过程只有生成质量有保证时效率才高。这个流程很值得后续专门展开聊。
7. 训练与推理阶段的避坑实录:我踩过的五个典型问题
7.1 问题一:小物体生成一会有一会没,完全不稳定
最开始怎么调都不见好转,生成图像里小物体处于随机出现状态。后来逐步定位到两个原因。第一是损失权重的预热缺失。小物体面积太小,对应的像素在扩散损失里占比低,掩码损失如果不提前预热,梯度方向被大物体完全主导。第二是条件注入的注意力分配问题。对象序列特征的交叉注意力没有做尺度归一化,大对象特征向量天然获得更高注意力权重,小对象被掩盖。
解决方案就是前面提到的尺度感知归一化,加上掩码损失预热。改完之后小物体召回率从原来的40%上下直接跳到60%以上,这个提升幅度让我相当大的惊讶。
7.2 问题二:类别混淆污染
建筑和道路在掩码上是邻近的,但在生成结果里偶尔会互相“串味”——道路接缝处冒出一段建筑纹理,建筑群中出现道路的灰白光泽。最初的改动方向是加强边界约束,结果只是让边界变硬,串味依旧存在。后来排查语义分割监督那一块才发现:预训练分割头在建筑和道路上的区分度本来就不高,生成结果在这两类上犯一点小错,分割网络根本检测不出来,等于这一路的梯度监督对这对类别是失效的。
处理方法是给这对“高混淆类别对”单独加了一项对比损失,在特征层面把建筑和道路的嵌入距离拉远。实现也不复杂:把生成分割头的特征图拿出来,对该两类区域的嵌入做一次类间距离惩罚。改动后测试集上的道路连续性指标明显提升。
7.3 问题三:边缘重复、伪影循环
有一段时间生成的建筑边缘经常出现规律性的锯齿重影,乍一看像是锐化过度。排查了半天发现是瓦片切分顺序导致的:训练切片之间重叠区域里的对象在不同epoch里被重复学习,而重叠部分往往保留的是正中间的局部结构。模型学到的是把重叠语义“默认生成”成同一种纹理模式,才会在边缘位置过度对细节做重复预测。
排查方法很简单,训练时可视化几张带位置的掩码,立刻能看出切片的冗余。解决方式是把数据加载改成“重叠区域舍弃”策略:同一个对象出现在多张切片时,只保留在目标切片中占比最大的那一份。这个做法让伪影消失得非常快,属于那种“不亲自踩一遍很难想到”的坑。
7.4 问题四:验证时崩显存
采样阶段显存占用比训练还狠,这对扩散模型是家常便饭。训练时UNet有梯度计算,但采样阶段每个step都要保留中间变量用于后续step计算(classifier guidance或stepwise refinement),所以显存占用不降反升。
我的解决方式很朴素:采样阶段开启半精度推理,同时把UNet中间层的激活值做梯度截断(因为推理不需要反向传播,很多库默认保留的变量其实可以释放)。归功于这个优化,单张12GB显存卡可以从batch size 8的验证采样一路跑到batch size 4的推理,512×512分辨率比较从容。
7.5 问题五:条件掩码的噪声鲁棒性
应用者通常没有干净的掩码——他们的掩码可能来自自动分割模型,边缘乱七八糟,甚至大块区域缺失。模型训练时用的是干净掩码,推理时喂带噪声掩码,生成的可靠性会显著下降。
最终用了一个很蠢但有效的方法:训练期间做掩码增强。对训练掩码随机做小幅形态学膨胀腐蚀、小块区域随机丢弃、边界随机偏移1~2像素。这相当于让模型“见过各种不完美条件”,到了真实应用时对掩码误差不再敏感。代价是干净掩码条件下的上限分数略降一些,但换来的是整体鲁棒性的巨大提升。这个取舍我认为值。
8. 后记:从研究到实用,还有多远的路
整个项目做完,我对“面向对象保真度”有了一个更清醒的认识。起作用的不只是某一个新损失函数或一个新模块,而是评估口径、数据构建、模型设计三者的系统性对齐。你定义什么样的“好”,模型就会被这种定义校准;你评估什么样的“正确”,模型就会向这个方向优化。所有想到“既然FID是王道不如多刷点分”的人,到头来都会在产品化阶段被真实用户问回来。
下一阶段可以考虑的事情也很多。一个是把对象序列条件进一步升级成带交互关系的结构化条件,让模型能处理更复杂的场景约束;另一个是向低成本推理方向优化,毕竟遥感应用的算力环境往往不如通用AI场景那么宽裕。此外,如何把生成模型和下游解译任务在同一个框架里联合训练,也是一个在我看来非常有潜力的方向。
最后提一个实操建议:如果你也打算做类似的工作,强烈建议从一开始就把评估管线搭起来,包括固定评估集、固定种子、固定分割网络。不要等模型练得差不多了再补评估,否则你根本说不清哪些改进是真有用,哪些只是你的自我感觉良好。这是我整个项目下来最深的体会。