简介基于深度学习实现的高分辨率城市遥感图像水体提取Python源码是一份面向遥感图像分析方向的完整项目适合计算机、人工智能等专业的毕业设计、课程设计或初期立项演示也便于深度学习入门者对照学习。压缩包共27个文件以11个Python脚本为核心涵盖main、evaluation、train、solver等训练评估流程并配有数据预处理、图像增强、工具模块及13张结果示例图同时附带U-Net和Attention U-Net网络定义、训练权重.pth、CSV结果数据与项目说明文档整体仅726KB目录结构清晰易检索。目前已有493人学习/下载项目代码经运行测试可直接复现城市水体提取流程。学习者可借助项目说明与示例图像理解高分辨率遥感图像中水体目标的标注、训练与预测思路若具备一定基础还可将网络与数据处理流程迁移至其他地物提取或语义分割任务是兼顾学习与二次开发的实用源码包。1. 高分辨率城市遥感图像的水体提取为什么传统阈值法在城市里总翻车“基于深度学习实现的高分辨率城市遥感图像的水体提取python源码”要解决的痛点不是“哪里有水”而是“哪些像水但不是水”。0.3米分辨率的城市影像里屋顶阴影、深色沥青、蓝色广场砖的反射特性和水体高度接近传统阈值法一进城区就翻车。深度学习把水体提取变成逐像素分类用标注好的切片训练分割网络训练、验证、推理全程用python串起来。这套源码适合遥感、GIS、测绘方向的学生和工程师也适合刚入门深度学习图像分割、想拿真实遥感数据练手的开发者。它最终回答一个实际问题给你一景城市影像你能不能自动输出一张水陆分离的栅格图精度还能说服业务方。2. 从影像到训练集切片、标注与增强的落地步骤高质量训练集比模型结构更值钱。高分辨率城市遥感影像通常以GeoTIFF或JPEG2000形式存在单景可能覆盖几十平方公里文件体积几个GB不可能整张喂给神经网络。训练集构建要依次解决三个问题切片、标注、增强。2.1 影像切片高分辨率图像不能整张喂给网络深度学习分割模型本质是卷积网络输入张量有固定大小。城市水体普遍面积小比如小区景观湖、河道岔口如果把整景影像缩到几百像素小水体在图上只剩一两个点模型根本学不到边界。所以第一步是把影像和标签按同一窗口切成512×512或1024×1024的切片并设置重叠区域避免水体边界被一刀切断。# 用 rasterio 按滑窗把大幅影像切成训练切片 import os import rasterio from rasterio.windows import Window def slide_crop(src_path, dst_dir, bands[3, 2, 1], win_size512, overlap64): os.makedirs(dst_dir, exist_okTrue) with rasterio.open(src_path) as src: width, height src.width, src.height step win_size - overlap idx 0 for row in range(0, height - win_size 1, step): for col in range(0, width - win_size 1, step): data src.read(bands, windowWindow(col, row, win_size, win_size)) # 转成(height, width, bands)排列方便后续训练读取 arr data.transpose(1, 2, 0) transform src.transform * rasterio.Affine.translation(col, row) out_path os.path.join(dst_dir, ftile_{idx:05d}.tif) with rasterio.open( out_path, w, driverGTiff, heightwin_size, widthwin_size, countlen(bands), dtypearr.dtype, crssrc.crs, transformtransform ) as dst: dst.write(arr.transpose(2, 0, 1)) idx 1这段代码每次从原始影像中读一个window而不是把整张影像load进内存所以几十GB的影像也能在配置一般的机器上切。bands参数决定了训练时的通道顺序很多采购影像的波段顺序不是RGB先确认再切否则后续所有实验都在错位基础上做。transform的计算让每个切片保留真实地理坐标训练完做预测或在GIS里叠图时不会错位。win_size512是显存和水体尺度的折中。若显卡是16GB可以提到1024减少切片数量但对窄河道的边界保持反而不如512。overlap64保证相邻切片至少有64像素重叠推理时用同等重叠能消除拼缝。切完影像后标签mask必须用同一套行列号切不然模型学到的几何位置是错位的。切片尺寸的选择不能只看GPU显存还要看目标水体的尺度。城市河道宽度可能只有10-20像素512×512切片里依然看得清边界如果为了省显存切成256×256模型能感知的下文变小阴影误检率会明显上升。反过来1024×1024切片里水体虽完整但一个batch里的样本数减少训练不稳定。我的习惯是先量目标水体在原始分辨率下的像素宽度窄河道用512大面积湖泊才考虑1024。2.2 水体标注从矢量勾绘到掩膜生成切片决定模型吃什么标注决定模型学什么。遥感水体提取的标签是逐像素掩膜不是框。常见做法是在QGIS或ArcGIS里人工勾绘水体面再把矢量面转成栅格。另一种半自动做法是用近红外波段算NDWI把大于阈值的区域作为初稿再人工修正。这里先说矢量转掩膜这一步因为只要做过一个真实项目这一步就绕不开。# 把矢量shp水体面转成与影像对齐的mask import geopandas as gpd import rasterio from rasterio import features def shp_to_mask(shp_path, ref_raster_path): with rasterio.open(ref_raster_path) as src: shapes gpd.read_file(shp_path) if shapes.crs ! src.crs: shapes shapes.to_crs(src.crs) mask features.rasterize( [(geom, 1) for geom in shapes.geometry], out_shape(src.height, src.width), transformsrc.transform, fill0, dtypeuint8 ) return maskrasterize把面矢量变成指定网格的栅格1代表水体0代表背景。关键点是shp_path和ref_raster_path必须在同一个坐标系最好都转成UTM投影否则transform的含义对不上mask会和影像错位。out_shape要和切片的宽高一致这样后面切片时可以直接按同一窗口切。dtypeuint8足够存二分类标签。如果要做三分类可以水体标1、阴影标2、背景标0训练时再决定是否把1和2合并保留中间类别能单独检查模型有没有把阴影学成水体。fill0意味着所有未标注区域都变成背景漏标一个水体模型就把那个位置当负样本所以标注后的人工检查不能省。标注时最容易被忽略的是“蓝色屋顶”和“游泳馆”这类难负样本。它们在城市里数量不少如果漏标成背景推理时模型大概率把相似屋顶也识别成水体。所以在勾绘水体范围时我一般会单独建一个图层把“像水但不是水”的区域也标记出来作为负样本参与训练这比事后洗数据有效得多。2.3 数据增强城市水体最该做的是亮度扰动和阴影模拟遥感分割的数据增强和自然图像不完全一样。城市影像在不同季节、不同太阳高度角下拍摄同一个湖上午是深蓝、下午是墨绿阴影位置和形状也在变。增强的目标不是让模型记住水的颜色而是学会忽略亮度差异。# 建议用albumentations做在线增强 import albumentations as A train_transform A.Compose([ A.RandomBrightnessContrast(p0.6, brightness_limit0.15, contrast_limit0.1), A.HueSaturationValue(hue_shift_limit5, sat_shift_limit20, val_shift_limit15, p0.3), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.3), A.Rotate(limit15, border_mode0, p0.5), A.RandomScale(scale_limit0.1, interpolation1, p0.3), ])HueSaturationValue对BGR顺序的影像要小心如果输入是RGB要先确认albumentations采用的通道顺序不一致时先转换否则色相增强会把天空变成奇怪的绿色。Rotate的border_mode0表示旋转后空缺区域填0遥感切片没有合理的背景填充用反射填充会在影像边缘引入真实不存在的纹理。brightness_limit0.15是经验值超过0.2会让阴影区和水体的亮度差完全消失模型把阴影当水体几乎是必然结果。RandomScale可以模拟不同分辨率下的尺度变化但scale_limit不要超过0.15超过后切片里的有效内容缩水小水体容易被随机裁剪裁掉。如果数据里窄河道多可以加A.RandomSizedCrop但要确保mask和图像同步裁剪。不要使用CoarseDropout或Cutout这类随机遮挡增强。自然图像分类任务里它能让模型更鲁棒但在像素级分割里随机遮挡会直接抹掉真实水体或背景边界等于给标签制造噪声。这一步是新手最容易翻车的地方。切片数量通常很大不一定要全部进入训练。我习惯切完后统计每个切片的水体像素占比占比为0且占比超过30%的切片直接删掉前者浪费训练时间后者通常是标注错误。纯背景切片不要全扔保留1%-2%作为难负样本能抑制蓝色屋顶、广场砖这类误报。训练时还可以把样本权重放进Dataset水体占比越低的切片权重越高对城市细长水体特别有效。3. 模型选型与源码结构U-Net、DeepLabV3和SegFormer怎么选这类python源码几乎都基于PyTorch模型结构放在models或net目录下训练入口通常是train.py配置入口通常是config.yaml。选模型不是越复杂越好要看显存、数据量和最终分辨率要求。遥感分割源码里U-Net出现频率最高DeepLabV3次之SegFormer是近年比较多的选项。3.1 源码里最常见的模型配置U-Net是默认起点U-Net是遥感分割源码里最常见的默认模型。结构对称编码器下采样提取语义解码器上采样恢复分辨率中间用skip connection把浅层位置信息直接传给深层。水体边界清晰但形状不规则skip connection能保边界所以很多源码默认模型就是U-Net。# 简化版U-Net核心结构完整实现还要加上下采样和跳连 import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class SimpleUNet(nn.Module): def __init__(self, in_channels3, num_classes1): super().__init__() self.enc1 DoubleConv(in_channels, 32) self.enc2 DoubleConv(32, 64) self.dec1 DoubleConv(64, 32) self.out nn.Conv2d(32, num_classes, 1) def forward(self, x): x self.enc1(x) x self.enc2(x) x self.dec1(x) return self.out(x)这个SimpleUNet为了演示去掉了pooling和skip connection真正的U-Net要在每次下采样后保存feature map再在上采样时拼接回来。看源码时要理解U-Net参数量集中在通道数上32-64-128-256这种递进是常规做法。in_channels3对应RGB影像如果数据源是多光谱改成4或8。num_classes1表示二分类掩膜输出模型最后没有加sigmoid损失函数要用带logits的版本。BatchNorm2d在batch_size很小时会不稳定如果显存只够跑batch_size2可以把BN换成分组归一化GroupNorm这个细节在第5章还会展开。3.2 DeepLabV3的ASPP对城市细节的意义DeepLabV3在遥感水体提取里也很常见核心是ASPP模块用多个不同空洞率的空洞卷积并行提取特征。空洞卷积相当于在卷积核里“打洞”让感受野变大且不增加参数量。对城市影像来说如果一个切片里只有一小块水面周围是密集建筑和道路ASPP能捕捉到“水体周围是什么地物”这个上下文降低把深色屋顶误判为水的概率。# ASPP用不同空洞率并行卷积后融合 import torch.nn as nn import torch.nn.functional as F class ASPP(nn.Module): def __init__(self, in_ch, out_ch256, rates(6, 12, 18)): super().__init__() self.branches nn.ModuleList() self.branches.append(nn.Sequential( nn.Conv2d(in_ch, out_ch, 1), nn.BatchNorm2d(out_ch), nn.ReLU() )) for r in rates: self.branches.append(nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, paddingr, dilationr), nn.BatchNorm2d(out_ch), nn.ReLU() )) self.fuse nn.Conv2d(out_ch * (len(rates) 1), out_ch, 1) def forward(self, x): feats [branch(x) for branch in self.branches] x F.interpolate(self.fuse(torch.cat(feats, dim1)), sizex.shape[2:], modebilinear, align_cornersFalse) return xASPP每个分支的感受野不同rate6关注近处小目标rate18关注远处上下文。对水体提取来说洞率太大时感受野覆盖整张512×512切片反而把邻近水体的阴影也当成上下文背景所以超大rate不总是好事。rates(6,12,18)是DeepLabV3的常见默认值如果输入是1024×1024可以放大到(12,24,36)如果还是512保持默认即可。源码里一般会把这个rates做成配置项调整它比换模型便宜得多。还有一个值得注意的细节backbone从torchvision加载ImageNet预训练权重时很多人直接照用但遥感影像是俯视图ImageNet是物体视图特征分布差异很大。数据量超过1万张切片时可以尝试关闭预训练从头训数据量小则还是用预训练权重迁移学习更好至少收敛快。3.3 用配置文件切换模型参数表与训练入口我见过的遥感分割源码几乎都遵循“配置外置”的套路。训练代码不写死在文件里用config.yaml或json控制模型类型、backbone、输入尺寸、学习率。这样同一套代码可以复用于不同项目。# config.yaml 常用字段示例 model: name: deeplabv3plus # unet | deeplabv3plus backbone: resnet50 # resnet34 / resnet50 / mobilenetv3 num_classes: 1 train: in_channels: 3 input_size: 512 batch_size: 8 lr: 0.0001 epochs: 80 loss: dice data: train_image_dir: ./data/train/images train_mask_dir: ./data/train/masks val_image_dir: ./data/val/images val_mask_dir: ./data/val/masksmodel.name切换模型结构model.backbone切换编码器。换backbone不是为了追新而是显存不够时从resnet50降到resnet34或直接上mobilenetv3能省近一半显存。lr写成0.0001是Adam/AdamW类优化器的常见起点不要照搬ImageNet训练用的0.01水体数据集通常只有几千到几万张切片学习率过大会震荡。import yaml from models import build_model # 我一般会把构建逻辑放在models/__init__.py with open(config.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) model build_model(cfg) print(model)build_model根据配置文件返回模型实例通常还提供freeze_backboneTrue参数冻结编码器预训练权重时可以减少显存和训练时间。模型选型可以简单分三类模型显存占用短边水体效果复杂上下文适用情况U-Net中好一般首选跑通流程窄河道多DeepLabV3高中好大水块多阴影误检严重SegFormer中中好显存小且想用Transformer结构SegFormer是较新的选择源码里不一定预置需要自己移植不建议一上来就追潮。4. 训练、验证与推理参数设置和结果评估模型选好之后真正决定好用不好用的是训练细节。这一章把损失函数、推理拼接、评估指标三个关键环节说透。4.1 损失函数选型Binary Cross Entropy还是Dice Loss城市水体分类的显著特点是类别不平衡。一个512×512切片里水体可能只占几百像素。如果只用BCE损失模型会把所有像素预测成背景因为这样loss已经很低。Dice Loss直接衡量预测和标签的交叠区域对前景和背景的错分都敏感所以在分割任务里经常被选作主损失。# 常用组合损失BCE Dice兼顾稳定性和类别平衡 import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) pred pred.reshape(pred.size(0), -1) target target.reshape(target.size(0), -1) intersection (pred * target).sum(dim1) union pred.sum(dim1) target.sum(dim1) return 1 - (2 * intersection smooth) / (union smooth) def combined_loss(pred, target): bce F.binary_cross_entropy_with_logits(pred, target) dice dice_loss(pred, target) return bce dicedice_loss里先对模型输出做sigmoid因为训练时模型最后输出的是logits不是概率直接算Dice数值不稳。smooth1.0避免某张切片完全没水体时分母为0也减少梯度抖动。combined_loss把BCE和Dice相加BCE保证梯度稳定Dice让模型把精力放在水体和背景的交叠区域。如果水体占比极低比如小于2%建议把Dice部分的权重调成2.0也就是bce 2 * dice。如果数据集里水体分布均匀保持1:1即可。不建议单独用Dice尤其训练前20轮Dice的梯度变化很剧烈容易把训练带崩。4.2 推理与拼接滑窗越大越好吗训练完成后模型只能吃512或1024的切片但预测目标是整景影像。推理时用滑窗把所有窗口都过一遍再拼回原图大小。拼接方式直接决定最终图斑是否连续。# 带重叠的滑窗推理重叠区做等权平均 import numpy as np import torch def slide_inference(model, image_tensor, win_size512, overlap128): model.eval() _, _, h, w image_tensor.shape step win_size - overlap prob_sum np.zeros((h, w), dtypenp.float32) weight np.zeros((h, w), dtypenp.float32) with torch.no_grad(): for y in range(0, h - win_size 1, step): for x in range(0, w - win_size 1, step): tile image_tensor[:, :, y:ywin_size, x:xwin_size] p torch.sigmoid(model(tile)).squeeze().cpu().numpy() prob_sum[y:ywin_size, x:xwin_size] p weight[y:ywin_size, x:xwin_size] 1 prob_map prob_sum / np.maximum(weight, 1) return prob_map每个窗口预测一个概率图概率图叠加进全图的prob_sumweight记录每个位置被几个窗口覆盖。重叠区的概率被多次平均非重叠区保持单次结果这样能明显减少接缝。overlap128在512窗口下步长约384重叠约25%对水体提取128是稳妥值时间紧可以降到64。滑窗越大不一定越好。过大窗口会稀释小水体细节一个小池塘在1024窗口里的像素占比更低模型输出边界反而模糊。如果显卡允许我通常对城市细长水体用512窗口对大面积水体才用1024。4.3 评估指标IoU、Recall和边缘精度训练日志里的loss不能说明质量最终要以验证集的像素级指标为准。遥感水体提取最常用IoU也就是预测水体与真实水体的交集除以并集。但只看IoU会掩盖问题漏掉一条细窄河道IoU可能只掉0.01业务上却完全不可接受。def compute_metrics(pred_mask, gt_mask, threshold0.5): pred pred_mask threshold gt gt_mask 0 inter (pred gt).sum() union (pred | gt).sum() iou inter / (union 1e-6) recall inter / (gt.sum() 1e-6) # fp_rate 预测为水但不是水的像素 / 预测为水像素 fp_rate (pred ~gt).sum() / (pred.sum() 1e-6) return {iou: iou, recall: recall, fp_rate: fp_rate}threshold0.5是常用二值化阈值但实际项目中我经常在0.3到0.5之间选水体边缘像素的概率普遍不高阈值一高边界就缩一圈。fp_rate反映误报城市里误报多来自阴影和屋顶这个指标比precision更直观。评估时还有几个问题要注意指标计算方式关注点IoU交集 / 并集综合质量Recall真实水体中被预测到的比例漏检多不多False Positive Rate预测水体中错分的比例阴影、屋顶误报多不多Boundary F1边界带内的F1窄河道边界是否锯齿验证集不能和训练集共用切片尤其不能把相邻切片都塞进训练重叠区域会让指标虚高。如果数据是从一景影像里切出来的最好按地理坐标分块左边训练右边验证而不是随机分。这样才能验证模型在新空间位置上的表现。5. 水体提取训练与推理的避坑排查5个高频翻车点这一章按我自己的踩坑经历整理排序大致是出现频率从高到低。5.1 训练Loss不降标签里的阴影和水体混在一起现象训练到第20轮训练loss卡在0.55左右验证IoU只有0.3。把预测图叠加到原始影像上建筑阴影连成片被识别成水真实水面边界反而残缺。原因标注时用了偷懒方法把近红外单波段的低值区域全部当成水体或用的外部标签本来就是粗分辨率反演结果阴影和深色屋顶都被标成了水。模型拟合的是错误标注无论怎么调学习率都救不回来。解决先清洗标签而不是调模型。用NDWI做初筛辅助人工修正。# 用绿波段和近红外波段计算NDWI辅助检查标签 def ndwi_mask(green_band, nir_band, threshold0.15): green green_band.astype(np.float32) nir nir_band.astype(np.float32) ndwi (green - nir) / (green nir 1e-6) return ndwi thresholdNDWI对开阔水体很敏感但对城市阴影同样会误报不能直接拿它当训练标签只能用来筛出“疑似水体”再做人工修正。threshold0.15是经验值不同影像辐射范围差异大先看直方图再定阈值不要照抄。5.2 推理拼接出现接缝滑窗重叠设成了0现象整景影像的预测结果放大会看到一条条横竖线水体边界错开一截明显是切片和图边缘不一致。原因推理滑窗步长等于窗口大小窗口之间没有重叠。模型对切片边界像素的置信度天然偏低每个窗口独立预测后直接拼在一起边界不一致就形成接缝。解决把推理滑窗的overlap设为64或128重叠区用平均概率。更讲究的做法是距离加权越接近窗口中心的像素权重越高越靠边权重越低。# 用三角窗权重加在重叠区上消除接缝 import numpy as np def _window_weights(size): w np.minimum(np.arange(size), size - np.arange(size) - 1) w w.astype(np.float32) / (size // 2) return w[:, None] * w[None, :]三角窗在窗口中心权重为1边界为0。重叠区拼接时边界附近大概率被另一个窗口的中心覆盖视觉上就不会出现明显接缝。这个技巧会牺牲一点边界信息换取成图连续性对最终目视效果很有用。5.3 GPU显存不够batch_size设成1还是OOM现象8GB显存batch_size8直接CUDA out of memory改成2还是爆甚至batch_size1也报错。原因显存占用不只看batch_size还看输入尺寸、backbone、验证阶段是否同时占显存。512×512输入加ResNet50峰值显存就可能超过10GB很多人只调batch_size没注意到验证阶段也在占用显存。解决先把输入降到512×512backbone从resnet50换resnet34或mobilenetv3这一步能省一半显存。如果还不够用梯度累积模拟batch。# 梯度累积batch_size1, accum_steps4 等价于 batch_size4 accum_steps 4 optimizer.zero_grad() for i, (images, masks) in enumerate(train_loader): images, masks images.cuda(), masks.cuda() loss combined_loss(model(images), masks) / accum_steps loss.backward() if (i 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()loss / accum_steps让每个step的梯度缩放到1/4累积4次后更新参数等价于用4个样本的梯度更新一次。注意BatchNorm在batch_size很小时会不稳定解决方式是固定BN的running stats或者改用GroupNorm。梯度累积只解决显存峰值训练时间不会缩短。5.4 蓝色屋顶和泳池被识别成水体现象在新城区验证集里蓝色塑胶屋顶、露天泳池被大块预测成水体阈值提到0.8也压不掉。原因训练集里没有足够的“像水的非水”样本。模型学到的不是“水面”而是“蓝色平滑块状”的表象。城市里蓝色屋顶很多模型一旦见过几个样本就会把它们当成水。解决难负样本挖掘。把训练后在验证集上误报的图斑剪出来作为负样本重新训练。如果源码支持在线难例挖掘直接打开如果没有手动把误报图斑切成切片加入训练集。再加后处理规则用面积阈值过滤小于500平方米的连通域再检查周围地物类型被建筑轮廓包围的大概率是屋顶。这个规则不能生搬硬套小区消防水池面积可能只有几十平方米。提示项目需求里要明确“最小制图单元”是多大。如果用户只关心面积大于1000平方米的水体后处理直接过滤小图斑模型压力也小。5.5 环境依赖冲突python版本太新导致源码跑不起来现象按README装依赖import rasterio时崩溃或torch加载报undefined symbol。python3.11装什么库都报兼容问题。原因源码通常在python3.8或3.9下开发新版python对C扩展库的ABI兼容性更严。另外torch、rasterio、opencv这些库直接pip装可能装到CPU版和旧版混合互相冲突。解决不要用系统python裸跑单独创建虚拟环境用python3.8最稳妥。安装torch时按显卡CUDA版本选择对应命令别让pip默认装CPU版。conda create -n water_extract python3.8 -y conda activate water_extract pip install torch torchvision pip install rasterio opencv-python albumentations pyyaml第一行把python版本固定住后续pip安装都在该环境里。pip install torch默认装最新版源码里依赖的torch API可能已经变了更稳妥的做法是拿到源码后先看requirements.txt有没有锁版本。提示不要在不检查CUDA版本的情况下直接pip install torchnvidia-smi显示的CUDA版本决定该装哪个cu版本。装错会导致模型能加载但训练时疯狂报错。6. 验证与进阶把模型迁移到新城区前先做这三件事从一个城区训好的模型不可能直接迁移到另一个城区还保持同样精度。我现在的习惯是新数据到手先做一次盲测看误报图斑集中在哪种地物上再决定加数据还是改后处理。6.1 先做小范围盲测再谈迁移选一块新城区里完全没参与训练的影像做盲测统计IoU和漏检水体数量。如果IoU比训练城市掉了超过0.15说明影像分布差异过大需要补充新城区切片重训而不是微调最后几层。这一步能帮你判断继续投入的价值。6.2 用颜色归一化消除传感器差异不同卫星和航拍传感器的辐射定标不一样直接迁移时亮度分布可能完全不同。对影像做分波段直方图匹配把目标影像的统计信息对齐到训练影像更简单的是在训练阶段加入RandomBrightnessContrast让模型对亮度不敏感。前者每次换数据都要重跑后者只在训练时生效两种都做更稳。6.3 叠加边缘损失保窄河道城市水体的窄河道和池塘在IoU指标里占比很小但业务上往往是核心。训练时给损失函数加一个边缘项把Canny或Laplacian得到的边缘权重加进去让模型更关注边界附近。推理后对预测图做一次形态学闭运算能填掉河道中间的小裂缝但不要反复开运算否则细河道会被直接抹掉。这套源码能跑通只是开始能不能在下一个城区复用才是它真正的价值。希望帮到你。本文还有配套的精品资源点击获取