flaskでREST APIを体験してみる
今更感ありますが、、、今回はREST APIについてまとめていきたいと思います。
また、REST APIについて記事や本を読んでもよく分からず、
実際にREST APIを体験するためにflaskでREST APIを作成してみましたので、
備忘録も兼ねてブログにまとめたいと思います!!
■REST APIとは?
そもそもRESTとは、、
REpresentational State Transferの略になります。
このRESTに関して、RESTの4原則があり、この4原則を満たすものをRESTfulと呼んだりします。
RESTの4原則ですが、
1.セッションなどの状態管理を行わない。(やり取りされる情報はそれ自体で完結して解釈することができる)
2.情報を操作する命令の体系が予め定義・共有されている。(HTTPのGETやPOSTメソッドなど)
3.すべての情報は汎用的な構文で一意に識別される。(URLやURIなど)
4.情報の内部に、別の情報や(その情報の別の)状態へのリンクを含めることができる。
になります。
このRESTの考えを用いてAPIを設計・作成したものをREST APIと言います。
具体的には、
RESTful APIとは、Webシステムを外部から利用するためのプログラムの呼び出し規約(API)の種類の一つで、
「REST」(レスト)と呼ばれる設計原則に従って策定されたもの。
RESTそのものは適用範囲の広い抽象的なモデルだが、
一般的にはRESTの考え方をWeb APIに適用したものをRESTful APIと呼んでいる。
※参考:RESTful API(REST API)とは - IT用語辞典 e-Words
とのことです!!
具体的にはこちらの記事をご参照ください。
0からREST APIについて調べてみた - Qiita
こちらに掲載されている、
RESTなAPIとそうではないAPIの例
が個人的に分かりやすく、REST APIに関して理解が進みました。
■FlaskでREST APIを作成してみる
REST APIに関して文字でまとめてみましたが、、
文字だけではよく分からない!!と思いましたので、実際に作成して動かすことで体験してみました。
今回使用するのはpythonのFlaskです。
Flaskを使用した理由ですが、
個人的に一番使い慣れたサーバーサイドのWebアプリケーションフレームワークだったためです。
では早速コードを載せます。
今回作成したコードはこちら!!
from flask import Flask, jsonify, abort, request import json app = Flask(__name__) datas = [ {"id":1, "name":"hoge", "category":"manga","quantity":5}, {"id":2, "name":"huga", "category":"magazine","quantity":10}, {"id":3, "name":"hogehuga", "category":"magazine","quantity":1} ] # 一覧取得 @app.route('/bookstore', methods=['GET']) def findBookFromName(): retList = [] for d in datas: retList.append(d) if len(retList) > 0: return jsonify(retList) else: return "",400 # idで指定した項目の抽出 @app.route('/bookstore/<int:id>', methods=['GET']) def findBookFromId(id): ret = "" for d in datas: if d["id"] == id: ret = d if ret != "": return jsonify(ret) else: return "",400 # データ追加 @app.route('/bookstore', methods=['POST']) def addBooks(): postedData = request.data.decode('utf-8') postedData = json.loads(postedData) bookData = { "id":int(postedData["id"]), "name":postedData["name"], "category":postedData["category"], "quantity":int(postedData["quantity"]) } datas.append(bookData) return '',200 #データ更新 @app.route('/bookstore/', methods=['PUT']) def putBook(): putData = request.data.decode('utf-8') putData = json.loads(putData) ret = "" print(putData["name"]) for d in datas: if d["id"] == putData["id"]: ret = d ret["name"] = putData["name"] ret["category"] = putData["category"] ret["quantity"] = putData["quantity"] return '',200 if __name__ == "__main__": app.run(debug=True)
APIはそれぞれこちらのようになります。
・一覧取得 :('/bookstore', methods=['GET'])
・idで検索したデータを抽出 :app.route('/bookstore/
・データ追加 :app.route('/bookstore', methods=['POST'])
・データ更新 :app.route('/bookstore/', methods=['PUT'])
では動作確認をしていきます。
curlコマンドを用いてレスポンスを確認する最も簡単になります。
先ほどのコードを実行した上でcurlコマンドを用いて、
curl GET http://127.0.0.1:5000/bookstore
と実行してみてください。
すると、
[ { "category": "manga", "id": 1, "name": "hoge", "quantity": 5 }, { "category": "magazine", "id": 2, "name": "huga", "quantity": 10 }, { "category": "magazine", "id": 3, "name": "hogehuga", "quantity": 1 } ]
といったjson形式であらかじめ登録していたdata変数が表示されるかと思います。
datas = [ {"id":1, "name":"hoge", "category":"manga","quantity":5}, {"id":2, "name":"huga", "category":"magazine","quantity":10}, {"id":3, "name":"hogehuga", "category":"magazine","quantity":1} ]
C++でラムダ式を用いたコールバック関数作成
今回はラムダ関数を用いてC++でコールバック関数を作成していきたいと思います!!
クラス間でコールバック関数を用いてメンバ変数に値をSet/Getしてみたかったので調べてみたところ、
便利そうな方法を見つけたので備忘録としてまとめることに。
■ラムダ式とは?
ラムダ式とは、
簡易的な関数オブジェクトをその場で定義するための機能になります。
例えば、
#include <stdio.h> int main(){ auto add = [](int a,int b){ return a + b; }; int result = add(3,5); printf("%d \n", result); return 0; }
をビルド⇒実行すると、
resultには3+5の結果である8が格納されます。
ラムダ式は、
auto add = [](int a,int b){ return a + b; };
というような形式で書かれます。
意味合いとしては、
「int型のパラメータを2つとり、それらを足し合わせたint型オブジェクトを返す関数オブジェクト」を定義している。
となります。
さらに、ラムダ式の外側の変数を使用する場合には、「キャプチャ」という機能を利用します。
[ ]にどの変数をどのようにキャプチャするかを示します。
例えば、
#include <stdio.h> int main(){ int x = 3; auto chgval = [&]{x = 1;}; chgval(); auto addfive = [=]{ return x + 5;}; int result = addfive(); printf("%d \n", result); return 0; }
といったコードをビルド⇒実行するとresultに6が格納されます。
これは、
auto chgval = [&]{x = 1;};
でラムダ式の外部で定義したxを1に置き換え、
auto addfive = [=]{ return x + 5;};
を実行するとxに格納された1と5が足し合わされた6が返る式になります。
■ラムダ式を用いたコールバック関数
では早速ラムダ式を用いてコールバック関数を作成していきます。
今回はClassAとClassBを作成し、ClassBからClassAへコールバック関数を用いて値をセットしに行くコードを作成します。
まず各コードですがClassA、ClassBそれぞれこちらのようになります。
ClassA.hpp
#include <stdio.h> #include "ClassB.hpp" class hogeA{ private: int NumA; int NumB; hogeB m_hogeB = hogeB(); public: hogeA(){ NumA = 0; NumB = 0; } void DoingFunc(); void SetNumber(int a,int b); int GetNumberA(){return NumA;} int GetNumberB(){return NumB;} };
ClassA.cpp
void hogeA::DoingFunc(){ m_hogeB.Set_HogeA([](int a,int b){ SetNumber(a,b); }); } void hogeA::SetNumber(int a,int b){ NumA = a; NumB = b; }
ClassB.hpp
#include <stdio.h> #include <functional> class hogeB{ typedef std::function<void(int,int)> FUNC_POINTER; FUNC_POINTER p; public: void Set_HogeA(const FUNC_POINTER &func); void Set_func(); };
classB.cpp
#include <stdio.h> #include "ClassB.hpp" void hogeB::Set_HogeA(const FUNC_POINTER &func){ p = func; } void hogeB::Set_func(){ p(1,2); }
まずはClassBに関してですが、
void hogeB::Set_HogeA(const FUNC_POINTER &func){ p = func; }
でコールバックをセットする関数を用意します。
FUNC_POINTER ですが、
typedef std::function<void(int,int)> FUNC_POINTER; FUNC_POINTER p;
と定義しております。
次にClassAですが、
先ほどのラムダ式を用いて、
void hogeA::DoingFunc(){ m_hogeB.Set_HogeA([&](int a,int b){ SetNumber(a,b); }); }
というようにClassB側にコールバック関数として
SetNumber(a,b);
をセットします。
コールバック関数を使っても旨味の無い処理になってしまいますが、、、
試しにこちらのコードを作成し実行してみます。
int main(){ hogeA m_hogeA = hogeA(); hogeB m_hogeB = hogeB(); printf("hogeA:%d \n", m_hogeA.GetNumberA()); printf("hogeB:%d \n", m_hogeA.GetNumberB()); printf("============================ \n"); m_hogeA.DoingFunc(); m_hogeB.Set_func(); printf("hogeA:%d \n", m_hogeA.GetNumberA()); printf("hogeB:%d \n", m_hogeA.GetNumberB()); printf("============================ \n"); }
結果は下記の通りになるかと思います。
hogeA:0 hogeB:0 ============================ hogeA:1 hogeB:2 ============================
これは、
m_hogeA.DoingFunc();
でClassBにClassAで定義したSet関数をコールバック関数として定義し、
m_hogeB.Set_func();
によりコールバック関数を用いてClassBからClassAのメンバ変数にSet関数を用いて値(1,2)をセットしたことによります。
github APIを用いたコマンドでのIssueの取得や登録
今回はgithub上に上がっているIssueの一覧を取得する方法についてまとめていきたいと思います。
個人やOSSなど、ソースコードやバグ・要望などなどをgithub上で管理することがあるかと思います。
その際に、バグや要望の一覧が見たいな!!となったことはないでしょうか?
今回は、APIを用いてIssueの一覧を取得してみたいと思います。
■APIを用いてgithub上のリポジトリからIssueを取得
githubではCUIで操作するためのAPIが用意されております。
下記が公式のドキュメントになります。
REST APIを使ってみる - GitHub Docs
こちらのAPIを使用するためには、
アクセストークン
が必要になります。
アクセストークンの取得方法はこちらの公式ページに詳しくまとまっておりますが、
Settings ⇒ Developer settings
に移動して、
Personal access tokens
でアクセストークンを作成すればOKです。
docs.github.com
curl -H "Authorization: token $TOKEN" https://api.github.com/repos/$USER_NAME/$REPO_NAME/issues
と実行すれば、指定したリポジトリに対するIssueの一覧をjson形式で取得が可能です。
ここで、、
・$TOKEN:アクセストークン
・$USER_NAME:ユーザ名
・$REPO_NAME:リポジトリ名
を指定ください。
もしOpenされたIssueのみを取得したい場合には、
curl -H "Authorization: token $TOKEN" https://api.github.com/repos/$USER_NAME/$REPO_NAME/issues?state=open
とすればOKです。
■Issueの登録
github上のリポジトリにIssueを登録する場合にはこちらのスクリプトを実行すればOKです。
curl -H "Authorization: token $TOKEN" \ -X POST \ -d'{"title": "test Issue", "body":"testです", "labels":["bug"]}' \ https://api.github.com/repos/$USER_NAME/$REPO_NAME/issues
ここで、
・$TOKEN:アクセストークン
・$USER_NAME:ユーザ名
・$REPO_NAME:リポジトリ名
となるのは先ほどのIssue取得と同様です。
■(おまけ)Issueへのコメント追加
github上のIssueに対してコメントを追加する場合には、
curl -H "Authorization: token $TOKEN" \ -X POST \ -d'{"body":"コメントテスト"}' \ https://api.github.com/repos/$USER_NAME/$REPO_NAME/issues/$IssueNumber/comments
とすればOK。
$IssueNumberには登録されているIssueの番号(#**)が入ります。
Dear PyGuiでフォントサイズを変更してみる
先日Dear PyGuiとMediaPipeを連携させてジャンケンゲームを作成しました。
elsammit-beginnerblg.hatenablog.com
こちらのアプリを作成していた時に以外に苦戦したことが記事中にも記載した通り、
Dear PyGuiでフォントサイズどうやって変えるのか?
といった課題?でした。
今回苦戦したので、以降は迷わないようにまとめておこうと思います。
■文字フォントを変更する方法その1
まず、
set_global_font_scale(font size)
APIを使用してフォントサイズを指定することにしてみました。
今回は、テキスト毎に変更するわけではないし、一括で変更すればいいか。
と思ったため採用した策でした。
まずはおためしで下記のようなコードを作成し実行!!
import dearpygui.dearpygui as dpg dpg.create_context() dpg.create_viewport() dpg.setup_dearpygui() dpg.set_global_font_scale(20) with dpg.window(label="testWindow",pos=[10,10]): dpg.add_text("Hello World") dpg.show_viewport() dpg.start_dearpygui() dpg.destroy_context()
結果は、、、
となってしまい、WIndowのフォントサイズまで大きくなってしまった。。
確かにglobalと記載されているので全体にかかるだろうと思っていましたが、、、
まさかwindowのタイトルとかまで大きくなってしまうとは。。
ついでに、
import dearpygui.dearpygui as dpg dpg.create_context() dpg.create_viewport() dpg.setup_dearpygui() with dpg.window(label="testWindow",pos=[10,10]): dpg.set_global_font_scale(20) dpg.add_text("Hello World") dpg.show_viewport() dpg.start_dearpygui() dpg.destroy_context()
と記載しても結果は変わらず。。。
■文字フォントを変更する方法その2
その1では確かにフォントサイズ大きくなりましたが、、
流石にちょっと、、、
ということで別案を検討することに。
そこで、
dpg.add_font("フォント種類", フォントサイズ, tag=タグ名)
のAPIを発見。
早速使用してみることにしました。
では、その1で使用したコードを流用して試しに動かしてみます。
コードはこちら。
import dearpygui.dearpygui as dpg dpg.create_context() dpg.create_viewport() dpg.setup_dearpygui() with dpg.font_registry(show=False): dpg.add_font("./arial.ttf", 50, tag="HelloTag") with dpg.window(label="testWindow",pos=[10,10]): dpg.add_text("Hello World") dpg.bind_item_font(dpg.last_item(), "HelloTag") dpg.show_viewport() dpg.start_dearpygui() dpg.destroy_context()
add_fontですが、
with dpg.font_registry(show=False): dpg.add_font("./arial.ttf", 50, tag="ttf-font")
といったようにfont_registryであらかじめセットしておく必要があります。
さらに、add_fontで指定したタグ名とadd_textのタグ名を一致させるとエラーになってしまいます。
ここが一番自分が困った所でした。。
普通に考えて、add_fontで指定したタグ名と合わせればそのtextのフォントサイズが変わるでしょ。。
仕方ないので、直前の文字サイズを指定するための、
bind_item_font
を用いて、直前に定義したテキストのみのサイズを変更させる方法にたどり着きました。
こちらのコードを実行すると、
となり、定義したテキスト文章のみ文字サイズを大きくすることが出来ました。
■最後に
今回はDear PyGuiを利用する上で苦戦した文字サイズの変更についてまとめてみました。
次は何をしようかな??
Dear PyGuiとMediaPipeを用いたGUIアプリを作成
先日、MediaPipeを用いてじゃんけんゲームのアプリを作成しました。
elsammit-beginnerblg.hatenablog.com
今回はこちらのMediaPipeとDear PyGUIを連携させてGUIアプリを作成しましたのでご紹介します。
■ソースコード
コードに関してはgithubに掲載しておりますのでご参照ください。
github.com
■使い方
こちらを利用する場合には、Dear PyGUIとMediaPipe、OpenCVが必要になります。
それぞれ、
[Dear PyGUI]
Dear PyGuiを用いてPython GUIを作成してみる - Elsaの技術日記(徒然なるままに)
[MediaPipe]
pip install mediapipe
[OpenCV]
pip install opencv-python pip install opencv-contrib-python
にてインストールが可能です。
準備が完了した後は、githubよりコードをダウンロードし、下記コマンドを実行。
python JankenMain.py
アプリを落とす場合には、
DearPyGUIで表示させているGUIアプリの閉じるボタンを押せばOKです。
(注)ポンの音声が出力されるまではアプリが落ちませんのでご注意ください。
■アプリを動作させてみる
アプリを動作させた結果はこちらに掲載してみました。
よろしかったら是非ご覧ください。
youtu.be
■概要
こちらのコードをざっくりご紹介しますと、
作成していたMediaPipeを用いたじゃんけんゲームをthread化し、
生成された手+CPUの手の結合画像をDear PyGUIに表示しているのみです。
※生成された手+CPUの手の結合画像とはこちらのことです。
先日、Dear PyGUIで動画再生アプリを作成していましたのでそちらを流用することで比較的簡単に作成することが出来ました。
elsammit-beginnerblg.hatenablog.com
GUI表示部と画像処理threadとのやり取りはコールバック関数を用いております。
pythonは関数にコールバック関数を引数に与え、使いたい箇所で実行すればよいので楽ですね!!
どこぞのプログラミング言語だと関数ポインタ駆使しないといけないので、
コールバック関数1つ定義するにも一苦労なんですよね。。
話しがそれてしまいました。
Pythonのコールバック関数の使い方はこちらに掲載されておりますので気になった方はご覧ください。
qiita.com
ただ、、、
thread化したことによりフレーム画像の同期がとれていないためか画像上に判定結果を載せようとするとチカチカして見えにくくなる。。。
ということで、
判定結果は別テキストボックスに出力するようにいたしました。
Dear PyGUIで文字サイズを変更するのに苦労しました。。
Dear PyGUIで文字サイズ変更方法に関してはまた別のブログにてまとめたいと思います。
■最後に
今回は自身が作成したアプリのご紹介でした。
細かい部分について割愛してしまったので、あとで躓いた点はまとめておきたいと思います。
それにしてもMediaPipeはすごいですね。
まだまだ面白い使い方ありそうなので色々試してみたいと思います。
MediaPipeでじゃんけんゲームを作成
先日MediaPipeで手の形状を検出してみました。
elsammit-beginnerblg.hatenablog.com
最後に手の形状を検出してじゃんけんの判定を行ってみました。
これを見てふと、、、
せっかくなのでCPUと対戦出来るじゃんけんゲーム作ったら面白いかな??
と思い立ち、作成することに。
最初はとりあえず、OpenCVのimshowでの表示を用いてじゃんけんゲームを作成してみました。
■できたアプリ
作成できたアプリはこちら!!
音声も自分で用意したものを流しております。
youtu.be
■コードはgithubにて公開中
コードはこちらに格納しております。
よろしければご覧ください。
https://github.com/elsammit/rockpaperscissors-usingmediapipe
こちらのコードですが、
git cloneで取ってきたソースコードにて、
python JankenMain.py
を実行すればOKです。
なお、MediaPipeライブラリのインストールが必要になります。
インストール手順は前回のブログにまとめておりますのでこちらを参考に。
elsammit-beginnerblg.hatenablog.com
■コードに関して
こちらのコードですが、クラスとして下記定義しております。
・handsクラス:MediaPipeライブラリを用いて検出したじゃんけんの手
・Pertnerクラス:CPUの出す手
・Judgeクラス:CPUの手とユーザの手の結果を元に勝ち負け判定
・Onseiクラス:「じゃんけん。ぽん。」音声出力
handsクラスで実施している内容は前回のブログで記載済みになりますので詳細は割愛します。
また、Onseiクラスに関してもメソッドコールで音声出力しているのみになります。
このため、今回はPartnerクラス内で実施している内容について以下でまとめていきたいと思います。
■CPUが出す手の処理
CPU側の処理は下記コードの通りになります。
import cv2 import threading import JankenOnsei import random import time import Define m_Define = Define.Define() m_Onsei = JankenOnsei.Onsei() class Pertner: looretCnt = 1000 filePath = m_Define.HANDSHAPE_ROCK_IMG result_img = cv2.imread(m_Define.HANDSHAPE_ROCK_IMG) StartFlg = False JankenResult = False rsp = "" FinishFlg = False testFlg = False def StartOnseiThread(self): thread1 = threading.Thread(target=m_Onsei.jankenOnsei) thread1.start() def GetHandsMat(self, cnt): if cnt % 10 <= 2: self.filePath = m_Define.HANDSHAPE_ROCK_IMG self.rsp = m_Define.HANDSHAPE_ROCK elif cnt % 10 <= 5: self.filePath = m_Define.HANDSHAPE_SCISSORS_IMG self.rsp = m_Define.HANDSHAPE_SCISSOR elif cnt % 10 <= 9: self.filePath = m_Define.HANDSHAPE_PAPPERS_IMG self.rsp = m_Define.HANDSHAPE_PAPPER def PartnerHands(self): if m_Onsei.onseiStatus == 0: self.GetHandsMat(self.looretCnt) if self.looretCnt > 1000: self.looretCnt = 0 elif m_Onsei.onseiStatus == 1: x = random.randint(0,5) self.GetHandsMat(x) self.JankenResult = True m_Onsei.onseiStatus += 1 elif m_Onsei.onseiStatus == 2: self.JankenResult = False elif m_Onsei.onseiStatus == 3: m_Onsei.onseiStatus = 0 self.StartOnseiThread() self.JankenResult = False self.looretCnt = 0 time.sleep(0.1) resultImage = cv2.imread(self.filePath) return resultImage def PartnerLoop(self): m_Onsei.onseiStatus = 3 while True: self.looretCnt += 1 self.result_img = self.PartnerHands() self.StartFlg = True time.sleep(0.01) if self.FinishFlg == True: break
CPU側の処理としては、大きく分けると
・じゃんけんの掛け声の間は数百ミリ秒単位でグー、チョキ、パーをローリング
・ぽんの掛け声でランダムに処理を切り替える
の2つになります。
こちらの2つの処理の切り替えですが、
音声と同期をとるために、m_Onsei.onseiStatusにて管理しております。
m_Onsei.onseiStatusですが、
m_Onsei.onseiStatus = 0 :じゃんけんの掛け声中。
m_Onsei.onseiStatus = 1 :ぽんを言う直前。CPUの手を決定し表示。
m_Onsei.onseiStatus = 2 :ぽんを言った後。CPUとの勝敗を表示している間の待ち。
m_Onsei.onseiStatus = 3 :次のじゃんけんを行うための初期化処理。
音声ですが、本クラス内でOnseiインスタンスを生成させ、別スレッドで音声出力を実行しております。
じゃんけんの掛け声中のグー、チョキ、パーのルーレットですが、
looretCntの数値により切り替えております。
(こちらは別の方法の方がいいかも?)
こちらの切り替えを実施しているのが、
PartnerHands関数になります。
そして、PartnerHands関数を定期的にコールしているのが、
PartnerLoop関数。
PartnerLoop関数内では10ms間隔でループを実行。
PartnerLoop関数ですが、
このじゃんけんゲームのmain関数にあたる、
JankenMain.py内でmainとは別スレッドでコールしております。
デバッグ用ですがCPU側のみを出力させることも出来るようになっております。
こちらのコードを実行するとCPU側だけの処理が動くようになります。
python PartnerHands.py
実際に実行するとこちらのようになります。
※実際には音声も出力されています。
■最後に
とりあえずのレベルですがじゃんけんゲーム動くレベルまで持っていくことが出来ました!!
ちょっと気になる点もあるので、時間を見て修正していきたいと思います!!
また最新版が出来たらご連絡したいと思います。
MediaPipeで手の動作をリアルタイムで検出する
たまたまになるのですが、MediaPipeという手や顔の動きをリアルタイムで検出するライブラリを発見。
面白そうだったので触ってみることにしました。
■MediaPipeとは?
MediaPipeは、Googleが提供しているライブメディアやストリーミングメディア向けのMLソリューションであり、
CPUのみで検出が行えるツールになります。
MediaPipeの特徴として、
・エンドツーエンドの高速化:一般的なハードウェアでも高速化された組み込みの高速ML推論と処理
・一度構築してどこにでもデプロイ:統合ソリューションはAndroid、iOS、デスクトップ/クラウド、ウェブ、IoTで機能します
・すぐに使用できるソリューション:フレームワークのフルパワーを実証する最先端のMLソリューション
・フリーでオープンソース: Apache 2.0でのフレームワークとソリューション、完全に拡張可能でカスタマイズ可能
があります。
本ライブラリのソリューションとして、
・顔検知
・フェイスメッシュ
・手検知
・ポーズ
・物体検出
・インスタントモーショントラッキング
などなど、、、
12種類用意されております。
また、MediaPipeは様々なプログラミング言語に対応しております。
対応しているプログラミング言語やソリューションの詳細は下記をご確認ください。
https://google.github.io/mediapipe/
■MediaPipeインストール
今回はpythonを用いて手の動作検知を行っていきます。
そこで、pythonでMediaPipeが利用できるようにMediaPipeをインストールしていきます。
といってもpythonであれば、
pip install mediapipe or pip3 install mediapipe
でOKです。
pythonはライブラリインストールが簡単でありがたいです。
自分はインストール済みでしたが、OpenCVが必要になります。
インストールがまだの方は下記を実行してOpenCVをインストールください。
pip install opencv-python pip install opencv-contrib-python or pip3 install opencv-python pip3 install opencv-contrib-python
■MediaPipeを用いて手の動作を検知
では早速、てのモーションをリアルタイムで検知していきたいと思います!!
コードは公式が掲載しているコードを少し改変したものを載せます。
https://google.github.io/mediapipe/solutions/hands.html
コードはこちら。
import cv2 import mediapipe as mp class hands: mp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solutions.drawing_styles mp_hands = mp.solutions.hands after_img = "" StartFlg = False rsp = "" FinishFlg = False def HandsLoop(self): cap = cv2.VideoCapture(0) with self.mp_hands.Hands( min_detection_confidence=0.5, min_tracking_confidence=0.5) as hands: while cap.isOpened(): success, image = cap.read() if not success: print("Ignoring empty camera frame.") continue image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) before_image = image.copy() results = hands.process(image) if results.multi_hand_landmarks: for hand_landmarks in results.multi_hand_landmarks: self.mp_drawing.draw_landmarks( image, hand_landmarks, self.mp_hands.HAND_CONNECTIONS, self.mp_drawing_styles.get_default_hand_landmarks_style(), self.mp_drawing_styles.get_default_hand_connections_style()) self.after_img = cv2.flip(image - before_image, 1) if __name__ == '__main__': cv2.imshow('MediaPipe Hands', self.after_img) if cv2.waitKey(5) & 0xFF == 27: break cap.release() if __name__ == '__main__': m_hands = hands() m_hands.HandsLoop()
こちらのコードで手のリアルタイム検知結果がimshowで表示させることが出来ます。
パラメータに関してはとりあえずデフォルト設定で利用しておりますが、
・min_detection_confidence:検出が成功したと判断するための最小信頼値(0.0 ~ 1.0の範囲)
・min_tracking_confidence:手のランドマークが正常に追跡されたと見なされるためのランドマーク追跡モデルからの最小信頼値(0.0 ~ 1.0の範囲)
を意味しております。
処理フローですが、
まずOpenCVのVideoCaptureによりカメラからのフレーム画像を取得します。
その後、min_detection_confidence、min_tracking_confidenceをそれぞれ0.5のパラメータで
hands.processにてフレーム画像の解析を行い、
self.mp_drawing.draw_landmarksにて解析結果を画像上に描画しているようです。
最後に、imshowにて画像を表示。
この際、手の検知結果のみを表示させるために、
image - before_image
により検出結果の画像から元画像を差し引いています。
こちらのコードを実行するとこちらのような映像が表示されます。
■グー、チョキ、パーを認識してみる
手を検知するだけだとサンプルのまんまですので、、、
簡単にグー、チョキ、パーかを判別してみたいと思います。
コードはこちら。
import cv2 import mediapipe as mp from PIL import ImageFont, ImageDraw, Image import numpy as np import time FONT = ImageFont.truetype('C:\Windows\Fonts\meiryo.ttc' , 32) class hands: mp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solutions.drawing_styles mp_hands = mp.solutions.hands after_img = "" StartFlg = False rsp = "" FinishFlg = False def HandShape(self, hand_landmarks, image): image_width, image_height = image.shape[1], image.shape[0] index_finger = [ int(hand_landmarks.landmark[8].y * image_height), int(hand_landmarks.landmark[6].y * image_height) ] index_middle = [ int(hand_landmarks.landmark[12].y * image_height), int(hand_landmarks.landmark[10].y * image_height) ] index_ring = [ int(hand_landmarks.landmark[16].y * image_height), int(hand_landmarks.landmark[14].y * image_height) ] index_pinky = [ int(hand_landmarks.landmark[20].y * image_height), int(hand_landmarks.landmark[18].y * image_height) ] fingersList = [index_finger, index_middle, index_ring, index_pinky] return fingersList def JudgeRPS(self, fingersList): hand_ret = "" if fingersList[0][0] > fingersList[0][1] and fingersList[1][0] > fingersList[1][1] and \ fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]: hand_ret = "グー" elif fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]: hand_ret = "チョキ" else: hand_ret = "パー" return hand_ret def HandsLoop(self): cap = cv2.VideoCapture(0) with self.mp_hands.Hands( min_detection_confidence=0.5, min_tracking_confidence=0.5) as hands: while cap.isOpened(): success, image = cap.read() if not success: print("Ignoring empty camera frame.") continue image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) before_image = image.copy() results = hands.process(image) if results.multi_hand_landmarks: for hand_landmarks in results.multi_hand_landmarks: self.mp_drawing.draw_landmarks( image, hand_landmarks, self.mp_hands.HAND_CONNECTIONS, self.mp_drawing_styles.get_default_hand_landmarks_style(), self.mp_drawing_styles.get_default_hand_connections_style()) fingerList = self.HandShape(hand_landmarks, image) self.rsp = self.JudgeRPS(fingerList) self.after_img = cv2.flip(image - before_image, 1) img_pil = Image.fromarray(self.after_img) draw = ImageDraw.Draw(img_pil) draw.text((10, 350), self.rsp, font = FONT, fill = (255,0,255)) self.after_img = np.array(img_pil) cv2.imshow('MediaPipe Hands', self.after_img) if cv2.waitKey(5) & 0xFF == 27: break cap.release() if __name__ == '__main__': m_hands = hands() m_hands.HandsLoop()
先ほど解析しませんでしたが、
hand_landmarks
に検知した手の節?の座標が格納されております。
hand_landmarksは{x,y}構造体の配列になっておりそれぞれ下記画像の通り割り当てられております。
こちらの座標情報を元に、
手の形状解析を行っております。
解析のコードはこちら。
def HandShape(self, hand_landmarks, image): image_width, image_height = image.shape[1], image.shape[0] index_finger = [ int(hand_landmarks.landmark[8].y * image_height), int(hand_landmarks.landmark[6].y * image_height) ] index_middle = [ int(hand_landmarks.landmark[12].y * image_height), int(hand_landmarks.landmark[10].y * image_height) ] index_ring = [ int(hand_landmarks.landmark[16].y * image_height), int(hand_landmarks.landmark[14].y * image_height) ] index_pinky = [ int(hand_landmarks.landmark[20].y * image_height), int(hand_landmarks.landmark[18].y * image_height) ] fingersList = [index_finger, index_middle, index_ring, index_pinky] return fingersList
解析の結果から、グー、チョキ、パーの解析を行います。
コードはこちら。
今回は簡単のために、y座標のみをチェックかつ先端(TIP)がDIPの位置以下になった場合に指が曲がったと判断し、
その曲がった指をチェックすることでグー、チョキ、パーを解析しています。
def JudgeRPS(self, fingersList): hand_ret = "" if fingersList[0][0] > fingersList[0][1] and fingersList[1][0] > fingersList[1][1] and \ fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]: hand_ret = "グー" elif fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]: hand_ret = "チョキ" else: hand_ret = "パー" return hand_ret
後は解析した結果を画像に重畳させてimshowで表示。
self.after_img = cv2.flip(image - before_image, 1) img_pil = Image.fromarray(self.after_img) draw = ImageDraw.Draw(img_pil) draw.text((10, 350), self.rsp, font = FONT, fill = (255,0,255)) self.after_img = np.array(img_pil) cv2.imshow('MediaPipe Hands', self.after_img)
こちらのコードを実行した結果は下記に掲載しました。
よろしければご覧ください。
youtu.be
■最後
MediaPipeですが、結構検出精度が高く使っていてとても楽しかったです。
もう少し使ってみたいと思います。
また、何か出来たら掲載していきたいと思います。