人3Dモデルを当てはめてポーズ推定 

# 前提

写真に映った人のポーズ推定をする方法としてOpenPose( https://arxiv.org/abs/1611.08050 )がある。
検出方法はたしか各部(二の腕とかふとももとか)を判別し、それのもっともらしい接続を導くことで全身の推定をするとかだったはず。

別の方法を思いついたのでやってみた。

人の3Dのモデルをどう移動、関節回転させれば画像中の人にうまく重なるかで推定を行う。
こんな感じ。
![pic.png](https://qiita-image-store.s3.amazonaws.com/0/108313/0109c6b2-14ea-c215-60b9-6130c1bdd6c2.png)


人物画像を入力し、モデルの中心である骨盤のx,y,zの位置と3軸角度、各部位の3軸角度を推定できればOK。

CNNで人物画像を入力とし、複数の値を同時に回帰で出力し推定する。
CNNにはDenseNet( https://arxiv.org/abs/1608.06993 )を使う。

とりあえずフィージビリティスタディで、3Dモデルの関節をランダムに曲げた画像を作って入力画像とし、回転角度を求めさせた。



# 学習用画像の作り方

モデルはライセンスの自由度が高いハッカドール(©2015 DeNA Co.,Ltd. https://hackadoll.com/mmd )の1号をお借りした。

これをblenderでレンダリングする。
blenderではpythonスクリプトでレンダリングを制御できる。

ハッカドールのモデルはMikuMikuDanseの形式のため、blenderで読み込むためにmmd_tool( 
https://github.com/sugiany/blender_mmd_tools )を使用した。

読み込んだら、各関節をランダムで動かす範囲を決めるため、適当に動かしながら一般的な可動範囲を求める。
例えば肘は伸ばした状態を0度、通常曲がる方を+とすると、0度～+150度ぐらいが可動範囲。
他の回転軸や関節との関係とかは面倒なので考えない。

この可動範囲の中でランダムに曲げて画像を作る。
blenderに食わせるスクリプトは以下。

```python:random_render.py
#ランダムに角度を変えながら画像化。

output_file_num = 10000

import bpy
import copy
import random
import math
import re

#初期設定。
print('syoki settei.')
random_string = "{:06d}".format(random.randrange(100000))
list_filename = "output/f"+random_string+"_filelist.txt"
print('random_string', random_string)
print('list_filename', list_filename)

def myrand(x1,x2,y1,y2,z1,z2):
    return [random.uniform(x1,x2), random.uniform(y1,y2), random.uniform(z1,z2)]

print('change rotation_mode.')
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Waist"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["UpperBody"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["UpperBody2"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Head"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Arm_L"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Elbow_L"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Wrist_L"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Arm_R"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Elbow_R"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Wrist_R"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["LowerBody"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Leg_L"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Knee_L"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Ankle_L"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Leg_R"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Knee_R"].rotation_mode = 'XYZ'
bpy.data.objects["おんだ式ハッカドール1号 v1.00_arm"].pose.bones["Ankle_R"].rotation_mode = 'XYZ'

#後で元に戻すためにバックアップ。
print('sisei backup.')
org_pos = copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Waist"].location)
org_rot = [
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Waist"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["UpperBody"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["UpperBody2"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Head"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Arm_L"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Elbow_L"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Wrist_L"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Arm_R"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Elbow_R"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Wrist_R"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["LowerBody"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Leg_L"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Knee_L"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Ankle_L"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Leg_R"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Knee_R"].rotation_euler),
    copy.copy(bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Ankle_R"].rotation_euler),
]

#角度のリストをファイル出力。
print('output sisei file.')
with open(list_filename, "w") as f:
    for i in range(0,output_file_num):
        Waist_pos = [random.uniform(-5,5),random.uniform(-5,5),random.uniform(-5,5)]
        Waist_rot = myrand(-180,180,-180,180,-180,180)
        UpperBody_rot = myrand(-20,40,-30,30,-20,20)
        UpperBody2_rot = myrand(-20,30,-30,30,-20,20)
        Head_rot = myrand(-30,30,-70,70,-30,30)
        Arm_L_rot = myrand(-60,50,-90,30,-120,50)
        Elbow_L_rot = myrand(-150,0,-10,10,-160,0)
        Wrist_L_rot = myrand(-60,60,-90,50,-50,50)
        Arm_R_rot = myrand(-60,50,-30,90,-50,120)
        Elbow_R_rot = myrand(-150,0,-10,10,0,160)
        Wrist_R_rot = myrand(-60,60,-50,90,-50,50)
        LowerBody_rot = myrand(-30,20,-20,20,-20,20)
        Leg_L_rot = myrand(-140,70,-80,40,-60,20)
        Knee_L_rot = myrand(0,150,-2,2,-2,2)
        Ankle_L_rot = myrand(-30,40,-30,20,-3,30)
        Leg_R_rot = myrand(-140,70,-40,80,-20,60)
        Knee_R_rot = myrand(0,150,-2,2,-2,2)
        Ankle_R_rot = myrand(-30,40,-20,30,-30,3)

        output_filename="output/f"+random_string+"_{:010d}".format(i)+".png"

        print(output_filename,
            Waist_pos[0], Waist_pos[1], Waist_pos[2],
            Waist_rot[0], Waist_rot[1], Waist_rot[2], UpperBody_rot[0], UpperBody_rot[1], UpperBody_rot[2], UpperBody2_rot[0], UpperBody2_rot[1], UpperBody2_rot[2], Head_rot[0], Head_rot[1], Head_rot[2], Arm_L_rot[0], Arm_L_rot[1], Arm_L_rot[2], Elbow_L_rot[0], Elbow_L_rot[1], Elbow_L_rot[2], Wrist_L_rot[0], Wrist_L_rot[1], Wrist_L_rot[2], Arm_R_rot[0], Arm_R_rot[1], Arm_R_rot[2], Elbow_R_rot[0], Elbow_R_rot[1], Elbow_R_rot[2], Wrist_R_rot[0], Wrist_R_rot[1], Wrist_R_rot[2], LowerBody_rot[0], LowerBody_rot[1], LowerBody_rot[2], Leg_L_rot[0], Leg_L_rot[1], Leg_L_rot[2], Knee_L_rot[0], Knee_L_rot[1], Knee_L_rot[2], Ankle_L_rot[0], Ankle_L_rot[1], Ankle_L_rot[2], Leg_R_rot[0], Leg_R_rot[1], Leg_R_rot[2], Knee_R_rot[0], Knee_R_rot[1], Knee_R_rot[2], Ankle_R_rot[0], Ankle_R_rot[1], Ankle_R_rot[2],
            file=f)
        f.flush()

#出力した角度のリストを読み込み、実際にその角度にする。
print('rotate and generate picture.')
with open(list_filename, "r") as f:
    lines = f.readlines()
    for line in lines :
        line = line.rstrip()
        line = line.translate(line.maketrans(","," ")) #CSV形式でも良いように、「,」をスペースに。
        l = line.split() #タブやスペースのところを分割してくれる

        output_filename = l[0]

        bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Waist"].location = [float(i) for i in l[1:3+1]]

        temp = l[4:]
        temp = [ 2*math.pi*float(i)/360.0 for i in temp ]
        temp = [ temp[i*3:i*3+3] for i in range(int(len(temp)/3)) ]
        (
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Waist"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["UpperBody"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["UpperBody2"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Head"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Arm_L"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Elbow_L"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Wrist_L"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Arm_R"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Elbow_R"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Wrist_R"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["LowerBody"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Leg_L"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Knee_L"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Ankle_L"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Leg_R"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Knee_R"].rotation_euler,
            bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Ankle_R"].rotation_euler,
        ) = temp

        bpy.ops.render.render()

        bpy.data.images['Render Result'].save_render(filepath=output_filename)

#バックアップした姿勢を元に戻す。
print('sisei restore.')
bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Waist"].location = org_pos
(
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Waist"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["UpperBody"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["UpperBody2"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Head"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Arm_L"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Elbow_L"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Wrist_L"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Arm_R"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Elbow_R"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Wrist_R"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["LowerBody"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Leg_L"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Knee_L"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Ankle_L"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Leg_R"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Knee_R"].rotation_euler,
    bpy.data.objects['おんだ式ハッカドール1号 v1.00_arm'].pose.bones["Ankle_R"].rotation_euler,
) = org_rot

print('all done.')
```

以下で実行。

```bash:
blender -b hackadoll.blend  -P ./f001_renzoku_random_render.py
```



# コード

Lossは特に工夫せず、位置や各回転角度の正解と推定値とのmean_squared_errorを使った。
特に座標と角度の正規化や重み付けもしていない。
角度は0度と360度とでは同じ意味だが、特にそれも考慮せず、純粋に数値として比較した。
(うまくいかないようなら推定した角度でレンダリングし、生成モデルのように画像どうしで比較してLossを計算するつもりだったが、そこまでしなくてもそこそこうまくいったのでやっていない)

コードは以下。

```python:model_make.py
#!/usr/bin/env python3
#! -*- coding: utf-8 -*-

import sys
import numpy as np
import datetime

import tensorflow as tf
import tensorflow.contrib.keras as keras
from tensorflow.python.keras.models import Sequential, load_model
from tensorflow.python.keras.layers import Dense, Dropout, Flatten, Activation
from tensorflow.python.keras.layers import Conv2D, MaxPooling2D, BatchNormalization, LeakyReLU
from tensorflow.python.keras.callbacks import ModelCheckpoint,TensorBoard,ProgbarLogger
from tensorflow.python.keras.preprocessing.image import load_img, img_to_array, ImageDataGenerator
from tensorflow.python.keras.optimizers import Adam
from tensorflow.python.keras import backend as K

#メモリを一気に全部確保するのではなく順次確保する。
import tensorflow as tf
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.gpu_options.visible_device_list = "0"
K.set_session(tf.Session(config=config))

IMAGE_SIZE = 64
USE_COLOR_PICTURE = True
PICTURE_COLOR_NUM = 3

filename_train="filelist_train.txt"
filename_val="filelist_val.txt"
max_epoch=1000
batch_size=100

if __name__ == '__main__':
    np.set_printoptions(threshold=np.inf)

    # データを入れる配列
    train_image = []
    train_label = []
    val_image = []
    val_label = []

    def load_dataset(filename, print_text) :
        image = []
        label = []
        filepath = []
        f = open(filename, 'r')
        for line in f:
            line = line.rstrip()
            line = line.translate(line.maketrans(","," ")) #CSV形式でも良いように、「,」をスペースに。
            l = line.split() #タブやスペースのところを分割してくれる
            #print(print_text+" file:%s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s"%(l[0],l[1],l[2],l[3],l[4],l[5],l[6],l[7],l[8],l[9],l[10],l[11],l[12],l[13],l[14],l[15],l[16],l[17],l[18],l[19],l[20],l[21],l[22],l[23],l[24],l[25],l[26],l[27],l[28],l[29],l[30],l[31],l[32],l[33],l[34],l[35],l[36],l[37],l[38],l[39],l[40],l[41],l[42],l[43],l[44],l[45]))

            img = load_img(l[0], grayscale=not(USE_COLOR_PICTURE), target_size=(IMAGE_SIZE, IMAGE_SIZE))
            image.append(img_to_array(img)/256.0) # 一列にした後、0-1のfloat値にする
            #片腕の場合。
            #label.append(   [float(n) for n in (l[16],l[17],l[18],l[19],l[20],l[21],l[22],l[23],l[24],)]   )
            #上半身の場合。
            label.append(   [float(n) for n in (l[7],l[8],l[9],l[10],l[11],l[12],l[13],l[14],l[15],l[16],l[17],l[18],l[19],l[20],l[21],l[22],l[23],l[24],l[25],l[26],l[27],l[28],l[29],l[30],l[31],l[32],l[33],)]   )
            filepath.append(l[0])
        f.close()

        # numpy形式に変換
        image = np.asarray(image)
        label = np.asarray(label)

        return image, label, filepath

    train_image, train_label, _ = load_dataset(filename_train, "train")
    val_image, val_label, _ = load_dataset(filename_val, "val")

    K.set_learning_phase(True)

    import densenet
    model = densenet.DenseNet(output_val_num=27,
                              img_dim=train_image.shape[1:],
                              depth=3*5+4,
                              nb_dense_block=3,
                              growth_rate=12,
                              nb_initial_filter=64,
                              dropout_rate=0.2,
                              weight_decay=0.0001)
    # Model output
    model.summary()

    model.compile(loss=keras.losses.mean_squared_error,
                  #optimizer=Adam(lr=1E-3, beta_1=0.9, beta_2=0.999, epsilon=1e-08),
                  optimizer=keras.optimizers.SGD(),
                  metrics=['accuracy'])

    modelcheckpoint = ModelCheckpoint(filepath='logs_model_epoch{epoch:05d}_valloss{val_loss:.5f}.h5', verbose=1, save_best_only=True)
    tensorboard = TensorBoard(log_dir='./logs', histogram_freq=0, write_graph=True, write_grads=False, write_images=False)
    print("If you want to see TensorBoard, you exec below.")
    print("    tensorboard --logdir=(full path of ./logs)")
    print("and access to http://(IPaddress):6006")

    model.fit(train_image, train_label,
              batch_size=batch_size,
              epochs=max_epoch,
              verbose=1,
              validation_data=(val_image, val_label),
              callbacks=[modelcheckpoint, tensorboard])

    score = model.evaluate(val_image, val_label, verbose=1)
    print('Val loss:', score[0])
    print('Val accuracy:', score[1])

    model.save('model_last.h5')
    print("save model and weight. done.")

    print("finish.")
```

```python:densenet.py
#ベースは以下。
#  https://github.com/tdeboissiere/DeepLearningImplementations/blob/master/DenseNet/densenet.py

from tensorflow.python.keras.models import Model
from tensorflow.python.keras.layers import Dense, Dropout, Activation
from tensorflow.python.keras.layers import Conv2D
from tensorflow.python.keras.layers import AveragePooling2D
from tensorflow.python.keras.layers import GlobalAveragePooling2D
from tensorflow.python.keras.layers import Input, Concatenate
from tensorflow.python.keras.layers import BatchNormalization
from tensorflow.python.keras.regularizers import l2
from tensorflow.python.keras import backend as K

def conv_factory(x, concat_axis, nb_filter,
                 dropout_rate=None, weight_decay=1E-4):
    """Apply BatchNorm, Relu 3x3Conv2D, optional dropout

    :param x: Input keras network
    :param concat_axis: int -- index of contatenate axis
    :param nb_filter: int -- number of filters
    :param dropout_rate: int -- dropout rate
    :param weight_decay: int -- weight decay factor

    :returns: keras network with b_norm, relu and Conv2D added
    :rtype: keras network
    """

    x = BatchNormalization(axis=concat_axis,
                           gamma_regularizer=l2(weight_decay),
                           beta_regularizer=l2(weight_decay))(x)
    x = Activation('relu')(x)
    x = Conv2D(nb_filter, (3, 3),
               kernel_initializer="he_uniform",
               padding="same",
               use_bias=False,
               kernel_regularizer=l2(weight_decay))(x)
    if dropout_rate:
        x = Dropout(dropout_rate)(x)

    return x

def transition(x, concat_axis, nb_filter,
               dropout_rate=None, weight_decay=1E-4):
    """Apply BatchNorm, Relu 1x1Conv2D, optional dropout and Maxpooling2D

    :param x: keras model
    :param concat_axis: int -- index of contatenate axis
    :param nb_filter: int -- number of filters
    :param dropout_rate: int -- dropout rate
    :param weight_decay: int -- weight decay factor

    :returns: model
    :rtype: keras model, after applying batch_norm, relu-conv, dropout, maxpool

    """

    x = BatchNormalization(axis=concat_axis,
                           gamma_regularizer=l2(weight_decay),
                           beta_regularizer=l2(weight_decay))(x)
    x = Activation('relu')(x)
    x = Conv2D(nb_filter, (1, 1),
               kernel_initializer="he_uniform",
               padding="same",
               use_bias=False,
               kernel_regularizer=l2(weight_decay))(x)
    if dropout_rate:
        x = Dropout(dropout_rate)(x)
    x = AveragePooling2D((2, 2), strides=(2, 2))(x)

    return x

def denseblock(x, concat_axis, nb_layers, nb_filter, growth_rate,
               dropout_rate=None, weight_decay=1E-4):
    """Build a denseblock where the output of each
       conv_factory is fed to subsequent ones

    :param x: keras model
    :param concat_axis: int -- index of contatenate axis
    :param nb_layers: int -- the number of layers of conv_
                      factory to append to the model.
    :param nb_filter: int -- number of filters
    :param dropout_rate: int -- dropout rate
    :param weight_decay: int -- weight decay factor

    :returns: keras model with nb_layers of conv_factory appended
    :rtype: keras model

    """

    list_feat = [x]

    for i in range(nb_layers):
        x = conv_factory(x, concat_axis, growth_rate,
                         dropout_rate, weight_decay)
        list_feat.append(x)
        x = Concatenate(axis=concat_axis)(list_feat)
        nb_filter += growth_rate

    return x, nb_filter

def denseblock_altern(x, concat_axis, nb_layers, nb_filter, growth_rate,
                      dropout_rate=None, weight_decay=1E-4):
    """Build a denseblock where the output of each conv_factory
       is fed to subsequent ones. (Alternative of a above)

    :param x: keras model
    :param concat_axis: int -- index of contatenate axis
    :param nb_layers: int -- the number of layers of conv_
                      factory to append to the model.
    :param nb_filter: int -- number of filters
    :param dropout_rate: int -- dropout rate
    :param weight_decay: int -- weight decay factor

    :returns: keras model with nb_layers of conv_factory appended
    :rtype: keras model

    * The main difference between this implementation and the implementation
    above is that the one above
    """

    for i in range(nb_layers):
        merge_tensor = conv_factory(x, concat_axis, growth_rate,
                                    dropout_rate, weight_decay)
        x = Concatenate(axis=concat_axis)([merge_tensor, x])
        nb_filter += growth_rate

    return x, nb_filter

def DenseNet(output_val_num, img_dim, depth, nb_dense_block, growth_rate,
             nb_initial_filter, dropout_rate=None, weight_decay=1E-4):
    """ Build the DenseNet model

    :param output_val_num: int -- 最終的にいくつの値を出力するか。
    :param img_dim: tuple -- (channels, rows, columns)
    :param depth: int -- how many layers
    :param nb_dense_block: int -- number of dense blocks to add to end
    :param growth_rate: int -- number of filters to add
    :param nb_initial_filter: int -- number of filters
    :param dropout_rate: float -- dropout rate
    :param weight_decay: float -- weight decay

    :returns: keras model with nb_layers of conv_factory appended
    :rtype: keras model

    """
    
    concat_axis = -1

    model_input = Input(shape=img_dim)

    assert (depth - 4) % 3 == 0, "Depth must be 3 N + 4"

    # layers in each dense block
    nb_layers = int((depth - 4) / 3)

    # Initial convolution
    x = Conv2D(nb_initial_filter, (3, 3),
               kernel_initializer="he_uniform",
               padding="same",
               name="initial_conv2D",
               use_bias=False,
               kernel_regularizer=l2(weight_decay))(model_input)

    nb_filter = nb_initial_filter
    # Add dense blocks
    for block_idx in range(nb_dense_block - 1):
        x, nb_filter = denseblock(x, concat_axis, nb_layers,
                                  nb_filter, growth_rate, 
                                  dropout_rate=dropout_rate,
                                  weight_decay=weight_decay)
        # add transition
        x = transition(x, concat_axis, nb_filter, dropout_rate=dropout_rate,
                       weight_decay=weight_decay)

    # The last denseblock does not have a transition
    x, nb_filter = denseblock(x, concat_axis, nb_layers,
                              nb_filter, growth_rate, 
                              dropout_rate=dropout_rate,
                              weight_decay=weight_decay)

    x = BatchNormalization(axis=concat_axis,
                           gamma_regularizer=l2(weight_decay),
                           beta_regularizer=l2(weight_decay))(x)
    x = Activation('relu')(x)
    x = GlobalAveragePooling2D(data_format=K.image_data_format())(x)
    x = Dense(output_val_num,
              kernel_regularizer=l2(weight_decay),
              bias_regularizer=l2(weight_decay))(x)

    densenet = Model(inputs=[model_input], outputs=[x], name="DenseNet")

    return densenet
```

model_make.pyを実行すればOK。



# 実験1 片腕のみ可動させて推定

胴体は固定のまま、左腕の肩、肘、手首について、それぞれ3軸回転させた。
全9パラメータを推定する。

40000枚生成し、38000枚をtraining用に、2000枚をvalidation用にした。

学習はGTX 1060で行った。
750epochで31時間程度。

学習後、validation lossが良かったモデルを使って推定を行い、出力された角度を元にレンダリングを行ったものが以下。
![hackadoll_kataude1.png](https://qiita-image-store.s3.amazonaws.com/0/108313/7ee5306b-cf26-c1b7-4f20-94ac5c5ac36b.png) ![hackadoll_kataude2.png](https://qiita-image-store.s3.amazonaws.com/0/108313/2ff416ac-f1a9-b648-5424-02ab0b3ed156.png)

思った以上にうまくいってびっくり。特に肩はともかく肘や手首は肩の角度の影響を受けるので難しいかと思ったが、問題なかった。



# 実験2 上半身のみ可動させて推定

骨盤の位置や角度は動かさず、上半身の関節について回転させた。
全27パラメータを推定する。

100000枚生成し、90000枚をtraining用に、10000枚をvalidation用にした。

学習はGTX 1060で行った。
600epochで38時間程度。

以下が結果。
![hackadoll_jouhansin1.png](https://qiita-image-store.s3.amazonaws.com/0/108313/ec03d593-f579-61ac-deb7-74151582fd26.png) ![hackadoll_jouhansin2.png](https://qiita-image-store.s3.amazonaws.com/0/108313/4eb00390-eea0-01fb-b999-3b7775b32ed6.png)

部位によって当たっているところもあれば当たっていないところもある。
胴体から頭にかけてはわりと良いが、一方で手はあまり当たっていない印象。
胴体や頭は面積が大きいのと、頭は黄色で目立つため学習しやすかったのかもしれない。



# まとめ

まあまあ良い結果となった。

他に骨盤の位置、全身関節角度も含め、体全体のパラメータを推定もさせてみたが、これについてはうまくいかなかった。
もう少し工夫が必要と思われる。

以下やってみたい点等である。

* GPUのメモリサイズの上限のため、学習用画像のサイズが64x64とかなり小さくしている。
  このサイズだと画像を目で見てもよく分からないレベル。
  もっといいGPUで学習させたい…RTX2080Ti欲しい…
* 長時間学習させたい。電気代が…
* 学習しやすいよう、各パラメータ毎の誤差に対して重みをつけてLossを計算する。
  はじめは学習しやすいよう、胴体から頭にかけてのパラメータの重みを大きくし、
  それらが正しく推定できるようになってきたら手の重みを大きくするとよさそう。
* いろいろな3Dモデルを使ったり、適当な背景を設定して学習させたい。
  今は1つだけなのでおそらく人物写真を推定させてもうまくいかない。