简介面向计算机视觉与遥感应用方向的毕业设计、课程设计提供一套基于深度学习的高分辨率城市遥感图像水体提取Python实现覆盖数据预处理、模型构建、训练评估与预测全流程适合在校学生、科研人员及企业开发者直接使用或二次开发。压缩包共27个文件大小约726KB核心为11个Python脚本包含U-Net、AttU-Net等网络定义及训练/测试逻辑另含1个模型权重文件pth可直接加载使用13张PNG图片用于示意网络结构与提取效果还附项目介绍文档md和结果记录表csv。已有493人在CSDN学习下载。项目内代码均经过运行验证目录划分清晰提供了train/valid/test数据集划分、数据增强、评估脚本等模块可帮助快速复现高分辨率城市遥感图像的水体提取实验也便于在此框架上调整网络或数据以完成其他遥感分割任务适用于课程报告、项目初期演示或毕设延伸。1. 城市里的水体提取为什么在高分辨率场景下必须拥抱深度学习过去在遥感影像里提水体最省事的办法是算NDWI归一化差异水体指数。公式很简单逻辑也直观水体在绿光波段反射率高、在近红外波段吸收强一个比值就能把水面从背景里点亮。这种指数法在开阔水域、低分辨率影像上非常稳定直到今天很多论文依旧把它当作基线。但只要影像分辨率上到米级甚至亚米级场景换成城市事情就变了高楼阴影、深色屋顶、沥青路面、停在阴影里的深色车辆它们在波谱特征上和水体高度相似指数法会把它们一起当成水。我曾经用NDWI提取一个南方城市的建成区水网结果把一段高架桥的阴影直接提取成一条上百米宽的“河流”面积误差比漏检还要吓人。所以基于深度学习实现的高分辨率城市遥感图像水体提取核心并不是要把算法做得多花哨而是要解决一个实际矛盾在高分辨率城市影像里水面像素和阴影像素太像了需要用上下文语义而不是单像素波谱来判定。一套完整的Python源码工程通常包含三个环节——样本数据准备、分割模型训练、大图推理成图。适合做内涝风险普查、河道岸线监测、生态空间评估的测绘与遥感工程师也适合刚入门深度学习、想找一份能跑通全流程的源码来复现的算法新人。下面按我自己跑通这条路线的顺序写数据怎么凑、网络怎么选、训练怎么设、大图推理怎么切以及最后有哪些坑会让结果直接翻车。2. 高分辨率城市水体样本构造从一张原始大图到成对训练切片2.1 影像源怎么选没有正式的高分数据时怎么先跑通写这种方案时我被问得最多的问题是“数据从哪来”。高分辨率城市影像这个说法在实际项目里对应的通常是0.5米到2米空间分辨率的影像例如高分二号、WorldView、资源三号这类商业或半官方数据源。这类数据大多需要采购不是随便就能下载的。所以我通常建议分两步走先用能拿到的公开影像把整个流程跑通确认模型结构和基础代码没有低级错误再用正式项目数据做微调。第一步能利用的公开影像有两个方向。其一是从谷歌地球、天地图这类在线地图服务导出影像切片导出时固定缩放级别、坐标系和投影避免不同时间、不同来源的影像叠在一起后出现几像素的错位。这类影像肉眼能看清桥、船和台阶用来做样本标注完全足够但要留意许可约束在线浏览没问题直接拿去商业化发布有合规风险做算法验证通常是可以接受的。其二是用Sentinel-2这类免费多光谱影像它的空间分辨率是10米虽然算不上真正的高分辨率但胜在带有近红外波段而水体在近红外的强吸收特性是深度学习网络容易学到的强特征。常见的做法是先在Sentinel-2数据上预训练一个分割模型再在高分影像上用小规模高质量标注做微调。就算只有两三百张256像素的高分切片效果也比直接在小样本上从零训练稳定很多。提示导出在线地图切片时要记录影像对应的投影与坐标系后面如果要把预测结果输出成地理参考的GeoTIFF投影信息必须跟着切片一起保存否则推理结果是没法落回地图坐标的。2.2 切片与平衡采样把上万像素的大图切成能进模型的批量小块一幅高分辨率城市影像动辄上万乘上万的像素直接整幅输入网络既不现实也不合理第一步一定是裁切。我常用的切片尺寸是256×256和512×512两种。256的小切片对细窄河道更友好一个切片里包含的水体斑块更小边界细节保留得更好缺点是样本数量大、训练时间成倍增加512的切片上下文信息更足模型判断“这是阴影还是水”的时候能看得更远但对显存的要求也更高。二者没有绝对优劣更常见的策略是训练用256推理时用512因为推理不涉及反向传播显存压力要小得多。下面是切片脚本的核心部分输入是影像和对应掩膜输出一对对的npy文件import numpy as np from osgeo import gdal TILE_SIZE 256 # 切片边长常用256或512 STRIDE 256 # 步长切片边长时无重叠设成192可获得重叠切片 OUT_DIR ./tiles def split_tiles(image_path, label_path): img_ds gdal.Open(image_path) img img_ds.ReadAsArray() # (bands, h, w)多波段顺序为R,G,B,NIR... lab gdal.Open(label_path).ReadAsArray() # (h, w)单波段二值掩膜 h, w img.shape[1], img.shape[2] for i in range(0, h - TILE_SIZE 1, STRIDE): for j in range(0, w - TILE_SIZE 1, STRIDE): tile_img img[:, i:iTILE_SIZE, j:jTILE_SIZE] tile_lab lab[i:iTILE_SIZE, j:jTILE_SIZE] if tile_lab.sum() 0 and np.random.random() 0.5: continue np.save(f{OUT_DIR}/img_{i}_{j}.npy, tile_img.astype(np.uint16)) np.save(f{OUT_DIR}/lab_{i}_{j}.npy, tile_lab.astype(np.uint8))逻辑不复杂两层循环按步长滑动取出影像块和标注块。两个参数值得说明。STRIDE等于TILE_SIZE时切片不重叠训练数据不重复如果想用重叠切片做数据增强把STRIDE改成192样本量会膨胀不少训练时长要心里有数。第二个是tile_lab.sum() 0时随机保留一半这是处理样本不平衡最原始也最有效的手段。城市影像里水体往往只占百分之几大量切片是纯背景如果不做这种过滤负样本占比过高模型很快就学成“永远预测背景”损失下降得漂亮但什么都提不出来。2.3 标注规则桥面、停船和泥岸到底算不算水体这一步最花精力也最容易决定模型上限。水面掩膜的标注质量直接决定深度学习模型能到达的精度上限标注本身不一致会把网络学到的东西带偏。我实际项目里遇到最多的标注纠纷是三个地方。第一个是桥面。城市河道上桥梁密集桥面像素从颜色上看完全不是水但桥下水面常被桥体遮挡。如果标注时只沿目视可见的水面画桥下区域就会变成“缝隙”模型被迫猜测桥下是不是水猜的结果通常很乱。我的规则是只标注桥两侧可见的水面桥体本身既不标为水也不标为背景在样本里用忽略值如255标签避让让损失函数不计算该区域的梯度。第二个是停船和亲水平台。停在岸边的船、伸入水面的栈桥轮廓在0.5米分辨率影像上通常只有几个像素宽标注偏差一两个像素就会成为训练噪音。这类细节我的做法是只有被水体完整环抱的船才按水处理贴岸的船按岸线背景处理保证“水面完全连通”的基本空间直觉不被破坏。第三个是泥岸和浅滩。南方城市河岸常有大片泥滩涨水时被浸泡退水后呈深褐色光谱上和深水区接近。如果标注时把湿泥和水面混在一起模型学到的就是“深色像素就是水”。我最终使用的规则很死板以拍摄时刻的可见水面线为准泥滩、湿土、水生植物群落全部算背景。死板换来的是标注在不同影像、不同日期之间的一致性数据一致性比单张图标注得“很聪明”重要得多。3. 网络选型与训练代码用Python搭一个能跑通的水体分割模型3.1 为什么首选U-Net这一类编码器-解码器结构水体提取是二分类语义分割任务对每个像素判断“是水”还是“不是水”。这类任务最稳的模型族是编码器-解码器结构代表就是U-Net。编码器逐层下采样扩大感受野让模型在高层特征中看到“这一片到底是河道格局还是建筑区布局”解码器通过跳连接把高分辨率细节逐层恢复保住细窄河道和岸线边界的精度。“看得远又能找得细”正是城市水体提取需要的组合。更常见的做法是把U-Net的编码器换成在ImageNet上预训练过的分类骨干比如ResNet34或EfficientNet。这类编码器已经具备了对边缘、角点、纹理的基本抽象在小样本遥感项目里效果比从头训练稳定。我用得比较多的库是segmentation_models_pytorch一行代码就能建起带预训练权重的U-Net比自己写卷积块再手写加载逻辑省事。不过也不必一上来就换DeepLabV3或Transformer类模型。它们在高难度通用分割集上确实更强但在水体这种结构清晰、类别单一的任务上U-Net往往在同样的精度下训练成本和显存占用低很多。就城市水体提取这个具体场景我一般建议从U-Net加ResNet34起步把所有模块跑通了再考虑升级。模型基座显存占用512像素切片训练耗时细长河道边界表现适用判断原始U-Net无预训练低快中等样本量小容易欠拟合U-Net ResNet34中中好训练稳定最常用DeepLabV3中高慢好有余量时再升级3.2 一个能跑的训练循环骨架下面这段是训练环节的骨架重点放在数据读取和模型定义没有把验证循环写全避免篇幅被重复代码占掉import numpy as np import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader import segmentation_models_pytorch as smp class WaterDataset(Dataset): def __init__(self, img_paths, lab_paths): self.imgs img_paths self.labs lab_paths def __getitem__(self, idx): img np.load(self.imgs[idx]).astype(np.float32) # (c, h, w) lab np.load(self.labs[idx]).astype(np.float32) # (h, w) img img / 255.0 # 归一化到 [0,1] if np.random.rand() 0.5: # 随机水平翻转 img img[:, :, ::-1].copy() lab lab[:, ::-1].copy() return torch.from_numpy(img), torch.from_numpy(lab[None, ...]) model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels4, # 若是RGBNIR四波段设4纯RGB则设3 classes1, # 单通道输出接sigmoid表示水体概率 )这段代码包含三个值得留意的细节。第一数据加载里没有写__len__实际使用必须补齐它只是返回样本总数。第二归一化放到__getitem__里而不是加载时一次性处理配合随机翻转才能在每次迭代时都生成不同的数据形态强迫模型学习增强不变性。第三模型输出层只出一个通道后面接sigmoid和最后的损失函数配对使用。如果标记改为classes2再加softmax也能得到相同效果但二分类场景输出单通道在内存和数值稳定性上都更省我用的是前者。3.3 损失函数设置不要只用BCE也不要只用Dice城市水体提取几乎必然面对正负样本严重不平衡。如果只用Binary Cross Entropy模型会被背景像素主导梯度信号基本被背景淹没预测结果往往过度保守漏检严重。如果只用Dice Loss虽然它天然对类别不平衡不敏感但训练初期的梯度波动很大单独使用经常会出现损失抖动、收敛缓慢。我实际项目里用的组合是BCE与Dice以一半对一半混合具体权重按数据集情况微调def bce_dice_loss(pred_logits, target, dice_weight0.5): bce nn.functional.binary_cross_entropy_with_logits(pred_logits, target) probs torch.sigmoid(pred_logits) inter (probs * target).sum(dim(1, 2, 3)) union probs.sum(dim(1, 2, 3)) target.sum(dim(1, 2, 3)) dice 1 - (2.0 * inter 1.0) / (union 1.0) return (1.0 - dice_weight) * bce dice_weight * dice.mean()参数上dice_weight常用的范围是0.3到0.7我一般从0.5开始调。如果发现验证集IoU还行但漏检多适当调高dice_weight如果训练过程损失波动太大就调回0.3附近。另一个常被忽略的参数是batch size我建议在显存允许的范围内尽量设大16或32都行太小的话BN层的统计量不稳定分割模型的最终效果会忽好忽坏。4. 训练后半段要做的事监控指标、保存检查点与大图推理拼接4.1 训练时要看什么损失曲线只是及格线IoU才是最终标准训练水体分割模型时最容易被数字欺骗。BCE损失一路下降看着很舒服但如果只降到0.01附近而验证集IoU只有0.3说明模型学到的是“把大部分像素判为背景”的偷懒解。所以我训练时的习惯是每个epoch结束都在验证集上算一次IoU同时记录验证损失IoU明显更高时保存一个新的检查点覆盖之前的旧权重。这个“只保存最优验证IoU对应权重”的习惯能避免训练后期过拟合导致验证集持续变差时误用了最后的权重做推理。学习率方面我一般用AdamW初始学习率取1e-3配一个Cosine退火或者等间隔折半的调度器让后30%的训练阶段把精度慢慢磨上去。水体这种二分类任务的epoch数通常不需要太多几十轮就能看到收敛趋势关键是不要等到损失曲线完全走平才停。4.2 大图推理重叠滑动窗口与均匀加权拼接训练完成后面对的是整幅大影像推理时必须切成小块送进网络再把输出拼回原始大小。最简单的是无重叠切块推理但块与块之间会留下明显的拼接痕迹河道这种细长目标容易在边界处断掉。我一般让相邻窗口重叠32到64像素然后把多次预测结果逐像素做平均相当于对拼接边缘做了一个软化效果比直接切块稳定得多import numpy as np def sliding_predict(model, full_img, tile512, overlap64): model.eval() h, w full_img.shape[:2] prob np.zeros((h, w), dtypenp.float32) # 累计概率 weight np.zeros((h, w), dtypenp.float32) # 每个位置的累计次数 step tile - overlap for i in range(0, h - tile 1, step): for j in range(0, w - tile 1, step): patch full_img[i:itile, j:jtile] with torch.no_grad(): p torch.sigmoid(model(torch.from_numpy(patch).unsqueeze(0))).squeeze().numpy() prob[i:itile, j:jtile] p weight[i:itile, j:jtile] 1.0 result prob / np.maximum(weight, 1.0) return result这段代码的作用是逐窗口推理并对重复覆盖区域取平均。step tile - overlap决定了相邻窗口的重叠量overlap取32到64比较合理重叠太小起不到平滑作用重叠太大推理时间成倍增加。weight这个累计矩阵保证了每个像素除以出现次数后落在0到1区间整幅图输出的概率分布连续。还可以进一步升级把重叠区改成离中心越近权重越高的高斯加权方式能进一步消除接缝但对多数城市水体项目来说均匀平均已经足够。4.3 后处理二值化、去噪和形态学修整推理得到的是每个像素属于水体的概率图要变成最终可交付的矢量底图还需要一步巩固处理。from scipy import ndimage mask prob 0.5 # 概率阈值可在0.4~0.6之间微调 mask ndimage.binary_opening(mask, structurenp.ones((3, 3))) # 去掉椒盐噪声点 mask ndimage.binary_closing(mask, structurenp.ones((5, 5))) # 填补水体内部细碎空洞这里有两个参数值得解释。阈值0.5是默认值实际项目中要把预测结果和专业判读人工勾画比对如果误检多就上调到0.55甚至0.6漏检多就下调。binary_opening是先腐蚀后膨胀用来消除那些零星散落的误检小点binary_closing是先膨胀后腐蚀用来填补水面内部的细小空洞。城市水体通常是小河和湖泊混合形态结构元素用3×3和5×5比较合适再大的卷积核会把细窄河道抹平得不偿失。5. 水体提取模型的避坑与排查五个常见问题的根治办法5.1 训练正常结束推理出来却是一张全黑或全白图现象训练损失正常下降验证集IoU看起来也还行但用到新的大图上输出的概率图整体接近0或接近1看不出任何水体形状。 原因最常见的是数据归一化范围不一致。训练时把每个批量的像素缩放到0到1推理时却用了0到255的原始值模型看到的输入分布完全变了。其次是通道顺序不一致比如训练用的是R、G、B、NIR四通道推理时把波段顺序传成了NIR、R、G、B。 解决在推理代码里严格复用训练时的预处理流程把归一化、通道顺序、dtype都写成同一个函数两处共用。我建议在训练和推理的脚本里都保留一份固定的预处理函数不要分别在两个文件里重写。5.2 阴影和深色屋顶被提取成水体误检率居高不下现象输出结果里高楼阴影、深色屋顶、沥青操场被误认为是水体尤其在建成区中心大片成块地出现。 原因城市阴影在可见光波段确实和水体非常接近模型如果只靠RGB学习容易建立“暗色等于水”的错误映射。标注时如果阴影区域没有被系统性地排除或训练集里阴影像素和水体像素比例失衡问题会更明显。 解决这一步的重点在样本层面。我常用的手段是专门挑一批阴影样本密集的切片把阴影在标注里明确标成背景并在训练集中人工提高这类切片的占比。如果阴影问题仍然顽固可以在输入通道里加入NDWI或近红外波段让模型有额外的波谱信息可以区分二者。后处理阶段还可以用简单的空间规则做一次过滤对每个连通域计算圆形度城市水体通常是细长或面状形态阴影区域往往有非常规整的几何形状不过这条规则要慎用太强的形态过滤会把规则形状的人工湖也抹掉。5.3 训练损失持续下降验证集IoU却始终没有明显增长现象前几个epoch损失降得很快但验证IoU停留在0.3到0.4之间不管怎么调学习率都很难涨上去。 原因我在实际项目中遇到这种情况通常不是模型问题而是标签和影像对不上。比如GeoTIFF影像和标注掩膜不是完全对齐的哪怕只有两三个像素的偏移对细窄河道来说就相当于标签整体错位模型在训练中不断被矛盾样本干扰。 解决先做严格的对齐检查。在切片之前把影像和标注叠加成半透明图层目视检查十来个随机位置重点看河岸线和桥体轮廓是否跟影像重合。如果发现有系统性偏移多半是投影或分辨率不匹配造成的重新配准后再做切片。另一个可能的来源是训练集和验证集切片来自同一幅大图的相邻区域模型实际上是在记忆纹理而不是泛化这时要把不同日期的影像分到不同集合至少保证同一幅大图的切片只能落在训练集或验证集中的一个。5.4 显存不足或训练中断几小时白跑现象训练进行到中途CUDA Out of Memory直接崩掉或者因为主机重启、GPU被占用导致训练中断前功尽弃。 原因最直接的原因是batch size和切片尺寸之和超过了显存上限还有一个隐性原因是验证阶段也在完整计算梯度占用了额外显存。 解决复现时建议先设置一个很小的batch size比如2跑通一个batch确认显存占用再逐步加大。代码里对验证阶段用with torch.no_grad():包住推理时也不需要梯度缓存。如果显存还是不够可以把切片降到256或者打开PyTorch的自动混合精度训练半精度下的显存占用能省下将近一半。训练中断的问题我习惯每两个epoch就存一次检查点保存内容包括优化器状态和当前epoch数万一中断从检查点续跑而不是从头再来。这是这条路线里真正的后悔药。5.5 大图拼接后出现条带错位河道断成一段段现象推理结果拼回大图后能看到一条一条的拼接缝河道在缝的位置断开或者出现明显重影。 原因如果推理时使用了无重叠裁剪边界处模型看的信息不完整对细小目标的预测自然不连续。另一方面如果影像本身在切片时就没有保持地理坐标对齐各分块之间本来就存在位移。 解决把推理改成重叠滑动窗口并做均值加权能解决大部分条带问题。如果重叠后仍有重影要检查是不是切片顺序和拼接顺序没有严格对应切片时记录下来的i、j坐标在拼接时有没有原样用回去。一个容易被忽视的细节是GeoTIFF读取时行、列顺序和NumPy数组的i、j方向必须一致我就在这上面翻过车把影像转置后拼接河道湖面全糊了。6. 想再压榨一点精度还有什么值得做的水体提取能做到出图下一步通常还有四件成本不高、收益很明显的事。第一认真看错误样本。把预测结果和真实掩膜叠加输出到一张图上统计误检和漏检的像素分布十个错误样本里至少有两三个是标注本身的问题而不是模型的问题。修正这些标注噪音比换更大模型、调更复杂的超参数都有效。第二给输入通道加一个NDWI或直接用多光谱数据里的近红外波段。很多城市的正式高分数据都有近红外波段加一个通道对深色屋顶和阴影误检的改善立竿见影。如果只有三波段RGB数据也可以顺手在预处理里把NDWI当作第四个通道合成出来虽然不如真实近红外准确但对阴影区域依然有帮助。第三加一点在线的强数据增强。除了随机翻转还可以做随机旋转90度、亮度抖动和对比度扰动。城市水体分割对光照变化相当敏感同一个河段早晚不同时间的影像色调差异很大增强能帮助模型减少对光照的依赖。第四做一次阈值微调。概率阈值不一定要固定在0.5在验证集上从0.35到0.65每隔0.05算一次F1分数选F1最高点作为推理阈值。这个操作几乎不花成本但常常比调整模型结构带来更明显的提升。我自己的习惯是每次项目结束都会把训练参数、数据集版本、阈值选择过程写成一段备注存到工程目录里。下一次跑类似任务时直接复用这些记录就不会再靠玄学调参。这条基于深度学习的城市水体提取路线从数据准备到推理成图你照着走一遍基本能稳定拿到可用的结果剩下的误差都是数据噪音和标注质量决定的。希望帮到你。本文还有配套的精品资源点击获取
