估計(jì)驅(qū)動(dòng)運(yùn)動(dòng)計(jì)數(shù):從關(guān)鍵點(diǎn)角度到狀態(tài)機(jī)實(shí)現(xiàn))
說(shuō)到運(yùn)動(dòng)計(jì)數(shù)APP開(kāi)發(fā)上一篇我們把攝像頭預(yù)覽、媒體管線(xiàn)和基礎(chǔ)工程走通了框架搭起來(lái)之后其實(shí)真正決定項(xiàng)目能不能落地的是后面這兩塊硬骨頭一塊是姿態(tài)估計(jì)模型給出的原始關(guān)鍵點(diǎn)數(shù)據(jù)怎么變成有意義的動(dòng)作信息另一塊是計(jì)數(shù)邏輯在各種真實(shí)動(dòng)作下怎么做到不誤計(jì)、不漏計(jì)、不重計(jì)。這篇就把這兩個(gè)核心環(huán)節(jié)完整拆開(kāi)從坐標(biāo)數(shù)據(jù)講到角度計(jì)算從狀態(tài)機(jī)講到閾值調(diào)參全是實(shí)測(cè)跑的流程。這篇文章適合兩類(lèi)朋友一是已經(jīng)在用姿態(tài)估計(jì)做點(diǎn)東西但卡在“關(guān)鍵點(diǎn)拿到了卻不會(huì)用”的階段二是想做個(gè)健身計(jì)數(shù)類(lèi)APP正在糾結(jié)計(jì)數(shù)邏輯怎么實(shí)現(xiàn)。不管你是剛接觸姿態(tài)估計(jì)還是已經(jīng)跑通了Demo這篇文章都能讓你少走不少?gòu)澛贰?. 姿態(tài)估計(jì)選型與數(shù)據(jù)基礎(chǔ)1.1 模型怎么選從實(shí)時(shí)性和精度看移動(dòng)端方案做運(yùn)動(dòng)計(jì)數(shù)APP模型選型基本就決定了你后續(xù)所有工作的難度。姿態(tài)估計(jì)方案常見(jiàn)的無(wú)非這么幾個(gè)方向一種是傳統(tǒng)的多人姿態(tài)估計(jì)模型精度不錯(cuò)但模型體積大、推理速度慢在手機(jī)CPU上跑尤其是實(shí)時(shí)處理視頻流時(shí)幀率低得沒(méi)法看另一種是輕量級(jí)方案專(zhuān)門(mén)為移動(dòng)端和嵌入式場(chǎng)景設(shè)計(jì)模型體積小、推理速度快精度雖然比不上大模型但對(duì)動(dòng)作計(jì)數(shù)這種場(chǎng)景來(lái)說(shuō)已經(jīng)完全夠用。我最后選的是輕量級(jí)方案核心決策因素有三個(gè)第一實(shí)時(shí)性。運(yùn)動(dòng)計(jì)數(shù)APP要處理的是連續(xù)視頻幀模型推理速度直接決定計(jì)數(shù)響應(yīng)是跟手還是滯后。實(shí)測(cè)中輕量級(jí)模型在主流手機(jī)上單幀推理能控制在10到30毫秒而重量級(jí)模型隨便一跑就是上百毫秒體驗(yàn)差距天壤之別。第二跨平臺(tái)支持。項(xiàng)目如果用跨平臺(tái)框架開(kāi)發(fā)模型方案最好能同時(shí)覆蓋雙端省去在多個(gè)平臺(tái)各做一套模型集成的成本。第三關(guān)鍵點(diǎn)輸出格式。運(yùn)動(dòng)計(jì)數(shù)需要穩(wěn)定的關(guān)節(jié)點(diǎn)坐標(biāo)目標(biāo)模型直接輸出33個(gè)關(guān)鍵點(diǎn)的歸一化坐標(biāo)、可見(jiàn)性和深度信息這就夠了不依賴(lài)額外的目標(biāo)檢測(cè)網(wǎng)絡(luò)。對(duì)比一下幾個(gè)方案的差異更直觀(guān)方案模型體積移動(dòng)端推理速度關(guān)鍵點(diǎn)數(shù)量適合場(chǎng)景輕量級(jí)方案A3-5MB10-30ms33個(gè)實(shí)時(shí)移動(dòng)端應(yīng)用中量級(jí)方案B10-20MB50-100ms17-25個(gè)離線(xiàn)分析、PC端重量級(jí)方案C200MB500ms以上多人體多個(gè)點(diǎn)位實(shí)驗(yàn)室、服務(wù)器所以選型的結(jié)論很明確移動(dòng)端做實(shí)時(shí)計(jì)數(shù)優(yōu)先考慮輕量級(jí)方案把模型精度放在第二位把實(shí)時(shí)性和集成便利性放在第一位。1.2 33個(gè)關(guān)鍵點(diǎn)怎么讀別把坐標(biāo)搞反了拿到模型之后第一步就是把關(guān)鍵點(diǎn)的數(shù)據(jù)結(jié)構(gòu)吃透。以輕量級(jí)模型輸出的33個(gè)關(guān)鍵點(diǎn)為例每個(gè)關(guān)鍵點(diǎn)包含三個(gè)核心字段x歸一化坐標(biāo)范圍0到1表示關(guān)鍵點(diǎn)相對(duì)輸入圖像寬度的橫向位置y歸一化坐標(biāo)范圍0到1表示關(guān)鍵點(diǎn)相對(duì)輸入圖像高度的縱向位置z深度信息表示關(guān)鍵點(diǎn)相對(duì)人體中心的深度數(shù)值越大通常表示離鏡頭越遠(yuǎn)其中x和y是歸一化坐標(biāo)這個(gè)很好理解但我在實(shí)際開(kāi)發(fā)中遇到不少朋友在這里踩坑拿到坐標(biāo)后直接當(dāng)成像素坐標(biāo)用了結(jié)果畫(huà)疊加層的時(shí)候點(diǎn)位全部偏移。這里的關(guān)鍵是如果你想在畫(huà)面上的正確位置畫(huà)點(diǎn)畫(huà)線(xiàn)必須先把歸一化坐標(biāo)乘上畫(huà)面的實(shí)際寬高double pixelX landmark.x * imageWidth; double pixelY landmark.y * imageHeight;另外還有一個(gè)很重要的點(diǎn)關(guān)鍵點(diǎn)的索引編號(hào)是固定的這是后續(xù)角度計(jì)算的基石。運(yùn)動(dòng)計(jì)數(shù)常用的關(guān)鍵點(diǎn)索引對(duì)應(yīng)關(guān)系大致如下索引部位運(yùn)動(dòng)計(jì)數(shù)用途11 / 12左肩 / 右肩俯臥撐角度計(jì)算13 / 14左肘 / 右肘俯臥撐、彎舉角度計(jì)算15 / 16左腕 / 右腕俯臥撐末端判定23 / 24左髖 / 右髖深蹲角度計(jì)算起點(diǎn)25 / 26左膝 / 右膝深蹲、箭步蹲角度計(jì)算中樞27 / 28左踝 / 右踝深蹲角度計(jì)算終點(diǎn)這個(gè)索引表建議你記在便利貼上因?yàn)楹罄m(xù)所有角度計(jì)算都要反復(fù)用到。如果記串了角度算出來(lái)就是錯(cuò)的但看數(shù)據(jù)又很難立刻發(fā)現(xiàn)排查起來(lái)特別痛苦。2. 關(guān)節(jié)角度計(jì)算與動(dòng)作特征提取2.1 三點(diǎn)法求關(guān)節(jié)角度點(diǎn)積公式與NaN防護(hù)拿到關(guān)鍵點(diǎn)坐標(biāo)之后最核心的任務(wù)就是把“坐標(biāo)系里的點(diǎn)”變成“有物理意義的關(guān)節(jié)角度”。這里用一個(gè)非常經(jīng)典的三點(diǎn)法以關(guān)節(jié)為頂點(diǎn)取相鄰兩個(gè)關(guān)鍵點(diǎn)構(gòu)成兩條向量求夾角的余弦再轉(zhuǎn)換成角度值。比如計(jì)算膝關(guān)節(jié)角度取髖點(diǎn)索引23/24、膝點(diǎn)索引25/26、踝點(diǎn)索引27/28以膝點(diǎn)為頂點(diǎn)分別判斷兩側(cè)叉開(kāi)的角度。數(shù)學(xué)公式是這樣的cos(θ) (BA · BC) / (|BA| * |BC|) θ arccos(cos(θ))翻譯成代碼就是O(1)的向量運(yùn)算import dart:math; double calculateAngle(Offset a, Offset b, Offset c) { // a、b、c分別是三個(gè)關(guān)鍵點(diǎn)的坐標(biāo) // 頂點(diǎn)是ba和c是兩側(cè)點(diǎn) double baX a.dx - b.dx; double baY a.dy - b.dy; double bcX c.dx - b.dx; double bcY c.dy - b.dy; double dotProduct baX * bcX baY * bcY; double magBA sqrt(baX * baX baY * baY); double magBC sqrt(bcX * bcX bcY * bcY); if (magBA 1e-6 || magBC 1e-6) { return _lastAngle; // 關(guān)鍵點(diǎn)重合時(shí)返回上一幀角度 } double cosTheta dotProduct / (magBA * magBC); cosTheta cosTheta.clamp(-1.0, 1.0).toDouble(); double angleInDegrees acos(cosTheta) * 180 / pi; _lastAngle angleInDegrees; return angleInDegrees; }這里有兩個(gè)特別容易被忽略的坑第一個(gè)是向量模長(zhǎng)可能為零。當(dāng)兩個(gè)關(guān)鍵點(diǎn)完全重合比如手遮擋在肘關(guān)節(jié)前面模型把兩個(gè)點(diǎn)估計(jì)到同一位置時(shí)除法會(huì)導(dǎo)致數(shù)值異常。必須加一個(gè)最小模長(zhǎng)判斷否則你的角度值會(huì)出現(xiàn)抖動(dòng)的怪異數(shù)據(jù)計(jì)數(shù)邏輯直接亂掉。第二個(gè)是acos的入?yún)⒎秶8↑c(diǎn)計(jì)算下dotProduct / (magBA * magBC)理論上應(yīng)該在-1到1之間但由于浮點(diǎn)誤差可能算出1.0000001之類(lèi)的值導(dǎo)致acos返回NaN。加上clamp(-1.0, 1.0)這行代碼能一次性把這個(gè)坑堵死。2.2 用角度曲線(xiàn)刻畫(huà)動(dòng)作深蹲和俯臥撐的判定特征關(guān)節(jié)角度是一種連續(xù)變化的曲線(xiàn)動(dòng)作本質(zhì)上就體現(xiàn)為特定關(guān)節(jié)角度的“下降-保持-回升”過(guò)程。以深蹲為例我們關(guān)注的是膝關(guān)節(jié)角度站立時(shí)膝蓋基本伸直角度在170度左右下蹲到最低點(diǎn)時(shí)膝關(guān)節(jié)角度降到90度以下然后再回升到170度左右。一次完整的深蹲角度曲線(xiàn)就是一個(gè)清晰的V字形。俯臥撐則稍有不同關(guān)注的是肘關(guān)節(jié)角度手臂伸直撐起時(shí)為180度左右身體下沉到最低點(diǎn)時(shí)肘關(guān)節(jié)角度降到60度以下?lián)纹饡r(shí)再回到180度。同樣是一個(gè)U字形曲線(xiàn)。這里有個(gè)很重要的實(shí)操經(jīng)驗(yàn)深蹲和俯臥撐的計(jì)數(shù)采集方向建議讓用戶(hù)側(cè)對(duì)鏡頭。正對(duì)鏡頭時(shí)關(guān)節(jié)的深度信息不可靠而且人體容易遮擋關(guān)鍵點(diǎn)角度計(jì)算的誤差會(huì)明顯增大側(cè)對(duì)鏡頭時(shí)關(guān)鍵點(diǎn)的二維投影能比較好地反映真實(shí)的關(guān)節(jié)彎曲狀態(tài)。另外原始角度數(shù)據(jù)往往非常抖動(dòng)主要是因?yàn)槟P洼敵霰旧泶嬖谠肼暭由嫌脩?hù)動(dòng)作不是完全平滑的。直接拿原始角度做計(jì)數(shù)判斷會(huì)產(chǎn)生大量誤觸發(fā)。我這里用的是指數(shù)移動(dòng)平均EMA做平滑double _smoothAngle 0; bool _isFirstFrame true; double smoothAngle(double rawAngle) { if (_isFirstFrame) { _smoothAngle rawAngle; _isFirstFrame false; } else { _smoothAngle _smoothAngle * 0.7 rawAngle * 0.3; } return _smoothAngle; }0.7和0.3這兩個(gè)權(quán)重是實(shí)測(cè)比較合適的值平滑效果夠好同時(shí)滯后不會(huì)太大。如果想讓曲線(xiàn)更平滑可以調(diào)到0.8/0.2但滯后會(huì)明顯增加計(jì)數(shù)響應(yīng)會(huì)顯得遲鈍反過(guò)來(lái)調(diào)到0.6/0.4響應(yīng)快了但噪聲也更明顯。具體權(quán)重可以根據(jù)你目標(biāo)動(dòng)作的節(jié)奏微調(diào)。3. 運(yùn)動(dòng)計(jì)數(shù)邏輯的核心實(shí)現(xiàn)3.1 狀態(tài)機(jī)思路把連續(xù)動(dòng)作切成離散狀態(tài)角度曲線(xiàn)拿到之后接下來(lái)就是計(jì)數(shù)邏輯的設(shè)計(jì)。很多初學(xué)者最容易犯的錯(cuò)誤是只用簡(jiǎn)單的閾值判斷角度小于某個(gè)值就加一。這樣做的后果很慘用戶(hù)在最低點(diǎn)微調(diào)一下、或者角度在閾值附近來(lái)回抖動(dòng)計(jì)數(shù)就會(huì)連續(xù)加好幾次。正確的做法是用狀態(tài)機(jī)來(lái)管理動(dòng)作過(guò)程。核心思路是把一次完整動(dòng)作抽象成幾個(gè)離散狀態(tài)只有在狀態(tài)之間完成特定的轉(zhuǎn)換序列才算作一次有效計(jì)數(shù)。以深蹲為例我用的狀態(tài)序列是等待狀態(tài)WAITING初始狀態(tài)角度在站立區(qū)間等待動(dòng)作開(kāi)始下降狀態(tài)DESCENDING角度持續(xù)下降跨越半程閾值進(jìn)入下蹲過(guò)程回升狀態(tài)ASCENDING角度開(kāi)始回升跨越半程閾值說(shuō)明正在站起只有在“下降狀態(tài)”進(jìn)入“回升狀態(tài)”這個(gè)轉(zhuǎn)換發(fā)生時(shí)計(jì)數(shù)加一。這樣設(shè)計(jì)的原因是一次完整的深蹲必然包含“角度先降后升”的過(guò)程只在下降沿或只在上升沿加一都會(huì)導(dǎo)致重復(fù)計(jì)數(shù)而把“下降上升”作為一個(gè)整體單元來(lái)統(tǒng)計(jì)天然就避免了這個(gè)問(wèn)題。代碼層面狀態(tài)機(jī)的骨架大致是這樣的enum RepState { waiting, descending, ascending } class RepCounter { RepState _state RepState.waiting; int _count 0; void update(double angle, double threshold, double minAngle) { switch (_state) { case RepState.waiting: if (angle threshold) { _state RepState.descending; } break; case RepState.descending: if (angle minAngle) { _state RepState.ascending; } break; case RepState.ascending: if (angle threshold) { _count; _state RepState.waiting; } break; } } }這個(gè)骨架是通用框架深蹲、俯臥撐、卷腹等動(dòng)作都能套用只要替換對(duì)應(yīng)的角度計(jì)算關(guān)鍵點(diǎn)和閾值參數(shù)就行。3.2 自適應(yīng)閾值的取舍與代碼落地剛才的代碼里有一個(gè)關(guān)鍵參數(shù)threshold半程閾值。最簡(jiǎn)單的方案是寫(xiě)死一個(gè)值比如深蹲用125度。但實(shí)際用下來(lái)這個(gè)方案問(wèn)題很大不同用戶(hù)的蹲姿深度差異很大有人蹲得很低角度能到70度有人只蹲到90度就起來(lái)了。寫(xiě)死125度對(duì)前者沒(méi)問(wèn)題對(duì)后者可能在下降過(guò)程中就觸發(fā)了或者在回升時(shí)因?yàn)樽藙?shì)變化導(dǎo)致漏計(jì)。解決思路是改成自適應(yīng)閾值記錄上一次完整動(dòng)作的峰值和谷值然后取兩者的中值作為下一次動(dòng)作的觸發(fā)閾值。這樣動(dòng)作幅度大的用戶(hù)閾值自動(dòng)放低動(dòng)作幅度小的用戶(hù)閾值自動(dòng)抬高適配性大大增強(qiáng)。class AdaptiveRepCounter { double _peak 170; // 站立角度初始值 double _valley 80; // 最小角度初始值 double get _threshold (_peak _valley) / 2; int _count 0; RepState _state RepState.waiting; double _currentMin 180; void update(double angle) { // 實(shí)時(shí)更新峰值 if (angle _peak) _peak angle; // 跟蹤本次動(dòng)作的最小角度 if (angle _currentMin) _currentMin angle; switch (_state) { case RepState.waiting: if (angle _threshold) { _state RepState.descending; _currentMin angle; } break; case RepState.descending: if (angle _threshold) { // 直接回升跨過(guò)閾值說(shuō)明下降不夠徹底 if (_currentMin _valley 15) { _valley _currentMin; } _count; _state RepState.waiting; } break; } } }順著這個(gè)邏輯你就能理解為什么狀態(tài)機(jī)里對(duì)“最低點(diǎn)到位”的判斷那么重要。如果用戶(hù)只蹲下去一點(diǎn)點(diǎn)就回升了角度根本沒(méi)跨過(guò)有效深度那這次動(dòng)作不應(yīng)該被計(jì)數(shù)。因此需要設(shè)置一個(gè)minAngle來(lái)強(qiáng)制要求動(dòng)作深度。這個(gè)值可以取上次谷值加15到20度的容差區(qū)間既要放掉那些半途而廢的動(dòng)作又要防止真正的大幅度動(dòng)作被漏掉。這里還有一個(gè)細(xì)節(jié)用戶(hù)動(dòng)作的節(jié)奏快慢會(huì)影響判斷。如果用戶(hù)做了個(gè)慢蹲快起角度下降沿和上升沿的時(shí)間間隔可能很長(zhǎng)如果做了快速深蹲間隔又很短。狀態(tài)機(jī)天然能適應(yīng)這個(gè)變化因?yàn)樗魂P(guān)心底角度的變化方向不關(guān)心速度。但要防止“用戶(hù)只蹲到一半就站起”這種無(wú)效動(dòng)作必須加上最低有效深度約束。我在實(shí)際項(xiàng)目里還把“單個(gè)動(dòng)作最短時(shí)長(zhǎng)”加了進(jìn)去——一次動(dòng)作最少要持續(xù)0.3秒小于這個(gè)時(shí)長(zhǎng)的角度變化大概率是抖動(dòng)或者肢體晃動(dòng)不是有效動(dòng)作。這個(gè)參數(shù)在用戶(hù)做快速波比跳之類(lèi)的動(dòng)作時(shí)特別管用。4. 性能優(yōu)化與真機(jī)調(diào)參實(shí)錄4.1 推理線(xiàn)程與UI分離別讓模型拖垮界面姿態(tài)估計(jì)模型在手機(jī)上跑最大的問(wèn)題就是推理耗時(shí)會(huì)占據(jù)主線(xiàn)程導(dǎo)致界面卡頓、幀率驟降。我在項(xiàng)目里的做法是把整條鏈路分成兩條線(xiàn)程UI線(xiàn)程負(fù)責(zé)攝像頭預(yù)覽、自定義疊加層繪制關(guān)鍵點(diǎn)連線(xiàn)、計(jì)數(shù)信息展示推理線(xiàn)程負(fù)責(zé)幀數(shù)據(jù)轉(zhuǎn)換、模型推理、關(guān)鍵點(diǎn)后處理兩條線(xiàn)程之間用異步通道傳遞數(shù)據(jù)UI線(xiàn)程只拿推理結(jié)果不做任何耗時(shí)計(jì)算。實(shí)測(cè)下來(lái)這種架構(gòu)下界面始終保持流暢即使模型單幀推理耗時(shí)較長(zhǎng)也不會(huì)卡住預(yù)覽畫(huà)面。具體操作上有幾個(gè)參數(shù)直接影響性能表現(xiàn)配置項(xiàng)高配方案中配方案低配方案輸入分辨率1280x720640x480320x240模型復(fù)雜度高中低處理幀率30fps20fps15fps適用機(jī)型旗艦機(jī)型中端機(jī)型入門(mén)機(jī)型我在中端機(jī)型上測(cè)試時(shí)一開(kāi)始用1280x720輸入加高模型復(fù)雜度幀率只有12fps角度曲線(xiàn)跳變明顯。后來(lái)降成640x480加中模型復(fù)雜度幀率直接提升到25fps角度數(shù)據(jù)也穩(wěn)定了很多。這里的關(guān)鍵認(rèn)知是姿態(tài)估計(jì)模型輸出的關(guān)鍵點(diǎn)坐標(biāo)都是相對(duì)輸入圖像的歸一化坐標(biāo)輸入分辨率只要滿(mǎn)足基本的人體尺寸比例就能保證識(shí)別效果。分辨率降一半推理速度能翻倍而精度損失遠(yuǎn)沒(méi)有想象中那么嚴(yán)重。4.2 實(shí)測(cè)幀率與發(fā)熱控制實(shí)測(cè)定幀率這件事建議放在功能完成之后立刻做不要拖到最后。我用三條不同檔位的測(cè)試機(jī)型跑過(guò)整理出來(lái)的參考數(shù)據(jù)大致是這樣的機(jī)型檔位輸入分辨率實(shí)時(shí)幀率連續(xù)運(yùn)行10分鐘后的溫度變化旗艦機(jī)640x48028-30fps小幅溫?zé)嶂卸藱C(jī)640x48020-25fps溫?zé)峥山邮苋腴T(mén)機(jī)480x36015-18fps明顯發(fā)熱需降頻發(fā)熱控制是個(gè)很現(xiàn)實(shí)的問(wèn)題。運(yùn)動(dòng)類(lèi)APP一拍就是十幾二十分鐘手機(jī)發(fā)熱后系統(tǒng)會(huì)主動(dòng)降頻推理速度跟著變慢計(jì)數(shù)精度就崩了。我的處理方案有三板斧第一啟用相機(jī)的幀率限制不要無(wú)腦跑滿(mǎn)幀。運(yùn)動(dòng)計(jì)數(shù)的有效幀率上限其實(shí)不需要太高25fps完全夠用更高幀率只增加發(fā)熱不會(huì)提升計(jì)數(shù)精度。第二推理線(xiàn)程采用“丟幀處理”策略如果模型推理跟不上幀到達(dá)的速率就自動(dòng)丟棄待處理隊(duì)列里的舊幀而不是讓它無(wú)限堆積。堆積只會(huì)增加延遲而且堆積到一定程度后算出來(lái)的計(jì)數(shù)結(jié)果對(duì)應(yīng)的已經(jīng)不是當(dāng)前畫(huà)面了。第三考慮到運(yùn)動(dòng)場(chǎng)景中用戶(hù)主要是看畫(huà)面提示來(lái)完成動(dòng)作不是長(zhǎng)期盯著屏幕可以在檢測(cè)到持續(xù)運(yùn)動(dòng)中主動(dòng)降低屏幕亮度或者提示用戶(hù)鎖定屏幕減少屏幕耗電和發(fā)熱。有一組數(shù)據(jù)可以直觀(guān)說(shuō)明問(wèn)題不做任何優(yōu)化時(shí)入門(mén)機(jī)連續(xù)跑10分鐘后幀率暴跌到8fps左右做了上述三板斧之后同一臺(tái)機(jī)器能穩(wěn)定在15fps左右溫度也從“燙手”變成“溫?zé)帷?。這個(gè)差距對(duì)計(jì)數(shù)體驗(yàn)是決定性的。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 關(guān)鍵點(diǎn)跳變、角度NaN等典型問(wèn)題開(kāi)發(fā)過(guò)程中踩過(guò)的坑我整理成了一張排查速查表遇到了直接對(duì)著查現(xiàn)象可能原因處理建議關(guān)鍵點(diǎn)偶爾跳到畫(huà)面外人體部分被遮擋或置信度低過(guò)濾置信度低于0.5的關(guān)鍵點(diǎn)跳變時(shí)不更新計(jì)數(shù)狀態(tài)角度出現(xiàn)明顯異常跳變向量模長(zhǎng)接近零或浮點(diǎn)誤差越界增加最小模長(zhǎng)判斷acos入?yún)⒈仨歝lamp(-1,1)計(jì)數(shù)重復(fù)加一狀態(tài)機(jī)缺少回升判斷或在閾值附近抖動(dòng)改用“下降回升”完整狀態(tài)序列增加閾值回差帶計(jì)數(shù)不增加最低動(dòng)作深度約束太嚴(yán)格或用戶(hù)動(dòng)作幅度不足放寬minAngle閾值或改用自適應(yīng)閾值跟蹤谷值深蹲時(shí)角度算反了用錯(cuò)了關(guān)鍵點(diǎn)索引或角度頂點(diǎn)取錯(cuò)核對(duì)髖、膝、踝索引畫(huà)線(xiàn)調(diào)試輸出檢測(cè)到的關(guān)鍵點(diǎn)偏移歸一化坐標(biāo)沒(méi)有換算成像素坐標(biāo)繪制疊加層前乘上畫(huà)面實(shí)際寬高正面識(shí)別效果差深度信息受二維投影影響關(guān)鍵點(diǎn)在身體前方遮擋引導(dǎo)用戶(hù)側(cè)對(duì)鏡頭這里有一個(gè)非常值得注意的現(xiàn)象關(guān)鍵點(diǎn)置信度過(guò)濾。模型會(huì)為每個(gè)關(guān)鍵點(diǎn)輸出一個(gè)置信度分?jǐn)?shù)但在運(yùn)動(dòng)場(chǎng)景里手指出汗擋鏡頭、衣服和背景顏色相近、動(dòng)作幅度過(guò)大導(dǎo)致關(guān)鍵點(diǎn)暫時(shí)出畫(huà)這些都會(huì)讓置信度驟降。硬過(guò)濾置信度低于閾值直接不用會(huì)導(dǎo)致角度數(shù)據(jù)斷檔影響計(jì)數(shù)連續(xù)性我后來(lái)用的是軟過(guò)濾置信度低于閾值時(shí)保留上一幀的值不更新角度保持曲線(xiàn)平滑過(guò)渡。這個(gè)思路用在跳變問(wèn)題上非常有效。5.2 計(jì)數(shù)不準(zhǔn)的調(diào)參方向和調(diào)試方法計(jì)數(shù)不準(zhǔn)的調(diào)試本質(zhì)上就是對(duì)著角度曲線(xiàn)找異常。我強(qiáng)烈建議在開(kāi)發(fā)階段保留一個(gè)調(diào)試模式把實(shí)時(shí)的關(guān)鍵點(diǎn)連線(xiàn)、角度數(shù)值、狀態(tài)機(jī)狀態(tài)直接繪制到預(yù)覽畫(huà)面上同時(shí)把角度數(shù)據(jù)以日志形式輸出。這樣每出現(xiàn)一次誤計(jì)或者漏計(jì)你都能精確地回放當(dāng)時(shí)發(fā)生了什么。具體調(diào)參的方向分幾種情況如果出現(xiàn)“多計(jì)”大概率是閾值回差不夠。給狀態(tài)機(jī)增加回差帶是解決這個(gè)問(wèn)題的標(biāo)準(zhǔn)手段比如下降沿跨過(guò)閾值觸發(fā)下降狀態(tài)回升時(shí)要超過(guò)閾值加上一個(gè)偏移量例如15度才算回升完成。這個(gè)偏移量把閾值附近的無(wú)意義抖動(dòng)全部濾掉了。如果出現(xiàn)“漏計(jì)”優(yōu)先檢查最低動(dòng)作深度的約束條件。很多用戶(hù)深蹲時(shí)蹲得不夠低如果你把最低角度設(shè)定在70度他蹲到85度就起來(lái)了那永遠(yuǎn)不會(huì)觸發(fā)計(jì)數(shù)。這種情況下要么放寬minAngle要么使用自適應(yīng)閾值去循環(huán)更新谷值讓系統(tǒng)自己適應(yīng)用戶(hù)的實(shí)際動(dòng)作幅度。如果出現(xiàn)“延遲計(jì)數(shù)”就是用戶(hù)已經(jīng)做完動(dòng)作一秒后才加一這大概率是平滑權(quán)重太偏平滑了或者處理幀率太低導(dǎo)致?tīng)顟B(tài)轉(zhuǎn)換反應(yīng)遲鈍。把EMA的平滑系數(shù)從0.7/0.3調(diào)整到0.6/0.4同時(shí)檢查推理線(xiàn)程是否有幀堆積。如果出現(xiàn)“邏輯錯(cuò)亂”比如沒(méi)做動(dòng)作也計(jì)數(shù)看狀態(tài)機(jī)的初始狀態(tài)是否被正確復(fù)位。用戶(hù)站在畫(huà)面里切換動(dòng)作類(lèi)型時(shí)狀態(tài)機(jī)還保留著上一個(gè)動(dòng)作的狀態(tài)會(huì)直接把當(dāng)前動(dòng)作的起始角度當(dāng)成回升過(guò)程導(dǎo)致誤計(jì)。每次切換動(dòng)作模式時(shí)必須重置狀態(tài)機(jī)。最后分享一個(gè)調(diào)試心得運(yùn)動(dòng)計(jì)數(shù)的調(diào)參過(guò)程非常依賴(lài)錄制回放。如果是實(shí)時(shí)調(diào)試動(dòng)作重現(xiàn)的隨機(jī)性太強(qiáng)你根本沒(méi)法對(duì)比參數(shù)調(diào)整前后的差異。我的做法是先用調(diào)試模式錄制一段包含多次標(biāo)準(zhǔn)動(dòng)作的視頻然后把這段視頻反復(fù)回放喂給計(jì)數(shù)邏輯改一次參數(shù)跑一遍回放用同一段數(shù)據(jù)對(duì)比不同參數(shù)下的計(jì)數(shù)結(jié)果。這樣調(diào)參才有依據(jù)否則就是瞎試。我實(shí)測(cè)下來(lái)用這個(gè)方法把深蹲計(jì)數(shù)從最初的一做動(dòng)作就亂跳調(diào)到能穩(wěn)定識(shí)別連續(xù)20次深蹲大概花了兩個(gè)晚上。相比之下如果靠實(shí)時(shí)測(cè)試來(lái)調(diào)參可能要花一到兩周而且調(diào)完還不能確定是參數(shù)生效了還是動(dòng)作剛好做得更標(biāo)準(zhǔn)了。所以先錄視頻再調(diào)參這條路建議所有做類(lèi)似項(xiàng)目的人優(yōu)先選擇。這個(gè)項(xiàng)目后期還可以繼續(xù)擴(kuò)展的方向我目前正在做的是加入動(dòng)作質(zhì)量評(píng)估模塊利用角度曲線(xiàn)的平滑度和最低點(diǎn)深度給每次動(dòng)作打一個(gè)質(zhì)量分提醒用戶(hù)動(dòng)作是否做得足夠標(biāo)準(zhǔn)。從單一計(jì)數(shù)到質(zhì)量反饋?zhàn)藨B(tài)估計(jì)類(lèi)應(yīng)用的想象空間其實(shí)很大但底層的角度計(jì)算和狀態(tài)機(jī)邏輯永遠(yuǎn)是最值得打磨的地基。