
简介一份面向计算机、人工智能、遥感等专业学生及从业者的高分辨率城市遥感图像水体提取Python源码项目基于深度学习语义分割模型包括标准U-Net与引入注意力机制的AttU-Net实现完整流程旨在解决复杂城区背景下的水体自动识别问题。代码覆盖图像增强与预处理、数据集构建、网络定义、训练求解、评估验证与单张预测等关键模块并配有项目开发文档与预训练权重既可直接运行复现也可针对新数据微调使用适合作为毕业设计、课程设计或深度学习进阶练手项目。资源压缩包共27个文件包含11个Python脚本、13张网络结构与效果对比图、1个CSV评估结果、1个Markdown说明及1个pth权重文件总大小仅约726KB轻量且目录层次清楚便于快速定位相关代码。目前已有493人学习下载对于希望从代码层面理解遥感语义分割落地细节的同学是一份值得参考的实战样例。1. 城市遥感水体提取这份源码能让你少走三个月弯路做城市内涝监测、蓝绿空间规划这类课题的人大概率被同一件事卡过拿到了高分辨率城市遥感图像想用深度学习把水体提出来结果自己从零搭一套 U-Net光是数据处理、训练调参、后处理去噪就能磨掉两三个月。这份基于深度学习实现的高分辨率城市遥感图像的水体提取 Python 源码把网络结构、数据预处理、训练闭环、评估推理全部打包好了——它同时实现了 U-Net 和 Attention U-Net 两条技术路线配好了 Urben_pre 城市遥感数据集训练好的权重也直接放在 models 目录下。适合正在做毕业设计、课程设计的计算机和遥感相关专业学生也适合想快速拿一套基准结果做对照的入门研究者。你不需要补全任何一行主流程代码按下面的思路走一遍就能复现出实验数字。2. 网络选型U-Net 和 Attention U-Net 在遥感水体提取中的设计取舍2.1 编码器-解码器结构跳跃连接为什么对细碎水体重要城市遥感影像里的水体不是一整片干干净净的块它包含主干河道、细窄的支流、人工湖、屋顶蓄水设施尺度跨度很大。U-Net 的核心是“编码器逐层下采样提语义 解码器逐层上采样恢复分辨率”中间的跳跃连接把每一层编码器的特征图拼到对应解码器层上让浅层的边缘纹理信息和深层的语义类别信息互相补充。高分辨率的边缘细节对水体这种边界模糊、又容易被建筑物阴影干扰的目标来说是保住 IoU 的关键。在这个源码包的 network.py 里编码器采用典型的多级卷积 最大池化结构底层通道数逐级翻倍。常见的 U-Net 实现会从 64 个通道起步逐层变成 128、256、512池化用 2×2 步长为 2 的最大池化。解码器恢复分辨率时先用上采样将特征图尺寸翻倍再把对应编码器层的特征在通道维度拼接。这个设计直接影响的是小目标召回率——拼接让解码器同时拿到高层语义和低层细节不然细支流很容易被池化和上采样吃掉。# network.py 中 U-Net 核心模块的常见写法 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, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x)这段代码里两个连续卷积组成了“双卷积”结构前面一层负责提取局部纹理后面一层做非线性组合。BatchNorm 放在卷积之后、激活之前目的是稳定训练时的数据分布同时能让学习率适当调大。padding1 保持特征图尺寸不变这样后续的跳跃连接不需要额外处理尺寸对齐问题。我一般建议不要动这个模块的基础结构真正影响水体提取效果的是下面要说到的注意力机制和损失函数搭配。2.2 Attention U-Net注意力门控如何压阴影和建筑干扰高分辨率城市影像里最头疼的不是“水没看出来”而是“非水被误判成水”。建筑物阴影在可见光波段和深色水体极其相似柏油路面在雨后也常被模型当成水体。这份资源里同时提供了 Attention U-Net 结构它在 U-Net 的跳跃连接前加了一个注意力门控Attention Gate让模型在融合特征时自动抑制与水体无关的区域响应。注意力门控的做法是把编码器当前层的特征图和解码器深层传上来的门控信号分别做 1×1 卷积相加后经过 ReLU 再做一次 1×1 卷积和 Sigmoid生成一个 0 到 1 的注意力权重图。特征图在拼接进解码器之前先乘上这个权重图等于告诉网络“阴影区域先别急着恢复细节重点看那些更像水的区域”。实际训练时我观察到的结果是加了注意力门控后预测图里的阴影误检块明显变少尤其是在建筑物密集的城区效果比单纯加大训练数据更直接。# Attention Gate 的简化实现思路 class AttentionGate(nn.Module): def __init__(self, in_ch, gate_ch): super().__init__() self.conv_x nn.Conv2d(in_ch, gate_ch, 1) self.conv_g nn.Conv2d(gate_ch, gate_ch, 1) self.relu nn.ReLU(inplaceTrue) self.psi nn.Conv2d(gate_ch, 1, 1) def forward(self, x, gate): # x: 编码器低层特征, gate: 解码器上采样后的高层语义 theta_x self.conv_x(x) phi_g self.conv_g(gate) f self.relu(theta_x phi_g) attn torch.sigmoid(self.psi(f)) return x * attn这段代码里 gate 信号来自解码器深层的上采样结果它带有更强的语义类别信息能判断当前位置大概率属于水体还是背景。theta_x 和 phi_g 相加时要求两者的空间尺寸一致所以在实际工程里通常先对 gate 做上采样或对 x 做下采样对齐。最后一层 psi 输出单通道注意力图Sigmoid 保证权重在区间 [0,1] 内。这里有个经验值attention 里 gate_ch 一般取编码器通道数的一半太大容易过拟合太小注意力图会糊成一团。如果训练时发现注意力图输出全是 0.5 附近的均值多半是门控信号没传对检查一下上采样路径的通道对齐。2.3 network.py 里我会重点改的三个位置拿到这份源码不建议一上来就拿去跑先花二十分钟读一下 network.py 的模型定义。我改过几次这类项目最值得动手的三个位置是一是编码器第一层的卷积核数量默认 64 起步输入图像如果是 512×512 以上可以减到 32 或保持 64参数量差异明显但精度差别不大二是最后一层输出的激活函数水体提取是二分类问题水/非水输出层应该用 Sigmoid 而不是默认的线性输出否则训练出来的预测值不落在 [0,1] 区间里后处理阈值没法定三是配合损失函数的选择如果只用了普通的 BCE Loss在高分辨率影像中背景像素远多于水体像素模型会倾向全部预测成背景这时候需要在 network 前向输出后面接一个 Dice Loss 或者带权重的 BCE。这三个位置前两个直接关系能否正常训练最后一个关系边界精度。3. 数据与预处理Urben_pre 数据集从原始影像到训练样本3.1 数据集目录结构和标注格式这个资源的数据组织方式很清晰Urben_pre 主目录下分 train、annotation、valid、test 四个子目录。train 放原始城市遥感影像annotation 放与 train 对应的标注图valid 和 test 分别是验证集和测试集。标注图我这边看文件命名规则和常见的遥感语义分割数据集一样标注图里水体区域像素值为 255白色背景为 0黑色灰度模式单通道。训练前最先要做的一件事是把 train 和 annotation 里的文件按文件名逐一配对确认有多少张图是“有成对标注”的哪些只有影像没有标注——多出来的影像既不能进训练集也不要进测试集只做无监督增强素材。# 检查 train 和 annotation 是否一一对应 import os train_dir Urben_pre/train ann_dir Urben_pre/annotation train_files sorted(os.listdir(train_dir)) ann_files sorted(os.listdir(ann_dir)) train_names [f.split(.)[0] for f in train_files] ann_names [f.split(.)[0] for f in ann_files] paired set(train_names) set(ann_names) print(train 数量:, len(train_names)) print(annotation 数量:, len(ann_names)) print(能配成对的数量:, len(paired))这段脚本的作用是快速核对数据配对情况。现实中这种资源最容易出的幺蛾子就是文件名带了多余后缀比如 train 叫 IMG_001.tifannotation 叫 IMG_001_mask.tif直接按完整文件名匹配会漏掉一大半。我一般先把后缀统一去掉再用集合求交集找出那些对不上的文件单独打印出来看一眼再决定怎么处理避免数据集里混进脏数据让训练损失曲线飘忽不定。3.2 enhance_image.py高分辨率影像增强与归一化城市遥感影像直接喂网络效果很差原因一是原始影像可能是 16 位深度像素值范围到几千甚至上万而深度学习模型期望输入在 [0,1] 或 [-1,1] 区间二是城市地物的反射差异大水体在暗部、建筑物在亮部不做对比度增强的话模型只能学到“亮的地方是非水”水体几乎全被漏掉。这个源码包里带了一个 enhance_image.py里面的常见做法是先做线性拉伸把像素值压到 [0,255]再叠加 CLAHE限制对比度自适应直方图均衡化来增强局部对比度。CLAHE 比普通直方图均衡好的地方在于它限制对比度放大幅度不会把城市影像里本来就接近的色调拉到失真。我在自己的项目里也是这么处理的城市影像的高光建筑区和阴影水道在同一张图里亮度差异能到几十倍全局均衡化会把暗部的水体提亮过头反而更分不清。下面的代码逻辑和 enhance_image.py 的处理流程一致# enhance_image.py 中常用的增强与归一化流程 import cv2 import numpy as np def enhance_and_norm(img_path, clip_limit2.0, tile_grid(8, 8)): img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if img.dtype np.uint16: img cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX) img img.astype(np.uint8) # 转 LAB 空间只对亮度通道做 CLAHE lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimitclip_limit, tileGridSizetile_grid) l_new clahe.apply(l) lab_new cv2.merge([l_new, a, b]) img_aug cv2.cvtColor(lab_new, cv2.COLOR_LAB2BGR) return img_aug.astype(np.float32) / 255.0增强完成后直接除以 255 归一化到 [0,1]这样模型输入的数值范围是稳定的。clip_limit 控制局部的对比度上限取值太小增强效果不明显太大会出现光晕伪影水体边界会被一层亮边污染。城市影像我一般用 2.0 到 3.0 之间。tile_grid 是局部窗口划分8×8 是稳妥的默认值图像尺寸小可以降到 4×4。注意增强完要检查一下水体边界是否出现过曝——如果出现过曝把 clip_limit 调小再跑一遍这个参数需要按你自己的数据集微调没有固定最优值。3.3 dataset.py 和 data_loader.py样本配对、尺寸裁剪与批处理dataset.py 负责定义“一条训练样本长什么样”data_loader.py 负责批量加载和并行取数。对于遥感影像这类大尺寸图像常见做法是训练前先做随机裁剪把整幅大图切成比如 256×256 或 512×512 的 patch再按 patch 喂给网络。这样做的原因很直接显存有限原始影像可能是 4000×4000 的量级整图直接 forward 必然爆显存另外固定尺寸的 patch 才能组成 batch 训练。这个资源里 dataset.py 的典型流程是读影像和对应标注 → 随机或滑窗裁剪 → 数据增强翻转、旋转→ 转 Tensor 并归一化。# dataset.py 中读取并裁剪训练样本的简化逻辑 import random from torch.utils.data import Dataset class WaterBodyDataset(Dataset): def __init__(self, img_dir, ann_dir, patch_size256): self.img_paths, self.ann_paths self._load_paired(img_dir, ann_dir) self.patch_size patch_size def __getitem__(self, idx): img cv2.imread(self.img_paths[idx]) ann cv2.imread(self.ann_paths[idx], cv2.IMREAD_GRAYSCALE) h, w img.shape[:2] # 随机裁剪 y random.randint(0, h - self.patch_size) x random.randint(0, w - self.patch_size) img_patch img[y:yself.patch_size, x:xself.patch_size] ann_patch ann[y:yself.patch_size, x:xself.patch_size] # 统一增强 if random.random() 0.5: img_patch cv2.flip(img_patch, 1) ann_patch cv2.flip(ann_patch, 1) img_tensor torch.from_numpy(img_patch.transpose(2,0,1)).float() / 255.0 ann_tensor torch.from_numpy(ann_patch).long() / 255.0 return img_tensor, ann_tensor随机裁剪的实现注意两个细节一是裁剪起点的上限必须用图像高度减去 patch_size防止越界如果图像尺寸小于 patch_size要先做 padding 或 resize二是标注图和影像图要用完全相同的裁剪坐标和翻转方式这个配对一旦错位训练出来的模型边界会错乱——这类问题在训练集里基本看不出来到测试集上边界会差出好几个像素。data_loader.py 里的 batch_size 和 num_workers 是另外两个关键参数workers 数量取决于机器 CPU 核数和是否有合适的存储设备Windows 下 worker 数量过大会报 DataLoader worker died 的错误可以先设成 0 确认流程跑通再加。4. 训练与排查配置好 solver 参数踩坑记录一口气看完4.1 main.py 与 solver.py 的工作流main.py 是整个项目的入口它把网络、数据加载、优化器、训练循环串成一条流水线。solver.py 承担训练细节前向计算损失、反向传播、梯度更新、学习率调度、定期保存模型。从文件组织能看出这个项目的设计思路——main.py 只做组装不写具体训练逻辑训练策略都在 solver 里。这种分层方式对毕设答辩很友好介绍结构时可以说“模型定义与训练策略解耦”代码也容易改想换优化器只用改 solver.py 里的 optimizer 定义不用动主流程。我见过很多同学在训练时直接把整个模型和训练循环写在一个文件里到后期想加验证、换损失函数、做学习率衰减每次都要动主流程很容易把能跑的代码改坏。这个项目把训练逻辑拆到 solver 里是值得借鉴的。solver 里有一个值得注意的设计训练到中间轮次会定期保存模型权重这就是 models/train.pth 的来源。加载这个权重继续训练或者直接推理都行属于“后悔药”机制——训练崩了也不用从头再来。4.2 训练超参配置参考solver.py 里能调的参数主要集中在这几个初始学习率、batch_size、训练轮数、优化器类型、损失函数权重。适合遥感水体提取的配置区间如下参数推荐取值说明输入尺寸256×256 或 512×512尺寸越大保留水体细节越多显存占用成倍增加batch_size816512 尺寸取 48按显存容量往下调不要硬撑否则训练中断初始学习率1e-4 3e-4Adam 用 1e-4 起步SGD 需要配合动量优化器Adam 或 SGDMomentumAdam 收敛快SGD 最终精度略高但调参更麻烦训练轮数80120水体提取不需要太多轮超过 150 轮容易过拟合损失函数BCE Dice 组合单独 BCE 在类别不平衡下效果差学习率衰减StepLR 或 CosineAnnealing建议用余弦退火后期 refine 边界效果好训练时每轮结束在 valid 集上算一次 mIoU观察验证集指标而不是只看训练损失。训练损失持续下降到接近 0但验证集 mIoU 不涨就是过拟合的开始这时候应该停止训练而不是继续调低学习率。4.3 六个高频翻车与排查记录我在拆这类项目时踩过的坑以及和身边同学交流时高频遇到问题记录如下。现象一训练损失第一轮就是 Nan。 原因输入影像里有纯黑或纯白区域除以 0或者标注图里出现非 0/255 的中间值归一化后超出网络预期范围。 解决检查数据预处理后的张量 min/max把标注图二值化处理确保像素值严格为 0 和 1 两类。现象二损失一直在 0.6 附近降不下去。 原因类别严重不平衡背景像素占到 90% 以上模型全部预测为背景就能维持低损失。 解决换用 Dice Loss 或给 BCE 加权重按“负样本权重 正样本数量 / 总像素数”计算水体区域权重调到 510 倍。现象三验证集 mIoU 很高但可视化预测图里水体边界呈锯齿状。 原因模型本身没问题是推理时没有做后处理直接用原始概率图二值化导致边界毛糙另外标注图边界本身也可能带噪声。 解决在预测概率图上做一次高斯平滑或形态学开运算把离散的毛刺去掉再按 0.5 阈值二值化。现象四推理时单张图能跑批量测试时显存溢出。 原因批量测试时按整图尺寸喂网络没有裁剪显存峰值翻倍。 解决batch_size 调成 1并把输入缩放到训练时一致的 512×512或者实现滑窗拼接后返回原图尺寸。现象五加了 Attention 模块后训练速度明显变慢但精度没提升。 原因注意力门控里的 1×1 卷积增加了参数量而且在早期训练阶段注意力权重还没学到有效信息gate 输出接近均匀分布相当于白算。 解决先把 Attention U-Net 放到一边用普通 U-Net 训练一遍拿到基准分再用 Attention U-Net 跑对比实验如果注意力权重可视化后确实能聚焦到水体区域再保留。现象六换了自己的数据集后完全不能训练。 原因自己的影像和 Urben_pre 的波段数、尺寸、标注格式不一致比如多光谱图像通道数超过 3或者标注图是 RGB 格式而不是单通道灰度。 解决先写脚本统一格式把多光谱选 3 个波段合成 RGB标注图转成单通道灰度再做增强和裁剪流程和第三章完全一致。5. 推理评估与一个让结果更干净的技巧5.1 test_one_data.py 和 test_data.py 怎么用这两个文件分别对应单张推理和批量推理。test_one_data.py 适合测试单张图的效果阅读它的代码能直观理解“训练好的权重 → 预处理 → 模型 forward → 概率图 → 二值化”的完整链路。test_data.py 则对 test 目录批量推理并把结果保存下来最终汇总到 result 目录。第一次跑建议先用单张图验证权重能正常加载、输出尺寸和输入一致再批量运行避免批量跑了一半才发现模型输出尺寸不对浪费大量时间。5.2 evaluation.pymIoU、F1 与 res.csv 怎么读evaluation.py 计算预测结果和标注图之间的指标核心是混淆矩阵里的 TP、FP、FN 统计。mIoU 的计算方式是交集面积除以并集面积公式为 TP / (TP FP FN)它同时惩罚漏检和误检是遥感语义分割最常用的指标。F1 分数是精确率和召回率的调和平均对水体这种“小目标但重要”的类别比单独看准确率更有参考价值。res.csv 里每一行是一条测试样本的指标明细注意看每张图的 IoU 分布而不是只看平均——如果某几张图的 IoU 远低于平均值大概率是那几张图里有大面积阴影或建筑干扰模型泛化能力在这里暴露问题。5.3 后处理技巧形态学开运算去掉碎块水体提取结果最常见的瑕疵是预测图里出现零散的孤立小斑块——一个 3×3 的白色像素点被预测成水视觉上极其刺眼。这时用形态学开运算处理一次就够了先腐蚀再膨胀把小于结构元素大小的白色区域直接抹掉同时保住大面积水体的真实边界。# 后处理去掉预测图中小于阈值的碎块 import cv2 import numpy as np def postprocess(pred_prob, kernel_size5, min_area100): # pred_prob: (H, W) 的 float 概率图 binary (pred_prob 0.5).astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 去除面积小于 min_area 的连通域 num_labels, labels cv2.connectedComponents(opened) for i in range(1, num_labels): if np.sum(labels i) min_area: opened[labels i] 0 return openedkernel_size 选择要克制5×5 是保守值再看效果微调。调太大比如 15×15会把细支流整段抹掉这类河道本身就是几个像素宽开运算反而把真目标删了。连通域面积过滤是一个更安全的补充手段只删掉面积小于阈值的孤立点不动大水体。这套流程我反复用了好几轮最大的教训是任何一次预处理改动都要先可视化确认“影像和标注对齐了”再开训练。那次我改了数据增强后没有重新画叠加图检查结果翻转逻辑里标注没跟着翻整整两天都在训一个错位的数据集损失曲线看着正常验证集效果就是上不去。从那以后我每次改完数据代码都强制跑一遍样例可视化核对图片和标注叠加在一起看一眼才继续这个习惯帮我省了不知道多少返工时间希望帮到你。本文还有配套的精品资源点击获取