# CVZone poseture estimation with Virtual Calculator
### 參考影片連結
> https://www.computervision.zone/lessons/video-lesson-11/
> https://www.youtube.com/watch?v=DZMJ77akgec&t=1558s
### 其他作品連結
[剪刀石頭布辨識](https://colab.research.google.com/drive/1hgEl3K2JZgHNG4MuXh6rllv32sj8z6jT#scrollTo=sUtv0J7ic9IC)
### 互動式計算機
今天來實作利用CVZone poseture estimation做一個互動式的計算機,做出來大概會像是這樣

那麼該如何完成呢? 首先,需要先打開你的Spyder,在環境裡面先下載好以下安裝套件
> pip install opencv-python
> pip install mediapipe
> pip install cvzone
詳細情況可以參考 [這裡](https://hackmd.io/7FeCQbaRRb6RU0gs3GNsEw) 鍾毓驥老師的教學講義
等到環境都設置完成了之後,接下來就是我們程式碼的重頭戲拉!
## 程式碼設計
首先,就是要先打開我們的鏡頭影像
### 設置相機
```python=
import cv2
from cvzone.HandTrackingModule import HandDetector
#Webcam
cap = cv2.VideoCapture(0 , cv2.CAP_DSHOW)
#抓攝影機第一(0)號,cv2.CAP_DSHOW 在windows變得更快
while True:
#Get image from webcam
success, img = cap.read()
#success or not , picture
img = cv2.flip(img, 1) #0->Y,1->X
#Display image
cv2.imshow('image',img)
if cv2.waitKey(1) == ord('q'):
#if input "q" then stop
#ord transfer to num
break
cap.release()#讓其他程式可以使用攝影機
cv2.destroyAllWindows()
```
這邊要注意的就是鏡頭會左右顛倒,可以使用``flip``幫助我們將影像左右交換,1是左右交換,0是上下交換。
>if cv2.waitKey(1) == ord('q'):
不一樣的是,不知道為什麼使用影片教學的方法沒有辦法順利的跑出無限迴圈,所以這邊採用停止迴圈的方法是就是偵測有沒有敲擊鍵盤*Q*判斷是否停止。
還有因為是使用WINDOWS系統的關係,在```cap = cv2.VideoCapture```,後面加上```cv2.CAP_DSHOW```會跑的流暢一點
設置完了相機,接著就是要捕捉我們的手的動作了
### 動作捕捉
```python=
#Webcam
cap = cv2.VideoCapture(0 , cv2.CAP_DSHOW)
#抓攝影機第一(0)號,cv2.CAP_DSHOW 在windows變得更快
cap.set(3,1280) #width
cap.set(4,720) #height 設定螢幕大小 1024*768
detector = HandDetector(detectionCon=0.8,maxHands=1)
#condition more higher and only one hands can enter it
#Loop
while True:
#Get image from webcam
success, img = cap.read()
#success or not , picture
img = cv2.flip(img, 1) #0->Y,1->X
# Detection of hand
hands,img = detector.findHands(img,flipType = False)
#send hands and img back
```
設置螢幕的適當大小,是因為需要增加畫面大小,好讓我們的計算機有空間可以置入。再來就是```HandDetector```是捕捉我們手的關鍵,後面的參數就是信心度要達到80%才會顯示出來,設置高的信心度是因為輸入計算機時若是容易出錯的話也容易讓使用這體驗不佳,一次也只能捕捉一隻手,不可以同時輸入。
>detector.findHands
下方LOOP裡面也增加了讓捕捉到的影像可以在畫面上被顯示出來,後面的```flipType```轉成```False```是為了將左右手的標示相反過來。
### 按鈕定義
```python=
class Button:
def __init__(self,pos,width,height,value): #defend it
self.pos = pos
self.width = width
self.height = height
self.value = value
def draw(self,img):
cv2.rectangle(img,self.pos,
(self.pos[0]+self.width,self.pos[1]+self.height),
(225,225,225),cv2.FILLED) #button
cv2.rectangle(img,self.pos,
(self.pos[0]+self.width,self.pos[1]+self.height),
(50,50,50),3) #border
cv2.putText(img,self.value,
(self.pos[0] + 40 ,self.pos[1]+60),
cv2.FONT_HERSHEY_PLAIN,2,(50,50,50),2) #number
```
>self , pos , width , height , value
因為需要建立16個按鈕,寫成CLASS的方式可以讓程式變得更易讀之外,在未來想更改時也不至於不容易更改。先定義所需的數值,然後將根據這些定義的變數名稱將按鈕繪出。
以*button*來說明的話```cv2.retangle```是繪出長方形,當然,如果長寬一樣的話就是為正方形。繪圖在```img```上,也就是影像。```self.pos```繪圖的位子,不能只輸入```pos```那是會出錯的。
> self.pos[0]+self.width
> self.pos[1]+self.height
再來是分別是寬和高,其實事實上是輸入的是(x2,y2),這也就是為什麼需要在寬和高上面加上自己本身的位子。```(225,225,225)```是顏色,沒有用完全白的顏色是因為像要和輸入時變化的按鈕顏色有所區分,所以才會用類似米白的顏色,最後```cv2.FILLED```就是完全填滿顏色在按鈕裡面。這樣就可以形成我們的按鈕了。
再來就是需要偵測我們是否有輸入按鈕。
### 按鈕設置
終於將畫面用好了,來試著將它呼叫到畫面上
```python=
#Creating Buttons
buttonListValues = [['7','8','9','*'],
['4','5','6','-'],
['1','2','3','+'],
['/','0','.','=']]
buttonList = []
for x in range(4):
for y in range(4):
xpos = x * 100 + 800
ypos = y *100 + 150
buttonList.append(Button((xpos,ypos),100,100,buttonListValues[y][x]))
while True:
#上方輸入欄
cv2.rectangle(img,(800,50),(800 + 400 , 70 + 100),
(255,255,255),cv2.FILLED)
cv2.rectangle(img,(800,50),(800 + 400 , 70 + 100),
(50,50,50),3) #border
#Draw all buttons
for Button in buttonList:
Button.draw(img)
```
需要*4x4*的按鈕方格,使用了迴圈,根據按鈕長寬各100加上一開始的起始位置(800,150),加到```buttonList```裡面,按鈕上的數字則是以```buttonListValues```裡的來做輸入,最後在迴圈裡加上迴圈,繪出按鈕。
### 按鈕偵測定義
接下來就是計算機的重頭戲,要如何計算以及偵測輸入。
```python=
def checkClick(self,x,y):
if self.pos[0] < x < self.pos[0] + self.width
and self.pos[1] < y < self.pos[1] + self.width:
cv2.rectangle(img,self.pos,
self.pos[0]+self.width,self.pos[1]+self.height)
,(255,255,255),cv2.FILLED)
cv2.rectangle(img,self.pos,
self.pos[0]+self.width,self.pos[1]+self.height)
,(50,50,50),3)
cv2.putText(img,self.value,
(self.pos[0] + 25,self.pos[1]+80),
cv2.FONT_HERSHEY_PLAIN,
5,(0,0,0),5)
return True
else:
return False
```
> self.pos[0] < x < self.pos[0] + self.width and self.pos[1] < y < self.pos[1] + self.width
這邊其實跟剛剛按鈕設置一樣,只是改了如果偵測到輸入按鈕時,按鈕需要變的顏色和字體大小,就不再贅述。重點是偵測的條件,輸入時的x,y如果有在按鈕位子的高跟寬裡面的話,就視為有輸入,回傳```True```或是```False```。
### 計算機的計算
連結到上面的偵測,放置到迴圈中
```python=
#Variables
myEquation = ""
#Loop
while True:
#上方輸入欄
cv2.rectangle(img,(800,50),(800 + 400 , 70 + 100),
(255,255,255),cv2.FILLED)
cv2.rectangle(img,(800,50),(800 + 400 , 70 + 100),
(50,50,50),3) #border
hands,img = detector.findHands(img,flipType = False)
#send hands and img back
#Check for Hand
if hands:
lmList = hands[0]['lmList']
length,_,img = detector.findDistance(lmList[8], lmList[12],img)
#print(length)
x,y = lmList[8]
if length < 50:
for i,button in enumerate(buttonList):
if button.checkClick(x, y):
myValue = buttonListValues[int(i%4)][int(i/4)]
if myValue == "=":
myEquation = str(eval(myEquation))
else:
myEquation += myValue
delayCounter = 1
#Display the Equation/Result
cv2.putText(img,myEquation,(810 ,120),cv2.FONT_HERSHEY_PLAIN,
3,(50,50,50),2)
```
> hands,img = detector.findHands(img,flipType = False)
來到了我們的重頭戲,如何輸入我們的按鈕上的數字並做計算呢?除了上方輸入欄的製作之外,最重要的就是我們判別我們手指頭輸入的方式了,如果上面的程式碼都沒有出錯的話,還記得我們請```img```回傳的同時,我們也同時回傳的```hands```的數值,現在正是需要用這個的時候了。
>length,_,img = detector.findDistance(lmList[8], lmList[12],img)
我們偵測的那一隻手,手上*landmark*手指關鍵點,來做手指距離的計算。分別是```lmList[8], lmList[12]```食指和中指的關鍵點。兩點之間的距離```detector.findDistance(lmList[8], lmList[12],img)```回傳```length```。而我們主要的指向性為我們的食指,相當於我們的滑鼠。
我們把偵測距離取到 *50* 以下,如果有的話回傳值
> myValue = buttonListValues[int(i%4)][int(i/4)]
這個為何要有兩個```buttonListValues[][]```空白的值?事實上如果實際操作的話會發現如果後面只有輸入```buttonListValues[i]```的話是會出錯的,原因是```buttonListValues```是二維矩陣,但```buttonList```是一維矩陣,所以將一維矩陣的值回傳給二維矩陣,得到的會是一整排的串列,沒辦只回傳一個值。
ex:如果你在畫面上按 *4* 的話,回傳的值是``` ['4','5','6','-']```
但是我們是需要二維矩陣裡的字串轉為數值,為了修正這個問題,我們將一維串列的數值轉為 *4x4* 的表格,*i* 先取餘數,再將 *i* 除以4取整數。
舉例:將```buttonList[15]```進行轉換,*14* 除以 *4* 結果為 *3* 餘 *2*,那麼得到的就會是```buttonListValues[2][3]```結果出來就會為 *+* 號
> myEquation = ""
> myEquation += myValue
> myEquation = str(eval(myEquation))
解決完上面矩陣的問題,後面計算的問題就顯得簡單多了,再輸入時只要將字串一直疊加上去就行,最後就是偵測是否有輸入```=```如果有的話,將這些字串轉為數值計算,也就是```eval()```這個語法,就能完成簡單的計算機了。
>cv2.putText(img,myEquation,(810 ,120),cv2.FONT_HERSHEY_PLAIN,
3,(50,50,50),2)
也別忘了要加上顯示輸入的數值給使用者回饋,基本上都是大同小異就是將輸入直改成```myEquation```就可以了。
### 計算輸入改善
完成沒多久就會發現有一點點的小bug出現,就是輸入數字的時候會一直重複輸入到好幾次相同的數字,那影片作者也給了兩種不同改善的方法。一個是```import time```然後在每次輸入後都停頓約1~5毫秒左右,是可行但是卻不是很優的方式,作者也提供了另外的方式供大家參考。
```python=
#Variables
myEquation = ""
delayCounter = 0
#Loop
while True:
#上方輸入欄
cv2.rectangle(img,(800,50),(800 + 400 , 70 + 100),
(255,255,255),cv2.FILLED)
cv2.rectangle(img,(800,50),(800 + 400 , 70 + 100),
(50,50,50),3) #border
hands,img = detector.findHands(img,flipType = False)
#send hands and img back
#Check for Hand
if hands:
lmList = hands[0]['lmList']
length,_,img = detector.findDistance(lmList[8], lmList[12],img)
#print(length)
x,y = lmList[8]
if length < 50:
for i,button in enumerate(buttonList):
if button.checkClick(x, y) and delayCounter == 0:
myValue = buttonListValues[int(i%4)][int(i/4)]
if myValue == "=":
myEquation = str(eval(myEquation))
else:
myEquation += myValue
delayCounter = 1
#Display the Equation/Result
cv2.putText(img,myEquation,(810 ,120),cv2.FONT_HERSHEY_PLAIN,
3,(50,50,50),2)
#Avoid Duplicates
if delayCounter != 0 :
delayCounter +=1
if delayCounter >10 :
delayCounter = 0
```
> delayCounter = 0
一開始的```delayCounter```的預設值為0,當你開始輸入的時候,就會變成```delayCounter = 1```
>if button.checkClick(x, y) and delayCounter == 0:
當偵測到不是*1*的時候,下面的判斷式(迴圈)會開始數 *10* 次,當大於*10* 時又會自動變回 *1* ,開始下一次的輸入
這樣的方法可以防止一直在判斷按鈕迴圈裡,減少當機的機率。
### 歸零的方法
就如我一開始所說,按照影片的方法沒有辦法脫逃無限迴圈,所以我用了不一樣的方式來清除我計算機輸入的值。
```python=
#Loop
while True:
#Get image from webcam
success, img = cap.read()
#success or not , picture
img = cv2.flip(img, 1) #0->Y,1->X
# Detection of hand
hands,img = detector.findHands(img,flipType = False)
#send hands and img back
if cv2.waitKey(1) == ord('c'):
myEquation = ''
#clean
if cv2.waitKey(1) == ord('q'):
#每隔一小段時間,如果敲擊q的話,就停止
#ord 轉換數字
break
cap.release()#讓其他程式可以使用攝影機
cv2.destroyAllWindows()
```
>if cv2.waitKey(1) == ord('c'):
偵測鍵盤敲擊```c```的時候,可以將```myEquation```的值清空
如此,終於式大功告成拉!
## 個人的小小心得
花了時間自己看影片學習,雖然一開始是為了報告才開始動的手,但是其實在這之中學到了很多東西,自己也是覺得非常有成就感,寫的過程中越覺得如果能將這個寫的詳細點也許真的能夠幫助到人。反覆看著教學影片,一邊思考作者想要表達的,有時還需要上網查詢才能理解。過程繁瑣但我也卻覺得樂在其中,希望大家也都能在程式裡找到樂趣拉!至少我覺得這個計算機的專案不錯,程式碼也都在上面了,也不妨複製到python上去玩看看,說不定會得到意想不到的感覺喔!