vipsim 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
vipsim-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Rädler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
vipsim-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,355 @@
1
+ Metadata-Version: 2.5
2
+ Name: vipsim
3
+ Version: 0.1.0
4
+ Summary: Visual-impairment simulation filters: a NumPy/PyTorch port of the VIP-Sim Unity shaders
5
+ Project-URL: Homepage, https://github.com/Max-Raed/VIP-Sim_Port
6
+ Project-URL: Upstream, https://github.com/Max-Raed/VIP-Sim
7
+ Project-URL: Issues, https://github.com/Max-Raed/VIP-Sim_Port/issues
8
+ Author: Max Rädler, Omid Rajabi, Mark Colley
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: VIP-Sim,accessibility,image filters,low vision,simulation,visual impairment
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: numpy>=2.3
23
+ Requires-Dist: pillow>=12.0
24
+ Provides-Extra: all
25
+ Requires-Dist: gymnasium>=1.0.0; extra == 'all'
26
+ Requires-Dist: scikit-image>=0.24; extra == 'all'
27
+ Requires-Dist: torch>=2.11; extra == 'all'
28
+ Provides-Extra: gpu
29
+ Requires-Dist: torch>=2.11; extra == 'gpu'
30
+ Provides-Extra: gym
31
+ Requires-Dist: gymnasium>=1.0.0; extra == 'gym'
32
+ Requires-Dist: torch>=2.11; extra == 'gym'
33
+ Provides-Extra: teichopsia
34
+ Requires-Dist: scikit-image>=0.24; extra == 'teichopsia'
35
+ Description-Content-Type: text/markdown
36
+
37
+ <div align="center">
38
+
39
+ # 👁️ VipSim
40
+
41
+ ### Visual-impairment simulation filters for accessibility research
42
+
43
+ *A Python (NumPy/PIL) and GPU (PyTorch) reimplementation of the [VIP-Sim](https://github.com/Max-Raed/VIP-Sim) Unity shaders — proven equivalent to the originals and built for VLM accessibility analysis and embodied-AI training.*
44
+
45
+ ![Python](https://img.shields.io/badge/Python-3.12%2B-3776AB?logo=python&logoColor=white)
46
+ ![PyTorch](https://img.shields.io/badge/PyTorch-GPU-EE4C2C?logo=pytorch&logoColor=white)
47
+ ![NumPy](https://img.shields.io/badge/NumPy-Pillow-013243?logo=numpy&logoColor=white)
48
+ ![Filters](https://img.shields.io/badge/filters-20-brightgreen)
49
+ ![Validated](https://img.shields.io/badge/Unity%20equivalence-17%2F19%20validated-success)
50
+ ![License](https://img.shields.io/badge/license-MIT-blue)
51
+ ![PyPI](https://img.shields.io/pypi/v/vipsim)
52
+
53
+ </div>
54
+
55
+ ---
56
+
57
+ ## What this is
58
+
59
+ VipSim takes an ordinary image and renders it as someone with a given visual impairment would experience it — color-blindness, cataracts, macular degeneration, double vision, migraine aura, and more. It exists to let downstream systems (vision-language model audits, accessibility evaluations, RL agents with simulated low vision) operate on perceptually faithful inputs.
60
+
61
+ The original VIP-Sim runs as a stack of HLSL shaders inside Unity. This repository re-implements every filter in plain Python so it can run server-side, and again in PyTorch so it can run batched on a GPU inside training loops. Crucially, the Python port is **validated against the Unity original** rather than merely "looking right" — see [Validation](#-validation).
62
+
63
+ ## ✨ Highlights
64
+
65
+ - **20 visual-impairment filters** spanning color, optical, field-loss, distortion, and time-varying effects.
66
+ - **24 clinical presets** that map named conditions (e.g. *macular degeneration*, *diplopia*, *scintillating scotoma*) to ready-made filter chains.
67
+ - **Two backends** — a NumPy/PIL reference (`vipsim.filters`) and a batched PyTorch port (`vipsim.gpu`). The GPU port covers every filter, preset and profile, with the CPU's keyword arguments, and reproduces the CPU output bit for bit wherever it has been tested (see [Under the hood](#-under-the-hood-gpu-port)).
68
+ - **Checked against Unity** — 17 of 19 tracked filters pass the repo's SSIM_blur criterion against matched Unity captures; read the [caveats](#-validation) before citing this.
69
+ - **Drop-in Gym/MyoSuite wrapper** for applying impairment chains to RL observations on-device.
70
+ - **`vipsim` command** for one-off image processing, installed with the package.
71
+
72
+ ## 📦 Repository contents
73
+
74
+ Only the `vipsim/` package goes into the PyPI release; everything else is for development and validation.
75
+
76
+ | File | Role |
77
+ |---|---|
78
+ | `vipsim/filters.py` | CPU reference implementation — 20 filters, 24 presets, and the CLI. The source of truth for behavior. |
79
+ | `vipsim/gpu.py` | PyTorch port of every filter, operating on batched, channels-first tensors `[N, 3, H, W]` and matching the CPU output. Runs presets and profiles; includes a Gym/MyoSuite observation wrapper. |
80
+ | `vipsim/__init__.py`, `vipsim/__main__.py` | Public API (`from vipsim import apply_preset`, …), `__version__`, and `python -m vipsim`. |
81
+ | `vipsim/assets/` | The macular-degeneration overlay used by `apply_field_loss` (shipped in the package). |
82
+ | `vipsim_filters.py`, `vipsim_gpu.py` | Aliases so older commands (`python vipsim_filters.py …`) keep working; not part of the installed package. |
83
+ | `pyproject.toml` | Package metadata and build config (hatchling; distribution name `vipsim`). |
84
+ | `.github/workflows/publish.yml` | Builds and publishes to PyPI via Trusted Publishing when a GitHub release is published. |
85
+ | `tests/` | Unit tests (CPU filters, CLI, packaging, GPU-vs-CPU parity). |
86
+ | `vipsim_equivalence_report.md` | The validation document: how the Python port is shown to reproduce the Unity shaders, with methods, metrics, and results. |
87
+ | `validate.sh`, `validation_scripts/` | One-command Unity-equivalence validation and the scripts behind it. |
88
+ | `vipsim_assets/`, `abs_diff_out/` | Input scene, Unity reference captures, and validation outputs. |
89
+ | `vipsim_profiles_v2/` | The 7 validated VIP-Sim participant profiles as filter stacks (`p1-p7.json`), calibrated against the paper's Figure-6 panels, with side-by-side renders. |
90
+ | `validation_scripts/profile_tuner.py` | Interactive browser tuner (localhost:8765): live sliders for every parameter, similarity scoring vs a reference, auto-tune, and a generated `/docs` page. |
91
+ | `validation_scripts/render_profiles_v2.py` | Regenerates the paper-vs-ours comparison renders for the profiles. |
92
+ | `unity_helpers/` | Unity capture scripts used to produce the reference renders. |
93
+ | `filter_documentation.html` | Plain-language documentation of every filter and parameter (static export of the tuner's `/docs` page). |
94
+
95
+ ## 🚀 Quick start
96
+
97
+ ### Install
98
+
99
+ Requires Python 3.12 or newer.
100
+
101
+ ```bash
102
+ pip install vipsim # CPU reference (numpy, pillow)
103
+ pip install "vipsim[teichopsia]" # + scikit-image, needed by apply_teichopsia
104
+ pip install "vipsim[gpu]" # + torch, for vipsim.gpu / VipSimGPU
105
+ pip install "vipsim[gym]" # + torch and gymnasium, for VisualImpairmentWrapper
106
+ pip install "vipsim[all]" # everything
107
+ ```
108
+
109
+ From a checkout of this repository: `pip install -e ".[all]"`.
110
+
111
+ ### Command line
112
+
113
+ ```bash
114
+ # Single filter (`python -m vipsim ...` and `python vipsim_filters.py ...` work too)
115
+ vipsim input.png output.png --filter blur --severity 0.5
116
+ vipsim input.png output.png --filter cvd --type deuteranomaly --severity 0.8
117
+ vipsim input.png output.png --filter cataracts --severity 0.5
118
+
119
+ # Brightness / contrast / gamma
120
+ vipsim input.png output.png --filter bcg --brightness 0.8 --contrast 0.7 --gamma 1.5
121
+
122
+ # Named preset (a chain of filters)
123
+ vipsim input.png output.png --preset moderate_low_vision
124
+
125
+ # See everything available
126
+ vipsim --list-presets
127
+ ```
128
+
129
+ ### Python API
130
+
131
+ ```python
132
+ from PIL import Image
133
+ from vipsim import apply_cvd, apply_preset
134
+
135
+ img = Image.open("scene.png").convert("RGB")
136
+
137
+ # Apply one filter
138
+ out = apply_cvd(img, cvd_type="deuteranomaly", severity=0.6)
139
+
140
+ # Apply a clinical preset (chained filters)
141
+ out = apply_preset(img, "macular_degeneration")
142
+
143
+ out.save("scene_impaired.png")
144
+ ```
145
+
146
+ Or as a module: `import vipsim as vip`, then `vip.apply_blur(img, 0.5)`, `vip.PRESETS`, `vip.__version__`. Every filter is exported as `apply_<name>`.
147
+
148
+ ### GPU / batched
149
+
150
+ The PyTorch port works on a whole batch of frames at once and keeps everything on-device. Every filter is a `VipSimGPU` method with the CPU name and keyword arguments (`sim.cvd(x, cvd_type=..., severity=...)` is `apply_cvd(img, cvd_type=..., severity=...)` on a batch). Image-independent data (warp fields, index maps, masks, lookup tables) is computed once per parameter set and frame size by the CPU's own NumPy code and kept in a bounded on-device cache.
151
+
152
+ Needs `pip install "vipsim[gpu]"`. `VipSimGPU` and `VisualImpairmentWrapper` can also be imported straight from `vipsim`; torch is only loaded when you first touch them, so a plain `import vipsim` stays fast and works without torch.
153
+
154
+ ```python
155
+ import json
156
+ import torch
157
+ from vipsim import VipSimGPU
158
+ from vipsim.gpu import frames_to_gpu, gpu_to_frames
159
+
160
+ sim = VipSimGPU(device="cuda")
161
+ x = frames_to_gpu(frames, sim.device) # frames: [N, H, W, 3] uint8 -> [N, 3, H, W] in [0,1]
162
+
163
+ x = sim.cvd(x, cvd_type="deuteranomaly", severity=0.8)
164
+ x = sim.blur(x, severity=0.4)
165
+ x = sim.apply_preset(x, "macular_degeneration")
166
+ x = sim.apply_profile(x, json.load(open("vipsim_profiles_v2/p3.json")))
167
+ out = gpu_to_frames(x) # back to uint8, same pixels as the CPU chain
168
+ ```
169
+
170
+ ### Inside an RL / MyoSuite environment
171
+
172
+ ```python
173
+ from vipsim import VisualImpairmentWrapper # needs vipsim[gym]
174
+
175
+ env = VisualImpairmentWrapper(
176
+ env,
177
+ chain=[("cvd", {"severity": 0.8}), ("blur", {"severity": 0.4})],
178
+ device="cuda",
179
+ image_key=None, # set this if the observation is a dict
180
+ )
181
+ ```
182
+
183
+ `chain` takes any filter with its CPU keyword arguments; `vipsim.PRESETS[name]["filters"]` is a ready chain, and `vipsim.gpu.profile_chain(profile)` turns a `vipsim_profiles_v2` profile into one.
184
+
185
+ ## 🎛️ Filter catalogue
186
+
187
+ Every filter takes a `severity` in `[0, 1]` unless noted. The **Simulates** column is the clinical condition the matching preset is named for.
188
+
189
+ | Filter | Simulates | Unity equivalence |
190
+ |---|---|:---:|
191
+ | `cvd` | Color-vision deficiency (prot / deuter / tritanomaly, monochrome) | ✅ proof |
192
+ | `bcg` | Brightness / contrast / gamma shift | ✅ proof |
193
+ | `field_loss` | Macular degeneration (central) / tunnel vision (peripheral) | ✅ proof |
194
+ | `blur` | Low-vision acuity loss | ✅ |
195
+ | `bloom` | Photophobia / glare from bright regions | ✅ |
196
+ | `cataracts` | Cataracts (blur + yellowing + halo scatter) | ✅ |
197
+ | `distortion` | Metamorphopsia (wavy lines) | ✅ |
198
+ | `double_vision` | Diplopia | ✅ |
199
+ | `pixelation` | Very low acuity / low resolution | ✅ |
200
+ | `vortex` | Local swirling distortion | ✅ |
201
+ | `noise` | Visual snow / interference | ✅ |
202
+ | `teichopsia` | Scintillating scotoma (migraine aura) | ✅ |
203
+ | `foveal_darkness` | Central scotoma | ✅ |
204
+ | `floaters` | Vitreous floaters | ✅ |
205
+ | `flickering_stars` | Random bright dots (time-varying) | ✅ burst |
206
+ | `wiggle` | Sinusoidal warp (time-varying) | ✅ burst |
207
+ | `nystagmus` | Involuntary eye-oscillation motion (time-varying) | ✅ burst |
208
+ | `glitch` | RGB channel-shift glitch | ⚠️ dropped |
209
+ | `led` | LED-board cellular dot pattern | ⚠️ marginal |
210
+ | `detail_loss` | Contrast-sensitivity loss (posterization) | — not yet tracked |
211
+
212
+ `✅ proof` = closed by source-level algorithmic equivalence · `✅` / `✅ burst` = passes the measured-fidelity threshold (single-frame or multi-frame burst) · `⚠️` = below threshold, see report.
213
+
214
+ ## 🧩 Clinical presets
215
+
216
+ Presets bundle one or more filters under a recognizable name. A selection:
217
+
218
+ | Preset | What it models |
219
+ |---|---|
220
+ | `mild_low_vision` / `moderate_low_vision` / `severe_low_vision` | Graded blur + contrast loss |
221
+ | `deuteranomaly_moderate` / `protanomaly_moderate` | Red-green color blindness |
222
+ | `cataracts_moderate` / `elderly_vision` | Cataracts, with optional age-related CVD |
223
+ | `macular_degeneration` / `tunnel_vision` | Central vs. peripheral field loss |
224
+ | `metamorphopsia` / `diplopia` | Wavy distortion / double vision |
225
+ | `scintillating_scotoma` / `central_scotoma` | Migraine aura / central dark spot |
226
+ | `photophobia` | Glare sensitivity |
227
+ | `nystagmus_motion` / `floaters` / `visual_noise` | Motion blur, floaters, visual snow |
228
+
229
+ Run `vipsim --list-presets` for the full set of 24 with descriptions.
230
+
231
+ ## 🧑‍🤝‍🧑 Participant profiles (`vipsim_profiles_v2/`)
232
+
233
+ The VIP-Sim paper validated its shader stacks with 7 low-vision participants
234
+ (Table 4) and shows the same street scene through each participant's eyes
235
+ (Figure 6). This repo carries recreations of those 7 profiles as filter stacks:
236
+ one filter per validated shader, calibrated against clean extractions of the
237
+ Figure-6 panels. Each `pN.json` documents its Table-4 mapping and carries the
238
+ stack itself; `renders/` holds paper-vs-ours comparisons. Similarity vs the
239
+ paper panels (mean of SSIM, pixel, and CIEDE2000 color): 81 to 98 percent.
240
+
241
+ Tune or inspect them interactively:
242
+
243
+ ```bash
244
+ pip install scipy scikit-image # tuner extras
245
+ python validation_scripts/profile_tuner.py # then open http://localhost:8765
246
+ ```
247
+
248
+ The tuner shows the reference next to a live render, scores the similarity as
249
+ you drag sliders, and can auto-tune all numeric values (coordinate descent).
250
+ Every filter and parameter is explained in plain language at `/docs` (also
251
+ exported as `filter_documentation.html`). Steps may carry `"_disabled": true`
252
+ to switch them off without deleting them; all scripts in this repo honor that.
253
+
254
+ ## 🔬 Under the hood (GPU port)
255
+
256
+ All 21 filters of `vipsim.filters` (the catalogue above plus `vignette`) are ported. The CPU filters are the reference: each GPU twin reproduces today's CPU output, quirks included (nearest-index truncation, 8-bit intermediates, Pillow's integer arithmetic), and has been **bit-exact** with it in every comparison so far, on CUDA and on CPU torch, with Pillow 12.2 and 13. The CPU only ever sees a uint8 image, so each twin reads its input as 8-bit levels and returns `level / 255`: a chain of GPU filters sees exactly the CPU chain's images. One exception concerns batches: a stochastic filter seeded with a shared `numpy.random.Generator` (or `None`) draws one field per frame, in batch order, so a batched chain of two such filters consumes the draws filter by filter (it equals running each CPU filter over the whole batch in turn, not the CPU chain frame by frame). Fixed integer seeds, one Generator per filter, or one frame per call (as in the wrapper) avoid this.
257
+
258
+ There are no custom kernels. The backend is built from a few primitives:
259
+
260
+ 1. **Per-level tables and channel mixes.** Every step that maps one channel value to another (sRGB ↔ linear, `pow`, brightness/contrast/gamma, posterize, the floaters' blend) is the CPU's own NumPy code, evaluated once on the 256 levels and looked up on the device; torch's `pow` is about 1 ulp off NumPy's, which is enough to flip 8-bit levels. Three-channel mixes (the `cvd` matrices, luminance) add their products in the order NumPy's BLAS does.
261
+ 2. **Pillow-exact blur and resize.** `pil_gaussian_blur` replays `ImageFilter.GaussianBlur` (three extended-box passes per axis in 24-bit fixed point, each rounded to 8 bits) with cumulative sums in integers, so its cost does not grow with the radius. `VipSimGPU.pil_resize` replays Pillow's BILINEAR and NEAREST resamplers, including the installed Pillow's pass order (12.2 and 13 differ). Neither uses `conv2d`, which runs in TF32 by default on Ampere and later GPUs. They serve `blur`, `bloom`, the `field_loss` pyramid and `pixelation`.
262
+ 3. **Pixel gathers.** `gather_nearest` and `gather_bilinear` replace NumPy fancy indexing for `distortion`, `wiggle`, `vortex`, the cataract frost and the `led` cells; slices and rolls cover `double_vision`, `nystagmus` and `glitch`.
263
+
264
+ Anything that doesn't depend on the image content (index maps, masks, noise fields, tables) is computed **once** per parameter set and frame size by the CPU's NumPy code, seeded RNG included, and kept in a bounded LRU cache on the device, so per-frame work is a few batched tensor operations. Cache keys are exact and typed: under NumPy 2, `np.float64(0.5)` and `0.5` can give different CPU results, so they never share an entry.
265
+
266
+ **What is checked.**
267
+
268
+ - `tests/test_gpu_*.py` compare every GPU filter with its CPU filter pixel for pixel. They use random and natural frames at several sizes (odd, portrait, 1 pixel wide and very tall), severities near 0 and 1, every keyword argument (NumPy scalar types included), batches of different frames, other input dtypes, cache reuse, and the CPU's exceptions for invalid input.
269
+ - `tests/test_gpu_primitives.py` checks the blur and resize against Pillow itself and the gathers against NumPy indexing.
270
+ - `tests/test_gpu_profiles.py` renders all 7 profiles (as `render_profiles_v2.py` does) on both backends step by step and end to end, and all 24 presets end to end.
271
+ - `validation_scripts/validate_gpu_vs_numpy.py` runs every ported filter on the 2048×1024 scene and exits with an error if one exceeds its documented tolerance (`vipsim.gpu.TWIN_TOLERANCE`; results in `abs_diff_out/gpu_vs_numpy_summary.*`) or is skipped for a missing optional dependency (`--allow-skip` accepts that). The tolerance is 0 levels for every filter except `cvd`, `bcg` and `foveal_darkness`, which may differ by 1 level on another BLAS build, because it can sum their channel mix in a different order. The tolerance is per filter, so `foveal_darkness` gets it in every mode, although only `mode="desat"` mixes channels.
272
+
273
+ Because the GPU port equals the NumPy port, it inherits the NumPy port's validation against Unity (below).
274
+
275
+ **Speed.** In the port's verification runs on an RTX 4050 Laptop GPU, one 2048×1024 frame took 3–30 ms per filter (0.1–0.2 s for `field_loss`), against 0.01–2.5 s for the NumPy filter. A batch of 64 frames of 84×84 cost about as much as one frame for most filters; a single small frame is launch-bound and no faster than NumPy. `pixelation` is the exception: like the CPU, it resamples to a grid of cells that does not shrink with the frame (about 510 cells across at the default severity, 800 at severity 0.2), so a batch of 64 frames of 84×84 takes about 0.1–0.2 s and up to 2 GB of GPU memory. The first call with new parameters or a new frame size runs the CPU code to build the cached fields: up to about 1.5 s at 2048×1024 for `vortex`. The cache is bounded by entries, not bytes, so a sweep over many parameter sets at full resolution can hold several GB of fields.
276
+
277
+ ## ✅ Validation
278
+
279
+ The Python port isn't just visually plausible — it's checked against the Unity original. Two complementary methods are used, documented in full in [`vipsim_equivalence_report.md`](https://github.com/Max-Raed/VIP-Sim_Port/blob/main/vipsim_equivalence_report.md):
280
+
281
+ - **Method A — algorithmic proof.** For pure deterministic arithmetic filters (`bcg`, `cvd`), the HLSL fragment math and the NumPy code are lined up and shown to compute the same function. Fed Unity's own unfiltered capture, both match the Unity render to within 1 level (MAE 0.14 for bcg, 0.07 for cvd).
282
+ - **Method B — measured fidelity.** For everything else (noise, kernels, halo scatter, UV warps, gaze-contingent and time-varying effects), matched 2048×1024 captures are compared through a stack of published metrics: **MAE, SSIM, SSIM_blur, ΔE2000, Wasserstein-1, LPIPS**.
283
+
284
+ A filter passes when its best sweep point reaches **SSIM_blur ≥ 0.90** against the matched Unity reference. SSIM_blur is SSIM after a σ = 4 px Gaussian pre-blur of both images (`validation_scripts/metric_constants.py`), which discounts detail finer than about 10 px.
285
+
286
+ For the three inherently time-varying filters (`flickering_stars`, `wiggle`, `nystagmus`) a **burst-frame protocol** captures 30 matched frames over a fixed window and aggregates the metric across them.
287
+
288
+ **Status: 17 of 19 tracked filters pass.** `glitch` sits just below threshold (SSIM_blur 0.890, non-deterministic) and `led` is marginal pending cell-phase tuning.
289
+
290
+ **Caveats (audit, 2026-10-29).** A pass is weaker evidence than it looks:
291
+
292
+ - The unfiltered image already scores SSIM_blur ≥ 0.90 against 12 of the 17 passing Unity references, so for those filters the criterion does not separate the port from doing nothing. Of the passing filters only `bcg`, `cataracts` and `double_vision` are clearly separated; `cvd` rests on its Method A proof instead.
293
+ - The best sweep point is chosen on the same reference it is scored against; nothing is held out.
294
+ - The Unity references were rendered from a block-compressed (DXT/BC1) copy of the input, so Unity and Python start from images that differ by MAE ≈ 2.6. That offset, not fp16 rounding, is the ~3 MAE "floor" in the results table.
295
+ - The `flickering_stars` and `nystagmus` burst validators test their own time-aware models, not `apply_flickering_stars` / `apply_nystagmus`.
296
+ - `field_loss` has no source-level proof and is now listed under Method B.
297
+
298
+ ### Reproduce the sweep
299
+
300
+ This needs a clone of the repository (the Unity captures and scripts are not in the PyPI package). `./validate.sh` does all of it, including the virtualenv; by hand:
301
+
302
+ ```bash
303
+ source .venv/bin/activate
304
+ python validation_scripts/validate_all_filters.py --no-strips
305
+
306
+ # burst-frame validators for the time-varying filters
307
+ python validation_scripts/validate_flickering_stars_burst.py
308
+ python validation_scripts/validate_wiggle_burst.py
309
+ python validation_scripts/validate_nystagmus_burst.py
310
+ ```
311
+
312
+ ### Tests
313
+
314
+ ```bash
315
+ python -m unittest discover -s tests -v
316
+ python validation_scripts/validate_gpu_vs_numpy.py # GPU vs NumPy on the 2048x1024 scene
317
+ ```
318
+
319
+ The GPU tests run on CUDA when it is available, else on CPU torch, and are skipped without torch.
320
+
321
+ ### Releasing to PyPI
322
+
323
+ The distribution name is `vipsim`; the version lives in `vipsim/__init__.py` (`__version__`), which the build reads.
324
+
325
+ Releases are published by [`.github/workflows/publish.yml`](https://github.com/Max-Raed/VIP-Sim_Port/blob/main/.github/workflows/publish.yml) through PyPI Trusted Publishing, so no API token is involved:
326
+
327
+ 1. Bump `__version__` in `vipsim/__init__.py` and commit.
328
+ 2. Create a GitHub release with a matching tag (e.g. `v0.1.0`). Publishing the release builds the sdist and wheel, checks them with `twine check`, and uploads them to PyPI.
329
+
330
+ To check a build locally before releasing:
331
+
332
+ ```bash
333
+ python -m pip install build twine
334
+ python -m build # writes dist/vipsim-<version>.tar.gz and .whl
335
+ python -m twine check dist/*
336
+ ```
337
+
338
+ ## 📚 References
339
+
340
+ - Wang, Bovik, Sheikh & Simoncelli (2004). *Image quality assessment: from error visibility to structural similarity.* IEEE TIP 13(4).
341
+ - Sharma, Wu & Dalal (2005). *The CIEDE2000 color-difference formula.* Color Research & Application 30(1).
342
+ - Zhang, Isola, Efros, Shechtman & Wang (2018). *The Unreasonable Effectiveness of Deep Features as a Perceptual Metric.* CVPR.
343
+ - Kantorovich (1942) / Villani (2008) — optimal transport (Wasserstein distance).
344
+
345
+ ## 📄 License & attribution
346
+
347
+ VipSim is released under the **MIT License** (see [`LICENSE`](https://github.com/Max-Raed/VIP-Sim_Port/blob/main/LICENSE)).
348
+
349
+ It reimplements shader code from **[VIP-Sim](https://github.com/Max-Raed/VIP-Sim)** by Max Raed, which is licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); please keep that attribution when you use or redistribute VipSim.
350
+
351
+ ---
352
+
353
+ <div align="center">
354
+ <sub>Built for accessibility research and embodied-AI evaluation.</sub>
355
+ </div>
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ # 1.27 is the first hatchling with PEP 639 (license expression + license-files).
3
+ requires = ["hatchling>=1.27"]
4
+ build-backend = "hatchling.build"
5
+
6
+ [project]
7
+ name = "vipsim"
8
+ dynamic = ["version"]
9
+ description = "Visual-impairment simulation filters: a NumPy/PyTorch port of the VIP-Sim Unity shaders"
10
+ readme = "readme.md"
11
+ requires-python = ">=3.12"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [
15
+ { name = "Max Rädler" },
16
+ { name = "Omid Rajabi" },
17
+ { name = "Mark Colley" },
18
+ ]
19
+ keywords = ["visual impairment", "low vision", "accessibility", "simulation", "image filters", "VIP-Sim"]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Intended Audience :: Science/Research",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Scientific/Engineering :: Image Processing",
30
+ ]
31
+ dependencies = [
32
+ "numpy>=2.3",
33
+ "pillow>=12.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ # apply_teichopsia needs scikit-image (HSV conversion, polygon raster).
38
+ teichopsia = ["scikit-image>=0.24"]
39
+ gpu = ["torch>=2.11"]
40
+ gym = ["vipsim[gpu]", "gymnasium>=1.0.0"]
41
+ all = ["vipsim[teichopsia,gpu,gym]"]
42
+
43
+ [project.scripts]
44
+ vipsim = "vipsim.filters:main"
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/Max-Raed/VIP-Sim_Port"
48
+ Upstream = "https://github.com/Max-Raed/VIP-Sim"
49
+ Issues = "https://github.com/Max-Raed/VIP-Sim_Port/issues"
50
+
51
+ [tool.hatch.version]
52
+ # Single source of truth: __version__ in vipsim/__init__.py (currently 0.1.0).
53
+ path = "vipsim/__init__.py"
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ # Only the package; vipsim/assets/ (the field-loss overlay) ships inside it.
57
+ packages = ["vipsim"]
58
+
59
+ [tool.hatch.build.targets.sdist]
60
+ # No tests, validation scripts, Unity captures, renders or abs_diff_out.
61
+ only-include = ["vipsim", "readme.md", "LICENSE"]