我們需要一個數據集來訓練姿勢估計模型,我們的選擇有像COCO、MPII和CrowdPose這樣的公共資料集,姿態估計屬於比較複雜一類的問題。為神經網路模型建立一個合適的資料集是很困難的,影象中每個人的每個關節都必須定位和標記,這是一項瑣碎而費時的任務。
目前最流行的姿態估計資料集是COCO資料集。它有大約80類影象和大約250000個人物例項。
如果檢查此資料集中的一些隨機影象,你可能會碰到一些與要解決的問題無關的例項,學術界希望達到最高的精度,但在實際生產環境中並不總是如此。
在實際環境中,我們更希望在特定的場景下製作良好的模型,例如行人、籃球運動員、健身房等。
現在我們從COCO資料集中檢視此影象:
你看到紅點了嗎?這是關鍵點:鼻子。
你可能不希望網路看到僅包含頭部一部分的示例,尤其是在幀的底部。
本文將向你展示COCO資料集的一個示例分析。
資料集由影象檔案和註釋檔案組成。註釋檔案是一個JSON,包含關於一個人(或其他一些類別)的所有元資料。在這裡我們將找到邊界框的位置和大小,區域,關鍵點,源影象的檔名等。
具體來說,我們只需要人的註釋。zip中有兩個有趣的檔案:annotations_trainval2017.zip:person_keypoints_train2017.json和person_keypoints_val2017.json。
我建議將檔案放在這個資料夾層次結構中:
dataset_coco |---annotations |---person_keypoints_train2017.json |---person_keypoints_val2017.json |---train2017 |---*.jpg |---val2017 |---*.jpg
下面是顯示如何載入註釋的程式碼:
from pycocotools.coco import COCO...train_annot_path = 'dataset_coco/annotations /person_keypoints_train2017.json'val_annot_path = 'dataset_coco/annotations/person_keypoints_val2017.json'train_coco = COCO(train_annot_path) # 載入訓練集的註釋val_coco = COCO(val_annot_path) # 載入驗證集的註釋...# 函式遍歷一個人的所有資料庫並逐行返回相關資料def get_meta(coco): ids = list(coco.imgs.keys()) for i, id in enumerate(ids): meta = coco.imgs[img_id] ann_ids = coco.getAnnIds(imgIds=img_id) # 影象的基本引數 file_name = meta['file_name'] w = meta['width'] h = meta['height'] # 檢索當前影象中所有人的元資料 anns = coco.loadAnns(ann_ids) yield [img_id, file_name, w, h, anns]...# 迭代影象for id, fname, w, h, meta in get_meta(train_coco): ... # 遍歷影象的所有註釋 for m in meta: # m是字典 keypoints = m['keypoints'] ......
首先,我們必須載入COCO物件,它是json資料的包裝器(第6-7行);
在第11行,我們載入所有影象識別符號。
在接下來的幾行中,我們為每個影象載入元資料。這是一個包含影象寬度、高度、名稱、許可證等一般資訊的詞典。
在第14行,我們載入給定影象的註釋元資料。這是一個字典列表,每個字典代表一個人。
第27-32行顯示瞭如何載入整個訓練集(train_coco)。類似地,我們可以載入驗證集(val_coco)。
將COCO轉換為Pandas資料幀讓我們將COCO元資料轉換為pandas資料幀。我們使用如matplotlib、sklearn 和pandas。
資料的過濾、視覺化和操作變得更加容易。此外,我們還可以將資料匯出為csv或parquet等。
def convert_to_df(coco): images_data = [] persons_data = [] # 遍歷所有影象 for id, fname, w, h, meta in get_meta(coco): images_data.append({ 'image_id': int(img_id), 'path': fname, 'width': int(w), 'height': int(h) }) # 遍歷所有元資料 for m in meta: persons_data.append({ 'image_id': m['image_id'], 'is_crowd': m['iscrowd'], 'bbox': m['bbox'], 'area': m['area'], 'num_keypoints': m['num_keypoints'], 'keypoints': m['keypoints'], }) # 建立帶有影象路徑的資料幀 images_df = pd.DataFrame(images_data) images_df.set_index('image_id', inplace=True) # 建立與人相關的資料幀 persons_df = pd.DataFrame(persons_data) persons_df.set_index('image_id', inplace=True) return images_df, persons_df
我們使用get_meta函式構造兩個資料幀—一個用於影象路徑,另一個用於人的元資料。在一個影象中可能有多個人,因此是一對多的關係。
在下一步中,我們合併兩個表(left join操作)並將訓練集和驗證集組合。另外,我們添加了一個新列source,值為0表示訓練集,值為1表示驗證集。
這樣的資訊是必要的,因為我們需要知道應該在哪個資料夾中搜索影象。如你所見,這些影象位於兩個資料夾中:train2017/和val2017/。
images_df, persons_df = convert_to_df(train_coco)train_coco_df = pd.merge(images_df, persons_df, right_index=True, left_index=True)train_coco_df['source'] = 0images_df, persons_df = convert_to_df(val_coco)val_coco_df = pd.merge(images_df, persons_df, right_index=True, left_index=True)val_coco_df['source'] = 1coco_df = pd.concat([train_coco_df, val_coco_df], ignore_index=True)
最後,我們有一個表示整個COCO資料集的資料幀。
影象中有多少人現在我們執行第一個分析。
COCO資料集包含多個人的影象。我們想知道有多少影象只包含一個人。
程式碼如下:
# 計數annotated_persons_df = coco_df[coco_df['is_crowd'] == 0]crowd_df = coco_df[coco_df['is_crowd'] == 1]print("Number of people in total: " + str(len(annotated_persons_df)))print("Number of crowd annotations: " + str(len(crowd_df)))persons_in_img_df = pd.DataFrame({ 'cnt': annotated_persons_df['path'].value_counts()})persons_in_img_df.reset_index(level=0, inplace=True)persons_in_img_df.rename(columns = {'index':'path'}, inplace = True)# 按cnt分組,這樣我們就可以在一張圖片中得到帶有註釋人數的資料幀persons_in_img_df = persons_in_img_df.groupby(['cnt']).count()# 提取陣列x_occurences = persons_in_img_df.index.valuesy_images = persons_in_img_df['path'].values# 繪圖plt.bar(x_occurences, y_images)plt.title('People on a single image ')plt.xticks(x_occurences, x_occurences)plt.xlabel('Number of people in a single image')plt.ylabel('Number of images')plt.show()
結果圖表:
如你所見,大多數COCO圖片都包含一個人。
但是,有相當多的多人照片。舉幾個例子:
好吧,甚至有一張圖片有19個註解(非人群):
這個影象的頂部區域不應該標記為一個人群嗎?
是的,應該。但我們有多個沒有關鍵點的邊界框!它們應該被遮蔽。
在這張圖片中,只有中間的3個方框有一些關鍵點。
讓我們最佳化查詢,以獲取包含有/沒有關鍵點的人的影象的統計資訊,以及有/沒有關鍵點的人的總數:
annotated_persons_nokp_df = coco_df[(coco_df['is_crowd'] == 0) & (coco_df['num_keypoints'] == 0)]annotated_persons_kp_df = coco_df[(coco_df['is_crowd'] == 0) & (coco_df['num_keypoints'] > 0)]print("Number of people (with keypoints) in total: " + str(len(annotated_persons_kp_df)))print("Number of people without any keypoints in total: " + str(len(annotated_persons_nokp_df)))persons_in_img_kp_df = pd.DataFrame({ 'cnt': annotated_persons_kp_df[['path','source']].value_counts()})persons_in_img_kp_df.reset_index(level=[0,1], inplace=True)persons_in_img_cnt_df = persons_in_img_kp_df.groupby(['cnt']).count()x_occurences_kp = persons_in_img_cnt_df.index.valuesy_images_kp = persons_in_img_cnt_df['path'].valuesf = plt.figure(figsize=(14, 8))width = 0.4plt.bar(x_occurences_kp, y_images_kp, width=width, label='with keypoints')plt.bar(x_occurences + width, y_images, width=width, label='no keypoints')plt.title('People on a single image ')plt.xticks(x_occurences + width/2, x_occurences)plt.xlabel('Number of people in a single image')plt.ylabel('Number of images')plt.legend(loc = 'best')plt.show()
現在我們可以看到區別是明顯的。
新增額外列一旦我們將COCO轉換成pandas資料幀,就可以很容易地新增額外的列,從現有的列中計算出來。
我認為最好將所有的關鍵點座標提取到單獨的列中。此外可以新增一個具有比例因子的列。
特別是關於一個人的邊界框的規模的資訊可能非常有用。例如,我們可能希望丟棄所有太小規模的人,或者執行放大操作。
為了實現這個目標,我們將使用Python庫sklearn中的transformer物件。
一般來說,sklearn transformers是用於清理、減少、擴充套件和生成資料科學模型中的特徵表示的強大工具。我們只會用一小部分的api。
程式碼如下:
from sklearn.base import BaseEstimator, TransformerMixinclass AttributesAdder(BaseEstimator, TransformerMixin): def __init__(self, num_keypoints, w_ix, h_ix, bbox_ix, kp_ix): """ :param num_keypoints: 關鍵點的數量 :param w_ix: 包含影象寬度的列的索引 :param h_ix: 包含影象高度的列的索引 :param bbox_ix: 包含邊框資料的列的索引 :param kp_ix: 包含關鍵點資料的列的索引 """ self.num_keypoints = num_keypoints self.w_ix = w_ix self.h_ix = h_ix self.bbox_ix = bbox_ix self.kp_ix = kp_ix def fit(self, X, y=None): return self # 沒有別的事可做 def transform(self, X): # 檢索特定列 w = X[:, self.w_ix] h = X[:, self.h_ix] bbox = np.array(X[:, self.bbox_ix].tolist()) # to matrix keypoints = np.array(X[:, self.kp_ix].tolist()) # to matrix # 計算邊框的比例因子 scale_x = bbox[:,2] / w scale_y = bbox[:,3] / h aspect_ratio = w / h # 計算規模類別 scale_cat = pd.cut(scale_y, bins=[0., 0.4, 0.6, 0.8, np.inf], labels=['S', 'M', 'L', 'XL']) return np.c_[X, scale_x, scale_y, scale_cat, aspect_ratio, keypoints]# 用於新增新列的transformer物件attr_adder = AttributesAdder(num_keypoints=17, ...)coco_extra_attribs = attr_adder.transform(coco_df.values)# 建立新的列列表keypoints_cols = [['x'+str(idx), 'y'+str(idx), 'v'+str(idx)] for idx, k in enumerate(range(num_keypoints))]keypoints_cols = np.concatenate(keypoints_cols).tolist()# 建立新的更豐富的資料z幀coco_extra_attribs_df = pd.DataFrame( coco_extra_attribs, columns=list(coco_df.columns) + ["scale_x", "scale_y", "scale_cat", "aspect_ratio"] + keypoints_cols, index=coco_df.index)
計算規模(第32-34行)不需要進一步解釋。更有趣的是下一行:38。在這裡,我們為每一行指定規模類別(S、M、L或XL)。計算方法如下:
如果scale_y在[0–0.4)範圍內,則類別為S
如果scale_y在[0.4–0.6)範圍內,則類別為M
如果scale_y在[0.6–0.8)範圍內,則類別為L
如果scale_y在[0.8–1.0)範圍內,則類別為XL
在第42行,我們將原始列與新列合併。
仔細看看我們如何將關鍵點擴充套件到單獨的列中—第28行。COCO資料集中的關鍵點資料由一個一維列表表示:[x0,y0,v0,x1,y1,…]。我們可以把這個列轉換成一個矩陣:[num of rows]x[num of keypoints*3]。然後,我們可以不需要任何額外的努力就可以返回它(第42行)。
最後,我們建立一個新的資料幀(第58-63行)。
鼻子在哪裡?我知道這是個奇怪的問題。但我向你保證,鼻子是在影象中找到。
是的,如果我們想檢查影象中頭部位置的分佈呢。最簡單的方法是找到鼻子的座標,然後在標準化的二維圖表中畫一個點。
呈現此圖表的程式碼如下:
# 對水平影象進行關鍵點座標標準化horiz_imgs_df = coco_extra_attribs_df[coco_extra_attribs_df['aspect_ratio'] >= 1.]# 獲取平均寬度和高度-用於縮放關鍵點座標avg_w = int(horiz_imgs_df['width'].mean())avg_h = int(horiz_imgs_df['height'].mean())class NoseAttributesAdder(BaseEstimator, TransformerMixin): def __init__(self, avg_w, avg_h, w_ix, h_ix, x1_ix, y1_ix, v1_ix): self.avg_w = avg_w self.avg_h = avg_h self.w_ix = w_ix self.h_ix = h_ix self.x1_ix = x1_ix self.y1_ix = y1_ix self.v1_ix = v1_ix def fit(self, X, y=None): return self # 沒有別的事可做 def transform(self, X): w = X[:, self.w_ix] h = X[:, self.h_ix] x1 = X[:, self.x1_ix] y1 = X[:, self.y1_ix] # 標準化鼻子座標,提供平均寬度和高度 scale_x = self.avg_w / w scale_y = self.avg_h / h nose_x = x1 * scale_x nose_y = y1 * scale_y return np.c_[X, nose_x, nose_y] # 用於標準化鼻子座標列的transformer物件w_ix = horiz_imgs_df.columns.get_loc('width')h_ix = horiz_imgs_df.columns.get_loc('height')x1_ix = horiz_imgs_df.columns.get_loc('x0') # 鼻子的x座標在'x0'列中y1_ix = horiz_imgs_df.columns.get_loc('y0') # 鼻子的y座標在'y0'列中v1_ix = horiz_imgs_df.columns.get_loc('v0') # 鼻頭的可見性attr_adder = NoseAttributesAdder(avg_w, avg_h, w_ix, h_ix, x1_ix, y1_ix, v1_ix)coco_noses = attr_adder.transform(horiz_imgs_df.values)# 使用標準化的資料建立新資料幀coco_noses_df = pd.DataFrame( coco_noses, columns=list(horiz_imgs_df.columns) + ["normalized_nose_x", "normalized_nose_y"], index=horiz_imgs_df.index)# 過濾-只有可見的鼻子coco_noses_df = coco_noses_df[coco_noses_df["v0"] == 2]coco_noses_df.plot(kind="scatter", x="normalized_nose_x", y="normalized_nose_y", alpha=0.3).invert_yaxis()
與前面一樣,我們將使用一個轉換器來新增新列。
COCO資料集包含不同寬度和高度的影象。我們必須標準化每個影象中鼻子的x,y座標,這樣我們就能在輸出圖表中畫出代表鼻子的點。
我們首先確定所有影象的平均寬度和高度(第7-8行)。我們可以使用任何值,因為它只用於確定比例因子。
在第40-44行,我們從dataframe中找到所需列的索引。
隨後,我們執行轉換(第46-47行)並建立一個新的資料幀,其中包含新的列normalized_nose_x和normalized_nose_y(第51-55行)。
最後一行繪製二維圖表。
現在可以檢查一些影象。例如我們想檢查一些頭部位置非常接近影象底邊的影象,為了實現這一點,我們透過列normalized_nose_y過濾資料幀。
low_noses_df = coco_noses_df[coco_noses_df['normalized_nose_y'] > 430 ]low_noses_df
以下是滿足此條件的示例影象:
關鍵點數量具有特定數量關鍵點的邊界框的數量是附加的有用資訊。
為什麼要邊界框?
邊界框有一個特殊的標誌iscrowd,用來確定內容是應該作為一個群組(沒有關鍵點)還是一個人(應該有關鍵點)。一般來說,iscrowd是為包含許多人的小例項(例如網球比賽中的觀眾)的邊界框設定的。
y_images = coco_extra_attribs_df['num_keypoints'].value_counts()x_keypoints = y_images.index.values# 繪圖plt.figsize=(10,5)plt.bar(x_keypoints, y_images)plt.title('Histogram of keypoints')plt.xticks(x_keypoints)plt.xlabel('Number of keypoints')plt.ylabel('Number of bboxes')plt.show()# 帶有若干關鍵點(行)的bboxes(列)百分比kp_df = pd.DataFrame({ "Num keypoints %": coco_extra_attribs_df[ "num_keypoints"].value_counts() / len(coco_extra_attribs_df)}).sort_index()
如你所見,在表中顯示相同的資訊非常容易:
規模這是迄今為止最有價值的指標。
訓練姿態估計深度神經網路模型對樣本中人的規模變化非常敏感。提供一個平衡的資料集是關鍵。否則,模型可能會偏向於一個更具優勢的規模。
你還記得一個額外的屬性scale_cat嗎?現在我們要好好利用它。
程式碼:
persons_df = coco_extra_attribs_df[coco_extra_attribs_df['num_keypoints'] > 0]persons_df['scale_cat'].hist()
可以呈現以下圖表:
我們清楚地看到,COCO資料集包含了很多小人物——不到影象總高度的40%。我們把它放到表格中:
scales_props_df = pd.DataFrame({ "Scales": persons_df["scale_cat"].value_counts() / len(persons_df)})scales_props_df
COCO資料集的分層抽樣
首先,讓我們定義什麼是分層抽樣。
當我們將整個資料集劃分為訓練集/驗證集等時,我們希望確保每個子集包含相同比例的特定資料組。
假設我們有1000人,男性佔57%,女性佔43%。我們不能只為訓練集和驗證集選取隨機資料,因為在這些資料子集中,一個組可能會被低估。我們必須從57%的男性和43%的女性中按比例選擇。
換句話說,分層抽樣在訓練集和驗證集中保持了57%的男性/43%的女性的比率。
同樣,我們可以檢查COCO訓練集和驗證集中是否保持了不同規模的比率。
persons_df = coco_extra_attribs_df[coco_extra_attribs_df['num_keypoints'] > 0]train_df = persons_df[persons_df['source'] == 0]val_df = persons_df[persons_df['source'] == 1]scales_props_df = pd.DataFrame({ "Scales in train set %": train_df["scale_cat"].value_counts() / len(train_df), "Scales in val set %": val_df["scale_cat"].value_counts() / len(val_df)})scales_props_df["Diff 100%"] = 100 * \ np.absolute(scales_props_df["Scales in train set %"] - scales_props_df["Scales in val set %"])
在第2-3行,我們將資料幀拆分為訓練集和驗證集的單獨資料幀。這與我們分別從person_keypoints_train2017.json和person_keypoints_val2017.json載入資料幀相同。
接下來我們用訓練集和驗證集中每個規模組的基數建立一個新的資料幀。此外,我們添加了一個列,其中包含兩個資料集之間差異的百分比。
結果如下:
如我們所見,COCO資料集的分層非常好。訓練集和驗證集中的規模組之間只有很小的差異(1-2%)。
現在,讓我們檢查不同的組-邊界框中關鍵點的數量。
train_df = coco_extra_attribs_df[coco_extra_attribs_df['source'] == 0]val_df = coco_extra_attribs_df[coco_extra_attribs_df['source'] == 1]kp_props_df = pd.DataFrame({ "Num keypoints in train set %": train_df["num_keypoints"].value_counts() / len(train_df), "Num keypoints in val set %": val_df["num_keypoints"].value_counts() / len(val_df)}).sort_index()kp_props_df["Diff 100%"] = 100 * \ np.absolute(kp_props_df["Num keypoints in train set %"] - kp_props_df["Num keypoints in val set %"])
類似地,我們看到關鍵點的數量在COCO訓練和驗證集中是相等的。很好!
現在,你可以將所有資料集(MPII、COCO)合併到一個包中,然後自己進行拆分;有一個很好的sklearn類:StratifiedShuffleSplit。
總結在本文中,我描述了分析COCO資料集的過程。瞭解其中的內容可以幫助你更好地決定增加或丟棄一些不相關的樣本。
分析可以在Jupyter notebook上進行。
我從COCO資料集中展示了一些或多或少有用的指標,比如影象中人的分佈、人的邊界框的規模、某些特定身體部位的位置。
最後我描述了驗證集分層的過程。