🚧 Work in progress 🚧
FreeBrowse is a full-stack, web-based neuroimaging viewer and editor. It aspires to be a web-based version of the FreeSurfer tool FreeView.
This is a re-write of the initial FreeBrowse implementation, based off of the niivue fullstack demo.
Are you looking for the original FreeBrowse codebase? That now lives on the
branch v1.
Try out the 'serverless' version: https://freesurfer.github.io/freebrowse/
- A FreeSurfer segmentation overlayed on a t1-weighted anatomical MRI; with cortex and pial surfaces
- A FreeSurfer surface with a curvature overlay
- FreeSurfer surfaces with annotation overlays
- PET total volume of distribution (VT) overlaid on an MNI template
- 4D PET data overlayed on MNI305
- This one may take some time to load. NiiVue has optimizations for 4d data which have yet to be implemented
Frontend:
- React + TypeScript
- Vite (build tool)
- Tailwind CSS 4 + Radix UI components
- NiiVue (neuroimaging viewer)
Backend:
A FreeBrowse scene is a plain NiiVue document
(NVDocumentData, schema version 9).
The Download dialog writes it as JSON (text) or CBOR (binary);
loading detects the encoding, so either can be passed to ?nvd=, dropped onto
the viewer, or embedded in a single-file build.
- Downloaded documents are self-contained: every volume's voxels are embedded
(
volumes[i].data), so the file stands alone. Meshes are embedded as geometry too, but mesh overlays (curvature, thickness) are not yet persisted — see #43. - Saved-to-backend documents are linked: volumes are uploaded as
.nii.gzand the document references them by URL. The one FreeBrowse-specific save behaviour isretargetVolumeUrls(frontend/src/lib/nvd-volume-urls.ts), which points each volume entry at the path it is being uploaded to; a volume given no destination keeps its bytes. - Loading changes only what the document sets. FreeBrowse loads with
niivue's
fill: "current"policy, so a document that says nothing about, say, the crosshair colour or the view layout leaves the current viewer settings alone.
Documents from FreeBrowse ≤ 2.4 (classic niivue) do not load — the viewer
reports "This .nvd predates the niivue document schema". Convert them once
with the stdlib-only migration script, which writes <name>.v9.nvd next to
each input (originals are never touched) and handles documents with embedded
volumes:
python3 scripts/migrate-nvd.py path/to/docs/ # directory, file or glob
python3 scripts/migrate-nvd.py scene.nvd --cbor # binary output (needs cbor2)scripts/nvd-embed.py embeds a document into a single-file build (see
Build), and python3 -m pytest runs the script's tests.
FreeBrowse is published to npm as freebrowse:
npm install freebrowse @niivue/niivue@1.0.0-rc.38@niivue/niivue is a peer dependency: you create the NiiVue instance, FreeBrowse renders
its UI for it, and anything you do to the instance through niivue's API shows up in that UI
(see Driving FreeBrowse from your own code). A
classic-niivue (0.x) instance cannot be used — the package targets niivue 1.0.
Vanilla JavaScript (React is bundled; nothing else to install):
import { NiiVue } from "@niivue/niivue";
import { mountFreeBrowse } from "freebrowse";
import "freebrowse/style.css";
const nv = new NiiVue({ backgroundColor: [0, 0, 0, 1] });
const fb = mountFreeBrowse(document.getElementById("viewer"), { nv });
await nv.addVolume({ url: "https://niivue.com/demos/images/mni152.nii.gz" });
// ... later
fb.destroy();React 19 hosts (react / react-dom are peers on this entry, not bundled):
import { FreeBrowse, configureFreeBrowse } from "freebrowse/react";
import "freebrowse/style.css";
configureFreeBrowse({ backend: null }); // once, before the first render
<FreeBrowse nv={nv} hostInstance />mountFreeBrowse(container, options) options, all optional:
| option | default | meaning |
|---|---|---|
nv |
created for you | Your NiiVue instance. When given, FreeBrowse seeds its UI from the instance's settings and never pushes its own defaults onto it. |
backend |
null |
{ dataUrl, aiUrl } of a FreeBrowse backend. null hides Save-to-server, the Data/NVD tabs and AI. |
persist |
"freebrowse-user-settings" |
localStorage key for user preferences, or false to keep them in memory. |
readUrlParams |
false |
Honour ?nvd= / ?vol= on the host page URL. |
exposeGlobal |
false |
Also publish the instance as window.freebrowse.nv. |
downloadDisabled |
false |
Disable the Download button and niivue's save-to-disk methods. |
dragDrop |
true |
false turns off drag-and-drop loading (niivue's canvas drop and the drop zone) and shows the canvas from the start — for hosts that load data themselves. |
sidebar |
true |
Show the sidebar initially; the header button still toggles it. |
footer |
true |
Show the footer (coordinate readout) initially. |
The handle returned has nv and destroy(). One mount at a time: FreeBrowse's UI state is a
single store, so a second concurrent mount throws; destroy the first one. Styles are scoped to
the mounted element (.freebrowse-root) and never touch your page's html/body; import
freebrowse/style.css once. Working examples for both entries live in
frontend/examples/; they install freebrowse from npm.
Releasing (maintainers): bump version in frontend/package.json (the app and the package
share it), then from frontend/:
npm run build:package && npm run pack:check # publint + arethetypeswrong on the tarball
npm publish # a `-next.N` pre-release: npm publish --tag nextFreeBrowse's UI is a derived view of its NiiVue instance: the React store is
updated only by the events niivue emits, and FreeBrowse's own controls go
through the same path as any external caller. The live instance is exposed as
window.freebrowse.nv, so from the browser console (or an embedding page) you
can drive the viewer with niivue's API and watch the sidebar, toolbar and footer
follow:
const nv = window.freebrowse.nv;
await nv.addVolume({ url: "https://niivue.com/demos/images/mni152.nii.gz" });
await nv.setVolume(0, { colormap: "Hot", opacity: 0.8 }); // sidebar updates
nv.sliceType = 4; // toolbar switches to Render
await nv.loadDocument(fileOrUrl); // document path, same eventsSupported mutations: anything that emits an event: setVolume(i, {...}),
setMesh(i, {...}), setMeshLayerProperty, add/remove/move methods, the
flat setters (sliceType, showRender, isRadiological, crosshairColor,
drawOpacity, …), the drawing API and loadDocument.
Not supported: raw field writes such as nv.volumes[0].colormap = "Hot".
Niivue-mono volumes and meshes are plain objects, so nothing observes the write
and the UI will not update.
Requirements:
- Node.js (for frontend environment)
- npm (for frontend environment)
- pixi (for backend environment)
- git
This should install all dependencies on an Ubuntu 24.04 system:
sudo apt update && sudo apt upgrade -y
sudo apt install nodejs npm git -y
curl -fsSL https://pixi.sh/install.sh | sh
Then clone the repo:
git clone git@github.com:freesurfer/freebrowse.git
If you want to use the example data, run get-example-volumes.sh in the data/ folder:
cd freebrowse/data
./get-example-volumes.shIf you want to perform AI assisted annotations, run get-model-weights.sh in the models/ folder:
cd freebrowse/models
./get-model-weights.shcd frontend
npm installcd backend
pixi installThe frontend has several build targets:
Builds a fully serverless frontend (no backend required) that can be deployed to any static file host:
cd frontend
npm run build:serverlessThis creates a static build in frontend/dist/. You can set VITE_BASE_PATH to
control the base URL path (defaults to /).
Builds the serverless version configured for GitHub Pages deployment:
cd frontend
npm run build:githubOutput is in frontend/dist-github/.
Builds the serverless version configured for embedding inside JupyterLab:
cd frontend
npm run build:jupyterOutput is in frontend/dist-jupyter/. It also gets copied to jupyter/jupyterlab_freebrowse/static/freebrowse/. See the Jupyter Integration for more information.
Builds a single standalone HTML file that can be distributed alongside output of processing pipelines:
cd frontend
npm run build:singlefileThis creates a single standalone HTML file in frontend/dist-singlefile/ that
is compatible with the file:// protocol. Users can use this to view local
imaging data or self-contained NiiVue documents. The latest version of the
standalone HTML file is available at
https://freesurfer.github.io/freebrowse/downloads/freebrowse-<version>.html
Where <version> is the current version of freebrowse
The singlefile build is particularily useful with the python scripts in the
scripts directory
to embed a niivue document and related imaging data directly into a the single
HTML file. The resulting HTML file can be used to visualize the output of
processing pipelines, shared with collaborators and is offline compatible. For
a real-world example, see the petsurfer-bids
repository.
The GitHub Pages site only ever hosts the latest single-file build (each deploy replaces the whole site).
Every released version as of 2.4.7 is instead archived as a GitHub Release with its standalone HTML attached as a release asset. This is the place to grab an older build to test for regressions. To download a specific version:
# Direct download URL (replace the version):
# https://github.com/freesurfer/freebrowse/releases/download/v<version>/freebrowse-<version>.html
# Or with the GitHub CLI:
gh release download v2.4.7 --repo freesurfer/freebrowse --pattern 'freebrowse-*.html'Then open the file directly (file://) or serve the containing folder locally:
python3 -m http.server
# then browse to http://localhost:8000/freebrowse-2.4.7.htmlReleases are created automatically by the deploy workflow whenever the
frontend/package.json version is bumped on main.
Builds the frontend for use with the FastAPI backend:
cd frontend
npm run buildSome deployment settings are controlled at build time via Vite environment variables. These are read-only at runtime.
| Variable | Values | Effect |
|---|---|---|
VITE_DISABLE_DOWNLOAD |
true / unset |
Disables the Download button and no-ops niivue's save-to-disk methods (saveMesh, saveBitmap, saveDocument, and saveVolume / saveDrawing whenever they are given a filename). In-memory exports (saveVolume({filename: ""}), serializeDocument()) keep working, so backend Save, AI sessions, save-drawing and edit-volume are unaffected. For deploying into secure environments where local data export should be turned off. |
VITE_SERVERLESS |
true / unset |
Builds for the file:// protocol with no backend (set automatically by build:serverless). Also disables the backend Save button. |
VITE_BASE_PATH |
e.g. /freebrowse/ |
Base URL path for routing/assets. |
How a file opens from the Jupyter file browser (document tab or browser tab, who downloads it) is not a build variable: it is runtime Jupyter server configuration, see Jupyter Integration → Configuration.
VITE_DISABLE_DOWNLOAD composes with every build target. Just prepend it to
whichever build you run:
# Static / serverless
VITE_DISABLE_DOWNLOAD=true npm run build:serverless
# JupyterLab embed
VITE_DISABLE_DOWNLOAD=true npm run build:jupyter
# GitHub Pages
VITE_DISABLE_DOWNLOAD=true npm run build:github
# Single standalone HTML file
VITE_DISABLE_DOWNLOAD=true npm run build:singlefile
# Full stack (with backend)
VITE_DISABLE_DOWNLOAD=true npm run buildNote: While VITE_DISABLE_DOWNLOAD may stop well-intentioned users from
exporting data from a secure environment, it is not a guarantee against a
malicious user, who can still read pixels from the GPU or intercept data via t
he browser console.
This hot reloads the backend when changes are made to the code.
cd backend
pixi run devcd frontend
npm run devThen navigate to http://localhost:5173/
To enable GitHub Pages:
- Go to your repo on GitHub: Settings --> Pages
- Under "Build and deployment", set Source to "GitHub Actions"
- Push changes to main
You should then be able to view the 'serverless' version of your changes at https://{github-username.github.io/freebrowse/
cd frontend
npm run buildFreeBrowse can be used as a NIfTI file viewer inside JupyterLab or Jupyter
Notebook 7. Clicking a .nii, .nii.gz or .nvd (niivue document) file in the
file browser opens it in FreeBrowse, either in a document tab inside JupyterLab
(the default) or in a new browser tab (see Configuration).
Create and activate a conda environment with either JupyterLab or Notebook
7 and run pip install -e . from the jupyter/ directory.
The file jupyter/environment.yml contains a sample maximal environment that
contains both JupyterLab, Jupyter Notebook and nodejs (for development) as well
as ipyniivue to run niivue directly
inside of Jupyter notebooks.
cd ./jupyter
conda env create -f environment.ymlStart JupyterLab or Notebook:
jupyter lab # if using JupyterLab
jupyter notebook # if using Notebook 7Navigate to a directory containing .nii, .nii.gz or .nvd files, then either:
- Double-click a file to open it in FreeBrowse
- Right-click a file and select Open in FreeBrowse
Both open the file the same way: in a FreeBrowse document tab inside JupyterLab by default, or in a new browser tab if the deployment is configured that way (see Configuration).
ipyniivue is also installed in the example
freebrowse-jupyter environment. See the example notebooks repository
for examples on how to use niivue directly inside jupyter
By default the file is downloaded by the JupyterLab page through Jupyter's contents service and authenticated server connection, so JupyterHub base paths and filenames containing spaces or Unicode are supported. The viewer receives a temporary blob URL and the original filename. Downloads are cancelled and blob URLs released when a file is renamed or its tab closes (or, for a viewer in its own browser tab, when that tab closes). Viewer pages and assets require the same authentication as the Jupyter server.
Two deployment settings control how files open. They are server-side, sysadmin-level settings: the Jupyter server reads them once at startup and pushes them to every JupyterLab page, so a change needs a server restart and there is nothing for users to set in JupyterLab's settings editor.
| Setting | Values | Default | Meaning |
|---|---|---|---|
open_in |
document, browser |
document |
document: a FreeBrowse tab inside JupyterLab. browser: a new browser tab (the JupyterLab tab that briefly appears closes itself once the viewer tab has the file). |
file_access |
blob, url |
blob |
blob: the JupyterLab page downloads the file through its authenticated connection and hands the viewer an in-memory copy. url: the viewer fetches the file from Jupyter's /files/ endpoint itself, using the browser's Jupyter cookie plus the session's _xsrf query parameter that Jupyter requires on /files/ requests. |
When to pick what:
blobworks with any Jupyter authentication scheme (JupyterHub included) but keeps a copy of each open file in the JupyterLab page, and a viewer in its own browser tab cannot be reloaded once the JupyterLab page that opened it is gone.urlholds no copy and viewer tabs reload fine, but it needs the browser to send the Jupyter cookie along with the viewer's request, which some JupyterHub setups do not allow. If files fail to load withurl(a403on/files/in the server log), switch back toblob.browserneeds the browser to allow pop-ups from the JupyterLab site. If they are blocked, the JupyterLab tab stays open and says so.- Notebook 7 opens every file in its own browser tab anyway, so it ignores
open_inand honoursfile_access.
There are two ways to set them. An environment variable always wins over a config file; a config file at a higher-priority directory wins over one at a lower-priority directory.
Environment variables FREEBROWSE_OPEN_IN and FREEBROWSE_FILE_ACCESS,
set wherever the Jupyter server is launched. Users cannot override them, which
makes them the right choice for JupyterHub and containers:
# try it once
FREEBROWSE_OPEN_IN=browser jupyter lab# Docker image
ENV FREEBROWSE_OPEN_IN=browser FREEBROWSE_FILE_ACCESS=url# jupyterhub_config.py: every single-user server gets them
c.Spawner.environment = {
"FREEBROWSE_OPEN_IN": "browser",
"FREEBROWSE_FILE_ACCESS": "url",
}# zero-to-jupyterhub values.yaml
singleuser:
extraEnv:
FREEBROWSE_OPEN_IN: browser
FREEBROWSE_FILE_ACCESS: urlConfig file named jupyter_server_config.json (or .py) in one of the
directories jupyter --paths lists under config:. The directory sets the
scope, the file name is the same at every level:
$ jupyter --paths
config:
~/.jupyter <- one user; overrides the ones below
~/.local/etc/jupyter
$CONDA_PREFIX/etc/jupyter <- one conda env or image
/usr/local/etc/jupyter
/etc/jupyter <- whole machine
mkdir -p "$CONDA_PREFIX/etc/jupyter"
cat > "$CONDA_PREFIX/etc/jupyter/jupyter_server_config.json" <<'EOF'
{
"FreeBrowse": {
"open_in": "browser",
"file_access": "url"
}
}
EOFor, in jupyter_server_config.py:
c.FreeBrowse.open_in = "browser"
c.FreeBrowse.file_access = "url"Leave out any setting you want at its default. If the file already exists, add
the FreeBrowse block next to its other keys. Do not put the file in
jupyter_server_config.d/: Jupyter only reads that directory for the list of
extensions to enable, and a FreeBrowse block there is ignored. Because the
per-user directory has the highest priority, a user can override a config file
from ~/.jupyter; use the environment variables, or start the server with
JUPYTER_PREFER_ENV_PATH=1, if that matters.
Restart the server, then confirm with the startup log line
jupyterlab_freebrowse server extension loaded (open_in=..., file_access=...)
or with jupyter server --show-config, which lists the loaded files and the
FreeBrowse values. A value outside the table above (for example tab) stops
the server extension from loading, with a TraitError in the log naming the
allowed values. jupyter lab --FreeBrowse.open_in=... on the command line is not
supported; use the environment variable instead.
Run the Jupyter integration regression tests after installing the JavaScript and Python test dependencies:
cd jupyter
jlpm install
python -m pip install -r requirements-test.txt
jlpm testThese tests exercise document opening, authenticated file downloads, filename handling, rename/disposal races, and real Jupyter HTTP authentication for viewer assets at root and JupyterHub base paths.
The FreeBrowse bundle served inside JupyterLab
(jupyter/jupyterlab_freebrowse/static/freebrowse/) is a build output and is
not committed: a fresh checkout has to generate it before installing the
extension, and any frontend change needs it regenerated before it becomes
visible in notebooks. (The server extension logs a warning at startup if the
bundle is missing.)
cd frontend
npm run build:jupyterTo embed FreeBrowse in a secure environment where data export is disabled, set
VITE_DISABLE_DOWNLOAD=true for the build (see
Deployment configuration):
cd frontend
VITE_DISABLE_DOWNLOAD=true npm run build:jupyterTo re-install the JupyterLab extensions:
cd jupyter
jlpm install
jlpm build
pip install -e .FreeBrowse was generously funded by Gates Ventures. The original implementation was performed by zuehlke.
Version 2 is based off of the niivue fullstack demo which was developed during a Google Summer of Code Project in close collaboration with the niivue team.