资讯动态

卫星遥感图像识别落地实践:TensorFlow语义分割全链路解析

发布时间:2026/10/18 7:16:29 来源:尧图企业网站定制
卫星遥感图像识别这几年是被讨论得最多、也最容易翻车的一个方向——数据集难搞、标注标准乱、模型精度上不去、推理一跑就内存爆炸。我这次做的这套系统用的是纯Python技术栈深度学习框架选了TensorFlow目标是做一套能真正落地、能处理大范围卫星影像的识别系统。目前整套流程已经稳定跑通了从数据处理到模型训练再到推理部署全链路都有可复用的方案。这篇就把整个项目从架构设计到踩坑细节完整拆一遍内容会很长但每一步都是实打实跑过的。1. 遥感影像识别系统的架构设计与技术选型先说清楚这套系统到底是干什么的。卫星遥感图像识别本质上是从高分辨率或者多光谱的卫星影像里自动提取出地表信息——水体、建筑、农田、裸地、道路这些地物类别。应用场景很直接比如城市扩张监测、耕地保护、灾后评估、土地利用变化检测等等。我在设计这套系统的时候第一步没有急着写模型而是先把整个数据流向画清楚了。卫星影像和普通图片最大的区别在于三个地方尺寸巨大、波段丰富、地物尺度差异悬殊。一张原始影像动辄几万乘几万像素直接丢进任何深度学习模型都是不现实的。所以我设计了三个核心模块数据预处理模块负责把大分幅影像的波段组合、裁切、归一化、样本增强最终输出成模型可以吃进去的固定尺寸切片。模型训练模块基于TensorFlow搭建语义分割网络实现逐像素的地物分类。推理与后处理模块把训练好的模型重新滑窗推理到整幅大影像上再通过后处理去掉碎片噪声输出最终的矢量或者栅格分类结果。技术选型上深度学习框架最终敲定TensorFlow主要原因是它在生产部署上成熟度更高tf.data的流式数据管道、tf.train.Checkpoint的断点续训、tf.saved_model的导出格式整个链路都是原生闭环后端换到生产环境也不用重写推理代码。Python作为主语言没有悬念遥感领域大量的开源工具都是Python生态。依赖库清单也一并列出来方便直接参考tensorflow2.8模型框架rasterio遥感栅格读取支持TIFF/GeoTIFFnumpy/pandas数据处理opencv-python传统几何变换增强scikit-learn评估指标、精度评定albumentations遥感增强库推荐能处理mask同步变换geopandas/shapely后处理矢量化的可选方案这套结构下来从原始影像到最终分类成果每一步都是松耦合的模块调试起来非常清晰。后面每个模块我都会展开讲具体实现。2. 数据工程卫星影像的预处理与样本生产的核心细节2.1 波段组合与影像范围的预处理逻辑卫星影像的源数据通常是包含多个波段的TIFF文件有的还有全色波段、热红外波段之类的。这里必须强调一个新手最容易忽略的问题波段顺序不是你想用就能直接用必须先确认影像波段的实际排列。不同卫星的波段排序可能不同有的索引0是蓝波段有的是红波段直接按固定索引去取很容易拿错。我的做法是先打印影像元数据查看description字段再用波段合成的方式确认假彩色效果是否合理。import rasterio with rasterio.open(scene.tif) as src: print(src.count) # 波段数量 print(src.crs) # 坐标参考系统 print(src.bounds) # 地理范围 print(src.indexes) # 波段索引顺序拿到波段索引之后我再按需组合。一般情况下模型训练我优先使用RGB三个波段加近红外波段因为近红外对植被和水体的区分效果极好。波段组合之后要考虑的一个关键处理就是辐射归一化。不同时间、不同光照条件下获取的影像其绝对辐射值差别很大。虽然深度学习模型在理论上能够适应亮度变化但在实际训练中不做归一化的模型往往在泛化性上差一大截。我的做法是统一缩放到0到1区间这相当于把模型的输入分布稳定下来训练收敛速度和最终精度都会明显改善。2.2 样本标注源与切片生产的准则标注数据是这整个系统里最需要花时间的地方。卫星遥感语义分割的公开数据集虽然存在但往往类别体系跟你的项目需求不完全一致所以大部分项目都得靠人工标注或半自动标注来生产样本。我第一次做这个项目的时候踩过一个大坑直接把整幅大图丢给标注工具在小范围内放大做勾绘结果最后检测出来大量边界线错位问题。问题的根源在于标注是在全分辨率视图上完成的但模型训练时是切成小图输入切割之后目标边界如果刚好落在切片边缘就会被硬生生截断而且切片边缘的上下文信息严重缺失。后续的解决方案是标准化的做法也是业界比较通用的思路使用固定窗口把大影像切成256x256或512x512的切片切片的步长设置为窗口大小的一半保证相邻切片之间有重叠区域避免目标被切开每个切片同时输出影像和对应的标注mask命名保持一致剔除掉mask中完全没有有效标注的切片减少无效样本。重叠切片的优点在于虽然会造成一部分重复计算但能显著减少地物被截断的概率。推理阶段我也沿用这个重叠思路后面对齐输出时用投票或者加权平均来融合重叠部分。2.3 数据增强策略兼顾精度和过拟合遥感影像的数据增强跟普通自然图像有所不同。因为地物是俯视角特征翻转和旋转都是安全的增强手段但随机裁剪和尺度抖动要慎重——这类操作会破坏地物的尺度一致性比如你切出来的一个建筑跟道路的尺度比例被扭曲了模型学出来的特征就不可靠。我实际采用的增强组合是水平翻转、垂直翻转概率0.590度旋转遥感俯视图旋转具有语义不变性亮度对比度微调调节范围控制在±20%高斯噪声注入模拟传感器噪声增强抗干扰性随机gamma校正还有一个有效且容易忽视的增强手段是多光谱通道随机丢失类似Dropout的思路训练时随机将某个波段的值置为0强迫模型不依赖单一波段这对提升跨季节影像的泛化能力有明显帮助。关于增强的实现强烈建议使用albumentations而不是tf.image。原因很简单遥感场景需要同步变换影像和maskalbumentations提供了一套管道式API传一个字典同时输出增强后的图像和mask逻辑清晰且效率高。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.GaussNoise(var_limit(0.0, 0.02), p0.3), ]) def apply_transform(image, mask): augmented train_transform(imageimage, maskmask) return augmented[image], augmented[mask]这套增强组合在训练初期能把验证集上的mIoU稳定提升2到3个百分点且基本不增加训练成本。3. 核心算法模型的落地选型与关键实现3.1 语义分割的网络架构决策过程遥感地物识别本质上是语义分割任务也就是给每个像素打上类别标签。因为涉及的类别水体、植被、建筑、道路、裸地等在空间上分布比较不规则传统的滑窗加分类器做法效果太差所以我直接采用了编码器-解码器结构。模型主干的选型我对比过几个方案网络结构优点缺点适合场景U-Net结构简单小样本表现好特征提取能力一般大场景精度受限小范围快速实验DeepLabV3ASPP模块擅长多尺度上下文解码器较浅边界细节有损失大范围地物分类U-NetResNet编码器兼顾深度特征和细节恢复参数量大训练周期长精度要求高的主任务最终我选择了以ResNet50作为编码器、以U-Net风格解码器为主干的组合。原因是遥感地物识别对边缘细节的要求很高单纯的DeepLabV3在边界处理上容易过度平滑而U-Net的跳层连接能把浅层的空间细节跟深层的语义特征拼接起来边界定位精度更好。同时ResNet50作为编码器特征提取能力又比原始U-Net的卷积层强不少。3.2 模型输入维度的设计与损失函数的坑模型输入维度需要特别说明一下。TensorFlow中输入张量形状设计成[B, H, W, C]B是批大小H和W是切片尺寸C是通道数。我用的切片尺寸是256x256通道数是4RGB近红外批大小根据显卡显存调成8或者16。关于损失函数我用的是softmax交叉熵和Dice损失的加权组合。交叉熵负责像素级分类的平稳收敛Dice损失负责解决类别不平衡的问题。在遥感场景中建筑物和道路往往只占整张图的很小比例而植被和裸地占了大头。如果只使用交叉熵模型会被大比例背景类别主导最终得到的mIoU虚高但目标类别的识别精度非常差。加入Dice损失之后小类别的召回明显改善。import tensorflow as tf def combined_loss(y_true, y_pred): ce_loss tf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue)(y_true, y_pred) # 将y_true转为one-hot y_true_onehot tf.one_hot(tf.cast(y_true, tf.int32), depthtf.shape(y_pred)[-1]) y_prob tf.nn.softmax(y_pred, axis-1) # 按样本计算Dice系数 intersection tf.reduce_sum(y_true_onehot * y_prob, axis[1, 2]) union tf.reduce_sum(y_true_onehot, axis[1, 2]) tf.reduce_sum(y_prob, axis[1, 2]) dice_loss 1.0 - tf.reduce_mean((2.0 * intersection 1e-6) / (union 1e-6)) return ce_loss dice_loss一个需要留意的细节Dice损失里的y_prob要从logits先过softmax不能直接用logits算否则数值范围不对。我一开始直接拿logits算Dice结果损失曲线震荡得很厉害模型久久不收敛后来改成softmax输出再算才恢复正常。4. 模型训练与精度评测的关键步骤4.1 训练流程中的数据集划分和超参数设置数据集划分这里我采用了标准的三方划分训练集70%、验证集20%、测试集10%。特别注意的一点是划分单位必须是完整的影像不能是切片。如果先把切片打乱再划分同一个大影像里生成的很多切片会同时出现在训练集和验证集模型相当于变相见过了验证集数据最后得到的验证指标严重虚高。这个坑在学术场景里叫数据泄漏在工程实践里尤其容易踩因为在切片的层面看起来划分是随机的。训练超参数方面我对几个核心参数做了多次实验对比最终的取值仅供参考但思路值得记录优化器AdamW学习率初始为1e-4配合余弦退火策略批大小164通道256x256输入单卡需要约12到14GB显存训练轮数80到120轮第40轮左右开始明显收敛早停策略验证集mIoU连续12轮不提升则停止混合精度开启TensorFlow的mixed_float16训练提速约1.8倍显存占用下降约40%开混合精度要注意一个事情就是BN层的数值稳定性。由于混合精度下部分Tensor以Float16参与计算BN层的统计量容易出现微小的震荡。我的做法是前10轮先用Float32训练等BN统计量稳定后再切到混合精度实测这样能减少后期验证集上的精度抖动。4.2 训练过程的监控与断点续训训练施工过程中我使用了ModelCheckpoint回调保存最优权重并且跟踪验证集的mIoU来更新最优模型。具体实现上每轮训练结束之后我会额外做这样几件事用TensorBoard跟踪训练损失和验证损失的曲线判断是否有过拟合迹象在验证集上随机抽几个批次的预测结果把原图、标签、预测结果拼在一起可视化直观观察边界细节和类别混淆记录mIoU、准确率和各个类别的IoU用于后续判断哪些类别识别效果差。TensorBoard的可视化非常必要。因为光看mIoU数字你可能根本不知道模型是把道路识别成裸地还是把阴影识别成了水体只有可视化结果才能让你快速定位问题出处。断点续训方面我用tf.train.CheckpointManager管理检查点每次保存最近5个epoch的权重。训练到后面部分轮次如果因机器重启中断直接从最近的检查点恢复能避免从头再跑。checkpoint tf.train.Checkpoint(modelmodel, optimizeroptimizer) manager tf.train.CheckpointManager( checkpoint, directorycheckpoints/, max_to_keep5, keep_checkpoint_every_n_hours2 ) # 每个epoch结束后调用 manager.save()4.3 评估指标的解读方式评估阶段我一共计算了三层指标像素准确率Pixel Accuracy所有分类正确的像素数除以总像素数。平均交并比mIoU逐类计算IoU再平均。每类IoU这个指标最能反映模型的短板在哪。打个比方整体像素准确率可能达到92%但其中建筑这一类的IoU只有45%这个场景里像素准确率就是虚高的因为建筑像素占总像素比例太低即使模型完全没认出建筑来也不会对整体准确率产生多大影响。所以核心指标一定参考mIoU和每类IoU而不仅是准确率。模型最终在测试集上的效果大概是这样整体mIoU达到0.71其中植被IoU最高为0.87水体IoU为0.82建筑IoU为0.63道路IoU为0.54。道路这类细长线状地物是几乎不可避免的难点因为切片尺寸有限横向长距离道路在单张切片里可能占据大量像素或者只有一小段容易跟裸地混淆。5. 大影像推理与后处理从切片预测到完整分类图5.1 滑窗推理和重叠区的融合模型训练好之后真正的硬骨头是整幅大影像的推理。一次读入整场景来做预测基本不可行内存占用过大我采用跟训练一致的滑窗策略。推理过程中窗口以步长等于窗口宽度的方式推进相邻窗口之间会重叠一部分。每个像素如果在多个窗口中出现就会有多份预测结果最后的类别选择采用投票策略——选择在预测结果中出现次数最多的那个类别。如果出现平票选概率均值更高的那类。这个投票融合策略相对简单但是实测能明显减少切片边缘处产生的接缝噪点。如果对后处理效果要求更高可以考虑按离中心的距离做高斯加权的融合或者计算每个像素的softmax概率和再取概率最大者效果各有侧重。这里还有一个性能优化的关键点如果一次推理一张256x256的切片整幅大影像会有大量的小批次推理调用GPU利用率很低推理速度会非常慢。我后来把推理批大小调到32甚至64一次把32张切片塞进去单卡推理速度提升了将近5倍。def sliding_window_inference(model, tiff_path, window_size256, stride128, batch_size32): with rasterio.open(tiff_path) as src: width, height src.width, src.height profile src.profile result np.zeros((height, width), dtypenp.uint8) count np.zeros((height, width), dtypenp.uint8) windows [] positions [] for top in range(0, height - window_size 1, stride): for left in range(0, width - window_size 1, stride): windows.append(src.read(window((top, top window_size), (left, left window_size)))) positions.append((top, left)) # 批推理 for idx in range(0, len(windows), batch_size): batch np.stack(windows[idx:idx batch_size]) preds model.predict(batch, verbose0) preds np.argmax(preds, axis-1).astype(np.uint8) for pred, (top, left) in zip(preds, positions[idx:idx batch_size]): result[top:top window_size, left:left window_size] pred count[top:top window_size, left:left window_size] 1 result np.divide(result, count, wherecount 0).astype(np.uint8) return result注意最后一个边界问题大影像的宽高不一定是滑窗尺寸的整数倍如果滑窗没有覆盖到边缘区域就需要对边界做镜像padding否则最终成图会有一段黑边或者噪声区域。5.2 后处理阶段常见的碎斑与椒盐噪声模型的原始输出直接用来做成果图往往是不达标的。遥感影像切片预测之后会出现大量孤立的碎斑、椒盐噪声比如一小块森林里莫名其妙冒出来一个建筑像素或者道路中间出现了几个植被像素。这种结果直接拿去上专题图或者给政企做分析几乎无法用。我采用的后处理流程分四步中值滤波使用cv2.medianBlur滤波核大小设为3或者5能有效消除孤立的单像素噪声。形态学闭运算对建筑和道路这类目标先闭运算再开运算闭运算可以填补内部空洞开运算能分解细小的连接点保证地物边界平滑。最小面积滤波通过skimage.morphology.remove_small_objects把像素数量小于设定阈值的连通域删除阈值大小视场景而定一般取50-200像素。边界平滑如果需要栅格转矢量的流程在转的时候再用简化工具来平滑。最小面积滤波这个步骤尤其重要。我在河道提取的实验里如果不做这个处理成图会有大量零星的河道断裂点整体会被评价为破碎、不可用。加上这一步之后河道连通性明显改善整幅图干净很多。from skimage.morphology import remove_small_objects def postprocess(mask, min_area100): mask mask.copy() # 移除小面积连通域fill_label0表示将小目标置为0 mask remove_small_objects(mask.astype(bool), min_sizemin_area).astype(np.uint8) return mask这一套后处理下来分类成果在视觉上和使用层面都达到了可以直接交付的程度。6. 精度提升之路从0.60到0.71 mIoU的优化复盘训练过程中投入最多时间的就是精度提升阶段。我把从0.60到0.71 mIoU之间验证有效的优化手段按贡献大小梳理出来。6.1 类别不均衡的重新思考最初的模型在植被和水体上表现不错但建筑物和道路严重拉胯。我查了训练集的类别像素占比才意识到植被占据了约45%的像素裸地占30%建筑只占8%道路只占5%左右。我做了两件事修改采样逻辑在生成批次时按照类别权重进行过采样把建筑和道路区域占比较高的小切片在训练中出现的概率提高损失函数调整为交叉熵加Dice加Focal损失三分量混合Focal损失的γ参数设置为2.0进一步压低易分类样本的梯度贡献。这两步操作之后建筑的IoU从0.42提升到了0.55道路从0.35提升到了0.48。6.2 多尺度推理带来的必要收益语义分割模型对目标尺度比较敏感同一个建筑在不同成像条件或者不同分辨率的影像上看起来大小差异很大。我在推理阶段做了多尺度预测融合——将原切片分别缩放为0.75倍、1.0倍和1.25倍三个尺度分别输入模型预测结果统一缩放到原尺寸再对softmax概率求平均。这个操作对整体mIoU提升约1.5个百分点代价就是推理时间增加了将近2倍。如果项目对推理速度有高要求可以只用于精度评估阶段生产环境关闭多尺度融合。6.3 训练数据质量缺陷的追溯有一次模型在某个区域验证效果特别差我检查后发现是因为该区域在影像上被云层覆盖了。遥感图像里云层和云影是个很麻烦的干扰因素云层覆盖的区域下方地物无法获取模型只能靠周围的纹理瞎猜。解决办法是对训练切片做一次视觉筛查凡是切片中云层覆盖面积超过25%的直接丢弃。同时推理阶段如果发现大影像有云层覆盖可以直接在有云区域输出一个“云/未知”类别避免模型给出错误预测。这里让我认识到一个很关键的道理数据层面的问题往往比模型层面的问题影响更大。你的模型再先进数据里全是云、全是坏样本也训练不出好的效果来。7. 训练和推理过程中的几件快稳秘事工程调优跟模型调优同样重要记录几个我实测下来最有价值的优化点。这些细节单独拿出来看都不大但累积起来对整体效率和稳定性影响巨大。7.1 tf.data管道让IO不再成为瓶颈刚开始训练时我的数据管道用的是tf.data.Dataset.from_generator配合自定义的读取函数结果发现GPU利用率经常只有50%出头大量时间被浪费在CPU数据读取和磁盘IO等待上模型的每个epoch耗时特别长。之后换成了tf.data的原生管道方案用map配合tf.numpy_function加载增强并且设置了prefetch和num_parallel_calls参数。改进后GPU利用率稳定在90%以上单个epoch耗时缩短了一倍多。关键配置简要列举dataset tf.data.Dataset.from_tensor_slices((image_paths, mask_paths)) dataset dataset.shuffle(buffer_size1000) dataset dataset.map(load_and_preprocess, num_parallel_callstf.data.AUTOTUNE) dataset dataset.batch(batch_size) dataset dataset.prefetch(buffer_sizetf.data.AUTOTUNE)prefetch(AUTOTUNE)和num_parallel_calls(AUTOTUNE)这两个配置缺一不可很多写的训练代码慢就是因为少了这两行。7.2 多块GPU和单卡精度的取舍我用的是单卡训练。中途也尝试过tf.distribute.MirroredStrategy跑到两张卡上但有个现象值得注意批大小翻倍后相同的epoch下单卡精度反而比双卡略高一点。原因是同步训练时每step处理的样本数翻倍等效于学习率没有做相应调整导致收敛轨迹变化。如果要在多卡上开启同步训练记得把学习率按卡数同比放大并且适当增加总训练轮数。否则宁可单卡慢慢训最终的精度风险更可控。7.3 模型导出时最容易忽略的预处理一致性模型训练结束不是终点部署才是。我在导出推理模型的时候踩过一个非常无语的坑训练阶段输入模型的数值范围是0到1归一化后的但部署推理脚本直接读取原始像素值0到255送进了模型最终所有预测结果都是错的。排查了很久才发现问题出在输入预处理不一致。痛苦的教训是训练和推理必须使用同一套预处理流程解决办法也简单我把归一化、波段组合这些逻辑封装成了一个独立的preprocess.py模块训练和推理脚本统一从这个模块导入从源头杜绝流程分叉。还有模型导出的格式TensorFlow的SavedModel格式是首选既能保留完整的计算图又能配合后续的TensorFlow Serving做线上部署。model.save(exported_model, save_formattf)8. 评价与投入使用这套系统的现实边界在哪里项目做完之后我对这套系统的能力边界和适用条件有了一个更清醒的认识。比较适合的场景是大范围、中分辨率的地表覆盖分类比如全国或者省级的土地利用变化监测。整景影像输入出完整分类图这类需求非常适合这套系统的流水线。不太适合的场景包括细碎地物提取比如单棵树木的识别。这类需求还是得用更高分辨率的影像加实例分割模型显然不是语义分割能搞定的。地物边界精度要求极高的矢量制图。语义分割输出的栅格边界往往带有锯齿状即使应用了后处理距离测绘级的精度标准还有明显差距。实时响应场景。整景大影像的滑窗推理耗时跟影像尺寸正相关即便批大小调到最大一张6000乘6000的影像也要跑上几十秒甚至数分钟想要秒级响应不现实。跨传感器迁移。用A卫星数据训练出来的模型直接拿到B卫星数据上预测精度往往会掉一大截。不同传感器的波段设置、空间分辨率、辐射特性差异很大。如果传感器变了至少要重新做归一化和微调训练不能指望零成本迁移。我在项目中还发现的一个局限性是季节漂移问题。同一个地区夏季影像和冬季影像之间的地表特征差异巨大尤其是植被覆盖范围、水体面积这些随时间变化的因素。仅仅跨越季节预测模型的表现下降得比跨传感器还要明显。解决思路倒也清晰训练数据按季节分组建模或者按季节分别训练专用模型。这在后面的演进版本里我会优先纳入。写在最后的体会这套卫星遥感图像识别系统做到最后让我印象最深的不是模型结构有多复杂而是整个链路里数据工程和数据处理所占的精力远超想象。数据标注、切片、增强、归一化每一环出问题最后都会体现在精度和稳定性上。模型本身反而只是其中一个环节调优空间相对有限。回头看整条技术路线TensorFlow在数据管道和模型导出的闭环能力确实让整个工程落地顺畅了很多tf.data、CheckpointManager、SavedModel这些原生工具串联起来的开发体验相当顺手。希望这篇拆解能让正在做遥感图像识别或者准备入坑这个方向的朋友少走几步弯路。后面我还会继续迭代这个系统重点要解决季节漂移和跨传感器泛化的问题到时候再来分享。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑