Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

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コマンドと先ほどのAPIを用いて、、

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の番号(#**)が入ります。

■最後に

今回はAPIを用いてgithub上の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()

結果は、、、
f:id:Elsammit:20220127230927p:plain

となってしまい、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

を用いて、直前に定義したテキストのみのサイズを変更させる方法にたどり着きました。

こちらのコードを実行すると、
f:id:Elsammit:20220127232425p:plain
となり、定義したテキスト文章のみ文字サイズを大きくすることが出来ました。

■最後に

今回は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の手の結合画像とはこちらのことです。
f:id:Elsammit:20220123113829p:plain

先日、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

実際に実行するとこちらのようになります。
※実際には音声も出力されています。
f:id:Elsammit:20220116105735g:plain

■最後に

とりあえずのレベルですがじゃんけんゲーム動くレベルまで持っていくことが出来ました!!
ちょっと気になる点もあるので、時間を見て修正していきたいと思います!!
また最新版が出来たらご連絡したいと思います。



MediaPipeで手の動作をリアルタイムで検出する

たまたまになるのですが、MediaPipeという手や顔の動きをリアルタイムで検出するライブラリを発見。
面白そうだったので触ってみることにしました。




■MediaPipeとは?

MediaPipeは、Googleが提供しているライブメディアやストリーミングメディア向けのMLソリューションであり、
CPUのみで検出が行えるツールになります。

MediaPipeの特徴として、
 ・エンドツーエンドの高速化:一般的なハードウェアでも高速化された組み込みの高速ML推論と処理
 ・一度構築してどこにでもデプロイ:統合ソリューションはAndroidiOS、デスクトップ/クラウド、ウェブ、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
により検出結果の画像から元画像を差し引いています。

こちらのコードを実行するとこちらのような映像が表示されます。
f:id:Elsammit:20220113224901g:plain

■グー、チョキ、パーを認識してみる

手を検知するだけだとサンプルのまんまですので、、、
簡単にグー、チョキ、パーかを判別してみたいと思います。
コードはこちら。

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}構造体の配列になっておりそれぞれ下記画像の通り割り当てられております。
f:id:Elsammit:20220111221902p:plain

こちらの座標情報を元に、
手の形状解析を行っております。
解析のコードはこちら。

    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ですが、結構検出精度が高く使っていてとても楽しかったです。
もう少し使ってみたいと思います。
また、何か出来たら掲載していきたいと思います。



Linuxでのファイルzip化時のフォルダ構成についてまとめてみる

Linuxにてzip化を行う場合にはこちらのコマンドを実行すれば可能なのです。

zip dist.zip srcfile 

もしディレクトリごとzip化したい場合には、

zip -r dist.zip srcdir

とすればOKです。

■zipコマンド実行後の挙動
しかしながら、、、1つ困ったことがあります。
それは、指定したパスの通りにzip暗号化してしまう点です!!
例えば、

zip dist.zip /home/hoge/huga/srcdir

とした場合、dist.zipが生成されるのですが、
このdist.zipを展開すると、
/home/hoge/huga/srcdir
のフォルダ構成でsrcdirが格納されます。
圧縮したい対象がsrcdir配下に格納したファイルのみであった場合、
ここまで階層深い状態でファイルが格納されていると結構面倒。。。

ということで階層をコントロールして、自分が欲しいフォルダ構成の状態でzip化する方法についてまとめていきたいと思います。

■ファイルのみをzip化したい場合
もしファイルのみをzip化したい場合には、--junk-paths(-j)オプションを付与すればOKです。
例えば、srcdir配下に、
 ・srcfile1
 ・srcfile2
 ・srcfile3
が格納された構成に対して--junk-paths(-j)オプションを付与して、

zip -r -j dist.zip /home/hoge/huga/srcdir

を実行するとdist.zipが格納されます。
このdist.zipを解凍すると、
 ・srcfile1
 ・srcfile2
 ・srcfile3
が展開されることが確認できるかと思います。
階層が深くならなずに必要なファイルのみ暗号化出来るので便利ですね。

■フォルダパスを指定して圧縮する方法
先ほどの--junk-paths(-j)オプションを付与した方法であればファイルのみを圧縮できるのですが、、、
1つ難点があります。
それは、同じファイル名が存在する場合には圧縮出来ない!!ということです。
まぁ、ファイル名が被ってしまうので当たり前と言えばそうなのですが。

そこで、今度はフォルダパスを指定して圧縮する方法をまとめていきます。

そもそも、zip圧縮にて階層が深くなってしまう理由はzipコマンドを実行する際に
/home/hoge/huga/srcdir
のように深い階層で指定しまうことになります。

そこで、自分の好きなパスまで移動してからzipコマンドを使用すればOK!!
例えば、srcdirをzip圧縮したい場合には

cd /home/hoge/huga/
zip -r dist.zip srcdir

とすればOK。

さらに、別のフォルダも一緒に含めてzip圧縮をしたい場合には、

    • update(-u)オプションを駆使すればOK。

例えば、/home/hoge/huga/srcdir以外に、/home/hoge/huga/srcdir2も圧縮させたい場合には、

cd /home/hoge/huga/
zip -r -u dist.zip srcdir

cd /home/hoge/huga/
zip -r -u dist.zip srcdir2

とすればOK。
圧縮したdist.zipを解凍すると、
rcdirフォルダ
srcdir2フォルダ
が格納されているのが確認できるかと思います。

■最後に
基礎的な部分ですが以外に知らない内容だったので勉強になってよかったです。
また不明点などがあったら調べてまとめていきたいと思います。

■参考
https://atmarkit.itmedia.co.jp/ait/articles/1608/01/news039.html
https://uxmilk.jp/50165