news 2026/10/23 6:45:29

PyTorch卫星图像分割实战:U-Net语义分割全流程与工程避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch卫星图像分割实战:U-Net语义分割全流程与工程避坑指南

简介:这份资源面向遥感图像处理与计算机视觉方向的开发者、学生及研究人员,聚焦卫星图像分割这一核心任务,帮助读者理解从图像预处理、特征提取到分割算法选择与后处理的完整流程。包内共3个文件,均为Python脚本,压缩包约3KB,涵盖主程序入口、屋顶检测示例与数据准备脚本,便于直接运行和二次修改。已有252人学习下载,说明其在入门与实践中具有一定参考价值。内容涉及GDAL/OGR读取遥感影像、OpenCV边缘检测与阈值分割、Scikit-image区域生长算法,以及基于TensorFlow或PyTorch构建U-Net等深度学习分割模型,可应用于土地覆盖分类、灾害监测与农业估产等场景。读者可借此快速搭建实验环境,理解传统方法与深度学习方法在卫星图像分割中的差异与实现思路,适合作为课程设计、科研入门或项目原型的参考代码。

1. 卫星图像分割:从遥感大图里把每一类地物抠出来

拿到一份标注好的卫星影像数据集,想把建筑、水体、道路、植被逐像素分开,这件事听起来像学术任务,实际落地时全是工程细节。我最近拆的这个卫星图像分割项目,就是干这个的:输入一张多波段遥感图,输出一张同尺寸的类别掩膜,每个像素归到某一类地物。它适合两类人——一类是想跑通语义分割全流程的新手,另一类是要把模型塞进实际遥感处理链路、需要改数据管道和推理逻辑的开发者。项目用 Python 写,训练和推理都围绕 PyTorch 展开,数据增强、损失函数、评价指标这些环节一个不少。下面我按「能干什么、怎么跑、怎么改」的顺序,把这份源码拆开讲。

2. 项目结构与技术选型:为什么是这套组合

2.1 目录骨架与各模块职责

这个项目的目录结构是典型的语义分割工程布局,没有花哨的封装,读起来不费劲。常见做法是分成数据、模型、训练、推理、配置五大块,我按实际拆包看到的组织方式说明:

satellite-seg/ ├── configs/ # yaml 配置,数据集路径、超参、模型名 ├── datasets/ # Dataset 与 DataLoader 构建 │ ├── __init__.py │ ├── seg_dataset.py # 读取影像与掩膜,做同步增强 │ └── transforms.py # 几何+辐射增强 ├── models/ # 网络定义 │ ├── __init__.py │ ├── unet.py # 编码器-解码器主干 │ └── blocks.py # 卷积块、注意力块 ├── losses/ # 损失函数 │ ├── dice.py │ └── combo.py # CE + Dice 组合 ├── utils/ # 指标、日志、可视化 │ ├── metrics.py # IoU / mIoU / F1 │ └── logger.py ├── train.py # 训练入口 ├── predict.py # 单图/大图推理入口 └── requirements.txt

datasets/seg_dataset.py负责把影像和标签成对读进来,关键是保证几何变换同步——影像旋转了,掩膜必须跟着转,否则标签就错位了。models/unet.py是主干网络,losses/combo.py把交叉熵和 Dice 拼在一起,utils/metrics.py算 mIoU。训练入口train.py读配置、建数据、建模型、跑循环。推理入口predict.py单独拆出来,因为遥感图往往很大,推理逻辑和训练不一样。

2.2 技术栈与依赖说明

核心依赖就几个,版本不用卡太死,但要注意 PyTorch 和 torchvision 的匹配:

依赖作用备注
Python 3.8+运行环境3.10 也稳
PyTorch 1.12+训练与推理有 GPU 走 CUDA
torchvision预训练权重、变换和 torch 版本对齐
numpy数组运算读 tif 常用
opencv-python图像读写、增强比 PIL 快
rasterio读多波段遥感图处理地理坐标时用
pyyaml读配置轻量
tqdm进度条训练时看进度

选 PyTorch 而不是别的框架,主要是调试方便,动态图写自定义损失和指标时不用跟静态图较劲。遥感图常是多波段(比如 4 波段含近红外),rasterio读进来是(C, H, W),正好对上 PyTorch 的通道优先格式,省一次转置。数据增强用opencv而不是torchvision.transforms,是因为几何变换要同时作用在影像和掩膜上,自己写更可控。

2.3 数据管道:影像与掩膜的同步增强

数据管道是这类项目最容易翻车的地方。影像和掩膜必须做完全相同的几何变换,随机裁剪、翻转、旋转都要同步。看seg_dataset.py的核心逻辑:

import cv2 import numpy as np import torch from torch.utils.data import Dataset class SegDataset(Dataset): def __init__(self, img_paths, mask_paths, transform=None): self.img_paths = img_paths self.mask_paths = mask_paths self.transform = transform # 自定义同步增强 def __getitem__(self, idx): # 读多波段影像,保持 (C, H, W) img = cv2.imread(self.img_paths[idx], cv2.IMREAD_UNCHANGED) mask = cv2.imread(self.mask_paths[idx], cv2.IMREAD_GRAYSCALE) # 归一化到 [0,1],按波段做 img = img.astype(np.float32) / 255.0 if img.ndim == 2: img = img[..., None] # 单波段补通道维 # 同步增强:同一组随机参数作用在 img 和 mask 上 if self.transform: img, mask = self.transform(img, mask) # 转成 tensor,通道优先 img = torch.from_numpy(img.transpose(2, 0, 1)).float() mask = torch.from_numpy(mask).long() return img, mask def __len__(self): return len(self.img_paths)

逻辑说明:__getitem__每次返回一对(img, mask)。影像读成浮点并归一化,掩膜保持整型类别索引。transform接收影像和掩膜两个参数,内部用同一组随机数做翻转和裁剪,这是同步的关键。最后transpose(2, 0, 1)把(H, W, C)转成(C, H, W),因为 PyTorch 卷积要求通道在前。

参数说明:img_paths和mask_paths是等长列表,顺序必须一一对应,建议在构建时就断言长度相等。transform如果传None,就跳过增强,验证集通常这么用。掩膜用IMREAD_GRAYSCALE读,保证是单通道类别图,如果标签是彩色索引图,要先做颜色到类别的映射。

注意:掩膜的类别值必须是 0 到 num_classes-1 的连续整数,中间不能跳号,否则交叉熵会报越界。

3. 模型与损失:编码器-解码器怎么搭、损失怎么配

3.1 U-Net 主干与跳跃连接

这个项目的主干是 U-Net 结构,编码器逐层下采样提特征,解码器逐层上采样恢复分辨率,中间用跳跃连接把浅层细节引到深层。看models/unet.py的核心:

import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.block = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), ) def forward(self, x): return self.block(x) class UNet(nn.Module): def __init__(self, in_ch=4, num_classes=6, base=64): super().__init__() # 编码器 self.enc1 = DoubleConv(in_ch, base) self.enc2 = DoubleConv(base, base * 2) self.enc3 = DoubleConv(base * 2, base * 4) self.pool = nn.MaxPool2d(2) # 解码器 self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2) self.dec2 = DoubleConv(base * 4, base * 2) self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2) self.dec1 = DoubleConv(base * 2, base) self.head = nn.Conv2d(base, num_classes, 1) def forward(self, x): e1 = self.enc1(x) # 浅层,细节多 e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) d2 = self.up2(e3) d2 = self.dec2(torch.cat([d2, e2], dim=1)) # 跳跃连接 d1 = self.up1(d2) d1 = self.dec1(torch.cat([d1, e1], dim=1)) return self.head(d1)

逻辑说明:DoubleConv是两次卷积加 BN 加 ReLU 的基础块。编码器每经过一次pool分辨率减半、通道翻倍。解码器用转置卷积上采样,然后torch.cat把同分辨率的编码器特征拼进来,这就是跳跃连接,作用是让解码器在恢复边界时能用到浅层的高频信息。最后head用 1x1 卷积把通道压到类别数。

参数说明:in_ch是输入波段数,多光谱常见 4 或 8,按实际数据改。num_classes是地物类别数,含背景。base是基础通道数,显存不够就降到 32。跳跃连接拼接时通道数翻倍,所以dec2的输入是base*4,这个数要对上,改base时别漏。

3.2 组合损失:交叉熵加 Dice

遥感分割里类别极不平衡,水体可能只占几个百分点,单用交叉熵会让模型偏向多数类。常见做法是交叉熵加 Dice,前者管像素级分类,后者管区域重叠。看losses/combo.py:

import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, logits, target): # logits: (N, C, H, W), target: (N, H, W) probs = F.softmax(logits, dim=1) target_onehot = F.one_hot(target, num_classes=logits.shape[1]) target_onehot = target_onehot.permute(0, 3, 1, 2).float() # 按类别算 Dice 再平均 dims = (0, 2, 3) inter = (probs * target_onehot).sum(dims) union = probs.sum(dims) + target_onehot.sum(dims) dice = (2 * inter + self.smooth) / (union + self.smooth) return 1 - dice.mean() class ComboLoss(nn.Module): def __init__(self, ce_weight=0.5, dice_weight=0.5): super().__init__() self.ce = nn.CrossEntropyLoss() self.dice = DiceLoss() self.ce_w = ce_weight self.dice_w = dice_weight def forward(self, logits, target): return self.ce_w * self.ce(logits, target) + self.dice_w * self.dice(logits, target)

逻辑说明:DiceLoss先把 logits 过 softmax 得到概率,把 target 转成 one-hot,然后按类别算交并比形式的 Dice 系数,最后取1 - mean作为损失。ComboLoss把交叉熵和 Dice 加权相加。交叉熵对每个像素一视同仁,Dice 对区域重叠敏感,两者互补。

参数说明:smooth防止分母为零,一般取 1.0。ce_weight和dice_weight是权重,默认各 0.5,如果小目标分割差,可以把 Dice 权重提到 0.7。target必须是(N, H, W)的整型张量,不能是 one-hot,否则CrossEntropyLoss会报错。

提示:如果显存吃紧,Dice 那步的 one-hot 会额外占显存,可以改成用scatter或分块计算,但代码会复杂些,先跑通再优化。

3.3 训练循环与评价指标

训练入口train.py把前面几块串起来。核心循环不复杂,关键是每个 epoch 后在验证集上算 mIoU,别只看 loss。看utils/metrics.py的指标实现:

import numpy as np class IoUMetric: def __init__(self, num_classes): self.num_classes = num_classes self.reset() def reset(self): # 混淆矩阵,行是真实类,列是预测类 self.conf = np.zeros((self.num_classes, self.num_classes), dtype=np.int64) def update(self, pred, target): # pred, target: (N, H, W) 整型 pred = pred.flatten() target = target.flatten() mask = (target >= 0) & (target < self.num_classes) idx = target[mask] * self.num_classes + pred[mask] self.conf += np.bincount(idx, minlength=self.num_classes ** 2).reshape(self.num_classes, self.num_classes) def compute(self): # 逐类 IoU,再算 mIoU inter = np.diag(self.conf) union = self.conf.sum(1) + self.conf.sum(0) - inter iou = inter / np.maximum(union, 1) return iou, np.nanmean(iou)

逻辑说明:用混淆矩阵累计预测和真实标签的对应关系,update每批调用一次,compute在 epoch 末算逐类 IoU 和 mIoU。np.bincount把二维索引压成一维计数,比循环快很多。np.maximum(union, 1)防止某类没出现时除零。

参数说明:num_classes要和模型输出、掩膜类别数一致。pred是 argmax 后的类别索引,不是 logits。如果某类在验证集里完全没出现,它的 IoU 会是 nan,nanmean会忽略它,但你要心里有数,别被虚高的 mIoU 骗了。

4. 跑起来:环境、训练、推理的完整步骤

4.1 环境搭建与数据准备

先把环境弄干净,建议用虚拟环境。命令按顺序来:

# 建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 装依赖 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt # 验证 GPU 可用 python -c "import torch; print(torch.cuda.is_available())"

数据准备是重点。影像和掩膜要分两个目录,文件名一一对应,掩膜是单通道灰度图,像素值就是类别索引。常见做法是写个脚本先检查一遍:

import os import numpy as np import cv2 def check_dataset(img_dir, mask_dir, num_classes): imgs = sorted(os.listdir(img_dir)) masks = sorted(os.listdir(mask_dir)) assert len(imgs) == len(masks), "影像和掩膜数量不一致" for name in masks: m = cv2.imread(os.path.join(mask_dir, name), cv2.IMREAD_GRAYSCALE) vals = np.unique(m) # 检查类别值是否越界 if vals.max() >= num_classes or vals.min() < 0: print(f"{name} 类别值异常: {vals}") print("检查完成") check_dataset("data/images", "data/masks", num_classes=6)

逻辑说明:这个脚本先断言影像和掩膜数量相等,再逐个读掩膜,检查像素值是否落在[0, num_classes-1]。遥感标签常见问题是背景被标成 255,直接喂进去交叉熵就崩了,提前查出来能省很多调试时间。

参数说明:img_dir和mask_dir换成你的实际路径。num_classes和配置里保持一致。如果掩膜是彩色索引图,要先做颜色映射转成灰度,这一步别偷懒。

4.2 配置与启动训练

配置用 yaml,configs/下放一份基础配置。关键字段:

data: img_dir: data/images mask_dir: data/masks num_classes: 6 in_ch: 4 batch_size: 8 num_workers: 4 model: name: unet base: 64 train: epochs: 100 lr: 0.001 weight_decay: 0.0001 ce_weight: 0.5 dice_weight: 0.5 save_dir: checkpoints

启动训练:

python train.py --config configs/base.yaml

训练循环里我一般会加两件事:一是每个 epoch 存一次最优 mIoU 的权重,二是学习率用余弦退火。看train.py的核心片段:

import torch from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR from utils.metrics import IoUMetric def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for img, mask in loader: img, mask = img.to(device), mask.to(device) optimizer.zero_grad() logits = model(img) loss = criterion(logits, mask) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(loader) def validate(model, loader, device, num_classes): model.eval() metric = IoUMetric(num_classes) with torch.no_grad(): for img, mask in loader: img = img.to(device) logits = model(img) pred = logits.argmax(dim=1).cpu().numpy() metric.update(pred, mask.numpy()) return metric.compute()

逻辑说明:train_one_epoch走标准的前向、反向、更新三步,累加 loss 求平均。validate切到 eval 模式,关掉梯度,用 argmax 拿预测类别,喂给指标类。注意model.eval()和torch.no_grad()都要有,前者影响 BN 和 Dropout,后者省显存。

参数说明:device一般是cuda或cpu。optimizer用 Adam,学习率从配置读。CosineAnnealingLR的T_max设成总 epoch 数。验证时mask.numpy()要确保在 CPU 上,别直接对 GPU 张量调 numpy。

4.3 大图推理与结果拼接

遥感图动辄几千像素,直接整张喂进网络会爆显存。常见做法是滑窗切块推理再拼回去。看predict.py的核心:

import numpy as np import torch def sliding_window_predict(model, img, patch=512, stride=256, num_classes=6, device="cuda"): # img: (C, H, W) 浮点 C, H, W = img.shape prob_map = np.zeros((num_classes, H, W), dtype=np.float32) count_map = np.zeros((H, W), dtype=np.float32) model.eval() with torch.no_grad(): for y in range(0, H, stride): for x in range(0, W, stride): y2 = min(y + patch, H) x2 = min(x + patch, W) y1 = max(0, y2 - patch) x1 = max(0, x2 - patch) crop = img[:, y1:y2, x1:x2] tensor = torch.from_numpy(crop).unsqueeze(0).float().to(device) logits = model(tensor) prob = torch.softmax(logits, dim=1).squeeze(0).cpu().numpy() prob_map[:, y1:y2, x1:x2] += prob count_map[y1:y2, x1:x2] += 1 # 重叠区域取平均 prob_map /= np.maximum(count_map, 1) return prob_map.argmax(axis=0)

逻辑说明:滑窗按stride步进,每次取patch大小的块,边界处往回对齐保证块尺寸一致。每块的 softmax 概率累加到prob_map,同时用count_map记录覆盖次数,最后取平均再 argmax。重叠区域取平均能缓解拼接缝。

参数说明:patch是切块大小,要和显存匹配,512 一般够用。stride小于patch才有重叠,设成patch的一半比较稳。num_classes和训练一致。如果显存大,patch可以提到 1024,但边界效应会更明显。

注意:推理时的归一化方式必须和训练完全一致,训练用/255.0,推理也得/255.0,否则预测会整体偏移。

5. 避坑与排查:那些让我重跑训练的记录

5.1 掩膜类别值不连续导致 loss 报错

现象:训练刚起步就抛CUDA error: device-side assert triggered,或者交叉熵报 index out of range。原因:掩膜里出现了大于等于num_classes的值,比如背景标成 255,或者类别跳号(0、1、3)。解决:跑 4.1 里的检查脚本,把所有越界值映射回合法范围,背景统一成 0,类别重映射成连续整数。这个坑我踩过两次,现在每次拿到新数据先跑检查。

5.2 影像和掩膜增强不同步

现象:训练 loss 能降,但验证 mIoU 死活上不去,预测边界全是乱的。原因:几何增强时影像和掩膜用了不同的随机参数,比如影像翻转了掩膜没翻,标签和像素对不上。解决:增强函数必须接收(img, mask)两个参数,内部用同一组随机数。用cv2做旋转时,warpAffine的矩阵要同时作用在两者上,插值方式影像用双线性、掩膜用最近邻。

5.3 显存溢出与 batch size 的取舍

现象:训练几个 batch 后 OOM。原因:batch_size太大,或者 Dice 损失里的 one-hot 占了大头。解决:先把batch_size降到 4 或 2,base通道数从 64 降到 32。如果还不够,把 Dice 的 one-hot 改成逐类循环计算,牺牲点速度换显存。另外num_workers别设太大,数据加载进程也会占内存。

5.4 验证集 mIoU 虚高

现象:mIoU 看着 0.9,实际预测图一塌糊涂。原因:验证集里某些类完全没出现,nanmean把它们忽略了,剩下的类又恰好容易分。解决:看逐类 IoU,别只看 mIoU。如果某类 IoU 是 nan,说明验证集没这个类,要么补数据,要么在报告里注明。另外检查验证集有没有和训练集重叠,遥感图相邻区域很像,切分时要按地理块切,别随机切。

5.5 推理结果有拼接缝

现象:大图推理拼出来能看到网格状的缝。原因:滑窗stride等于patch,块之间没有重叠,边界处上下文不足。解决:把stride设成patch的一半,让相邻块有重叠,重叠区域取平均。如果缝还在,检查归一化是否一致,以及模型有没有用 padding 导致边界像素偏移。

6. 二次开发:换主干、加类别、接后处理

跑通之后,二次开发无非三个方向:换更强的编码器、加类别、接地理后处理。换主干最直接,把UNet的编码器换成带预训练权重的 ResNet 或 EfficientNet,解码器保留。做法是torchvision.models.resnet34(pretrained=True)取前几层,把enc1到enc3替换掉,注意通道数要对上,ResNet 的layer1输出是 64 通道,正好接base=64。换完先冻结编码器训几个 epoch,再解冻微调,不然预训练权重容易被大学习率冲垮。

加类别要动三处:配置里的num_classes、模型head的输出通道、损失函数里的类别数。掩膜也要重新映射,新类别给新的连续索引。如果新类别样本很少,把dice_weight提到 0.7,再考虑加个类别权重到交叉熵里,nn.CrossEntropyLoss(weight=torch.tensor([...])),权重按类别频率的倒数设。

接后处理是遥感特有的。模型输出的是逐像素类别,但实际地物有空间连续性,常见做法是接一个条件随机场或简单的形态学开闭运算去噪。我一般先用cv2.morphologyEx做开运算去掉孤立小斑块,再按最小面积过滤连通域。如果项目要输出矢量,用rasterio.features.shapes把掩膜转成多边形,再按地理坐标写出去。

验证改造效果时,别只看 mIoU。我习惯固定几张典型图,把改造前后的预测并排看,重点看边界和小目标。有一次我换了个更强的编码器,mIoU 涨了两个点,但小水体反而漏了,后来发现是新主干的感受野太大,小目标被平滑掉了。从那以后我每次换主干都强制走一遍逐类 IoU 加可视化对比,不敢只看一个总数。希望这些能帮到你,少走点我踩过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 6:44:47

labelme标注工具全流程:安装、标注、JSON转数据集与避坑指南

简介&#xff1a;这份资源是面向计算机视觉、机器学习初学者及数据标注从业者的 Labelme 安装与使用教程文档&#xff0c;帮助读者快速搭建图像分割与标注环境&#xff0c;解决从零配置工具链时容易遇到的版本与环境问题。资源包共 1 个 docx 文件&#xff0c;约 14KB&#xff…

作者头像 李华
网站建设 2026/10/23 6:43:38

DeepSeek本地部署实战:Ollama+ChatBox安装配置与踩坑指南

简介&#xff1a;当线上DeepSeek频繁出现服务中断与排队时&#xff0c;这份面向普通用户和专业技术人员的保姆级本地部署教程&#xff0c;给出了以Ollama为核心、搭配ChatBox图形界面的完整解决路径&#xff0c;教你把DeepSeek完整跑在自有电脑上&#xff0c;保障日常使用的连续…

作者头像 李华
网站建设 2026/10/23 6:42:47

APP手机网站模板深度拆解:路由、适配与二次开发实战

简介&#xff1a;面向APP应用与手机网站开发的HTML页面模板包&#xff0c;内置完整可编辑的前端代码&#xff0c;包括HTML结构、CSS样式、JavaScript脚本及配套界面图片&#xff0c;适合前端初学者、移动端网页开发者及需要快速搭建手机端原型页面的团队参考和使用。压缩包为RA…

作者头像 李华
网站建设 2026/10/23 6:42:40

计算机等级考试一级选择题高频考点与答题技巧全解析

简介&#xff1a;这是一份面向全国计算机等级考试一级考生的试题练习资料&#xff0c;覆盖计算机发展史、硬件组成、数制转换、ASCII码、操作系统、存储器、输入输出设备、病毒特征、指令系统与编程语言等核心考点&#xff0c;适合备考刷题、考前自测及知识点查漏补缺。资源仅含…

作者头像 李华
网站建设 2026/10/23 6:40:19

C#经典小项目合集:从语法到实战的完整训练路径

简介&#xff1a;这份资源面向C#初学者与希望夯实基础的开发者&#xff0c;汇集30个由浅入深的经典练习项目&#xff0c;覆盖变量与流程控制、函数与类对象、面向对象封装继承多态、事件与委托驱动的窗体界面、ADO.NET数据库增删改查&#xff0c;以及async/await异步编程与LINQ…

作者头像 李华
网站建设 2026/10/23 6:38:16

自动驾驶:扩散模型作为规划器

端到端范式下,自动驾驶规划器几乎无一例外地被训练为"回归":输入场景特征,输出一条轨迹,损失函数是均方误差(MSE)。本文论证这一范式的结构性缺陷——在多模态驾驶场景中,MSE 的最优解是各模态的条件期望,而条件期望往往不在任何一条合理轨迹上。同一场景既可绕左也可…

作者头像 李华