简介基于Python深度学习实现高分辨率城市遥感图像的水体提取系统是一套包含源码与文档的完整毕业设计面向计算机视觉、遥感相关专业学生适用于毕业设计、期末大作业及课程设计场景。项目整合了模型定义、训练与评估脚本、数据预处理工具、已训练权重及详细说明代码注释清晰便于新手理解与二次开发。资源共27个文件包括11个Python脚本模型构建、训练、测试及单图预测、13张网络结构示意图如U-Net、AttU-Net、1个CSV结果表、1个Markdown文档和1个模型权重文件压缩包总大小约726KB结构紧凑部署便捷。目前已有144人学习项目经过严格调试可稳定运行系统功能完善、操作界面直观能为快速搭建遥感水体提取应用提供切实参考。1. 城市遥感图像的水体提取系统这个毕业设计到底在解决什么问题每年汛期城市内涝应急需要快速摸清积水范围河湖监管要识别新增水面土地利用变化监测要先剔除水体干扰。这些任务落到高分辨率遥感影像上本质都是同一件事把每个像元判断成水或非水。高分辨率城市影像里水体被建筑物阴影、立交桥、黑色沥青路面干扰得很严重传统阈值法容易翻车而基于python深度学习的语义分割模型要稳得多。毕业设计选这个题目优势是数据来源多、可视化效果直观、工作量和难度可控搭配源码和文档说明能完整覆盖从数据准备、模型训练到结果分析的全部闭环。这篇笔记会把整个落地路径讲清楚数据怎么来、标注怎么做、模型怎么选、训练参数怎么设、最容易在哪里踩坑。2. 水体遥感提取的数据底子传统阈值法为什么在城区失效深度学习到底在学什么动手写代码之前得先想明白一个问题NDWI归一化差异水体指数这种经典方法也能提取水体为什么还要上深度学习我的答案是NDWI 在高分辨率城市影像里的误检太严重。建筑物阴影在近红外波段的表现和水体非常相似黑色沥青路面、立交桥下方的暗区也全都会被算成水。深度学习分割模型本质上是在学习一个非线性映射把每个像素周围一小块邻域的光谱与纹理特征映射成类别概率它同时利用了光谱信息和空间上下文——比如知道阴影通常紧贴建筑物而水体往往呈带状延伸或被河岸约束。这就是它在城区场景里比阈值法更可靠的原因。2.1 水体在遥感影像上的光谱特征与空间特征水体在可见光到近红外波段有一个核心特征对近红外光的吸收极强反射率低在短波红外波段吸收更强。植被恰恰相反叶绿素在近红外波段的反射率很高所以植被和水体在近红外波段的差异非常明显。城区里的阴影区因为没有直接光照各波段的反射率都低在水体指数上会形成假阳性这是误检的主要来源。空间特征则补足光谱的不足水体很少呈孤立点状存在河流呈条带、湖泊呈面状边缘相对平滑建筑物阴影的形状则严格跟随建筑物轮廓多为矩形或不规则块状。基于这些特征我在挑选数据时会做一个判断如果主要做城区提取只靠光谱信息远不够必须让模型有足够的感受野去看到上下文。这也是为什么 U-Net 这类编码器-解码器结构比逐像素分类器更适合这个任务。编码器通过逐级下采样扩大感受野解码器逐级恢复分辨率在最后一层通过跳跃连接把浅层细节拼回来。水体边界往往很窄比如小河沟、池塘边沿浅层特征里才有这些细节所以跳跃连接在这里不是可有可无的加分项而是保命设计。2.2 数据源选择高分影像、公开数据集与自备数据的取舍常见的数据源有三条路公开遥感数据集、自己下载高分辨率影像后标注、学校或导师提供的课题数据。公开数据集里有一些城市分割和数据集的介绍例如 DeepGlobe 数据集包含卫星图像分割任务部分类别涉及水但它的分辨率和城市场景覆盖不一定满足毕设需求。自己下载影像更常见高分一号、高分二号、哨兵二号都可以用但要注意高分二号全色分辨率 0.8 米、多光谱 3.2 米需要先做融合哨兵二号是 10 米分辨率对城市小水体来说偏粗只能做大体量的河流湖泊。我个人的建议是如果导师能提供数据优先用课题数据因为真实应用场景的标注规范往往已经在课题里定义好了如果没有就从 Google Earth 或公开地理数据平台下载 0.5 到 2 米分辨率的影像。分辨率的底线是 1 米左右低于这个水平城市里宽度不到 10 米的小河沟就只剩一两个像元标注和预测的难度都会陡增。如果自己下载建议把原始影像设置为 RGB 三个波段就够了很多预训练分割模型是基于 RGB 图像训练的用多光谱需要自己改模型输入通道复杂度会上升一大截毕设阶段不划算。2.3 预处理步骤几何校正与裁剪的先后顺序预处理里最容易出问题的是坐标系。标注掩膜和原始影像如果不做投影对齐模型训练时图像和标签错位轻则精度暴跌重则完全学不到东西。我的常规处理顺序是先做几何校正确保待标注影像的坐标是准确的再统一投影到同一个坐标系比如 WGS84 或 UTM 投影最后才是裁剪成训练切片切片这一步在第三章展开。辐射定标和大气校正在水体提取里可以不做的场景也很多如果训练集和预测集来自同一个数据源、同一天的成像条件跳过大气校正不会造成明显精度损失。但如果你混用了不同时期的影像做训练色调差异会干扰模型这时候至少要做一次简单的直方图匹配。实践中我会用一个脚本统一切片和配准核心是保证原始影像和标注掩膜在裁剪时使用完全相同的切片坐标避免任何一个像素的偏差。处理流程落到代码上大概是import rasterio from rasterio.windows import Window import numpy as np from pathlib import Path def crop_image_and_label(image_path, label_path, out_dir, patch_size512, overlap64): 对影像和对应掩膜按相同窗口裁剪overlap 用于保留边缘上下文。 with rasterio.open(image_path) as img_src, rasterio.open(label_path) as lbl_src: width, height img_src.width, img_src.height step patch_size - overlap for y in range(0, height - patch_size 1, step): for x in range(0, width - patch_size 1, step): win Window(x, y, patch_size, patch_size) img_array img_src.read([1, 2, 3], windowwin) lbl_array lbl_src.read(1, windowwin) # 跳过标签里没有任何水体像元的切片减少训练时的无效样本 if np.count_nonzero(lbl_array) 0: continue out_img_path out_dir / fimg_{y}_{x}.tif out_lbl_path out_dir / flbl_{y}_{x}.tif write_tif(img_array, out_img_path, img_src, win) write_tif(lbl_array, out_lbl_path, lbl_src, win)这段代码里step patch_size - overlap保证相邻切片之间有重叠区。重叠不是为了增加样本量而是为了让切片边缘的语义信息不丢失预测阶段用同样的重叠再做拼接时能够通过平均重叠区域的预测概率消除接缝效应。skip everything with no water pixels这一步很实用遥感影像里水体往往只占一小部分如果全图滑窗切片大量切片标签都是全零背景模型很快会陷入“什么都预测成背景”的退化状态。3. 从影像到训练数据集切片尺寸、数据增强和目录结构怎么定把大影像切成小切片只是第一步。切片尺寸、数据增强策略、目录组织方式直接影响后面训练的稳定性和最终分割精度。我在做过几次遥感分割项目之后总结出一个经验遥感图像分割的数据准备阶段决定了上半场模型结构只决定下半场而大部分翻车事故都是在切片的环节埋下的雷。3.1 切片尺寸256、512 还是 1024切片尺寸会影响两个因素显存占用和感受野。小切片如 256×256 在显存受限的 GPU 上能跑更大的 batch但每张切片包含的空间上下文太少模型容易把小块阴影当成水。大切片如 1024×1024 上下文信息足但显存占用大batch 必须调小训练稳定性变差。我通常把 512×512 作为默认值在 RTX 3060 12G 或 2080Ti 这类显卡上batch size 设 8 左右能跑 U-Net 或 DeepLabV3不会频繁爆显存同时 512 的尺寸对城市水体来说足够看到河流的走向和周边的建筑物关系。如果显卡只有 6G 显存降到 384 或 256 是更现实的选择。尺寸确定后要注意patch_size必须能被下采样倍数整除。U-Net 的原始版本下采样 5 次每次 stride 2输入尺寸如果不是 32 的倍数最后一个尺度的特征图尺寸对不上模型会报 shape 错误。用 DeepLabV3 的 ResNet 骨干时下采样倍率通常是 16所以切片尺寸要能被 16 整除。这不是什么深奥问题但每次换模型骨架时都会遇到一次属典型的“看着模型对、跑起来就错”的坑。3.2 数据增强的强度摇一摇就好别摇过头水体提取的标注成本很高样本量通常只有几百到几千张切片不靠数据增强等于主动放弃。常用增强包括水平翻转、垂直翻转、随机旋转 90 度、随机亮度对比度调整、高斯模糊。其中翻转和旋转是免费的因为语义分割的标签也跟着做同样的几何变换不会产生语义错误。亮度调整要谨慎水体本身受光照影响很大但过度改变亮度会破坏“阴影是深色区块”这一重要线索导致模型学会把很暗的区域都当成水。我一般把亮度对比度增强的概率设为 0.3强度不超过 0.15这样既增加了扰动又不至于把水体光谱特征洗掉。Albumentations 库里有遥感分割专用的增强集合代码写起来也非常简洁import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.RandomCrop(512, 512), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.25), A.RandomRotate90(p0.5), A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.3), A.GaussNoise(var_limit(10, 50), p0.2), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) val_transform A.Compose([ A.Resize(512, 512), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ])这个配置里RandomCrop放在了Resize之前训练时直接在大影像或大图上随机裁块来替代固定切片变相增加了样本多样性。Normalize使用的是 ImageNet 的均值方差因为多数预训练编码器的权重是在 ImageNet 上预训练的输入分布保持一致才能让预训练权重发挥价值。如果你打算从头训练可以不规范化也可以用数据集的真实均值方差代替但从头训练在小样本场景下效果通常更差我不推荐。3.3 数据目录结构与 Dataset 加载数据组织方式要兼顾训练脚本的简单性和可读性。我的目录结构固定是这样的dataset/ ├── train/ │ ├── images/ │ ├── masks/ ├── val/ │ ├── images/ │ └── masks/ ├── test/ │ ├── images/ │ └── masks/这样的好处是 PyTorch 的ImageFolder或自定义Dataset类实现起来都不复杂而且验证集、测试集和训练集天然分离避免数据泄漏。注意masks目录里的文件必须和images里的文件名一一对应我在项目里见过把 mask 后缀命名为.png、图像命名为.tif的方式虽然文件名不同但前缀一致也能对齐但还是推荐格式完全一致、仅目录区分因为某些第三方可视化工具和标注软件导出时会按文件名排序不同后缀会导致顺序错位到时候排查起来很痛苦。import torch from torch.utils.data import Dataset from PIL import Image from pathlib import Path class WaterDataset(Dataset): def __init__(self, images_dir, masks_dir, transformNone): self.images_dir Path(images_dir) self.masks_dir Path(masks_dir) self.image_paths sorted(p for p in self.images_dir.glob(*.tif)) self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] mask_path self.masks_dir / img_path.name image np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) mask (mask 128).astype(np.float32) # 二值化水1背景0 if self.transform: augmented self.transform(imageimage, maskmask) image, mask augmented[image], augmented[mask] return image, mask.unsqueeze(0)mask.unsqueeze(0)这行很重要。语义分割任务的标签通常不像图像分类那样取一个标量而是保持原图尺寸的二维矩阵。PyTorch 期望的输入是[B, C, H, W]格式所以 mask 需要加一个通道维度。(mask 128)这行把标注图转成 0/1 掩膜假设标注软件用白色或亮色表示水体。如果你用 LabelMe 之类的工具标完导出的是多边形 JSON还要先栅格化成掩膜图这一步不复杂但容易漏后面会说。4. 分割模型选型与训练配置U-Net、DeepLabV3、损失函数与指标参数模型选型可能是整个毕设里最受关注的部分但说实话对于水体提取这个任务模型结构的选择空间并不大。更关键的是把所选模型的训练配置调对。我见过很多人拿着 DeepLabV3 却全程用默认参数训练 100 个 epoch 出来效果还不如 U-Net 训练 50 个 epoch这不是模型不行是参数没做适配。4.1 U-Net 和 DeepLabV3 怎么选U-Net 和 DeepLabV3 是遥感语义分割里用得最多的两类模型。U-Net 的结构对称、参数量少、训练收敛快在小样本和单类分割场景下表现很稳。DeepLabV3 用了空洞卷积和 ASPP 模块多尺度特征融合能力更强对尺度变化大的目标更友好但模型更大、训练更慢对显存要求也更高。水体提取通常是单类二分类目标形态相对简单我自己的经历是 U-Net 往往够用而且训练时间短调参迭代快更容易在有限时间内跑完整个毕设周期。如果数据集里同时包含大江大河和细窄沟渠尺度差异很大这时换 DeepLabV3 或 U-Net 才会看到明显收益。如果不需要从零写模型直接用现成的分割库就行常见的分割模型库如 segmentation-models-pytorch 里封装了多种结构代码简洁。但要注意水体提取只有一个前景类输出通道数应为 1配合 sigmoid 激活使用很多教程默认用多类分割的输出通道数如num_classes21之类的设置照抄过来会出现输出维度不匹配或 sigmoid 应用错位的问题。4.2 损失函数BCE、Dice 和 Focal 的组合逻辑水体提取的类别不平衡问题非常突出影像中水体占比通常只有 5% 到 15%背景像素占据绝大多数。直接使用 BCE二分类交叉熵会让模型倾向把所有像素预测为背景因为即使这样做整体损失也降得很低。解决思路是使用 Dice Loss 或 Focal Loss 来惩罚这种退化。Dice Loss 直接优化 DICE 系数对小目标更敏感Focal Loss 通过调制因子让模型关注难分样本对边缘和模糊区域的效果更好。在实践里我把两种损失结合起来import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth1e-6): super().__init__() self.smooth smooth def forward(self, pred, target): pred torch.sigmoid(pred).contiguous().view(-1) target target.contiguous().view(-1) intersection (pred * target).sum() dice (2.0 * intersection self.smooth) / (pred.sum() target.sum() self.smooth) return 1 - dice class CombinedLoss(nn.Module): def __init__(self, alpha0.6): super().__init__() self.alpha alpha self.bce nn.BCEWithLogitsLoss() self.dice DiceLoss() def forward(self, pred, target): bce_loss self.bce(pred, target) dice_loss self.dice(pred, target) return self.alpha * bce_loss (1 - self.alpha) * dice_lossalpha这个超参数值得微调。我倾向于 0.6 到 0.7 的取值BCE 占比高一些可以提供更稳定的梯度Dice 占比低一些可以避免训练初期的震荡。刚开始调参时如果发现 loss 曲线剧烈波动第一步不是改学习率而是看看 dice 损失的权重是不是设得太高了。Dice Loss 在预测完全错误比如全 0时梯度不稳定smooth项不能省否则偶尔会报除零错误。4.3 训练参数学习率、batch size、epoch 和早停学习率是最值得调的参数。用 ImageNet 预训练编码器时我通常把编码器部分的学习率设为解码器的 0.1 倍因为预训练权重的特征已经很好了更新太快反而会破坏学到的通用特征。Adam 优化器的默认学习率 1e-3 偏高配 Dice Loss 后前期容易震荡我更常用 3e-4 或 5e-4。如果显存允许batch size 尽量不低于 4否则批归一化层的统计量漂移严重训练曲线会上下抖动得厉害。早停策略也不要省。按 epoch 记录验证集 DICE 或 IoU连续 15 到 20 个 epoch 没有提升就停止同时保存历史最佳权重。这里有一个很实用的技巧把“历史最佳”的判断标准设为验证集 IoU 而不是 DICE虽然它们相关但 IoU 更贴近任务最终的评价尺度很多论文都用 IoU 作为分割任务主指标。训练结束后用最佳权重重新跑一遍测试集得到的结果才是你写进毕业论文里的指标。PyTorch 训练循环的骨架如下我把它保留为最小的可运行版本criterion CombinedLoss(alpha0.6) optimizer torch.optim.Adam(model.parameters(), lr3e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemax, factor0.5, patience8) best_iou 0.0 patience_counter 0 for epoch in range(100): model.train() train_loss 0.0 for images, masks in train_loader: images, masks images.cuda(), masks.cuda() preds model(images) loss criterion(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() train_loss loss.item() val_iou evaluate_iou(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) patience_counter 0 else: patience_counter 1 if patience_counter 15: print(fEarly stop at epoch {epoch}) breakReduceLROnPlateau在验证指标连续 8 个 epoch 不上升时把学习率减半这种温和的衰减策略比固定步长衰减更省心不用事先预估总 epoch 数。evaluate_iou在验证时需要注意的是关闭梯度计算否则推理阶段占用显存之外还可能影响 batchnorm 的统计状态养成验证循环里面写torch.no_grad()的习惯没有坏处。5. 水体提取系统避坑指南从数据处理到模型推理的 5 个高频踩坑点这部分写下的坑全部来自真实项目经历。每一件都用“现象→原因→解决”的方式来记录便于在出问题时直接按索引排查。5.1 染色溢出现象预测结果里出现大量连片假阳性现象模型预测结果中大块阴影区域被误判为水尤其在高楼密集的城区影像上很明显。整体 IoU 看起来不错但是目视结果里黑乎乎的一大片完全没法交付。原因训练样本中水体与阴影样本的比例失衡。我标注的时候下意识地把肉眼可见的阴影排除了但标注很难做到全覆盖模型通过有限样本学到的规律是“深色且大面积的区域就是水”没有学到“阴影紧邻建筑物”这一空间规律。解决一是增加阴影样本的多样性在标注阶段专程挑选含有大片高层建筑阴影的区域补充到训练集二是把损失函数中 Focal Loss 的 gamma 系数调大增大模型在难分类样本其本质就是阴影与水的边界上的注意力三是在后处理阶段做一个形态学滤波用连通域分析丢掉面积过小的水块同时用对象与相邻建筑物阴影的相对位置做一次启发式过滤。这个后处理属于工程技巧写进论文里可以当作“基于规则的优化”部分毕业答辩时还能加分。5.2 训练时显存“莫名其妙”爆掉现象用 512 尺寸、batch size 8 训练 U-Net第 20 个 epoch 突然报 CUDA OOM但前 19 个 epoch 都正常。原因PyTorch 的 autograd 机制会在某些操作下把中间激活值保留得更久常见于 loss 计算或 DICE 指标计算中的应用了不必要的张量操作导致峰值显存超过稳定值另外验证时没有关闭梯度计算也会推高峰值。解决在验证循环外层用torch.inference_mode()包住并确保 loss 和指标计算不会意外创建梯度记录如果还偶尔爆显存把batch size从 8 降到 6 或把裁片尺寸从 512 降到 384而不是调模型。同时养成分阶段清理torch.cuda.empty_cache()的习惯。这只是缓存清空不会释放真正在用的显存但可以避免缓存控件碎片化带来的偶发 OOM。5.3 验证集 DICE 高、测试集目视效果差现象验证集 IOU 0.85但放到另一景完整城市影像上预测结果错漏明显边界毛糙。原因验证集切片是从同一批大图上切出来的相邻切片之间高度相似模型把背景纹理“背”下来了泛化能力远低于表面指标。或者验证集本身是人工挑选的“干净”样本没有包含云雾、噪声、时相差异。解决把验证集和训练集分组时直接按影像文件分组不按切片分组——同一景影像的所有切片要么都在训练集要么都在验证集绝不混用。这样能保证验证集反映的是“跨影像泛化能力”这才是实际部署时关心的指标。测试集固定预留两到三景完全没参与训练的城市影像包括不同季节、不同天气情况的测试结果要在论文里如实呈现。5.4 全图推理时出现网格式拼缝现象把 512×512 的预测切片拼回完整大图相邻切片边界处有明显的矩形接缝。原因切片边缘的预测置信度低尤其是水体边界在切片边缘断开时模型缺少上下文拼回去后出现亮度突变或类别不连续。解决推理时使用重叠切片法我沿用训练阶段的overlap64重叠区域对两个相邻切片的预测概率做线性加权平均权重从矩形中心高向边缘低过渡。在代码里用一套统一的推理函数管理不用在推理脚本里再手写def slide_inference(model, big_image, patch_size512, overlap64): model.eval() h, w big_image.shape[2], big_image.shape[3] # 假设已转为张量 step patch_size - overlap output torch.zeros((1, 1, h, w), devicenext(model.parameters()).device) weight torch.zeros((1, 1, h, w), devicenext(model.parameters()).device) for y in range(0, h - patch_size 1, step): for x in range(0, w - patch_size 1, step): patch big_image[:, :, y:ypatch_size, x:xpatch_size] with torch.inference_mode(): pred torch.sigmoid(model(patch)) output[:, :, y:ypatch_size, x:xpatch_size] pred weight[:, :, y:ypatch_size, x:xpatch_size] 1 return (output / weight).cpu()这里每个重叠区简单地被预测次数做平均不额外做高斯加权已经能消除大部分接缝。如果对边界要求更高就在weight上叠加一个锥形权重矩阵离中心越近权重越大。多做一个加权实际作用很有限除非接缝问题严重到肉眼可见否则不建议在毕设阶段多花这个力气。5.5 数据集标注错位mask 比图像整体偏移几个像素现象训练时 loss 不降仔细看训练样本发现物体边缘对不上图像里的河岸线在 mask 里偏了 3 到 5 个像素。原因标注软件导出的 mask 尺寸或坐标基准和原始影像不一致或者预处理阶段做了重采样导致像素对齐偏差。很多标注工具导出 PNG 时自动做了坐标偏移肉眼难以发现但网络照样能拟合出一定的错位规律。解决在标注完成后做一个自动校验脚本随机抽取 20 对图像和 mask计算图像边缘强度与 mask 边缘变化的交叉相关性检查最大相关位置是否在原点附近。发现系统性偏移时不要手动一张张修写脚本按固定偏移量平移校正即可。这是数据质量控制中性价比最高的一步花十分钟检查能避免训练三小时后发现数据有问题的惨剧。6. 最后一道工序精度怎么验证、系统怎么交付模型训练完毕业设计主体已经做完一大半但最后一步的工程化处理往往决定答辩结果。我自己习惯把第一人称的技术经验收在验证环节因为对水体提取系统来说完整验证不仅是指标体系还包括可复现的实验流程。最后这个章节是常用的检验方法顺带给出工程打包建议照着做能少走不少弯路。6.1 四项评估指标与可视化报告毕业设计论文里通常需要提供准确率、精确率、召回率、IoU、DICE 五类指标。写清楚每个指标的类别定义很重要我的习惯是同时给出基于所有测试样本的像素级指标和基于单张影像的均值指标两者差异能说明模型在不同影像上的稳定性。可视化报告按“原图 / 真值 / 预测 / 叠加重合”四图一行排列红色表示漏检绿色表示误检黄色表示正确预测。数据准备阶段保留 test 集的三景影像不要动代码里加入生成 matplotlib 报告的脚本操作上整体可控。6.2 用一个轻量接口把模型封装起来最后用 Flask 包一个最小的推理服务能够接收一张图片返回水体掩膜和叠加可视化结果。代码量不大但对毕业设计的“系统”描述和答辩演示很有价值。要注意模型加载时设置map_location和eval()模式并把预处理与后处理统一封装进同一个类避免调用方自己写预处理逻辑。from flask import Flask, request, jsonify import torch, numpy as np from PIL import Image app Flask(__name__) model load_model(best_water_extract.pth) model.eval() app.route(/predict, methods[POST]) def predict(): file request.files[image] img np.array(Image.open(file.stream).convert(RGB)) tensor preprocess(img) # resize、normalize、转张量 with torch.inference_mode(): pred torch.sigmoid(model(tensor)) mask (pred[0, 0].cpu().numpy() 0.5).astype(np.uint8) * 255 return jsonify({water_ratio: float(mask.mean() / 255), mask_shape: mask.shape})最后要说的是水体提取系统的性能瓶颈往往不在模型结构而在训练数据与预处理质量上。把标注数据做精细、把切片逻辑写统一、把验证流程搭完整就已经胜过一大批泛泛跑模型的方案。希望这份实践笔记能帮你在毕业设计的水体提取项目上少踩几个坑真正把深度学习装进自己的系统里。本文还有配套的精品资源点击获取