2023年1月14日 星期六

[AI, Python, 深蹲, squat] 用變數傾印法玩 Python 蹲

用變數傾印法玩 Python 蹲

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

Jan. 14, 2023
[1]

本文將一門深入陳會安老師在 https://reurl.cc/mZ1mD1 分享的《看圖學Python人工智慧程式設計》(https://reurl.cc/rZW79r)一書中的範例。此範例使用了 Google  MediaPipehttps://reurl.cc/nZg8mX)跨平台機器學習解決方案,讓我們得以使用短短的 4x 行 Python 程式碼就能實現深蹲(squat)姿勢的判別並能計次。

會安老師說:「只要懂得程式語言三大結構(循序、選擇與重複),再了解一點 Python 程式語言的語法,就能開始玩 AI了!讀者們,Python 蹲、Python 蹲,Python 蹲完換你們蹲了喔!


開發環境建置

本文使用會安老師開發的 fChart 流程圖轉語法中的 Thonny IDE [2],它已裝妥了 CVZone 與 Tinker 套件。

下載 英文版「fChartThonny6_cvzone_en.zip可由 https://reurl.cc/gQ59Vb 下載。此外,深蹲範例程式的原始碼「Squat.zip」可由 https://reurl.cc/x1qzE5 下載。
解壓縮 將上述的兩支 .zip 檔解壓縮至磁機的」目錄(root directory)下(例如:c:\),如圖 1 所示。

 

圖 1:將「fChartThonny6_cvzone_en.zip」和「Squat.zip」解壓縮至 c:\

Thonny 雙撃「C:\fChartThonny6_cvzone_en\WinPython」路徑下的「thonny.vbs」以開啟 Thonny(圖 2)。

圖 2:開啟 Thonny IDE

Squat.py 在視窗左手邊的 Files 檔案管理中切換到「C:\Squat」後再雙撃「Squat.py」(圖 3)以載入原始碼(source code)

圖 3:在 Thonny 中開啟 Squat.py

程式碼追蹤

Squat.py 援用了「PoseModule.py」中的「PoseDetector」類別(class),並載入「squat3_clip.mp4」預錄的影像檔做為測試資料。 在 4x 行的程式碼中,最核心(core)的指令為第 17 行定義於的 PoseDetector 類別的 findAngle() 方法(method)!讀者們可先學習呼叫它的方法和需傳入的參數列(parameter list)。有需要再鑽進該類別去深究它是如何從影像資料中計算出深蹲的夾角的。
本小節將使用「變數傾印(dump)」暴力法(紮實的蹲馬步)將範例程式中插入「print(變數名)」,將用到變數一一印出來觀察其內容,藉以了解程式的設計邏輯。我們以
L行號 變數名稱
方式來標示查看程式碼的第幾行變數值。

L10 img 在第 10 和第 11 行中插入 print(img) 指令,我們可以看到變數 img 的第一筆內容如下:

[[[  0  44  37]
  [  2  50  43]
  [  7  63  54]
  ...
  [ 20  48  78]
  [ 19  47  77]
  [ 18  46  76]]

 [[  0  44  37]
  [  2  50  43]
  [  7  63  54]
  ...
  [ 18  46  76]
  [ 17  45  75]
  [ 16  44  74]]

 [[  0  45  37]
  [  0  51  43]
  [  4  62  53]
  ...
  [ 15  43  73]
  [ 12  40  70]
  [ 11  39  69]]

 ...

 [[121 115 110]
  [123 117 112]
  [127 121 116]
  ...
  [ 19  40  71]
  [ 22  43  74]
  [ 23  44  75]]

 [[144 138 133]
  [146 140 135]
  [152 146 141]
  ...
  [ 23  44  75]
  [ 23  44  75]
  [ 23  44  75]]

 [[153 147 142]
  [152 146 141]
  [152 146 141]
  ...
  [ 23  44  75]
  [ 21  42  73]
  [ 19  40  71]]]

圖 4:變數傾印法

L12 img 
[[[  1  47  40]
  [ 10  66  57]
  [ 12  70  61]
  ...
  [ 16  44  74]
  [ 20  48  78]
  [ 18  46  76]]

 [[  0  48  40]
  [  8  66  57]
  [  1  56  47]
  ...
  [ 16  44  74]
  [ 16  44  74]
  [ 13  41  71]]

 [[  1  50  42]
  [  8  66  57]
  [  5  61  52]
  ...
  [ 15  43  73]
  [ 13  41  71]
  [  9  37  67]]

 ...

 [[151 145 140]
  [143 137 132]
  [133 127 122]
  ...
  [ 15  36  67]
  [ 12  33  64]
  [ 15  36  67]]

 [[128 122 117]
  [135 129 124]
  [145 139 134]
  ...
  [ 17  38  69]
  [ 19  40  71]
  [ 23  44  75]]

 [[151 145 140]
  [153 147 142]
  [156 150 145]
  ...
  [ 22  43  74]
  [ 23  44  75]
  [ 21  42  73]]]

L13 h, w, c 「480 640 3」 color channel, 彩色圖片是3

L14 detector.findPose(img, draw=True) 
({'lmList': [[331, 126], [334, 121], [336, 121], [338, 121], [328, 121], [326, 121], [324, 121], [339, 124], [321, 124], [335, 133], [328, 132], [355, 164], [306, 163], [357, 213], [302, 210], [371, 198], [289, 200], [376, 194], [283, 193], [375, 190], [281, 190], [372, 191], [284, 192], [344, 253], [315, 254], [358, 328], [301, 332], [368, 399], [291, 400], [365, 407], [294, 410], [374, 422], [283, 423]], 'bbox': (282, 97, 97, 337), 'center': (330, 265)}, array([[[ 1, 47, 40],
[ 10, 66, 57],
[ 12, 70, 61],
...,
[ 16, 44, 74],
[ 20, 48, 78],
[ 18, 46, 76]],
[[ 0, 48, 40],
[ 8, 66, 57],
[ 1, 56, 47],
...,
[ 16, 44, 74],
[ 16, 44, 74],
[ 13, 41, 71]],
[[ 1, 50, 42],
[ 8, 66, 57],
[ 5, 61, 52],
...,
[ 15, 43, 73],
[ 13, 41, 71],
[ 9, 37, 67]],
...,
[[151, 145, 140],
[143, 137, 132],
[133, 127, 122],
...,
[ 15, 36, 67],
[ 12, 33, 64],
[ 15, 36, 67]],
[[128, 122, 117],
[135, 129, 124],
[145, 139, 134],
...,
[ 17, 38, 69],
[ 19, 40, 71],
[ 23, 44, 75]],
[[151, 145, 140],
[153, 147, 142],
[156, 150, 145],
...,
[ 22, 43, 74],
[ 23, 44, 75],
[ 21, 42, 73]]], dtype=uint8))

L14 pose 
{'lmList': [[331, 126], [334, 121], [336, 121], [338, 121], [328, 121], [326, 121], [324, 121], [339, 124], [322, 124], [335, 133], [328, 132], [355, 163], [306, 163], [357, 215], [299, 214], [371, 199], [289, 199], [376, 194], [283, 191], [376, 191], [281, 188], [374, 192], [283, 189], [344, 254], [316, 255], [358, 328], [301, 332], [368, 400], [291, 400], [365, 409], [294, 410], [374, 422], [283, 423]], 'bbox': (282, 97, 97, 337), 'center': (330, 265)}

L14 img 
[[[ 1 47 40]
[ 10 66 57]
[ 12 70 61]
...
[ 16 44 74]
[ 20 48 78]
[ 18 46 76]]
[[ 0 48 40]
[ 8 66 57]
[ 1 56 47]
...
[ 16 44 74]
[ 16 44 74]
[ 13 41 71]]
[[ 1 50 42]
[ 8 66 57]
[ 5 61 52]
...
[ 15 43 73]
[ 13 41 71]
[ 9 37 67]]
...
[[151 145 140]
[143 137 132]
[133 127 122]
...
[ 15 36 67]
[ 12 33 64]
[ 15 36 67]]
[[128 122 117]
[135 129 124]
[145 139 134]
...
[ 17 38 69]
[ 19 40 71]
[ 23 44 75]]
[[151 145 140]
[153 147 142]
[156 150 145]
...
[ 22 43 74]
[ 23 44 75]
[ 21 42 73]]]

L16 lmList 
[[331, 126], [334, 121], [336, 121], [338, 121], [328, 121], [326, 121], [324, 121], [339, 124], [322, 124], [335, 133], [328, 132], [355, 163], [306, 163], [357, 215], [299, 214], [371, 199], [289, 199], [376, 194], [283, 191], [376, 191], [281, 188], [374, 192], [283, 189], [344, 254], [316, 255], [358, 328], [301, 332], [368, 400], [291, 400], [365, 409], [294, 410], [374, 422], [283, 423]]

L17 detector.findAngle(lmList[24], lmList[26], lmList[28], img) 
[ 15, 36, 67],
[ 12, 33, 64],
[ 15, 36, 67]],
[[128, 121, 118],
[135, 128, 125],
[145, 138, 135],
...,
[ 17, 38, 69],
[ 19, 40, 71],
[ 23, 44, 75]],
[[151, 144, 141],
[153, 146, 143],
[156, 149, 146],
...,
[ 22, 43, 74],
[ 23, 44, 75],
[ 21, 42, 73]]], dtype=uint8))
(178.20385684750428, array([[[ 1, 47, 40],
[ 10, 66, 57],
[ 12, 70, 61],
...,
[ 16, 44, 74],
[ 20, 48, 78],
[ 18, 46, 76]],
[[ 0, 48, 40],
[ 8, 66, 57],
[ 1, 56, 47],
...,
[ 16, 44, 74],
[ 16, 44, 74],
[ 13, 41, 71]],
[[ 1, 50, 42],
[ 8, 66, 57],
[ 5, 61, 52],
...,
[ 15, 43, 73],
[ 13, 41, 71],
[ 9, 37, 67]],
...,
[[151, 144, 141],
[143, 136, 133],
[133, 126, 123],
...,
[ 15, 36, 67],
[ 12, 33, 64],
[ 15, 36, 67]],
[[128, 121, 118],
[135, 128, 125],
[145, 138, 135],
...,
[ 17, 38, 69],
[ 19, 40, 71],
[ 23, 44, 75]],
[[151, 144, 141],
[153, 146, 143],
[156, 149, 146],
...,
[ 22, 43, 74],
[ 23, 44, 75],
[ 21, 42, 73]]], dtype=uint8))

L17 angle 177.34243016028947

L17 img 
  [ 15  36  67]
  [ 12  33  64]
  [ 15  36  67]]

 [[128 122 117]
  [135 129 124]
  [145 139 134]
  ...
  [ 17  38  69]
  [ 19  40  71]
  [ 23  44  75]]

 [[151 145 140]
  [153 147 142]
  [156 150 145]
  ...
  [ 22  43  74]
  [ 23  44  75]
  [ 21  42  73]]]
[[[  1  47  40]
  [ 10  66  57]
  [ 12  70  61]
  ...
  [ 16  44  74]
  [ 20  48  78]
  [ 18  46  76]]

 [[  0  48  40]
  [  8  66  57]
  [  1  56  47]
  ...
  [ 16  44  74]
  [ 16  44  74]
  [ 13  41  71]]

 [[  1  50  42]
  [  8  66  57]
  [  5  61  52]
  ...
  [ 15  43  73]
  [ 13  41  71]
  [  9  37  67]]

 ...

 [[151 144 141]
  [143 136 133]
  [133 126 123]
  ...
  [ 15  36  67]
  [ 12  33  64]
  [ 15  36  67]]

 [[128 121 118]
  [135 128 125]
  [145 138 135]
  ...
  [ 17  38  69]
  [ 19  40  71]
  [ 23  44  75]]

 [[151 144 141]
  [153 146 143]
  [156 149 146]
  ...
  [ 22  43  74]
  [ 23  44  75]
  [ 21  42  73]]]

原理探討

從上小節一步一腳印地以土法煉綱的方式將程式中的每一個存放資料的變數印在 Thonny 的「互動環境(Shell)」中一一檢視。我們試圖以達文西的解剖術,一刀刀的拆解出基元後再拼湊出藏在程式碼背後的密碼──流程圖(flowchart)

本範例以右大腿和小腿間因直立/深蹲所造成的夾角(angle)變化來判斷動作的完成與否。角度計算函式 findAngle() (定義於 PoseModule.py 中)以「髖骨─膝蓋─腳踝」三點的座標計算之(圖 5):

蹲下, £ 105 
直立, ³ 170


 5深蹲動作的角度值範例判斷

另一方面,深蹲的判別及其次數計算方式為(圖 6):

dir = 0,站直,count <- count + 0.5(蹲半下)
dir = 1,蹲下,count <- count + 0.5(蹲半下)
其中,「<-」表示指定(assign),意即將符號右邊的值指定給左邊(將計次變數 count 加上蹲半下 0.5 後指定到 count 之中)。

圖 6:深蹲動作的判別及計次

因此,我們可以整理出程式的執行流程如圖 7 所示。

圖 7:本文的流程圖

進階問題

  1. 將「夾角」的偵測擴充到四足,讓程式可以判斷出四手腳的動作。
  2. 找一個運動的肢體變化過程,例如國標舞舞步,將深蹲改寫為「國標舞 AI 教練」。
  3. 請模仿我們先前拙著《AI 成績查詢系統》(https://reurl.cc/YdzLAX》一文,使用 fChart 內建的 Tinker 視窗設計工具包(GUI toolkit),替本文訂製一個使用者界面。
  4. 參考拙著《Python玩AI,你也可以 – 從CVZone入門吧!》(https://reurl.cc/AyZGkE),將測試資料置換成由攝影機餵入的即時(real-time)影像資料。

[2] https://reurl.cc/gQ59Vb 中的「fChart_CVZone工具」連結