package edu.ysu.itrace.trackers; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.LinkedBlockingQueue; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.ui.PlatformUI; import edu.ysu.itrace.*; import edu.ysu.itrace.calibration.CalibrationStatusDisplay; import edu.ysu.itrace.exceptions.CalibrationException; import edu.ysu.itrace.exceptions.EyeTrackerConnectException; public class TobiiTracker implements IEyeTracker { private static class BackgroundThread extends Thread { private TobiiTracker parent = null; public BackgroundThread(TobiiTracker parent) { this.parent = parent; } public void run() { //TODO: Handle error condition jniBeginTobiiMainloop(); } private native boolean jniBeginTobiiMainloop(); } private static class Calibrator extends edu.ysu.itrace.calibration.Calibrator { private TobiiTracker parent = null; public Calibrator(TobiiTracker tracker) throws IOException { super(); parent = tracker; } protected void startCalibration() throws Exception { jniStartCalibration(); } protected void stopCalibration() throws Exception { jniStopCalibration(); } protected void useCalibrationPoint(double x, double y) throws Exception { jniAddPoint(x, y); } protected void displayCalibrationStatus(JFrame frame) throws Exception { double[] pointsNormalized = jniGetCalibration(); if (pointsNormalized == null) throw new IOException("Can't get calibration data!"); int zeros = 0; for( double ord: pointsNormalized){ if( ord <= 0 || ord > 1) zeros++; } ArrayList<Point2D.Double> points = new ArrayList<Point2D.Double>(); ArrayList<Point2D.Double> invalidpoints = new ArrayList<Point2D.Double>(); //if( zeros > 0 ) throw new IOException("zeros in points: "+zeros+"/"+pointsNormalized.length); int itemCount = pointsNormalized.length/4; for( int i=0; i < itemCount; i++ ){ points.add(new Point2D.Double(pointsNormalized[i],pointsNormalized[i+itemCount])); points.add(new Point2D.Double(pointsNormalized[(2*itemCount)+i],pointsNormalized[i+(itemCount*3)])); } Rectangle2D.Double rect = new Rectangle2D.Double(0.0,0.0,1.0,1.0); for(Point2D.Double p: points){ if( !rect.contains(p) ) invalidpoints.add(p); } for (int i = 0; i < pointsNormalized.length; i++) { if (pointsNormalized[i] < 0.0001) { pointsNormalized[i] = 0.0001; } else if (pointsNormalized[i] > 0.9999) { pointsNormalized[i] = 0.9999; } else { //do nothing } } Point2D.Double[] calibrationData = new Point2D.Double[itemCount+1]; for (int j = 0; j < itemCount; j+=2) { calibrationData[j] = (new Point2D.Double(pointsNormalized[j],pointsNormalized[itemCount+j])); if(j != itemCount) calibrationData[j+1] = (new Point2D.Double(pointsNormalized[2*itemCount+j],pointsNormalized[3*itemCount+j])); } JFrame calibFrame = frame; CalibrationStatusDisplay calibDisplay = new CalibrationStatusDisplay(calibFrame,calibrationPoints,calibrationData); calibFrame.add(calibDisplay); calibFrame.setUndecorated(false); calibFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); calibFrame.setExtendedState(JFrame.MAXIMIZED_BOTH); calibFrame.setMinimumSize(new Dimension(600,300)); calibFrame.setTitle("Calibration: "+new Date()); Insets insets = calibFrame.getInsets(); int width = calibFrame.getSize().width-(insets.left+insets.right); int height = calibFrame.getSize().height-(insets.top+insets.bottom); calibDisplay.windowDimension = new Dimension(width,height); calibFrame.setVisible(true); calibDisplay.repaint(); } private native void jniAddPoint(double x, double y) throws RuntimeException, IOException; private native void jniStartCalibration() throws RuntimeException, IOException; private native void jniStopCalibration() throws RuntimeException, IOException; private native double[] jniGetCalibration() throws RuntimeException, IOException; } private BackgroundThread bg_thread = null; private volatile ByteBuffer native_data = null; private LinkedBlockingQueue<Gaze> gaze_points = new LinkedBlockingQueue<Gaze>(); private LinkedBlockingQueue<Gaze> recentGazes = new LinkedBlockingQueue<Gaze>(); private Calibrator calibrator; private double xDrift = 0, yDrift = 0; private Long previousTrackerTime; private long time = 0; private IEventBroker eventBroker; static { System.loadLibrary("TobiiTracker"); } public TobiiTracker() throws EyeTrackerConnectException, IOException { calibrator = new Calibrator(this); //Initialise the background thread which functions as the main loop in //the Tobii SDK. bg_thread = new BackgroundThread(this); bg_thread.start(); while (native_data == null); //Wait until background thread sets native_data if (!jniConnectTobiiTracker(10)) { this.close(); throw new EyeTrackerConnectException(); } eventBroker = PlatformUI.getWorkbench().getService(IEventBroker.class); } public static void main(String[] args) { TobiiTracker tobii_tracker = null; try { tobii_tracker = new TobiiTracker(); System.out.println("Connected successfully to eyetracker."); Dimension window_bounds = Toolkit.getDefaultToolkit() .getScreenSize(); System.out.println("Screen size: (" + window_bounds.width + ", " + window_bounds.height + ")"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } tobii_tracker.calibrate(); tobii_tracker.startTracking(); tobii_tracker.displayCrosshair(true); long start = (new Date()).getTime(); while ((new Date()).getTime() < start + 5000); tobii_tracker.stopTracking(); tobii_tracker.displayCrosshair(false); tobii_tracker.clear(); tobii_tracker.startTracking(); start = (new Date()).getTime(); while ((new Date()).getTime() < start + 25000) { Gaze gaze = tobii_tracker.getGaze(); if (gaze != null) { System.out.println("Gaze at " + gaze.getTrackerTime() + ": (" + (int) (gaze.getX() * window_bounds.width) + ", " + (int) (gaze.getY() * window_bounds.height) + ") with validity (Left: " + gaze.getLeftValidity() + ", Right: " + gaze.getRightValidity() + ")"); } } tobii_tracker.stopTracking(); tobii_tracker.close(); } catch (EyeTrackerConnectException e) { System.out.println("Failed to connect to Tobii eyetracker."); } catch (CalibrationException e) { tobii_tracker.close(); System.out.println("Could not calibrate. Try again."); } catch (IOException e) { tobii_tracker.close(); System.out.println("IO failure occurred."); } System.out.println("Done!"); } public void clear() { gaze_points = new LinkedBlockingQueue<Gaze>(); } public void calibrate() throws CalibrationException { calibrator.calibrate(); } public Gaze getGaze() { return gaze_points.poll(); } public void setXDrift(int drift) { xDrift = ((double) drift) / 100; } public void setYDrift(int drift) { yDrift = ((double) drift) / 100; } public void newGazePoint(long timestamp, double left_x, double left_y, double right_x, double right_y, int left_validity, int right_validity, double left_pupil_diameter, double right_pupil_diameter) { if(left_validity == 4 && right_validity == 4) return; //Ignore new gaze if(previousTrackerTime != null && (timestamp/1000) == (previousTrackerTime/1000)){ //Ignore new gaze; return; }else{ //Set previousGaze to new gaze previousTrackerTime = timestamp; } if(left_validity == 4 && right_validity == 4) return; //Ignore new gaze //Left eye out of bounds. if( left_x >= 1.0 || left_x <= 0.0 || left_y >= 1.0 || left_y <= 0.0 ) { //Right eye out of bounds. if( right_x >= 1.0 || right_x <= 0.0 || right_y >= 1.0 || right_y <= 0.0) { /* * Check the time, * if both eyes have been out of bounds for 1 second * remove the crosshair. */ if( time == 0 ) time = timestamp; else if( timestamp-time > 1000000 ) calibrator.moveCrosshair(-10, -10); return; } left_x = right_x; left_y = right_y; }else{ //Right eye out of bounds. if( right_x >= 1.0 || right_x <= 0.0 || right_y >= 1.0 || right_y <= 0.0 ){ right_x = left_x; right_y = left_y; } } time = 0; //Drift left_x += xDrift; right_x += xDrift; left_y += yDrift; right_y += yDrift; //Average left and right eyes for each value. double x = (left_x + right_x) / 2; double y = (left_y + right_y) / 2; //Clamp x values to [0.0, 1.0]. if (left_x >= 1.0) left_x = 1.0; else if (left_x <= 0.0) left_x = 0.0; if (right_x >= 1.0) right_x = 1.0; else if (right_x <= 0.0) right_x = 0.0; //Clamp y values to [0.0, 1.0] if (left_y >= 1.0) left_y = 1.0; else if (left_y <= 0.0) left_y = 0.0; if (right_y >= 1.0) right_y = 1.0; else if (right_y <= 0.0) right_y = 0.0; double gaze_left_validity = 1.0 - ((double) left_validity / 4.0); double gaze_right_validity = 1.0 - ((double) right_validity / 4.0); double left_x_mod = left_x, right_x_mod = right_x, left_y_mod = left_y, right_y_mod = right_y; try { Gaze gaze = new Gaze(left_x, right_x, left_y, right_y, gaze_left_validity, gaze_right_validity, left_pupil_diameter, right_pupil_diameter, timestamp); if (recentGazes.size() >= 15) recentGazes.remove(); recentGazes.add(gaze); for (Object curObj : recentGazes.toArray()) { Gaze curGaze = (Gaze) curObj; left_x_mod += curGaze.getLeftX(); right_x_mod += curGaze.getRightX(); left_y_mod += curGaze.getLeftY(); right_y_mod += curGaze.getRightY(); } left_x_mod /= recentGazes.size() + 1; right_x_mod /= recentGazes.size() + 1; left_y_mod /= recentGazes.size() + 1; right_y_mod /= recentGazes.size() + 1; Gaze modifiedGaze = new Gaze(left_x_mod, right_x_mod, left_y_mod, right_y_mod, gaze_left_validity, gaze_right_validity, left_pupil_diameter, right_pupil_diameter, timestamp); gaze_points.put(modifiedGaze); eventBroker.post("iTrace/newgaze", modifiedGaze); } catch (InterruptedException e) { //Ignore this point. } Dimension screen_size = Toolkit.getDefaultToolkit().getScreenSize(); int screen_x = (int) (screen_size.width * ((left_x_mod + right_x_mod) / 2)); int screen_y = (int) (screen_size.height * ((left_y_mod + right_y_mod) / 2)); calibrator.moveCrosshair(screen_x, screen_y); } private native boolean jniConnectTobiiTracker(int timeout_seconds); public native void close(); public native void startTracking() throws RuntimeException, IOException; public native void stopTracking() throws RuntimeException, IOException; public void displayCrosshair(boolean enabled) { calibrator.displayCrosshair(enabled); } }