自定义变换

! [ -e /content ] && pip install -Uqq fastai  # 在Colab上升级fastai

在计算机视觉中使用 DatasetsPipelineTfmdListsTransform

概述

from fastai.vision.all import *

创建您自己的 Transform

创建自己的 Transform 比你想象的要简单得多。实际上,每当你将标签函数传递给数据块 API 或 ImageDataLoaders.from_name_func 时,你实际上已经创建了一个 Transform,而你并没有意识到。从本质上讲,Transform 只是一种函数。我们来展示一下如何通过实现一个包装来自 albumentations 库 的数据增强的变换来轻松地添加一个变换。

首先,你需要安装 albumentations 库。如果需要,请取消注释以下单元格以进行安装:

# !pip install albumentations

然后,查看转换结果将在比之前的 mnist 图像更大的彩色图像上变得更加简单,因此让我们从 PETS 数据集中加载一些内容。

source = untar_data(URLs.PETS)
items = get_image_files(source/"images")

我们仍然可以使用 PIL.Image.create 打开它:

img = PILImage.create(items[0])
img

我们将展示如何封装一个变换,但是你可以同样轻松地封装在 Compose 方法中封装的任何一组变换。这里我们来做一些 ShiftScaleRotate

from albumentations import ShiftScaleRotate

albumentations转换适用于numpy图像,因此我们在将其重新包装为PILImage.create之前,只需将PILImage转换为numpy数组(此函数接受文件名、数组或张量)。

aug = ShiftScaleRotate(p=1)
def aug_tfm(img): 
    np_img = np.array(img)
    aug_img = aug(image=np_img)['image']
    return PILImage.create(aug_img)
aug_tfm(img)

我们可以在每次期待一个 Transform 时传递这个函数,而fastai库会自动进行转换。这是因为你可以直接传递这样的函数来创建一个 Transform

tfm = Transform(aug_tfm)

如果您的转换中有一些状态,您可能需要创建 Transform 的子类。在这种情况下,您想要应用的函数应该写在 <code>encodes</code> 方法中(与您为 PyTorch 模块实现 forward 的方式相同):

class AlbumentationsTransform(Transform):
    def __init__(self, aug): self.aug = aug
    def encodes(self, img: PILImage):
        aug_img = self.aug(image=np.array(img))['image']
        return PILImage.create(aug_img)

我们还添加了类型注释:这将确保此转换仅应用于 PILImage 及其子类。对于任何其他对象,它将不执行任何操作。您还可以根据需要编写多个具有不同类型注释的 <code>encodes</code> 方法,Transform 将正确地分发它接收的对象。

这是因为在实践中,转换通常作为 item_tfms(或 batch_tfms)应用,您在数据块 API 中传递这些项目。这些项目是不同类型对象的元组,转换可能在元组的每个部分上具有不同的行为。

让我们在这里检查一下它是如何工作的:

tfm = AlbumentationsTransform(ShiftScaleRotate(p=1))
a,b = tfm((img, 'dog'))
show_image(a, title=b);

转换是应用于元组(img, "dog")的。img是一个PILImage,因此应用了我们编写的encodes方法。而"dog"是一个字符串,所以转换对它没有做任何处理。

然而,有时您需要让转换整体处理元组:例如,albumentations同样适用于图像和分割掩码。在这种情况下,您需要子类化ItemTransform而不是Transform。让我们来看一下这如何实现:

cv_source = untar_data(URLs.CAMVID_TINY)
cv_items = get_image_files(cv_source/'images')
img = PILImage.create(cv_items[0])
mask = PILMask.create(cv_source/'labels'/f'{cv_items[0].stem}_P{cv_items[0].suffix}')
ax = img.show()
ax = mask.show(ctx=ax)

我们接着编写一个ItemTransform的子类,它可以包装任何albumentations增强变换,但仅适用于分割问题:

class SegmentationAlbumentationsTransform(ItemTransform):
    def __init__(self, aug): self.aug = aug
    def encodes(self, x):
        img,mask = x
        aug = self.aug(image=np.array(img), mask=np.array(mask))
        return PILImage.create(aug["image"]), PILMask.create(aug["mask"])

我们可以检查它是如何在元组 (img, mask) 上应用的。这意味着您可以将其作为 item_tfms 传递到任何分割问题中。

tfm = SegmentationAlbumentationsTransform(ShiftScaleRotate(p=1))
a,b = tfm((img, mask))
ax = a.show()
ax = b.show(ctx=ax)

分割

通过在 after_item 中使用相同的变换,但使用不同类型的目标(这里是分割掩码),目标会自动按照类型分派系统进行处理。

cv_source = untar_data(URLs.CAMVID_TINY)
cv_items = get_image_files(cv_source/'images')
cv_splitter = RandomSplitter(seed=42)
cv_split = cv_splitter(cv_items)
cv_label = lambda o: cv_source/'labels'/f'{o.stem}_P{o.suffix}'
class ImageResizer(Transform):
    order=1
    "Resize image to `size` using `resample`"
    def __init__(self, size, resample=BILINEAR):
        if not is_listy(size): size=(size,size)
        self.size,self.resample = (size[1],size[0]),resample

    def encodes(self, o:PILImage): return o.resize(size=self.size, resample=self.resample)
    def encodes(self, o:PILMask):  return o.resize(size=self.size, resample=NEAREST)
tfms = [[PILImage.create], [cv_label, PILMask.create]]
cv_dsets = Datasets(cv_items, tfms, splits=cv_split)
dls = cv_dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor()])

如果我们想使用之前创建的增强变换,我们只需要为它添加一件事:我们希望它仅在训练集上应用,而不是在验证集上。为此,我们通过添加 split_idx=0 来指定它仅在我们的划分中的特定 idx 上应用(0 表示训练集,1 表示验证集):

class SegmentationAlbumentationsTransform(ItemTransform):
    split_idx = 0
    def __init__(self, aug): self.aug = aug
    def encodes(self, x):
        img,mask = x
        aug = self.aug(image=np.array(img), mask=np.array(mask))
        return PILImage.create(aug["image"]), PILMask.create(aug["mask"])

我们可以检查它是如何应用于元组 (img, mask) 的。这意味着您可以将其作为 item_tfms 传递给任何分割问题。

cv_dsets = Datasets(cv_items, tfms, splits=cv_split)
dls = cv_dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor(), 
                                              SegmentationAlbumentationsTransform(ShiftScaleRotate(p=1))])
dls.show_batch(max_n=4)

使用不同的转换管道和 DataBlock API

在训练数据集和验证数据集上使用不同的转换是非常常见的。目前我们的 AlbumentationsTransform 在两个数据集上执行相同的转换,让我们看看是否可以使其在我们想要的方面更灵活。

让我们考虑一个我们的例子场景:

我希望各种数据增强,例如 HueSaturationValueFlip,能够像 fastai 一样,只在训练数据集上运行,而在验证数据集上不运行。我们需要对我们的 AlbumentationsTransform 做些什么呢?

class AlbumentationsTransform(DisplayedTransform):
    split_idx,order=0,2
    def __init__(self, train_aug): store_attr()
    
    def encodes(self, img: PILImage):
        aug_img = self.train_aug(image=np.array(img))['image']
        return PILImage.create(aug_img)

这是我们新写的变换。但有什么变化呢?

我们添加了一个 split_idx,它决定了在验证集和训练集上运行哪些变换(训练集为 0,验证集为 1,None 则表示两者都适用)。

除此之外,我们将 order 设置为 2。这意味着如果我们有任何执行调整大小操作的 fastai 变换,这些变换会在我们的新变换之前执行。这让我们确切知道我们的变换何时会被应用,以及我们如何与之合作!

让我们来看一个使用 Composed albumentations 变换的例子:

import albumentations
def get_train_aug(): return albumentations.Compose([
            albumentations.HueSaturationValue(
                hue_shift_limit=0.2, 
                sat_shift_limit=0.2, 
                val_shift_limit=0.2, 
                p=0.5
            ),
            albumentations.CoarseDropout(p=0.5),
            albumentations.Cutout(p=0.5)
])

我们可以使用 Resize 和我们的新训练增强方法来定义我们的 ItemTransforms

item_tfms = [Resize(224), AlbumentationsTransform(get_train_aug())]

这次我们使用更高级的 DataBlock API:

path = untar_data(URLs.PETS)/'images'

def is_cat(x): return x[0].isupper()
dls = ImageDataLoaders.from_name_func(
    path, get_image_files(path), valid_pct=0.2, seed=42,
    label_func=is_cat, item_tfms=item_tfms)

并查看一些数据:

dls.train.show_batch(max_n=4)

dls.valid.show_batch(max_n=4)

我们可以看到我们的转换仅成功应用于训练数据!太好了!

现在,如果我们想对训练集验证集都应用特殊的不同行为呢?我们来看一下:

class AlbumentationsTransform(RandTransform):
    "A transform handler for multiple `Albumentation` transforms"
    split_idx,order=None,2
    def __init__(self, train_aug, valid_aug): store_attr()
    
    def before_call(self, b, split_idx):
        self.idx = split_idx
    
    def encodes(self, img: PILImage):
        if self.idx == 0:
            aug_img = self.train_aug(image=np.array(img))['image']
        else:
            aug_img = self.valid_aug(image=np.array(img))['image']
        return PILImage.create(aug_img)

我们来看看这里发生了什么。我们将 split_idx 更改为 None,这使我们能够在设置 split_idx 时进行指定。

我们还继承了 RandTransform,这使我们能够在 before_call 中设置 split_idx

最后,我们检查当前的 split_idx 是什么。如果它是 0,则运行训练增强,否则运行验证增强。

让我们看一个典型训练设置的例子:

def get_train_aug(): return albumentations.Compose([
            albumentations.RandomResizedCrop(224,224),
            albumentations.Transpose(p=0.5),
            albumentations.VerticalFlip(p=0.5),
            albumentations.ShiftScaleRotate(p=0.5),
            albumentations.HueSaturationValue(
                hue_shift_limit=0.2, 
                sat_shift_limit=0.2, 
                val_shift_limit=0.2, 
                p=0.5),
            albumentations.CoarseDropout(p=0.5),
            albumentations.Cutout(p=0.5)
])

def get_valid_aug(): return albumentations.Compose([
    albumentations.CenterCrop(224,224, p=1.),
    albumentations.Resize(224,224)
], p=1.)

接下来我们将构建我们的新的 AlbumentationsTransform

item_tfms = [Resize(256), AlbumentationsTransform(get_train_aug(), get_valid_aug())]

并将其传递给我们的 DataLoaders: > 由于我们在组合的变换中已经声明了缩放,因此这里不需要任何项变换。

dls = ImageDataLoaders.from_name_func(
    path, get_image_files(path), valid_pct=0.2, seed=42,
    label_func=is_cat, item_tfms=item_tfms)

我们可以再次比较我们的训练和验证增强,发现它们的确是不同的:

dls.train.show_batch(max_n=4)

dls.valid.show_batch(max_n=4)

查看验证 DataLoaderx 的形状,我们会发现我们的 CenterCrop 也被应用了:

x,_ = dls.valid.one_batch()
print(x.shape)
(64, 3, 224, 224)
Note

我们首先使用fastai的裁剪,因为由于某些图像尺寸过小,需要一些填充。

结束 -