TensorFlow 2.0 计算机视觉秘籍(四)
原文:
annas-archive.org/md5/cf3ce16c27a13f4ce55f8e29a1bf85e1译者:飞龙
第八章:第八章:通过分割实现对图像的精细理解
图像分割是计算机视觉研究领域中最大的领域之一。它通过将共享一个或多个定义特征(如位置、颜色或纹理)的像素组合在一起,简化图像的视觉内容。与计算机视觉的许多其他子领域一样,图像分割也得到了深度神经网络的极大推动,特别是在医学和自动驾驶等行业。
虽然对图像的内容进行分类非常重要,但往往仅仅分类是不够的。假如我们想知道一个物体具体在哪里呢?如果我们对它的形状感兴趣呢?如果我们需要它的轮廓呢?这些精细的需求是传统分类技术无法满足的。然而,正如我们将在本章中发现的那样,我们可以用一种非常类似于常规分类项目的方式来框定图像分割问题。怎么做?我们不是给整张图像标注标签,而是给每个像素标注!这就是图像分割,也是本章食谱的核心内容。
在本章中,我们将涵盖以下食谱:
-
创建一个用于图像分割的全卷积网络
-
从头开始实现 U-Net
-
使用迁移学习实现 U-Net
-
使用 Mask-RCNN 和 TensorFlow Hub 进行图像分割
让我们开始吧!
技术要求
为了实现和实验本章的食谱,建议你拥有一台 GPU。如果你有访问基于云的服务提供商,如 AWS 或 FloydHub,那就太好了,但请注意相关费用,因为如果不小心的话,费用可能会飙升!在每个食谱的准备工作部分,你会找到你需要为接下来的内容做准备的所有信息。本章的代码可以在这里找到:github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch8。
查看以下链接,观看《代码实践》视频:
创建一个用于图像分割的全卷积网络
如果你在知道图像分割本质上就是像素级分类的情况下,创建你的第一个图像分割网络,你会怎么做?你可能会选择一个经过验证的架构,并将最后的层(通常是全连接层)替换为卷积层,以便生成一个输出体积,而不是输出向量。
好的,这正是我们在本食谱中要做的,基于著名的VGG16网络构建一个全卷积网络(FCN)来进行图像分割。
让我们开始吧!
准备工作
我们需要安装几个外部库,首先是 tensorflow_docs:
$> pip install git+https://github.com/tensorflow/docs
接下来,我们需要安装 TensorFlow Datasets、Pillow 和 OpenCV:
$> pip install tensorflow-datasets Pillow opencv-contrib-python
关于数据,我们将从the Oxford-IIIT Pet数据集中分割图像。好消息是,我们将通过tensorflow-datasets来访问它,所以在这方面我们实际上不需要做任何事情。该数据集中的每个像素将被分类如下:
-
1: 像素属于宠物(猫或狗)。
-
2: 像素属于宠物的轮廓。
-
3: 像素属于周围环境。
这里是数据集中的一些示例图像:
](tos-cn-i-73owjymdk6/1fc221d2f87c42fba43a600a314509a1)
图 8.1 – 来自 Oxford-IIIT Pet 数据集的示例图像
让我们开始实现吧!
如何实现……
按照以下步骤完成此配方:
-
导入所有必需的包:
import pathlib import cv2 import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_datasets as tfds import tensorflow_docs as tfdocs import tensorflow_docs.plots from tensorflow.keras.layers import * from tensorflow.keras.losses import \ SparseCategoricalCrossentropy from tensorflow.keras.models import Model from tensorflow.keras.optimizers import RMSprop -
为
tf.data.experimental.AUTOTUNE定义一个别名:AUTOTUNE = tf.data.experimental.AUTOTUNE -
定义一个函数,用于将数据集中的图像归一化到[0, 1]范围。为了保持一致性,我们将从掩膜中的每个像素减去 1,这样它们的范围就从 0 扩展到 2:
def normalize(input_image, input_mask): input_image = tf.cast(input_image, tf.float32) / 255.0 input_mask -= 1 return input_image, input_mask -
定义
load_image()函数,给定一个 TensorFlow 数据集元素,该函数加载图像及其掩膜。我们将借此机会将图像调整为256x256。另外,如果train标志设置为True,我们可以通过随机镜像图像及其掩膜来进行一些数据增强。最后,我们必须对输入进行归一化:@tf.function def load_image(dataset_element, train=True): input_image = tf.image.resize(dataset_element['image'], (256, 256)) input_mask = tf.image.resize( dataset_element['segmentation_mask'], (256, 256)) if train and np.random.uniform() > 0.5: input_image = tf.image.flip_left_right(input_image) input_mask = tf.image.flip_left_right(input_mask) input_image, input_mask = normalize(input_image, input_mask) return input_image, input_mask -
实现
FCN()类,该类封装了构建、训练和评估所需的所有逻辑,使用RMSProp作为优化器,SparseCategoricalCrossentropy作为损失函数。请注意,output_channels默认值为 3,因为每个像素可以被分类为三类之一。还请注意,我们正在定义基于VGG16的预训练模型的权重路径。我们将使用这些权重在训练时为网络提供一个良好的起点。 -
现在,是时候定义模型的架构了:
def _create_model(self): input = Input(shape=self.input_shape) x = Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding='same', name='block1_conv1')(input) x = Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding='same', name='block1_conv2')(x) x = MaxPooling2D(pool_size=(2, 2), strides=2, name='block1_pool')(x) -
我们首先定义了输入和第一块卷积层以及最大池化层。现在,定义第二块卷积层,这次每个卷积使用 128 个滤波器:
x = Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding='same', name='block2_conv1')(x) x = Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding='same', name='block2_conv2')(x) x = MaxPooling2D(pool_size=(2, 2), strides=2, name='block2_pool')(x) -
第三块包含 256 个滤波器的卷积:
x = Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding='same', name='block3_conv1')(x) x = Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding='same', name='block3_conv2')(x) x = Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding='same', name='block3_conv3')(x) x = MaxPooling2D(pool_size=(2, 2), strides=2, name='block3_pool')(x) block3_pool = x -
第四块使用了 512 个滤波器的卷积:
x = Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same', name='block4_conv1')(x) x = Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same', name='block4_conv2')(x) x = Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same', name='block4_conv3')(x) block4_pool = MaxPooling2D(pool_size=(2, 2), strides=2, name='block4_pool')(x) -
第五块是第四块的重复,同样使用 512 个滤波器的卷积:
x = Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same', name='block5_conv1')(block4_pool) x = Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same', name='block5_conv2')(x) x = Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same', name='block5_conv3')(x) block5_pool = MaxPooling2D(pool_size=(2, 2), strides=2, name='block5_pool')(x) -
我们到目前为止命名层的原因是,为了在接下来导入预训练权重时能够与它们匹配(请注意
by_name=True):model = Model(input, block5_pool) model.load_weights(self.vgg_weights_path, by_name=True) -
在传统的VGG16架构中,
output由全连接层组成。然而,我们将用反卷积层替换它们。请注意,我们正在将这些层连接到第五块的输出:output = Conv2D(filters=self.output_channels, kernel_size=(7, 7), activation='relu', padding='same', name='conv6')(block5_pool) conv6_4 = Conv2DTranspose( filters=self.output_channels, kernel_size=(4, 4), strides=4, use_bias=False)(output) -
创建一个 1x1 卷积层,接着是一个反卷积层,并将其连接到第四块的输出(这实际上是一个跳跃连接):
pool4_n = Conv2D(filters=self.output_channels, kernel_size=(1, 1), activation='relu', padding='same', name='pool4_n')(block4_pool) pool4_n_2 = Conv2DTranspose( filters=self.output_channels, kernel_size=(2, 2), strides=2, use_bias=False)(pool4_n) -
将第三块的输出通过一个 1x1 卷积层。然后,将这三条路径合并成一条,传递通过最后一个反卷积层。这将通过
Softmax激活。这个输出即为模型预测的分割掩膜:pool3_n = Conv2D(filters=self.output_channels, kernel_size=(1, 1), activation='relu', padding='same', name='pool3_n')(block3_pool) output = Add(name='add')([pool4_n_2, pool3_n, conv6_4]) output = Conv2DTranspose (filters=self.output_channels, kernel_size=(8, 8), strides=8, use_bias=False)(output) output = Softmax()(output) return Model(input, output) -
现在,让我们创建一个私有辅助方法来绘制相关的训练曲线:
@staticmethod def _plot_model_history(model_history, metric, ylim=None): plt.style.use('seaborn-darkgrid') plotter = tfdocs.plots.HistoryPlotter() plotter.plot({'Model': model_history}, metric=metric) plt.title(f'{metric.upper()}') if ylim is None: plt.ylim([0, 1]) else: plt.ylim(ylim) plt.savefig(f'{metric}.png') plt.close() -
train()方法接受训练和验证数据集,以及执行的周期数和训练、验证步骤数,用于拟合模型。它还将损失和准确率图保存到磁盘,以便后续分析:def train(self, train_dataset, epochs, steps_per_epoch, validation_dataset, validation_steps): hist = \ self.model.fit(train_dataset, epochs=epochs, steps_per_epoch=steps_per_epoch, validation_steps=validation_steps, validation_data=validation_dataset) self._plot_model_history(hist, 'loss', [0., 2.0]) self._plot_model_history(hist, 'accuracy') -
实现
_process_mask(),用于使分割掩膜与 OpenCV 兼容。这个函数的作用是创建一个三通道版本的灰度掩膜,并将类值上采样到 [0, 255] 范围:@staticmethod def _process_mask(mask): mask = (mask.numpy() * 127.5).astype('uint8') mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) return mask -
_save_image_and_masks()辅助方法创建了原始图像、真实标签掩膜和预测分割掩膜的马赛克图像,并将其保存到磁盘以便后续修订:def _save_image_and_masks(self, image, ground_truth_mask, prediction_mask, image_id): image = (image.numpy() * 255.0).astype('uint8') gt_mask = self._process_mask(ground_truth_mask) pred_mask = self._process_mask(prediction_mask) mosaic = np.hstack([image, gt_mask, pred_mask]) mosaic = cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR) cv2.imwrite(f'mosaic_{image_id}.jpg', mosaic) -
为了将网络输出的体积转换为有效的分割掩膜,我们必须在每个像素位置选择值最高的索引。这对应于该像素最可能的类别。
_create_mask()方法执行此操作:@staticmethod def _create_mask(prediction_mask): prediction_mask = tf.argmax(prediction_mask, axis=-1) prediction_mask = prediction_mask[..., tf.newaxis] return prediction_mask[0] -
_save_predictions()方法使用了我们在 步骤 18 中定义的_save_image_and_mask()辅助方法:def _save_predictions(self, dataset, sample_size=1): for id, (image, mask) in \ enumerate(dataset.take(sample_size), start=1): pred_mask = self.model.predict(image) pred_mask = self._create_mask(pred_mask) image = image[0] ground_truth_mask = mask[0] self._save_image_and_masks(image, ground_truth_mask, pred_mask, image_id=id) -
evaluate()方法计算 FCN 在测试集上的准确率,并为一部分图像生成预测结果,然后将其保存到磁盘:def evaluate(self, test_dataset, sample_size=5): result = self.model.evaluate(test_dataset) print(f'Accuracy: {result[1] * 100:.2f}%') self._save_predictions(test_dataset, sample_size) -
使用 TensorFlow Datasets 下载(或加载,如果已缓存)
Oxford IIIT Pet Dataset及其元数据:dataset, info = tfdata.load('oxford_iiit_pet', with_info=True) -
使用元数据定义网络在训练和验证数据集上的步数。此外,还要定义批处理和缓冲区大小:
TRAIN_SIZE = info.splits['train'].num_examples VALIDATION_SIZE = info.splits['test'].num_examples BATCH_SIZE = 32 STEPS_PER_EPOCH = TRAIN_SIZE // BATCH_SIZE VALIDATION_SUBSPLITS = 5 VALIDATION_STEPS = VALIDATION_SIZE // BATCH_SIZE VALIDATION_STEPS //= VALIDATION_SUBSPLITS BUFFER_SIZE = 1000 -
定义训练和测试数据集的管道:
train_dataset = (dataset['train'] .map(load_image, num_parallel_ calls=AUTOTUNE) .cache() .shuffle(BUFFER_SIZE) .batch(BATCH_SIZE) .repeat() .prefetch(buffer_size=AUTOTUNE)) test_dataset = (dataset['test'] .map(lambda d: load_image(d,train=False), num_parallel_calls=AUTOTUNE) .batch(BATCH_SIZE)) -
实例化 FCN 并训练 120 个周期:
fcn = FCN(output_channels=3) fcn.train(train_dataset, epochs=120, steps_per_epoch=STEPS_PER_EPOCH, validation_steps=VALIDATION_STEPS, validation_dataset=test_dataset) -
最后,在测试数据集上评估网络:
unet.evaluate(test_dataset)如下图所示,测试集上的准确率应约为 84%(具体来说,我得到的是 84.47%):
图 8.2 – 训练和验证准确率曲线
训练曲线显示出健康的行为,意味着网络确实学到了东西。然而,真正的考验是通过视觉评估结果:
图 8.3 – 原始图像(左),真实标签掩膜(中),预测掩膜(右)
在前面的图像中,我们可以看到,网络生成的掩膜跟真实标签分割的形状相符。然而,分割部分存在令人不满意的像素化效果,并且右上角有噪点。让我们看看另一个例子:
图 8.4 – 原始图像(左),真实标签掩膜(中),预测掩膜(右)
在上面的图像中,我们可以看到一个非常不完整、斑点状且总体质量较差的掩膜,这证明网络仍需要大量改进。这可以通过更多的微调和实验来实现。然而,在下一个食谱中,我们将发现一个更适合执行图像分割并能以更少的努力产生真正好掩膜的网络。
我们将在*它是如何工作的…*部分讨论我们刚刚做的事情。
它是如何工作的…
在本食谱中,我们实现了一个FCN用于图像分割。尽管我们将一个广为人知的架构VGG16进行了调整以适应我们的需求,实际上,FCN有许多不同的变种,它们扩展或修改了其他重要的架构,如ResNet50、DenseNet以及其他VGG的变体。
我们需要记住的是,UpSampling2D()配合双线性插值或ConvTranspose2D()的使用。最终的结果是,我们不再用一个输出的概率向量来对整个图像进行分类,而是生成一个与输入图像相同尺寸的输出体积,其中每个像素包含它可能属于的各个类别的概率分布。这种像素级的预测分割掩膜就被称为预测分割掩膜。
另见
你可以了解更多关于Oxford-IIIT Pet Dataset的信息,访问官方站点:www.robots.ox.ac.uk/~vgg/data/pets/。
从零开始实现 U-Net
要谈论图像分割,不能不提到U-Net,它是像素级分类的经典架构之一。
U-Net是一个由编码器和解码器组成的复合网络,正如其名,网络层以 U 形排列。它旨在快速且精确地进行分割,在本食谱中,我们将从零开始实现一个。
让我们开始吧,怎么样?
准备工作
在本例中,我们将依赖几个外部库,如 TensorFlow Datasets、TensorFlow Docs、Pillow和OpenCV。好消息是,我们可以通过pip轻松安装它们。首先,安装tensorflow_docs,如下所示:
$> pip install git+https://github.com/tensorflow/docs
接下来,安装其余的库:
$> pip install tensorflow-datasets Pillow opencv-contrib-python
在本食谱中,我们将使用Oxford-IIIT Pet Dataset。不过,现阶段我们不需要做任何事情,因为我们将通过tensorflow-datasets下载并操作它。在这个数据集中,分割掩膜(一个图像,每个位置包含原始图像中相应像素的类别)包含分类为三类的像素:
-
1: 像素属于宠物(猫或狗)。
-
2: 像素属于宠物的轮廓。
-
3: 像素属于周围环境。
以下是数据集中的一些示例图像:
图 8.5 – 来自 Oxford-IIIT Pet 数据集的示例图像
太好了!让我们开始实现吧!
如何实现…
按照以下步骤实现您自己的U-Net,这样您就可以对自己宠物的图像进行分割:
-
让我们导入所有必需的依赖项:
import cv2 import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_datasets as tfdata import tensorflow_docs as tfdocs import tensorflow_docs.plots from tensorflow.keras.layers import * from tensorflow.keras.losses import \ SparseCategoricalCrossentropy from tensorflow.keras.models import * from tensorflow.keras.optimizers import RMSprop -
定义
tf.data.experimental.AUTOTUNE的别名:AUTOTUNE = tf.data.experimental.AUTOTUNE -
定义一个函数,用于归一化数据集中的图像。我们还需要归一化掩膜,使得类别编号从 0 到 2,而不是从 1 到 3:
def normalize(input_image, input_mask): input_image = tf.cast(input_image, tf.float32) / 255.0 input_mask -= 1 return input_image, input_mask -
定义一个函数,根据 TensorFlow 数据集结构中的元素加载图像。请注意,我们将图像和掩膜的大小调整为256x256。此外,如果
train标志设置为True,我们会通过随机镜像图像及其掩膜来进行数据增强。最后,我们对输入进行归一化处理:@tf.function def load_image(dataset_element, train=True): input_image = tf.image.resize(dataset element['image'], (256, 256)) input_mask = tf.image.resize( dataset_element['segmentation_mask'],(256, 256)) if train and np.random.uniform() > 0.5: input_image = tf.image.flip_left_right(input_image) input_mask = tf.image.flip_left_right(input_mask) input_image, input_mask = normalize(input_image, input_mask) return input_image, input_mask -
现在,让我们定义一个
UNet()类,它将包含构建、训练和评估所需的所有逻辑,使用RMSProp作为优化器,SparseCategoricalCrossentropy作为损失函数。请注意,output_channels默认为3,因为每个像素可以被分类为三类之一。 -
现在,让我们定义
_downsample()助手方法,用于构建下采样块。它是一个卷积层,可以(可选地)进行批量归一化,并通过LeakyReLU激活:@staticmethod def _downsample(filters, size, batch_norm=True): initializer = tf.random_normal_initializer(0.0, 0.02) layers = Sequential() layers.add(Conv2D(filters=filters, kernel_size=size, strides=2, padding='same', kernel_initializer=initializer, use_bias=False)) if batch_norm: layers.add(BatchNormalization()) layers.add(LeakyReLU()) return layers -
相反,
_upsample()助手方法通过转置卷积扩展其输入,该卷积也进行批量归一化,并通过ReLU激活(可选地,我们可以添加一个 dropout 层来防止过拟合):def _upsample(filters, size, dropout=False): init = tf.random_normal_initializer(0.0, 0.02) layers = Sequential() layers.add(Conv2DTranspose(filters=filters, kernel_size=size, strides=2, padding='same', kernel_initializer=init, use_bias=False)) layers.add(BatchNormalization()) if dropout: layers.add(Dropout(rate=0.5)) layers.add(ReLU()) return layers -
凭借
_downsample()和_upsample(),我们可以迭代地构建完整的U-Net架构。网络的编码部分只是一个下采样块的堆叠,而解码部分则如预期那样,由一系列上采样块组成:def _create_model(self): down_stack = [self._downsample(64, 4, batch_norm=False)] for filters in (128, 256, 512, 512, 512, 512, 512): down_block = self._downsample(filters, 4) down_stack.append(down_block) up_stack = [] for _ in range(3): up_block = self._upsample(512, 4, dropout=True) up_stack.append(up_block) for filters in (512, 256, 128, 64): up_block = self._upsample(filters, 4) up_stack.append(up_block) -
为了防止网络出现梯度消失问题(即深度网络遗忘已学内容的现象),我们必须在每个层级添加跳跃连接:
inputs = Input(shape=self.input_size) x = inputs skip_layers = [] for down in down_stack: x = down(x) skip_layers.append(x) skip_layers = reversed(skip_layers[:-1]) for up, skip_connection in zip(up_stack, skip_layers): x = up(x) x = Concatenate()([x, skip_connection])U-Net的输出层是一个转置卷积,其尺寸与输入图像相同,但它的通道数与分割掩膜中的类别数相同:
init = tf.random_normal_initializer(0.0, 0.02) output = Conv2DTranspose( filters=self.output_channels, kernel_size=3, strides=2, padding='same', kernel_initializer=init)(x) return Model(inputs, outputs=output) -
让我们定义一个
helper方法,用于绘制相关的训练曲线:@staticmethod def _plot_model_history(model_history, metric, ylim=None): plt.style.use('seaborn-darkgrid') plotter = tfdocs.plots.HistoryPlotter() plotter.plot({'Model': model_history}, metric=metric) plt.title(f'{metric.upper()}') if ylim is None: plt.ylim([0, 1]) else: plt.ylim(ylim) plt.savefig(f'{metric}.png') plt.close() -
train()方法接受训练和验证数据集,以及进行训练所需的轮次、训练和验证步数。它还会将损失和准确率图保存到磁盘,以供后续分析:def train(self, train_dataset, epochs, steps_per_epoch, validation_dataset, validation_steps): hist = \ self.model.fit(train_dataset, epochs=epochs, steps_per_epoch=steps_per_epoch, validation_steps=validation_steps, validation_data=validation_dataset) self._plot_model_history(hist, 'loss', [0., 2.0]) self._plot_model_history(hist, 'accuracy') -
定义一个名为
_process_mask()的助手方法,用于将分割掩膜与 OpenCV 兼容。此函数的作用是创建一个三通道的灰度掩膜版本,并将类别值扩大到[0, 255]的范围:@staticmethod def _process_mask(mask): mask = (mask.numpy() * 127.5).astype('uint8') mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) return mask -
_save_image_and_masks()助手方法会创建一个由原始图像、真实掩膜和预测分割掩膜组成的马赛克,并将其保存到磁盘,供以后修订:def _save_image_and_masks(self, image, ground_truth_mask, prediction_mask, image_id): image = (image.numpy() * 255.0).astype('uint8') gt_mask = self._process_mask(ground_truth_mask) pred_mask = self._process_mask(prediction_mask) mosaic = np.hstack([image, gt_mask, pred_mask]) mosaic = cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR) cv2.imwrite(f'mosaic_{image_id}.jpg', mosaic) -
为了将网络产生的输出体积传递到有效的分割掩膜,我们必须获取每个像素位置上最高值的索引,这对应于该像素最可能的类别。
_create_mask()方法执行了这个操作:@staticmethod def _create_mask(prediction_mask): prediction_mask = tf.argmax(prediction_mask, axis=-1) prediction_mask = prediction_mask[...,tf.newaxis] return prediction_mask[0]_save_predictions()方法使用了我们在步骤 13中定义的_save_image_and_mask()辅助方法:def _save_predictions(self, dataset, sample_size=1): for id, (image, mask) in \ enumerate(dataset.take(sample_size), start=1): pred_mask = self.model.predict(image) pred_mask = self._create_mask(pred_mask) image = image[0] ground_truth_mask = mask[0] self._save_image_and_masks(image, ground_truth_mask, pred_mask, image_id=id) -
evaluate()方法计算U-Net在测试集上的准确度,并为一些图像样本生成预测,之后将其存储到磁盘上:def evaluate(self, test_dataset, sample_size=5): result = self.model.evaluate(test_dataset) print(f'Accuracy: {result[1] * 100:.2f}%') self._save_predictions(test_dataset, sample_size) -
使用 TensorFlow Datasets 下载(或加载,如果已缓存)
Oxford IIIT Pet Dataset及其元数据:dataset, info = tfdata.load('oxford_iiit_pet', with_info=True) -
使用元数据来定义网络在训练和验证数据集上将进行的相应步数。还需定义批量和缓冲区大小:
TRAIN_SIZE = info.splits['train'].num_examples VALIDATION_SIZE = info.splits['test'].num_examples BATCH_SIZE = 64 STEPS_PER_EPOCH = TRAIN_SIZE // BATCH_SIZE VALIDATION_SUBSPLITS = 5 VALIDATION_STEPS = VALIDATION_SIZE // BATCH_SIZE VALIDATION_STEPS //= VALIDATION_SUBSPLITS BUFFER_SIZE = 1000 -
定义训练和测试数据集的管道:
train_dataset = (dataset['train'] .map(load_image, num_parallel_ calls=AUTOTUNE) .cache() .shuffle(BUFFER_SIZE) .batch(BATCH_SIZE) .repeat() .prefetch(buffer_size=AUTOTUNE)) test_dataset = (dataset['test'] .map(lambda d: load_image(d, train=False), num_parallel_calls=AUTOTUNE) .batch(BATCH_SIZE)) -
实例化U-Net并训练 50 个 epoch:
unet = UNet() unet.train(train_dataset, epochs=50, steps_per_epoch=STEPS_PER_EPOCH, validation_steps=VALIDATION_STEPS, validation_dataset=test_dataset) -
最后,在测试数据集上评估网络:
unet.evaluate(test_dataset)测试集上的准确度应该在 83%左右(在我的情况下,我得到了 83.49%):
图 8.6 – 训练和验证准确度曲线
在这里,我们可以看到,大约在第 12 个 epoch 之后,训练准确度曲线和验证准确度曲线之间的差距开始慢慢扩大。这不是过拟合的表现,而是表明我们可以做得更好。那么,这个准确度是如何转化为实际图像的呢?
看一下下面的图片,展示了原始图像、地面真实掩膜和生成的掩膜:
图 8.7 – 原始图像(左侧)、地面真实掩膜(中间)和预测掩膜(右侧)
在这里,我们可以看到,地面真实掩膜(中间)和预测掩膜(右侧)之间有很好的相似性,尽管存在一些噪声,比如小的白色区域和狗轮廓下半部分明显的隆起,这些噪声通过更多的训练可以清理掉:
图 8.8 – 原始图像(左侧)、地面真实掩膜(中间)和预测掩膜(右侧)
前面的图片清楚地表明,网络可以进行更多的训练或微调。这是因为尽管它正确地获取了狗的整体形状和位置,但掩膜中仍有太多噪声,导致其无法在实际应用中使用。
让我们前往*它是如何工作的……*部分,进一步连结各个环节。
它是如何工作的……
在这个示例中,我们从头开始实现并训练了一个U-Net,用于分割家庭宠物的身体和轮廓。正如我们所看到的,网络确实学到了东西,但仍然有改进的空间。
在多个领域中,语义分割图像内容的能力至关重要,例如在医学中,比知道是否存在病症(如恶性肿瘤)更重要的是确定病变的实际位置、形状和面积。U-Net首次亮相于生物医学领域。2015 年,它在使用远少于数据的情况下,超越了传统的分割方法,例如滑动窗口卷积网络。
U-Net是如何取得如此好结果的呢?正如我们在本食谱中所学到的,关键在于其端到端的结构,其中编码器和解码器都由卷积组成,形成一个收缩路径,其任务是捕捉上下文信息,还有一个对称的扩展路径,从而实现精确的定位。
上述两条路径的深度可以根据数据集的性质进行调整。这种深度定制之所以可行,是因为跳跃连接的存在,它允许梯度在网络中进一步流动,从而防止梯度消失问题(这与ResNet所做的类似,正如我们在第二章中学到的,执行图像分类)。
在下一个食谱中,我们将结合这个强大的概念与Oxford IIIT Pet Dataset的实现:迁移学习。
另见
一个很好的方法来熟悉Oxford IIIT Pet Dataset,可以访问官方网站:www.robots.ox.ac.uk/~vgg/data/pets/。
在本食谱中,我们提到过梯度消失问题几次,因此,阅读这篇文章以了解这一概念是个好主意:en.wikipedia.org/wiki/Vanishing_gradient_problem。
实现带迁移学习的 U-Net
从头开始训练一个U-Net是创建高性能图像分割系统的一个非常好的第一步。然而,深度学习在计算机视觉中的最大超能力之一就是能够在其他网络的知识基础上构建解决方案,这通常会带来更快且更好的结果。
图像分割也不例外,在本食谱中,我们将使用迁移学习来实现一个更好的分割网络。
让我们开始吧。
准备就绪
本食谱与前一个食谱(从头开始实现 U-Net)非常相似,因此我们只会深入讨论不同的部分。为了更深入的理解,我建议在尝试本食谱之前,先完成从头开始实现 U-Net的食谱。正如预期的那样,我们需要的库与之前相同,都可以通过pip安装。让我们首先安装tensorflow_docs,如下所示:
$> pip install git+https://github.com/tensorflow/docs
现在,让我们设置剩余的依赖项:
$> pip install tensorflow-datasets Pillow opencv-contrib-python
我们将再次使用Oxford-IIIT Pet Dataset,可以通过tensorflow-datasets访问。该数据集中的每个像素都属于以下类别之一:
-
1:该像素属于宠物(猫或狗)。
-
2:该像素属于宠物的轮廓。
-
3:该像素属于周围环境。
以下图片展示了数据集中的两张样本图像:
](tos-cn-i-73owjymdk6/a827a5fd92ba4c9a87c3bc7c583345e1)
图 8.9 – 来自 Oxford-IIIT Pet 数据集的样本图像
这样,我们就可以开始了!
如何实现……
完成以下步骤以实现一个基于转移学习的U-Net:
-
导入所有需要的包:
import cv2 import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_datasets as tfdata import tensorflow_docs as tfdocs import tensorflow_docs.plots from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import * from tensorflow.keras.losses import \ SparseCategoricalCrossentropy from tensorflow.keras.models import * from tensorflow.keras.optimizers import RMSprop -
为
tf.data.experimental.AUTOTUNE定义一个别名:AUTOTUNE = tf.data.experimental.AUTOTUNE -
定义一个函数,用于对数据集中的图像和掩码进行归一化处理:
def normalize(input_image, input_mask): input_image = tf.cast(input_image, tf.float32) / 255.0 input_mask -= 1 return input_image, input_mask -
定义一个函数,根据 TensorFlow Datasets 数据结构中的一个元素加载图像及其对应的掩码。可选地,函数可以对训练图像执行图像镜像操作:
@tf.function def load_image(dataset_element, train=True): input_image = tf.image.resize(dataset element['image'],(256, 256)) input_mask = tf.image.resize( dataset_element['segmentation_mask'], (256,256)) if train and np.random.uniform() > 0.5: input_image = tf.image.flip_left_right(input_ image) input_mask = tf.image.flip_left_right(input_mask) input_image, input_mask = normalize(input_image, input_mask) return input_image, input_mask -
定义
UNet(),这是一个容器类,包含了构建、训练和评估我们的转移学习辅助的RMSProp优化器和SparseCategoricalCrossentropy损失函数所需的逻辑。注意,output_channels默认值为3,因为每个像素可以归类为三种类别之一。编码器将使用预训练的MobileNetV2,但我们只会使用其中一部分层,这些层在self.target_layers中定义。 -
现在,我们来定义
_upsample()辅助方法,构建一个上采样模块:@staticmethod def _upsample(filters, size, dropout=False): init = tf.random_normal_initializer(0.0, 0.02) layers = Sequential() layers.add(Conv2DTranspose(filters=filters, kernel_size=size, strides=2, padding='same', kernel_initializer=init, use_bias=False)) layers.add(BatchNormalization()) if dropout: layers.add(Dropout(rate=0.5)) layers.add(ReLU()) return layers -
利用我们预训练的
MobileNetV2和_upsample()方法,我们可以逐步构建完整的self.target_layers,这些层被冻结(down_stack.trainable = False),这意味着我们只训练解码器或上采样模块:def _create_model(self): layers = [self.pretrained_model.get_layer(l).output for l in self.target_layers] down_stack = Model(inputs=self.pretrained_model. input, outputs=layers) down_stack.trainable = False up_stack = [] for filters in (512, 256, 128, 64): up_block = self._upsample(filters, 4) up_stack.append(up_block) -
现在,我们可以添加跳跃连接,以促进梯度在网络中的流动:
inputs = Input(shape=self.input_size) x = inputs skip_layers = down_stack(x) x = skip_layers[-1] skip_layers = reversed(skip_layers[:-1]) for up, skip_connection in zip(up_stack, skip_layers): x = up(x) x = Concatenate()([x, skip_connection]) -
U-Net的输出层是一个转置卷积,其尺寸与输入图像相同,但通道数与分割掩码中的类别数相同:
init = tf.random_normal_initializer(0.0, 0.02) output = Conv2DTranspose( filters=self.output_channels, kernel_size=3, strides=2, padding='same', kernel_initializer=init)(x) return Model(inputs, outputs=output) -
定义
_plot_model_history(),一个辅助方法,用于绘制相关的训练曲线:@staticmethod def _plot_model_history(model_history, metric, ylim=None): plt.style.use('seaborn-darkgrid') plotter = tfdocs.plots.HistoryPlotter() plotter.plot({'Model': model_history}, metric=metric) plt.title(f'{metric.upper()}') if ylim is None: plt.ylim([0, 1]) else: plt.ylim(ylim) plt.savefig(f'{metric}.png') plt.close() -
定义
train()方法,负责拟合模型:def train(self, train_dataset, epochs, steps_per_epoch, validation_dataset, validation_steps): hist = \ self.model.fit(train_dataset, epochs=epochs, steps_per_epoch=steps_per_epoch, validation_steps=validation_steps, validation_data=validation_dataset) self._plot_model_history(hist, 'loss', [0., 2.0]) self._plot_model_history(hist, 'accuracy') -
定义
_process_mask(),一个辅助方法,使分割掩码与 OpenCV 兼容:@staticmethod def _process_mask(mask): mask = (mask.numpy() * 127.5).astype('uint8') mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) return mask -
定义
_save_image_and_masks()辅助方法,用于创建原始图像的可视化,以及真实和预测的掩码:def _save_image_and_masks(self, image, ground_truth_mask, prediction_mask, image_id): image = (image.numpy() * 255.0).astype('uint8') gt_mask = self._process_mask(ground_truth_mask) pred_mask = self._process_mask(prediction_mask) mosaic = np.hstack([image, gt_mask, pred_mask]) mosaic = cv2.cvtColor(mosaic, cv2.COLOR_RGB2BGR) cv2.imwrite(f'mosaic_{image_id}.jpg', mosaic) -
定义
_create_mask(),该方法根据网络的预测生成有效的分割掩码:@staticmethod def _create_mask(prediction_mask): prediction_mask = tf.argmax(prediction_mask, axis=-1) prediction_mask = prediction_mask[..., tf.newaxis] return prediction_mask[0] -
_save_predictions()方法使用了我们在第 13 步中定义的_save_image_and_mask()辅助方法:def _save_predictions(self, dataset, sample_size=1): for id, (image, mask) in \ enumerate(dataset.take(sample_size), start=1): pred_mask = self.model.predict(image) pred_mask = self._create_mask(pred_mask) image = image[0] ground_truth_mask = mask[0] self._save_image_and_masks(image, ground_truth_mask, pred_mask, image_id=id) -
evaluate()方法计算U-Net在测试集上的准确度,并生成一组图像样本的预测。预测结果随后会被存储到磁盘上:def evaluate(self, test_dataset, sample_size=5): result = self.model.evaluate(test_dataset) print(f'Accuracy: {result[1] * 100:.2f}%') self._save_predictions(test_dataset, sample_size) -
使用 TensorFlow Datasets 下载(或加载缓存的)
Oxford IIIT Pet Dataset及其元数据:dataset, info = tfdata.load('oxford_iiit_pet', with_info=True) -
使用元数据定义网络在训练和验证数据集上将执行的步骤数。还要定义批量大小和缓存大小:
TRAIN_SIZE = info.splits['train'].num_examples VALIDATION_SIZE = info.splits['test'].num_examples BATCH_SIZE = 64 STEPS_PER_EPOCH = TRAIN_SIZE // BATCH_SIZE VALIDATION_SUBSPLITS = 5 VALIDATION_STEPS = VALIDATION_SIZE // BATCH_SIZE VALIDATION_STEPS //= VALIDATION_SUBSPLITS BUFFER_SIZE = 1000 -
定义训练和测试数据集的管道:
train_dataset = (dataset['train'] .map(load_image, num_parallel_ calls=AUTOTUNE) .cache() .shuffle(BUFFER_SIZE) .batch(BATCH_SIZE) .repeat() .prefetch(buffer_size=AUTOTUNE)) test_dataset = (dataset['test'] .map(lambda d: load_image(d, train=False), num_parallel_calls=AUTOTUNE) .batch(BATCH_SIZE)) -
实例化U-Net并训练它 30 个周期:
unet = UNet() unet.train(train_dataset, epochs=50, steps_per_epoch=STEPS_PER_EPOCH, validation_steps=VALIDATION_STEPS, validation_dataset=test_dataset) -
在测试数据集上评估网络:
unet.evaluate(test_dataset)在测试集上的准确率应该接近 90%(在我的案例中,我得到了 90.78%的准确率):
图 8.10 – 训练和验证准确率曲线
准确率曲线显示,网络没有发生过拟合,因为训练和验证图表遵循相同的轨迹,且差距非常小。这也确认了模型所获得的知识是可迁移的,并且可以用于未见过的数据。
让我们看一下网络的一些输出,从以下图像开始:
图 8.11 – 原始图像(左)、真实标签掩码(中)和预测掩码(右)
与图 8.7中从头实现 U-Net一节中的结果相比,在前面的图像中,我们可以看到U-Net产生了一个更干净的结果,背景(灰色像素)、轮廓(白色像素)和宠物(黑色像素)被清晰地分开,并且几乎与真实标签掩码(中)完全相同:
图 8.12 – 原始图像(左)、真实标签掩码(中)和预测掩码(右)
与从头实现 U-Net一节中的图 8.8相比,前面的图像是一个显著的改进。这次,预测掩码(右),虽然不是完美的,但呈现出更少的噪声,并且更接近实际的分割掩码(中)。
我们将在*它是如何工作的…*一节中深入探讨。
它是如何工作的……
在这个例子中,我们对在庞大的ImageNet数据集上训练的MobileNetV2做了一个小但重要的改动。
迁移学习在这个场景中效果如此出色的原因是,ImageNet中有成百上千的类别,专注于不同品种的猫和狗,这意味着与Oxford IIIT Pet数据集的重叠非常大。然而,如果情况并非如此,这并不意味着我们应该完全放弃迁移学习!我们在这种情况下应该做的是通过使编码器的某些(或全部)层可训练,来微调它。
通过利用MobileNetV2中编码的知识,我们将测试集上的准确率从 83%提升到了 90%,这是一项令人印象深刻的提升,带来了更好、更清晰的预测掩码,即使是在具有挑战性的例子上。
另见
你可以阅读原始的Oxford IIIT Pet Dataset,请访问www.robots.ox.ac.uk/~vgg/data/pets/。想了解如何解决梯度消失问题,请阅读这篇文章:en.wikipedia.org/wiki/Vanishing_gradient_problem。
使用 Mask-RCNN 和 TensorFlow Hub 进行图像分割
COCO数据集。这将帮助我们进行开箱即用的物体检测和图像分割。
准备工作
首先,我们必须安装Pillow和TFHub,如下所示:
$> pip install Pillow tensorflow-hub
我们还需要将cd安装到你选择的位置,并克隆tensorflow/models仓库:
$> git clone –-depth 1 https://github.com/tensorflow/models
接下来,安装TensorFlow 对象检测 API,方法如下:
$> sudo apt install -y protobuf-compiler
$> cd models/research
$> protoc object_detection/protos/*.proto --python_out=.
$> cp object_detection/packages/tf2/setup.py .
$> python -m pip install -q .
就这样!让我们开始吧。
如何做到这一点…
按照以下步骤学习如何使用Mask-RCNN进行图像分割:
-
导入必要的包:
import glob from io import BytesIO import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_hub as hub from PIL import Image from object_detection.utils import ops from object_detection.utils import visualization_utils as viz from object_detection.utils.label_map_util import \ create_category_index_from_labelmap -
定义一个函数,将图像加载到 NumPy 数组中:
def load_image(path): image_data = tf.io.gfile.GFile(path, 'rb').read() image = Image.open(BytesIO(image_data)) width, height = image.size shape = (1, height, width, 3) image = np.array(image.getdata()) image = image.reshape(shape).astype('uint8') return image -
定义一个函数,使用Mask-RCNN进行预测,并将结果保存到磁盘。首先加载图像并将其输入到模型中:
def get_and_save_predictions(model, image_path): image = load_image(image_path) results = model(image) -
将结果转换为
NumPy数组:model_output = {k: v.numpy() for k, v in results.items()} -
从模型输出中提取检测掩膜和框,并将它们转换为张量:
detection_masks = model_output['detection_masks'][0] detection_masks = tf.convert_to_tensor(detection_masks) detection_boxes = model_output['detection_boxes'][0] detection_boxes = tf.convert_to_tensor(detection_boxes) -
将框掩膜转换为图像掩膜:
detection_masks_reframed = \ ops.reframe_box_masks_to_image_masks(detection_ masks,detection_boxes, image.shape[1], image.shape[2]) detection_masks_reframed = \ tf.cast(detection_masks_reframed > 0.5, tf.uint8) model_output['detection_masks_reframed'] = \ detection_masks_reframed.numpy() -
创建一个可视化图,显示检测结果及其框、得分、类别和掩膜:
boxes = model_output['detection_boxes'][0] classes = \ model_output['detection_classes'][0].astype('int') scores = model_output['detection_scores'][0] masks = model_output['detection_masks_reframed'] image_with_mask = image.copy() viz.visualize_boxes_and_labels_on_image_array( image=image_with_mask[0], boxes=boxes, classes=classes, scores=scores, category_index=CATEGORY_IDX, use_normalized_coordinates=True, max_boxes_to_draw=200, min_score_thresh=0.30, agnostic_mode=False, instance_masks=masks, line_thickness=5 ) -
将结果保存到磁盘:
plt.figure(figsize=(24, 32)) plt.imshow(image_with_mask[0]) plt.savefig(f'output/{image_path.split("/")[-1]}') -
加载
COCO数据集的类别索引:labels_path = 'resources/mscoco_label_map.pbtxt' CATEGORY_IDX =create_category_index_from_labelmap(labels_path) -
从TFHub加载Mask-RCNN:
MODEL_PATH = ('https://tfhub.dev/tensorflow/mask_rcnn/' 'inception_resnet_v2_1024x1024/1') mask_rcnn = hub.load(MODEL_PATH) -
运行
output文件夹。让我们回顾一个简单的例子:
图 8.13– 单实例分割
在这里,我们可以看到网络正确地检测并分割出了狗,准确率为 100%!我们来尝试一个更具挑战性的例子:
图 8.14 – 多实例分割
这张图片比之前的更拥挤,尽管如此,网络仍然正确地识别出了场景中的大部分物体(如汽车、人、卡车等)——即使是被遮挡的物体!然而,模型在某些情况下仍然失败,如下图所示:
图 8.15 – 带有错误和冗余的分割
这次,网络正确地识别了我和我的狗,还有咖啡杯和沙发,但它却出现了重复和荒谬的检测结果,比如我的腿被识别为一个人。这是因为我抱着我的狗,照片中我的身体部分被分离,导致了不正确或置信度低的分割。
让我们继续下一个部分。
它是如何工作的…
在这篇教程中,我们学习了如何使用现有最强大的神经网络之一:Mask-RCNN,来检测物体并执行图像分割。训练这样的模型并非易事,更不用说从零开始实现了!幸运的是,得益于TensorFlow Hub,我们能够通过仅几行代码使用它的全部预测能力。
我们必须考虑到,这个预训练模型在包含网络已训练过的物体的图像上表现最好。更具体地说,我们传递给COCO的图像越多,结果越好。不过,为了实现最佳检测效果,仍然需要一定程度的调整和实验,因为正如我们在前面的例子中看到的,虽然网络非常强大,但并不完美。
另见
您可以在这里了解我们使用的模型:tfhub.dev/tensorflow/mask_rcnn/inception_resnet_v2_1024x1024/1。此外,阅读Mask-RCNN的论文也是一个明智的决定:arxiv.org/abs/1703.06870。
第九章:第九章:通过目标检测在图像中定位元素
目标检测是计算机视觉中最常见但最具挑战性的任务之一。它是图像分类的自然演变,我们的目标是识别图像中的内容。另一方面,目标检测不仅关注图像的内容,还关注数字图像中感兴趣元素的位置。
与计算机视觉中的许多其他知名任务一样,目标检测已经通过各种技术得到解决,从简单的解决方案(如目标匹配)到基于机器学习的解决方案(如 Haar 级联)。尽管如此,如今最有效的检测器都由深度学习驱动。
从零开始实现最先进的目标检测器(如YOLO(一次看全)(YOLO)和快速区域卷积神经网络(Fast R-CNN))是一个非常具有挑战性的任务。然而,我们可以利用许多预训练的解决方案,不仅可以进行预测,还可以从零开始训练我们自己的模型,正如本章所介绍的那样。
这里列出了我们将要快速处理的配方:
-
使用图像金字塔和滑动窗口创建目标检测器
-
使用 YOLOv3 进行目标检测
-
使用 TensorFlow 的目标检测应用程序编程接口(API)训练你自己的目标检测器
-
使用TensorFlow Hub(TFHub)进行目标检测
技术要求
鉴于目标检测器的复杂性,拥有图形处理单元(GPU)是个不错的选择。你可以使用许多云服务商来运行本章中的配方,我个人最喜欢的是 FloydHub,但你可以使用你最喜欢的任何服务!当然,如果你不想有意外费用,记得关注费用问题!在准备工作部分,你将找到每个配方的准备步骤。本章的代码可以在github.com/PacktPublishing/Tensorflow-2.0-Computer-Vision-Cookbook/tree/master/ch9上找到。
查看以下链接,观看代码实战视频:
使用图像金字塔和滑动窗口创建目标检测器
传统上,目标检测器通过一种迭代算法工作,该算法将窗口以不同的尺度滑过图像,以检测每个位置和视角下的潜在目标。尽管这种方法因其明显的缺点(我们将在*工作原理...*部分中进一步讨论)而已过时,但它的一个重要优点是它对我们使用的图像分类器类型没有偏见,这意味着我们可以将其作为一个框架,将任何分类器转变为目标检测器。这正是我们在第一个配方中所做的!
让我们开始吧。
准备工作
我们需要安装一些外部库,比如OpenCV、Pillow和imutils,可以通过以下命令轻松完成:
$> pip install opencv-contrib-python Pillow imutils
我们将使用一个预训练模型来为我们的物体检测器提供支持,因此我们不需要为此食谱提供任何数据。
如何实现…
按照以下步骤完成食谱:
-
导入必要的依赖项:
import cv2 import imutils import numpy as np from tensorflow.keras.applications import imagenet_utils from tensorflow.keras.applications.inception_resnet_v2 \ import * from tensorflow.keras.preprocessing.image import img_to_array -
接下来,定义我们的
ObjectDetector()类,从构造函数开始:class ObjectDetector(object): def __init__(self, classifier, preprocess_fn=lambda x: x, input_size=(299, 299), confidence=0.98, window_step_size=16, pyramid_scale=1.5, roi_size=(200, 150), nms_threshold=0.3): self.classifier = classifier self.preprocess_fn = preprocess_fn self.input_size = input_size self.confidence = confidence self.window_step_size = window_step_size self.pyramid_scale = pyramid_scale self.roi_size = roi_size self.nms_threshold = nms_thresholdclassifier只是一个经过训练的网络,我们将用它来分类每个窗口,而preprocess_fn是用于处理每个窗口的函数,在将其传递给分类器之前进行处理。confidence是我们允许检测结果的最低概率,只有达到这个概率才能认为检测结果有效。剩余的参数将在下一步中解释。 -
现在,我们定义一个
sliding_window()方法,该方法提取输入图像的部分区域,尺寸等于self.roi_size。它将在图像上水平和垂直滑动,每次移动self.window_step_size像素(注意使用了yield而不是return——这是因为它是一个生成器):def sliding_window(self, image): for y in range(0, image.shape[0], self.window_step_size): for x in range(0, image.shape[1], self.window_step_size): y_slice = slice(y, y + self.roi_size[1], 1) x_slice = slice(x, x + self.roi_size[0], 1) yield x, y, image[y_slice, x_slice] -
接下来,定义
pyramid()方法,该方法会生成输入图像的越来越小的副本,直到达到最小尺寸(类似于金字塔的各个层级):def pyramid(self, image): yield image while True: width = int(image.shape[1] / self.pyramid_scale) image = imutils.resize(image, width=width) if (image.shape[0] < self.roi_size[1] or image.shape[1] < self.roi_size[0]): break yield image -
因为在不同尺度上滑动窗口会很容易产生与同一物体相关的多个检测结果,我们需要一种方法来将重复项保持在最低限度。这就是我们下一个方法
non_max_suppression()的作用:def non_max_suppression(self, boxes, probabilities): if len(boxes) == 0: return [] if boxes.dtype.kind == 'i': boxes = boxes.astype(np.float) pick = [] x_1 = boxes[:, 0] y_1 = boxes[:, 1] x_2 = boxes[:, 2] y_2 = boxes[:, 3] area = (x_2 - x_1 + 1) * (y_2 - y_1 + 1) indexes = np.argsort(probabilities) -
我们首先计算所有边界框的面积,并按概率升序对它们进行排序。接下来,我们将选择具有最高概率的边界框的索引,并将其添加到最终选择中(
pick),直到剩下indexes个边界框需要进行修剪:while len(indexes) > 0: last = len(indexes) - 1 i = indexes[last] pick.append(i) -
我们计算选中的边界框与其他边界框之间的重叠部分,然后剔除那些重叠部分超过
self.nms_threshold的框,这意味着它们很可能指的是同一个物体:xx_1 = np.maximum(x_1[i],x_1[indexes[:last]]) yy_1 = np.maximum(y_1[i],y_1[indexes[:last]]) xx_2 = np.maximum(x_2[i],x_2[indexes[:last]]) yy_2 = np.maximum(y_2[i],y_2[indexes[:last]]) width = np.maximum(0, xx_2 - xx_1 + 1) height = np.maximum(0, yy_2 - yy_1 + 1) overlap = (width * height) / area[indexes[:last]] redundant_boxes = \ np.where(overlap > self.nms_threshold)[0] to_delete = np.concatenate( ([last], redundant_boxes)) indexes = np.delete(indexes, to_delete) -
返回选中的边界框:
return boxes[pick].astype(np.int) -
detect()方法将物体检测算法串联在一起。我们首先定义一个rois列表及其对应的locations(在原始图像中的坐标):def detect(self, image): rois = [] locations = [] -
接下来,我们将使用
pyramid()生成器在多个尺度上生成输入图像的不同副本,并在每个层级上,我们将通过sliding_windows()生成器滑动窗口,提取所有可能的 ROI:for img in self.pyramid(image): scale = image.shape[1] / float(img.shape[1]) for x, y, roi_original in \ self.sliding_window(img): x = int(x * scale) y = int(y * scale) w = int(self.roi_size[0] * scale) h = int(self.roi_size[1] * scale) roi = cv2.resize(roi_original, self.input_size) roi = img_to_array(roi) roi = self.preprocess_fn(roi) rois.append(roi) locations.append((x, y, x + w, y + h)) rois = np.array(rois, dtype=np.float32) -
一次性通过分类器传递所有的 ROI:
predictions = self.classifier.predict(rois) predictions = \ imagenet_utils.decode_predictions(predictions, top=1) -
构建一个
dict来将分类器生成的每个标签映射到所有的边界框及其概率(注意我们只保留那些概率至少为self.confidence的边界框):labels = {} for i, pred in enumerate(predictions): _, label, proba = pred[0] if proba >= self.confidence: box = locations[i] label_detections = labels.get(label, []) label_detections.append({'box': box, 'proba': proba}) labels[label] = label_detections return labels -
实例化一个在 ImageNet 上训练的
InceptionResnetV2网络,作为我们的分类器,并将其传递给新的ObjectDetector。注意,我们还将preprocess_function作为输入传递:model = InceptionResNetV2(weights='imagenet', include_top=True) object_detector = ObjectDetector(model, preprocess_input) -
加载输入图像,将其最大宽度调整为 600 像素(高度将相应计算以保持宽高比),并通过物体检测器进行处理:
image = cv2.imread('dog.jpg') image = imutils.resize(image, width=600) labels = object_detector.detect(image) -
遍历所有对应每个标签的检测结果,首先绘制所有边界框:
GREEN = (0, 255, 0) for i, label in enumerate(labels.keys()): clone = image.copy() for detection in labels[label]: box = detection['box'] probability = detection['proba'] x_start, y_start, x_end, y_end = box cv2.rectangle(clone, (x_start, y_start), (x_end, y_end), (0, 255, 0), 2) cv2.imwrite(f'Before_{i}.jpg', clone)然后,使用非最大抑制(NMS)去除重复项,并绘制剩余的边界框:
clone = image.copy() boxes = np.array([d['box'] for d in labels[label]]) probas = np.array([d['proba'] for d in labels[label]]) boxes = object_detector.non_max_suppression(boxes, probas) for x_start, y_start, x_end, y_end in boxes: cv2.rectangle(clone, (x_start, y_start), (x_end, y_end), GREEN, 2) if y_start - 10 > 10: y = y_start - 10 else: y = y_start + 10 cv2.putText(clone, label, (x_start, y), cv2.FONT_HERSHEY_SIMPLEX, .45, GREEN, 2) cv2.imwrite(f'After_{i}.jpg', clone)这是没有应用 NMS 的结果:
图 9.1 – 同一只狗的重叠检测
这是应用 NMS 后的结果:
图 9.2 – 使用 NMS 后,我们去除了冗余的检测
尽管我们在前面的照片中成功检测到了狗,但我们注意到边界框并没有像我们预期的那样紧密包裹住物体。让我们在接下来的章节中讨论这个问题以及传统物体检测方法的其他问题。
它是如何工作的…
在这个方案中,我们实现了一个可重用的类,利用迭代方法在不同的视角层次(图像金字塔)提取 ROI(滑动窗口),并将其传递给图像分类器,从而确定照片中物体的位置和类别。我们还使用了非最大抑制(NMS)来减少这种策略所特有的冗余和重复检测。
尽管这是创建对象检测器的一个很好的初步尝试,但它仍然存在一些缺陷:
-
它非常慢,这使得它在实时场景中不可用。
-
边界框的准确性很大程度上取决于图像金字塔、滑动窗口和 ROI 大小的参数选择。
-
该架构不是端到端可训练的,这意味着边界框预测中的误差不会通过网络反向传播,以便通过更新权重来产生更好、更准确的检测结果。相反,我们只能使用预训练模型,这些模型仅限于推断,而无法学习,因为框架不允许它们学习。
然而,别急着排除这种方法!如果你处理的图像在尺寸和视角上变化很小,且你的应用程序绝对不在实时环境中运行,那么本方案中实现的策略可能会对你的项目大有裨益!
另见
你可以在这里阅读更多关于 NMS 的内容:
towardsdatascience.com/non-maximum-suppression-nms-93ce178e177c
使用 YOLOv3 检测物体
在使用图像金字塔和滑动窗口创建物体检测器的实例中,我们学会了如何通过将任何图像分类器嵌入到依赖于图像金字塔和滑动窗口的传统框架中,来将其转变为物体检测器。然而,我们也学到,这种方法并不理想,因为它无法让网络从错误中学习。
深度学习之所以在物体检测领域占据主导地位,是因为它的端到端方法。网络不仅能弄清楚如何对物体进行分类,还能发现如何生成最佳的边界框来定位图像中的每个元素。
基于这个端到端的策略,网络可以在一次遍历中检测到无数个物体!当然,这也使得这样的物体检测器极为高效!
YOLO 是开创性的端到端物体检测器之一,在这个实例中,我们将学习如何使用预训练的 YOLOv3 模型进行物体检测。
我们开始吧!
准备工作
首先安装tqdm,如下所示:
$> pip install tqdm
我们的实现深受精彩的keras-yolo3库的启发,该库由*Huynh Ngoc Anh(GitHub 上的 experiencor)*实现,你可以在这里查看:
github.com/experiencor/keras-yolo3
因为我们将使用预训练的 YOLO 模型,所以需要下载权重文件。它们可以在这里获取:pjreddie.com/media/files/yolov3.weights。在本教程中,我们假设这些权重文件位于伴随代码库中的ch9/recipe2/resources文件夹内,名为yolov3.weights。这些权重与 YOLO 的原作者使用的是相同的。更多关于 YOLO 的内容,请参考另见部分。
一切准备就绪!
如何操作…
按照以下步骤完成该实例:
-
首先导入相关的依赖:
import glob import json import struct import matplotlib.pyplot as plt import numpy as np import tqdm from matplotlib.patches import Rectangle from tensorflow.keras.layers import * from tensorflow.keras.models import * from tensorflow.keras.preprocessing.image import * -
定义一个
WeightReader()类,自动加载 YOLO 的权重,无论原作者使用了什么格式。请注意,这是一个非常底层的解决方案,但我们不需要完全理解它就可以加以利用。让我们从构造函数开始:class WeightReader: def __init__(self, weight_file): with open(weight_file, 'rb') as w_f: major, = struct.unpack('i', w_f.read(4)) minor, = struct.unpack('i', w_f.read(4)) revision, = struct.unpack('i', w_f.read(4)) if (major * 10 + minor) >= 2 and \ major < 1000 and \ minor < 1000: w_f.read(8) else: w_f.read(4) binary = w_f.read() self.offset = 0 self.all_weights = np.frombuffer(binary, dtype='float32') -
接下来,定义一个方法来从
weights文件中读取指定数量的字节:def read_bytes(self, size): self.offset = self.offset + size return self.all_weights[self.offset- size:self.offset] -
load_weights()方法加载了组成 YOLO 架构的 106 层每一层的权重:def load_weights(self, model): for i in tqdm.tqdm(range(106)): try: conv_layer = model.get_layer(f'conv_{i}') if i not in [81, 93, 105]: norm_layer = model.get_layer(f'bnorm_{i}') size = np.prod(norm_layer. get_weights()[0].shape) bias = self.read_bytes(size) scale = self.read_bytes(size) mean = self.read_bytes(size) var = self.read_bytes(size) norm_layer.set_weights([scale, bias, mean, var]) -
加载卷积层的权重:
if len(conv_layer.get_weights()) > 1: bias = self.read_bytes(np.prod( conv_layer.get_weights()[1].shape)) kernel = self.read_bytes(np.prod( conv_layer.get_weights()[0].shape)) kernel = kernel.reshape(list(reversed( conv_layer.get_weights()[0].shape))) kernel = kernel.transpose([2, 3, 1, 0]) conv_layer.set_weights([kernel, bias]) else: kernel = self.read_bytes(np.prod( conv_layer.get_weights()[0].shape)) kernel = kernel.reshape(list(reversed( conv_layer.get_weights()[0].shape))) kernel = kernel.transpose([2, 3, 1, 0]) conv_layer.set_weights([kernel]) except ValueError: pass -
定义一个方法来重置偏移量:
def reset(self): self.offset = 0 -
定义一个
BoundBox()类,封装边界框的顶点,以及该框中元素为物体的置信度(objness):class BoundBox(object): def __init__(self, x_min, y_min, x_max, y_max, objness=None, classes=None): self.xmin = x_min self.ymin = y_min self.xmax = x_max self.ymax = y_max self.objness = objness self.classes = classes self.label = -1 self.score = -1 def get_label(self): if self.label == -1: self.label = np.argmax(self.classes) return self.label def get_score(self): if self.score == -1: self.score = self.classes[self.get_label()] return self.score -
定义一个
YOLO()类,封装网络的构建和检测逻辑。让我们从构造函数开始:class YOLO(object): def __init__(self, weights_path, anchors_path='resources/anchors.json', labels_path='resources/coco_labels.txt', class_threshold=0.65): self.weights_path = weights_path self.model = self._load_yolo() self.labels = [] with open(labels_path, 'r') as f: for l in f: self.labels.append(l.strip()) with open(anchors_path, 'r') as f: self.anchors = json.load(f) self.class_threshold = class_thresholdYOLO 的输出是一组在锚框上下文中定义的编码边界框,这些锚框是由 YOLO 的作者精心挑选的。这是基于对
COCO数据集中物体大小的分析。因此,我们将锚框存储在self.anchors中,COCO的标签存储在self.labels中。此外,我们依赖于self._load_yolo()方法(稍后定义)来构建模型。 -
YOLO 由一系列卷积块和可选的跳跃连接组成。
_conv_block()辅助方法允许我们轻松地实例化这些块:def _conv_block(self, input, convolutions, skip=True): x = input count = 0 for conv in convolutions: if count == (len(convolutions) - 2) and skip: skip_connection = x count += 1 if conv['stride'] > 1: x = ZeroPadding2D(((1, 0), (1, 0)))(x) x = Conv2D(conv['filter'], conv['kernel'], strides=conv['stride'], padding=('valid' if conv['stride'] > 1 else 'same'), name=f'conv_{conv["layer_idx"]}', use_bias=(False if conv['bnorm'] else True))(x) -
检查是否需要添加批量归一化、leaky ReLU 激活和跳跃连接:
if conv['bnorm']: name = f'bnorm_{conv["layer_idx"]}' x = BatchNormalization(epsilon=1e-3, name=name)(x) if conv['leaky']: name = f'leaky_{conv["layer_idx"]}' x = LeakyReLU(alpha=0.1, name=name)(x) return Add()([skip_connection, x]) if skip else x -
_make_yolov3_architecture()方法,如下所示,通过堆叠一系列卷积块来构建 YOLO 网络,使用先前定义的_conv_block()方法:def _make_yolov3_architecture(self): input_image = Input(shape=(None, None, 3)) # Layer 0 => 4 x = self._conv_block(input_image, [ {'filter': 32, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 0}, {'filter': 64, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 1}, {'filter': 32, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 2}, {'filter': 64, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 3}]) ...因为这个方法比较大,请参考附带的代码库获取完整实现。
-
_load_yolo()方法创建架构、加载权重,并实例化一个 TensorFlow 可理解的训练过的 YOLO 模型:def _load_yolo(self): model = self._make_yolov3_architecture() weight_reader = WeightReader(self.weights_path) weight_reader.load_weights(model) model.save('model.h5') model = load_model('model.h5') return model -
定义一个静态方法来计算张量的 Sigmoid 值:
@staticmethod def _sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) -
_decode_net_output()方法解码 YOLO 产生的候选边界框和类别预测:def _decode_net_output(self, network_output, anchors, obj_thresh, network_height, network_width): grid_height, grid_width = network_output.shape[:2] nb_box = 3 network_output = network_output.reshape( (grid_height, grid_width, nb_box, -1)) boxes = [] network_output[..., :2] = \ self._sigmoid(network_output[..., :2]) network_output[..., 4:] = \ self._sigmoid(network_output[..., 4:]) network_output[..., 5:] = \ (network_output[..., 4][..., np.newaxis] * network_output[..., 5:]) network_output[..., 5:] *= \ network_output[..., 5:] > obj_thresh for i in range(grid_height * grid_width): r = i / grid_width c = i % grid_width -
我们跳过那些不能自信地描述物体的边界框:
for b in range(nb_box): objectness = \ network_output[int(r)][int(c)][b][4] if objectness.all() <= obj_thresh: continue -
我们从网络输出中提取坐标和类别,并使用它们来创建
BoundBox()实例:x, y, w, h = \ network_output[int(r)][int(c)][b][:4] x = (c + x) / grid_width y = (r + y) / grid_height w = (anchors[2 * b] * np.exp(w) / network_width) h = (anchors[2 * b + 1] * np.exp(h) / network_height) classes = network_output[int(r)][c][b][5:] box = BoundBox(x_min=x - w / 2, y_min=y - h / 2, x_max=x + w / 2, y_max=y + h / 2, objness=objectness, classes=classes) boxes.append(box) return boxes -
_correct_yolo_boxes()方法将边界框调整为原始图像的尺寸:@staticmethod def _correct_yolo_boxes(boxes, image_height, image_width, network_height, network_width): new_w, new_h = network_width, network_height for i in range(len(boxes)): x_offset = (network_width - new_w) / 2.0 x_offset /= network_width x_scale = float(new_w) / network_width y_offset = (network_height - new_h) / 2.0 y_offset /= network_height y_scale = float(new_h) / network_height boxes[i].xmin = int((boxes[i].xmin - x_ offset) / x_scale * image_width) boxes[i].xmax = int((boxes[i].xmax - x_ offset) /x_scale * image_ width) boxes[i].ymin = int((boxes[i].ymin - y_ offset) / y_scale * image_height) boxes[i].ymax = int((boxes[i].ymax - y_ offset) / y_scale * image_height) -
我们稍后会执行 NMS,以减少冗余的检测。为此,我们需要一种计算两个区间重叠量的方法:
@staticmethod def _interval_overlap(interval_a, interval_b): x1, x2 = interval_a x3, x4 = interval_b if x3 < x1: if x4 < x1: return 0 else: return min(x2, x4) - x1 else: if x2 < x3: return 0 else: return min(x2, x4) - x3 -
接下来,我们可以计算前面定义的
_interval_overlap()方法:def _bbox_iou(self, box1, box2): intersect_w = self._interval_overlap( [box1.xmin, box1.xmax], [box2.xmin, box2.xmax]) intersect_h = self._interval_overlap( [box1.ymin, box1.ymax], [box2.ymin, box2.ymax]) intersect = intersect_w * intersect_h w1, h1 = box1.xmax - box1.xmin, box1.ymax - box1.ymin w2, h2 = box2.xmax - box2.xmin, box2.ymax - box2.ymin union = w1 * h1 + w2 * h2 - intersect return float(intersect) / union -
有了这些方法,我们可以对边界框应用 NMS,从而将重复检测的数量降到最低:
def _non_max_suppression(self, boxes, nms_thresh): if len(boxes) > 0: nb_class = len(boxes[0].classes) else: return for c in range(nb_class): sorted_indices = np.argsort( [-box.classes[c] for box in boxes]) for i in range(len(sorted_indices)): index_i = sorted_indices[i] if boxes[index_i].classes[c] == 0: continue for j in range(i + 1, len(sorted_indices)): index_j = sorted_indices[j] iou = self._bbox_iou(boxes[index_i], boxes[index_j]) if iou >= nms_thresh: boxes[index_j].classes[c] = 0 -
_get_boxes()方法仅保留那些置信度高于构造函数中定义的self.class_threshold方法(默认值为 0.6 或 60%)的框:def _get_boxes(self, boxes): v_boxes, v_labels, v_scores = [], [], [] for box in boxes: for i in range(len(self.labels)): if box.classes[i] > self.class_threshold: v_boxes.append(box) v_labels.append(self.labels[i]) v_scores.append(box.classes[i] * 100) return v_boxes, v_labels, v_scores -
_draw_boxes()在输入图像中绘制最自信的检测结果,这意味着每个边界框都会显示其类别标签及其置信度:@staticmethod def _draw_boxes(filename, v_boxes, v_labels, v_scores): data = plt.imread(filename) plt.imshow(data) ax = plt.gca() for i in range(len(v_boxes)): box = v_boxes[i] y1, x1, y2, x2 = \ box.ymin, box.xmin, box.ymax, box.xmax width = x2 - x1 height = y2 - y1 rectangle = Rectangle((x1, y1), width, height, fill=False, color='white') ax.add_patch(rectangle) label = f'{v_labels[i]} ({v_scores[i]:.3f})' plt.text(x1, y1, label, color='green') plt.show() -
YOLO()类中的唯一公共方法是detect(),它实现了端到端的逻辑,用于检测输入图像中的物体。首先,它将图像传入模型:def detect(self, image, width, height): image = np.expand_dims(image, axis=0) preds = self.model.predict(image) boxes = [] -
然后,它解码网络的输出:
for i in range(len(preds)): boxes.extend( self._decode_net_output(preds[i][0], self.anchors[i], self.class_threshold, 416, 416)) -
接下来,它修正这些框,使它们与输入图像的比例正确。它还应用 NMS 来去除冗余的检测结果:
self._correct_yolo_boxes(boxes, height, width, 416, 416) self._non_max_suppression(boxes, .5) -
最后,它获取有效的边界框,并将其绘制到输入图像中:
valid_boxes, valid_labels, valid_scores = \ self._get_boxes(boxes) for i in range(len(valid_boxes)): print(valid_labels[i], valid_scores[i]) self._draw_boxes(image_path, valid_boxes, valid_labels, valid_scores) -
定义了
YOLO()类后,我们可以按如下方式实例化它:model = YOLO(weights_path='resources/yolov3.weights') -
最后一步是遍历所有测试图像,并在其上运行模型:
for image_path in glob.glob('test_images/*.jpg'): image = load_img(image_path, target_size=(416, 416)) image = img_to_array(image) image = image.astype('float32') / 255.0 original_image = load_img(image_path) width, height = original_image.size model.detect(image, width, height)这是第一个示例:
![图 9.3 – YOLO 检测到狗,具有非常高的置信度]
图 9.3 – YOLO 以非常高的置信度检测到了这只狗
我们可以观察到,YOLO 非常自信地检测到了我的狗,并且置信度高达 94.5%!太棒了!接下来看看第二张测试图像:
图 9.4 – YOLO 在一次处理过程中检测到了不同尺度的多个物体
尽管结果很拥挤,但快速一瞥便能看出,网络成功识别了前景中的两辆车,以及背景中的人。这是一个有趣的例子,因为它展示了 YOLO 作为端到端物体检测器的强大能力,它能够在一次处理过程中对许多不同的物体进行分类和定位,且尺度各异。是不是很令人印象深刻?
我们前往*如何工作...*部分,来连接这些点。
如何工作…
在这个配方中,我们发现了端到端物体检测器的巨大威力——特别是其中最著名和最令人印象深刻的一个:YOLO。
尽管 YOLO 最初是用 C++ 实现的,但我们利用了Huynh Ngoc Anh 的精彩 Python 适配,使用这个架构的预训练版本(特别是第 3 版)在开创性的 COCO 数据集上进行物体检测。
正如你可能已经注意到的那样,YOLO 和许多其他端到端物体检测器都是非常复杂的网络,但它们相对于传统方法(如图像金字塔和滑动窗口)有明显的优势。结果不仅更好,而且得益于 YOLO 能够一次性查看输入图像并产生所有相关检测的能力,处理速度也更快。
但如果你想在自己的数据上训练一个端到端的物体检测器呢?难道你只能依赖现成的解决方案吗?你需要花费数小时去解读难懂的论文,才能实现这些网络吗?
好吧,那是一个选项,但还有另一个,我们将在下一个配方中探讨,它涉及 TensorFlow 物体检测 API,这是一个实验性仓库,汇集了最先进的架构,能够简化并提升你的物体检测工作!
另见
YOLO 是深度学习和物体检测领域的一个里程碑,因此阅读这篇论文是一个非常明智的时间投资。你可以在这里找到它:
你可以直接从作者的网站了解更多关于 YOLO 的信息,网址如下:
如果你有兴趣了解我们基于其实现的keras-yolo3工具,可以参考这个链接:
github.com/experiencor/keras-yolo3
使用 TensorFlow 的物体检测 API 训练你自己的物体检测器
现代物体检测器无疑是实现和调试最复杂、最具挑战性的架构之一!然而,这并不意味着我们不能利用这个领域的最新进展,在我们自己的数据集上训练物体检测器。*怎么做?*你问。那就让我们来了解一下 TensorFlow 的物体检测 API!
在这个食谱中,我们将安装这个 API,准备一个自定义数据集进行训练,调整几个配置文件,并使用训练好的模型在测试图像上定位物体。这个食谱与你之前做过的有所不同,因为我们将在 Python 和命令行之间来回切换。
你准备好了吗?那就让我们开始吧。
准备工作
有几个依赖项需要安装才能使这个食谱工作。让我们从最重要的开始:TensorFlow 物体检测 API。首先,cd到你喜欢的位置并克隆 tensorflow/models 仓库:
$> git clone –-depth 1 https://github.com/tensorflow/models
接下来,像这样安装 TensorFlow 物体检测 API:
$> sudo apt install -y protobuf-compiler
$> cd models/research
$> protoc object_detection/protos/*.proto –-python_out=.
$> cp object_detection/packages/tf2/setup.py .
$> python -m pip install -q .
就本食谱而言,我们假设它与ch9文件夹位于同一层级(github.com/PacktPublis…
$> pip install pandas Pillow
我们将使用的数据集是Fruit Images for Object Detection,托管在 Kaggle 上,你可以通过以下链接访问:www.kaggle.com/mbkinaci/fruit-images-for-object-detection。登录或注册后,下载数据并保存到你喜欢的位置,文件名为fruits.zip(数据可以在本书配套仓库的ch9/recipe3文件夹中找到)。最后,解压缩它:
图 9.5 – 数据集中三类样本图像:苹果、橙子和香蕉
这个数据集的标签采用Pascal VOC格式,其中VOC代表视觉物体类别。请参考*另见…*部分了解更多信息。
现在,我们准备好了!让我们开始实现。
如何进行操作……
完成这些步骤后,你将使用 TensorFlow 物体检测 API 训练出你自己的最先进物体检测器:
-
在这个食谱中,我们将处理两个文件:第一个用于准备数据(你可以在仓库中找到它,名为
prepare.py),第二个用于使用物体检测器进行推理(在仓库中为inference.py)。打开prepare.py并导入所有需要的包:import glob import io import os from collections import namedtuple from xml.etree import ElementTree as tree import pandas as pd import tensorflow.compat.v1 as tf from PIL import Image from object_detection.utils import dataset_util -
定义
encode_class()函数,将文本标签映射到它们的整数表示:def encode_class(row_label): class_mapping = {'apple': 1, 'orange': 2, 'banana': 3} return class_mapping.get(row_label, None) -
定义一个函数,将标签的数据框(我们稍后会创建)拆分成组:
def split(df, group): Data = namedtuple('data', ['filename', 'object']) groups = df.groupby(group) return [Data(filename, groups.get_group(x)) for filename, x in zip(groups.groups.keys(), groups.groups)] -
TensorFlow 目标检测 API 使用一种名为
tf.train.Example的数据结构。下一个函数接收图像的路径及其标签(即包含的所有对象的边界框集和真实类别),并创建相应的tf.train.Example。首先,加载图像及其属性:def create_tf_example(group, path): groups_path = os.path.join(path, f'{group.filename}') with tf.gfile.GFile(groups_path, 'rb') as f: encoded_jpg = f.read() image = Image.open(io.BytesIO(encoded_jpg)) width, height = image.size filename = group.filename.encode('utf8') image_format = b'jpg' -
现在,存储边界框的维度以及图像中每个对象的类别:
xmins = [] xmaxs = [] ymins = [] ymaxs = [] classes_text = [] classes = [] for index, row in group.object.iterrows(): xmins.append(row['xmin'] / width) xmaxs.append(row['xmax'] / width) ymins.append(row['ymin'] / height) ymaxs.append(row['ymax'] / height) classes_text.append(row['class'].encode('utf8')) classes.append(encode_class(row['class'])) -
创建一个
tf.train.Features对象,包含图像及其对象的相关信息:features = tf.train.Features(feature={ 'image/height': dataset_util.int64_feature(height), 'image/width': dataset_util.int64_feature(width), 'image/filename': dataset_util.bytes_feature(filename), 'image/source_id': dataset_util.bytes_feature(filename), 'image/encoded': dataset_util.bytes_feature(encoded_jpg), 'image/format': dataset_util.bytes_feature(image_format), 'image/object/bbox/xmin': dataset_util.float_list_feature(xmins), 'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs), 'image/object/bbox/ymin': dataset_util.float_list_feature(ymins), 'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs), 'image/object/class/text': dataset_util.bytes_list_feature(classes_text), 'image/object/class/label': dataset_util.int64_list_feature(classes) }) -
返回一个用先前创建的特征初始化的
tf.train.Example结构:return tf.train.Example(features=features) -
定义一个函数,将包含图像边界框信息的可扩展标记语言(XML)文件转换为等效的逗号分隔值(CSV)格式文件:
def bboxes_to_csv(path): xml_list = [] bboxes_pattern = os.path.sep.join([path, '*.xml']) for xml_file in glob.glob(bboxes_pattern): t = tree.parse(xml_file) root = t.getroot() for member in root.findall('object'): value = (root.find('filename').text, int(root.find('size')[0].text), int(root.find('size')[1].text), member[0].text, int(member[4][0].text), int(member[4][1].text), int(member[4][2].text), int(member[4][3].text)) xml_list.append(value) column_names = ['filename', 'width', 'height', 'class','xmin', 'ymin', 'xmax', 'ymax'] df = pd.DataFrame(xml_list, columns=column_names) return df -
遍历
fruits文件夹中的test和train子集,将标签从 CSV 转换为 XML:base = 'fruits' for subset in ['test', 'train']: folder = os.path.sep.join([base, f'{subset}_zip', subset]) labels_path = os.path.sep.join([base,f'{subset}_ labels. csv']) bboxes_df = bboxes_to_csv(folder) bboxes_df.to_csv(labels_path, index=None) -
然后,使用相同的标签生成与当前正在处理的数据子集对应的
tf.train.Examples:writer = (tf.python_io. TFRecordWriter(f'resources/{subset}.record')) examples = pd.read_csv(f'fruits/{subset}_labels.csv') grouped = split(examples, 'filename') path = os.path.join(f'fruits/{subset}_zip/{subset}') for group in grouped: tf_example = create_tf_example(group, path) writer.write(tf_example.SerializeToString()) writer.close() -
在运行第 1 步至第 10 步中实现的
prepare.py脚本后,你将获得适合 TensorFlow 目标检测 API 训练的数据形状。下一步是下载EfficientDet的权重,这是我们将要微调的最先进架构。从Desktop文件夹下载权重。 -
创建一个文件,将类别映射到整数。命名为
label_map.txt并将其放在ch9/recipe3/resources中:item { id: 1 name: 'apple' } item { id: 2 name: 'orange' } item { id: 3 name: 'banana' } -
接下来,我们必须更改该网络的配置文件,以使其适应我们的数据集。你可以将其放置在
models/research/object_detection/configs/tf2/ssd_efficientdet_d0_512x512_coco17_tpu-8.config(假设你已将 TensorFlow 目标检测 API 安装在与ch9文件夹同一级别的伴随库中),或者直接从以下网址下载:github.com/tensorflow/models/blob/master/research/object_detection/configs/tf2/ssd_efficientdet_d0_512x512_coco17_tpu-8.config。无论你选择哪种方式,请将文件复制到ch9/recipe3/resources中,并修改第 13 行,以反映我们数据集中类别的数量:num_classes: 3然后,修改第 140 行,使其指向我们在第 7 步中下载的
EfficientDet权重:fine_tune_checkpoint: "/home/jesus/Desktop/efficientdet_d0_coco17_tpu-32/checkpoint/ckpt-0"在第 143 行将
fine_tune_checkpoint_type从classification改为detection:fine_tune_checkpoint_type: "detection"修改第 180 行,使其指向第 8 步中创建的
label_map.txt文件:label_map_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/label_map.txt"修改第 182 行,使其指向第 11 步中创建的
train.record文件,该文件对应于已准备好的训练数据:input_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/train.record"修改第 193 行,使其指向第 12 步中创建的
label_map.txt文件:label_map_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/label_map.txt"修改第 197 行,使其指向第 11 步中创建的
test.record文件,该文件对应于已准备好的测试数据:input_path: "/home/jesus/Desktop/tensorflow-computer-vision/ch9/recipe3/resources/test.record" -
到了训练模型的时候!首先,假设你在配套仓库的根目录下,
cd进入 TensorFlow 对象检测 API 中的object_detection文件夹:$> cd models/research/object_detection然后,使用以下命令训练模型:
$> python model_main_tf2.py --pipeline_config_path=../../../ch9/recipe3/resources/ssd_efficientdet_d0_512x512_coco17_tpu-8.config --model_dir=../../../ch9/recipe3/training --num_train_steps=10000在这里,我们正在训练模型进行
10000步训练。此外,我们将把结果保存在ch9/recipe3中的training文件夹内。最后,我们通过--pipeline_config_path选项指定配置文件的位置。这个步骤将持续几个小时。 -
一旦网络进行了精调,我们必须将其导出为冻结图,以便用于推理。为此,再次
cd进入 TensorFlow 对象检测 API 中的object_detection文件夹:$> cd models/research/object_detection现在,执行以下命令:
$> python exporter_main_v2.py --trained_checkpoint_dir=../../../ch9/recipe3/training/ --pipeline_config_path=../../../ch9/recipe3/resources/ssd_efficientdet_d0_512x512_coco17_tpu-8.config --output_directory=../../../ch9/recipe3/resources/inference_graphtrained_checkpoint_dir参数用于指定训练好的模型所在的位置,而pipeline_config_path则指向模型的配置文件。最后,冻结的推理图将保存在ch9/recipe3/resources/inference_graph文件夹中,正如output_directory标志所指定的那样。 -
打开一个名为
inference.py的文件,并导入所有相关的依赖项:import glob import random from io import BytesIO import matplotlib.pyplot as plt import numpy as np import tensorflow as tf from PIL import Image from object_detection.utils import ops from object_detection.utils import visualization_utils as viz from object_detection.utils.label_map_util import \ create_category_index_from_labelmap -
定义一个函数,从磁盘加载图像并将其转换为 NumPy 数组:
def load_image(path): image_data = tf.io.gfile.GFile(path, 'rb').read() image = Image.open(BytesIO(image_data)) width, height = image.size shape = (height, width, 3) image = np.array(image.getdata()) image = image.reshape(shape).astype('uint8') return image -
定义一个函数,在单张图像上运行模型。首先,将图像转换为张量:
def infer_image(net, image): image = np.asarray(image) input_tensor = tf.convert_to_tensor(image) input_tensor = input_tensor[tf.newaxis, ...] -
将张量传递给网络,提取检测的数量,并在结果字典中保留与检测数量相等的值:
num_detections = int(result.pop('num_detections')) result = {key: value[0, :num_detections].numpy() for key, value in result.items()} result['num_detections'] = num_detections result['detection_classes'] = \ result['detection_classes'].astype('int64') -
如果有检测掩膜存在,将它们重框为图像掩膜并返回结果:
if 'detection_masks' in result: detection_masks_reframed = \ ops.reframe_box_masks_to_image_masks( result['detection_masks'], result['detection_boxes'], image.shape[0], image.shape[1]) detection_masks_reframed = \ tf.cast(detection_masks_reframed > 0.5, tf.uint8) result['detection_masks_reframed'] = \ detection_masks_reframed.numpy() return result -
从我们在步骤 12中创建的
label_map.txt文件中创建类别索引,同时从步骤 15中生成的冻结推理图中加载模型:labels_path = 'resources/label_map.txt' CATEGORY_IDX = \ create_category_index_from_labelmap(labels_path, use_display_name=True) model_path = 'resources/inference_graph/saved_model' model = tf.saved_model.load(model_path) -
随机选择三张测试图像:
test_images = list(glob.glob('fruits/test_zip/test/*.jpg')) random.shuffle(test_images) test_images = test_images[:3] -
在样本图像上运行模型,并保存结果检测:
for image_path in test_images: image = load_image(image_path) result = infer_image(model, image) masks = result.get('detection_masks_reframed', None) viz.visualize_boxes_and_labels_on_image_array( image, result['detection_boxes'], result['detection_classes'], result['detection_scores'], CATEGORY_IDX, instance_masks=masks, use_normalized_coordinates=True, line_thickness=5) plt.figure(figsize=(24, 32)) plt.imshow(image) plt.savefig(f'detections_{image_path.split("/")[-1]}')我们可以在图 9.6中看到结果:
图 9.6 – EfficientDet 在随机样本测试图像上的检测结果
我们可以在图 9.6中看到,我们精调后的网络产生了相当准确且自信的检测结果。考虑到我们仅关注数据准备和推理,并且在架构方面我们只是根据需要调整了配置文件,结果相当令人印象深刻!
让我们继续阅读*如何工作...*部分。
如何工作...
在本食谱中,我们发现训练一个物体检测器是一个艰难且富有挑战的任务。然而,好消息是,我们可以使用 TensorFlow 对象检测 API 来训练各种前沿网络。
由于 TensorFlow 物体检测 API 是一个实验性工具,它使用与常规 TensorFlow 不同的约定,因此,为了使用它,我们需要对输入数据进行一些处理,将其转化为 API 可以理解的格式。这是通过将Fruits for Object Detection数据集中的标签(最初是 XML 格式)转换为 CSV,再转为序列化的tf.train.Example对象来完成的。
然后,为了使用训练好的模型,我们通过exporter_main_v2.py脚本将其导出为推理图,并利用 API 中的一些可视化工具显示样本测试图像上的检测结果。
那么,训练呢?可以说这是最简单的部分,包含三个主要步骤:
-
创建从文本标签到整数的映射(步骤 12)
-
修改与模型对应的配置文件,以便在所有相关位置进行微调(步骤 13)
-
运行
model_main_tf2.py文件来训练网络,并传递正确的参数(步骤 14)
这个方案为你提供了一个模板,你可以对其进行调整和适应,以便在任何你选择的数据集上训练几乎所有现代物体检测器(API 支持的)。相当酷,对吧?
另见
你可以在这里了解更多关于 TensorFlow 物体检测 API 的信息:
github.com/tensorflow/models/tree/master/research/object_detection
此外,我鼓励你阅读这篇精彩的文章,了解更多关于EfficientDet的信息:
towardsdatascience.com/a-thorough-breakdown-of-efficientdet-for-object-detection-dc6a15788b73
如果你想深入了解Pascal VOC格式,那么你一定要观看这个视频:
www.youtube.com/watch?v=-f6TJpHcAeM
使用 TFHub 进行物体检测
TFHub 是物体检测领域的一个丰富宝库,充满了最先进的模型。正如我们在这个方案中将发现的那样,使用它们来识别图像中的感兴趣元素是一项相当直接的任务,尤其是考虑到它们已经在庞大的COCO数据集上进行了训练,这使得它们成为现成物体检测的绝佳选择。
准备工作
首先,我们必须安装Pillow和 TFHub,步骤如下:
$> pip install Pillow tensorflow-hub
此外,由于我们将使用的一些可视化工具位于 TensorFlow 物体检测 API 中,我们必须先安装它。首先,cd到你喜欢的位置,并克隆tensorflow/models仓库:
$> git clone –-depth 1 https://github.com/tensorflow/models
接下来,安装 TensorFlow 物体检测 API,像这样:
$> sudo apt install -y protobuf-compiler
$> cd models/research
$> protoc object_detection/protos/*.proto –-python_out=.
$> cp object_detection/packages/tf2/setup.py .
$> python -m pip install -q .
就是这样!让我们开始吧。
如何操作……
按照以下步骤学习如何使用 TFHub 检测你自己照片中的物体:
-
导入我们需要的包:
import glob from io import BytesIO import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_hub as hub from PIL import Image from object_detection.utils import visualization_utils as viz from object_detection.utils.label_map_util import \ create_category_index_from_labelmap -
定义一个函数,将图像加载到 NumPy 数组中:
def load_image(path): image_data = tf.io.gfile.GFile(path, 'rb').read() image = Image.open(BytesIO(image_data)) width, height = image.size shape = (1, height, width, 3) image = np.array(image.getdata()) image = image.reshape(shape).astype('uint8') return image -
定义一个函数,通过模型进行预测,并将结果保存到磁盘。首先加载图像并将其传入模型:
def get_and_save_predictions(model, image_path): image = load_image(image_path) results = model(image) -
将结果转换为 NumPy 数组:
model_output = {k: v.numpy() for k, v in results.items()} -
创建一个包含检测框、得分和类别的可视化结果:
boxes = model_output['detection_boxes'][0] classes = \ model_output['detection_classes'][0].astype('int') scores = model_output['detection_scores'][0] clone = image.copy() viz.visualize_boxes_and_labels_on_image_array( image=clone[0], boxes=boxes, classes=classes, scores=scores, category_index=CATEGORY_IDX, use_normalized_coordinates=True, max_boxes_to_draw=200, min_score_thresh=0.30, agnostic_mode=False, line_thickness=5 ) -
将结果保存到磁盘:
plt.figure(figsize=(24, 32)) plt.imshow(image_with_mask[0]) plt.savefig(f'output/{image_path.split("/")[-1]}') -
加载
COCO的类别索引:labels_path = 'resources/mscoco_label_map.pbtxt' CATEGORY_IDX =create_category_index_from_labelmap(labels_path) -
从 TFHub 加载 Faster R-CNN:
MODEL_PATH = ('https://tfhub.dev/tensorflow/faster_rcnn/' 'inception_resnet_v2_1024x1024/1') model = hub.load(MODEL_PATH) -
在所有测试图像上运行 Faster R-CNN:
test_images_paths = glob.glob('test_images/*') for image_path in test_images_paths: get_and_save_predictions(model, image_path)一段时间后,标注过的图像应该会出现在
output文件夹中。第一个示例展示了网络的强大能力,它以 100%的信心检测到了照片中的两只大象:
图 9.7 – 两只大象被检测到,且得分完美
然而,也有模型出现一些错误的情况,像这样:
图 9.8 – 网络错误地将桌布中的一个人检测出来
在这个例子中,网络将桌布中的一个人检测出来,置信度为 42%,虽然它正确识别了我的狗是巴哥犬,准确率为 100%。通过提高传递给visualize_boxes_and_labels_on_image_array()方法的min_score_thresh值,可以防止这种误报和其他假阳性。
让我们继续进入下一部分。
它是如何工作的…
在这个示例中,我们利用了 TFHub 中强大模型的易用性,进行开箱即用的物体检测,并取得了相当不错的结果。
为什么我们应该将 TFHub 视为满足物体检测需求的可行选择呢?好吧,那里绝大多数模型在从零开始时实现起来非常具有挑战性,更不用说训练它们以达到可接受的结果了。除此之外,这些复杂的架构是在COCO上训练的,COCO是一个庞大的图像数据集,专门用于物体检测和图像分割任务。然而,我们必须牢记,无法重新训练这些网络,因此它们最适用于包含COCO中已有物体的图像。如果我们需要创建自定义物体检测器,本章中介绍的其他策略应该足够了。
参见
您可以在此访问 TFHub 中所有可用物体检测器的列表: