! [ -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 数据集中加载一些内容。
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 ShiftScaleRotatealbumentations转换适用于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 在两个数据集上执行相同的转换,让我们看看是否可以使其在我们想要的方面更灵活。
让我们考虑一个我们的例子场景:
我希望各种数据增强,例如 HueSaturationValue 或 Flip,能够像 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 albumentationsdef 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)
查看验证 DataLoader 中 x 的形状,我们会发现我们的 CenterCrop 也被应用了:
x,_ = dls.valid.one_batch()
print(x.shape)(64, 3, 224, 224)
我们首先使用fastai的裁剪,因为由于某些图像尺寸过小,需要一些填充。