OpenPoseで骨格推定【Mac】 Anaconda/OpenCV/TensorFlow

スポンサーリンク
OpenPose

本記事はTensorflow 1.xを使用した古い内容になります。M1 Mac/Intel Mac/Windows11に対応したTensorflow 2.x使用の新しい手順はこちらをご覧ください。

OpenPoseはカーネギーメロン大学で開発された人物のポーズ推定ライブラリで、無料で利用することができます。(商用利用にはライセンス料が発生)

Pythonの環境であるAnacondaをベースに以下3つの入力に対しOpenPoseによる骨格推定をする方法を紹介します。

  1. 画像ファイル
  2. 内蔵カメラ
  3. 動画ファイル

本記事はMacで動作確認しています。

(2021/4/17追記)
以前使用していたtf-pose-estimationのGitリポジトリが削除されたため、正しく動作する別のリポジトリに変更しました。

以下の動画に環境構築から動作確認までを収めていますので参考にしてください。

動作環境

項目内容
PCMacBook Pro
OSmacOS Catalina 10.15.7
AnacondaAnaconda3-2020.11-MacOSX-x86_64
Python3.6.12
OpenCV3.4.0
TensorFlow1.14.0

環境構築

Macの準備

HomeBrewをインストールし、以降の処理で必要になるwget(Webからファイルをダウンロード)、swig(C/C++で書かれたプログラムを別の言語から呼び出す)、Xquartz(X11ウィンドウを表示する)をインストールします。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install wget swig
brew install --cask xquartz

ここでMacを再起動します。

Anacondaのインストール

以下の記事に従いAnacondaをインストールします。

今回のOpenPose作業用に仮想環境poseを作ります。(名称は任意)

Pythonバージョンは3.6を選択します。

3.6が出ない場合は「Update index…」ボタンを押すと選べるようになります。

以降はここで作成した仮想環境poseのターミナルでの作業になります。

作業ディレクトリは任意の場所で構いません。

OpenPoseの準備

OpenPoseのライブラリをダウンロードします。

git clone https://github.com/jiajunhua/ildoonet-tf-pose-estimation

(2021/4/17修正)
本記事を執筆当初は以下のGitリポジトリを使用していましたが、削除されたため、上記に修正しました。
https://github.com/ildoonet/tf-pose-estimation

ダウンロードしたildoonet-tf-pose-estimationディレクトリに移動します。

cd ildoonet-tf-pose-estimation

viエディタ等でrequirements.txtの2行目dillの右に ==0.2.7.1 を書き足します。(以下は編集後の内容)

argparse
dill==0.2.7.1
fire
matplotlib
numba
psutil
pycocotools
requests
scikit-image
scipy
slidingwindow
tqdm
git+https://github.com/ppwwyyxx/tensorpack.git

(2021/4/17追記)
上記編集を行わないとdill==0.3.3がインストールされ、依存関係が崩れてしまうため、この編集作業を追加しました。

viの使い方がわからない方は↓の動画でテキスト編集の方法を紹介していますので参考にしてください。

各種モジュールをインストールします。

pip install -r requirements.txt

TensorFlowをインストールします。

pip install tensorflow==1.14

他のサイトではバージョンを指定しないインストール方法が紹介されている場合がありましたが、2021年1月現在、バージョン指定のない場合に2.xがインストールされ、OpenPoseを正常に実行できませんでした。

OpenCVをインストールします。

pip install opencv-python==3.4.0.14

必要なファイルのダウンロードを行います。

cd models/graph/cmu
bash download.sh

pafprocessのビルドを行います。

cd ../../../tf_pose/pafprocess
swig -python -c++ pafprocess.i && python3 setup.py build_ext --inplace

これで環境構築は完了です。

OpenPose実行

tf-pose-estimationディレクトリに戻ります。cd ../..

以下のコマンドでサンプルとして付属されている画像を使ってOpenPoseを実行してみます。

python run.py --model=mobilenet_thin --resize=432x368 --image=./images/p2.jpg

以下のように表示されれば成功です。

左上の画像が骨格を元画像に重ね合わせた結果で、右上は関節の確度、下段は関節同士をつなぐためのX,Y方向のベクトルマップです。

--modelで指定できるモデルは、cmu / mobilenet_thin / mobilenet_v2_large / mobilenet_v2_small の4種類で、cmuはより正確に骨格を推定できるものの実行速度が遅いです。

他の3つはさほど変わらないように思えました。

--resizeオプションでサイズを432×368にしているのは、学習時の入力画像がこのサイズのため、この設定にすることで認識しやすくなるためのようです。

終了するにはqを押すか、表示されたウィンドウのX印を押します。

次に内蔵カメラを使ってリアルタイムのOpenPose実行を試します。

python run_webcam.py --model=mobilenet_thin --resize=432x368 --camera=0

以下のようにパソコン内臓のカメラ画像にリアルタイムに骨格が重ね合わせて表示されれば成功です。

骨格だけでなく、顔の中の目、耳、鼻の位置も捉えていることがわかります。

終了するにはターミナルでCtrl+cを押します。

最後に動画を入力とするrun_video.pyを試したのですが、うまく骨格が表示できなかったので、こちらのサイトの情報から、一部修正して以下をrun_video2.pyとして使用しました。

import argparse
import logging
import time

import cv2
import numpy as np

from tf_pose.estimator import TfPoseEstimator
from tf_pose.networks import get_graph_path, model_wh

logger = logging.getLogger('TfPoseEstimator-Video')
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

fps_time = 0

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='tf-pose-estimation Video')
    parser.add_argument('--video', type=str, default='')
    parser.add_argument('--write_video', type=str, default='')
    parser.add_argument('--resize', type=str, default='0x0',
                        help='if provided, resize images before they are processed. default=0x0, Recommends : 432x368 or 656x368 or 1312x736 ')
    parser.add_argument('--resize-out-ratio', type=float, default=4.0,
                        help='if provided, resize heatmaps before they are post-processed. default=1.0')
    parser.add_argument('--model', type=str, default='mobilenet_thin', help='cmu / mobilenet_thin')
    parser.add_argument('--show-process', type=bool, default=False,
                        help='for debug purpose, if enabled, speed for inference is dropped.')
    parser.add_argument('--showBG', type=bool, default=True, help='False to show skeleton only.')
    args = parser.parse_args()

    logger.debug('initialization %s : %s' % (args.model, get_graph_path(args.model)))
    
    w, h = model_wh(args.resize)
    if w > 0 and h > 0:
        e = TfPoseEstimator(get_graph_path(args.model), target_size=(w, h))
    else:
        e = TfPoseEstimator(get_graph_path(args.model), target_size=(432, 368))
            
    cap = cv2.VideoCapture(args.video)
    if args.write_video:
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = cap.get(cv2.CAP_PROP_FPS)
        fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
        writer = cv2.VideoWriter(args.write_video, fmt, fps, (width, height))

    if cap.isOpened() is False:
        print("Error opening video stream or file")
    while cap.isOpened():
        ret_val, image = cap.read()
        
        logger.debug('image process+')
        humans = e.inference(image, resize_to_default=(w > 0 and h > 0), upsample_size=args.resize_out_ratio)
        if not args.showBG:
            image = np.zeros(image.shape)
        
        logger.debug('postprocess+')
        image = TfPoseEstimator.draw_humans(image, humans, imgcopy=False)

        logger.debug('show+')
        cv2.putText(image, "FPS: %f" % (1.0 / (time.time() - fps_time)), (10, 10),  cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
        cv2.imshow('tf-pose-estimation result', image)
        if args.write_video:
            writer.write(image)

        fps_time = time.time()
        if cv2.waitKey(1) == 27:
            break

    cv2.destroyAllWindows()
logger.debug('finished+')

入力する動画はPixaboyから最小サイズの640×360をダウンロードしました。

以下のコマンドでダウンロードした動画を入力としてOpenPoseを実行します。

python run_video2.py --model=mobilenet_thin --resize=432x368 --video=/Users/user_name/Downloads/People\ -\ 45353.mp4

user_nameの部分はお使いのユーザ名に合わせて置き換えてください。

結果を動画ファイルに保存するには以下のように–write_video=<ファイル名>オプションを付けます。

python run_video2.py --model=mobilenet_thin --resize=432x368 --video=/Users/user_name/Downloads/People\ -\ 45353.mp4 --write_video=out.mp4

以下のように表示されれば成功です。

上図の場面では、水色の服の人は左腕が青い色の線になっていて、他の人とは左右反対です。

スマホを見ながら、うつむき加減でマスクも着用しているため、顔の認識ができず、後ろ向きと判断されてしまったようです。

このように誤認識してしまうケースもありますが、たいていは問題なく認識できています。

応用編

読者の方から、認識結果をファイルに落とす方法が知りたいとのご要望があり、run_video2.pyを少し発展させて以下のrun_video2_fileout.pyをサンプルとして作成しました。

import argparse
import logging
import time

import cv2
import numpy as np

from tf_pose.estimator import TfPoseEstimator
from tf_pose.networks import get_graph_path, model_wh

logger = logging.getLogger('TfPoseEstimator-Video')
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

fps_time = 0


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='tf-pose-estimation Video')
    parser.add_argument('--video', type=str, default='')
    parser.add_argument('--write_video', type=str, default='')
    parser.add_argument('--resize', type=str, default='0x0',
                        help='if provided, resize images before they are processed. default=0x0, Recommends : 432x368 or 656x368 or 1312x736 ')
    parser.add_argument('--resize-out-ratio', type=float, default=4.0,
                        help='if provided, resize heatmaps before they are post-processed. default=1.0')
    parser.add_argument('--model', type=str, default='mobilenet_thin', help='cmu / mobilenet_thin')
    parser.add_argument('--show-process', type=bool, default=False,
                        help='for debug purpose, if enabled, speed for inference is dropped.')
    parser.add_argument('--showBG', type=bool, default=True, help='False to show skeleton only.')
    args = parser.parse_args()

    logger.debug('initialization %s : %s' % (args.model, get_graph_path(args.model)))

    w, h = model_wh(args.resize)
    if w > 0 and h > 0:
        e = TfPoseEstimator(get_graph_path(args.model), target_size=(w, h))
    else:
        e = TfPoseEstimator(get_graph_path(args.model), target_size=(432, 368))

    cap = cv2.VideoCapture(args.video)
    if args.write_video:
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = cap.get(cv2.CAP_PROP_FPS)
        fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
        writer = cv2.VideoWriter(args.write_video, fmt, fps, (width, height))

    with open('humans.txt', mode='w') as f:

        if cap.isOpened() is False:
            print("Error opening video stream or file")
        while cap.isOpened():
            ret_val, image = cap.read()
            if ret_val:
                logger.debug('image process+')
                humans = (e.inference(image, resize_to_default=(w > 0 and h > 0), upsample_size=args.resize_out_ratio))
                #print(humans)
                #print(type(humans))
                #print(len(humans))
                #exit(0)
                tmp_h = []
                for human in humans:
                    tmp_h.append(str(human))
                str_h = ";".join(tmp_h)
                print(str_h)
                f.write(str_h)
                f.write("\n")
                #exit(0)
                if not args.showBG:
                    image = np.zeros(image.shape)
        
                logger.debug('postprocess+')
                image = TfPoseEstimator.draw_humans(image, humans, imgcopy=False)
        
                logger.debug('show+')
                cv2.putText(image, "FPS: %f" % (1.0 / (time.time() - fps_time)), (10, 10),  cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
                cv2.imshow('tf-pose-estimation result', image)
                        if args.write_video:
                            writer.write(image)

                fps_time = time.time()
                if cv2.waitKey(1) == 27:
                    break
            else:
                break

    cv2.destroyAllWindows()

logger.debug('finished+')

認識結果はhumansにHumanクラスのインスタンスのリストとして格納されていました。

humansの中身を確認した際のコードもコメントとして残してあります。

Humanクラスはtf_pose/estimator.pyに定義されています。

実行方法は同様で以下のように使用します。

python run_video2_fileout.py --model=mobilenet_thin --resize=432x368 --video=/Users/user_name/Downloads/People\ -\ 45353.mp4

user_nameの部分はお使いのユーザ名に合わせて置き換えてください。

実行するとhumans.txtというファイルができて、以下のようにBodyPart:0〜17の座標(画角に対し0〜1に正規化した値)とscore(確信の度合いと思われる)が空白区切りで出力されます。

BodyPart:0-(0.50, 0.30) score=0.88 BodyPart:1-(0.50, 0.33) score=0.84 ...

人が認識される個数は入力する絵によって異なります。複数認識の場合、それぞれをセミコロン(;)で区切るようにしてあります。

BodyPartは毎度0〜17のすべてが出力される訳ではないようです。

このようにテキストに落としてしまえば、外部でいろいろ処理ができそうです。

もっと情報をしぼって出力したい場合は、tf_pose/estimator.pyの中身を見てTfPoseEstimatorクラスのdraw_humansメソッドを参考に、human.body_parts[i].xのようにコアなデータにアクセスするコードを書くのが良いと思います。

まとめ

AnacondaをベースにOpenPoseを実行できる環境の構築を行い、以下3種の入力に対しOpenPoseを実行することができました。

  1. 画像ファイル
  2. 内蔵カメラ
  3. 動画ファイル

このようにOpenPoseを使うと単眼カメラの画像から骨格を2Dの座標情報として推定し、画像に重ね合わせてかんたんに表示することができます。

取得した座標をもとに何らかのデータとして処理するなどの応用が考えられますので、今後そういった内用についても記事を書いてみたいと思います。

書籍紹介

Pythonや画像処理、機械学習関連のおすすめ書籍を紹介します。

人気ブロガーからあげ先生のとにかく楽しいAI自作教室

実践して楽しむことを重視していて、充実のサンプルコードで画像認識やテキスト分析、GAN生成といったAI応用をGoogle Colab環境下で体験できます。

入門 Python 3

基礎から応用までわかりやすい説明でコード例も多数載っていて、初学者の入門書としては必要十分と感じています。

さらにPythonらしいコードの書き方も言及されていて、スマートなコードが書けるようになります。

詳解 OpenCV 3 ―コンピュータビジョンライブラリを使った画像処理・認識

最新のOpenCV4には対応していないのですが、やはりオライリー、基礎を学ぶには十分な情報があり、この本から入ることをおすすめします。

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

この本は、機械学習の仕組みを学ぶのにとても良い本だと思います。

Tensorflow/Kerasなどの既存プラットフォームを使うのではなく、基本的な仕組みを丁寧に説明し、かんたんなPythonコードで実装していく手順を紹介してくれます。

機械学習のコードがどのような処理をしているのか、想像できるようになります。

参考文献

タイトルとURLをコピーしました