Skip to content

Processing at a glance

Every step from a recording folder to the exported ΔPD, in the order the code runs it. For each step: what it does, the parameters and defaults that control it, and the function that does it. The linked pages give the reasoning and the equations. The defaults are those of Parameters() and VideoSettings() in cwtool/params.py; all parameters are listed in Parameters.

flowchart LR
    A[1 Read] --> B[2 Video pass]
    B --> C[3 Luminance]
    A --> D[4 Pupil signal]
    C --> E[5 Expected pupil]
    D --> F[6 Scale and offset]
    E --> F
    F --> G[7 ΔPD]
    G --> H[8 Statistics, events, export]
    B -.-> K[9 Calibration fit]
    D -.-> K
    E -.-> K

Steps 1 and 2 run once per recording (the video pass is cached). Steps 3–8 are the signal pass, pipeline.run, which re-runs on every parameter change. Step 9 runs on demand and writes its results into the parameters.

1. Read the recording

devices.load(folder, device=None, **options) → Recording (Devices)

Detection each reader's detect(folder) in turn: Varjo, Pupil Core, Pupil Neon, Tobii Pro Glasses 3
Pupil per eye, in device units; NaN where invalid
Validity Varjo: tracking status 1 or lower (overall or either eye); Core: confidence below 0.6; Neon: not worn, and 0.1 s around Cloud blinks; Glasses 3: empty samples. Zero or negative diameters are invalid on every device
Grid irregular samples are averaged into bins at the measured rate (common.bin_samples, common.sample_rate)
Time base seconds from the first scene video frame (can start below 0); epoch_start is its Unix time
Gaze normalised scene video coordinates, origin top left
Lux the lux logs in the recording, its lux subfolder or the logger folder (--logs/Logger folder…, --lux) (lux.find_lux_folder, lux.read_lux), moved onto the recording clock with epoch_start
Events event_log CSV (common.read_event_log) plus Neon and Glasses 3 recording events (common.events_from_markers)
Profile pupil unit and scale, luminance source (display or lux_sensor), camera field of view, circular scene, native rate, adapting field, experimental

Neon and Glasses 3 readers are experimental: every result from them carries a warning (step 8).

2. Scene video pass

video.analyse_video(...) → VideoResult, cached as cwtool_video.csv + .json (Scene video analysis)

Step Settings Code
Match each valid gaze sample to its frame: nearest recorded timestamp (Core, Neon) or constant frame rate (Varjo, Glasses 3) – FrameClock
Decode (PyAV, else OpenCV) in parallel chunks of at least 150 frames; only frames with gaze are converted --workers (default one per core) _frames_pyav, _frames_opencv
Downscale with nearest-neighbour sampling, convert to RGB analysis_width 500 px prepare_frame
Gaze circle: disc of the given radius at the gaze point fixation_radius_deg 16.35° (radius = degrees / vertical field of view × height) radii, analyse_frame
Background: whole frame, or a centred circle on circular (Varjo) videos; minus the gaze circle field_radius 0.8, background_excludes_fixation on field_mask
Per area and channel: 256-bin histogram → mean code value and mean of \((C/255)^\gamma\) for γ = 1.4, 1.6 … 3.0 (gaze circle, background, whole visible scene) – _histograms, _means
Cache, reused only if format, γ grid, settings, video name, frame clock and gaze all match – VideoResult.save, load_cached

3. Luminance \(L(t)\), cd/m²

pipeline.prepare (Luminance)

3a. Linear colour at the current gamma. The stored linear means are interpolated to gamma (default 2.2) in log space (video.at_gamma), and the two areas weighted: \(\mathbf{C}_w = w\,\mathbf{C}_\text{gaze} + (1-w)\,\mathbf{C}_\text{background}\), fixation_weight \(w\) = 0.26 (VideoResult.weighted).

3b. One of four methods, chosen from the device and the data (reported as Result.luminance_mode):

Mode When Luminance Parameters Code
display display devices (Varjo) \(\sum_c k_c\,(L_\text{max}\,\tfrac{g_c}{\bar g}\,C_{w,c} + L_\text{min}(1-C_{w,c}))\), \(k\) = BT.709 weights l_min 0.01, l_max 100 cd/m², gamma 2.2 (display photometry file); gain_r/g/b 1 (participant) scene_luminance, luminance.absolute_luminance
lux sensor glasses with a lux log \(\bar L \cdot Y_w / Y_\text{frame}\), the ratio kept between 1/limit and limit, \(\bar L = (g\,E + o)/\Omega\) from the smoothed lux \(E\) (Savitzky–Golay 11, order 6) lux_gain 1, lux_offset 0, lux_solid_angle 2.2, lux_use_video on (off: \(\bar L\) alone), lux_ratio_limit 10 luminance_from_lux, relative_luminances, lux.smooth, lux.average_luminance
camera, fixed exposure glasses without a lux log, camera_exposure = fixed \(L_\text{white}\,\tfrac{t_\text{ref}}{t}\,Y_w\); warning if the gaze area is saturated in more than 5 % of samples camera_white 1000 cd/m², camera_reference_ms 0, camera_exposure_ms 0 relative_luminances, Parameters.camera_full_scale, clipped_fraction
camera, relative glasses without a lux log, camera_exposure = auto as display, with a warning that it is only relative l_min, l_max, gamma scene_luminance

\(Y\) is the relative luminance with the BT.709 weights and the channel balance \(g_c/\bar g\).

3c. Time. Video and lux luminance are interpolated onto the analysis grid (step 4), with their timestamps shifted by timelag (0 s).

3d. Coverage. Where the luminance is only interpolated or held, it is unknown, and those samples are marked invalid like long pupil gaps (left out of ΔPD, the offset and the statistics): outside the lux log or between lux readings more than 1 s apart (LUX_MAX_GAP), and, whenever the video enters the luminance, between analysed video samples more than max_gap apart (gaze lost or outside the scene video). A note gives the share when it exceeds 1 %. The expected pupil is still computed through such stretches, so its filters run on.

4. Pupil signal

pipeline.prepare (Pupil signal)

# Step Parameters Code
4.1 Scale to mm: device units × profile pupil_scale × pupil_correction. Pixel data have no scale yet (step 6) pupil_correction 1 prepare
4.2 Range: diameters outside 1–9 mm removed, per eye (skipped for pixel data) PUPIL_RANGE_MM prepare
4.3 Artefacts: speed above the limit towards a sample at least 40 ms away, widened by the padding, per eye (skipped for pixel data) max_pupil_speed 10 mm/s (0 = off), artefact_padding 0.05 s artefacts
4.4 Combine the eyes; both averages, bridging a missing eye with the other plus their median difference eye both combine_eyes
4.5 Resample onto a uniform grid by linear interpolation; gaps longer than max_gap marked invalid analysis_rate 100 Hz (0 = native), max_gap 0.5 s resample
4.6 Smooth: Savitzky–Golay order 2 over about 0.5 s (measured) and order 6 over about 0.25 s, at least 9 samples (measured raw, used by the calibration fits); both NaN in long gaps – prepare

5. Expected pupil

pipeline.expected_pupil (Expected pupil model)

# Step Parameters Code
5.1 Light sensitivity: \(s \cdot L\) sensitivity 1 steady_pupil
5.2 Watson & Yellott: flux = \(sL\) × adapting field area (profile, deg²) × 1 (two eyes) or 0.1 (one); Stanley & Davies diameter; age correction against 28.58 years age 25, eyes 2 model.watson_yellott
5.3 Latency: shift right, padding with the first value; a delay between samples is interpolated linearly delay 0.5 s model.delay
5.4 Dynamics switch: 5.5–5.6 only when on dynamics on dynamic_pupil
5.5 Attack/release filter: one pole, τ = attack while the expected diameter rises, release while it falls; with 2 stages a second release stage acts during constriction only (S-shaped onset) attack 6 s, release 0.5 s, constriction_stages 2 model.attack_release
5.6 Transient (pupillary escape): minus transient × \(h/(h+0.2)\), \(h\) = rise of log₁₀ L over its low-pass with τ = escape; delayed and passed through one low-pass (τ = release) per constriction stage transient 0 mm (off), escape 2 s model.escape_transient, model.lowpass

6. Pupil scale and offset

pipeline.run, pipeline.alignment_offset (ΔPD and alignment)

Step Parameters
Pixel data: scale so that mean measured = mean expected, then × pupil_correction pupil_correction 1
Offset added to the measured pupil: recording median(expected − measured) over valid samples; baseline the same over the named events (whole recording, with a warning, if none); fixed pupil_offset; none 0 alignment recording, baseline_events "Riposo, Rest, Baseline", pupil_offset 0 mm
Plausibility: warning if the median measured pupil, after the offset, is outside 2–8 mm –

7. ΔPD

pipeline.run, pipeline.windowed_difference

  1. Measured − expected, averaged over consecutive windows of cw_window (0.2 s), ignoring invalid samples; windows without any stay NaN.
  2. Smoothed with a Savitzky–Golay filter of order 1 over cw_smoothing (1) windows on each side; gaps stay NaN.

8. Statistics, events, export

Output Definition Code
ΔPD RMS root mean square of ΔPD about zero residual_rms
ΔPD SD standard deviation of ΔPD; ΔPD / SD is exported as the normalised ΔPD run
Gap fraction share of the grid left out: pupil gaps longer than max_gap and unknown luminance (3d) run
Light left in ΔPD slope of ΔPD on log₁₀ luminance (mm per tenfold luminance) and R² of ΔPD on log₁₀ luminance and its recent change (its excess over a 2 s low-pass), per ΔPD window, leaving out the windows within 0.5 s of a luminance step (a change of more than 0.1 in log₁₀ luminance within 0.1 s); a note above R² 0.1. Near 0 when the model removes the light; NaN when the luminance barely varies or too few windows are left light_leakage
Black and white point expected pupil at l_min and l_max (display and relative camera modes only) run
Warnings experimental reader, missing lux data, saturation, baseline events not found, implausible pupil prepare, run
Event means mean ΔPD (mm and SD units) inside each event event_means
Files <name>_pupil.csv, <name>_cw.csv, <name>_events.csv, <name>_params.json (+ <name>_plot.pdf; with sensor data also the sensor files and, for a Shimmer's ECG, <name>_<sensor>_hrv.csv and _beats.csv, see Heart rate variability) export, plot.plot_result

See Output files for the columns.

9. Participant calibration (Varjo)

Run on the participant's calibration sequence, in this order (Calibration fit, Participant calibration). The display photometry is taken as given throughout.

# Step Details Code
9.1 Locate the sequence change points (> 8 code values, ≥ 0.3 s apart) in the gaze circle colour; candidate starts scored by the relative RMS colour error in linear light with one gain per channel, skipping 0.5 s after each change; step length rescaled if the recording's steps differ; at least half of the window must have analysed video. The app rejects matches with an error above 35 % calibration.locate
9.2 Light response level per step = mean of the measured raw pupil over its last 30 % (after delay + 0.1 s), colour = mean weighted linear colour after 0.5 s; weighted least squares of measured ≈ WY(s · L(gains, γ, Lmin)) (+ d) over log s, log gains (optionally γ, log Lmin with Lmax held, and d), weak priors on gains (SD 1.5 in log) and γ (0.2), none on s; five starting sensitivities; the pupil scale is not fitted. Sets sensitivity, gain_r/g/b (mean 1), with the options gamma, l_min, pupil_offset −d photometry.fit_light_response
9.3 Latency median constriction onset at brightening steps (20–50 % line back to the pre-change level; constrictions ≥ 0.3 mm; onsets < 0.1 s excluded; at least 3), minus the onset bias of the model's constriction shape for each candidate τ. Otherwise fitted fit.onset_latency, fit.latency_from_onset
9.4 Dynamics, offset Nelder–Mead over log attack (0.3–30 s), log release (0.05–5 s) and optionally (the app: whenever it fits the dynamics) log transient (0.01–3 mm) and log escape (0.3–30 s); a transient at its lower limit is set to 0; the window has 30 s of pre-roll; the pupil scale and, by default, the offset are not fitted (with fit_offset the offset is the mean of expected − measured, and alignment = fixed is set) fit.fit_calibration

Glasses without a calibration sequence can still use a camera calibration: pipeline.calibrate_camera sets camera_white from a fixed-exposure recording with a lux log (median of \(\bar L / Y_\text{frame}\) over samples that are neither black nor saturated; a warning above ×2 spread).