├── .gitignore ├── .gitmodules ├── README.md ├── categories └── democategory │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── code ├── constructNPYheader.m ├── datToNPY.m ├── detectfixaties2015.m ├── detectfixaties2018.m ├── detectfixaties2018fmark.m ├── detectfixaties2018thr.m ├── detectfixaties2022thr.m ├── detectswitches.m ├── detvel.m ├── dispTobiiInstructions.m ├── fixdetect.m ├── fixdetectmovingwindow.m ├── folderfromfolder.m ├── gazecode.m ├── gazesplash.m ├── initeventdetect.m ├── leesTSPupInvis200Ex.m ├── leesTSPupNeon200.m ├── leesgazePupInvis200Exdata.m ├── leesgazePupNeon200data.m ├── leesgazedata.m ├── leesgazedataPosSci.m ├── leesgazedataPupFG.m ├── leesgazedataSMI.m ├── leesgazedataTobii.m ├── maxall.m ├── outputStreamSelectorGUI.m ├── pythagoras.m ├── readNPY.m ├── readNPYheader.m ├── streamSelectorGUI.m ├── tempdat.mat ├── test.m ├── truescreensize.m └── writeNPY.m ├── data ├── demoTobiiSD │ └── projects │ │ └── raoscyb │ │ ├── calibrations │ │ └── 3gqkra3 │ │ │ └── calibration.json │ │ ├── participants │ │ └── vxgtdb2 │ │ │ └── participant.json │ │ ├── project.json │ │ └── recordings │ │ └── gzz7stc │ │ ├── participant.json │ │ ├── recording.json │ │ ├── segments │ │ └── 1 │ │ │ ├── calibration.json │ │ │ ├── et.tslv.gz │ │ │ ├── eyesstream.mp4 │ │ │ ├── fullstream.mp4 │ │ │ ├── livedata.json.gz │ │ │ ├── mems.tslv.gz │ │ │ └── segment.json │ │ └── sysinfo.json └── demodata.txt ├── images ├── splash.png └── videopause.png └── results └── demoresults.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | results/* 3 | *.asv 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "GlassesViewer"] 2 | path = GlassesViewer 3 | url = https://github.com/dcnieho/GlassesViewer.git 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GazeCode (GlassesViewer integrated + Pupil Labs Invisible/Neon supported version) 2 | 3 | This software allows for fast and flexible manual classification of any 4 | mobile eye-tracking data. Currently Pupil Labs, Pupil Labs Invisible, Pupil Labs Neon, SMI, Positive Science and Tobii Pro Glasses 2 & 3 data 5 | are supported. The software loads an exported visualisation (video file) 6 | and the data of a mobile eye-tracking measurement, runs a fixation 7 | detection algorithm (adaptation of Hooge and Camps, 2013, Frontiers in Psychology, 4, 8 | 996) and offers a simple to use interface to browse through and 9 | categorise the detected fixations. This version is integrated with 10 | [GlassesViewer](https://github.com/dcnieho/GlassesViewer) to 11 | enable directly opening Tobii Pro Glasses 2 and 3 recordings as stored on the recording 12 | unit's SD card (no visualisation export needed). As such, GlassesViewer needs to be available to GazeCode. 13 | The recommended way to make this work is to use the `git` tool to download GazeCode. 14 | Alternatively you can download GazeCode's components separately and place them in the 15 | right locations. Here are instructions for these two routes: 16 | 17 | Before you download anything, GazeCode and GlassesViewer run in Matlab. Make sure Matlab is installed. 18 | 19 | We know our software works with: 20 | - Matlab 2021B on Mac OS 12.2.1 Monterey 21 | - Matlab 2022b on Windows 11 22 | 23 | (other combinations will probably work as well, but have not been tested). 24 | 25 | 1. Using Git 26 | 1. install git from https://git-scm.org if you don't already have it. If you do not 27 | like using the command line/terminal, consider using a graphical git tool such as 28 | SmartGit, which is available free for non-commercial use 29 | 1. Download GazeCode and GlassesViewer in one go using the following command: 30 | `git clone --recurse-submodules -j8 https://github.com/jsbenjamins/gazecode.git` 31 | 1. Should this not work due to your git version being too old, try executing the 32 | following commands: 33 | ``` 34 | git clone https://github.com/jsbenjamins/gazecode.git 35 | cd gazecode 36 | git submodule update --init --recursive 37 | ``` 38 | If you have already cloned GazeCode but do not have the GlassesViewer submodule populated yet, 39 | issuing the `git submodule update --init --recursive` command will take care of that. 40 | 1. Manual download: 41 | 1. First download GazeCode and place it, unzipped if necessary, in your preferred folder. 42 | 1. Then download GlassesViewer (available from https://github.com/dcnieho/GlassesViewer). 43 | 1. Put the GlassesViewer directory inside GazeCode at the right location: 44 | `/GlassesViewer`). 45 | 46 | How to use: 47 | 1) Place the GazeCode files in a directory to which you can easily navigate 48 | in Matlab 49 | 50 | 2) Prepare the data you want to analyze: 51 | 1. When using Pupil-labs, SMI or Positive Science data: 52 | 1. If needed, export both a visualisation video of raw mobile eye-tracking data and 53 | the data itself. 54 | 1. Put the video file and the data file in the data folder of GazeCode 55 | (demo data are available at http://tinyurl.com/gazecodedemodata for Pupil-labs, SMI and Positive Science) 56 | 57 | 1. Tobii Pro Glasses 2 recordings as stored on its SD card can be directly 58 | loaded with GazeCode by making use of functionality provided by 59 | [GlassesViewer](https://github.com/dcnieho/GlassesViewer). 60 | (demo data for Tobii Glasses are included in this repository) 61 | 1. [GlassesViewer](https://github.com/dcnieho/GlassesViewer) allows to 62 | manually label events in the eye-tracking data or other data streams 63 | provided by the Tobii Pro Glasses 2 to be coded manually, or for algorithmic 64 | coding to be manually adjusted. GlassesViewer can also be used to simply 65 | view the labeled events. One of these labeled events can then be selected 66 | when opening a recording in GazeCode. See [GlassesViewer's 67 | manual](https://github.com/dcnieho/GlassesViewer/blob/master/manual.md) for 68 | the complete workflow. 69 | 70 | 3) Find a set of nine 100 x 100 non-transparant PNGs (images) reflecting the 71 | different categories you want to use (thenounproject.com is good place to 72 | start). Use white empty PNGs if you have less than 9 categories. Put 73 | these files in the categories folder of GazeCode. 74 | 75 | 4) In Matlab set the working directory to the code folder of GazeCode and 76 | type gazecode in the Matlab command window to start GazeCode. 77 | 78 | 5) When prompted, point GazeCode to the category set, files and folders it 79 | requests. 80 | 81 | 6) Browse through the detected fixations using the arrow buttons (or Z 82 | and X keys on the keyboard) in the left panel of GazeCode. 83 | 84 | 7) Assign a category to a fixation by using the category buttons in the 85 | right panel of GazeCode or use (numpad) keyboard keys 1-9. Category 86 | buttons are numbered left-to-right, bottom-to-top (analogue to the 87 | keyboard numpad). Numpad 0 resets a category. 88 | 89 | 8) Category codes of fixations can be exported to a file using the Save 90 | option in the menu of GazeCode. 91 | 92 | This open-source toolbox has been developed by J.S. Benjamins, R.S. Hessels 93 | and I.T.C. Hooge. When you use it, please cite: 94 | 95 | [Jeroen S. Benjamins, Roy S. Hessels, and Ignace T. C. Hooge. 2018. 96 | Gazecode: open-source software for manual mapping of mobile eye-tracking 97 | data. Proceedings of the 2018 ACM Symposium on Eye Tracking Research & 98 | Applications (ETRA '18). ACM, New York, NY, USA, Article 54. DOI: 10.1145/3204493.3204568](https://doi.org/10.1145/3204493.3204568) 99 | 100 | For importing data from Tobii Glasses 2 & 3, GazeCode uses 101 | [GlassesViewer](https://github.com/dcnieho/GlassesViewer). When 102 | using this toolbox with Tobii Glasses data, please also cite 103 | [Niehorster, D.C., Hessels, R.S., and Benjamins, J.S. (2020). 104 | GlassesViewer: Open-source software for viewing and analyzing data from 105 | the Tobii Pro Glasses 2 eye tracker. Behavior Research Methods. doi: 106 | 10.3758/s13428-019-01314-1](https://link.springer.com/article/10.3758/s13428-019-01314-1) 107 | 108 | For more information, questions, or to check whether we have updated to a 109 | better version, e-mail: j.s.benjamins@uu.nl GazeCode is available from 110 | www.github.com/jsbenjamins/gazecode and GlassesViewer from 111 | https://github.com/dcnieho/glassesviewer 112 | 113 | Most parts of GazeCode are licensed under the Creative Commons Attribution 114 | 4.0 (CC BY 4.0) license. Some functions are under MIT license, and some 115 | may be under other licenses. 116 | -------------------------------------------------------------------------------- /categories/democategory/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/1.png -------------------------------------------------------------------------------- /categories/democategory/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/2.png -------------------------------------------------------------------------------- /categories/democategory/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/3.png -------------------------------------------------------------------------------- /categories/democategory/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/4.png -------------------------------------------------------------------------------- /categories/democategory/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/5.png -------------------------------------------------------------------------------- /categories/democategory/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/6.png -------------------------------------------------------------------------------- /categories/democategory/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/7.png -------------------------------------------------------------------------------- /categories/democategory/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/8.png -------------------------------------------------------------------------------- /categories/democategory/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/categories/democategory/9.png -------------------------------------------------------------------------------- /code/constructNPYheader.m: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function header = constructNPYheader(dataType, shape, varargin) 5 | 6 | if ~isempty(varargin) 7 | fortranOrder = varargin{1}; % must be true/false 8 | littleEndian = varargin{2}; % must be true/false 9 | else 10 | fortranOrder = true; 11 | littleEndian = true; 12 | end 13 | 14 | dtypesMatlab = {'uint8','uint16','uint32','uint64','int8','int16','int32','int64','single','double', 'logical'}; 15 | dtypesNPY = {'u1', 'u2', 'u4', 'u8', 'i1', 'i2', 'i4', 'i8', 'f4', 'f8', 'b1'}; 16 | 17 | magicString = uint8([147 78 85 77 80 89]); %x93NUMPY 18 | 19 | majorVersion = uint8(1); 20 | minorVersion = uint8(0); 21 | 22 | % build the dict specifying data type, array order, endianness, and 23 | % shape 24 | dictString = '{''descr'': '''; 25 | 26 | if littleEndian 27 | dictString = [dictString '<']; 28 | else 29 | dictString = [dictString '>']; 30 | end 31 | 32 | dictString = [dictString dtypesNPY{strcmp(dtypesMatlab,dataType)} ''', ']; 33 | 34 | dictString = [dictString '''fortran_order'': ']; 35 | 36 | if fortranOrder 37 | dictString = [dictString 'True, ']; 38 | else 39 | dictString = [dictString 'False, ']; 40 | end 41 | 42 | dictString = [dictString '''shape'': (']; 43 | 44 | % if length(shape)==1 && shape==1 45 | % 46 | % else 47 | % for s = 1:length(shape) 48 | % if s==length(shape) && shape(s)==1 49 | % 50 | % else 51 | % dictString = [dictString num2str(shape(s))]; 52 | % if length(shape)>1 && s+1==length(shape) && shape(s+1)==1 53 | % dictString = [dictString ',']; 54 | % elseif length(shape)>1 && s %s', tempFilename, inFilename, outFilename)); 38 | 39 | otherwise 40 | fprintf(1, 'I don''t know how to concatenate files for your OS, but you can finish making the NPY youself by concatenating %s with %s.\n', tempFilename, inFilename); 41 | end 42 | 43 | -------------------------------------------------------------------------------- /code/detectfixaties2015.m: -------------------------------------------------------------------------------- 1 | function [fmark] = detectfixaties2015(mvel,f,time) 2 | 3 | % cleaned up on 4 | % 16 october 2011 IH 5 | 6 | thr = f.thr; 7 | counter = f.counter; 8 | minfix = f.minfix; % minfix in ms 9 | 10 | qvel = mvel < thr; % look for velocity below threshold 11 | qnotnan = ~isnan(mvel); 12 | qall = qnotnan & qvel; 13 | meanvel = mean(mvel(qall)); % determine the velocity mean during fixations 14 | stdvel = std(mvel(qall)); % determine the velocity std during fixations 15 | 16 | counter = 0; 17 | oldthr = 0; 18 | while 1, 19 | thr2 = meanvel + f.lambda*stdvel; 20 | qvel = mvel < thr2; % look for velocity below threshold 21 | 22 | if round(thr2) == round(oldthr) | counter == f.counter, % f.counter for maximum number of iterations 23 | break; 24 | end 25 | meanvel = mean(mvel(qvel)); 26 | stdvel = std(mvel(qvel)); % determine the velocity std during fixations 27 | oldthr = thr2; 28 | counter = counter + 1; 29 | end 30 | 31 | thr2 = meanvel + 3*stdvel; % determine new threshold based on data noise 32 | qvel = mvel < thr2; % look for velocity below threshold 33 | [on,off] = detectswitches(qvel'); % determine fixations 34 | 35 | on = time(on); % convcert to time 36 | off = time(off); % convert to time 37 | 38 | qfix = off - on > minfix; % look for small fixations 39 | on = on(qfix); % delete fixations smaller than minfix 40 | off = off(qfix); % delete fixations smaller than minfix 41 | 42 | on(2:end) = on(2:end); % 43 | off(1:end-1) = off(1:end-1); % 44 | 45 | fmark = sort([on;off]); % sort the markers 46 | -------------------------------------------------------------------------------- /code/detectfixaties2018.m: -------------------------------------------------------------------------------- 1 | function [fmark,thr2] = detectfixaties2018(mvel,f,time) 2 | 3 | % cleaned up on 4 | % 16 october 2011 IH 5 | 6 | thr = f.thr; 7 | counter = f.counter; 8 | minfix = f.minfix; % minfix in ms 9 | 10 | qvel = mvel < thr; % look for velocity below threshold 11 | qnotnan = ~isnan(mvel); 12 | qall = qnotnan & qvel; 13 | meanvel = mean(mvel(qall)); % determine the velocity mean during fixations 14 | stdvel = std(mvel(qall)); % determine the velocity std during fixations 15 | 16 | counter = 0; 17 | oldthr = 0; 18 | while 1, 19 | thr2 = meanvel + f.lambda*stdvel; 20 | qvel = mvel < thr2; % look for velocity below threshold 21 | 22 | if round(thr2) == round(oldthr) | counter == f.counter, % f.counter for maximum number of iterations 23 | break; 24 | end 25 | meanvel = mean(mvel(qvel)); 26 | stdvel = std(mvel(qvel)); % determine the velocity std during fixations 27 | oldthr = thr2; 28 | counter = counter + 1; 29 | end 30 | 31 | thr2 = meanvel + 3*stdvel; % determine new threshold based on data noise 32 | qvel = mvel < thr2; % look for velocity below threshold 33 | [on,off] = detectswitches(qvel'); % determine fixations 34 | 35 | on = time(on); % convcert to time 36 | off = time(off); % convert to time 37 | 38 | qfix = off - on > minfix; % look for small fixations 39 | on = on(qfix); % delete fixations smaller than minfix 40 | off = off(qfix); % delete fixations smaller than minfix 41 | 42 | on(2:end) = on(2:end); % 43 | off(1:end-1) = off(1:end-1); % 44 | 45 | fmark = sort([on;off]); % sort the markers 46 | -------------------------------------------------------------------------------- /code/detectfixaties2018fmark.m: -------------------------------------------------------------------------------- 1 | function [fmark] = detectfixaties2018fmark(mvel,f,time,thr2) 2 | 3 | minfix = f.minfix; % minfix in ms 4 | 5 | qvel = mvel < thr2; % look for velocity below threshold 6 | [on,off] = detectswitches(qvel'); % determine fixations 7 | 8 | on = time(on); % convcert to time 9 | off = time(off); % convert to time 10 | 11 | qfix = off - on > minfix; % look for small fixations 12 | on = on(qfix); % delete fixations smaller than minfix 13 | off = off(qfix); % delete fixations smaller than minfix 14 | 15 | on(2:end) = on(2:end); % 16 | off(1:end-1) = off(1:end-1); % 17 | 18 | fmark = sort([on;off]); % sort the markers 19 | -------------------------------------------------------------------------------- /code/detectfixaties2018thr.m: -------------------------------------------------------------------------------- 1 | function [thrfinal] = detectfixaties2018thr(mvel,f) 2 | 3 | % cleaned up on 4 | % 16 october 2011 IH 5 | 6 | thr = f.thr; 7 | counter = f.counter; 8 | minfix = f.minfix; % minfix in ms 9 | 10 | qvel = mvel < thr; % look for velocity below threshold 11 | qnotnan = ~isnan(mvel); 12 | qall = qnotnan & qvel; 13 | meanvel = mean(mvel(qall)); % determine the velocity mean during fixations 14 | stdvel = std(mvel(qall)); % determine the velocity std during fixations 15 | 16 | counter = 0; 17 | oldthr = 0; 18 | while 1, 19 | thr2 = meanvel + f.lambda*stdvel; 20 | qvel = mvel < thr2; % look for velocity below threshold 21 | 22 | if round(thr2) == round(oldthr) | counter == f.counter, % f.counter for maximum number of iterations 23 | break; 24 | end 25 | meanvel = mean(mvel(qvel)); 26 | stdvel = std(mvel(qvel)); % determine the velocity std during fixations 27 | oldthr = thr2; 28 | counter = counter + 1; 29 | end 30 | 31 | thr2 = meanvel + 3*stdvel; % determine new threshold based on data noise 32 | 33 | % make vector for thr2 of length mvel 34 | thrfinal = repmat(thr2,numel(mvel),1); -------------------------------------------------------------------------------- /code/detectfixaties2022thr.m: -------------------------------------------------------------------------------- 1 | function [thrfinal] = detectfixaties2022thr(mvel,f) 2 | 3 | % cleaned up on 4 | % 16 october 2011 IH 5 | 6 | thr = f.thr; 7 | counter = f.counter; 8 | minfix = f.minfix; % minfix in ms 9 | 10 | qvel = mvel < thr; % look for velocity below threshold 11 | qnotnan = ~isnan(mvel); 12 | qall = qnotnan & qvel; 13 | meanvel = mean(mvel(qall)); % determine the velocity mean during fixations 14 | stdvel = std(mvel(qall)); % determine the velocity std during fixations 15 | 16 | counter = 0; 17 | oldthr = 0; 18 | while 1, 19 | thr2 = meanvel + f.lambda*stdvel; 20 | qvel = mvel < thr2; % look for velocity below threshold 21 | 22 | if round(thr2) == round(oldthr) | counter == f.counter, % f.counter for maximum number of iterations 23 | break; 24 | end 25 | meanvel = mean(mvel(qvel)); 26 | stdvel = std(mvel(qvel)); % determine the velocity std during fixations 27 | oldthr = thr2; 28 | counter = counter + 1; 29 | end 30 | 31 | thr2 = meanvel + f.lambda*stdvel; % determine new threshold based on data noise 32 | 33 | % make vector for thr2 of length mvel 34 | thrfinal = repmat(thr2,numel(mvel),1); -------------------------------------------------------------------------------- /code/detectswitches.m: -------------------------------------------------------------------------------- 1 | function [on2,off2] = detectswitches(data) 2 | % feed this fucntion a boolean vector (zeros and ones only). 3 | % if it contains a series of ones it will return the start and end position 4 | 5 | % add zeros to beginning and end, such that ones at the beginning and end of 6 | % the original data file get ercognized as such 7 | data = [0 data 0]; 8 | 9 | % find the transitions by using the shifting trick 10 | data11 = data(1:end-1); 11 | data12 = data(2:end); 12 | 13 | numvect = [1:1:length(data11)]; 14 | 15 | mdata = data11 - data12; 16 | on = mdata == -1; % this is a sample to early 17 | off = mdata == 1; % this is the correct one 18 | 19 | on2 = numvect(on); % correct for the index of too early 20 | off2 = numvect(off)-1; 21 | -------------------------------------------------------------------------------- /code/detvel.m: -------------------------------------------------------------------------------- 1 | function [v] = detvel(x,time) 2 | 3 | dt1 = time(2:end-1) - time(1:end-2); 4 | dx1 = x(2:end-1) - x(1:end-2); 5 | dt2 = time(3:end) - time(2:end-1); 6 | dx2 = x(3:end) - x(2:end-1); 7 | v = [NaN;(dx1./dt1 + dx2./dt2)/2;NaN]; 8 | -------------------------------------------------------------------------------- /code/dispTobiiInstructions.m: -------------------------------------------------------------------------------- 1 | function dispTobiiInstructions() 2 | % Tobii Pro Glasses note 3 | % ====================== 4 | % 5 | % Tobii Pro Glasses has no default export filenames for data and visualisa 6 | % tion videos, and saves data in a proprietary file format with extension TSV. 7 | % Open the TSV file in Excel and save it as a tab delimited text, extension 8 | % TXT. For easy of use store this data export TXT file and the exported 9 | % visualisation (in MP4 format) in one and the same folder. GazeCode will 10 | % prompt you for this video and data file. 11 | % 12 | % This version of GazeCode expects a certian number of columns in the exported 13 | % data file, make sure when exporting the data you only export these varia 14 | % bles and place them in this particular order: 15 | % 16 | % Project name 17 | % Recording name 18 | % Recording date 19 | % Recording timestamp 20 | % Gaze point X 21 | % Gaze point Y 22 | % Gaze direction left X 23 | % Gaze direction left Y 24 | % Pupil position left X 25 | % Pupil position left Y 26 | % Eye movement type 27 | % Gaze event duration 28 | % 29 | % If you do not have these files ready, choose "No & quit" at the next 30 | % prompt you get. 31 | % ====================== 32 | 33 | commandwindow; 34 | disp('... press any key to continue'); 35 | pause(); -------------------------------------------------------------------------------- /code/fixdetect.m: -------------------------------------------------------------------------------- 1 | function fmark = fixdetect(datx,daty,dattime,gv) 2 | 3 | dat.x = datx; 4 | dat.y = daty; 5 | dat.time = dattime; 6 | 7 | %%%%% eye tracker defaults 8 | gv.tracker = gv.datatype; 9 | 10 | 11 | gv.camres = gv.wcr; % resolution of eye camera 12 | gv.trackres = gv.ecr; % resolution of scene camera 13 | 14 | 15 | f = initeventdetect(gv.tracker); 16 | 17 | %%%%% Make sure there are NaNs where there is data loss (pupillabs uses zero) 18 | % assuming there is an x, y and time signal 19 | % for example dat.x, dat.y, dat.time 20 | 21 | %%%%% determine velocity 22 | vx = detvel(dat.x,dat.time); 23 | vy = detvel(dat.y,dat.time); 24 | dat.v = pythagoras(vx,vy); 25 | 26 | %%%%% detect fixations 27 | fmark = detectfixaties2015(dat.v,f,dat.time); 28 | -------------------------------------------------------------------------------- /code/fixdetectmovingwindow.m: -------------------------------------------------------------------------------- 1 | function fmark = fixdetectmovingwindow(datx,daty,dattime,gv) 2 | % NOTE: lamdba does not function exactly as you might think. Lamdba is used 3 | % to iteratively remove high velocities (mean + lamdba * std). Final 4 | % velocity is hard coded to mean + 3*std in detectfixaties2018thr. 5 | 6 | dat.x = datx; 7 | dat.y = daty; 8 | dat.time = dattime; 9 | 10 | %%%%% eye tracker defaults 11 | gv.tracker = gv.datatype; 12 | 13 | 14 | gv.camres = gv.wcr; % resolution of eye camera 15 | gv.trackres = gv.ecr; % resolution of scene camera 16 | 17 | 18 | f = initeventdetect(gv.tracker); 19 | 20 | %%%%% Make sure there are NaNs where there is data loss (pupillabs uses zero) 21 | % assuming there is an x, y and time signal 22 | % for example dat.x, dat.y, dat.time 23 | 24 | %%%%% determine velocity 25 | vx = detvel(dat.x,dat.time); 26 | vy = detvel(dat.y,dat.time); 27 | dat.v = pythagoras(vx,vy); 28 | 29 | %%%%% detect fixations with moving window averaged threshold 30 | % max windowstart 31 | maxwinstart = numel(dat.time)-f.windowsize+1; 32 | 33 | for b=1:maxwinstart 34 | %tempt = dat.time(b:b+f.windowsize-1); % get time 35 | tempv = dat.v(b:b+f.windowsize-1); % get vel 36 | 37 | % get fixation-classification threshold 38 | thrfinal = detectfixaties2022thr(tempv,f); 39 | 40 | if b==1 41 | thr = thrfinal; 42 | ninwin = ones(length(thrfinal),1); 43 | else 44 | % append threshod 45 | thr(end-length(thrfinal)+2:end) = thr(end-length(thrfinal)+2:end)+thrfinal(1:end-1); 46 | thr = [thr; thrfinal(end)]; 47 | % update number of times in win 48 | ninwin(end-length(thrfinal)+2:end) = ninwin(end-length(thrfinal)+2:end)+1; 49 | ninwin = [ninwin; 1]; 50 | end 51 | end 52 | 53 | % now get final thr 54 | thr = thr./ninwin; 55 | 56 | fmark = detectfixaties2018fmark(dat.v,f,dat.time,thr); -------------------------------------------------------------------------------- /code/folderfromfolder.m: -------------------------------------------------------------------------------- 1 | function [fold,teller] = folderfromfolder(folder,mode) 2 | % [fold,nfold] = folderfromfolder(folder) 3 | % 4 | % Returns all folders in the folder "folder" 5 | 6 | if nargin == 1 7 | silent = false; 8 | elseif strcmp(mode,'silent') 9 | silent = true; 10 | else 11 | silent = false; 12 | end 13 | 14 | A = double('A'); % asci-code A 15 | Z = double('Z'); % asci-code Z 16 | a = double('a'); % asci-code a 17 | z = double('z'); % asci-code z 18 | nul = double('0'); % asci-code 0 19 | negen = double('9'); % asci-code 9 20 | 21 | filelist = dir(folder); 22 | 23 | teller = 0; 24 | for p=1:length(filelist), 25 | stri = filelist(p).name; 26 | fc = double(stri(1)); % fc is asci code of first character 27 | if ((fc >= A && fc <= Z)||(fc >= a && fc <= z)||(fc >= nul && fc <= negen)) && filelist(p).isdir==1, 28 | teller = teller +1; 29 | fold(teller).name = stri; 30 | fold(teller).date = datestr(filelist(p).datenum,1); 31 | end 32 | end 33 | 34 | if teller == 0, 35 | fold = []; 36 | if silent 37 | disp(sprintf(['folderfromfolder: No folders found in: ' strrep(folder,'\','\\')])); 38 | elseif ~silent 39 | error(['folderfromfolder: No folders found in: ' folder]); 40 | end 41 | end -------------------------------------------------------------------------------- /code/gazecode.m: -------------------------------------------------------------------------------- 1 | function gazecode(settings) 2 | 3 | % GazeCode (GlassesViewer integrated + Pupil Labs Invisible supported version) 4 | % 5 | % See readme.md for usage details 6 | % 7 | % This open-source toolbox has been developed by J.S. Benjamins, R.S. 8 | % Hessels and I.T.C. Hooge. When you use it, please cite: 9 | % 10 | % Jeroen S. Benjamins, Roy S. Hessels, and Ignace T. C. Hooge. 2018. 11 | % Gazecode: open-source software for manual mapping of mobile eye-tracking 12 | % data. In Proceedings of the 2018 ACM Symposium on Eye Tracking Research & 13 | % Applications (ETRA '18). ACM, New York, NY, USA, Article 54, 4 pages. 14 | % DOI: https://doi.org/10.1145/3204493.3204568 15 | % 16 | % For importing data from Tobii Glasses 2 and 3, it uses GlassesViewer. When 17 | % using this toolbox with Tobii Glasses data, please also cite 18 | % Niehorster, D.C., Hessels, R.S., and Benjamins, J.S. (2020). 19 | % GlassesViewer: Open-source software for viewing and analyzing data from 20 | % the Tobii Pro Glasses 2 eye tracker. Behavior Research Methods, 52, 21 | % 1244–1253. 22 | % DOI: https://doi.org/10.3758/s13428-019-01314-1 23 | % 24 | % For more information, questions, or to check whether we have updated to a 25 | % better version, e-mail: j.s.benjamins@uu.nl. GazeCode is available from 26 | % www.github.com/jsbenjamins/gazecode and GlassesViewer from 27 | % https://github.com/dcnieho/glassesviewer 28 | % 29 | % Most parts of GazeCode are licensed under the Creative Commons 30 | % Attribution 4.0 (CC BY 4.0) license. Some functions are under MIT 31 | % license, and some may be under other licenses. 32 | % 33 | % Tested on: 34 | % Matlab 2021b on Mac OSX 12.2.1 35 | % Matlab 2022bx, on Windows 11 36 | 37 | %% start fresh 38 | clear all; close all; clc; 39 | qDEBUG = false; 40 | if qDEBUG 41 | dbstop if error 42 | end 43 | 44 | % myDir = fileparts(mfilename('fullpath')); 45 | 46 | 47 | % set it to false. If coding from GlassesViewer exists it will be loaded 48 | % from there. 49 | dataIsCrap = false; 50 | 51 | %% HARDCODED 52 | 53 | resampleFreq = 200; 54 | 55 | %% ======================================================================== 56 | % BIG NOTE: every variable that is needed somewhere in this code is stored 57 | % in a structure array called gv that is added to the main window with the 58 | % handle hm as userdata. When changing or adding variables in this struct 59 | % it is thus needed to fetch using gv = get(hm, 'userdata') an update using 60 | % use set(hm,'userdata',gv); 61 | %% ======================================================================== 62 | 63 | % global gv; 64 | gv.curframe = 1; 65 | gv.minframe = 1; 66 | gv.maxframe = 1; 67 | gv.curfix = 1; 68 | gv.maxfix = 1; 69 | skipdataload = false; 70 | 71 | % buttons 72 | gv.fwdbut = 'x'; % move forward (next fixation) 73 | gv.bckbut = 'z'; % move backward (previous fixation) 74 | gv.cat1but = {'1','numpad1'}; 75 | gv.cat2but = {'2','numpad2'}; 76 | gv.cat3but = {'3','numpad3'}; 77 | gv.cat4but = {'4','numpad4'}; 78 | gv.cat5but = {'5','numpad5'}; 79 | gv.cat6but = {'6','numpad6'}; 80 | gv.cat7but = {'7','numpad7'}; 81 | gv.cat8but = {'8','numpad8'}; 82 | gv.cat9but = {'9','numpad9'}; 83 | gv.catjbut = 'j'; 84 | gv.cattbut = 't'; 85 | 86 | 87 | %% directories and settings 88 | gv.fs = filesep; 89 | gv.codedir = cd; cd ..; 90 | gv.rootdir = cd; 91 | cd(gv.rootdir); 92 | 93 | gv.resdir = [cd gv.fs 'results']; 94 | cd(gv.resdir); 95 | 96 | cd ..; 97 | 98 | gv.catdir = [cd gv.fs 'categories']; 99 | cd(gv.catdir); 100 | 101 | cd ..; 102 | 103 | gv.appdir = [cd gv.fs 'images']; 104 | cd(gv.appdir); 105 | 106 | cd ..; 107 | 108 | gv.datadir = [cd gv.fs 'data']; 109 | cd(gv.datadir); 110 | 111 | cd(gv.rootdir); 112 | 113 | gv.glassesviewerdir = [cd gv.fs 'GlassesViewer']; 114 | cd(gv.glassesviewerdir); 115 | 116 | addpath(genpath(gv.rootdir),genpath(gv.glassesviewerdir)); 117 | cd(gv.codedir); 118 | 119 | if nargin<1 || isempty(settings) 120 | if ~isempty(which('matlab.internal.webservices.fromJSON')) 121 | jsondecoder = @matlab.internal.webservices.fromJSON; 122 | elseif ~isempty(which('jsondecode')) 123 | jsondecoder = @jsondecode; 124 | else 125 | error('Your MATLAB version does not provide a way to decode json (which means its really old), upgrade to something newer'); 126 | end 127 | settings = jsondecoder(fileread(fullfile(gv.glassesviewerdir,'defaults.json'))); 128 | end 129 | 130 | gv.splashh = gazesplash([gv.appdir gv.fs 'splash.png']); 131 | gv.pauseplaat = imread(fullfile(gv.appdir, 'videopause.png')); 132 | pause(1); 133 | close(gv.splashh); 134 | %% Select type of data you want to code (currently a version for Pupil Labs and Tobii Pro Glasses are implemented 135 | 136 | % this is now a question dialog, but needs to be changed to a dropdown for 137 | % more options. Question dialog allows for only three options 138 | models = {'Tobii Pro Glasses 2','Tobii Pro Glasses 3','Pupil Labs invisible (200 Hz)','Pupil Labs Neon (200 Hz)','Pupil Labs (first gen + Core)','SMI Glasses','Positive Science'}; 139 | modelIdx = listdlg ('ListString', models,'SelectionMode', 'Single', 'PromptString', 'Select eye-tracker', 'Initialvalue', 1,'Cancelstring','Quit','ListSize',[160 160]); 140 | assert(~isempty(modelIdx),'You did not select a type of mobile eye-tracking data, exiting'); 141 | 142 | gv.datatype = models{modelIdx}; 143 | % set camera settings of eye-tracker data 144 | switch gv.datatype 145 | case 'Pupil Labs (first gen + Core)' 146 | gv.wcr = [1280 720]; % world cam resolution 147 | gv.ecr = [640 480]; % eye cam resolution 148 | case 'SMI Glasses' 149 | gv.wcr = [960 720]; % world cam resolution, can also be 960 x 720 150 | gv.ecr = [640 480]; % eye cam resolution (assumption, not known ATM) 151 | case 'Positive Science' 152 | gv.wcr = [640 480]; % wordl cam resolution 153 | gv.ecr = [320 240]; % eye cam resolution (assumption, not known ATM) 154 | case 'Tobii Pro Glasses 2' 155 | % this is in glassesViewer's export, at 156 | % data.video.scene.width, data.video.scene.height 157 | % data.video.eye.width, data.video.eye.height 158 | case 'Tobii Pro Glasses 3' 159 | % this is in glassesViewer's export, at 160 | % data.video.scene.width, data.video.scene.height 161 | % data.video.eye.width, data.video.eye.height 162 | case 'Pupil Labs invisible (200 Hz)' 163 | gv.wcr = [1088 1080]; 164 | gv.ecr = [1088 1080]; 165 | case 'Pupil Labs Neon (200 Hz)' 166 | gv.wcr = [1600 1200]; 167 | gv.ecr = [192 192]; 168 | end 169 | 170 | %% 171 | % UI interface fix. On Mac/Unix title of dialog boxes is not displayed, 172 | % thus added an extra info pop-up with instruction; 173 | if ~ ispc; uiwait(msgbox('Select directory of categories','Info','modal')); end 174 | disp('Select directory of categories'); 175 | gv.catfoldnaam = uigetdir(gv.catdir,'Select directory of categories'); 176 | % clc; 177 | assert(ischar(gv.catfoldnaam),'You did not select a categories directory, exiting'); 178 | 179 | %% load data folder 180 | switch gv.datatype 181 | case 'Tobii Pro Glasses 2' 182 | % do the selecor thing 183 | % UI interface fix. On Mac/Unix title of dialog boxes is not displayed, 184 | % thus added an extra info pop-up with instruction; 185 | if ~ ispc; uiwait(msgbox('Select projects directory of SD card','Info','modal')); end 186 | disp('Select projects directory of SD card'); 187 | selectedDir = uigetdir(gv.datadir,'Select the PROJECTS directory of your SD card'); 188 | % adding disp as Mac does not show title of UI elements 189 | clc; 190 | assert(ischar(selectedDir),'You did not select a data directory, exiting'); 191 | 192 | if exist(fullfile(selectedDir,'segments'),'dir') && exist(fullfile(selectedDir,'recording.json'),'file') 193 | % user selected what is very likely a recording's dir directly 194 | recordingDir = selectedDir; 195 | else 196 | % assume this is a project dir. G2ProjectParser will fail if it is not 197 | if ~exist(fullfile(selectedDir,'lookup.xls'),'file') 198 | success = G2ProjectParser(selectedDir); 199 | if ~success 200 | error('Could not find projects in the folder: %s',selectedDir); 201 | end 202 | end 203 | recordingDir = recordingSelector(selectedDir); 204 | if isempty(recordingDir) 205 | return 206 | end 207 | end 208 | 209 | gv.foldnaam = recordingDir; 210 | 211 | filmnaam = fullfile(gv.foldnaam,'segments','1','fullstream.mp4'); 212 | gv.filmnaam = filmnaam; 213 | if length(folderfromfolder(fullfile(gv.foldnaam,'segments'))) > 1 214 | disp('found multiple segments'); 215 | gv.filmnaam = {}; 216 | disp('multiple segments'); 217 | subfoldernames = folderfromfolder(fullfile(gv.foldnaam,'segments')); 218 | for ft = 1:length(subfoldernames) 219 | disp(ft); 220 | filmnaam = fullfile(gv.foldnaam,'segments',subfoldernames(ft).name,'fullstream.mp4'); 221 | gv.filmnaam{ft} = filmnaam; 222 | end 223 | gv.multfilm = 1; 224 | else 225 | gv.multfilm =[]; 226 | end 227 | 228 | fid = fopen(fullfile(gv.foldnaam,'recording.json'),'rt'); 229 | recording = jsondecoder(fread(fid,inf,'*char').'); 230 | fclose(fid); 231 | gv.recName = recording.rec_info.Name; 232 | fid = fopen(fullfile(gv.foldnaam,'participant.json'),'rt'); 233 | participant = jsondecoder(fread(fid,inf,'*char').'); 234 | gv.partName = participant.pa_info.Name; 235 | fclose(fid); 236 | case 'Tobii Pro Glasses 3' 237 | % do the selecor thing 238 | % UI interface fix. On Mac/Unix title of dialog boxes is not displayed, 239 | % thus added an extra info pop-up with instruction; 240 | % gv.multfilm is only needed/implemented for TG2, but further down 241 | % cases are TG@ and TG3, so set it to empty here 242 | gv.multfilm =[]; 243 | if ~ ispc; uiwait(msgbox('Select the ROOT directory of your SD card','Info','modal')); end 244 | disp('Select ROOT directory of SD card'); 245 | selectedDir = uigetdir(gv.datadir,'Select ROOT directory of SD card'); 246 | % adding disp as Mac does not show title of UI elements 247 | clc; 248 | assert(ischar(selectedDir),'You did not select a data directory, exiting'); 249 | 250 | if exist(fullfile(selectedDir,'segments'),'dir') && exist(fullfile(selectedDir,'recording.json'),'file') 251 | % user selected what is very likely a recording's dir directly 252 | recordingDir = selectedDir; 253 | else 254 | % assume this is a project dir. G2ProjectParser will fail if it is not 255 | if ~exist(fullfile(selectedDir,'lookup.xls'),'file') 256 | success = G3ProjectParser(selectedDir); 257 | if ~success 258 | error('Could not find projects in the folder: %s',selectedDir); 259 | end 260 | end 261 | recordingDir = recordingSelector(selectedDir); 262 | if isempty(recordingDir) 263 | return 264 | end 265 | end 266 | 267 | gv.foldnaam = recordingDir; 268 | 269 | filmnaam = fullfile(gv.foldnaam,'scenevideo.mp4'); 270 | gv.filmnaam = filmnaam; 271 | 272 | gv.jsonfile = fullfile(gv.foldnaam, 'recording.g3'); 273 | recording = jsondecoder(fileread(gv.jsonfile)); 274 | 275 | gv.recName = recording.name; 276 | 277 | % participant file may not exist 278 | pFile = fullfile(gv.foldnaam,recording.meta_folder,'participant'); 279 | if ~~exist(pFile,"file") 280 | gv.partName = jsondecoder(fileread(pFile)).name; 281 | end 282 | 283 | case 'Pupil Labs invisible (200 Hz)' 284 | if ~ ispc; uiwait(msgbox('Select data directory to code','Info','modal')); end 285 | disp('Select data directory to code'); 286 | gv.foldnaam = uigetdir(gv.datadir,'Select data directory to code'); 287 | % adding disp as Mac does not show title of UI elements 288 | % clc; 289 | assert(ischar(gv.foldnaam),'You did not select a data directory, exiting'); 290 | 291 | filmnaam = strsplit(gv.foldnaam,gv.fs); 292 | gv.filmnaam = filmnaam{end}; 293 | 294 | gv.resdir = [gv.resdir gv.fs gv.filmnaam]; 295 | 296 | gv.foldnaam = fullfile(gv.foldnaam, 'exports','000'); 297 | 298 | 299 | if exist([gv.resdir gv.fs gv.filmnaam '.mat']) > 0 300 | oudofnieuw = questdlg(['There already is a results directory with a file for ',gv.filmnaam,'. Do you want to load previous results or start over? Starting over will overwrite previous results.'],'Folder already labeled?','Load previous','Start over','Load previous'); 301 | if strcmp(oudofnieuw,'Load previous') 302 | % IMPORANT NOTE FOR TESTING! This reloads gv! Use start over when 303 | % making changes to this file. 304 | load([gv.resdir gv.fs gv.filmnaam '.mat']); 305 | skipdataload = true; 306 | else 307 | rmdir(gv.resdir,'s'); 308 | mkdir(gv.resdir); 309 | end 310 | elseif exist(gv.resdir) == 0 311 | mkdir(gv.resdir); 312 | else 313 | % do nothing 314 | end 315 | case 'Pupil Labs Neon (200 Hz)' 316 | if ~ ispc; uiwait(msgbox('Select data directory to code','Info','modal')); end 317 | disp('Select data directory to code'); 318 | gv.foldnaam = uigetdir(gv.datadir,'Select data directory to code'); 319 | % adding disp as Mac does not show title of UI elements 320 | % clc; 321 | assert(ischar(gv.foldnaam),'You did not select a data directory, exiting'); 322 | 323 | filmnaam = strsplit(gv.foldnaam,gv.fs); 324 | gv.filmnaam = filmnaam{end}; 325 | 326 | gv.resdir = [gv.resdir gv.fs gv.filmnaam]; 327 | 328 | gv.foldnaam = fullfile(gv.foldnaam); 329 | 330 | 331 | if exist([gv.resdir gv.fs gv.filmnaam '.mat']) > 0 332 | oudofnieuw = questdlg(['There already is a results directory with a file for ',gv.filmnaam,'. Do you want to load previous results or start over? Starting over will overwrite previous results.'],'Folder already labeled?','Load previous','Start over','Load previous'); 333 | if strcmp(oudofnieuw,'Load previous') 334 | % IMPORANT NOTE FOR TESTING! This reloads gv! Use start over when 335 | % making changes to this file. 336 | load([gv.resdir gv.fs gv.filmnaam '.mat']); 337 | skipdataload = true; 338 | else 339 | rmdir(gv.resdir,'s'); 340 | mkdir(gv.resdir); 341 | end 342 | elseif exist(gv.resdir) == 0 343 | mkdir(gv.resdir); 344 | else 345 | % do nothing 346 | end 347 | 348 | otherwise 349 | % UI interface fix. On Mac/Unix title of dialog boxes is not displayed, 350 | % thus added an extra info pop-up with instruction; 351 | if ~ ispc; uiwait(msgbox('Select data directory to code','Info','modal')); end 352 | disp('Select data directory to code'); 353 | gv.foldnaam = uigetdir(gv.datadir,'Select data directory to code'); 354 | % adding disp as Mac does not show title of UI elements 355 | % clc; 356 | assert(ischar(gv.foldnaam),'You did not select a data directory, exiting'); 357 | 358 | filmnaam = strsplit(gv.foldnaam,gv.fs); 359 | gv.filmnaam = filmnaam{end}; 360 | 361 | gv.resdir = [gv.resdir gv.fs gv.filmnaam]; 362 | 363 | if exist([gv.resdir gv.fs gv.filmnaam '.mat']) > 0 364 | oudofnieuw = questdlg(['There already is a results directory with a file for ',gv.filmnaam,'. Do you want to load previous results or start over? Starting over will overwrite previous results.'],'Folder already labeled?','Load previous','Start over','Load previous'); 365 | if strcmp(oudofnieuw,'Load previous') 366 | % IMPORANT NOTE FOR TESTING! This reloads gv! Use start over when 367 | % making changes to this file. 368 | load([gv.resdir gv.fs gv.filmnaam '.mat']); 369 | skipdataload = true; 370 | else 371 | rmdir(gv.resdir,'s'); 372 | mkdir(gv.resdir); 373 | end 374 | elseif exist(gv.resdir) == 0 375 | mkdir(gv.resdir); 376 | else 377 | % do nothing 378 | end 379 | end 380 | 381 | %% init main screen, don't change this section if you're not sure what you are doing 382 | hm = figure('Name','GazeCode','NumberTitle','off','Visible','off'); 383 | hmmar = [100 50]; 384 | 385 | set(hm, 'Units', 'pixels'); 386 | ws = truescreensize(); 387 | ws = [1 1 ws]; 388 | 389 | % set figure full screen, position is bottom left width height!; 390 | set(hm,'Position',[ws(1) + hmmar(1), ws(2) + hmmar(2), ws(3)-2*hmmar(1), ws(4)-2*hmmar(2)]); 391 | % this is the first time gv is set to hm! 392 | set(hm,'windowkeypressfcn',@verwerkknop,'closerequestfcn',@sluitaf,'userdata',gv); 393 | 394 | % get coordinates main screen 395 | hmpos = get(hm,'Position'); 396 | 397 | hmxmax = hmpos(3); 398 | hmymax = hmpos(4); 399 | 400 | % disable all button etc. 401 | set(hm,'MenuBar','none'); 402 | set(hm,'DockControls','off'); 403 | set(hm,'Resize','off'); 404 | 405 | 406 | % main menu 1 407 | mm1 = uimenu(hm,'Label','Menu'); 408 | % sub menu of main menu 1 409 | sm0 = uimenu(mm1,'Label','Save to text','CallBack',@savetotext); 410 | sm1 = uimenu(mm1,'Label','About GazeCode','CallBack','uiwait(msgbox(''This is version 1.0.1'',''About FixLabel''))'); 411 | 412 | % first get sizes of panels 413 | pmar = [50 20]; 414 | knopsizeL = [100 100]; 415 | knopsizeR = [150 150]; 416 | knopmar = [20 10]; 417 | panelWidth(1) = (hmxmax/2)-2*pmar(1); 418 | panelWidth(2) = knopsizeR(1)*3+knopmar(1)*4; 419 | % position them 420 | leftoverSpace = hmxmax - sum(panelWidth); 421 | pmarhn = leftoverSpace/3; 422 | 423 | %% left panel (Child 2 of hm), don't change this section if you're not sure what you are doing 424 | 425 | lp = uipanel(hm,'Title','Fix Playback/Lookup','Units','pixels','FontSize',16,'BackgroundColor',[0.8 0.8 0.8]); 426 | gv.lp = lp; 427 | 428 | set(lp,'Position',[1+pmarhn 1+pmar(2) panelWidth(1) hmymax-2*pmar(2)]); 429 | lpsize = get(lp,'Position'); 430 | lpsize = lpsize(3:end); 431 | 432 | 433 | 434 | fwknop = uicontrol(lp,'Style','pushbutton','string','>>','callback',@playforward,'userdata',gv.fwdbut); 435 | set(fwknop,'position',[lpsize(1)-knopsizeL(1)-knopmar(1) knopmar(2) knopsizeL]); 436 | % set(fwknop,'backgroundcolor',[1 0.5 0]); 437 | set(fwknop,'fontsize',20); 438 | 439 | bkknop = uicontrol(lp,'Style','pushbutton','string','<<','callback',@playback,'userdata',gv.bckbut); 440 | set(bkknop,'position',[knopmar knopsizeL]); 441 | % set(bkknop,'backgroundcolor',[1 0.5 0]); 442 | set(bkknop,'fontsize',20); 443 | 444 | %% right panel (child 1 of hm), don't change this section if you're not sure what you are doing 445 | rp = uipanel('Title','Categories','Units','pixels','FontSize',16,'BackgroundColor',[0.8 0.8 0.8]); 446 | 447 | set(rp,'Position',[pmarhn*2+panelWidth(1) 1+pmar(2) panelWidth(2) hmymax-2*pmar(2)]); 448 | rpsize = get(rp,'Position'); 449 | rpsize = rpsize(3:end); 450 | 451 | % right panel buttons 452 | % make grid 453 | widthgrid = knopsizeR(1)*3+2*knopmar(1); 454 | heightgrid = knopsizeR(2)*3+2*knopmar(1); 455 | xstartgrid = floor((rpsize(1) - widthgrid)/2); 456 | ystartgrid = rpsize(2) - heightgrid - knopmar(1); 457 | 458 | opzij = knopmar(1)+knopsizeR(1); 459 | omhoog = knopmar(1)+knopsizeR(2); 460 | gridpos = [... 461 | xstartgrid, ystartgrid, knopsizeR;... 462 | xstartgrid+opzij, ystartgrid, knopsizeR;... 463 | xstartgrid+2*opzij, ystartgrid, knopsizeR;... 464 | 465 | xstartgrid ystartgrid+omhoog knopsizeR;... 466 | xstartgrid+opzij ystartgrid+omhoog knopsizeR;... 467 | xstartgrid+2*opzij ystartgrid+omhoog knopsizeR;... 468 | 469 | xstartgrid ystartgrid+2*omhoog knopsizeR;... 470 | xstartgrid+opzij ystartgrid+2*omhoog knopsizeR;... 471 | xstartgrid+2*opzij ystartgrid+2*omhoog knopsizeR;... 472 | ]; 473 | 474 | gv.knoppen = []; 475 | 476 | for p = 1:size(gridpos,1) 477 | gv.knoppen(p) = uicontrol(rp,'Style','pushbutton','HorizontalAlignment','left','string','','callback',@labelfix,'userdata',gv); 478 | set(gv.knoppen(p),'position',gridpos(p,:)); 479 | set(gv.knoppen(p),'backgroundcolor',[1 1 1]); 480 | set(gv.knoppen(p),'fontsize',20); 481 | set(gv.knoppen(p),'userdata',p); 482 | % imgData = imread([gv.catfoldnaam gv.fs num2str(p) '.png']); 483 | % imgData = imgData./maxall(imgData); 484 | % imgData = double(imgData); 485 | [imgData,cmap,a] = imread([gv.catfoldnaam gv.fs num2str(p) '.png']); 486 | if ~isempty(cmap) 487 | % if indexed image, turn into normal 488 | cmap = permute(cmap,[1 3 2]); 489 | imgData = reshape(cmap(imdata(:)+1,1,:),[size(imdata) 3]); 490 | % don't think this occurs, but be safe in case it does 491 | if isa(a,'uint8') 492 | a = double(a)/255; 493 | end 494 | end 495 | % imgData = cat(3,imgData,a); % add alpha channel, if any 496 | if isa(imgData,'uint8') 497 | imgData = double(imgData)/255; 498 | elseif isa(imgData,'uint16') 499 | imgData = double(imgData)/65535; 500 | end 501 | set(gv.knoppen(p),'CData',imgData); 502 | end 503 | set(hm,'userdata',gv); 504 | 505 | %% position axes of right panel to upper part of right panel. Don't change this section if you're not sure what you are doing 506 | rpax = axes('Units','normal', 'Position', [0 0 1 1], 'Parent', rp,'visible','off'); 507 | set(rpax,'Units','pixels'); 508 | rpaxpos = get(rpax,'position'); 509 | 510 | tempx = rpaxpos(1); 511 | tempy = rpaxpos(2); 512 | tempw = rpaxpos(3); 513 | temph = rpaxpos(4); 514 | 515 | tempw = tempw - mod(tempw,16); 516 | temph = (tempw/16) * 9; 517 | 518 | tempx = floor((rpaxpos(3) - tempw)/2); 519 | tempy = rpaxpos(4)-temph; 520 | 521 | set(rpax,'Position',[tempx tempy tempw temph],'visible','off'); 522 | gv.rpaxpos = get(rpax,'position'); 523 | 524 | 525 | %% position axes of left panel to upper part of left panel. Don't change this section if you're not sure what you are doing 526 | lpax = axes('Units','normal', 'Position', [0 0 1 1], 'Parent', lp,'visible','off'); 527 | set(lpax,'Units','pixels'); 528 | lpaxpos = get(lpax,'position'); 529 | 530 | tempx = lpaxpos(1); 531 | tempy = lpaxpos(2); 532 | tempw = lpaxpos(3); 533 | temph = lpaxpos(4); 534 | 535 | tempw = tempw - mod(tempw,11); 536 | temph = (tempw/11) * 10; 537 | 538 | tempx = floor((lpaxpos(3) - tempw)/2); 539 | tempy = lpaxpos(4)-temph; 540 | 541 | set(lpax,'Position',[tempx tempy tempw temph],'visible','on'); 542 | gv.lpaxpos = get(lpax,'position'); 543 | 544 | %% read video of visualisation, here cases can be added for other mobile eye trackers 545 | 546 | % the Pupil Labs version expects the data to be in a specific folder that 547 | % is default when exporting visualisations in Pupil Labs with a default 548 | % name of the file, Tobii Pro Glasses does not, so select it. 549 | switch gv.datatype 550 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 551 | if ~isempty(gv.multfilm) 552 | % now assuming just one pause 553 | gv.vidObj = VideoReader(gv.filmnaam{1}); 554 | gv.vidObj2 = VideoReader(gv.filmnaam{2}); 555 | gv.maxframe2 = get(gv.vidObj2,'NumberOfFrames'); 556 | else 557 | gv.vidObj = VideoReader(gv.filmnaam); 558 | end 559 | otherwise 560 | cd(gv.foldnaam); 561 | disp('Select the video file'); 562 | [videofile,videopath] = uigetfile('*.asf;*.asx;*.avi;*.m4v;*.mj2;*.mov;*.mp4;*.mpg;*.wmv;','Select video file'); 563 | % clc; 564 | cd(gv.codedir); 565 | disp('loading video file...') 566 | gv.vidObj = VideoReader([videopath gv.fs videofile]); 567 | % clc; 568 | end 569 | 570 | gv.fr = get(gv.vidObj,'FrameRate'); 571 | frdur = 1000/gv.fr; 572 | nf = get(gv.vidObj,'NumberOfFrames'); 573 | gv.maxframe = nf; 574 | gv.centerx = gv.vidObj.Width/2; 575 | gv.centery = gv.vidObj.Height/2; 576 | set(hm,'userdata',gv); 577 | 578 | %% fixation detection using data 579 | % read data and determine fixations, here cases can be added for other 580 | % mobile eye trackers, as well as changing the fixation detection algorithm 581 | % by altering the function that now runs on line 423. 582 | if ~skipdataload 583 | % to be done still, select data file from results directory if you 584 | % want to skip data loading 585 | 586 | gv = get(hm,'userdata'); 587 | switch gv.datatype 588 | case 'SMI Glasses' 589 | gv.multfilm = []; % no multiple segments supported 590 | cd(gv.foldnaam); 591 | disp('Select data file with gaze positions'); 592 | [filenaam, filepad] = uigetfile('.txt','Select data file with gaze positions'); 593 | % clc; 594 | cd(gv.codedir); 595 | % gv.wcr gets updated based om data as an extra safety measure 596 | [gv.datt, gv.datx, gv.daty] = leesgazedataSMI([filepad gv.fs filenaam]); 597 | 598 | % determine start and end times of each fixation in one vector (odd 599 | % number start times of fixations even number stop times) 600 | 601 | % The line below determines fixations start and stop times since the 602 | % beginning of the recording. For this detection of fixations the algo- 603 | % rithm of Hessels et (2019 submitted) is used, but this can be replaced 604 | % by your own favourite or perhaps more suitable fixation detection algorithm. 605 | % Important is that this algorithm returns a vector that has fixation 606 | % start times at the odd and fixation end times at the even positions 607 | % of it. 608 | 609 | disp('Determining fixations...'); 610 | gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 611 | 612 | disp('Determining fixations...'); 613 | % % gv.fmark = fixdetect(gv.datx,gv.daty,gv.datt,gv); 614 | % gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 615 | whichclass = questdlg('Which slow phase classifier do you want to use?','Pick event classifier','Hooge & Camps (2013)','Hessels et al. (2020)','Hooge & Camps (2013)'); 616 | if strcmp(whichclass,'Hooge & Camps (2013)') 617 | disp('Using Hooge & Camps (2013)'); 618 | gv.fmark = fixdetect(gv.datx,gv.daty,gv.datt,gv); 619 | else 620 | disp('Using Hessels et al. (2020)'); 621 | gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 622 | end 623 | case 'Positive Science' 624 | gv.multfilm = []; % no multiple segments supported 625 | cd(gv.foldnaam); 626 | disp('Select data file with gaze positions'); 627 | [filenaam, filepad] = uigetfile('.txt','Select data file with gaze positions'); 628 | % clc; 629 | cd(gv.codedir); 630 | 631 | % gv.wcr gets updated based om data as an extra safety measure 632 | [gv.datt, gv.datx, gv.daty] = leesgazedataPosSci([filepad gv.fs filenaam]); 633 | 634 | % determine start and end times of each fixation in one vector (odd 635 | % number start times of fixations even number stop times) 636 | 637 | % The line below determines fixations start and stop times since the 638 | % beginning of the recording. For this detection of fixations the algo- 639 | % rithm of Hessels et (2019 submitted) is used, but this can be replaced 640 | % by your own favourite or perhaps more suitable fixation detection algorithm. 641 | % Important is that this algorithm returns a vector that has fixation 642 | % start times at the odd and fixation end times at the even positions 643 | % of it. 644 | 645 | disp('Determining fixations...'); 646 | whichclass = questdlg('Which slow phase classifier do you want to use?','Pick event classifier','Hooge & Camps (2013)','Hessels et al. (2020)','Hooge & Camps (2013)'); 647 | if strcmp(whichclass,'Hooge & Camps (2013)') 648 | disp('Using Hooge & Camps (2013)'); 649 | gv.fmark = fixdetect(gv.datx,gv.daty,gv.datt,gv); 650 | else 651 | disp('Using Hessels et al. (2020)'); 652 | gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 653 | end 654 | case 'Pupil Labs (first gen + Core)' 655 | gv.multfilm = []; % no multiple segments supported 656 | cd(gv.foldnaam); 657 | disp('Select data file with gaze positions'); 658 | [filenaam, filepad] = uigetfile('.csv','Select data file with gaze positions'); 659 | % clc; 660 | cd(gv.codedir); 661 | 662 | % this file reads the exported data file from Pupil Labs and gets time 663 | % stamp and x and y coordinates 664 | [gv.datt, gv.datx, gv.daty] = leesgazedataPupFG(fullfile(filepad,filenaam)); 665 | % recalculate absolute timestamps to time from onset (0 ms) 666 | gv.datt = double(gv.datt); 667 | gv.datt2 = gv.datt; 668 | gv.datt2(1) = 0; 669 | for p = 2:length(gv.datt) 670 | gv.datt2(p) = gv.datt(p) - gv.datt(p-1); 671 | gv.datt2(p) = gv.datt2(p) + gv.datt2(p-1); 672 | end 673 | gv.datt2 = gv.datt2*1000; 674 | gv.datt = gv.datt2; 675 | 676 | gv.datx(gv.datx > 1) = NaN; 677 | gv.datx(gv.datx < 0) = NaN; 678 | 679 | gv.daty(gv.daty > 1) = NaN; 680 | gv.daty(gv.daty < 0) = NaN; 681 | 682 | gv.datx = gv.datx * gv.wcr(1); 683 | gv.daty = gv.wcr(2) - gv.daty * gv.wcr(2); 684 | 685 | % determine start and end times of each fixation in one vector (odd 686 | % number start times of fixations even number stop times) 687 | 688 | % The line below determines fixations start and stop times since the 689 | % beginning of the recording. For this detection of fixations the algo- 690 | % rithm of Hessels et (2019 submitted) is used, but this can be replaced 691 | % by your own favourite or perhaps more suitable fixation detection algorithm. 692 | % Important is that this algorithm returns a vector that has fixation 693 | % start times at the odd and fixation end times at the even positions 694 | % of it. 695 | 696 | disp('Determining fixations...'); 697 | whichclass = questdlg('Which slow phase classifier do you want to use?','Pick event classifier','Hooge & Camps (2013)','Hessels et al. (2020)','Hooge & Camps (2013)'); 698 | if strcmp(whichclass,'Hooge & Camps (2013)') 699 | disp('Using Hooge & Camps (2013)'); 700 | gv.fmark = fixdetect(gv.datx,gv.daty,gv.datt,gv); 701 | else 702 | disp('Using Hessels et al. (2020)'); 703 | gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 704 | end 705 | 706 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 707 | 708 | % data = getTobiiDataFromGlasses(gv.foldnaam,qDEBUG); 709 | if strcmp(gv.datatype,'Tobii Pro Glasses 2') 710 | data = readG2DataFiles(gv.foldnaam,settings.userStreams,qDEBUG); 711 | elseif strcmp(gv.datatype,'Tobii Pro Glasses 3') 712 | data = readG3DataFiles(gv.foldnaam,settings.userStreams,qDEBUG); 713 | else 714 | assert((strcmp(gv.datatype,'Tobii Pro Glasses 2')|strcmp(gv.datatype,'Tobii Pro Glasses 3')),'Unknown Tobii Pro Glasses model. Exiting'); 715 | end 716 | data.quality = computeDataQuality(gv.foldnaam, data, settings.dataQuality.windowLength); 717 | data.ui.haveEyeVideo = isfield(data.video,'eye'); 718 | coding = getCodingData(gv.foldnaam, '', settings.coding, data); 719 | coding.dataIsCrap = dataIsCrap; 720 | gv.coding = coding; 721 | 722 | % use coding from getCodingData. 723 | [streamIdx,eventToCode] = streamSelectorGUI(gv.coding); 724 | assert(~isempty(streamIdx),'You did not select a stream to code, exiting'); 725 | streams = find(coding.stream.available); 726 | qGazeCodeStream = isfield(coding.settings.streams{streams(streamIdx)},'tag') && strcmp(coding.settings.streams{streams(streamIdx)}.tag,'gazeCodeStream'); 727 | assert(~isempty(streamIdx)||qGazeCodeStream,'You did not select an event to code, exiting') 728 | if ~isempty(eventToCode) 729 | assert(~isempty(gv.coding.type{streamIdx})&&any(gv.coding.type{streamIdx}==eventToCode),'Selected stream does not contain any events of the selected category. Nothing to code. Exiting.') 730 | end 731 | 732 | % ask user where to store coding output 733 | [outStreamIdx,newStreamName] = outputStreamSelectorGUI(coding,streamIdx); 734 | assert(~isempty(outStreamIdx),'You did not select a stream for storing coding output, exiting'); 735 | gv.coding.outIdx = outStreamIdx; 736 | gv.coding.streamName = newStreamName; 737 | % make new coding stream for user's output if wanted 738 | if outStreamIdx~=streamIdx 739 | assert(isempty(newStreamName) || gv.coding.outIdx==length(gv.coding.mark)+1,'internal error, contact developer') 740 | gv.coding.mark{gv.coding.outIdx} = gv.coding.mark{streamIdx}; 741 | gv.coding.type{gv.coding.outIdx} = gv.coding.type{streamIdx}; 742 | end 743 | 744 | % prep output stream, if not loading and editing existing 745 | % stream or copying a GazeCode stream 746 | if outStreamIdx~=streamIdx && ~qGazeCodeStream 747 | % set everything not of interest to type 1 ('other') 748 | gv.coding.type{gv.coding.outIdx}(gv.coding.type{gv.coding.outIdx}~=eventToCode) = 1; 749 | % set everything of interest to type 2 ('uncoded') 750 | gv.coding.type{gv.coding.outIdx}(gv.coding.type{gv.coding.outIdx}==eventToCode) = 2; 751 | % merge adjacent, coding should not contain consecutive 752 | % same events 753 | iAdj = find(diff(gv.coding.type{gv.coding.outIdx})==0); 754 | i=length(iAdj); 755 | while i>0 756 | % find start and end of run of adjacent events 757 | e = iAdj(i)+1; 758 | s = iAdj(i); 759 | while i>1&&iAdj(i-1)==s-1 760 | i = i-1; 761 | s = iAdj(i); 762 | end 763 | gv.coding.mark{gv.coding.outIdx}(s+1:e) = []; 764 | gv.coding.type{gv.coding.outIdx}(s+1:e) = []; 765 | i=i-1; 766 | end 767 | end 768 | 769 | % get start and end marks of those events the user wanted to 770 | % code 771 | if outStreamIdx~=streamIdx && ~qGazeCodeStream 772 | qEvents = gv.coding.type{gv.coding.outIdx}==2; 773 | else 774 | % include also already coded events, since we are reloading 775 | % GazeCode session 776 | qEvents = gv.coding.type{gv.coding.outIdx}>=2; 777 | end 778 | % fmark should contain start and end of each event to code, one 779 | % after the other 780 | iEvents = find(qEvents); 781 | gv.fmark = reshape([gv.coding.mark{gv.coding.outIdx}(iEvents); gv.coding.mark{gv.coding.outIdx}(iEvents+1)]*1000,1,[]); % s->ms 782 | 783 | % only select data from ts > 0, ts is nulled at onset scene camera! 784 | sel = data.eye.binocular.ts >= data.time.startTime & data.eye.binocular.ts <= data.time.endTime; 785 | 786 | gv.datt = data.eye.binocular.ts(sel); 787 | gv.datx = data.eye.binocular.gp(sel,1); 788 | gv.daty = data.eye.binocular.gp(sel,2); 789 | 790 | gv.datt = gv.datt*1000; 791 | % gv.datx = gv.datx * data.video.scene.width; 792 | % gv.daty = gv.daty * data.video.scene.height; 793 | case 'Pupil Labs invisible (200 Hz)' 794 | gv.multfilm = []; % no multiple segments supported yet 795 | disp('Selecting data file with gaze positions...'); 796 | [gv.datt, gv.datx, gv.daty] = leesgazePupInvis200Exdata(fullfile(gv.foldnaam,'gaze_positions.csv')); 797 | 798 | disp('Selecting data file with timestamps of world video'); 799 | [gv.datwt] = leesTSPupInvis200Ex(fullfile(gv.foldnaam,'world_timestamps.csv')); 800 | % convert to ms 801 | gv.datwt = gv.datwt*1000; 802 | % recalculate absolute timestamps to time from onset (0 ms) 803 | gv.datt = double(gv.datt); 804 | gv.datt2 = gv.datt; 805 | gv.datt2(1) = 0; 806 | for p = 2:length(gv.datt) 807 | gv.datt2(p) = gv.datt(p) - gv.datt(p-1); 808 | gv.datt2(p) = gv.datt2(p) + gv.datt2(p-1); 809 | end 810 | 811 | % convert to ms 812 | gv.datt2 = gv.datt2*1000; 813 | gv.datt = gv.datt2; 814 | 815 | gv.datx(gv.datx > 1) = NaN; 816 | gv.datx(gv.datx < 0) = NaN; 817 | 818 | gv.daty(gv.daty > 1) = NaN; 819 | gv.daty(gv.daty < 0) = NaN; 820 | 821 | [gv.datx, gv.datt2] = resample(gv.datx,gv.datt/1000.0,resampleFreq); % divide by 1000 to convert to seconds 822 | [gv.daty, gv.datt2] = resample(gv.daty,gv.datt/1000.0,resampleFreq); % divide by 1000 to convert to seconds 823 | 824 | 825 | gv.datt = gv.datt2*1000.0; % mulitply with 1000 to go back to ms 826 | 827 | gv.datx = gv.datx * gv.wcr(1); 828 | % flip vertical normilzed coordinates to match with video coordinates 829 | gv.daty = gv.wcr(2) - gv.daty * gv.wcr(2); 830 | % determine start and end times of each fixation in one vector (odd 831 | % number start times of fixations even number stop times) 832 | 833 | % The line below determines fixations start and stop times since the 834 | % beginning of the recording. For this detection of fixations the algo- 835 | % rithm of Hessels et (2019 submitted) is used, but this can be replaced 836 | % by your own favourite or perhaps more suitable fixation detection algorithm. 837 | % Important is that this algorithm returns a vector that has fixation 838 | % start times at the odd and fixation end times at the even positions 839 | % of it. 840 | 841 | disp('Determining fixations...'); 842 | whichclass = questdlg('Which slow phase classifier do you want to use?','Pick event classifier','Hooge & Camps (2013)','Hessels et al. (2020)','Hooge & Camps (2013)'); 843 | if strcmp(whichclass,'Hooge & Camps (2013)') 844 | disp('Using Hooge & Camps (2013)'); 845 | gv.fmark = fixdetect(gv.datx,gv.daty,gv.datt,gv); 846 | else 847 | disp('Using Hessels et al. (2020)'); 848 | gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 849 | end 850 | case 'Pupil Labs Neon (200 Hz)' 851 | gv.multfilm = []; % no multiple segments supported yet 852 | disp('Selecting data file with gaze positions...'); 853 | [gv.datt, gv.datx, gv.daty] = leesgazePupNeon200data(fullfile(gv.foldnaam,'gaze.csv')); 854 | 855 | % from nano to s 856 | gv.datt = gv.datt/1000000000; 857 | disp('Selecting data file with timestamps of world video'); 858 | [gv.datwt] = leesTSPupNeon200(fullfile(gv.foldnaam,'world_timestamps.csv')); 859 | % convert to s 860 | gv.datwt = gv.datwt/1000000000; 861 | gv.worldgazeTdiff = gv.datt(1) - gv.datwt(1); 862 | % gv.datwt(1) = 0 863 | % for p = 2:length(gv.datwt) 864 | % gv.datwt(p) = gv.datwt(p) - gv.datt(p-1); 865 | % gv.datt2(p) = gv.datt2(p) + gv.datt2(p-1); 866 | % end 867 | % recalculate absolute timestamps to time from onset (0 ms) 868 | gv.datt = double(gv.datt); 869 | gv.datt2 = gv.datt; 870 | gv.datt2(1) = 0; 871 | for p = 2:length(gv.datt) 872 | gv.datt2(p) = gv.datt(p) - gv.datt(p-1); 873 | gv.datt2(p) = gv.datt2(p) + gv.datt2(p-1); 874 | end 875 | 876 | % if world timestamps start earlier, set first gaze time stamps 877 | % to difference between first world time stamps and first gaze 878 | % timestamp as movie gaze is projected onto movie 879 | gv.datt2 = gv.datt2 + gv.worldgazeTdiff; 880 | 881 | % convert to ms 882 | gv.datt2 = gv.datt2*1000; 883 | gv.datt = gv.datt2; 884 | 885 | gv.datx(gv.datx > 1600) = NaN; 886 | gv.datx(gv.datx < 0) = NaN; 887 | 888 | gv.daty(gv.daty > 1200) = NaN; 889 | gv.daty(gv.daty < 0) = NaN; 890 | 891 | [gv.datx, gv.datt2] = resample(gv.datx,gv.datt/1000.0,resampleFreq); % divide by 1000 to convert to seconds 892 | [gv.daty, gv.datt2] = resample(gv.daty,gv.datt/1000.0,resampleFreq); % divide by 1000 to convert to seconds 893 | 894 | gv.datt = gv.datt2*1000.0; % mulitply with 1000 to go back to ms 895 | 896 | % gv.datx = gv.datx * gv.wcr(1); 897 | % % flip vertical normilzed coordinates to match with video coordinates 898 | % gv.daty = gv.wcr(2) - gv.daty * gv.wcr(2); 899 | % determine start and end times of each fixation in one vector (odd 900 | % number start times of fixations even number stop times) 901 | 902 | % The line below determines fixations start and stop times since the 903 | % beginning of the recording. For this detection of fixations the algo- 904 | % rithm of Hessels et (2019 submitted) is used, but this can be replaced 905 | % by your own favourite or perhaps more suitable fixation detection algorithm. 906 | % Important is that this algorithm returns a vector that has fixation 907 | % start times at the odd and fixation end times at the even positions 908 | % of it. 909 | 910 | disp('Determining fixations...'); 911 | whichclass = questdlg('Which slow phase classifier do you want to use?','Pick event classifier','Hooge & Camps (2013)','Hessels et al. (2020)','Hooge & Camps (2013)'); 912 | if strcmp(whichclass,'Hooge & Camps (2013)') 913 | disp('Using Hooge & Camps (2013)'); 914 | gv.fmark = fixdetect(gv.datx,gv.daty,gv.datt,gv); 915 | else 916 | disp('Using Hessels et al. (2020)'); 917 | gv.fmark = fixdetectmovingwindow(gv.datx,gv.daty,gv.datt,gv); 918 | end 919 | otherwise 920 | disp('Unknown data type, crashing in 3,2,1,...'); 921 | end 922 | 923 | 924 | fixB = gv.fmark(1:2:end)'; 925 | fixE = gv.fmark(2:2:end)'; 926 | fixD = fixE- fixB; 927 | 928 | [temptijd,idxtB,idxfixB] = intersect(gv.datt,fixB); 929 | xstart = gv.datx(idxtB); 930 | ystart = gv.daty(idxtB); 931 | 932 | [temptijd,idxtE,idxfixE] = intersect(gv.datt,fixE); 933 | xend = gv.datx(idxtE); 934 | yend = gv.daty(idxtE); 935 | 936 | for p = 1:length(idxtB) 937 | xmean(p,1) = mean(gv.datx(idxtB(p):idxtE(p))); 938 | ymean(p,1) = mean(gv.daty(idxtB(p):idxtE(p))); 939 | 940 | xsd(p,1) = std(gv.datx(idxtB(p):idxtE(p))); 941 | ysd(p,1) = std(gv.daty(idxtB(p):idxtE(p))); 942 | end 943 | 944 | fixnr = [1:length(fixB)]'; 945 | fixlabel = zeros(size(fixnr)); 946 | 947 | if size(fixB,2) > 1 948 | fixB = fixB'; 949 | fixE = fixE'; 950 | fixD = fixD'; 951 | end 952 | 953 | 954 | % determine begin and end frame beloning to fixations start and end 955 | % times 956 | switch gv.datatype 957 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 958 | % use frame time info from GlassesViewer's export 959 | [gv.bfr,gv.efr] = deal(nan(size(fixB))); 960 | for p=1:length(fixB) 961 | if (fixB(p)/1000) < min(data.video.scene.fts) 962 | disp(['Time stamp of ' num2str(p), ' out of ',num2str(length(fixB)),' events, beginning at ',num2str(fixB(p)/1000),', lies before first possible time stamp scene video: ',num2str(min(data.video.scene.fts))]); 963 | disp(['Setting event ', num2str(p), ' to first frame of scene video']) 964 | gv.bfr(p) = 1; 965 | else 966 | try 967 | gv.bfr(p) = find(data.video.scene.fts<=fixB(p)/1000,1,'last'); 968 | catch 969 | disp(['Last possible time stamp scene video ' , num2str(max(data.video.scene.fts))]); 970 | disp(['Event ' num2str(p), ' out of ',num2str(length(fixB)), ' has time stamp: ',num2str(fixB(p)/1000)]); 971 | gv.bfr(p) = find(data.video.scene.fts<=fixB(p)/1000,1,'last'); 972 | end 973 | end 974 | gv.efr(p) = find(data.video.scene.fts<=fixE(p)/1000,1,'last'); 975 | end 976 | 977 | % case {'Pupil Labs invisible (200 Hz)'} 978 | % % use world timestamps (in gv.datwt) for Pupil Player export 979 | % [gv.bfr,gv.efr] = deal(nan(size(fixB))); 980 | % for p=1:length(fixB) 981 | % gv.bfr(p) = find(gv.datwt<=fixB(p),1,'last'); 982 | % gv.efr(p) = find(gv.datwt<=fixE(p),1,'last'); 983 | % end 984 | 985 | otherwise 986 | gv.bfr = floor(fixB/frdur); 987 | gv.efr = ceil(fixE/frdur); 988 | end 989 | gv.bfr(gv.bfr<1) = 1; 990 | gv.efr(gv.efr<1) = 1; 991 | 992 | if ~isempty(gv.multfilm) % for Tobii Glasses only 993 | % now assuming just one pause... 994 | gv.whichfilm = zeros(size(gv.bfr)); 995 | gv.whichfilm(1:find(gv.efr<=data.video.scene.segframes(1) & gv.efr-gv.bfr>0,1,'last')) = 1; 996 | gv.whichfilm(find(gv.bfr>=data.video.scene.segframes(1) & gv.efr-gv.bfr>0,1,'first'):end) = 2; 997 | 998 | gv.bfr(gv.whichfilm ==2) = gv.bfr(gv.whichfilm == 2) - data.video.scene.segframes(1); 999 | gv.efr(gv.whichfilm ==2) = gv.efr(gv.whichfilm == 2) - data.video.scene.segframes(1); 1000 | 1001 | else 1002 | gv.bfr(gv.bfr>gv.maxframe) = gv.maxframe; 1003 | gv.efr(gv.efr>gv.maxframe) = gv.maxframe; 1004 | end 1005 | 1006 | 1007 | 1008 | % determine the frame between beginning and end frame for a fixations, 1009 | % this one will be displayed 1010 | gv.mfr = floor((gv.bfr+gv.efr)/2); 1011 | 1012 | % needed for marker in scene camera 1013 | gv.fixxpos = xmean; 1014 | gv.fixypos = ymean; 1015 | 1016 | gv.fixxposB = xstart; 1017 | gv.fixyposB = ystart; 1018 | 1019 | gv.fixxposE = xend; 1020 | gv.fixyposE = yend; 1021 | 1022 | gv.data = [fixnr, fixB, fixE, fixD, xstart, ystart, xend, yend, xmean, xsd, ymean, ysd, fixlabel]; 1023 | gv.maxfix = max(fixnr); 1024 | 1025 | % added for time stamp search and labels 1026 | gv.maxtijd = max(gv.data(:,3)); 1027 | gv.maxtijdm = floor(gv.maxtijd/(1000*60)); 1028 | gv.maxtijds = floor((gv.maxtijd-gv.maxtijdm*1000*60)/1000); 1029 | gv.maxtijdstr = [pad(num2str(gv.maxtijdm),2,'left','0'),':',pad(num2str(gv.maxtijds),2,'left','0')]; 1030 | 1031 | switch gv.datatype 1032 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 1033 | if outStreamIdx==streamIdx || qGazeCodeStream % TODO: this is specific to Tobii code... 1034 | % when loading existing file, put already coded labels into gv.data 1035 | qWhich = gv.coding.type{gv.coding.outIdx}>1; 1036 | assert(sum(qWhich)==size(gv.data,1),'internal error contact developer') 1037 | gv.data(:,end) = log2(gv.coding.type{gv.coding.outIdx}(qWhich))-1; 1038 | % added such that if previously coded, GazeCode will pickup at the 1039 | % last coded event. 1040 | % JSB: changed for the special case where somebody coded some data, 1041 | % but then resets all to zero and then closes. 1042 | wheretocontinue = find(gv.data(:,end)>1, 1,'last'); 1043 | if ~isempty(wheretocontinue) 1044 | gv.curfix = wheretocontinue; 1045 | end 1046 | end 1047 | otherwise 1048 | % do nothing 1049 | end 1050 | 1051 | set(hm,'userdata',gv); 1052 | disp('... done'); 1053 | 1054 | set(hm,'userdata',gv); 1055 | end 1056 | 1057 | %% start to show the first frame or when reloading the frame last coded. 1058 | showmainfr(hm,gv); 1059 | set(hm,'Visible','on'); 1060 | 1061 | end 1062 | 1063 | % function to attribute a category code to the fixation, this is a function 1064 | % of the buttons in the right panel 1065 | function labelfix(src,evt) 1066 | if isempty(evt) 1067 | categorie = get(src,'userdata'); 1068 | rp = get(src,'parent'); 1069 | hm = get(rp,'parent'); 1070 | gv = get(hm,'userdata'); 1071 | else 1072 | categorie = get(src,'userdata'); 1073 | rp = get(src,'parent'); 1074 | hm = get(rp,'parent'); 1075 | gv = get(hm,'userdata'); 1076 | if gv.data(gv.curfix,end) == categorie 1077 | if categorie ~= 0 1078 | categorie = 0; 1079 | end 1080 | end 1081 | end 1082 | data = gv.data; 1083 | data(gv.curfix,end) = categorie; 1084 | switch gv.datatype 1085 | case 'Tobii Pro Glasses 2' 1086 | % put categorie also in coding struct, note that categories are power of 2, 1087 | % and that 1 is "other". categorie 0 ("uncoded") should correspond to code 1088 | % 2, categorie 1 to code 4, etc, so categorie+1 below 1089 | qWhich = gv.coding.mark{gv.coding.outIdx}==gv.fmark(2*gv.curfix-1)/1000; 1090 | assert(sum(qWhich)==1,'Internal error, contact developer') 1091 | gv.coding.type{gv.coding.outIdx}(qWhich) = 2^(categorie+1); 1092 | otherwise 1093 | % nothing 1094 | end 1095 | gv.data = data; 1096 | setlabel(gv); 1097 | 1098 | set(hm,'userdata',gv); 1099 | end 1100 | 1101 | % function to show the current frame and fixation being labeled 1102 | function showmainfr(hm,gv) 1103 | if ~isempty(gv.multfilm) 1104 | if gv.whichfilm(gv.curfix) == 1 1105 | try 1106 | plaat = read(gv.vidObj,gv.mfr(gv.curfix)); 1107 | segmstr =', Segment 1'; 1108 | catch 1109 | warning('Could not find video frame, perhaps paused, showing nothing'); 1110 | plaat = read(gv.vidObj,1); 1111 | plaat = zeros(size(plaat)); 1112 | end 1113 | elseif gv.whichfilm(gv.curfix) == 2 1114 | try 1115 | plaat = read(gv.vidObj2,gv.mfr(gv.curfix)); 1116 | segmstr =', Segment 2'; 1117 | catch 1118 | warning('Could not find video frame, perhaps paused, showing nothing'); 1119 | plaat = read(gv.vidObj,1); 1120 | plaat = zeros(size(plaat)); 1121 | end 1122 | else 1123 | warning(['event ' , num2str(gv.curfix),'/',num2str(gv.maxfix),' not in one of the scene camera segments, scene camera paused, showing nothing']) 1124 | plaat = gv.pauseplaat; 1125 | end 1126 | else 1127 | plaat = read(gv.vidObj,gv.mfr(gv.curfix)); 1128 | segmstr = ''; 1129 | end 1130 | imagesc(plaat); 1131 | gv.frameas = gca; 1132 | axis off; 1133 | axis equal; 1134 | 1135 | temptijd = (gv.data(gv.curfix,2) + gv.data(gv.curfix,3))/2; 1136 | 1137 | gv.curfixtijdm = floor(temptijd/(1000*60)); 1138 | gv.curfixtijds = floor((temptijd-gv.curfixtijdm*1000*60)/1000); 1139 | gv.curfixtijdstr = [pad(num2str(gv.curfixtijdm),2,'left','0'),':',pad(num2str(gv.curfixtijds),2,'left','0')]; 1140 | 1141 | disp(['Current event: ', num2str(gv.curfix),'/',num2str(gv.maxfix), ', Time: ', gv.curfixtijdstr, ', End time: ',gv.maxtijdstr,segmstr]); 1142 | 1143 | set(gv.lp,'Title',['Current event: ', num2str(gv.curfix),'/',num2str(gv.maxfix), ', Time: ', gv.curfixtijdstr, ', End time: ',gv.maxtijdstr,segmstr]); 1144 | hold(gv.frameas,'on'); 1145 | stip = scatter(gv.fixxpos(gv.curfix),gv.fixypos(gv.curfix),1000,'ro'); 1146 | set(stip,'MarkerEdgeColor',[0 0.85 1],'MarkerFaceAlpha',.65,'MarkerFaceColor',[0 0.85 1],'LineWidth',2); 1147 | 1148 | 1149 | hold(gv.frameas,'off'); 1150 | setlabel(gv); 1151 | 1152 | if isempty(gv.data(:,end)==0) 1153 | msgbox('All fixations of this directory seem to be labeled','Warning','warn'); 1154 | end 1155 | 1156 | set(hm,'userdata',gv); 1157 | end 1158 | 1159 | function setlabel(gv) 1160 | if gv.data(gv.curfix,end) > 0 1161 | for p = 1:size(gv.knoppen,2) 1162 | if p == gv.data(gv.curfix,end) 1163 | set(gv.knoppen(p),'backgroundcolor',[1 0.5 0]); 1164 | else 1165 | set(gv.knoppen(p),'backgroundcolor',[1 1 1]); 1166 | end 1167 | end 1168 | else 1169 | for p = 1:size(gv.knoppen,2) 1170 | set(gv.knoppen(p),'backgroundcolor',[1 1 1]); 1171 | end 1172 | end 1173 | end 1174 | 1175 | % function to move one fixation further, function of button in left panel 1176 | function playforward(src,evt) 1177 | lp = get(src,'parent'); 1178 | hm = get(lp,'parent'); 1179 | gv = get(hm,'userdata'); 1180 | 1181 | if gv.curfix < gv.maxfix 1182 | gv.curfix = gv.curfix + 1; 1183 | showmainfr(hm,gv); 1184 | else 1185 | msgbox('Last fixation reached','Warning','warn'); 1186 | end 1187 | set(hm,'userdata',gv); 1188 | end 1189 | 1190 | % function move one fixation back, function of button in left panel 1191 | function playback(src,evt) 1192 | lp = get(src,'parent'); 1193 | hm = get(lp,'parent'); 1194 | gv = get(hm,'userdata'); 1195 | 1196 | if gv.curfix > 1 1197 | gv.curfix = gv.curfix - 1; 1198 | showmainfr(hm,gv); 1199 | else 1200 | h = msgbox('First fixation reached','Warning','warn'); 1201 | end 1202 | set(hm,'userdata',gv); 1203 | end 1204 | 1205 | % function to handle keypresses as shortcuts, function of main screen 1206 | function verwerkknop(src,evt) 1207 | gv = get(src,'userdata'); 1208 | switch evt.Key 1209 | case gv.fwdbut 1210 | playforward(findobj('UserData',gv.fwdbut),evt); 1211 | case gv.bckbut 1212 | playback(findobj('UserData',gv.bckbut),evt); 1213 | case gv.cat1but 1214 | labelfix(findobj('UserData',1),evt); 1215 | case gv.cat2but 1216 | labelfix(findobj('UserData',2),evt); 1217 | case gv.cat3but 1218 | labelfix(findobj('UserData',3),evt); 1219 | case gv.cat4but 1220 | labelfix(findobj('UserData',4),evt); 1221 | case gv.cat5but 1222 | labelfix(findobj('UserData',5),evt); 1223 | case gv.cat6but 1224 | labelfix(findobj('UserData',6),evt); 1225 | case gv.cat7but 1226 | labelfix(findobj('UserData',7),evt); 1227 | case gv.cat8but 1228 | labelfix(findobj('UserData',8),evt); 1229 | case gv.cat9but 1230 | labelfix(findobj('UserData',9),evt); 1231 | case gv.catjbut 1232 | welkefix = inputdlg('Jump to which event?','Jump event',1,{num2str(gv.curfix)}); 1233 | if isempty(welkefix),return,end 1234 | welkefix = str2num(welkefix{:}); 1235 | if isnumeric(welkefix) 1236 | if isempty(welkefix)|| welkefix < 1 || welkefix > gv.maxfix 1237 | fprintf('Wrong input! Use numbers between 1 and %d\n',gv.maxfix); 1238 | else 1239 | gv.curfix = welkefix; 1240 | set(src,'userdata',gv); 1241 | showmainfr(src,gv); 1242 | end 1243 | end 1244 | case gv.cattbut 1245 | prevfix = gv.curfix; 1246 | welketijd = inputdlg(['Jump to which time (mm:ss)?, Max: ',gv.maxtijdstr],'Jump time',1,{'mm:ss'}); 1247 | welketijd = strsplit(welketijd{:},':'); 1248 | % currently only works for recordings under the hour, which is 1249 | % advised in any case 1250 | if length(welketijd) ~=2, disp('Non critical error: wrong time format, use mm:ss'); return,end 1251 | welketijdm = str2num(welketijd{1}); 1252 | welketijds = str2num(welketijd{2}); 1253 | if (welketijdm < 0) || (welketijdm > 59), disp('Non critical error: minutes should be between 0 and 59'); return,end 1254 | if (welketijds < 0) || (welketijds > 59), disp('Non critical error: seconds should be between 0 and 59'); return,end 1255 | 1256 | welketijd = welketijdm*60*1000 + welketijds*1000; 1257 | 1258 | if welketijd > max(gv.data(:,3)), disp('Non critical error: time chosen beyond end time of last event'); return, end 1259 | 1260 | % gv.curfix = find(welketijd > gv.data(:,2) & welketijd < gv.data(:,3)) 1261 | gv.curfix = find(welketijd < gv.data(:,2)); 1262 | gv.curfix = gv.curfix(1); 1263 | % don't think this can actually happen, but better be safe than 1264 | % sorry 1265 | if isempty(gv.curfix) 1266 | disp('cannot find fixation with timestamp, taking previous one'); 1267 | gv.curfix = prevfix; 1268 | end 1269 | if length(gv.curfix) > 1 1270 | gv.curfix = gv.curfix(1); 1271 | end 1272 | set(src,'userdata',gv); 1273 | showmainfr(src,gv); 1274 | otherwise 1275 | % disp('Unknown key pressed'); 1276 | end 1277 | 1278 | end 1279 | 1280 | function savetotext(src,evt) 1281 | disp('Saving to text...'); 1282 | mm1 = get(src,'parent'); 1283 | hm = get(mm1,'parent'); 1284 | gv = get(hm,'userdata'); 1285 | switch gv.datatype 1286 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 1287 | tempresdir = fullfile(gv.resdir,gv.partName,gv.recName); 1288 | if ~exist(tempresdir) 1289 | mkdir(fullfile(gv.resdir,gv.partName,gv.recName)); 1290 | end 1291 | streamName = gv.coding.streamName; 1292 | % remove invalid characters 1293 | streamName = regexprep(streamName,'[^\w\.!@#$^+=-]','_'); % remove characters invalid in Windows filename from stream name 1294 | filenaam = fullfile(gv.resdir,gv.partName,gv.recName,[gv.recName,'_',streamName, '.xls']); 1295 | while exist(filenaam,'file') 1296 | answer = inputdlg(['File: ''', filenaam ,''' already exists. Enter a new file name'],'Warning: file already exists',1,{[gv.recName,'_',streamName,'_01.xls']}); 1297 | if isempty(answer) % to prevent pressing cancel going wrong (TODO, kill cancel button) 1298 | disp('... saving cancelled'); 1299 | return; % test this! 1300 | else 1301 | filenaam = fullfile(gv.resdir,gv.partName,gv.recName, answer{1}); 1302 | end 1303 | end 1304 | otherwise 1305 | filenaam = fullfile(gv.resdir,[gv.filmnaam '.xls']); 1306 | while exist(filenaam,'file') 1307 | answer = inputdlg(['File: ''', filenaam ,''' already exists. Enter a new file name'],'Warning: file already exists',1,{[gv.filmnaam '_01.xls']}); 1308 | if isempty(answer) % to prevent pressing cancel going wrong (TODO, kill cancel button) 1309 | % filenaam = filenaam; 1310 | disp('... saving cancelled'); 1311 | return; 1312 | else 1313 | filenaam = fullfile(gv.resdir, answer{1}); 1314 | end 1315 | end 1316 | end 1317 | fid = fopen(filenaam,'w+'); 1318 | fprintf(fid,[repmat('%s\t',1,12),'%s\n'],'fix nr','fix start (ms)','fix end (ms)','fix dur (ms)','x start','y start','x end','y end','mean x','sd x','mean y','sd y','label'); 1319 | fgetl(fid); 1320 | fprintf(fid,[repmat('%d\t',1,12),'%d\n'],gv.data'); 1321 | fclose(fid); 1322 | disp('... done'); 1323 | end 1324 | 1325 | % closing and saving function 1326 | function sluitaf(src,evt) 1327 | try 1328 | gv = get(src,'userdata'); 1329 | knopsluit = questdlg('You''re about to close the program. Are you sure you''re done and want to quit?','Are you sure?','Yes','No','No'); 1330 | if strcmp('Yes',knopsluit) 1331 | switch gv.datatype 1332 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 1333 | % get gazeCodes for GlasseViewer and write them to a text 1334 | % file 1335 | streamName = gv.coding.streamName; 1336 | % remove invalid characters 1337 | streamName = regexprep(streamName,'[^\w\.!@#$^+=-]','_'); % remove characters invalid in Windows filename from stream name 1338 | fname = fullfile(gv.foldnaam,['gazeCodeCoding_' streamName '.txt']); 1339 | gazecodes = [gv.coding.mark{gv.coding.outIdx}(1:end-1)', gv.coding.type{gv.coding.outIdx}']; 1340 | fid = fopen(fname,'w'); % TODO: unieke naam per stream? lijkt me wel handig 1341 | fprintf(fid,'%f\t%d\n',gazecodes'); 1342 | fclose(fid); 1343 | % also store to coding.mat 1344 | % 1. add stream to settings, if needed 1345 | if gv.coding.outIdx>sum(gv.coding.stream.available) 1346 | str = getCodingStreamSetup(gv.coding.streamName); 1347 | gv.coding.settings.streams = [gv.coding.settings.streams; str]; 1348 | % 2. add also to coding.stream.available 1349 | gv.coding.stream.available = [gv.coding.stream.available true]; 1350 | end 1351 | % 3. mark and type are already good, we're ready to save 1352 | coding = rmfield(gv.coding,{'outIdx','streamName'}); 1353 | save(fullfile(gv.foldnaam,'coding.mat'),'-struct','coding'); 1354 | otherwise 1355 | gv = rmfield(gv,'lp'); 1356 | save(fullfile(gv.resdir,[gv.filmnaam '.mat']),'gv'); 1357 | end 1358 | 1359 | set(src,'closerequestfcn','closereq'); 1360 | rmpath(genpath(gv.rootdir),genpath(gv.glassesviewerdir)); 1361 | delete(src); 1362 | else 1363 | disp('Program not closed. Continuing.'); 1364 | end 1365 | catch 1366 | closereq() 1367 | end 1368 | end 1369 | 1370 | 1371 | function str = getCodingStreamSetup(name) 1372 | str.type = 'handStream'; 1373 | str.tag = 'gazeCodeStream'; 1374 | str.lbl = name; 1375 | str.locked = false; 1376 | str.categories = {'other';20;'uncoded';1;'GC1';2;'GC2';3;'GC3';4;'GC4';5;'GC5';6;'GC6';7;'GC7';8;'GC8';9;'GC9';10}; 1377 | end -------------------------------------------------------------------------------- /code/gazesplash.m: -------------------------------------------------------------------------------- 1 | function [fh] = gazesplash(plaat) 2 | 3 | % create a figure that is not visible yet, and has minimal titlebar properties 4 | fh = figure('Visible','off','MenuBar','none','NumberTitle','off','DockControls','off','Resize','off','Toolbar','none','windowstyle','modal'); 5 | % put an axes in it 6 | ah = axes('Parent',fh,'Visible','off'); 7 | % put the image in it 8 | X = imread(plaat); 9 | ih = image(X,'parent',ah); 10 | % colormap(map) 11 | % set the figure size to be just big enough for the image, and centered at 12 | % the center of the screen 13 | imxpos = get(ih,'XData'); 14 | imypos = get(ih,'YData'); 15 | set(ah,'Unit','Normalized','Position',[0,0,1,1]); 16 | figpos = get(fh,'Position'); 17 | figpos(3:4) = [imxpos(2) imypos(2)]; 18 | set(fh,'Position',figpos); 19 | movegui(fh,'center') 20 | % make the figure visible 21 | set(fh,'Visible','on'); 22 | -------------------------------------------------------------------------------- /code/initeventdetect.m: -------------------------------------------------------------------------------- 1 | function [f] = initeventdetect(eyetracker) 2 | 3 | switch eyetracker 4 | case {'Tobii Pro Glasses 2','Tobii Pro Glasses 3'} 5 | f.thr = 5000; % set very high 6 | f.counter = 200; 7 | f.minfix = 80; % ms 8 | f.lambda = 2.5; % lambda rel treshhold in sd's 9 | f.windowlength = 8000; % ms moving window average 10 | f.sf = 50; % sampling freq 11 | f.windowsize = round(f.windowlength./(1000/f.sf)); % window size in samples 12 | case 'Pupil Labs (first gen + Core)' % experimental, settings not thoroughly tested 13 | f.thr = 5000; % set very high 14 | f.counter = 200; 15 | f.minfix = 60; % ms 16 | f.lambda = 4; % lambda rel treshhold in sd's 17 | f.windowlength = 8000; % ms moving window average 18 | f.sf = 50; % sampling freq 19 | f.windowsize = round(f.windowlength./(1000/f.sf)); % window size in samples 20 | case 'SMI Glasses' % experimental, settings not thoroughly tested 21 | f.thr = 5000; % set very high 22 | f.counter = 200; 23 | f.minfix = 60; % ms 24 | f.lambda = 4; % lambda rel treshhold in sd's 25 | f.windowlength = 8000; % ms moving window average 26 | f.sf = 50; % sampling freq 27 | f.windowsize = round(f.windowlength./(1000/f.sf)); % window size in samples 28 | case 'Positive Science' % experimental, settings not thoroughly tested 29 | f.thr = 5000; % set very high 30 | f.counter = 200; 31 | f.minfix = 60; % ms 32 | f.lambda = 4; % lambda rel treshhold in sd's 33 | f.windowlength = 8000; % ms moving window average 34 | f.sf = 50; % sampling freq 35 | f.windowsize = round(f.windowlength./(1000/f.sf)); % window size in samples 36 | case 'Pupil Labs invisible (200 Hz)' 37 | f.thr = 5000; % set very high 38 | f.counter = 200; 39 | f.minfix = 60; % ms 40 | f.lambda = 2.5; % lambda rel treshhold in sd's 41 | f.windowlength = 8000; % ms moving window average 42 | f.sf = 200; % sampling freq 43 | f.windowsize = round(f.windowlength./(1000/f.sf)); 44 | disp('using Pupil invisible 200 hz settings'); 45 | case 'Pupil Labs Neon (200 Hz)' 46 | f.thr = 5000; % set very high 47 | f.counter = 200; 48 | f.minfix = 60; % ms 49 | f.lambda = 2.5; % lambda rel treshhold in sd's 50 | f.windowlength = 8000; % ms moving window average 51 | f.sf = 200; % sampling freq 52 | f.windowsize = round(f.windowlength./(1000/f.sf)); 53 | disp('using Pupil Neon 200 hz settings'); 54 | otherwise 55 | f.thr = 5000; % set very high 56 | f.counter = 200; 57 | f.minfix = 60; % ms 58 | f.lambda = 4; % lambda rel treshhold in sd's 59 | f.windowlength = 8000; % ms moving window average 60 | f.sf = 50; % sampling freq 61 | f.windowsize = round(f.windowlength./(1000/f.sf)); % window size in samples 62 | end 63 | 64 | -------------------------------------------------------------------------------- /code/leesTSPupInvis200Ex.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesTSPupInvis200Ex(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,[repmat('%f',1,2)],'delimiter',','); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,1}; 18 | 19 | 20 | 21 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesTSPupNeon200.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesTSPupNeon200(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,'%s%s%f','delimiter',','); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,3}; 18 | 19 | 20 | 21 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazePupInvis200Exdata.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazePupInvis200Exdata(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,[repmat('%f',1,21)],'delimiter',','); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,1}; 18 | x = dummy{:,4}; 19 | y = dummy{:,5}; 20 | 21 | 22 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazePupNeon200data.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazePupNeon200data(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,['%s%s',repmat('%f',1,8)],'delimiter',','); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,3}; 18 | x = dummy{:,4}; 19 | y = dummy{:,5}; 20 | 21 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazedata.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazedata(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,[repmat('%f',1,21)],'delimiter',','); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,3}; 18 | x = dummy{:,4}; 19 | y = dummy{:,5}; 20 | 21 | 22 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazedataPosSci.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazedataPosSci(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 7; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,'%d%d%f%s%s%f%f%f%f%f%f%f%f','delimiter',' '); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,5}; 18 | tijd2 = cellfun(@(x) strsplit(x,'.'),tijd,'UniformOutput',false); 19 | tijd2a = cellfun(@(x) strsplit(x{2},'/'),tijd2,'UniformOutput',false); 20 | tijd2b = cellfun(@(x) datevec(datenum(x{1},'DD:HH:MM:SS')),tijd2,'UniformOutput',false); 21 | tijdexms = cellfun(@(x) (x(4)*60*60*1000 + x(5)*60*1000 + x(6)*1000),tijd2b,'UniformOutput',false); 22 | tijdms = cellfun(@(x) 1000*(str2num(x{1})/str2num(x{2})),tijd2a,'UniformOutput',false); 23 | tijdms = cell2mat(tijdms); 24 | tijdexms = cell2mat(tijdexms); 25 | 26 | finaltijd = tijdexms + tijdms; 27 | 28 | tijd = finaltijd; 29 | 30 | x = dummy{:,6}; 31 | y = dummy{:,7}; 32 | 33 | 34 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazedataPupFG.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazedataPupFG(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | % 2023-06-26: changed textscan string to circumvent issue with sixth column not being numerical in Pupil Labs FG and Core data 14 | dummy = textscan(fid,[repmat('%f',1,5),'%s',repmat('%f',1,15)],'delimiter',','); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,1}; 18 | x = dummy{:,4}; 19 | y = dummy{:,5}; 20 | 21 | 22 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazedataSMI.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazedataSMI(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | firstskip = 16; % to skip header unitl calibration info 10 | for p=1:firstskip, 11 | fgetl(fid); 12 | end 13 | 14 | calibarea = fgetl(fid); 15 | calibparams = strsplit(calibarea,'\t'); 16 | wcr = [str2num(calibparams{2}) str2num(calibparams{3})]; 17 | 18 | secondskip = 17; % to skip rest of the header 19 | for p=1:secondskip, 20 | fgetl(fid); 21 | end 22 | 23 | 24 | dummy = textscan(fid,'%f%s%f%f%f%f%f%f%f%f%f%f%f%s%f%f%f%f%f%f%f%f%f%f%f%f','delimiter','\t'); 25 | fclose(fid); 26 | 27 | tijd = dummy{:,1}; 28 | % time needs to be set to zero and is in microseconds! 29 | tijd2 = tijd; 30 | tijd2(2:end) = (tijd2(2:end)- tijd2(1))/1000; 31 | tijd2(1) = 0; 32 | 33 | tijd = tijd2; 34 | 35 | leftx = dummy{:,10}; 36 | % correct for data that is beyond the world camera 37 | leftx(leftx<0) = NaN; 38 | leftx(leftx>wcr(1)) = NaN; 39 | lefty = dummy{:,11}; 40 | % correct for data that is beyond the world camera 41 | lefty(lefty<0) = NaN; 42 | lefty(lefty>wcr(2)) = NaN; 43 | 44 | rightx = dummy{:,12}; 45 | % correct for data that is beyond the world camera 46 | rightx(rightx<0) = NaN; 47 | rightx(rightx>wcr(1)) = NaN; 48 | righty = dummy{:,13}; 49 | % correct for data that is beyond the world camera 50 | righty(righty<0) = NaN; 51 | righty(righty>wcr(2)) = NaN; 52 | 53 | x = nanmean([leftx,rightx],2); 54 | y = nanmean([lefty,righty],2); 55 | 56 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); -------------------------------------------------------------------------------- /code/leesgazedataTobii.m: -------------------------------------------------------------------------------- 1 | function [tijd,x,y] = leesgazedataTobii(filenaam) 2 | 3 | fid = fopen(filenaam); 4 | [fid,message] = fopen(filenaam); 5 | if fid == -1 6 | error(message); 7 | end 8 | 9 | skip = 1; % to skip header 10 | for p=1:skip, 11 | fgetl(fid); 12 | end 13 | 14 | dummy = textscan(fid,'%s%s%s%f%f%f%f%f%f%f%s%f','delimiter','\t'); 15 | fclose(fid); 16 | 17 | tijd = dummy{:,4}; 18 | x = dummy{:,5}; 19 | y = dummy{:,6}; 20 | 21 | 22 | disp(sprintf('%d lines of file %s processed',length(tijd),filenaam)); 23 | -------------------------------------------------------------------------------- /code/maxall.m: -------------------------------------------------------------------------------- 1 | function s = maxall(in) 2 | 3 | s = max(in(:)); -------------------------------------------------------------------------------- /code/outputStreamSelectorGUI.m: -------------------------------------------------------------------------------- 1 | function [stream,name] = outputStreamSelectorGUI(coding,codeStreamIdx) 2 | 3 | % can only overwrite/continue GazeCode streams 4 | % so shows list of selectable GazeCode streams, plus a textbox to write 5 | % name of new stream 6 | 7 | qGazeCodeStream = cellfun(@(x) isfield(x,'tag') && strcmp(x.tag,'gazeCodeStream'),coding.settings.streams); 8 | qGazeCodeStream(~coding.stream.available) = []; 9 | % get number of characters in longest stream label 10 | nCharS = max(cellfun(@length,coding.stream.lbls(qGazeCodeStream))); 11 | if isempty(nCharS) 12 | nCharS = 1; 13 | end 14 | streamIdxs = [find(qGazeCodeStream); length(qGazeCodeStream)+1]; 15 | 16 | selector = dialog('WindowStyle', 'normal', 'Position',[100 100 200 200],'Name','Select where to store coding output','Visible','off'); 17 | 18 | 19 | % create panel 20 | marginsB = [2 5 5]; % horizontal: [margin from left edge, margin between radiobutton and text, vertical item spacing] 21 | buttonSz = [80 24]; 22 | textBoxSz= [220 20]; 23 | 24 | 25 | % temp checkbox and label because we need their sizes too 26 | % use largest label 27 | h = uicomponent('Style','radiobutton', 'Parent', selector,'Units','pixels','Position',[10 10 400 100], 'String',repmat('m',1,max(nCharS))); 28 | drawnow 29 | % get sizes, delete 30 | textSz = h.Extent; textSz(3) = textSz(3)+15; % radiobutton not counted in, guess a bit safe 31 | delete(h); 32 | 33 | % determine size of popup 34 | rowWidth = marginsB(1)*2+marginsB(2)+max([textSz(3),textBoxSz(1)+15+marginsB(2)]); 35 | popUpWidth = rowWidth; 36 | % determine height of popup 37 | rowHeight = max([textSz(4) textBoxSz(2)]) + marginsB(3); 38 | nRow = sum(qGazeCodeStream)+1; 39 | popUpHeight = rowHeight*nRow + nRow*2*marginsB(3) + buttonSz(2); 40 | 41 | % determine position and create in right size 42 | scrSz = get(0,'ScreenSize'); 43 | pos = [(scrSz(3)-popUpWidth)/2 (scrSz(4)-popUpHeight)/2 popUpWidth popUpHeight]; 44 | selector.Position = pos; 45 | 46 | % create button 47 | selector.UserData.button = uicontrol(... 48 | 'Style','pushbutton','Tag','executeReload','Position',[3+marginsB(1) marginsB(3) buttonSz],... 49 | 'Callback',@(hBut,~) buttonClick(hBut),'String','Use selection',... 50 | 'Parent',selector); 51 | 52 | % make items in popup 53 | for s=1:nRow 54 | p=nRow-s; 55 | if p>0 56 | lbl = coding.stream.lbls{streamIdxs(s)}; 57 | else 58 | lbl = ''; 59 | end 60 | selector.UserData.streamItems(s) = uicomponent('Style','radiobutton', 'Parent', selector,'Units','pixels','Position',[3 p*(textSz(4)+2*marginsB(3)) + 2*marginsB(3)+buttonSz(2) 200 20], 'String',lbl,'Value',false, 'Callback',@(src,~) changeEventStream(src,selector)); 61 | if streamIdxs(s)==codeStreamIdx 62 | selector.UserData.streamItems(s).FontWeight = 'bold'; 63 | end 64 | if p==0 65 | selector.UserData.editBox = uicomponent('Style','edit', 'Parent', selector,'Units','pixels','Position',[3+15+marginsB(2) 2*marginsB(3)+buttonSz(2) textBoxSz], 'String','change me!','HorizontalAlignment','left','KeyPressFcn',@(src,~) editBoxCB(src,selector)); 66 | end 67 | end 68 | if nRow==1 && isscalar(selector.UserData.streamItems) 69 | % preselect if only one possible output, else easy to click button 70 | % without selection causing execution to stop 71 | selector.UserData.streamItems.Value = 1; 72 | end 73 | 74 | selector.Visible = 'on'; 75 | uiwait(selector); 76 | 77 | if ishghandle(selector) 78 | name = ''; 79 | stream = find([selector.UserData.streamItems.Value]); 80 | if ~isempty(stream) 81 | if stream==nRow 82 | name = selector.UserData.editBox.String; 83 | end 84 | stream = streamIdxs(stream); 85 | end 86 | else 87 | [stream,name] = deal([],''); 88 | end 89 | 90 | delete(selector); 91 | end 92 | 93 | function changeEventStream(src,selector) 94 | % see which was selected 95 | qSel = selector.UserData.streamItems==src; 96 | % make sure others are not selected 97 | [selector.UserData.streamItems(~qSel).Value] = deal(false); 98 | end 99 | 100 | function editBoxCB(~,selector) 101 | % make sure editbox item is selected 102 | selector.UserData.streamItems(end).Value = true; 103 | % make sure others are not selected 104 | [selector.UserData.streamItems(1:end-1).Value] = deal(false); 105 | end 106 | 107 | function buttonClick(~) 108 | uiresume; 109 | end -------------------------------------------------------------------------------- /code/pythagoras.m: -------------------------------------------------------------------------------- 1 | function [z] = pythagoras(x,y) 2 | 3 | z = sqrt((x).^2 + (y).^2); 4 | -------------------------------------------------------------------------------- /code/readNPY.m: -------------------------------------------------------------------------------- 1 | 2 | 3 | function data = readNPY(filename) 4 | % Function to read NPY files into matlab. 5 | % *** Only reads a subset of all possible NPY files, specifically N-D arrays of certain data types. 6 | % See https://github.com/kwikteam/npy-matlab/blob/master/tests/npy.ipynb for 7 | % more. 8 | % 9 | 10 | [shape, dataType, fortranOrder, littleEndian, totalHeaderLength, ~] = readNPYheader(filename); 11 | 12 | if littleEndian 13 | fid = fopen(filename, 'r', 'l'); 14 | else 15 | fid = fopen(filename, 'r', 'b'); 16 | end 17 | 18 | try 19 | 20 | [~] = fread(fid, totalHeaderLength, 'uint8'); 21 | 22 | % read the data 23 | data = fread(fid, prod(shape), [dataType '=>' dataType]); 24 | 25 | if length(shape)>1 && ~fortranOrder 26 | data = reshape(data, shape(end:-1:1)); 27 | data = permute(data, [length(shape):-1:1]); 28 | elseif length(shape)>1 29 | data = reshape(data, shape); 30 | end 31 | 32 | fclose(fid); 33 | 34 | catch me 35 | fclose(fid); 36 | rethrow(me); 37 | end 38 | -------------------------------------------------------------------------------- /code/readNPYheader.m: -------------------------------------------------------------------------------- 1 | 2 | 3 | function [arrayShape, dataType, fortranOrder, littleEndian, totalHeaderLength, npyVersion] = readNPYheader(filename) 4 | % function [arrayShape, dataType, fortranOrder, littleEndian, ... 5 | % totalHeaderLength, npyVersion] = readNPYheader(filename) 6 | % 7 | % parse the header of a .npy file and return all the info contained 8 | % therein. 9 | % 10 | % Based on spec at http://docs.scipy.org/doc/numpy-dev/neps/npy-format.html 11 | 12 | fid = fopen(filename); 13 | 14 | % verify that the file exists 15 | if (fid == -1) 16 | if ~isempty(dir(filename)) 17 | error('Permission denied: %s', filename); 18 | else 19 | error('File not found: %s', filename); 20 | end 21 | end 22 | 23 | try 24 | 25 | dtypesMatlab = {'uint8','uint16','uint32','uint64','int8','int16','int32','int64','single','double', 'logical'}; 26 | dtypesNPY = {'u1', 'u2', 'u4', 'u8', 'i1', 'i2', 'i4', 'i8', 'f4', 'f8', 'b1'}; 27 | 28 | 29 | magicString = fread(fid, [1 6], 'uint8=>uint8'); 30 | 31 | if ~all(magicString == [147,78,85,77,80,89]) 32 | error('readNPY:NotNUMPYFile', 'Error: This file does not appear to be NUMPY format based on the header.'); 33 | end 34 | 35 | majorVersion = fread(fid, [1 1], 'uint8=>uint8'); 36 | minorVersion = fread(fid, [1 1], 'uint8=>uint8'); 37 | 38 | npyVersion = [majorVersion minorVersion]; 39 | 40 | headerLength = fread(fid, [1 1], 'uint16=>uint16'); 41 | 42 | totalHeaderLength = 10+headerLength; 43 | 44 | arrayFormat = fread(fid, [1 headerLength], 'char=>char'); 45 | 46 | % to interpret the array format info, we make some fairly strict 47 | % assumptions about its format... 48 | 49 | r = regexp(arrayFormat, '''descr''\s*:\s*''(.*?)''', 'tokens'); 50 | if isempty(r) 51 | error('Couldn''t parse array format: "%s"', arrayFormat); 52 | end 53 | dtNPY = r{1}{1}; 54 | 55 | littleEndian = ~strcmp(dtNPY(1), '>'); 56 | 57 | dataType = dtypesMatlab{strcmp(dtNPY(2:3), dtypesNPY)}; 58 | 59 | r = regexp(arrayFormat, '''fortran_order''\s*:\s*(\w+)', 'tokens'); 60 | fortranOrder = strcmp(r{1}{1}, 'True'); 61 | 62 | r = regexp(arrayFormat, '''shape''\s*:\s*\((.*?)\)', 'tokens'); 63 | shapeStr = r{1}{1}; 64 | arrayShape = str2num(shapeStr(shapeStr~='L')); 65 | 66 | 67 | fclose(fid); 68 | 69 | catch me 70 | fclose(fid); 71 | rethrow(me); 72 | end 73 | -------------------------------------------------------------------------------- /code/streamSelectorGUI.m: -------------------------------------------------------------------------------- 1 | function [stream,eventCat] = streamSelectorGUI(coding) 2 | 3 | % panel layout: 4 | %%%%%%% 5 | % () stream 1 6 | % () stream 2 7 | % () stream N 8 | %%%%%%% 9 | % if not a gazeCode stream, then: 10 | % for selected stream, show categories, user needs to select one also 11 | %%%%%%% 12 | 13 | nStream = length(coding.mark); 14 | qGazeCodeStream = cellfun(@(x) isfield(x,'tag') && strcmp(x.tag,'gazeCodeStream'),coding.settings.streams); 15 | qGazeCodeStream(~coding.stream.available) = []; 16 | % get names of code categories for each stream 17 | names(~qGazeCodeStream) = arrayfun(@(x)getCodeCategories(coding,x),find(~qGazeCodeStream),'uni',false); 18 | [names{qGazeCodeStream}]= deal({'This is a GazeCode stream, no need to select anything here to modify or review it'}); 19 | % get number of characters in longest stream label 20 | nCharS = max(cellfun(@length,coding.stream.lbls)); 21 | % get number of characters for each code category label 22 | nCharC = cellfun(@(x) cellfun(@length,x),names,'uni',false); 23 | nCharC = max(cat(1,nCharC{:})); 24 | 25 | selector = dialog('WindowStyle', 'normal', 'Position',[100 100 200 200],'Name','Select an event stream to code/review','Visible','off'); 26 | selector.UserData.catNames = names; 27 | 28 | % create panel 29 | marginsP = [3 3]; 30 | marginsB = [2 5 5]; % horizontal: [margin from left edge, margin between radiobutton and text, vertical item spacing] 31 | buttonSz = [80 24]; 32 | 33 | 34 | % temp uipanel because we need to figure out size of margins 35 | temp = uipanel('Units','pixels','Position',[10 10 400 400],'title','Xxj','Parent',selector); 36 | 37 | % temp checkbox and label because we need their sizes too 38 | % use largest label 39 | h= uicomponent('Style','radiobutton', 'Parent', temp,'Units','pixels','Position',[10 10 400 100], 'String',repmat('m',1,max([nCharS nCharC]))); 40 | drawnow 41 | % get sizes, delete 42 | relExt = h.Extent; relExt(3) = relExt(3)+15; % radiobutton not counted in, guess a bit safe 43 | off = [temp.InnerPosition(1:2)-temp.Position(1:2) temp.Position(3:4)-temp.InnerPosition(3:4)]; 44 | delete(temp); 45 | 46 | % determine size of popup 47 | rowWidth = marginsB(1)*2+marginsB(2)+relExt(3); 48 | panelWidth = rowWidth+ceil(off(3)); 49 | popUpWidth = panelWidth+marginsP(1)*2; 50 | % determine height of two panels (stream selector, code cat selector) 51 | rowHeight = relExt(4) + marginsB(3); 52 | panelHeight(1) = rowHeight*nStream + nStream*2*marginsB(3); 53 | nCodeCatMax = max(cellfun(@length,names)); 54 | panelHeight(2) = rowHeight*nCodeCatMax + nCodeCatMax*2*marginsB(3); 55 | % popup height 56 | popUpHeight = sum(panelHeight)+ 2*ceil(off(4)) + 3*marginsP(2)+buttonSz(2); 57 | 58 | selector.UserData.relExt = relExt; 59 | selector.UserData.marginsB = marginsB; 60 | 61 | % determine position and create in right size 62 | scrSz = get(0,'ScreenSize'); 63 | pos = [(scrSz(3)-popUpWidth)/2 (scrSz(4)-popUpHeight)/2 popUpWidth popUpHeight]; 64 | selector.Position = pos; 65 | 66 | % create button 67 | selector.UserData.button = uicontrol(... 68 | 'Style','pushbutton','Tag','executeReload','Position',[3+marginsB(1) marginsP(2) buttonSz],... 69 | 'Callback',@(hBut,~) buttonClick(hBut),'String','Use selection',... 70 | 'Parent',selector); 71 | 72 | % create panels 73 | selector.UserData.streamPanel = uipanel('Units','pixels','Position',[marginsP(1) 2*marginsP(2)+buttonSz(2)+ceil(off(4))+panelHeight(2) panelWidth panelHeight(1)],'Parent',selector,'title','select event stream'); 74 | selector.UserData.catPanel = uipanel('Units','pixels','Position',[marginsP(1) 2*marginsP(2)+buttonSz(2) panelWidth panelHeight(2)],'Parent',selector,'title','select event category to code'); 75 | 76 | % make items in stream selector panel 77 | for s=1:nStream 78 | p=nStream-s; 79 | selector.UserData.streamPanelItems(s) = uicomponent('Style','radiobutton', 'Parent', selector.UserData.streamPanel,'Units','pixels','Position',[3 p*(relExt(4)+2*marginsB(3)) + marginsB(3) 200 20], 'String',coding.stream.lbls{s},'Value',false, 'Callback',@(src,~) changeEventStream(src,selector)); 80 | end 81 | selector.UserData.catPanelItems = gobjects(0); 82 | selector.UserData.nCodeCatMax = nCodeCatMax; 83 | 84 | selector.Visible = 'on'; 85 | uiwait(selector); 86 | 87 | if ishghandle(selector) 88 | stream = find([selector.UserData.streamPanelItems.Value]); 89 | if ~isempty(stream) 90 | eventCat = 2^(find([selector.UserData.catPanelItems.Value])-1); % this is safe because the flag fields removed above are always the last in the code cats. Else store a look up table somewhere and use that 91 | end 92 | else 93 | [stream,eventCat] = deal([]); 94 | end 95 | 96 | delete(selector); 97 | end 98 | 99 | function changeEventStream(src,selector) 100 | % see which was selected 101 | qSel = selector.UserData.streamPanelItems==src; 102 | % make sure others are not selected 103 | [selector.UserData.streamPanelItems(~qSel).Value] = deal(false); 104 | 105 | % delete all current category panel items 106 | delete(selector.UserData.catPanelItems); 107 | selector.UserData.catPanelItems(:) = []; 108 | % make new ones 109 | if selector.UserData.streamPanelItems(qSel).Value 110 | names = selector.UserData.catNames{qSel}; 111 | for s=1:length(names) 112 | p=selector.UserData.nCodeCatMax-s; 113 | selector.UserData.catPanelItems(s) = uicomponent('Style','radiobutton', 'Parent', selector.UserData.catPanel,'Units','pixels','Position',[3 p*(selector.UserData.relExt(4)+2*selector.UserData.marginsB(3)) + selector.UserData.marginsB(3) selector.UserData.relExt(3) 20], 'String',names{s},'Value',false, 'Callback',@(src,~) changeEventCat(src,selector)); 114 | end 115 | end 116 | end 117 | 118 | function changeEventCat(src,selector) 119 | % see which was selected 120 | qSel = selector.UserData.catPanelItems==src; 121 | % make sure others are not selected 122 | [selector.UserData.catPanelItems(~qSel).Value] = deal(false); 123 | end 124 | 125 | function buttonClick(~) 126 | uiresume; 127 | end 128 | 129 | function names = getCodeCategories(coding,idx) 130 | % this skips flag fields and for names removes the flag-possible indicator 131 | names = coding.codeCats{idx}(:,1); 132 | for p=length(names):-1:1 133 | if names{p}(1) == '*' 134 | names(p) = []; 135 | elseif names{p}(end) == '+' 136 | names{p}(end) = []; 137 | end 138 | end 139 | end -------------------------------------------------------------------------------- /code/tempdat.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/code/tempdat.mat -------------------------------------------------------------------------------- /code/test.m: -------------------------------------------------------------------------------- 1 | load('../results/2022-12-20-12-56-29/2022-12-20-12-56-29.mat') 2 | 3 | figure; 4 | ax1 = subplot(2,1,1); 5 | plot(gv.datt,gv.datx,'r-'); 6 | hold on 7 | 8 | for a=1:numel(gv.mfr) 9 | st = gv.datwt(gv.bfr(a)); 10 | mt = gv.datwt(gv.mfr(a)); 11 | et = gv.datwt(gv.efr(a)); 12 | 13 | plot([st mt et],[gv.fixxposB(a) gv.fixxpos(a) gv.fixxposE(a)],'k.-') 14 | end 15 | 16 | xlabel('Time (ms)') 17 | ylabel('Horizontal position (pix)') 18 | 19 | ax2 = subplot(2,1,2); 20 | plot(gv.datt,gv.daty,'b-') 21 | hold on 22 | 23 | for a=1:numel(gv.mfr) 24 | st = gv.datwt(gv.bfr(a)); 25 | mt = gv.datwt(gv.mfr(a)); 26 | et = gv.datwt(gv.efr(a)); 27 | 28 | plot([st mt et],[gv.fixyposB(a) gv.fixypos(a) gv.fixyposE(a)],'k.-') 29 | end 30 | 31 | xlabel('Time (ms)') 32 | ylabel('Vertical position (pix)') 33 | 34 | linkaxes([ax1 ax2],'x') -------------------------------------------------------------------------------- /code/truescreensize.m: -------------------------------------------------------------------------------- 1 | function [screensize] = truescreensize() 2 | 3 | 4 | % function that detects the true screen resolution of the screen Matlab is 5 | % running on, based on java rather tahn Matlab's get(0,'ScreenSize') which 6 | % seems to go haywire on multiple screen setups. 7 | 8 | ge = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment; 9 | gd = ge.getDefaultScreenDevice; 10 | screensize = [gd.getDisplayMode.getWidth gd.getDisplayMode.getHeight]; 11 | 12 | % take display scaling into account 13 | screensize = screensize/getDPIScale; -------------------------------------------------------------------------------- /code/writeNPY.m: -------------------------------------------------------------------------------- 1 | 2 | 3 | function writeNPY(var, filename) 4 | % function writeNPY(var, filename) 5 | % 6 | % Only writes little endian, fortran (column-major) ordering; only writes 7 | % with NPY version number 1.0. 8 | % 9 | % Always outputs a shape according to matlab's convention, e.g. (10, 1) 10 | % rather than (10,). 11 | 12 | 13 | shape = size(var); 14 | dataType = class(var); 15 | 16 | header = constructNPYheader(dataType, shape); 17 | 18 | fid = fopen(filename, 'w'); 19 | fwrite(fid, header, 'uint8'); 20 | fwrite(fid, var, dataType); 21 | fclose(fid); 22 | 23 | 24 | end 25 | 26 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/calibrations/3gqkra3/calibration.json: -------------------------------------------------------------------------------- 1 | { 2 | "ca_id": "3gqkra3", 3 | "ca_info": { 4 | 5 | }, 6 | "ca_participant": "vxgtdb2", 7 | "ca_state": "calibrated", 8 | "ca_data": "DHUAEQJYO6HUAJFDSY6R5NSDHUA52ACBOVQJUQG6V4GLZM335Q6Q", 9 | "ca_error_code": 0, 10 | "ca_type": "default", 11 | "ca_created": "2019-03-20T13:25:52+0000", 12 | "ca_project": "raoscyb" 13 | } 14 | 15 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/participants/vxgtdb2/participant.json: -------------------------------------------------------------------------------- 1 | { 2 | "pa_id": "vxgtdb2", 3 | "pa_info": { 4 | "EagleId": "55a6b648-094f-4c83-bcf1-abe0ddbf24c9", 5 | "Name": "Roy005", 6 | "Notes": "" 7 | }, 8 | "pa_project": "raoscyb", 9 | "pa_calibration": "3gqkra3", 10 | "pa_created": "2019-03-20T13:44:35+0000" 11 | } 12 | 13 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "pr_id": "raoscyb", 3 | "pr_info": { 4 | "CreationDate": "03/18/2019 14:14:24", 5 | "EagleId": "5cda1c59-72cf-47a8-9535-2b1df14ed0f9", 6 | "Name": "DemoIntegratie" 7 | }, 8 | "pr_created": "2019-03-18T14:14:46+0000" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/participant.json: -------------------------------------------------------------------------------- 1 | { 2 | "pa_id": "vxgtdb2", 3 | "pa_info": { 4 | "EagleId": "55a6b648-094f-4c83-bcf1-abe0ddbf24c9", 5 | "Name": "Roy005", 6 | "Notes": "" 7 | }, 8 | "pa_project": "raoscyb", 9 | "pa_calibration": "3gqkra3", 10 | "pa_created": "2019-03-20T13:44:35+0000" 11 | } 12 | 13 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/recording.json: -------------------------------------------------------------------------------- 1 | { 2 | "rec_id": "gzz7stc", 3 | "rec_info": { 4 | "EagleId": "3e51fbf9-b7ca-4a1c-b333-5795d7af0c29", 5 | "Name": "Recording011", 6 | "Notes": "" 7 | }, 8 | "rec_participant": "vxgtdb2", 9 | "rec_project": "raoscyb", 10 | "rec_state": "done", 11 | "rec_segments": 1, 12 | "rec_length": 28, 13 | "rec_calibration": "3gqkra3", 14 | "rec_created": "2019-03-20T13:25:47+0000", 15 | "rec_et_samples": 1424, 16 | "rec_et_valid_samples": 1331, 17 | "ts_right_x": -32.5, 18 | "ts_right_y": -27.0, 19 | "ts_right_z": -19.0, 20 | "ts_left_x": 32.5, 21 | "ts_left_y": -27.0, 22 | "ts_left_z": -19.0, 23 | "ts_green_limit_radius": 10.0, 24 | "ts_yellow_limit_radius": 12.5 25 | } 26 | 27 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/calibration.json: -------------------------------------------------------------------------------- 1 | { 2 | "ca_id": "3gqkra3", 3 | "ca_info": { 4 | 5 | }, 6 | "ca_participant": "vxgtdb2", 7 | "ca_state": "calibrated", 8 | "ca_data": "DHUAEQJYO6HUAJFDSY6R5NSDHUA52ACBOVQJUQG6V4GLZM335Q6Q", 9 | "ca_error_code": 0, 10 | "ca_type": "default", 11 | "ca_created": "2019-03-20T13:25:52+0000", 12 | "ca_project": "raoscyb" 13 | } 14 | 15 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/et.tslv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/et.tslv.gz -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/eyesstream.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/eyesstream.mp4 -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/fullstream.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/fullstream.mp4 -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/livedata.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/livedata.json.gz -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/mems.tslv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/mems.tslv.gz -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/segments/1/segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "seg_id": 1, 3 | "seg_length": 28, 4 | "seg_length_us": 28494205, 5 | "seg_calibrating": true, 6 | "seg_calibrated": true, 7 | "seg_t_start": "2019-03-20T13:25:54+0000", 8 | "seg_t_stop": "2019-03-20T13:26:22+0000", 9 | "seg_created": "2019-03-20T13:25:54+0000", 10 | "seg_end_reason": "api", 11 | "seg_eyesstream": true 12 | } 13 | -------------------------------------------------------------------------------- /data/demoTobiiSD/projects/raoscyb/recordings/gzz7stc/sysinfo.json: -------------------------------------------------------------------------------- 1 | { "servicemanager_version": "1.25.3-citronkola", 2 | "fpga_v_maj": 0, 3 | "fpga_v_min": 0, 4 | "fpga_v_rel": 62, 5 | "fpga_variant": "normal", 6 | "board_type": "PVT Board", 7 | "hu_serial": "TG02G-010105956192", 8 | "ru_serial": "TG02B-080105043691", 9 | "sys_ec_preset": "Indoor", 10 | "sys_sc_preset": "GazeBasedExposure" 11 | } 12 | -------------------------------------------------------------------------------- /data/demodata.txt: -------------------------------------------------------------------------------- 1 | data folders with content should be placed here 2 | get demo data for the different folders at: https://tinyurl.com/gazecodedemodata -------------------------------------------------------------------------------- /images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/images/splash.png -------------------------------------------------------------------------------- /images/videopause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbenjamins/gazecode/bcb4847065bc698fb374a5503271b0883458fc38/images/videopause.png -------------------------------------------------------------------------------- /results/demoresults.txt: -------------------------------------------------------------------------------- 1 | results will appear here as folders --------------------------------------------------------------------------------