我們需要一個數據集來訓練姿勢估計模型,我們的選擇有像COCO、MPII和CrowdPose這樣的公共數據集,姿態估計屬於比較複雜一類的問題。為神經網絡模型建立一個合適的數據集是很困難的,圖像中每個人的每個關節都必須定位和標記,這是一項瑣碎而費時的任務。
目前最流行的姿態估計數據集是COCO數據集。它有大約80類圖像和大約250000個人物實例。
如果檢查此數據集中的一些隨機圖像,你可能會碰到一些與要解決的問題無關的實例,學術界希望達到最高的精度,但在實際生產環境中並不總是如此。
在實際環境中,我們更希望在特定的場景下製作良好的模型,例如行人、籃球運動員、健身房等。
現在我們從COCO數據集中查看此圖像:
你看到紅點了嗎?這是關鍵點:鼻子。
你可能不希望網絡看到僅包含頭部一部分的示例,尤其是在幀的底部。
本文將向你展示COCO數據集的一個示例分析。
COCO數據集
COCO數據集是用於許多計算機視覺任務的大規模通用數據集,包含150萬個對象實例,80個對象類別,25萬人。我們可以在源站點上找到更多詳細信息,在那裡還可以下載所有必需的文件:https://cocodataset.org/。
數據集由圖像文件和注釋文件組成。注釋文件是一個JSON,包含關於一個人(或其他一些類別)的所有元數據。在這裡我們將找到邊界框的位置和大小,區域,關鍵點,源圖像的文件名等。
我們不必手動解析JSON。有一個方便的Python庫pycocotools(https://github.com/cocodataset/cocoapi/tree/master/PythonAPI)。
我們需要train2017.zip(https://cocodataset.org/#download),val2017.zip(https://cocodataset.org/#download),annotations_trainval2017.zip(https://cocodataset.org/#download)。
具體來說,我們只需要人的注釋。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, img_id in enumerate(ids): img_meta = coco.imgs[img_id] ann_ids = coco.getAnnIds(imgIds=img_id) # 圖像的基本參數 img_file_name = img_meta['file_name'] w = img_meta['width'] h = img_meta['height'] # 檢索當前圖像中所有人的元數據 anns = coco.loadAnns(ann_ids) yield [img_id, img_file_name, w, h, anns]...# 迭代圖像for img_id, img_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 img_id, img_fname, w, h, meta in get_meta(coco): images_data.append({ 'image_id': int(img_id), 'path': img_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頁面上寫過有25萬人擁有關鍵點,而我們只查到156165個這樣的例子,他們可能應該刪除「帶關鍵點」這幾個字。
添加額外列
一旦我們將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數據集中展示了一些或多或少有用的指標,比如圖像中人的分布、人的邊界框的規模、某些特定身體部位的位置。
最後我描述了驗證集分層的過程。
github倉庫連結:https://github.com/michalfaber/dataset_toolkit