2021年11月23日 星期二

Python 玩 AI,你也可以

Python玩AI,你也可以

Line:ted2016.kpvs
Email:Lct4246@gmail.com
FBhttp://gg.gg/TedLeeFB/
Bloghttp://gg.gg/TedLeeBlog/

Nov. 23, 2021
[1]
深度學習(Deep Learning)深幾許?中學生可以玩嗎?那要怎麼入門呢?

現今科技的進步,帶動了一個潮流:

原本入門門檻高的複雜技術,竟然被包裝成更簡化的流程了(甚至可以像填表一樣,基本資料填一填就好,連程式都不用寫半行)。

CVZone(https://reurl.cc/mvbmRY)就是在這樣一個概念下產生的Python開源套件(package)。它架構在OpenCVMediaPipe函式庫(libraries)上層(圖1),並提供影像處理(image processing)和AI的功能。

圖1:從原始碼(https://tinyurl.com/yxb5o6sg)中可以明確看到CVZone架構於OpenCV和MediaPipe的匯入敘述。


本文將從陳會安老師的剪刀石頭布手勢識別上課範例(https://reurl.cc/gzbqlb)(文後皆以RSP簡稱)[2] 出發,一步一步說明如何細細將這個例子拆解(decompose),以便我們得以通透的了解它整個完整編程的邏輯概念,進而能從中整理與歸納出一套可行的學習路徑(learning path)

學習路徑

懂和程式碼,再拆解出「基本元素」,然後發想自己的相關應用applications,動動手去改動範例程式試看看。之後,就可以甞試著從頭開始規畫寫一個中、小型專案(project)了。

RSP拆解

曹齊平老師(https://tinyurl.com/y29jruc2多年的教學經驗發現:

拆家電是學生活科技(living technologies)最好的活脫脫免費教材(https://tinyurl.com/yxacn8kh)。

然而,軟體學習又比硬體學習來的更低成品。但不變的是由大到小、由簡馭繁的基本原理、原則罷了。本小節即秉持此心法,動手將RSP細部拆解出兩個基礎程式範例:控制攝影機(camera) CVZoneRSP拆解_控攝影機.py 和手部的辨識 RSP拆解:手部辨識.py。本文所有程式都是使用陳會安老師包好的fChart_CVZonehttps://reurl.cc/gzbqlb)這包已裝好CVZone中的Thonny IDE。緊接著,我們將陳老師版的RSP全數加上註解(comments)以供讀者輕鬆地閱讀程式碼。最後,我們再把程式中手部辨識的程式片段獨立成一個函式(function)來管理。

1. RSP拆解:控攝影機 cap


我們以單行註解 # 及 多行註解 ''' ''' 的方式,採用試誤法(trail and error)逐步砍掉暫時用不到的指令來還原OpenCV控制攝影機的程式片段(snippet) CVZoneRSP拆解_控攝影機.py。成功執行的畫面如圖2所示。

圖2:「CVZoneRSP拆解_控攝影機.py」在陳老師版Thonny上的執行結果。

接著,我們在程式中插人適當的中斷點(break point)進入除錯模式(debugging mode)來追蹤出圖3的執行流程。

圖3:「CVZoneRSP拆解_控攝影機.py」的流程圖。

CVZoneRSP拆解_控攝影機.py
import cv2

cap = cv2.VideoCapture(0) #攝影機的handler

while cap.isOpened(): #當攝影機被開啟時
    success, img = cap.read() #回傳攝影機讀到的影像
    
    cv2.imshow("Image", img) #顯示圖片img,視窗標題為Image
    
    if cv2.waitKey(1) & 0xFF == ord("q"): #按q離開迴圈
       break

cap.release() #釋放攝影機handler cap
cv2.destroyAllWindows() #關閉視窗

2. RSP拆解:手部辨識 detector

承1,匯入圖4的 HandTrackingModule 模組(https://tinyurl.com/yxfn45et)就能引用 HandDetector 函式來識別左右手的骨架。執行結果如圖5所示。相關程式碼為「RSP拆解:手部辨識.py」。

圖4:。

圖5:「RSP拆解:手部辨識.py」的執行結果。

RSP拆解:手部辨識.py
from cvzone.HandTrackingModule import HandDetector #L5初始化用
import cv2

cap = cv2.VideoCapture(0) #攝影機的handler
detector = HandDetector(detectionCon=0.5, maxHands=1) #detectionCon:偵測的信心值(confidence)、maxHands:可測到幾隻手

while cap.isOpened(): #當攝影機被開啟時
    success, img = cap.read() #回傳攝影機讀到的影像
    hands, img = detector.findHands(img) #從影像中偵測手

    cv2.imshow("Image", img) #顯示圖片img,視窗標題為Image
    
    if cv2.waitKey(1) & 0xFF == ord("q"): #按q離開迴圈
        break
    
cap.release() #釋放攝影機handler cap
cv2.destroyAllWindows() #關閉視窗


3. RSP完整程式碼加註
承2,在 CVZoneRSP_詳註.py 中加入了手指識別 detector.fingersUp() 來判斷使用者是出剪刀(2隻手指)、石頭(無手指)和布(5隻手指)。程式的報行結果如圖6所示。

圖6:「CVZoneRSP_詳註.py」的執行結果。

CVZoneRSP_詳註.py
from cvzone.HandTrackingModule import HandDetector #L5初始化用
import cv2

cap = cv2.VideoCapture(0) #攝影機的handler
detector = HandDetector(detectionCon=0.5, maxHands=1) #detectionCon:偵測的信心值(confidence)、maxHands:可測到幾隻手

while cap.isOpened(): #當攝影機被開啟時
    success, img = cap.read() #回傳攝影機讀到的影像
    hands, img = detector.findHands(img) #從影像中中偵測手
    
    if hands: #如果有偵測到手
        hand = hands[0]
        bbox = hand["bbox"] #bbox: bounding box
        fingers = detector.fingersUp(hand) #fingers = [手指1, 手指2, 手指3, 手指4, 手指5]
        totalFingers = fingers.count(1) #fingers裡有個1
        print(totalFingers)
        
        msg = "None" #顯示剪刀、石頭、布
        if totalFingers == 5:
            msg = "Paper"
        if totalFingers == 0:
            msg = "Rock"
        if totalFingers == 2:
            if fingers[1] == 1 and fingers[2] == 1: #食指 + 中指
                msg = "Scissors"
                
        cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) #顯示bbox框框 + 訊息
    
cv2.imshow("Image", img) #顯示圖片img,視窗標題為Image
    
    if cv2.waitKey(1) & 0xFF == ord("q"): #按q離開迴圈
        break
    
cap.release() #釋放攝影機handler cap
cv2.destroyAllWindows() #關閉視窗

4. RSP改寫:主/副程式版

承3,我們將之獨立出一個手指識別的副程式 detectHand() ,讓主程式更清爽而容易閱讀(readable)

CVZonRSP_主副程式版.py
from cvzone.HandTrackingModule import HandDetector #L5初始化用
import cv2

cap = cv2.VideoCapture(0) #攝影機的handler
detector = HandDetector(detectionCon=0.5, maxHands=1) #detectionCon:偵測的信心值(confidence)、maxHands:可測到幾隻手

def detectHand(img):
    hands, img = detector.findHands(img) #從影像中中偵測手
    
    if hands: #如果有偵測到手
        hand = hands[0]
        bbox = hand["bbox"] #bbox: bounding box
        fingers = detector.fingersUp(hand) #fingers = [手指1, 手指2, 手指3, 手指4, 手指5]
        totalFingers = fingers.count(1) #fingers裡有個1
        print(totalFingers)
        
        msg = "None" #顯示剪刀、石頭、布
        if totalFingers == 5:
            msg = "Paper"
            
        if totalFingers == 0:
            msg = "Rock"
            
        if totalFingers == 2:
            if fingers[1] == 1 and fingers[2] == 1: #食指 + 中指
                msg = "Scissors"
                
        cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) #顯示bbox框框 + 訊息

while cap.isOpened(): #當攝影機被開啟時
    success, img = cap.read() #回傳攝影機讀到的影像

    detectHand(img)
    
    cv2.imshow("Image", img) #顯示圖片img,視窗標題為Image
    
    if cv2.waitKey(1) & 0xFF == ord("q"): #按q離開迴圈
        break
    
cap.release() #釋放攝影機handler cap
cv2.destroyAllWindows() #關閉視窗


以下是筆者加註的陳老師版RSP範例:

from cvzone.HandTrackingModule import HandDetector #L5初始化用
import cv2
cap = cv2.VideoCapture(0) #攝影機的handler
detector = HandDetector(detectionCon=0.5, maxHands=1) #detectionCon:偵測的信心值(confidence)、maxHands:可測到幾隻手
while cap.isOpened(): #當攝影機被開啟時
    success, img = cap.read() #回傳攝影機讀到的影像
    hands, img = detector.findHands(img) #從影像中中偵測手
    if hands: #如果有偵測到手
        hand = hands[0]
        bbox = hand["bbox"] #bbox: bounding box
        fingers = detector.fingersUp(hand) #fingers = [手指1, 手指2, 手指3, 手指4, 手指5]
        totalFingers = fingers.count(1) #fingers裡有個1
        print(totalFingers)
        msg = "None" #顯示剪刀、石頭、布
        if totalFingers == 5:
            msg = "Paper"
        if totalFingers == 0:
            msg = "Rock"
        if totalFingers == 2:
            if fingers[1] == 1 and fingers[2] == 1: #食指 + 中指
                msg = "Scissors"
        cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30),
                    cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2) #顯示bbox框框 + 訊息
    cv2.imshow("Image", img) #顯示圖片img,視窗標題為Image
    if cv2.waitKey(1) & 0xFF == ord("q"): #按q離開迴圈
        break
cap.release() #釋放攝影機handler cap
cv2.destroyAllWindows() #關閉視窗

註:CVZone給的原始手部辨識程式 HandTrackingExample.pyhttps://tinyurl.com/yxbpstwt)為:

from cvzone.HandTrackingModule import HandDetector
import cv2

cap = cv2.VideoCapture(0)
detector = HandDetector(detectionCon=0.8, maxHands=2)
while True:
    # Get image frame
    success, img = cap.read()
    # Find the hand and its landmarks
    hands, img = detector.findHands(img)  # with draw
    # hands = detector.findHands(img, draw=False)  # without draw

    if hands:
        # Hand 1
        hand1 = hands[0]
        lmList1 = hand1["lmList"]  # List of 21 Landmark points
        bbox1 = hand1["bbox"]  # Bounding box info x,y,w,h
        centerPoint1 = hand1['center']  # center of the hand cx,cy
        handType1 = hand1["type"]  # Handtype Left or Right

        fingers1 = detector.fingersUp(hand1)

        if len(hands) == 2:
            # Hand 2
            hand2 = hands[1]
            lmList2 = hand2["lmList"]  # List of 21 Landmark points
            bbox2 = hand2["bbox"]  # Bounding box info x,y,w,h
            centerPoint2 = hand2['center']  # center of the hand cx,cy
            handType2 = hand2["type"]  # Hand Type "Left" or "Right"

            fingers2 = detector.fingersUp(hand2)

            # Find Distance between two Landmarks. Could be same hand or different hands
            length, info, img = detector.findDistance(lmList1[8], lmList2[8], img)  # with draw
            # length, info = detector.findDistance(lmList1[8], lmList2[8])  # with draw
    # Display
    cv2.imshow("Image", img)
    cv2.waitKey(1)

RSP延伸:可自動判斷輸贏的RSP

RSP遊戲自2017年micro:bit發行以來一直是個經典教材(https://tinyurl.com/yyvo4jxa),它分成「出拳」和「判斷輸贏」兩個階段。其中,能讓板子自動判猜拳結果更是膾炙人口。因此,我們使用了深度學習的技術識別玩家的出拳之後,再搭配暴力法(brute force)的推演,便可以完成用電腦來當「公親」(台語)的任務了。我們將陳老師原程式「CVZoneRSP_詳註.py」 加上這個功能而改寫為「sleep 」和「旗標(flag) + 延時迴圈(delayed loop)」兩個新版本。其中,程式會不斷地去監測攝影機是否正常連線,會造成不斷的取得影像。所以,要想法子讓程式跑慢一點。然而,加了 sleep 指令會讓程式「打睏」,所以會有影像不及時之感。故而改用「空迴圈」的程式設計技巧來克服之。

1. sleep版:RSP自動判輸贏_sleep.py


RSP自動判輸贏_sleep.py
def judge(m, c):
    print('Computer = ' + str(c))
    print('      Me = ' + str(m))
    
    if (me == 0):
        if (c == 0):
            print('=> Tie')
            
        if (c == 1):
            print('   => Me won.')
            
        if (c == 2):
            print('   => Computer won.')
            
    if (me == 1):
        if (c == 0):
            print('   => Computer won.')
            
        if (c == 1):
            print('   => Tie')
            
        if (computer == 2):
            print('   => Me won.')           
        
    if (me == 2):
        if (c == 0):
            print('   => Me won.')
            
        if (c == 1):
            print('   => Computer won.')
            
        if (c == 2):
            print('   => Tie')
            
from random import randint
computer = randint(0, 2)
print('Computer: ' + str(computer))
print('   0: rock')
print('   1: scissors')
print('   2: paper')

me = 99
flag = 0

import time

from cvzone.HandTrackingModule import HandDetector
import cv2

cap = cv2.VideoCapture(0) #使用第一台攝影機
detector = HandDetector(detectionCon=0.5, maxHands=1) #手部偵測

#攝影机已開啟
while cap.isOpened():
    success, img = cap.read() #讀入影像
    hands, img = detector.findHands(img) #找手
#找到手
    if hands:
        hand = hands[0]
        bbox = hand["bbox"]
        
        #有幾根手指
        fingers = detector.fingersUp(hand)
        totalFingers = fingers.count(1)
        #print(totalFingers)
        
        msg = "None"
        if totalFingers == 5:
           msg = "Paper"
           me = 2
           judge(me, computer)
        
        if totalFingers == 0:    
           msg = "Rock"
           me = 0
           judge(me, computer)
        
        if totalFingers == 2:
           if fingers[1] == 1 and fingers[2] == 1:
              msg = "Scissors"
              me = 1
              judge(me, computer)
        
        cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)

        if me < 3:
            me = 99
            computer = randint(0, 2)
            print('----------------------------------------------------')
            print('Computer: ' + str(computer))
            print('   0: rock')
            print('   1: scissors')
            print('   2: paper')    

    cv2.imshow("Image", img)
    time.sleep(1)
    
#按q離開
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break
        
cap.release()
cv2.destroyAllWindows()

2. 「旗標 + 延時迴圈」版:RSP自動判輸贏_delayed loop.py

承1,雖然我們努力調整了 sleep() 函式睡著的時間,但影像不即時看起來就是不順暢。所以,我們借用北商大資管系張隆君教授在研習時教授的延時程式技巧,讓程式執行起來較為流暢許多(圖7)。其中,讀者需先了解程式設計中常用的旗標跳脫法,在循環的迴圈中,埋下適當的越獄暗門。「舉旗待定.py

圖7:「舉旗待定.py」的執行結果。

舉旗待定.py
flag = 0
i = 0

while flag == 0:
   i = i + 1

   if (i < 50):
      if (i % 3 == 0): 
          print(str(i) + ' ', end = '')
   else:
      flag = 1


RSP自動判輸贏_delayed loop.py
def judge(m, c):
    print('Computer = ' + str(c))
    print('      Me = ' + str(m))
    
    if (me == 0):
        if (c == 0):
            print('=> Tie')
            
        if (c == 1):
            print('   => Me won.')
            
        if (c == 2):
            print('   => Computer won.')
            
    if (me == 1):
        if (c == 0):
            print('   => Computer won.')
            
        if (c == 1):
            print('   => Tie')
            
        if (computer == 2):
            print('   => Me won.')           
        
    if (me == 2):
        if (c == 0):
            print('   => Me won.')
            
        if (c == 1):
            print('   => Computer won.')
            
        if (c == 2):
            print('   => Tie')
            
from random import randint
computer = randint(0, 2)
print('Computer: ' + str(computer))
print('   0: rock')
print('   1: scissors')
print('   2: paper')

me = 99
delay_find = 0
flag = 0

import time

from cvzone.HandTrackingModule import HandDetector
import cv2

cap = cv2.VideoCapture(0) #使用第一台攝影機
detector = HandDetector(detectionCon=0.5, maxHands=1) #手部偵測

#攝影机已開啟
while cap.isOpened():
    success, img = cap.read() #讀入影像
    hands, img = detector.findHands(img) #找手
    
    if flag == 0:       
        #找到手
        if hands:
            hand = hands[0]
            bbox = hand["bbox"]
            
            #有幾根手指
            fingers = detector.fingersUp(hand)
            totalFingers = fingers.count(1)
            #print(totalFingers)
            
            msg = "None"
            if totalFingers == 5:
               msg = "Paper"
               me = 2
               judge(me, computer)
            
            if totalFingers == 0:    
               msg = "Rock"
               me = 0
               judge(me, computer)
            
            if totalFingers == 2:
               if fingers[1] == 1 and fingers[2] == 1:
                  msg = "Scissors"
                  me = 1
                  judge(me, computer)
            
            cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)
            cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)
            
            if me < 3:
                me = 99
                computer = randint(0, 2)
                print('----------------------------------------------------')
                print('Computer: ' + str(computer))
                print('   0: rock')
                print('   1: scissors')
                print('   2: paper')
                
                           
            flag = 1
            delay_find = 0
    #cv2.putText(img, msg, (bbox[0]+200,bbox[1]-30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)
    cv2.imshow("Image", img)
    
    delay_find = delay_find + 1    
    if delay_find > 30:
       flag = 0
       
#按q離開
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break
        
cap.release()
cv2.destroyAllWindows()

RSP應用發想

技術問題搞定了,接下來就是教學發想的時刻。
RSP揭示的是手掌骨骼(skeleton)辨識技術,電腦能「看到」我們的手之後,可以怎麼玩呢?
  1. 體感(gestures)控制:投影片(slides)的控制或機器人前進/後退/停止控制、YouTube播放控制…。
  2. 電腦教九九乘乘表。
  3. 手語(sign language)辨識。
讀者們可能更想問:能辨識掌紋算命嗎?

手部辨識原理解說

在這篇 https://tinyurl.com/y4zwklz9 文章中講述的十分清楚,利用手骨估計模型(Hand Landmark Model),我們可以取得21個特徵點(feature points)送到機器學習(machine learning)中的深度學習去演算出手指的資訊。


  1. 六種授權條款
  2. 僅以此文向陳老師致上最深的啟蒙敬意。