[手勢辨識應用] Google Mediapipe 手勢控制LED呼吸燈

前言

MediaPipe是一款由Google於2019年開發並開源處理機器學習應用框架專案,提供了跨平台的相關應用,我們之前已經介紹了 Mediapipe 豐富的範例,並於 Raspberry pi Jetson Nano 單板電腦上執行Mediapipe的例子,有興趣的讀者歡迎看看與分享喔!

本篇應用的是 Mediapipe 的 Hand API,由下圖可看到手部各點的定義。

程式會在手掌上標記21個點,本篇是將大拇指和食指的標記抓出來,也就是第4點和第8點,並計算兩點的距離,進而控制「Arduino首次接觸就上手」套件的LED燈,使LED燈產生呼吸燈的效果。

撰寫/攝影 許鈺莨
前情提要
時間 30分鐘 材料表 Arduino首次接觸就上手教學套件 x 1

 


本次專案程式主要是來自 Murtaza 這位 Youtuber ,只要在他的網站上CVZONE中註冊就可以免費得到程式碼。建議大家可以觀看 Murtaza的手部追蹤 手勢控制 的影片。本範例的實際執行影片如下

本文分成以下步驟:
1. 電腦虛擬環境安裝。
2. 手勢控制程式套件安裝。
3. 「首次接觸就上手」的硬體接線。
4. 「首次接觸就上手」的程式燒錄。
5. 電腦端執行手勢控制程式。

第1步 電腦虛擬環境安裝

在執行手勢控制的程式前,需先在電腦中安裝Anaconda軟體並在其軟體中再安裝虛擬環境,安裝步驟請參考本文:AI人工智慧-神經運算】環境建置:安裝Anaconda、Tensorflow、Keras與openCV(Windows篇)

在此所建立的虛擬環境名為 AI_7697,您可以隨意命名。

第2步 手勢控制程式套件安裝

2-1安裝Mediapipe套件0.8.7.1版:

pip install Mediapipe==0.8.7.1
install mediapipe

p.s.筆者試過,若Mediapipe安裝最新版,會無法執行!

2-2 安裝pyserial套件

此套件是為了讓電腦透過 USB 序列埠與「Arduino首次接觸就上手」套件溝通。

pip install pyserial
install pyserial

第3步 「Arduino首次接觸就上手」的硬體接線

請將「首次接觸就上手」的LED燈,用Grove的連接線另外接到Arduino開發板的D3,因為D3腳位才支援PWM 控制 (預設的D4 無法 PWM)。當然也可以另外找一顆LED來接。

p.s. PWM腳位除可以接D3外,還有D5、D6、D9、D10、D11,但Arduino程式要改腳位。

第4步 「Arduino首次接觸就上手」的程式燒錄

4-1 下載程式碼

請由本連結中下載相關程式,解壓縮後在 Arduino_code 資料夾中找到 Arduino_LED.ino

4-2  燒錄程式

請用 Hangeekduino 壓縮檔中的Arduino IDE 1.8.5上傳Arduino_LED.ino程式。Hangeekduino 軟體請點我下下載,教學請參考:【Arduino首次接觸就上手】快速執行AI圖像辨識

第5步 電腦端執行手勢控制程式

下載程式後,在Python_code資料夾中找到 MediaPipeHandPose.py

執行前請先用 micro USB 傳輸線連接「首次接觸就上手」套件接上電腦,並確定 Arduino USB Com Port 編號,再輸入指令:

python MediaPipeHandPose.py --video 2 --com 3
run python script

執行成果如本文開頭的影片

重要程式段落說明

Python程式

#主要程式來源來自:https://www.youtube.com/c/MurtazasWorkshopRoboticsandAI/featured

import serial
import argparse
import cv2
import time
import numpy as np
import math
import mediapipe as mp

########## 手部追蹤偵測 #############
class handDetector():
    def __init__(self, mode=False, maxHands=2, detectionCon=0.5, trackCon=0.5):
        self.mode = mode
        self.maxHands = maxHands
        self.detectionCon = detectionCon
        self.trackCon = trackCon

        self.mpHands = mp.solutions.hands
        self.hands = self.mpHands.Hands(self.mode, self.maxHands,
                                        self.detectionCon, self.trackCon)
        self.mpDraw = mp.solutions.drawing_utils

    def findHands(self, img, draw=True):
        imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        self.results = self.hands.process(imgRGB)
        # print(results.multi_hand_landmarks)

        if self.results.multi_hand_landmarks:
            for handLms in self.results.multi_hand_landmarks:
                if draw:
                    self.mpDraw.draw_landmarks(img, handLms,
                                               self.mpHands.HAND_CONNECTIONS)
        return img

    def findPosition(self, img, handNo=0, draw=True):

        lmList = []
        if self.results.multi_hand_landmarks:
            myHand = self.results.multi_hand_landmarks[handNo]
            for id, lm in enumerate(myHand.landmark):
                # print(id, lm)
                h, w, c = img.shape
                cx, cy = int(lm.x * w), int(lm.y * h)
                # print(id, cx, cy)
                lmList.append( [id, cx, cy])
                if draw:
                    cv2.circle(img, (cx, cy), 15, (255, 0, 255), cv2.FILLED)

        return lmList

def main():
    
############## 各參數設定 ##################
    pTime  = 0
    minPwm = 0
    maxPwm = 255
    briArd = 0
    briBar = 400
    briPer = 0
    
############## 指定WEBCAM和Arduino Serial Port編號的指令 ##################
    
    parser = argparse.ArgumentParser(
      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument(
    '--video', help='Video number', required=False, type=int, default=0)
    parser.add_argument(
      '--com', help='Number of UART prot.', required=True)
    args = parser.parse_args()
    
    COM_PORT = 'COM'+str(args.com)
    BAUD_RATES = 9600
    ser = serial.Serial(COM_PORT, BAUD_RATES)
    
    args = parser.parse_args()
    
    
############## WEBCAM相關參數定義 ##################
    wCam, hCam = 640, 480   
    cap = cv2.VideoCapture(args.video) # 攝影機編號預設為0,也可以輸入其他編號!
    cap.set(3, wCam)
    cap.set(4, hCam)
    detector = handDetector(detectionCon=0.7)

    try:
        while True:
            success, img = cap.read()
            img = detector.findHands(img)
            lmList = detector.findPosition(img, draw=False)
            #print(lmList)
            
            
            if len(lmList) != 0:
                x1, y1 = lmList[4][1], lmList[4][2]
                x2, y2 = lmList[8][1], lmList[8][2]
                cx, cy = (x1 + x2) // 2, (y1 + y2) // 2

                cv2.circle(img, (x1, y1), 15, (255, 0, 255), cv2.FILLED)
                cv2.circle(img, (x2, y2), 15, (255, 0, 255), cv2.FILLED)
                
                #計算大拇指和食指的直線中點距離
                cv2.line(img, (x1, y1), (x2, y2), (255, 0, 255), 3)
                cv2.circle(img, (cx, cy), 15, (255, 0, 255), cv2.FILLED)
                
                #計算大拇指和食指的直線距離
                length = math.hypot(x2 - x1, y2 - y1)
                #print(length)

                #將大拇指和食指的直線距離換算成0~255,Arduino PWM控制數值亦為0~255
                brightness = np.interp(length, [50, 300], [minPwm, maxPwm])
                
                briBar = np.interp(length, [50, 300], [400, 150])  
                briArd = np.around(brightness,2)
               
                
                if length < 50:
                    cv2.circle(img, (cx, cy), 15, (0, 255, 0), cv2.FILLED)
                         
            #畫出直方圖
            cv2.rectangle(img,(50,150),(85,400),(255, 0, 255),3)    
            cv2.rectangle(img,(50, int(briBar)),(85,400),(255, 0, 255),cv2.FILLED)
            cv2.putText(img, f'brightness: {int(briArd)}', (15, 140), cv2.FONT_HERSHEY_COMPLEX, 1, (255, 0, 255), 3)
            
            #送出數值給Arduino
            ser.write(str(briArd).encode())
      
      
            #計算每秒跑幾張
            cTime = time.time()
            fps = 1 / (cTime - pTime)
            pTime = cTime
            cv2.putText(img, f'FPS: {int(fps)}', (40, 50), cv2.FONT_HERSHEY_COMPLEX, 1, (255, 0, 0), 3)

            #顯示畫面 
            cv2.imshow("HandDetector", img)
           
                      
            #按q停止程式          
            if cv2.waitKey(10) & 0xFF == ord('q'):
                break
                
    except KeyboardInterrupt:
        ser.close()
        cap.release()
        cv2.destroyAllWindows()
    
if __name__ == '__main__' :
    main()
  • 12~50:手部偵測動作,偵測手勢並標記手部的節點,一次可以偵測到兩隻手。
  • 64~76:執行程式時所指定的Arduino Serial Com Port 編號,和攝影機編號。若沒指定攝影機編號,則預設為0。
  • 94~123 抓出大拇指和食指的節點位置後,算出兩指節點的距離,將距離轉換成數值0~255,再將數值傳送至Arduino,並用直方圖顯示。

 

Arduino程式

//Arduno D3腳位
int LEDPin = 3;
String number = "" ;
int i = 0 ;
long pwm_val ;

void setup()
{
  //各協定通訊初始化
  Serial.begin(9600);
  pinMode(LEDPin, OUTPUT);
}

void loop()
{
  //執行command副函式
  command();
}

long command() {
  while (Serial.available()) {
    if (i == 0) {
      number = "";
    }
    // 扣除ASCII碼值
    number += Serial.read() - 48; 
    i++;
  }
  // 字串轉換成整數值
  pwm_val = number.toInt(); 
  i = 0 ;
  
  Serial.println(pwm_val);

  //PWM控制LED
  analogWrite(LEDPin, (pwm_val));
  delay(100);
}
  • 2:指定 LED 接在 LED D3 腳位,若要換其他PWM腳位,請在此行程式修改。
  • 20~38:接收從電腦傳來的數值,但由於 Arduino 會以 ASCII 來處理,所以要減去 48 才是正確數字。最後由於這時的”數字”其實還是字串型態,所以還需要將其轉成整數型態,才可以當作控制LED的PWM數值。

本篇到此結束,也歡迎參考阿吉老師的 Arduino 首次接觸就上手全系列教學影片喔!

 

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。