! [ -e /content ] && pip install -Uqq fastai # 在Colab上升级fastai
自定义变换
在计算机视觉中使用
Datasets
、Pipeline
、TfmdLists
和Transform
概述
from fastai.vision.all import *
创建您自己的 Transform
创建自己的 Transform
比你想象的要简单得多。实际上,每当你将标签函数传递给数据块 API 或 ImageDataLoaders.from_name_func
时,你实际上已经创建了一个 Transform
,而你并没有意识到。从本质上讲,Transform
只是一种函数。我们来展示一下如何通过实现一个包装来自 albumentations 库 的数据增强的变换来轻松地添加一个变换。
首先,你需要安装 albumentations 库。如果需要,请取消注释以下单元格以进行安装:
# !pip install albumentations
然后,查看转换结果将在比之前的 mnist 图像更大的彩色图像上变得更加简单,因此让我们从 PETS 数据集中加载一些内容。
= untar_data(URLs.PETS)
source = get_image_files(source/"images") items
我们仍然可以使用 PIL.Image.create
打开它:
= PILImage.create(items[0])
img img
我们将展示如何封装一个变换,但是你可以同样轻松地封装在 Compose
方法中封装的任何一组变换。这里我们来做一些 ShiftScaleRotate
:
from albumentations import ShiftScaleRotate
albumentations转换适用于numpy图像,因此我们在将其重新包装为PILImage.create
之前,只需将PILImage
转换为numpy数组(此函数接受文件名、数组或张量)。
= ShiftScaleRotate(p=1)
aug def aug_tfm(img):
= np.array(img)
np_img = aug(image=np_img)['image']
aug_img return PILImage.create(aug_img)
aug_tfm(img)
我们可以在每次期待一个 Transform
时传递这个函数,而fastai库会自动进行转换。这是因为你可以直接传递这样的函数来创建一个 Transform
:
= Transform(aug_tfm) tfm
如果您的转换中有一些状态,您可能需要创建 Transform
的子类。在这种情况下,您想要应用的函数应该写在 <code>encodes</code>
方法中(与您为 PyTorch 模块实现 forward
的方式相同):
class AlbumentationsTransform(Transform):
def __init__(self, aug): self.aug = aug
def encodes(self, img: PILImage):
= self.aug(image=np.array(img))['image']
aug_img return PILImage.create(aug_img)
我们还添加了类型注释:这将确保此转换仅应用于 PILImage
及其子类。对于任何其他对象,它将不执行任何操作。您还可以根据需要编写多个具有不同类型注释的 <code>encodes</code>
方法,Transform
将正确地分发它接收的对象。
这是因为在实践中,转换通常作为 item_tfms
(或 batch_tfms
)应用,您在数据块 API 中传递这些项目。这些项目是不同类型对象的元组,转换可能在元组的每个部分上具有不同的行为。
让我们在这里检查一下它是如何工作的:
= AlbumentationsTransform(ShiftScaleRotate(p=1))
tfm = tfm((img, 'dog'))
a,b =b); show_image(a, title
转换是应用于元组(img, "dog")
的。img
是一个PILImage
,因此应用了我们编写的encodes
方法。而"dog"
是一个字符串,所以转换对它没有做任何处理。
然而,有时您需要让转换整体处理元组:例如,albumentations同样适用于图像和分割掩码。在这种情况下,您需要子类化ItemTransform
而不是Transform
。让我们来看一下这如何实现:
= untar_data(URLs.CAMVID_TINY)
cv_source = get_image_files(cv_source/'images')
cv_items = PILImage.create(cv_items[0])
img = PILMask.create(cv_source/'labels'/f'{cv_items[0].stem}_P{cv_items[0].suffix}')
mask = img.show()
ax = mask.show(ctx=ax) ax
我们接着编写一个ItemTransform
的子类,它可以包装任何albumentations增强变换,但仅适用于分割问题:
class SegmentationAlbumentationsTransform(ItemTransform):
def __init__(self, aug): self.aug = aug
def encodes(self, x):
= x
img,mask = self.aug(image=np.array(img), mask=np.array(mask))
aug return PILImage.create(aug["image"]), PILMask.create(aug["mask"])
我们可以检查它是如何在元组 (img, mask)
上应用的。这意味着您可以将其作为 item_tfms
传递到任何分割问题中。
= SegmentationAlbumentationsTransform(ShiftScaleRotate(p=1))
tfm = tfm((img, mask))
a,b = a.show()
ax = b.show(ctx=ax) ax
分割
通过在 after_item
中使用相同的变换,但使用不同类型的目标(这里是分割掩码),目标会自动按照类型分派系统进行处理。
= untar_data(URLs.CAMVID_TINY)
cv_source = get_image_files(cv_source/'images')
cv_items = RandomSplitter(seed=42)
cv_splitter = cv_splitter(cv_items)
cv_split = lambda o: cv_source/'labels'/f'{o.stem}_P{o.suffix}' cv_label
class ImageResizer(Transform):
=1
order"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)
= [[PILImage.create], [cv_label, PILMask.create]]
tfms = Datasets(cv_items, tfms, splits=cv_split)
cv_dsets = cv_dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor()]) dls
如果我们想使用之前创建的增强变换,我们只需要为它添加一件事:我们希望它仅在训练集上应用,而不是在验证集上。为此,我们通过添加 split_idx=0
来指定它仅在我们的划分中的特定 idx
上应用(0 表示训练集,1 表示验证集):
class SegmentationAlbumentationsTransform(ItemTransform):
= 0
split_idx def __init__(self, aug): self.aug = aug
def encodes(self, x):
= x
img,mask = self.aug(image=np.array(img), mask=np.array(mask))
aug return PILImage.create(aug["image"]), PILMask.create(aug["mask"])
我们可以检查它是如何应用于元组 (img, mask)
的。这意味着您可以将其作为 item_tfms
传递给任何分割问题。
= Datasets(cv_items, tfms, splits=cv_split)
cv_dsets = cv_dsets.dataloaders(bs=64, after_item=[ImageResizer(128), ToTensor(), IntToFloatTensor(),
dls =1))]) SegmentationAlbumentationsTransform(ShiftScaleRotate(p
=4) dls.show_batch(max_n
使用不同的转换管道和 DataBlock API
在训练数据集和验证数据集上使用不同的转换是非常常见的。目前我们的 AlbumentationsTransform
在两个数据集上执行相同的转换,让我们看看是否可以使其在我们想要的方面更灵活。
让我们考虑一个我们的例子场景:
我希望各种数据增强,例如 HueSaturationValue
或 Flip
,能够像 fastai 一样,只在训练数据集上运行,而在验证数据集上不运行。我们需要对我们的 AlbumentationsTransform
做些什么呢?
class AlbumentationsTransform(DisplayedTransform):
=0,2
split_idx,orderdef __init__(self, train_aug): store_attr()
def encodes(self, img: PILImage):
= self.train_aug(image=np.array(img))['image']
aug_img 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(=0.2,
hue_shift_limit=0.2,
sat_shift_limit=0.2,
val_shift_limit=0.5
p
),=0.5),
albumentations.CoarseDropout(p=0.5)
albumentations.Cutout(p ])
我们可以使用 Resize
和我们的新训练增强方法来定义我们的 ItemTransforms
:
= [Resize(224), AlbumentationsTransform(get_train_aug())] item_tfms
这次我们使用更高级的 DataBlock
API:
= untar_data(URLs.PETS)/'images'
path
def is_cat(x): return x[0].isupper()
= ImageDataLoaders.from_name_func(
dls =0.2, seed=42,
path, get_image_files(path), valid_pct=is_cat, item_tfms=item_tfms) label_func
并查看一些数据:
=4) dls.train.show_batch(max_n
=4) dls.valid.show_batch(max_n
我们可以看到我们的转换仅成功应用于训练数据!太好了!
现在,如果我们想对训练集和验证集都应用特殊的不同行为呢?我们来看一下:
class AlbumentationsTransform(RandTransform):
"A transform handler for multiple `Albumentation` transforms"
=None,2
split_idx,orderdef __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:
= self.train_aug(image=np.array(img))['image']
aug_img else:
= self.valid_aug(image=np.array(img))['image']
aug_img 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([
224,224),
albumentations.RandomResizedCrop(=0.5),
albumentations.Transpose(p=0.5),
albumentations.VerticalFlip(p=0.5),
albumentations.ShiftScaleRotate(p
albumentations.HueSaturationValue(=0.2,
hue_shift_limit=0.2,
sat_shift_limit=0.2,
val_shift_limit=0.5),
p=0.5),
albumentations.CoarseDropout(p=0.5)
albumentations.Cutout(p
])
def get_valid_aug(): return albumentations.Compose([
224,224, p=1.),
albumentations.CenterCrop(224,224)
albumentations.Resize(=1.) ], p
接下来我们将构建我们的新的 AlbumentationsTransform
:
= [Resize(256), AlbumentationsTransform(get_train_aug(), get_valid_aug())] item_tfms
并将其传递给我们的 DataLoaders
: > 由于我们在组合的变换中已经声明了缩放,因此这里不需要任何项变换。
= ImageDataLoaders.from_name_func(
dls =0.2, seed=42,
path, get_image_files(path), valid_pct=is_cat, item_tfms=item_tfms) label_func
我们可以再次比较我们的训练和验证增强,发现它们的确是不同的:
=4) dls.train.show_batch(max_n
=4) dls.valid.show_batch(max_n
查看验证 DataLoader
中 x
的形状,我们会发现我们的 CenterCrop
也被应用了:
= dls.valid.one_batch()
x,_ print(x.shape)
(64, 3, 224, 224)
我们首先使用fastai的裁剪,因为由于某些图像尺寸过小,需要一些填充。