想學習MXNet的同學建議看一看這位博主的博客,受益良多。
在本節中,我們將學習如何在MXNet中預處理和加載圖像數據。
在MXNet中加載圖像數據有4種方式。
- 使用 mx.image.imdecode 加載原始數據文件
- 使用在Python中實現的mx.img.ImageIter ,很方便自定義。 它可以從.rec(RecordIO)文件和原始圖像文件讀取。
- 使用C ++實現的MXNet后端的mx.io.ImageRecordIter 。 對於自定義不太靈活,但提供了多種語言綁定。
- 創建自定義的迭代器,繼承mx.io.DataIter
預處理圖像的方式有多種,我們列舉其中的幾種:
- 使用mx.io.ImageRecordIter ,快速但不是很靈活。 對於像圖像識別這樣的簡單任務來說,這是非常好的,但是對於更復雜的任務(如檢測和分割)來說,不是很有用
- 使用mx.recordio.unpack_img(或cv2.imread,skimage等)+ numpy。由於Python 全局解析鎖(GIL),靈活但是緩慢。
- 使用MXNet提供的mx.image 包。它以NDArray 格式存儲圖像,並利用MXNet的依賴引擎來自動並行化處理並規避GIL。
一、mx.image 包常用預處理
import matplotlib.pyplot as plt img = mx.image.imdecode(open('test.jpeg', 'rb').read()) plt.imshow(img.asnumpy()); plt.show() img.shape
# resize to w x h tmp = mx.image.imresize(img, 100, 70) plt.imshow(tmp.asnumpy()); plt.show()
# crop a random w x h region from image tmp, coord = mx.image.random_crop(img, (150, 200)) print(coord) plt.imshow(tmp.asnumpy()); plt.show()
二、MXNet存儲格式轉換工具:im2rec
MXNet框架用於做圖像相關的項目時,讀取圖像主要有兩種方式:
- 第一種是讀.rec格式的文件,類似Caffe框架中LMDB,優點是.rec文件比較穩定,移植到別的電腦上也能復現,缺點是占空間(.rec文件的大小基本上和圖像的存儲大小差不多),而且增刪數據不大靈活。需要idx搭配使用,下面腳本會一並生成。
- 第二種是.lst和圖像結合的方式,首先在前面生成.rec文件的過程中也會生成.lst文件,這個.lst文件就是圖像路徑和標簽的對應列表,也就是說通過維護這個列表來控制你訓練集和測試集的變化,優點是靈活且不占空間,缺點是如果圖像格式不符合要求的話容易出錯而且如果列表中的某些圖像路徑對應的圖像文件夾中圖像被刪除,就尋找不到,另外如果你不是從固態硬盤上讀取圖像的話,速度會很慢。
1.生成.lst
需要准備的就是你的圖像。假設你的圖像數據放在/home/image文件夾下,一共有10個類別,那么在/home/image文件夾下應該有10個子文件夾,每個子文件夾放屬於這個類的圖像文件,你可以用英文名命名這些子文件夾來表達類別,這個都無所謂,即便用1到10這10個數字來分別命名這10個子文件夾也沒什么,只不過用英文名會方便你記憶這個文件夾包含的圖像是屬於哪個類別的。另外假設你要將生成的.lst文件放在/home/lst文件夾下,你的mxnet項目的路徑是~/incubator-mxnet,那么運行下面的命令就可以生成.lst文件:
python ~/incubator-mxnet/tools/im2rec.py --list True // list參數必須要是True,說明你是要生成.lst文件 --recursive True // recursive參數必須為True,表示要將所有圖像路徑寫進成.lst文件 --train-ratio 0.9 /home/lst/data /home/image // train-ratio參數表示將train和val以多少比例划分,默認為1,表示都是train的數據
這樣在/home/lst文件夾下就會生成data_train.lst和data_val.lst兩個文件。
.lst文件樣例:第一列是index,第二列是label,第三列是圖像路徑
當然有時候可能你的數據圖像不是按照一個類別放在一個文件夾這種方式,那么就要考慮修改這個腳本來生成相同格式的.lst文件才能用於后續生成.rec文件。
2.生成.rec
python ~/incubator-mxnet/tools/im2rec.py --num-thread 4 /home/lst /home/image
需要准備的就是第一步生成的.lst文件和你的圖像。
倒數第二個參數:/home/lst是你的.lst文件所放的路徑,可以不用指明.lst文件名稱,因為代碼會自動搜索/home/lst文件夾下所有以.lst結尾的文件。
最后一個參數:/home/image是你的圖像所放的路徑。
–num-thread 4 這個參數是表示用4個線程來執行,當你數據量較大的時候,生成.rec的過程會比較慢,所以這樣可以加速。
3.使用python腳本完成數據生成
假設你要將生成的.rec文件放在.lst文件相同的/home/lst文件夾下(一般都會這樣操作),那么運行下面的命令就可以生成.rec文件:
在了解如何使用兩個內置Image迭代器讀取數據之前,需要將其轉換為記錄rec格式。
mxnet提供工具im2rec.py
import subprocess im2rec_path = mx.test_utils.get_im2rec_path() # im2rec腳本路徑 data_path = os.path.join('data','101_ObjectCategories') # 圖像加載路徑(里面是不同類別的文件夾) prefix_path = os.path.join('data','caltech') # 文件生成路徑 with open(os.devnull, 'wb') as devnull: subprocess.check_call(['python', im2rec_path, '--list', '--recursive', '--test-ratio=0.2', prefix_path, data_path], stdout=devnull) print(im2rec_path, '\n', data_path, '\n', prefix_path)
/home/hellcat/anaconda3/lib/python3.6/site-packages/mxnet/tools/im2rec.py
data/101_ObjectCategories
data/caltech
三、圖像數據讀取
MXNet的圖像數據導入模塊主要有mxnet.io.ImageRecordIter和mxnet.image.ImageIter兩個類,前者主要用來讀取.rec格式的數據,后者既可以讀.rec格式文件,也可以讀原圖像數據。
Using ImageRecordIter
ImageRecordIter
can be used for loading image data saved in record io format. To use ImageRecordIter, simply create an instance by loading your record file:
# ImageRecordIter 可用於加載以io格式保存的圖像數據 data_iter = mx.io.ImageRecordIter( path_imgrec="./data/caltech.rec", # the target record file data_shape=(3, 227, 227), # output data shape. An 227x227 region will be cropped from the original image. batch_size=4, # number of samples per batch resize=256 # resize the shorter edge to 256 before cropping # ... you can add more augumentation options as defined in ImageRecordIter. ) data_iter.reset() batch = data_iter.next() data = batch.data[0] for i in range(4): plt.subplot(1,4,i+1) plt.imshow(data[i].asnumpy().astype(np.uint8).transpose((1,2,0))) plt.show()
dataiter = mx.io.ImageRecordIter( #rec文件所在位置
path_imgrec="MNIST.rec", #rec文件中圖像大小以及通道數量 data_shape=(3,28,28), #每個batch中圖像的數量 batch_size=100, #平均圖像,如果設置了平均圖像,則輸入圖像將減去該平均圖像 mean_img="data/cifar/cifar10_mean.bin", #隨機對圖像進行裁剪 rand_crop=True, #隨機對圖像進行鏡像 rand_mirror=True, #從rec文件中隨機取出圖像 shuffle=False, #預處理線程數 preprocess_threads=4, #預取緩存 prefetch_buffer=1)
Using ImageIter
ImageIter is a flexible interface that supports loading of images in both RecordIO and Raw format.
# ImageIter 是一個靈活的界面,支持以RecordIO和Raw格式加載圖像 data_iter = mx.image.ImageIter(batch_size=4, data_shape=(3, 227, 227), path_imgrec="./data/caltech.rec", path_imgidx="./data/caltech.idx" ) data_iter.reset() batch = data_iter.next() data = batch.data[0] for i in range(4): plt.subplot(1,4,i+1) plt.imshow(data[i].asnumpy().astype(np.uint8).transpose((1,2,0))) plt.show()
腳本image.py可以在~/mxnet/python/mxnet/image.py找到,里面是各個函數和類的具體實現細節。另外,這個包的各個函數的介紹可以看官網地址:http://mxnet.io/api/python/io.html#api-reference
這里先提另外一個函數:mxnet.image.imdecode(buf, **kwargs) # 將圖像編碼成NDArray格式,我們知道在MXNet框架中,數據存儲為NDArray格式,圖像數據也是如此,因此mxnet.image中的很多函數的輸入輸出都是NDArray格式。
mxnet.image.ImageIter是一個非常重要的類。在MXNet中,當你要讀入圖像數據時,可以用im2rec.py生成lst和rec文件,然后用mxnet.io.ImageRecordIter類來讀取rec文件或者用這個mxnet.image.ImageIter類來讀取rec文件,但是這個函數和前者相比還能直接讀取圖像文件,這樣就可以不用生成占內存的rec文件了,只需要原圖像文件和lst文件即可。另外,在mxnet.io.ImageRecordIter中對於數據的預處理操作都是固定的,不好修改,但是mxnet.image.ImageIter卻可以非常靈活地添加各種預處理操作。接下來看看這個類。
mxnet.image.ImageIter(batch_size, data_shape, label_width=1, path_imgrec=None, path_imglist=None, path_root=None, path_imgidx=None, shuffle=False, part_index=0, num_parts=1, aug_list=None, imglist=None, data_name='data', label_name='softmax_label', **kwargs)
參數:
● batch_size (int) – Number of examples per batch. ● data_shape (tuple) – Data shape in (channels, height, width) format. For now, only RGB image with 3 channels is supported. ● label_width (int, optional) – Number of labels per example. The default label width is 1. ● path_imgrec (str) – Path to image record file (.rec). Created with tools/im2rec.py or bin/im2rec. ● path_imglist (str) – Path to image list (.lst). Created with tools/im2rec.py or with custom script. Format: Tab separated record of index, one or more labels and relative_path_from_root. ● imglist (list) – A list of images with the label(s). Each item is a list [imagelabel: float or list of float, imgpath]. ● path_root (str) – Root folder of image files. ● path_imgidx (str) – Path to image index file. Needed for partition and shuffling when using .rec source. ● shuffle (bool) – Whether to shuffle all images at the start of each iteration or not. Can be slow for HDD. ● part_index (int) – Partition index. ● num_parts (int) – Total number of partitions. ● data_name (str) – Data name for provided symbols. ● label_name (str) – Label name for provided symbols. ● kwargs – More arguments for creating augmenter. See mx.image.CreateAugmenter.
使用.lst和圖像時,示意如下:
train = mx.image.ImageIter( batch_size = args.batch_size, data_shape = (3,224,224), label_width = 1, path_imglist = args.data_train, path_root = args.image_train, part_index = rank, shuffle = True, data_name = 'data', label_name = 'softmax_label', aug_list = mx.image.CreateAugmenter((3,224,224),resize=224,rand_crop=True,rand_mirror=True,mean=True))
這里的path_imglist參數和path_root參數是這個類特有的,分別表示.lst文件和圖像的路
只是一個列表文件,大大節省了存儲空間,也方便以后對數據的增刪改變,因為只要重新生成.lst文件即可,而不需要花時間生成占空間的.rec文件。
參數aug_list,表示所有預處理的列表,在image.py腳本中ImageIter類的init()函數的這幾行代碼:
if aug_list is None: self.auglist = CreateAugmenter(data_shape, **kwargs) else: self.auglist = aug_list
如果aug_list這個參數沒有賦值(默認是None),那么就不對圖像做預處理;如果這個參數有值,那么就調用CreateAugmenter()函數生成預處理列表。CreateAugmenter()函數相關見第四部分。
Using ImageDetIter
目標檢測用,示意見SSD教程
標號的形狀是batch_size x num_object_per_image x 5
。每個標號由長為5的數組表示,第一個元素是其對用物體的標號,其中-1
表示非法物體,僅做填充使用。后面4個元素表示邊框。
四、附錄:CreateAugmenter
def CreateAugmenter(data_shape, resize=0, rand_crop=False, rand_resize=False, rand_mirror=False,mean=None, std=None, brightness=0, contrast=0, saturation=0, pca_noise=0, inter_method=2): """Creates an augmenter list.""" auglist = [] # resize這個參數很重要,一般都要做resize,如果你的resize參數設置為224,你的原圖像是350*300,那么最后resize的大小就是 # (350*300/224)*224。這里ResizeAug()函數調用resize_short()函數,resize_short()函數調用OpenCV的imresize()函數完成resize # ,interp參數為2表示采用雙三次插值做resize,可以參考:http://docs.opencv.org/master/da/d54/group__imgproc__transform.html。 if resize > 0: auglist.append(ResizeAug(resize, inter_method)) crop_size = (data_shape[2], data_shape[1]) # 如果rand_resize參數是true,那么會調用RandomSizedCropAug()函數,輸入是size,min_area,retio,interp, # 這個函數既做resize又做crop,因此這邊才會寫成if elif的語句。RandomSizedCropAug()函數調用random_size_crop()函數, # 這個函數會先生成隨機的坐標點和長寬值,然后調用fixed_crop()函數做crop。 #這里還有一個語句是assert rand_crop,python的assert語句是用來聲明其布爾值必須為真,如果表達式為假,就會觸發異常。 # 也就是說要調用RandomSizedCropAug()函數的前提是rand_crop是True。 if rand_resize: assert rand_crop auglist.append(RandomSizedCropAug(crop_size, 0.3, (3.0 / 4.0, 4.0 / 3.0), inter_method)) #如果rand_crop參數是true,表示隨機裁剪,randomCropAug()函數的輸入之一是crop_size, # 這個crop_size就是CreateAugmenter()函數的輸入data_shape的圖像大小,然后randomCropAug()函數調用random_crop()函數, # random_crop()函數會先生成新的長寬值和坐標點,然后以此調用fixed_crop()函數做crop, # 最后返回crop后的圖像和坐標即長寬值,因為生成中心坐標點的時候是隨機的,所以還是random crop。 elif rand_crop: auglist.append(RandomCropAug(crop_size, inter_method)) # 如果前面兩個if條件都不滿足,就調用CenterCropAug()函數做crop,這個函數的輸入也包括了crop_size,也就是你的輸入data_shape, # 所以這個參數是很有用的。CenterCropAug()函數調用center_crop()函數,這個函數的輸入輸出都是NDArray格式。 # center_crop()函數和random_crop()函數的區別在於前者坐標點的生成不是隨機的,而是和原圖像一樣, # 然后再將坐標點和新的長寬作為fixed_crop()函數的輸入。 else: auglist.append(CenterCropAug(crop_size, inter_method)) #可以看出不管你是否要做crop,只要你給定了data_shape參數,就默認要將輸入圖像做crop操作。 # 因此如果你不想在test的時候做crop,可以在這修改源碼。 # 隨機鏡像處理,參數是0.5,HorizontalFlipAug()函數調用nd.flip()函數做水平翻轉 if rand_mirror: auglist.append(HorizontalFlipAug(0.5)) # CastAug()函數主要是將數據格式轉化為float32 auglist.append(CastAug()) # 這三個參數分別是亮度,對比度,飽和度。當你對這三個參數設置了值, # 就會調用ColorJitterAug()函數對其相應的亮度或對比度或飽和度做改變 if brightness or contrast or saturation: auglist.append(ColorJitterAug(brightness, contrast, saturation)) # 這個部分主要是添加pca噪聲的,具體可以看LightingAug()函數 if pca_noise > 0: eigval = np.array([55.46, 4.794, 1.148]) eigvec = np.array([[-0.5675, 0.7192, 0.4009], [-0.5808, -0.0045, -0.8140], [-0.5836, -0.6948, 0.4203]]) auglist.append(LightingAug(pca_noise, eigval, eigvec)) # mean這個參數主要是和歸一化相關。這里的assert語句前面已經介紹過了。mean參數默認是None,這種情況下是不會進入下面的if elif條件函數的。 # 如果想進行均值操作,可以設置mean為True,那么就會進入第一個if條件,如果你設置為其他值,就會進入elif條件, # 這個時候如果你的mean不符合要求,比如isinstance函數用來判斷類型,就會觸發異常。 if mean is True: mean = np.array([123.68, 116.28, 103.53]) elif mean is not None: assert isinstance(mean, np.ndarray) and mean.shape[0] in [1, 3] # std與mean同理 if std is True: std = np.array([58.395, 57.12, 57.375]) elif std is not None: assert isinstance(std, np.ndarray) and std.shape[0] in [1, 3] # 這里需要mean和std同時都設置正確才能進行預處理,如果你只設置了mean,沒有設置std,那么還是沒有啟動歸一化的預處理。 # 這里主要調用ColorNormalizeAug()函數,這個函數調用color_normalize()函數,這個函數的實現很簡單, # 就是將原圖像的像素值減去均值mean,然后除以標准差std得到返回值。 if mean is not None and std is not None: auglist.append(ColorNormalizeAug(mean, std)) # 最后返回預處理的列表 return auglist