☰
航拍图像语义分割毕设实战:Python+DeepLabv3+全流程指南
2026/10/1 13:28:58 网站建设 项目流程

简介:这份资源是面向高校计算机、人工智能及相关专业学生的毕业设计参考项目,围绕Python与DeepLabv3+模型展开高分辨率航拍图像的语义分割任务,适合具备一定深度学习基础、需要完成遥感影像分割课题或希望复现经典分割网络的学习者。压缩包共184个文件,以95个py源码文件与84个pyc编译文件为主体,另含3个ipynb实验笔记、1个txt与1个md说明文档,整体约477KB,体积轻量便于本地部署与二次修改。源码中涵盖swin、resnet、hrnet、twins、bisenetv2、beit等多种骨干网络实现,并配有离线与在线推理的notebook示例,可帮助读者理解编码器—解码器结构、空洞卷积与多尺度特征融合等关键设计。目前已有204人学习下载,可作为毕业设计选题、模型对比实验与代码调试的实用起点。

1. 航拍语义分割毕设:为什么 DeepLabv3+ 仍是性价比最高的起点

做遥感或航拍方向的毕业设计,绕不开一个核心问题:怎么把一张几千乘几千像素的航拍图,逐像素分成建筑、道路、植被、水体这些类别。这就是语义分割要干的事。而 DeepLabv3+ 在这个任务上,长期是一个"稳"字当头的选择——它把空洞卷积和编解码结构捏在一起,既能在高分辨率图上保住大感受野,又不会像纯 FCN 那样把边界糊成一团。很多同学一上来就想追最新的分割大模型,结果显存炸了、训练周期拖到答辩前一周还没收敛,血泪经验告诉我们:毕设要的是能跑通、能出图、能写进论文的完整闭环,不是刷榜。这篇笔记就围绕 Python + DeepLabv3+ 这条线,把航拍图像语义分割从环境搭建、数据制作、模型训练到推理出图整条链路拆开讲清楚,适合正在做计算机毕业设计、遥感方向或想入门语义分割的从业者照着复现。

2. 航拍语义分割的任务边界与 DeepLabv3+ 选型逻辑

2.1 航拍图像和自然图像到底差在哪

很多人拿 DeepLabv3+ 直接套在航拍数据上翻车,根源是没意识到航拍图和 COCO、VOC 那类自然图像根本不是一个分布。自然图像里一只猫占画面三分之一,航拍图里一栋楼可能只占几十个像素;自然图像有明确的透视和前景背景,航拍图是近似正射的俯视图,同一类目标在不同高度、不同光照下尺度差异极大。这带来三个直接影响:第一,类别极不平衡,道路这种细长目标像素占比可能不到 2%;第二,边界模糊,屋顶和地面的过渡往往只有一两个像素;第三,图像尺寸大,动辄 4000×3000,直接送进网络显存扛不住。

理解这三点,才能理解为什么航拍分割对模型的要求和自然图像不一样。你需要一个能在下采样过程中保留空间细节、又能对大尺度目标维持感受野的结构。DeepLabv3+ 的 ASPP 模块用不同膨胀率的空洞卷积并行提取多尺度上下文,解码器再把浅层特征和深层特征融合,恰好对上航拍图"多尺度 + 边界敏感"这两个痛点。这也是它在遥感分割论文里被反复当 baseline 的原因。

2.2 DeepLabv3+ 的三个关键结构

把 DeepLabv3+ 拆开看,核心就三块。第一块是主干网络,常见做法是用 ResNet-50 或 ResNet-101 配合空洞卷积,把最后几个 stage 的下采样步长改掉,让输出特征图保持较大的空间分辨率。第二块是 ASPP,也就是空洞空间金字塔池化,用 1×1 卷积加三个不同膨胀率(通常 6、12、18)的 3×3 空洞卷积,再加一个全局平均池化分支,五路 concat 起来。第三块是解码器,把主干浅层特征经过 1×1 卷积降维,和 ASPP 输出上采样后拼接,再经过几次卷积恢复分辨率。

这里有个容易被忽略的点:主干的下采样倍率决定了你最终输出 stride 是 8 还是 16。航拍图边界要求高,一般用 output_stride=8,也就是主干只下采样 8 倍,代价是显存和计算量上升。如果显存紧张,退到 16 也能用,但小目标边界会明显变糙。这个参数在动手前就要定好,中途改会牵一发动全身。

2.3 为什么毕设场景优先选它而不是追新

从落地角度讲,DeepLabv3+ 有三个现实优势。一是开源实现成熟,PyTorch 生态里有大量可参考的复现,改起来心里有底;二是参数量适中,ResNet-50 版本在 8G 显存的卡上就能训,学生党用实验室或云上的单卡完全够;三是论文好写,结构清晰、消融实验好设计,ASPP 膨胀率、主干深度、output_stride 都能拿来做对比。相比之下,一些更新的分割架构要么依赖超大预训练权重,要么训练 trick 多到难以复现,对毕设这种时间紧、要交付完整文档的场景并不友好。选型不是选最强,是选最匹配你约束条件的。

3. 用 Python 把航拍数据做成 DeepLabv3+ 能吃的格式

3.1 环境搭建:conda 建环境与关键依赖

先把环境隔离出来,别在系统 Python 里乱装。下面这套命令在 Linux 和 Windows 的 conda 下都通用,Python 版本建议 3.8 到 3.10,太新有些库轮子还没跟上。

# 创建独立环境,避免污染系统 Python conda create -n deeplab_aerial python=3.9 -y conda activate deeplab_aerial # 安装 PyTorch,按你的 CUDA 版本去官网选对应命令,这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 分割任务常用依赖 pip install opencv-python pillow numpy matplotlib tqdm scikit-learn tensorboard

逻辑说明:conda 负责隔离解释器和底层库,pip 装 PyTorch 时一定要带--index-url指向官方轮子源,否则容易装到 CPU 版。opencv-python用于读写大图和做几何变换,tqdm看训练进度,tensorboard记录 loss 曲线方便写论文。参数上,CUDA 版本要和驱动匹配,用nvidia-smi看右上角支持的 CUDA 版本再决定装哪个。

3.2 航拍数据集制作:标注、类别与目录结构

航拍语义分割的数据来源通常是公开数据集(如 LoveDA、Potsdam)或自己用无人机采集后标注。标注工具常见的是 LabelMe 或 ArcGIS,导出成掩膜 PNG,每个像素值对应类别索引。这里有个硬性要求:掩膜必须是单通道 8 位图,像素值 0、1、2……对应背景、类别一、类别二,千万别存成 RGB 彩色图,否则读进来全是坑。

目录结构建议这样组织,训练脚本按这个约定去读:

dataset/ ├── images/ │ ├── train/ │ │ ├── 001.png │ │ └── 002.png │ └── val/ │ └── 003.png └── masks/ ├── train/ │ ├── 001.png │ └── 002.png └── val/ └── 003.png

图像和掩膜文件名必须一一对应,这是后面 Dataset 类能对齐的前提。类别数在动手前就统计清楚,比如背景 + 建筑 + 道路 + 植被 + 水体就是 5 类,这个数字直接决定模型输出通道数,中途改要重训。

3.3 大图切分与数据增强脚本

航拍图动辄几千像素,必须切块。下面这个脚本把大图和对应掩膜按固定窗口切分,带重叠避免边界目标被切断。

import os import cv2 import numpy as np def slide_crop(img, mask, crop_size=512, stride=384): """按滑动窗口切分大图和掩膜,stride 小于 crop_size 产生重叠""" h, w = img.shape[:2] patches = [] for y in range(0, h - crop_size + 1, stride): for x in range(0, w - crop_size + 1, stride): img_patch = img[y:y+crop_size, x:x+crop_size] mask_patch = mask[y:y+crop_size, x:x+crop_size] # 跳过几乎全背景的块,减少无效样本 if np.mean(mask_patch > 0) < 0.02: continue patches.append((img_patch, mask_patch)) return patches def process_folder(img_dir, mask_dir, out_img_dir, out_mask_dir): os.makedirs(out_img_dir, exist_ok=True) os.makedirs(out_mask_dir, exist_ok=True) for name in os.listdir(img_dir): img = cv2.imread(os.path.join(img_dir, name)) mask = cv2.imread(os.path.join(mask_dir, name), cv2.IMREAD_GRAYSCALE) if img is None or mask is None: continue for i, (ip, mp) in enumerate(slide_crop(img, mask)): base = os.path.splitext(name)[0] cv2.imwrite(f"{out_img_dir}/{base}_{i}.png", ip) cv2.imwrite(f"{out_mask_dir}/{base}_{i}.png", mp) if __name__ == "__main__": process_folder("dataset/images/train", "dataset/masks/train", "dataset/crops/images/train", "dataset/crops/masks/train")

逻辑说明:crop_size是切块边长,512 是显存和上下文的一个平衡点;stride控制重叠,设成 384 意味着相邻块有 128 像素重叠,能缓解目标被切断的问题。np.mean(mask_patch > 0) < 0.02这行是过滤纯背景块,航拍图里大片空地很多,不过滤会让正负样本严重失衡。参数怎么调:显存小就把 crop_size 降到 256,但太小会丢失大目标的上下文;重叠太多会让训练集膨胀、训练变慢,一般 stride 取 crop_size 的 0.7 到 0.75 倍比较合适。

数据增强方面,航拍图适合用随机翻转、90 度旋转、颜色抖动,但不要用大幅度的透视变换,因为俯视图本身没有透视畸变,硬加反而引入噪声。增强在 Dataset 的__getitem__里用 albumentations 做最方便,注意图像和掩膜要同步变换。

4. 训练 DeepLabv3+:损失、参数与显存控制

4.1 损失函数选择:交叉熵不够用的时候

航拍分割最大的问题是类别不平衡,道路、小建筑这些类像素占比极低,纯交叉熵会让模型倾向于全预测成背景或植被。常见做法是交叉熵加 Dice 损失组合,Dice 对前景占比敏感,能把小类的梯度拉起来。

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, targets): # logits: [B, C, H, W], targets: [B, H, W] probs = F.softmax(logits, dim=1) num_classes = logits.shape[1] targets_onehot = F.one_hot(targets, num_classes).permute(0, 3, 1, 2).float() intersection = (probs * targets_onehot).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + targets_onehot.sum(dim=(2, 3)) dice = (2 * intersection + self.smooth) / (union + self.smooth) return 1 - dice.mean() class CombinedLoss(nn.Module): def __init__(self, ce_weight=1.0, dice_weight=1.0): super().__init__() self.ce = nn.CrossEntropyLoss(ignore_index=255) self.dice = DiceLoss() self.ce_weight = ce_weight self.dice_weight = dice_weight def forward(self, logits, targets): return self.ce_weight * self.ce(logits, targets) + \ self.dice_weight * self.dice(logits, targets)

逻辑说明:ignore_index=255让标注里未定义区域不参与损失,这在航拍数据里很常见。Dice 用 softmax 后的概率算,smooth防止分母为零。两个权重一般各取 1.0 起步,如果发现小类还是学不出来,把 dice_weight 提到 2.0 试试。注意 Dice 对每个类单独算再平均,这样小类不会被大类淹没。

4.2 训练参数:学习率、batch size 与 output_stride

学习率用多项式衰减是分割任务的老套路,初始 0.01 配 SGD,或者 1e-4 配 Adam。航拍数据量通常不大,Adam 收敛更快,毕设时间紧可以优先用 Adam。batch size 受显存限制,8G 卡上 ResNet-50、crop 512、output_stride=8 大概能跑 batch 4,用梯度累积凑等效大 batch。

import torch from torch.optim import Adam from torch.optim.lr_scheduler import PolynomialLR model = DeepLabV3Plus(num_classes=5, backbone="resnet50", output_stride=8) model = model.cuda() optimizer = Adam(model.parameters(), lr=1e-4, weight_decay=1e-4) # 多项式衰减,power=0.9 是分割任务常用值 scheduler = PolynomialLR(optimizer, total_iters=100 * len(train_loader), power=0.9) for epoch in range(100): model.train() for imgs, masks in train_loader: imgs, masks = imgs.cuda(), masks.cuda() logits = model(imgs) loss = criterion(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()

逻辑说明:weight_decay防过拟合,航拍数据少的时候尤其重要。PolynomialLR的total_iters要按总迭代次数算,不是 epoch 数,写错会导致学习率提前衰减到零。output_stride=8 精度高但慢,如果训练一轮要几小时,可以先降到 16 快速验证流程通不通,再切回 8 正式训。

4.3 显存不够时的三个降级手段

显存爆了别急着换卡,先试这三招。第一,开混合精度训练,用torch.cuda.amp,显存能省三成左右,速度还快。第二,减小 crop_size,从 512 降到 384 或 256,代价是上下文变少。第三,用梯度累积,把 batch 拆成几次前向再统一更新,等效大 batch 但显存占用按小 batch 算。

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() accum_steps = 4 for i, (imgs, masks) in enumerate(train_loader): imgs, masks = imgs.cuda(), masks.cuda() with autocast(): logits = model(imgs) loss = criterion(logits, masks) / accum_steps scaler.scale(loss).backward() if (i + 1) % accum_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()

逻辑说明:autocast让前向用半精度,GradScaler处理梯度缩放防止下溢。loss 除以accum_steps是为了让累积后的梯度量级和真实大 batch 一致。注意optimizer.zero_grad()要放在累积周期结束时,放错位置梯度就白攒了。

5. 推理出图与精度评估:把结果变成论文里的图表

5.1 大图滑窗推理与拼接

训练时切块,推理时也要切块,但拼接要处理好重叠区域,否则接缝处会有明显断层。常见做法是重叠区取平均或取最大概率。

import torch import numpy as np import cv2 def inference_large_image(model, img_path, crop_size=512, stride=384, num_classes=5): model.eval() img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] # 累加概率图和计数图,用于重叠区平均 prob_map = np.zeros((num_classes, h, w), dtype=np.float32) count_map = np.zeros((h, w), dtype=np.float32) for y in range(0, h, stride): for x in range(0, w, stride): y2 = min(y + crop_size, h) x2 = min(x + crop_size, w) y1 = max(0, y2 - crop_size) x1 = max(0, x2 - crop_size) patch = img[y1:y2, x1:x2] tensor = torch.from_numpy(patch).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): logits = model(tensor.cuda()) prob = torch.softmax(logits, dim=1).squeeze(0).cpu().numpy() prob_map[:, y1:y2, x1:x2] += prob count_map[y1:y2, x1:x2] += 1 count_map[count_map == 0] = 1 prob_map /= count_map pred = np.argmax(prob_map, axis=0).astype(np.uint8) return pred

逻辑说明:y1 = max(0, y2 - crop_size)这行保证边缘块也能取满 crop_size,不会因为越界而变小。概率累加再除以计数,等价于重叠区加权平均,比直接取最后一个块的结果平滑得多。参数 stride 和训练时保持一致,否则尺度对不上。

5.2 mIoU 与混淆矩阵:论文里必须有的指标

语义分割论文绕不开 mIoU,它按类算交并比再平均,能反映小类的表现。光看像素准确率会被背景类带偏,一个全预测背景的模型准确率可能也有 80%,但 mIoU 惨不忍睹。

import numpy as np def compute_confusion_matrix(preds, targets, num_classes): mask = (targets >= 0) & (targets < num_classes) hist = np.bincount( num_classes * targets[mask].astype(int) + preds[mask], minlength=num_classes ** 2 ).reshape(num_classes, num_classes) return hist def compute_miou(conf_matrix): intersection = np.diag(conf_matrix) union = conf_matrix.sum(axis=1) + conf_matrix.sum(axis=0) - intersection iou = intersection / np.maximum(union, 1) return np.nanmean(iou), iou

逻辑说明:混淆矩阵的行是真实类,列是预测类,np.bincount用类别索引编码一次性统计。np.maximum(union, 1)防止某类完全没出现时除零。返回的iou数组能看出哪一类拖后腿,写论文时针对低 IoU 的类做分析,比只报一个总分有说服力。

5.3 可视化:让答辩老师一眼看懂

出图时把原图、真值、预测三张并排,用固定颜色表映射类别,比单张预测图直观得多。颜色表要固定,别每次随机,否则不同实验的图没法对比。边界处可以叠加原图做半透明,突出分割贴合程度。这些图直接进论文的实验章节,比堆数字有效。

6. 航拍分割避坑清单:五个我踩过的坑

6.1 掩膜存成 RGB 导致类别全乱

现象:训练 loss 一直不降,预测图颜色诡异。原因:标注工具导出的是彩色掩膜,读进来是三通道,像素值是 RGB 组合而不是类别索引。解决:用cv2.imread(path, cv2.IMREAD_GRAYSCALE)读,并检查唯一值是不是 0 到 num_classes-1 的整数,发现是彩色图就用调色板反查回索引。

6.2 图像和掩膜增强不同步

现象:训练集上表现正常,验证集 mIoU 极低。原因:数据增强时图像做了随机翻转,掩膜没跟着翻,标签和内容对不上。解决:用 albumentations 的A.Compose把图像和掩膜放一起变换,或者手写增强时对两者用同一组随机参数。

6.3 忽略背景类导致 mIoU 虚高

现象:mIoU 报出来 0.7,但预测图里道路全断。原因:计算 mIoU 时把背景类也算进去,背景占比大拉高了均值。解决:单独看每个类的 IoU,论文里明确说明是否含背景,通常汇报时把背景排除或单独列出。

6.4 学习率衰减写错迭代次数

现象:训练到一半 loss 突然不动。原因:PolynomialLR的total_iters按 epoch 数填了,实际应该是总迭代次数,导致学习率提前归零。解决:total_iters = epochs * len(train_loader),训练前打印一下确认。

6.5 推理拼接出现网格接缝

现象:大图推理结果有明显方块边界。原因:滑窗推理时重叠区直接覆盖,没有做概率平均。解决:按 5.1 的累加计数法拼接,重叠区取平均概率,接缝自然消失。

7. 把 DeepLabv3+ 用到极致:几个能拉开差距的进阶技巧

想让毕设从"能跑"变成"有亮点",可以在几个地方做文章。第一个是主干替换,把 ResNet-50 换成轻量的 MobileNetV2 或更深的 ResNet-101,做一组对比实验,讨论精度和速度的权衡,这在论文里是标准的消融设计。第二个是注意力模块,在 ASPP 后面接一个 SE 或 CBAM,航拍图里通道间差异明显,注意力往往能带来一两个点的 mIoU 提升,代码改动量也不大。

第三个是损失函数的类别权重,针对你数据里最稀少的类,在交叉熵里给它更高的权重,或者用 Focal Loss 替代普通交叉熵,对小目标效果明显。第四个是测试时增强(TTA),推理时把图翻转、多尺度各跑一遍再平均,mIoU 通常能涨零点几个点,代价是推理变慢,答辩演示够用。

验证这些改动有没有效,别只看最终 mIoU,要固定随机种子、固定数据划分,每次只改一个变量,记录到表格里。我一般会建一个实验记录表,列清楚主干、output_stride、损失权重、mIoU、单类 IoU,跑完一组填一行,写论文时直接拿来用,省得回头翻日志。

最后说个习惯:航拍数据标注成本高,别一上来就标几千张,先标一两百张把整条链路跑通,确认模型能学起来、推理出图正常,再批量标注扩充。我见过太多人闷头标了一个月数据,结果训练脚本有 bug,全白干。先小后大、先通后优,这是做分割毕设最省命的节奏。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询