图像分割示例
训练数据可下载
shell
!wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
!wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz
!tar -xf images.tar.gz
!tar -xf annotations.tar.gz
图片和图片的掩码
python
import os
import matplotlib.pyplot as plt
from keras.src.utils import load_img, img_to_array
def display_target(target_array):
# 原始标签是123 我们减1 标签的值变成0~2 然后乘以127
# 原始标签中 1代表前景 2代表背景 3代表轮廓
# 标签就是 0黑色 127灰色 254接近白色
normalized_target_array = (target_array.astype("uint8") - 1) * 127
plt.axis("off")
plt.imshow(normalized_target_array[:, :, 0])
plt.show()
if __name__ == '__main__':
input_dir = "images/"
target_dir = "annotations/trimaps/"
input_img_paths = sorted(
[os.path.join(input_dir, fname) for fname in os.listdir(input_dir) if fname.endswith(".jpg")]
)
target_img_paths = sorted([os.path.join(target_dir, fname) for fname in os.listdir(target_dir) if
fname.endswith(".png") and not fname.startswith(".")])
plt.axis("off")
# 注意这个show图片的方法 核心是load_image 这个是keras里面提供的方法
plt.imshow(load_img(input_img_paths[9]))
# 展示原图
plt.show()
# grayscale 代表加载的图具有单一颜色通道
img = img_to_array(load_img(input_img_paths[9],color_mode="grayscale"))
display_target(img)
原图:
对应目标的掩码图
准备训练数据
python
import os
import random
import matplotlib.pyplot as plt
import numpy as np
from keras.src.utils import load_img, img_to_array
def path_to_input_image(path):
return img_to_array(load_img(path, target_size=img_size))
def path_to_target(path):
img = img_to_array(load_img(path, target_size=img_size, color_mode="grayscale"))
# 使标签变为012
img = img.astype("uint8") - 1
return img
def display_target(target_array):
# 原始标签是123 我们减1 标签的值变成0~2 然后乘以127
# 原始标签中 1代表前景 2代表背景 3代表轮廓
# 标签就是 0黑色 127灰色 254接近白色
normalized_target_array = (target_array.astype("uint8") - 1) * 127
plt.axis("off")
plt.imshow(normalized_target_array[:, :, 0])
plt.show()
if __name__ == '__main__':
input_dir = "images/"
target_dir = "annotations/trimaps/"
input_img_paths = sorted(
[os.path.join(input_dir, fname) for fname in os.listdir(input_dir) if fname.endswith(".jpg")]
)
target_img_paths = sorted([os.path.join(target_dir, fname) for fname in os.listdir(target_dir) if
fname.endswith(".png") and not fname.startswith(".")])
img_size = (200, 200)
num_imgs = len(input_img_paths)
# 使用相同的种子来打乱顺序,目的是为了 数据和标签能11对应上
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_img_paths)
# 将所有图像加载到float32的数组中 掩码加载到uint8的数组中
# 主要还是数据集很小,所以一次性把图片加载到内存中
# 3和1 的区别 前面讲过了 3代表 rgb 3个通道
# 1代表只有1个通道的 掩码图
input_imgs = np.zeros((num_imgs,) + img_size + (3,), dtype="float32")
targets = np.zeros((num_imgs,) + img_size + (1,), dtype="uint8")
print(f"input_imgs.shape: {input_imgs.shape}")
print(f"targets.shape: {targets.shape}")
for i in range(num_imgs):
input_imgs[i] = path_to_input_image(input_img_paths[i])
targets[i] = path_to_target(target_img_paths[i])
num_val_samples = 1000
# 将数据划分为 数据集和验证集
train_input_imgs = input_imgs[:-num_val_samples]
train_targets = targets[:-num_val_samples]
val_input_imgs = input_imgs[-num_val_samples:]
val_targets = targets[-num_val_samples:]
看下shape
构建模型
python
import os
import random
import matplotlib.pyplot as plt
import numpy as np
from keras import layers
from keras.src.utils import load_img, img_to_array
from tensorflow import keras
def path_to_input_image(path):
return img_to_array(load_img(path, target_size=img_size))
def path_to_target(path):
img = img_to_array(load_img(path, target_size=img_size, color_mode="grayscale"))
# 使标签变为012
img = img.astype("uint8") - 1
return img
def display_target(target_array):
# 原始标签是123 我们减1 标签的值变成0~2 然后乘以127
# 原始标签中 1代表前景 2代表背景 3代表轮廓
# 标签就是 0黑色 127灰色 254接近白色
normalized_target_array = (target_array.astype("uint8") - 1) * 127
plt.axis("off")
plt.imshow(normalized_target_array[:, :, 0])
plt.show()
def get_model(img_size, num_classes):
inputs = keras.Input(shape=img_size + (3,))
# 将输入图像的尺寸调整到[0.1] 区间
x = layers.Rescaling(1. / 255)(inputs)
# padding = same 是避免 边界填充对特征图大小造成影响
# 这一步的目的是希望最终得到 25 25 256 的图像 将图像编码为较小的特征图
x = layers.Conv2D(64, 3, strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(128, 3, strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(128, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(256, 3, strides=2, padding="same", activation="relu")(x)
x = layers.Conv2D(256, 3, activation="relu", padding="same")(x)
# 这一步的目的是将 25 25 256的特征图还原到 200 200 3
# 注意上下的2步 是成对出现的 固定写法
x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same", strides=2)(x)
x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same", strides=2)(x)
x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same", strides=2)(x)
outputs = layers.Conv2D(num_classes, 3, activation="softmax", padding="same")(x)
model = keras.Model(inputs, outputs)
return model
if __name__ == '__main__':
input_dir = "images/"
target_dir = "annotations/trimaps/"
input_img_paths = sorted(
[os.path.join(input_dir, fname) for fname in os.listdir(input_dir) if fname.endswith(".jpg")]
)
target_img_paths = sorted([os.path.join(target_dir, fname) for fname in os.listdir(target_dir) if
fname.endswith(".png") and not fname.startswith(".")])
img_size = (200, 200)
num_imgs = len(input_img_paths)
# 使用相同的种子来打乱顺序,目的是为了 数据和标签能11对应上
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_img_paths)
# 将所有图像加载到float32的数组中 掩码加载到uint8的数组中
# 主要还是数据集很小,所以一次性把图片加载到内存中
# 3和1 的区别 前面讲过了 3代表 rgb 3个通道
# 1代表只有1个通道的 掩码图
input_imgs = np.zeros((num_imgs,) + img_size + (3,), dtype="float32")
targets = np.zeros((num_imgs,) + img_size + (1,), dtype="uint8")
print(f"input_imgs.shape: {input_imgs.shape}")
print(f"targets.shape: {targets.shape}")
for i in range(num_imgs):
input_imgs[i] = path_to_input_image(input_img_paths[i])
targets[i] = path_to_target(target_img_paths[i])
num_val_samples = 1000
# 将数据划分为 数据集和验证集
train_input_imgs = input_imgs[:-num_val_samples]
train_targets = targets[:-num_val_samples]
val_input_imgs = input_imgs[-num_val_samples:]
val_targets = targets[-num_val_samples:]
model = get_model(img_size=img_size, num_classes=3)
model.summary()
看下模型。可以清晰的看到 模型的 转换过程
训练
没有cuda的话,这个训练时间很长。我mac arm 电脑 要10个小时左右。
python
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")
callbacks = [
keras.callbacks.ModelCheckpoint("oxford_segmentation.keras",
save_best_only=True)
]
history = model.fit(train_input_imgs, train_targets,
epochs=50,
callbacks=callbacks,
batch_size=64,
validation_data=(val_input_imgs, val_targets))
epochs = range(1, len(history.history["val_loss"]) + 1)
loss = history.history["loss"]
val_loss = history.history["val_loss"]
plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Traning and validation loss")
plt.legend()
plt.show()
看下损失曲线,大概在25次的时候出现过拟合的现象
使用该模型
我们可以验证一下 该模型的效果
scss
def display_mask(pred):
mask = np.argmax(pred, axis=-1)
mask *= 127
plt.axis("off")
plt.imshow(mask)
plt.show()
ini
# 将数据划分为 数据集和验证集
train_input_imgs = input_imgs[:-num_val_samples]
tarin_targets = targets[:-num_val_samples]
val_input_imgs = input_imgs[-num_val_samples:]
val_targets = targets[-num_val_samples:]
model = keras.models.load_model("oxford_segmentation.keras")
i = 4
test_image = val_input_imgs[i]
plt.axis('off')
plt.imshow(array_to_img(test_image))
plt.show()
mask = model.predict(np.expand_dims(test_image, 0))[0]
display_mask(mask)
原图:
训练后的
中间激活值的可视化
这一小节 主要看下一个输入如何被分解成不同的滤波器 这些滤波器将由神经网络进行学习
ini
import matplotlib.pyplot as plt
from keras import layers
from tensorflow import keras
import numpy as np
# 下载测试图像
img_path = keras.utils.get_file(
fname="cat.jpg",
origin="https://img-datasets.s3.amazonaws.com/cat.jpg")
def get_img_array(img_path, target_size):
# 打开图像 并且调整尺寸
img = keras.utils.load_img(
img_path, target_size=target_size)
# 将图像转换为数组
array = keras.utils.img_to_array(img)
array = np.expand_dims(array, axis=0)
return array
if __name__ == '__main__':
img_tensor = get_img_array(img_path, target_size=(180, 180))
plt.axis("off")
plt.imshow(img_tensor[0].astype("uint8"))
plt.show()
# 用一下 前面第八章我们的猫狗分类模型
model = keras.models.load_model("convent_from_scratch_with_aug.keras")
# 看下这个模型的结构
model.summary()
layer_outputs = []
layer_names = []
for layer in model.layers:
if isinstance(layer, (layers.Conv2D, layers.MaxPooling2D)):
layer_outputs.append(layer.output)
layer_names.append(layer.name)
activation_model = keras.Model(inputs=model.input, outputs=layer_outputs)
activations = activation_model.predict(img_tensor)
# 第一层其实是一个 (1, 178, 178, 32)
first_layer_activation = activations[0]
print(f"activations: {first_layer_activation.shape}")
# 绘制下第五层的通道 看看啥样,也可以看看其他通道啥样
plt.matshow(first_layer_activation[0, :, :, 5], cmap="viridis")
plt.show()
简单看下这个模型的结构
原图:
看上去有点像边缘检测
再改一下代码,把每层的每个通道都可视化一下
python
images_per_row = 16
for layer_name, layer_activation in zip(layer_names, activations):
n_features = layer_activation.shape[-1]
size = layer_activation.shape[1]
n_cols = n_features // images_per_row
display_grid = np.zeros(((size + 1) * n_cols - 1,
images_per_row * (size + 1) - 1))
for col in range(n_cols):
for row in range(images_per_row):
channel_index = col * images_per_row + row
channel_image = layer_activation[0, :, :, channel_index].copy()
if channel_image.sum() != 0:
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype("uint8")
display_grid[
col * (size + 1): (col + 1) * size + col,
row * (size + 1): (row + 1) * size + row] = channel_image
scale = 1. / size
plt.figure(figsize=(scale * display_grid.shape[1],
scale * display_grid.shape[0]))
plt.title(layer_name)
plt.grid(False)
plt.axis("off")
plt.imshow(display_grid, aspect="auto", cmap="viridis")
plt.show()
再看下图:
可以看出来一个规律 随着层数越多,关于图像视觉的内容越来越少,图像类别的信息则越来越多
随着层数的增多,层所提取的特征越来越抽象
其实很像人类的思考方式,你看过一个场景以后,你能记住这个场景有自行车,但是你记不住这个自行车的细节长啥样。
其实大脑也是将视觉输入完全抽象化的