iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Build on Google AI

Build on Google AI :長者照護 —— 口腔機能訓練 與 延緩認知退化系列 第 26 篇

少年,我看你骨骼精奇。Google MediaPipe Pose、Hand、Gesture、Holistic 你聽過嗎?(下)

  • 分享至 

  • xImage
  •  

一、前言

Day25 四位俠客院子裡過招,
老爺看完比試,決定先請 Pose Landmarker 進府,
陪長輩做暖身、跟著音樂動一動!

讓我們看看:

座標要怎麼變成「舉左手」「拍手」?
家人和長輩一起做,App 怎麼分出誰做到了?

看看 Pose 如何陪長輩活動筋骨!


二、請進府:跟 Face Landmarker 共用相機

(一)同一個 plugin,多一個模式

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 個點
  • 共用相機:App 只開一台前鏡頭,兩個模式輪流用。
  • 不鏡像:
    預覽畫面才鏡像;送進模型的畫面維持原樣,
    Pose 標的左右,就是長輩本人的左右(Day25 第五節)。
  • 不用加新套件:
    App 原本的 MediaPipe(iOS 0.10.21、Android 0.10.26.1)就有 Pose,
    只多兩個模型檔。

lite 約 5.8 MB、full 約 9.4 MB。
兩個都先放進 App,實機比完速度再決定留哪一個(第六節)。

(二)建立 Pose Landmarker

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 寫的。

(三)結果傳給 Dart

每個人 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 秒,記下這個人坐著時的樣子(基準):

基準記什麼 用在
肩寬 所有距離的尺
鼻子在兩耳中點的哪裡 抬頭、低頭、轉頭
肩膀到兩耳的高度差 聳肩
兩邊肩膀的高度差、鼻子的位置 身體往左彎、往右彎

之後每一格都跟基準比:動了多少,而不是頭在畫面哪裡。

3 秒內動了,就重新計時;
畫面上會告訴長輩缺什麼,例如「看不到肩膀」。

(四)看不清楚,就先不判斷

每一格的結果有三種:做到、還沒做到、看不清楚。

點看不清楚,就不硬判斷。

要用的點 visibility 低於 0.5 時,
回「看不清楚」,畫面提示「手要留在畫面裡」。

手伸出畫面時,模型會猜一個位置(Day24 第七節),
所以手腕看不清楚時,改看手肘。

(五)防抖動:兩段門檻

點位會小小抖動,剛好在門檻邊緣時,結果會一下做到、一下沒做到。

所以用兩段門檻:

  • 進入:達到門檻,而且連續 0.15 秒,才算做到。
  • 離開:退到門檻的 70% 以下,才算放下。

(六)13 個動作的規則

左右一律看特徵點的編號,不看它在畫面的哪一邊。

例如「舉左手」看 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 個肩寬以內)。

  • 耳朵看不到時:頭髮蓋住耳朵,就改用兩眼的外眼角(3、6 號)。
  • 轉頭用比例:
    鼻子到兩耳中點的位移,除以兩耳的距離;
    轉頭時兩耳的距離也會變,用比例比較穩。

舉左手簡化後是這樣(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 自己給每個人一個編號:

  • 照螢幕左右:用長輩看到的鏡像畫面排,左邊是 0 號、右邊是 1 號。
  • 跨格不換號:下一格找離上一格位置最近的人,沿用同一個編號。
  • 暫時不見:2.5 秒內回來,還是同一個編號,基準也保留;
    位置跳開但肩寬差不多,也接回原本的人,不用重新坐好。
  • 同一個人被回報成兩個:Pose 偶爾會這樣(實機遇過),
    兩個位置幾乎重疊時只留一個。
  • 各自的基準:家人中途才入鏡,自己坐好 3 秒後才開始判斷。

每個人跟自己比,各自打勾。


四、暖身動作

(一)先肩膀,再頭頸

衛福部口腔健康司的咀嚼吞嚥障礙健康照護手冊,
健口操在鼓腮、舌頭、發音之前,先做肩膀和頭頸運動:

手冊的動作 原文
肩膀運動 吸氣上提肩膀 呼氣放鬆肩膀
頭頸運動 緩慢的抬頭→擺正→低頭→擺正
頭頸運動 緩慢的右轉→擺正→左轉→擺正

App 的暖身就照這個順序:

聳肩 → 抬頭 → 低頭 → 轉頭看右邊 → 轉頭看左邊

手冊沒有寫要停幾秒。
App 的做法是:頭部動作跟著口令倒數五到一,停住 2 秒就算做到;
聳肩配合吸氣、呼氣做兩次,不倒數。

(二)App 怎麼做

實測影片:
https://youtube.com/shorts/MBV322nr5tI

暖身動作實測影片

  • 不擋臉:骨架只畫肩膀和手臂,頭頂的標籤放在頭的上方或旁邊。
  • 骨架線可以關:首頁右上角的齒輪裡,有「顯示骨架線」的開關。

健口操的校正畫面是全黑的,只看指示;
暖身的「坐好 3 秒」反過來,要看得到自己,才知道手有沒有在畫面裡。

(三)安全提醒

App 開始前顯示的提醒,每一句都對照官方資料:

App 的提醒 出處
動作慢慢做,別勉強 國健署《全民身體活動指引》:「每一個動作,越慢越好」;
不舒服就先停 國健署《長者防跌妙招手冊》:「請先暫停,勿勉強進行。」
坐在穩固的椅子上 國健署《長者防跌妙招手冊》:「座椅應穩固,不能使用有滑輪的椅子。」
慢慢抬頭,別往後仰太多 咀嚼吞嚥障礙健康照護手冊的「緩慢的抬頭」

五、跟著音樂動一動

音樂和口令分開:

  • 背景音樂:用 AI Studio 的 Lyria 3.5 生成純音樂(Day19),
    循環播放。
  • 口令:我自己念的,一句一個檔案,App 照時間軸播;
    口令播放時,背景音樂自動調小聲。

Lyria 連歌詞一起唱時,中文咬字不夠準;
口令要讓長輩聽清楚,所以改成自己念。

(一)兩段音樂、10 個動作

曲目 動作
動一動① 舉左手、舉右手、張開雙臂、拍手、雙手舉高高
動一動② 雙手摸肩膀、身體往左彎、身體往右彎、張開雙臂、拍手

動一動② 的側彎,照國健署的做法:
「一手高舉過頭向對側彎」,所以是右手舉高、身體往左彎。

(二)時間軸:第幾秒說什麼、做什麼

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 下 拍手
  • text:畫面上顯示的字;沒寫就用動作的名稱。
  • 口令:另外列出第幾秒播哪一句。
  • 時間以 App 自己的時鐘為準:背景音樂循環播放,長短都不影響判斷。
  • 秒數不用手算:
    用一個小程式,依每句口令的長度,
    加上留給長輩做動作的時間,排出整份時間軸;每句後面多留 0.3 秒。
  • 口令不互相打斷:
    手機卡一下時,上一句可能晚開始;
    下一句會等上一句念完再播。

(三)判斷時間放寬一點

長輩聽到口令、到手舉起來,需要一點時間。
所以判斷的時間是 開始前 0.3 秒 ~ 結束後 1 秒:

  • 提前 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 個:

  • A70 動一動①:第二次「張開雙臂」有判斷,但只到門檻七成左右,判定沒做到。
  • iPhone 動一動①:開頭畫面停頓了幾次,App 把我當成新的人、要重新坐好,
    前兩個舉手沒有判斷到;所以改成「肩寬差不多就接回原本的人」(第三節)。

七、小結

  • 請進府:
    同一個 plugin 加 Pose 模式,共用前鏡頭;
    送進模型不鏡像,左右就是本人的左右。
  • 座標變成動作:
    換成同一個單位、肩寬當尺、坐好 3 秒跟自己比;
    看不清楚就不判斷,兩段門檻防抖動。
  • 暖身動作:
    照衛福部咀嚼吞嚥障礙健康照護手冊,先肩膀、再頭頸;
    安全提醒都對照官方資料。
  • 跟著音樂動一動:
    時間軸決定第幾秒做什麼;兩個人照螢幕左右編號,各自打勾。
  • 實機:
    A70 每秒判斷約 8 格、iPhone 9~21 格,舉手、彎腰、拍手都判斷得到;
    左右彎要同時看肩膀和頭,才不會把舉手當成彎腰。

看懂座標之後,長輩的每個動作,App 都能陪著一起數。


八、預告

明天進入 Day1 提過的第三個目標:介面設計。
請 Google Stitch 幫長輩把畫面變得更好讀、更好按!

長輩看得清楚、點得準的畫面,要怎麼跟 AI 說?
Stitch 設計好的畫面,能帶回 Flutter,在手機、平板上都好用嗎?

讓我們明天繼續看下去~

感謝有緣看到這邊的你~
希望佛菩薩也祝福你:🌟平安順遂 安心自在🌟
南無觀世音菩薩🍀 南無地藏菩薩🏠 南無阿彌陀佛☀️


上一篇
少年,我看你骨骼精奇。Google MediaPipe Pose、Hand、Gesture、Holistic 你聽過嗎?(中)
下一篇
Google Stitch:妙筆俠客登場(上)
系列文
Build on Google AI :長者照護 —— 口腔機能訓練 與 延緩認知退化 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言