LoginSignup
29
20

More than 3 years have passed since last update.

【mediapipe入門】ほんとに簡単に動くね♬

Last updated at Posted at 2021-01-10

Pythonでインストール出来るというので、遊んでみました。
ほぼ、以下の参考のとおりです。
参考②を真似してpytorch-lightningでグー・チョキ・パーをMLPしてみたのが、特に苦労しました。
つまり、画像とかでなく、普通?の自前データデビュー出来ました。
【参考】
ML solutions in MediaPipe
Pythonパッケージ版のMediaPipeが超お手軽 + 簡易なMLPで指ジェスチャー推定

インストールは以下で非常に簡単に入りました。
なお、Raspi4ではversionが未対応とエラーが出て入りませんでした。

pip install mediapipe

やったこと

  • 全部動かしてみる??
  • handsの詳細
  • データ蓄積
  • データ分析する(散布図、cos類似度)
  • matplotlibでプロットしてみる
  • 自前データのdataset, Dataloader
  • networkと学習
  • predictionしつつ描画

・全部動かしてみる??

上記のページには、15個のML solutionsがあるようですが、そのうちPython対応しているのが、以下の4個のようです。

全部動かしてみました。
大変なように聞こえますが、コードは全て同じような構造なので、分かり易いです。
ということで、動いたコードを以下に並べてみます。
※上記サイトのコードのfor webcam inputの部分のコピーです

Face Mesh
import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_face_mesh = mp.solutions.face_mesh

# For webcam input:
face_mesh = mp_face_mesh.FaceMesh(
    min_detection_confidence=0.5, min_tracking_confidence=0.5)
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)    

cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue
    # Flip the image horizontally for a later selfie-view display, and convert
    # the BGR image to RGB.
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    # To improve performance, optionally mark the image as not writeable to
    # pass by reference.
    image.flags.writeable = False
    results = face_mesh.process(image)
    # Draw the face mesh annotations on the image.
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    if results.multi_face_landmarks:
        for face_landmarks in results.multi_face_landmarks:
            mp_drawing.draw_landmarks(
                image=image,
                landmark_list=face_landmarks,
                connections=mp_face_mesh.FACE_CONNECTIONS,
                landmark_drawing_spec=drawing_spec,
                connection_drawing_spec=drawing_spec)
        cv2.imshow('MediaPipe FaceMesh', image)
    if cv2.waitKey(5) & 0xFF == 27:
        break
face_mesh.close()
cap.release()

Hands
import mediapipe as mp
import cv2

mp_drawing = mp.solutions.drawing_utils
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
    min_detection_confidence=0.7,
    min_tracking_confidence=0.5,
)
cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue
    # Flip the image horizontally for a later selfie-view display, and convert
    # the BGR image to RGB.
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    # To improve performance, optionally mark the image as not writeable to
    # pass by reference.
    image.flags.writeable = False
    results = hands.process(image)
    # Draw the hand annotations on the image.
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    image_width, image_height = image.shape[1], image.shape[0]
    if results.multi_hand_landmarks:
        for hand_landmarks in results.multi_hand_landmarks:
            #print('Handedness:', results.multi_handedness)
            mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
            cv2.imshow('MediaPipe Hands', image)
    if cv2.waitKey(5) & 0xFF == 27:
        break
hands.close()
cap.release()

Pose
import mediapipe as mp
import cv2

mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose

pose = mp_pose.Pose(
    min_detection_confidence=0.5, min_tracking_confidence=0.5)
cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue
    # Flip the image horizontally for a later selfie-view display, and convert
    # the BGR image to RGB.
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    # To improve performance, optionally mark the image as not writeable to
    # pass by reference.
    image.flags.writeable = False
    results = pose.process(image)
    # Draw the pose annotation on the image.
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    mp_drawing.draw_landmarks(
        image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
    cv2.imshow('MediaPipe Pose', image)
    if cv2.waitKey(5) & 0xFF == 27:
        break
hands.close()
cap.release()

Holistic;これが集大成face_mesh+左右Hands+Poseがすべて描画されます
import cv2
import mediapipe as mp

mp_drawing = mp.solutions.drawing_utils
mp_holistic = mp.solutions.holistic
# For webcam input:
holistic = mp_holistic.Holistic(
    min_detection_confidence=0.5, min_tracking_confidence=0.5)
cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue
    # Flip the image horizontally for a later selfie-view display, and convert
    # the BGR image to RGB.
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    # To improve performance, optionally mark the image as not writeable to
    # pass by reference.
    image.flags.writeable = False
    results = holistic.process(image)
    # Draw landmark annotation on the image.
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    mp_drawing.draw_landmarks(
        image, results.face_landmarks, mp_holistic.FACE_CONNECTIONS)
    mp_drawing.draw_landmarks(
        image, results.left_hand_landmarks, mp_holistic.HAND_CONNECTIONS)
    mp_drawing.draw_landmarks(
        image, results.right_hand_landmarks, mp_holistic.HAND_CONNECTIONS)
    mp_drawing.draw_landmarks(
        image, results.pose_landmarks, mp_holistic.POSE_CONNECTIONS)
    cv2.imshow('MediaPipe Holistic', image)
    if cv2.waitKey(5) & 0xFF == 27:
        break
holistic.close()
cap.release()

つまり、以下のCamera入力のコードにmediapipeのMLでの分析結果を載せているということです。
上記のコードではミラー像、つまり通常の鏡に映った画像を出力しています。
※以下のコードでは、ミラー像と鏡面像を両方出力するとちょっと変な気持ちがします

Camera base
import cv2

cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue
    #cv2.imshow('Camera', image) #Camera sight    
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    cv2.imshow('amera_mirror', image) #Mirror sight
    if cv2.waitKey(5) & 0xFF == 27:
        break
cap.release()


以下のコードでは、別途blank.jpgファイルをブラシなどで作成して、保存して下さい。
上のCamera_baseと比較するとどのように挿入すれば動くのか一目瞭然だと思います。

Camera base + face_mesh + hands +pose
import mediapipe as mp
import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils

mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
    min_detection_confidence=0.5, min_tracking_confidence=0.5)
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)    

mp_pose = mp.solutions.pose
pose = mp_pose.Pose(
    min_detection_confidence=0.5, min_tracking_confidence=0.5)

mp_hands = mp.solutions.hands
hands = mp_hands.Hands(
    min_detection_confidence=0.7,
    min_tracking_confidence=0.5,
)

image_blank = cv2.imread('blank.jpg') #白紙を台紙にします
sk = 0
cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    image_blank = cv2.imread('blank.jpg')
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    cv2.imshow('Camera', image)
    image.flags.writeable = False
    results_face = face_mesh.process(image)
    results_pose = pose.process(image)
    results_hands = hands.process(image)
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    if results_face.multi_face_landmarks:
        for face_landmarks in results_face.multi_face_landmarks:
            mp_drawing.draw_landmarks(
                image=image_blank,
                landmark_list=face_landmarks,
                connections=mp_face_mesh.FACE_CONNECTIONS,
                landmark_drawing_spec=drawing_spec,
                connection_drawing_spec=drawing_spec)
        cv2.imshow('MediaPipe FaceMesh', image_blank)
        cv2.imwrite('./image/blank/face/image'+ str(sk) + '.png', image_blank)

    if results_hands.multi_hand_landmarks:
        for hand_landmarks in results_hands.multi_hand_landmarks:
            #print('Handedness:', results.multi_handedness)
            mp_drawing.draw_landmarks(image_blank, hand_landmarks, mp_hands.HAND_CONNECTIONS)
            cv2.imshow('MediaPipe Hands', image_blank)
            cv2.imwrite('./image/blank/facehands/image'+ str(sk) + '.png', image_blank)
    mp_drawing.draw_landmarks(
        image_blank, results_pose.pose_landmarks, mp_pose.POSE_CONNECTIONS)
    cv2.imshow('MediaPipe Pose', image_blank)        
    sk += 1
    if cv2.waitKey(5) & 0xFF == 27:
        break
face_mesh.close()
hands.close()
pose.close()
cap.release()

表情豊かな顔画像が得られました。表情ばかりか声が聞こえてきそうです。
faceの動画
face.gif
face+handsの動画
facehands.gif

faceではなくてhandsの詳細

コードは確認できていませんが、説明上以下のポイントを測定してくれています。
Face_meshの詳細は、FACE DETECTION MODEL+FACE LANDMARK MODELとのことです。上の画像から分かるように非常に高精度かつ高速だと思います。
そして、handsも同様な手法で検知・描画しているとのことです。
取得できるlandmarkは以下の通りです。参考①より
hand_landmarks.png
以下のコードでresults_handsに取得したlandmaksと結線をimage_blankに重畳描画しているのが分かります。

results_hands = hands.process(image)
if results_hands.multi_hand_landmarks:
        for hand_landmarks in results_hands.multi_hand_landmarks:
            #print('Handedness:', results.multi_handedness)
            mp_drawing.draw_landmarks(image_blank, hand_landmarks, mp_hands.HAND_CONNECTIONS)

アニメーションは以下のコードで実施しています。
handsは手が映っていないとき中断するので、コマ落ちしています。
そこで、コマ落ちした画像ファイルを飛ばせるように以下のように例外処理をしていますが、コマ落ちがたくさんあるので、s0という変数で中断している番号を飛ばすようにしました。
※このアニメーションではたまたま100(100個コマ落ち)でうまく描画できました
 この書き方は最初は一つずつfor文をいくつも記載していたのですが、それだと中断コマ数をデータから見出す手間(これは毎回異なる)、その度に一つfor文が増えるのでやっていて収束が見えません。そこで、それをfor文で外側で回し始めて、最終的に以下のコードにたどり着いたものです。
つまり、当初設計したわけではなく、コード回しながらたどり着いた設計です。
ここまで来ると最初から以下のコードでs0を適当に選んで実行して例外出力見てこの値を調整できることが分かります。

animation
import numpy as np
import PIL.Image

s=583
images = []
s1=1
s0 =100
sk = 0
for j in range(0,s0,1):
    try:
        for i in range(s1,s,2):
            im = PIL.Image.open('./image/blank/facehands/image'+str(i)+'.png') 
            im =im.resize(size=(640, 480), resample=PIL.Image.NEAREST)
            images.append(im)
    except Exception as e:
        s1=i+1
        sk += 1
        print(sk, e)
print("finish", s)
images[0].save('./image/blank/facehands.gif', save_all=True, append_images=images[1:s], duration=100*1, loop=0)  

データ蓄積

上記は単に取得した画像データを表示しているものです。
ここからは、このlandmarkを学習して、グー・チョキ・パーの判別をしていこうと思います。
その前に、まずlandmarkデータを保存して確認してみましょう。
コードは以下を追加するだけです。

landmarkデータをcsvに吐き出すためのコード
    with open('./hands/sample_hands.csv',  'a', newline='') as f:
        landmark_point = []
        writer = csv.writer(f)
        if results.multi_hand_landmarks:
            idx += 1
            print('Handedness:', results.multi_handedness)
            for hand_landmarks in results.multi_hand_landmarks:
                for index, landmark in enumerate(hand_landmarks.landmark):
                    landmark_x = min(int(landmark.x * image_width), image_width - 1)
                    landmark_y = min(int(landmark.y * image_height), image_height - 1)

                    landmark_ = landmark_x,landmark_y
                    landmark_point.append(landmark_x)
                    landmark_point.append(landmark_y)

                    print(landmark_point)
                writer.writerow(np.array(landmark_point))


少しコードを説明します。
最初に格納するcsvファイルを定義して追加書込み'a'でopenします。
landmark毎に改行したいので、newline=''としています。
landmarkの座標となるlandmark_point配列を初期化しています。
そして、writerを定義します。

    with open('./hands/sample_hands8.csv',  'a', newline='') as f:
        landmark_point = []
        writer = csv.writer(f)

次は、landmarkが取得できていたら、位置情報を座標(整数値)に変換して1セットのlandmark_pointを取得します。
※このコードは上記参考②のリンク先のコードを参考にさせていただきました
※実際には、左右の手の座標が得られる場合がありますが、今回は右手情報は保存後削除して、左手だけを採用することとしました
※ここも改善できますが(右手左手情報が取得できている)、グーチョキパーではどちらかの手が必要ということで簡単に上の対処のままとしています

1セット(1,21,2)のlandmark_pointを取得出来たらwriter.writerowで書込みます。
※上にも書きましたが、1回でデータが二個取れたときは、一行に二個出現します。

        if results.multi_hand_landmarks:
            idx += 1
            #print('Handedness:', results.multi_handedness)
            for hand_landmarks in results.multi_hand_landmarks:
                for index, landmark in enumerate(hand_landmarks.landmark):
                    landmark_x = min(int(landmark.x * image_width), image_width - 1)
                    landmark_y = min(int(landmark.y * image_height), image_height - 1)
                    landmark_ = landmark_x,landmark_y
                    landmark_point.append(landmark_x)
                    landmark_point.append(landmark_y)
                writer.writerow(np.array(landmark_point))

残りは、画面にcv2.imshowし、ファイル保存します。
ミラー画像がいいか、まんまがいいかは好みだと思います。
※データをよく見ると右手左手は必ずしも保存の左右と一致していないので、削除は注意が必要です
※以下では、左手のみで学習データを保存することとします

データ取得時のコード全体
import mediapipe as mp
from PIL import Image
import cv2
import csv
import numpy as np

mp_drawing = mp.solutions.drawing_utils
mp_hands = mp.solutions.hands

hands = mp_hands.Hands(
    min_detection_confidence=0.7,
    min_tracking_confidence=0.5,
)
idx = 0
cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue

    # Flip the image horizontally for a later selfie-view display, and convert
    # the BGR image to RGB.
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    # To improve performance, optionally mark the image as not writeable to
    # pass by reference.
    image.flags.writeable = False
    results = hands.process(image)

    # Draw the hand annotations on the image.
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    image_width, image_height = image.shape[1], image.shape[0]
    with open('./hands/sample_hands6.csv',  'a', newline='') as f:
        list_landmarks = []
        landmark_point = []
        writer = csv.writer(f)
        if results.multi_hand_landmarks:
            idx += 1
            print('Handedness:', results.multi_handedness)
            for hand_landmarks in results.multi_hand_landmarks:
                for index, landmark in enumerate(hand_landmarks.landmark):
                    landmark_x = min(int(landmark.x * image_width), image_width - 1)
                    landmark_y = min(int(landmark.y * image_height), image_height - 1)

                    landmark_ = landmark_x,landmark_y #[idx,index, np.array((landmark_x, landmark_y))]
                    landmark_point.append(landmark_x)
                    landmark_point.append(landmark_y)

                    print(landmark_point)
                writer.writerow(np.array(landmark_point))
                mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS)

            cv2.imshow('MediaPipe Hands', image)
            #cv2.imwrite('./image/annotated_image' + str(idx) + '.png', cv2.flip(image, 1))
            cv2.imwrite('./image/annotated_image' + str(idx) + '.png', image)
    if cv2.waitKey(5) & 0xFF == 27:
        break
hands.close()
cap.release()

データ分析する(cos類似度・クラスタリング)

cos類似度

データの確認(・使えるか・グーチョキパーの確認)のために、まずcos類似度を取ってみましょう。
どこか基準を決めて計算すれば、グーチョキパーなら類似度が異なるだろうという思惑です。
まず、最初に0-42(=21x2(x,y))の数字をヘッダーとして付与します。
なお、cos類似度の計算は参考③を利用させていただきました。
【参考】
【Python NumPy】コサイン類似度の求め方

cos類似度の計算するためのコード
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

df = pd.read_csv('./hands/sample_hands9.csv', sep=',')
#print(df.head(3)) #データの確認
df = df.astype(int)

print(df.iloc[0, :])

#以下のfor文で原点0(df.iloc[i,0], df.iloc[i,1])からの座標とするかどうかを決めている
for i in range(1,len(df),1):
    for j in range(0,21,2):
        df.iloc[i,2*j+1] = df.iloc[i,2*j+1]-df.iloc[i,1]
        df.iloc[i,2*j] = df.iloc[i,2*j]-df.iloc[i,0]

cs_sim =[]
for i in range(1,len(df),1):
    cs= cos_sim(df.iloc[30,:], df.iloc[i,:])
    #print(df.iloc[i,:]-df.iloc[i,0])
    print('cos similarity: {}-{}'.format(30,i),cs)
    cs_sim.append(cs)

plt.figure(figsize=(12, 6))
plt.plot(cs_sim)
plt.ylim(0.9,)
plt.savefig('./hands/cos_sim_hands_plot9.png')
plt.show()


結果は以下のようにそれぞれ(原点補正済、原点補正無し)以下のとおりになりました。
パー・グー・チョキ・グー・チョキ・パー・グーと変化させています。
まあ、一応変化を見える化できていると思います。
原点補正あり
cos_sim_hands_plot9.png
原点補正無しもあまり上と変わりませんでした。
原点が大きく動くような動作だと変わるのかもですが、今回は変動の絶対値は上の方が大きくなっていますが、ほぼ変わらないという結果です。
原点補正無し
cos_sim_hands_plot9-2.png

クラスタリング

k-meansでクラスタリングしてみます。
コードは以下の通りです。

k-meansとPCAを用いたクラスタリングのコード
import pandas as pd
import matplotlib.pyplot as plt
from pandas import plotting
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

df = pd.read_csv('./hands/sample_hands9.csv', sep=',')
print(df.head(3))
df = df.astype(int)

plotting.scatter_matrix(df[df.columns[1:11]], figsize=(6,6), alpha=0.8, diagonal='kde')
plt.savefig('./hands/scatter_plot0-10.png')
plt.pause(5)
plt.close()

# この例では 3 つのグループに分割 (メルセンヌツイスターの乱数の種を 10 とする)
kmeans_model = KMeans(n_clusters=3, random_state=10).fit(df.iloc[:, :])
# 分類結果のラベルを取得する
labels = kmeans_model.labels_

# 分類結果を確認
print(len(labels),labels)

# それぞれに与える色を決める。
color_codes = {0:'#00FF00', 1:'#FF0000', 2:'#0000FF'} #,3:'#FF00FF', 4:'#00FFFF', 5:'#FFFF00', 6:'#000000'}
# サンプル毎に色を与える。
colors = [color_codes[x] for x in labels]

# 色分けした Scatter Matrix を描く。
plotting.scatter_matrix(df[df.columns[1:11]], figsize=(6,6),c=colors, diagonal='kde', alpha=0.8)   #データのプロット
plt.savefig('./hands/scatter_color_plot0-10.png')
plt.pause(1)
plt.close()

#主成分分析の実行
pca = PCA()
pca.fit(df.iloc[:, :])
PCA(copy=True, n_components=None, whiten=False)

# データを主成分空間に写像 = 次元圧縮
feature = pca.transform(df.iloc[:, :])

# 第一主成分と第二主成分でプロットする
plt.figure(figsize=(6, 6))
for x, y, name in zip(feature[:, 0], feature[:, 1], df.iloc[:, 0]):
    plt.text(x, y, name, alpha=0.8, size=10)
plt.scatter(feature[:, 0], feature[:, 1], alpha=0.8, color=colors[:])
plt.title("Principal Component Analysis")
plt.xlabel("The first principal component score")
plt.ylabel("The second principal component score")
plt.savefig('./hands/PCA_hands_plot.png')
plt.pause(1)
plt.close()


各ポイント間について、以下のような相関図が得られます。
※座標に変換したいところですが、手抜きしています
plotting.scatter_matrix(df[df.columns[0:10]], figsize=(6,6), alpha=0.8, diagonal='kde')
結果
scatter_plot0-10.png

色分けした Scatter Matrix を描く。

plotting.scatter_matrix(df[df.columns[0:10]], figsize=(6,6),c=colors, diagonal='kde', alpha=0.8)
結果;上の散布図に3色の色がついて、landmark 5までの点においても相関の違いが異なり、クラスタリングされることが分かります
scatter_color_plot0-10.png

次にPCA主成分分析を行い、データを二次元に射影したのち、クラスタリングします。
結果は以下のとおり、綺麗に3つに分割されました。それぞれのクラスタの境界領域側に分布している点(14,15,64,65,88,89,132,133など)は、上記のcos-類似度のグラフの遷移領域にある点だと分かります。
因みに、青;グー、緑;チョキ、赤;パーです。
plt.scatter(feature[:, 0], feature[:, 1], alpha=0.8, color=colors)
PCA_hands_plot__.png

139データのk-meansでのカテゴライズは以下の結果となっています。

139 
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2]

こうして教師無しでもグーチョキパーは分類できそうです。
※まあ、目で見れば当然ですが、。。。
最後に、次の教師あり深層学習をやるために、この分類データを42番目のデータとして、csvに追記します。
※実際には、追記ではなく再作成しています
【参考】
PythonでCSVファイルを読み込み・書き込み(入力・出力)

k-meansに基づくタグをデータに追記するコード
import pandas as pd
import matplotlib.pyplot as plt
from pandas import plotting
from sklearn.cluster import KMeans
import csv
import numpy as np

df = pd.read_csv('./hands/sample_hands9.csv', sep=',')

# この例では 3 つのグループに分割 (メルセンヌツイスターの乱数の種を 10 とする)
kmeans_model = KMeans(n_clusters=3, random_state=10).fit(df.iloc[:, :])
# 分類結果のラベルを取得する
labels = kmeans_model.labels_

# 分類結果を確認
print(len(labels),labels)
df['42']=labels
header = ['name', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42]

with open('./hands/sample_hands9_.csv',  'a', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(header)
    for i in range(len(df)):
        writer.writerow(np.array(df.iloc[i,:]))

#以下検証用        
df_ = pd.read_csv('./hands/sample_hands9_.csv', sep=',')
print(df_.head(3))
print(df_['42'].astype(int))

plotting.scatter_matrix(df_[df_.columns[1:11]], figsize=(6,6), alpha=0.8, diagonal='kde')
plt.savefig('./hands/scatter_plot_0-10.png')
plt.pause(5)
plt.close()

matplotlibでプロットしてみる

その前にデータの確認用アプリを作成しました。
pandasでcsvデータを読込、xに二次元配列として、インデックスの(x,y)座標を入れます。
yに分類を入れてmatplotlibで描画しています。

データとk-meansの分類タグに基いてmatplotlibで手を描画するコード
import cv2
import csv
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('./hands/sample_hands9_.csv', sep=',')
#print(df.head(3))
data_num = len(df)
#print(data_num)
df = df.astype(int)

x = []
for j in range(data_num):
    x_ = []
    for i in range(0,21,1):
        x__ = [df['{}'.format(2*i)][j],df['{}'.format(2*i+1)][j]]
        x_.append(x__)
    x.append(x_)

y = df['42']    
x = np.array(x)
y = np.array(y)
print(x.shape,y.shape)

fig = plt.figure()
ax = plt.axes()
while 1:
    for j in range(0,data_num):
        for i in range(20):
            plt.plot(x[j][i][0],x[j][i][1],color='black', marker='o')
        plt.text(600,-120,y[j],size=50)
        plt.xlim(700,0)
        plt.ylim(600,-200)
        plt.title(j)
        plt.pause(0.1)
        plt.savefig('./hands/draw/data_plot{}.png'.format(j))
        plt.clf()
    if cv2.waitKey(5) & 0xFF == 27:
        break


hands.gif

自前データのdataset, Dataloader

このところ、pytorch-lightningをやってきたので、今回もこのフレームワークを使おうと思います。
自前データなので、より理解が深まると思います。
まず、大切なことはdatasetが自前なので、それに合わせてDataloaderを作成する必要があります。
皆目分かりませんでしたが、参考⑤を見つけました。
これを参考として開発を進めます。
【参考】
pytorch Dataloader@農学情報科学

ということで、以下のようなコードに行きつけました。
ほぼ、参考⑤のとおりです。
ただし、以下のコードはどこかでこういう記述方法をしていたのを採用しています。
dataとlabelデータの返し方はDataloaderを使う上での肝です
【参考】
Pytorchで遊ぼう【データ成形からFNNまで】
Input numpy ndarray instead of images in a CNN

        #以下のfloat() とlong()の指定は今回の肝です
        self.data = torch.from_numpy(np.array(x)).float()
        self.label = torch.from_numpy(np.array(y)).long()

datasetは、ほぼ上で利用して来たcsvの読込を利用しています。
transformは今回は明示的には定義していません。

HandsDatasetのコード
class HandsDataset(torch.utils.data.Dataset):
    def __init__(self, data_num, transform=None):
        self.transform = transform
        self.data_num = data_num
        self.data = []
        self.label = []
        df = pd.read_csv('./hands/sample_hands7.csv', sep=',')
        print(df.head(3)) #データの確認
        df = df.astype(int)
        x = []
        for j in range(self.data_num):
            x_ = []
            for i in range(0,21,1):
                x__ = [df['{}'.format(2*i)][j],df['{}'.format(2*i+1)][j]]
                x_.append(x__)
            x.append(x_)
        y = df['42'][:self.data_num]

        #以下のfloat() とlong()の指定は今回の肝です
        self.data = torch.from_numpy(np.array(x)).float()
        print(self.data)
        self.label = torch.from_numpy(np.array(y)).long()
        print(self.label)

    def __len__(self):
        return self.data_num

    def __getitem__(self, idx):
        out_data = self.data[idx]
        out_label =  self.label[idx]
        if self.transform:
            out_data = self.transform(out_data)
        return out_data, out_label


そして、これを利用するためのDataloaderはこれまでの作法通り、以下のようにNetworkで定義しています。ここではNetworkの形式的構造とDataloader部分を示しています。
データダウンロードなどの必要がある場合は、def prepare_data(self)がありましたが、今回は利用しないこととします。
data_setup()でself.train_data, self.val_data, self.test_dataを定義しています。
その他は、今までのものと同じコードとしています。

上のdatasetを利用するためのNetwork LitHandsのコード
class LitHands(pl.LightningModule):

    def __init__(self, hidden_size=10, learning_rate=2e-4):
        super().__init__()
        ...
        # Hardcode some dataset specific attributes
        self.num_classes = 3
        self.dims = (1, 21, 2)
        channels, width, height = self.dims
        ...

    def forward(self, x):
        ...
        return F.log_softmax(x, dim=1)

    def training_step(self, batch, batch_idx):
        ...
        return loss

    def validation_step(self, batch, batch_idx):
        ...
        return loss

    def test_step(self, batch, batch_idx):
        ...    
    def configure_optimizers(self):
        ...

    def setup(self, stage=None):
        data_num=1350 #292   
        self.dataset = HandsDataset(data_num, transform=None)
        n_train = int(len(self.dataset)*0.5)
        n_val = int(len(self.dataset)*0.3)
        n_test = len(self.dataset)-n_train-n_val
        print("n_train, n_val, n_test ",n_train, n_val, n_test)

        self.train_data, self.val_data, self.test_data = random_split(self.dataset,[n_train, n_val, n_test])
        print('type(train_data)',type(self.train_data))

    def train_dataloader(self):
        self.trainloader = DataLoader(self.train_data, shuffle=True, drop_last = True, batch_size=32, num_workers=0)
        return self.trainloader

    def val_dataloader(self):
        return DataLoader(self.val_data, shuffle=False, batch_size=32, num_workers=0)

    def test_dataloader(self):
        return DataLoader(self.test_data, shuffle=False, batch_size=32)

networkと学習

network

最後にnetworkと学習を記載します。
コードは参考②を参考に以下の通りとしました。

入力;(batch,index,(x,y))=(32,21,2)
出力class_num=3(グー、チョキ、パー);(3)
42⇒20⇒20⇒3とchを変更しています。
推論の最後の出力;F.log_softmax(x, dim=1)
としています。
training_stepでは、loss = F.nll_loss(logits, y)として、これを小さくするように学習します。
logitsが推論されたchでyが教師データのchです。
※今回はカテゴライズなので、MNISTなどのカテゴライズと同じF.nll_lossを利用しています。
nll_lossについては、参考⑤で考察されています。
因みに、nll_lossは、Negative Log-Likelihood (NLL) loss(負対数尤度損失)ということですね。
 参考⑥⑦に詳細が解説されています。
【参考】
pytorch の NLLLoss の挙動
Understanding softmax and the negative log-likelihood
また、validation_stepでは、predsと正解yとの間の、acc = accuracy(preds, y)を定義して、これで精度を見ています。
客観的な精度を見るためにtest_stepを用意しました。
configure_optimizers(self)では、いつものようにAdamを利用することとしました。
データが少ないので、学習率Ir=2e-4は固定としています。

上のdatasetを利用するためのNetwork LitHandsのコード
class LitHands(pl.LightningModule):
    def __init__(self, hidden_size=10, learning_rate=2e-4):
        super().__init__()
        # Set our init args as class attributes

        self.hidden_size = hidden_size
        self.learning_rate = learning_rate

        # Hardcode some dataset specific attributes
        self.num_classes = 3
        self.dims = (1, 21, 2)
        channels, width, height = self.dims

        # Define PyTorch model
        self.model = nn.Sequential(
            nn.Flatten(),
            nn.Linear(channels * width * height, 2*hidden_size),
            nn.ReLU(),
            #nn.Dropout(0.1),
            nn.Linear(2*hidden_size, 2*hidden_size),
            nn.ReLU(),
            #nn.Dropout(0.1),
            nn.Linear(2*hidden_size, self.num_classes)
        )

    def forward(self, x):
        x = self.model(x)
        return F.log_softmax(x, dim=1)

    def training_step(self, batch, batch_idx):
        x, y = batch
        logits = self(x)
        loss = F.nll_loss(logits, y)
        #print(logits,y)
        return loss

    def validation_step(self, batch, batch_idx):
        x, y = batch
        logits = self(x)
        loss = F.nll_loss(logits, y)
        preds = torch.argmax(logits, dim=1)
        acc = accuracy(preds, y)

        # Calling self.log will surface up scalars for you in TensorBoard
        self.log('val_loss', loss, prog_bar=True)
        self.log('val_acc', acc, prog_bar=True)
        return loss

    def test_step(self, batch, batch_idx):
        # Here we just reuse the validation_step for testing
        return self.validation_step(batch, batch_idx)

    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=self.learning_rate)
        return optimizer

学習

学習は、以下のようにmain()を定義しています。
モデルは以下の実行結果に表示されているものです。

上のNetwork LitHandsを利用して学習するためのコード
def main():
    model = LitHands()
    print(model)
    trainer = pl.Trainer(max_epochs=2000) #収束は速い
    #trainer = pl.Trainer(max_epochs=1, gpus=1) #データ多くなったら使うかな
    trainer.fit(model) #, DataLoader(train, batch_size = 32, shuffle= True), DataLoader(val, batch_size = 32))
    trainer.test(model)
    print('training_finished')
    PATH = "hands_mlp.ckpt"
    trainer.save_checkpoint(PATH) #学習結果を保存

    pretrained_model = model.load_from_checkpoint(PATH)  #pretrainedな重みを読み込む;今は今学習した重み
    pretrained_model.freeze()
    pretrained_model.eval()

    a =  torch.tensor([[315., 420.],  #試しに出力
            [409., 401.],
            [485., 349.],
            [534., 302.],
            [574., 279.],
            [418., 205.],
            [442., 126.],
            [462.,  74.],
            [477.,  33.],
            [364., 186.],
            [370.,  89.],
            [379.,  22.],
            [386., -33.],
            [312., 192.],
            [311.,  98.],
            [316.,  37.],
            [321.,  -9.],
            [259., 218.],
            [230., 154.],
            [215., 113.],
            [204.,  77.]])
    print(a[:])
    results = pretrained_model(a[:].reshape(1,21,2))
    print(results)
    preds = torch.argmax(results)
    print(preds)

    df = pd.read_csv('./hands/sample_hands7.csv', sep=',') #教師データとは異なる適当なデータを用意
    print(df.head(3)) #データの確認
    df = df.astype(int)
    data_num = len(df)
    x = []
    for j in range(data_num):
        x_ = []
        for i in range(0,21,1):
            x__ = [df['{}'.format(2*i)][j],df['{}'.format(2*i+1)][j]]
            x_.append(x__)
        x.append(x_)
    data_ = torch.from_numpy(np.array(x)).float()
    y = df['42'][:data_num]
    label_ = torch.from_numpy(np.array(y)).long()
    count = 0
    for j in range(data_num):
        a = data_[j] #全データに対して計算
        results =  pretrained_model(a[:].reshape(1,21,2)) #全データに対して予測値計算
        preds = torch.argmax(results)
        print(j,preds,label_[j]) #予測値predsと本来の値labelと表示
        if preds== label_[j]:
            count += 1
    acc=count/data_num
    print("acc = ",acc)

if __name__ == '__main__':
    start_time = time.time()
    main()
    print('elapsed time: {:.3f} [sec]'.format(time.time() - start_time))    

実行結果は以下のようになりました。

LitHandsのコード;実行結果
>python mediapipe_mlp_last.py
LitHands(
  (model): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=42, out_features=20, bias=True)
    (2): ReLU()
    (3): Linear(in_features=20, out_features=20, bias=True)
    (4): ReLU()
    (5): Linear(in_features=20, out_features=3, bias=True)
  )
)
GPU available: True, used: False
TPU available: None, using: 0 TPU cores
...
n_train, n_val, n_test  675 405 270
type(train_data) <class 'torch.utils.data.dataset.Subset'>
...
  | Name  | Type       | Params
-------------------------------------
0 | model | Sequential | 1.3 K
-------------------------------------
1.3 K     Trainable params
0         Non-trainable params
1.3 K     Total params
Epoch 1999: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████| 34/34 [00:00<00:00, 558.63it/s, loss=0.00861, v_num=449, val_loss=0.184, val_acc=0.983]
...
Testing: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 9/9 [00:00<00:00, 1128.01it/s]
--------------------------------------------------------------------------------
DATALOADER:0 TEST RESULTS
{'val_acc': tensor(1.), 'val_loss': tensor(0.0014)}
--------------------------------------------------------------------------------
training_finished

acc =  0.9911176905995559
elapsed time: 128.257 [sec]

predictionしつつ描画

最後に、上記の予測をリアルタイム表示するアプリを示します。
コードは以下の通り、ここまで来ると解説いらないかもですが、以下二つだけ
1.train済重みをロードして、モデル表示して、パラメータ活性化して、固定して、そして予測の手順
【参考】
load_from_metrics

pretrained_model = model.load_from_checkpoint(PATH)
print(pretrained_model)
pretrained_model.eval()
pretrained_model.freeze()
preds = pretrained_model(X) #このコードはループ内でwebcameraでxを取得したときに実施

2.予測は以下のコードで実施
 要点は、datasetと同じようにa = torch.from_numpy(a).float()で予測しますが、複数データの場合があるので、a = a[:42]でデータセットを1セットに制限します。あとは、predsを計算しています。

a = np.array(landmark_point).astype(int)
a = torch.from_numpy(a).float()
#print(a.reshape(1,21,2))
a = a[:42]
results_ = pretrained_model(a[:].reshape(1,21,2))
print(results_)
preds = torch.argmax(results_)

3.画像格納をDirを分けています。ミスを見つけ易いです

グー・チョキ・パーリアルタイム予測表示
import mediapipe as mp
from PIL import Image
import cv2
import csv
import numpy as np
import torch

from mediapipe_mlp_last import LitHands

mp_drawing = mp.solutions.drawing_utils
mp_hands = mp.solutions.hands

hands = mp_hands.Hands(
    min_detection_confidence=0.7,
    min_tracking_confidence=0.5,
)

model = LitHands()
PATH = "hands_mlp.ckpt"
pretrained_model = model.load_from_checkpoint(PATH)
print(pretrained_model)
pretrained_model.eval()
pretrained_model.freeze()

image0 = cv2.imread('blank.jpg') #白紙を台紙にします

idx = 0
cap = cv2.VideoCapture(0)
while cap.isOpened():
    success, image = cap.read()
    image_blank = image0.copy() #白紙を台紙にします
    cv2.imwrite('./image/x/image_o' + str(idx) + '.png', image)
    if not success:
        print("Ignoring empty camera frame.")
        # If loading a video, use 'break' instead of 'continue'.
        continue

    # Flip the image horizontally for a later selfie-view display, and convert
    # the BGR image to RGB.
    image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
    # To improve performance, optionally mark the image as not writeable to
    # pass by reference.
    image.flags.writeable = False
    results = hands.process(image)

    # Draw the hand annotations on the image.
    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    image_width, image_height = image.shape[1], image.shape[0]
    with open('./hands/sample_hands8.csv',  'a', newline='') as f:
        list_landmarks = []
        landmark_point = []
        writer = csv.writer(f)
        if results.multi_hand_landmarks:
            idx += 1
            #print('Handedness:', results.multi_handedness)
            for hand_landmarks in results.multi_hand_landmarks:
                for index, landmark in enumerate(hand_landmarks.landmark):
                    landmark_x = min(int(landmark.x * image_width), image_width - 1)
                    landmark_y = min(int(landmark.y * image_height), image_height - 1)

                    landmark_ = landmark_x,landmark_y
                    landmark_point.append(landmark_x)
                    landmark_point.append(landmark_y)

                #print(landmark_point)
                a = np.array(landmark_point).astype(int)
                a = torch.from_numpy(a).float()
                #print(a.reshape(1,21,2))
                a = a[:42]
                results_ = pretrained_model(a[:].reshape(1,21,2))
                print(results_)
                preds = torch.argmax(results_)
                print(preds)
                landmark_point.append(preds)
                writer.writerow(np.array(landmark_point))

            mp_drawing.draw_landmarks(image_blank, hand_landmarks, mp_hands.HAND_CONNECTIONS)

            cv2.imshow('MediaPipe Hands_{}'.format(preds), image_blank)
            cv2.imwrite('./'+'image/{}'.format(preds) +'/image{}_'.format(preds) + str(idx) + '.png', cv2.flip(image_blank, 1))

    if cv2.waitKey(5) & 0xFF == 27:
        break
hands.close()
cap.release()


結果は、以下のような動画になりました。

元画像
hands_results.gif
学習結果のプロット(容量の関係で偶数番号間引き)
hands_results_plot2.gif
少し苦労したので、コード載せておきます。
※学習結果のプロットのアニメ表示はコメントアウトを外して、こちらを生かします

アニメーションのためのコード
import os
import pickle
import numpy as np
import PIL.Image
import pandas as pd

df = pd.read_csv('./hands/sample_hands_results_.csv', sep=',')
print(df.head(3))
df = df.astype(int)
print(df['name'], df['42'])

s=len(df) #139 #583
images = []
s1=1
s0 =3 #100
sk=0
for j in range(0,s0,1):
    try:
        for i in range(s1,s,2):
            im = PIL.Image.open('./image/{}'.format(df['42'][i])+'/image{}_'.format(df['42'][i])+str(df['name'][i])+'.png')
            #im = PIL.Image.open('./hands/draw_results/data_plot'+str(i)+'.png')
            im =im.resize(size=(640, 478), resample=PIL.Image.NEAREST)
            images.append(im)
    except Exception as e:
        s1=i+1
        sk += 1
        print(sk,e)

print("finish", s, len(images))

images[0].save('./hands/hands_results_.gif', save_all=True, append_images=images[1:s], duration=100*1, loop=0)  


こちらは、ファイル名変更だけで、上と同じコードです。

matplotlibで結果のcsvデータをプロット表示するコード
import cv2
import csv
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('./hands/sample_hands_results_.csv', sep=',')
print(df.head(3))
data_num = len(df)
print(data_num)
df = df.astype(int)

x = []
for j in range(data_num):
    x_ = []
    for i in range(0,21,1):
        x__ = [df['{}'.format(2*i)][j],df['{}'.format(2*i+1)][j]]
        x_.append(x__)
    x.append(x_)

y = df['42']    
x = np.array(x)
y = np.array(y)
print(x.shape,y.shape)

fig = plt.figure()
ax = plt.axes()
while 1:
    for j in range(0,data_num):
        for i in range(20):
            plt.plot(x[j][i][0],x[j][i][1],color='black', marker='o')
        plt.text(600,-120,y[j],size=50)
        plt.xlim(700,0)
        plt.ylim(600,-200)
        plt.title(j)
        plt.pause(0.1)
        plt.savefig('./hands/draw_results/data_plot{}.png'.format(j))
        plt.clf()
    if cv2.waitKey(5) & 0xFF == 27:
        break

まとめ

・mediapipeで遊んでみた♬
・Handsについて取得indexからcos類似度、k-meansクラスタリング、そしてPCA主成分分析によりグーチョキパーのクラスタリングしてみた
・pytorch-lightningによりカテゴライズしてみた
・自前のdataset, Dataloaderの作り方を理解した
・mediapipeのface_meshのプロットが面白い
・今回はコードが多く、summaryを多用してみたが読みやすさの効果はあると思う

・face_meshから、実際の顔へのマッピング(変換)をやりたい

29
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
20