Repository navigation
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.
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.
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_cmis smaller than half height. Then Y = 0 is high on the display, and a negativefix_eye_yis 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].
| 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.
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.
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.
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”).
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).
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).
| 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.