憨批的语义分割重制版11——Keras 搭建自己的HRNetV2语义分割平台

Posted Bubbliiiing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了憨批的语义分割重制版11——Keras 搭建自己的HRNetV2语义分割平台相关的知识,希望对你有一定的参考价值。

憨批的语义分割重制版11——Keras 搭建自己的HRNetV2语义分割平台

学习前言

最近学了一下HRnet,代码真的好难看懂,还只有Pytorch版本的,Keras复现很有难度,作者写的逻辑很厉害,只能慢慢攻破啦!

什么是HRNetV2模型

传统的卷积神经网络模型是自上而下不断进行特征提取的,如VGG、Mobilenet、Resnet系列等,VGG网络中存在5个步长为(2, 2)最大池化,Mobilenet网络中存在5个步长为(2, 2)的逐层卷积,Resnet网络中存在5个步长为(2, 2)的普通卷积。

这些网络常常存在多个下采样的过程,输入进来的图片利用卷积或者最大池化进行高和宽的压缩。在特征提取的过程中,我们会获得不同形状的特征层。传统的卷积神经网络模型仅仅会有自上而下的特征提取,而HRNet则改变了传统卷积神经网络模型的工作模式,使得网络在工作时刻保持高分辨率特征图。通过下采样以及上采样,在网络进行特征提取时融合不同形状的特征。

最开始的HRNet被用于人体的姿态检测,该论文发表于2019年的CVPR上。在原来HRNet的基础上,官方修改出HRNetV2并且让他可以适应不同任务的需要,语义分割就是其中之一。HRNet提取出来的特征极其丰富,包含各种的分辨率,理论上可以适应不同的CV需求(目标检测、语义分割、实例分割等),本文以HRNetv2做分割为例,进行介绍。

整个HRNetV2由三部分组成,分别是主干部分特征整合部分预测头部分。如上图所示,灰色方框的左半部分就是主干部分,灰色部分与右半部分就是分割头部分。

在主干部分,HRNetV2会在特征提取的过程中同时进行下采样与上采样,在这个过程里会获得不同形状特征图并进行特征融合

在特征整合部分,HRNetV2会将获取到的所有特征图进行特征融合,将宽高较小的特征图网上进行上采样,然后与最大的特征图进行特征融合,然后再利用1个卷积+标准化+激活函数进行特征整合。

在预测头部分,HRNetV2会利用一个卷积核为(1, 1)的卷积调整通道数为num_classes,利用resize进行上采样使得最终输出层,宽高和输入图片一样。。

代码下载

Github源码下载地址为:
https://github.com/bubbliiiing/hrnet-keras

复制该路径到地址栏跳转。

HRNetV2实现思路

一、预测部分

1、主干网络介绍


上图是官方给出的HRNetV2用于分割的示意图,但其实这个图里面遗漏了一些细节。遗漏的细节容易引起误会。

上图将整个HRNetV2的主干分为四个部分,为了方便理解,我们将其命名为Section-1,Section-2,Section-3,Section-4.

a、Section-1


Section-1是第一部分,用于进行初步的特征提取,在图中对应了最左边的四个方块。在实际代码中,图片处理的过程不仅仅包含这四个方块。

如果将该部分进行详细绘制,其本质上应该是这样的。

对于输入进来的图片,HRNetV2还会使用两次步长为(2, 2)、卷积核大小为(3, 3)、通道为64的卷积对输入图片进行高和宽的压缩与特征提取。

假设输入进来的图片是[480, 480, 3]的,在经过两次步长为(2, 2)、卷积核大小为(3, 3)、通道为64的卷积后,获得的特征层为[120, 120, 64]。

此时再进行四次bottleneck_Block。这里用到的bottleneck_Block,其实就是Resnet系列里面用到的残差卷积,主要用在Resnet50、Resnet101里面。

bottleneck_Block的结构如下图所示,bottleneck_Block可以分为两个部分,左边部分为主干部分,存在两次卷积、标准化、激活函数和一次卷积、标准化;右边部分为残差边部分,不经过处理或者经过少量处理直接与输出相接

由图可知,结构分为两个情况:
如果不发生高宽以及通道的变化,则bottleneck_Block如左图所示,此时残差边不经过任何处理便直接与主干部分相接,输入特征层和输出特征层的shape是相同的。

如果发生高宽以及通道的变化,则bottleneck_Block如右图所示,此时残差边经过少量处理便直接和主干相接,可以改变输出特征层的宽高和通道数。

在经过四次bottleneck_Block后,我们获得的特征层的shape为[120, 120, 256]。

总体上,Section-1的构建代码如下:

def bottleneck_Block(input, out_filters, strides=(1, 1), with_conv_shortcut=False, name=""):
    expansion = 4
    de_filters = int(out_filters / expansion)

    x = Conv2D(de_filters, 1, use_bias=False, kernel_initializer='he_normal', name=name+'.conv1')(input)
    x = BatchNormalization(epsilon=1e-5, name=name+'.bn1')(x)
    x = Activation('relu')(x)

    x = Conv2D(de_filters, 3, strides=strides, padding='same', use_bias=False, kernel_initializer='he_normal', name=name+'.conv2')(x)
    x = BatchNormalization(epsilon=1e-5, name=name+'.bn2')(x)
    x = Activation('relu')(x)

    x = Conv2D(out_filters, 1, use_bias=False, kernel_initializer='he_normal', name=name+'.conv3')(x)
    x = BatchNormalization(epsilon=1e-5, name=name+'.bn3')(x)

    if with_conv_shortcut:
        residual = Conv2D(out_filters, 1, strides=strides, use_bias=False, kernel_initializer='he_normal', name=name+'.downsample.0')(input)
        residual = BatchNormalization(epsilon=1e-5, name=name+'.downsample.1')(residual)
        x = add([x, residual])
    else:
        x = add([x, input])

    x = Activation('relu')(x)
    return x

def stem_net(input):
    x = ZeroPadding2D(((1, 1),(1, 1)))(input)
    x = Conv2D(64, 3, strides=(2, 2), padding='valid', use_bias=False, kernel_initializer='he_normal', name="conv1")(x)
    x = BatchNormalization(epsilon=1e-5, name="bn1")(x)
    x = Activation('relu')(x)

    x = ZeroPadding2D(((1, 1),(1, 1)))(x)
    x = Conv2D(64, 3, strides=(2, 2), padding='valid', use_bias=False, kernel_initializer='he_normal', name="conv2")(x)
    x = BatchNormalization(epsilon=1e-5, name="bn2")(x)
    x = Activation('relu')(x)

    x = bottleneck_Block(x, 256, with_conv_shortcut=True, name="layer1.0")
    x = bottleneck_Block(x, 256, with_conv_shortcut=False, name="layer1.1")
    x = bottleneck_Block(x, 256, with_conv_shortcut=False, name="layer1.2")
    x = bottleneck_Block(x, 256, with_conv_shortcut=False, name="layer1.3")
    return x

b、Section-2


Section-2是第二部分,用于进行进一步的特征提取,对应图中中间靠左的12个方块。如果将该部分进行详细绘制,其本质上应该是这样的。

对于输入进来的特征层,HRNetV2分别利用两个卷积进行处理,一个卷积的步长为(1, 1)、通道为32,一个卷积的步长为(2, 2)、通道为64。此时,我们获得一个[120,120, 32]的特征层和一个[60, 60, 64]的特征层。

之后对这两个特征层,分别进行四次basic_Block的处理。这里用到的basic_Block,也是Resnet系列里面用到的残差卷积,主要用在Resnet18、Resnet32里面。

basic_Block的结构如下图所示,basic_Block可以分为两个部分,左边部分为主干部分,存在一次卷积、标准化、激活函数和一次卷积、标准化;右边部分为残差边部分,不经过处理或者经过少量处理直接与输出相接

由图可知,结构分为两个情况:
如果不发生高宽以及通道的变化,则basic_Block如左图所示,此时残差边不经过任何处理便直接与主干部分相接,输入特征层和输出特征层的shape是相同的。

如果发生高宽以及通道的变化,则basic_Block如右图所示,此时残差边经过少量处理便直接和主干相接,可以改变输出特征层的宽高和通道数。

在分别经过进行四次basic_Block的处理后,我们依然获得一个[120,120, 32]的特征层和一个[60, 60, 64]的特征层。

之后对[120,120, 32]的特征层进行下采样,与[60, 60, 64]的特征层相加;对[60, 60, 64]的特征层进行上采样,与[120,120, 32]相加。

总体上,Section-2的构建代码如下:

def transition_layer1(x, out_filters_list=[32, 64]):
    x0 = Conv2D(out_filters_list[0], 3, padding='same', use_bias=False, kernel_initializer='he_normal', name="transition1.0.0")(x)
    x0 = BatchNormalization(epsilon=1e-5, name="transition1.0.1")(x0)
    x0 = Activation('relu')(x0)

    x1 = ZeroPadding2D(((1, 1),(1, 1)))(x)
    x1 = Conv2D(out_filters_list[1], 3, strides=(2, 2), padding='valid', use_bias=False, kernel_initializer='he_normal', name = "transition1.1.0.0")(x1)
    x1 = BatchNormalization(epsilon=1e-5, name="transition1.1.0.1")(x1)
    x1 = Activation('relu')(x1)
    return [x0, x1]

def make_stage2(x_list, out_filters_list=[32, 64]):
    x0, x1 = x_list

    x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage2.0.branches.0.0")
    x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage2.0.branches.0.1")
    x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage2.0.branches.0.2")
    x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage2.0.branches.0.3")

    x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage2.0.branches.1.0")
    x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage2.0.branches.1.1")
    x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage2.0.branches.1.2")
    x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage2.0.branches.1.3")

    x0_0 = x0
    x0_1 = Conv2D(out_filters_list[0], 1, use_bias=False, kernel_initializer='he_normal', name="stage2.0.fuse_layers.0.1.0")(x1)
    x0_1 = BatchNormalization(epsilon=1e-5, name="stage2.0.fuse_layers.0.1.1")(x0_1)
    x0_1 = UpsampleLike(name="Upsample1")([x0_1, x0_0])
    x0_out = add([x0_0, x0_1])
    x0_out = Activation('relu')(x0_out)

    x1_0 = ZeroPadding2D(((1, 1),(1, 1)))(x0)
    x1_0 = Conv2D(out_filters_list[1], 3, strides=(2, 2), padding='valid', use_bias=False, kernel_initializer='he_normal', name="stage2.0.fuse_layers.1.0.0.0")(x1_0)
    x1_0 = BatchNormalization(epsilon=1e-5, name="stage2.0.fuse_layers.1.0.0.1")(x1_0)
    x1_1 = x1
    x1_out = add([x1_0, x1_1])
    x1_out = Activation('relu')(x1_out)
    
    return x0_out, x1_out

c、Section-3


Section-3是第三部分,用于进行进一步的特征提取,对应图中中间靠右的18个方块。如果将该部分进行详细绘制,其本质上应该是这样的。

对于输入进来的特征层,HRNetV2利用一个步长为(2, 2)、通道为128的卷积对[60, 60, 64]进行处理。此时,我们获得一个[30, 30, 128]的特征层。加上初始的两个特征层,我们存在三个特征层。

之后对这三个特征层,分别进行四次basic_Block的处理。这里用到的basic_Block,与Stage-2里面一样。然后利用[128, 128, 32]、[60, 60, 64]、[30, 30, 128]三个特征层,利用上采样和下采样建立密集连接,完成高度的特征融合。这个过程需要重复四次。

总体上,Section-3的构建代码如下:

def transition_layer2(x, out_filters_list=[32, 64, 128]):
    x2 = ZeroPadding2D(((1, 1),(1, 1)))(x[1])
    x2 = Conv2D(out_filters_list[2], 3, strides=(2, 2), padding='valid', use_bias=False, kernel_initializer='he_normal', name="transition2.2.0.0")(x2)
    x2 = BatchNormalization(epsilon=1e-5, name="transition2.2.0.1")(x2)
    x2 = Activation('relu')(x2)
    return [x[0], x[1], x2]

def make_stage3(x_list, num_modules, out_filters_list=[32, 64, 128]):
    for i in range(num_modules):
        x0, x1, x2 = x_list
        
        x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.0.0")
        x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.0.1")
        x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.0.2")
        x0 = basic_Block(x0, out_filters_list[0], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.0.3")

        x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.1.0")
        x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.1.1")
        x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.1.2")
        x1 = basic_Block(x1, out_filters_list[1], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.1.3")

        x2 = basic_Block(x2, out_filters_list[2], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.2.0")
        x2 = basic_Block(x2, out_filters_list[2], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.2.1")
        x2 = basic_Block(x2, out_filters_list[2], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.2.2")
        x2 = basic_Block(x2, out_filters_list[2], with_conv_shortcut=False, name="stage3." + str(i) + ".branches.2.3")

        x0_0 = x0
        x0_1 = Conv2D(out_filters_list[0], 1, use_bias=False, kernel_initializer='he_normal', name="stage3." + str(i) + ".fuse_layers.0.1.0")(x1)
        x0_1 = BatchNormalization(epsilon=1e-5, name="stage3." + str(i) + ".fuse_layers.0.1.1")(x0_1)
        x0_1 = UpsampleLike(name="Upsample." + str(i) + ".2")([x0_1, x0_0])
        x0_2 = Conv2D(out_filters_list[0], 1, use_bias=False, kernel_initializer='he_normal', name="stage3." + str(i) + ".fuse_layers.0.2.0")(x2)
        x0_2 = BatchNormalization(epsilon=1e-5, name="stage3." + str(i) + ".fuse_layers.0.2.1")(x0_2)
        x0_2 = UpsampleLike(name="Upsample." + str(i) + ".3")([x0_2, x0_0])
        x0_out = add([x0_0, x0_1, x0_2])
        x0_out = Activation('relu')(x0_out)

        
        x1_0 = ZeroPadding2D(((1, 1),(1, 1)))(x0
        
                


前言

最近由于在寻找方向上迷失自我,准备了解更多的计算机视觉任务重的模型。看到语义分割任务重Unet一个有意思的模型,我准备来复现一下它。

一、什么是语义分割

语义分割任务,如下图所示:

简而言之,语义分割任务就是将图片中的不同类别,用不同的颜色标记出来,每一个类别使用一种颜色。常用于医学图像,卫星图像任务。
那如何做到将像素点上色呢?
其实语义分割的输出和图像分类网络类似,图像分类类别数是一个一维的one hot 矩阵。例如:三分类的[0,1,0]。
语义分割任务最后的输出特征图 是一个三维结构,大小与原图类似,通道数就是类别数。 如下图(图片来源于知乎)所示:

其中通道数是类别数,每个通道所标记的像素点,是该类别在图像中的位置,最后通过argmax 取每个通道有用像素 合成一张图像,用不同颜色表示其类别位置。 语义分割任务其实也是分类任务中的一种,他不过是对每一个像素点进行细分,找到每一个像素点所述的类别。 这就是语义分割任务啦~
下面我们来复现 unet 模型

二、Unet

1.基本原理

什么是Unet,它的网络结构如下图所示:

整个网络是一个“U” 的形状,Unet 网络可以分成两部分,上图红色方框中是特征提取部分,和其他卷积神经网络一样,都是通过堆叠卷积提取图像特征,通过池化来压缩特征图。蓝色方框中为图像还原部分(这样称它可能不太专业,大家理解就好),通过上采样和卷积来来将压缩的图像进行还原。特征提取部分可以使用优秀的网络,例如:Resnet50,VGG等。

注意:由于 Resnet50和VGG 网络太大。本文将使用Mobilenet 作为主干特征提取网络。为了方便理解Unet,本文将使用自己搭建的一个mini_unet 去帮祝大家理解。为了方便计算,复现过程会把压缩后的特征图上采样和输入的特征图一样大小。

代码github地址: 一直上不去
先上传到码云: https://gitee.com/Boss-Jian/unet

2.mini_unet

mini_unet 是搭建来帮助大家理解语义分割的网络流程,并不能作为一个优秀的模型完成语义分割任务,来看一下代码的实现:


from keras.layers import Input,Conv2D,Dropout,MaxPooling2D,Concatenate,UpSampling2D
from numpy import pad
from keras.models import Model
def unet_mini(n_classes=21,input_shape=(224,224,3)):

    img_input = Input(shape=input_shape)

   
    #------------------------------------------------------
    # #encoder 部分
    #224,224,3 - > 112,112,32
    conv1 = Conv2D(32,(3,3),activation='relu',padding='same')(img_input)
    conv1 = Dropout(0.2)(conv1)
    conv1 = Conv2D(32,(3,3),activation='relu',padding='same')(conv1)
    pool1 = MaxPooling2D((2,2),strides=2)(conv1)


    #112,112,32 -> 56,56,64
    conv2 = Conv2D(64,(3,3),activation='relu',padding='same')(pool1)
    conv2 = Dropout(0.2)(conv2)
    conv2 = Conv2D(64,(3,3),activation='relu',padding='same')(conv2)
    pool2 = MaxPooling2D((2,2),strides=2)(conv2)


    #56,56,64 -> 56,56,128
    conv3 = Conv2D(128,(3,3),activation='relu',padding='same')(pool2)
    conv3 = Dropout(0.2)(conv3)
    conv3 = Conv2D(128,(3,3),activation='relu',padding='same')(conv3)

    #-------------------------------------------------
    # decoder 部分
    #56,56,128 -> 112,112,64 
    up1 = UpSampling2D(2)(conv3)
    #112,112,64 -> 112,112,64+128
    up1 = Concatenate(axis=-1)([up1,conv2])
    #  #112,112,192 -> 112,112,64
    conv4  = Conv2D(64,(3,3),activation='relu',padding='same')(up1)
    conv4  = Dropout(0.2)(conv4)
    conv4  = Conv2D(64,(3,3),activation='relu',padding='same')(conv4)

    #112,112,64 - >224,224,64
    up2 = UpSampling2D(2)(conv4)
    #224,224,64 -> 224,224,64+32
    up2 = Concatenate(axis=-1)([up2,conv1])
    # 224,224,96 -> 224,224,32
    conv5 =  Conv2D(32,(3,3),activation='relu',padding='same')(up2)
    conv5  = Dropout(0.2)(conv5)
    conv5  = Conv2D(32,(3,3),activation='relu',padding='same')(conv5)
    
    o = Conv2D(n_classes,1,padding='same')(conv5)

    return Model(img_input,o,name="unet_mini")

if __name__=="__main__":
    model = unet_mini()
    model.summary()

mini_unet 通过encoder 部分将 224x224x3的图像 变成 112x112x64 的特征图,再通过 上采样方法将特征图放大到 224x224x32。最后通过卷积:

o = Conv2D(n_classes,1,padding='same')(conv5)

将特征图的通道数调节成和类别数一样。

3. Mobilenet_unet

Mobilenet_unet 是使用Mobinet 作为主干特征提取网络,并且加载预训练权重来提升特征提取的能力。decoder 的还原部分和上面一致,下面是Mobilenet_unet 的网络结构:

from keras.models import *
from keras.layers import *
import keras.backend as K
import keras
from tensorflow.python.keras.backend import shape

IMAGE_ORDERING =  "channels_last"# channel last
def relu6(x):
    return K.relu(x, max_value=6)


def _conv_block(inputs, filters, alpha, kernel=(3, 3), strides=(1, 1)):
   
    channel_axis = 1 if IMAGE_ORDERING == 'channels_first' else -1
    filters = int(filters * alpha)
    x = ZeroPadding2D(padding=(1, 1), name='conv1_pad',
                      data_format=IMAGE_ORDERING)(inputs)
    x = Conv2D(filters, kernel, data_format=IMAGE_ORDERING,
               padding='valid',
               use_bias=False,
               strides=strides,
               name='conv1')(x)
    x = BatchNormalization(axis=channel_axis, name='conv1_bn')(x)
    return Activation(relu6, name='conv1_relu')(x)


def _depthwise_conv_block(inputs, pointwise_conv_filters, alpha,
                          depth_multiplier=1, strides=(1, 1), block_id=1):

    channel_axis = 1 if IMAGE_ORDERING == 'channels_first' else -1
    pointwise_conv_filters = int(pointwise_conv_filters * alpha)

    x = ZeroPadding2D((1, 1), data_format=IMAGE_ORDERING,
                      name='conv_pad_%d' % block_id)(inputs)
    x = DepthwiseConv2D((3, 3), data_format=IMAGE_ORDERING,
                        padding='valid',
                        depth_multiplier=depth_multiplier,
                        strides=strides,
                        use_bias=False,
                        name='conv_dw_%d' % block_id)(x)
    x = BatchNormalization(
        axis=channel_axis, name='conv_dw_%d_bn' % block_id)(x)
    x = Activation(relu6, name='conv_dw_%d_relu' % block_id)(x)

    x = Conv2D(pointwise_conv_filters, (1, 1), data_format=IMAGE_ORDERING,
               padding='same',
               use_bias=False,
               strides=(1, 1),
               name='conv_pw_%d' % block_id)(x)
    x = BatchNormalization(axis=channel_axis,
                           name='conv_pw_%d_bn' % block_id)(x)
    return Activation(relu6, name='conv_pw_%d_relu' % block_id)(x)

def get_mobilnet_eocoder(input_shape=(224,224,3),weights_path=""):

    # 必须是32 的倍数
    assert input_shape[0] % 32 == 0
    assert input_shape[1] % 32 == 0

    alpha = 1.0
    depth_multiplier = 1

    img_input = Input(shape=input_shape)
    #(None, 224, 224, 3) ->(None, 112, 112, 64)
    x = _conv_block(img_input, 32, alpha, strides=(2, 2))
    x = _depthwise_conv_block(x, 64, alpha, depth_multiplier, block_id=1)
    f1 = x
 
    #(None, 112, 112, 64) -> (None, 56, 56, 128)
    x = _depthwise_conv_block(x, 128, alpha, depth_multiplier,
                              strides=(2, 2), block_id=2)
    x = _depthwise_conv_block(x, 128, alpha, depth_multiplier, block_id=3)
    f2 = x
   #(None, 56, 56, 128) -> (None, 28, 28, 256)
    x = _depthwise_conv_block(x, 256, alpha, depth_multiplier,
                              strides=(2, 2), block_id=4)
    x = _depthwise_conv_block(x, 256, alpha, depth_multiplier, block_id=5)
    f3 = x
    # (None, 28, 28, 256) ->  (None, 14, 14, 512)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier,
                              strides=(2, 2), block_id=6)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=7)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=8)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=9)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=10)
    x = _depthwise_conv_block(x, 512, alpha, depth_multiplier, block_id=11)
    f4 = x
    # (None, 14, 14, 512) -> (None, 7, 7, 1024)
    x = _depthwise_conv_block(x, 1024, alpha, depth_multiplier,
                              strides=(2, 2), block_id=12)
    x = _depthwise_conv_block(x, 1024, alpha, depth_multiplier, block_id=13)
    f5 = x
    # 加载预训练模型
    if weights_path!="":
        Model(img_input, x).load_weights(weights_path, by_name=True, skip_mismatch=True)
    # f1: (None, 112, 112, 64)
    # f2: (None, 56, 56, 128)
    # f3: (None, 28, 28, 256)
    # f4: (None, 14, 14, 512)
    # f5: (None, 7, 7, 1024)
    return img_input, [f1, f2, f3, f4, f5]


def mobilenet_unet(num_classes=2,input_shape=(224,224,3)):
    
    #encoder 
    img_input,levels = get_mobilnet_eocoder(input_shape=input_shape,weights_path="model_data\\mobilenet_1_0_224_tf_no_top.h5")

    [f1, f2, f3, f4, f5] = levels

    # f1: (None, 112, 112, 64)
    # f2: (None, 56, 56, 128)
    # f3: (None, 28, 28, 256)
    # f4: (None, 14, 14, 512)
    # f5: (None, 7, 7, 1024)

    #decoder
    #(None, 14, 14, 512) - > (None, 14, 14, 512)
    o = f4
    o = ZeroPadding2D()(o)
    o = Conv2D(512, (3, 3), padding='valid' , activation='relu' , data_format=IMAGE_ORDERING)(o)
    o = BatchNormalization()(o)

    #(None, 14, 14, 512) ->(None,28,28,256)
    o = UpSampling2D(2)(o)
    o = Concatenate(axis=-1)([o,f3])
    o = ZeroPadding2D()(o)
    o = Conv2D(256, (3, 3), padding='valid' , activation='relu' , data_format=IMAGE_ORDERING)(o)
    o = BatchNormalization()(o)
    # None,28,28,256)->(None,56,56,128)
    o = UpSampling2D(2)(o)
    o = Concatenate(axis=-1)([o,f2])
    o = ZeroPadding2D()(o)
    o = Conv2D(128, (3, 3), padding='valid' , activation='relu' , data_format=IMAGE_ORDERING)(o)
    o = BatchNormalization()(o)
    #(None,56,56,128) ->(None,112,112,64)
    o = UpSampling2D(2)(o)
    o = Concatenate(axis=-1)([o,f1])
    o = ZeroPadding2D()(o)
    o = Conv2D(128, (3, 3), padding='valid' , activation='relu' , data_format=IMAGE_ORDERING)(o)
    o = BatchNormalization()(o)
    #(None,112,112,64) -> (None,112,112,num_classes)

    # 再上采样 让输入和出处图片大小一致
    o = UpSampling2D(2)(o)
    o = ZeroPadding2D()(o)
    o = Conv2D(64, (3, 3), padding='valid' , activation='relu' , data_format=IMAGE_ORDERING)(o)
    o = BatchNormalization()(o)

    o = Conv2D(num_classes, (3, 3), padding='same',
               data_format=IMAGE_ORDERING)(o)

    return Model(img_input,o)

if __name__=="__main__":
    mobilenet_unet(input_shape=(512,512,3)).summary()


特征图的大小变化,以及代码含义都已经注释在代码里了。大家仔细阅读吧

4.数据加载部分

import math
import os
from random import shuffle

import cv2
import keras
import numpy as np
from PIL import Image
#-------------------------------
# 将图片转换为 rgb
#------------------------------
def cvtColor(image):
    if len(np.shape(image)) == 3 and np.shape(image)[2] == 3:
        return image 
    else:
        image = image.convert('RGB')
        return image 
#-------------------------------
# 图片归一化 0~1
#------------------------------
def preprocess_input(image):
    image = image / 127.5 - 1
    return image
#---------------------------------------------------
#   对输入图像进行resize
#---------------------------------------------------
def resize_image(image, size):
    iw, ih  = image.size
    w, h    = size

    scale   = min(w/iw, h/ih)
    nw      = int(iw*scale)
    nh      = int(ih*scale)

    image   = image.resize((nw,nh), Image.BICUBIC)
    new_image = Image.new('RGB', size, (128,128,128))
    new_image.paste(image, ((w-nw)//2, (h-nh)//2))

    return new_image, nw, nh


class UnetDataset(keras.utils.Sequence):
    def __init__(self, annotation_lines, input_shape, batch_size, num_classes, train, dataset_path):
        self.annotation_lines   = annotation_lines
        self.length             = len(self.annotation_lines)
        self.input_shape        = input_shape
        self.batch_size         = batch_size
        self.num_classes        = num_classes
        self.train          

以上是关于憨批的语义分割重制版11——Keras 搭建自己的HRNetV2语义分割平台的主要内容,如果未能解决你的问题,请参考以下文章

数据增强图像数据生成器 Keras 语义分割

Keras深度学习实战(18)——语义分割详解

搭建自己的语义分割平台deeplabV3+

Keras中语义分割的不平衡数据?

语义分割 Keras 的交叉熵损失

keras/tensorflow中语义图像分割的多类加权损失

(c)2006-2024 SYSTEM All Rights Reserved IT常识