简介:这是一份基于图像分割的卫星遥感图像国土分类Python项目源码,面向计算机相关专业在做课程设计、期末大作业的学生,以及需要项目实战练习的开发者。项目围绕国土利用分类任务,整合了数据加载、预处理、模型训练等完整流程,涉及PSPNet、DeepLabV3与DeepLabV3+等常用分割网络,并配备了针对水陆区域的预处理脚本,便于掌握遥感图像分割与分类的落地方法。资源共12个文件,以8个py源码脚本为核心,另含2个log训练日志、1个txt数据说明和1张png展示海报,压缩包仅2.49MB,结构紧凑、便于快速部署。所有源码已严格调试,下载即可直接运行;目前已有158人学习。相比零散教程,这份源码给出从数据准备、预处理到深度模型训练与日志分析的完整链路,适合直接作为课程设计基础框架,也可在真实遥感数据上继续扩展调优,能有效节省环境搭建和排错时间。
1. 用图像分割做国土分类,课程设计选它划算在哪
把一张卫星遥感图输入程序,输出一张和原图同尺寸的“地类图”,每个像素标成耕地、林地、建筑、水体或裸地——这就是基于图像分割的卫星遥感图像国土分类,一个用 Python 实现起来非常有展示度的课程设计题目。它在图像分割方向上做了最典型的像素级应用:不是告诉你“图里有什么”,而是告诉你“每一个点属于什么”。相比普通图像分类,它有更清晰的评判标准、更直观的彩色结果图,还能带出数据预处理、模型训练、可视化评估一整条工程链路。适合想在课程设计里同时体现算法理解和工程能力、又不想背太重业务包袱的人。
2. 国土分类的本质是像素级分割:从问题到算法选型
2.1 为什么一张图一个标签不够用
普通图像分类模型的输出层是一个全连接层,最后给出“这张图属于哪个类”的单一标签。你输入一张农田照片,它输出“农田”两个字,这对相册分类、目标识别这类场景够用。但遥感影像不一样:一张 1024x1024 的影像里可能有十几栋房子、几块农田、一条河、一片林子。你让分类模型用一个标签去概括整张图,标注阶段就会开始打架——这块区域标成耕地还是建设用地?边界上的人工地物算哪一类?模型训练时也会反复震荡,因为同样的影像内容换了位置,标签却对不上。
国土分类真正要解决的是“地图上每一个坐标点落在什么地类上”,这是一个像素级预测问题,也就是图像分割里的语义分割。输入是 C×H×W 的多波段影像,输出是 H×W 的掩码矩阵,每个像素取值 0 到 N-1,N 是类别总数。类别定义一般围绕土地利用/土地覆盖展开,常见的是五类:耕地、林地、建设用地、水体、裸地。如果你拿到的是城市范围影像,也可以把建设用地拆成房屋、道路、广场,把耕地拆成水田、旱地,类别越细,对分割模型的压力越大,课程设计的标注成本也越高。
从模型结构上看,普通分类网络的尾部是全局池化加全连接,空间信息被压缩掉;分割网络则要把特征图上采样回原图分辨率。两者不是简单换一个损失函数就能互相转换的。这也是为什么很多课程设计一开始套用分类模型做遥感识别,最后发现边界一片糊——因为结构上就走错了路。
2.2 分割方案哪家强:U-Net 凭什么当课程设计首选
图像分割算法可以分三个层次来看。第一层是传统图像处理:Otsu 阈值、区域生长、K-means 聚类。Otsu 适合单波段灰度图做二类分割,对“水体/非水体”这种对比强烈的场景还能用,但多类别国土分类基本无能为力。K-means 不需要标注,直接对光谱特征聚类,能用来做无监督摸底,看看影像里大致能分几类,但聚类结果和真实地物的对应关系不稳定,每次运行结果都可能漂移。
第二层是深度学习里的经典分割网络:FCN 是开山之作,用卷积替代全连接,但上采样倍数大时细节丢失严重。U-Net 针对这个问题做了对称编码-解码结构,用跨越连接把编码器各层的细节特征拼到解码器对应层,对小数据集和精细边界都更友好。第三层是更强但更重的方案,比如 DeepLabV3+ 用空洞卷积金字塔捕捉多尺度上下文,SegFormer 用 Transformer 做全局建模,效果通常更好,但代码量和调参复杂度也上去了。
| 方案 | 是否需要标注 | 边界精细度 | 实现成本 | 适合场景 |
|---|---|---|---|---|
| Otsu 阈值 | 否 | 低 | 很低 | 快速摸底、二类分割 |
| K-means 聚类 | 无监督 | 低 | 低 | 光谱聚类探索 |
| FCN | 是 | 中 | 中 | 理解分割原理 |
| U-Net | 是 | 高 | 中 | 课程设计首选 |
| DeepLabV3+ | 是 | 高 | 较高 | 想冲高指标 |
我的建议是课程设计选 U-Net,理由很实际:结构清晰,encoder 和 decoder 对称,跳过连接一张图能讲明白;参数量适中,单张消费级显卡就能训;对几百到几千张小 patch 的数据集能收敛;不依赖任何预训练权重也能跑出一个能看的 baseline。如果你后面想拔高分数,可以在同一个数据集上再跑一个 DeepLabV3+ 或 PSPNet 做对比实验,但主体架构用 U-Net 最稳。
2.3 先定类别和指标:IoU 比 Accuracy 诚实
动手写代码之前,必须先定两件事:类别清单和评估指标。类别清单决定了掩码文件的像素值定义。建议用 0 表示背景或“其他”,1 到 N-1 表示具体地类,同时准备一个类别到颜色的映射表,例如水体蓝色、林地绿色、建设用地灰色。没有颜色映射表,后面可视化预测结果时只能用灰度图,答辩时观众很难一眼看清效果。
评估指标是课程设计报告里最能体现专业度的地方。语义分割最常用 IoU(Intersection over Union),公式是:IoU = TP / (TP + FP + FN),也就是预测区域和真实区域的重叠面积除以两者并集面积。单个类别的 IoU 只能反映一类分得好不好,把所有类别的 IoU 取平均得到 mIoU,这是分割任务的主指标。
为什么不直接看 Accuracy?遥感影像里大面积的连片耕地或林地占比很高,如果模型把所有像素都预测成耕地,Pixel Accuracy 也能到 70% 以上,看着像模像样,实际上建设用地和水体全错了。IoU 对每一类都做独立计算,大类占便宜、小类吃亏的问题比 Accuracy 轻得多。课程设计报告里贴 mIoU 和各类别 IoU,同时附上混淆矩阵,比只贴一张 loss 曲线图有说服力得多。
3. 遥感图像预处理与数据集制作:跑不出结果多半卡在这里
3.1 用 rasterio 读多波段影像:别直接转 uint8
遥感影像和普通照片最大的区别是多波段和宽位深。常见的 Sentinel-2、Landsat 影像通常有多个波段,单波段值域经常是 uint16,也就是 0 到 65535,直接用 OpenCV 的 imread 读会丢失信息,因为 imread 默认按 8 位解码。我一般用 rasterio 读,它原生支持多波段、地理坐标和 NoData。
import rasterio import numpy as np from pathlib import Path src_path = Path("data/input/landsat8.tif") with rasterio.open(src_path) as src: img = src.read() # 返回 (C, H, W) profile = src.profile # 保存元数据,后面写结果图要用 nodata = src.nodata print("影像 shape:", img.shape) # 例如 (4, 1024, 1024) print("dtype:", img.dtype) # 常见 uint16、float32 # 取前三个波段转 RGB 预览 rgb = img[:3].transpose(1, 2, 0) rgb = (rgb - rgb.min()) / (rgb.max() - rgb.min() + 1e-9)逻辑说明:src.read()返回的数组维度是 (波段数, 高, 宽),和 PyTorch 默认的 CHW 一致,但不和 OpenCV 的 HWC 一致,做可视化或转 numpy 时记得transpose(1, 2, 0)。归一化要用全图 min-max,不要每个 patch 单独归一化,否则 patch 之间的亮度标准不一致,训练时模型会认为亮度是类别特征,预测时换一张图就翻车。
参数说明:nodata要单独处理,常见做法是先把 NoData 像素置为 0,在掩码里也把对应位置标成背景类。如果影像本身就是 RGB 三波段且值域在 0 到 255,可以省掉归一化,但推荐还是转 float32 并归一化到 0 到 1,能加快收敛。
3.2 滑动窗口裁剪 patch:256 还是 512,步长怎么设
大影像不能直接整张丢进网络,显存不够,也没有必要。常见做法是滑动窗口裁剪成固定大小的 patch,再按 patch 训练。patch 尺寸选 256 还是 512,取决于你的显存和地物尺度。256 对小房子、小地块更友好,但感受野小,容易漏掉大面积农田的上下文;512 上下文更完整,但显存占用约是 256 的四倍。课程设计默认 256,batch size 可以开到 8 到 16,GTX 1060 级别的卡也能跑。步长等于 patch 大小时是硬切,patch 之间没有重叠;步长小于 patch 大小时有重叠,能增加样本量,但也带来重复训练。
import numpy as np from pathlib import Path PATCH_SIZE = 256 STRIDE = 256 def crop_patches(img, mask, save_dir, prefix): """把一张大图和对应的掩码同步裁剪成 patch""" c, h, w = img.shape idx = 0 for y in range(0, h, STRIDE): for x in range(0, w, STRIDE): p_img = img[:, y:y+PATCH_SIZE, x:x+PATCH_SIZE] p_mask = mask[y:y+PATCH_SIZE, x:x+PATCH_SIZE] if p_img.shape[1] != PATCH_SIZE or p_img.shape[2] != PATCH_SIZE: continue # 边缘不足一个 patch 直接丢弃 np.save(save_dir / f"{prefix}_{idx}_img.npy", p_img) np.save(save_dir / f"{prefix}_{idx}_mask.npy", p_mask) idx += 1 # 假设 img 和 mask 已经同步读过 crop_patches(img, mask, Path("data/patches"), "scene_01")逻辑说明:y和x同时作用于 img 和 mask,保证裁剪位置一一对应。边缘不足 256 的 patch 直接跳过,而不是用 padding 补零,因为补零区域进入网络后会产生假边界,预测时会出现黑边。两个np.save分别保存图像和掩码,文件名用同一个idx关联。
参数说明:STRIDE设成 256 是最省事的选择,样本不重叠;如果数据量少,可以改成 192 或 128 制造重叠 patch,相当于数据增强。PATCH_SIZE从 256 提到 512 时,建议 batch size 减半,否则显存很容易爆。
3.3 数据增强:图像和掩码要同一个变换
遥感影像方向不敏感,水平翻转、垂直翻转、90 度旋转都是安全的增强方式,因为地物不会因为翻转而改变类别。亮度和对比度扰动也常用,但要小心幅度过大把水体变耕地。我一般用 albumentations,它最大的好处是保证图像和掩码走同一个随机变换。
import albumentations as A train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.3), ]) # 假设 img 是 (H, W, C),mask 是 (H, W) aug = train_transform(image=rgb, mask=mask) aug_img = aug["image"].transpose(2, 0, 1) # 转回 CHW aug_mask = aug["mask"]逻辑说明:albumentations 的Compose接收的是 HWC 格式图像和 HW 格式掩码,所以用完要转回 CHW。翻转和旋转类增强不会改变像素值分布,适合所有地类;亮度扰动不能太大,否则原来很暗的建筑物区可能被模型识成水体或阴影。如果你用的是 PyTorch 原生的torchvision.transforms,必须手动保证图像和掩码使用同一个随机种子,否则增强后两者错位,训练不收敛是小事,可视化结果里边界错乱才是大问题。
参数说明:brightness_limit和contrast_limit建议控制在 0.2 以内,遥感影像的光谱反射率有其物理含义,增强幅度过大会引入虚假的光谱组合。要不要加高斯噪声?加了能提高鲁棒性,但对水体这类低纹理区域,噪声容易淹没边界,课程设计里可加可不加。
4. 用 PyTorch 搭 U-Net 并训练:课程设计最核心的一段
4.1 U-Net 网络代码:普通卷积块和跳过连接最关键
U-Net 本质是编码器提取语义、解码器恢复分辨率、跳过连接补充细节。编码器每层包含两次卷积,中间用最大池化下采样;解码器用转置卷积上采样,再和编码器对应层拼接。重点是torch.cat拼接的位置和通道数,拼接后通道翻倍,后续卷积要按翻倍后的通道数配置。
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, 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.conv(x) class UNet(nn.Module): def __init__(self, in_channels=3, num_classes=5): super().__init__() self.enc1 = DoubleConv(in_channels, 64) self.enc2 = DoubleConv(64, 128) self.enc3 = DoubleConv(128, 256) self.enc4 = DoubleConv(256, 512) self.pool = nn.MaxPool2d(2) self.bottleneck = DoubleConv(512, 1024) self.up4 = nn.ConvTranspose2d(1024, 512, 2, stride=2) self.dec4 = DoubleConv(1024, 512) self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2) self.dec3 = DoubleConv(512, 256) self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2) self.dec2 = DoubleConv(256, 128) self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.dec1 = DoubleConv(128, 64) self.out = nn.Conv2d(64, 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)) b = self.bottleneck(self.pool(e4)) d4 = self.dec4(torch.cat([self.up4(b), e4], dim=1)) d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1)) return self.out(d1)逻辑说明:forward里每层先池化再进下一层,跳跃连接拼接的是池化前的结果,比如e4是enc4的输出,d4解码时要把上采样后的b和e4拼起来,通道数是 512+512=1024,所以dec4第一层卷积输入是 1024 通道。最后的out用 1x1 卷积把 64 通道映射到类别数,不做 softmax,因为损失函数里会用CrossEntropyLoss自带 softmax。
参数说明:in_channels=3对应 RGB 三波段;如果影像不是 RGB 而是多光谱,你可以把前三个波段继续当 RGB 用,也可以改成in_channels=波段数。第一个编码层的通道数 64 是经验值,显存紧张可以改成 32,但精度会下降。num_classes必须和掩码的最大值加一一致,否则训练直接报错。
4.2 损失函数:交叉熵配 Dice,类别不均衡的解法
遥感分割最常见的翻车点是类别不均衡。建设用地和水体在影像里占比通常很低,交叉熵损失会被耕地、林地这种大类牵着走。解决办法一是给类别加权,二是叠加 Dice Loss。Dice Loss 对每个类别的预测区域和真实区域计算重叠率,天然不受像素数量影响,两类损失组合起来效果更稳。
class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, probs, targets): num_classes = probs.shape[1] targets_onehot = torch.eye(num_classes, device=probs.device)[targets].permute(0, 3, 1, 2) dice = 0.0 for i in range(num_classes): p = probs[:, i, :, :] t = targets_onehot[:, i, :, :] intersection = (p * t).sum() union = p.sum() + t.sum() dice += (2 * intersection + self.smooth) / (union + self.smooth) return 1 - dice / num_classes class CombinedLoss(nn.Module): def __init__(self, class_weights=None, dice_weight=0.3): super().__init__() self.ce = nn.CrossEntropyLoss(weight=class_weights) self.dice = DiceLoss() self.dice_weight = dice_weight def forward(self, logits, targets): return self.ce(logits, targets) + self.dice_weight * self.dice(logits.softmax(dim=1), targets)逻辑说明:DiceLoss先把真实掩码转成 one-hot 格式,然后对每个类别单独算 Dice 系数,1 减去平均 Dice 就是损失。交叉熵部分可以直接接受 logits,Dice 部分传入 softmax 后的概率,因为 Dice 需要在 0 到 1 之间的概率上计算。dice_weight控制两部分的相对权重,0.3 是常见起点。
参数说明:class_weights可以用torch.bincount统计训练集掩码里每个类别的像素占比,取倒数并归一化。如果不想手动统计,可以先不加权重,只靠 Dice 拉一把,效果通常也够。
4.3 训练循环:验证集算 mIoU,保留最优权重
训练循环本身不复杂,但有两个习惯要注意:一是每个 epoch 结束都要在验证集上算 mIoU,而不是只看训练 loss;二是只保存验证集 mIoU 最高的权重,防止最后几轮过拟合把结果带坏。
def evaluate(model, loader, device, num_classes=5): """返回验证集 mIoU 和各类别 IoU""" model.eval() cm = np.zeros((num_classes, num_classes), dtype=np.int64) with torch.no_grad(): for imgs, masks in loader: logits = model(imgs.to(device)) pred = logits.argmax(1).cpu().numpy().flatten() mask = masks.numpy().flatten() cm += np.bincount(mask * num_classes + pred, minlength=num_classes * num_classes).reshape(num_classes, num_classes) ious = [] for i in range(num_classes): tp = cm[i, i] fp = cm[:, i].sum() - tp fn = cm[i, :].sum() - tp ious.append(tp / (tp + fp + fn + 1e-9)) model.train() return np.mean(ious), ious model = UNet(in_channels=3, num_classes=5).to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3) criterion = CombinedLoss() best_miou = 0.0 for epoch in range(30): model.train() total_loss = 0.0 for imgs, masks in train_loader: imgs = imgs.to(device) masks = masks.to(device).long() logits = model(imgs) loss = criterion(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() val_miou, val_ious = evaluate(model, val_loader, device) scheduler.step(val_miou) if val_miou > best_miou: best_miou = val_miou torch.save(model.state_dict(), "best_model.pth") print(f"Epoch {epoch} | loss {total_loss / len(train_loader):.4f} | mIoU {val_miou:.4f}")逻辑说明:evaluate使用混淆矩阵统计每个类别的 TP、FP、FN,最后统一算 IoU,比在 batch 内逐次取平均更准确,因为每个 batch 的类别分布不同。训练时模型处于model.train()状态,验证时切到model.eval(),否则 BatchNorm 在验证模式下会更新统计量,导致结果抖动。ReduceLROnPlateau在验证集 mIoU 连续 3 个 epoch 不涨时降低学习率,这是遥感分割里比固定学习率省心很多的设置。
参数说明:lr=1e-4适合 Adam 配 U-Net,比这个高容易震荡,比这个低收敛太慢。30个 epoch 是起步,小数据集上 50 到 80 个 epoch 更稳,具体看验证集 mIoU 是否还在上升。如果显存不够,在 DataLoader 里把batch_size从 16 调到 8,而不是去调小 patch。
5. 避坑:遥感分割课程设计最容易翻车的 5 个地方
5.1 显存不够:代码还没跑起来就 OOM
现象:训练第一个 batch 就报CUDA out of memory,或者跑到一半被系统杀掉。
原因:patch 太大、batch 太大、输入通道太多,三者叠加直接顶爆显存。遥感影像即使裁剪到 256x256,如果是 8 波段或 10 波段,解码器早期层的内存占用会明显高于普通 RGB 图像。
解决:优先把 batch size 降到 4 或 2,看能不能跑起来;再不行把 patch 从 256 降到 224;最后考虑只取前三个波段做输入,放弃多余波段。PyTorch 的torch.cuda.amp混合精度对显存帮助很大,代码只多两行,课程设计里完全可以用。
5.2 mIoU 一直卡在 0.1 附近不涨
现象:训练 loss 在下降,但验证集 mIoU 始终很低,预测结果基本是一团噪声。
原因:类别严重不均衡,模型把所有像素都学成了占比最大的类别,小类别完全没被优化。还有一种可能是掩码文件里类别编号不连续,比如类值是 1、3、7,而模型输出是 0、1、2,这种情况下损失函数计算时对应关系错位,训练永远学不对。
解决:先用np.unique(mask)检查掩码像素值,确认是 0 到 N-1 的连续整数。如果是类别不均衡,给CrossEntropyLoss传入class_weights,或者用前面写的组合损失。还可以先把训练集里各类别的像素占比打印出来,占比低于 5% 的类别重点关照。
5.3 预测大图出现网格状接缝
现象:把预测 patch 拼回大图后,边界处出现明显横竖纹,地块边界在 patch 边缘断裂。
原因:推理时每个 patch 独立预测,边缘像素看不到足够的上下文,patch 和 patch 之间预测不一致。步长等于 patch 时尤其明显。
解决:推理阶段用重叠滑动窗口,也就是步长小于 patch 大小,重叠区域把多个 patch 的预测概率取平均,再做 argmax。代码在第 6 章给出。另一个副作用是推理时间变长,但课程设计不要求实时性,多等几秒可以接受。
5.4 全部预测成同一个类:模型退化
现象:训练一段时间后,预测 mask 全是一个颜色,mIoU 突然跌到接近 0,之前还能看的边界全没了。
原因:学习率太大导致损失震荡,模型塌缩到局部最优;或者训练和验证时对影像的归一化不一致,模型看到的分布完全变化;还有一个容易被忽略的原因是标签里有错位,某些 patch 的 img 和 mask 不是同一个区域,模型学到了“图像和标签无关”,干脆输出最稳妥的大类。
解决:先可视化检查数据,随机挑几对img和mask拼在一起看,确认没有错位。然后学习率降到 1e-5 重新训练,同时检查归一化代码是否对训练集和验证集使用了同一套统计量。如果数据本身是对的,模型退化通常就是学习率问题。
5.5 图像和掩码方向不一致
现象:可视化增强后的图像,房子是正的,但掩码里的建筑区域转了 90 度,或者翻到了另一侧。
原因:图像和掩码用了不同的随机变换,比如自己写翻转代码时对图像和掩码分别调用了random.random(),两次随机结果不同,图像水平翻转了但掩码没有。
解决:用 albumentations 这类保证图像和掩码共享随机状态的库,或者自己写代码时固定一个随机种子再对两者同时做变换。判断是否错位最快的方法,是把增强前后的图像和掩码叠加显示,透明度过一层就能看到轮廓是否吻合。
6. 从能跑到能答辩:重叠窗口拼图与源码组织
6.1 预测大图怎么拼:让接缝消失的滑动窗口
推理阶段想要减少接缝,思路是重叠采样加概率平均。每个位置被多个窗口覆盖,每个窗口独立推理得到各类别概率,最终把概率累加后取 argmax。这个技巧比先预测 patch 再拼接整洁得多,也是我在最终交付时一定会做的处理。
def predict_full_image(model, img, device, patch_size=256, overlap=64): """重叠滑窗预测,返回整张大图的类别 mask""" c, h, w = img.shape num_classes = 5 step = patch_size - overlap prob_map = np.zeros((num_classes, h, w), dtype=np.float32) model.eval() with torch.no_grad(): for y in range(0, h - patch_size + 1, step): for x in range(0, w - patch_size + 1, step): patch = img[:, y:y+patch_size, x:x+patch_size] patch_tensor = torch.from_numpy(patch).unsqueeze(0).float().to(device) logits = model(patch_tensor) prob = torch.softmax(logits, dim=1).squeeze(0).cpu().numpy() prob_map[:, y:y+patch_size, x:x+patch_size] += prob return prob_map.argmax(axis=0)逻辑说明:step等于patch_size - overlap,重叠区域会累加多个 patch 的概率,最后argmax相当于多个模型的局部投票,接缝处的预测会更平滑。prob_map用 float32 累加,不会溢出,但推理时间会比硬切多一倍左右,属于典型的以时间换质量。
6.2 源码目录与答辩演示:让老师顺着你的脚本走
课程设计源码的组织方式直接影响老师能不能快速看懂你的项目。我习惯把源码拆成四个文件:dataset.py负责数据读取和增强,model.py放网络结构,train.py放训练和验证逻辑,predict.py负责大图推理和可视化。另加一个README.md,写清楚环境依赖、数据集格式和运行命令。不要把所有代码塞进一个文件里跑到底,那样即使结果好,老师追问时你也很难自圆其说。
答辩演示时先跑一次predict.py输出一张彩色分类图,再贴一张训练过程里 mIoU 曲线,最后把best_model.pth拿上台演示。只要代码能按 README 一键跑通,结果图边界干净、mIoU 有上升趋势,这门课程设计基本就稳了。这些年我自己的习惯是:每次改完模型,先看一眼推断结果里最薄的类别(比如道路)画得对不对,再看 mIoU,因为指标可以协商,图错了就是错了。这套流程也希望能帮到你。
本文还有配套的精品资源,点击获取