Junpei Qawasaki

博報堂アイ・スタジオという会社でテクノロジーをベースに新規事業とかインタラクティブなものとかを色々とやっております

【EYE TRIBE】視線でのマウス操作をスムーズにしてみた

なんとEYETRIBEを借りました。
起動にはSDKが必要だったりするのですがそのあたりが知りたい方は貸主の1Fさんブログをご覧下さい。

EYETRIBEを起動してみた!EYETRIBEを起動してみた! | HACKist

開発環境はVisualStudioなので別に未知の領域ではないのですが正直C#はそんなに得意じゃないです。
なので無茶はしないでとりあえず講演用にマウス操作を視線で行いたいな、と。

もともとのソースはこちら
TheEyeTribe - The Eye Tribeで視線でマウスカーソル動かす(C#) - Qiita
から拝借しました。参照設定とかも書かれているのですごく参考になります。

ただこの参考ソースはセンサーの値がそのままなので動きがピーキー過ぎて使いこなせません。
そこで、センサーからのとった値の移動平均値計算処理いれました。

現在の値 = 0.9 * ひとつ前の値 + 0.1 * センサの値

所詮は暫定対処のため実用化、というほどのクオリティには届いていませんが「視線操作出来てる体験」は出来るくらいになりました。でもちょっと丸められすぎなのかも。

加重平均値という話も出たけれど多分、手間をかけてもそこまで改善する気がしないので今回はやめておきます。

合わせて右目閉じたらクリックって処理にしてみたのですが、こちらもコンソールログみると拾い過ぎなので5回に1回の割合で拾ってます。

ソース全体はこんな感じ。

using System;
using TETCSharpClient;
using TETCSharpClient.Data;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public class EyeCursorControl : IGazeListener
{
    public double before_screenX = 0; //一つ前の座標(平均値用)
    public double before_screenY = 0;
    public double lowpassX = 0;
    public double lowpassY = 0; 

    public int CursorX = 0; //最終的にカーソル位置になる座標
    public int CursorY = 0;
    public int eyeclose_cnt = 0; //右目閉じカウンタ

    public bool Enabled { get; set; }
    public bool Smooth { get; set; }
    public Screen ActiveScreen { get; set; }

    public EyeCursorControl() : this(Screen.PrimaryScreen, false, false) { }

    public EyeCursorControl(Screen screen, bool enabled, bool smooth)
    {
        GazeManager.Instance.AddGazeListener(this);
        ActiveScreen = screen;
        Enabled = enabled;
        Smooth = smooth;
    }

    public void OnGazeUpdate(GazeData gazeData)
    {
        if (!Enabled)
        {
            return;
        }

        // start or stop tracking lost animation
        if ((gazeData.State & GazeData.STATE_TRACKING_GAZE) == 0 &&
           (gazeData.State & GazeData.STATE_TRACKING_PRESENCE) == 0)
        {
            //Console.WriteLine("start or stop tracking lost animation");
            return;
        }

        // tracking coordinates
        var x = ActiveScreen.Bounds.X;
        var y = ActiveScreen.Bounds.Y;
        var gX = Smooth ? gazeData.SmoothedCoordinates.X : gazeData.RawCoordinates.X;
        var gY = Smooth ? gazeData.SmoothedCoordinates.Y : gazeData.RawCoordinates.Y;

        //センサ値
        var sensorX = (int)Math.Round(x + gX, 0);
        var sensorY = (int)Math.Round(y + gY, 0);
        Console.WriteLine(gX + "," + gY);

        if (gazeData.RightEye.SmoothedCoordinates.X == 0 && gazeData.RightEye.SmoothedCoordinates.Y == 0 && eyeclose_cnt > 5)
        {
            Console.WriteLine("right eye close done!");
            eyeclose_cnt = 0;
            NativeMethods.LeftClick();
        }
        else if (gazeData.RightEye.SmoothedCoordinates.X == 0 && gazeData.RightEye.SmoothedCoordinates.Y == 0 && eyeclose_cnt < 5)
        {
            Console.WriteLine("right eye close count");
            eyeclose_cnt++;
        }
        else
        {
            //指数移動平均計算
            lowpassX = 0.9 * before_screenX + 0.1 * sensorX;
            lowpassY = 0.9 * before_screenY + 0.1 * sensorY;

            //カーソル命令のメソッドがintじゃないと落ちるのでcast
            CursorX = (int)lowpassX;
            CursorY = (int)lowpassY;

            //スクリーン座標にマウスポイントを移動
            NativeMethods.SetCursorPos(CursorX, CursorY);

            //ひとつ前の値を保持しとく
            before_screenX = lowpassX;
            before_screenY = lowpassY;
        }
    }

    public class NativeMethods
    {
        [System.Runtime.InteropServices.DllImportAttribute("user32.dll", EntryPoint = "SetCursorPos")]
        [return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)]

        public static extern bool SetCursorPos(int x, int y);

        [DllImport("user32.dll")]

        private static extern void SendInput(int nInputs, ref INPUT pInputs, int cbsize);

        public static void LeftClick()
        {
            const int num = 2;
            INPUT[] inp = new INPUT[num];

            // マウスの左ボタンを押す
            inp[0].type = INPUT_MOUSE;
            inp[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
            inp[0].mi.dx = 0;
            inp[0].mi.dy = 0;

            inp[0].mi.mouseData = 0;
            inp[0].mi.dwExtraInfo = 0;
            inp[0].mi.time = 0;

            // マウスの左ボタンを離す
            inp[1].type = INPUT_MOUSE;
            inp[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
            inp[1].mi.dx = 0;
            inp[1].mi.dy = 0;
            inp[1].mi.mouseData = 0;
            inp[1].mi.dwExtraInfo = 0;
            inp[1].mi.time = 0;

            SendInput(num, ref inp[0], Marshal.SizeOf(inp[0]));
        }

        private const int INPUT_MOUSE = 0;                  // マウスイベント
        private const int MOUSEEVENTF_MOVE = 0x1;           // マウスを移動する
        private const int MOUSEEVENTF_ABSOLUTE = 0x8000;    // 絶対座標指定
        private const int MOUSEEVENTF_LEFTDOWN = 0x2;       // 左 ボタンを押す
        private const int MOUSEEVENTF_LEFTUP = 0x4;         // 左 ボタンを離す

        [StructLayout(LayoutKind.Explicit)]
        private struct INPUT
        {
            [FieldOffset(0)]
            public int type;
            [FieldOffset(4)]
            public MOUSEINPUT mi;
        };

        // マウスイベント(mouse_eventの引数と同様のデータ)
        [StructLayout(LayoutKind.Sequential)]
        private struct MOUSEINPUT
        {
            public int dx;
            public int dy;
            public int mouseData;
            public int dwFlags;
            public int time;
            public int dwExtraInfo;
        };
    }

    static void Main(string[] args)
    {
        GazeManager.Instance.Activate(GazeManager.ApiVersion.VERSION_1_0, GazeManager.ClientMode.Push);
        if (!GazeManager.Instance.IsConnected)
        {
            Console.WriteLine("EyeTribe Server has not been started");
        }
        else if (GazeManager.Instance.IsCalibrated)
        {
            Console.WriteLine(GazeManager.Instance.LastCalibrationResult);
            Console.WriteLine("Re-Calibrate");
        }
        else
        {
            Console.WriteLine("Start");
        }
        EyeCursorControl cursor = new EyeCursorControl(Screen.PrimaryScreen, true, true);
    }
}