Keras 实现 FCN 语义分割并训练自己的数据之 多分类

  • 一. 数据标注
  • 二. 标签图像数据处理
  • 三. 网络输出层处理
  • 四. 预测类别判断
  • 五. 预测标记
  • 六. 代码下载


一. 数据标注

在 语义分割之 数据标注 中已经讲过了二分类与多分类的图像如何标注, 不清楚的话可以倒回去看一下

二. 标签图像数据处理

二分类时标记的类别只有背景和目标, 目标像素值是 1, 所以处理很简单, 转换成 float 类型就可以了. 对于多分类, 处理方式不一样. 首先多分类激活函数是 softmax, 用的是 one-hot 编码. 我们标注的数据是单通道, 像素值分别是 0(背景), 1, 2, 3…, 怎么转换成 one-hot 的形式呢?

可以新建一张和标签图像同样大小像素值全为 0 的图像, 通道数等于类别数(包括背景). 暂且取名为 one_hot_img. 依次判断标签图像每个位置的像素值, 如果是 0, 就把 one_hot_img 第 1 个通道对应位置的值设为 1, 如果是 3, 就把第 4 个通道对应位置的值设为 1. 这样, one_hot_img 的每个像素位置所有通道就组合成了一个 one-hot 编码的 vector. 如下图所示

coco语义分割表 语义分割 fcn_Keras

在 语义分割之 加载训练数据 中我们用 Generator 的方式加载了训练数据和对应的标签图像, 适用二分类的情况. 多分类也可以用 Generator 加载数据进行训练, 只是需要在加载时将标签图像转换成 one-hot 格式, 需要增加的代码如下. 增加的位置你自己思考一下放在哪里. 如果还是不知道的话, 可以下载 Jupyter Notebook 代码示例 看一下, 里面有提示

class_num = 4 # 类别数量, 包括背景
class_vec = np.identity(class_num, dtype = np.float) # 类别向量, 如下
'''
[[1, 0, 0, 0],
 [0, 1, 0, 0],
 [0, 0, 1, 0],
 [0, 0, 0, 1]]
'''
one_hot = np.zeros((y.shape[0], y.shape[1], class_num), dtype = np.float)
   
for r in range(one_hot.shape[0]):
    for c in range(one_hot.shape[1]):
        one_hot[r][c] = class_vec[y[r][c]]
            
y = one_hot

有了以上的代码, 读入的标签就成了 one-hot 的格式了

但是, 我们没有必要这么做! 因为还有一个好用的损失函数 sparse_categorical_crossentropy. 如果用 sparse_categorical_crossentropy 作为损失函数, 我们之前定义的 segment_reader 函数都不用修改. 继续往下看

三. 网络输出层处理

在二分类的时候, 输出层只有一个通道, 多分类就需要有 class_num(类别数, 包括背景) 个通道, 所以最后一个卷积时需要有 class_num 个 Filer, 还要修改激活函数为 softmax . 修改如下

# Keras 实现 FCN 语义分割并训练自己的数据之 FCN-8s 中用的代码

# _8s 上采样 8 倍后与输入尺寸相同
up7 = keras.layers.UpSampling2D(size = (8, 8), interpolation = "bilinear",
                                name = "upsamping_7")(_8s)

# 这里的输出通道数为类别数量, 激活函数要换成 softmax
conv_7 = keras.layers.Conv2D(class_num, kernel_size = (3, 3), activation = "softmax",
                        padding = "same", name = "conv_7")(up7)

除了输出层, 编译时损失函数修改为 sparse_categorical_crossentropy. 上面已提到, 如果用 one-hot 作为标签的话, 损失函数就是我们比较容易理解的 categorical_crossentropy

model.compile(optimizer = "adam",
              loss = "sparse_categorical_crossentropy",
              metrics = ["accuracy"])

那 categorical_crossentropy 和 sparse_categorical_crossentropy 区别是什么? 区别在于输入的标签形式不同

  • categorical_crossentropy: 标签为 one-hot 编码形式, 即每个样本的标签是一个向量, 向量的长度等于类别的数量. 其中只有一个元素为 1, 其余元素为 0, 表示该样本属于哪个类别
  • sparse_categorical_crossentropy: 标签为整数形式, 即每个样本的标签是一个整数(1, 2, 3, …, n 这样的整数), 表示该样本属于哪个类别

因此, 如果使用 categorical_crossentropy 作为损失函数, 需要将标签转换为 one-hot 编码形式. 如果使用 sparse_categorical_crossentropy 作为损失函数, 需要将标签转换为整数形式. 模型会自动将整数形式的标签转换为 one-hot 编码形式, 并计算损失. 所以, 对于标签为整数形式的多分类问题, 使用 sparse_categorical_crossentropy 更加方便和高效

四. 预测类别判断

预测输出是一张 class_num 通道的图像, 只要判断 每个像素位置哪一个通道的值最大, 就把此通道的通道序号作为最后的类别. 预测代码同二分类还是不变, 需要处理的是最后类别的判断

# 显示预测结果
show_index = 0 # 显示 batch_size 中的序号, 这里显示第 0 个
roi = batch_roi[show_index]

x = batch_x[show_index][roi[1]: roi[1] + roi[3], roi[0]: roi[0] + roi[2]]
y = batch_y[show_index][roi[1]: roi[1] + roi[3], roi[0]: roi[0] + roi[2]]

# predict 就是预测结果, 是一张单通道图像, 其像素值就是类别序号
predict = np.zeros((y.shape[0], y.shape[1]), np.uint8)

# 将多通道变成一个通道, 不同的像素值代表了不同的类别
for r in range(y.shape[0]):
    for c in range(y.shape[1]):
        predict[r][c] = np.argmax(y[r][c])
        
plt.figure("segment", figsize = (8, 4))
plt.subplot(1, 2, 1)
plt.axis("on")
plt.title("test", color = "orange")
plt.imshow(x[..., : : -1])

plt.subplot(1, 2, 2)
plt.axis("on")
plt.title("predict", color = "orange")
plt.imshow(np.squeeze(predict), cmap = "gray")
plt.show()

这样修改后, 显示效果如下

coco语义分割表 语义分割 fcn_coco语义分割表_02


右图中, 有三个类别(包括背景), 中等亮度的部分是 类别1, 像素值为 1, 最亮的是 类别 2, 像素值为 2, 因为 matplotlib 显示时做了转换. 要不然看起来是全黑的

五. 预测标记

如果想把预测结果标记到原图的话, 把需要标记的颜色做一个索引表, 用结果图像的像素值去索引取出 RGB 值, 这个 RGB 值就是对应目标标记的颜色. 这一步可以在类别判断的时候做, 也可以用 predict 来做

# 标记预测结果
img_marked = x.copy() / 1.4 # 标记后的图像

# 标记颜色表, 这个颜色选你喜欢的就好, 只是通道要反过来
mark_bgr = [[0,  0,  0], # 背景
            [0,  0, 64], # 类别 1 红
            [0, 64,  0], # 类别 2 绿
            [0, 64, 64]] # 类别 3 蓝

mark_bgr = np.array(mark_bgr).astype(np.float32) / 255

for r in range(img_marked.shape[0]):
    for c in range(img_marked.shape[1]):
        img_marked[r][c] += mark_bgr[predict[r][c]]
        # 如果在判断时做就是
        # img_marked[r][c] = mark_bgr[np.argmax(y[r][c])]
        
plt.figure("mark_image", figsize = (6, 3))

plt.subplot(1, 1, 1)
plt.axis("on")
plt.title("img_marked", color = "orange")
plt.imshow(img_marked[..., : : -1])

标记效果如下

coco语义分割表 语义分割 fcn_语义分割_03

至此, FCN 基本原理的代码就完成了, 针对不同的任务, 网络结构需要优化, 以目前的情况看不是一个网络走天下, 什么都通吃. 要分析问题的特点选择或设计合适的网络