Electron-microscopy image analysis: TEM/STEM image viewing, EELS / EDS / diffraction analysis, measurements, and image processing. Python (FastAPI) backend + React frontend + Tauri desktop shell.
Ground-up port of fermi-viewer (MATLAB), and the long-term home of this project line.
Formats: DM3/DM4/DM5 (Gatan), EMD (Velox/NCEM), BCF (Bruker), SER (TIA), MRC, HDF5 family (.hspy, NeXus .nxs/.nx5, generic .h5/.hdf5), MSA/EMSA spectra, JEOL Analysis Station (.img/.map/.pts), EDAX .spc, Lispix .rpl/.raw, TIFF (pixel size and stage tilt read from Thermo Fisher/FEI SEM-FIB, Zeiss SmartSEM and ImageJ tags), PNG/JPEG/BMP/GIF, headerless RAW, AFM/SPM (Bruker Nanoscope .spm/.000, Gwyddion .gwy/.gsf, Asylum .ibw, JPK .jpk, WSxM .top/.stp, Nanosurf .nid, NT-MDT .mdt, Nanonis .sxm), 4D-STEM (Merlin .mib, 4D HyperSpy). Analysis: EELS (background, maps, quantification ± σ, thickness, Kramers–Kronig, Fourier-log, model-based peak fitting, SVD), EDS (Cliff–Lorimer / ZAF / ζ-factor composition with mass-thickness ± σ, maps, artifacts, peak fitting, composite overlays), 4D-STEM virtual-detector imaging (BF/ABF/ADF/custom apertures), diffraction (camera-length & ellipse calibration, CIF phase import, spot detection, phase indexing, d-spacings), GPA strain, CTF estimation, atom columns, particles, grains (k-means / watershed / paint-to-train classifier), cross-section layer & interface-roughness analysis, FFT filtering, drift alignment, batch recipes with folder watch and macro record/replay, Python scripting API, and a full measurement/annotation suite.
Install
Option 1 — Windows installer (recommended)
Grab FermiViewer_x64-setup.exe from the
latest release and run it. It is fully
self-contained (~47 MB) — no Python, no Node, nothing else required.
Launch FermiViewer from the Start menu; closing the window shuts
everything down.
The installer is currently unsigned, so Windows SmartScreen may warn on first run — choose More info → Run anyway.
Option 2 — standalone server (no installer)
Download fv-server-win64.zip from the same release, unzip anywhere, and
run:
fv-server\fv-server.exe
This starts the full app at http://127.0.0.1:8000 and opens your
browser. The server exits on its own when the last tab closes
(--no-auto-shutdown to keep it running, --no-browser to skip the
auto-open).
Option 3 — from source
Requirements: uv (a suitable Python is
fetched automatically) and Node 22+ for the frontend (see .nvmrc). Get
the code with git clone, or download Source code (zip) from any
release and extract it — no git needed.
git clone https://github.com/pquarterman17/fermiviewer
cd fermiviewer
cd frontend && npm ci && npm run build && cd .. # build the web UI (once)
uv sync # install backend deps
uv run fv # → http://127.0.0.1:8000
That is the whole build — after the first time, uv run fv is all you
run. uv run fv --desktop opens a native window instead of the browser
(pywebview). Building from source needs internet (PyPI + npm) — for a
machine without it, use the ready-made offline bundle in Option 5,
which skips Node and PyPI entirely. For the Tauri shell / installer
build, see Packaging below.
For one-click local testing, use Run-FermiViewer.bat on Windows or
Run-FermiViewer.command on macOS. Both launch from the repository
directory and accept the same optional flags as uv run fv when invoked
from a terminal, for example Run-FermiViewer.bat --dev.
OneDrive checkouts (Windows): if the repo lives in a synced folder, move the venv out of OneDrive's reach before the first sync:
New-Item -ItemType Junction -Path .venv -Target "$env:LOCALAPPDATA\fermiviewer-venv"(
[tool.uv] link-mode = "copy"is already set inpyproject.toml; the junction prevents sync-lock races during installs.)
Windows Store Python recovery: a
.venvcreated from the Microsoft Store interpreter can stop building wheels after the Store updates Python; the old executable is a stale reparse point, souvfails while copying it into an isolated build environment (os error 1920). Recreate the environment with a uv-managed interpreter instead:uv python install 3.13 uv venv --managed-python --python 3.13 --clear .venv uv sync --group dev --managed-python --python 3.13 uv run pytest -q tests\test_offline_install.pyThe final command verifies the wheel and baked-SPA path that exposes the stale-interpreter problem. It should report four passing tests. This is safe with the OneDrive junction above:
--clearempties the junction's target without replacing the link, so.venvstays out of OneDrive.
Option 4 — install from PyPI (pip / uv)
FermiViewer is on PyPI with the web UI already baked into the wheel — no Node, no checkout, no release download; any 64-bit Python 3.10+ works. Install once, then launch from any terminal — including from a folder of images:
uv tool install fermiviewer # exposes `fermiviewer` on PATH
fermiviewer # launch from anywhere
fermiviewer C:\data\session-42 # …or point it at a folder
pip install fermiviewer works too (same wheel; use pip if you don't
have uv). From a source checkout, the equivalent is
uv tool install --from . fermiviewer.
fermiviewer opens a browser tab once the server is confirmed up (no
more racing a cold start), and the in-app Open dialog defaults to the
folder you launched from. If a copy is already running it just opens a
new tab; if port 8000 is taken by another app it steps to the next free
port. fv remains as a short alias for the same entry point.
Option 5 — air-gapped / offline machine (source install, no exe)
For machines with no internet access — or where IT policy rules out
running downloaded executables — each release ships per-OS
fv-offline-*.zip bundles: the FermiViewer wheel (web UI already baked
in) plus every dependency as pre-built wheels and a standard-library-only
installer. The only requirement on the target is a 64-bit Python 3.10+.
- Download
fv-offline-win64.zip(or-macos-arm64/-linux-x64) from the latest release and carry it over. - Extract anywhere writable and run
py install.py(macOS/Linux:python3 install.py). - Launch with the generated
FermiViewer.bat/./fermiviewer.
Everything lives in that one folder (nothing downloaded, no admin
rights); deleting the folder uninstalls it. Full details, including how
to review the exact pinned dependency versions, are in the bundle's
README-OFFLINE.md. To build a bundle yourself from a checkout (on a
connected machine of the same OS):
uv run python tools/offline/make_bundle.py
Usage
| Command | What it does |
|---|---|
fermiviewer / fv |
API + SPA on :8000, opens the browser once healthy, exits when the last tab closes |
fermiviewer <dir> |
…and defaults the in-app Open dialog to <dir> |
fermiviewer --desktop |
Native window (pywebview), exits on close |
fermiviewer --dev |
Vite HMR (:5173) + auto-reloading backend, one terminal |
fermiviewer --no-browser --no-auto-shutdown |
Plain server, stays up |
Open files via File → Open… (the launch-folder list when started from one, otherwise the native picker), drag-and-drop, or File → Open by Path… for large files already on the server's disk. Press ? in the app for the full keyboard map, ⌘K for the command palette.
Keyboard & accessibility
The whole interface works without a mouse. Tab moves between the major regions — menu bar, library, stage, inspector — and each region is a single tab stop, so the arrow keys move within it:
| Where | Keys |
|---|---|
| Menu bar | ← → between menus · ↓ / Enter to open · ↑ ↓ · Home/End within · → / ← into and out of submenus · Esc to close |
| Image library | ↑ ↓ ← → and Home/End to move · Enter/Space to select · Ctrl/⌘+Enter/Space to add · Shift+Enter/Space for a range · Shift+F10 for the context menu |
| Dialogs | Tab / Shift+Tab cycle inside the dialog · Esc closes · focus returns to whatever opened it |
| Command palette | ⌘K / Ctrl-K to open · ↑ ↓ to move · Enter to run |
Unavailable menu items are skipped while arrowing, and app-wide shortcuts are suspended while a dialog is open. Controls carry the ARIA roles screen readers expect — menu bar, multi-selectable listbox, modal dialog, combobox — icon-only buttons carry text labels, and toggles report their pressed state. Dark, light, and system themes, five Color scheme accents, and three interface densities live in Preferences → Appearance.
Full reference: Keyboard & Accessibility.
Documentation
Feature walkthroughs, screenshots, and how-tos live in the project wiki:
- Getting Started — install and your first image
- Viewing & Display — colormaps, the calibrated color scale, scale bar
- Measurements — line/box profiles, distances, ROIs, annotations
- Analysis Workshops — Elemental Analysis (EELS+EDS), diffraction, 4D-STEM
- Calibration & Standards — per-axis calibration, instrument profiles, known standards, measured k/ζ/σ factors, and QC
- Structure & Grains — grain segmentation (incl. paint-to-train), cross-section layer & interface-roughness analysis
- 4D-STEM — pixelated-detector datasets, probing, virtual-detector imaging
- Scripting & Automation — Python API, headless
fv --script, batch/watch/macro - AFM Support (in-repo copy) — Bruker, Gwyddion, Asylum, JPK, WSxM, Nanosurf, NT-MDT and Nanonis scans; levelling, ISO 25178 surface analysis, step height, grain heights; force curves and force maps
- Keyboard & Accessibility — working without a mouse, screen-reader support, themes & density
- Supported Formats
| Cross-section layer stack | Trained grain classifier |
|---|---|
| Calibrated color scale (AFM height) | EELS analysis |
|---|---|
Scripting
Every registered operation is also a headless Python surface —
fermiviewer.api — the same engine the server and a notebook both use,
with no FastAPI/browser dependency. Registered-op coverage is 75 of 83
analysis endpoints; the eight remaining endpoints are 4D-STEM/tomography
work parked behind roadmap items 8 and 9. The generated audit of exactly
what is reachable headlessly is
docs/operation-coverage.md:
import fermiviewer.api as fv
img = fv.open("scan.dm4") # any registered format
denoised = img.gaussian(sigma=2).image.median(window_size=3).image
stats = denoised.image_stats()
print(stats.value) # {'mean': ..., 'std': ..., 'min': ..., 'max': ..., 'shape': ...}
csv_bytes = stats.to_csv() # a one-row CSV, ready to write or upload
print(denoised.methods()) # "scan.dm4 was processed with fermiviewer X.Y.Z:
# gaussian (sigma=2.0); median (window_size=3)."
| Surface | What it gives you |
|---|---|
fv.open(path) / fv.Session() |
Load any registered format into an Image; a Session tracks opened + derived images and their lineage |
Image.<op>(**params) |
Every registered op as a method — .gaussian(), .eds_quantify(), .roughness(), ... — returns a Result |
Image.pipeline([{"op": ..., "params": {...}, "inputs": {...}}], reference=other) |
Run a whole recipe, including portable named auxiliary inputs (the same shape a saved batch preset uses) |
Result.value / .image / .params |
A plain value (scalar/table), a derived Image, and the resolved parameters |
Result.to_csv() / .to_json() |
Export a dict-shaped result as a table |
Image.methods() / .provenance_json() |
A reproducible methods paragraph / full JSON ancestry for that image's pipeline |
| op categories | filter, geometry, analysis, spectral, eels, eds, diffraction — see fv.ops() or docs/api-reference.md for the full, generated list with every parameter |
Full generated reference (every class, method, and op parameter,
regenerated from source): docs/api-reference.md.
Complete worked scripts (filters + roughness, EDS quantification, and the
same recipe against real corpus data): examples/.
For a no-code, headless batch run over a folder of files, fv --script
replays a saved recipe (the exact .fvbatch.json the GUI's batch dialog
exports, or a bare JSON recipe) with no server and no browser:
fv --script recipe.json scan1.dm4 scan2.dm4 --out results/
{
"steps": [
{ "op": "gaussian", "params": { "sigma": 1.5 } },
{ "op": "median", "params": { "window_size": 3 } },
{ "op": "image_stats", "params": {} }
]
}
Each input gets its derived image (TIFF), every value-producing step's
result (CSV + JSON), and a provenance JSON written into --out — see
fv --script --help for directory/glob input expansion and defaults.
Development
uv sync --group dev # + ruff, mypy, pytest
uv run pytest # golden-verified; realdata tests
# auto-skip if the corpus is absent
uv run pytest -m "eels and golden" # marker-scoped
uv run ruff check src tests
uv run mypy src
cd frontend
npm run dev # Vite on :5173, /api proxied
npx tsc --noEmit && npm run build
Hard rules (enforced by tests/test_repo_integrity.py):
io/andcalc/are pure libraries — they never import FastAPI/Pydantic;routes/are thin adapters.- 500-line ceiling per source module.
- No GPL runtime dependencies (rosettasciio lives only in the
oracletest group; PyInstaller only in thebundlebuild group). - Physics constants port verbatim from the MATLAB reference — annotated do-not-"fix" items are calibrated/intentional.
Packaging
A tagged push builds and publishes everything automatically
(.github/workflows/release.yml):
git tag v0.2.0 && git push origin v0.2.0
# → Release with the per-OS installers (.exe / .dmg / .deb), the
# fv-server-* standalone archives, the fv-offline-* air-gapped
# bundles, and the auto-updater's latest.json
# → PyPI wheel (SPA baked in), published via trusted publishing after
# a clean-venv install + boot smoke test
Local equivalents (Windows; needs Rust + VS Build Tools for the shell):
cd frontend && npm run build && cd ..
uv sync --group bundle
uv run pyinstaller tools/bundle/fv-server.spec --noconfirm --distpath dist-sidecar
cd src-tauri
npx @tauri-apps/cli@^2 build --config \
'{"bundle":{"resources":{"../dist-sidecar/fv-server":"fv-server"}}}'
The desktop shell shows a loading splash and only navigates to the app once
/api/healthanswers, so a slow first start no longer lands on a "can't reach this page" error. This lives in the Rust shell — installed copies must be rebuilt and reinstalled to pick it up (rebuild the SPA first sofrontend/dist/loading.htmlis bundled).
Project docs
| Doc | Purpose |
|---|---|
docs/parity_report.md |
Three-way parity vs the MATLAB reference + design prototype |
docs/w3_imaging_audit.md |
Per-algorithm port decisions (map / port / hybrid) |
docs/api-reference.md |
Generated reference for the fermiviewer.api scripting surface — every class/method/op, regenerate with tools/gen_api_reference.py |
docs/operation-coverage.md |
Generated GUI/headless parity audit — every analysis endpoint, its GUI surface, registered-op reach, ADR 0004 result kinds, and wave; regenerate with tools/gen_coverage_table.py |
examples/ |
Worked fermiviewer.api scripts (filters + roughness, EDS quantification, real-corpus recipe) |
tests/golden/ |
Frozen MATLAB reference values (see tools/matlab/) |
plans/ (local-only) |
Per-machine working plans (gitignored, fermi-viewer convention) |
License
Apache-2.0. Bundled JetBrains Mono is under the SIL OFL
(frontend/public/fonts/OFL.txt).
Metadata
Release files for fermiviewer 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fermiviewer-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / fermiviewer-0.10.0-py3-none-any.whl
| Download URL | fermiviewer-0.10.0-py3-none-any.whl |
|---|---|
| Size | 1.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f2c85b3512b8de91d390dc5caa7558f40138fe5f6a931955ba6fe2ce3e2de0fb
|
|
BLAKE2b-256 checksum How to use checksums |
e5a06f74054eb10ffc6f14c5b512aea70173ed93bc49ec60da408c3abf2f2d3d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log