
简介基于Python深度学习的高分辨率城市遥感图像水体提取系统源码源自作者大四毕业设计经导师指导并以98.5分通过评审。项目面向计算机科学、人工智能、数据科学等专业正在做毕设或需要项目实战练习的学生也适合用于课程设计、期末大作业与创新实践。资源共56个文件压缩包仅1.42MB核心为22个Python脚本覆盖数据加载、模型构建、训练、评估与单样本测试全流程附带2个PyTorch模型权重、26张网络结构与结果图像以及说明文档和结果CSV便于快速复现与对比。已有196人学习下载。源码在本地运行验证通过包含U-Net/Attention U-Net等结构、数据预处理与增强脚本、测试入口整体结构清晰代表性强。既适合小白入门进阶也支持在基础上修改扩展、二次开发遇到问题可私信交流。1. 城市遥感水体提取为什么先把NDWI放下手头拿到一景GF-2亚米级城市影像第一反应是用NDWI做水体提取三个小时后你会得到一张满是阴影误检的图。城市场景和乡村不一样高楼阴影、暗色屋面、沥青路面都能在水体指数里冒充水而喷泉池、窄水渠这类小目标又常常漏掉。这套基于Python深度学习的遥感图像水体提取方案做的事就是用语义分割模型替代固定阈值让网络在标注样本上学出“哪些暗色像元真的是水”。它适合遥感方向的毕业设计也适合自然资源、水利监测里需要定期给城市水体摸底的一线从业者——你先准备好几百个标注样本后面训练、推理、后处理就能闭环跑通精度比NDWI高一个量级。2. 数据与标注先行城市水体数据集的三个必修项遥感图像语义分割项目里模型吃的是标注模型的瓶颈也是标注。城市水体之所以难不是难在网络结构而是难在“边界怎么定义”和“哪些暗色像元算水”。这一章把数据这件事拆成三个必修项认识城市水体的光谱与几何特征、定一套能执行的标注规则、用区域划分和增强把训练集做扎实。顺序不能反先定规则再标注否则返工成本会高到你想放弃。2.1 城市水体的光谱与几何特征为什么不能照搬中分辨率经验中分辨率影像做水体提取的经典方案是SENTINEL-2加上NDWI阈值10米分辨率下大片河流、湖泊确实能提出来但放进城市就露馅一条6米宽的景观河道在10米像元里只有一个混合像元NDWI算出来不到阈值直接漏检。高分辨率影像0.3米到2米把问题换了一个方向——水体看得清了干扰也看得清了。特征维度乡村/自然水体城市水体典型尺寸河流、湖泊、水库动辄百米以上景观湖、喷泉池、窄渠很多小于20米边界形状自然弯曲岸线渐变人工几何边界常有垂直岸壁光谱干扰植被、裸土高楼阴影、暗色屋面、沥青路面类内差异相对均匀深水浅水混着水面反光、绿藻漂浮城市水体的类内差异和类间混淆同时被放大这就是为什么固定阈值的指数方法在这里集体翻车。深度学习模型要学的不是“蓝水”这种光谱规则而是“暗色且连续且位于低洼处的区域更可能是水”这种空间上下文——这正好是CNN和语义分割模型擅长的事。2.2 标注规范与工具先定规则再动手否则返工三次标注最怕的不是慢是前后标准不一致。第一次做城市水体标注我踩过最大的坑上午把水面倒影标成水下午把暗色屋面也顺手标了模型学到一堆互相矛盾的东西。后来固定了一套规则按这个规则标返工率大幅下降可见水面算水体包括水面上的倒影区域岸边半干半湿的泥滩、滩涂不算水体停在水面上的船体、桥体不算水体水从桥洞穿过的地方保留阴影、暗色屋面、沥青操场不标水体但有条件就单列一类“类水背景”。工具用LabelMe直接在原始分辨率影像上画多边形不要先压缩影像再标否则多边形和mask永远对不上。标完导出JSON用下面这段脚本把多边形转成二值掩膜掩膜要和影像严格同尺寸。# json_to_mask.py —— LabelMe标注转水体掩膜 import json import numpy as np import cv2 def json_to_mask(json_path, height, width): with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros((height, width), dtypenp.uint8) for shape in data.get(shapes, []): if shape.get(label) ! water: continue pts np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [pts], 1) return mask逻辑说明遍历LabelMe导出JSON里的每个shape只取label为water的多边形用OpenCV的fillPoly把多边形内部填成1其余保持0。注意三点一是标签名必须严格统一water和Water都会被脚本跳过最后检查时你会觉得莫名其妙二是pts必须转int32fillPoly不接受float坐标三是多边形的点不要画得太密一个几百米的湖岸线用几百个点就够点太多只会让标注文件膨胀对精度没有帮助。标完20张图以后建议抽3张把mask叠加到影像上肉眼检查一遍重点看细窄水道的连通性——如果很多窄渠在标注阶段就已经断开模型训练得再好也学不回来。2.3 数据增强与划分区域划分比随机划分更接近验证目标城市水体数据集常见规模是100到300张每张在训练前切成512×512的patch。这里有个新手容易忽略的点patch划分必须按“景”来同一景影像的patch放进训练集相邻patch的水体纹理高度相似模型会在验证集上产生虚高分数。我一般做法是把影像按地理位置或文件名前缀分组一组整体划入训练集或验证集绝不让同一景影像同时出现在两边。数据增强方面城市水体场景最需要模拟的是不同时相、不同光照下的色偏。固定用这组参数随机水平翻转概率0.5、随机90度旋转、亮度扰动±0.2、对比度扰动±0.2、HSV饱和度扰动±0.1。不要用随机裁剪和缩放因为高分辨率水体的尺度对模型很重要过强的几何变形会让细长河道的标注语义失真。训练样本量上我的经验是500个带标注的512×512 patch是一个及格线能让DeepLabV3在IoU上跑到0.8以上200个以下也能跑但阴影误检会明显变多这时候优先做的是补标阴影区域的负样本而不是盲目加更多水体样本。3. 模型与训练DeepLabV3 复现城市水体分割的完整流程数据准备好以后模型选型和训练就是核心工作。城市水体分割本质上是二分类语义分割任务深度学习里成熟的分割框架可以直接用但选型要看场景。这一章从选型理由讲起再给一份能直接跑的训练脚本最后说清楚训练过程中盯哪几个指标、参数怎么调少走弯路。3.1 选型理由ASPP空洞卷积和低层特征融合对城市小水体的意义城市水域和遥感图像语义分割里常见的通用分割不一样它有两个特点一是目标尺度跨度大既有几百米宽的湖面也有几米宽的窄渠二是小目标数量多漏掉一条渠可能就漏掉了一个排口。DeepLabV3在这类任务上比U-Net更好用核心原因是空洞卷积金字塔ASPP用多个不同膨胀率的空洞卷积并联在不大幅降低特征图分辨率的情况下同时看全局和局部2米宽的窄渠和几百米宽的湖面都能被同一组特征覆盖到。U-Net的优势是结构简单、小样本下更稳但它靠逐层下采样和上采样恢复细节高分辨率影像下细长水体容易在多次池化后从特征图里消失。DeepLabV3的decoder会把编码器第一层的低层特征直接跳连进来这部分特征保留了大量边缘细节对河道边界恢复帮助很大。实际工程里我一般用smp库的封装骨干网络选ResNet50在ImageNet预训练权重上微调城市水体这种类内差异小的任务不需要更深的ResNet101。3.2 训练主循环从数据加载到最优权重保存下面这段训练脚本是完整可跑的数据集类WaterDataset需要你自己按第2章的mask格式实现返回(img_tensor, mask_tensor)即可。这里把关键参数都写在代码里配合注释说明。# train_water.py —— 城市水体分割训练主循环 import torch from torch.utils.data import DataLoader import segmentation_models_pytorch as smp model smp.DeepLabV3Plus( encoder_nameresnet50, encoder_weightsimagenet, # 用ImageNet预训练权重收敛大幅加快 classes1, # 二分类水/非水 activationNone, # 输出logits配合BCEWithLogitsLoss ) optim torch.optim.AdamW([ {params: model.encoder.parameters(), lr: 1e-5}, # 编码器微调学习率要低 {params: model.decoder.parameters(), lr: 1e-3}, # 解码器从头学学习率放开 ], weight_decay1e-4) dice_loss smp.losses.DiceLoss(modebinary) bce_loss torch.nn.BCEWithLogitsLoss() best_iou 0.0 for epoch in range(60): model.train() for img, mask in train_loader: # train_loader: batch_size8, shuffleTrue img img.cuda() mask mask.cuda().float() # shape: (B,1,512,512), 值为0/1 logits model(img) loss 0.7 * dice_loss(logits, mask) 0.3 * bce_loss(logits, mask) optim.zero_grad() loss.backward() optim.step() val_iou evaluate_iou(model, val_loader) # 自定义评估函数 print(fepoch {epoch}: val_iou {val_iou:.4f}) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_water.pt)逻辑说明模型用smp的DeepLabV3Plus封装encoder_weights设为imagenet会自动下载ResNet50预训练权重。损失函数用0.7倍DiceLoss加0.3倍BCEWithLogitsLoss这是城市水体分割的标准组合。理由有两点水体在整幅影像里通常只占不到5%背景像素远多于水体DiceLoss直接优化重叠率对类别不平衡不敏感但单纯DiceLoss在小目标上梯度不稳定加一点BCE能保持每个像素的梯度信号训练更稳。参数说明编码器学习率1e-5、解码器1e-3是保守且有效的组合解码器参数是随机初始化的学习率低了学不动batch_size取8的前提是你有12GB以上的显存显存小就改4并同步把patch从512降到384不要只改batch不降分辨率否则OOM。60个epoch是我按500个patch的规模估的早停条件下面一节讲。3.3 训练过程监控的4个指标与早停策略训练时盯着loss下降没有意义必须盯验证集指标。城市水体二分类任务只看四个指标这里给出参考阈值指标含义参考阈值IoU预测与真值交集/并集≥ 0.85F1 Score查准率与查全率的调和平均≥ 0.90像素准确率 PA分类正确的像素占比≥ 0.95Kappa去除随机一致后的总体一致性≥ 0.80特别提醒PA在城市水体任务里几乎都好看因为背景占比95%以上全预测成背景都有95%的PA所以PA只能当底线参考IoU和F1才是真正的裁判。我一般在训练日志里同时打印F1、IoU和每类IoU水体类的IoU比平均IoU更重要。早停策略可以这样定从第30个epoch开始如果连续10个epoch验证集IoU没有刷新最佳值就保存当前最佳权重并停止训练。注意保存时机用“刷新最佳”而不是“最后一次”因为深度学习训练后期震荡是常态最后一轮往往不是最好的那轮。这个细节是血泪经验早期训练每次都用最后一轮权重检查结果时发现模型反而退化了。4. 高分辨率城市水体提取五个高频翻车现场与排查方法训练loss再好看放到真实城市影像上一定会遇到问题。这五条是城市水体提取项目里最高频的翻车场景每条按“现象、原因、解决”排列覆盖阴影误检、细部断裂、验证集虚高、推理OOM、材质混淆这几个绕不开的坎。4.1 现象一高层建筑阴影被识别成水现象预测结果里高楼的深色阴影区域整片被标成水体尤其分布在影像西北侧背光方向。原因是城市高楼阴影在RGB影像上和深水区光谱高度接近都是低亮度、偏蓝模型在训练时学到的是“暗蓝水”这种肤浅规则而不是真正的空间上下文。解决分三步走训练时把大量阴影区域标为背景负样本让模型见过足够多“长得像水但不是水”的样本预测后处理加亮度约束8bit影像里均值亮度低于70的像元直接置0如果手里有多光谱数据再加一个NDWI0.15的约束做二次确认只有光谱和模型同时认可才保留。4.2 现象二细长河道从中间断开现象预测的河道呈现断续点状6米宽的渠被切成一段一段这在10米分辨率上是常态但高分辨率下出现这个现象就是网络的问题。原因是DeepLabV3即便有ASPPResNet50编码器仍然做了多次下采样细线特征在深层特征图里只剩一两个像素解码器恢复边缘时又叠加了上采样误差细目标就断了。解决思路是从输入尺度和损失函数两头堵训练时把512×512的patch在原图上按1.5倍放大再裁剪相当于让模型在更大尺度上看清水面形状损失函数改用边界加权Dice先算出mask的Canny边缘边缘附近5像素内的体素在损失里权重乘2逼模型把力气花在边界连续性上。推理阶段把滑窗重叠从64加大到128也能减少跨patch断接缝。4.3 现象三验证集IoU远低于训练集现象训练IoU已经0.92验证集只有0.72差距超过10个点。最常见的原因是数据划分踩了坑随机划分patch时同一景影像的patch同时进训练集和验证集模型记住了这景影像的纹理细节验证时碰见没见过的影像马上原形毕露。解决方法是切换到按景划分一个原始影像文件的所有patch只能出现在一边同时加强色偏增强模拟不同卫星传感器、不同季节的光谱差异最后一定要逐张看验证集预测图平均数只代表整体水平不代表单张图能用。这种过拟合在遥感二分类里非常隐蔽因为loss曲线完全正常不看图根本发现不了。4.4 现象四整图推理OOM现象输入一张两万像素宽的城区大图推理到一半显存爆掉或者程序直接被杀。原因是高分辨率遥感影像直接整图进网络任何分割模型都扛不住这个输入尺寸。解决方法是滑窗推理这是遥感图像处理里最成熟的方案把大图切成512×512的patchpatch之间保留64像素重叠推理完对重叠区域取平均值而不是最大值最后拼接成完整概率图。需要注意边界处理影像宽高往往不是stride的整数倍推理前用反射填充先把图pad到整数倍推理后再裁剪掉多余部分。滑窗参数里patch越小显存越省但速度越慢且更容易出现接缝错位512这个值是我试过速度和精度的平衡点。4.5 现象五模型把暗色屋面当成水体现象预测图里深色彩钢瓦屋顶、沥青操场被标成水体形状不规则但光谱接近水。原因是暗色屋面和深水的光谱特征几乎不可分这是纯像素信息的天然上限。解决要靠标注和几何规则标注时把常见的“类水背景”单独设一个类别训练成三类背景、水、类水背景让模型学会区分它们的细微纹理差异后处理再用几何特征做约束真实水体是连续面状且边界自然屋面则是孤立块状、边缘明显带直角可以用连通域分析剔除形状过于规整或面积过小的碎块。这两个手段配合屋面误检基本能压到可接受范围。5. 交付前验证用五张未训练图摸清模型的真实水分训练结束不代表能交付价值在验证。我的习惯是专门留出五张不参与任何训练和验证的影像做盲测选图时覆盖三个维度至少一张含密集高楼阴影至少一张含窄河道至少一张来自不同传感器或时相。下面是推理脚本的骨架配合第3章保存的best_water.pt就能出预测结果。# predict_large.py —— 高分辨率大图滑窗推理 import numpy as np import cv2 import torch def predict_large_image(model, img, patch512, overlap64): H, W, _ img.shape stride patch - overlap pad_h (stride - H % stride) % stride pad_w (stride - W % stride) % stride img_pad cv2.copyMakeBorder(img, 0, pad_h, 0, pad_w, cv2.BORDER_REFLECT) Hp, Wp img_pad.shape[:2] pred np.zeros((Hp, Wp), dtypenp.float32) count np.zeros((Hp, Wp), dtypenp.float32) for y in range(0, Hp - patch stride, stride): for x in range(0, Wp - patch stride, stride): crop img_pad[y:ypatch, x:xpatch] crop_t preprocess(crop) # 标准化到[0,1]与训练一致 with torch.no_grad(): out model(crop_t.unsqueeze(0)) pred[y:ypatch, x:xpatch] out[0, 0].sigmoid().cpu().numpy() count[y:ypatch, x:xpatch] 1 prob pred / np.maximum(count, 1) mask (prob[:H, :W] 0.5).astype(np.uint8) return mask参数说明overlap设为64是为了让每个像素至少被推理两次取平均后接缝处的预测会更平稳。0.5是默认分类阈值如果Observed漏检偏多把阈值降到0.4误检偏多则升到0.6这个要按你下游任务容忍度调。盲测后做两件事一是统计每张图的IoU和F1重点看漏报多于误报还是反过来——城市水体提取的下游通常是生态监测或排污口排查漏一条河比多报一个水坑严重得多如果Recall低于0.85就回去补细长河道样本二是做面积定量验证用0.8米分辨率的影像算水体面积等于水体像素数乘以0.64平方米把预测面积和现状水面资料对比误差在10%以内才算合格。我现在的习惯是把每张盲测图拼成“原图-标注-预测”三联图存档连同训练日志一起作为交付材料。数值可以修图骗不了人评审时直接翻图比读指标更有说服力。希望帮到你。本文还有配套的精品资源点击获取