从零开始的图像语义分割:FCN快速复现教程(Pytorch+CityScapes数据集)


前言

摆了两周,突然觉得不能一直再颓废下去了,应该利用好时间,并且上个月就读了一些经典的图像分割论文比如FCN、UNet和Mask R-CNN,但仅仅只是读了论文并且大概了解了图像分割是在做什么任务的,于是今天就拉动手复现一下,因为只有代码运行起来了,才能进行接下来的代码阅读以及其他改进迁移等后续工作。
本文着重在于代码的复现,其他相关知识会涉及得较少,需要读者自行了解。
看完这篇文章,您将收获一个完整的图像分割项目(一个通用的图像分割数据集及一份可正常执行的代码)。

一、图像分割开山之作FCN


图来自FCN,Jonathan Long,Evan Shelhamer,Trevor Darrell CVPR2015

图像分割可以大致为实例分割、语义分割,其中语义分割(Semantic Segmentation)是对图像中每一个像素点进行分类,确定每个点的类别(如属于背景、人或车等),从而进行区域划分。目前,语义分割已经被广泛应用于自动驾驶、无人机落点判定等场景中。
FCN全程Fully Convolutional Networks,最早发表于CVPR2015,原论文链接如下:
FCN论文链接:https://arxiv.org/abs/1411.4038
正如其名称全卷积网络,实则是将早年的网络比如VGG的全连接层代替为卷积层,这样做的目的是让模型可以输入不同尺寸的图像,因为全连接层一旦被创建输入输出维度都是固定的,追根溯源就是输入图片的尺寸固定,并且语义分割是像素级别操作,替换为卷积层也更加合理(卷积操作就是像素级别,这些都是后话了)。
更具体的学习视频可以跳转到b站FCN网络结构详解(语义分割)


二、代码及数据集获取

1.源项目代码

在这里插入图片描述
进入FCN论文链接,点击Code&Data再进入Community Code跳转到paperwithcode网站。
在这里插入图片描述
很神奇地是会发现有两个FCN的检索链接,本文所需要的pytorch项目代码在红框这个链接中
在这里插入图片描述
Star最高的就是本文所需项目,这个大佬还有自己的个人网页,而且号称是FCN最简单的实现,我可以作证此言不虚,的确是众多代码中最简洁明朗的。

2.CityScapes数据集

CityScapes数据集官方下载链接:CityScapes Download
然而下载这个数据集需要注册账号,而且需要的是教育邮箱,可能是按照是否带edu.cn域名判断的吧,本人使用学校邮箱成功注册下载了数据集。读者若有不便可以上网其他途径获取或淘宝买个账号。

在这里插入图片描述
只需下载前3个数据集即可,gtFine_trainvaltest是精确标注(最主要最关键部分),gtCoarse是粗略标注,leftimg8bit_trainvaltest是原图。虽然模型训练的时候只需要用到gtFine但是因为接下来还需要预处理数据集,因此要将三个数据集下载好,才能执行官方给的预处理代码。
重构数据集
在这里插入图片描述
将三个zip解压然后新建一个文件夹命名为CityScapes,然后将三个解压文件里的内容按上图目录放置好,为数据集预处理做准备。


三、代码复现

1.数据预处理

这里需要先下载官方的脚本:cityscapesScripts
接下来对其中的一些地方进行修改,最重要的两个文件为项目下cityscapesscripts\helpers\labels.py和cityscapesscripts\preparation\createTrainIdLabelImgs.py。

在这里插入图片描述
蓝色框为原本的代码,直接注释掉添加红框处代码,即指定自己本地的数据集目录,比如我就将CityScapes放到了E盘的dataset目录下。
在这里插入图片描述
然后是在label.py文件里按照训练的需要更改trainid,255为不被模型所需要的id,因为FCN中为19类+背景板,所以为20类,刚好符合所以不需要更改label文件中任何内容。
在这里插入图片描述
最后运行createTrainIdLabelImgs.py,如果报错的话大概率是因为缺少上图蓝框所示的库,将其直接注释掉就可以了。

2.代码修改

之所以需要修改是因为原本的代码里面数据预处理那块太慢了,Cityscapes_utils.py要将trainId写入npy文件,运行速度极慢,这也是先前用官方预处理脚本cityscapesScripts来预处理的原因,预处理的目的其实也只是生成TrainIds的mask图片,和labelIds的png图片是同理的,只是每个像素所对应类别按照label.py里面的label表进行改变。
其实pytorch官方有给出加载CityScapes的数据集代码,但其直接拿来用并不能满足我们要求,所以需要修改一下,就原项目代码的Cityscapes_loader.py和torchvision.datasets.Cityscapes的代码结合,得到如下可执行代码。读者只需用其替换train.py文件即可。

# -*- coding: utf-8 -*-
# Author: Reganzhx

from __future__ import print_function

import random
from tqdm import tqdm # 由于训练缓慢,添加进度条方便观察
import imageio
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
from torch.autograd import Variable
from torch.utils.data import DataLoader

from fcn import VGGNet, FCN32s, FCN16s, FCN8s, FCNs
# from Cityscapes_loader import CityScapesDataset
from CamVid_loader import CamVidDataset
from torchvision.datasets import Cityscapes
from matplotlib import pyplot as plt
import numpy as np
import time
import sys
import os
from PIL import Image


class CityScapesDataset(Cityscapes):
    def __init__(self, root: str,
                 split: str = "train",
                 mode: str = "fine",
                 target_type="semantic",
                 transform=None,
                 target_transform=None,
                 transforms=None):
        super(CityScapesDataset, self).__init__(root,
                                                split,
                                                mode,
                                                target_type,
                                                transform,
                                                target_transform,
                                                transforms)
        self.means = np.array([103.939, 116.779, 123.68]) / 255.
        self.n_class = 20
        self.new_h = 512 # 数据集图片过大,需要剪裁
        self.new_w = 1024

    def __getitem__(self, index):
        img = imageio.imread(self.images[index], pilmode='RGB')
        targets = []
        for i, t in enumerate(self.target_type):
            if t == "polygon":
                target = self._load_json(self.targets[index][i])
            else:
                target = imageio.imread(self.targets[index][i])
            targets.append(target)

        target = tuple(targets) if len(targets) > 1 else targets[0] # 针对多目标 可不关注
        h, w, _ = img.shape
        top = random.randint(0, h - self.new_h)
        left = random.randint(0, w - self.new_w)
        img = img[top:top + self.new_h, left:left + self.new_w]
        label = target[top:top + self.new_h, left:left + self.new_w]

        # reduce mean
        img = img[:, :, ::-1]  # switch to BGR
        img = np.transpose(img, (2, 0, 1)) / 255.
        img[0] -= self.means[0]
        img[1] -= self.means[1]
        img[2] -= self.means[2]

        # convert to tensor
        img = torch.from_numpy(img.copy()).float()
        label = torch.from_numpy(label.copy()).long()

        # create one-hot encoding
        h, w = label.size()
        target = torch.zeros(self.n_class, h, w)
        for c in range(self.n_class):
            target[c][label == c] = 1

        sample = {'X': img, 'Y': target, 'l': label}

        return sample

    def __len__(self) -> int:
        return len(self.images)

    def _get_target_suffix(self, mode: str, target_type: str) -> str:
        if target_type == "instance":
            return f"{mode}_instanceIds.png"
        elif target_type == "semantic": # 让其指向预处理好的target图片
            return f"{mode}_labelTrainIds.png"
        elif target_type == "color":
            return f"{mode}_color.png"
        else:
            return f"{mode}_polygons.json"


n_class = 20
batch_size = 2 # 根据测试,1batch需要2G显存,请按实际设置
epochs = 500
lr = 1e-4
momentum = 0
w_decay = 1e-5
step_size = 50
gamma = 0.5
configs = "FCNs-BCEWithLogits_batch{}_epoch{}_RMSprop_scheduler-step{}-gamma{}_lr{}_momentum{}_w_decay{}".format(
    batch_size, epochs, step_size, gamma, lr, momentum, w_decay)
print("Configs:", configs)

# create dir for model
model_dir = "models"
if not os.path.exists(model_dir):
    os.makedirs(model_dir)
model_path = os.path.join(model_dir, configs)

use_gpu = torch.cuda.is_available()
num_gpu = list(range(torch.cuda.device_count()))

# 自行更改root
train_data = CityScapesDataset(root='E:/datasets/CityScapes', split='train', mode='fine',
                               target_type='semantic')

train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True)

val_data = CityScapesDataset(root='E:/datasets/CityScapes', split='val', mode='fine',
                             target_type='semantic')

val_loader = DataLoader(val_data, batch_size=1)

vgg_model = VGGNet(requires_grad=True, remove_fc=True)
fcn_model = FCNs(pretrained_net=vgg_model, n_class=n_class)

if use_gpu:
    ts = time.time()
    vgg_model = vgg_model.cuda()
    fcn_model = fcn_model.cuda()
    fcn_model = nn.DataParallel(fcn_model, device_ids=num_gpu)
    print("Finish cuda loading, time elapsed {}".format(time.time() - ts))

criterion = nn.BCEWithLogitsLoss()
optimizer = optim.RMSprop(fcn_model.parameters(), lr=lr, momentum=momentum, weight_decay=w_decay)
scheduler = lr_scheduler.StepLR(optimizer, step_size=step_size,
                                gamma=gamma)  # decay LR by a factor of 0.5 every 30 epochs

# create dir for score
score_dir = os.path.join("scores", configs)
if not os.path.exists(score_dir):
    os.makedirs(score_dir)
IU_scores = np.zeros((epochs, n_class))
pixel_scores = np.zeros(epochs)


def train():
    for epoch in range(epochs):
        scheduler.step()

        ts = time.time()
        for iter, batch in enumerate(tqdm(train_loader)):
            optimizer.zero_grad()

            if use_gpu:
                inputs = Variable(batch['X'].cuda())
                labels = Variable(batch['Y'].cuda())
            else:
                inputs, labels = Variable(batch['X']), Variable(batch['Y'])

            outputs = fcn_model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            if iter % 10 == 0:
                print("epoch{}, iter{}, loss: {}".format(epoch, iter, loss.item()))

        print("Finish epoch {}, time elapsed {}".format(epoch, time.time() - ts))
        torch.save(fcn_model, model_path)
        val(epoch)


def val(epoch):
    fcn_model.eval()
    total_ious = []
    pixel_accs = []
    for iter, batch in enumerate(val_loader):
        if use_gpu:
            inputs = Variable(batch['X'].cuda())
        else:
            inputs = Variable(batch['X'])

        output = fcn_model(inputs)
        output = output.data.cpu().numpy()

        N, _, h, w = output.shape
        pred = output.transpose(0, 2, 3, 1).reshape(-1, n_class).argmax(axis=1).reshape(N, h, w)

        target = batch['l'].cpu().numpy().reshape(N, h, w)
        for p, t in zip(pred, target):
            total_ious.append(iou(p, t))
            pixel_accs.append(pixel_acc(p, t))

    # Calculate average IoU
    total_ious = np.array(total_ious).T  # n_class * val_len
    ious = np.nanmean(total_ious, axis=1)
    pixel_accs = np.array(pixel_accs).mean()
    print("epoch{}, pix_acc: {}, meanIoU: {}, IoUs: {}".format(epoch, pixel_accs, np.nanmean(ious), ious))
    IU_scores[epoch] = ious
    np.save(os.path.join(score_dir, "meanIU"), IU_scores)
    pixel_scores[epoch] = pixel_accs
    np.save(os.path.join(score_dir, "meanPixel"), pixel_scores)


# borrow functions and modify it from https://github.com/Kaixhin/FCN-semantic-segmentation/blob/master/main.py
# Calculates class intersections over unions
def iou(pred, target):
    ious = []
    for cls in range(n_class):
        pred_inds = pred == cls
        target_inds = target == cls
        intersection = pred_inds[target_inds].sum()
        union = pred_inds.sum() + target_inds.sum() - intersection
        if union == 0:
            ious.append(float('nan'))  # if there is no ground truth, do not include in evaluation
        else:
            ious.append(float(intersection) / max(union, 1))
        # print("cls", cls, pred_inds.sum(), target_inds.sum(), intersection, float(intersection) / max(union, 1))
    return ious


def pixel_acc(pred, target):
    correct = (pred == target).sum()
    total = (target == target).sum()
    return correct / total


if __name__ == "__main__":
    val(0)  # show the accuracy before training
    train()

3.运行结果

分别在自己办公电脑1030显卡(显存4G)和3060显卡(显存12G)上测试,根据两台电脑运行上看每增加1batch就需要消耗2G显存,因为3060上最大只能将batch size设置为6。3060显卡上1个epoch需要8min,也就是说训练完500epoch需要三天时间,可见图像分割真的是极其消耗资源。而1030上1代竟然耗时2h20min,所以按照时间来看首选设备是3090,这样才可能在一天之内进行完一次完整500epoch训练。
在这里插入图片描述
第1轮迭代后pixel accuracy就有75%,目前到第25轮pixel accuracy达到85%,随着epoch数增加,pixel acc也越来越高,希望其最终能突破90%,原论文中可是达到96%pixel准确率。
在这里插入图片描述
下图为3060上训练150epoch的结果,每5epoch进行一次val评估。最后使用matplotlib绘制如下曲线,pixel_acc和meanIoU的获取请读者自行额外编写代码获得,此处仅提供绘图代码。
第135epoch取得最高pixel accuracy=0.8766716842651368,meanIoU=0.3268041800950261

在这里插入图片描述

from matplotlib import pyplot as plt

x=[i for i in range(0,151,5)] #横坐标
# 此处给出我的数据,浮点数都用round函数取到小数点后7位
pix_acc_list=[0.7520696,0.7918097,0.6557526,0.8310604,0.8453417,0.8509236,0.8534471,0.8378322,0.8489639,0.8563263,0.8538324,0.8572157,0.860767,0.8660216,0.8631711,0.8631837,0.8670352,0.8597714,0.8689239,0.8647407,0.8698506,0.8712046,0.8719427,0.8722804,0.8732114,0.871852,0.8714358,0.8766717,0.86854,0.8661136,0.8761132]
meanIoU_list=[0.1333057,0.185366,0.1383637,0.2432535,0.2634509,0.2799635,0.2831553,0.2642947,0.2924905,0.3027259,0.3123738,0.2976701,0.3113799,0.3239229,0.3163488,0.3170467,0.3246953,0.3236825,0.3242375,0.3262411,0.3355112,0.3285704,0.3388148,0.328427,0.3378653,0.3385619,0.3358321,0.3268042,0.3297385,0.3347885,0.3379351]
plt.figure()
plt.plot(x,pix_acc_list,color='blue',label='pixel acc')
plt.plot(x,meanIoU_list,color='red',label='meanIoU')

plt.xticks(fontsize=16)
plt.yticks(fontsize=16)

plt.xlabel('Epoch',fontsize=20)
plt.ylabel('Score',fontsize=20)
plt.legend(fontsize=16)
plt.show()

总结

希望您读到这里能有所收获,本文所参考资料也在文末给出,大家可以查阅获取更多知识细节,后续还将不断完善本文内容,敬请期待……


参考网站

https://bbs.huaweicloud.com/blogs/306716
https://developer.aliyun.com/article/797607
https://www.cnblogs.com/dotman/p/cityscapes_dataset_tips.html
https://zhuanlan.zhihu.com/p/147195575
https://codeantenna.com/a/uD5sJceaS1
https://blog.csdn.net/zz2230633069/article/details/84591532
https://www.zhihu.com/question/276325769/answer/2418207657
https://blog.csdn.net/zz2230633069/article/details/84668984
https://blog.csdn.net/yumaomi/article/details/124847721

  • 20
    点赞
  • 103
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
### 回答1: 遥感图像语义分割是指将遥感图像中的每个像素点进行分类,确定其对应的地物类别,如建筑、道路、植被等。PyTorch是一种用于构建和训练深度学习模型的开源框架,可以高效地实现遥感图像语义分割。 以下是使用PyTorch实现遥感图像语义分割的简要教程: 1. 数据准备:首先,需要准备用于训练的遥感图像数据集。该数据集应包含遥感图像及对应的标签图像,其中每个像素点都标注了地物类别。可以使用现有的公开数据集,或者通过遥感图像数据集的制作工具对自己的数据进行标注。 2. 数据加载:使用PyTorch中的数据加载器来加载训练数据。可以自定义一个数据加载类,继承PyTorch的Dataset类,实现__getitem__和__len__方法,将遥感图像和对应的标签图像读取并返回。 3. 模型设计:选择适合任务的深度学习模型,如U-Net、DeepLab等。可以使用PyTorch提供的预训练模型作为基础网络,然后根据具体任务进行修改。在模型中添加适当的卷积、池化和上采样层,并加入跳跃连接等技巧以提高模型性能。 4. 损失函数定义:在语义分割中,常使用交叉熵损失函数来度量模型输出与标签之间的差异。可以使用PyTorch提供的交叉熵损失函数或自定义损失函数。 5. 模型训练:使用定义好的数据加载器、模型和损失函数进行训练。通过定义优化器和学习率,使用PyTorch自带的训练函数进行模型的训练。可以设置合适的批量大小、学习率衰减等超参数,根据训练集和验证集的损失和准确率进行调整。 6. 模型评估:训练完成后,使用测试集对模型进行评估,计算准确率、召回率、F1值等指标,评估模型在遥感图像语义分割任务上的性能。 以上是一个简要的遥感图像语义分割PyTorch中的实现教程,希望对你有帮助。当然,实际应用中还可能涉及到更多细节和技巧,需要根据具体情况进行调整和改进。 ### 回答2: 遥感图像语义分割是指使用遥感图像数据进行像素级别的分类和分割,即将图像中的每个像素按照其所属的类别进行标注。PyTorch是一种流行的深度学习框架,可以用于实现遥感图像语义分割。 以下是一个简单的遥感图像语义分割PyTorch实现教程: 1. 数据准备:首先,准备好遥感图像数据集,包括训练集和测试集。每张图像都需要有相应的标注,标注应为像素级别的类别信息。 2. 数据预处理:对于遥感图像数据进行预处理,包括图像增强、尺寸调整和标准化等操作。这可以使用Python的PIL库等工具来实现。 3. 搭建模型:选择适合遥感图像语义分割的模型,比如U-Net、DeepLab等。使用PyTorch搭建网络模型,定义网络结构、损失函数和优化器等。 4. 数据加载和训练:使用PyTorch的数据加载器加载训练数据集,并使用定义的优化器和损失函数进行训练。可以设置适当的批次大小和训练轮数。 5. 模型评估:在训练过程中,可以使用测试集对模型进行评估,计算准确率、召回率、F1分数等指标,以了解模型的性能。 6. 模型优化:根据评估结果,可以尝试调整模型的参数、损失函数或优化器等,以提高模型的准确性和鲁棒性。 7. 模型应用:训练好的模型可以应用于新的遥感图像数据,进行像素级别的语义分割任务。 总结:遥感图像语义分割PyTorch实现可以按照上述步骤进行,其中数据准备、搭建模型、数据加载和训练等是关键步骤。通过不断优化和调整,可以得到高准确性的语义分割模型,从而应用于遥感图像的各种应用场景。 ### 回答3: 遥感图像语义分割是指利用遥感图像对地表进行分类和分割的技术。PyTorch是一个流行的深度学习框架,提供了强大的功能和易于使用的API,因此在遥感图像语义分割任务中也经常被使用。 以下是一个简要的遥感图像语义分割PyTorch实现教程: 1. 数据准备:首先,你需要准备用于训练的遥感图像数据集。这些数据集应包含遥感图像和相应的标签图像,其中标签图像用于指示每个像素的类别。可以使用遥感图像处理软件,如ENVI或GDAL,来预处理和准备数据。 2. 数据加载:使用PyTorch中的数据加载器,如torch.utils.data.DataLoader,加载准备好的数据集。你可以自定义一个子类,继承自torch.utils.data.Dataset,来处理数据加载和转换。 3. 构建模型:在PyTorch中,可以使用torch.nn模块来构建语义分割模型。常用的模型包括U-Net、FCN和DeepLab等。你可以根据任务的具体需求选择适当的模型结构,并根据需要进行修改和调整。 4. 定义损失函数:在语义分割任务中,常用的损失函数是交叉熵损失函数。在PyTorch中,可以使用torch.nn.CrossEntropyLoss来定义损失函数。 5. 训练模型:使用PyTorch的训练循环,将图像输入模型,计算损失函数,更新模型参数,并循环迭代该过程。你需要选择合适的优化器,如SGD或Adam,并选择适当的超参数。 6. 评估和预测:训练完成后,可以使用模型对新的遥感图像进行预测。通过将图像输入模型,可以得到每个像素的类别预测结果。你可以使用各种评估指标,如交并比和准确率,来评估模型的性能。 以上是一个简单的遥感图像语义分割PyTorch实现教程。通过理解和实践这些步骤,你可以开始进行遥感图像语义分割任务,并逐渐提升你的模型和技术水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值