简介:这份资源是面向计算机相关专业在校生与项目实战学习者的毕业设计级源码包,主题为基于Python深度学习的高分辨率城市遥感图像水体提取系统。项目已通过导师评审并获98.5分,适合用作毕设、课程设计、期末大作业或竞赛初期立项演示,也便于具备一定基础者进行二次开发。压缩包共56个文件,约1.42MB,包含22个Python脚本、26张PNG图像、2个pth模型权重、2个CSV结果文件及若干说明文档,覆盖数据预处理、模型训练、测试评估与结果输出等完整流程,并附有U-Net与AttU-Net网络结构示意图。已有196人学习关注。读者可从中获得可本地运行的完整工程代码、训练好的模型权重、遥感水体数据集组织方式以及从数据加载到精度评估的排错与调参思路,对理解深度学习语义分割在遥感场景中的落地具有较高参考价值。
1. 从一张高分卫星图里把水面抠出来:这套 Python 深度学习水体提取系统到底在做什么
拿到一幅 0.5 米分辨率的城市遥感影像,人眼一眼就能分出河道、湖泊、景观水池和湿地,但要让程序自动把水面像素标出来,事情立刻变得麻烦:建筑阴影是暗的、沥青路面是暗的、蓝色屋顶也偏蓝,传统 NDWI 阈值一调就顾此失彼。这套「基于 Python 深度学习实现高分辨率城市遥感图像的水体提取系统」要解决的正是这个问题——用语义分割网络替代人工阈值,把水体提取做成可训练、可复现、可批量推理的工程流程。它适合三类人:做遥感解译想升级到深度学习的技术员、手里有高分影像但缺标注和训练经验的测绘/GIS 从业者、以及想找一个完整深度学习项目练手的入门者。核心链路是「影像切片 → 标注 → 训练分割模型 → 推理拼接 → 精度评估」,下面按这条链路拆开讲。
2. 水体提取为什么必须上深度学习:从 NDWI 失效到语义分割选型
2.1 传统指数法在高分城市影像上的三个硬伤
NDWI(归一化水体指数)用绿波段和近红外波段做差,原理是水体在近红外强吸收、绿光反射相对高。在 Landsat 这类 30 米中分辨率影像上它很好用,但到了 0.5~2 米的高分城市影像,问题集中爆发。第一是阴影混淆:高层建筑投下的阴影在近红外同样很暗,NDWI 值接近水体,成片误提。第二是浑浊水体与湿地的边界模糊:含泥沙的河水、浅水湿地、雨后积水,光谱特征介于水和陆地之间,单一阈值切不干净。第三是混合像元:城市水体往往窄,一条 3 米宽的河道在高分图上只有几个像素,边缘像元被岸边地物污染,指数法直接漏掉。
我一般会先跑一遍 NDWI 作为基线,把它的结果和人工标注叠在一起看,通常能直观看到阴影区被整片误判。这个基线不是为了用,而是为了让你相信深度学习的必要性——如果 NDWI 已经够用,就没必要上模型。
2.2 语义分割为什么比分类、目标检测更合适
水体提取本质是逐像素二分类,输出和输入同尺寸的掩膜,这正是语义分割的任务形态。目标检测给的是框,框里还混着岸、桥、船,没法直接用;图像分类给的是整图标签,更不沾边。语义分割里常见的选择是 U-Net 系和 DeepLab 系:U-Net 的编码器-解码器加跳跃连接,对小目标、细窄水体边界保留得好,适合城市河道这种细长结构;DeepLab 用空洞卷积扩大感受野,对大面湖泊和上下文判断更强。城市高分影像里细窄水体占比高,我通常首选 U-Net 结构,骨干网换成 ResNet34 或 EfficientNet-B0 提升特征提取能力。
选型还要看数据量。U-Net 参数量相对小,几百到几千张切片就能收敛;Transformer 类分割模型(如 SegFormer)精度上限高,但对数据量和显存要求也高,新手容易在训练不稳定上翻车。所以这套系统的合理默认是「U-Net + 预训练骨干 + 二分类输出」,先把流程跑通,再考虑换更强的主干。
2.3 数据准备:切片、标注与格式约定
高分影像动辄上万像素,直接进网络显存扛不住,必须切片。常见做法是滑窗切 512×512,重叠 64~128 像素,避免边缘目标被切断。标注用 labelme 或 ArcGIS 导出二值掩膜,水体为 1、背景为 0。这里有个容易被忽略的点:切片和掩膜必须严格同名同坐标,否则训练时对不上,loss 会一直不降,很多人卡在这里找不到原因。
import os import numpy as np from PIL import Image def slide_crop(img_path, mask_path, out_img_dir, out_mask_dir, patch=512, stride=384): """滑窗切片:patch 为切片边长,stride 为步长,stride<patch 产生重叠""" img = np.array(Image.open(img_path)) mask = np.array(Image.open(mask_path).convert("L")) 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): img_patch = img[y:y+patch, x:x+patch] mask_patch = mask[y:y+patch, x:x+patch] # 全背景切片直接丢弃,缓解正负样本失衡 if mask_patch.max() == 0: continue Image.fromarray(img_patch).save( os.path.join(out_img_dir, f"{idx:05d}.png")) Image.fromarray(mask_patch).save( os.path.join(out_mask_dir, f"{idx:05d}.png")) idx += 1 return idx这段代码的关键参数是patch和stride。patch=512是显存和上下文信息的折中,显存小就降到 256;stride=384意味着重叠 128 像素,重叠越大边缘目标越完整,但切片数量成倍增长。mask_patch.max()==0那行是过滤纯背景切片,城市影像里背景占大头,不过滤会导致正样本被淹没,模型倾向于全预测背景。切片数量、正负比例建议在切完后统计一次,正样本占比低于 10% 就要考虑加权损失。
3. 用 Python 把训练流程跑通:环境、模型与损失函数
3.1 环境搭建与依赖版本
深度学习环境最怕版本打架。这套流程依赖 PyTorch、OpenCV、numpy、albumentations(数据增强)、segmentation-models-pytorch(现成分割模型库)。我一般用 conda 建独立环境,Python 3.8 或 3.9 都稳,PyTorch 选和 CUDA 匹配的版本。装 cv2 用pip install opencv-python,numpy 跟着 PyTorch 自动装即可,不要手动指定版本,容易和 torch 冲突。
conda create -n water python=3.9 -y conda activate water # 按本机 CUDA 版本选对应命令,这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python albumentations segmentation-models-pytorch装完先跑一句python -c "import torch; print(torch.cuda.is_available())",输出 True 才算环境通了。如果显存不够,把 batch size 降到 4 甚至 2,别硬撑,OOM 报错会浪费大量调试时间。
3.2 数据集类与数据增强
数据增强对遥感水体提取特别重要,因为标注成本高、样本有限。常用的有随机翻转、旋转 90 度、亮度对比度扰动。注意:颜色扰动要克制,水体靠颜色和纹理区分,扰动过猛会让模型学到错误的颜色先验。几何变换必须图像和掩膜同步,albumentations 的A.Compose能保证这一点。
import cv2 import numpy as np import torch from torch.utils.data import Dataset import albumentations as A train_tf = A.Compose([ A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3), ]) class WaterDataset(Dataset): def __init__(self, img_dir, mask_dir, transform=None): self.img_dir = img_dir self.mask_dir = mask_dir self.names = sorted(os.listdir(img_dir)) self.transform = transform def __len__(self): return len(self.names) def __getitem__(self, i): name = self.names[i] img = cv2.cvtColor(cv2.imread( os.path.join(self.img_dir, name)), cv2.COLOR_BGR2RGB) mask = cv2.imread(os.path.join(self.mask_dir, name), cv2.IMREAD_GRAYSCALE) if self.transform: aug = self.transform(image=img, mask=mask) img, mask = aug["image"], aug["mask"] img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1)) # HWC -> CHW mask = (mask > 127).astype(np.float32) # 二值化 return torch.from_numpy(img), torch.from_numpy(mask).unsqueeze(0)brightness_limit=0.1是刻意压小的,就是为了不让模型过度依赖绝对亮度。mask > 127把标注里 255 的水体转成 1,保证和 sigmoid 输出对齐。返回的 mask 用unsqueeze(0)加通道维,因为后面 BCE 损失要求形状一致。
3.3 模型、损失与训练循环
模型用segmentation_models_pytorch一行就能建 U-Net,骨干选 ResNet34 并加载 ImageNet 预训练权重,收敛快很多。损失函数用 Dice + BCE 组合:BCE 稳定梯度,Dice 直接优化重叠度,对正负失衡更鲁棒。优化器用 AdamW,学习率 1e-4,配合余弦退火。
import segmentation_models_pytorch as smp import torch.nn as nn model = smp.Unet( encoder_name="resnet34", encoder_weights="imagenet", in_channels=3, classes=1, ) bce = nn.BCEWithLogitsLoss() def dice_loss(pred, target, eps=1e-6): pred = torch.sigmoid(pred) inter = (pred * target).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) return 1 - ((2 * inter + eps) / (union + eps)).mean() def criterion(pred, target): return bce(pred, target) + dice_loss(pred, target)训练循环里每轮记录 loss 和验证集 IoU,IoU 比 loss 更能反映真实提取效果。学习率 1e-4 是预训练骨干微调的常用起点,如果 loss 前几轮就震荡,降到 5e-5;如果 loss 几乎不动,检查数据对齐和标签二值化。batch size 在 8GB 显存下用 4~8,配合torch.cuda.amp混合精度能再省一半显存。
4. 推理、拼接与精度评估:让结果能交付
4.1 大图推理与重叠拼接
训练完的模型只能吃固定尺寸切片,要还原整幅影像就得滑窗推理再拼回去。重叠区域用加权平均融合,边缘权重低、中心权重高,能消除拼接缝。这一步不做,成果图上会出现明显的网格状接缝,交付时很难看。
import torch import numpy as np def infer_big_image(model, img, patch=512, stride=384, device="cuda"): model.eval().to(device) h, w = img.shape[:2] prob = np.zeros((h, w), dtype=np.float32) weight = np.zeros((h, w), dtype=np.float32) # 高斯权重,中心高边缘低 g = np.outer(np.hanning(patch), np.hanning(patch)).astype(np.float32) for y in range(0, h - patch + 1, stride): for x in range(0, w - patch + 1, stride): p = img[y:y+patch, x:x+patch].astype(np.float32) / 255.0 p = torch.from_numpy(p.transpose(2, 0, 1)).unsqueeze(0).to(device) with torch.no_grad(): out = torch.sigmoid(model(p))[0, 0].cpu().numpy() prob[y:y+patch, x:x+patch] += out * g weight[y:y+patch, x:x+patch] += g return prob / np.maximum(weight, 1e-6)np.hanning生成一维汉宁窗,外积得到二维权重,中心接近 1、边缘接近 0,重叠区自然平滑过渡。stride要和训练切片保持一致,否则尺度分布对不上,精度会掉。推理完用 0.5 阈值二值化,再按需做形态学去噪。
4.2 精度评估指标怎么选
水体提取常用 IoU、F1、precision、recall。城市水体里小目标多,单看总体 IoU 会被大湖泊主导,掩盖窄河道的漏提。我一般额外统计「按目标尺寸分层的 recall」:把水体连通域按面积分成小(<500 像素)、中、大三档,分别算 recall。这样能看出模型是不是只学会了提大水面。评估代码用混淆矩阵累加即可,注意在验证集上做,别拿训练集自欺欺人。
| 指标 | 含义 | 城市水体场景关注点 |
|---|---|---|
| IoU | 交并比 | 总体精度,易被大目标主导 |
| F1 | 精确率与召回率调和 | 综合权衡误提与漏提 |
| Precision | 预测为水中真正是水的比例 | 阴影误提会拉低它 |
| Recall | 真实水中被提出的比例 | 窄河道漏提会拉低它 |
4.3 后处理:形态学与连通域过滤
模型输出难免有零星噪点和小误提。常见做法是先开运算去孤立点,再按连通域面积过滤掉小于阈值的斑块。但要注意:城市景观水池可能就几十个像素,面积阈值设太大反而把真目标滤掉。我的经验是阈值设在 30~50 像素,并且对细长河道单独放宽,别一刀切。
5. 避坑与排查:这套流程最容易翻车的五个地方
5.1 训练 loss 不降,模型输出全黑或全白
现象:训练几轮后 loss 卡在高位,推理结果整幅全背景或全水体。原因通常是正负样本严重失衡,或者标签没二值化(掩膜里是 0/255,模型学的是回归)。解决:先确认 mask 已转成 0/1,再统计正样本占比,低于 10% 时给 BCE 加pos_weight,或提高 Dice 损失权重。全黑全白还可能是学习率过大,降到 1e-4 以下重试。
5.2 验证集精度高,换一幅新影像就崩
现象:验证 IoU 0.85,换城市、换季节的影像精度骤降到 0.5。原因是模型过拟合了训练影像的色调和地物分布。解决:训练时加入更强的颜色扰动和不同来源的影像,做跨区域验证而不是随机划分。随机划分会让同一幅图的切片同时进训练和验证,造成精度虚高,这是最常见的评估陷阱。
5.3 拼接成果出现网格接缝
现象:整幅成果图上每隔一段有明暗条纹。原因是滑窗推理没做重叠融合,或者融合权重是硬边界。解决:用 4.1 的高斯加权融合,确保stride < patch,重叠区至少 64 像素。如果还有缝,检查推理时的归一化是否和训练一致。
5.4 显存溢出(CUDA out of memory)
现象:训练中途报 OOM。原因多是 batch size 太大、切片太大,或验证时没加torch.no_grad()导致计算图累积。解决:降 batch size 和 patch 尺寸,验证和推理包在torch.no_grad()里,开启混合精度torch.cuda.amp。别忽略验证阶段,它同样吃显存。
5.5 阴影被大面积误提为水体
现象:建筑阴影区成片标成水。原因是阴影和水在 RGB 上都是暗色,模型没学到足够区分特征。解决:训练样本里必须包含带阴影的负样本,让模型见到足够多的「暗但非水」案例;输入可以加一个近红外波段或 NDWI 作为额外通道,给模型提供光谱先验。只有 RGB 时,靠数据增强和负样本多样性来补。
6. 把精度再往上推一档:波段扩展与多尺度推理的实战技巧
流程跑通后,真正拉开差距的是细节。第一个技巧是输入通道扩展:如果影像有近红外波段,把它作为第 4 通道送进网络,同时把 NDWI 作为第 5 通道。模型能直接利用光谱先验,阴影误提会明显下降。改法是把in_channels=3改成 5,数据集里同步拼接波段,注意归一化要按波段分别做,近红外和 NDWI 的数值范围与 RGB 不同,统一除以 255 会出问题。
第二个技巧是多尺度推理(TTA)。同一张切片做原尺度、水平翻转、垂直翻转三次推理,概率图取平均,再二值化。代价是推理时间翻三倍,但 IoU 通常能涨 1~3 个百分点,对交付精度要求高的项目值得。实现上把 4.1 的推理函数包一层,对输入做变换、对输出做逆变换再累加即可。
第三个技巧是难例挖掘。训练一轮后,把验证集里 IoU 最低的切片挑出来,人工检查是标注错误还是模型能力不足。标注错误就修,能力不足就把这些切片加权采样进下一轮训练。我一般会做两到三轮这样的迭代,比盲目加数据有效得多。
| 技巧 | 预期收益 | 代价 | 适用场景 |
|---|---|---|---|
| 增加近红外/NDWI 通道 | 阴影误提下降明显 | 需要多光谱数据 | 有近红外波段的影像 |
| 多尺度 TTA | IoU +1~3% | 推理时间 ×3 | 精度优先的交付 |
| 难例挖掘迭代 | 边界和小目标改善 | 人工复核成本 | 有标注维护能力 |
最后说个习惯:每次改完参数,我都会固定用同一批验证切片跑评估,把 IoU、分层 recall 记在一个表里,改了什么、涨了还是跌了一目了然。没有这个记录,调参就是玄学,改到最后自己都不记得哪版最好。这套水体提取系统真正的门槛不在模型多复杂,而在数据对齐、评估可信和后处理克制这三件事上,把这三件做扎实,U-Net 这种「老结构」也能交出能用的成果。希望帮到你。
本文还有配套的精品资源,点击获取