代码示例 / 计算机视觉 / 关键点检测与迁移学习

关键点检测与迁移学习

作者: Sayak Paul,由 Muhammad Anas Raza 转换为 Keras 3
创建日期: 2021/05/02
最后修改: 2023/07/19
描述: 通过数据增强和迁移学习训练关键点检测器。

在 Colab 中查看 GitHub 源代码

关键点检测的任务是定位对象的关键部分。例如,我们面部的关键部分包括鼻尖、眉毛、眼角等等。这些部分有助于以特征丰富的方式表示基础对象。关键点检测的应用包括姿势估计、人脸检测等。

在这个示例中,我们将使用 StanfordExtra 数据集 构建一个关键点检测器,使用迁移学习。这个示例需要 TensorFlow 2.4 或更高版本,以及 imgaug 库,可以使用以下命令安装:

!pip install -q -U imgaug

数据收集

StanfordExtra 数据集包含 12,000 张狗的图像以及关键点和分割图。该数据集是基于 Stanford dogs dataset 开发的。可以使用下面的命令下载:

!wget -q http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar

在 StanfordExtra 数据集中,注释以单个 JSON 文件提供,用户需要填写 这个表单 以获取访问权限。作者明确要求用户不要分享 JSON 文件,而这个示例也尊重这个愿望:您应该自己获取 JSON 文件。

预计 JSON 文件在本地可用,文件名为 stanfordextra_v12.zip

下载文件后,我们可以提取归档文件。

!tar xf images.tar
!unzip -qq ~/stanfordextra_v12.zip

导入模块

from keras import layers
import keras

from imgaug.augmentables.kps import KeypointsOnImage
from imgaug.augmentables.kps import Keypoint
import imgaug.augmenters as iaa

from PIL import Image
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
import json
import os

定义超参数

IMG_SIZE = 224
BATCH_SIZE = 64
EPOCHS = 5
NUM_KEYPOINTS = 24 * 2  # 每对有 x 和 y 坐标的 24 个关键点

加载数据

作者还提供了一个元数据文件,指定有关关键点的附加信息,例如颜色信息、动物姿势名称等。我们将以 pandas 数据框的形式加载此文件,以提取用于可视化目的的信息。

IMG_DIR = "Images"
JSON = "StanfordExtra_V12/StanfordExtra_v12.json"
KEYPOINT_DEF = (
    "https://github.com/benjiebob/StanfordExtra/raw/master/keypoint_definitions.csv"
)

# 加载真实标注。
with open(JSON) as infile:
    json_data = json.load(infile)

# 设置一个字典,将所有真实信息与图像路径进行映射。
json_dict = {i["img_path"]: i for i in json_data}

A single entry of json_dict looks like the following:

'n02085782-Japanese_spaniel/n02085782_2886.jpg':
{'img_bbox': [205, 20, 116, 201],
 'img_height': 272,
 'img_path': 'n02085782-Japanese_spaniel/n02085782_2886.jpg',
 'img_width': 350,
 'is_multiple_dogs': False,
 'joints': [[108.66666666666667, 252.0, 1],
            [147.66666666666666, 229.0, 1],
            [163.5, 208.5, 1],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [54.0, 244.0, 1],
            [77.33333333333333, 225.33333333333334, 1],
            [79.0, 196.5, 1],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [150.66666666666666, 86.66666666666667, 1],
            [88.66666666666667, 73.0, 1],
            [116.0, 106.33333333333333, 1],
            [109.0, 123.33333333333333, 1],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]],
 'seg': ...}

在这个例子中,我们感兴趣的键是:

  • img_path
  • joints

joints 中总共有 24 个条目。每个条目包含 3 个值:

  • x 坐标
  • y 坐标
  • 关键点的可见性标志(1 表示可见,0 表示不可见)

如我们所见,joints 包含多个 [0, 0, 0] 条目,这表示这些关键点未被标记。在这个例子中,我们将考虑不可见的关键点以及未标记的关键点,以便进行小批量学习。

# 加载元数据定义文件并预览。
keypoint_def = pd.read_csv(KEYPOINT_DEF)
keypoint_def.head()

# 提取颜色和标签。
colours = keypoint_def["Hex colour"].values.tolist()
colours = ["#" + colour for colour in colours]
labels = keypoint_def["Name"].values.tolist()


# 读取图像和获取其注释的工具。
def get_dog(name):
    data = json_dict[name]
    img_data = plt.imread(os.path.join(IMG_DIR, data["img_path"]))
    # 如果图像是RGBA格式,则转换为RGB格式。
    if img_data.shape[-1] == 4:
        img_data = img_data.astype(np.uint8)
        img_data = Image.fromarray(img_data)
        img_data = np.array(img_data.convert("RGB"))
    data["img_data"] = img_data

    return data

可视化数据

现在,我们编写一个实用函数来可视化图像及其关键点。

# 此代码的一部分来自这里:
# https://github.com/benjiebob/StanfordExtra/blob/master/demo.ipynb
def visualize_keypoints(images, keypoints):
    fig, axes = plt.subplots(nrows=len(images), ncols=2, figsize=(16, 12))
    [ax.axis("off") for ax in np.ravel(axes)]

    for (ax_orig, ax_all), image, current_keypoint in zip(axes, images, keypoints):
        ax_orig.imshow(image)
        ax_all.imshow(image)

        # 如果关键点是由 `imgaug` 形成的,则需要以不同的方式迭代坐标。
        if isinstance(current_keypoint, KeypointsOnImage):
            for idx, kp in enumerate(current_keypoint.keypoints):
                ax_all.scatter(
                    [kp.x],
                    [kp.y],
                    c=colours[idx],
                    marker="x",
                    s=50,
                    linewidths=5,
                )
        else:
            current_keypoint = np.array(current_keypoint)
            # 由于最后一个条目是可见性标志,我们将其丢弃。
            current_keypoint = current_keypoint[:, :2]
            for idx, (x, y) in enumerate(current_keypoint):
                ax_all.scatter([x], [y], c=colours[idx], marker="x", s=50, linewidths=5)

    plt.tight_layout(pad=2.0)
    plt.show()


# 随机选择四个样本进行可视化。
samples = list(json_dict.keys())
num_samples = 4
selected_samples = np.random.choice(samples, num_samples, replace=False)

images, keypoints = [], []

for sample in selected_samples:
    data = get_dog(sample)
    image = data["img_data"]
    keypoint = data["joints"]

    images.append(image)
    keypoints.append(keypoint)

visualize_keypoints(images, keypoints)

png

这些图表显示我们有不统一大小的图像,在大多数现实场景中这是可以预期的。然而,如果我们将这些图像调整大小以具有统一的形状(例如(224 x 224)),它们的真实注释也会受到影响。如果我们对图像应用任何几何变换(例如水平翻转),同样会适用。幸运的是,imgaug 提供了可以处理此问题的工具。 在下一节中,我们将编写一个数据生成器,继承 keras.utils.Sequence 类,该类使用 imgaug 对数据批次应用数据增强。


准备数据生成器

class KeyPointsDataset(keras.utils.PyDataset):
    def __init__(self, image_keys, aug, batch_size=BATCH_SIZE, train=True, **kwargs):
        super().__init__(**kwargs)
        self.image_keys = image_keys
        self.aug = aug
        self.batch_size = batch_size
        self.train = train
        self.on_epoch_end()

    def __len__(self):
        return len(self.image_keys) // self.batch_size

    def on_epoch_end(self):
        self.indexes = np.arange(len(self.image_keys))
        if self.train:
            np.random.shuffle(self.indexes)

    def __getitem__(self, index):
        indexes = self.indexes[index * self.batch_size : (index + 1) * self.batch_size]
        image_keys_temp = [self.image_keys[k] for k in indexes]
        (images, keypoints) = self.__data_generation(image_keys_temp)

        return (images, keypoints)

    def __data_generation(self, image_keys_temp):
        batch_images = np.empty((self.batch_size, IMG_SIZE, IMG_SIZE, 3), dtype="int")
        batch_keypoints = np.empty(
            (self.batch_size, 1, 1, NUM_KEYPOINTS), dtype="float32"
        )

        for i, key in enumerate(image_keys_temp):
            data = get_dog(key)
            current_keypoint = np.array(data["joints"])[:, :2]
            kps = []

            # To apply our data augmentation pipeline, we first need to
            # form Keypoint objects with the original coordinates.
            for j in range(0, len(current_keypoint)):
                kps.append(Keypoint(x=current_keypoint[j][0], y=current_keypoint[j][1]))

            # We then project the original image and its keypoint coordinates.
            current_image = data["img_data"]
            kps_obj = KeypointsOnImage(kps, shape=current_image.shape)

            # Apply the augmentation pipeline.
            (new_image, new_kps_obj) = self.aug(image=current_image, keypoints=kps_obj)
            batch_images[i,] = new_image

            # Parse the coordinates from the new keypoint object.
            kp_temp = []
            for keypoint in new_kps_obj:
                kp_temp.append(np.nan_to_num(keypoint.x))
                kp_temp.append(np.nan_to_num(keypoint.y))

            # More on why this reshaping later.
            batch_keypoints[i,] = np.array(kp_temp).reshape(1, 1, 24 * 2)

        # Scale the coordinates to [0, 1] range.
        batch_keypoints = batch_keypoints / IMG_SIZE

        return (batch_images, batch_keypoints)

要了解如何在 imgaug 中使用关键点,请查看 此文档


定义增强变换

train_aug = iaa.Sequential(
    [
        iaa.Resize(IMG_SIZE, interpolation="linear"),
        iaa.Fliplr(0.3),
        # `Sometimes()` 随机以给定的概率(在此情况为 0.3)
        # 对输入应用函数。
        iaa.Sometimes(0.3, iaa.Affine(rotate=10, scale=(0.5, 0.7))),
    ]
)

test_aug = iaa.Sequential([iaa.Resize(IMG_SIZE, interpolation="linear")])

创建训练和验证拆分

np.random.shuffle(samples)
train_keys, validation_keys = (
    samples[int(len(samples) * 0.15) :],
    samples[: int(len(samples) * 0.15)],
)

数据生成器调查

train_dataset = KeyPointsDataset(
    train_keys, train_aug, workers=2, use_multiprocessing=True
)
validation_dataset = KeyPointsDataset(
    validation_keys, test_aug, train=False, workers=2, use_multiprocessing=True
)

print(f"训练集中的总批次数: {len(train_dataset)}")
print(f"验证集中的总批次数: {len(validation_dataset)}")

sample_images, sample_keypoints = next(iter(train_dataset))
assert sample_keypoints.max() == 1.0
assert sample_keypoints.min() == 0.0

sample_keypoints = sample_keypoints[:4].reshape(-1, 24, 2) * IMG_SIZE
visualize_keypoints(sample_images[:4], sample_keypoints)
训练集中的总批次数: 166
验证集中的总批次数: 29

png


模型构建

斯坦福狗数据集(斯坦福Extra数据集的基础)是基于ImageNet-1k数据集构建的。 因此,基于ImageNet-1k数据集预训练的模型对于此任务很可能会有用。我们将使用在此数据集上预训练的MobileNetV2作为骨干网络,从图像中提取有意义的特征,然后将这些特征传递给一个自定义回归头,以预测坐标。

def get_model():
    # 加载MobileNetV2的预训练权重并冻结权重
    backbone = keras.applications.MobileNetV2(
        weights="imagenet",
        include_top=False,
        input_shape=(IMG_SIZE, IMG_SIZE, 3),
    )
    backbone.trainable = False

    inputs = layers.Input((IMG_SIZE, IMG_SIZE, 3))
    x = keras.applications.mobilenet_v2.preprocess_input(inputs)
    x = backbone(x)
    x = layers.Dropout(0.3)(x)
    x = layers.SeparableConv2D(
        NUM_KEYPOINTS, kernel_size=5, strides=1, activation="relu"
    )(x)
    outputs = layers.SeparableConv2D(
        NUM_KEYPOINTS, kernel_size=3, strides=1, activation="sigmoid"
    )(x)

    return keras.Model(inputs, outputs, name="keypoint_detector")

我们的自定义网络是完全卷积的,这使得它比拥有全连接密集层的网络版本对参数更友好。

get_model().summary()
从 https://storage.googleapis.com/tensorflow/keras-applications/mobilenet_v2/mobilenet_v2_weights_tf_dim_ordering_tf_kernels_1.0_224_no_top.h5 下载数据
 9406464/9406464 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
模型: "keypoint_detector"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ 层 (类型)                       输出形状                  参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ input_layer_1 (输入层)      │ (None, 224, 224, 3)       │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ true_divide (TrueDivide)        │ (None, 224, 224, 3)       │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ subtract (Subtract)             │ (None, 224, 224, 3)       │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ mobilenetv2_1.00_224            │ (None, 7, 7, 1280)        │  2,257,984 │
│ (Functional)                    │                           │            │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dropout (Dropout)               │ (None, 7, 7, 1280)        │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ separable_conv2d                │ (None, 3, 3, 48)          │     93,488 │
│ (SeparableConv2D)               │                           │            │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ separable_conv2d_1              │ (None, 1, 1, 48)          │      2,784 │
│ (SeparableConv2D)               │                           │            │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 总参数: 2,354,256 (8.98 MB)
 可训练参数: 96,272 (376.06 KB)
 非可训练参数: 2,257,984 (8.61 MB)

注意网络的输出形状: (None, 1, 1, 48)。这就是我们将坐标重新调整为: batch_keypoints[i, :] = np.array(kp_temp).reshape(1, 1, 24 * 2) 的原因。


模型编译与训练

在本例中,我们将仅训练网络五个时期。

model = get_model()
model.compile(loss="mse", optimizer=keras.optimizers.Adam(1e-4))
model.fit(train_dataset, validation_data=validation_dataset, epochs=EPOCHS)
第 1 轮/5
 166/166 ━━━━━━━━━━━━━━━━━━━━ 84s 415ms/步 - 损失: 0.1110 - 验证损失: 0.0959
第 2 轮/5
 166/166 ━━━━━━━━━━━━━━━━━━━━ 79s 472ms/步 - 损失: 0.0874 - 验证损失: 0.0802
第 3 轮/5
 166/166 ━━━━━━━━━━━━━━━━━━━━ 78s 463ms/步 - 损失: 0.0789 - 验证损失: 0.0765
第 4 轮/5
 166/166 ━━━━━━━━━━━━━━━━━━━━ 78s 467ms/步 - 损失: 0.0769 - 验证损失: 0.0731
第 5 轮/5
 166/166 ━━━━━━━━━━━━━━━━━━━━ 77s 464ms/步 - 损失: 0.0753 - 验证损失: 0.0712

<keras.src.callbacks.history.History 在 0x7fb5c4299ae0>

进行预测并可视化结果

sample_val_images, sample_val_keypoints = next(iter(validation_dataset))
sample_val_images = sample_val_images[:4]
sample_val_keypoints = sample_val_keypoints[:4].reshape(-1, 24, 2) * IMG_SIZE
predictions = model.predict(sample_val_images).reshape(-1, 24, 2) * IMG_SIZE

# 真实值
visualize_keypoints(sample_val_images, sample_val_keypoints)

# 预测值
visualize_keypoints(sample_val_images, predictions)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 7s 7s/step

png

png

预测结果可能会随着更多训练而改善。


更进一步

  • 尝试使用其他 imgaug 中的增强变换,研究这如何改变结果。
  • 在这里,我们线性转移了预训练网络的特征,即我们没有对其进行 微调。建议你对这个任务进行微调,看是否能提高性能。你也可以尝试不同的架构,看看它们如何影响最终性能。