旅行好きなソフトエンジニアの備忘録

プログラミングや技術関連のメモを始めました

【アイトラッキング】 オンラインスムージングフィルタの実装

アイトラッカーから取得したデータにオンラインでスムージングフィルタをかけようとしています。Manu Kumar著 “GUIDe SACCADE DETECTION AND SMOOTHING ALGORITHM"に書いてある疑似コードを参考に実装しました。プログラムの注意点としては論文の疑似コードが分かりにくいため間違っているかもしれないこと、スレッドセーフでないためサンプリングレートの高いアイトラッカーにコードを適用した場合意図した通りに動作しないことです。

まずはアイトラッカーからのデータを格納するGazePointクラスの実装です。-演算子オーバーロードし、二点間の距離を計算できるようにしています。

public class GazePoint
{
    public double X; // X座標
    public double Y; // Y座標
    public long TimeStamp; // タイムスタンプ

    public static double operator- (GazePoint p1, GazePoint p2)
    {
        return Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));
    }

    public GazePoint Clone()
    {
        return new GazePoint() { X = X, Y = Y, TimeStamp = TimeStamp };
    }
}


次にオンラインスムージングフィルタを行うクラスをOnlineGazeFilterクラスとして実装します(2017/06/14追記:OnlineGazeFilterクラスにバグがあったため修正)。

public class OnlineGazeFilter
{
    private readonly uint _windowSize;
    private readonly double _saccadeThreshold;
    private readonly long _timeout;
    private List<GazePoint> _points = new List<GazePoint>();
    private GazePoint _currentFixation = null;
    private GazePoint _potentialFixation = null;
    private GazePoint _output = null;

    public OnlineGazeFilter(uint windowSize, double saccadeThreshold, long timeout)
    {
        _windowSize = windowSize;
        _saccadeThreshold = saccadeThreshold;
        _timeout = timeout;
    }

    public void AddPoint(GazePoint point)
    {
        // 新しいデータが前回のデータから大分時間が経っている場合は古いデータを消去する
        Reset(point.TimeStamp);
        if (_currentFixation == null)
        {
            AddPoint(point, _windowSize, ref _points);
            _points.Add(point);
            _currentFixation = ComputeFixation(_points);
            return;
        }
        if (_potentialFixation != null)
        {
            double distanceFromCurrent = point - _currentFixation;
            double distanceFromPotential = point - _potentialFixation;
            if (distanceFromCurrent <= distanceFromPotential)
            {
                if (distanceFromCurrent < _saccadeThreshold)
                {
                    // 現在の固視位置を固視し続けている
                    AddPoint(point, _windowSize, ref _points);
                    _potentialFixation = null;
                    _currentFixation = ComputeFixation(_points);
                    _output = _currentFixation.Clone();
                }
                else
                {
                    // サッカード、もしくは固視位置の変更の可能性有り
                    _potentialFixation = point;
                    _output = _currentFixation.Clone();
                }
            }
            else
            {
                if (distanceFromPotential < _saccadeThreshold)
                {
                    // 固視位置が変わった
                    _potentialFixation = point;
                    _points.Clear();
                    AddPoint(point, _windowSize, ref _points);
                    _currentFixation = _potentialFixation;
                    _output = _currentFixation.Clone();
                }
                else
                {
                    // サッカード中
                    GazePoint savedPotentialFixation = _potentialFixation.Clone();
                    _potentialFixation = point;
                    _output = savedPotentialFixation;
                }
            }
        }
        else
        {
            double distance = point - _currentFixation;
            if (distance > _saccadeThreshold)
            {
                // サッカード、もしくは固視位置の変更の可能性有り
                _potentialFixation = point;
                _output = _currentFixation.Clone();
            }
            else
            {
                // 現在の固視位置を固視し続けている
                AddPoint(point, _windowSize, ref _points);
                _currentFixation = ComputeFixation(_points);
                _output = _currentFixation.Clone();
            }
        }
        if (_points.Count > _windowSize)
        {
            _points.RemoveAt(0);
        }
    }

    private void Reset(long newTimeStamp)
    {
        if (_currentFixation != null)
        {
            long lastTimeStamp;
            if (_potentialFixation != null)
            {
                lastTimeStamp = Math.Max(_currentFixation.TimeStamp, _potentialFixation.TimeStamp);
            }
            else
            {
                lastTimeStamp = _currentFixation.TimeStamp;
            }
            if (newTimeStamp - lastTimeStamp > _timeout)
            {
                _currentFixation = null;
                _potentialFixation = null;
                _points.Clear();
            }
        }
    }

    private void AddPoint(GazePoint point, uint windowSize, ref List<GazePoint> points)
    {
        points.Add(point);
        if (points.Count > windowSize)
        {
            points.RemoveAt(0);
        }
    }

    /// <summary>
    /// スムージングフィルタをかけるメソッド
    /// </summary>
    /// <param name="fixations">視線データ群</param>
    /// <returns>フィルタ後の視線位置</returns>
    private GazePoint ComputeFixation(List<GazePoint> fixations)
    {
        double sumX = 0.0;
        double sumY = 0.0;
        double denominator = 0.0;
        for (int i = 0; i < fixations.Count; ++i)
        {
            denominator += (i + 1);
            sumX += (i + 1)*fixations[i].X;
            sumY += (i + 1)*fixations[i].Y;
        }
        return new GazePoint { X = sumX / denominator, Y = sumY / denominator, TimeStamp = fixations.Last().TimeStamp };
    }

    /// <summary>
    /// フィルタ後の視線位置を取得する
    /// </summary>
    /// <returns>フィルタ後の視線位置</returns>
    public GazePoint GetCurrentPoint()
    {
        return _output;
    }
}


最後にOnlineGazeFilterクラスの使い方を示します。

class Program
{
    static void Main(string[] args)
    {
        uint windowSize = 6;
        double saccadeThreshold = 50.0;
        long timeout = 1000;
        var filter = new OnlineGazeFilter(windowSize, saccadeThreshold, timeout);
        var data = new List<GazePoint>();
        data.Add(new GazePoint() { X = 630.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 635.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 645.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 635.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 640.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 635.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 640.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 638.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 640.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 660.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 645.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 645.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 640.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 640.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 750.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 760.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 740.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 745.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 745.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 750.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 745.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 750.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 745.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 710.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 720.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 715.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 600.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 730.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 740.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 730.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 735.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 730.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 735.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 645.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 650.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 640.0, Y = 0.0 });
        data.Add(new GazePoint() { X = 635.0, Y = 0.0 });
        var filtered = new List<GazePoint>();
        foreach (var point in data)
        {
            filter.AddPoint(point);
            var filteredPoint = filter.GetCurrentPoint();
            if (filteredPoint != null)
            {
                filtered.Add(filteredPoint);
            }
        }
        var xs = data.Select(i => i.X).ToArray();
        var filteredXs = filtered.Select(i => i.X).ToArray();
    }
}


下記図が上記コードの実行結果になります(青が生データ、オレンジがフィルタ後のデータ)。アルゴリズムの性質上どうしても遅延が避けられませんが、アイトラッカーにありがちなスパイクノイズを防いでいることが読み取れます。 f:id:ni4muraano:20170611214551p:plain