简介面向遥感专业与深度学习方向的毕业设计开发者完整Python解决方案涵盖高分辨率城市遥感图像水体提取全流程包括数据预处理、模型训练、评估与单图测试等环节。项目基于深度学习实现涵盖U-Net与Attention U-Net等主流分割网络配套11个Python源码文件、13张网络结构示意图、1个测试集CSV、1份README文档说明以及训练好的pth权重模型共27个文件整体打包在约726KB的zip压缩包中目录结构紧凑、标注清晰便于快速部署复现。代码注释细致新手也能理解核心流程可直接作为毕业设计、期末大作业或课程设计的完整参考项目。已有199人学习下载适合需要优质开源项目与高分答辩素材的遥感、GIS相关专业本科生。1. 高分辨率城市遥感图像的水体提取一份能放进毕设的完整工程“高分辨率城市遥感图像的水体提取”这个标题本质是一套组合任务高分辨率影像获取、python源代码组织、数据集制作、文档说明。毕设拿到它目标不是把网络跑通出一张预测图而是要讲清楚为什么这样做、坑在哪里。城市水体以细长河流、小型坑塘、人工景观池为主宽度常只有几米30m分辨率的Landsat影像里它们只是一两个混浊像素。用高分二号、北京二号这类亚米级影像边界清晰了但阴影、黑屋顶、黑色路面又会与水体光谱混淆这个矛盾会贯穿整个方案设计。下面按数据准备、模型选型、训练避坑、指标评估的顺序把可落地的工程路径讲清楚并给出能直接改写的python代码。适合遥感、地信、计算机视觉方向的本科毕设人群也适合想快速上手遥感语义分割的从业者。2. 数据集准备城市水体样本从哪来、怎么切成模型能吃的尺寸城市水体提取看起来是模型问题但十次翻车有九次发生在数据上。模型训练前需要先回答三件事影像分辨率够不够、标注规则是否统一、切片策略会不会引入偏差。这一章按数据源选型、预处理、切片标注、增强的顺序讲代码部分用Python的rasterio与albumentations实现。2.1 数据源选型高分二号还是Sentinel-2如果题目自带数据集先用自带的高分辨率遥感图像和掩膜省事也便于文档对齐。没有自带数据时常见做法是先看影像地面分辨率。城市水体中的小坑塘、窄河涌在10m级影像里会退化成一线或一个点所以我建议优先用亚米级产品高分二号全色0.8m、多光谱3.2m融合后约1m北京二号、吉林一号也常用。如果搞不到亚米级退而求其次用Sentinel-2的10m波段但这类影像只适合提取主干河道和大型湖泊在论文里写“高分辨率”容易被质疑。数据来源方面学校购买的影像、导师课题数据或者遥感数据公开平台申请都可以作为来源重点是下载后先确认几何精度和波段顺序。2.2 预处理从原始影像到训练切片拿到原始GeoTIFF要处理的问题包括重投影、裁切和归一化。模型不看坐标系所以重投影未必非做不可但如果你想用矢量面做评估坐标系务必统一到WGS84。波段顺序也要确认我一般按蓝、绿、红、近红外排列。下面这段代码把16位整型影像切成0~1浮点并生成训练切片。import rasterio import numpy as np # 按波段顺序读取这里的1、2、3、4对应蓝、绿、红、近红外 with rasterio.open(urban_scene.tif) as src: image src.read([1, 2, 3, 4]) profile src.profile image image.astype(np.float32) # 高分影像动态范围常见是0~2047线性拉伸到0~1 min_val, max_val 0.0, 2047.0 image (image - min_val) / (max_val - min_val) image np.clip(image, 0.0, 1.0)这段代码先读取四个波段转成float32是为了避免后续NDWI计算时整数溢出。线性拉伸到0~1后模型输入范围就固定了推理时要用同一套min_val和max_val否则同一景影像不同切片会出现亮度不一致。有些影像已经做了辐射定标DN值范围可能是0~65535那就把max_val改掉不要想当然用2047。如果只有RGB三波段就把读取波段改成[1,2,3]后续模型输入通道也相应调整。接着做滑动窗口切片。城市细碎水体容易被切片边缘切断所以我会让相邻切片保持50%重叠也就是stride取patch_size的一半。256x256的切片配128的步长既保留了重叠又不会让切出来的样本数量爆炸。def sliding_window(image, maskNone, patch_size256, stride128): h, w image.shape[1], image.shape[2] num_c image.shape[0] patches [] mask_patches [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch image[:, y:ypatch_size, x:xpatch_size] patches.append(patch) if mask is not None: m mask[y:ypatch_size, x:xpatch_size] mask_patches.append(m) if mask_patches: return np.stack(patches), np.stack(mask_patches) return np.stack(patches), Nonestride128意味着左右和上下都只有一半区域是新内容。推理时会用到同样的窗口设置但推理阶段overlap的作用是让边界处的预测更稳定这里先不展开。切片完成后还有一个关键动作过滤掉那些水体像素占比太低的切片。keep_idx [] for i, m in enumerate(mask_patches): ratio (m 0).sum() / m.size if ratio 0.05: keep_idx.append(i) patches patches[keep_idx] mask_patches mask_patches[keep_idx]0.05这个阈值是我试过几次后留下的经验值。城市影像里水体占比可能只有1%~3%如果阈值设成0.1训练样本会少一大半设成0.01又会让大量只有几个水体像素的切片参与训练导致模型被背景淹没。这个数字不是固定的可以先统计所有切片的ratio分布再取中位数附近的值。2.3 掩膜标注规则边界松紧比画得漂亮更值钱数据自带mask的话第一步是检查标注规则。常见问题有三种有人把水草覆盖区域标成水体有人把桥下阴影标成水体有人标到水陆交界处时手抖留了一圈“过渡带”。这些不一致会直接拉低验证集指标而且模型会学到错误边界。我一般建议统一成“只标可见水面”的规则。水草、浮萍、桥下阴影、岸线植被遮挡都不算水体。这样虽然mask看起来没那么“完整”但模型学到的光谱特征更纯。标注工具用QGIS或LabelMe都行QGIS里勾完多边形后可以直接栅格化输出与影像分辨率一致的GeoTIFF。不管用什么工具最后都要转成单通道二值掩膜1代表水体0代表背景。格式上建议存成uint8的单通道GeoTIFF或npy文件不要存成RGB三通道的PNG否则后处理读取时要额外转换。2.4 数据增强先想清楚哪些操作不翻车水体提取的增强顺序比增强本身更重要。水平翻转、垂直翻转、随机旋转90度都不改变像素的光谱关系可以放心加。随机亮度、对比度增强要保守因为城市水体与阴影、深色路面的光谱差异本来就不大把亮度扰动拉大模型可能把更暗的阴影也当成水。如果后面把NDWI作为额外通道输入亮度增强会直接破坏NDWI的一致性这种情况干脆关掉亮度类增强。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.RandomBrightnessContrast( brightness_limit0.1, contrast_limit0.1, p0.5 ), ])这里的RandomBrightnessContrast只允许10%的幅度而且概率只有0.5。我见过不少同学把brightness_limit设到0.3结果模型在训练集上表现很好验证集上却把大片阴影识别成水原因就是增强后的训练样本里深色阴影的亮度分布已经和水体重叠了。另外要注意RandomRotate90和随机裁剪不要同时用尤其是裁剪比例太小时会把细长河流从中间截断生成大量只有半截河道的切片模型很难学出连续边界。3. 模型与Python训练脚本U-Net还是DeepLabV3我选它模型选型这一步没有太多玄学城市水体提取是典型的语义分割任务目标是像素级二分类。U-Net和DeepLabV3都是成熟方案但毕设场景我更推荐U-Net原因很直接代码短、显存占用低、对细碎小目标友好。3.1 为什么U-Net是城市水体提取里的永动机U-Net的核心是跳跃连接它把编码器每一层的浅层特征直接拼到解码器对应层让模型同时保留高分辨率边界细节和低分辨率语义信息。城市水体里最值钱的恰恰是小河沟、小坑塘的边界这些信息很容易在连续下采样中丢光跳跃连接相当于给解码器开了一条“快车道”。DeepLabV3的空洞卷积扩大感受野适合大目标、多类别分割但小水体本身只需要局部上下文过大的感受野反而可能把周围阴影信息卷进来造成误判。从工程角度看U-Net用普通卷积就能实现不需要加载预训练权重也能跑出可接受的结果这对没有GPU服务器的学生会友好很多。3.2 一个能直接改的U-Net模型定义下面这个精简版U-Net用PyTorch实现输入通道默认4对应蓝绿红近红外输出用sigmoid得到概率图。import torch import torch.nn as nn 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), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels4, num_classes1): super().__init__() self.enc1 DoubleConv(in_channels, 32) self.enc2 DoubleConv(32, 64) self.enc3 DoubleConv(64, 128) self.enc4 DoubleConv(128, 256) self.pool nn.MaxPool2d(2) self.up4 nn.ConvTranspose2d(256, 128, kernel_size2, stride2) self.dec4 DoubleConv(256, 128) self.up3 nn.ConvTranspose2d(128, 64, kernel_size2, stride2) self.dec3 DoubleConv(128, 64) self.up2 nn.ConvTranspose2d(64, 32, kernel_size2, stride2) self.dec2 DoubleConv(64, 32) self.out nn.Conv2d(32, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) d4 self.dec4(torch.cat([self.up4(e4), e3], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e2], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e1], dim1)) return torch.sigmoid(self.out(d2))通道数从32逐步加深到256显存开销比标准U-Net小很多。如果显存紧张可以把最底层的256改成128但不要在编码器第一层就开始256那样小目标细节会在第一层就被压缩掉。forward里的dim1是通道维因为这里输入是[N, C, H, W]和PyTorch默认布局保持一致。最后这个torch.sigmoid很关键它把网络输出压到0~1后续阈值调整、Dice Loss计算都要依赖这个概率值。3.3 损失函数BCE、Dice Loss还是Focal Loss城市水体像素占比低直接用BCE Loss很容易出现全背景预测。BCE是逐像素独立计算背景样本多梯度就被背景带跑而Dice Loss是把预测和真值当作一个整体直接优化区域重叠程度对类别不平衡更稳定。我见过一个做法是只用Dice Loss训练初期loss波动很大更稳妥的是把BCE和Dice按0.5的权重组合起来。def dice_loss(pred, target, smooth1.0): pred pred.contiguous().view(pred.size(0), -1) target target.contiguous().view(target.size(0), -1) intersection (pred * target).sum(dim1) dice (2.0 * intersection smooth) / (pred.sum(dim1) target.sum(dim1) smooth) return 1.0 - dice.mean() alpha 0.5 loss alpha * F.binary_cross_entropy(pred, target) (1 - alpha) * dice_loss(pred, target)smooth的作用是防止分母为零一般取1.0。alpha这个权重可以微调如果发现训练loss很稳但IoU不涨把alpha往0.4调如果模型预测出的水体区域特别碎把alpha往0.6调让BCE帮忙稳定边界。Focal Loss也可以解决问题但它的两个超参数gamma和alpha调起来比dice组合更依赖经验毕设时间有限的话不建议从它开始。3.4 训练循环与关键超参数训练脚本不要一次写太长核心是优化器、学习率调度和模型保存。我习惯用AdamW学习率设1e-4weight_decay设1e-5。调度器用ReduceLROnPlateau监控验证IoU当指标连续5轮不涨时学习率减半。import torch.optim as optim optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.5, patience5 ) best_iou 0.0 for epoch in range(60): model.train() for img, mask in train_loader: img img.to(device) mask mask.to(device) pred model(img) loss combined_loss(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() val_iou validate(model, val_loader) scheduler.step(val_iou) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_water_extract.pth)这里有几个参数值得细说weight_decay从1e-4降到1e-5避免正则化过重把细碎水体全抹掉ReduceLROnPlateau的modemax表示我们监控IoU指标越高越好如果你不小心写成modemin学习率会在IoU升高时反而变大patience5表示容忍5轮验证指标连续不涨太短会让学习率过早下降太长又浪费时间。模型保存用“验证集IoU最大”而不是“最后一个epoch”否则过拟合权重会被原封不动留着演示现场对着一幅全是噪点的预测图就尴尬了。Batch size常规取8显存不够时优先把切片从256降到224或192不要硬塞16的batch否则会爆显存。3.5 文档说明怎么写才像高分的交付物毕设评审最忌讳的是“代码能跑但没人看得懂”。文档说明不需要像软件说明书那样厚但必须让答辩老师顺着思路走一遍。常见做法是写一个README.md结构分四块第一块写环境依赖列出Python版本和torch、rasterio、albumentations、scipy这几个核心库第二块写数据目录结构标注哪些是原始影像、哪些是切片、哪些是mask第三块写运行顺序preprocess.py、train.py、predict.py三步每个脚本用一行python命令启动第四块写评价指标定义和实测结果。代码注释不要每行都写要在关键位置写清“为什么”。例如损失函数里那行alpha0.5注释说明“用于平衡BCE与Dice水体占比低时保持0.5附近”。这就是一份及格的文档说明。4. 城市水体提取会翻车的五个场景避坑与排查高分辨率城市影像里的水体提取我做下来最深的感受是模型好坏只占一半另一半是光谱混叠处理。下面这五个翻车场景都是实际过程中反复出现的按“现象、原因、解决”写照着排查能省很多时间。4.1 阴影被当成水体验证集IoU虚高现象训练时loss下降正常预测图上建筑阴影被大片识别为水而且验证集IoU数字还不低因为阴影恰好把很多真实水体周围区域盖住了误判被“藏”在正确区域里。原因阴影在可见光波段亮度低色相偏暗蓝与水体的光谱曲线接近城市高分辨率影像里阴影面积又大模型很容易把“暗区域”学成“水”。解决最直接的方法是给模型增加一个NDWI先验通道绿光与近红外的比值能让大部分阴影露馅。另外在训练数据里多裁一些纯阴影的负样本切片让模型看到“看起来像水但不是水”的像素。如果这两步都做了还是误判对预测结果做连通域分析把与建筑边缘紧密贴合的暗色斑块排除。4.2 柏油路和深色屋顶把模型带偏现象预测掩膜中出现条状、块状的黑色斑块位置正好对应刚铺的柏油路和深色屋顶。原因这些目标在水体的可见光光谱里同样表现为低反射模型学到的不是“水”而是“暗色平滑区域”。解决我一般会在训练数据里专门加一批“硬负样本”只包含道路、屋顶、停车场掩膜全为0强制模型学习它们与水的差异。后处理层面如果手头有城市道路矢量数据直接把路面范围从预测结果中屏蔽掉没有矢量数据就用形态学开运算把细长条噪声消除但开运算核不能太大否则河涌也会被拦腰切断。4.3 水体占比太小模型直接学成“全黑输出”现象训练了十几个epochloss稳定下降但预测概率图几乎全0验证集IoU为0。原因图像中水体像素占比可能不到1%BCE Loss逐像素独立计算背景像素的梯度吞掉了前景模型发现“全预测背景”也亏不了多少。解决先换Dice Loss或BCEDice组合Dice对整个区域重叠敏感背景再大也骗不了它。然后回到数据切片把水体占比低于5%的切片过滤掉让训练loader里前景比例稳定在10%以上。如果样本数量不足可以把包含水体的切片在batch里重复采样用WeightedRandomSampler实现。4.4 切片边界出现棋盘格状的拼缝现象整景影像预测完成后拼接每条拼缝处水体被拦腰截断像棋盘格。原因推理时滑动窗口没有重叠模型看到的每个切片里水体都是断头的它自然倾向于预测“不连续”。解决推理阶段把stride设成patch_size的一半让相邻切片有重叠重叠区域取多次预测的平均概率。如果想让边界更自然可以在重叠区域用三角权重加权平均中间概率置信度高边缘置信度低。注意训练时的切片也有overlap但那是为了增加样本不会造成拼缝。4.5 验证集IoU和目视效果完全对不上现象验证集IoU到了0.85放大预测图却发现边界锯齿、小坑塘漏检和高分影像的视觉效果对不上。原因IoU对大目标不敏感城市水体以小目标为主漏掉几个小坑塘对总IoU影响很小但答辩时老师一眼就能看到。解决把验证指标拆成多组单独统计“只含小水体的切片”上的Recall也就是漏检率同时把不包含水体的背景切片从验证集中去掉它们会整体抬高IoU。评估报告里同时放IoU和F1必要时放Precision-Recall曲线这比只写一个IoU值有说服力得多。提示以上五类问题不是互相独立的阴影和道路误判经常同时出现排查时先看验证集里最典型的错误图再决定改数据还是改策略不要盲调学习率。5. 评估与后处理IoU、F1和让结果能看的形态学操作模型训练完成后还没有真正结束预测结果是概率图要变成能放进论文的整景水体分布图还需要做三项工作算清指标、选对阈值、处理后处理。这三项建议做成一个独立的eval.py脚本固定下来每次实验都跑同一条链路。5.1 评价指标计算代码IoU和F1是毕设里最常用的两个指标。IoU描述预测与真值的区域重叠程度F1描述Precision和Recall的平衡关系。不要自己实现一版、sklearn实现一版最后两边数字对不上。下面这个写法先用NumPy计算混淆矩阵逻辑清晰也和PyTorch预测结果直接兼容。import numpy as np def iou_score(pred, target, threshold0.5): pred_bin (pred threshold).astype(np.uint8) target_bin (target 0).astype(np.uint8) intersection (pred_bin target_bin).sum() union (pred_bin | target_bin).sum() return intersection / (union 1e-8) def precision_recall_f1(pred, target, threshold0.5): p (pred threshold).astype(np.uint8) t (target 0).astype(np.uint8) tp (p t).sum() fp (p (~t.astype(bool))).sum() fn ((~p.astype(bool)) t).sum() precision tp / (tp fp 1e-8) recall tp / (tp fn 1e-8) f1 2 * precision * recall / (precision recall 1e-8) return precision, recall, f1这段代码里pred是模型的概率输出target是二值掩膜。注意target可能不是严格的0/1而是0/255所以统一用target 0来转成bool。threshold默认0.5但后面会说到这个默认值不一定最优。1e-8是为了避免除零纯Python实现里很有必要。5.2 阈值怎么调才不是玄学通过sigmoid输出的概率图通常不是接近0就是接近1但阴影和道路处会出现大量0.3到0.7的模糊概率。默认0.5只是惯例不是定理。训练数据类别不平衡时最优阈值常常偏向0.3这一侧。我用验证集上Precision-Recall曲线来确定阈值from sklearn.metrics import precision_recall_curve f, t labels.flatten(), probs.flatten() precision, recall, thresholds precision_recall_curve(f, t) f1 2 * precision * recall / (precision recall 1e-8) best_thr thresholds[np.argmax(f1)] print(best threshold:, best_thr)sklearn的precision_recall_curve返回的thresholds长度比precision少一个直接取argmax(f1)时要注意对齐但多数情况下f1数组比thresholds多一个元素多出来的位置对应recall1、precision0的起点不影响argmax结果。如果算出的best_thr偏离0.5太多比如超过0.7说明模型输出概率整体偏高需要检查训练时的归一化和推理时是否一致。5.3 形态学后处理小噪点和小洞的后悔药预测图里的噪声表现为两类孤立的小斑块和大水体内部的小空洞。前者是误检后者是漏检。用scipy.ndimage的开闭运算就能处理不需要引入OpenCV那么重的依赖。from scipy import ndimage def postprocess(pred, thr0.5, min_area50): binary (pred thr).astype(np.uint8) binary ndimage.binary_opening( binary, structurenp.ones((3, 3)) ).astype(np.uint8) binary ndimage.binary_closing( binary, structurenp.ones((3, 3)) ).astype(np.uint8) labels, num ndimage.label(binary) for i in range(1, num 1): if (labels i).sum() min_area: binary[labels i] 0 return binary3x3结构元素做开运算能去掉1~2像素的孤立噪点闭运算填补水体内部的小空洞。min_area按影像分辨率调整1m分辨率时50像素大约对应50平方米适合过滤掉几十平方米的小噪块如果提取对象是主干河道可以调到200以上但也要小心丢掉真正的独立小坑塘。顺序上先开运算后闭运算不要反。反过来容易先填洞再扩噪点。5.4 拼接整景影像时的内存技巧一景高分影像可能有两万乘两万像素直接整张读进显存会爆。常见做法是分块推理再用rasterio按窗口写入结果。窗口写入的好处是整个过程不保留整景概率数组内存占用只取决于一个块的大小。import rasterio from rasterio.windows import Window out_profile src.profile.copy() out_profile.update(dtypenp.uint8, count1) with rasterio.open(result.tif, w, **out_profile) as dst: for y in range(0, height, 512): for x in range(0, width, 512): # 这里的predict_on_window返回一个512x512的二值数组 patch_bin predict_on_window(y, x, model, transform) dst.write(patch_bin, 1, windowWindow(x, y, 512, 512))Window的参数顺序是列偏移、行偏移、宽、高别写成行偏移、列偏移这是最常见的低级翻车点。写文件前确认out_profile里的count1否则可能写出四波段结果ArcGIS打开时什么都看不见。分块大小512是平衡速度与内存的一个值如果你的模型只能吃256x256那就分块256并保持块间overlap拼接时对重叠区取平均。6. 进阶玩法NDWI先验融合与多尺度推理让精度再上一个台阶基础流程跑通后如果想在答辩里拿出更亮眼的结果把NDWI先验融合和多尺度推理加进去是性价比最高的两个技巧。6.1 把NDWI变成第5个通道NDWI的计算公式是(绿光-近红外)/(绿光近红外)水体在这种变换下显著为正而阴影、道路、屋顶会被压缩到负值或零附近。直接把NDWI归一化后作为额外通道送入U-Net比让它自己从原始波段学出水体特征快得多。green image[1].astype(np.float32) nir image[3].astype(np.float32) ndwi (green - nir) / (green nir 1e-8) ndwi (ndwi 1) / 2 image np.concatenate([image[:4], ndwi[None, ...]], axis0)把NDWI缩放到0~1是为了让模型输入范围一致。注意这一步要在数据增强之外单独处理而且训练和推理必须用同一个计算流程。如果做了NDWI通道之前的RandomBrightnessContrast增强必须关掉否则NDWI数值会被扰乱等于给模型灌了噪音。6.2 多尺度预测和消融记录城市水体尺度差异很大大湖面需要较大感受野小河沟需要局部细节。常见做法是同一份输入分别以0.5、1.0、1.5倍尺度推理再插值回原始尺寸取平均。代价是推理时间变为三倍但IoU往往能提升1到2个百分点。def predict_ms(model, img, scales(0.5, 1.0, 1.5)): probs [] h, w img.shape[-2:] for s in scales: nh, nw int(h * s), int(w * s) resized F.interpolate(img, size(nh, nw), modebilinear, align_cornersFalse) with torch.no_grad(): p model(resized) probs.append(F.interpolate(p, size(h, w), modebilinear, align_cornersFalse)) return torch.stack(probs).mean(0).cpu().numpy()多尺度的收益在小目标上尤其明显但显存不足时不要一次性把三张图都放进GPU可以跑完一个尺度释放一个尺度用CPU列表暂存结果。最后说说记录习惯我会把“基础U-Net”“U-NetNDWI”“U-NetNDWI多尺度”三个实验的IoU、F1、推理时间列成一张表写进文档说明里。这张消融表比任何花哨的网络结构都更能说服评委因为每一项改动都有可量化的贡献。做完这两个进阶点整个毕设的技术路线就有了闭环。希望帮到你。本文还有配套的精品资源点击获取