Python玩AI,你也可以
Line:ted2016.kpvs
Email:Lct4246@gmail.com
FB:http://gg.gg/TedLeeFB/
Email:Lct4246@gmail.com
FB:http://gg.gg/TedLeeFB/
Blog:http://gg.gg/TedLeeBlog/
Nov. 23, 2021
現今科技的進步,帶動了一個潮流:
原本入門門檻高的複雜技術,竟然被包裝成更簡化的流程了(甚至可以像填表一樣,基本資料填一填就好,連程式都不用寫半行)。
CVZone(https://reurl.cc/mvbmRY)就是在這樣一個概念下產生的Python開源套件(package)。它架構在OpenCV和MediaPipe的函式庫(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_CVZone(https://reurl.cc/gzbqlb)這包已裝好CVZone中的Thonny IDE。緊接著,我們將陳老師版的RSP全數加上註解(comments)以供讀者輕鬆地閱讀程式碼。最後,我們再把程式中手部辨識的程式片段獨立成一個函式(function)來管理。
1. RSP拆解:控攝影機 cap
我們以單行註解 # 及 多行註解 ''' ''' 的方式,採用試誤法(trail and error)逐步砍掉暫時用不到的指令來還原OpenCV控制攝影機的程式片段(snippet) CVZoneRSP拆解_控攝影機.py。成功執行的畫面如圖2所示。
圖2:「CVZoneRSP拆解_控攝影機.py」在陳老師版Thonny上的執行結果。
圖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」。
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所示。
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.py (https://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」
舉旗待定.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)辨識技術,電腦能「看到」我們的手之後,可以怎麼玩呢?
RSP揭示的是手掌骨骼(skeleton)辨識技術,電腦能「看到」我們的手之後,可以怎麼玩呢?
- 體感(gestures)控制:投影片(slides)的控制或機器人前進/後退/停止控制、YouTube播放控制…。
- 電腦教九九乘乘表。
- 手語(sign language)辨識。
讀者們可能更想問:能辨識掌紋算命嗎?
手部辨識原理解說
在這篇 https://tinyurl.com/y4zwklz9 文章中講述的十分清楚,利用手骨估計模型(Hand Landmark Model),我們可以取得21個特徵點(feature points)送到機器學習(machine learning)中的深度學習去演算出手指的資訊。
- 六種授權條款。
- 僅以此文向陳老師致上最深的啟蒙敬意。
沒有留言:
張貼留言