Skip to content

Commit

Permalink
Merge pull request #54 from TheD2Lab/51-entropy
Browse files Browse the repository at this point in the history
Fixed entropy calculations and more
  • Loading branch information
ashkjones authored Aug 7, 2024
2 parents d2a3c68 + ba238e8 commit 436cdda
Show file tree
Hide file tree
Showing 19 changed files with 395 additions and 215 deletions.
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

0 comments on commit 436cdda

Please sign in to comment.