Steady-state Visual Evoked Potential (sVEP) and psychophysical contrast-sensitivity testing for non-verbal subjects like alert infant macaques, combining frequency-tagged stimuli, real-time eye tracking with a non-invasive 3D-printed head stabilisation.
This repository contains the 3D STL models, MATLAB code and GUIs used for the experiments described in:
"Non-invasive Measurements of Visual Sensitivity using Eye Tracking and Visual Evoked Potentials in Young Alert Macaques" Shu Wang, Yuanyang Huang, Jiahao Tu, Xiaochun Wang, Rui Liu, Shahab Zarei, Hong Liu, Ian M. Andolina.
The models can be 3D printed and modified as required. For an exmaple, the assembled helmet + ear-bridge + sample EEG electrode placements for sVEP can be seen here:
https://github.com/CogPlatform/sVEPTesting/blob/main/Helmet/EEG%20helmet.STL
The code in this repository implements the two measurement variants from our study:
- sVEP contrast-sensitivity (CS) measurement — objective, electrophysiological estimation of contrast thresholds from steady-state VEPs recorded while the monkey passively fixates gratings flickering at 15 Hz (0.066ms transition; the full phase cycle is 7.5Hz), with gaze-triggered trial selection and rigorous trial screening using an eye tracker.
- Psychophysical contrast-sensitivity measurement — an eye-movement-based behavioural choice task (saccade to a peripheral target = "I saw the grating", maintain fixation = "I did not see the grating"), which provides the reference behaviour for validating the sVEP estimates (Fig. 1D in the manuscript).
Both pipelines share the same stimulus family (vertically oriented sine-wave gratings, 0.5–8 cycles/deg, 0.5–96% contrast), the same eye tracker (Tobii TX300, via Titta), and the same reward system (Arduino-controlled juice delivery via arduinoManager).
If you use this code in your research, please cite the manuscript (once published), and the software on which it depends:
- Kleiner, M., et al. (2007). What's New in Psychtoolbox-3. Perception 36:1–16.
- Niehorster, D. C., Andersson, R., & Nyström, M. (2020). Titta: A toolbox for creating PsychToolbox and Psychopy experiments with Tobii eye trackers. Behavior Research Methods 52:1970–1979.
- Andolina, I. M. (2023). Opticka: Psychophysics-toolbox based experiment manager (v2.16.1). Zenodo. https://doi.org/10.5281/zenodo.13756387
- Oostenveld, R., et al. (2011). FieldTrip: Open source software for advanced analysis of MEG, EEG and invasive electrophysiological data. Computational Intelligence and Neuroscience 2011:156869.
- Niehorster, D. C., et al. (2023). Enhancing eye tracking for nonhuman primates… adaptive calibration. Behavior Research Methods 57:4.
| File | Purpose | Corresponding manuscript part |
|---|---|---|
Helmet/ |
The STL model files for 3D printing a non-invasive head restraint. | Fig. 1B-C |
basicTraining.mlapp |
GUI to train subjects to fixation and run the fixation-only sVEP test measuring contrast-sensitivity function. | "Fixation-only Contrast Threshold Measurement" |
runBasicTraining.m |
Core trial enfine for basicTraining. | "Fixation-only Contrast Threshold Measurement" |
behaviouralAcuity.mlapp |
GUI to run the eye-tracker-based psychophysical acuity/contrast task (training stages + measurement). | "Psychophysical Contrast Threshold Measurement" Fig. 1D |
runBehaviouralAcuity.m |
Core trial engine for the behavioural task: fixation, blank period, grating/blank choice via saccade; reward/error handling; data logging. | "Psychophysical Contrast Threshold Measurement" Fig. 1D |
behaviouralAcuityAnal.m |
Fits a psychometric function (Palamedes Gumbel) to the saved behavioural data to estimate the psychophysical contrast threshold. | "Psychophysical Contrast Threshold Estimation" |
behaviouralAcuityAnalysis.mlx |
Simple live-script example of the same fitting (quick PAL_Quick fit). | – |
loadEEG.mlapp |
GUI for sVEP analysis: load a session (.edf EEG + .mat experiment), set channels/trigger decoding, preprocess (FieldTrip), time-lock, FFT amplitude extraction, contrast-response (VER) curves, threshold estimation. | "sVEP Contrast Threshold Measurement / Estimation" |
runEEGAnalysis.m |
Core engine of loadEEG: full FieldTrip analysis path (trigger decode → preprocessing → timelock → FFT → tuning/contrast curves with linear fits). |
"sVEP Recording and Signal Analysis" |
loadCOGEEG.m |
FieldTrip custom trial function: decodes 8-bit strobed words from 8 analog EEG channels into trial markers, removes ring artifacts, and builds the FieldTrip trl matrix. |
"sVEP Recording" |
measureContrast.m |
Monitor calibration validation: measures luminance with a SpectroCAL photometer across contrast, phase and bit-depth steps — verifying the gamma-corrected display. | "sVEP Visual Stimulation" |
VEPTest.m |
Development/hardware smoke-test: checkerboard phase-reversal, LabJack strobe, Tobii + reward; demonstrates the full recording/synchronisation chain. | – |
- MATLAB (R2021b or later; the
.mlappfiles are App Designer apps and must be opened inside MATLAB — double-click them or typeopen behaviouralAcuity.mlappin the command window). - Opticka — the experiment-manager toolbox used by the manuscript. It provides
screenManager, multiple stimulus classes (gratingStimulus,discStimulus,fixationCrossStimulus,metaStimulus),calibrateLuminance(gamma-table calibration),audioManager,arduinoManager,labJackT(LabJack strobe),tobiiManager(unified Titta wrapper),stimulusSequence, and theanalysisCorehelper class used byrunEEGAnalysis. - Psychophysics Toolbox (PTB) — screen/audio/input, required by Opticka.
- Palamedes (v1.11+) — psychometric fitting (
PAL_*) used by the behavioural analysis and by the staircase option (we didn't use the staircase in the paper, but it is an option). - FieldTrip — EEG preprocessing / timelocked / frequency analysis (
ft_preprocessing,ft_definetrial,ft_timelockanalysis, ...). - Titta — improved Tobii eye-tracker wrapper (alternative to using the Tobii Pro SDK) used by Opticka's
tobiiManager. Especially for non-verbal subjects, Titta afford the best calibration interface of any commercial or open-source eyetracker available!
- Eye tracker: Tobii TX300 / Pro Spectrum / Pro 4C (selectable in the GUIs); remote, non-invasive.
- EEG system: GRAEL v2 (Compumedics) recorded to EDF in PSG online software at 1024 Hz, band-pass 0.1–300 Hz, 50 Hz notch. O1/O2 electrodes 1.5 cm from the occipital pole, reference on the vertex, ground on the ear.
- The EDF therefore contains two electrode channels plus 8 trigger ("bit") channels plus a photodiode channel (11 channels total in the default configuration).
- Trigger interface: LabJack (e.g. T4), writing 8 digital lines as analog signals into the EEG (see Trigger encoding).
- Reward pump: Arduino-controlled solenoid via Opticka
arduinoManager(asynchronoustimedTTL()), with acoustic feedback (1 kHz correct / 100 Hz error tone). - Display: AORUS 27" at 60 cm, gamma-corrected (SpectroCalII), native 10-bit mode (
Native10Bitin Opticka/PTB). - Non-invasive 3D-printed helmet for gentle restraint of the head (design described in the manuscript; CAD models distributed separately per its Code Availability statement).
Everything is driven by two App Designer GUIs plus the two analysis scripts:
flowchart TD
A[behaviouralAcuity.mlapp] -->|runBehaviouralAcuity.m| B[(data.mat: ana+task)]
B -->|behaviouralAcuityAnal.m| C[Psychometric threshold]
E[measureContrast.m] -->|gamma + 10-bit verification| F
F[basicTraining.mlapp sVEP experiment: Opticka gratings, 0.066 ms phase reversal, LabJack strobe] -->|.mat session| G
F -->|"EDF: 2 EEG + 8 bits + photodiode"| G
G -->|loadEEG.mlapp + runEEGAnalysis.m| H[sVEP amplitude at 7.5 Hz<br/>contrast-response curves]
H --> I[Linear regression, threshold at blank + 2·SE]
B --> J[CS = 1/threshold per SF]
I --> J
J --> K[CSF: double-exp fit, compare sVEP vs psychophysical, PVR]
-
Open the GUI:
open behaviouralAcuity.mlapp
On the Control tab fill in: subject name, save/result directory, luminance calibration file (gamma table, e.g.
AorusFI27-120Hz-NEWcalibration.mat), screen distance / pixels-per-cm, colour depth (Native10Biton the AORUS), background colour.The Control tab groups the experiment metadata into a General panel (subject, result directory, calibration file, monitor geometry/colour depth, task type and block count) and a Task / Stimulus Options panel (all stimulus parameter, timing and Bayesian-staircase settings listed in step 2), with GO! / Save! / Exit! at the bottom.
-
Stimulus / task settings (mirrors the manuscript):
TaskType: "Blank Stage 1", "Blank Stage 2", "Grating Alone", "Blank + Grating" — the training/measurement stages of Fig. 1D; "Blank + Grating" is the full task used for data collection.SFspatial frequency (0.5–8 cpd).contrastRange— five log-spaced levels per SF, plus 0% (blank catch) and a supra-threshold level (GUI default0 0.005 0.2; run piloting per subject as in the manuscript).gProbability— probability that the target period shows a grating (75% default, the manuscript value); the rest are blank trials.- Timing fields:
TargetON(0.2–0.5 s distractor onset delay),Switchtimes(0.5–1.25 s blank-period duration),FixTargets(750 ms hold on target),InitTargets(600 ms). Staircaseswitch: Bayesian staircase (PAL_AMPMwith optional priors) or method of constant stimuli (nBlocksblocks; the default 20 blocks present each contrast once per block, so two sessions give the 40 trials/condition fitted in the manuscript). The manuscript dataset was collected with the method of constant stimuli.
-
Eye-tracker tab: tracker model (
TX300,Pro Spectrum,Pro 4C), tracking modemacaque, sample rate (300 Hz), calibration/validation positions, and the Titta-based adaptive calibration for non-verbal subjects (Niehorster et al. 2024). Use dummy mode (isDummy) to test without hardware.The EyeTracker tab holds the Eyetracker settings (model, tracking mode, calibration stimulus, sample-rate dial), the Initial Fixation Window (X/Y,
INIT/FIXtimes, radius, strictness), Fixation Cross Parameters (size, alpha, line width) and the Calibration / Smoothing panel implementing the Titta adaptive/operator-paced calibration and gaze smoothing used for non-verbal subjects. -
Click GO: calibrate/validate the tracker, then per trial:
- Init: acquire the central fixation cross (
initTimetimeout,fixTimehold; defaults 3 s / 0.3 s — adjust per subject); - Blank period: blank disc at mean luminance; the fixation cross fades out; a small distractor appears 15° left, its onset randomized 200–500 ms after the blank period begins. Broken fixation →
BREAKBLANK, no reward; - Target period: grating or blank. Grating → saccade to the left target and hold
FixTargets750 ms →YESTARGET(reward + 1 kHz tone). Blank → maintain fixation in the central window →YESBLANK. Errors (BREAKTARGET) give a negative tone. - Press
qto abort cleanly.
- Init: acquire the central fixation cross (
-
Data saved to the result folder as
BAMOC_<subject>_<timestamp>.mat(method of constants) orBASTAIR_...(staircase), containingana,task,sM,stimuliandeT(see Data structures). ACRASH.matis written if the loop errors. -
Analyse with
behaviouralAcuityAnal(or the.mlxexample): select the.mat; it counts per-contrast hit/error trials, fits the Gumbel psychometric function by maximum likelihood (PalamedesPAL_PFML_Fit; fixed guess, free threshold/slope/lapse within limits) and plots the curve with threshold (alpha), slope and lapse, plus optional bootstrap goodness-of-fit. Contrast threshold = alpha → CS = 1/threshold at that spatial frequency. Repeat across the five SFs and fit the double-exponential CSF (Y = a·e^(bx) + c·e^(dx)) as described in the manuscript.
The sVEP session is a custom Opticka protocol run via a specialised GUI: vertical sine gratings, five SFs, 11 contrast levels + blank (96→0.5% + 0%), 10 trials per contrast per block, 4 blocks → 40 trials/condition; trial starts after 500 ms confirmed fixation, 1500 ms stimulus, 2000 ms inter-trial interval; 15 Hz phase reversal (7.5Hz full cycle); 15.6×26.4° at 60 cm; 75 cd/m², gamma-linearised, 10-bit. Save the Opticka session .mat and the EDF from the EEG.
[!INFO] This GUI is also used to control some basic eye tracker training, so note some of the GUI parameters (like random position) are not functional when running the sVEP task.
The synchronous strobe word is written at trial start (lM.strobeServer(conditionValue)) and 255 at trial end; see Trigger encoding.
loadEEG- Files: the MAT (Opticka session) and the EDF (EEG).
- Channel configuration: Data # (EEG channels, default
1:2), Trigger # (8 bit channels, default3:10), Photodiode (pDiode, default11). The header button auto-guesses: data1:n-9, triggersn-8:n-1, pDiode = n. - Trigger settings:
threshold(default5 * nanmedian),minTrigger(15 ms — events closer are treated as ringing),jitter(3 samples),pad(0.5 s pre/post). - FieldTrip Preprocessing: demean/detrend/poly/median/band-pass (e.g. 2–100 Hz), optional re-referencing, Visual artifact rejection (FieldTrip
ft_rejectvisual), optional trial surrogate generation. - Check strobes first with "Show triggers": raw traces with decoded trigger words and a table comparing triggers sent (from the
.mat) vs decoded triggers found; the lamp turns green when they match. This verifies exact synchronisation. - RUN:
ft_preprocessing→ft_timelockanalysisper condition → FFT power spectra per condition (window default 0.2–1.5 s) with fundamental (f1), 2nd harmonic (f2), DC (f0) amplitude/phase extracted per channel. - Contrast–VER curves: mean
f1power ± error (SE/SD/CI selectable) per contrast level per SF, plus a threshold line at blank + 2·(SE/SD) — the empirical noise floor. - Threshold estimation (manuscript Fig. 2A–E): use the "Points to exclude" field to limit the linear regression to the rising limb (first peak at high contrast down to the lowest contrast that leaves the blank baseline); the app fits the regression with 95% CI and threshold = intersection with the blank-response line. Extract per SF.
- CS: CS = 100 / threshold(%). The final CSF fit and statistics (Pearson sVEP vs psychophysics, PVR timings) follow the manuscript Methods.
Outputs: info structure in the base workspace, including info.daT — table of f0/f1/f2 power and phase (± errors) per condition — info.triggers, info.trl, info.rejected, info.origSettings and info.seq (the Opticka trial order) for custom re-analysis.
We use calibrateLuminance.m from opticka to perform the precise gamma curve measurements using a SpectroCalII from Cambridge Research Graphics. This function automatically adjusts the R | G | B | L values independently, measures a full photometric measurement (including luminance) and after sampling across N values of R G B and L fitting several models to the data, then generating a full gamma table, then applying the gamma table, then remeasuring and validating linearity. The calibrateLuminance object can be saved as a MAT file and then loaded using opticka's screenManager as both tasks above do. You can then use measureContrast.m to verify the gamma/linearity, bit-depth behaviour and contrast steps with a SpectroCAL: draws gratings at several luminance/contrast steps and measures output in cd/m².
Response codes used by runBehaviouralAcuity and the analyses:
| Constant | Value | Meaning |
|---|---|---|
YESBLANK |
1 | Blank trial, maintained fixation (correct rejection) |
YESTARGET |
2 | Target trial, saccade+hold to the peripheral target (hit) |
UNSURE |
4 | Legacy, unused |
BREAKINIT |
-100 | No fixation acquired in INIT |
BREAKBLANK |
-10 | Fixation broken during the blank period |
BREAKTARGET |
-1 | Fixation broken during the target period |
BREAKEXCL |
-5 | Fixation broken in the exclusion window |
UNDEFINED |
0 | Trial not completed |
Behavioural .mat: ana (settings + per-trial ana.task array: contrast, showGrating, response code, tStart/tBlank/tGrat/tEnd, RT, xAll/yAll eye-sample arrays, ana.gT/ana.bT %-correct accumulators), task (the Opticka stimulusSequence), sM (screen manager), stimuli, eT (eyetracker settings).
The EEG and stimulus are synchronised by strobed 8-bit words output from a LabJack (lM.strobeServer(value)) into 8 EEG analog channels (our GRAEL EEG system did not support strobed words directly so we had to use analog channels to record the 8 binary lines, other EEG systems will not need this if they can encode digital I/O directly):
- trial start:
lM.strobeServer(conditionValue)— the word encodes the stimulus condition (e.g. contrast index); - trial end:
lM.strobeServer(255).
loadCOGEEG.m detects the rising flanks on the 8 channels, removes events closer than minTrigger (ringing), converts bits → word, and defines a trial as strobed word ≠ 255 followed by exactly 255 (optionally also requiring word correctID = 250 as a final marker). The FieldTrip trl matrix column 4 carries the stimulus word (trialinfo), which maps to stimuli through info.seq (ana from the Opticka session). VEPTest.m is a complete demonstration of this chain.
Protocol constants (Methods of the manuscript): five SFs (0.5, 1, 2, 4, 8 cpd); 11 contrasts + blank per SF (96 → 0.5%); sVEP 15 Hz phase reversal; stimulus 15.6×26.4° at 60 cm, 75 cd/m²; EEG 1024 Hz 0.1–300 Hz + 50 Hz notch, O1/O2 vs vertex reference; psychophysics: five log contrasts + 0%/20% per SF, 20 trials/condition/session ×2; per-trial timing as in Fig.1D. Both CS (VEP and psych) are computed from thresholds as described above; CSFs (double-exponential) and the correlation (Pearson r) and PVR efficiency measure are then derived as described in the Methods.
- No trials in EEG: check the trigger channels in
loadEEG(must be the 8 physical lines from the LabJack; verify with "Show triggers"); trythreshold= 5 × median first; increaseminTriggerif ringing creates spurious words. - Eye not acquired in the task: increase
initTime/fixTime, disable dummy ("dummy OFF"), choose the correct eye and calibration positions. - Reward not delivered:
arduinoManagerCOM port and therM.openGUI=falsedefault inrunBehaviouralAcuitymust be configured; rewards fire only on successful trials. - Staircase file (
BASTAIR_) vs constants (BAMOC_) — both are analysed bybehaviouralAcuityAnal(it readstask.nVar(1).values); the manuscript thresholds come from the method-of-constants data. - Reproducibility: keep the Opticka session
.mat, the EDF and the gamma table together for each monkey/session.
All original code herein is released under the GNU General Public License v3 (see LICENSE). The dependencies (Opticka, PTB, Titta, FieldTrip, Palamedes) retain their own licenses.
Data supporting the manuscript are publicly available at: https://doi.org/10.6084/m9.figshare.31802695
Questions or suggestions → open an Issue in this repository or contact the corresponding authors of the manuscript (Ian Max Andolina; Hong Liu).



