手把手帶你復現ICCV 2017經典論文—PyraNet

PaperWeekly發表於2018-11-12

Learning Feature Pyramids for Human Pose Estimation 是發表在 ICCV 2017 的工作,論文提出了一個新的特徵金字塔模組,在卷積網路中學習特徵金字塔,並修正了現有的網路引數初始化方法,在人體姿態估計和影像分類中都取得了很好的效果。

手把手帶你復現ICCV 2017經典論文—PyraNet論文復現程式碼: http://aistudio.baidu.com/?_=1540892031956#/projectdetail/29380

LSP資料集簡介

LSP && LSP_extended 

這個資料集是由 Flickr 上‘Volleyball’, ‘Badminton’, ‘Athletics’, ‘Baseball’, ‘Gymnastics’, ‘Parkour’, ‘Soccer’, ‘Tennis’(原資料集), ‘parkour’, ‘gymnastics’, and ‘athletics’ (擴充套件集)等標籤所構成。

每個圖片都由 Amazon Mechanical Turk 和類似的途徑標註而來,並不高度準確。這些圖片被縮放至每個人大約 150 px 長度進行標註,包含了 14 個節點。 

LSP 地址:

http://sam.johnson.io/research/lsp_dataset.zip 

LSP 樣本數:2000 個(全身,單人) 

LSP_extended 地址:

http://sam.johnson.io/research/lspet_dataset.zip 

LSP_extended 樣本數:10000 個(全身,單人) 

LSP && LSP_extended 共 12000 個標註,節點是以下 14 個: 

{1. Right ankle 2. Right knee 3. Right hip 4. Left hip 5. Left knee 6.Left ankle 7.Right wrist 8. Right elbow 9. Right shoulder 10. Left shoulder 11. Left elbow 12. Left wrist 13. Neck 14. Head top} 

由於是單人資料集,該資料集的訓練難度比多人資料集更簡單。

MPII資料集簡介

MPII 人體姿勢資料集是人體姿勢預估的一個 benchmark,資料集包括了超過 40k 人的 25000 張帶標註圖片,這些圖片是從 YouTube video 中抽取出來的。在測試集中還收錄了身體部位遮擋、3D 軀幹、頭部方向的標註。 

MPII 地址:

http://human-pose.mpi-inf.mpg.de/#overview 

MPII 樣本數:25000 個(單人、多人) 

包括以下 16 類標註: 

{Head – 0, Neck – 1, Right Shoulder – 2, Right Elbow – 3, Right Wrist – 4, Left Shoulder – 5, Left Elbow – 6, Left Wrist – 7, Right Hip – 8, Right Knee – 9, Right Ankle – 10, Left Hip – 11, Left Knee – 12, Left Ankle – 13, Chest – 14, Background – 15}

資料集處理

MATLAB格式讀入

檔案 joints.mat 是 MATLAB 資料格式,包含了一個以 x 座標、y 座標和一個表示關節可見性的二進位制數字所構成的 3 x 14 x 10000 的矩陣。使用模組 scipy.io 的函式 loadmat 和 savemat 可以實現對 mat 資料的讀寫。讀入後對原始標註進行轉置,轉置目的是分離每個圖片的標註。

import scipy.io as sio
import numpy as np
data = sio.loadmat(self.lsp_anno_path[count])
joints = data['joints']
joints = np.transpose(joints, (2, 1, 0))

JSON格式讀入 

MPII 資料集是以 JSON 格式進行的標註,可以通過 JSON 庫進行讀入。

import json
anno = json.load(self.mpii_anno_pah)

將每個圖片打包成(圖片,標註,bounding box)的形式,bounding box 即圖片大小,其目的是將大小不一的圖片處理成 256 x 256 的大小。

from PIL import Image
for idd, joint_idd in enumerate(joints):
    image_name = "im%s.jpg" % str(idd + 1).zfill(5) if count else "im%s.jpg" % str(idd + 1).zfill(4)
    joint_id = idd + len(joints) if count else idd
    im_path = os.path.join(self.lsp_data_path[count], image_name)
    im = Image.open(im_path)
    im = np.asarray(im)
    shape = im.shape
    bbox = [0, 0, shape[1], shape[0]]
    joint_dict[joint_id] = {'imgpath': im_path, 'joints': joint_idd, 'bbox': bbox}

資料增強 

作者用到了幾種資料增強的手段:

  • 縮放 scale 

  • 旋轉 rotate 

  • 翻轉 flip 

  • 新增顏色噪聲 add color noise

縮放

讀入資料後,需要先把大小不一的標註圖片統一轉換成 256 x 256。

對於 LSP 測試集,作者使用的是影像的中心作為身體的位置,並直接以影像大小來衡量身體大小。資料集裡的原圖片是大小不一的(原圖尺寸存在 bbox 裡),一般採取 crop 的方法有好幾種,比如直接進行 crop,然後放大,這樣做很明顯會有丟失關節點的可能性。也可以先把圖片放在中間,然後將圖片縮放到目標尺寸範圍內原尺寸的可縮放的大小,然後四條邊還需要填充的距離,最後 resize 到應有大小。 

這裡採用的是先擴充套件邊緣,然後放大圖片,再進行 crop,這樣做能夠保證圖片中心處理後依然在中心位置,且沒有關節因為 crop 而丟失。注意在處理圖片的同時需要對標註也進行處理。 

要注意 OpenCV 和 PIL 讀入的 RGB 順序是不一樣的,在使用不同庫進行處理時要轉換通道。

import cv2
big_img = cv2.copyMakeBorder(img, add, add, add, add, borderType = cv2.BORDER_CONSTANT, value=self.pixel_means.reshape(-1))
#self.show(bimg)  
bbox = np.array(dic['bbox']).reshape(4, ).astype(np.float32)
bbox[:2] += add
if 'joints' in dic:
    process(joints_anno)
objcenter = np.array([bbox[0] + bbox[2] / 2., bbox[1] + bbox[3] / 2.])
minx, miny, maxx, maxy = compute(extend_border, objcenter, in_size, out_size)
img = cv2.resize(big_img[min_y: max_y, min_x: max_x,:], (width, height))

示例圖: 

手把手帶你復現ICCV 2017經典論文—PyraNet▲ 左:原圖,右:縮放後示例圖的十四個標註點: 

(88.995834, 187.24898);(107.715065, 160.57408);(119.648575, 124.30561) (135.3259, 124.53958);(145.38748, 155.4263);(133.68799, 165.95587) (118.47862, 109.330215);(108.41703, 104.65042);(120.81852, 84.05927) (151.70525, 86.63316);(162.93677, 101.14057);(161.29883, 124.773575) (136.0279, 85.93119);(138.13379, 66.509995)

旋轉 

旋轉後點的座標需要通過一個旋轉矩陣來確定,在網上的開原始碼中,作者使用了以下矩陣的變換矩陣圍繞著 (x,y) 進行任意角度的變換。

手把手帶你復現ICCV 2017經典論文—PyraNet

在 OpenCV 中可以使用:

cv2.getRotationMatrix2D((center_x, center_y) , angle, 1.0)
newimg = cv2.warpAffine(img, rotMat, (width, height))

得到轉換矩陣,並通過仿射變換得到旋轉後的影像。而標註點可以直接通過旋轉矩陣獲得對應點。

rot = rotMat[:, : 2]
add = np.array([rotMat[0][2], rotMat[1][2]])
coor = np.dot(rot, coor) + w

該部分程式碼:

def rotate(self, img, cord, anno, center):
    angle = random.uniform(45, 135)
    rotMat = cv2.getRotationMatrix2D((center[0], center[1]) , angle, 1.0)
    newimg = cv2.warpAffine(img, rotMat, (width, height))
    for i in range(n):
        x, y = anno[i][0], anno[i][1]
        coor = np.array([x, y])
        rot = rotMat[:, : 2]
        add = np.array([rotMat[0][2], rotMat[1][2]])
        coor = np.dot(rot, coor) + add
        label.append((coor[0], coor[1]))
    newimg = newimg.transpose(2, 0, 1)
    train_data[cnt++] = newimg
    train_label[cnt++] = np.array(label)

手把手帶你復現ICCV 2017經典論文—PyraNet翻轉

使用 OpenCV 中的 flip 進行翻轉,並對標註點進行處理。在 OpenCV 中 flip 函式的引數有 1 水平翻轉、0 垂直翻轉、-1 水平垂直翻轉三種。

def flip(self, img, cod, anno_valid, symmetry):
    '''對圖片進行翻轉'''
    newimg = cv2.flip(img, 1)
    train_data[counter] = newimg.transpose(2, 0, 1)
    '''處理標註點,symmetry是flip後所對應的標註,具體需要自己根據實際情況確定'''
    for (l, r) in symmetry:
        cod[l], cod[r] = cod[l], cod[r]
    for i in range(n):
        label.append((cod[i][0],cod[i][1]))
    train_label[cnt++] = np.array(label)

手把手帶你復現ICCV 2017經典論文—PyraNet

新增顏色噪聲

我所採用的方法是直接新增 10% 高斯分佈的顏色點作為噪聲。人為地損失部分通道資訊也可以達到新增彩色噪聲的效果。

def add_color_noise(self, image, percentage=0.1):
    noise_img = image 
    '''產生影像大小10%的隨機點'''
    num = int(percentage*image.shape[0]*image.shape[1])
    '''新增噪聲'''
    for i in range(num): 
        x = np.random.randint(0,image.shape[0]) 
        y = np.random.randint(0,image.shape[1]) 
        for j in range(3):
            noise_img[x, y, i] = noise_img[x, y, i] + random.gauss(2,4)
            noise_img[x, y, i] = 255 if noise_img[x, y, ch] > 255 else 0
    return noise_img

手把手帶你復現ICCV 2017經典論文—PyraNet

除此之外,以下資料增強的方法也很常見:

1. 從顏色上考慮,還可以做影像亮度、飽和度、對比度變化、PCA Jittering(按照 RGB 三個顏色通道計算均值和標準差後在整個訓練集上計算協方差矩陣,進行特徵分解,得到特徵向量和特徵值); 

2. 從影像空間性質上考慮,還可以使用隨機裁剪、平移;

3. 從噪聲角度,高斯噪聲、椒鹽噪聲、模糊處理; 

4. 從類別分佈的角度,可以採用 label shuffle、Supervised Data Augmentation(海康威視 ILSVRC 2016 的 report)。 

在這個具體例子中,進行資料增強的時候要考慮的是:1)形變會不會影響結果;2)會不會丟掉部分節點。

製作Paddle資料

使用 paddle.batch 批量讀入資料,並製作成 Paddle 的資料格式。

reader = paddle.batch(self.read_record(test_list, joint_dict, 
                mode = 'train'), batch_size=1)
fluid.recordio_writer.convert_reader_to_recordio_file("./work/test_"  + str(i) + "_test.recordio", 
                feeder=feeder, reader_creator=reader) 

其他資料相關內容

論文的評價標準

PCK:檢測的關鍵點與其對應的 groundtruth 之間的歸一化距離小於設定閾值的比例。在本篇論文中,作者將圖片中心作為身體的位置,並以圖片大小作為衡量身體尺寸的標準。 

PCK@0.2 on LSP && LSP-extended:以驅幹直徑為歸一化標準。

PCKh@0.5 on MPII:以頭部為歸一化標準。

關於訓練的過擬合搶救

對於容易過擬合的資料,資料增強是比較重要的,訓練的時候學習率需要不能太大,當一次訓練過擬合後,可以從 loss 曲線波動的地方回溯到較為平穩的點,並以平穩點的學習率為起點,以更低的學習率接上上次學習。

關於PaddlePaddle

查了一下資料,PaddlePaddle 最早在 16 年就已經對外開放,然而可能因為本人入門做機器學習時間較晚有關,在復現活動之前,我只是聽過有一個開源深度學習平臺而不知道其名字。

從官方開源的一些 demo 專案來講,對推薦和文字處理方面的應用比較友好,搜尋相關關鍵字也能獲得很多入門的部落格、在不同環境的安裝指南,官方甚至還做了教學視訊。

據說當前版本的 Fluid 在編寫邏輯上和過去的版本已經有了很大的區別,在使用上直觀的感受是和 TensorFlow 有一定的相似性。

但由於不熟悉這個框架,也會遇到一些問題:一開始在 AI 開放平臺上找了半天沒找到文件入口,在搜尋引擎上才發現有另一個 paddlepaddle.org 的入口;當一些運算元的名字和其他框架不太一樣的時候,不太容易從文件裡找到;不清楚不同版本之間的區別(能跑就行?);官網介紹對大規模計算友好、對視覺化的支援均沒有體驗;Notebook 非常容易崩等問題等等……

儘管如此,在使用一定時間後,我覺得還是覺得挺方便的。這個框架的使用群體目前並不多,對大公司來講大家都有內部各自對 TensorFlow 的封裝性優化平臺,對入門者而言往往又不是那麼首選.

從個人學習路徑來講,我覺得就 TensorFlow 和現在流行的 PyTorch 而言,前者是業界工程依賴程度高,後者是研究者使用方便,PaddlePaddle 需要有一個清晰的受眾定位和有效的推廣機制。

相關文章