Unityでブレイン・コンピュータ・インターフェース(BCI)開発 - チュートリアル

ブレイン・コンピュータ・インターフェース(BCI)をUnityで実装するチュートリアルです。

Unity、BCIとは何か、についてはUnityの公式記事があったので読んでみてください。

https://note.com/unityjapan/n/n6ca815cfee37

本記事では脳波を使ったBCIを想定していますが、脳波(成分)の基本的な説明や判別アルゴリズムの実装方法については解説しません。ソフトウェアの実装方法のみ解説します。

脳波処理については、[「脳波処理とブレイン・コンピュータ・インタフェース: 計測・処理・実装・評価の基礎」](https://www.amazon.co.jp/dp/4339014044)がおすすめです。

# 読者の想定レベル
+ プログラミング初心者(授業等でプログラミングを習った程度)
+ Unityの入門書籍を読み終え、自分でコードを書いたことがある。
+ Pythonの超入門書籍・ブログを読み終えている。(プログラミングできなくても大丈夫です)

UnityとPythonの基本的な使い方や文法の説明は割愛しています。

# 前提
+ 実際にBCIを操作する場合は、脳波を測定できる脳波計という装置が必要です。個人での購入は難しいと思うので、脳波計を所有している研究室を探して訪ねてください。
+ 本チュートリアルは脳波計が無くても大丈夫です。(実際の脳波の代わりにダミーデータを生成し、利用します)

# 流れ

1. BCI2000のセットアップ
   + 1から全てを作るのは無謀なので、フレームワークを使用します。  
1. UnityBCI2000の導入
   + BCI2000とUnityを繋げる説明です。
1. BCPy2000の導入
   + BCI2000とPythonを繋げる説明です。
1. BCI開発チュートリアル


# BCI2000のセットアップ
## BCI2000とは
BCIアプリを1から作るのは大変ですし、1から作っても車輪の再発明になってしまう部分(例えばB社の脳波計から信号を取得する処理)があるので、今回は、オープンソースソフトウェアとして提供されているBCI2000を利用します。
※BCI2000はUnityとは全く別のものです。

https://www.bci2000.org/mediawiki/index.php/Main_Page

BCI2000は、BCIに必要な次の機能を提供してくれます。

1. 信号取得モジュール : 特定の脳波計から脳波信号を取得する処理を担います。
    + 例
        + RDAClient : BrainVision Recorder(脳波測定ソフトウェア)から情報を受け取るモジュール
        + SignalGenerator : ダミー信号を生成するモジュール
1. 信号処理モジュール : 脳波信号から必要な情報を解読する処理を担います。
1. アプリケーションモジュール : 必要な情報に応じて画面に表示したり音を鳴らしたりロボットを動かしたりする処理を担います。
+ オペレータモジュール : 1,2,3のモジュールを管理する処理を担います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/a28e20c9-0fd7-22f0-224f-755f58dc5185.png)
> 出典 : https://www.bci2000.org/mediawiki/index.php/Technical_Reference:System_Design

## BCI2000セットアップ
まずは、BCI2000のセットアップから始めます。  
一般的なソフトウェアは、実行ファイルやインストーラをダウンロードしてポチポチ画面の指示に従えば使えるようになりますが、BCI2000は設計図の状態でダウンロードされるので、自分で使えるように組み立てる必要があります(Unityで例えるならビルド)。このビルドの前に使いたい機能を色々と追加する必要があります。
一応ビルド後の状態でも配布されていますが、何故か更新が全然されていなくてかなり古いので本記事では使えません。

セットアップ方法は、こちらのNotionに翻訳+補足したのでやってみてください。**5番の「BCI2000をコンパイル」** まで終えたら、この記事に戻ってきてください。多分1時間くらいはかかると思います。

https://fast-magpie-e3a.notion.site/BCI2000-93a044a56d264d338ff7e1417fd3dbd9

元の公式記事は[こちら](https://www.bci2000.org/mediawiki/index.php/Programming_Howto:Building_and_Customizing_BCI2000)

# UnityBCI2000の導入
BCI2000の標準アプリケーションモジュールを使うこともできるのですが、便利な開発ツールを使ったほうが楽なので、Unityを使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/86c32628-d026-0057-e9e0-ead3460f659a.png)

Unityを使うとなると、UnityとBCI2000を繋げる処理が必要になります。

## UnityBCI2000
UnityBCI2000はこの繋げる処理を担う、Unity側に配置するモジュールです。このモジュールは、去年開発者が突然やる気をだして設計を見直したことで使いやすくなった一方、原形がなくなりました。
そのため、現在あるBCIUnity2000のチュートリアルは英語版含め見直し前で情報が古いので注意してください。

本記事では、2024年4月時点での最新版に対応しています。

### 導入の流れ
1. Unityプロジェクトの作成
1. UnityBCI2000のインストール

#### 1. Unityプロジェクトの作成
普通にUnityプロジェクトを作成してください。

#### 2. UnityBCI2000のインストール
本家のリポジトリをダウンロードしてプロジェクトのAssets下にファイルを置く。
以下からダウンロードしてください。

https://github.com/neurotechcenter/UnityBCI2000

たまに開発者がリポジトリにある`BCI2000RemoteNET.dll`だけ更新を忘れ、動かなくなったりするので[リリースページ](https://github.com/neurotechcenter/UnityBCI2000/releases)からダウンロードしても良いです。

設定等は後述します。

## BCPy2000の導入
次にPythonを使って信号処理モジュールを作成します。
理想はUnity単体でBCIを動かすことですが、情報が整理できていないのでとりあえずPythonを使用します。UnityBCI2000のようにPythonとBCI2000を繋げるモジュールです。

![名称未設定のデザイン (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/39f36c38-863c-5a34-8efd-d92408cc3f58.png)

BCPy2000のセットアップはこちらにまとめました。長いので割愛します。

https://fast-magpie-e3a.notion.site/BCPy2000-0c033961e0f84f87bf81f5026ed44ecc?pvs=74

BCPy2000はメンテ頻度に対して機能が多すぎる(あとリポジトリが重すぎる)ので、軽量なSlim版を作成中です。

# BCI開発チュートリアル
ここまでがセットアップです。これからこれらの機能を使ってUnityでBCIを開発するチュートリアルをします。

**セットアップが完了している前提で話を進めます。**

### チュートリアル用Unityプロジェクト
以下のリポジトリをダウンロードしてUnityで開いてください。

https://github.com/s-n-1-0/UnityBCI2000Tutorial

### 1. 初期設定
#### UnityBCI2000コンポーネント
Assets直下にある`TutorialScene`を開くと`BCI2000`オブジェクトがHierarchyに展開されます。BCI2000オブジェクトを選択すると、BCI2000の設定項目が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/79b6bb1a-c155-1bf1-67df-2b777de8b12e.png)

重要な項目のみ説明します。
+ **OperatorPath** : BCI2000のある場所を指定する項目です。BCI2000(フォルダ)内にあるOperator.exeのパスを指定してください。
+ **Timeout** : 通信がタイムアウトする秒数(ミリ秒)です。PCのスペックによってさらに大きい値を指定する必要があるかもしれません。
+ **Module1** : 信号生成/取得モジュールを指定します。脳波計の種類によって異なります。
    + SignalGeneratorを置き換えてください。 
+ **Module2** : 信号処理モジュールを指定します。必ず**PythonSignalProcessing**にしてください
+ **Module2 Args** : 次のコマンドを指定してください。コマンドの意味はBCPy2000のドキュメントを参照してください。
   + `PythonSigClassFile=tutorial_processing/BciSignalProcessing.py`
       + これはBCI2000内にあるpythonフォルダの中身を指定します。
    + `PythonSigShell=0`
    + `PythonSigLog=Siglogger.txt`
        + この2つはとりあえず指定してください。
+ **Module3** : 必ず**DummyApplication**を指定してください。
+ **InitCommands** : Pythonを起動する下のコマンドを指定してください。
    + `execute script ../batch/FindPortablePython.bat`
        + これによってUnityがBCI2000を起動し、BCI2000がPythonを起動します。
#### Pythonに信号処理コードを置く
以下のコードを**BciSignalProcessing.py**という名前で保存し、
BCI2000内pythonフォルダに**tutorial_processing**というフォルダを作成してその中に入れてください。
```python
from pathlib import Path
import os
import sys
import random
import time
import threading

sys.path.append(os.path.join(Path().resolve(), "tutorial_processing"))


class BciSignalProcessing(BciGenericSignalProcessing):
	def Construct(self):
		parameters = [
			
		]
		states = [
			"predictClass  3 0 0 0"
		]
		return (parameters, states)
		
	def Preflight(self, sigprops):
		pass
	
	def Initialize(self, indim, outdim): #indim[0] = ch
		pass
	
	def StartRun(self):
		thread = threading.Thread(target=processing,args=[self])
		thread.start()

	def Process(self, stream_sig):
		# Unityへ渡す変数
		self.states['predictClass'] = self.predict_class
        # Unityから受け取る変数
		trial_num = self.states['trialNum']
		
	
	def StopRun(self):
		pass
		
def processing(module:BciSignalProcessing):
	while True:
		module.predict_class = random.randint(0,1)
		time.sleep(0.5)
```
#### 接続確認1 BCI2000が起動できる
OperatorPathが適切ならPlay時に以下のウィンドウが起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/ff2398b3-c3aa-a2b9-fea3-8588a8c55aa1.png)
※Playを終了しても勝手にウィンドウは閉じないので手動で閉じてください。

#### 接続確認2 Pythonと接続できる
適切に設定できているなら↑のスクリーンショットの下にあるように**PythonSignalProcessing:running**と表示されます。
うまくいかないと赤字でエラーが出ます。


### 2. Pythonから信号処理結果を受け取る
Pythonは前述したコードによって「predictClass」という変数名でBCI2000を介してUnityに値を飛ばしています。Unity側ではこれを受け取れるようにします。

#### 手順
1. BCI2000オブジェクトにSampleReceiverコンポーネントをアタッチします。
1. 実行(play)します。
1. 受け取った0と1のランダムな値がログに出力されます。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515818/4563ba1a-1562-1f90-a940-fdc409e055e4.png)

SampleReceiverコンポーネントの中身を示します。
```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleReceiver : MonoBehaviour
{
    UnityBCI2000 bci2000;
    void Start()
    {
        bci2000 = GetComponent<UnityBCI2000>();
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log(bci2000.GetState("predictClass"));
    }
}
```

見てわかる通り、UnityBCI2000コンポーネントのGetStateメソッドに変数名を指定して呼ぶことで値を取得することができます。


### 3. Pythonへ渡す
逆にPythonへ値を渡したい場合もあるかもしれません。
SampleSenderコンポーネントをBCI2000オブジェクトへアタッチすると、trialNumという変数の値が単調増加しながらPythonへ渡されます。
ちゃんと渡されているか確認するには、Python側でログを出すか、BCI2000の変数監視機能を使ってみることができます。

```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleSender : MonoBehaviour
{
    CustomSetVariable trialNum;
    int count = 0;
    void Start()
    {
        var bci2000 = GetComponent<UnityBCI2000>();
       trialNum = new CustomSetVariable(bci2000, "trialNum");
    }

    // Update is called once per frame
    void Update()
    {
        trialNum.Update(count++);
    }
}


class CustomSetVariable
{
    UnityBCI2000 bci2000;
    string keyName;
    public CustomSetVariable(UnityBCI2000 bci2000, string keyName)
    {
        bci2000.AddState(keyName);
        this.keyName = keyName;
        this.bci2000 = bci2000;
    }
    public void Update(int newValue)
    {
        if (newValue != value)
        {
            bci2000.SetState(keyName, newValue);
            value = newValue;
        }
    }
    public int value { get; private set; } = -1;
}
```

---
以上でBCI(モドキ)を構築することができます。実際にはPythonで信号処理を実装する必要がありますが、アルゴリズムの説明からしなければいけないので割愛しています。

もしチュートリアル後、ちゃんとBCIを作るならば

1. 脳波の信号処理や分類に必要な知識を学習
1. とりあえずキーボード等を使って操作できる通常のアプリケーションを作る
1. 信号処理を実装する
1. 2で作ったものをBCI2000を使って脳波用に置き換える
1. (研究ならばログ機能なども実装する)

になると思います。