news 2026/10/7 18:35:25

FCN语义分割实战:从全卷积网络到遥感图像切片推理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FCN语义分割实战:从全卷积网络到遥感图像切片推理

简介:面向已掌握 CNN 基础、想尽快上手语义分割的深度学习开发者,这是一套以全卷积网络(FCN)为核心的实战代码包,覆盖像素级分类、反卷积上采样、任意尺寸输入处理等关键环节,可用于 Pascal VOC 等数据集的训练、验证与预测。压缩包大小约 335.39MB,文件总数标注为 2000,文件类型以 XML 标注、PNG/JPG 图像为主,另含少量 Python 脚本与 JSON 类别配置,数据与代码搭配完整。包内训练、评估、预测流程均提供对应脚本,并包含数据集读取、数据增强、分布式训练辅助模块和类别/调色板配置,目录结构清晰,方便直接替换数据后复现模型效果,也便于逐模块阅读源码理解 FCN 实现细节。已有 1533 人学习下载,适合希望从理论走向项目实操、需要一份可运行语义分割基线工程的读者快速上手与二次开发。

1. 先把话说透:FCN是什么,为什么语义分割绕不开它

FCN(Fully Convolutional Network)全卷积网络,是语义分割方向绕不开的起点模型。它把分类网络末尾令人头疼的全连接层整个换成卷积层,于是网络不再输出一个类别分数,而是直接输出一张与原图同尺寸的类别图。你给它一张街景照片,它返回的是一张每个像素都被标了“道路”“汽车”“天空”的标签图。对从遥感影像提取建筑、在医学切片里分离病灶、替自动驾驶标注车道线的工程师来说,这套思想到今天依然是很多语义分割模型的底色,值得花一个周末亲手跑通。这篇笔记适合刚做过图像分类但没接触过逐像素预测的读者,也适合想用 FCNet 快速出第一版分割结果的人。

2. 语义分割为什么绕不开FCN:从全连接到全卷积的三个关键转变

2.1 第一个转变:把全连接层扔掉,让网络输出变成 h×w 的图

分类网络(以 VGG16 为代表)最后三层全连接把特征图拍扁成了 4096 维向量,最后接 Softmax 给出 1000 类概率。这个结构对“整张图属于哪一类”没问题,可语义分割需要的是“每个像素属于哪一类”。你无法为一张 512×1024 的街景图单独跑 1024×512 次分类来给每个像素定类别,计算量是天方夜谭。FCN 的思路是把最后 3 层全连接改造成 1×1 卷积,这样网络不再关心输入尺寸,卷积层滑过整张图,输出变成通道数为类别数、宽高和输入相关的热力图。

实际实现里我一般不会手工改 VGG 的权重文件,而是把结构写清楚:conv5_3 之后的特征图经过两个 1×1 卷积,通道数从 512 先降到 4096 再降到类别数。注意第一层的 stride 不能乱动,保持卷积核滑动窗口走满全图。改完之后,网络参数只是换了排列方式,几乎不增加额外参数量,却能从(batch, 512, h, w)一路映射到(batch, num_classes, h, w),恢复出空间维度。

这个转变里踩过坑的人都知道,最容易翻车的是输入尺寸必须能被 stride 整除。VGG16 总步长是 32,输入尺寸若不是 32 的倍数,最后的特征图宽高会出现小数取整,导致后续上采样和原图的对应关系错位。我一般会用 padding 或 resize 先把输入固定成 32 的倍数。

2.2 第二个转变:上采样三选一,为什么常见落地选双线性

特征图经过上面这些卷积,尺寸缩小到了原图的 1/32,比如 512×512 的输入变成了 16×16。要把这个粗糙的热力图恢复回原尺寸,FCN 作者给出了三种上采样:转置卷积、双线性插值、以及转置卷积前用跳跃连接叠加浅层特征。最原始的 FCN-32s 直接把 16×16 的结果用 32 倍双线性插值拉回 512×512,缺点一目了然:边缘太糊,车和路的分界线像浆糊。

转置卷积是有学习参数的上采样,它能学习到更锐利的边界,代价是会让网络多出大量参数,并且在输入尺寸不是 32 的整数倍时容易产生棋盘格伪影。所以很多实战项目不会一上来就用转置卷积,而是先上双线性到目标分辨率,再接一层普通卷积去修正细节。torchvision 里提供的 fcn_resnet50 也是这种组合:上采样层用的是双线性插值(scale_factor 配合 align_corners=True),再通过一个 conv2d 把类别概率图磨得更干净。

我一般做工程验证时优先选双线性,原因很简单:不需要额外调转置卷积的 kernel_size 和 output_padding,参数少,训练过程中更省心。等模型基线正常了,再决定要不要把上采样换成更贵的算子。

2.3 第三个转变:跳跃连接,语义和边界缺一不可

FCN-32s 恢复出来的结果虽然大方向不错,但边界细节完全不可用。原因在于 1/32 分辨率的热力图只剩高层语义,池化把大量几何信息丢掉了。FCN 论文里有两个改进版本:FCN-16s 和 FCN-8s。它们做的事本质上是把浅层特征通过跳跃连接加到深层上采样结果上,再把融合结果继续上采样。

FCN-8s 之所以成为经典,是因为它叠加了 pool3 和 pool4 两层的特征,既有足够语义又保留了一部分边界响应。你可以在代码里看到这样的 shape 变化:pool3 输出是输入的 1/8,pool4 输出是 1/16,conv7 输出是 1/32。把 conv7 的结果上采样 2 倍和 pool4 相加,得到 1/16,再上采样 2 倍和 pool3 相加,得到 1/8,最后上采样 8 倍回原图。这三个版本里我建议直接做 8s 版本,16s 作为中间调试用。8s 为了对齐尺寸,在 add 前往往会做一个 1×1 卷积把浅层通道数映射到类别数,这一步不能省,否则 add 会因为通道数不一致直接报错。

还有一点不少新手忽略:跳跃连接要把 pool3、pool4 的坐标空间处理好,浅层特征不需要经过额外的对齐层,但深层的上采样尺寸必须和浅层特征完全一样。由于 pool3 在代码里很容易被误改成 ceil_mode=True 导致输出多一个像素,建议每次 add 前都打印一下 tensor.shape 确认。

2.4 感受野与显存的账:为什么FCN只能接受小 batch

FCN 本身不复杂,复杂的是它的显存账单。骨干网络 VGG16 总步长 32,前向传播时特征图即使经过 maxpool 体积缩小,但 conv1 层保持原图分辨率卷积,显存消耗极大。512×512 输入在 VGG16 上跑一轮训练,batch size 开到 8 就已经让一张 12G 的卡吃紧。原版论文训练时用的输入是缩略图加随机裁剪,很多人照搬“必须整图输入”的错觉,结果 OOM 后把 batch 降到 1,又发现 BN 层统计量失效。常见做法是固定分辨率训练:先把训练图等比缩放到短边约 400 像素,再随机裁剪成 384×384 或 400×400 的 patch 喂给网络。这样显存可控,BN 也有统计意义。

它同时解释了另一个现象:FCN 对目标尺寸极度敏感。如果训练集里目标占图比例很大,随机裁剪后小目标很容易被裁出画面外,模型就会偏向大目标。遥感图像语义分割尤其明显,建筑、道路目标在整幅影像里只占几个百分点,不切片训练几乎学不到小目标。我的做法是在裁剪时叠加类别分布约束,让每个 patch 至少包含 k 个目标像素,否则重新裁剪。这个逻辑在第四章会再细讲。

3. 把VOC变成FCN能吃的标签:数据准备与class字典

3.1 全卷积网络进场前:先认清VOC数据里到底有什么

PASCAL VOC 2007/2012 里,语义分割标注文件一般叫 SegmentationClass,里面是彩色的 PNG 图片。别看它是 PNG 格式,它的编码方式和日常照片完全不同:它是调色板 PNG(Indexed PNG),文件里有一张颜色表(PALETTE),每个像素存储的值不是 RGB 三元组,而是颜色表索引。你如果直接用 cv2.imread 去读,OpenCV 会默认把它转成 BGR 彩色图,索引信息完全丢失,标签类别就废了。这就是为什么很多人在数据准备阶段就翻车。

正确读法是用 PIL 的 Image.open,保持 P 模式不要 convert 到 RGB,然后 np.array 读出 shape 为(h, w)的索引数组。这个索引数组里 0 代表背景,1 是飞机,2 是自行车,以此类推,20 类是室内物品;255 代表的是标注者都拿不准的难例,训练时应该被忽略。还有 SegmentationClassAug 这个版本,部分类别被打散重组,只有 21 类以内的小模型能用,别盲目混用。

在动手解析前我们要明确一个目标:把原始彩色标签全部转成两部分——索引标注图(.png)和类别映射字典。索引图用于训练时的 CrossEntropyLoss,字典用于把预测结果还原成彩色可视化图。有的实践里把这个字典结构称为 hdict 语义分割标签,本质就是 {类别名: 索引} 的映射。你可以把它序列化成 json,跑完推理后用同一份字典上色,保证颜色顺序始终一致。先写类名清单再写转换脚本,顺序搞反后面调色会疯。

3.2 解析XML并生成索引标注图:转换脚本与参数说明

下面这个脚本是我常用的 V0C 转换方案,重点不在于逐行实现,而在于理解为什么用 PIL 而不是 OpenCV。

import os import json import numpy as np import xml.etree.ElementTree as ET from PIL import Image # 类别字典:顺序固定,训练、可视化、推理共用同一份 CLASSES = [ "background", "aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor" ] CLASS_TO_ID = {name: idx for idx, name in enumerate(CLASSES)} def parse_voc_xml(xml_path): # 只取 segmentation 任务需要的 object 框和类别名 tree = ET.parse(xml_path) objects = [] for obj in tree.findall("object"): name = obj.find("name").text if name not in CLASS_TO_ID: continue # 跳过未定义类别 objects.append(name) return objects def convert_voc_seg(seg_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for label_path in sorted(os.listdir(seg_dir)): if not label_path.endswith(".png"): continue label_png = os.path.join(seg_dir, label_path) img = Image.open(label_png) # 不要 convert RGB label_idx = np.array(img, dtype=np.uint8) save_path = os.path.join(out_dir, label_path) Image.fromarray(label_idx, mode="P").save(save_path, format="PNG") with open(os.path.join(out_dir, "class_dict.json"), "w") as f: json.dump(CLASS_TO_ID, f, indent=2)

逻辑说明:PIL 打开后始终保持 P 模式,np.array 得到的二维索引数组就是训练标签,这期间不再做任何 RGB 转换,避免调色板被破坏。保存时用 mode="P" 的意思是告诉 PIL 把二维索引原样写回调色板 PNG,而不是当成灰度图去映射到黑白图像。注意 parse_voc_xml 在这个脚本里只用于校验类别名是否在预定义范围内,真正生成 label 时并不需要修改原始 PNG。

参数说明:CLASS_TO_ID 就是前文说的 hdict 语义分割标签,key 是类别名,value 是训练时 loss 的 target 索引。如果你的数据集里出现了 VOC 没有的类别,比如遥感里的植被和水体,请先改 CLASSES,再重算索引,两处必须同步更新。另外,原始 VOC 标签里的 255(难例)在这个脚本中被原样保留,这个 255 在训练时才决定是忽略还是参与计算。

提示:如果训练时出现类别索引越界报错,第一件事回头检查 class_dict.json,看看类别数和 num_classes 是否对得上。索引图、调色板、类别字典三者永远要绑在一起迁移。

3.3 可视化检查:你辛苦做的标签是不是一坨黑

做完转换先别急着训练。打开一张标签,用 Python 快速检查像素分布,确认不是一整块黑。因为你如果忘了把标签里的 255 处理掉,或者用了 cv2 读图导致所有值变成奇怪的 BGR 组合,训练过程里 loss 会一会正常一会 NaN。可视化检查其实很简单:读取索引图,用调色板上色后再保存一份 JPG。

import numpy as np from PIL import Image # 读取刚才生成的索引图 idx = np.array(Image.open("VOCdevkit/VOC2007/SegmentationClass/000001.png"), dtype=np.uint8) # 打印像素分布,检查类别是否正常 unique, counts = np.unique(idx, return_counts=True) print(dict(zip(unique.tolist(), counts.tolist()))) # 按调色板上色,还原成肉眼可读的彩色图 palette_img = Image.open("原始彩色标签路径/000001.png") palette = np.array(palette_img.getpalette(), dtype=np.uint8).reshape(-1, 3) color_img = Image.fromarray(idx, mode="P") color_img.putpalette(palette) color_img.save("check_000001.jpg")

这段脚本里最关键的是 print 像素分布。正常图片应该同时出现 0 和一些 1~20 的类别值;如果出现 255,说明难例像素原样保留,要在训练时对应 ignore_index;如果出现 250 以上非 255 的数值,说明源标签和你的类别定义不匹配,基本是用了 SegmentationClassAug 且没做重映射。可视化上色环节里从原始图片调色板取颜色,是为了确保你看到的效果和 VOC 官方一致,颜色顺序错了模型不背锅,只会让你白白浪费时间 debug 可视化。

3.4 数据增强的边界:插值不允许碰标注图

训练 FCN 基本都会做随机翻转和随机缩放。很多人会用同一个 transform 同时作用于图像和标签,这是个容易被忽略的细节。图像增强的插值方式可以是 bilinear,标签增强却不能用 bilinear,否则 0 和 2 之间会插出像素值 1,类别凭空多出来。你的数据管线里如果出现了原本不存在的类别 id,损失函数会计算出一个虚假的交叉熵,训练还不会报错,验证时预测多了一个类别,你还以为是模型幻觉。

做法是给图像和标签分别走增强管线:图像用 bilinear,标签用 nearest。翻转没有插值问题,可以用同一套随机种子,但要小心水平翻转和垂直翻转的随机状态要一致。有个快速检查:增强后的标签用 np.unique 打印类别集合,跟 CLASSES 集合做一个差集,如果出现新值,说明你的标签增强里混入了插值。这一条在遥感切片里尤其常见,因为遥感图像尺寸大、切片多,增强代码容易图省事直接复用图像管线的插值模式。

4. 用FCN做语义分割网络训练:模型搭建、损失函数与关键参数

4.1 最小可用模型:VGG骨干换成全卷积的写法

不建议自己从零实现 FCN。工程上最省事的方式是直接使用 torchvision 里的现成实现,这并不违背标题里“使用 FCN 实现语义分割”的核心诉求,反而让你把精力花在数据、损失和推理上。我一般直接用 fcn_resnet50,它是官方把 ResNet50 骨干和 FCN 头部缝合好的版本,跳跃连接、双线性上采样都处理好了。

import torch from torchvision.models.segmentation import fcn_resnet50 # pretrained_backbone=True 表示骨干用 ImageNet 预训练权重 model = fcn_resnet50(weights_backbone="IMAGENET1K_V2", num_classes=21) model.train() # 输入是 (B, C, H, W),H/W 不必是32倍数,但建议固定到32的倍数 dummy = torch.randn(2, 3, 384, 384) out = model(dummy)["out"] print(out.shape) # (2, 21, 384, 384)

逻辑说明:fcn_resnet50 把输出封装成字典,["out"] 是(B, num_classes, H, W)的 logits,跟原版 FCN 思想一致:resnet 骨干做下采样,头部双线性上采样恢复分辨率。我用它做主模型主要是省去手动搭跳跃连接的步骤,代码 review 时也更简单。如果确实想复现论文里的 FCN-8s 三联跳连,也可以加载 VGG16 的 features,再写一个上采样 head,但那个头需要自己保证四个 tensor 的 shape 一致,新手调起来容易半夜挠头。

参数说明:weights_backbone="IMAGENET1K_V2" 不是说模型已经完成了语义分割训练,它只代表骨干部分用了分类预训练,分割头仍然是随机初始化,要用分割数据继续训练。如果你的数据集类别只有 5 类(比如最常见的遥感四类加背景),把 num_classes 改成 5 即可,不要继续用 21。忘记改 num_classes 会让最后一层超出训练标签的最大索引,训练中大概率报错或预测出无意义类别。

关于骨干选择,ResNet50 比原版 VGG16 快、省显存,边界表现差不了太多。你要是显存只有 4G,把 resnet50 换 resnet18 或 resnet34,效果降一点但能跑。

4.2 损失函数与学习率:CrossEntropy、ignore_index与poly衰减

训练语义分割,损失函数几乎默认是逐像素交叉熵,但有两个参数值得单独说:ignore_index 和类别不平衡。给出一个最小训练循环。

import torch import torch.nn.functional as F def train_one_step(model, images, labels, optimizer, device): images = images.to(device) labels = labels.to(device).long() # 索引图必须是 long,不能是 float optimizer.zero_grad() logits = model(images)["out"] loss = F.cross_entropy(logits, labels, ignore_index=255) loss.backward() optimizer.step() return loss.item() # poly 学习率衰减:分割任务里比 step 衰减更常用的曲线 def poly_lr(optimizer, base_lr, cur_iter, total_iter, power=0.9): lr = base_lr * (1 - cur_iter / total_iter) ** power for param_group in optimizer.param_groups: param_group["lr"] = lr

逻辑说明:labels 必须转成 long 型,且形状和 logits 的空间尺寸完全一致,CrossEntropy 会自动在通道维做 softmax 并计算每个像素的交叉熵,最终返回的是全图平均损失。ignore_index=255 正是处理 3.1 节那种难例,loss 在 255 处梯度直接置零,这样难例不会教坏模型。如果 labels 不是 long 而是 float,CrossEntropy 会抛错说 expected target type Long;如果分辨率不一致,则会出现 broadcast 失败。

参数说明:poly_lr 是语义分割里比 steplr 更常用的曲线。power 默认取 0.9,意思是学习率随训练步数平滑走低,让后期微调边界。base_lr 我用 0.01 配 SGD + momentum=0.9 + weight_decay=5e-4;换成 Adam 的话 base_lr 降到 1e-4。batch_size 如果小于 4,BN 的统计量会抖,这时要么开 SyncBN,要么把输入分辨率降一点来换取更大 batch。

训练终止用什么做信号?不要只看 loss。我一般每隔几百步存一个 checkpoint,在验证集上算 mIoU 再决定是否继续。只盯着训练 loss 经常会发现它降到 0.1 以下了,边界还是一团糟,因为 FCN 对类别不平衡敏感,loss 低不代表小目标被识别了。

4.3 遥感图像语义分割的训练差异与显存调整

如果你手头不是 VOC 而是遥感影像,训练逻辑要换一下。遥感图像语义分割的目标精细但稀疏,原始影像动辄几千像素宽,直接 resize 到 512 会把建筑和道路磨成线状,模型根本学不到形状。常见做法是大图切片:用滑动窗把原图切成 512×512 或 1024×1024 的切片,切之前先统计标签里各类别像素占比,尽量保证每个切片至少包含一类前景。这个过程用 PIL 或 OpenCV 都能做,切片之间保留 overlap,推理时再用重叠投票消除边界缝隙。

显存方面,遥感切片往往还用多光谱数据,比如 4 波段以上,第一层卷积的 in_channels 就得从 3 改成对应通道数。此时不能直接加载 ImageNet 预训练权重,常见做法是保留 RGB 三个通道的预训练参数,其余通道用随机初始化,或者干脆做通道平均复制。改完注意重新初始化第一层,否则预训练权重 shape 对不上,加载就报错。训练时 BN 统计量需要预热,所以必须先跑几十个 step 让统计量就位再开始评估。

5. 语义分割模型推理翻车现场:5个让输出乱码的常见坑

5.1 预测图和原图颜色对不上,整张图像霓虹灯

现象:训练结束做推理,代码跑通了,出来的预测图颜色和 VOC 标签颜色截然不同:明明天空应该是浅蓝,预测图里天空变成了品红色。

原因:预测输出是索引图,你直接拿它当灰度或者按训练时的 CLASS_TO_ID 重新上色,但是调色板顺序没和 VOC 官方保持一致。VOC 的 PALETTE 不是简单的纯色排列,不同类别穿插得很怪,你从某个开源库抄的调色板只要错一个位置,后面整条街的颜色全错位。

解决:推理时从原始标签 PNG 里读取 palette,预测索引图也用同一份 palette 上色。代码顺序是:先用 np.argmax 得到(h, w)索引图,Image.fromarray 成 P 模式,putpalette 原始 palette,再转 RGB 保存。这样保证预测颜色和标注颜色永远一致,不要试图用“肉眼调色”。

5.2 模型没开eval(),BatchNorm把每张图的输出搞得不一样

现象:同一个 checkpoint,训练时验证 mIoU 还有 58,单独推理时同一张图输出却有大量杂点,且每次跑结果还不一样。

原因:模型还在 model.train() 模式。BN 层在 train 模式下会用当前 batch 的均值和方差做归一化,推理时数据是一张张进来的,batch 统计量反映的只是这一张图的特性;如果模型里有 Dropout,train 模式会随机砍一部分神经元,输出自然飘。

解决:推理前必须 model.eval(),并且包在 torch.no_grad() 里面。eval 模式下 BN 会切换到累计的 running_mean 和 running_var,输出就正常了。这是 FCN 实战里最典型的一类“玄学”问题,很多人看到输出乱码首先怀疑模型,却忘了这行代码。另外注意,如果推理脚本里用了 DataLoader 且 num_workers>0,多进程环境下每个 worker 都要共享同一个 model 实例的 eval 状态,可以在 worker_init_fn 里也设置 model.eval()。

5.3 输入尺寸不是32的倍数,边缘出现锯齿和贴图错位

现象:对任意尺寸的测试图直接推理,预测图边缘出现规律的锯齿,部分区域像是被拉伸过。

原因:ResNet/FCN 骨干的 stride 总和是 32,所以输入尺寸必须能被 32 整除。打印一下 shape 就会发现,输入宽高一旦不是 32 倍数,下采样后特征图被取整,上采样时插值到原图尺寸就和真实位置对不上,边缘尤其明显。

解决:推理前做 padding 到 32 的倍数,推理后再 crop 回原尺寸。常见做法是右侧和下侧做零填充;如果输入图巨大,把它切成 512×512 的 patch 分别推理再拼接,不要直接整图塞进去,否则 GPU 直接 OOM。切图重叠 50 像素以上,拼图时用重叠区域的平均概率代替 argmax,边界会干净很多。

5.4 背景类占95%像素,mIoU虚高而Kappa难看

现象:遥感场景里房子只占图面 3%,训练结束后 mIoU 是 0.62 听起来不错,可 Kappa 系数只有 0.2,业务方看了直摇头。为什么 mIoU 虚高?因为背景类像素占比大,而 mIoU 是所有类别 IoU 算数平均,背景这一类就能把整体拉高。

解决:这时要按类别查看 IoU,逐类别打印,确认小目标类别的 IoU。训练阶段可以通过设置类别权重,让 loss 对前景类更敏感,比如给背景权重设 1、前景权重设 5~10。另一种办法是采样时对前景目标做约束,训练数据里必须有这个 patch,没有的 patch 跳过。验证指标建议两个都算:mIoU 和 Kappa;部署报告里写“整体 mIoU”容易被人一眼看穿,写成逐类 IoU 表格才是硬功夫。

5.5 loss变NaN或前期一直振荡,怎么排查

现象:训练前几百步 loss 从 2.3 降到 1.6,然后某一步直接 NaN;或者 loss 上下跳跃方差极大,batch size 已经往大调了还是不稳。

原因:常见原因有三个。一是标签里出现了超出 num_classes 的像素值,比如把 255 当普通标签,导致 CrossEntropy 索引越界;二是学习率过高,FCN 的分割头是随机初始化的,直接用骨干的 0.01 学习率,头部的梯度会爆炸;三是数据归一化没做,像素值直接 0~255 进网络,激活值一开就很大。

解决:先看一眼数据集像素分布,确认标签只有 0~20 和 255;随后把分割头的 learning_rate 单独设成 0.01,骨干用 0.001 去微调,因为骨干已经收敛;归一化做到 mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]。如果还 NaN,查看最近几轮 loss 增量,把 base_lr 缩小 10 倍重新训练,基本不会再翻车。顺带说一个血泪教训:保存断点时要同时保存 optimizer 状态和 epoch/iter,不然从断点恢复后 poly_lr 会重新按第 0 轮算,学习率突然又猛又陡,训练出问题也难查。

6. 把FCN用到遥感图像语义分割切片推理:多尺度投票与验证

6.1 多尺度推理:让FCN在切片上更可靠

现在模型稳定了,推理时要拿更高精度。单尺度推理用一张原图直接预测,多尺度则把图片缩放到多个尺度分别预测,再把概率图叠到一起取平均,最后 argmax。对遥感建筑这类多尺度目标,0.5x、1.0x、1.5x 三个尺度通常够用。给出一个小函数:

import torch import torch.nn.functional as F def infer_multiscale(model, image, scales=(0.5, 1.0, 1.5)): model.eval() _, H, W = image.shape probs = [] for s in scales: new_h, new_w = int(H * s) // 32 * 32, int(W * s) // 32 * 32 resized = F.interpolate( image.unsqueeze(0), size=(new_h, new_w), mode="bilinear", align_corners=False ) with torch.no_grad(): logits = model(resized)["out"] prob = torch.softmax(logits, dim=1) prob = F.interpolate(prob, size=(H, W), mode="bilinear", align_corners=False) probs.append(prob) avg = torch.stack(probs).mean(dim=0) return avg.argmax(dim=1).squeeze(0).cpu().numpy()

逻辑说明:每个尺度先 resize 到 32 的倍数(乘 scale 后取整数再对齐到 32),预测完把概率图插值回原图尺寸,最后直接对概率做平均。这样比 argmax 平均更稳,还能缓解模糊边缘的单尺度抖动。scales 列表不要贪多,实测 0.5/1.0/1.5 和 0.6/1.0/1.4 差别不大,尺度过多推理时间线性上涨,工业环境会先算好推理预算再做取舍。

6.2 切片拼接的后处理:重叠与投票

遥感图经常是几万乘几万的大幅影像,一次性推理不可能;我会用 1024×1024 窗口、128 像素重叠滑窗推理,然后重叠区域用最大概率投票拼接。做过这个的都知道,普通拼接缝极其明显,重叠区域投票后缝隙几乎肉眼不可见。在拼接阶段需要把每个 patch 的概率图存储下来,而不是只存 argmax,否则投票无从谈起。存储用 np.float16 保存概率图,内存能省一半。

CRF 后处理:如果还有余力,对多尺度概率图做一次 DenseCRF,可以把建筑边缘的毛刺磨掉一点。但 CRF 很慢,1024×1024 的图一张要好几秒,只有离线生产才推荐,在线推理直接放弃。用 opencv-contrib 里带的条件随机场版本能提速,但需要自行调参数。我的经验是:先花时间做多尺度,CRF 放到最后,因为大多数项目多尺度收益已经足够。

6.3 一个实在习惯:先跑通后调优,再算账

我每次接到 FCN 任务,都先用小数据集、小分辨率、少 epoch 先跑通全链路,确认推理输出能与标签对上色,再开始堆数据和调优。这个习惯救过我很多次:有一次明明是训练代码里忘记做归一化,我却在模型结构上折腾了两天;后来回到“先跑通最小 case”的习惯,十分钟就发现了问题。所以如果你正打算用 FCN 做语义分割,建议第一步先把训练脚本缩到单卡小 batch 跑通,第二步去验证一眼可视化结果,第三步才开始调 poly_lr、多尺度、后处理。调参的时候一个变量一个变量改,每次只改一个,不然翻车都不知道哪一步引入的。做 FCN 和排障一样:先把底打对,再谈锦上添花。希望帮到你。

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

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

大模型应用技术:Prompt工程实践复盘,从模糊需求到可控输出

最近一个月我几乎把全部业余时间都投在了“大模型应用技术”这条线路上,从模型选型、接口调用,到本地部署、微调,一路试下来,最后发现一个反直觉的真相:决定一个AI应用效果上限的,往往不是模型本身的聪明程…

作者头像 李华
网站建设 2026/10/7 18:33:53

深度学习语义分割实战:从U-Net到DeepLabv3+毕设全流程指南

简介:面向高校毕设与课程作业场景,这份语义分割项目包整合了深度学习模型实现、Python/C混合编程与系统化工程配置,适合需要完成场景解析任务的学生参考。包内共24个文件,以Python训练/测试脚本为主,辅以XML工程配置、…

作者头像 李华
网站建设 2026/10/7 18:33:45

C++ 面试必问STL:map 和 unordered_map 有什么区别?

map 用有序树组织元素,unordered_map 用哈希表组织元素。二者都能按照键查找值,但复杂度保证、遍历顺序、内存开销和失效规则并不相同。 一、先看红黑树与哈希表 对比项mapunordered_map常见底层结构平衡搜索树,通常是红黑树哈希表&#xff…

作者头像 李华
网站建设 2026/10/7 18:32:31

Java调用海康威视SDK实战:PS流拉取、RTMP推流与录像下载

简介:本资源是一套基于Java语言的海康威视设备SDK二次开发实战项目,面向安防监控领域Java开发者及音视频集成工程师,解决网络摄像机与NVR设备在Java生态中难以高效接入、流媒体推拉、图像抓取与录像下载等核心问题。项目完整封装了实时视频流…

作者头像 李华
网站建设 2026/10/7 18:30:55

74LS138驱动共阴数码管的硬件时序与驱动设计

1. 项目概述:为什么用74LS138驱动数码管显示学号,而不是直接接单片机? 在数字电路教学和嵌入式入门实践中,“用数码管显示学号”几乎是每个电子类专业学生绕不开的第一个综合性实操任务。但很多人一上来就想着用51单片机IO口直驱—…

作者头像 李华
网站建设 2026/10/7 18:30:55

PyTorch花卉图像识别实战:轻量CNN从数据到部署全流程

简介:本资源是一份面向计算机相关专业学生的高分课程实践项目,聚焦花卉图像识别这一经典计算机视觉任务,基于Python与TensorFlow框架构建CNN模型,适用于期末大作业、课程设计或毕业设计参考。资源包共13个文件,包含6个…

作者头像 李华