计算机视觉简介

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

在计算机视觉中使用 fastai 库。

from fastai.vision.all import *

本教程重点介绍如何快速构建一个 Learner 并对预训练模型进行微调,以应对大多数计算机视觉任务。

单标签分类

在这个任务中,我们将使用牛津-印度理工大学宠物数据集,该数据集包含37种不同品种的猫和狗的图像。我们将首先展示如何构建一个简单的猫狗分类器,然后是一个稍微复杂一点的模型,可以分类所有品种。

可以使用以下代码下载并解压数据集:

path = untar_data(URLs.PETS)

它只会下载一次,并返回解压缩归档文件的位置。我们可以使用 .ls() 方法检查里面的内容。

path.ls()
(#2) [Path('/home/jhoward/.fastai/data/oxford-iiit-pet/images'),Path('/home/jhoward/.fastai/data/oxford-iiit-pet/annotations')]

我们暂时忽略注释文件夹,专注于图像文件夹。get_image_files 是一个 fastai 函数,它帮助我们快速获取一个文件夹中所有的图像文件(递归)。

files = get_image_files(path/"images")
len(files)
7390

猫与狗

为了给我们的猫与狗问题标记数据,我们需要知道哪些文件名是狗的图片,哪些是猫的图片。区分这两者的简单方法是:文件名以大写字母开头表示猫,以小写字母开头表示狗。

files[0],files[6]
(Path('/home/jhoward/.fastai/data/oxford-iiit-pet/images/basset_hound_181.jpg'),
 Path('/home/jhoward/.fastai/data/oxford-iiit-pet/images/beagle_128.jpg'))

我们可以定义一个简单的标签函数:

def label_func(f): return f[0].isupper()

为了使我们的数据准备好供模型使用,我们需要将其放入一个 DataLoaders 对象中。这里我们有一个使用文件名进行标记的函数,因此我们将使用 ImageDataLoaders.from_name_func。还有其他的 ImageDataLoaders 工厂方法可能更适合您的问题,因此请确保在 vision.data 中检查它们所有。

dls = ImageDataLoaders.from_name_func(path, files, label_func, item_tfms=Resize(224))

我们已经将当前工作目录、我们抓取的files、我们的label_func 和最后一个参数item_tfms传递给了这个函数:item_tfms是应用于我们数据集中所有项目的Transform,它会将每张图片调整为224 x 224的大小,通过对最大维度进行随机裁剪使其变为正方形,然后再调整为224 x 224。如果我们不传递这个参数,后面会出现错误,因为无法将项目一起打包。

我们可以使用show_batch方法检查一切是否正常(True表示猫,False表示狗):

dls.show_batch()

然后我们可以创建一个 Learner,这是一个 fastai 对象,它将数据和模型结合起来进行训练,并使用迁移学习在仅仅两行代码中微调预训练模型:

learn = vision_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(1)
epoch train_loss valid_loss error_rate time
0 0.150819 0.023647 0.007442 00:09
epoch train_loss valid_loss error_rate time
0 0.046232 0.011466 0.004736 00:10

第一行下载了一个名为 ResNet34 的模型,该模型在 ImageNet 上进行了预训练,并将其调整为我们的特定问题。然后对该模型进行了微调,并在相对较短的时间内,我们得到了一个错误率远低于 1% 的模型……真是令人惊叹!

如果您想对新图像进行预测,可以使用 learn.predict

learn.predict(files[0])
('False', TensorImage(0), TensorImage([9.9998e-01, 2.0999e-05]))

predict 方法返回三个内容:解码后的预测(这里是 False 表示不是狗),预测类的索引以及所有类的概率张量,按照它们的索引标签顺序(在这种情况下,模型对这是狗的判断非常有信心)。此方法接受一个文件名、一个 PIL 图像或在这种情况下直接接受一个张量。 我们还可以通过 show_results 方法查看一些预测结果:

learn.show_results()

查看本教程中涉及的其他应用程序,如文本或表格,您会发现它们都有一个一致的API,用于收集数据并进行查看,创建一个Learner,训练模型并查看一些预测结果。

分类品种

为了使用品种名称标记我们的数据,我们将使用正则表达式从文件名中提取它。回顾一下文件名,我们有:

files[0].name
'great_pyrenees_173.jpg'

所以类名是最后一个 _ 后面跟一些数字之前的所有内容。一个可以捕捉到这个名称的正则表达式是:

pat = r'^(.*)_\d+.jpg'

由于使用正则表达式对数据进行标记非常普遍(通常,标签隐藏在文件名中),因此有一个工厂方法可以做到这一点:

dls = ImageDataLoaders.from_name_re(path, files, pat, item_tfms=Resize(224))

如之前所示,我们可以使用 show_batch 来查看我们的数据:

dls.show_batch()

由于在37种不同品种中准确分类猫或狗的确切品种是一个更困难的问题,我们将稍微修改DataLoaders的定义,以使用数据增强:

dls = ImageDataLoaders.from_name_re(path, files, pat, item_tfms=Resize(460),
                                    batch_tfms=aug_transforms(size=224))

这次我们在分批处理之前将大小调整为更大,并添加了 batch_tfmsaug_transforms 是一个提供数据增强转换集合的函数,其默认值在许多数据集上表现良好。您可以通过向 aug_transforms 传递适当的参数来定制这些转换。

dls.show_batch()

我们可以像之前一样创建我们的 Learner 并训练我们的模型。

learn = vision_learner(dls, resnet34, metrics=error_rate)

我们之前使用了默认的学习率,但我们可能想要找到最佳的学习率。为此,我们可以使用学习率查找器:

learn.lr_find()
SuggestedLRs(lr_min=0.010000000149011612, lr_steep=0.0063095735386013985)

它绘制了学习率查找器的图形,并给出两个建议(最小值除以10和最陡的梯度)。我们将在这里使用 3e-3。我们还将进行更多的训练周期:

learn.fine_tune(2, 3e-3)
epoch train_loss valid_loss error_rate time
0 1.270041 0.308686 0.109608 00:16
epoch train_loss valid_loss error_rate time
0 0.468626 0.355379 0.117050 00:21
1 0.418402 0.384385 0.110961 00:20
2 0.267954 0.220428 0.075778 00:21
3 0.143201 0.203174 0.064953 00:20

我们可以再次通过show_results查看一些预测:

learn.show_results()

另一个有用的东西是解释对象,它可以向我们显示模型在哪些地方做出了最差的预测:

interp = Interpretation.from_learner(learn)
interp.plot_top_losses(9, figsize=(15,10))

单标签分类 - 使用数据块 API

我们还可以使用数据块API将数据放入DataLoaders中。这有点高级,如果您对学习新的API还不感到舒适,可以随意跳过这一部分。

数据块通过给fastai库提供一系列信息来构建:

  • 通过一个名为blocks的参数指定使用的类型:在这里我们有图像和类别,所以我们传递ImageBlockCategoryBlock
  • 如何获取原始项,在这里是我们的函数get_image_files
  • 如何标记这些项,这里使用与之前相同的正则表达式。
  • 如何将这些项进行分割,这里使用随机分割器。
  • 像以前一样的item_tfmsbatch_tfms
pets = DataBlock(blocks=(ImageBlock, CategoryBlock), 
                 get_items=get_image_files, 
                 splitter=RandomSplitter(),
                 get_y=using_attr(RegexLabeller(r'(.+)_\d+.jpg$'), 'name'),
                 item_tfms=Resize(460),
                 batch_tfms=aug_transforms(size=224))

pets对象本身是空的:它只包含将帮助我们收集数据的函数。我们必须调用dataloaders方法以获取DataLoaders。我们传递给它数据的来源:

dls = pets.dataloaders(untar_data(URLs.PETS)/"images")

然后我们可以使用 dls.show_batch() 查看我们的图片。

dls.show_batch(max_n=9)

多标签分类

对于这个任务,我们将使用Pascal数据集,该数据集包含不同种类的物体/人物的图像。它最初是一个用于物体检测的数据集,意味着任务不仅仅是检测图像中是否存在某一类别的实例,而是还要在其周围绘制一个边界框。在这里,我们将尝试预测给定图像中的所有类别。

多标签分类与之前的不同之处在于,每张图像并不属于一个类别。例如,一张图像可能同时包含一个人一匹马。或者没有我们研究的任何类别。

和之前一样,我们可以相当容易地下载这个数据集:

path = untar_data(URLs.PASCAL_2007)
path.ls()
(#9) [Path('/home/jhoward/.fastai/data/pascal_2007/valid.json'),Path('/home/jhoward/.fastai/data/pascal_2007/test.json'),Path('/home/jhoward/.fastai/data/pascal_2007/test'),Path('/home/jhoward/.fastai/data/pascal_2007/train.json'),Path('/home/jhoward/.fastai/data/pascal_2007/test.csv'),Path('/home/jhoward/.fastai/data/pascal_2007/models'),Path('/home/jhoward/.fastai/data/pascal_2007/segmentation'),Path('/home/jhoward/.fastai/data/pascal_2007/train.csv'),Path('/home/jhoward/.fastai/data/pascal_2007/train')]

每张图像的标签信息存储在名为 train.csv 的文件中。我们使用 pandas 加载它:

df = pd.read_csv(path/'train.csv')
df.head()
fname labels is_valid
0 000005.jpg chair True
1 000007.jpg car True
2 000009.jpg horse person True
3 000012.jpg car False
4 000016.jpg bicycle True

多标签分类 - 使用高级API

这很简单:对于每个文件名,我们获取不同的标签(用空格分隔),最后一列指示它是否在验证集中。为了快速将其放入DataLoaders中,我们有一个工厂方法from_df。我们可以指定所有图像的基础路径,一个额外的文件夹(在基础路径和文件名之间添加,这里是train),用于考虑验证集的valid_col(如果不指定,我们将取一个随机子集),一个用于分割标签的label_delim,以及之前的item_tfmsbatch_tfms

请注意,我们不必指定fn_collabel_col,因为它们默认分别是第一列和第二列。

dls = ImageDataLoaders.from_df(df, path, folder='train', valid_col='is_valid', label_delim=' ',
                               item_tfms=Resize(460), batch_tfms=aug_transforms(size=224))

与之前一样,我们可以使用 show_batch 方法查看数据。

dls.show_batch()

训练模型与以前一样简单:相同的函数可以应用,fastai库将自动检测到我们处于多标签问题,因此选择正确的损失函数。唯一不同的是我们传递的指标:error_rate 对于多标签问题无效,但我们可以使用 accuracy_threshF1ScoreMulti。我们还可以更改指标的默认名称,例如,我们可能希望看到使用 macrosamples 平均的F1分数。

f1_macro = F1ScoreMulti(thresh=0.5, average='macro')
f1_macro.name = 'F1(macro)'
f1_samples = F1ScoreMulti(thresh=0.5, average='samples')
f1_samples.name = 'F1(samples)'
learn = vision_learner(dls, resnet50, metrics=[partial(accuracy_multi, thresh=0.5), f1_macro, f1_samples])

和之前一样,我们可以使用 learn.lr_find 来选择一个合适的学习率:

learn.lr_find()
SuggestedLRs(lr_min=0.025118863582611083, lr_steep=0.03981071710586548)

我们可以选择建议的学习率,并对我们的预训练模型进行微调:

learn.fine_tune(2, 3e-2)
epoch train_loss valid_loss accuracy_multi time
0 0.437855 0.136942 0.954801 00:17
epoch train_loss valid_loss accuracy_multi time
0 0.156202 0.465557 0.914801 00:20
1 0.179814 0.382907 0.930040 00:20
2 0.157007 0.129412 0.953924 00:20
3 0.125787 0.109033 0.960856 00:19

和以前一样,我们可以轻松查看结果:

learn.show_results()

或者获取给定图像的预测:

learn.predict(path/'train/000005.jpg')
((#2) ['chair','diningtable'],
 TensorImage([False, False, False, False, False, False, False, False,  True, False,
          True, False, False, False, False, False, False, False, False, False]),
 TensorImage([1.6750e-03, 5.3663e-03, 1.6378e-03, 2.2269e-03, 5.8645e-02, 6.3422e-03,
         5.6991e-03, 1.3682e-02, 8.6864e-01, 9.7093e-04, 6.4747e-01, 4.1217e-03,
         1.2410e-03, 2.9412e-03, 4.7769e-01, 9.9664e-02, 4.5190e-04, 6.3532e-02,
         6.4487e-03, 1.6339e-01]))

对于单个分类预测,我们获得了三样东西。最后一个是模型对每个类别的预测(范围从0到1)。倒数第二个对应于一个独热编码的目标(对于所有预测的类别,概率大于0.5的类别为True),第一个是解码后的可读版本。

和之前一样,我们可以检查模型表现最差的地方:

interp = Interpretation.from_learner(learn)
interp.plot_top_losses(9)
target predicted probabilities loss
0 car;person;tvmonitor car tensor([7.2388e-12, 5.9609e-06, 1.7054e-11, 3.8985e-09, 7.7078e-12, 3.4044e-07,\n 9.9999e-01, 7.2118e-12, 1.0105e-05, 3.1035e-09, 2.3334e-09, 9.1077e-09,\n 1.6201e-09, 1.1083e-08, 1.0809e-02, 2.1072e-07, 9.5961e-16, 5.0478e-07,\n 4.4531e-10, 9.6444e-12]) 1.494603157043457
1 boat car tensor([8.3430e-06, 1.9416e-03, 6.9865e-06, 1.2985e-04, 1.6142e-06, 8.2200e-05,\n 9.9698e-01, 1.3143e-06, 1.0047e-03, 4.9794e-05, 1.9155e-05, 4.7409e-05,\n 7.5056e-05, 1.6572e-05, 3.4760e-02, 6.9266e-04, 1.3006e-07, 6.0702e-04,\n 1.5781e-05, 1.9860e-06]) 0.7395917773246765
2 bus;car car tensor([2.2509e-11, 1.0772e-05, 6.0177e-11, 4.8728e-09, 1.7920e-11, 4.8695e-07,\n 9.9999e-01, 9.0638e-12, 1.9819e-05, 8.8023e-09, 5.1272e-09, 2.3535e-08,\n 6.0401e-09, 7.2609e-09, 4.4117e-03, 4.8268e-07, 1.2528e-14, 1.2667e-06,\n 8.2282e-10, 1.6300e-11]) 0.7269787192344666
3 chair;diningtable;person person;train tensor([1.6638e-03, 2.0881e-02, 4.7525e-03, 2.6422e-02, 6.2972e-04, 4.7170e-02,\n 1.2263e-01, 2.9744e-03, 5.5352e-03, 7.1830e-03, 1.0062e-03, 2.6123e-03,\n 1.8208e-02, 5.9618e-02, 7.6859e-01, 3.3504e-03, 1.1324e-03, 2.3881e-03,\n 6.5440e-01, 1.7040e-03]) 0.6879587769508362
4 boat;chair;diningtable;person person tensor([0.0058, 0.0461, 0.0068, 0.1083, 0.0094, 0.0212, 0.4400, 0.0047, 0.0166,\n 0.0054, 0.0030, 0.0258, 0.0020, 0.0800, 0.5880, 0.0147, 0.0026, 0.1440,\n 0.0219, 0.0166]) 0.6826764941215515
5 bicycle;car;person car tensor([3.6825e-09, 7.3755e-05, 1.7181e-08, 4.5056e-07, 3.5667e-09, 1.0882e-05,\n 9.9939e-01, 6.0704e-09, 5.7179e-05, 3.8519e-07, 9.3825e-08, 6.1463e-07,\n 3.9191e-07, 2.6800e-06, 3.3091e-02, 3.1972e-06, 2.6873e-11, 1.1967e-05,\n 1.1480e-07, 3.3320e-09]) 0.6461981534957886
6 bottle;cow;person chair;person;sofa tensor([5.4520e-04, 4.2805e-03, 2.3828e-03, 1.4127e-03, 4.5856e-02, 3.5540e-03,\n 9.1525e-03, 2.9113e-02, 6.9326e-01, 1.0407e-03, 7.0658e-02, 3.1101e-02,\n 2.4843e-03, 2.9908e-03, 8.8695e-01, 2.2719e-01, 1.0283e-03, 6.0414e-01,\n 1.3598e-03, 5.7382e-02]) 0.6329519152641296
7 chair;dog;person cat tensor([3.4073e-05, 1.3574e-03, 7.0516e-04, 1.9189e-04, 6.0819e-03, 4.7242e-05,\n 9.6424e-04, 9.3669e-01, 9.0736e-02, 8.1472e-04, 1.1019e-02, 5.4633e-02,\n 2.6190e-04, 1.4943e-04, 1.2755e-02, 1.7530e-02, 2.2532e-03, 2.2129e-02,\n 1.5532e-04, 6.6390e-03]) 0.6249645352363586
8 car;person;pottedplant car tensor([1.3978e-06, 2.1693e-03, 2.2698e-07, 7.5037e-05, 9.4007e-07, 1.2369e-03,\n 9.9919e-01, 1.0879e-07, 3.1837e-04, 1.8340e-05, 7.5422e-06, 2.3891e-05,\n 2.5957e-05, 3.0890e-05, 8.4529e-02, 2.0280e-04, 4.1234e-09, 1.7978e-04,\n 2.3258e-05, 6.0897e-07]) 0.5489450693130493

多标签分类 - 使用数据块 API

我们也可以使用数据块 API 将数据加载到 DataLoaders 中。如前所述,如果您尚未准备好学习新的 API,可以随意跳过这一部分。

请记住我们的数据框中的数据结构:

df.head()
fname labels is_valid
0 000005.jpg chair True
1 000007.jpg car True
2 000009.jpg horse person True
3 000012.jpg car False
4 000016.jpg bicycle True

在这种情况下,我们通过提供以下内容来构建数据块:

  • 使用的类型:ImageBlockMultiCategoryBlock
  • 如何从我们的数据框中获取输入项:这里我们读取列 fname,并需要在开头添加 path/train/ 以获取正确的文件名。
  • 如何从我们的数据框中获取目标:这里我们读取列 labels,并需要通过空格进行分割。
  • 如何拆分项目,这里通过使用列 is_valid
  • 像之前一样的 item_tfmsbatch_tfms
pascal = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
                   splitter=ColSplitter('is_valid'),
                   get_x=ColReader('fname', pref=str(path/'train') + os.path.sep),
                   get_y=ColReader('labels', label_delim=' '),
                   item_tfms = Resize(460),
                   batch_tfms=aug_transforms(size=224))

这个块与之前略有不同:我们不需要传递一个函数来收集我们的所有项目,因为我们提供的数据框架已经包含了所有的项目。然而,我们确实需要对该数据框中的行进行预处理,以获取我们的输入,这就是我们传递 get_x 的原因。它默认使用 fastai 的 noop 函数,这就是我们之前不需要传递它的原因。

与之前一样,pascal 只是一个蓝图。我们需要将数据源传递给它,以便能够获取 DataLoaders

dls = pascal.dataloaders(df)

然后我们可以使用 dls.show_batch() 查看我们的一些图片。

dls.show_batch(max_n=9)

分割

分割是一个问题,我们需要为图像的每个像素预测一个类别。对于这个任务,我们将使用Camvid数据集,这是一个来自汽车摄像头的屏幕截图数据集。图像的每个像素都有一个标签,例如“道路”、“汽车”或“行人”。

和往常一样,我们可以使用我们的untar_data函数来下载数据。

path = untar_data(URLs.CAMVID_TINY)
path.ls()
(#3) [Path('/home/jhoward/.fastai/data/camvid_tiny/codes.txt'),Path('/home/jhoward/.fastai/data/camvid_tiny/images'),Path('/home/jhoward/.fastai/data/camvid_tiny/labels')]

images 文件夹包含图像,相应的标签分割掩码位于 labels 文件夹中。codes 文件包含对应类的整数(掩码为每个像素分配一个整数值)。

codes = np.loadtxt(path/'codes.txt', dtype=str)
codes
array(['Animal', 'Archway', 'Bicyclist', 'Bridge', 'Building', 'Car',
       'CartLuggagePram', 'Child', 'Column_Pole', 'Fence', 'LaneMkgsDriv',
       'LaneMkgsNonDriv', 'Misc_Text', 'MotorcycleScooter', 'OtherMoving',
       'ParkingBlock', 'Pedestrian', 'Road', 'RoadShoulder', 'Sidewalk',
       'SignSymbol', 'Sky', 'SUVPickupTruck', 'TrafficCone',
       'TrafficLight', 'Train', 'Tree', 'Truck_Bus', 'Tunnel',
       'VegetationMisc', 'Void', 'Wall'], dtype='<U17')

分割 - 使用高级API

与之前一样,get_image_files 函数帮助我们获取所有图像文件名:

fnames = get_image_files(path/"images")
fnames[0]
Path('/home/jhoward/.fastai/data/camvid_tiny/images/0006R0_f02910.png')

让我们来看看标签文件夹:

(path/"labels").ls()[0]
Path('/home/jhoward/.fastai/data/camvid_tiny/labels/0016E5_08137_P.png')

看起来分割掩码与图像具有相同的基本名称,但多了一个 _P,因此我们可以定义一个标签函数:

def label_func(fn): return path/"labels"/f"{fn.stem}_P{fn.suffix}"

我们可以使用 SegmentationDataLoaders 来收集我们的数据:

dls = SegmentationDataLoaders.from_label_func(
    path, bs=8, fnames = fnames, label_func = label_func, codes = codes
)

我们在这里不需要传递 item_tfms 来调整图像的大小,因为它们已经都是相同的尺寸。

和往常一样,我们可以使用 show_batch 方法查看我们的数据。在这个例子中,fastai 库使用每个像素特定颜色的方式叠加了掩码:

dls.show_batch(max_n=6)

传统的卷积神经网络(CNN)不适用于分割任务,我们必须使用一种特殊的模型,称为UNet,因此我们使用unet_learner来定义我们的Learner

learn = unet_learner(dls, resnet34)
learn.fine_tune(6)
epoch train_loss valid_loss time
0 2.802264 2.476579 00:03
epoch train_loss valid_loss time
0 1.664625 1.525224 00:03
1 1.440311 1.271917 00:02
2 1.339473 1.123384 00:03
3 1.233049 0.988725 00:03
4 1.110815 0.805028 00:02
5 1.008600 0.815411 00:03
6 0.924937 0.755052 00:02
7 0.857789 0.769288 00:03

和之前一样,我们可以通过 show_results 获取一些预测结果的概念。

learn.show_results(max_n=6, figsize=(7,8))

我们还可以使用 SegmentationInterpretation 类对验证集上的模型错误进行排序,然后绘制对验证损失贡献最大的 k 个实例。

interp = SegmentationInterpretation.from_learner(learn)
interp.plot_top_losses(k=3)

分割 - 使用数据块API

我们也可以使用数据块 API 将数据放入 DataLoaders 中。正如之前所说的,如果您对学习新的 API 还不太熟悉,请随意跳过这部分。

在这种情况下,我们通过提供以下内容来构建数据块:

  • 使用的类型:ImageBlockMaskBlock。我们为 MaskBlock 提供 codes,因为无法从数据中推测它们。
  • 如何收集我们的项,这里使用 get_image_files
  • 如何从我们的项目中获取目标:通过使用 label_func
  • 如何拆分项目,这里是随机拆分。
  • batch_tfms 用于数据增强。
camvid = DataBlock(blocks=(ImageBlock, MaskBlock(codes)),
                   get_items = get_image_files,
                   get_y = label_func,
                   splitter=RandomSplitter(),
                   batch_tfms=aug_transforms(size=(120,160)))
dls = camvid.dataloaders(path/"images", path=path, bs=8)
dls.show_batch(max_n=6)

点数

本节使用数据块 API,因此如果您之前跳过了它,我们建议您也跳过本节。

现在我们来看一个任务,我们想要预测图片中的点。为此,我们将使用 Biwi Kinect 头部姿态数据集。首先,我们像往常一样开始下载数据集。

path = untar_data(URLs.BIWI_HEAD_POSE)

让我们看看我们有什么!

path.ls()
(#50) [Path('/home/sgugger/.fastai/data/biwi_head_pose/01.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/18.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/04'),Path('/home/sgugger/.fastai/data/biwi_head_pose/10.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/24'),Path('/home/sgugger/.fastai/data/biwi_head_pose/14.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/20.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/11.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/02.obj'),Path('/home/sgugger/.fastai/data/biwi_head_pose/07')...]

有24个目录,编号从01到24(它们对应于被拍摄的不同人员),以及一个相应的.obj文件(我们在这里不需要它们)。我们将查看其中一个目录的内容:

(path/'01').ls()
(#1000) [Path('01/frame_00087_pose.txt'),Path('01/frame_00079_pose.txt'),Path('01/frame_00114_pose.txt'),Path('01/frame_00084_rgb.jpg'),Path('01/frame_00433_pose.txt'),Path('01/frame_00323_rgb.jpg'),Path('01/frame_00428_rgb.jpg'),Path('01/frame_00373_pose.txt'),Path('01/frame_00188_rgb.jpg'),Path('01/frame_00354_rgb.jpg')...]

在子目录中,我们有不同的帧,每个帧都有一张图像 (\_rgb.jpg) 和一个姿态文件 (\_pose.txt)。我们可以轻松地通过 get_image_files 递归获取所有图像文件,然后编写一个函数,将图像文件名转换为其关联的姿态文件。

img_files = get_image_files(path)
def img2pose(x): return Path(f'{str(x)[:-7]}pose.txt')
img2pose(img_files[0])
Path('04/frame_00084_pose.txt')

我们可以看看我们的第一张图片:

im = PILImage.create(img_files[0])
im.shape
(480, 640)
im.to_thumb(160)

Biwi数据集网站解释了与每个图像关联的姿势文本文件的格式,该文件显示了头部中心的位置。对于我们的目的,这些细节并不重要,因此我们只展示用于提取头部中心点的函数:

cal = np.genfromtxt(path/'01'/'rgb.cal', skip_footer=6)
def get_ctr(f):
    ctr = np.genfromtxt(img2pose(f), skip_header=3)
    c1 = ctr[0] * cal[0][0]/ctr[2] + cal[0][2]
    c2 = ctr[1] * cal[1][1]/ctr[2] + cal[1][2]
    return tensor([c1,c2])

此函数返回坐标作为一个包含两个项的张量:

get_ctr(img_files[0])
tensor([372.4046, 245.8602])

我们可以将这个函数作为get_y传递给DataBlock,因为它负责给每个项目标记。我们将把图像调整为输入大小的一半,以加快训练速度。

需要注意的一点是,我们不应仅仅使用随机分割。这样做的原因是,在这个数据集中,同一个人出现在多个图像中——但我们希望确保我们的模型能够推广到它尚未见过的人。数据集中的每个文件夹包含一个人的图像。因此,我们可以创建一个分割函数,该函数仅对一个人返回true,从而产生一个仅包含该人图像的验证集。

与之前的数据块示例的唯一区别是第二个块是一个PointBlock。这是必要的,以便fastai知道标签表示坐标;这样,它知道在进行数据增强时,应对这些坐标执行与图像相同的增强。

biwi = DataBlock(
    blocks=(ImageBlock, PointBlock),
    get_items=get_image_files,
    get_y=get_ctr,
    splitter=FuncSplitter(lambda o: o.parent.name=='13'),
    batch_tfms=[*aug_transforms(size=(240,320)), 
                Normalize.from_stats(*imagenet_stats)]
)
dls = biwi.dataloaders(path)
dls.show_batch(max_n=9, figsize=(8,6))

现在我们已经整理好我们的数据,我们可以像往常一样使用fastai API的其他部分。vision_learner 在这种情况下表现得非常好,库会从数据中推断出合适的损失函数:

learn = vision_learner(dls, resnet18, y_range=(-1,1))
learn.lr_find()

然后我们可以训练我们的模型:

learn.fine_tune(1, 5e-3)
epoch train_loss valid_loss time
0 0.057434 0.002171 00:31
epoch train_loss valid_loss time
0 0.005320 0.005426 00:39
1 0.003624 0.000698 00:39
2 0.002163 0.000099 00:39
3 0.001325 0.000233 00:39

损失是均方误差,这意味着我们平均会犯一个错误。

math.sqrt(0.0001)
0.01

预测我们得分时的百分比!我们可以像往常一样查看这些结果:

learn.show_results()