Tobii XR Devzone > Develop > Tobii Ocumen > Ocumen Filters > Unity/C# >

Ocumen Filters - Getting Started in Unity/C#

This page details the installation of Ocumen and offers a tutorial on getting started with Ocumen Filters in Unity/C#. The tutorial demonstrates using the Fixation Dispersion Angles filter, with a similar approach applicable to other filters.

The entire C# script for this tutorial is available in Step 7.

Table of Contents

Step 1: Download and install Ocumen I/O for Unity with the Filters package

Follow the installation steps in Ocumen for Unity Getting Started and install the Filters package in step 3.3.

Step 2: Create a C# script and add a reference to a .ocumen recording

In a blank C# script in your Unity project, add a Windows path reference to a .ocumen recording file. You can create your own .ocumen recording by following the getting started steps in Ocumen for Unity Getting Started or you can use the test recording located in the Ocumen Studio download. The windows path to the recording can be absolute (eg. C:\Users\<YourUsername>\Documents\sample-recording.ocumen) or relative (eg. ../Recordings/sample-recording.ocumen).

private string SampleRecordingPath = "C:/Users/<YourUsername>/Documents/Ocumen Recordings/sample-recording.ocumen";

Step 3: Get gaze data with timestamps from the recording

Retrieving gaze data from a recording currently requires many steps but this will be streamlined in future versions.

The following function can be copied into your C# script. It takes a .ocumen recording path as input and outputs binocular gaze values with timestamps, split into two arrays.

private void GetBinocularGazeDataFromRecording(string recordingPath, out long[] binocularGazeTimestamps, out BinocularGaze[] binocularGazeData)
{
    // Load the recording.
    var ocumenConnector = WorldConnector.FromFile(System.IO.Path.GetFullPath(recordingPath));
    ocumenConnector.CheckCompatible();
    var ocumenRecording = WorldReader.FromConnector(ocumenConnector.Context);
    ocumenRecording.ReadIncremental();
    
    // Get recording data for the first user in the recording.
    var user = ocumenRecording.ListUsers().Copied[0];
    
    // Get all binocular gaze timestamps in the recording.
    binocularGazeTimestamps = ocumenRecording.TrackTimes(user, TrackedData.GazeBinocular).Copied;

    // Create a variable which defines a user and a time range for which you want to get data. Assign the user and set firstInclusive to 0.
    var userInTimeRange = new UserInTimeRange() {userId = user, firstInclusive = 0};

    // Initialize a final gaze data array which will hold nullable values instead of OptionTimeValueBinocularGaze values.
    binocularGazeData = new BinocularGaze[binocularGazeTimestamps.Length];

    // Loop through each timestamp.
    for (int i = 0; i < binocularGazeTimestamps.Length; i++)
    {
        // To get data at time i, set the time range's lastInclusive time to i.
        userInTimeRange.lastInclusive = binocularGazeTimestamps[i];
        
        // Get the binocular gaze data at time i by getting the most recently added (latest) entry in the defined time range.
        var latestBinocularGazeData = ocumenRecording.LatestGazeBinocularFor(userInTimeRange).ToNullable();
        
        // Convert the temporary OptionTimeValueBinocularGaze array to a standard array with nullable values.
        if (!latestBinocularGazeData.HasValue) continue;
        binocularGazeData[i] = latestBinocularGazeData.Value.value;
        binocularGazeTimestamps[i] = latestBinocularGazeData.Value.time;
    }
}

After copying in the GetBinocularGazeDataFromRecording function, it can be called from Start() or from another method.

// Get gaze data with timestamps from the recording.
GetBinocularGazeDataFromRecording(SampleRecordingPath, out _binocularGazeTimestamps, out _binocularGazeData);

Step 4: Get velocity values by running a velocity filter

One of the inputs required for the Fixation Dispersion Angles filter is gaze velocity at each timestamp. To obtain these velocities, you can use the Instantaneous Velocities filter.

Before running a filter, define and initialize an output array with the same length as the input array(s). Each filter uses a specific output type, such as a VelocityOutput array in this case. The output array is the final parameter in a filter function, and the function writes the results into this array.

The filter can be accessed from Tobii.Ocumen.Filters.Interop, where all filter functions are located.

All functions in the filter library return a FilterError result. Be sure to check this result for potential errors.

// Run a velocity filter and save the result, which will be used as input to the fixation dispersion angles filter.
// A velocity output array needs to initialized before running a velocity filter.
_velocityData = new VelocityOutput[_binocularGazeTimestamps.Length];
var velocityResult = Tobii.Ocumen.Filters.Interop.VelocityInstantaneous(_binocularGazeTimestamps, _binocularGazeData, _velocityData);
if (velocityResult != FilterError.Ok) {
    // Handle the possible errors here (f.ex. throw an exception)
    throw new Exception($"Failed to calculate velocities using VelocityInstantaneous with error {velocityResult}.");
}

Step 5: Run the Fixation Dispersion Angles filter with configuration values

The Dispersion Angles filter is a configurable filter. This filter operates similarly to Step 4, but with an additional config variable provided for the filter function. A config contains all adjustable parameters for a filter. You can find each filter’s configuration information and default values in the API Reference.

// Run the fixation dispersion angles filter with configuration values and save the result.
_fixationData = new FixationOutput[_velocityData.Length];
var config = new DispersionAnglesFilterConfig
{
    maxAngleForFixationsDeg = 3.0f,
    minDurationForFixationUs = 100_000,
    maxOutliers = 1
};
var fixationResult = Tobii.Ocumen.Filters.Interop.FixationDispersionAngles(config, _binocularGazeTimestamps, _velocityData, _fixationData);
if (fixationResult != FilterError.Ok) {
    // Handle the possible errors here (f.ex. throw an exception)
    throw new Exception($"Failed to calculate fixations using FixationDispersionAngles with error {fixationResult}.");
}

Step 6: Log the results of the Fixation Dispersion Angles filter

To see the final results, the Fixation Dispersion Angles filter output array is written to Unity’s console window. Each result in the filter’s output array is looped through and logged, while checking for null values to write “No Data” instead of an empty string.

// Log the output of the fixation dispersion angles filter.
for (var i = 0; i < _fixationData.Length; i++)
{
    var leftValue = _fixationData[i].isFixationLeft.ToNullable();
    var rightValue = _fixationData[i].isFixationRight.ToNullable();

    var leftString = leftValue == null ? "No Data" : leftValue.ToString();
    var rightString = rightValue == null ? "No Data" : rightValue.ToString();

    Debug.Log("Timestamp: " + _binocularGazeTimestamps[i] + ", L: " + leftString + ", R: " + rightString);
}

Step 7: Run the code

That’s it! You are now ready to run the C# code.

Here is the full code example:

using System;
using Tobii.Ocumen.Common;
using UnityEngine;
using Tobii.Ocumen.Filters;
using Tobii.Ocumen.IO;

public class FiltersGettingStarted : MonoBehaviour
{
    private const string SampleRecordingPath = "C:/Users/<YourUsername>/Documents/Ocumen Recordings/sample-recording.ocumen";

    private long[] _binocularGazeTimestamps;
    private BinocularGaze[] _binocularGazeData;
    private VelocityOutput[] _velocityData;
    private FixationOutput[] _fixationData;

    private void Start()
    {
        // Get gaze data with timestamps from the recording.
        GetBinocularGazeDataFromRecording(SampleRecordingPath, out _binocularGazeTimestamps, out _binocularGazeData);
        
        // Run a velocity filter and save the result, which will be used as input to the fixation dispersion angles filter.
        // A velocity output array needs to initialized before running a velocity filter.
        _velocityData = new VelocityOutput[_binocularGazeTimestamps.Length];
        var velocityResult = Tobii.Ocumen.Filters.Interop.VelocityInstantaneous(_binocularGazeTimestamps, _binocularGazeData, _velocityData);
        if (velocityResult != FilterError.Ok) {
            // Handle the possible errors here (f.ex. throw an exception)
            throw new Exception($"Failed to calculate velocities using VelocityInstantaneous with error {velocityResult}.");
        }

        // Run the fixation dispersion angles filter with configuration values and save the result.
        _fixationData = new FixationOutput[_velocityData.Length];
        var config = new DispersionAnglesFilterConfig
        {
            maxAngleForFixationsDeg = 3.0f,
            minDurationForFixationUs = 100_000,
            maxOutliers = 1
        };
        var fixationResult = Tobii.Ocumen.Filters.Interop.FixationDispersionAngles(config, _binocularGazeTimestamps, _velocityData, _fixationData);
        if (fixationResult != FilterError.Ok) {
            // Handle the possible errors here (f.ex. throw an exception)
            throw new Exception($"Failed to calculate fixations using FixationDispersionAngles with error {fixationResult}.");
        }
        
        // Log the output of the fixation dispersion angles filter.
        for (var i = 0; i < _fixationData.Length; i++)
        {
            var leftValue = _fixationData[i].isFixationLeft.ToNullable();
            var rightValue = _fixationData[i].isFixationRight.ToNullable();

            var leftString = leftValue == null ? "No Data" : leftValue.ToString();
            var rightString = rightValue == null ? "No Data" : rightValue.ToString();

            Debug.Log("Timestamp: " + _binocularGazeTimestamps[i] + ", L: " + leftString + ", R: " + rightString);
        }
    }

    private static void GetBinocularGazeDataFromRecording(string recordingPath, out long[] binocularGazeTimestamps, out BinocularGaze[] binocularGazeData)
    {
        // Load the recording.
        var ocumenConnector = WorldConnector.FromFile(System.IO.Path.GetFullPath(recordingPath));
        ocumenConnector.CheckCompatible();
        var ocumenRecording = WorldReader.FromConnector(ocumenConnector.Context);
        ocumenRecording.ReadIncremental();
        
        // Get recording data for the first user in the recording.
        var user = ocumenRecording.ListUsers().Copied[0];
        
        // Get all binocular gaze timestamps in the recording.
        binocularGazeTimestamps = ocumenRecording.TrackTimes(user, TrackedData.GazeBinocular).Copied;

        // Create a variable which defines a user and a time range for which you want to get data. Assign the user and set firstInclusive to 0.
        var userInTimeRange = new UserInTimeRange() {userId = user, firstInclusive = 0};

        // Initialize a final gaze data array which will hold nullable values instead of OptionTimeValueBinocularGaze values.
        binocularGazeData = new BinocularGaze[binocularGazeTimestamps.Length];

        // Loop through each timestamp.
        for (int i = 0; i < binocularGazeTimestamps.Length; i++)
        {
            // To get data at time i, set the time range's lastInclusive time to i.
            userInTimeRange.lastInclusive = binocularGazeTimestamps[i];
            
            // Get the binocular gaze data at time i by getting the most recently added (latest) entry in the defined time range.
            var latestBinocularGazeData = ocumenRecording.LatestGazeBinocularFor(userInTimeRange).ToNullable();
            
            // Convert the temporary OptionTimeValueBinocularGaze array to a standard array with nullable values.
            if (!latestBinocularGazeData.HasValue) continue;
            binocularGazeData[i] = latestBinocularGazeData.Value.value;
            binocularGazeTimestamps[i] = latestBinocularGazeData.Value.time;
        }
    }
}