NTH / M4 · Interactive companion

M4 · Phosphene simulator

A live in-browser playground for the same equations as dynaphos. Move the sliders, watch the basis change. Use this to build intuition before (or alongside) the notebook.

Module: M4 Pair with: phosphene-simulation.ipynb · Colab · VS Code
M1
Camera + CV
M2
Gaze
M3
Stimulation
M4 · here
Phosphenes
M5
Decoding

01How to simulate what the users see

A single electrode evokes a Gaussian-shaped bright spot. Its position is set by the retinotopic map; its size is set by current and local cortical magnification.

Example phosphene simulation. Real scene → edge map → phosphene field, frame by frame. From van der Grinten et al. 2024, eLife (Video 2, CC BY).
Equations — van der Grinten et al. (2024), eLife.click to expand

Each phosphene is a 2D Gaussian on the visual field; its size is set by the diameter of activated cortex and by cortical magnification:

\[ G_i(x, y) \;=\; \exp\!\left(-\,\frac{(x - x_i)^{2} + (y - y_i)^{2}}{2\,\sigma_i^{\,2}}\right) \] \[ M(r) \;=\; \frac{k\,(b - a)}{(r + a)\,(r + b)} \qquad\text{\small (Eq.\ 4)} \] \[ D(I) \;=\; 2\,\sqrt{\dfrac{I}{K}} \qquad\text{\small (Eq.\ 6)} \] \[ P \;=\; \dfrac{D}{M} \qquad\text{\small (Eq.\ 5)} \]

\((x_i,\,y_i)\) is the retinotopic centre of electrode \(i\); \(M(r)\) is the Polimeni–Schwartz dipole magnification (mm cortex per visual degree) with \(k = 17.3\), \(a = 0.75\), \(b = 120\); \(D(I)\) is the diameter (mm) of activated cortex for stimulation current \(I\) (µA) with excitability constant \(K = 675\ \mu\mathrm{A}\,\mathrm{mm}^{-2}\); \(P\) is the phosphene size in degrees. The Gaussian standard deviation used in the canvas below is \(\sigma_i = \mathrm{radius\_to\_sigma}\,\cdot\,(D/2)\,/\,M(r_i)\), with radius_to_sigma = 0.5 in config/params.yaml.

Single electrode

sigma = - deg | M = -
Visual field, 16 deg wide (dynaphos default). Fovea = center.
Slide eccentricity from 0.5 to 7.5. What happens to the phosphene?

It grows. Cortical magnification M(r) drops with eccentricity, and sigma_deg = radius_to_sigma * sqrt(I/K) / M(r). Smaller M -> bigger apparent size for the same current. This is why peripheral phosphenes appear large and blob-like, while foveal ones are crisp dots.

Halve K. Does the phosphene get bigger or smaller? Why?

Bigger. K is the current-spread constant (uA/mm^2). A smaller K means the same current spreads over a larger cortical area, activating more tissue -> larger phosphene radius -> larger sigma.

02Phosphene population

Real prostheses use hundreds to thousands of electrodes. The placement strategy determines what kinds of percepts are achievable.

Population

n_visible = -
All bases, max-projected
Electrode centers only
Switch from foveated to uniform. Which gives finer central detail and which finer peripheral detail?

Foveated provides finer central details and uniform provides finer peripheral ones. With the same N electrodes, foveated placement crowds them near the fovea, where sigma is also smaller, producing crisper central vision. Uniform placement gives the periphery the same density as the fovea but each phosphene is small in the centre and large in the periphery.

02.1Cortex to visual field

Before placing an implant on a real cortical surface in vimplant2, build the intuition here. Drag a Utah-array patch around the V1 map (left) and watch its phosphenes land in the visual field (right). The pink grid is the actual dynaphos dipole (Polimeni 2006 / Schira 2010 / dynaphos.cortex_models with model='dipole'): each solid iso-eccentricity curve on cortex maps to a circle in the visual field, each dashed iso-polar curve maps to a ray. Small cortical shifts near the occipital pole sweep huge distances in the periphery, and phosphenes grow with eccentricity because cortical magnification drops.

Cortical implant

centre ecc = -
V1 patch (mm cortex) — curved iso-eccentricity (solid) + iso-polar (dashed). Polimeni dipole, k=17.3 mm, a=0.75°, b=120°.
Visual field, 16° wide. Concentric circles = iso-eccentricity; rays = iso-polar; foveal cross at centre.
Drag the implant from x = 2 mm to x = 28 mm. How do the phosphenes move and grow?

They sweep outward (small steps in cortex = huge eccentricity changes in the periphery), and they grow (cortical magnification drops, so each electrode covers more degrees of visual angle). This is the engineering case for foveated electrode placement: phosphenes are cheap and large in the periphery, scarce and small in the fovea.

03Image to phosphenes

The forward pass: sample image intensity at each electrode's location, convert to a stimulation current, render the canvas.

Forward pass

- electrodes active
Target image
 
Phosphene render
Cycle through the target images. Which ones can you still recognize, and which fall apart?

The abstract shapes (square + disc, letter "E", grating bars, diagonal line) stay legible — their information lives in a few large, high-contrast regions that survive coarse sampling by the electrode lattice. The natural photographs (kitten, scene) do not: most of the recognisable signal in a real image lives in fine edges and texture that fall below the inter-electrode spacing, so they alias into a blobby mess that no longer reads as the original object.

On the kitten photo, what is the one characteristic you can still recognize?

The overall silhouette — a roughly round bright blob with two triangular ear notches against a darker background. Individual features (eyes, whiskers, fur texture, fur boundaries) are essentially gone: they sit inside that blob at spatial scales finer than the lattice can resolve. The same point generalises to faces: a pure intensity-based forward pass is a poor match whenever identity is carried by the geometry of internal features rather than by the outer silhouette. To carry that information you would need a representation that first extracts feature locations (eyes, mouth, ear tips) and renders those explicitly, instead of sampling raw pixel brightness.

On the scene photo, can you recognize the shapes of the buildings? If not, why?

Not really. The Canny preset gives you the edge map of the scene, which is the right kind of information — building outlines, rooflines, window frames — but once it is resampled onto the electrode lattice the edges come out as disconnected dots rather than continuous contours. There is no signal that says "these two phosphenes belong to the same line"; the visual system has to guess. For sparse arrays this guess fails on buildings because their identifying features are exactly long straight edges, which the lattice fragments into ambiguous point clouds. Denser arrays (push N electrodes up) help, but the underlying problem — no perceptual grouping cue between phosphenes — remains.

04Temporal dynamics

How does the appearance of phosphenes change as time goes on? In this section we will see two phenomena at play.

The first is build-up and decay: when an electrode turns on, brightness does not jump straight to its final value — the cell needs a few frames to integrate the incoming charge, and after stim stops the activation needs time to leak back down to zero. With the dynaphos defaults this build-up is essentially instantaneous, but the activation_decay/s slider lets you slow it down so the ramp becomes visible.

The second is adaptation. Even when the stim current is held constant, the cell gets tired. A slow internal trace state accumulates inside it and raises the effective threshold the stimulation has to clear, so the same current delivers less and less charge per second — and the percept fades within about a second even though nothing about the stimulation has changed. This is the dominant effect at the default settings, and the reason real phosphenes do not stay steady-on under sustained stim.

The lab below lets you turn each phenomenon on and off (slow down activation, disable the trace) and watch the brightness curve respond; the math behind both is in the panel just below.

Equations — van der Grinten et al. (2024), eLife 13:e85812.click to expand

Each electrode is governed by an effective-current law that feeds two leaky-integrator states (activation and trace), and a sigmoid that turns activation into perceived brightness:

\[ I_{\mathrm{eff}} \;=\; \max\!\Big(0,\; (I_{\mathrm{stim}} - I_0 - B)\,\cdot\,P_w\,\cdot\,f\Big) \qquad\text{\small (Eq.\ 7)} \] \[ \Delta A \;=\; \left(-\,\frac{A_{t-\Delta t}}{\tau_{\!A}} \;+\; I_{\mathrm{eff}}\,\cdot\,d\right)\Delta t \qquad\text{\small (Eq.\ 9)} \] \[ \Delta B \;=\; \left(-\,\frac{B_{t-\Delta t}}{\tau_{B}} \;+\; I_{\mathrm{eff}}\,\cdot\,\kappa\right)\Delta t \qquad\text{\small (Eq.\ 13)} \] \[ \mathrm{brightness} \;=\; \begin{cases} \dfrac{1}{1 + e^{-\lambda\,(A - A_{50})}}, & A > A_{\mathrm{th}} \\[4pt] 0, & A \le A_{\mathrm{th}} \end{cases} \qquad\text{\small (Eq.\ 10)} \]

What you set: \(I_{\mathrm{stim}}\) is the stimulation current (the slider). \(P_w\) and \(f\) are the pulse width and frequency of the pulse train (170 µs, 300 Hz).
What the cell does: \(I_0\) is the rheobase (23.9 µA) — the minimum current that does anything. \(B\) is the trace: a slowly-accumulating "fatigue" term that adds onto \(I_0\), so the longer you stimulate, the more current you need to get the same effect. \(\kappa\) controls how fast \(B\) builds up; \(\tau_A\) and \(\tau_B\) are how fast activation and trace each fade back to zero. With the defaults, activation is essentially instantaneous (\(\tau_A \approx 0.1\,\mathrm{s}\)), while the trace builds over ~1 s and that is what causes the percept to fade during sustained stim.
What you see: brightness is a sigmoid of the activation \(A\), with a hard cut-off below \(A_{\mathrm{th}}\) (no percept at all). \(A_{50}\) sets the half-max point, \(\lambda\) controls how sharp the sigmoid is.

Time

brightness = -
Mean canvas brightness over time. Shaded = stim ON.
Set Stim ON until frame to its maximum. The implant is now stimulating during the entire window. Why does the brightness still drop? What does the dashed yellow line (the trace) have to do with it? What happens if you switch to disable trace?

The fade is not the stim turning off — it is adaptation. The dashed yellow line plotted on top of the brightness curve is the mean trace state \(B\), and you can see it rising as soon as stimulation starts. The trace adds onto the rheobase in the leak term, so the effective amplitude (I − rheobase − trace) shrinks as the trace grows. Charge per second drops, activation drops, brightness fades — even though \(I_{\mathrm{stim}}\) has not moved. The fall in brightness mirrors the rise in the dashed line.

Switching to disable trace zeroes \(B\) out of the equations entirely. Now the leak is just the rheobase, the effective amplitude stays constant, and the brightness saturates and stays there for the whole stim window — a flat plateau instead of a curve that bends down. That flat behaviour is what you would see in an idealised, non-adapting cortex; the trace is the one ingredient that turns the simulator's output into something that matches what real cortical implants do.

In the same setup, what does increasing the activation_decay/s slider do to the brightness curve?

It postpones and smooths both the rise and the fall. At the yaml default the activation integrator is essentially instantaneous: brightness jumps to its peak in one frame when stim starts, and drops back in one frame when stim ends. Raising the slider keeps a larger fraction of \(A\) alive from frame to frame, so the cell takes more frames to build up to its peak and more frames to drain back to zero. The corners of the curve become rounded ramps; the trace-driven fade in the middle is still there, but now layered on top of a softer rise and a gentler tail.

Why is there a hard cut-off in the brightness curve — a region where the trace is non-zero but nothing is rendered?

Because dynaphos puts a hard activation threshold in front of the sigmoid (Eq. 10): brightness is only emitted when \(A > A_{\mathrm{th}}\) (yaml: \(9.14\times10^{-8}\)). Below that cut-off, the simulator returns exactly zero — no phosphene at all, regardless of how small a positive activation the equations would otherwise yield. This matches what is observed in human implant data: there is a minimum charge below which the percept simply does not appear; you do not get a "very faint" phosphene that fades smoothly to nothing.

Set Stim ON until frame ≈ 60, activation_decay/s = 3e-1, and stim current = 32 µA. Can you identify the part of the plot where the brightness drop is caused by adaptation, and the part where it is purely activation decay? (Hint: think about what role the Stim ON until frame setting plays.)

The Stim ON until frame slider is the divider line. Up to frame ~60 the implant is delivering current; after that, \(I_{\mathrm{stim}} = 0\) and the cell is just relaxing on its own. That gives you two qualitatively different regions in the plot.

Frames ~0–60 (stim ON, after the initial rise) — adaptation. With \(\tau_A \approx 0.83\,\mathrm{s}\) the activation now needs many frames to climb, so you see a soft ramp up. Then the brightness peaks and starts curving back down even though the current has not moved. The dashed yellow trace line is climbing in parallel: it adds to the leak, the effective amplitude \((I_{\mathrm{stim}} - I_0 - B)\) shrinks, charge per second drops, activation drops. This is the trace-driven adaptation regime — same input, falling output.

Frames ~60 onwards (stim OFF) — pure activation decay. The moment stim turns off, \(I_{\mathrm{stim}} = 0\) so \(I_{\mathrm{eff}} = 0\) and no new charge is injected. The trace is irrelevant here — it cannot eat into a current that is already zero. Activation simply leaks away at rate \(1/\tau_A\), and the brightness falls along an exponential tail purely set by the activation_decay slider. The dashed trace line is still high (it decays on \(\tau_B\), thousands of seconds), but it has no effect on the brightness any more — the curve is owned entirely by \(A\)'s leak.

So the trick is: anything that bends downward while stim is still on is the trace; anything that falls after the stim-ON boundary is the activation integrator emptying out.

05Your mouse is the user’s gaze

In a real cortical prosthesis, what the wearer perceives depends on where they’re looking — the camera crops around their fovea, the front-end picks a stimulus encoding (intensity, edges, segmented objects), and the electrodes light up under that gaze. This panel makes you the gaze. Move the cursor over the scene; whatever falls inside your foveal window (the brush radius) is encoded and rendered as phosphenes in real time. The encoding mode is the engineering decision M1 (vision) and M3 (stimulation) made upstream.

mode
foveal window (scroll to change)
source image
actions
source (rgb)
 
gaze trace (sampled by the front-end)
phosphenes the user sees
How to drive the gaze
  • Left-click / drag — look at a region (the front-end stamps it into the gaze trace)
  • Right-click / drag — look away (un-stamp; the trace cools down)
  • Scroll wheel — widen or narrow the foveal window
  • Ctrl+Z — undo a gaze pass  ·  Ctrl+U — redo
  • in-fill: front-end sends raw intensity under the gaze. Cheapest pipeline, weakest semantics — phosphenes light up wherever the gaze passes.
  • edges (DoG): front-end runs Difference-of-Gaussians on the source and sends only the edge pixels under the gaze. Outlines instead of blobs.
  • edges (Canny): same idea with the Canny detector — thinner, more selective edges than DoG.
  • segments: front-end recognises whole objects (SAM-style region grow on chroma-weighted colour distance). Click an object inside the gaze window and the prosthesis lights up the entire object — one fixation, one identified thing. (Left-click adds, right-click subtracts. Cleanest result on the default neoclassical-facade image: the coral wall, the mint door, and the mint shutters.)
encoding = in-fill | foveal window = 14 px | gaze coverage = 0.0%

06Self-check

Try to answer each one from memory before opening it.

Q1. Two electrodes are stimulated with exactly the same current. One sits near the fovea, the other far in the periphery. Which one produces the larger, blobbier phosphene, and what is the reason?

The peripheral one. Cortical magnification shrinks as eccentricity grows — the cortex devotes far more surface area per visual degree to the centre of the visual field than to the edges. The same disc of activated tissue therefore covers a wider patch of visual angle in the periphery, so the phosphene comes out bigger and less crisp. Near the fovea, magnification is large, and the same current paints a small, sharp dot.

Q2. You are given a fixed budget of electrodes and asked to design a layout that gives the user the best chance of resolving fine detail at the point they are looking at. Should you spread the electrodes evenly across the visual field, or concentrate them somewhere?

Concentrate them around the fovea (the "foveated" layout in Section 02). Two things line up there: (1) cortical magnification is largest, so each phosphene is naturally smallest and sharpest, and (2) that is where the user actually looks when they want to read or identify something. Spreading electrodes evenly across the field wastes budget on the periphery, where phosphenes are big and blurry anyway and where fine detail is rarely needed.

Q3. When you cycle through the targets in Section 03, the abstract shapes (square, letter, grating) stay recognizable but real photos (the kitten, the scene) turn into a vague blobby mess. Why does the prosthesis handle abstract test images so much better than natural ones?

Because the information that identifies a natural image lives in fine edges and textures — spatial frequencies above what the electrode lattice can carry. By the time the image is resampled onto a few hundred phosphenes, those high-frequency cues are gone. The abstract targets are different: their identity lives in a few big, high-contrast regions (a solid block, a bold letter stroke, a coarse bar pattern). Coarse sampling preserves that just fine, so the shape still reads. Phosphene arrays are a low-resolution carrier; natural images carry their meaning above that carrier's bandwidth.

Q4. In Section 04 you saw that even when the stim current stays perfectly constant, the percept fades within about a second. Which of the two internal states inside each electrode is responsible for the fade, and roughly how does it cause it?

The trace. It is a slow internal "fatigue" state that accumulates while the cell is being stimulated, and it adds onto the leak current alongside the rheobase. As the trace grows, the effective amplitude (the part of the stim current that survives the leak) shrinks, the charge per second delivered to the cell shrinks with it, and the activation that drives brightness drops. The input has not moved, but the cell's response has — that is the adaptation. The activation state on its own would just plateau at a constant brightness; the trace is what bends the curve back down.

Q5. Dynaphos has two different thresholds sitting at different points in the pipeline. What does each one gate, and is it possible for the simulator to be delivering charge to a cell and still show nothing on screen?

The first threshold is the rheobase. It is subtracted from the stim current before any charge is computed — if the stim current is below it, the cell receives no charge at all. The second is the activation threshold: even when charge is being delivered, the simulator only renders a phosphene if the accumulated activation state climbs above this cut-off. So yes — there is a band of currents above the rheobase where charge is being delivered but the activation never reaches the perception threshold, and the screen stays black. The rheobase gates charge delivery; the activation threshold gates whether you actually see anything.

07References

This playground is a teaching mirror of the dynaphos simulator. All the equations, constants and parameter defaults in the JS source come straight from the dynaphos repository.

Pipeline foundation

  1. Lozano A., Suárez J.S., Soto-Sánchez C., Garrigós J., Martínez-Alvarez J.J., Ferrández J.M., Fernández E. Neurolight: A Deep Learning Neural Interface for Cortical Visual Prostheses. International Journal of Neural Systems 30(09): 2050045 (2020). doi:10.1142/S0129065720500458. This is the end-to-end pipeline the bootcamp simulates — camera → visual processing → gaze → electrode pattern → safety-gated stimulator → perceived phosphenes → decoder feedback. Modules M1–M5 each enact one stage of that loop in browser-only form.

Simulator

  1. dynaphos — biologically plausible phosphene simulator. van der Grinten M., de Ruyter van Steveninck J., Lozano A., Pijnacker L., Bjanes D., Roelfsema P.R., van Gerven M.A.J., Güçlü U. Towards biologically plausible phosphene simulation for the differentiable optimization of visual cortical prostheses. eLife 13:e85812 (2024). Code: github.com/neuralcodinglab/dynaphos. Paper: doi:10.7554/eLife.85812. This page targets dynaphos 0.1.3; parameter defaults (rheobase 23.9µA, pw 170µs, freq 300 Hz, current_spread 675µA/mm², activation_threshold 9.14×10-8, activation_decay 1.23×10-4/s, trace_decay 0.99949/s, trace_increase 13.96, view_angle 16°, fps 35, cortex dipole k=17.3 / a=0.75 / b=120) mirror its config/params.yaml.

Cortex model (wedge-dipole)

  1. Schwartz E.L. Computational anatomy and functional architecture of striate cortex: a spatial mapping approach to perceptual coding. Vision Research 20(8): 645–669 (1980). doi:10.1016/0042-6989(80)90090-5. Introduces the log-polar / monopole conformal map from visual field to striate cortex, the simplest case of the dipole used here.
  2. Polimeni J.R., Balasubramanian M., Schwartz E.L. Multi-area visuotopic map complexes in macaque striate and extra-striate cortex. Vision Research 46(20): 3336–3359 (2006). doi:10.1016/j.visres.2006.03.006. Generalises Schwartz’s map to the dipole w = k·log[b(z+a) / a(z+b)] used by this widget and by dynaphos.cortex_models (model='dipole'). Constants k=17.3 mm, a=0.75°, b=120° come from this paper.
  3. Schira M.M., Tyler C.W., Spehar B., Breakspear M. Modeling magnification and anisotropy in the primate foveal confluence. PLoS Computational Biology 6(1): e1000651 (2010). doi:10.1371/journal.pcbi.1000651. Extends the dipole to a wedge-dipole that accommodates anisotropy and the V1/V2/V3 confluence; corresponds to model='wedge-dipole' in dynaphos.cortex_models with parameter α = 0.95. The pink iso-eccentricity and iso-polar curves on the cortex panel are drawn directly through the formulas in these last two papers.

Temporal dynamics & cortical-prosthesis foundations

  1. Schmidt E.M., Bak M.J., Hambrecht F.T., Kufta C.V., O’Rourke D.K., Vallabhanath P. Feasibility of a visual prosthesis for the blind based on intracortical microstimulation of the visual cortex. Brain 119(2): 507–522 (1996). doi:10.1093/brain/119.2.507. The landmark in-human study that established the temporal regime of cortical-prosthesis phosphenes — thresholds (~1.9 µA), minimum number of pulses to evoke a percept, and the dependence of phosphene brightness & flicker on pulse frequency. The activation build-up, trace adaptation and refractory-plateau parameters used in §04 of this page (and the activation_decay, trace_decay, trace_increase constants in dynaphos params.yaml) trace back to this experimental envelope.

External images

The primary photographs used by sections 03 and 05 are inlined as base64 data URLs in the page source. This avoids the CORS edge cases that hit when an HTML file is opened via file:// (some browsers refuse crossOrigin="anonymous" canvas reads from null origins) and means the page works fully offline. Two of the three images below (kitten photo, scene photo) are Wikimedia Commons thumbnails embedded verbatim — the Wikimedia File: page is their canonical source and license reference. A single Wikimedia network fallback (for the scene photo) is listed alongside, in case the inlined data is removed. The third image (the section 05 paint default) is not from Wikimedia Commons: it was taken from a public Pinterest board, the original photographer is not identified, no network fallback is configured for it, and it is treated separately below.

  1. Section 03 · kitten (photo): Six-week-old cat (2005), photo by André Karwath (Wikimedia user Aka). License: CC BY-SA 2.5. Strong silhouette plus rich short-range edges (eyes, whiskers, fur boundaries) make this kitten an ideal Canny target for the raw-intensity vs. edges comparison in §03.
  2. Section 03 · scene (photo, Canny edges): View of Empire State Building from Rockefeller Center, New York City (2021), photo by Dllu. License: CC BY-SA 4.0. Fallback: Hudson Yards from Hudson Commons (2019), photo by Rhododendrites, CC BY-SA 4.0.
  3. Section 05 · paint default image: Neoclassical Greek townhouse facade (coral walls, mint-green door and shutters) from the "νεοκλασσικα" (neoclassical) board curated by Pinterest user damianosspathar. The original photographer is not identified on the pin. Chosen at the request of the page author for its clean daylight composition, centered subject, and the large flat saturated color regions (wall / door / shutters / plants) which produce clean masks in the region-growing segments mode. No network fallback is configured for this image — if the inlined data is removed, the page draws a procedural placeholder instead.
  4. Wikimedia Commons reuse terms: commons.wikimedia.org/wiki/Commons:Reusing_content_outside_Wikimedia.

Algorithms

  1. Canny edge detector used by the section 03 "scene (photo)" preset. Canny J. A computational approach to edge detection. IEEE Trans. Pattern Anal. Mach. Intell. PAMI-8(6), 679-698 (1986). Implemented here as: Gaussian smoothing (σ=1.4) → Sobel gradients → non-maximum suppression along the quantised gradient direction → double threshold (0.08 / 0.20) → hysteresis linking weak edges to strong ones.
  2. Difference of Gaussians (DoG) edge detector used by the section 05 "edges" paint mode. Marr D. & Hildreth E. Theory of edge detection. Proc. R. Soc. Lond. B 207, 187-217 (1980).
  3. Segment Anything Model (SAM) is the interaction model the "segments" mode is inspired by. Kirillov A. et al. Segment Anything. arXiv:2304.02643 (2023) — segment-anything.com. For an offline, lightweight playground we approximate the click-to-mask interaction with BFS region growing on a 3x3-blurred copy of the source, in chroma-weighted RGB. Two acceptance criteria run in parallel: a local (neighbour-to-neighbour) threshold for smooth gradients and a global (anywhere-to-seed) guard that prevents drift across the whole image. A radius-2 morphological close fills pinholes along the boundary. This is not SAM — the real model would add ~50 MB of ONNX encoder/decoder and an ONNX Runtime Web dependency. The interaction shape (one click → one object mask) is preserved, and the result is clean on images whose objects are locally uniform in color, like the coral wall, the mint door and the mint shutters of the neoclassical facade used as the default.

Further reading — vision-restoration field