Pytorch tutorial 之Datar Loading and Processing (1)


引自Pytorch tutorial:  Data Loading and Processing Tutorial

這節主要介紹數據的讀入與處理。

數據描述:人臉姿態數據集。共有69張人臉,每張人臉都有68個點 。可視化其中一張如下:

 

.數據讀取

這些圖像名字與散點坐標存於 face_landmarks.csv 文件中,所以需要利用pandas庫來分析。

 引入需要的庫:

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 # Ignore warnings
import warnings warnings.filterwarnings("ignore") plt.ion() # interactive mode
View Code

利用pandas分析數據:

landmarks_frame = pd.read_csv('/faces/face_landmarks.csv') 
landmarks_frame.info()

輸出:

 

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 69 entries, 0 to 68
Columns: 137 entries, image_name to part_67_y
dtypes: int64(136), object(1)
memory usage: 73.9+ KB

可以看到共有69行,即69個人臉,137列,其中第一列為圖片名稱,后136列依次為散點x,y坐標。所以有136/2=68個點。所以這些散點我們我們應將其reshape為(68,2)的形狀,即第一列為散點橫坐標,第二列為縱坐標。然后我們試着查看前四個點:

landmarks_frame = pd.read_csv('faces/face_landmarks.csv') n = 65 img_name = landmarks_frame.iloc[n, 0]                      #查看第65張照片名
landmarks = landmarks_frame.iloc[n, 1:].as_matrix()        # 將后136列reshape為橫縱坐標形式
landmarks = landmarks.astype('float').reshape(-1, 2)       # 68行2列

print('Image name: {}'.format(img_name)) print('Landmarks shape: {}'.format(landmarks.shape)) print('First 4 Landmarks: {}'.format(landmarks[:4]))      # 查看前四個點

輸出:

Image name: person-7.jpg Landmarks shape: (68, 2) First 4 Landmarks: [[ 32. 65.] [ 33. 76.] [ 34. 86.] [ 34. 97.]]

 然后查看一張加了landmark的圖片demo:

def show_landmarks(image, landmarks): """Show image with landmarks""" plt.imshow(image) plt.scatter(landmarks[:, 0], landmarks[:, 1], s=10, marker='.', c='r') plt.pause(0.001)  # pause a bit so that plots are updated
 plt.figure() show_landmarks(io.imread(os.path.join('faces/', img_name)), landmarks) plt.show()

 

.Dataset class

class torch.utils.data.Dataset

這個類是表示數據集的抽象類,所有其他數據集都應該進行子類化。如果你要定制自己的dataset,那么一定要集成此類,並重載以下兩個方法:

__len__ :    __len__返回數據集的大小,用法:len(dataset

__getitem__ :__getitem__方法支持整數索引,范圍從0到len(self),用法:dataset[i]得到索引為i的樣本及標簽

下面我們將定制自己的dataset, 首先當然是繼承Dataset, 然后在__init__函數中實現csv數據讀入,但是在_getitem__中實現讀入圖片,這很高效,因為所有數據不必都一次性讀入到內存中,需要的時候再讀取。還要注意的是我們的dataset形式是字典,其鍵為image和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): # 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() landmarks = landmarks.astype('float').reshape(-1, 2) sample = {'image': image, 'landmarks': landmarks} # 為方便返回字典形式,其他形式也可以 if self.transform: sample = self.transform(sample) # 可以實現裁剪縮放等數據轉換(transform類是有__call__方法的) # 所以就可以利用函數形式transform(sample)來進行變換 return sample

然后我們實例化此類,就可以調用len(dataset)和 dataset[i](相當於調用dataset.__getitem__(i))

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] # 因為有__getitem__ 方法,所以可以查看索引,返回字典,即第i個樣本的image和landmarke
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) # 因為sample為字典,所以可以利用這種形式返回字典中所有鍵對應的值 if i == 3: plt.show() break

我們簡單看一下結果:

0 (324, 215, 3) (68, 2)
1 (500, 333, 3) (68, 2)
2 (250, 258, 3) (68, 2)
3 (434, 290, 3) (68, 2)

可以看到依次返回了四張大小不一的圖,以及其landmark。

 

三. Transforms

上面返回的圖都是原始圖像,大小不一,所以一般來說不會直接輸入到卷積網絡。上面我們在實現自己的dataset類時,可以傳入參數transform, 下面我們看一看如何實現transform,並傳入到dataset。

預處理操作主要有:

  • Rescale: 規范圖像尺寸
  • RandomCrop:隨機裁剪,一種數據增強手段
  • ToTensor:將numpy格式的圖像數據轉換為torch的FloatTensor格式,注意同時要轉換維度(w,h,c   -- 》  c,w,h)

這里我們將預處理操作都寫成可call的類,而不寫成函數,這樣transform的參數就不必每次調用時都傳遞。那么我們需要引入__call__方法,如果需要的話也有__init__方法。

引入__call__方法的類可以當作一個函數使用

tsfm = Transform(params) # 實例化一個含有_call__方法的transform類 transformed_sample = tsfm(sample)     # 此時tsfm為一個實例化后的對象,它可以作為一個函數來用,此時函數的輸入便為sample!

下面我們看看這三個transform類的具體實現:

class Rescale(object): # 第一個類規范圖像尺寸 """Rescale the image in a sample to a given size. Args: output_size (tuple or int): Desired output size. If tuple, output is matched to output_size. If int, smaller of image edges is matched to output_size keeping aspect ratio the same. """

    def __init__(self, output_size): # 此類需傳入的參數為圖像輸出大小 assert isinstance(output_size, (int, tuple)) # 這個size可以為int例如256,也可以為tuple,例如(256,256) 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): # 當輸出size為int時,將此值作為圖像的最短邊長,而長邊則需根據比例進行縮放 if h > w: 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: # 當輸出為tuple時,直接將此tuple作為圖像輸出尺寸 new_h, new_w = self.output_size new_h, new_w = int(new_h), int(new_w) img = transform.resize(image, (new_h, new_w)) # h and w are swapped for landmarks because for images,
        # x and y axes are axis 1 and 0 respectively
        landmarks = landmarks * [new_w / w, new_h / h] return {'image': img, 'landmarks': landmarks} # 注意__getitem__返回的是字典,所以這里也要返回字典 class RandomCrop(object): #第二個類隨機裁剪 """Crop randomly the image in a sample. Args: output_size (tuple or int): Desired output size. If int, square crop is made. """

    def __init__(self, output_size): # 此類需傳入輸出尺寸 assert isinstance(output_size, (int, tuple)) if isinstance(output_size, int): # 如果為int例如256則返回任意(256,256)大小的圖 self.output_size = (output_size, output_size) else: assert len(output_size) == 2                # 如果為tuple例如(211,985),則返回(211,985)大小的圖 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): # 第三個類轉numpy為tensor """Convert ndarrays in sample to Tensors."""

    def __call__(self, sample): # 無需init方法,直接將此類作為函數 image, landmarks = sample['image'], sample['landmarks'] # swap color axis because
        # numpy image: H x W x C
        # torch image: C X H X W
        image = image.transpose((2, 0, 1)) # 轉換維度,按照torch格式來 return {'image': torch.from_numpy(image), 'landmarks': torch.from_numpy(landmarks)}

ok,這三個預處理類實現完畢,這時可以在我們的dataset類中進行調用了!我們先在sample上檢驗一下:

我們將令短邊長為256, 隨機裁剪256×256大小的圖片, 當然還可以利用compose類同時結合這兩個操作!

scale = Rescale(256) # 實例化第一個類,此時該對象可當做函數使用 crop = RandomCrop(128) # 實例化第二個類,此時該對象可當做函數使用 composed = transforms.Compose([Rescale(256), # 結合兩個方法 RandomCrop(224)]) # 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) # sample作為參數傳入了函數里面,返回image、landmark字典 ax = plt.subplot(1, 3, i + 1) plt.tight_layout() ax.set_title(type(tsfrm).__name__) show_landmarks(**transformed_sample) # 調用之前的函數進行顯示 plt.show()

此外需要注意的是,在前兩個transform類的實現中,我們相應的對label也做了變換!而一般情況下我們只需對data做變換即可,這也體現了Pytorch的靈活,私人訂制。 

 

 

四. Iterating through the dataset 

根據上文實現的transform,現在我們可以將其放到我們定制的dataset類里面。

每當我們的dataset被采樣時便會讀取一張圖片、接着進行transform:

transformed_dataset = FaceLandmarksDataset(csv_file='faces/face_landmarks.csv', root_dir='faces/', transform=transforms.Compose([ Rescale(256), RandomCrop(224), ToTensor() ])) # 實例化我們定制的dataset! for i in range(len(transformed_dataset)): sample = transformed_dataset[i] # for循環, 每次采樣索引為i的一張圖片 print(i, sample['image'].size(), sample['landmarks'].size()) if i == 3: # 查看4張圖就好 break

 

0 torch.Size([3, 224, 224]) torch.Size([68, 2]) 1 torch.Size([3, 224, 224]) torch.Size([68, 2]) 2 torch.Size([3, 224, 224]) torch.Size([68, 2]) 3 torch.Size([3, 224, 224]) torch.Size([68, 2])

 

看啊,上面依靠for循環,才能每次索引一張圖,那么我們需要batch批量數據讀入shuffle打散數據multiprocessing並行處理該咋整?!

torch.utils.data.DataLoader 為我們提供好了一切,它有一個有趣的參數 collate_fn 可以實現你想要的batch形式。這里只需基本用法就足夠了:

dataloader = DataLoader(transformed_dataset, batch_size=4, # batch為4張,打散,進程數為4 shuffle=True, num_workers=4) # Helper function to show a batch
def show_landmarks_batch(sample_batched): # 顯示一個batch數據的函數,主要利用工具函數make_grid """Show image with landmarks for a batch of samples.""" images_batch, landmarks_batch = \ sample_batched['image'], sample_batched['landmarks'] batch_size = len(images_batch) im_size = images_batch.size(2) grid = utils.make_grid(images_batch) # 其輸入為FLoatTensor plt.imshow(grid.numpy().transpose((1, 2, 0))) # 只有當畫圖的時候才轉為numpy並轉換維度 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: # 只打印第4個batch plt.figure() show_landmarks_batch(sample_batched) plt.axis('off') plt.ioff() plt.show() break

 

0 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2]) 1 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2]) 2 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2]) 3 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])

看到每個batch都有4張圖

 

 

 

五. torchvision

其實這些transform類、datasets在torchvision包中都有,一般情況下可能無需定制,另一種生成dataset的方式我們在前文中已經介紹過了:ImageFolder(torchvision.datasets.ImageFolder)

它也繼承自Dataset類。所以也有len(dataset)或dataset.__len__()和 dataset[i]或dataset.__getitem__(i))方法。

但其要求圖片的存放為以下格式:

root/ants/xxx.png root/ants/xxy.jpeg root/ants/xxz.png . . . root/bees/123.jpg root/bees/nsdf3.png root/bees/asd932_.png

每一類圖片單獨存於一個文件夾,文件夾名字ants、bees等即為類別名labels!

好了,看一下其實例吧:

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='hymenoptera_data/train', transform=data_transform) dataset_loader = torch.utils.data.DataLoader(hymenoptera_dataset, batch_size=4, shuffle=True, num_workers=4)

另一點有趣的是:除了上述操作之外,torchvision中transforms還可通過Lambda封裝自定義的轉換策略。例如想對PIL Image進行隨機旋轉,則可寫成這樣trans=T.Lambda(lambda img: img.rotate(random()*360))。因為trans也是transforms類的實例化,因為此類有__call__()方法,所以可以直接利用函數形式trans(img)來轉換數據!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM