Skip to content

Draft: Chromostereopsis illusion (notes + stimulus prototype) - #24

Draft
DominiqueMakowski wants to merge 5 commits into
devfrom
feature/chromostereopsis
Draft

DominiqueMakowski wants to merge 5 commits into
devfrom
feature/chromostereopsis

Conversation

@DominiqueMakowski

Copy link
Copy Markdown
Member

Draft work towards a Chromostereopsis illusion. Nothing is exported — the submodule is not
imported from pyllusion/__init__.py, so the public API is unchanged. Targets dev so it stacks on
top of #23.

Chromostereopsis is the illusion that, on a flat image, red appears nearer than blue. It is
binocular: covering one eye largely abolishes it, and its strength depends on the viewer's optics
and the display. That is the central constraint here — unlike the size illusions, the image alone
cannot guarantee the effect.

The stimulus

Two square panels side by side, each a dithered coloured disc inside a dithered coloured surround
separated by a bare annulus, with the colour roles swapped between panels: a red disc on blue on one
side, a blue disc on red on the other. The panels are geometrically identical and use the same two
colours, so they differ only in which colour is figure and which is ground.

How it maps onto Pyllusion's two parameters

Chromostereopsis has no objective depth for difference to manipulate — a flat PNG has none. The
swapped-panel layout gets around this by moving the judged attribute from depth to size:

  • difference — the objective size difference between the two discs, defined exactly as in
    Delboeuf and Ebbinghaus (smaller disc stays at size, larger scales by sqrt(1 + abs(difference)),
    so it is an area difference). Verified to produce identical disc sizes to Delboeuf at every
    value tested.
  • illusion_strength — deferred by agreement. Only its sign is wired up, selecting which panel
    gets the color1 disc. What the magnitude should scale is still open.

⚠️ This rests on chromostereopsis actually biasing size, via size constancy. That is a hypothesis,
not an established finding, and it is flagged as such in the notes. If it does not hold, the design
falls back to a depth judgement with no ground truth.

Geometry

Matched to Delboeuf so the two can share a battery: size=0.25 and distance=1 give 75px discs with
centres 400px apart on the default 800x600 canvas, measured identical to pyllusion.Delboeuf(). Only
the discs are tied to Delboeuf; size_panel sizes the surround separately and by default fills each
half of the image, so the surround acts as the stimulus background.

Parameters exposed for study

The factors from the literature review are exposed as knobs so their effect can be measured:
density / density_inner / dither_size / dither_shared (patterning), luminance1 /
luminance2 / equiluminant (luminance), background (contrast), color1 / color2 (pairing),
size / size_panel / gap / distance (geometry), seed.

Luminance scales in linear light, not code values. The parameters dict also reports derived
quantities — per-colour luminance and contrast, areas, and each panel's predicted mean luminance,
computed analytically and agreeing with renders to within dither noise.

Things worth knowing

  • Filling the halves costs brightness balance. The surround becomes ~35x the disc area, making the
    red-surround half ~2.7x brighter than the blue-surround half. equiluminant=True removes this
    exactly at any panel size, and for large panels should be considered required rather than optional.
  • The panels touch at the midline, putting a hard red|blue edge down the centre. Border contrast
    is reported as able to reverse perceived depth, so it is not neutral; a slightly smaller
    size_panel inserts a gutter.
  • On black, Michelson contrast is 1 for every colour, so contrast can only be manipulated against
    a non-black background.
  • The literature summary in Chromostereopsis.py ends with a "Sources to verify" section: it was
    compiled from a review that has not been checked against primary sources, and one citation is
    attributed to two different first authors. Not for user-facing docs as-is.

Still open

The magnitude of illusion_strength; whether the size-bias assumption holds; whether to offer a
bar/grating variant to stay comparable with the literature (the published effect sizes used bars, not
discs). No Chromostereopsis class yet — the natural time to add one is once illusion_strength is
settled.

🤖 Generated with Claude Code

DominiqueMakowski and others added 5 commits September 19, 2026 11:04
Placeholder submodule holding a condensed review of chromostereopsis
(the illusion that red appears nearer than blue on a flat image), to
brainstorm the implementation from.

Nothing is exported and the submodule is not imported from the package
__init__, since the API is not settled. The notes end with the open
design questions: chromostereopsis has no objective depth attribute, so
how it maps onto Pyllusion's difference / illusion_strength pair needs
deciding before any code is written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two square panels side by side, each a dithered coloured disc inside a
dithered coloured surround with a bare annulus between them, and the
colour roles swapped between panels.

This suggests a way out of the "no objective depth attribute" problem
noted in the previous commit: move the judged attribute from depth to
size, so difference = objective size difference between the two discs
(as in Delboeuf/Ebbinghaus) and illusion_strength = the signed colour
assignment. Rests on chromostereopsis biasing size via size constancy,
which is a hypothesis and flagged as such.

Rendering it also turned up a luminance confound: with the default
geometry the red-surround panel is ~49% brighter than the blue-surround
one, since red is ~3x more luminous than blue in sRGB and the disc and
surround have different areas. Matching disc area to surround area
(radius 0.757 at gap 0.08) brings the panels to within dither noise.

Still a prototype, not exported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
difference is now the objective size difference between the two discs
(Delboeuf/Ebbinghaus semantics). illusion_strength stays deferred: only
its sign is wired up, selecting which panel gets the color1 disc.

Adds _chromostereopsis_parameters() following the package convention,
exposing the factors from the literature review as knobs so their effect
can be measured: density / density_inner / dither_size / dither_shared
for patterning, luminance1 / luminance2 / equiluminant for luminance,
background for contrast, color1 / color2 for the pairing, and
radius / gap / area_matched for geometry.

Luminance scaling is done in linear light rather than on code values, so
0.5 is really half the luminance. The dict also reports derived
quantities (per-colour luminance and contrast, areas, and the predicted
mean luminance of each panel), computed analytically so a condition's
brightness confound can be read off without rendering it; agrees with
renders to within dither sampling noise.

Two things worth recording: on a black background Michelson contrast is
1 for every colour, so a non-black background is needed to manipulate
contrast at all; and mid-grey #808080 is very nearly isoluminant with
pure red. Both noted in the docstrings.

Dither patterns are now shared between panels by default, leaving colour
assignment as the only difference between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sizes and distances now use Pyllusion's grid units instead of ad-hoc
panel-relative ones, with defaults matching Delboeuf: size=0.25 and
distance=1 give 75px discs with centres 400px apart on the default
800x600 canvas, verified identical to pyllusion.Delboeuf(). The disc
maps to Delboeuf's inner circle (the judged target), the square panel to
its outer circle (the context).

The "black outline" between square and circle was already there as
`gap`; it is now in grid units like everything else and documented as
such. Raising it grows the area-matched panel with it.

area_matched is replaced by size_panel=None, which computes the
area-matched panel side from a closed form (no more scipy solver), so
the brightness confound is off by default rather than opt-in.

Two fixes found while verifying against Delboeuf:

- The disc came out 73px instead of 75 because a panel-relative radius
  was discretised twice. The panel now takes its radius and gap in
  pixels, converted once from grid units the same way _coord_circle()
  does it.
- dither_size=10 suited the old 500px panels but is ~10% of a
  Delboeuf-matched 102px panel, which wrecks the disc. Default is now
  2px (~50 cells across, matching the proportions of the reference
  image), and Dither_Cells_Across is reported so the look can be held
  constant when rendering at other scales.

Noted in the notes: matching Delboeuf makes the stimulus small, and the
literature reports the effect as stronger for larger stimuli, so the
matched defaults may sit at the weak end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The discs stay pinned to Delboeuf's geometry; only the panels change.
size_panel now takes three forms: None (the default) fills each half,
its side equal to the centre-to-centre distance in pixels, so the
surround is the background of the stimulus rather than a patch floating
on black; "match" gives the area-matched size; a float is anything in
between. Resolving the automatic size needs the aspect ratio, since
Pyllusion sizes scale with height and positions with width, so the
parameters function now takes width/height and the image function
forwards its own.

Two consequences, both measured and documented rather than hidden:

- Filling the halves puts the surround at ~35x the disc area, so the
  red-surround half is 2.7x brighter than the blue-surround half (0.069
  vs 0.025) - worse than the imbalance the area match was introduced to
  fix. equiluminant=True removes it exactly at any panel size (measured
  ratio 0.99), because equal-luminance colours make the two panels hold
  the same coloured area just swapped. For large panels it is
  effectively required, not optional.
- The panels now touch at the midline, putting a hard red|blue edge down
  the centre. Border contrast is reported in the literature as able to
  reverse perceived depth, so this is not neutral; size_panel slightly
  below the automatic value inserts a gutter.

Also fixes two things found while verifying against Delboeuf:

- `difference` used a symmetric +/- split, but Delboeuf defines it on
  area (smaller disc stays at size, larger scales by sqrt(1+|d|)). The
  two now agree exactly at every difference, which is the whole point of
  matching. difference=0.3 gave a 1.35 diameter ratio, now 1.14 as in
  Delboeuf.
- The disc lost a pixel (74 instead of 75) on even-sized panels, because
  the centre fell between pixels. The disc centre is now placed on a
  pixel, giving 75px at every panel size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant