編集履歴
2024年9月30日:
Pupil Capture他のバージョンが3.に上がっているようです。
LSL Relayのインストール方法が少しだけ変わっているっぽいので追記しました。
もっと前
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を使ってコントロールすることができるのですが、色々と苦労したので記録します。
環境
2024年9月30日時点:
- Windows 11
- Python 3.10.11 (64bit)
- Pupil Core(World Camera付き, 両目のカメラ)
- Pupil Capture v3.5.1 (Windows, 64bit)
もっと前
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を使います。
必要パッケージ
必要なパッケージであるzmq
とmsgpack
をインストールします。
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
をクリックしてください。
Stringを送って操作する
基本的な使い方
例として、PythonからRecordingの開始を指示してみましょう。
socket.send_string('R')
socket.recv()
1行目で、R
というStringをCaptureに送ります。1
すると、Capture側が「Record始めろ」という命令として解釈してくれるというわけです。
2行目ではCapture側からの返事を受け取っているのだと思いますが、すみません、よくわかってません
(ちなみに、「OK」という文字が帰ってきます)。
しかしこの2行目を入れないと、次の送信をすることができません。
止めるときは、R
ではなく、r
をsend_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を一旦終了させてから試しましょう。
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()
辞書型変数n
のsubject
には、命令文が入っています。
二つ目以降のKeyで、細かい指定をしているようです。
send_multipart()
を使って、二つのデータを送っています。
一つ目は、subject
の中身'eye_process.should_start.0'
に'notify.'
をつけたものです。
この文字列を、UTF-8にエンコードしています。2
二つ目は、n
をmsgpack
というパッケージでシリアライズしたものです。
最後に、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()
インスタンスを作ると、
- Captureとの接続
- Eye Camera 0の起動
- Eye Camera 1の停止(僕のにはついていないので)
- 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日追記:
アップデートによってなのか、このプラグインもデフォルトで入れてくれているようになったようです。
恐らくこのセクションに書いてある手続きは不要です。
最近のバージョンではDLするモノが変わったので追記しました(2024年9月30日)
Pupil Captureに、LSLへ対応させるためのPluginです。
まず、こちらのリポジトリをDLしましょう。
DLしたファイルのうち、pupil_capture/pupil_capture_lsl_relay
をフォルダごとPupil CaptureのPluginディレクトリに保存します。
(古いバージョンでは、代わりにpupil_lsl_relay.py
をDLして保存します)
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側からの操作については、別の記事にまとめます。
(2024年9月30日追記)なお、キャリブレーションに成功した後でないとデータが送られないようです。
……昔試したときはそんなことなかった気がするのですが、World Camera付きのやつにしたからかなぁ?
Pythonでデータを取り出す
(10月17日追記)
Pupil Coreで測定したデータは、.pldata
や.npy
といったファイル形式で保存されています。
.npy
はnp.load()
で読み込めますが、注視位置や瞳孔径といった重要な情報のほとんどは.pldata
になっているようです。
通常はPupil Playerを起動して、データの保存されているフォルダをドラッグ&ドロップして、エクスポートを選ぶことで.csv
へと変換します。
しかし毎回これをやるのはめんどくさい。
PythonからRecord Stopすると同時に.csv
で指定したディレクトリへ吐き出してほしい。
そこで、.pldata
を読み取るスクリプトを下記にご紹介します。
詳細なご説明省きますが、だいたいこんな感じで読み込めます。
色々な情報の詰まってる.pldata
から、タイムスタンプと瞳孔径のデータだe
pd.DataFrame
として取り出します。
(2020年7月3日追記:
下記のスクリプトは、.pldataの仕様が変わったらしいPupil v2.0では通用しないようです。
修正したものを下に掲載します。)
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日追記)
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の開始忘れといった心配もなくて安心です。