[譯] 響應式腦電波—如何使用 RxJS、Angular、Web 藍芽以及腦電波頭戴裝置來讓我們的大腦做一些更酷的事

SangKa發表於2019-02-02

原文連結: medium.com/@urish/reac…
本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!
如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】

幾個月前,我偶然間發現了一臺藍芽智慧腦電波頭戴裝置。我突然意識到它的巨大潛力,使用它可以做一些超級酷的事情:使用 Web 藍芽,可以直接用大腦與網頁進行通訊!

腦電波 ( Electroencephalography,簡稱 EEG ) 本質上是監控腦電活動的一種方式。它通常需要在頭皮上放置幾個電極,然後收集關於神經元發射的資訊,最後將資訊記錄在圖表上。聽起來像是一些想當不錯的資料可供我使用!雖然腦電波主要用於醫療用途,但仍會不時出現一些新穎的使用案例。

其中一個新穎的使用案例便是 Muse,它是一種消費產品,花費$250便可以幫助你學習如何進行冥想,同時它還是自帶藍芽、消耗腦電波的實體裝置。雖然它能夠教會你如何平靜下來,但對我來說,只有弄清楚如何在網頁上消費這些資料後,我才能平靜下來!

(如果你也無法保持平靜的話,可選擇略過此部分,直接檢視下面的程式碼教程 ;-)

頭戴裝置配備 Android 或 IOS 應用,甚至還提供了一個庫,這樣你就可以獲取原始資料並構建自己的應用,但這個庫只能在原生應用中執行,而且原始碼不是開源的 (因此,我想用大腦控制網頁的夢想起初看來是視乎是無法達成的的)。

在參加 ng-cruise 時,我遇到了 Alex Castillo,他的演講展示瞭如何將他叫做 OpenBCI 的開源硬體腦電波頭戴裝置與 Angular 進行連線並將訊號視覺化。儘管這一切令人印象深刻,但他不得不使用 node.js 進行復雜的設定和 Web socket 伺服器來傳播資料,這離我想要的還有一定差距。後來在 ng-cruise 的黑客之夜,每個人都在嘗試使用各種硬體裝置來做一些很酷的東西,這些裝置中就包括腦電圖裝置,所以我自然不會錯過如此良機。

我嘗試對 Muse 的藍芽協議進行逆向工程,類似於這篇文章所做的。大約進行了一個小時,我想到之前可能有人已經做到了,所以我 google 了我所發現的一個特徵數字,並找到了這篇超棒的文章,反過來這篇文章指出了由 Alexandre Barachant 建立的 python 庫,突然間,我擁有了我所需的一切:這就是 muse-js 誕生的過程。

所以現在我可以將 Web 和 Muse 頭戴裝置進行連線並接受腦電波資料 (還包括電池電量、加速計/陀螺儀,等等)。萬歲!

那麼接下來我要用它做什麼呢?

硬體

在深入程式碼之前,我們首先來了解下 Muse 頭戴裝置。基本上,它就是一個輕量級的可充電頭帶。它配備了4個腦電波電極:2個在前額,眼睛稍微往上一些,另外2個與耳朵接觸。此外,它還配備了螺旋儀和加速計,這樣可以計算出頭的方位。我很高興我發現了它還有另外一個腦電波感測器,這樣就可以連線到自己的電極了 (儘管是 Micro USB 介面),我打算儘快進行嘗試。

注意頭帶有兩個版本:2014款和2016款。你想要的肯定是2016款,它使用了藍芽低耗能。2014款使用的是經典藍芽,因此無法與 Web 藍芽一起使用。

Muse 2016: AF7 和 AF8 是前額電極, TP9 和 TP10 是耳電極

使用 RxJS 的響應流

構建庫時,我需要決定如何暴露傳入的腦電波資料。使用 Web 藍芽,每當接收到新的資料包時都會觸發一個事件。每個資料包包含來自單個電極的12個樣本。我本可以讓使用者註冊一個 JavaScript 函式,每當接收到新資料時便呼叫此函式,但我最後決定使用 RxJS 庫 (JavaScript 的響應式擴充套件庫),它包括用於轉換,組合和查詢資料流的各種方法。

RxJS 的優勢是它提供了一組函式,可讓你操縱和處理從 Muse 頭戴裝置接收到的原始資料位元組,以便將其轉換為更有用的東西 (比如我們馬上要做的)。

視覺化

首先映入腦海的便是使用我們全新的 muse-js 視覺化資料。黑客之夜當晚,Alex 和我開始開發 angular-muse,這是一個 Angular 應用,它可以將腦電波資料和頭部方向進行視覺化。

我的 Muse 資料視覺化初始原型

事實上,如果你擁有 Muse 裝置和 支援 Web 藍芽的瀏覽器,你便可以實際開啟 Demo 頁面親自嘗試!

使用 Muse、 Angular 和 Smoothie Charts 將我的大腦活動進行視覺化

這個應用以一種簡單的方式證明了資料是流式傳輸,但老實說,檢視資料圖確實能夠吸引人,但如果只是這樣而已,那麼你將很快失去對它的興趣。

關於眨眼

腦電波所做的眾多事情之一便是測量頭皮上不同位置的電勢 (電壓)。測量的訊號是大腦活動的副作用,可用於檢測一般心理狀態 (如濃度水平、突發刺激的檢測,等等)。

除了大腦活動之外,還可以使用稱為眼球電圖檢查 (幸運的是,我的女朋友就是驗光師,她能夠教我很多這方面的知識) 的技術來檢測眼部運動。Muse 裝置有兩個電極位於前額 (在標準的 10-20定位系統中稱為 AF7 和 AF8),它們靠近雙眼,所以我們能夠輕而易舉地監控眼部運動。

我們的眼睛:角膜前方帶正電,視網膜背部帶負電

我們將使用這些電極的訊號作為我們腦電圖程式的 “Hello World”, 該程式會通過監測眼睛活動來檢測眨眼。

開始編碼!

我們的開發思路如下:我們從裝置中獲取傳入的腦電波樣本流 (如上所述,muse-js 將提供 RxJS Observable),然後過濾出我們所需的 AF7 電極 (也就是左眼),再然後我們會在訊號中找尋峰值,例如,絕對值超過500mV的樣本意味著發生了大變化。由於電極在眼睛旁邊,我們期望眼球的運動產生顯著的電勢差。

雖然這可能不是檢測眨眼最準確的方法,但它對我來說非常有用,並且程式碼簡單易行 (就像所有優秀的 “Hello World” 示例那樣 ;-) 。

但在開始之前,首先需要在專案中安裝 muse-js...

npm install --save muse-js複製程式碼

...然後在程式碼中進行匯入。在這個示例中,它是一個 Angular 應用,其實只是用 Angular CLI 建立的空專案,但也可以使用 React/VueJS,隨你喜歡,因為很少會有框架相關的程式碼。

接下來,我們將 muse-js 匯入到應用的根元件中:

import { MuseClient, channelNames } from `muse-js`;複製程式碼

MuseClient 類與頭戴裝置進行互動,channelNames 只是提供腦電圖頻道的對映,供開發者使用。

在元件中,我們會建立一個 MuseClient 的例項:

this.muse = new MuseClient();複製程式碼

現在我們將進入略微有些棘手的部分:連線頭戴裝置的邏輯。

Web 藍芽需要一些使用者互動,才能夠啟動連線,所以我們需要新增按鈕,並只有當使用者點選該按鈕時才實際去連線頭戴裝置。我們在 onConnectButtonClick 方法來實現連線邏輯:

async onConnectButtonClick() {
  await this.muse.connect();
  this.muse.start();
  // TODO: 訂閱腦電波資料
}複製程式碼

MuseClient 類例項的 connect() 方法啟動與頭戴裝置的連線,start() 方法命令頭戴裝置開始對腦電波資料進行取樣並將其傳送到電線上。

使用 Web 藍芽與 Muse 頭戴裝置配對

接下來我們需要訂閱 muse.eegReadings observable 上的腦電波資料 (這段程式碼放到上面的 TODO 註釋處):

const leftEyeChannel = channelNames.indexOf('AF7');

this.leftBlinks = this.muse.eegReadings
  .filter(r => r.electrode === leftEyeChannel)複製程式碼

上面的程式碼接收來自裝置的腦電波讀數,並過濾出位於左眼上方的 AF7 電極。每個資料包包含12個樣本,observable 流中每一項都是具有以下結構的物件:

interface EEGReading {
  electrode: number;
  timestamp: number;
  samples: number[];
}複製程式碼

electrode 包含電極的數字索引 (使用 channelNames 陣列對映出更友好的名稱),timestamp 包含相對於記錄開始時取樣的時間戳,samples 是12個浮點數的陣列,每項都是一個腦電波測量,以 mV (微伏) 為單位。

下一步,我們只想得到每個資料包中的最大值 (例如,最大輸出值的測量)。我們使用 RxJS 中的 map 操作符:

this.leftBlinks = this.muse.eegReadings
  .filter(r => r.electrode === leftEyeChannel)
  .map(r => Math.max(...r.samples.map(n => Math.abs(n))))複製程式碼

所以現在我們擁有一個簡單的數字流,我們可以過濾出值大於500的數字,那很可能就是我們正在找尋的眨眼:

this.leftBlinks = this.muse.eegReadings
    .filter(r => r.electrode === leftEyeChannel)
    .map(r => Math.max(...r.samples.map(n => Math.abs(n))))
    .filter(max => max > 500)複製程式碼

到這裡,我們有了一個簡單的 RxJS 管道,它用於眨眼檢測,但為了實際開始接收資料,我們還需要訂閱它。我們從一個簡單的 console.log開始:

this.leftBlinks.subscribe(value => {
  console.log('Blink!', value);
});複製程式碼

如果執行程式碼,你可能會看到大量的 “Blink!” 出現,直到你將頭戴裝置戴上,因為會有很多的靜態噪音。一旦你穿戴好了你的裝置,只有當你眨眼或觸控左眼時,才應該會看到 “Blink!” 訊息的出現:

哇,它真的有效果!

每當你眨眼時,你可能會看到若干 “Blink!” 出現在控制檯中。原因是眨眼會另電勢產生變化。為了必要出現過多的 “Blinks!”,我們需要進行去抖動過濾 ( debounce ),類似於這篇文章 所做的。

我們來做最後的補充:我們不再將資訊列印到控制檯,而是當眨眼時我們實際發出值1,然後再最後一次電勢改變後等待半秒再發出值0。這會過濾掉我們所看到的多餘的 “Blink!”:

this.leftBlinks = this.muse.eegReadings
    .filter(r => r.electrode === leftEyeChannel)
    .map(r => Math.max(...r.samples.map(n => Math.abs(n))))
    .filter(max => max > 500)
    .switchMap(() =>
      Observable.merge(
        Observable.of(1),
        Observable.timer(500).map(() => 0)
      )
    );複製程式碼

那麼 switchMap 到底施了什麼魔法?簡單來說,每當一個新項到達時,switchMap 會拋棄前一個流並呼叫給定的函式來產生新的流。新的流由兩項組成:第一個是值1,它是由 Observable.of 立即發出的,第二個是值0,它在500毫秒之後發出,但如果一個來自 filter 管道中的新項到達的話,將重新啟動 switchMap 並拋棄前一個流中仍未發出的值0

現在我們可以使用 leftBlinks observable 來對眨眼進行視覺化!可以使用 async pipe 將它繫結到 Angular 模板中:

<span [hidden]="leftBlinks|async">?</span>複製程式碼

每當眨眼時,上面的程式碼會隱藏眼睛符號,或者我們可以切換 CSS 類,然後在閃爍時對眼睛符號進行顏色改變或執行動畫:

<span [class.blink]=”leftBlinks|async”>?</span>複製程式碼

無論採用哪種方式,我建議每次只眨一隻眼睛,這樣可以確保你能觀察到你的程式碼是否正常工作?!

如果我們構建的是 React 應用,可以直接訂閱 observable 並在眨眼時更新元件的 state :

this.leftBlinks.subscribe(value => {
  this.setState({blinking: value});
});複製程式碼

現在我們做到了!腦電波的 “Hello World” 已經完成!

專案的完整程式碼在這裡

總結

幾年前,腦電波還是很昂貴的,笨重的裝置只能用於醫院和研究機構。如今,像你我一樣的 Web 開發者都可以使用我們每天都在使用的開發工具 (瀏覽器、RxJS 和 Angular ) ,輕而易舉地來連線和分析腦電波資料。

即使腦電波不是你的菜,你可以清楚地看到,由於各種“智慧”消費品的推動,已經為開發者創造了一系列真正的好機會。我們確實生活在一個令人振奮、每天都充滿驚喜的年代!

備註: 十分感謝 Ben Lesh 幫忙完善這些示例中的 RxJS 程式碼。

相關文章