[Python / プログラミング]AIでウンコ💩を伸ばしてみた

こんにちは、先日うんこミュージアムに行きウンコ💩に魅了されているIoT探検家のシンジです^_^。

https://unkomuseum.com/

Youtubeで、MediaPipeを応用してストリーミング動画のフレーム上に置いた画像を拡大縮小する、とても面白い動画を見つけました~。

今回はこの動画を真似して、のび~るウンコ💩を作ってみたいと思います^_^

MediaPipeとはGoogleが提供するオープンソースのメディアデータ向け機械学習用フレームワークで、顔認識や姿勢推定などのモデルをエッジで利用できる。


https://google.github.io/mediapipe/

以前の記事ではMediaPipeのPose(姿勢推定)とFace Mesh(顔のランドマーク推定)のモデルを利用しましたが、この動画で使われているのはHands(手のランドマーク推定)です。

用意したもの

・ラズパイ本体(Raspberry Pi 4)
・4インチのLCDモニター
・外付けのカメラ
・キーボード
・microSDカード
・Micro HDMI - HDMI ケーブル

作業の流れ

1)MediaPipeを使って手のランドマークを検出
2)手のランドマークと画像の位置関係から縮尺を計算して、画像の拡大縮小するコードの解説
3) 「ウンコがのび~る」ようにコードをちょっとカスタマイズ
4)「のび~るウンコ💩」を実演

実演に使ったコードはこのページの最後に置いてあります。

1)MediaPipeを使って手のランドマークを検出

import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands

# カメラから画像を取得
cap = cv2.VideoCapture(0)
with mp_hands.Hands(
    model_complexity=0,
    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.")
      # If loading a video, use 'break' instead of 'continue'.
      continue

    image.flags.writeable = False
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    # 画像から手のランドマークを取得
    results = hands.process(image)

    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
  
   # 画像にランドマークを描写  
    if results.multi_hand_landmarks:
      for hand_landmarks in results.multi_hand_landmarks:
        mp_drawing.draw_landmarks(
            image,
            hand_landmarks,
            mp_hands.HAND_CONNECTIONS,
            mp_drawing_styles.get_default_hand_landmarks_style(),
            mp_drawing_styles.get_default_hand_connections_style())
    
    cv2.imshow('MediaPipe Hands', cv2.flip(image, 1))
    if cv2.waitKey(5) & 0xFF == 27:
      break
cap.release()

まずはこの動画の元になっている、MediaPipeのHandsモデルのPythonのコード(上のコード)を動かしてみたいと思います。

すると下の画像のように手のランドマークが描かれます。(※MediaPipeの初期設定に関してはこちらの記事を御覧ください。)

https://google.github.io/mediapipe/solutions/hands

コード24行目のresults = hands.process(image)で画像から手のランドマークを取得して、32行目のdraw_landmarksで画像にランドマークを描写。また、ランドマークは下の図のように全部で21ヵ所あります。

2)手のランドマークと画像の位置関係から縮尺を計算して、画像の拡大縮小するコードの解説

そして、次はこのランドマークの座標とフレーム上に置く画像の位置関係を比較していきます。

それを実現するためにcvzoneのライブラリーをインストール。

https://github.com/cvzone/cvzone
# cvzoneをインストール
pip install cvzone
cvzone/cvzone/HandTrackingModule.py

このcvzoneライブラリーの中にあるHandTrackingModule.pyはMediaPipeのHands(手のランドマーク推定)を応用したモジュールになっており、手を矩形で囲ったり、右手の人差し指と左手の人差し指の距離を測ったりできるメソッドが用意されています。

import cv2
from cvzone.HandTrackingModule import HandDetector

cap = cv2.VideoCapture(1)
cap.set(3, 1280)
cap.set(4, 720)

# 手である確率が70%以上の時のみ手のランドマークを取得
detector = HandDetector(detectionCon=0.7)

while True:
    success, image = cap.read()
    # ランドマークを描画
    hands, image = detector.findHands(image)

    cv2.imshow('MediaPipe Hands', image)
    key = cv2.waitKey(1) 

上のコードのようにHandTrackingModule.pyのHandDetectorクラスをインポートし使ってみると、MediaPipeの公式のコードを動かしたときと同様に手のランドマークが描かれます。

# 画像を配置する座標
ox, oy = 640, 360

while True:
    success, image = cap.read()
    # ランドマークを描画
    hands, image = detector.findHands(image)
    
    # フレーム内にサイズが250pxの正方形の画像を配置
    image1 = cv2.imread("images/unchi.png", cv2.IMREAD_UNCHANGED)
    image[oy - 125:oy + 125, ox - 125:ox + 125] = image1

    cv2.imshow('MediaPipe Hands', image)
    key = cv2.waitKey(1) 

続いてwhile文内に上のようなコードを追加。するとウンコの画像がフレームに配置されました😆。

# 両手が検知された時に
if len(hands) == 2: 
       # fingersUpは5本の指の位置関係を取得する0と1からなる配列を取得するメソッド
       # [1, 1, 0, 0, 0]は親指と小指が他の指よりも高い位置にある時を指している
       if detector.fingersUp(hands[0]) == [1, 1, 0, 0, 0] and detector.fingersUp(hands[1]) == [1, 1, 0, 0, 0]:
           
           # lmlistキーには指のランドマークのxy座標の値が紐付いている
           # 右手と左手の各指の座標を取得
           lmList1 = hands[0]["lmList"]
           lmList2 = hands[1]["lmList"]

           # lmList1[8]の8は人差し指の先端に紐づく番号
           # findDistanceメソッドで右手の人差し指と左手の人差し指の先端同士の距離を取得
           length, info, image = detector.findDistance(lmList1[8], lmList2[8], image)
               startDist = length

次に上のコードで右手の人差し指と左手の人差し指の先端同士の距離length(上の画像の2つのピンクの点の間の距離)を取得。

fingersUpは5本の指の位置関係に関連する番号となっている0と1からなる配列を取得するメソッドです。そして親指と小指が他の指よりも高い位置にある時を指している配列の時にfindDistanceメソッドで距離を取得して、それを描画しています。

この距離の取得は指の動きと連動して画像を拡大縮小する実装への布石となっています。

# 比率を計算するための基準となる値
startDist = None
# 拡大縮小する比率の初期値を設定
scale = 0 
# cxとcyは右手の人差し指と左手の人差し指の先端同士の中間地点の座標
cx, cy = 640, 360

if len(hands) == 2: 
       if detector.fingersUp(hands[0]) == [1, 1, 0, 0, 0] and detector.fingersUp(hands[1]) == [1, 1, 0, 0, 0]:
           
           lmList1 = hands[0]["lmList"]
           lmList2 = hands[1]["lmList"]
           
           if startDist is None:
               length, info, image = detector.findDistance(lmList1[8], lmList2[8], image)
               startDist = length
          
           length, info, image = detector.findDistance(lmList1[8], lmList2[8], image)
           
           # 右手の人差し指と左手の人差し指の先端同士の距離と連動した比率を設定
           scale = int((length - startDist) // 2)
           # 右手の人差し指と左手の人差し指の先端同士の中間地点の座標を取得
           cx, cy = info[4:]   

       else:
           startDist = None 

       # 画像の高さと幅を取得
       h1, w1, _ = image1.shape
       # 比率を元にして拡大縮小する画像の高さと幅を設定           
       newH, newW = ((h1 + scale) // 2) * 2, ((w1 + scale) // 2) * 2
       # 画像をリサイズ
       image1 = cv2.resize(image1, (newW, newH))
       # フレーム内に画像を配置
       image[cy - newH//2:cy + newH//2, cx - newW//2:cx + newW//2] = image1

更に、先程のコードの中に指の動きと連動して画像を拡大縮小するコードを追記しました。

scaleは右手の人差し指と左手の人差し指の先端同士の距離と連動した比率となっており、その比率を元にして拡大縮小する画像の高さと幅が設定されています。

このコードを動かすと、指に動きに連動してウンコ画像が拡大縮小します🤣。とても面白いですね。

ここまでは、この記事の最初に紹介した動画「Virtual Zoom Gesture using OpenCV Python | CVZone」で実装されているコードとほぼ同じです。

ただ、これだとウンコ画像を拡大縮小させることは出来ていますがウンコが「のびている」感が出ないので、コードをちょっとカスタマイズして「のびている」感を出したいと思います^_^。

3) 「ウンコがのび~る」ようにコードをちょっとカスタマイズ

startFranction_x = 0
startFranction_y = 0
diffFranction_x = 0
diffFranction_y = 0

# 右手の人差し指の先端の座標
x_l1_8, y_l1_8 = lmList1[8][0],lmList1[8][1] 
# 左手の人差し指の先端の座標          
x_l2_8, y_l2_8 = lmList2[8][0],lmList2[8][1]

# 右手と左手それぞれの人差し指の先端のy座標の差分の絶対値
abs_y = abs(y_l1_8 - y_l2_8)
# 右手と左手それぞれの人差し指の先端のx座標の差分の絶対値
abs_x = abs(x_l1_8 - x_l2_8)

# 分母が0になることを避ける 
if abs_y == 0:
  abs_y = 1
if abs_x == 0:
  abs_x = 1

if startDist is None:
  # 初期の右手と左手の配置での、右手の人差し指の先端の座標と左手の人差し指の先端の座標を頂点に持つ四角形の高さの幅と横幅の比率を計算
  startFranction_x = float(abs_x / abs_y)
  startFranction_y = float(abs_y / abs_x)

# 右手の人差し指の先端の座標と左手の人差し指の先端の座標を頂点に持つ四角形の高さの幅と横幅の比率を計算
franction_x = float(abs_x / abs_y)
franction_y = float(abs_y / abs_x)

# 初期の右手と左手の配置での比率との差分を計算
diffFranction_x = franction_x - startFranction_x   
diffFranction_y = franction_y - startFranction_y

右手の人差し指の先端と左手の人差し指の先端の位置関係が横方向に広がった時には、画像も横方向に広げ、縦方向に広がった時には画像も横方向に広げることで「のびている」ように見せます。

それを実現するために右手の人差し指の先端の座標と左手の人差し指の先端の座標を頂点に持つ四角形の高さの幅と横幅の比率を計算。

try:
  h1, w1, _ = image1.shape
  # 右手と左手の位置関係が初期と比べて横方向に広がった時には、連動して画像の横幅も広げる
  if diffFranction_x > 0:
    newH, newW = ((h1+scale)//2)*2, ((w1+scale*2)//2)*2
  # 右手と左手の位置関係が初期と比べて縦方向に広がった時には、連動して画像の高さ幅も広げる         
  elif diffFranction_y > 0:
    newH, newW = ((h1+scale*2)//2)*2, ((w1+scale)//2)*2  
  else:
    newH, newW = ((h1+scale)//2)*2, ((w1+scale)//2)*2  

最後に横方向に広がった時と縦方向に広がった時を条件分岐して、画像を広げました。

4)「のび~るウンコ💩」を実演

そしてコードを動かすと、指に動きに連動してウンコが伸びます~🤣。

振り返り

・MediaPipeをHandsモデルを使うと簡単に手のランドマークを描画することができる

・Youtubeの動画「Virtual Zoom Gesture using OpenCV Python | CVZone」のコードを真似させてもらうことで画像を拡大縮小できる

・右手の人差し指の先端と左手の人差し指の先端の比率を計算して「のびている」感を出す

コード


フォルダ構成

  • images

    • unchi.png

  • visual_zoom_gesture.py

visual_zoom_gesture.py

import cv2
from cvzone.HandTrackingModule import HandDetector

cap = cv2.VideoCapture(1)
cap.set(3, 1280)
cap.set(4, 720)

detector = HandDetector(detectionCon=0.65)
startDist = None
scale = 0 
cx, cy = 640, 360
 
startFranction_x = 0
startFranction_y = 0
diffFranction_x = 0
diffFranction_y = 0

while True:
   success, image = cap.read()
   image = cv2.flip(image, 1)
   hands, image = detector.findHands(image, flipType=False)
   image1 = cv2.imread("images/unchi.png", cv2.IMREAD_UNCHANGED)
   ox, oy = 640, 360
 
   if len(hands) == 2:
       if detector.fingersUp(hands[0]) == [1, 1, 0, 0, 0] and \
           detector.fingersUp(hands[1]) == [1, 1, 0, 0, 0]:
 
           lmList1 = hands[0]["lmList"]
           lmList2 = hands[1]["lmList"]
 
           x_l1_8, y_l1_8 = lmList1[8][0],lmList1[8][1]           
           x_l2_8, y_l2_8 = lmList2[8][0],lmList2[8][1]
 
           abs_y = abs(y_l1_8 - y_l2_8)
           abs_x = abs(x_l1_8 - x_l2_8)
 
           if abs_y == 0:
               abs_y = 1
 
           if abs_x == 0:
               abs_x = 1

           if startDist is None:
               length, info, image = detector.findDistance(lmList1[8], lmList2[8], image)
               startDist = length
 
               startFranction_x = float(abs_x / abs_y)
               startFranction_y = float(abs_y / abs_x)
 
           length, info, image = detector.findDistance(lmList1[8], lmList2[8], image)
 
           franction_x = float(abs_x / abs_y)
           franction_y = float(abs_y / abs_x)
 
           diffFranction_x = franction_x - startFranction_x   
           diffFranction_y = franction_y - startFranction_y 
          
           scale = int((length - startDist) // 2)
           cx, cy = info[4:]  
   else:
       startDist = None
 
   try:  
       h1, w1, _ = image1.shape
 
       if diffFranction_x > 0:
           newH, newW = ((h1+scale)//2)*2, ((w1+scale*2)//2)*2
       elif diffFranction_y > 0:
           newH, newW = ((h1+scale*2)//2)*2, ((w1+scale)//2)*2
       else:
           newH, newW = ((h1+scale)//2)*2, ((w1+scale)//2)*2
 
       image1 = cv2.resize(image1, (newW, newH))
       h1_resize, w1_resize, _ = image1.shape
       image = overlayPNG(image, image1, [cx - (h1_resize // 2) , cy - (w1_resize // 2)])
      
   except:
       pass   
 
   cv2.imshow('MediaPipe Hands', image)
   key = cv2.waitKey(1) & 0xFF
   if key == ord("q"):
       break
 
cap.release()

Youtubeで見る

この記事が気に入ったらサポートをしてみませんか?