代码示例 / 计算机视觉 / 学习在计算机视觉中的图像缩放

学习在计算机视觉中的图像缩放

作者: Sayak Paul
创建日期: 2021/04/30
最后修改: 2023/12/18
描述: 如何为给定分辨率最佳化学习图像的表现。

在 Colab 中查看 GitHub 源代码

人们普遍认为,如果我们限制视觉模型以人类的方式感知事物,他们的性能可以得到改善。例如,在这项工作中,Geirhos 等人展示了预训练在 ImageNet-1k 数据集上的视觉模型是偏向于纹理的,而人类则主要使用形状描述符来发展共同的感知。但是,这种信念是否总是适用,特别是在改善视觉模型的性能时?

事实证明,这并不总是如此。在训练视觉模型时,通常会将图像调整为较低的维度((224 x 224)、(299 x 299)等),以便进行小批量学习,并保持计算限制。我们通常使用 双线性插值 等图像缩放方法,并且缩放后的图像在人眼中不会失去太多的感知特征。在学习计算机视觉任务的图像缩放中,Talebi 等人展示了如果我们尝试为视觉模型优化图像的感知质量而不是为人类眼睛,他们的性能可以进一步改善。他们研究了以下问题:

给定图像分辨率和模型,如何最佳地缩放给定图像?

论文中显示,这个想法有助于持续提高预训练在 ImageNet-1k 上的常见视觉模型(如 DenseNet-121、ResNet-50、MobileNetV2 和 EfficientNets)的性能。在这个例子中,我们将实现论文中提出的可学习图像缩放模块,并在Cats and Dogs 数据集上演示,使用DenseNet-121架构。


设置

import os

os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
from keras import ops
from keras import layers
import tensorflow as tf

import tensorflow_datasets as tfds

tfds.disable_progress_bar()

import matplotlib.pyplot as plt
import numpy as np

定义超参数

为了方便小批量学习,我们需要为给定批次中的图像设置一个固定形状。这就是为什么需要初始缩放。我们首先将所有图像调整为 (300 x 300) 的形状,然后为 (150 x 150) 分辨率学习它们的最佳表示。

INP_SIZE = (300, 300)
TARGET_SIZE = (150, 150)
INTERPOLATION = "bilinear"

AUTO = tf.data.AUTOTUNE
BATCH_SIZE = 64
EPOCHS = 5

在这个例子中,我们将使用双线性插值,但可学习图像缩放模块并不依赖于任何特定的插值方法。我们还可以使用其他方法,例如双三次插值。


加载和准备数据集

在这个例子中,我们将只使用总训练数据集的 40%。

train_ds, validation_ds = tfds.load(
    "cats_vs_dogs",
    # 保留 10% 用于验证
    split=["train[:40%]", "train[40%:50%]"],
    as_supervised=True,
)


def preprocess_dataset(image, label):
    image = ops.image.resize(image, (INP_SIZE[0], INP_SIZE[1]))
    label = ops.one_hot(label, num_classes=2)
    return (image, label)


train_ds = (
    train_ds.shuffle(BATCH_SIZE * 100)
    .map(preprocess_dataset, num_parallel_calls=AUTO)
    .batch(BATCH_SIZE)
    .prefetch(AUTO)
)
validation_ds = (
    validation_ds.map(preprocess_dataset, num_parallel_calls=AUTO)
    .batch(BATCH_SIZE)
    .prefetch(AUTO)
)

定义可学习的缩放工具

下图(来自:学习计算机视觉任务的图像缩放)展示了可学习缩放模块的结构:

def conv_block(x, filters, kernel_size, strides, activation=layers.LeakyReLU(0.2)):
    x = layers.Conv2D(filters, kernel_size, strides, padding="same", use_bias=False)(x)
    x = layers.BatchNormalization()(x)
    if activation:
        x = activation(x)
    return x


def res_block(x):
    inputs = x
    x = conv_block(x, 16, 3, 1)
    x = conv_block(x, 16, 3, 1, activation=None)
    return layers.Add()([inputs, x])

    # 注意:用户如果需要,可以将num_res_blocks更改为>1


def get_learnable_resizer(filters=16, num_res_blocks=1, interpolation=INTERPOLATION):
    inputs = layers.Input(shape=[None, None, 3])

    # 首先,进行简单的调整大小。
    naive_resize = layers.Resizing(*TARGET_SIZE, interpolation=interpolation)(inputs)

    # 第一个没有批归一化的卷积块。
    x = layers.Conv2D(filters=filters, kernel_size=7, strides=1, padding="same")(inputs)
    x = layers.LeakyReLU(0.2)(x)

    # 第二个带有批归一化的卷积块。
    x = layers.Conv2D(filters=filters, kernel_size=1, strides=1, padding="same")(x)
    x = layers.LeakyReLU(0.2)(x)
    x = layers.BatchNormalization()(x)

    # 作为瓶颈的中间调整大小。
    bottleneck = layers.Resizing(*TARGET_SIZE, interpolation=interpolation)(x)

    # 残差传递。
    # 第一个res_block将得到瓶颈输出作为输入
    x = res_block(bottleneck)
    # 剩余的res_blocks将得到上一个res_block的输出作为输入
    for _ in range(num_res_blocks - 1):
        x = res_block(x)

    # 投影。
    x = layers.Conv2D(
        filters=filters, kernel_size=3, strides=1, padding="same", use_bias=False
    )(x)
    x = layers.BatchNormalization()(x)

    # 跳跃连接。
    x = layers.Add()([bottleneck, x])

    # 最终调整大小的图像。
    x = layers.Conv2D(filters=3, kernel_size=7, strides=1, padding="same")(x)
    final_resize = layers.Add()([naive_resize, x])

    return keras.Model(inputs, final_resize, name="learnable_resizer")


learnable_resizer = get_learnable_resizer()

可视化可学习调整模块的输出

在这里,我们可视化了经过调整大小模块随机权重处理后的图像效果。

sample_images, _ = next(iter(train_ds))


plt.figure(figsize=(16, 10))
for i, image in enumerate(sample_images[:6]):
    image = image / 255

    ax = plt.subplot(3, 4, 2 * i + 1)
    plt.title("输入图像")
    plt.imshow(image.numpy().squeeze())
    plt.axis("off")

    ax = plt.subplot(3, 4, 2 * i + 2)
    resized_image = learnable_resizer(image[None, ...])
    plt.title("调整大小后的图像")
    plt.imshow(resized_image.numpy().squeeze())
    plt.axis("off")
Corrupt JPEG data: 65 extraneous bytes before marker 0xd9
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

png


模型构建工具

def get_model():
    backbone = keras.applications.DenseNet121(
        weights=None,
        include_top=True,
        classes=2,
        input_shape=((TARGET_SIZE[0], TARGET_SIZE[1], 3)),
    )
    backbone.trainable = True

    inputs = layers.Input((INP_SIZE[0], INP_SIZE[1], 3))
    x = layers.Rescaling(scale=1.0 / 255)(inputs)
    x = learnable_resizer(x)
    outputs = backbone(x)

    return keras.Model(inputs, outputs)

可学习图像调整模块的结构允许与不同视觉模型进行灵活集成。


使用可学习调整器编译和训练我们的模型

model = get_model()
model.compile(
    loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),
    optimizer="sgd",
    metrics=["accuracy"],
)
model.fit(train_ds, validation_data=validation_ds, epochs=EPOCHS)
Epoch 1/5

 146/146 ━━━━━━━━━━━━━━━━━━━━ 1790s 12s/step - accuracy: 0.5783 - loss: 0.6877 - val_accuracy: 0.4953 - val_loss: 0.7173

Epoch 2/5

 146/146 ━━━━━━━━━━━━━━━━━━━━ 1738s 12s/step - accuracy: 0.6516 - loss: 0.6436 - val_accuracy: 0.6148 - val_loss: 0.6605

Epoch 3/5

 146/146 ━━━━━━━━━━━━━━━━━━━━ 1730s 12s/step - accuracy: 0.6881 - loss: 0.6185 - val_accuracy: 0.5529 - val_loss: 0.8655

Epoch 4/5

 146/146 ━━━━━━━━━━━━━━━━━━━━ 1725s 12s/step - accuracy: 0.6985 - loss: 0.5980 - val_accuracy: 0.6862 - val_loss: 0.6070

Epoch 5/5

 146/146 ━━━━━━━━━━━━━━━━━━━━ 1722s 12s/step - accuracy: 0.7499 - loss: 0.5595 - val_accuracy: 0.6737 - val_loss: 0.6321

<keras.src.callbacks.history.History at 0x7f126c5440a0>

可视化经过训练的可视化器的输出

plt.figure(figsize=(16, 10))
for i, image in enumerate(sample_images[:6]):
    image = image / 255

    ax = plt.subplot(3, 4, 2 * i + 1)
    plt.title("输入图像")
    plt.imshow(image.numpy().squeeze())
    plt.axis("off")

    ax = plt.subplot(3, 4, 2 * i + 2)
    resized_image = learnable_resizer(image[None, ...])
    plt.title("调整大小后的图像")
    plt.imshow(resized_image.numpy().squeeze() / 10)
    plt.axis("off")
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

png

该图表显示,通过训练图像的视觉效果得到了改善。以下表格展示了与使用双线性插值相比,使用调整模块的好处: | 模型 | 参数数量(百万) | Top-1 精度 | |:-------------------------:|:----------------:|:----------:| | 带可学习缩放器的模型 | 7.051717 | 67.67% | | 不带可学习缩放器的模型 | 7.039554 | 60.19% |

有关更多详细信息,请查看该仓库。 请注意,以上报告的模型是在猫和狗的训练集的90%上训练了10个时期,而不是这个例子。另外,注意缩放模块导致的参数数量增加是非常微不足道的。为了确保性能的提升不是由于随机性,模型使用相同的初始随机权重进行了训练。

现在,值得提出的问题是 - 提高的准确率难道不是因为向模型中添加更多层(毕竟缩放器是一个迷你网络)而造成的吗?

为了证明情况并非如此,作者进行了以下实验:

  • 取一个预训练模型,训练某个尺寸,比如(224 x 224)。
  • 现在,首先,利用它对缩小分辨率的图像进行预测推断。记录性能。
  • 对于第二个实验,在预训练模型的顶部插入缩放器模块,并进行热启动训练。记录性能。

现在,作者认为使用第二种选项更好,因为这有助于模型学习如何根据给定分辨率更好地调整表示。由于结果完全是经验性的,分析跨通道交互等更多实验会更好。值得注意的是,像Squeeze and Excitation (SE) 块Global Context (GC) 块等元素也会向现有网络添加一些参数,但它们被认为有助于网络以系统的方式处理信息,从而提高整体性能。


注意事项

  • 为了在视觉模型中施加形状偏见,Geirhos 等人使用自然图像和风格化图像的组合对其进行了训练。调查这个可学习的缩放模块是否能够实现类似的效果会很有趣,因为输出似乎丢弃了纹理信息。
  • 缩放器模块可以处理任意分辨率和纵横比,这对目标检测和分割等任务非常重要。
  • 还有一个与自适应图像缩放密切相关的主题,它尝试在训练期间自适应地调整图像/特征图的缩放。EfficientV2 使用了这个想法。