▪️前置き(これまでの流れを個人的に整理・記録しておきたいだけですので以下の▪️本題に進んでください)
今年、学会の帰りにちょうどCEDECが開催されていたので見てきました。そのときにTobii社のEyeX Controllerの存在を知りました。そのときで既に発売されて半年以上過ぎた状態でした。アーリーアダプタとは全く呼べない状態。情けない。。。もちろん、帰宅してすぐに注文。納品後、少し遊んでみました。研究で視線追跡装置が欲しかったのですが数百万円します。。。実績がないと外部研究費の獲得は難しい。でも、装置がないと始められない。。。ジレンマです。そんな私に朗報でした。使ってみるとなかなかの性能です。ひと昔前の装置とひけをとりません。解析ソフトは自作しないといけませんが、以前購入していた視線追跡関係の書籍を読んでクリアできそうです。しかし、EyeXはMacには非対応でWindowsしか使えません。最近、Windowsパソコンを買っていなかったのでUSB3.0に対応したノートは巨大なものしかありません。Visual Studioも使えないわけではないのですが、できればあまりやりたくない。そんなときに知ったのがEyeTribe社の装置でした。注文してかなり待ちましたがやっと数日前に到着したのでした。
やっと到着。eyetribe。長旅お疲れ様。 pic.twitter.com/yh5zY0JE18
— Keiichi Takahashi (@ke_takahashi) 2014, 12月 4
で、やっと時間ができたので、研究室から持ち帰ったEyeTribeをMBAに接続して、本日というか2時間ほど前から作業を始めました。
eyetribeで遊び始めた。とりあえず動かしたい。しかし。。。まず三脚つけてノートPCでどうやって設置するねん。。。という状態。とりあえずeyexと同じように画面したにおいてみる。こうなると両面テープか何かで固定しないと精度が出なさそう。。。
— Keiichi Takahashi (@ke_takahashi) 2014, 12月 6
で、ろくにマニュアルも読まずに使い方もよくわからない状態でしたが、なんとかホームページに掲載されていたJavaのサンプルは動かせました。ついでなのでProcessingでも動かしてみようと思い、あれこれ調べてとりあえず動きました。動作の保証もクレーム対応もしませんが、もしそれでもよければ使ってみてください。
▪️本題
使用方法は、上記のリンクからzipファイルをダウンロード&解凍してProcessingのlibrariesフォルダ内に入れます。あとはその中のexampleフォルダ内にeyetribe_test.pdeがありますので起動して実行してください。実行前にEyeTribeUI.appを実行してサーバーを起動しキャリブレーションを済ませておいてください。うまく動作したりしなかったりしますが。。。このEyeTribeサーバーとの関係がよくわかっていません。そのうち調べます。一応、Processing2.2.1で動作確認しています。
ちなみにeyetribe_test.pdeに以下のメソッドがありますが、これがEyeTribeから視線データがPCに到着するたびに呼ばれるメソッドで、視線データは引数のGazeDataに入っています。
void gazeDataReceived(GazeData a) { gazex = (float)a.smoothedCoordinates.x; gazey = (float)a.smoothedCoordinates.y; }
ここはEyeTribeのJavaのサンプルのonGazeUpdateメソッドと同じです。GazeDataクラスはGitHubからダウンロードしてソースを見てみてください。
といっても面倒だよ、という人もいるかもしれませんので、以下に転載しておきます。
あ、そうそう。
EyeTribeを動かしてみたときに、ずいぶん遅いなぁ。。。と思ったのです。つぶやいたらきちんと教えてくれた方がいらっしゃいました。ありがとうございます。
@ke_takahashi fpsはデフォルトだと30に設定されてますが、設定ファイルを書き換えると60に変更できます!
— Tsuda Hiroyuki (@tsuhir) 2014, 12月 6
で、設定ファイルどうやって書くんだよー、設定ファイルはどこにおけばいいんだよー、と調べたのでそれも以下に書いておきますね。ファイル名はEyeTribe.cfgです。そして置き場所は~/eyetribe/の下です。EyeTribeはWindowsで開発しているようで、Windows環境やC#のサンプルなどは充実していますがMacの情報はあまりありません。想像しながら調べていくしかないようです。それでもこんな素晴らしい装置をMacにも対応してくださったことに、ただただ感謝です。
{ "config" : { "device" : 0, "remote" : false, "framerate" : 60, "port" : 6555 } }
GazeData.java
/* * Copyright (c) 2013-present, The Eye Tribe. * All rights reserved. * * This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. * */ package com.theeyetribe.client.data; import com.google.gson.annotations.SerializedName; import com.theeyetribe.client.Protocol; /** * Contains eye tracking results of a single frame. It holds a state that defines * the quality of the current tracking and fine grained tracking details down to eye level. */ public class GazeData { /** * Set when engine is calibrated and glint tracking successfully. */ public static final int STATE_TRACKING_GAZE = 1; /** * Set when engine has detected eyes. */ public static final int STATE_TRACKING_EYES = 1 << 1; /** * Set when engine has detected either face, eyes or glint. */ public static final int STATE_TRACKING_PRESENCE = 1 << 2; /** * Set when tracking failed in the last process frame. */ public static final int STATE_TRACKING_FAIL = 1 << 3; /** * Set when tracking has failed consecutively over a period of time defined by engine. */ public static final int STATE_TRACKING_LOST = 1 << 4; public Integer state; @SerializedName(Protocol.FRAME_TIME) public Long timeStamp = 0l; @SerializedName(Protocol.FRAME_TIMESTAMP) public String timeStampString; @SerializedName(Protocol.FRAME_RAW_COORDINATES) public Point2D rawCoordinates = new Point2D(); @SerializedName(Protocol.FRAME_AVERAGE_COORDINATES) public Point2D smoothedCoordinates = new Point2D(); @SerializedName(Protocol.FRAME_LEFT_EYE) public Eye leftEye = new Eye(); @SerializedName(Protocol.FRAME_RIGHT_EYE) public Eye rightEye = new Eye(); @SerializedName(Protocol.FRAME_FIXATION) public Boolean isFixated = false; public GazeData() { timeStamp = System.currentTimeMillis(); } public GazeData(GazeData other) { this.state = other.state; this.timeStamp = other.timeStamp; this.rawCoordinates = new Point2D(other.rawCoordinates); this.smoothedCoordinates = new Point2D(other.smoothedCoordinates); this.leftEye = new Eye(other.leftEye); this.rightEye = new Eye(other.rightEye); this.isFixated = new Boolean(other.isFixated); } public GazeData clone() { return new GazeData(this); } @Override public boolean equals(Object o) { if(o instanceof GazeData) { GazeData other = (GazeData) o; return this.state.intValue() == other.state.intValue() && this.timeStamp.longValue() == other.timeStamp.longValue() && this.rawCoordinates.equals(other.rawCoordinates) && this.smoothedCoordinates.equals(other.smoothedCoordinates) && this.leftEye.equals(other.leftEye) && this.rightEye.equals(other.rightEye) && this.isFixated.booleanValue() == other.isFixated.booleanValue(); } return false; } public void set(GazeData other) { this.state = other.state; this.timeStamp = other.timeStamp; this.rawCoordinates.x = other.rawCoordinates.x; this.rawCoordinates.y = other.rawCoordinates.y; this.smoothedCoordinates.x = other.smoothedCoordinates.x; this.smoothedCoordinates.y = other.smoothedCoordinates.y; this.leftEye = new Eye(other.leftEye); this.rightEye = new Eye(other.rightEye); this.isFixated = new Boolean(other.isFixated); } /** * Contains tracking results of a single eye. */ public class Eye { @SerializedName(Protocol.FRAME_RAW_COORDINATES) public Point2D rawCoordinates = new Point2D(); @SerializedName(Protocol.FRAME_AVERAGE_COORDINATES) public Point2D smoothedCoordinates = new Point2D(); @SerializedName(Protocol.FRAME_PUPIL_CENTER) public Point2D pupilCenterCoordinates = new Point2D(); @SerializedName(Protocol.FRAME_PUPIL_SIZE) public Double pupilSize = 0d; public Eye() { } public Eye(Eye other) { this.rawCoordinates = new Point2D(other.rawCoordinates); this.smoothedCoordinates = new Point2D(other.smoothedCoordinates); this.pupilCenterCoordinates = new Point2D(other.pupilCenterCoordinates); this.pupilSize = new Double(other.pupilSize); } @Override public boolean equals(Object o) { if(o instanceof Eye) { Eye other = (Eye) o; return this.rawCoordinates.equals(other.rawCoordinates) && this.smoothedCoordinates.equals(other.smoothedCoordinates) && this.pupilCenterCoordinates.equals(other.pupilCenterCoordinates) && Double.doubleToLongBits(this.pupilSize) == Double.doubleToLongBits(other.pupilSize); } return false; } } }