的C++數(shù)據(jù)采集:肌電與姿態(tài)信號(hào)實(shí)時(shí)落盤(pán)實(shí)戰(zhàn))
看到桌上這塊已經(jīng)有點(diǎn)泛黃的手環(huán)很多人第一反應(yīng)是“這不是早就停產(chǎn)了嗎”確實(shí)MYO手環(huán)現(xiàn)在不太好買(mǎi)到全新貨但在桌面端數(shù)據(jù)采集這個(gè)圈子里它還是一款性價(jià)比很高的實(shí)驗(yàn)器材。我這次想分享的是一個(gè)非常具體的項(xiàng)目基于MYO手環(huán)的C數(shù)據(jù)采集程序。它的目標(biāo)很直接把手環(huán)上的8路肌電信號(hào)、九軸姿態(tài)數(shù)據(jù)連續(xù)地讀出來(lái)帶上時(shí)間戳寫(xiě)進(jìn)CSV文件供后續(xù)手勢(shì)識(shí)別和機(jī)器學(xué)習(xí)建模使用。如果你正在找一套能長(zhǎng)期穩(wěn)定跑數(shù)據(jù)采集的代碼或者剛拿到一塊二手MYO手環(huán)不知道從哪里下手這篇文章應(yīng)該能幫你省掉一個(gè)星期的彎路。為什么這么說(shuō)因?yàn)镸YO的坑不在于“不會(huì)寫(xiě)代碼”而在于對(duì)它的SDK事件模型、藍(lán)牙連接策略、解鎖機(jī)制不熟悉你很容易出現(xiàn)這種情況程序好像連上了但EMG數(shù)據(jù)全是0或者采了不到兩分鐘就掉線又或者數(shù)據(jù)一會(huì)兒快一會(huì)兒慢根本沒(méi)法當(dāng)成訓(xùn)練集用。下面這些內(nèi)容全部圍繞我的實(shí)際改版經(jīng)驗(yàn)來(lái)寫(xiě)代碼可以直接抄坑也幫你提前踩了。1. 項(xiàng)目目標(biāo)與選型思考1.1 核心需求拆解這個(gè)項(xiàng)目的核心需求可以拆成四個(gè)部分連接設(shè)備、接收數(shù)據(jù)、緩沖數(shù)據(jù)、落盤(pán)數(shù)據(jù)。看起來(lái)很簡(jiǎn)單但每一部分都有細(xì)節(jié)。連接設(shè)備這塊需要處理藍(lán)牙BLE配對(duì)、Myo SDK初始化、設(shè)備超時(shí)等問(wèn)題。接收數(shù)據(jù)這塊MYO手環(huán)通過(guò)藍(lán)牙持續(xù)回傳肌電和姿態(tài)數(shù)據(jù)程序必須以事件回調(diào)方式快速處理。緩沖數(shù)據(jù)更關(guān)鍵因?yàn)榛卣{(diào)線程和寫(xiě)文件線程不能直接耦合否則一旦磁盤(pán)IO卡頓就會(huì)反過(guò)來(lái)阻塞藍(lán)牙數(shù)據(jù)接收。落盤(pán)數(shù)據(jù)則需要保證格式統(tǒng)一、時(shí)間戳完整方便后續(xù)用Python或者M(jìn)ATLAB直接讀。這個(gè)項(xiàng)目的“數(shù)據(jù)采集”不是單純地打印幾個(gè)數(shù)字而是要生成一批干凈、可復(fù)用的數(shù)據(jù)文件。打個(gè)比方后面做手勢(shì)分類(lèi)時(shí)訓(xùn)練集質(zhì)量直接決定模型效果所以采集端最重要的不是界面多炫而是數(shù)據(jù)不丟幀、時(shí)間軸對(duì)齊、標(biāo)簽不混亂。1.2 為什么選C而不是Python或者其他工具很多人會(huì)問(wèn)現(xiàn)在Python這么方便用pySerial或者第三方綁定不也能采集確實(shí)能但我最終選了C主要有三個(gè)原因。第一官方SDK的主場(chǎng)是C。MYO的SDK里C接口最完整示例代碼也最多回調(diào)機(jī)制非常清晰。第三方Python綁定雖然能調(diào)用但往往依賴(lài)?yán)习姹镜膸?kù)容易出現(xiàn)版本不兼容特別是手環(huán)固件更新后綁定庫(kù)可能直接失效。第二實(shí)時(shí)采集場(chǎng)景下C的掌控力更強(qiáng)。肌電數(shù)據(jù)的原始回調(diào)頻率不低如果中間還要做滑動(dòng)窗口、濾波、特征計(jì)算Python的GIL和內(nèi)存管理很容易成為瓶頸。C這邊可以直接用無(wú)鎖隊(duì)列、vector緩存、輕量級(jí)線程處理每一幀數(shù)據(jù)的延遲都能控制在穩(wěn)定范圍內(nèi)。第三從工程角度看C更適合做長(zhǎng)期穩(wěn)定的采集工具。Python腳本跑久了會(huì)累積內(nèi)存碎片而C程序只要設(shè)計(jì)合理可以連續(xù)跑幾個(gè)小時(shí)不出問(wèn)題。對(duì)采集手部動(dòng)作數(shù)據(jù)集這種需求來(lái)說(shuō)穩(wěn)定比開(kāi)發(fā)速度重要得多。1.3 這個(gè)項(xiàng)目適合誰(shuí)如果你屬于下面幾類(lèi)人這個(gè)項(xiàng)目可以當(dāng)作一個(gè)很不錯(cuò)的參考模板第一類(lèi)剛拿到MYO手環(huán)、想驗(yàn)證它能不能采集出有效數(shù)據(jù)的開(kāi)發(fā)者第二類(lèi)做手勢(shì)識(shí)別課題需要自己生成EMG數(shù)據(jù)集的在校學(xué)生或研究人員第三類(lèi)想把肌電信號(hào)接入自己已有C框架的工程師。如果你完全沒(méi)接觸過(guò)C建議先補(bǔ)一補(bǔ)類(lèi)繼承、回調(diào)函數(shù)、文件流這些基礎(chǔ)再來(lái)讀本文的代碼。2. 環(huán)境與工程配置先讓SDK跑起來(lái)2.1 需要準(zhǔn)備的硬件和依賴(lài)項(xiàng)目需要的東西不多但每一樣都別馬虎一臺(tái)能正常使用藍(lán)牙的電腦。桌面臺(tái)式機(jī)如果沒(méi)有藍(lán)牙建議準(zhǔn)備一個(gè)USB藍(lán)牙適配器。MYO手環(huán)一塊并保證已經(jīng)充滿電。電池電量低時(shí)數(shù)據(jù)回傳會(huì)變得很不穩(wěn)定。官方SDK包。解壓后你會(huì)看到include、lib、bin這些目錄里面放著myo.hpp頭文件、myo64.dll、myo32.dll、myo64.lib等文件。一套C構(gòu)建環(huán)境。我用的是Windows 10 x64 VS系列IDE配置Release x64模式編譯。這里要提醒一句別小看SDK路徑和庫(kù)文件版本。我見(jiàn)過(guò)很多同學(xué)把32位庫(kù)拷到64位工程里結(jié)果鏈接直接報(bào)錯(cuò)或者DLL忘了復(fù)制到輸出目錄程序一運(yùn)行就提示“找不到myo64.dll”。所以建議工程目錄做成下面這個(gè)樣子your_project/ ├── source/ │ └── main.cpp ├── third_party/ │ └── myo/ │ ├── include/ │ │ └── myo/ │ │ └── myo.hpp │ └── lib/ │ ├── myo64.dll │ ├── myo64.lib │ ├── myo32.dll │ └── myo32.lib └── output/ └── myo.csv2.2 工程配置三步走以VS工程為例配置三步就夠了。第一步設(shè)置包含目錄。在項(xiàng)目屬性里找到“C/C → 常規(guī) → 附加包含目錄”把third_party/myo/include加進(jìn)去。這樣的話代碼里寫(xiě)#include myo/myo.hpp時(shí)編譯器才能找到頭文件。第二步設(shè)置庫(kù)目錄和依賴(lài)項(xiàng)。在“鏈接器 → 常規(guī) → 附加庫(kù)目錄”里加third_party/myo/lib然后在“鏈接器 → 輸入 → 附加依賴(lài)項(xiàng)”里寫(xiě)myo64.lib。這里要注意如果你編譯的是x64平臺(tái)就鏈接myo64.libx86平臺(tái)就用myo32.lib千萬(wàn)別混。第三步把DLL復(fù)制到輸出目錄。最簡(jiǎn)單的方式是在“生成事件 → 后期生成事件”里加一條命令xcopy /Y $(ProjectDir)third_party\myo\lib\myo64.dll $(OutDir)不加這一步的話程序經(jīng)常在啟動(dòng)時(shí)就栽了。DLL加載失敗的錯(cuò)誤有時(shí)候不明顯排查起來(lái)特別煩人。2.3 連接驗(yàn)證怎么判斷手環(huán)真的通了配置完成之后先別急著寫(xiě)完整采集邏輯寫(xiě)一個(gè)最小程序把手環(huán)連上看看日志。連接成功有一個(gè)很典型的標(biāo)志——程序里出現(xiàn)“connected”回調(diào)緊接著過(guò)兩三秒會(huì)出現(xiàn)“arm synced”回調(diào)。我建議你在onConnect和onArmSync里各打一行日志。如果只有“connected”沒(méi)有“arm synced”說(shuō)明手環(huán)沒(méi)戴穩(wěn)或者固件還在等待手臂同步這時(shí)候數(shù)據(jù)雖然能收到但姿勢(shì)識(shí)別和姿態(tài)角可能會(huì)亂。如果兩個(gè)日志都出現(xiàn)了再開(kāi)始采集后面數(shù)據(jù)質(zhì)量會(huì)好很多。另外一個(gè)容易被忽略的問(wèn)題是官方連接工具。MYO手環(huán)第一使用往往要先用官方連接工具做一次配對(duì)和固件升級(jí)否則SDK直接掃不到設(shè)備。但配好之后建議把這個(gè)工具關(guān)掉只留你自己的程序。因?yàn)槿绻麅蓚€(gè)程序同時(shí)搶藍(lán)牙通道會(huì)出現(xiàn)“設(shè)備時(shí)斷時(shí)續(xù)、數(shù)據(jù)幀率減半”的奇怪現(xiàn)象。3. 采集程序的核心實(shí)現(xiàn)3.1 Hub與DeviceListener的配合方式MYO的C SDK最核心的兩個(gè)角色是Hub和DeviceListener。你可以把Hub理解成一個(gè)藍(lán)牙消息泵它負(fù)責(zé)連接設(shè)備、分發(fā)事件DeviceListener則是一組回調(diào)接口你繼承它重寫(xiě)里面的方法就能捕獲到不同的數(shù)據(jù)事件。主循環(huán)的骨架是這樣的#include myo/myo.hpp #include iostream #include fstream #include vector #include mutex #include thread #include atomic class DataListener : public myo::DeviceListener { public: void onConnect(myo::Myo* myo, uint64_t timestamp, myo::FirmwareVersion firmwareVersion) override { std::cout 已連接手環(huán) std::endl; myo-setStreamEmg(myo::Myo::streamEmgEnabled); myo-setLockingPolicy(myo::Myo::lockingPolicyNone); } }; int main() { try { myo::Hub hub(com.example.myo.emg-collector); std::cout 正在查找MYO手環(huán)... std::endl; myo::Myo* myo hub.waitForMyo(10000); if (!myo) { std::cerr 10秒內(nèi)沒(méi)有發(fā)現(xiàn)手環(huán) std::endl; return 1; } DataListener listener; hub.addListener(listener); while (true) { hub.run(100); } } catch (const std::exception e) { std::cerr 程序異常: e.what() std::endl; return 1; } return 0; }這里有個(gè)關(guān)鍵點(diǎn)hub.run(100)不是“開(kāi)始后臺(tái)跑線程”而是“阻塞處理100毫秒的事件”。所有回調(diào)都在run所在的線程里執(zhí)行。所以不要在回調(diào)里做耗時(shí)操作否則會(huì)拖慢事件處理最終導(dǎo)致藍(lán)牙數(shù)據(jù)積壓。3.2 顯式打開(kāi)EMG流否則數(shù)據(jù)全是0這是初學(xué)者最容易踩的坑程序連上了姿態(tài)數(shù)據(jù)也有但EMG數(shù)據(jù)一直是0或者只有偶爾跳幾個(gè)值。原因很可能是你沒(méi)有在onConnect里顯式打開(kāi)EMG流。上面示例代碼里的這兩行非常重要myo-setStreamEmg(myo::Myo::streamEmgEnabled); myo-setLockingPolicy(myo::Myo::lockingPolicyNone);第一行告訴手環(huán)“我要接收肌電數(shù)據(jù)”第二行則是關(guān)閉自動(dòng)鎖定策略。MYO默認(rèn)有省電鎖定機(jī)制一段時(shí)間沒(méi)有動(dòng)作就會(huì)休眠自動(dòng)鎖定會(huì)讓采集程序無(wú)故中斷。關(guān)閉自動(dòng)鎖定后長(zhǎng)時(shí)間采集才不會(huì)被手環(huán)自己的策略打斷。3.3 建立緩沖隊(duì)列別在回調(diào)里寫(xiě)文件拿到EMG數(shù)據(jù)后的第一反應(yīng)可能是直接在onEmgData里打印或者寫(xiě)文件。我勸你千萬(wàn)別這么干。onEmgData的回調(diào)頻率很高如果直接在回調(diào)里訪問(wèn)磁盤(pán)哪怕std::ofstream自帶緩沖也會(huì)造成回調(diào)阻塞。更嚴(yán)重的是如果在回調(diào)里加鎖處理復(fù)雜邏輯可能導(dǎo)致藍(lán)牙協(xié)議棧內(nèi)部的緩沖區(qū)溢出輕則丟幀重則斷連。正確做法是回調(diào)里只做輕量拷貝把幀數(shù)據(jù)塞進(jìn)內(nèi)存隊(duì)列然后由主循環(huán)或者獨(dú)立線程批量取走。示例代碼如下class DataListener : public myo::DeviceListener { public: struct Frame { int64_t timestamp; int8_t emg[8]; float roll, pitch, yaw; float ax, ay, az; float gx, gy, gz; }; std::vectorFrame queue; std::mutex queueMutex; void onEmgData(myo::Myo* myo, uint64_t timestamp, const int8_t* emg) override { Frame f{}; f.timestamp static_castint64_t(timestamp); for (int i 0; i 8; i) { f.emg[i] emg[i]; } f.roll lastRoll; f.pitch lastPitch; f.yaw lastYaw; f.ax lastAx; f.ay lastAy; f.az lastAz; f.gx lastGx; f.gy lastGy; f.gz lastGz; std::lock_guardstd::mutex lock(queueMutex); if (queue.size() 100000) { queue.push_back(f); } } float lastRoll 0.0f, lastPitch 0.0f, lastYaw 0.0f; float lastAx 0.0f, lastAy 0.0f, lastAz 0.0f; float lastGx 0.0f, lastGy 0.0f, lastGz 0.0f; };這里我把onEmgData作為“數(shù)據(jù)同步點(diǎn)”把最近一次的姿態(tài)和慣性數(shù)據(jù)也合并進(jìn)同一幀。因?yàn)镋MG和IMU的回調(diào)頻率不完全一致這種“以EMG幀為主、IMU值取最新”的做法雖然粗糙但實(shí)現(xiàn)簡(jiǎn)單數(shù)據(jù)也不會(huì)對(duì)不齊。3.4 數(shù)據(jù)落盤(pán)從內(nèi)存隊(duì)列到CSV有了內(nèi)存隊(duì)列下一步就是把隊(duì)列里的數(shù)據(jù)批量寫(xiě)入CSV。我建議主循環(huán)每執(zhí)行一次hub.run(100)就交換一次隊(duì)列然后統(tǒng)一寫(xiě)入文件int main() { try { myo::Hub hub(com.example.myo.emg-collector); std::cout 正在查找MYO手環(huán)... std::endl; myo::Myo* myo hub.waitForMyo(10000); if (!myo) { std::cerr 10秒內(nèi)沒(méi)有發(fā)現(xiàn)手環(huán) std::endl; return 1; } DataListener listener; hub.addListener(listener); std::ofstream out(emg_log.csv); out.precision(6); out timestamp; for (int i 1; i 8; i) out ,emg i; out ,roll,pitch,yaw,ax,ay,az,gx,gy,gz\n; std::cout 開(kāi)始采集按CtrlC結(jié)束 std::endl; while (true) { hub.run(100); std::vectorDataListener::Frame batch; { std::lock_guardstd::mutex lock(listener.queueMutex); batch.swap(listener.queue); } for (const auto f : batch) { out f.timestamp; for (int i 0; i 8; i) { out , static_castint(f.emg[i]); } out , f.roll , f.pitch , f.yaw; out , f.ax , f.ay , f.az; out , f.gx , f.gy , f.gz \n; } } } catch (const std::exception e) { std::cerr 程序異常: e.what() std::endl; return 1; } return 0; }CSV里每行就是一幀數(shù)據(jù)。時(shí)間戳來(lái)自MYO SDK單位是毫秒不同固件的起點(diǎn)可能不一樣但你只需要關(guān)心差值不用糾結(jié)絕對(duì)時(shí)間。寫(xiě)文件時(shí)emg1到emg8對(duì)應(yīng)的就是手環(huán)上8個(gè)肌電采集點(diǎn)的原始值。有一點(diǎn)必須強(qiáng)調(diào)int8_t是有符號(hào)數(shù)。打印的時(shí)候要static_castint轉(zhuǎn)換否則直接輸出字符你會(huì)看到一堆亂碼。我之前調(diào)試時(shí)犯過(guò)這個(gè)錯(cuò)屏幕上全是“?”這樣的符號(hào)排查了半天才發(fā)現(xiàn)只是格式化問(wèn)題。4. 實(shí)測(cè)中踩過(guò)的坑數(shù)據(jù)質(zhì)量相關(guān)4.1 藍(lán)牙掉線采集到一半就沒(méi)數(shù)據(jù)了這是采集程序最致命的坑。表現(xiàn)是程序一開(kāi)始很穩(wěn)過(guò)了兩三分鐘突然onDisconnect回調(diào)觸發(fā)然后就沒(méi)有然后了。常見(jiàn)誘因有三個(gè)。第一電腦自帶藍(lán)牙模塊太弱。很多筆記本的藍(lán)牙模塊和WiFi共用天線干擾一大連接就斷。這種問(wèn)題靠代碼解決不了換一個(gè)USB藍(lán)牙適配器明顯改善。第二電腦系統(tǒng)把藍(lán)牙設(shè)備識(shí)別成低功耗設(shè)備開(kāi)啟了節(jié)能模式。在設(shè)備管理器里找到手環(huán)對(duì)應(yīng)的藍(lán)牙設(shè)備在電源管理里把“允許計(jì)算機(jī)關(guān)閉此設(shè)備以節(jié)約電源”關(guān)掉。這個(gè)選項(xiàng)默認(rèn)是勾上的很多人沒(méi)注意。第三程序自身導(dǎo)致掉線。如果回調(diào)處理太重藍(lán)牙協(xié)議棧的緩沖被塞滿就會(huì)觸發(fā)異常斷開(kāi)。檢查一下你的回調(diào)里有沒(méi)有打印日志、寫(xiě)文件、做復(fù)雜計(jì)算。有的話全部挪到主循環(huán)里。4.2 基線漂移和初始校準(zhǔn)肌電信號(hào)有個(gè)特點(diǎn)手自然放松和微微用力時(shí)數(shù)值基線都不一樣。如果你一開(kāi)機(jī)就直接采集后面分析時(shí)會(huì)發(fā)現(xiàn)數(shù)據(jù)里的“0刻度”一直在漂。我的做法是開(kāi)始正式采集前先讓受試者手掌朝上放松5秒鐘程序把這5秒的EMG數(shù)據(jù)算出一個(gè)均值。后面分析手勢(shì)時(shí)用當(dāng)前值減去這個(gè)靜息基線再來(lái)算RMS或者差分特征。這樣能讓握拳、張開(kāi)等動(dòng)作的模式更明顯。4.3 自動(dòng)鎖定機(jī)制打斷采集MYO有鎖定機(jī)制正常模式下如果一段時(shí)間沒(méi)檢測(cè)到握拳等動(dòng)作設(shè)備就會(huì)進(jìn)入鎖定狀態(tài)。鎖定之后SDK會(huì)停止回傳EMG數(shù)據(jù)姿態(tài)數(shù)據(jù)也會(huì)變得異常。解決方式就是在onConnect里設(shè)置setLockingPolicy(myo::Myo::lockingPolicyNone)。如果你用的是老版本SDK可能會(huì)發(fā)現(xiàn)這個(gè)方法不存在。那就需要在檢測(cè)到onPose里的Pose::doubleTap時(shí)手動(dòng)調(diào)用myo-unlock(myo::Myo::unlockHold)來(lái)保持解鎖。雖然我的版本已經(jīng)用不上這步了但如果你手里的SDK比較舊這個(gè)備用方案可以試一下。4.4 幀率忽快忽慢數(shù)據(jù)沒(méi)法對(duì)齊我遇到過(guò)一次很奇怪的現(xiàn)象手環(huán)連接正常CSV也寫(xiě)出來(lái)了但同一秒內(nèi)幀數(shù)時(shí)多時(shí)少。后來(lái)排查下來(lái)原因有兩方面。一是電腦負(fù)載太高。當(dāng)時(shí)我一邊跑采集程序一邊開(kāi)著視頻會(huì)議藍(lán)牙數(shù)據(jù)包處理被系統(tǒng)調(diào)度拖累。把無(wú)關(guān)程序關(guān)掉之后幀率就穩(wěn)定多了。二是hub.run(100)這個(gè)參數(shù)可以調(diào)。如果你覺(jué)得數(shù)據(jù)太稀疏可以改成hub.run(20)或hub.run(20)但要注意CPU占用會(huì)上升。建議先保持100毫秒觀察幀率穩(wěn)定后再微調(diào)。還有個(gè)小技巧測(cè)試幀率是否穩(wěn)定可以打印每批數(shù)據(jù)的batch.size()和兩次主循環(huán)之間的耗時(shí)差。如果耗時(shí)差忽大忽小說(shuō)明系統(tǒng)調(diào)度有問(wèn)題優(yōu)先排查后臺(tái)程序。5. 導(dǎo)出CSV之后數(shù)據(jù)還能怎么用5.1 先看一眼原始數(shù)據(jù)結(jié)構(gòu)采集完成后用文本編輯器打開(kāi)CSV你會(huì)看到類(lèi)似這樣的數(shù)據(jù)timestamp,emg1,emg2,emg3,emg4,emg5,emg6,emg7,emg8,roll,pitch,yaw,ax,ay,az,gx,gy,gz 881230,-7,10,5,-2,0,8,-3,6,0.12,-0.03,0.21,0.55,-0.01,9.81,0.01,-0.02,0.00 881232,35,88,45,-10,4,50,12,20,0.12,-0.03,0.21,0.55,-0.01,9.81,0.01,-0.02,0.00你會(huì)發(fā)現(xiàn)放松狀態(tài)下EMG值在0附近小幅波動(dòng)握拳瞬間多個(gè)通道數(shù)值會(huì)快速拉離基線。這就是肌電信號(hào)最原始的形態(tài)后面分類(lèi)模型就是靠這些模式區(qū)分不同手勢(shì)的。5.2 滑動(dòng)窗口特征提取原始EMG直接拿來(lái)訓(xùn)練模型效果往往不好。業(yè)界常用的做法是先切成窗口每個(gè)窗口算一組特征。最常見(jiàn)的是RMS均方根和MAV平均絕對(duì)值。如果我們把連續(xù)N幀作為一個(gè)窗口RMS的計(jì)算代碼類(lèi)似這樣#include cmath #include vector double calcRms(const std::vectordouble values) { double sum 0.0; for (double v : values) { sum v * v; } return std::sqrt(sum / values.size()); }每個(gè)通道都算一組RMS8個(gè)通道就是8維特征。再配合窗口滑動(dòng)比如每50幀滑動(dòng)一次就能得到一條特征序列。之后無(wú)論是用傳統(tǒng)機(jī)器學(xué)習(xí)分類(lèi)還是直接用LSTM、CNN都有了干凈的輸入。5.3 組織成訓(xùn)練集的關(guān)鍵細(xì)節(jié)采集數(shù)據(jù)不是采完就算完標(biāo)簽管理非常重要。我的習(xí)慣是每個(gè)手勢(shì)一個(gè)文件夾一次動(dòng)作采一個(gè)文件文件名里帶上動(dòng)作編號(hào)和采集序號(hào)。dataset/ ├── fist/ │ ├── 001.csv │ ├── 002.csv │ └── ... ├── spread/ │ ├── 001.csv │ ├── 002.csv │ └── ... └── relax/ ├── 001.csv └── ...采集時(shí)還要注意每個(gè)動(dòng)作準(zhǔn)備之前先有一小段自然放松狀態(tài)每個(gè)動(dòng)作重復(fù)采集多次不同采集日期的數(shù)據(jù)最好分開(kāi)存。我踩過(guò)的坑是把所有數(shù)據(jù)混在一個(gè)文件里訓(xùn)練時(shí)模型精度很高但一換到新采集的數(shù)據(jù)效果就暴跌。后來(lái)發(fā)現(xiàn)是過(guò)擬合了某個(gè)特定日期的環(huán)境噪聲。數(shù)據(jù)文件分開(kāi)存至少能讓你后續(xù)做交叉驗(yàn)證時(shí)方便很多。5.4 還能往哪些方向擴(kuò)展這套C采集程序只是基礎(chǔ)往上擴(kuò)展的空間很大。我自己試過(guò)的方向有兩個(gè)一是加一個(gè)實(shí)時(shí)波形預(yù)覽窗口用簡(jiǎn)單的OpenGL或者Qt繪圖對(duì)著手環(huán)數(shù)據(jù)在線看波形調(diào)試姿勢(shì)識(shí)別邏輯時(shí)非常直觀二是把采集程序改成“手勢(shì)標(biāo)注模式”每按一個(gè)鍵就標(biāo)記當(dāng)前標(biāo)簽和采集同時(shí)進(jìn)行這樣錄完就是完美對(duì)齊的訓(xùn)練集不用后期再手動(dòng)切割。再進(jìn)一步的話可以把C端的數(shù)據(jù)通過(guò)網(wǎng)絡(luò)送到Python端做在線推理實(shí)現(xiàn)實(shí)時(shí)的握拳、張開(kāi)、手勢(shì)控制。不過(guò)這一步的前提是C端的數(shù)據(jù)幀率要穩(wěn)定。否則模型接收到的輸入節(jié)奏亂了在線推理的精度也會(huì)跟著受影響。最后再分享一個(gè)我的實(shí)際體會(huì)做數(shù)據(jù)采集程序最容易翻車(chē)的地方不是什么高深算法而是“底層數(shù)據(jù)不透明顯性”。當(dāng)我第一次把程序跑起來(lái)看到CSV里的EMG值能隨著握拳和放松真實(shí)改變時(shí)整個(gè)項(xiàng)目的信心就建立起來(lái)了。所以別急著上復(fù)雜的濾波和模型先把采集鏈路做扎實(shí)把數(shù)據(jù)完整、可重復(fù)地落盤(pán)。這一版程序我用它連續(xù)錄過(guò)好幾個(gè)小時(shí)的手勢(shì)數(shù)據(jù)中間基本不用重啟后面處理時(shí)也幾乎沒(méi)遇到缺幀問(wèn)題。這塊停產(chǎn)手環(huán)在我看來(lái)反而是做肌電數(shù)據(jù)預(yù)處理和嵌入式C訓(xùn)練的不錯(cuò)起點(diǎn)如果你手邊恰好有一塊不妨按這個(gè)框架把采集端跑起來(lái)。