Reputation: 21
This is our code:
import pandas as pd
import matplotlib as plt
import time
from collections import deque
import numpy as np
from scipy.spatial.distance import pdist
from sensoryimmersion.Utils import project_constants as con
def calculate_centroid(points):
"""
Calculate the centroid of a list of points represented as (x, y) coordinates.
:param points: A list of (x, y) coordinate tuples.
:return: The centroid as a (x, y) tuple.
"""
num_points = len(points)
if num_points == 0:
return None # Return None for an empty list of points
# Calculate the sum of x and y coordinates separately
sum_x = sum(point[0] for point in points)
sum_y = sum(point[1] for point in points)
# Calculate the centroid coordinates
centroid_x = sum_x / num_points
centroid_y = sum_y / num_points
return (centroid_x, centroid_y)
def vector_dispersion(vectors):
'''
from pupil labs: https://github.com/pupil-labs/pupil/blob/master/pupil_src/shared_modules/fixation_detector.py
Params
-------
vectors (3xM array)
3d gaze points of m original observations in an 3-dimensional space where each observation is (x, y, z)
Return
-------
dispersion_cosine (float)
value of the dispersion in degrees
'''
distances = pdist(vectors, metric="cosine")
dispersion_cosine = np.arccos(1.0 - distances.max()) # arccos function returns radians
dispersion_deg = np.rad2deg(dispersion_cosine)
return dispersion_deg
def fill_window_from_gazedeque(window, gaze_deque):
'''
Returns window deque and gaze deque with the minimum duration on the window deque from the gaze deque, or an
empty deque if not enough points are present on the gaze deque
Params
------
window (NxM deque of tuples)
deque of namedtuples from gaze dataframe with an attribute "gaze_timestamp" that marks the time in seconds
gaze_deque (NxM deque of tuples)
deque of namedtuples from gaze dataframe with an attribute "gaze_timestamp" that marks the time in seconds
Return
------
window (NxM deque of tuples)
deque with data that reaches the minimum duration, or empty if not enough data points are left
from the gaze deque
gaze_deque (NxM deque of tuples)
deque with data where the leftmost tuple does surpasses the minimum duration
'''
if len(window) == 0:
print("window is empty; make sure there is at least one point. returning empty window.")
else:
while ((window[-1].gaze_timestamp - window[0].gaze_timestamp) * 1000) < con.MIN_DURATION and len(
gaze_deque) > 0:
if len(gaze_deque) == 0:
# min duration cannot be reached, no more points left on gaze deque
return deque(), deque()
next_gaze_row = gaze_deque.popleft()
window.append(next_gaze_row)
return window, gaze_deque
def write_fixation_window_to_csv(
gaze_df,
window,
window_dispersion,
window_centroid,
fixation_id,
gaze_csv_path=con.EVENTS_FILEPATH,
fixations_only_csv_path=con.FIXATIONS_FILEPATH
):
window_start_idx = window[0].Index # row idx of first gaze point in data
window_end_idx = window[-1].Index # row idx of last gaze point in data
print("id:", fixation_id)
print("\tduration:", (window[-1].gaze_timestamp - window[0].gaze_timestamp) * 1000)
print("\tfixation start row:", window_start_idx, "fixation end row:", window_end_idx)
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"event_type"
] = "fixation"
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"fixation_id"
] = fixation_id
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"fixation_start"
] = window[0].gaze_timestamp
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"fixation_end"
] = window[-1].gaze_timestamp
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"fixation_duration"
] = (window[-1].gaze_timestamp - window[0].gaze_timestamp) * 1000
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"fixation_centroid_x"
] = window_centroid[0]
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"fixation_centroid_y"
] = window_centroid[1]
gaze_df.loc[
gaze_df.index[window_start_idx:window_end_idx + 1],
"dispersion"
] = window_dispersion
gaze_df.to_csv(gaze_csv_path, index=False)
fixation_df = gaze_df[gaze_df["event_type"] == "fixation"].copy()
fixation_df.to_csv(fixations_only_csv_path, index=False)
def find_end_of_fixation(window, gaze_deque):
'''
Identifies window where dispersion remains under the dispersion limit.
Params
-------
gaze_deque (NxN deque):
gaze rows that must still be processed, where some of the leftmost rows might count within the fixation
detect_fixation_window (NxN deque):
window of gaze rows with at least the minimum duration and is under the dispersion limit
Return
-------
gaze_deque (deque):
gaze rows with leftmost row being the row that goes over the dispersion limit
window (deque):
window of gaze rows that remain under the dispersion limit
'''
### 4. Add additional points to the window until the dispersion becomes greater than the dispersion threshold.
while (vector_dispersion(
np.array([(data.gaze_point_3d_x, data.gaze_point_3d_y, data.gaze_point_3d_z)
for data in window])) <= con.DISPERSION) and (len(gaze_deque) >= 0):
if len(gaze_deque) == 0:
# reached end of gaze deque while within dispersion-- mark as a fixation(?)
return window, gaze_deque
window.append(gaze_deque.popleft())
print(vector_dispersion(
np.array([(data.gaze_point_3d_x, data.gaze_point_3d_y, data.gaze_point_3d_z)
for data in window])), f"within {con.DISPERSION} degrees:", (vector_dispersion(
np.array([(data.gaze_point_3d_x, data.gaze_point_3d_y, data.gaze_point_3d_z)
for data in window])) <= con.DISPERSION))
### 5. If the dispersion becomes greater than the threshold, remove the last point added to the window that
# caused the dispersion to exceed the threshold.
gaze_deque.appendleft(window.pop())
return window, gaze_deque
def start_detection():
gaze_df = pd.read_csv(con.INTERPOLATED_FILEPATH)
gaze_df["event_type"] = ""
gaze_rows = []
for row in gaze_df.itertuples(name='Row', index=True):
gaze_rows.append(row)
# print(gaze_rows[0])
# create deques
fixation_count = 0
gaze_deque = deque(gaze_rows)
window = deque()
window.append(gaze_deque.popleft()) # add first data point
### 1. While there are still points in the gaze DataFrame.
while len(gaze_deque) > 0 and len(window) > 0:
### 2. Define duration threshold: Min duration - Max duration (150 - 350 ms).
### 3. Initialize the window to cover the first points to meet minimum duration threshold.
print("finding min duration")
window, gaze_deque = fill_window_from_gazedeque(window, gaze_deque)
if len(window) == 0:
# insufficient points to cover minimum duration; do not mark as fixation and exit
break
vectors = np.array([(data.gaze_point_3d_x, data.gaze_point_3d_y, data.gaze_point_3d_z) for data in window])
window_dispersion = vector_dispersion(vectors)
print("start frame:", window[0].gaze_timestamp, "end frame:", window[-1].gaze_timestamp, "duration:",
window[-1].gaze_timestamp - window[0].gaze_timestamp, "dispersion:", window_dispersion)
### 4. Check if the maximum dispersion of these points is less than or equal to the dispersion threshold.
if window_dispersion <= con.DISPERSION:
### 5. Make sure fixation end is within maximum duration threshold
window, gaze_deque = find_end_of_fixation(window, gaze_deque)
window_duration = (window[-1].gaze_timestamp - window[0].gaze_timestamp) * 1000
if window_duration <= con.MAX_DURATION:
### 6. Find the fixation at the centroid.
window_centroid = calculate_centroid(
np.array([(data.x_norm, data.y_norm)
for data in window]))
window_dispersion = vector_dispersion(
np.array([(data.gaze_point_3d_x, data.gaze_point_3d_y, data.gaze_point_3d_z)
for data in window]))
# write fixation to csv
write_fixation_window_to_csv(
gaze_df, window,
window_dispersion, window_centroid,
fixation_id=fixation_count)
fixation_count += 1
if len(gaze_deque) == 0:
break
else:
### 7. Remove these window points from the gaze DataFrame.
window = deque()
window.append(gaze_deque.popleft())
### 11. Start the loop again by moving the window to cover new points not previously looked at.
else:
window.popleft() # dispersion exceeds max dispersion; window is not a fixation
continue
In the end, I get an output_fixations only csv file that spits out 8 fixations. In the non-interpolated data, the pupil labs player software got us over 3000 fixations (across 20 mins of the game). So, clearly, the code is not doing something right.
This is the pseudo-code that the code above is based on:
1. While there are still points in the gaze DataFrame.
2. Define duration threshold: Min duration - Max duration (150 ms)
3. Initialize the window to cover the first points to meet duration threshold.
3. Check if the maximum dispersion of these points is less than or equal to the dispersion threshold.
4. Add additional points to the window until the dispersion becomes greater than the dispersion threshold.
5. If the dispersion becomes greater than the threshold, remove the last point added to the window that caused the dispersion to exceed the threshold.
6. Find fixation end.
7. Make sure fixation end + fixation start is within duration threshold
8. Note the fixation at the centroid of these window points. Save the event type in a CSV sheet along with corresponding
dispersion value, fixation start, fixation end, and duration.
9. Remove these window points from the gaze DataFrame.
11. Start the loop again by moving the window to cover new points not previously looked at.
12. Filter points that are above a maximum duration (350 ms)
It was very complicated to get this done. I got this far where at least I am able to get 8 fixations identified and some kind of output. I am not sure where to go from here at all. I would be grateful if someone could review my code. I have been working on it for the past 5-6 months.
Upvotes: 0
Views: 35