OpenPose を Finetuning (転移学習の手法)して見下ろし画像の姿勢推定にトライしてみる

2020.06.24

1 はじめに

おはようございます、もきゅりんです。

この稿では、OpenPose モデルを Finetuning して見下ろし画像(Top view)の姿勢推定を行います。

ここで扱う OpenPose モデルとは、つくりながら学ぶ! PyTorchによる発展ディープラーニング という書籍の第4章で紹介されているモデルとなります。 *1

このモデルを使った理由としては、カスタマイズして姿勢推定する上で最も手近だったという点になります。

Finetuning を行う背景として、既存の OpenPose モデルだと見下ろし画像やしゃがみポーズでの姿勢推定がうまくいかなかったことと、既存モデルを様々なシチュエーションに適応させていく経験を得るためです。

Finetuningをする上で、ある程度の量が必要となる見下ろし画像(Top view)のRGBデータセットがなかったため、ITOP Dataset というデータセットを使うことで、見下ろし画像の姿勢推定がうまくいくかどうか検証しました。

なお、自分はCVの研究者や専門家でもなく、専門教育を受けてきたわけでもないため、随所におかしな点が見られるかもしれませんが、ご容赦下さい。

何かありましたらご指摘頂けると幸いです。

2 用語

姿勢推定(Pose Estimation)とは

簡単に言えば、画像内の人間の各部位と部位とを検出して、それらをつなぎ合わせる技術です。

OpenPose とは姿勢推定の一手法で、高精度で計算が速い(かった?)、という特徴を持つモデルです。

姿勢推定(Pose Estimation)については下記まとめも分かりやすいです。すでに2Dは古いかと思いますが、OpenPose やその他過去の姿勢推定のモデルにも比較して触れられています。

コンピュータビジョンの最新論文調査 2D Human Pose Estimation 編

見下ろし画像(Top view)とは

以下イメージのように上から撮影された画像です。

top-view1

姿勢推定を行うと、あまりうまくいきません。

ITOP Dataset とは

Towards Viewpoint Invariant 3D Human Pose Estimation という論文のデータセットで使われている、3D姿勢推定を行うために、複数カメラ(正面を含む側面、天井)から撮影された Depth 画像です。

本来は、想定している入力画像が RGB であれば、Depth 画像から変換された RGB 画像ではなく、通常の RGB 画像で学習および検証テストさせるべきだと思いますが、利用できる、ある程度の量の見下ろし画像(Top View)が手元になかったため、この論文で使われている約5万の学習/検証の RGB 画像データセットを使ってやってみることにしました。

Finetuning とは

転移学習における一手法で、学習済みモデルの一部の層を解凍して、学習済みモデルをベースとする新しいモデルの変更部分と、解凍された層の両方で、目的となる入力データから学習を行う手法です。 *2

3 結果

結果として、元のパラメータ pose_model_scratch.pth で検出されなかった画像において姿勢推定が検出されます。

簡易的に 15epoch で学習しています。

  • train データが 7,000 (ITOP) + 3,000 (COCO) の10,000
  • val データが 3,000 (ITOP) + 900 (COCO) の3,900

元のモデルの検出

Finetuningされたモデル

ただし、残念ながら通常の RGB 画像ではうまく検出がされませんでした。

やはり、想定される入力画像に合わせて学習させる必要があるだろうと思います。

雑に単一色に加工した画像だと多少検出がされる場合がありました。

参考書籍において、本来の推論実行では推論対象のテスト画像をデータオーギュメンテーションして姿勢推定を行って、それらすべての結果を総合して姿勢推定を行うと記述されています。今回は、Finetuning を行った訓練や実験の意味合いが強く、精度にあまり厳密になる理由もなかったため、書籍同様、簡易的な推論としています。

また、本来は、 指標として ITOP Dataset に対してのmAPを出力する必要があるかと思いますが、関連するプログラムやファイルの修正に時間がかかりそうだったので断念しました。

参考までに、今回のFinetuningしたモデルのmAPを掲載しておくと下記です。

Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets= 20 ] = 0.547
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets= 20 ] = 0.623
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets= 20 ] = 0.623
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets= 20 ] = 0.187
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets= 20 ] = 0.878
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 20 ] = 0.573
 Average Recall     (AR) @[ IoU=0.50      | area=   all | maxDets= 20 ] = 0.636
 Average Recall     (AR) @[ IoU=0.75      | area=   all | maxDets= 20 ] = 0.636
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets= 20 ] = 0.180
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets= 20 ] = 0.900

評価の教師対象はCOCOのアノテーションなので、元の成績 (0.614) より落ちているのが分かります。(0.547)

おそらくは、ITOP Dataset に対するmAPは、元のモデルより高くなっているかと思います。

なお、mAPの説明についてはこちらが取っ掛かりやすかったです。

【物体検出】mAP ( mean Average Precision ) の算出方法

では、次節よりここまでの準備について記述します。

4 やることの整理

参考書籍に記載されている表を修正して掲示します。

No 対応 変更点
1 前処理 ITOP Dataset をDL,マスク画像を作成する, アノテーションファイルをITOP Datasetに合わせる, COCOとITOPとで前処理方法を別にする
2 Datasetの作成 書籍では検証データしか使っていないため、学習データ、検証データを使うように修正
3 DataLoaderの作成 なし
4 ネットワークモデルの作成 Finetuningするため修正
5 順伝搬(forward)の定義 なし
6 最適化手法の設定 学習率およびモーメントを変更
7 学習・検証の実施 なし
9 テストデータで推論 なし

5 準備

サーバの起動

こちらは SageMaker などの利用はせず、書籍に準じた対応をしています。

  • Region: us-east-1
  • AMI ID: jupyternotebook-AMI-20200526 (ami-0379369510191811d)
  • Instance type: p2.xlarge or p2.8xlarge
  • Python Ver: 3.6

データのダウンロード

画像データセットは ITOP Dataset Images からDLし、アノテーションは ITOP Dataset Labels をDLします。

ITOP Dataset のマスク画像を作成して data/mask 直下に格納してあげます。

# make_itopmask_image.py
import cv2
import numpy as np

height = 240 # 生成画像の高さ
width = 320 # 生成画像の幅
imgMask = np.full((height, width, 1), 1, dtype=np.uint8)

# マスク範囲を四角形で描画
boxFromY = 0 #マスク範囲開始位置 Y座標
boxFromX = 0 #マスク範囲開始位置 X座標
boxToY = 240 #マスク範囲終了位置 Y座標
boxToX = 320 #マスク範囲終了位置 X座標
cv2.rectangle(imgMask, (boxFromX, boxFromY), (boxToX, boxToY),(255), cv2.FILLED)

# マスク結果画像を保存
cv2.imwrite("MaskImg.png", imgMask)

アノテーションのマッピング

COCO.json から利用されているアノテーションの情報は基本的には下記なので、これらを基にITOP Dataset からCOCO.json 形式のアノテーションを作成します。

  • numOtherPeople
  • objpos
  • joint_self
  • img_paths

objpos は対象人物の中心を示します。

ITOP Dataset のすべての画像を確認していませんが、論文の主旨として、おそらく他の人物が写っていることはないと考え、numOtherPeople はすべて 0 としています。

COCO.json と ITOP Dataset のLabels の image_coordinates のマッピングは以下表のようにしています。

COCO 部位 ITOP 部位
0 0
1 1
2 右肩 2 右肩
3 右肘 4 右肘
4 右手首 6 右手
5 左肩 3 左肩
6 左肘 5 左肘
7 左手首 7 左手
8 右尻 9 右尻
9 右膝 11 右膝
10 右足首 13 右足
11 左尻 10 左尻
12 左膝 12 左膝
13 左足首 14 左足
14 右目 - -
15 左目 - -
16 右耳 - -
17 左耳 - -

objpos に ITOP Dataset Labels の 8 のTorso(胴体) をマップしています。

なお、COCO.json の各部位のx,y座標を示す joint_self には、0のときは、アノテーションの座標情報はあるが、画像内にはその部位は写っていない、1のときはアノテーションおよび画像内に部位が存在する、2はアノテーションにも画像内にも存在しないといった視認性の情報も記載されています。

ITOP Dataset Labels にも visible_joints という Key があるので、これとマッピングすると良いと思われますが、今回はこちらは利用せず、各部位にx,y座標が存在しているならば、視認性情報は 1 としました。COCO.json のアノテーションにあり、ITOP のアノテーションにないものの視認性は 2 としています。

下記の雑Pythonコードに引数をのせて叩いて作成します。

他者が利用する想定をしていなかったため、引数に対して特にバリデーションを設けたりしていません。

(自分は COCO および ITOP Dataset で全データを利用することは想定していなかったので、一部分を抽出するようにしています。)

# arg 学習データか検証データか, 抽出開始地点, 抽出終わり地点
python itop2coco.py train 3000 10000
python itop2coco.py test 1000 3000
# arg データサイズ, 検証学習比率
python extract_coco_json.py 3000 0.3
# 作成したファイルを記入
python merge_itop_coco.py
# itop2coco.py itopのh5ファイルからCOCO.json形式に変換する
import sys
import json
import h5py
import numpy as np

MODE = sys.argv[1]
START = int(sys.argv[2])
END = int(sys.argv[3])
SAMPLE_SIZE = END - START
output_file_path = "./itop_" + MODE + str(SAMPLE_SIZE) + ".json"
data = {}
data['root'] = []

input_h5file = "ITOP_top_" + MODE + "_labels.h5"
DIR = "top_" + MODE + "_images/"


class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        else:
            return super(MyEncoder, self).default(obj)


custom_itop_annotation_list = []
with h5py.File(input_h5file, 'r') as f:

    # for i in range(len(f['image_coordinates'])):
    for i in range(START, END):
        if np.any(f['image_coordinates'][i] < 0):
            continue
        annotation = {}
        itop_samples = f['image_coordinates'][i]
        if MODE == 'train':
            annotation['img_paths'] = MODE + "2014/" + DIR + \
                str(f['id'][i].decode()) + ".jpg"
        else:
            annotation['img_paths'] = "val2014/" + DIR + \
                str(f['id'][i].decode()) + ".jpg"
        annotation['numOtherPeople'] = 0
        itop_annotation_list = [j for j in itop_samples]

        # joint_selfデータの要素を追加
        custom_itop_annotation_list = []
        for itop_anno in itop_annotation_list:
            if itop_anno[0] != 0:
                itop_anno = np.insert(itop_anno, 2, 1)
            else:
                itop_anno = np.insert(itop_anno, 2, 0)
            custom_itop_annotation_list.append(itop_anno)

        # ITOPアノテーションからCOCOアノテーションに順番を入れ替える
        annotation['joint_self'] = []
        annotation['joint_self'].append(custom_itop_annotation_list[0])
        annotation['joint_self'].append(custom_itop_annotation_list[1])
        annotation['joint_self'].append(custom_itop_annotation_list[2])
        annotation['joint_self'].append(custom_itop_annotation_list[4])
        annotation['joint_self'].append(custom_itop_annotation_list[6])
        annotation['joint_self'].append(custom_itop_annotation_list[3])
        annotation['joint_self'].append(custom_itop_annotation_list[5])
        annotation['joint_self'].append(custom_itop_annotation_list[7])
        annotation['joint_self'].append(custom_itop_annotation_list[9])
        annotation['joint_self'].append(custom_itop_annotation_list[11])
        annotation['joint_self'].append(custom_itop_annotation_list[13])
        annotation['joint_self'].append(custom_itop_annotation_list[10])
        annotation['joint_self'].append(custom_itop_annotation_list[12])
        annotation['joint_self'].append(custom_itop_annotation_list[14])
        # 残り4つの要素を追加
        for n in range(4):
            annotation['joint_self'].append([0, 0, 2])
        # objposを胴体の値として定義
        annotation['objpos'] = custom_itop_annotation_list[8][:2]
        annotation['type'] = 1

        data['root'].append(annotation)

with open(output_file_path, 'w') as outfile:
    json.dump(data, outfile, indent=4, cls=MyEncoder)
# extract_coco_json.py COCO.jsonから適当なサイズを抽出する
import sys
import json

train_data_size = int(sys.argv[1])
train_val_ratio = float(sys.argv[2])
val_data_size = train_data_size * train_val_ratio
TRAIN_COUNTS = 0
VAL_COUNTS = 0

file_path = "./coco" + \
    str(int(train_data_size + val_data_size)) + "_sampling.json"
data = {}
data['root'] = []

with open('COCO.json', 'r') as data_file:
    json_dict = json.load(data_file)
    data_json = json_dict['root']

num_samples = len(data_json)
for i in range(num_samples):
    if 'train' in data_json[i]['img_paths'] and train_data_size > TRAIN_COUNTS:
        TRAIN_COUNTS += 1
        data_json[i]['type'] = 0
        data['root'].append(data_json[i])
    elif 'val' in data_json[i]['img_paths'] and val_data_size > VAL_COUNTS:
        VAL_COUNTS += 1
        data_json[i]['type'] = 0
        data['root'].append(data_json[i])

# jsonファイルに書き込み
with open(file_path, 'w') as outfile:
    json.dump(data, outfile, indent=4)
# merge_itop_coco.py COCO.json形式に変換したitopアノテーションと抽出したCOCOアノテーションをがっちゃんこする
import json

coco_file_path = "./coco3900_sampling.json"
test_file_path = "./itop_test2000.json"
train_file_path = "./itop_train7000.json"
file_path = "./itop_coco_extracted.json"

# coco.jsonを辞書に読み込み
with open(coco_file_path, 'r') as coco_data_file:
    coco_json_dict = json.load(coco_data_file)
    coco_data_json = coco_json_dict['root']

# test_jsonを辞書に読み込み
with open(test_file_path, 'r') as test_data_file:
    test_json_dict = json.load(test_data_file)
    test_data_json = test_json_dict['root']

# train_jsonを辞書に読み込み
with open(train_file_path, 'r') as train_data_file:
    train_json_dict = json.load(train_data_file)

# test_data合体
for test_anno_list in test_data_json:
    train_json_dict['root'].append(test_anno_list)

# coco_data合体
for coco_anno_list in coco_data_json:
    train_json_dict['root'].append(coco_anno_list)

# jsonファイルに書き込み
with open(file_path, 'w') as outfile:
    json.dump(train_json_dict, outfile, indent=4)

作成したアノテーションは data 直下に格納します。

書籍のコード修正

その他コードの修正点や詳細については、こちら の ForkしたGitHub をご参照、ご利用下さい。

学習結果

1GPUの p2.xlarge で約6時間くらいでした。

学習lossと検証lossとに乖離が生まれ始めており、若干過学習の兆候が現れています。

6 今後の課題

  1. 学習させる層の効果的な選択を行えること
  2. multi-GPUを使った学習
  3. 精度を上げるための学習

1 学習させる層の効果的な選択を行うこと

今回は特に理論的な背景ももたず、解凍する層を選択していたため、どのような場合にどの層を解凍するのが適切か、何かしら選択できる基準があると良いと思いました。

2 multi-GPUを使った学習

マルチGPUでトライしてみましたが、うまく学習が進まず、インスタンス代が高価なため何度も試行錯誤することにビビってしまい、今回は1GPUで対応しました。

3 精度を改善するための学習

1 にも関わる課題かもしれませんが、学習されたモデルによる姿勢推定で検出された各部位で検出されていない(されにくい)部位や、不正確に検出された部位を、さらに正確に検出できるようにするための手法や分析方法を学ぶ必要があると思いました。もちろん、そもそも別の適切なモデルを探して使う、というリサーチも必要です。

最後に

以上です。

まとめるのも含めいろいろと試行錯誤を要しました。。

また通常業務に比べて、Python でデータとにらめっこすることが多くて楽しかったです。

ITOP Dataset の作者には公的開示して利用できるようにしてもらっていたことに感謝致します。

Mさん、Yさん、 画像提供ありがとうございました。

どなたかのお役に立てば幸いです。

Appendix

mAPの出力についての備忘録

こちら をDLして Evalute を参考にして実施する。

  1. evaluation.py は evaluate ディレクトリの上に移動

  2. weights パラメータを読み込ませる

# evaluation.py 抜粋
...
#Notice, if you using the
with torch.autograd.no_grad():
    #net = OpenPoseNet()
    # マルチGPUのとき
    net = torch.nn.DataParallel(OpenPoseNet())
    # this path is with respect to the root of the project
    weight_name = './weights/YOUR_WEIGHTS.pth'
    net_weights = torch.load(weight_name)
    keys = list(net_weights.keys())

    weights_load = {}

    # ロードした内容を、本書で構築したモデルの
    # パラメータ名net.state_dict().keys()にコピーする
    for i in range(len(keys)):
        weights_load[list(net.state_dict().keys())[i]
                     ] = net_weights[list(keys)[i]]

    # コピーした内容をモデルに与える
    state = net.state_dict()
    state.update(weights_load)
    net.load_state_dict(state)
    print("using", torch.cuda.device_count(), "GPUs!")
    net.eval()
    net.float()
    net = net.cuda()
...
  1. 実行する
python evaluation.py

参考

脚注

  1. Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields の2017年初期版の実装
  2. 「転移学習」をどの文脈で使うかで色々と議論があるみたいですが、一般的には転移学習とFinetuningは別物のアプローチです。https://www.quora.com/What-is-the-difference-between-transfer-learning-and-fine-tuning