sensus 0.5.0

CLI for sensus: simulate sensory perception (color blindness, low vision, hearing loss) on images
sensus-0.5.0 is not a library.

sensus

Sensory perception simulation for images and audio — apply color blindness, low vision, visual field defects, and hearing loss filters to ordinary photos and audio buffers. Built as a Rust crate so it can power accessibility demos, educational tools, and apps like universal-experience.

sensus (Latin) — "sense, perception, feeling".

Status

v0.5.0 — published on crates.io. All vision filters through Phase 4 (color vision, refraction, visual field, light/transparency, balance/vertigo, eye fatigue, and more) are implemented, plus 14 hearing filters (usable from the CLI via --audio). See docs/roadmap.md for the full phase tracker.

Filters

Vision

Filter Status Phase Notes
protanopia ✅ implemented 1 (#2) L-cone loss (red-blind), Machado 2009
deuteranopia ✅ implemented 1 (#2) M-cone loss (green-blind), Machado 2009
tritanopia ✅ implemented 1 (#2) S-cone loss (blue-blind), Machado 2009
achromatopsia ✅ implemented 1 (#2) Total color blindness, BT.709 luma
tetrachromacy ✅ implemented 1+ (#3) Four-cone exploration, gamut expansion in LMS space
myopia ✅ implemented 2 (#4) Disk blur, -6 D max
hyperopia ✅ implemented 2 (#4) Disk blur, +4 D max
presbyopia ✅ implemented 2 (#4) Disk blur, +3 D add max
astigmatism ✅ implemented 2 (#4) 1D directional blur (pure cylindrical lens), configurable axis
glaucoma ✅ implemented 3 (#5) Radial vignette / peripheral field loss
macular_degeneration ✅ implemented 3 (#5) Foveal blur and dimming
hemianopia ✅ implemented 3 (#5) Half-field blanking
tunnel_vision ✅ implemented 3 (#5) Severe radial vignette
cataract ✅ implemented 3 (#6) Haze overlay
floaters ✅ implemented 3 (#6) Translucent blob compositing
photophobia ✅ implemented 3 (#6) Brightness boost and highlight halation
night-blindness ✅ implemented 3 (#6) Darkening and desaturation (nyctalopia)
vertigo ✅ implemented 4 (#9) Rotational displacement / dizziness
bppv-rotation ✅ implemented 4 (#9) Positional vertigo rotation
vestibular-neuritis ✅ implemented 4 (#9) Sustained spinning + blur
diplopia ✅ implemented 4 (#29) Double vision (ghost image offset)
nystagmus ✅ implemented 4 (#29) Involuntary directional motion blur
starbursts ✅ implemented 4 (#29) Light starbursts / glare from highlights
eye-strain ✅ implemented 4 (#36) Contrast loss, vignette, slight blur
dry-eye ✅ implemented 4 (#36) Tear-film tile distortion

Additional vision filters (library Filter enum and CLI --filter): metamorphopsia (#55), contrast-sensitivity (#56), detail-loss (#57), teichopsia (#58), and flickering-stars (#59). The library function names use underscores (contrast_sensitivity, detail_loss, flickering_stars); the CLI flag names and the night-blindness / nyctalopia pair follow the kebab-case ↔ snake-case mapping clap derives.

Depth-aware refraction is also available CLI-side only via --filter myopia-depth / hyperopia-depth / depth-of-field combined with --depth, --mpo, or --portrait (#19).

Hearing (library + CLI)

14 hearing filters operate on audio buffers (#7–#9, #102–#104): hearing_loss, sudden_hearing_loss, noise_induced_hearing_loss, tinnitus, hyperacusis, misophonia, paracusis, amusia, dysmelodia, pitch_shift_semitones, diplacusis, auditory_processing_disorder (APD), meniere, and labyrinthitis. Each takes a sensus_core::hearing::AudioBuffer (f32 interleaved PCM) and returns one.

From the CLI, use --audio <in.wav> --hearing <filter> -o <out.wav> (WAV only). See CLI usage. From Rust, use sensus_core::apply_hearing / HearingFilter, or chain via AudioPipeline.

Composite experiences (Experience)

Some conditions span both vision and hearing (e.g. Ménière's disease = spinning vertigo + low-frequency hearing loss + low-pitched tinnitus). Because sensus is a pure, separate-buffer (image vs audio) library, Experience canonicalises which vision filter pairs with which hearing filter, so consumers (e.g. the universal-experience GUI) don't hard-code the combination: Experience::MENIERE, BPPV, VESTIBULAR_NEURITIS, LABYRINTHITIS. Each carries an Urgency classification for "see a doctor" prompts.

Each filter ships with a short note on when to see a doctor — sensus is designed both as an empathy / education tool and as an early-warning primer for symptoms worth taking seriously.

Install

cargo install sensus

This installs the sensus CLI binary. Pre-built binaries for Linux, macOS (Intel & Apple Silicon), and Windows are also attached to each GitHub Release.

CLI usage

# Phase 1: color vision deficiency simulation
sensus -i photo.png -o photo-deuteranopia.png --filter deuteranopia --strength 1.0
sensus -i photo.png -o photo-grayscale.png    --filter achromatopsia --strength 1.0
sensus -i photo.png -o photo-mild-protan.png  --filter protanopia    --strength 0.5

# Phase 2: focus / refraction (disk blur in linear sRGB)
sensus -i photo.png -o photo-myopia.png       --filter myopia        --strength 1.0
sensus -i photo.png -o photo-presbyopia.png   --filter presbyopia    --strength 0.7
# astigmatism with explicit cylinder axis (degrees, 0..=180; default 90)
sensus -i photo.png -o photo-astig.png        --filter astigmatism   --strength 1.0 --axis 90

# Phase 3: visual field defects and light conditions
sensus -i photo.png -o photo-glaucoma.png     --filter glaucoma      --strength 1.0
sensus -i photo.png -o photo-cataract.png     --filter cataract      --strength 0.8

# Pipeline: chain multiple filters in one pass
sensus -i photo.png -o out.png --filter deuteranopia --filter myopia --strength 1.0
sensus -i photo.png -o out.png --filter glaucoma --filter cataract --strength 0.7

# Pipe mode: read JPEG frames from stdin, write filtered frames to stdout (ffmpeg integration)
# --output is not required when --pipe is used
ffmpeg -i video.mp4 -f image2pipe -vcodec mjpeg - | \
    sensus --filter deuteranopia --pipe | \
    ffmpeg -f mjpeg -i - -c:v libx264 out.mp4

# Hearing: apply hearing filters to a WAV file (#105). WAV in / WAV out only.
sensus --audio voice.wav -o voice-hearing-loss.wav --hearing hearing-loss -s 1.0
sensus --audio voice.wav -o voice-tinnitus.wav     --hearing tinnitus     --freq 6000 -s 0.7
# Chain hearing filters and use the composite Ménière hearing side
sensus --audio voice.wav -o voice-meniere.wav      --hearing meniere      -s 0.8
sensus --audio voice.wav -o voice-chain.wav        --hearing hearing-loss tinnitus --freq 4000

Flags:

Flag Description
-i, --input Input image path (PNG / JPEG / WebP, etc.).
-o, --output Output path (image, or WAV with --audio). Format is inferred from the extension.
-f, --filter Vision filter to apply (e.g. deuteranopia, cataract, glaucoma).
--audio Input audio path (WAV). Routes to hearing-filter mode; requires --hearing and -o. Cannot be combined with --filter / --pipe.
--hearing Hearing filter(s) for --audio (e.g. hearing-loss, tinnitus, meniere, labyrinthitis). Repeatable to chain.
--freq Frequency (Hz) for --hearing tinnitus / sudden-hearing-loss / misophonia. Default 4000.
--semitones Semitones for --hearing pitch-shift (negative = lower). Default 0.0.
-s, --strength Strength 0.0 – 1.0. 0.0 keeps the original; 1.0 is full effect. Default 1.0.
--axis Astigmatism cylinder axis in degrees 0.0 – 180.0 (astigmatism only). Default 90.0 (with-the-rule: vertical lines sharp, horizontal blurred).
--seed Random seed for stochastic filters (cataract, floaters, flickering-stars). Default 0.
--density, --gaze-x, --gaze-y, --size floaters: blob density 0–1, gaze position 0–1, and size multiplier 0.1–5.0.
--side hemianopia side: 0.0 = left field lost, 1.0 = right field lost.
--offset-x, --offset-y, --ghost-strength diplopia ghost-image offsets (min(W,H) ratio) and ghost strength.
--amplitude, --direction-deg nystagmus motion amplitude (min(W,H) ratio) and direction in degrees.
--num-rays, --ray-length, --threshold, --dispersion starbursts: ray count, ray length (min(W,H) ratio), brightness threshold, and chromatic dispersion (0 = white, 1 = rainbow).
--cell-size detail-loss pixelation tile size in pixels (>= 1; 1 = no effect). Default 8.
--meta-freq, --meta-seed metamorphopsia distortion spatial frequency and field seed.
--depth, --mpo, --portrait, --focus Depth-map sources and focus depth for the *-depth / depth-of-field filters.

Per-filter parameter flags only affect their own filter; passing one to an unrelated single filter prints a warning and is otherwise ignored. Run sensus --help for the full list of filter names and flags.

Library usage

sensus-core is the pure-logic crate (no I/O). Take a DynamicImage in, get a DynamicImage out.

# Cargo.toml
[dependencies]
sensus-core = "0.5"
use image::DynamicImage;
use sensus_core::{apply, vision, Filter};

fn examples(img: DynamicImage) -> sensus_core::Result<()> {
    // Direct call (most ergonomic for a known filter):
    let _full   = vision::deuteranopia(img.clone(), 1.0)?;
    let _mild   = vision::deuteranopia(img.clone(), 0.5)?;
    let _gray   = vision::achromatopsia(img.clone(), 1.0)?;
    let _near   = vision::myopia(img.clone(), 1.0)?;
    let _astig  = vision::astigmatism(img.clone(), 0.5, 45.0)?; // axis in degrees

    // Or via the dispatching facade (handy when the filter is dynamic).
    // Filter-specific parameters travel in the enum variant, so `apply()`
    // honours them (e.g. the astigmatism axis below):
    let _shifted = apply(Filter::Protanopia, img.clone(), 1.0)?;
    let _astig2  = apply(Filter::Astigmatism { axis_deg: 45.0 }, img, 1.0)?;
    Ok(())
}

strength is clamped to [0.0, 1.0]. Filters in Phase 1 simulate color vision deficiency in linear sRGB space using the Machado 2009 model; achromatopsia uses the BT.709 photopic luminance. (BT.601 luma coefficients are designed for NTSC CRTs and are colorimetrically wrong for sRGB images.) Phase 2 (refraction) applies an optically correct disk blur in linear sRGB — radius derived from 0.5 × pupil_diameter × |D| (Smith–Helmholtz gives angular diameter, so the radius is half), min(W, H)-relative, edge-replicated borders. astigmatism is 1D directional blur (pure cylindrical lens / line spread function), not an elliptical disk — see docs/overview.md for the full derivation.

The same function signatures can be applied frame-by-frame for video, or chained together via sensus_core::pipeline.

Hearing filters

Hearing filters live in sensus_core::hearing and are dispatched via apply_hearing. They operate on audio buffers, not images. From the CLI they are reachable via --audio <in.wav> --hearing <filter> -o <out.wav> (WAV only); from Rust use the API below or chain them via AudioPipeline.

use sensus_core::{apply_hearing, hearing::AudioBuffer, HearingFilter};

fn deafen(buf: AudioBuffer) -> sensus_core::Result<AudioBuffer> {
    let muffled = apply_hearing(HearingFilter::HearingLoss, buf, 1.0)?;
    // Tinnitus and pitch shift carry their own parameters in the variant:
    let ringing = apply_hearing(HearingFilter::Tinnitus { freq_hz: 4000.0 }, muffled, 0.5)?;
    Ok(ringing)
}

GLSL shaders (host integration)

sensus_core::shaders ships the same vision effects as GLSL fragment shaders for hosts that render on the GPU (Flutter FragmentProgram, WebGL, etc.). Some shaders need resolution-dependent uniforms — e.g. photophobia, eye_strain, dry_eye, teichopsia, glaucoma, macular_degeneration, tunnel_vision, and the disk-blur refraction filters consume values such as uRadiusPx and uTexelSize (vec2(1.0/width, 1.0/height)). When using these shaders in an external host you must set those uniforms from the matching *_uniforms() helper (e.g. shaders::photophobia_uniforms(strength, width, height)); passing only strength will produce incorrect (scale-dependent) results. This is the contract universal-experience relies on.

Crates

Crate Path Role
sensus-core crates/core/ Pure rendering / DSP core. No filesystem, no subprocess.
sensus crates/cli/ CLI binary. Owns image::open, file writes, etc.

License

MIT (c) 2026 kako-jun. See LICENSE.