2019年7月9日火曜日

姿勢推定(Pose Estimation)をする

最近のOpenCVってディープラーニングに対応しているのですね。
CaffeやTensorFlow、PyTorchで学習させた結果を読み込んで簡単にOpenCVで利用することができます。こりゃー便利、いろんなアプリ作れるじゃん。

ちょうどディープラーニング業界では、OpenPoseというものがはやってるし。
人間の頭手足などの位置や動きを数値化してくれるらしいです。

おじさん、これも作れる!
ということで、本日はOpenCVを使ってディープラーニングをして、Pose Estimationをしてみたいと思います。


1.最小のOpenCVのビルド

まず、最小のOpenCVを作ります。
OpenCV4.1のソースコードを取ってmodules/coreにあるソースだけをg++ *.cppなどでビルドします。
何か所かビルドエラーになりますが、簡単にビルドを通すことができます。
OpenCVってmodules/coreだけをビルドするとMatが使るんです。
とっても便利。

xxxxx.simd_declarations.hppが見つかりませんとか言われますが、cv_cpu_include_simd_declarations.hppに書いてある通り、以下のファイルを変更して使用します。

xxxxx.simd_declarations.hpp
-----------------
#define CV_CPU_SIMD_FILENAME "<filename>.simd.hpp"
#define CV_CPU_DISPATCH_MODE AVX2
#include "opencv2/core/private/cv_cpu_include_simd_declarations.hpp"
#define CV_CPU_DISPATCH_MODE SSE2
#include "opencv2/core/private/cv_cpu_include_simd_declarations.hpp"
-----------------

続いて、modules/dnnとmodules/imgprocもこれと同じようにソースコードを取ってきてg++ *.cppでビルドします。

これでOpenCVでディープラーニングと線や丸でお絵書きができるようになります。
僕のようにソースコードをスクラッチビルドをしなくても、OpenCVのビルド済みライブラリを取っててきもよいです。


2.ProtocolBuffersのビルド

OpenCVでDNNを使う場合は、caffe形式やTelsorFlow形式の学習済みデータを読み込むのにProtocolBuffersを使用しています。
OpenCV4.1はProtocolBuffers3.05で.protoファイルをコンパイルしていますが、今回はソースコードからビルドするので、最新のProtocolBuffers3.8を取ってきます。
これも g++ *.ccでビルドできますね。
最近のソースコードはどれもmakefileがなくてもビルドできるのが素晴らしい。
ビルドをするとProtocolBuffersのコンパイラも生成されるので、OpenCVのmoduke/dnnに入っている、*.protoファイルをコンパイルして、C++用のインタフェースファイルを生成します。

全部のソースコードをコンパイルすると、好きなバージョンのOpenCVと好きなバージョンのProtocolBuffersを使用してDNNをする最小のOpenCVを作ることができます。
BLASとかeigenとかnumpyとかのベクトル演算ライブラリや他のライブラリは一切必要ありません。これだけでディープラーニングができます。
とっても手軽?というか軽量ですね。

作った最小のOpenCVのソースはここにおいてあります。
https://drive.google.com/file/d/1P4VC3_gZXwANeL0yup-5Ni56IFyvs_VM/view?usp=sharing

このディープラーニングの結果を利用して計算する部分を NN Runtime(Neural Network Runtime)と言うらしいです。
OpenCVはC++で実装されている一番手軽なNN Runtimeです。
ONNXというフォーマットにも対応しているので他のディープラーニングのライブラリで学習した結果も簡単にOpenCVで利用できます。



3.姿勢推定(Pose Estimation)をする

いよいよPose Estimationをします。

https://www.learnopencv.com/deep-learning-based-human-pose-estimation-using-opencv-cpp-python/

ここに書いてあるものをちょっと改良して作ります。
公開されている学習済みデータもダウンロードして取ってきますが、サイズが200Mもあるじゃないか。


--------------------
#include<stdio.h>
#include<string>
#include<vector>

#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION


#include "opencv2_core.hpp"
#include "opencv2_imgproc.hpp"
#include "opencv2_imgproc_imgproc_c.h"
#include "opencv2_dnn.hpp"

#include "stb_image.h"
#include "stb_image_write.h"

void changeb_g(unsigned char* p, int x, int y, int c)
{
int ct = x*y;
int i;
unsigned char t;
for (i = 0; i < ct; i++) {
t = p[0];
p[0] = p[2];
p[2] = t;
p[3] = 255;
p += c;
}
}


#if 1

int main()
{
unsigned char* p;
int x=-1, y=-1, n=-1;

//
const int POSE_PAIRS[14][2] =
{
{ 0,1 },{ 1,2 },{ 2,3 },
{ 3,4 },{ 1,5 },{ 5,6 },
{ 6,7 },{ 1,14 },{ 14,8 },{ 8,9 },
{ 9,10 },{ 14,11 },{ 11,12 },{ 12,13 }
};
int nPoints = 15;


// Specify the paths for the 2 files
std::string protoFile = "pose_deploy_linevec_faster_4_stages.prototxt";
std::string weightsFile = "pose_iter_160000.caffemodel";


// Read the network into Memory
cv::dnn::Net net = cv::dnn::readNetFromCaffe(protoFile, weightsFile);

//
p = stbi_load("single.jpeg", &x, &y, &n, 4);
if (p == NULL || x < 1 || y < 1)return 1;
changeb_g(p, x, y, 4);
cv::Mat color = cv::Mat(y, x, CV_8UC4);
memcpy(color.data, p, x * 4 * y);
stbi_image_free(p);

//
cv::Mat frame;
cv::cvtColor(color, frame, CV_BGRA2BGR);


//
cv::Mat frameCopy = frame.clone();
int frameWidth = frame.cols;
int frameHeight = frame.rows;


// Specify the input image dimensions
int inWidth = 368;
int inHeight = 368;
float thresh = 0.1;

// Prepare the frame to be fed to the network
cv::Mat inpBlob = cv::dnn::blobFromImage(frame, 1.0 / 255, cv::Size(inWidth, inHeight), cv::Scalar(0, 0, 0), false, false);

// Set the prepared object as the input blob of the network
net.setInput(inpBlob);

cv::Mat output = net.forward();


int H = output.size[2];
int W = output.size[3];

// find the position of the body parts
std::vector<cv::Point> points(nPoints);
for (int n = 0; n < nPoints; n++)
{
// Probability map of corresponding body's part.
cv::Mat probMap(H, W, CV_32F, output.ptr(0, n));

cv::Point2f p(-1, -1);
cv::Point maxLoc;
double prob;
cv::minMaxLoc(probMap, 0, &prob, 0, &maxLoc);
if (prob > thresh)
{
p = maxLoc;
p.x *= (float)frameWidth / W;
p.y *= (float)frameHeight / H;

cv::circle(frameCopy, cv::Point((int)p.x, (int)p.y), 8, cv::Scalar(0, 255, 255), -1);
cv::putText(frameCopy, cv::format("%d", n), cv::Point((int)p.x, (int)p.y), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 0, 255), 2);

}
points[n] = p;
}

int nPairs = sizeof(POSE_PAIRS) / sizeof(POSE_PAIRS[0]);

for (int n = 0; n < nPairs; n++)
{
// lookup 2 connected body/hand parts
cv::Point2f partA = points[POSE_PAIRS[n][0]];
cv::Point2f partB = points[POSE_PAIRS[n][1]];

if (partA.x <= 0 || partA.y <= 0 || partB.x <= 0 || partB.y <= 0)
continue;

cv::line(frame, partA, partB, cv::Scalar(0, 255, 255), 8);
cv::circle(frame, partA, 8, cv::Scalar(0, 0, 255), -1);
cv::circle(frame, partB, 8, cv::Scalar(0, 0, 255), -1);
}

cv::cvtColor(frame, color, CV_BGR2BGRA);
x = color.cols;
y = color.rows;
changeb_g(color.data, x, y, 4);
stbi_write_png("result.png", x, y, 4, color.data, 4 * x);

return 0;
}

#endif

--------------------

とりあえずこんな感じで作ります。
1,2,3のファイルを全部同じディレクトリに入れてg++ *.c *.cpp *.ccでビルド。



おーできた。でもこれを実行するのがなんかとっても遅い。
リリース版でビルドしてもPCでも一回ポーズを推定するのに5秒以上かかります。
もう少し早くなればなぁ。でも認識率はかなり高い気がする。
DNNって速度が遅いのを除けば素晴らしいね。
これ、手の指の形や顔の形も認識できるらしいです。

このPose EstimationのベクトルデータをSVMで学習させればいろんな手のポーズでなんかを動作させるポーズ認証とかもできそう。
なんかPose Estimationって楽しそうなものをたくさん作れそうな気がします。

本日はPose Estimationでした。





0 件のコメント:

コメントを投稿