OpenPoseで骨格推定 Tensorflow 2.x版(M1 Mac/Intel Mac/Windows11対応)

スポンサーリンク
OpenPose

旧記事(Tensorflow 1.x)ではM1 Macでうまく動かない(zsh: illegal hardware instructionというエラーが出る)問題がありましたが、本記事の方法はM1 Mac/Intel Mac/Windows11でOpenPoseによる骨格推定を行えます。

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

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

  1. 画像ファイル
  2. 内蔵カメラ(Windows11 WSL2環境はUSBカメラ)
  3. 動画ファイル

なお、元記事の方法はTensorflow 1.xを使用していましたが、Tensorflow 1.xはメンテナンスがされておらず、うまく環境構築できなかったため、Tensorflow 2.xを使用したOpenPoseのGitHubリポジトリを利用しました。

以下の2つの動画にWindows11とM1 Macで本記事の内容を実践した様子を収めていますので、よろしければ参考にしてください。

【Windows11】

【M1 Mac使用】

上記動画では、wget, Xquartzはインストール済みの状態からスタートしています。

未インストールの場合は、本記事内「Macの準備」の章の手順が必要になりますのでご注意ください。

動作環境

以下の環境で動作確認しました。

PCM1 MacBook Pro 2020Intel MacBook Pro 2015MSI GF65 15.6-inch
OSmacOS Big SurmacOS MontereyWindows 11/WSL2 Ubuntu-20.04 LTS
AnacondaMiniconda 4.11.0Anaconda 4.11.0Anaconda 4.10.3
Python3.83.73.8
OpenCV4.5.54.5.14.5.1
Tensorflow2.6.02.0.02.4.1

事前準備

Macの場合

wget(Webからファイルをダウンロード)やXquartz(X11ウィンドウを表示する)をインストールしていない場合は以下のコマンドでインストールします。

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

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

Windows11の場合

WSL2のUtuntu-20.04 LTSをインストールして使用します。

Windowsマークを右クリック→Windowsターミナル(管理者)を立ち上げ、以下のコマンドでWSL2 Ubuntu-20.04 LTSをインストールします。

wsl --install -d Ubuntu-20.04

下図のようにプルダウンメニューからUbuntu-20.04を起動します。

Start wsl ubuntu

Anaconda/Minicondaインストール

M1 MacはMinicondaで実施する必要があります(Anacondaでは動きません)。すでにAnacondaをインストールしている場合はこちらのサイトを参考にAnacondaをアンインストールしてからMinicondaをインストールすることをおすすめします。

Intel MacはAnaconda、Minicondaどちらでも構いません。

Windows11は、WSL2 Ubuntu-20.04 LTS上でLinux用のAnacondaをインストールして動作確認しました。

Anacondaを使用する場合はこちらのサイトの一番下にあるリンクから入手してインストールしてください。

MinicondaのGitHubから、お使いの環境に合ったリンクをクリックします。

Intel MacMiniforge3-MacOSX-x86_64
M1 MacMiniforge3-MacOSX-arm64
Windows/WSL2 Ubuntu-20.04 LTSMiniforge3-Linux-x86_64

Minicondaのインストールスクリプトをダウンロードしたら、以下のようにbashで実行します。(下記ファイル名はM1 Macの場合であり、OSによって異なります)

bash Miniforge3-MacOSX-arm64.sh

~/.zshrcに自動的にMiniconda用の設定が追記されるので、sourceして有効にします。

source ~/.zshrc

Windows11 WSL2環境の場合、上記設定ファイル名は.zshrcではなく.bashrcになります。

condaをupdateしてバージョンを表示します。

conda update conda
conda -V

以下のようにバージョンが表示されるので、GitHubサイトのReleaseのバージョンと同じことを確認します。

conda 4.11.0

異なる場合、既にインストールしてあったAnacondaのcondaが呼ばれていることが考えられますので、以下のようにMinicondaのインストールディレクトリのbinが先になるように~/.zshrcにPATH設定を追記します。

export PATH="/Users/user_name/miniforge3/bin:$PATH"

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

Python 3.8、Numpy 1.19を指定したposeという仮想環境を作成し、アクティベートします。

conda create -n pose python=3.8 numpy=1.19
conda activate pose

Intel MacではAnaconda NavigatorのGUIからPython 3.8でCreateし、numpyはインストールせずに進めることでうまくいくことを確認済みです。
(以降のnumbaのインストールの際にnumpyもインストールされます。)

OpenPose環境構築

以降は、pose環境をアクティベートした状態でのコマンド入力になります。

Tensorflow 2.x用のOpenPose環境をダウンロードします。

git clone https://github.com/gsethi2409/tf-pose-estimation.git
cd tf-pose-estimation

以下のコマンドを順に実行してOpenPose環境を構築します。

conda install numba scipy
pip install -r requirements.txt
conda install -c conda-forge tensorflow
conda install swig
cd tf_pose/pafprocess
swig -python -c++ pafprocess.i && python3 setup.py build_ext --inplace
cd ../..
conda install -c conda-forge opencv
pip install git+https://github.com/adrianc-a/tf-slim.git@remove_contrib
cd models/graph/cmu
bash download.sh
cd ../../..

OpenPose実行

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

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

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

OpenPose result

左上の画像が骨格を元画像に重ね合わせた結果で、右上は関節の確度、下段は関節同士をつなぐための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

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

WebCam

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

終了するにはターミナルで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の部分はお使いのアカウント名で置き換えてください。

なお、Windows11 WSL2環境の場合、ダウンロードディレクトリ名は/mnt/c/Users/user_name/Downloadsのようになりますのでご注意ください。

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

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

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

Movie

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

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

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

応用編

読者の方から、認識結果をファイルに落とす方法が知りたいとのご要望があり、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のようにコアなデータにアクセスするコードを書くのが良いと思います。

書籍紹介

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

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

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

入門 Python 3

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

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

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

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

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

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

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

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

まとめ

M1 Mac/Intel Mac/Windows11でAnaconda/MinicondaをベースにOpenPoseで骨格推定を行える環境の構築を行い、以下3種の入力に対しOpenPoseを実行することができました。

  1. 画像ファイル
  2. 内蔵カメラ(Windows11 WSL2環境はUSBカメラ)
  3. 動画ファイル

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

人物に骨格を重ね合わせて表示されるのはとても楽しいので、是非お試しください。

参考文献

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