Plenoview is a multichannel image viewer for computational imaging, rendering, and vision workflows. It reveals the rich structure of images that contain more than color, including polarization, spectral, panoramas, depth, and AOVs.
- OpenEXR decode via a browser-safe
exrsWASM adapter with full layer/channel extraction. - Local EXR load via
File > Open...or drag/drop (drag-and-drop supports multiple files and recursive folder drops in one action). - Recursive folder EXR load via
File > Open Folder...; all.exrfiles under the selected folder are appended as sessions. File > Export...exports the full active display to PNG at display image size with configurable PNG compression and current channel/stokes, exposure/gamma, colormap, and alpha settings applied.File > Export Screenshot...exports an image-viewer or panorama-viewer screenshot region to PNG; multiple screenshot regions export as a ZIP, with optional reproduction JSON.File > Export Batch...exports selected file/channel combinations as a ZIP of PNG images.File > Export Colormap...exports any registered colormap as a standalone PNG gradient with configurable colormap, size, orientation, and filename.- Right-click
Copy Imagecopies the current display image to the clipboard. View > Image viewer/Panorama viewer/3D viewerswitches between the existing 2D image view, an equirectangular panorama projection suitable for 360-degree environment maps and HDRIs, and a point-cloud view for RGB plus depth or position data.View > Rulerstoggles pixel rulers inImage viewer.Windowcontrols include normal/full-screen preview plus single-pane, vertical split, and horizontal split viewer layouts.- Top-bar quick actions include Auto Fit, Auto Exposure, invalid-value warning, screenshot export, Metadata, app fullscreen, and the Settings gear.
Shift+ left-drag inImage viewercreates or replaces a persistent rectangular ROI for measurement; drag an existing ROI body or handles to edit it. ROI creation/editing is disabled inPanorama viewer.- Multi-image sessions:
- New image opens as active while previously opened images are kept in memory.
Open Fileslist allows switching active image by filename; rows show thumbnails/status and support filtering, inline rename, drag reorder, and drag-to-viewer-pane assignment.- Multi-layer EXR state is preserved per opened session. Display channel mapping, the active probe position, and the committed ROI carry across session switches when valid for the target image. The active viewer mode is preserved across session switches, and each session remembers separate image-view, panorama-view, and 3D-view camera state.
- When Auto Fit selected images is enabled, image-mode session switches and new loads fit to the viewer instead of carrying previous pan/zoom; this does not apply in
Panorama viewer. Colormap state carries only when the display selection remains compatible. - Decoded CPU pixels are included in the displayed memory usage. The display cache budget evicts retained display textures and materialized display buffers, so decoded pixels and browser/GPU overhead can exceed the selected cap.
Settingsdialog >Display Cache Budgetdefaults toAutomaticand also supports fixed presets (64,128,256,512,1024MB). - Per-file row
Reloadaction re-decodes the selected session from its original source. File > Reload Allre-decodes all opened sessions from their original sources.- Per-file row
Closeaction closes the selected filename entry. File > Close Allcloses all opened sessions at once.- Duplicate filenames are disambiguated as
name.exr (2),name.exr (3), etc.
- Visible loading indicator while large EXR files are decoding/loading.
- ROI inspector:
- Shows bounds, size, total pixels, per-channel valid sample counts, and
min/mean/maxfor the active display selection. - ROI survives view-mode switches, carries across image switches and new loads, clamps to the target image bounds when needed, and can be cleared from the Inspector.
- Shows bounds, size, total pixels, per-channel valid sample counts, and
- Display controls:
Noneis the default RGB display path and exposes Exposure and Gamma controls. Exposure uses slider + numeric input (-10to+10EV, step0.1).Colormapmaps current display luminance over the full active image through the selected NumPy LUT palette.- Built-in palettes are listed in
public/colormaps/manifest.jsonand stored as static.npyfiles in the same directory. - The app accepts LUT arrays with shape
(N, 3)or(N, 4)and dtypefloat32,float64, oruint8. - RGB Exposure/Gamma controls are hidden in
Colormapmode; colormap mode exposes separate EV/Gamma controls that affect LUT mapping. Paletteselects the active LUT without rebuilding the EXR display texture.vmin/vmaxcan be adjusted with one dual-handle slider or numeric inputs.Auto Rangehas two modes: highlighted always-auto mode follows each image/layer/channel, while one-time/manual mode preserves the current min/max across targets. Dynamic auto ranges usev=max(abs(min), abs(max))and map to[-v, v].- Selecting a diverging palette auto-enables
Zero Center, which keeps manual ranges symmetric around zero (min=-v,max=v) and also applies to fixed Stokes colormap defaults. Reverseflips the active colormap ramp.- Angle Stokes colormaps expose a paired degree modulation toggle: AoLP can be modulated by DoLP, CoP by DoCP, and ToP by DoP. AoLP also lets the modulation target be
V(HSV value) orS(HSV saturation), defaulting toV. CoP and ToP modulation default to on; AoLP defaults to off. - Leaves raw numeric probe values unchanged.
- Nearest-neighbor rendering at all zoom levels (no interpolation).
- Zoom range:
0.03125xto512x, wheel zoom anchored to cursor. - Pan with left mouse drag.
- Panorama viewer:
- Projects the current display texture onto a sphere using equirectangular sampling.
- Left drag orbits the camera;
W/A/S/Dalso orbit yaw/pitch; mouse wheel changes horizontal FOV from1to180degrees, with the widest range transitioning to a hemispherical projection. - The Inspector probe remains available through panorama ray-to-pixel lookup.
- Existing ROIs remain stored but cannot be created or edited until you return to
Image viewer. - Panorama mode does not draw on-canvas pixel value overlays.
- On-canvas probe rectangles remain hidden in panorama mode.
- Probe:
- Hover pixel readout in the Inspector.
- Click to lock/unlock probe pixel.
- Values are raw linear EXR channel values (pre-exposure, pre-display transform).
- Metadata:
- The top-bar Metadata dialog shows EXR header metadata for the active image/layer, including common attributes such as compression, data/display windows, line order, channels, type, capture date, renderer/integrator, and compatible custom attributes.
- On-image pixel labels at high zoom:
- RGB values shown inside image pixels.
- 3-channel values stacked vertically.
- Label colors follow channel mapping (
R,G,B). - Panorama mode reuses the same value formatting, but only draws labels for source pixels with a stable, sufficiently large projected footprint.
- Channel controls:
- Bottom channel thumbnail strip selects grouped channels such as
HOGE.R/G/B,FUGA.R/G/B,normal.X/Y/Z, andmotion.U/V; grouped RGB remains the default display when available, while XYZ and UV groups are used when no RGB group is available. XYZ mapsX/Y/Zto display red/green/blue, and UV mapsU/Vto display red/green with blue fixed at zero. - Alpha is applied to normal channel displays when a matching companion exists: bare
R/G/Band bare scalar channels use bareA, while namespaced channels such asbeauty.Rordepth.Zusebeauty.Aordepth.A. Collapsed channel choices group alpha into labels such asRGBA,mask,A, andbeauty.RGBAinstead of showing the companion alpha separately. - Auxiliary channels such as
Z, masks, and custom AOVs are selectable directly and display as grayscale by mapping that source channel into all three display channels, which makesColormapoperate on that channel directly. - Expandable channel stacks expose split component entries for RGB, XYZ, and UV groups plus
Awhen alpha exists. Scalar alpha pairs such asmask,Aexpose separatemaskandAentries in expanded stacks. - Spectral wavelength series are grouped into a
Spectral RGBentry by default using the built-in spectral-to-RGB conversion; expandable stacks expose individual wavelength channels. - Stokes layers with
S0/S1/S2/S3expose derivedStokes S1/S0,Stokes S2/S0,Stokes S3/S0,Stokes AoLP,Stokes DoP,Stokes DoLP,Stokes DoCP,Stokes CoP, andStokes ToPentries. Complete non-RGB suffixed sets such asS0.Y/S1.Y/S2.Y/S3.Yare also exposed as scalar Stokes entries with suffixed labels such asStokes AoLP.Y, while spectral Stokes sets such asS0.500nm/S1.500nm/S2.500nm/S3.500nmare grouped into entries such asS1/S0 Spectral RGBand expanded into per-wavelength entries such asS1/S0.500nm. Scalar AoLP uses HSV over[0, pi]; degree parameters use Black-Red over[0, 1]; CoP and ToP use signed ellipticity angle over[-pi/4, pi/4]. CoP enablesZero Centerby default. Switching within the same Stokes colormap group, such as DoP/DoLP/DoCP or S1/S0/S2/S0/S3/S0, preserves the current palette,vmin/vmax, auto/manual mode, and zero-center setting. - RGB Stokes layers with
S0.R/G/BthroughS3.R/G/Bexpose groupedS1/S0.RGB,S2/S0.RGB,S3/S0.RGB,AoLP.RGB,DoP.RGB,DoLP.RGB,DoCP.RGB,CoP.RGB, andToP.RGBentries. InNone, grouped entries derive the selected Stokes parameter independently forR,G, andB; inColormap, grouped entries keep the Rec.709 mono-derived visualization. Expanded stacks expose per-component entries such asS1/S0.R,AoLP.G, andDoP.B. - Mueller matrix layers with complete
M00throughM33channel sets expose aMueller Matrixentry rendered as a row-major 4x4 grayscale grid with no separator pixels. Complete non-RGB suffixed sets such asM00.YthroughM33.Yexpose suffixed entries such asMueller Matrix.Y. RGB Mueller sets withM00.R/G/BthroughM33.R/G/Bexpose a groupedMueller Matrix.RGBentry, and expanded stacks expose per-component entries such asMueller Matrix.R. - When a selected layer does not expose the previous channel mapping, the viewer falls back to the first non-Mueller RGB group, then RGB Mueller, then the first RGB group, then normal maps and UV groups, then spectral RGB when available, then XYZ/Position groups, then exact
Y, then grayscale, then a complete Mueller matrix grid, then the first non-alpha channel.
- Bottom channel thumbnail strip selects grouped channels such as
- Double-clicking the Display heading resets visualization mode/palette, RGB exposure/gamma, colormap EV/gamma/range/zero-center/reverse, without changing channel selection or view.
public/middlebury_chess1_rgb_p.exr is a half-resolution OpenEXR conversion of RGB plus metric camera-space position from the chess1 scene in the Middlebury 2021 mobile stereo datasets.
- Left panel:
Open Files. - Center: image viewer canvas.
- Bottom panel: channel thumbnails.
- Right side: Display, Probe, Spectral, ROI, View, and Image Stats panels.
- Vite + Vanilla TypeScript
- WebGL2 renderer
exrs(WASM OpenEXR decoder)- Vitest (unit/integration-style tests)
- Playwright (workflow E2E)
- Node.js 20+
- npm
- Modern browser with WebGL2
npm install
npm run devOpen the local Vite URL (usually http://localhost:5173) for the project page, or http://localhost:5173/app/ for the viewer app.
npm run build
npm run previewOutput is generated in dist/ and is static-hosting ready.
Plenoview is available as a Visual Studio Code extension for opening .exr files directly inside VS Code as readonly custom editors.
Marketplace: https://marketplace.visualstudio.com/items?itemName=elerac.plenoview-vscode
The extension reuses the Plenoview viewer UI and supports local EXR file/folder loading, metadata inspection, viewer modes, rulers, pane layouts, and derived PNG/ZIP exports. Source .exr files are not modified.
Prerequisites:
- Node.js 20+ and npm 10+
- Rust stable (
rustcandcargo) - Tauri platform prerequisites for your OS
Build the desktop app locally:
npm install
npm run desktop:buildOn macOS, the local unsigned app and DMG are generated under src-tauri/target/release/bundle/. Generated desktop bundles and build outputs should stay uncommitted.
On Windows, the installed executable is Plenoview.exe. The installer registers Plenoview as an OpenEXR .exr handler candidate, but Windows requires the user to choose the default app: open Settings > Apps > Default apps > Choose defaults by file type > .exr and select Plenoview, or right-click an .exr file and use Open with. Older builds may not appear automatically; browse to the installed Plenoview.exe if needed.
Stable desktop installers are published from tagged GitHub Releases. Push a tag named vX.Y.Z where X.Y.Z matches package.json, src-tauri/tauri.conf.json, and src-tauri/Cargo.toml; the desktop workflow builds the Windows x64 NSIS installer and macOS Apple Silicon DMG, attaches checksums, and publishes the release.
Latest desktop download URLs:
Windows x64: https://github.com/elerac/plenoview/releases/latest/download/Plenoview-windows-x64-setup.exe
macOS arm64: https://github.com/elerac/plenoview/releases/latest/download/Plenoview-macos-arm64.dmg
Releases: https://github.com/elerac/plenoview/releases/latest
This project is prepared for GitHub Pages with a project page and app route:
Project page: https://elerac.github.io/plenoview/
Viewer app: https://elerac.github.io/plenoview/app/
Open a remote EXR directly with ?src=<exr-url>, for example https://elerac.github.io/plenoview/app/?src=https://elerac.github.io/plenoview/cbox_rgb.exr. The EXR host must allow browser CORS requests.
GitHub Pages should use GitHub Actions as the publishing source. The repository now uses a dedicated CI workflow for lint, typecheck, coverage, Playwright, and build checks on pushes, and the Pages workflow deploys only after CI succeeds on main or when triggered manually. The Pages build runs with GITHUB_PAGES=true, which sets the Vite base path to /plenoview/, builds the landing page at the project root and the viewer at /app/, uploads the generated dist/ directory as the Pages artifact, and deploys it. Keep dist/ uncommitted; it is generated by the action.
Run the local quality gates:
npm run lint
npm run typecheck
npm run test
npm run test:coverageRun Playwright E2E tests:
npx playwright install
npm run test:e2eThe embed wrapper registers <plenoview-viewer> and window.Plenoview.
Load the deployed wrapper script, then add the custom element:
<script src="https://elerac.github.io/plenoview/embed/plenoview.js"></script>
<plenoview-viewer
src="https://elerac.github.io/plenoview/cbox_rgb.exr"
name="Cornell Box"
width="640"
height="420">
</plenoview-viewer>Common attributes:
| Attribute | Description |
|---|---|
src |
EXR URL to load. |
name |
Embedded source label and opened file name when applicable. |
view |
Initial mode: image, panorama, or depth. |
auto-load |
Set to false to defer loading until the embedded Click to load image button is clicked. Defaults to true. |
width / height |
CSS sizes; numeric values become pixels. Defaults: 100% / 320px. |
viewer-url |
Viewer deployment URL, needed if the wrapper script is served from another location. |
source-origin |
Loading policy: auto, parent, or viewer. |
bottom-panel |
Embed bottom content: probe, channels, or none. Defaults to probe. |
panorama-auto-rotate |
Set to true on panorama embeds to rotate the yaw automatically. Defaults to false. |
panorama-rotation-speed |
Signed panorama rotation speed in degrees per second. Defaults to 6; values are clamped to -60 to 60. |
three-d-auto-orbit |
Set to true on 3D embeds to animate a front-biased point-cloud orbit. Defaults to false. |
three-d-orbit-speed |
Maximum center yaw speed in degrees per second. Defaults to 6; values are clamped to 0 to 30. |
three-d-orbit-yaw |
3D orbit yaw amplitude in degrees. Defaults to 12; values are clamped to 0 to 30. |
three-d-orbit-pitch |
3D orbit pitch amplitude in degrees. Defaults to 2; values are clamped to 0 to 8. |
The embed supports pan, zoom, hover probe or compact channel selection, panorama auto-rotation, 3D point-cloud orbit, and an Open full viewer button.
Use Plenoview.create(target, options) for dynamic sources:
<div id="viewer"></div>
<script src="https://elerac.github.io/plenoview/embed/plenoview.js"></script>
<script>
const viewer = Plenoview.create('#viewer', {
src: './public/cbox_rgb.exr',
name: 'Cornell Box',
width: 640,
height: 420,
bottomPanel: 'channels',
autoLoad: true,
viewerUrl: 'https://elerac.github.io/plenoview/app/'
});
</script>Controller methods:
| Method | Description |
|---|---|
loadUrl(src, options) |
Load an EXR URL; options include name, view, sourceOrigin, panorama animation fields, and 3D orbit fields. |
loadFile(file, options) |
Load a browser File; options include name, view, panorama animation fields, and 3D orbit fields. |
setView(view) |
Switch to image, panorama, or 3d. |
setPanoramaAutoRotate(enabled) |
Enable or disable panorama auto-rotation without replacing the iframe. |
setPanoramaRotationSpeed(speedDegPerSecond) |
Set signed panorama rotation speed in degrees per second without replacing the iframe. |
setThreeDAutoOrbit(enabled) |
Enable or disable 3D point-cloud orbit without replacing the iframe. |
setThreeDOrbitSpeed(speedDegPerSecond) |
Set 3D orbit speed without replacing the iframe. |
setThreeDOrbitYaw(yawAmplitudeDeg) |
Set 3D orbit yaw amplitude without replacing the iframe. |
setThreeDOrbitPitch(pitchAmplitudeDeg) |
Set 3D orbit pitch amplitude without replacing the iframe. |
destroy() |
Remove the embedded viewer. |
- In
automode, relative andblob:URLs are fetched by the embedding page; absolute remote URLs load in the viewer. - Use
source-origin="parent"/sourceOrigin: 'parent'orsource-origin="viewer"/sourceOrigin: 'viewer'to force either side. - Remote viewer-loaded files must be CORS-readable by the viewer origin, such as
https://elerac.github.io. - Direct
file://relative EXR loading is not browser-portable. Use a local HTTP server for URL-based examples, orloadFile(file)for local files.
Open Fileslist: switch active image session by filename, filter rows, rename rows inline, or drag rows to reorder/assign to a split pane.Alt/Option+Up/Down: reorder the activeOpen Filesrow.Gallery > cbox_rgb.exr/Middlebury Stereo > middlebury_chess1_rgb_p.exr/Beachball > multipart.0001.exr/Poly Haven/KAIST Hyperspectral/Polanalyser: open a gallery sample and append it as a new session. Remote samples require network access.File > Open...: open one EXR file and append it as a new session.File > Open Folder...: recursively open every.exrfile under the selected folder and append them as new sessions.- Drag/drop: drop one or more
.exrfiles, or drop a folder to recursively load every.exrunder it. File > Export...: export the active full display to PNG at display image size, with configurable PNG compression.File > Export Screenshot...: select and export screenshot regions from Image or Panorama viewer; multiple regions export as a ZIP.File > Export Batch...: export selected file/channel combinations as a ZIP of PNG images; the batch dialog has its ownSplit RGBoption.File > Export Colormap...: export a registered colormap to a PNG gradient with selectable colormap,width,height,orientation, and filename.- Right-click viewer menu >
Copy Image: copy the current display image to the clipboard. - Settings dialog >
Display Cache Budget: useAutomaticor choose a fixed retained display residency budget from64,128,256,512, or1024MB. The memory breakdown also shows decoded pixels, GPU textures, CPU materialized buffers, analysis cache, and total tracked memory. The value persists inlocalStorage. - Settings dialog: configure theme, spectrum lattice motion, spectral grouping default, Stokes defaults/visibility, invalid Stokes masking, auto exposure percentile, and image load workers.
View > Image viewer/Panorama viewer: switch between planar image viewing and spherical panorama viewing.View > Rulers: toggle pixel rulers in Image viewer.Window > Full Screen Preview: show the viewer in browser fullscreen/fallback preview mode.Window > Single Pane/Split Vertically/Split Horizontally: reset or split the viewer panes.Cmd+Dsplits vertically, andCmd+Shift+Dsplits horizontally.- Per-file row
Reloadaction: reload and re-decode that entry inOpen Files. File > Reload All: reload and re-decode all opened image entries.- Per-file row
Closeaction: close that entry inOpen Files. File > Close All: close all opened image entries.- Mouse wheel: zoom around cursor.
+/-: zoom in/out.- Left drag: pan in Image viewer.
W/A/S/D: pan in Image viewer.- In
Panorama viewer, mouse wheel changes horizontal FOV, left drag orbits yaw/pitch, andW/A/S/Dalso orbit yaw/pitch. Ctrl/Cmd+S: openFile > Export....- Hover: live probe sample.
- Left click: lock/unlock probe.
Shift+ left drag inImage viewer: create or replace the current ROI; drag the existing ROI body/handles to edit it.
- Display path: normal RGB uses
linear * 2^EV, then display-gamma encode for screen; colormap mode maps display luminance through the selected.npyLUT after colormap EV/gamma, range, zero-center, and reverse settings. Channel-display alpha is composited over the viewer checkerboard on screen in both RGB and colormap modes; exports preserve image alpha when present. When split component entries are selected, separateR,G, andBchannel choices duplicate the selected source into RGB, so display luminance equals that channel value. Grouped XYZ uses the same direct component display path as RGB, and grouped UV bindsUandVto red and green while leaving blue at zero. Split component Stokes entries derive the selected parameter from only the chosen component's Stokes channels before duplicating the scalar into RGB. Grouped RGB Stokes entries deriveR,G, andBindependently inNone, but collapse to the existing Rec.709-derived mono path inColormap. For angle Stokes modulation, the LUT color is converted to HSV, its value component is multiplied by the clamped paired degree value, and the result is converted back to RGB; AoLP can instead multiply HSV saturation whenSmodulation is selected. - Panorama path: the same display texture is reused, but the fragment shader interprets it as an equirectangular environment map, casts a view ray from yaw/pitch/HFOV, and fetches the matching source pixel with nearest-neighbor sampling before applying the normal RGB or colormap display transform.
- Colormap authoring in Python:
Register the file in
import numpy as np lut = np.array([ [1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], ], dtype=np.float32) np.save("public/colormaps/red_black_green.npy", lut) loaded = np.load("public/colormaps/red_black_green.npy")
public/colormaps/manifest.json:{ "colormaps": [ { "label": "Red / Black / Green", "file": "red_black_green.npy" } ] } - Texture sampling uses
NEARESTfor bothMIN_FILTERandMAG_FILTER. - EXR WASM is initialized through a local adapter module backed by a vendored wasm loader, avoiding app-level deep imports into
exrsinternals. - EXR metadata is parsed directly from header bytes before pixel decode because the current WASM decoder only exposes dimensions, layers, channels, and pixel data. Metadata parse failures do not block image loading.
- Performance path for large images/channel sets:
- channel thumbnail DOM updates are throttled to selection/image changes only,
- decoded CPU pixels are tracked in displayed memory usage, while the configurable display cache budget evicts retained display textures and materialized display buffers with eviction protection limited to the currently bound display channels,
- the active display texture buffer is reused across channel and layer switches,
- GPU upload uses
texSubImage2Dfor same-size updates.