简介面向毕业设计场景的航拍图像语义分割项目以DeepLabv3为核心模型聚焦高分辨率遥感影像中的地物分类与像素级分割任务适合深度学习方向的本硕学生用于课题研究、算法实践或毕业设计答辩准备。整个压缩包共有一百八十四个文件包含九十五个Python源码脚本、八十四个pyc编译文件、三个Jupyter Notebook交互式演示以及README说明文档整体体积约四百七十七KB结构精简但模块完整。源码中整合了Swin、ResNet、HRNet、Twins、BiSeNetv2、BEiT等多种骨干网络实现同时提供在线与离线两种模式的推理Notebook既方便对比不同编码器的分割效果也便于开展消融实验与二次开发。目前已有二百零三人学习下载对于需要快速构建语义分割基线、完成毕业设计实验或进行算法选型对比的读者而言是一份值得参考的完整代码包。1. DeepLabv3 做高分辨率航拍语义分割为什么这个组合能扛住毕业设计也能扛住真实场景每年毕设季遥感语义分割都是热门方向但多数人第一次跑通时都会愣一下——航拍图和 COCO 那种自然图完全不是一回事一幅 5000×5000 的影像里耕地、水体、建筑、道路挤在一起目标尺度差异大GPU 显存一碰就爆。我给的答案是别折腾那些新出的花哨模块直接用 DeepLabv3。它的编码器-解码器结构、ASPP 多尺度上下文提取和成熟的预训练权重能让一个新手在一周内从零跑到可视化出图也能让老手在它上面改出能用的工程方案。这篇笔记把这条线从头到尾捋一遍原理怎么选型、数据怎么切、训练怎么配、坑在哪、最后怎么出面积估算结果。2. 拆开 DeepLabv3ASPP、编码器-解码器和骨干网络按航拍图的特点选型2.1 空洞卷积和 ASPP如何只加卷积不加参数量就看到更大范围DeepLabv3 的核心不是网络很深而是它处理“尺度差异”的方式。航拍图里一个篮球场大小的地块和一栋小房子在像素尺度上相差几十倍普通卷积堆叠想同时兼顾两者只能靠加深网络扩大感受野但下采样会丢掉边缘细节。空洞卷积解决的就是这个问题在卷积核里插入空洞不增加参数量却能成倍扩大感受野。标准 3×3 卷积在 stride1 时感受野是 3×3空洞率 rate2 时变成 7×7rate4 时变成 15×15。ASPPAtrous Spatial Pyramid Pooling把这个思路并联起来同一个特征图分别用 rate1、6、12、18 的 3×3 空洞卷积并行提取再加上一个全局平均池化分支最后拼到一起再过 1×1 卷积。这样同一个特征图上有四种不同尺度的上下文视野正好对应航拍图里“大块农田—中等街区—小型房屋—细窄道路”的多尺度目标。我自己的经验是ASPP 这层在航拍任务里基本不用改默认 6/12/18 三个空洞率就足够硬调反而容易让小目标崩掉。2.2 ResNet、Xception 还是 MobileNetV2高分辨率输入的骨干选型DeepLabv3 的编码器部分可以用很多骨干但实际用下来就三个选择ResNet101、Xception、MobileNetV2。ResNet101 是精度天花板参数多、训练慢如果只有一块 8GB 显存的卡输入尺寸 512×512、batch size 4 基本就是极限。Xception 是原论文主力精度接近 ResNet101但要自己改结构工程上维护成本高。MobileNetV2 轻量适合快速验证和 CPU 推理精度比前两个掉 3-5 个点 mIoU。骨干网络参数量级别优点缺点适用场景ResNet101约 44M精度高、权重好找、稳定显存占用大、训练慢有 11GB 以上显存、追求精度Xception约 41M原论文结构、精度接近 ResNet101改动大、麻烦想完全复现论文MobileNetV2约 7M训练快、显存友好、CPU 能跑精度偏低快速验证、边缘部署毕设和一般工程项目我推荐咬咬牙上 ResNet101精度高能给后面省很多事。显存不够就降输入尺寸或 batch size别换骨干。预训练权重要用 ImageNet 上预训练的那份不要随机初始化从头训否则收敛慢得让人怀疑人生而且 mIoU 会差 10 个点以上。2.3 OS16 和 OS8输出步长与细节恢复的取舍OSoutput stride是 DeepLabv3 里一个关键参数它表示输入分辨率与最终特征图分辨率的比值。默认 OS16意思是 512×512 的输入最后输出 32×32 的特征图。OS8 则保留 64×64边缘细节明显更好但显存占用和计算量都翻倍。航拍图里道路、屋顶边缘这些细长目标对边缘细节很敏感我一般都把 decoder 的 OS 设为 8。但要注意OS8 时 backbone 里某些层的 stride 或空洞率会被改动这会改变特征图的尺寸对齐关系如果预训练权重结构对不上加载时会报错或者静默错位。所以实际做法是网络结构里把骨干最后一个下采样层的 stride 改为 1同时把后续卷积的空洞率翻倍来补偿感受野损失。这一步最容易翻车建议改完结构后先打印一下各层输出尺寸确认没问题再开始训练。3. 高分辨率数据准备切块、标注与增强的完整流程3.1 航拍图为什么不能整张喂进网络显存与感受野的现实约束很多人第一次碰航拍图习惯性地想直接把整张影像 resize 成 512×512 丢进网络。这几乎是必翻车的操作。一张 5000×5000 的航拍图缩到 512×512一个本来占 50×50 像素的房子被压成 5×5 像素语义信息直接丢光模型学到的是“一块绿绿的东西”而不是一栋房子。反过来如果不缩放直接输入一张图就是 25M 像素即使不算显存forward 一次也要几十秒训练一轮要几天。正确做法是滑窗切块把大图按固定尺寸切成若干小块模型只吃小块预测完再拼回去。切块尺寸一般取 512×512 或 768×768太小上下文不足太大显存撑不住。重叠率设 10%-20%可以避免目标被硬生生切成两半导致预测错乱。另外切块要考虑类别均衡如果整张图水体占 70%切出来的块里也基本都是水模型会被带偏。我一般统计每块的类别比例让 sampled 数据分布尽量均匀。3.2 滑窗切块脚本带重叠率、支持标签同步切下面给一个我常用的切块脚本用 OpenCV 处理影像和标签支持重叠率设置同时输出路径清单方便后续做训练集划分import cv2 import numpy as np from pathlib import Path def sliding_crop(image_path, label_path, save_dir, crop_size512, overlap0.15): img cv2.imread(image_path) label cv2.imread(label_path, cv2.IMREAD_GRAYSCALE) h, w img.shape[:2] save_img Path(save_dir) / images save_lbl Path(save_dir) / labels save_img.mkdir(parentsTrue, exist_okTrue) save_lbl.mkdir(parentsTrue, exist_okTrue) stride int(crop_size * (1 - overlap)) idx 0 for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): crop_img img[y:y crop_size, x:x crop_size] crop_lbl label[y:y crop_size, x:x crop_size] cv2.imwrite(str(save_img / f{Path(image_path).stem}_{idx}.png), crop_img) cv2.imwrite(str(save_lbl / f{Path(image_path).stem}_{idx}.png), crop_lbl) idx 1 # 处理右侧和底部的边界块避免漏掉边缘目标 if w % stride ! 0: for y in range(0, h - crop_size 1, stride): x w - crop_size cv2.imwrite(str(save_img / f{Path(image_path).stem}_{idx}.png), img[y:y crop_size, x:x crop_size]) cv2.imwrite(str(save_lbl / f{Path(image_path).stem}_{idx}.png), label[y:y crop_size, x:x crop_size]) idx 1 if h % stride ! 0: for x in range(0, w - crop_size 1, stride): y h - crop_size cv2.imwrite(str(save_img / f{Path(image_path).stem}_{idx}.png), img[y:y crop_size, x:x crop_size]) cv2.imwrite(str(save_lbl / f{Path(image_path).stem}_{idx}.png), label[y:y crop_size, x:x crop_size]) idx 1 sliding_crop(aerial_001.tif, aerial_001_mask.png, ./dataset)逻辑上主循环负责按固定步长切割步长由 crop_size×1-overlap算出来overlap0.15 时 512 尺寸对应步长 435这样相邻两块有 77 像素重叠能有效缓解目标被切断的问题。边界处理单独补了右侧和底部两刀不然图像边缘的目标永远被裁掉一半。参数上crop_size 主要看 GPU 显存8GB 卡用 51211GB 以上可以上 768overlap 不要超过 30%否则切出来的块大量重复训练效率低且容易过拟合。3.3 标注格式与数据集读取从 LabelMe 到训练数据集的转换路线航拍图标注常用 LabelMe 或精灵标注助手输出的是 JSON 格式的多边形坐标不是直接的语义分割标签。需要一个转换步骤把 JSON 里的多边形逐个填充成掩码再合并成单通道的类别标签图。类别编号要从 0 开始连续排列0 留给背景1、2、3 依次给建筑、水体、耕地等类别。这一步最容易踩的坑是类别编号不连续比如跳过 2 直接从 1 到 3模型输出维度对不上训练必然报错。另一个常见问题是标签图里有 255 或 -1 这样的忽略像素。如果标注时把边缘或模糊区域标成 255训练时要在 loss 计算里显式忽略。Keras/TensorFlow 的 SparseCategoricalCrossentropy 里有个参数可以直接传PyTorch 的 CrossEntropyLoss 同样支持 ignore_index255。很多人第一次训练没设这个参数loss 直接飙到 NaN就是这个原因。3.4 数据增强旋转、翻转、色彩抖动怎么配航拍图的增强策略和自然图不一样。自然图常用的随机裁剪、水平翻转在航拍图上依然有效但要注意两点随机旋转按 90 的倍数转不要任意角度转否则标注框和边缘会插值出大量假边界色彩抖动不宜过强航拍图的色调相对稳定乱调会让模型对光照过度敏感。我常用的增强组水平翻转概率 0.5、垂直翻转概率 0.5、随机 90 度旋转概率 0.3、亮度饱和度抖动幅度 0.1-0.2。如果类别不均衡严重可以再配合随机裁剪时按类别比例采样而不是纯随机。4. 训练落地最小可跑通的参数集与训练脚本4.1 最小训练命令预训练权重、Batch Size 和 GPU 要求环境上Python 3.8 以上TensorFlow 2.6 以上或 PyTorch 1.10 以上都行。安装阶段最容易翻车的不是模型代码本身而是 CUDA、cuDNN 和框架版本不匹配建议先跑一句import tensorflow as tf; print(tf.test.is_gpu_available())确认 GPU 真能用到再开始训练省得跑了一半才发现模型在 CPU 上磨了几小时。我用 TensorFlow 2 跑通的最小训练代码骨架如下import tensorflow as tf from model import deeplabv3_plus # 自定义模型结构 # 输入 512x512x321 类0 背景 20 类地物 model deeplabv3_plus(input_shape(512, 512, 3), num_classes21, backboneresnet101) # 加载 ImageNet 预训练权重skip 掉分类头 model.load_weights(resnet101_imagenet.h5, by_nameTrue, skip_mismatchTrue) lr_schedule tf.keras.optimizers.schedules.PolynomialDecay( initial_learning_rate0.007, decay_steps20000, end_learning_rate0.00001, power0.9) model.compile( optimizertf.keras.optimizers.SGD(learning_ratelr_schedule, momentum0.9), losstf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue, ignore_index255), metrics[tf.keras.metrics.SparseCategoricalAccuracy()] ) model.fit(train_dataset, validation_dataval_dataset, epochs60, callbacks[...])这段代码的核心有三处load_weights 用 by_nameTrue 只加载骨干部分避免分类头维度不匹配学习率用 PolynomialDecay从 0.007 按 0.9 的 power 衰减到 0.00001这是 DeepLabv3 原论文的标配策略loss 里显式 ignore_index255。SGDmomentum 比 Adam 在分割任务上更稳虽然前期 loss 下降看着慢但最终 mIoU 通常高 1-2 个点。batch size 在 8GB 显存下取 411GB 取 8再多就爆显存。4.2 为什么用 poly 学习率分割模型和分类模型的训练节奏差别很多人从图像分类转过来习惯用 step decay 或者 ReduceLROnPlateau但在 DeepLabv3 上不推荐。语义分割的 loss 曲面更复杂前期需要大学习率快速跳到好区域后期要非常小地精细打磨边界poly 策略正好是一条平滑下降的曲线。power0.9 是原论文调好的别乱改。如果显存只够跑 batch size 2学习率要相应降到 0.003 左右否则梯度噪声太大loss 会震荡不收敛。真实项目中我遇到过训练到第 30 个 epoch loss 还在缓慢下降但 mIoU 已经不动的情况。这时先别急着调结构把学习率衰减曲线拉长或者把 epoch 数翻倍往往 mIoU 还能再涨 2-3 个点。DeepLabv3 的训练周期本来就比分类任务长60 个 epoch 是下限100 个 epoch 才是完整训练。4.3 loss 选择交叉熵、OHEM 与 dice loss 的取舍默认用 SparseCategoricalCrossentropy 就能跑但航拍图类别不均衡是常态比如“道路”类只占全图 3% 的像素普通交叉熵会让模型直接忽略它。解决方案有两档轻量方案是给 loss 加类别权重class_weight按像素频率倒数设置进阶方案是 OHEMOnline Hard Example Mining每轮只取 loss 最高的前 30% 像素回传梯度让模型把注意力放在难分区域。我一般先加类别权重如果小类别 mIoU 仍然特别低再上 OHEM。dice loss 在医学分割里很流行但航拍场景慎用。它的梯度在高置信区域容易饱和和交叉熵混合起来调参比较烦。如果要用按 0.5 交叉熵 0.5 dice 的比例起步dice 部分用平滑系数 1.0别直接用纯 dice。4.4 训练中看什么mIoU、PA 和 loss 曲线的判读训练日志里别只盯 loss。loss 下降不能说明分割效果好因为背景类像素占比大loss 会被它主导。核心指标就两个mIoU所有类别的 IoU 平均值和 PA像素准确率。PA 虚高哪怕把所有像素都预测成背景PA 也能有 70% 以上所以重点看 mIoU以及每一类单独的 IoU。训练时第 10 个 epoch mIoU 到 55%到 40 个 epoch 涨到 70%都属于正常轨迹如果第 5 个 epoch 还不到 30%优先检查标签是否正确、数据有没有对齐。验证集上如果 mIoU 还行但可视化时边界都是锯齿状那是 CRF 该上场的时候如果大面积粘连比如两栋相邻建筑被预测成一块是 ASPP 感受野不够或数据里这类场景太少该加数据而不是加后处理。5. 高分辨率航拍分割避坑五个我踩过的实战问题5.1 显存溢出改小 batch size 还是换切块尺寸现象训练一开始ResourceExhaustedError8GB 显存连 batch size 2 都跑不动。原因模型输入分辨率太高或 decoder 的 OS8 导致特征图大再加上 ASPP 四个并行分支同时占显存。解决优先把输入尺寸从 768 降到 512代价是 mIoU 会掉 1-2 个点其次用梯度累积每 4 个 batch 攒一次梯度再更新等效 batch size 翻倍但显存不变。我一般先降输入尺寸因为切块尺寸对显存是平方级影响而梯度累积只增加训练时间。5.2 小目标全丢OS8 和浅层特征融合的经验现象验证集上建筑、耕地这些大类 mIoU 有 80% 以上但车辆、独立树木这种小目标 IoU 只有个位数可视化图上小目标直接消失。原因backbone 下采样 32 倍后小目标在特征图里只剩一两个像素decoder 上采样救不回来。解决把 output stride 改成 8让特征图保留更高分辨率另外 DeepLabv3 的 decoder 里本身就有一个 1×1 卷积接 backbone 浅层 low-level feature 的分支这个分支对边缘定位很关键如果模型实现里省掉了它就补回去。5.3 CRF 后处理反而掉点什么时候别加后处理现象加完 CRF 后 mIoU 不仅没涨还掉了 1-2 个点尤其是建筑类边缘变差了。原因CRF 的势能参数没调默认值对航拍图的颜色分布不敏感或者模型本身已经较强边界已经很干净CRF 把正确的预测“修正”错了。解决先做可视化对比只在模型边界模糊、呈碎片状时才加 CRF。配套参数用w110, w23, alpha80, beta13, gamma3, iterations5起步这是遥感图一个比较常用的初始值。加完后要在验证集上重新算 mIoU别凭肉眼感觉。5.4 训练 loss 变成 NaN标签、学习率与 BN 的连环坑现象loss 在某个 epoch 突然变成 NaN或者从一开始就是 NaN。原因有三类按概率排序标签里有 255 或负值没设 ignore_index学习率太大或 batch size 太小导致梯度爆炸加载预训练权重时 BN 层的均值和方差被意外清零。解决先把 ignore_index 设好确认标签图像的像素值都在 [0, num_classes-1] 范围内再把学习率降到 0.001 试跑 10 个 step如果还不行检查一下 load_weights 是否加载了 BN 层的 gamma 和 beta只加载卷积层的权重一般最稳。5.5 切块边缘拼接缝明显重叠率与投票策略现象整图预测完成后拼接处出现一条条明显的切割线同一栋房子在切块边界两侧被预测成不同类别。原因切块时没有重叠模型看不到目标全貌边界区域的上下文信息不足。解决切块时设置重叠率 20% 以上预测时每个像素如果出现在多个切块中用概率平均而不是 argmax 取最大值即对每个切块的 softmax 输出做累加最后除以覆盖次数再取 argmax。这样拼接缝基本消失代价是推理时间增加约 20%但效果提升明显。6. 从分割图到面积估算一张航拍图的完整推理与结果输出6.1 推理脚本滑窗预测并拼回原分辨率训练完成后要处理整张航拍图不是一张张喂小块再手动拼那样漏边界也费时间。我一般写一个推理脚本同样用滑窗思路但对每个切块保留 softmax 概率输出按像素坐标累加后取平均最后一次性 argmax 得到整图分割结果。这个做法能直接复用训练时的切块逻辑也顺便解决了拼接缝问题。def predict_full_image(model, image, crop_size512, overlap0.2): h, w image.shape[:2] stride int(crop_size * (1 - overlap)) prob_map np.zeros((h, w, num_classes), dtypenp.float32) count_map np.zeros((h, w, 1), dtypenp.float32) for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): crop image[y:y crop_size, x:x crop_size] / 255.0 prob model.predict(np.expand_dims(crop, 0), verbose0)[0] prob_map[y:y crop_size, x:x crop_size] prob count_map[y:y crop_size, x:x crop_size] 1 prob_map / np.maximum(count_map, 1) return np.argmax(prob_map, axis-1)这段代码和训练时的切块共用同一套 stride 逻辑overlap 取 0.2 保证拼接区域的每个像素被预测至少两遍。prob_map 累加的是 softmax 概率而不是 argmax 类别只有概率平均能保留不确定性最后统一 argmax 相当于让多个切块对边界区域做了一次投票。推理时要注意把输入像素除以 255 归一化和训练时的预处理保持一致否则模型输出会整体偏移。6.2 地物面积估算像素数乘分辨率的落地算法分割出类别掩码后面积估算就是一个像素级统计问题。航拍影像通常带地理参考每个像素对应地面的实际尺寸已知——比如 0.5 米分辨率即每个像素代表 0.25 平方米。统计各类别像素数乘上单像素面积就能得到各地物类型的占地估算。这一步对建筑密度分析、耕地确权、土地利用变化监测都很有价值也是遥感图像语义分割与地物面积估算系统里最常被问到的落点。pixel_area 0.25 # 单位平方米根据影像分辨率调整 total_area np.sum(seg_map ! ignore_index) * pixel_area class_names [背景, 建筑, 水体, 耕地, 道路, 植被] for cls_id in range(1, len(class_names)): count np.sum(seg_map cls_id) print(f{class_names[cls_id]}: {count * pixel_area / 10000:.2f} 公顷)单像素面积参数是最容易出错的地方很多人直接拿原始图像的像素数乘比例尺忽略了切块和缩放带来的影响。我现在的习惯是拿到影像先看元数据里有没有地理分辨率信息有就直接用没有的话找图里一个已知尺寸的地物比如标准篮球场 28×15 米做像素标定反推单像素面积。最后输出用公顷或平方千米比纯平方米更符合航拍应用的阅读习惯在报告里也更好解释。从模型选型到数据切块再到训练推理和面积清单这条链路里每一步都有讨巧的空间也有必须死磕的细节。DeepLabv3 不是最潮的模型但它是这个场景下最不容易让你在毕设答辩或项目验收时翻车的选择——前提是你肯花一个晚上把切块脚本和 ignore_index 调对。训练时多看一眼每一类的 IoU别被整体 mIoU 骗了这是我做遥感分割这几年最值钱的一条经验。希望帮到你。本文还有配套的精品资源点击获取