LoginSignup
14
9

More than 3 years have passed since last update.

pupil labs (pupil core) をPythonで操作する

Last updated at Posted at 2019-01-24

編集履歴

2020年7月3日:
Pupil Capture他のバージョンが2.に上がっていましたね。
.pldataの仕様がちょっと変わったようです。
これに対応するよう「Pythonでデータを取り出す」を修正しました。

2019年10月17日:
Pupil Labsという装置名だったと思うのですが、いつの間にかPupil Coreという名前に変わったようですね。
Pupil Labs社製のPupil Labsだったのが、Pupil Labs社製のPupil Coreになったというわけですね。
タイトルだけ修正しましたが、記事の中身ではPupil Labsのままです。
現在のVer.でも動作するか確認するとともに、微修正しました。
さらに、Pythonで.pldataを読み取る方法についても追記しました。
これでPupil Playerを使わなくても、測定情報にアクセスできます。

この記事の目的

pupil labsというアイトラッカーを買いました。
Pythonを使ってコントロールすることができるのですが、色々と苦労したので記録します。

環境

2020年7月3日時点:
- Windows 10
- Python 3.7.7 (64bit)
- pupil labs (pupil coreのこと, World Cameraなし, Mono Eye Camera)
- Pupil v2.0 (Windows, 64bit)

2019年10月17日時点:
- Windows 10
- Python 3.7.3 (64bit)
- pupil labs (pupil coreのこと, World Cameraなし, Mono Eye Camera)
- pupil_capture_windows_x64_v1.16

2019年1月24日時点:
- Windows 10
- Python 3.7.2 (64bit)
- pupil labs (World Cameraなし, Mono Eye Camera)
- pupil_capture_windows_x64_v1.10

pupil labsとは

pupil labsは、グラス型の小型アイトラッカーです。
とても軽量(最低限の仕様で29g)である上、トラッキングの精度も高いようです。
なにより、アイトラッカーの中ではかなり安価(最低限の仕様で€1,590)なのが嬉しいです。

ZeroMQというパッケージを使って、PythonからTCPを介してpupil labsをコントロールすることができます。
Recordingの開始・停止やAnnotation(トリガー)の入力がPythonからできるので、PsychoPyを使った心理実験と相性バツグンです。

また、LabStreamingLayerを介してリアルタイムにデータを送ることもできます。
バイオ(?)フィードバックにも便利そうですね。

必要なものを揃える

Pupil Capture

pupil labsで測定を行うためのGUIアプリです。
公式サイトに、Download Pupil Appsというリンクがあるので、そこからDLしましょう。
公式サイトに、DOWNLOAD PUPIL CORE SOFTWAREというリンクがあるので、そこからDLしましょう。
DLした.7zファイルを解凍して、pupil_capture的なフォルダの中にあるpupil_capture.exeを起動します。

一緒に、Pupil PlayerとPupil Serviceというソフトも解凍されるかと思います。
Pupil Playerは、Recordしたアイトラッキングを後から確認したり、.csvなどにexportするものです。
Pupil Serviceは、GUIのないPupil Captureという扱いだそうです。
今回のようにPythonで動かすのであれば、CaptureでなくServiceでよさそうなものですが、なぜかうまくいきませんでした。
今回はCaptureを使います。

必要パッケージ

必要なパッケージであるzmqmsgpackをインストールします。
pip一発です。

pip install zmq msgpack

TCPで接続する

初めに、TCPでPupil Captureに接続します。

import zmq, msgpack

context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect('tcp://localhost:50020')

5002の部分はポート番号です。
ポート番号を確認するには、Pupil Capture-Worldウィンドウの右メニューからPupil Remoteをクリックしてください。

Pupil Capture - World 2019_01_23 14_13_09.png

Stringを送って操作する

基本的な使い方

例として、PythonからRecordingの開始を指示してみましょう。

socket.send_string('R')
socket.recv()

1行目で、RというStringをCaptureに送ります。1
すると、Capture側が「Record始めろ」という命令として解釈してくれるというわけです。

2行目ではCapture側からの返事を受け取っているのだと思いますが、すみません、よくわかってません
(ちなみに、「OK」という文字が帰ってきます)。
しかしこの2行目を入れないと、次の送信をすることができません。

止めるときは、Rではなく、rsend_string()してやります。

socket.send_string('r')
socket.recv()

Session名を指定してRecord開始することも可能です。

socket.send_string('R session_no_namae')
socket.recv()

としてやると、「session_no_namae」というSession名で測定してくれます。

そのほかにできること

ほかにも、send_string()すると対応してくれるStringがいくつかあります(参照)。

String function
'R' Recordを始める
'R namae' namaeというsession名でRecord開始
'r' Recordを止める
'C' キャリブレーション(Screen Marker)を始める
'c' キャリブレーション(Screen Marker)を止める
'T 1234.56' タイムスタンプを1234.56からスタートさせる
't' タイムスタンプを取得する
'v' ソフトウェアバージョンを取得する

より複雑な命令をする

基本的な使い方

ほかにも様々な操作をPythonから行うことができますが、書き方が少々ややこしくなります。
例としてまず、Eye Cameraを起動させます。

Eye Cameraは、眼球を写しているカメラです。
仕様によって、一つか二つ備えつけられています
(World Camera のみという仕様の場合はついていません)。
これが起動していると、Pupil Capture - eye 0 というウィンドウがPupil Capture -Worldとは別に表示されます。
ちなみに、さらに二つ目のEye Cameraを起動すると、Pupil Capture - eye 1が表示されます。

Eye Camera 0を起動させるには、下記のコードを実行します。
すでにeye 0が起動しているようなら、そのウィンドウを閉じることでProcessを一旦終了させてから試しましょう。
python
n = {'subject': 'eye_process.should_start.0',
'eye_id' : 0}
socket.send_multipart([('notify.eye_process.should_start.0').encode('utf-8'),
msgpack.dumps(n)])
socket.recv()

辞書型変数nsubjectには、命令文が入っています。
二つ目以降のKeyで、細かい指定をしているようです。

send_multipart()を使って、二つのデータを送っています。
一つ目は、subjectの中身'eye_process.should_start.0''notify.'をつけたものです。
この文字列を、UTF-8にエンコードしています。2
二つ目は、nmsgpackというパッケージでシリアライズしたものです。

最後に、socket.recv()でCaptureからのお返事を聞いています。

このnの中身を変えることで、様々な操作をすることができます。
まず、もう少し使いやすいように関数化してしまいましょう。

def send_recv_notification(n, socket):
    socket.send_multipart([('notify.%s'%n['subject']).encode('utf-8'),
                           msgpack.dumps(n)])
    return socket.recv()

subjectの中身に'notify.'をつける作業を自動でやらせているだけです。
これを使って、Eye Camera 0 を終了させてやりましょう。

n = {'subject': 'eye_process.should_stop.0',
     'eye_id' : 0}
send_recv_notification(n, socket)

そのほかにできること

nの中身を変えてやることで、ほかにも様々な操作ができます。
以下に、列挙します。

# EYE CAMERA 1を起動する
n = {'subject': 'eye_process.should_start.1',
     'eye_id' : 1,}
send_recv_notification(n, socket)

# Recordingを開始する
n = {'subject'     : 'recording.should_start',
     'session_name': 'test_session'}
send_recv_notification(n, socket)
#socket.send_string('R test_session')
#socket.recv()
# と同じ

# Recordingを停止する
n = {'subject'     : 'recording.should_stop',
     'session_name': 'test_session'}
send_recv_notification(n, socket)
#socket.send_string('r')
#socket.recv()
# と同じ

# Plugin(Annotation Capture)を起動する
n = {'subject': 'start_plugin',
     'name'   : 'Annotation_Capture'}
send_recv_notification(n, socket)

他にもできることがあるかもしれません。
この「命令文」の一覧が見つからないもので…

Annotationを入れる

まず、Annotation CaptureのPluginを起動してください。
Annotation(トリガー)を入れる場合は、書き方が少し変わります。

socket.send_string('t')
ts = socket.recv()
n = {'topic'    : 'annotation',
     'label'    : 'raberu_no_namae',
     'timestamp': float(ts)}
socket.send_multipart([(n['topic']).encode('utf-8'),
                        msgpack.dumps(n)])
socket.recv()

これで、「raberu_no_namae」というAnnotationが入力されます。
ほかと違う点は二つです。
- 'subject'ではなく'topic'というキー名にする3
- send_multipart()に入れるとき、'notify.'をつけない。

'timestamp'のキーにタイムスタンプを入れてやる必要があります。
send_string('t')して、返ってきた数値を入力してやりましょう。

Annotation用の関数を作っておくと便利でしょう。

def send_annotation(n, socket):
    socket.send_string('t')
    ts = socket.recv()
    n['topic'] = 'annotation'
    n['timestamp'] = float(ts)
    socket.send_multipart([(n['topic']).encode('utf-8'),
                            msgpack.dumps(n)])
    socket.recv()

ちなみにnには、ほかにもいくつかキーを設定できます。

n = {'label'    : 'session_start',
     'duration' : 1.0,
     'source'   : 'a test script',
     'record'   : True}
send_annotation(n, socket)

追加したキーはいずれも大事な引数のように見えますが、なくても問題なく動くようです。
それぞれ指定してやると、.csvへAnnotationを出力したとき、duration, record, sourceの列ができて、指定した内容が記載されます。
うまく使えば便利、くらいのものなのかもしれません。

Classにまとめてみた

Classにまとめたら使いやすくなるかなと思って、書いてみました。
まだ書き慣れていないので、間違いなどあったら教えてほしいです。

import zmq, msgpack
class PupilLabs:    
    def __init__(self, session_name):

        def send_command(n, socket):
            socket.send_multipart([('notify.%s'%n['subject']).encode('utf-8'), msgpack.dumps(n)])
            return socket.recv()

        self.send_command = send_command
        self.session_name = session_name
        context = zmq.Context()
        self.socket = context.socket(zmq.REQ)
        self.socket.connect('tcp://localhost:50020')

        # EYE CAMERA 1 (/2) ACTIVATE
        n = {'subject': 'eye_process.should_start.0',
             'eye_id' : 0}
        send_command(n, self.socket)

        # EYE CAMERA 2 (/2) INACTIVATE
        n = {'subject': 'eye_process.should_stop.0',
             'eye_id' : 1}
        send_command(n, self.socket)

        # START PLUGIN
        n = {'subject': 'start_plugin',
             'name'   : 'Annotation_Capture'}
        send_command(n, self.socket)

    def init_recording(self):
        n = {'subject'     : 'recording.should_start',
             'session_name': self.session_name}
        self.send_command(n, self.socket)

    def stop_recording(self):
        n = {'subject'     : 'recording.should_stop'}
        self.send_command(n, self.socket)

    def send_annotation(self, label):
        self.socket.send_string('t')
        ts = float(self.socket.recv())
        n = {'topic'    : 'annotation',
             'label'    : label,
             'timestamp': ts}
        self.socket.send_multipart([(n['topic']).encode('utf-8'), msgpack.dumps(n)])
        self.socket.recv()

インスタンスを作ると、
1. Captureとの接続
2. Eye Camera 0の起動
3. Eye Camera 1の停止(僕のにはついていないので)
4. Annotation Capture Pluginの起動
を順に行ってくれます。
必要な準備を一通りやってくれるわけです。

メソッドは以下の通りです。
- init_recording() : Recordingの開始
- stop_recording() : Recordingの停止
- send_annotation(label=str): strというラベルでのAnnotation入力

使用例としては、こんな感じです。

from time import sleep
pupillabs = PupilLabs('test')
sleep(5)
pupillabs.init_recording()
for i in range(3):
    sleep(1)
    pupillabs.send_annotation('test {}'.format(i))
sleep(1)
pupillabs.stop_recording()

pupil labsからLSLでデータを送る

LabStreamingLayer(LSL)という方式で、pupil labsからPythonへリアルタイムにデータを送れます(参照)。
LSLの使い方については別の記事に譲りますが、pupil labsのデータをLSLに流すまでの手順をまとめます。

追加で必要なものを揃える

pylsl

LSLをPythonで使うためのパッケージです。
pipでどうぞ。

pip install pylsl

Pupil LSL Relay Plugin

2019年10月17日追記:
アップデートによってなのか、このプラグインもデフォルトで入れてくれているようになったようです。
恐らくこのセクションに書いてある手続きは不要です。

Pupil Captureに、LSLへ対応させるためのPluginです。
まず、こちらからpupil_lsl_relay.pyをDLします。
これを、Pupil CaptureのPluginディレクトリに保存します。
Pluginディレクトリは、下記にあります(参照)。
C:/Users/(ユーザー名)/pupil_capture_settings/plugins

続いて、先ほどpipでインストールしたpylslを、フォルダごとPluginディレクトリにコピーします。
pylslは、下記ディレクトリにあるはずです。
C:\Users\(ユーザー名)\AppData\Local\Programs\Python\Python37\Lib\site-packages

LSL Pluginを起動する

Pupil Captureを開き、Plugin ManagerからPupil LSL Relayをクリックします。
あるいは先ほどの応用で、下記のスクリプトを走らせます。

import zmq, msgpack
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect('tcp://localhost:50020')

def send_recv_notification(n, socket):
    socket.send_multipart([('notify.%s'%n['subject']).encode('utf-8'),
                           msgpack.dumps(n)])
    return socket.recv()

n = {'subject': 'start_plugin',
     'name'   : 'Pupil_LSL_Relay'}
print(send_recv_notification(n, socket))

オプションとして以下の三つが表示されます。
- Relay pupil data
- Relay gaze data
- Relay notifications
オフにすれば、その情報はLSLで流れてきません。

Pythonからオプションをオフにするためには、下記のようにします。

n = {'subject': 'start_plugin',
     'name'   : 'Pupil_LSL_Relay',
     'args'   : {'relay_pupil'        : True,
                 'relay_gaze'         : True,
                 'relay_notifications': False}}
send_recv_notification(n, socket)

これで、瞳孔サイズといった測定データを、リアルタイムにPython上にて取得することができます。
データを受け取るためのPython側からの操作については、別の記事にまとめます。

Pythonでデータを取り出す

(10月17日追記)
Pupil Coreで測定したデータは、.pldata.npyといったファイル形式で保存されています。
.npynp.load()で読み込めますが、注視位置や瞳孔径といった重要な情報のほとんどは.pldataになっているようです。
通常はPupil Playerを起動して、データの保存されているフォルダをドラッグ&ドロップして、エクスポートを選ぶことで.csvへと変換します。

しかし毎回これをやるのはめんどくさい。
PythonからRecord Stopすると同時に.csvで指定したディレクトリへ吐き出してほしい。
そこで、.pldataを読み取るスクリプトを下記にご紹介します。
詳細なご説明省きますが、だいたいこんな感じで読み込めます。
色々な情報の詰まってる.pldataから、タイムスタンプと瞳孔径のデータだe
pd.DataFrame として取り出します。

(2020年7月3日追記:
下記のスクリプトは、.pldataの仕様が変わったらしいPupil v2.0では通用しないようです。
修正したものを下に掲載します。)

v1.x用
pldata_dir = .pldataのあるディレクトリ
with open(pldata_dir / 'pupil.pldata', 'rb') as f:
    data = [[msgpack.unpackb(payload)[b'timestamp'],
             msgpack.unpackb(payload)[b'diameter'],
             msgpack.unpackb(payload)[b'diameter_3d']]
            for _, payload in msgpack.Unpacker(f)]   
data = pd.DataFrame(data, columns=['timestamp', 'diameter', 'diameter_3d'])

Pupil Playerでエクスポートした'gaze_positions.csv'と見比べても一致しています。
…ただし、gaze_positions.csvのほうでは始めの3サンプルがなくなっていました。
どういう…?

(2020年7月3日追記)

v2.0用
def pldata2dataframe(plpath, columns, method_is_3d):
    """
    Export .pldata to pandas DataFrame.

    Parameters
    ----------
    plpath: str
        Path to the pupil.pldata.
    columns: list-like
        Items to export from pupil.pldata.
    method_is_3d: bool
        Use data detected by 3D method or 2D method.

    Returns
    -------
    dataframe: pandas.DataFrame
    """

    method = '3' if method_is_3d else '2'
    with open(plpath, 'rb') as f:
        payloads = [payload for _, payload in msgpack.Unpacker(f)
                    if msgpack.unpackb(payload)['method'].startswith('3')]
    data = [[msgpack.unpackb(payload)[col] for col in columns]
            for payload in payloads]
    return pd.DataFrame(data, columns=columns)

plpath = '//XXX//000//pupil.pldata'
columns = ['timestamp', 'diameter', 'diameter_3d']
method_is_3d = True
pldata2dataframe(plpath, columns, method_is_3d)

まとめ

必要となりそうなPupil Captureの操作は、一通りPythonから行うことができました。
わざわざ測定前にPupil Captureをカチカチしなくても、PsychoPyで作った実験課題を起動するだけで準備完了しますね。
Recordの開始忘れといった心配もなくて安心です。

今回使ったコードのipynbファイル


  1. 公式ドキュメントには、send_string()ではなくsend()とあるのですが、Python3.xでこれを使うとエラーになります。 

  2. 公式ドキュメントにエンコードのことは書いていませんが、Python3.xではこれをやらないとエラーになります。 

  3. 'subject'でも一見通るのですが、Recording中にAnnotationを入れようとするとフリーズしました。 

14
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9