
Day25 四位俠客院子裡過招,
老爺看完比試,決定先請 Pose Landmarker 進府,
陪長輩做暖身、跟著音樂動一動!
讓我們看看:
座標要怎麼變成「舉左手」「拍手」?家人和長輩一起做,App 怎麼分出誰做到了?
看看 Pose 如何陪長輩活動筋骨!
Day11 為了 Face Landmarker,App 自己寫了一個 plugin(face_mesh),
負責開相機、跑模型、把結果傳給 Dart。
這次不另外橋接,而是在同一個 plugin 加上 Pose 模式:
| Face 模式(健口操) | Pose 模式(這次) | |
|---|---|---|
| 相機 | 前鏡頭、640×480 | 同一台、同樣設定 |
| 送進模型的畫面 | 直立、不鏡像 | 一樣 |
| 模型檔 | face_landmarker.task | pose_landmarker_lite.task、pose_landmarker_full.task |
| 每一格傳給 Dart | 嘴唇輪廓、blendshape | 每個人 33 個點 |
lite 約 5.8 MB、full 約 9.4 MB。
兩個都先放進 App,實機比完速度再決定留哪一個(第六節)。
Android 的寫法(iOS 的選項名稱幾乎一樣):
val options = PoseLandmarker.PoseLandmarkerOptions.builder()
.setBaseOptions(
BaseOptions.builder().setModelAssetBuffer(buffer).build()
)
// 相機一格一格送進來,結果從 listener 回來
.setRunningMode(RunningMode.LIVE_STREAM)
// 家人一起入鏡:最多 2 個人
.setNumPoses(2)
.setMinPoseDetectionConfidence(0.5f)
.setMinPosePresenceConfidence(0.5f)
.setMinTrackingConfidence(0.5f)
// 用不到人形遮罩
.setOutputSegmentationMasks(false)
.setResultListener { result, input -> handle(result, input) }
.build()
val poseLandmarker =
PoseLandmarker.createFromOptions(context, options)
小提醒:
官方 Pose Android 指南建立 Task 的程式碼,照貼會編譯不過(3 個錯);
寫法以官方範例 App 為準,上面這段就是照範例 App 寫的。
每個人 33 個點,每個點傳 4 個值,
跟 Day11 的嘴唇輪廓一樣,攤平成一整塊 Float32List:
// 每人 33 點 × 4 個值:x、y、visibility、presence
val landmarks = FloatArray(people.size * 33 * 4)
var offset = 0
people.forEach { person ->
person.forEach { point ->
landmarks[offset++] = point.x()
landmarks[offset++] = point.y()
// 沒有值時填 -1
landmarks[offset++] = point.visibility().orElse(-1f)
landmarks[offset++] = point.presence().orElse(-1f)
}
}
沒有人也要送:送一格「0 個人」,畫面才知道人離開了。
另外附推論時間:送進模型到結果回來的毫秒數,第六節比速度用。
拿到 33 個點之後,App 要回答一件事:在這一個影格,長輩動作有做到嗎?
x 除以畫面的寬、y 除以畫面的高,兩個方向的單位不一樣(Day25 第四節)。
所以先換單位,再來算距離和角度:
y 乘上「高÷寬」,兩個方向都變成「畫面寬」的單位。
長輩坐得近,點就拉開;坐得遠,點就縮在一起。
所以距離一律除以肩寬(11 號到 12 號):
門檻寫成「幾個肩寬」,坐遠坐近都一樣。
每位長輩的活動範圍不同,
有人抬頭抬得高,有人頭本來就微微低著。
所以開始前先請長輩坐好 3 秒,記下這個人坐著時的樣子(基準):
| 基準記什麼 | 用在 |
|---|---|
| 肩寬 | 所有距離的尺 |
| 鼻子在兩耳中點的哪裡 | 抬頭、低頭、轉頭 |
| 肩膀到兩耳的高度差 | 聳肩 |
| 兩邊肩膀的高度差、鼻子的位置 | 身體往左彎、往右彎 |
之後每一格都跟基準比:動了多少,而不是頭在畫面哪裡。
3 秒內動了,就重新計時;
畫面上會告訴長輩缺什麼,例如「看不到肩膀」。
每一格的結果有三種:做到、還沒做到、看不清楚。
點看不清楚,就不硬判斷。
要用的點 visibility 低於 0.5 時,
回「看不清楚」,畫面提示「手要留在畫面裡」。手伸出畫面時,模型會猜一個位置(Day24 第七節),
所以手腕看不清楚時,改看手肘。
點位會小小抖動,剛好在門檻邊緣時,結果會一下做到、一下沒做到。
所以用兩段門檻:
達到門檻,而且連續 0.15 秒,才算做到。退到門檻的 70% 以下,才算放下。左右一律看特徵點的編號,不看它在畫面的哪一邊。

例如「舉左手」看 15 號(左手腕);左右的定義見 Day25 第五節。
| 動作 | 看哪些點 | 怎麼算做到(跟基準比) |
|---|---|---|
| 抬頭、低頭 | 鼻子 0、兩耳 7、8 | 鼻子相對兩耳中點,往上、往下移 0.08 個肩寬 |
| 轉頭看左邊、右邊 | 鼻子 0、兩耳 7、8 | 鼻子往左耳、右耳那一側移(變化 0.2,以兩耳距離為單位) |
| 聳肩 | 兩肩 11、12、兩耳 | 肩膀到兩耳的高度縮短 0.06 個肩寬 |
| 舉左手、舉右手 | 肩 11、12,手腕 15、16 | 手腕高過同側肩膀 0.25 個肩寬 |
| 雙手舉高高 | 同上 | 兩隻手都舉起 |
| 張開雙臂 | 肩、手腕 | 手腕沿著肩膀連線往外,超出肩膀 0.6 個肩寬 |
| 拍手 | 手腕 15、16 | 兩手腕距離大於 0.6、再小於 0.35 個肩寬,算一下 |
| 雙手摸肩膀 | 食指根 19、20,肩 | 離同側肩膀不到 0.45 個肩寬,手腕沒有高過肩膀 0.3 個肩寬 |
| 身體往左彎、往右彎 | 兩肩 11、12,鼻子 0 | 左右肩的高度差變化 0.15 個肩寬,而且鼻子往同一側偏 0.3 個肩寬 |
左右彎為什麼要看鼻子:
手舉過頭時,同一側的肩膀點也會跟著抬高。
只看肩膀的話,A70 實測「舉左手」時,「往右彎」的進度就到了門檻的 1.45 倍。
真的彎腰時,頭會整個往旁邊偏(實測 0.72~0.86 個肩寬);
只舉手時,頭幾乎不動(0.14 個肩寬以內)。
舉左手簡化後是這樣(y 往下變大,所以「高過」是肩膀的 y 減手腕的 y):
final shoulder = pose[11]; // 左肩
final wrist = pose[15]; // 左手腕
// 高過肩膀幾個肩寬
final lift = (shoulder.y - wrist.y) / baseline.shoulderWidth;
// 到 1 就是達到門檻;0.5 時畫面提示「再一點點」
final progress = lift / 0.25;
Day25 第七節的實測:Pose 回傳的人,順序不是照畫面左右排。
所以 App 自己給每個人一個編號:
每個人跟自己比,各自打勾。
衛福部口腔健康司的咀嚼吞嚥障礙健康照護手冊,
健口操在鼓腮、舌頭、發音之前,先做肩膀和頭頸運動:
| 手冊的動作 | 原文 |
|---|---|
| 肩膀運動 | 吸氣上提肩膀 呼氣放鬆肩膀 |
| 頭頸運動 | 緩慢的抬頭→擺正→低頭→擺正 |
| 頭頸運動 | 緩慢的右轉→擺正→左轉→擺正 |
App 的暖身就照這個順序:
聳肩 → 抬頭 → 低頭 → 轉頭看右邊 → 轉頭看左邊
手冊沒有寫要停幾秒。
App 的做法是:頭部動作跟著口令倒數五到一,停住 2 秒就算做到;
聳肩配合吸氣、呼氣做兩次,不倒數。
實測影片:
https://youtube.com/shorts/MBV322nr5tI
健口操的校正畫面是全黑的,只看指示;
暖身的「坐好 3 秒」反過來,要看得到自己,才知道手有沒有在畫面裡。
App 開始前顯示的提醒,每一句都對照官方資料:
| App 的提醒 | 出處 |
|---|---|
| 動作慢慢做,別勉強 | 國健署《全民身體活動指引》:「每一個動作,越慢越好」; |
| 不舒服就先停 | 國健署《長者防跌妙招手冊》:「請先暫停,勿勉強進行。」 |
| 坐在穩固的椅子上 | 國健署《長者防跌妙招手冊》:「座椅應穩固,不能使用有滑輪的椅子。」 |
| 慢慢抬頭,別往後仰太多 | 咀嚼吞嚥障礙健康照護手冊的「緩慢的抬頭」 |
音樂和口令分開:
Lyria 連歌詞一起唱時,中文咬字不夠準;
口令要讓長輩聽清楚,所以改成自己念。
| 曲目 | 動作 |
|---|---|
| 動一動① | 舉左手、舉右手、張開雙臂、拍手、雙手舉高高 |
| 動一動② | 雙手摸肩膀、身體往左彎、身體往右彎、張開雙臂、拍手 |
動一動② 的側彎,照國健署的做法:
「一手高舉過頭向對側彎」,所以是右手舉高、身體往左彎。
App 要知道兩件事:第幾秒播哪一句口令,接下來那段時間要看哪個動作。
所以每段音樂配一個時間軸 JSON 設定檔(不是模型輸出)。
voices 是第幾秒播哪一句口令,cues 是每個動作的判斷時間和算法(節錄):
{
"music": "assets/music/warmup_bgm.m4a",
"voices": [
{"at": 3, "clip": "v03"},
{"at": 8.4, "clip": "v04"}
],
"cues": [
{
"move": "shrug",
"start": 3,
"end": 8.4,
"kind": "once",
"text": "吸氣,肩膀往上提"
}
]
}
| kind | 怎麼算做到 | 用在 |
|---|---|---|
| once | 時間內做到一次 | 舉手、張開雙臂、摸肩膀、側彎 |
| hold | 時間內累計停住 holdSeconds 秒 | 暖身的頭部動作 |
| count | 時間內拍手達 count 下 | 拍手 |
長輩聽到口令、到手舉起來,需要一點時間。
所以判斷的時間是 開始前 0.3 秒 ~ 結束後 1 秒:
沒做到不扣分,畫面也不出現叉叉。
實測影片:
https://youtube.com/shorts/cbiEQVkQzPo
我坐在椅子上,iPhone 和 Galaxy A70 各跑一輪。
測試條件:debug 版,同時開著螢幕錄影和開發用的點位紀錄。
相機每秒拍約 30 個影格,預覽畫面每一格都會顯示;
但模型還在算上一格時,新的影格不會送進模型,骨架和打勾會先停在上一次的結果。
每秒判斷幾格是指「每秒有幾格拿到模型結果」。
Galaxy A70,同一首動一動①,lite、full 各跑一次:
| lite | full | |
|---|---|---|
| 每秒判斷幾格(平均) | 8.7 | 7.5 |
| 推論時間(平均) | 160 ms | 179 ms |
| 9 個動作做到 | 8 個 | 8 個 |
lite 和 full 判斷得一樣,lite 比較快;這次的動作用 lite 就夠。
iPhone(lite):
| 段落 | 每秒判斷幾格(平均) | 推論時間(平均) |
|---|---|---|
| 第 1 段:暖身 | 20.8 | 68 ms |
| 第 2 段:動一動① | 9.3 | 145 ms |
| 第 3 段:動一動② | 9.3 | 144 ms |
iPhone 一開始每秒判斷約有 20 格,連續跑下來降到剩 9 格左右。
第 1 段裡,推論時間從 34 ms 升到 101 ms,後兩段維持在 140 ms 左右。
可能原因:當時手機在充電,也連續跑了十多分鐘;
這次沒有放涼後重測。
| 段落 | iPhone | A70 |
|---|---|---|
| 暖身(6 個) | 6 | 6 |
| 動一動①(9 個) | 7 | 8 |
| 動一動②(11 個) | 11 | 11 |
三段共 26 個動作,App 判定做到的:iPhone 24 個、A70 25 個。
這次實測,每秒判斷 8 格左右,就足夠判斷這些動作。
其他 3 個:
看懂座標之後,長輩的每個動作,App 都能陪著一起數。
明天進入 Day1 提過的第三個目標:介面設計。
請 Google Stitch 幫長輩把畫面變得更好讀、更好按!
長輩看得清楚、點得準的畫面,要怎麼跟 AI 說?Stitch 設計好的畫面,能帶回 Flutter,在手機、平板上都好用嗎?
讓我們明天繼續看下去~
感謝有緣看到這邊的你~
希望佛菩薩也祝福你:🌟平安順遂 安心自在🌟
南無觀世音菩薩🍀 南無地藏菩薩🏠 南無阿彌陀佛☀️