Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed entropy calculations and more #54

Merged
merged 6 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
Expand Down
57 changes: 29 additions & 28 deletions src/main/java/com/github/thed2lab/analysis/Analysis.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.github.thed2lab.analysis;

import static com.github.thed2lab.analysis.Constants.SCREEN_HEIGHT;
import static com.github.thed2lab.analysis.Constants.SCREEN_WIDTH;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -12,8 +15,6 @@ public class Analysis {
* Its role is to iterate over each file, and process it into DataEntry objects for gaze,
* validity, and fixations.
*/
private final static int SCREEN_WIDTH = 1920;
private final static int SCREEN_HEIGHT = 1080;

private final static int MIN_PATTERN_LENGTH = 3;
private final static int MAX_PATTERN_LENGTH = 7;
Expand Down Expand Up @@ -54,10 +55,10 @@ public boolean run() {
System.out.println("Analyzing " + pName);

// Build DataEntrys
DataEntry allGaze = FileHandler.buildDataEntry(f);
DataEntry validGaze = DataFilter.filterByValidity(allGaze);
DataEntry allGaze = DataFilter.applyScreenSize(FileHandler.buildDataEntry(f), SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry validGaze = DataFilter.filterByValidity(allGaze, SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry fixations = DataFilter.filterByFixations(allGaze);
DataEntry validFixations = DataFilter.filterByValidity(fixations);
DataEntry validFixations = DataFilter.filterByValidity(fixations, SCREEN_HEIGHT, SCREEN_WIDTH);

// Write DataEntrys to file
validGaze.writeToCSV(pDirectory, pName + "_valid_all_gaze");
Expand All @@ -81,7 +82,7 @@ public boolean run() {
allParticipantDGMs.add(dgms);

// Generate AOIs
AreaOfInterests.generateAOIs(allGaze, pDirectory, pName);
AreaOfInterests.generateAOIs(allGaze, fixations, pDirectory, pName);

// Generate windows
Windows.generateWindows(allGaze, pDirectory, settings);
Expand Down Expand Up @@ -149,9 +150,7 @@ public boolean run() {
* @return a {@code List<List<String>} where the first inner-list is the measure names, and second inner-list is the calculated values.
*/
static List<List<String>> generateResults(DataEntry allGaze, DataEntry fixations) {
DataEntry validGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry validAoiFixations = DataFilter.applyScreenSize(DataFilter.filterByValidity(fixations), SCREEN_WIDTH, SCREEN_HEIGHT);
var results = generateResultsHelper(validGaze, validGaze, validAoiFixations);
var results = generateResultsHelper(allGaze, allGaze, fixations);
return results;
}

Expand All @@ -164,33 +163,35 @@ static List<List<String>> generateResults(DataEntry allGaze, DataEntry fixations
* @return a {@code List<List<String>} where the first inner-list is the measure names, and second inner-list is the calculated values.
*/
static List<List<String>> generateResults(DataEntry allGaze, DataEntry aoiGaze, DataEntry aoiFixations) {
DataEntry validAllGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry validAoiGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry validAoiFixations = DataFilter.applyScreenSize(DataFilter.filterByValidity(aoiFixations), SCREEN_WIDTH, SCREEN_HEIGHT);
var results = generateResultsHelper(validAllGaze, validAoiGaze, validAoiFixations);
var results = generateResultsHelper(allGaze, aoiGaze, aoiFixations);
return results;
}

/**
* Helper methods that generates the descriptive gaze measures.
* @param validAllGaze all gaze data, filtered by validity.
* @param validAoiGaze the gaze data that ocurred within an aoi, filtered by validity. For the whole screen, this is the same
* data as validAllGaze.
* @param validAoiFixation the gaze data that ocurred within an aoi, filtered by fixation and validity.
* @return a list the descriptive gaze measures where the first row is the headers and the second row is the values.
* @param allGaze all gaze data for the whole screen, with screen size applied.
* @param areaGaze the gaze data that ocurred within a target portion of screen, i.e., either the whole screen or an AOI, with screen
* size applied.
* @param areaFixations the gaze data that ocurred within a target portion of screen, i.e., either the whole screen or an AOI,
* and filtered by fixation with screen size applied.
* @return a {@code List<List<String>} where the first inner-list is the measure names, and second inner-list is the calculated values.
*/
private static List<List<String>> generateResultsHelper(DataEntry validAllGaze, DataEntry validAoiGaze, DataEntry validAoiFixation) {
private static List<List<String>> generateResultsHelper(DataEntry allGaze, DataEntry areaGaze, DataEntry areaFixations) {
// an argument could be made to filter before entering this function; there is a code smell due to blink rate and saccadeV changing
DataEntry validAllGaze = DataFilter.filterByValidity(allGaze, SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry validAreaGaze = DataFilter.filterByValidity(areaGaze, SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry validAreaFixations = DataFilter.filterByValidity(areaFixations, SCREEN_WIDTH, SCREEN_HEIGHT);

LinkedHashMap<String,String> resultsMap = new LinkedHashMap<String, String>();
resultsMap.putAll(Fixations.analyze(validAoiFixation));
resultsMap.putAll(Saccades.analyze(validAoiFixation));
resultsMap.putAll(SaccadeVelocity.analyze(validAllGaze, validAoiFixation));
resultsMap.putAll(Angles.analyze(validAoiFixation));
resultsMap.putAll(ConvexHull.analyze(validAoiFixation));
resultsMap.putAll(GazeEntropy.analyze(validAoiFixation));
resultsMap.putAll(Blinks.analyze(validAoiGaze));
resultsMap.putAll(Gaze.analyze(validAoiGaze));
resultsMap.putAll(Event.analyze(validAoiGaze));
resultsMap.putAll(Fixations.analyze(validAreaFixations));
resultsMap.putAll(Saccades.analyze(validAreaFixations));
resultsMap.putAll(SaccadeVelocity.analyze(validAllGaze, validAreaFixations));
resultsMap.putAll(Angles.analyze(validAreaFixations));
resultsMap.putAll(ConvexHull.analyze(validAreaFixations));
resultsMap.putAll(GazeEntropy.analyze(validAreaFixations));
resultsMap.putAll(Blinks.analyze(areaGaze));
resultsMap.putAll(Gaze.analyze(validAreaGaze));
resultsMap.putAll(Event.analyze(validAreaGaze));

var resultsList = new ArrayList<List<String>>(2);
resultsList.add(new ArrayList<>(resultsMap.keySet()));
Expand Down
13 changes: 7 additions & 6 deletions src/main/java/com/github/thed2lab/analysis/Angles.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package com.github.thed2lab.analysis;

import static com.github.thed2lab.analysis.Constants.FIXATION_ID;
import static com.github.thed2lab.analysis.Constants.FIXATION_X;
import static com.github.thed2lab.analysis.Constants.FIXATION_Y;

import java.util.ArrayList;
import java.util.LinkedHashMap;

public class Angles {
final static String FIXATIONID_INDEX = "FPOGID";
final static String FIXATIONX_INDEX = "FPOGX";
final static String FIXATIONY_INDEX = "FPOGY";

static public LinkedHashMap<String,String> analyze(DataEntry data) {
LinkedHashMap<String,String> results = new LinkedHashMap<String,String>();
ArrayList<Coordinate> allCoordinates = new ArrayList<>();

for (int row = 0; row < data.rowCount(); row++) {
Coordinate eachCoordinate = new Coordinate(
Double.valueOf(data.getValue(FIXATIONX_INDEX, row)),
Double.valueOf(data.getValue(FIXATIONY_INDEX, row)),
Integer.valueOf(data.getValue(FIXATIONID_INDEX, row))
Double.valueOf(data.getValue(FIXATION_X, row)),
Double.valueOf(data.getValue(FIXATION_Y, row)),
Integer.valueOf(data.getValue(FIXATION_ID, row))
);
allCoordinates.add(eachCoordinate);
}
Expand Down
51 changes: 26 additions & 25 deletions src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
package com.github.thed2lab.analysis;

import java.util.Arrays;
import static com.github.thed2lab.analysis.Constants.AOI_LABEL;
import static com.github.thed2lab.analysis.Constants.FIXATION_DURATION;
import static com.github.thed2lab.analysis.Constants.FIXATION_ID;
import static com.github.thed2lab.analysis.Constants.SCREEN_HEIGHT;
import static com.github.thed2lab.analysis.Constants.SCREEN_WIDTH;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;

public class AreaOfInterests {
final static String FIXATIONID_INDEX = "FPOGID"; //CNT
final static String DURATION_INDEX = "FPOGD";
final static String AOI_INDEX = "AOI";


private static final String[] additionalHeaders = {"aoi", "proportion_of_fixations_spent_in_aoi","proportion_of_fixations_durations_spent_in_aoi"};
private static final String[] perAoiHeaders = {"aoi_pair", "transition_count", "proportion_including_self_transitions", "proportion_excluding_self_transitions"};


public static void generateAOIs(DataEntry allGazeData, String outputDirectory, String fileName) {
public static void generateAOIs(DataEntry allGazeData, DataEntry fixationData, String outputDirectory, String fileName) {
LinkedHashMap<String, DataEntry> aoiMetrics = new LinkedHashMap<>();
for (int i = 0; i < allGazeData.rowCount(); i++) {
String aoi = allGazeData.getValue(AOI_INDEX, i);
String aoiKey = aoi.equals("") ? "No AOI" : aoi;
String aoi = allGazeData.getValue(AOI_LABEL, i);
String aoiKey = aoi.equals("") ? "Undefined Area" : aoi;
if (!aoiMetrics.containsKey(aoiKey)) {
DataEntry d = new DataEntry(allGazeData.getHeaders());
aoiMetrics.put(aoiKey, d);
}
aoiMetrics.get(aoiKey).process(allGazeData.getRow(i));
}


DataEntry filteredFixations = DataFilter.filterByValidity(fixationData, SCREEN_WIDTH, SCREEN_HEIGHT);
LinkedHashMap<String, DataEntry> aoiFixationMetrics = new LinkedHashMap<>();
DataEntry allFixations = DataFilter.filterByValidity(DataFilter.filterByFixations(allGazeData));
//System.out.println(allFixations.rowCount());
for (int i = 0; i < allFixations.rowCount(); i++) {
String aoi = allFixations.getValue(AOI_INDEX, i);
String aoiKey = aoi.equals("") ? "No AOI" : aoi;
for (int i = 0; i < filteredFixations.rowCount(); i++) {
String aoi = filteredFixations.getValue(AOI_LABEL, i);
String aoiKey = aoi.equals("") ? "Undefined Area" : aoi;
if (!aoiFixationMetrics.containsKey(aoiKey)) {
DataEntry d = new DataEntry(allFixations.getHeaders());
DataEntry d = new DataEntry(filteredFixations.getHeaders());
aoiFixationMetrics.put(aoiKey, d);
}
aoiFixationMetrics.get(aoiKey).process(allFixations.getRow(i));
aoiFixationMetrics.get(aoiKey).process(filteredFixations.getRow(i));
}

// For any AOIs not in aoiFixationMetrics, add an empty DataEntry
Expand All @@ -59,7 +60,7 @@ public static void generateAOIs(DataEntry allGazeData, String outputDirectory, S
ArrayList<List<String>> metrics = new ArrayList<>();
metrics.add(new ArrayList<String>());

double totalDuration = getDuration(allFixations);
double totalDuration = getDuration(filteredFixations);
LinkedHashMap<String, DataEntry> validAOIs = new LinkedHashMap<>();
boolean isFirst = true;
Set<String> aoiKeySet = aoiMetrics.keySet();
Expand All @@ -80,11 +81,11 @@ public static void generateAOIs(DataEntry allGazeData, String outputDirectory, S
}
results.get(1).add(aoiKey);
metrics.add(results.get(1));
metrics.get(row).addAll(getProportions(allFixations, singleAoiFixations, totalDuration));
metrics.get(row).addAll(getProportions(filteredFixations, singleAoiFixations, totalDuration));
validAOIs.put(aoiKey, singleAoiFixations);
row++;
}
ArrayList<List<String>> pairResults = generatePairResults(allFixations, aoiMetrics);
ArrayList<List<String>> pairResults = generatePairResults(filteredFixations, aoiMetrics);
FileHandler.writeToCSV(metrics, outputDirectory, fileName + "_AOI_DGMs");
FileHandler.writeToCSV(pairResults, outputDirectory, fileName+"_AOI_Transitions");
}
Expand All @@ -102,7 +103,7 @@ public static ArrayList<String> getProportions(DataEntry fixations, DataEntry ao
public static double getDuration(DataEntry fixations) {
double durationSum = 0.0;
for (int i = 0; i < fixations.rowCount(); i++) {
durationSum += Double.valueOf(fixations.getValue(DURATION_INDEX, i));
durationSum += Double.valueOf(fixations.getValue(FIXATION_DURATION, i));
}

return durationSum;
Expand All @@ -112,12 +113,12 @@ public static ArrayList<List<String>> generatePairResults(DataEntry fixations, L
LinkedHashMap<String, ArrayList<Integer>> totalTransitions = new LinkedHashMap<>(); // ArrayList<Integer>(Transtions, Inclusive, Exlusive);
LinkedHashMap<String,LinkedHashMap<String, Integer>> transitionCounts = new LinkedHashMap<>();
for (int i = 0; i < fixations.rowCount()-1; i++) {
String curAoi = fixations.getValue(AOI_INDEX, i);
curAoi = curAoi.equals("") ? "No AOI" : curAoi;
int curId = Integer.valueOf(fixations.getValue(FIXATIONID_INDEX, i));
String nextAoi = fixations.getValue(AOI_INDEX, i+1);
nextAoi = nextAoi.equals("") ? "No AOI" : nextAoi;
int nextId = Integer.valueOf(fixations.getValue(FIXATIONID_INDEX, i+1));
String curAoi = fixations.getValue(AOI_LABEL, i);
curAoi = curAoi.equals("") ? "Undefined Area" : curAoi;
int curId = Integer.valueOf(fixations.getValue(FIXATION_ID, i));
String nextAoi = fixations.getValue(AOI_LABEL, i+1);
nextAoi = nextAoi.equals("") ? "Undefined Area" : nextAoi;
int nextId = Integer.valueOf(fixations.getValue(FIXATION_ID, i+1));
boolean isValidAOI = (validAOIs.containsKey(curAoi) && validAOIs.containsKey(nextAoi));
if (isValidAOI && nextId == curId + 1) { //Check if fixations are subsequent
if (!totalTransitions.containsKey(curAoi)) { //Ensure AOI is initialized in map.
Expand Down
16 changes: 9 additions & 7 deletions src/main/java/com/github/thed2lab/analysis/Blinks.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.github.thed2lab.analysis;

import static com.github.thed2lab.analysis.Constants.BLINK_ID;
import static com.github.thed2lab.analysis.Constants.DATA_ID;
import static com.github.thed2lab.analysis.Constants.TIMESTAMP;

import java.util.LinkedHashMap;

public class Blinks {

final static String BLINK_ID_INDEX = "BKID";
final static String TIME_INDEX = "TIME";
final static String DATA_ID_INDEX = "CNT"; // unique for each line of raw data
final static String DEFAULT_BKID = "0"; // BKID when no blink is detected
/** Blink id when no blink is being detected. */
final static String DEFAULT_BKID = "0";

static public LinkedHashMap<String,String> analyze(DataEntry allGazeData) {

Expand All @@ -18,14 +20,14 @@ static public LinkedHashMap<String,String> analyze(DataEntry allGazeData) {
double prevTimestamp = 0;
int prevDataId = -10; // IDs are always non-negative
for (int i = 0; i < allGazeData.rowCount(); i++) {
int curDataId = Integer.parseInt(allGazeData.getValue(DATA_ID_INDEX, i));
double curTimestamp = Double.parseDouble(allGazeData.getValue(TIME_INDEX, i));
int curDataId = Integer.parseInt(allGazeData.getValue(DATA_ID, i));
double curTimestamp = Double.parseDouble(allGazeData.getValue(TIMESTAMP, i));
// calculate time window between data records if they are consecutive
if (curDataId == prevDataId + 1) {
timeTotal += curTimestamp - prevTimestamp;
}

String curBlinkId = allGazeData.getValue(BLINK_ID_INDEX, i);
String curBlinkId = allGazeData.getValue(BLINK_ID, i);
if (!curBlinkId.equals(DEFAULT_BKID) && !curBlinkId.equals(prevBlinkId)) {
blinkCnt++; // new blink occurred
}
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/com/github/thed2lab/analysis/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.github.thed2lab.analysis;

/**
* Hardcoded constants that are used throughout the package so there are fewer duplicated
* constants throughout the files.
*/
final class Constants {
/*
* Notice the access modifier is default (package-private).
* We could make this an injectable if we wanted to be better OO programmers, but I think
* this package is closely coupled enough that we don't care anymore.
*/

private Constants() {
// do not instantiate this class EVER!!!
}

/** Screen width in pixels */
final static int SCREEN_WIDTH = 1920;
/** Screen height in pixels */
final static int SCREEN_HEIGHT = 1080;

/** Header for timestamp of when the data line was recorded since the start of the recording in seconds */
final static String TIMESTAMP = "TIME";
/** Header for the unique ID given to each line of data */
final static String DATA_ID = "CNT";

/** Header for fixation ID. */
final static String FIXATION_ID = "FPOGID";
/** Header for the fixations starting timestamp */
final static String FIXATION_START = "FPOGS";
/** Header for fixation validity. 1 for true and 2 for false */
final static String FIXATION_VALIDITY = "FPOGV";
/** Header for the x-coordinate of the fixation point of gaze */
final static String FIXATION_X = "FPOGX";
/** Header for the y-coordinate of the fixation point of gaze */
final static String FIXATION_Y = "FPOGY";
/** Header for the duration of a fixation */
final static String FIXATION_DURATION = "FPOGD";

/** Header for the diameter of the left pupil in mm */
final static String LEFT_PUPIL_DIAMETER = "LPMM";
/** Header for the valid flag for the left pupil. A value of 1 is valid */
final static String LEFT_PUPIL_VALIDITY = "LPMMV";
/** Header for the diameter of the right pupil in mm */
final static String RIGHT_PUPIL_DIAMETER = "RPMM";
/** Header for the valid flag for the right pupil. A value of 1 is valid */
final static String RIGHT_PUPIL_VALIDITY = "RPMMV";

/** Header for cursor events */
final static String CURSOR_EVENT = "CS";
/** Header for the blink ID */
final static String BLINK_ID = "BKID";
/** Header for the blink rate per minute */
final static String BLINK_RATE = "BKPMIN";
/** Header for the AOI Label */
final static String AOI_LABEL = "AOI";

/** Header for the saccade magnitude */
final static String SACCADE_MAGNITUDE = "SACCADE_MAG";
/** Header for the saccade direction */
final static String SACCADE_DIR = "SACCADE_DIR";

}
Loading
Loading