## 實作項目說明
該項目主要是以MediaPipe進行實作,利用了其「身體姿態估測」的功能來判斷伏地挺身動作的正確性。
與參考資料「[AI Fitness Trainer – Build Using MediaPipe For Squat Analysis](https://learnopencv.com/ai-fitness-trainer-using-mediapipe/)」的設計構想類似,該項目也是藉由特定身體部位的角度變化來進行計數。
程式碼分為兩個檔案:
一個為參考「[MediaPipe基础(5)Pose(姿势)](https://blog.csdn.net/weixin_43229348/article/details/120541448?fbclid=IwAR2m_m2-yAg_5J5ly4qqArUJsVL-L07dweQEEOkwf9d4XJGdqHHA5VAclwU)」撰寫的姿勢偵測模組「PoseModule」,其功能主要為「用MediaPipe得出landmark,再以指定的lankmark進行角度計算」。
另一個則為主程式,會用到PoseModule的函數來對伏地挺身進行計數,並把進行狀況及累計次數顯示在視窗中(另有參考「[Real-Time 3D Pose Detection & Pose Classification with Mediapipe and Python](https://bleedaiacademy.com/introduction-to-pose-detection-and-basic-pose-classification/?fbclid=IwAR3H-R0He7fmBSSsJYFa2m38S_U-OpE66Ks2BXK3E1RHh54Jq_cuEZGDlzg)」)。
以下會呈現附有註解的程式碼,並解釋實際操作情況(以gif檔表示)。
## 程式碼
### 1. PoseModule
```python=
import cv2
import mediapipe as mp
import math
class poseDetector() :
def __init__(self, mode=False, complexity=1, smooth_landmarks=True,
enable_segmentation=False, smooth_segmentation=True,
detectionCon=0.5, trackCon=0.5):
self.mode = mode
self.complexity = complexity
self.smooth_landmarks = smooth_landmarks
self.enable_segmentation = enable_segmentation
self.smooth_segmentation = smooth_segmentation
self.detectionCon = detectionCon
self.trackCon = trackCon
self.mpDraw = mp.solutions.drawing_utils
self.mpPose = mp.solutions.pose
self.pose = self.mpPose.Pose(self.mode, self.complexity, self.smooth_landmarks,
self.enable_segmentation, self.smooth_segmentation,
self.detectionCon, self.trackCon)
def findPose (self, img, draw=True):
imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
self.results = self.pose.process(imgRGB)
if self.results.pose_landmarks:
if draw:
self.mpDraw.draw_landmarks(img,self.results.pose_landmarks,
self.mpPose.POSE_CONNECTIONS)
return img
def findPosition(self, img, draw=True):
self.lmList = []
if self.results.pose_landmarks:
for id, lm in enumerate(self.results.pose_landmarks.landmark):
# 確認圖片的長寬
h, w, c = img.shape
# 確認 landmarks 的 pixels
cx, cy = int(lm.x * w), int(lm.y * h)
self.lmList.append([id, cx, cy])
if draw:
cv2.circle(img, (cx, cy), 5, (255,0,0), cv2.FILLED)
return self.lmList
def findAngle(self, img, p1, p2, p3, draw=True):
# 取得 landmarks
x1, y1 = self.lmList[p1][1:]
x2, y2 = self.lmList[p2][1:]
x3, y3 = self.lmList[p3][1:]
# 計算角度
angle = math.degrees(math.atan2(y3-y2, x3-x2) -
math.atan2(y1-y2, x1-x2))
if angle < 0:
angle += 360
if angle > 180:
angle = 360 - angle
elif angle > 180:
angle = 360 - angle
# print(angle)
# 繪製結果
if draw:
cv2.line(img, (x1, y1), (x2, y2), (255,255,255), 3)
cv2.line(img, (x3, y3), (x2, y2), (255,255,255), 3)
cv2.circle(img, (x1, y1), 5, (0,0,255), cv2.FILLED)
cv2.circle(img, (x1, y1), 15, (0,0,255), 2)
cv2.circle(img, (x2, y2), 5, (0,0,255), cv2.FILLED)
cv2.circle(img, (x2, y2), 15, (0,0,255), 2)
cv2.circle(img, (x3, y3), 5, (0,0,255), cv2.FILLED)
cv2.circle(img, (x3, y3), 15, (0,0,255), 2)
cv2.putText(img, str(int(angle)), (x2-50, y2+50),
cv2.FONT_HERSHEY_PLAIN, 2, (0,0,255), 2)
return angle
def main():
detector = poseDetector()
cap = cv2.VideoCapture(0)
while cap.isOpened():
ret, img = cap.read() # ret = return variable
if ret:
img = detector.findPose(img)
cv2.imshow('Pose Detection', img)
if cv2.waitKey(10) & 0xFF == ord('q'): # 退出鍵為 Q
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
```
### 2. 主程式
```python=
import cv2
import mediapipe as mp
import numpy as np
import PoseModule as pm
cap = cv2.VideoCapture(0)
detector = pm.poseDetector()
count = 0
direction = 0
form = 0
feedback = "Fix Form"
while cap.isOpened():
ret, img = cap.read() # 640 x 480
# 確認影片尺寸,用於建立 box
width = cap.get(3) # width 為 float
height = cap.get(4) # height 為 float
# print(width, height)
img = detector.findPose(img, False)
lmList = detector.findPosition(img, False)
# print(lmList)
if len(lmList) != 0:
elbow = detector.findAngle(img, 11, 13, 15) # 手肘角度
shoulder = detector.findAngle(img, 13, 11, 23) # 肩膀角度
hip = detector.findAngle(img, 11, 23,25) # 臀部角度
# 伏地挺身完成度(依據手肘角度計算)
per = np.interp(elbow, (90, 160), (0, 100))
# 伏地挺身進度條
bar = np.interp(elbow, (90, 160), (380, 50))
# 確認姿勢就預備位置(軀幹打直,手肘伸直撐地)
if elbow > 160 and shoulder > 40 and hip > 160:
form = 1
# 檢測伏地挺身進行狀況
if form == 1: # 姿勢就位
if per == 0: # 若手肘角度小於 90 度
if elbow <= 90 and hip > 160: # 且身體打直(忽略肩膀角度)
feedback = "Up" # 提示身體上升(手肘伸直)
if direction == 0:
count += 0.5 # 計數增加 0.5 個伏地挺身
direction = 1
else:
feedback = "Fix Form" # (若身體未打直)提示姿勢需調整
if per == 100: # 若手肘角度大於 160 度(伸直)
if elbow > 160 and shoulder > 40 and hip > 160: # 且身體打直
feedback = "Down" # 提示身體下降(手肘彎曲)
if direction == 1:
count += 0.5 # 計數增加 0.5 個伏地挺身(0.5 + 0.5 = 1)
direction = 0
else:
feedback = "Fix Form" # (若身體未打直)提示姿勢需調整
# form = 0
# print(count)
# 繪製進度條
if form == 1:
cv2.rectangle(img, (580, 50), (600, 380), (0, 255, 0), 3)
cv2.rectangle(img, (580, int(bar)), (600, 380), (0, 255, 0), cv2.FILLED)
cv2.putText(img, f'{int(per)}%', (565, 430), cv2.FONT_HERSHEY_PLAIN, 2,
(255, 0, 0), 2)
# 伏地挺身計數器
cv2.rectangle(img, (0, 380), (100, 480), (0, 255, 0), cv2.FILLED)
cv2.putText(img, str(int(count)), (25, 455), cv2.FONT_HERSHEY_PLAIN, 5,
(255, 0, 0), 5)
# 動作提示(上升、下降、姿勢調整)
cv2.rectangle(img, (500, 0), (640, 40), (255, 255, 255), cv2.FILLED)
cv2.putText(img, feedback, (500, 40 ), cv2.FONT_HERSHEY_PLAIN, 2,
(0, 255, 0), 2)
cv2.imshow('Pushup counter', img)
if cv2.waitKey(10) & 0xFF == ord('q'): # 退出鍵為 Q
break
cap.release()
cv2.destroyAllWindows()
```
## 實際操作情況
### 1. 姿勢正確時

#### 說明
上圖為姿勢正確時的程式運行狀況,而視窗中出現的各項資訊分別為:
1. 左下角的數字 -> 伏地挺身的完成次數
2. 右上角的文字 -> 動作提示
3. 右側長條 -> 動作完成度
4. 肢體上的數字 -> 手肘、肩膀、臀部的彎曲角度
上述的資訊主要由主程式30到62行進行運算(可參考註解),伏地挺身的完成次數會在正確地「Down」和「Up」後增加一次,動作提示則會顯示當前應進行動作,動作完成度分為「Down:100%至0%」和「Up:0%至100%」,這是由手肘角度90度到160度的變化所換算,肢體上的數字則顯示當前各部位的角度。
### 2. 姿勢錯誤(手肘角度未達標)

#### 說明
當手肘彎曲角度未達90度時,右側的完成度長條不會到達0%,右上的提示文字也仍顯示為「Down」,因此完成次數並未增加。
### 3. 姿勢錯誤(臀部角度未達標)

當臀部彎曲角度小於160度時(也就是未打直身體),右上的提示文字會顯示為「Fix Form」,表示姿勢需修正。在修正姿勢前,即便完成度達標,完成次數也不會變化。
### 4. 總結
這次實作的伏地挺身計數器在經過測試後,確實發揮了功用。計數器除了幫助經常健身者紀錄自己的進度,也能替初學者矯正姿勢,以便達到健身的預期功效,可謂一舉兩得。
## 心得
這次的實作也就是上個題目的延伸,MediaPipe的執行也賴於先前的經驗,沒遇到什麼問題,只要把心思放在運用的方式上就好。
是說寫程式時雖有許多參考資料予以協助,不過到了要實際測試時,能依靠的只有自己的肌力。原本看網上的示範影片感覺不難,但由於平時沒在鍛鍊手臂,在試圖做出教科書般的正確姿勢時,吃了不少苦頭。
總結來說,藉由這次實作,我除了對姿勢估測器的應用有深切的認識,也在姿勢估測器的檢測下,習得了伏地挺身的正確動作,頗有收穫。