package edu.cmu.cs.eyetrack.analysis.score; import java.util.HashMap; import java.util.List; import java.util.Map; import edu.cmu.cs.eyetrack.analysis.score.compare.PointComparator; import edu.cmu.cs.eyetrack.analysis.score.distance.DistanceFunction; import edu.cmu.cs.eyetrack.analysis.struct.Trajectory; import edu.cmu.cs.eyetrack.analysis.struct.tobii.TobiiFrame; import edu.cmu.cs.eyetrack.analysis.struct.trackit.TrackItFrame; import edu.cmu.cs.eyetrack.analysis.struct.trackit.Trial; import edu.cmu.cs.eyetrack.helper.Util; import edu.cmu.cs.eyetrack.test.SingleRunScore; public class FixationPointScorer extends Scorer { // For Euclidean Distance metric, this is #pixels; for current Track-It tests, object width=120px private double closenessThreshold = 150; public FixationPointScorer(PointComparator pFunc, DistanceFunction dist, double closenessThreshold) { super(pFunc, dist); this.closenessThreshold = closenessThreshold; } public String getName() { return "Fixation Points-only Scorer"; } private boolean trajectoryLengthsOkay(Trajectory<TrackItFrame> actual, Trajectory<TobiiFrame> subject) { if( subject.getLengthMS() < actual.getLengthMS()) { Util.dPrint("Couldn't parse Spacebar-delimited Tobii file correctly; skipping trial here.\nPlease ignore any child with this message!"); return false; } return true; } public double getClosenessThreshold() { return closenessThreshold; } public void setClosenessThreshold(double closenessThreshold) { this.closenessThreshold = closenessThreshold; } /** * * @param actual * @param subject * @param record * @param recordList * @return Mapping of Fixation Points to their individual numeric scores */ private Map<Integer, Double> calcFixationPtScores(Trajectory<TrackItFrame> actual, Trajectory<TobiiFrame> subject, SingleRunScore record, List<Double> recordList) { long wallclockOffset = subject.getTimestampRecStart(); long validFrameCount = 0; long totalFrameCount = 0; Map<Integer, Double> fixationIdxScores = new HashMap<Integer, Double>(); for(TobiiFrame subjFrame : subject.getFrames()) { totalFrameCount++; // If either of the subject's eyes weren't found in this frame, skip it if(!subjFrame.bothEyesTracked()) { if(recordList != null) { recordList.add(null); } continue; } long timestamp = subjFrame.getTimestamp(); double frameScore = pFunc.score(dist, timestamp, wallclockOffset, actual, subject); // If the Tobii data goes beyond when the objects were on screen and moving, cut // off the scoring function if( timestamp - wallclockOffset > actual.getLengthMS()) { break; } Integer fixationPtIdx = subjFrame.getFixationIndex(); if(null != fixationPtIdx) { // If this gaze point is part of a fixation point, think about it Double bestScore = fixationIdxScores.get(fixationPtIdx); if(null == bestScore || frameScore < bestScore) { // We've seen this fixation pt before; is this time's score better than the last best score? // Or we've never seen this fixation pt before, so its current score is the best score fixationIdxScores.put(fixationPtIdx, frameScore); } } validFrameCount++; } record.setFrameCountOnTarget(validFrameCount); record.setFrameCountOverall(totalFrameCount); return fixationIdxScores; } private Map<Integer, Double> calcFixationPtScores(Trajectory<TrackItFrame> actual, Trajectory<TobiiFrame> subject, SingleRunScore record) { return calcFixationPtScores(actual, subject, record, null); } /** * For each fixation point in the Tobii gaze trajectory (typically 20-120, based on the length of the * run and the accuracy of the eye tracking), calculate the minimum distance between that fixation point * and the target during the same time period, then add it to the final score. Normalize by the number * of fixation points used. * @param actual * @param subject * @param recordList * @return */ @Override public SingleRunScore calcScore(Trajectory<TrackItFrame> actual, Trajectory<TobiiFrame> subject, List<Double> recordList) { if(!trajectoryLengthsOkay(actual, subject)) { SingleRunScore record = new SingleRunScore(); record.setFrameCountOnTarget(0); record.setFrameCountOverall(-1); record.setScore(Double.NaN); return record; } // Calculate a mapping of {Fixation Point Index -> that Fixation Point's score} SingleRunScore record = new SingleRunScore(); Map<Integer, Double> fixationIdxScores = calcFixationPtScores(actual, subject, record, recordList); // Normalize based on number of fixation points if(fixationIdxScores.size() > 0) { double score = 0.0; for(Integer fixationIdx : fixationIdxScores.keySet()) { score += fixationIdxScores.get(fixationIdx); } score /= (double) fixationIdxScores.size(); System.err.println("Score: " + score + ", Fixation Pts: " + fixationIdxScores.size()); record.setFixationPointCount(fixationIdxScores.size()); record.setScore(score); return record; } else { System.err.println("NO FIXATION POINTS FOUND"); return new SingleRunScore().fakeTheScore(); } } /** * Given the target T, for each fixation point F, if [F is not near T], * then for each distractor D, if [F is near D], we consider this fixation * point to be on a distractor and not on the target. * * @param record A base SingleRunScore that has already been filled out by calcScore * @param actual The gold standard data from Track-It (actual positions of objects) * @param subject The subject's Tobii data * @returns true if this method is implemented meaningfully */ @Override public boolean calcFixationStats(SingleRunScore record, Trial actualFull, Trajectory<TobiiFrame> subject) { // Grab the target object's trajectory; we'll ignore fixation points that are close to this int targetIdx = actualFull.getTargetIdx(); Trajectory<TrackItFrame> targetTrajectory = actualFull.getTrajectories().get(targetIdx); if(!trajectoryLengthsOkay(targetTrajectory, subject)) { return true; } // Calculate the scores for each fixation point relative to the target. We won't count // those fixation points that have a "good enough" score, because they are tracking the target Map<Integer, Double> fixationIdxTargetScores = calcFixationPtScores(targetTrajectory, subject, record); // Calculate the best scores for each fixation point relative to any distractor Map<Integer, Double> fixationIdxDistractorScores = new HashMap<Integer, Double>(); for(int trajIdx=0; trajIdx < actualFull.getTrajectories().size(); trajIdx++) { // Only care about distractors now, not the target if(trajIdx == targetIdx) { continue; } // Get the mapping of fixation points to scores for this specific distractor Trajectory<TrackItFrame> distractorTrajectory = actualFull.getTrajectories().get(trajIdx); Map<Integer, Double> specificFixationScores = calcFixationPtScores(distractorTrajectory, subject, record); // If this distractor beats the other distractors for any fixation point, update for(Integer fixationIdx : specificFixationScores.keySet()) { Double currentBestScore = fixationIdxDistractorScores.get(fixationIdx); Double newScore = specificFixationScores.get(fixationIdx); if(null == currentBestScore || newScore.compareTo(currentBestScore) < 0) { fixationIdxDistractorScores.put(fixationIdx, newScore); } } } // End of trajectory (distractors) loop // Now, for each fixation point, if its best distractor score is much-much better than its best target score, // count it as a distractor-on-target fixation point int fixationNearDistractorNotTargetCt = 0; for(Integer fixationPtIDx : fixationIdxTargetScores.keySet()) { double tScore = fixationIdxTargetScores.get(fixationPtIDx); double dScore = fixationIdxDistractorScores.get(fixationPtIDx); // High score w.r.t. Target, low score w.r.t. Distractor if(tScore > closenessThreshold && dScore <= closenessThreshold) { fixationNearDistractorNotTargetCt++; } } record.setFixationNearDistractorNotTargetCount(fixationNearDistractorNotTargetCt); record.setFixationPointCount(fixationIdxTargetScores.size()); return true; } }