axinc-ai / ailia-models

The collection of pre-trained, state-of-the-art AI models for ailia SDK
2.06k stars 330 forks source link

ADD 3d mpe posenet #293

Closed kyakuno closed 3 years ago

kyakuno commented 4 years ago

https://github.com/mks0601/3DMPPE_POSENET_RELEASE MIT

mucunwuxian commented 3 years ago

@kyakuno こちら、未アサインであれば、担当させていただいてもよろしいでしょうか? (お手数お掛けして申し訳ないのですが、self assignができないようでした…。🙇💦)

kyakuno commented 3 years ago

はい、お願いします。

mucunwuxian commented 3 years ago

📝 ちょっと冗長で恐縮なのですが、備忘になります…。


論文リンク

ICCV2019、韓国の方々 https://arxiv.org/pdf/1907.11346.pdf


GitHubリンク

Pytorch、Official、Star:363 https://github.com/mks0601/3DMPPE_POSENET_RELEASE


サンプルの動かし方

GPUサーバーにて

READMEに沿って、demo.pyを実施すれば、簡単に動きます。 ただし、リモートで繋いでいる場合、3D vidualizeのfigure plotが、表れてきません。 ./common/utils/vis.pydef vis_3d_multiple_skeletonに、plt.savefig('output_pose_3d.png')などとコード追加すれば、ワンショットだけは画像として取得できます。 或いは、以下のようなコードで、複数アングルが取得できます。

def vis_3d_multiple_skeleton(kpt_3d, kpt_3d_vis, kps_lines, filename=None):

    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')

    # Convert from plt 0-1 RGBA colors to 0-255 BGR colors for opencv.
    cmap = plt.get_cmap('rainbow')
    colors = [cmap(i) for i in np.linspace(0, 1, len(kps_lines) + 2)]
    colors = [np.array((c[2], c[1], c[0])) for c in colors]

    for l in range(len(kps_lines)):
        i1 = kps_lines[l][0]
        i2 = kps_lines[l][1]

        person_num = kpt_3d.shape[0]
        for n in range(person_num):
            x = np.array([kpt_3d[n,i1,0], kpt_3d[n,i2,0]])
            y = np.array([kpt_3d[n,i1,1], kpt_3d[n,i2,1]])
            z = np.array([kpt_3d[n,i1,2], kpt_3d[n,i2,2]])

            if kpt_3d_vis[n,i1,0] > 0 and kpt_3d_vis[n,i2,0] > 0:
                ax.plot(x, z, -y, c=colors[l], linewidth=2)
            if kpt_3d_vis[n,i1,0] > 0:
                ax.scatter(kpt_3d[n,i1,0], kpt_3d[n,i1,2], -kpt_3d[n,i1,1], c=colors[l], marker='o')
            if kpt_3d_vis[n,i2,0] > 0:
                ax.scatter(kpt_3d[n,i2,0], kpt_3d[n,i2,2], -kpt_3d[n,i2,1], c=colors[l], marker='o')

    if filename is None:
        ax.set_title('3D vis')
    else:
        ax.set_title(filename)

    ax.set_xlabel('X Label')
    ax.set_ylabel('Z Label')
    ax.set_zlabel('Y Label')
    ax.legend()

    # add code here!!!
    for elev in range(0, 360, 15):
        for azim in range(0, 360, 15):
            ax.view_init(elev=elev, azim=azim)
            plt.savefig('output_pose_3d_elev%03d_azim%03d.png' % 
                        (elev, azim))

    plt.show()
    cv2.waitKey(0)

(入力画像) input

(出力3Dプロットを結合したもの) sample


CPUローカルにて

コードが、GPU駆動前提で書かれている為、それを調整することで、CPUで動かすことができる。 以下、修正したコードを添付する。

(./demo/demo.py)

import sys
import os
import os.path as osp
import argparse
import numpy as np
import cv2
import torch
import torchvision.transforms as transforms
from torch.nn.parallel.data_parallel import DataParallel
import torch.backends.cudnn as cudnn

sys.path.insert(0, osp.join('..', 'main'))
sys.path.insert(0, osp.join('..', 'data'))
sys.path.insert(0, osp.join('..', 'common'))
from config import cfg
from model import get_pose_net
from dataset import generate_patch_image
from utils.pose_utils import process_bbox, pixel2cam
from utils.vis import vis_keypoints, vis_3d_multiple_skeleton

def parse_args():
    parser = argparse.ArgumentParser()
    # parser.add_argument('--gpu', type=str, dest='gpu_ids')
    parser.add_argument('--test_epoch', type=str, dest='test_epoch')
    args = parser.parse_args()

    # # test gpus
    # if not args.gpu_ids:
    #     assert 0, print("Please set proper gpu ids")

    # if '-' in args.gpu_ids:
    #     gpus = args.gpu_ids.split('-')
    #     gpus[0] = 0 if not gpus[0].isdigit() else int(gpus[0])
    #     gpus[1] = len(mem_info()) if not gpus[1].isdigit() else int(gpus[1]) + 1
    #     args.gpu_ids = ','.join(map(lambda x: str(x), list(range(*gpus))))

    assert args.test_epoch, 'Test epoch is required.'
    return args

# argument parsing
args = parse_args()
# cfg.set_args(args.gpu_ids)
# cudnn.benchmark = True

# MuCo joint set
joint_num = 21
joints_name = ('Head_top', 'Thorax', 'R_Shoulder', 'R_Elbow', 'R_Wrist', 'L_Shoulder', 'L_Elbow', 'L_Wrist', 'R_Hip', 'R_Knee', 'R_Ankle', 'L_Hip', 'L_Knee', 'L_Ankle', 'Pelvis', 'Spine', 'Head', 'R_Hand', 'L_Hand', 'R_Toe', 'L_Toe')
flip_pairs = ( (2, 5), (3, 6), (4, 7), (8, 11), (9, 12), (10, 13), (17, 18), (19, 20) )
skeleton = ( (0, 16), (16, 1), (1, 15), (15, 14), (14, 8), (14, 11), (8, 9), (9, 10), (10, 19), (11, 12), (12, 13), (13, 20), (1, 2), (2, 3), (3, 4), (4, 17), (1, 5), (5, 6), (6, 7), (7, 18) )

# snapshot load
model_path = './snapshot_%d.pth.tar' % int(args.test_epoch)
assert osp.exists(model_path), 'Cannot find model at ' + model_path
print('Load checkpoint from {}'.format(model_path))
model = get_pose_net(cfg, False, joint_num)
# model = DataParallel(model).cuda()
model = DataParallel(model)  # delete cuda, DataParallel need for load model also on CPU...
ckpt = torch.load(model_path, map_location=torch.device('cpu'))
model.load_state_dict(ckpt['network'])
model.eval()

# prepare input image
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=cfg.pixel_mean, std=cfg.pixel_std)])
img_path = 'input.jpg'
original_img = cv2.imread(img_path)
original_img_height, original_img_width = original_img.shape[:2]

# prepare bbox
bbox_list = [
[139.41, 102.25, 222.39, 241.57],\
[287.17, 61.52, 74.88, 165.61],\
[540.04, 48.81, 99.96, 223.36],\
[372.58, 170.84, 266.63, 217.19],\
[0.5, 43.74, 90.1, 220.09]] # xmin, ymin, width, height
root_depth_list = [11250.5732421875, 15522.8701171875, 11831.3828125, 8852.556640625, 12572.5966796875] # obtain this from RootNet (https://github.com/mks0601/3DMPPE_ROOTNET_RELEASE/tree/master/demo)
assert len(bbox_list) == len(root_depth_list)
person_num = len(bbox_list)

# normalized camera intrinsics
focal = [1500, 1500] # x-axis, y-axis
princpt = [original_img_width/2, original_img_height/2] # x-axis, y-axis
print('focal length: (' + str(focal[0]) + ', ' + str(focal[1]) + ')')
print('principal points: (' + str(princpt[0]) + ', ' + str(princpt[1]) + ')')

# for each cropped and resized human image, forward it to PoseNet
output_pose_2d_list = []
output_pose_3d_list = []
for n in range(person_num):
    bbox = process_bbox(np.array(bbox_list[n]), original_img_width, original_img_height)
    img, img2bb_trans = generate_patch_image(original_img, bbox, False, 1.0, 0.0, False) 
    # img = transform(img).cuda()[None,:,:,:]
    img = transform(img)[None,:,:,:]  # delete cuda...

    # forward
    with torch.no_grad():
        pose_3d = model(img) # x,y: pixel, z: root-relative depth (mm)

    # inverse affine transform (restore the crop and resize)
    pose_3d = pose_3d[0].cpu().numpy()
    pose_3d[:,0] = pose_3d[:,0] / cfg.output_shape[1] * cfg.input_shape[1]
    pose_3d[:,1] = pose_3d[:,1] / cfg.output_shape[0] * cfg.input_shape[0]
    pose_3d_xy1 = np.concatenate((pose_3d[:,:2], np.ones_like(pose_3d[:,:1])),1)
    img2bb_trans_001 = np.concatenate((img2bb_trans, np.array([0,0,1]).reshape(1,3)))
    pose_3d[:,:2] = np.dot(np.linalg.inv(img2bb_trans_001), pose_3d_xy1.transpose(1,0)).transpose(1,0)[:,:2]
    output_pose_2d_list.append(pose_3d[:,:2].copy())

    # root-relative discretized depth -> absolute continuous depth
    pose_3d[:,2] = (pose_3d[:,2] / cfg.depth_dim * 2 - 1) * (cfg.bbox_3d_shape[0]/2) + root_depth_list[n]
    pose_3d = pixel2cam(pose_3d, focal, princpt)
    output_pose_3d_list.append(pose_3d.copy())

# visualize 2d poses
vis_img = original_img.copy()
for n in range(person_num):
    vis_kps = np.zeros((3,joint_num))
    vis_kps[0,:] = output_pose_2d_list[n][:,0]
    vis_kps[1,:] = output_pose_2d_list[n][:,1]
    vis_kps[2,:] = 1
    vis_img = vis_keypoints(vis_img, vis_kps, skeleton)
cv2.imwrite('output_pose_2d.jpg', vis_img)

# visualize 3d poses
vis_kps = np.array(output_pose_3d_list)
vis_3d_multiple_skeleton(vis_kps, np.ones_like(vis_kps), skeleton, 'output_pose_3d (x,y,z: camera-centered. mm.)')

(./main/model.py)

def soft_argmax(heatmaps, joint_num):

    heatmaps = heatmaps.reshape((-1, joint_num, cfg.depth_dim*cfg.output_shape[0]*cfg.output_shape[1]))
    heatmaps = F.softmax(heatmaps, 2)
    heatmaps = heatmaps.reshape((-1, joint_num, cfg.depth_dim, cfg.output_shape[0], cfg.output_shape[1]))

    accu_x = heatmaps.sum(dim=(2,3))
    accu_y = heatmaps.sum(dim=(2,4))
    accu_z = heatmaps.sum(dim=(3,4))

    # accu_x = accu_x * torch.arange(cfg.output_shape[1]).float().cuda()[None,None,:]
    # accu_y = accu_y * torch.arange(cfg.output_shape[0]).float().cuda()[None,None,:]
    # accu_z = accu_z * torch.arange(cfg.depth_dim).float().cuda()[None,None,:]

    accu_x = accu_x * torch.arange(cfg.output_shape[1]).float()[None,None,:]
    accu_y = accu_y * torch.arange(cfg.output_shape[0]).float()[None,None,:]
    accu_z = accu_z * torch.arange(cfg.depth_dim).float()[None,None,:]

    accu_x = accu_x.sum(dim=2, keepdim=True)
    accu_y = accu_y.sum(dim=2, keepdim=True)
    accu_z = accu_z.sum(dim=2, keepdim=True)

    coord_out = torch.cat((accu_x, accu_y, accu_z), dim=2)

    return coord_out

ローカルで動かす場合には、以下のようなfigureが出力され、グルグル回しながら、結果を確認できる。 image


調査していて分かったこと

別途、detectorが必要

当該リポジトリによる、3D pose estimationの出力は、人のdetect結果としてのBounding Box座標が必要となります。 人の映り込みをcropしてから、器官点を予測するアルゴリズムになっています。

(論文figure) image

demoプログラムにおいては、プログラム中にBounding Boxの座標が直書きされています。 即ち、汎用的に作られたdemoプログラムではなく、特定の画像にのみ対応する形となっています。

また、図中のDetectNetについては、提案手法でなく、一般的なdetectorを指すようでした。 論文には、DetectNetにMask R-CNNを採用したと書いてありました。 その為、detectorは、ailiaにあるMask R-CNNを使ってみようと思います。

尚、実験データに関しては、以下のGoogle DriveにBounding Box情報があるとのことでした。 https://drive.google.com/drive/folders/1oBluPpX1YV5YLOU7qytbvdUkn0tp_Yyk


別途、RootNetが必要

また、論文中に登場するRootNetというものもありますが、これも3D pose estimationの出力に必要とのことでした。 論文の著者、かつ、PoseNetリポジトリの管理者であるGyeongsik Moonさんが、PoseNetと別に、RootNetのリポジトリも以下に管理しています。 https://github.com/mks0601/3DMPPE_ROOTNET_RELEASE

こちらも、3D pose estimationのために踏襲する必要があります。 併せて、進めていこうと思います。

尚、PoseNetの計算と、RootNetの計算は独立しています。 PoseNetは、heatmap形式のpose estimationとなっており、256x256x3 の画像を入力に、21x64x64x64のheatmapを出力します。 heatmapは、確率的な解釈をすることで、座標値に変換されます。 尚、このPoseNet単体から出力される座標値は、絶対座標ではなく、相対座標とのこと。 この相対座標に対して、距離を掛けると、絶対座標に変換することができます。 RootNetは、その距離を推定する推定器となっています。

demoプログラムにおいては、Bounding Box座標同様、プログラム中に他人事の距離情報が直書きされています。 つまり、距離情報が無いと、絶対的な3D pose estimationが完成されないということなので、RootNetをキャッチアップする必要があります。 ↓ RootNetのリポジトリを覗いてみたところ、PoseNetの作りと全く同様であり、GPUでの稼働想定であった。 が、PoseNetの際と同様のCPUローカルへの対応方針にて、CPUローカルで動作させることができました。 その出力は、PoseNetのdemoプログラム内に直書きされている値でした。

demo % python demo.py --test_epoch 18
Load checkpoint from ./snapshot_18.pth.tar
focal length: (1500, 1500)
principal points: (320.0, 196.0)
Root joint depth: 11250.573 mm
Root joint depth: 15522.868 mm
Root joint depth: 11831.384 mm
Root joint depth: 8852.558 mm
Root joint depth: 12572.597 mm

これで、一連の動作が実現できそうです。

kyakuno commented 3 years ago

詳細な検証、どうもありがとうございます。引き続き、onnxへのエクスポートと実装をお願いできればと思います。

mucunwuxian commented 3 years ago

ありがとうございます。🙇 メカニズムの理解は、大分進みましたので、次は近日中に、onnxエクスポートと実装を行いたいと思います。

mucunwuxian commented 3 years ago

📝 こちら、備忘となります…。


MaskR-CNNに対して行った修正方針について

当該PoseNetのISSUEにて、MaskR-CNNが登場する経緯

少し上のISSUEにも記載させていただきましたが、当該PoseNetは、単体で実現がされず、前段として、DetectNetと、RootNetが別途必要となります。

image


その為、PoseNet機能を実現させるには、それらもonnx化する必要があります。

RootNetについては、PoseNetと同じ著者/リポジトリ管理者のofficialリポジトリがあるので、そちらを使用すれば問題ありません。

DetectNetについては、同系統のリポジトリは存在せず、論文中にはMaskR-CNNを用いたとの記載があります。 クラスの中に、person が含まれますので、それをDetectNetとして使っているようです。

PoseNetリポジトリのISSUEを覗くと、facebookresearchのmaskrcnn-benchmarkをベースにしているという記載がありました。 そして、ネットワークアーキテクチャを変更しておらず、そのリポジトリにて再現はできるだろう、というようなコメントも記載されていました。

(ISSUE) https://github.com/mks0601/3DMPPE_POSENET_RELEASE/issues/5

(DetectNetのベースになっているリポジトリ) https://github.com/facebookresearch/maskrcnn-benchmark


ailia-modelsのMaskR-CNNについての備忘

恐縮ながら、ailia-modelsのMaskR-CNNについても追わせていただきましたところ、こちらの引用が、onnxリポジトリからの引用であるというコメントを見つけさせていただきました。

(ailia-modelsのMaskR-CNN) https://github.com/axinc-ai/ailia-models/tree/4b5ae9b704c27240663cca8069d75d7388db0f41/object_detection/maskrcnn

(ailia-modelsのMaskR-CNNに関するISSUE) https://github.com/axinc-ai/ailia-models/issues/44

(ailia-modelsのMaskR-CNNの引用元) https://github.com/onnx/models/tree/master/vision/object_detection_segmentation/mask-rcnn

また、onnxリポジトリの引用元は、PoseNetの引用元と同じ、facebookresearchのmaskrcnn-benchmarkリポジトリであるとの記載が、READMEに書かれていました。 ですので、ailia-modelsと、PoseNetとが、すごくキレイに繋がった形になりました。


onnxリポジトリのMaskR-CNNに含まれる想定外の挙動と、その改修案について

そこで、ailia-modelsのMaskR-CNNを動かしてみることとしました。 用意されているプログラムなどが分かりやすく、所定のailiaインストール手順などを踏んで、実施することができました。

尚、以下が、ailia-modelsのMaskR-CNNフォルダ配下にある、demo用のinput画像と、それにMaskR-CNNを適用した結果です。

(input) demo

(output) output

すごくキレイな結果が出力されることが確認できました。 引用元である、onnxリポジトリについても、READMEに沿って実行することで、同じinput/outputが実現されることを確認しました。

しかしながら、以下の画像を入力とした時に、検出結果が少し違和感を感じてしまいました。 人の検出が、半身だけされる形です。

(input) 00000

(output) output_00000

或いは、以下の画像を入力にすると、右端にいる人がdetectionしてもらえないようでした。

(input) 00014

(output) output_00014

尚、この事象は、pixabayから、適当なデモ用動画を見つけて、試した時に発見した次第でした。 先程、検出されなかった人は、画面左手前から右奥に流れていくのですが、右端に行く前は検出がされていて、右端にいる時だけ検出がされない形でした。

(上記、想定外結果を出力する入力動画の引用元) https://pixabay.com/ja/videos/%E7%A7%8B-%E4%BA%BA-%E5%A5%B3%E6%80%A7-%E5%B8%82-%E3%82%B7%E3%83%BC%E3%83%B3-6091/

(上記動画に対して、MaskR-CNN→RootNet→PoseNetを適用した結果) output

こちらの件について、色々と調べてみると、画像のwidthが1,333を超えたことが原因のようでした。 onnxの引用元であるfacebookresearchのmaskrcnn-benchmarkでは、configで画像の最小サイズと最大サイズを記載するようで、それがdefaultは、最小サイズ:800最大サイズ:1,333となっています。 また、onnxリポジトリのMaskR-CNNのREADMEには、以下のような前処理コードが用意されています。

(前処理コード) https://github.com/onnx/models/tree/master/vision/object_detection_segmentation/mask-rcnn#preprocessing-steps

(前処理コード・抜粋)

import numpy as np
from PIL import Image

def preprocess(image):
    # Resize
    ratio = 800.0 / min(image.size[0], image.size[1])
    image = image.resize((int(ratio * image.size[0]), int(ratio * image.size[1])), Image.BILINEAR)

…

画像について、アスペクト比は維持したまま、縦横の短い方を800pixelにするように、拡大するというロジックです。 学習時/テスト時共に、この設定で行われているようです。

(facebookresearch/maskrcnn-benchmarkリポジトリのconfigのdefault) https://github.com/facebookresearch/maskrcnn-benchmark/blob/master/maskrcnn_benchmark/config/defaults.py

(configのdefault・抜粋)

# Size of the smallest side of the image during training
_C.INPUT.MIN_SIZE_TRAIN = (800,)  # (800,)
# Maximum size of the side of the image during training
_C.INPUT.MAX_SIZE_TRAIN = 1333
# Size of the smallest side of the image during testing
_C.INPUT.MIN_SIZE_TEST = 800
# Maximum size of the side of the image during testing
_C.INPUT.MAX_SIZE_TEST = 1333

そして、詳細終えていないのですが、どうもこのサイズをはみ出すと、そのエリアはdetectionしてもらえないようでした。

(はみ出した部分がdetectionされないよ、というISSUE) https://github.com/facebookresearch/maskrcnn-benchmark/issues/915

その為、onnxリポジトリにて示される前処理コードであると、アスペクト比が大きい横長画像のような場合、縦のpixelを800にすべくresizeをした結果、横のpixelが1,333を超えてしまう事態となり、1,333を超えた範囲のdetectionがしてもらえないようでした。 尚、右端が検知してもらえない事象が発生した画像のサイズは、縦360 x 横640でした。 800 ÷ 360 ✕ 640 = 1422.222...という形で、1,333pixelをオーバーしてしまう計算です。

そこで、前処理のコードを、以下のように変更してみました。

(改修案コード・テスト未実施)

    # Resize
    ratio = 800.0 / min(image.size[0], image.size[1])
    resize_w = int(ratio * image.size[0])
    resize_h = int(ratio * image.size[1])
    if (max(resize_w, resize_h) > 1333):
        ratio = 1333.0 / max(image.size[0], image.size[1])
        resize_w = int(ratio * image.size[0])
        resize_h = int(ratio * image.size[1])

画像の縦の長さ(短い方)を、800pixelに調整した時に、もしも、画像の横の長さ(長い方)が、1,333pixelを超える場合には、拡大スケールを、横の長さがギリギリ1,333pixelに収まるまでに、抑えるようにする形です。 即ち、抑えた場合には、画像の縦の長さ(短い方)が、800pixelよりも小さくなります。 尚、元々、画像の縦横の長さが変わらないような真四角に近い画像の場合、上記の制御は働かない形となります。

上記の改修案を適用してみたところ、結果は以下のようになりました。

(改修案コードを適用した結果) output

左端の検知に比べて、まだ少し、右端のdetectionが怪しい形ですが、少し改善しました。

右端のdetectionが怪しいのは、アンカーが上手くハマっていないことが原因ではないかと思いまして、調べましたところ、アンカー設定は以下でした。

(facebookresearch/maskrcnn-benchmarkリポジトリのconfigのdefault) https://github.com/facebookresearch/maskrcnn-benchmark/blob/master/maskrcnn_benchmark/config/defaults.py

(configのdefault・抜粋)

# Base RPN anchor sizes given in absolute pixels w.r.t. the scaled network input
_C.MODEL.RPN.ANCHOR_SIZES = (32, 64, 128, 256, 512)

そこで、長い方のpixelを、1,280pixelにまで抑えるようにコードを変更してみました。 32/64/128/256pixelにて、割り切れる形です。

(改修案コード・テスト未実施)

    # Resize
    ratio = 800.0 / min(image.size[0], image.size[1])
    resize_w = int(ratio * image.size[0])
    resize_h = int(ratio * image.size[1])
    if (max(resize_w, resize_h) > 1280):
        ratio = 1280.0 / max(image.size[0], image.size[1])
        resize_w = int(ratio * image.size[0])
        resize_h = int(ratio * image.size[1])

(改修案コードを適用した結果) output

これにて、右端のdetetionまでできていそうな次第です。 まだ、配慮が足りない部分はありそうなのですが、一旦はこのコードで進めようと思います。


尚、描画する際の、bboxのrescaleにつきましては、以下コードにて実施したところうまく行きました。

(改修前)

    # Resize boxes
    ratio = 800.0 / min(image.size[0], image.size[1])
    boxes /= ratio

↓ (改修案)

    # Resize boxes
    ratio = 800.0 / min(image.size[0], image.size[1])
    resize_w = int(ratio * image.size[0])
    resize_h = int(ratio * image.size[1])
    if (max(resize_w, resize_h) > 1280):
        ratio = 1280.0 / max(image.size[0], image.size[1])
    boxes /= ratio


その他考察

上記コード方針は、そこまでアスペクト比が大きくない場合には、有効かと思われますが、例えば、かなり横長な画像などが入力された場合には、画像が小さくなってしまって、検出率が落ちる恐れがあります。

その場合は、画像を横2つに分断して、別々に予測を行った後、結合をした方が良いかもしれませんが、その場合の別途配慮はまた難しそうです。


また、参考までになのですが、onnxリポジトリのMaskR-CNNにて、用意してくれているpreprocessコードについて、以下のようなコードがあります。

preprocessコードへのリンク) https://github.com/onnx/models/tree/master/vision/object_detection_segmentation/mask-rcnn#preprocessing-steps

(該当コード抜粋)

    # Pad to be divisible of 32
    padded_h = int(math.ceil(image.shape[1] / 32) * 32)
    padded_w = int(math.ceil(image.shape[2] / 32) * 32)

    padded_image = np.zeros((3, padded_h, padded_w), dtype=np.float32)
    padded_image[:, :image.shape[1], :image.shape[2]] = image
    return padded_image

こちらのコードも、anchorに対する配慮のコードになるかと思います。 anchor girdに含まれない画像の端部分が生じないように、zero paddingをしている形となります。 本当に恐縮ながら、こちらについても、例えば、32pixelだけ補填するのではなく、256pixelなど補填した方が間違いが無いのではと思ったりしてしまったのですが。 提供のonnxモデルの学習データが、COCOであり、そのデータを眺めてみると、オクルージョンデータが多数ある、かつ、オクルージョンで隠れた部分を含めたBBOXになってなさそうであることから、anchorのためのpaddingを、大袈裟に用意する必要は無さそうだなという考えに至りました。

(onnxモデルの学習データ情報) https://github.com/onnx/models/tree/master/vision/object_detection_segmentation/mask-rcnn#dataset-train-and-validation

(COCOデータセットのoverview) https://cocodataset.org/#explore

また、念の為、32から、256に変更して、出力結果が変わるかどうかを、ailia-modelsのMaskR-CNNテストしてみましたが、出力結果に変化がなかった為、改めて、改修不要と考えました。

(元ロジックの32pixel paddingでの出力結果) output_32

(試しに256pixel paddingにしてみた際の出力結果) output_256

MaskR-CNNの処理前提として、画像サイズの最小サイズを800pixelにしている辺りが、こういった微妙なチューニング配慮をある程度排除するためのものかなどとも考えました。

最後に念の為、その観点にて、MaskR-CNNの論文や、そこから参照されているFPNの論文を参照してみましたが、学習条件の紹介としまして、Images are resized such that their scale (shorter edge) is 800 pixels.という記載がサラリとあるだけで、特に深い言及はなかった次第です。

mucunwuxian commented 3 years ago

@kyakuno 客野さん お疲れ様です。 ちょっと質問させて下さい。


当該3d mpe posenetについてですが、DetectNetとRootNetは問題ないのですが、最後のPoseNetにて、少し問題があります。 問題と言いますのは、onnxでは動くものが、medium記事にありますような、或いは、ailia-modelsのmaskrccにて実装されているような、ailia SDKを用いた推論ですと、エラーが起きてしまうことです。

エラーの内容は以下になります。

Traceback (most recent call last):
  File "demo_onnx.py", line 598, in <module>
    main()
  File "demo_onnx.py", line 594, in main
    recognize_from_image()
  File "demo_onnx.py", line 540, in recognize_from_image
    posenet_to_image(original_img=original_img, bbox_list=bbox_list)
  File "demo_onnx.py", line 482, in posenet_to_image
    pose_3d = net_pose.predict([to_numpy(img)])[0]
  File "/opt/anaconda3/envs/dev38/lib/python3.8/site-packages/ailia/wrapper.py", line 289, in predict
    self.set_input_blob_data( input[i], idx )
  File "/opt/anaconda3/envs/dev38/lib/python3.8/site-packages/ailia/wrapper.py", line 511, in set_input_blob_data
    core.check_error(code, self.__net)
  File "/opt/anaconda3/envs/dev38/lib/python3.8/site-packages/ailia/core.py", line 654, in check_error
    raise e(detail)
ailia.core.AiliaInvalidLayerException: code : -10
+ error detail : 528(ReduceSum_DNN)input blob should be not virtual.



私なりにエラーを追わせてもらったのですが、このエラーは、prototxtに記載されるop_type: "ReduceSum"が、-10 : AiliaInvalidLayerExceptionであるために起きているものかと思いました。

実は、3d mpe posenetは、heatmap座標表現から座標値への変換までを、modelのforward内で行っています。

図に示すと以下となります。 イラスト616

該当コードは以下になります。(コード全体) 図に示させてもらったPose Processというのが、コード中のsoft_argmax(hm, self.joint_num)です。

class ResPoseNet(nn.Module):
    def __init__(self, backbone, head, joint_num):
        super(ResPoseNet, self).__init__()
        self.backbone = backbone
        self.head = head
        self.joint_num = joint_num

    def forward(self, input_img, target=None):
        fm = self.backbone(input_img)
        hm = self.head(fm)
        coord = soft_argmax(hm, self.joint_num)

        if target is None:
            return coord
        else:
            target_coord = target['coord']
            target_vis = target['vis']
            target_have_depth = target['have_depth']

            ## coordinate loss
            loss_coord = torch.abs(coord - target_coord) * target_vis
            loss_coord = (loss_coord[:,:,0] + loss_coord[:,:,1] + loss_coord[:,:,2] * target_have_depth)/3.

            return loss_coord



そして、このsoft_argmax(hm, self.joint_num)の中で、torchのtensorのsum関数が多用されています。 heatmap表現を、座標値に変換する際に、使用している形です。

そのアルゴリズムを図に示させていただきますと、以下となります。 イラスト619

該当コードは以下になります。

def soft_argmax(heatmaps, joint_num):

    heatmaps = heatmaps.reshape((-1, joint_num, cfg.depth_dim*cfg.output_shape[0]*cfg.output_shape[1]))
    heatmaps = F.softmax(heatmaps, 2)
    heatmaps = heatmaps.reshape((-1, joint_num, cfg.depth_dim, cfg.output_shape[0], cfg.output_shape[1]))

    accu_x = heatmaps.sum(dim=(2,3))
    accu_y = heatmaps.sum(dim=(2,4))
    accu_z = heatmaps.sum(dim=(3,4))

    accu_x = accu_x * torch.arange(cfg.output_shape[1]).float().cuda()[None,None,:]
    accu_y = accu_y * torch.arange(cfg.output_shape[0]).float().cuda()[None,None,:]
    accu_z = accu_z * torch.arange(cfg.depth_dim).float().cuda()[None,None,:]

    accu_x = accu_x.sum(dim=2, keepdim=True)
    accu_y = accu_y.sum(dim=2, keepdim=True)
    accu_z = accu_z.sum(dim=2, keepdim=True)

    coord_out = torch.cat((accu_x, accu_y, accu_z), dim=2)

    return coord_out



エラーは、このsum関数が、ailia SDKにとって、-10 : AiliaInvalidLayerExceptionであることが起因かと思いました。


前段が長くなってしまったのですが、ここで質問になります。 上記、私の認識は合っておりますでしょうか? つまりは、networkのforwardの中に、sum関数があると、ailia SDKの推論でエラーが起きてしまうという認識になります。 いかがでしょうか? もし、私の認識が誤っておりましたら、ご指摘頂けますと有り難いです。

また、重ねて質問なのですが、もし上記の認識が合っているようであれば、座標値を計算するPost Processを、modelのforwardから外出しした上で、onnx・prototxtのエクスポートをしようかと思いますが、いかがでしょうか?


以上、何卒よろしくお願い致します。

kyakuno commented 3 years ago

onnxruntimeで推論できるモデルは全てailia SDKで推論できるべきですので、ailia SDK側を修正して対応します。そのため、サンプルで--onnxを指定するとonnxruntime、指定しないとailiaで推論できるようにしておいていただければ、SDKの方で対応いたします。

mucunwuxian commented 3 years ago

@kyakuno 早速の返答ありがとうございます。

了解しました。 --onnx指定にて、ailia SDKの使用を避けるように、コードを組ませていただきます。 明快な回答、ありがとうございます。

尚、あくまで念の為にですが、以下のようにtorch.sumに置き換えて試してみましたが、先程記載させてもらったエラーと、同じものが出力されることを確認致しました。

torch.sumに置き換えたコード

def soft_argmax(heatmaps, joint_num):

    heatmaps = heatmaps.reshape((-1, joint_num, cfg.depth_dim*cfg.output_shape[0]*cfg.output_shape[1]))
    heatmaps = F.softmax(heatmaps, 2)
    heatmaps = heatmaps.reshape((-1, joint_num, cfg.depth_dim, cfg.output_shape[0], cfg.output_shape[1]))

    accu_x = torch.sum(heatmaps, dim=(2,3))
    accu_y = torch.sum(heatmaps, dim=(2,4))
    accu_z = torch.sum(heatmaps, dim=(3,4))

    accu_x = accu_x * torch.arange(cfg.output_shape[1]).float()[None,None,:]
    accu_y = accu_y * torch.arange(cfg.output_shape[0]).float()[None,None,:]
    accu_z = accu_z * torch.arange(cfg.depth_dim).float()[None,None,:]

    accu_x = torch.sum(accu_x, dim=2, keepdim=True)
    accu_y = torch.sum(accu_y, dim=2, keepdim=True)
    accu_z = torch.sum(accu_z, dim=2, keepdim=True)

    coord_out = torch.cat((accu_x, accu_y, accu_z), dim=2)

    return coord_out

出たエラー

Traceback (most recent call last):
  File "demo_onnx.py", line 598, in <module>
    main()
  File "demo_onnx.py", line 594, in main
    recognize_from_image()
  File "demo_onnx.py", line 540, in recognize_from_image
    posenet_to_image(original_img=original_img, bbox_list=bbox_list)
  File "demo_onnx.py", line 482, in posenet_to_image
    pose_3d = net_pose.predict([to_numpy(img)])[0]
  File "/opt/anaconda3/envs/dev38/lib/python3.8/site-packages/ailia/wrapper.py", line 289, in predict
    self.set_input_blob_data( input[i], idx )
  File "/opt/anaconda3/envs/dev38/lib/python3.8/site-packages/ailia/wrapper.py", line 511, in set_input_blob_data
    core.check_error(code, self.__net)
  File "/opt/anaconda3/envs/dev38/lib/python3.8/site-packages/ailia/core.py", line 654, in check_error
    raise e(detail)
ailia.core.AiliaInvalidLayerException: code : -10
+ error detail : 528(ReduceSum_DNN)input blob should be not virtual.


原理が同じであると推察されます。


以上、引き続き、何卒よろしくお願い致します。

mucunwuxian commented 3 years ago

PRを出させて頂きました。 https://github.com/axinc-ai/ailia-models/pull/336