Metadata-Version: 2.5
Name: fermiviewer
Version: 0.1.30
Summary: Electron-microscopy image analysis — Python/FastAPI port of fermi-viewer
Author: Paige Quarterman
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.138.0
Requires-Dist: h5py>=3.11
Requires-Dist: jsonschema>=4.18
Requires-Dist: numpy>=2.1
Requires-Dist: orjson>=3.10
Requires-Dist: pillow>=11
Requires-Dist: pydantic>=2.9
Requires-Dist: python-multipart>=0.0.18
Requires-Dist: pywebview>=6.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: scikit-image>=0.24
Requires-Dist: scipy>=1.14
Requires-Dist: tifffile>=2024.8
Requires-Dist: uvicorn[standard]>=0.32
Description-Content-Type: text/markdown

<p align="center">
  <img src="docs/images/fermiviewer-logo.svg" width="760" alt="FermiViewer — electron microscopy image analysis">
</p>

<p align="center">
  <a href="https://pypi.org/project/fermiviewer/"><img src="https://img.shields.io/pypi/v/fermiviewer" alt="PyPI"></a>
</p>

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](https://github.com/pquarterman17/fermi-viewer)
(MATLAB), and the long-term home of this project line.

![FermiViewer — multi-format image viewer with EELS/EDS/diffraction analysis and a measurement suite](docs/images/overview.png)

**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, Bruker Nanoscope AFM
(.spm/.000), 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](../../releases/latest) 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:

```powershell
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](https://docs.astral.sh/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](../../releases/latest) and extract it — no git needed.

```bash
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:
>
> ```powershell
> New-Item -ItemType Junction -Path .venv -Target "$env:LOCALAPPDATA\fermiviewer-venv"
> ```
>
> (`[tool.uv] link-mode = "copy"` is already set in `pyproject.toml`;
> the junction prevents sync-lock races during installs.)

> **Windows Store Python recovery:** a `.venv` created from the Microsoft
> Store interpreter can stop building wheels after the Store updates Python;
> the old executable is a stale reparse point, so `uv` fails while copying it
> into an isolated build environment (`os error 1920`). Recreate the environment
> with a uv-managed interpreter instead:
>
> ```powershell
> 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.py
> ```
>
> The 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: `--clear` empties the junction's
> target without replacing the link, so `.venv` stays out of OneDrive.

### Option 4 — install from PyPI (`pip` / `uv`)

FermiViewer is [on PyPI](https://pypi.org/project/fermiviewer/) 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:

```powershell
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+.

1. Download `fv-offline-win64.zip` (or `-macos-arm64` / `-linux-x64`)
   from the [latest release](../../releases/latest) and carry it over.
2. Extract anywhere writable and run `py install.py`
   (macOS/Linux: `python3 install.py`).
3. 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):

```bash
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 &amp; Accessibility](https://github.com/pquarterman17/fermiviewer/wiki/Keyboard-and-Accessibility)**.

---

## Documentation

Feature walkthroughs, screenshots, and how-tos live in the
**[project wiki](https://github.com/pquarterman17/fermiviewer/wiki)**:

- **[Getting Started](https://github.com/pquarterman17/fermiviewer/wiki/Getting-Started)** — install and your first image
- **[Viewing &amp; Display](https://github.com/pquarterman17/fermiviewer/wiki/Viewing-and-Display)** — colormaps, the calibrated color scale, scale bar
- **[Measurements](https://github.com/pquarterman17/fermiviewer/wiki/Measurements)** — line/box profiles, distances, ROIs, annotations
- **[Analysis Workshops](https://github.com/pquarterman17/fermiviewer/wiki/Analysis-Workshops)** — Elemental Analysis (EELS+EDS), diffraction, 4D-STEM
- **[Structure &amp; Grains](https://github.com/pquarterman17/fermiviewer/wiki/Structure-and-Grains)** — grain segmentation (incl. paint-to-train), cross-section layer &amp; interface-roughness analysis
- **[4D-STEM](https://github.com/pquarterman17/fermiviewer/wiki/4D-STEM)** — pixelated-detector datasets, probing, virtual-detector imaging
- **[Scripting &amp; Automation](https://github.com/pquarterman17/fermiviewer/wiki/Scripting-and-Automation)** — Python API, headless `fv --script`, batch/watch/macro
- **[AFM Support](https://github.com/pquarterman17/fermiviewer/wiki/AFM-Support)** — Bruker Nanoscope height maps + Z-scale color bar
- **[Keyboard &amp; Accessibility](https://github.com/pquarterman17/fermiviewer/wiki/Keyboard-and-Accessibility)** — working without a mouse, screen-reader support, themes &amp; density
- **[Supported Formats](https://github.com/pquarterman17/fermiviewer/wiki/Supported-Formats)**

| Cross-section layer stack | Trained grain classifier |
|---|---|
| [![Cross-section Layers workshop — layer-stack band diagram with calibrated thickness table](docs/images/layers-workshop.png)](https://github.com/pquarterman17/fermiviewer/wiki/Structure-and-Grains) | [![Trained grains — painted class examples and a pixel-classification preview](docs/images/grains-trained.png)](https://github.com/pquarterman17/fermiviewer/wiki/Structure-and-Grains) |

| Calibrated color scale (AFM height) | EELS analysis |
|---|---|
| [![AFM height map with calibrated color scale](docs/images/afm-colorscale.png)](https://github.com/pquarterman17/fermiviewer/wiki/AFM-Support) | [![EELS analysis workshop](docs/images/eels-workshop.png)](https://github.com/pquarterman17/fermiviewer/wiki/Analysis-Workshops) |

---

## Scripting

Everything the GUI can do is also a headless Python surface —
`fermiviewer.api` — the same engine the server and a notebook both use,
with no FastAPI/browser dependency:

```python
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": {...}}, ...])` | Run a whole recipe (the same shape a saved batch preset uses) in one call |
| `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`, `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`](docs/api-reference.md)**.
Complete worked scripts (filters + roughness, EDS quantification, and the
same recipe against real corpus data): **[`examples/`](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:

```bash
fv --script recipe.json scan1.dm4 scan2.dm4 --out results/
```

```json
{
  "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

```bash
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/` and `calc/` 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 `oracle`
  test group; PyInstaller only in the `bundle` build 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`):

```bash
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):

```bash
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/health` answers, 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 so `frontend/dist/loading.html` is 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` |
| `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`).
