Skip to content

Stimulus positioning and coordinate systems

Igor Kagan edited this page Aug 20, 2026 · 3 revisions

Stimulus positioning and coordinate systems

How monkeypsych places stimuli on the subject display, what the numbers in condition files mean, and how those numbers relate to get_monkey, get_setup, the online GUI, and ma1.

Related code: deg2pix_xy.m, pix2deg_xy.m, deg2pix_withOffset.m, cm2deg.m, aux_FillPar in monkeypsych.m.
Known conversion bugs: Issues.md C1–C5.


1. Three spaces

Stimulus positions and behavioral windows are specified in task space. Mapping onto the hardware uses two further spaces.

Space Unit What it is Axes
Task deg visual angle Positions in task.eye.* / task.hnd.*, condition files, saved trial, online GUI, ma1 +X right, −X left, +Y up, −Y down
Subject display cm The monitor the animal sees. Physical width/height, and the location of task (0,0) on that monitor (horizontal midpoint, screen_uh_cm down from the top edge) top / bottom / left / right of the subject display
PTB pixels Drawing coordinates for Screen('Draw*') origin top-left; +pixel-Y is down the subject display

GUI and ma1 plot task space.

PTB’s downward pixel axis is undone in deg2pix_xy (pixels_y = center_y − n_pixels_y), so +deg Y still goes toward the top of the subject display.


2. Where (0,0) deg is: task.screen_uh_cm

Task origin is straight-ahead (cyclopean gaze at rest), not the geometric center of the subject display.

task.screen_uh_cm (upper half) is centimeters from the top edge of the subject display down to that origin. It does not shift individual stimuli relative to each other. It places Y = 0 on the display.

top of subject display
|------------------|  +Y  (uh_cm above origin)
|                  |
|*****  Y = 0 *****|  ← task.screen_uh_cm below the top edge
|                  |
|                  |  −Y
|------------------|
bottom                 (screen_h_cm − uh_cm below origin)
  • If the animal looks at the middle of the display, screen_uh_cm ≈ screen_h_cm / 2.
  • If the head is aimed at the upper part of a tall touchscreen (reach space below), screen_uh_cm is smaller than half height. Then Y = 0 is high on the display, and a negative fix_eye_y is still on the upper part of the panel — it is simply below straight-ahead.

Horizontal origin is always the left–right midpoint (screen_w_cm / 2). There is no screen_uh analog for X.

Copied into SETTINGS.screen_uh_cm at startup. Pixel of the origin:

center_x = screen_w_pix / 2
center_y = screen_h_pix * screen_uh_cm / screen_h_cm   % from the top, PTB

Visible range in task deg (GUI ylim), using atan(cm/vd):

screen_uh_deg = atan(uh_cm / vd)              % above origin
screen_lh_deg = atan((h_cm − uh_cm) / vd)     % below origin

GUI: Ylim = [−screen_lh_deg, +screen_uh_deg].


3. Who sets what

Quantity Set in Meaning
SETTINGS.screen_w_cm, screen_h_cm get_setup (per setup) Subject-display size
SETTINGS.screen_w_pix, screen_h_pix get_setup Resolution
task.vd get_monkey (per subject, often per setup) Eye-to-display distance (cm)
task.screen_uh_cm get_monkey How far below the top edge task (0,0) is placed
fix_*, ecc, angles, offset_con, var_* condition file Stimulus positions in task deg

get_setup never sets vd or screen_uh_cm. Setup = display size; monkey = origin + viewing distance; condition file = positions relative to that origin.

Changing screen_uh_cm slides the entire deg map up/down the subject display without changing condition-file numbers.


4. How a stimulus position is assembled (condition file)

All of this is already task deg. Typical combined_condition_file_*.m / Feno.m:

home:     fix_eye_x, fix_eye_y, fix_hnd_x, fix_hnd_y
+ polar:  eccentricity × angle  →  tar_dis_x, tar_dis_y
+ jitter: Current_con.var_x, var_y
+ extra:  offset_con  (optional extra home shift; often a no-op if the body is commented out)
        →  task.eye.fix / task.eye.tar(k) / task.hnd.*

aux_FillPar copies x,y,size,radius to par.deg.* (behavior, GUI) and converts to par.pix.* for PTB.


5. Deg ↔ cm ↔ pix

see also https://www.sr-research.com/eye-tracking-blog/background/visual-angle/

Positions (drawn stimuli) — deg2pix_xy:

cm = vd · tan(θ)
pix_x = center_x + cm_x · (w_pix / w_cm)
pix_y = center_y − cm_y · (h_pix / h_cm)

True inverse of that map: θ = atan(cm / vd).

Touch / ViewPoint-cal points — pix2deg_xy (and cm2deg):

θ = 2 · atan((cm/2) / vd)

That is the visual angle subtended by a centered object of width cm, not the angle of a point at offset cm. It is not the inverse of deg2pix_xy. Round trip θ → pix → θ' gives θ' = 2·atan(tan(θ)/2) > θ (worse in the periphery).

The inverse of pix2deg_xy is still sitting commented out in deg2pix_xy (2·vd·tan(θ/2)). Draw was switched to vd·tan; pix2deg_xy was left on the old chord formula.

Mouse-as-eye (aux_GetCalibratedEyePos) already uses atan(cm/vd) and therefore matches drawn positions. Touch does not.

Drawn size — deg2pix_withOffset(size_deg, offset_deg):

cm = (vd/2) · (tan(o+d) − tan(o−d))

o is eccentricity of the stimulus from task origin (atan(hypot(tan x, tan y))), not from fixation if FP is shifted. Same size deg → more pixels at larger |o|. Acquisition windows use par.deg.radius (deg), not this pixel size.

aux_FillPar then feeds already-pixel stim(3) back into deg2pix_withOffset for radius (Issues C3). Drawn window ≠ intended deg.


6. Other quantities named “offset”

These are not screen_uh_cm and they do not move drawn stimuli.

Name Where Space Role
task.eye.offset_x/y, gain_* F1 / SETTINGS.eyecal (last_eyecal.mat) Affine on raw eye y = gain*raw + offset. Puts the eye trace into task deg. Targets stay put.
SETTINGS.automatic_offset_update get_setup same Optional nudge of that eye cal during FIX_HOL.
touch x/y_gain, x/y_offset get_setup voltage → pixels, then pix2deg_xy Puts the finger into task deg.
SETTINGS.GUI_coordinates get_setup Windows desktop pixels Where the figure window sits.
offset_con condition file task deg Extra FP / array home shift, if the script actually applies it.

Eye cal and touch cal are input maps into task space. Stimulus positions are already in task space. They meet only in the window test (“did eye/hand land on the target”).


7. Worked example: Feno, setup 1

From get_setup case 1 and get_monkey case 'Feno':

Subject display 59.5 × 33 cm, 1920×1080
task.vd 29 cm
task.screen_uh_cm 7 cm (origin 7 cm from the top, not 16.5)

Range above origin ≈ atan(7/29) ≈ +13.6°. Range below ≈ atan(26/29) ≈ −41.9°. Geometric mid-display is ≈ −18° in this frame.

Condition file (Feno.m):

fix_eye_y = -5;    % 5° below straight-ahead
fix_hnd_y = -14;
fix_eye_x = 12;    % also 12° right of midline (hardcoded)
fix_hnd_x = 12;

On the subject display, Y = −5 is ~2.5 cm below the high origin (~9.5 cm from the top of a 33 cm panel) — still the upper part of the display. GUI/ma1 show −5 because that is the task coordinate.

offset_con in current Feno is a no-op (the if body is commented out).


8. Shifted arrays (equal ±ecc around a shifted FP)

deg2pix_xy uses tan on absolute screen-centered deg. Equal ±E around a shifted FP ⇒ unequal cm on the subject display, equal visual angle from the FP if the head is at the display center. That is the intended tan model (Issues C1), not a conversion bug.

deg2pix_withOffset then sizes by eccentricity from task origin, so the outer choice target is drawn larger than the inner one for the same size deg (Issues C4).


9. Pointers

File Role
get_setup.m Display cm/pix, touch gain/offset, GUI window position
get_monkey.m vd, screen_uh_cm, path to condition file
condition *.m fix_*, ecc/angles, offset_con, var_*
deg2pix_xy.m Task deg → PTB pix (positions)
pix2deg_xy.m Touch / VP-cal pix → task deg (not inverse of the above)
deg2pix_withOffset.m Task deg size → pix, eccentricity-corrected
aux_FillPar Deg copy for behavior + pix copy for drawing
aux_IsWithinRadius Uses par.deg.radius
SETTINGS.eyecal Eye offset/gain .mat

Older stub: Converting screen coordinates to visual angle.

Clone this wiki locally