天天看點

如何用PyTorch處理人臉姿态的資料?

一、資料加載和處理教程

在解決機器學習問題時, 我們需要付出很多努力來準備資料, 為了使代碼更具可讀性, PyTorch提供了許多工具來使資料加載變得簡單易行。在本教程中, 我們将要學習如何對 一個重要的資料集進行加載、預處理資料增強。

為了運作這個教程,請确認下列包已安裝:

  • scikit-image :用來讀取圖檔和圖像變換
  • pandas: 更友善地解析csv檔案
from __future__ import print_function,division

import os
import torch
import pandas as pd
from skimage import io,transform
import numpy as np
import matplotlib.pyplot as plt

from torch.utils.data import Dataset,DataLoader
from torchvision import transforms,utils

# 忽略警告
import warnings

warnings.filterwarnings('ignore')

#開啟交換模式
plt.ion()           

我們将要處理的資料是人臉姿态。這就是對人臉的表示如下:

如何用PyTorch處理人臉姿态的資料?

每張人臉圖像上, 總共有68個不同的标注點被标記出來。

提示

打開

http://t.cn/EaOQdfy

下載下傳資料集, 這些圖像在目錄 'faces/'下. 這個資料集實際上是從imagenet資料集中選取标記為人臉的一些圖檔, 使用'dlib’s pose estimation 方法生成的.

### 下載下傳圖檔資料
 
import os
import os.path
import errno
url ='https://download.pytorch.org/tutorial/faces.zip'
filename='faces.zip'
 
def download(root):
    '''
    下載下傳資料人臉圖像和标注點的壓縮包。
    使用zipfile包解壓。
    '''
    root = os.path.expanduser(root)
    import zipfile
    
    #下載下傳圖檔壓縮包到指定路徑
    download_url(url,root,filename)
    
    print("資料下載下傳完畢!")
    #獲得目前路徑
    cwd = os.getcwd()    
    path = os.path.join(root, filename)
    tar = zipfile.ZipFile(path, "r")
    #解壓檔案
    tar.extractall(root)
    tar.close()
    #切換到目前工作路徑
    os.chdir(cwd)
 
def download_url(url, root, filename):
    from six.moves import urllib
    root = os.path.expanduser(root)
    fpath = os.path.join(root, filename)
    
    try:
        os.makedirs(root)
    except OSError as e:
        if e.errno == errno.EEXIST:
            pass
        else:
            raise
    
    # downloads file
    if os.path.isfile(fpath) :
        print('使用已下載下傳檔案: ' + fpath)
    else:
        try:
            print('下載下傳 ' + url + ' 到 ' + fpath)
            urllib.request.urlretrieve(url, fpath)
        except:
            if url[:5] == 'https':
                url = url.replace('https:', 'http:')
                print('下載下傳失敗。嘗試将https -> http'
                      ' 下載下傳  ' + url + ' 到 ' + fpath)
                urllib.request.urlretrieve(url, fpath)
 
download('./')           
如何用PyTorch處理人臉姿态的資料?

資料集中的csv檔案記錄着标注資訊, 像下面這樣:

image_name,part_0_x,part_0_y,part_1_x,part_1_y,part_2_x, ... ,part_67_x,part_67_y

0805personali01.jpg,27,83,27,98, ... 84,134

1084239450_e76e00b7e7.jpg,70,236,71,257, ... ,128,312           

讓我們快速地讀取CSV檔案, 以(N,2)的數組形式獲得标記點, 其中N表示标記點的個數.

  1. 讀取标記點CSV檔案,使用pandas的read_csv方法。
landmarks_frame = pd.read_csv('faces/face_landmarks.csv')           
  1. 觀察資料的結構,可以使用headinfo等方法,head()預設傳回前5行,info()顯示資料名稱和數量。
landmarks_frame.info()           
如何用PyTorch處理人臉姿态的資料?
landmarks_frame.head()           
如何用PyTorch處理人臉姿态的資料?

由上表可知,每行表示一張圖檔和标記點的X、Y坐标,共有68個标記點。

  1. 已第4張圖檔為例。首先獲得圖檔名稱,其次擷取所有标記點資訊,首先轉換成行向量(1×136),最後向量轉換成N行2列的浮點型矩陣。
n = 3
img_name = landmarks_frame.iloc[n, 0]
landmarks = landmarks_frame.iloc[n, 1:].as_matrix()
landmarks = landmarks.astype('float').reshape(-1, 2)

print('Image name: {}'.format(img_name))
print('Landmarks shape: {}'.format(landmarks.shape))
print('First 4 Landmarks: {}'.format(landmarks[:4]))           
如何用PyTorch處理人臉姿态的資料?

我們寫一個函數來顯示一張圖檔和它的标記點, 然後用這個函數來顯示一個樣本。

def show_landmarks(image, landmarks):
    """顯示帶标記點的圖檔"""
    plt.imshow(image)
    plt.scatter(landmarks[:, 0], landmarks[:, 1], s=10, marker='.', c='r')
    plt.pause(0.001)  # 暫停一下, 使plots更新

plt.figure()
show_landmarks(io.imread(os.path.join('faces/', img_name)),
               landmarks)
plt.show()
           
如何用PyTorch處理人臉姿态的資料?

二、Dataset 類

orch.utils.data.Dataset 是一個表示資料集的抽象類。你自定義dataset類需繼承Dataset并重寫下列方法:

__ len __ 使用len(dataset)可以傳回資料集的大小。

__ getitem __ 支援索引, 以便于使用 dataset[i] 可以 擷取第i個樣本。           

讓我們為landmars 資料集建構一個dataset類。我們将在__ init 中讀取csv檔案,在 getitem __ 中讀取圖像。這可以高效利用記憶體,因為所有圖像不會立即存儲在記憶體中,而是根據需要讀取。

我們的資料集樣本将是一個dict {'image':image,'landmarks':landmarks}。我們的資料集将采用可選的參數變換,以便可以對樣本應用任何所需的處理。我們将在下一節中看到變換的有用性。

class FaceLandmarksDataset(Dataset):
    ''' Face Landmarks Dataset '''
    def __init__(self,csv_file,root_dir,transform=None):
        """
        Args:
            csv_file (string): Path to the csv file with annotations.
            root_dir (string): Directory with all the images.
            transform (callable, optional): Optional transform to be applied
                on a sample.
        """
        self.landmarks_frame = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = transform
    
    def __len__(self):
        return len(self.landmarks_frame)
    
    def __getitem__(self,idx):
        #獲得指定索引圖檔的路徑
        img_name = os.path.join(self.root_dir,
                                self.landmarks_frame.iloc[idx,0])
        #讀取圖檔資料
        image = io.imread(img_name)
        #将标注點資料轉換成矩陣(行向量)
        landmarks = self.landmarks_frame.iloc[idx,1:].as_matrix()
        #将矩陣轉換成N行2列的矩陣。
        landmarks = landmarks.astype('float').reshape(-1,2)
        
        sample = {'image':image,'landmarks':landmarks}
        
        if self.transform:
            sample = self.transform(sample)
        
        return sample           

我們執行個體化這個資料集并疊代資料樣本。我們将列印前4個樣本并顯示它們的标注點。

face_dataset = FaceLandmarksDataset(csv_file='faces/face_landmarks.csv',
                                    root_dir='faces/')

fig = plt.figure()

for i in range(len(face_dataset)):
    sample = face_dataset[i]

    print(i, sample['image'].shape, sample['landmarks'].shape)

    ax = plt.subplot(1, 4, i + 1)
    plt.tight_layout()
    ax.set_title('Sample #{}'.format(i))
    ax.axis('off')
    show_landmarks(**sample)

    if i == 3:
        plt.show()
        break           
如何用PyTorch處理人臉姿态的資料?

三、Transforms

我們從上面可以發現一個問題是樣本的尺寸(指寬和高)不相同。大部分的神經網絡都期望一個固定尺寸的圖像。是以,我們需要寫一些預處理的代碼。

讓我們寫三個 transform:

Rescale: 縮放圖像

RandomCrop: 從圖像中随機裁剪。這是資料增加。

ToTensor: 将numpy形式的圖像資料轉換成torch形式的圖像資料(我們需要交換軸)。

我們将它們編寫為可調用類而不是簡單函數,這樣每次調用時都不需要傳遞 transform 的參數。是以,我們隻需要實作 call 方法,同時如果需要,也可實作 init 方法。我們可以像這樣使用 transform:

tsfm = Transform(params)

transform_sample = tsfm(sample)

請注意以下這些 transforms 必須如何應用于圖像和landmarks。

class Rescale(object):
    '''
    将樣本圖像縮放至給定尺寸。
    Args:
        output_size(tuple or int):期望的輸出尺寸。如果是tuple,
        則輸出與output_size比對。如果是int,則較小的圖像邊緣
        與output_size比對,保持縱橫比相同。
    '''
    
    def __init__(self,output_size):
        assert isinstance(output_size,(int,tuple))
        self.output_size = output_size
    
    def __call__(self,sample):
        image ,landmarks = sample['image'],sample['landmarks']
        
        h , w = image.shape[:2]
        
                
        if isinstance(self.output_size, int):            
            if h > w:
                # new_h/new_w = h/w ,new_w = output_size
                new_h , new_w = self.output_size * h / w , self.output_size
            else:
              # 與上面的相反
                new_h , new_w = self.output_size, self.output_size * w / h
        
        else:
            # 直接以給定的資料為新的w、h
            new_h , new_w = self.output_size
        
        new_h , new_w = int(new_h) , int(new_w)
        #使用skimage包中tansform類的resize方法縮放圖像。
        img = transform.resize(image,(new_h,new_w))
        
        # 将landmarks的位置按照圖像的縮放比較,就行縮放。
        landmarks = landmarks * [new_w / w, new_h / h]
        
        return {'image': img, 'landmarks': landmarks}
                
import numpy as np
class RandomCrop(object):
    '''
    随機修剪樣本圖像。
    
    Args:
        output_size(tuple or int):期望的輸出尺寸。如果為int,
        則進行方形裁剪。
    '''
    
    def __init__(self,output_size):
        assert isinstance(output_size, (int, tuple))
        if isinstance(output_size, int):
            self.output_size = (output_size, output_size)
        else:
            assert len(output_size) == 2
            self.output_size = output_size
   
    
    def __call__(self,sample):
        image, landmarks = sample['image'], sample['landmarks']
        
        #獲得圖像的尺寸。
        h, w = image.shape[:2]
        new_h, new_w = self.output_size

        top = np.random.randint(0, h - new_h)
        left = np.random.randint(0, w - new_w)

        image = image[top: top + new_h,
                      left: left + new_w]
        landmarks = landmarks - [left, top]

        return {'image': image, 'landmarks': landmarks}
      

                
class ToTensor(object):
    """Convert ndarrays in sample to Tensors."""

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        # 交換顔色的軸
        # numpy image: H x W x C
        #軸對應編号:0,1,2
        # torch image: C X H X W
        #軸對應編号:2,0,1
        
        '''
        transpose()的操作對象是矩陣。
        我們用一個例子來說明這個函數:
        [[[0 1] 
        [2 3]]

        [[4 5] 
        [6 7]]]

        這是一個shape為(2,2,2)的矩陣,現在對它進行transpose操作。
        首先我們對矩陣的次元進行編号,上述矩陣有三個次元,則編号分别為0,1,2,而transpose函數的參數輸         入就是基于這個編号的,如果我們調用transpose(0,1,2),那麼矩陣将不發生變化,如果我們不輸入參         數,直接調用transpose(),其效果就是将矩陣進行轉置,起作用等價與transpose(2,1,0)。

        在舉個例子,對上面那個矩陣調用transpose(0,2,1) 
        下面為結果 
        [[[0 2] 
        [1 3]]

        [[4 6] 
        [5 7]]] 
        其實就是矩陣中每個元素按照一樣的規則進行位置變換。
        '''
        
        image = image.transpose((2, 0, 1))
        return {'image': torch.from_numpy(image),
                'landmarks': torch.from_numpy(landmarks)}           

Compose transforms

現在,我們已經在樣本上使用了 transforms。讓我們看看,我們想将圖像的短邊縮短至256在随機從圖像中裁剪一個224的方形。我們想組成Rescale 和RandomCrop的聯合transforms。torchvision.transforms.Compose是一個允許我們做這些的簡單可調用類。

scale = Rescale(256)
crop = RandomCrop(100)
composed = transforms.Compose([Rescale(256),
                               RandomCrop(102)])

# Apply each of the above transforms on sample.
fig = plt.figure()
sample = face_dataset[65]
for i, tsfrm in enumerate([scale, crop, composed]):
    transformed_sample = tsfrm(sample)

    ax = plt.subplot(1, 3, i + 1)
    plt.tight_layout()
    ax.set_title(type(tsfrm).__name__)
    show_landmarks(**transformed_sample)

plt.show()           
如何用PyTorch處理人臉姿态的資料?

四、疊代資料

Iterating through the dataset

讓我們把這些組合在一起建立一個包含組合轉換器(composed transforms)的資料集(dataset)。

  • 總而言之, 每次取樣資料集時:
  • 即時從檔案中讀取圖像
  • 變換應用于讀取的圖像
  • 由于其中一個變換是随機的,是以在采樣時會增加資料
transformed_dataset = FaceLandmarksDataset(csv_file='faces/face_landmarks.csv',
                                    root_dir='faces/',
                                    transform =transforms.Compose([
                                        Rescale(256),
                                        RandomCrop(204),
                                        ToTensor(),
                                    ]))
for i in range(len(transformed_dataset)):
    sample = transformed_dataset[i]
    
    print(i,sample['image'].size(),sample['landmarks'].size())
    
    if i == 3:
     break           
如何用PyTorch處理人臉姿态的資料?

然而,我們使用簡單的for循環來疊代資料将會就丢失很多特性。事實上,我們錯過了:

  • 批處理資料
  • 打亂(洗牌)資料
  • 使用multiprocessing作業,并行加載資料。

torch.utils.data.DataLoader 是提供了所有特性的疊代器。下面使用的參數應該是清楚的。感興趣的一個參數是collate_fn。您可以使用collate_fn指定需要批量處理樣品的準确程度。但是,對于大多數用例,預設整理應該可以正常工作。

#定義資料加載器,資料需要打亂,每次取4個樣本。
#num_workers,我暫時無法了解。
dataloader = DataLoader(transformed_dataset,batch_size=4,
                        shuffle=True,num_workers=4)

# 幫助類,展示一個批次的資料。

def show_landmarks_batch(sample_batched):
    '''
    顯示一個批次的圖像。
    '''
    images_batch , landmarks_batch = \
        sample_batched['image'],sample_batched['landmarks']
    
    batch_size = len(sample_batched)
    
    im_size = images_batch.size(2)
    
    grid = utils.make_grid(images_batch)
    
    plt.imshow(grid.numpy().transpose((1,2,0)))
    
    for i in range(batch_size):
        plt.scatter(landmarks_batch[i, :, 0].numpy() + i * im_size,
                    landmarks_batch[i, :, 1].numpy(),
                    s=10, marker='.', c='r')
        plt.title('Batch from dataloader')
        
        
for i_batch,sample_batched in enumerate(dataloader):
    print(i_batch, sample_batched['image'].size(),
          sample_batched['landmarks'].size())

    # observe 4th batch and stop.
    if i_batch == 3:
        plt.figure()
        show_landmarks_batch(sample_batched)
        plt.axis('off')
        plt.ioff()
        plt.show()
        break           
如何用PyTorch處理人臉姿态的資料?

後記:torchvision

在這個教程中, 我們學習了如何寫和使用資料集, 圖像變換和dataloder. torchvision 提供了常用的資料集和圖像變換, 或許你甚至不必寫自定義的類和變換. 在torchvision中一個最經常用的資料集是ImageFolder. 它要求資料按下面的形式存放:

/root/hymenoptera_data/ants/xxx.png

/root/hymenoptera_data/ants/xxy.jpeg

/root/hymenoptera_data/ants/xxz.png

.

/root/hymenoptera_data/bees/123.jpg

/root/hymenoptera_data/bees/nsdf3.png

/root/hymenoptera_data/bees/asd932_.png

‘ants’, ‘bees’ 等是圖像的類标. 同樣, PIL.Image 中出現的一般的圖像變換像 RandomHorizontalFlip, Scale 也是可以使用的. 你可以像下面這樣用這些函數來寫dataloader:

import torch
from torchvision import transforms, datasets

data_transform = transforms.Compose([
        transforms.RandomSizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406],
                             std=[0.229, 0.224, 0.225])
    ])
hymenoptera_dataset = datasets.ImageFolder(root='./root/hymenoptera_data/train',
                                           transform=data_transform)
dataset_loader = torch.utils.data.DataLoader(hymenoptera_dataset,
                                             batch_size=4, shuffle=True,
                                             num_workers=4)           

⭐總結

自定義dataset。首先需要繼承torch.utils.data.Dataset 類;其次要實作__ len (使用len(dataset)可以傳回資料集的大小)和 getitem __ (支援索引, 以便于使用 dataset[i] 可以 擷取第i個樣本)方法。

自定義transform。首先要要繼承object類;其次實作__ call 方法,需要時可實作 init __ 方法。