Inspecting MRSI spectra interactively
I want to click around a T1 slice and see the MRSI spectrum under my cursor update live — without keeping a Python kernel running.
inspect_MRSI_spectra() shows a T1 image with an
MRSI overlay next to a spectrum plot. Click a voxel, use the arrow keys, or drag the slice slider,
and the spectrum panel updates to match. Every anatomical slice is pre-rendered once and every
voxel’s spectrum is precomputed into a single buffer embedded in the widget, so all of that
interaction runs client-side — it keeps working after this page is built into a static site, the
same way the slider in Displaying image arrays of any shape does.
| Function | What it does here |
|---|---|
inspect_MRSI_spectra() | builds and displays the interactive inspector |
from mnutils.GESeries import MRISeries, MRSISeries
from mnutils.plotting.images import inspect_MRSI_spectra
from mnutils.testing import build_fake_exam
nist_data = build_fake_exam("nist_phantom_exam") / "data"1. Load a matching T1/MRSI pair¶
Same fake phantom exam as Drawing an MRSI voxel on an anatomical slice — series 2 is the T1-weighted anatomical scan, series 8 the MRSI acquisition covering it.
t1 = MRISeries(nist_data, 2)
mrsi = MRSISeries(nist_data, 5)t1.display()2. Display the inspector¶
blocky=True (the default) resamples the raw, un-interpolated MRSI grid — one cell per acquired
voxel — rather than the smoothed NIfTI, so the overlay and the voxel picker line up with what was
actually measured. autophase=True (also the default) autophases each voxel’s spectrum before
display; pass magnitude=True for the magnitude spectrum instead.
inspect_MRSI_spectra(t1, mrsi)Click anywhere on the left image to move the selected voxel there, use the arrow keys to nudge it by one voxel, or drag the slider below the image to change the anatomical slice — the MRSI slice shown updates to whichever one that anatomical slice actually falls inside, and the spectrum panel on the right always reflects the currently selected voxel.
The spectrum panel carries its own two range sliders: the horizontal one under the plot sets the ppm window (the whole acquired sweep is embedded, so it reaches every sample), and the vertical one beside it sets the y limits. Touching the vertical slider ticks Fixed y-axis for you — an axis you dialled in by hand only stays meaningful if it stops rescaling itself — and unticking the box hands the axis back to the automatic per-voxel scaling. Leaving the box ticked without touching the slider instead scales the axis to the loudest voxel anywhere in the grid, so amplitudes stay comparable as you step from voxel to voxel.
What embedding the whole sweep costs
The widget used to crop the spectra to a ppm window before embedding them, on the assumption that a
wide sweep would make the payload unwieldy — a 2H acquisition at 3 T spans ~255 ppm, of which the
interesting ±20 ppm is a sixth. In practice the compressed buffer is small enough either way: for
the 16×16×16 grid at 700 points on this page it is ~6.7 MB embedded against ~1.1 MB cropped, and
the widget stays responsive. Not cropping is what lets the ppm slider reach every acquired sample
instead of a window fixed at call time, so the crop (and its ppm_range argument) is gone.
Grids large enough to matter still log a warning while encoding.
3. Magnitude vs. autophased vs. raw real spectrum¶
The three display modes are mutually exclusive; magnitude=True takes priority; the spectrum
label shown in the widget’s header names whichever one is active:
inspect_MRSI_spectra(t1, mrsi, magnitude=True)inspect_MRSI_spectra(t1, mrsi, autophase=False)