简介面向遥感信息处理、计算机视觉方向的本硕学生及毕业设计开发者这份资源提供了一套完整的高分辨率城市遥感图像水体提取系统。项目基于Python深度学习实现内含11个Python脚本覆盖数据加载、模型训练、网络评估和单图预测的完整流程同步配备13张PNG网络结构图、1个PTH预训练权重、1个CSV实验结果和1份Markdown说明文档资源共27个文件压缩包仅726KB结构紧凑、目录清晰便于离线研读与二次开发。已有199人学习下载。代码注释详尽新手也能快速上手内置U-Net与AttU-Net双模型对比及urban水体测试数据集部署后即可运行系统功能完善、操作简洁经过严格调试可直接作为毕业设计、课程设计或期末大作业的完整高分参考方案。1. 高分辨率城市遥感图像的水体提取一个能直接跑的 Python 深度学习毕设把一张 0.5 米分辨率的高分影像丢给传统 NDWI 阈值分割城市里的建筑物阴影会跟水体一起被划进来——这是我在做内涝淹没范围评估时第一次被遥感水体提取教育。后来换成基于 U-Net 和 Attention U-Net 的语义分割方案这套 Python 源代码才真正把 V 型河道、景观水池和立交桥下的窄水体从复杂城市背景里剥离出来。项目带完整数据集、train.pth 预训练权重、数据预处理与增强脚本、训练和评估全流程解压后按 README 走一遍就能出结果。适合正在做毕业设计、课程设计或者想快速搭一套遥感语义分割基线来改的从业者新手上手成本低熟手可以直接替换主干网络或更换数据集。2. 源码结构与模型选型文件拆解与 Attention U-Net 改进逻辑2.1 源码文件清单从入口到训练到推理的调用链拿到压缩包后别急着跑先把文件职责理清。这套项目的核心入口并不复杂所有训练和推理路径都围绕 main.py、solver.py、network.py、dataset.py 四个文件展开其余脚本服务于数据准备和结果评估。文件职责main.py训练与推理的统一入口负责参数解析与流程调度solver.py核心训练逻辑包含 loss 计算、反向传播、模型保存network.pyU-Net 与 AttU-Net 网络结构定义models/权重存放目录训练产物落在这里train.pth预训练权重文件可直接加载推理dataset.py / data_loader.py数据集封装与批次加载data_preprocess.py原始影像滑窗裁剪、归一化、标签生成enhance_image.py数据增强evaluate.py验证集指标计算test_one_data.py单张遥感影像推理test_data.py多文件批量测试result/ 与 res.csv预测结果与逐项指标导出调用链是这样走的main.py 初始化全局配置后调用 solver.pysolver.py 交替使用 data_loader.py 取数据、network.py 建模型、evaluate.py 做验证。离线环节由 data_preprocess.py 和 enhance_image.py 完成它们把原始遥感图加工成模型能吃到的样本对。如果想快速验证效果直接跑 test_one_data.py 加载 train.pth 就行不必重训练。2.2 语义分割视角为什么水体提取必须选 U-Net 而不是分类网络有人会问水体提取不就是把像素分成“水”和“非水”两类吗用 ResNet 加全连接层做二分类不行不行。图像分类输出的是整张图的概率而水体提取要求每个像素一个标签本质是稠密预测。城市遥感影像里水体往往只占画面的 5%~15%背景占比极大用分类网络的结果就是整张图被预测成大量碎块边界一塌糊涂。U-Net 解决这个问题的思路是编码器-解码器结构。编码器通过四次下采样把空间尺寸从 512 缩到 32特征图从 3 通道扩大到 512 通道这一步让网络学到“深语义”——比如阴影和水体的差别、水面纹理特征。解码器再把特征图逐级上采样回原尺寸。关键在于跳连编码器第 i 层的浅层特征直接拼到解码器对应阶段浅层特征保留了边缘和纹理细节正好弥补深层特征丢失的空间信息。城市水体的目标形态以细长河道、不规则池塘为主边界精度直接决定提取结果能不能用。DeeplabV3 的空洞卷积对中小目标也有效但它依赖在 ImageNet 上预训练的主干网络U-Net 从零训练就能取得不错效果对算力有限、没条件加载大规模预训练权重的毕设场景更友好。另一个实际原因也很直接项目里直接给了 U-Net 和 AttU-Net 两套结构跑通后可以对比注意力机制加进来前后的差异这在论文里是现成的实验素材。2.3 Attention Gate 实现AttU-Net 比 U-Net 强在哪AttU-Net 的改动并不是替换主干而是在跳连路径上插入注意力门控。原始 U-Net 把编码器特征原封不动传给解码器其中混杂了大量背景信息AttU-Net 的做法是让解码器的深语义特征作为门控信号自动给编码器的浅层特征打一个 0 到 1 的空间权重。import torch import torch.nn as nn class AttentionGate(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() # 1x1 卷积把输入特征与门控特征对齐到同一通道数 self.conv_in nn.Conv2d(in_ch, out_ch, kernel_size1) self.conv_g nn.Conv2d(out_ch, out_ch, kernel_size1) self.psi nn.Conv2d(out_ch, 1, kernel_size1) self.relu nn.ReLU(inplaceTrue) self.sigmoid nn.Sigmoid() def forward(self, x, gate): # x: 解码器上采样后的深特征gate: 编码器浅层特征 g1 self.conv_g(gate) x1 self.conv_in(x) out self.relu(x1 g1) # 对齐后相加融合位置信息 alpha self.sigmoid(self.psi(out)) # 0~1 的空间注意力系数 return x * alpha每个参数关键点在注释里已经标出。in_ch 和 out_ch 在每一层跳连位置不同第一层通常是 64 进 64 出后层随通道数倍增。这里的gate取自编码器同层特征x是解码器上采样后的特征两者分辨率一致才能相加。alpha越接近 0 表示该位置与水体无关越接近 1 表示需要重点保留。对城市遥感这种背景极其复杂的场景注意力门控带来的提升是肉眼可见的。建筑物阴影在水体提取里是最头号的干扰阴影和水体在 RGB 空间都是低亮度暗色区域传统阈值方法几乎无法区分。Attention Gate 在训练过程中会逐渐学会“深语义判断”——如果该区域从上下文看属于建筑物或高架桥即使亮度特征像水也照样抑制。项目里的 U-Net.png 和 AttU-Net.png 两张结构图可以对照着看差别就是跳连路径上多了一个绿色菱形的小模块部署时训练 AttU-Net 通常比 U-Net 的 IoU 高出 2~4 个百分点。3. 数据组织与预处理从原始遥感图到可训练的样本对3.1 数据集目录约定与标签语义Urben_pre 目录存放的是城市预处理影像data_set 是后续裁剪好的训练样本。这个项目的数据组织方式遵循遥感语义分割的常见约定影像放在 img 目录对应标签放在 label 目录两边的文件名一一对应标签是单通道灰度图像素值为 0 和 255分别代表背景和水体。拿到数据后第一件事是检查标签是否有值域异常。常见问题是标注软件导出时用了 JPEG 压缩导致水体边界出现灰色过渡带255 变成 254 或 250。训练前建议把标签统一二值化否则损失函数会在过渡带产生虚假梯度。另一个要确认的点是影像通道顺序遥感影像加载进来是 BGR 还是 RGB取决于用 OpenCV 还是 PIL 读图预处理脚本里必须统一后面推理时也要走完全一样的读图方式。城市水体提取有个隐藏陷阱值得单独说阴影和水体的视觉特征高度相似。如果数据增强方案里完全没有针对性地加入亮度扰动模型很容易学到“暗色像素就是水体”这种偷懒的映射表面指标好看换到新影像立刻翻车。3.2 data_preprocess.py滑窗裁剪、归一化与训练集划分原始遥感影像动辄几千乘几千像素直接整图进模型不现实显存放不下也没必要。常见做法是滑窗裁剪把大图切成 512×512 的瓦片同时保留一部分重叠区域避免水体正好被切在瓦片边界上导致目标断裂。import cv2 import os import numpy as np def slide_crop(img_path, label_path, save_img_root, save_lab_root, patch512, stride384): img cv2.imread(img_path) label cv2.imread(label_path, cv2.IMREAD_GRAYSCALE) h, w img.shape[:2] idx 0 for y in range(0, h - patch 1, stride): for x in range(0, w - patch 1, stride): sub_img img[y:ypatch, x:xpatch] sub_lab label[y:ypatch, x:xpatch] # 丢弃水体占比过小的瓦片减少背景过采样 if sub_lab.mean() 0.003: continue cv2.imwrite(f{save_img_root}/{os.path.basename(img_path)[:-4]}_{idx}.png, sub_img) cv2.imwrite(f{save_lab_root}/{os.path.basename(img_path)[:-4]}_{idx}.png, sub_lab) idx 1这里每个参数都直接影响训练质量。patch 取 512 是因为 U-Net 下采样四次后特征图是 32×32这个尺寸下感受野刚好覆盖中等规模水体。stride 取 384 意味着相邻瓦片有 128 像素重叠重叠区域的样本会被重复采样等于给边缘像素加了权重——对细窄河道来说这是必要的否则切在瓦片边界上的水体段会被截断。sub_lab.mean() 0.003这个丢弃阈值过滤掉纯背景瓦片城市影像背景占比经常超过 90%如果不做过滤网络学出来会变成“永远预测背景”准确率也能到 85% 以上但 IoU 几乎为零。这个阈值在实际操作中需要根据数据集特点微调。我一般放在 0.3%~0.5% 之间太严会丢掉只有零星几个水像素的负样本太松则正样本不足。划分训练验证集时要注意按瓦片随机划分会出问题同一个原始影像裁剪出的相邻瓦片内容高度相似如果一部分进训练集、一部分进验证集验证指标会虚高。正确做法是按原始影像分组划分同一张大图的所有瓦片必须进同一个集合。归一化方面常见做法是用全数据集的均值标准差做标准化而不是简单除以 255。对遥感影像来说城市地表反照率高水体亮度低均值标准差归一化能把两类像素的分布拉开模型收敛速度明显更快。预处理脚本里通常会把均值和标准差打印出来记下这两组数推理时要用同一个。3.3 enhance_image.py数据增强与阴影鲁棒性数据增强不是走过场它对城市水体的作用比一般分类任务更大。因为城市影像里“暗色区域”既有水体也有阴影模型必须学会用上下文区分二者而上下文信息恰恰需要多样化的亮度样本来逼模型去学。import imgaug.augmenters as iaa seq iaa.Sequential([ iaa.Fliplr(0.5), # 水平翻转 iaa.Flipud(0.2), # 垂直翻转 iaa.Multiply((0.8, 1.2)), # 亮度扰动模拟不同光照条件 iaa.LinearContrast((0.9, 1.1)), # 对比度调整 iaa.Add((-25, 25)), # 像素偏移增强阴影鲁棒性 iaa.CropAndPad(px(-20, 0), keep_sizeTrue) # 随机裁剪边缘保持输出尺寸 ])增强操作分成两类几何变换和光度变换。翻转是安全的几何变换不影响标签语义CropAndPad带负像素值表示裁剪中心区域相当于随机平移增强模型对目标位置的鲁棒性。关键坑在于影像和标签必须使用同一个随机种子同步变换否则标签错位模型训练时 loss 根本不收敛。用 imgaug 的Sequential可以保证这一点它会自动对输入列表中的所有图像应用相同的几何变换。光度变换要克制。Multiply((0.8, 1.2))把整体亮度乘上 0.8 到 1.2 的随机系数模拟不同天气和时间段的光照变化LinearContrast调整对比度让模型不能只靠“暗”来识别水体。加Add像素偏移是为了让水面纹理、波纹细节有变化防止模型过拟合到特定影像的灰度分布。增强参数的上限值得注意。亮度乘子超过 1.2 会把阴影区压得太黑让标签里的水体边界也变得模糊像素偏移超过 ±25 会让暗区和亮区的区分度下降反而增加误检。跑训练之前把增强后的样本可视化输出一遍确认肉眼能认出水体、标签没有错位再做正式训练。4. 训练与评估全流程从哪里开始跑超参数怎么调4.1 先跑通推理再谈训练train.pth 与 test_one_data.py训练是重体力活推理是轻量活。项目里既然带了 train.pth 预训练权重正确的打开方式是先跑推理确认整个环境、依赖、数据路径都通了再去碰 training。test_one_data.py 的使用非常简单指定影像路径、指定权重路径、指定输出路径模型加载后输出一张与输入等大的二值掩膜。第一次跑通后找一个包含建筑阴影的区域看效果重点观察阴影集中的地方有没有被误判成水体。跑推理时检查一个关键点加载torch.load出来的权重 dict 里的 key 是否和当前 network.py 里模型参数名一致。如果 model 定义文件被人改过或者加载时指定了strictFalse权重会部分丢失预测结果会异常最常见的表现是整张图输出灰蒙蒙一片边界完全丢失。项目里已经跑好的权重一般不会出这类问题但如果你改了网络结构再加载旧权重这个坑一定会遇到。至于为什么先推理后训练还有一个现实原因训练要花的时间长中途发现环境问题会浪费大量时间推理只要几秒能最快暴露 PyTorch 版本、CUDA 驱动、OpenCV 读取路径等底层问题。跑通推理后你对项目的把握程度完全不同改代码时心里有底。4.2 main.py 与 solver.py训练循环、损失函数与关键参数训练入口是 main.py真正干活的在 solver.py。solver 里需要重点理解三个东西损失函数怎么组合、优化器怎么设置、权重怎么保存。这套项目用的损失是 BCE 加 Dice 的联合损失这是遥感分割任务的主流做法。# solver.py 中训练循环的核心结构 for epoch in range(epochs): model.train() for batch in train_loader: images batch[image].to(device) # [B, 3, H, W]已归一化 labels batch[mask].to(device) # [B, 1, H, W]取值 0/1 preds model(images) # 网络原始输出 logits loss_bce nn.BCEWithLogitsLoss()(preds, labels) dice 1 - dice_coef(torch.sigmoid(preds), labels) loss loss_bce dice # 联合损失缓解样本不平衡 optimizer.zero_grad() loss.backward() optimizer.step() # 每个 epoch 结束后在验证集上计算指标只保留最优权重 if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), models/best.pth)BCEWithLogitsLoss直接在网络 logits 上计算内部自带 sigmoid不要再对 preds 手动做 sigmoid否则梯度传播会出问题。Dice 项的作用是缓解类别不平衡——城市影像里背景占 90% 以上单独用 BCE 时背景像素贡献了绝大部分梯度模型会偏向预测背景Dice 直接优化预测和真实标签的重叠度不依赖像素数量分布对小水体更友好。两个 loss 相加的系数是 1:1常见做法里也有人给 Dice 加权到 1.5 或 2.0这个要根据验证集表现来调。超参数没有玄学但有合理的起点。下面的表是这套项目在 512×512 输入下比较稳妥的设置参数推荐值说明输入尺寸512×512与预处理脚本保持一致batch size8~16显存不够就降到 4配合梯度累积epochs80~120小数据集 80 轮足够看验证集 IoU 停优化器Adamlr1e-4分割任务默认选 Adam比 SGD 稳学习率衰减ReduceLROnPlateaupatience5验证 IoU 连续 5 轮不涨就降一半梯度裁剪clip_grad_norm_(5.0)防止 loss 偶发爆炸优化器用 Adam 或 AdamW 都行lr 从 1e-4 起步不要上来就用 1e-3遥感影像的标签噪声比自然图像大学习率过高会让 loss 在前几十个迭代里剧烈震荡。衰减策略用ReduceLROnPlateau监控验证集 IoUpatience 设 5 轮。如果训练到后期 loss 降不下去但 IoU 还在缓慢涨说明模型在优化边界细节这时候可以停掉不需要硬跑满预设 epoch。一个容易被忽略的设置是num_workers。在 Windows 上如果数据加载线程数开太大可能出现内存溢出或者训练卡死常见做法是设 2~4够用且稳定。4.3 evaluate.py 与 res.csvIoU、F1、像素精度的计算口径训练完看什么指标准确率在这个任务里没有参考价值——全是背景的图上准确率都有 85% 以上。重点看 IoU 和 F1。def compute_metrics(pred_bin, gt_bin): pred_bin (pred_bin 0.5).astype(np.uint8) gt_bin (gt_bin 0.5).astype(np.uint8) inter np.logical_and(pred_bin, gt_bin).sum() union np.logical_or(pred_bin, gt_bin).sum() iou inter / (union 1e-6) tp inter fp pred_bin.sum() - tp fn gt_bin.sum() - tp precision tp / (tp fp 1e-6) recall tp / (tp fn 1e-6) f1 2 * precision * recall / (precision recall 1e-6) pa (pred_bin gt_bin).mean() return {IoU: iou, F1: f1, PA: pa}代码里的1e-6是平滑项防止除零。IoU 分母是预测和真实标签的并集对漏检和误检都敏感是最能反映水体提取质量的单一指标F1 是精确率和召回率的调和平均如果你关心“找出来的水体有多少是真的”和“真实水体有多少被找到”之间的平衡看它PA 像素精度只能作为参考。注意所有指标都要在二值掩膜上计算预测输出过 sigmoid 后大于 0.5 置 1否则灰度值直接比较会产生虚高结果。evaluate.py 会把每张验证影像的指标逐行写进 res.csv末尾附平均值。提交论文时用平均 IoU 和 F1展示单张结果时挑一张有代表性、包含建筑阴影和细窄河道的影像让评审看到难点被解决了而不是只挑最漂亮的图。5. 遥感水体提取的避坑清单五个真实翻车现场5.1 现象一预测图大面积全黑或全白原因不止一个排查顺序从数据通道开始OpenCV 读图默认 BGR如果训练时用 PIL 读成 RGB推理时用 OpenCV模型看到的通道顺序反了所有特征整体错乱输出就是一张没意义的全黑图。第二种常见原因是保存方式不对——网络输出 logits 后没有过 sigmoid直接cv2.imwrite保存float 值域在 0 到 1 之间的结果会被保存成接近全黑的图像。第三种原因是归一化参数不一致。解决训练和推理共用同一个预处理函数把归一化的均值和标准差写入一个常量文件保存掩膜时统一走pred (torch.sigmoid(logits) 0.5).float() * 255先转 uint8 再存图。这三种原因按顺序排查先解决通道顺序问题再看归一化最后看保存逻辑。5.2 现象二验证集 IoU 高达 0.85换一张新图效果稀烂这是遥感分割最经典的翻车现场。原因是数据划分时按瓦片随机分训练验证集没有按原始影像分组。相邻瓦片有 128 像素重叠内容高度相似训练集和验证集之间发生了数据泄漏模型相当于“见过”验证集的影像。验证指标虚高一旦换真正陌生的影像立刻露馅。解决预处理阶段按原始影像编号做分组同一个原始影像的所有子瓦片只能落在训练集或验证集其中一方。Urben_pre 目录下按原始影像建子目录存放数据脚本按子目录划分。这个坑在论文里最好主动提一句“按原始影像划分训练验证集”算是加分项。5.3 现象三512 输入一跑训练就爆显存勉强跑起来还频繁中断根本原因是显存分配策略不合理不是模型本身太大。512×512 输入加 U-Net 结构batch size 16 在 8GB 显存上必然爆。解决思路有几个维度的选择把 batch size 降到 4 或 6这是最直接的显存仍然吃紧就用梯度累积累积 4 步再更新一次参数等效于 batch 16 的效果把输入尺寸从 512 降到 384模型参数量不变但中间特征图显存占用大幅下降。另一个经常被忽略的因素是num_workers开太大会把内存占满导致进程被杀调到 2~4。5.4 现象四训练时 loss 不降反升或者跳来跳去完全不收敛最常见的原因是标签错位。增强脚本里翻转了影像但没同步翻转 mask或者预处理脚本里影像做了归一化而标签跟着做了归一化都会让模型无从学起。另一个原因是学习率过大Adam 在 lr1e-3 时对噪声标注非常敏感loss 曲线会出现典型的分岔跳变。解决训练开始前写一段可视化代码把 dataset 的前 5 个 batch 的影像和标签叠加画出来肉眼看影像中的水体轮廓和标签的白色区域是否对齐这一步能排除 80% 的数据问题。确认数据没问题后把 lr 降到 1e-4再加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)loss 曲线在一个 epoch 内就能看到下降趋势。5.5 现象五粗水体提取完整细窄河道断裂成一段一段边界锯齿严重一方面要知道这是 U-Net 类结构的通病下采样过深后细小目标的细节信息大量丢失跳连只能部分恢复。另一方面后处理环节没有设计好也会加剧断裂。解决思路按效果排序训练时把滑窗 stride 从 384 调小到 256让同一河道被更多瓦片覆盖相当于给细窄结构加了采样权重推理后做 5×5 核的形态学闭运算能把断开的河道补起来追求更高精度就换膨胀卷积或增加一个边缘细化分支但毕设场景下前两步已经足够。后处理的核大小要控制5×5 的闭运算能补断线但核太大比如 11×11 会把两像素宽的河道直接抹平。这里的原则是先测量数据里最细的河道宽度让核尺寸小于它的一半。6. 后处理与面积统计把分割 Mask 变成可交付的结果6.1 形态学开闭运算与连通域过滤模型输出的掩膜不要直接拿去论文里贴图先过一遍后处理。常见做法是形态学开闭加连通域过滤代码量不大但能显著提升结果观感。import cv2 import numpy as np def refine_mask(mask, min_area64, close_kernel5): mask mask.astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (close_kernel, close_kernel)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 闭运算补水体内部孔洞 mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 开运算去掉孤立噪点 num, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] min_area: mask[labels i] 0 # 面积小于阈值的连通域直接抹掉 return mask闭运算在前开运算在后顺序有讲究先闭运算把水体内部的孔洞补上再开运算把离散的小噪点消掉顺序反过来会让噪点先被放大。min_area64的单位是像素按 0.5 米分辨率算相当于 16 平方米小于这个面积的水体碎片大概率是误检的阴影。如果数据里有真实的小水洼这个值要下调否则真目标会被误删。6.2 水体面积统计与类别混淆回验分割掩膜的地理意义在于算面积这块可以用几行代码完成pixel_size 0.5 ** 2 # 单像素地面面积0.5 米分辨率就是 0.25 m² water_area_m2 np.sum(refined_mask 0) * pixel_size water_area_km2 water_area_m2 / 1e6 print(f水体面积{water_area_km2:.3f} km²)0.5 ** 2里的 0.5 来自数据集对应影像的分辨率换成自己的数据时务必查清 GSD 再改这个参数写错会导致面积结果整体偏大或偏小四倍。除了面积统计还建议做一项“类别混淆回验”从验证集里专门抽出以建筑阴影为主的影像单独计算 IoU。如果整体 IoU 不错但阴影区的误检率特别高说明模型学到的是“暗色等于水体”而不是真正的空间语义这时要回到增强环节补充阴影样本或者换用含近红外波段的影像数据。从那以后我每次跑完遥感分割实验都强制走一遍后处理加回验的流程先看阴影区误检再统计细窄河道断裂率最后才看整体指标。这套流程帮我揪出过好几个表面漂亮实则翻车的模型。希望帮到你。本文还有配套的精品资源点击获取