Stability & Roadmap
neuroimjs is pre-1.0, so minor releases may change APIs. See the release notes for version-specific changes and upgrade instructions. The tables below describe the supported surfaces and known limitations; the Space embedding API is supported within its documented data and geometry contract.
Legend:
stable — solid, tested, safe to depend on. known issue — works in the common case but has a confirmed bug or limitation; verify for your data. unavailable — not implemented yet; don't use.
Recently fixed ✅
These were correctness bugs in earlier drafts of the library and are now fixed, with tests guarding them. If older docs or comments still warn about them, those notes are out of date:
- NIfTI intensity scaling on read —
readVolandreadNiftiArrayBufferapplyscl_slope/scl_inter. A slope of 0 or a non-finite slope means no scaling: both fields are ignored, as the NIfTI spec and nibabel require. - NIfTI big-endian data — big-endian image data is byte-swapped on read.
NeuroSpace.reorient()— preserves world coordinates (the reoriented affine is composed correctly).getSliceAt()/ live crosshair — samples through the reoriented grid; no more identity-transform slices.- Colormap NaN handling — non-finite voxels render transparent, not opaque black.
getRange/dilate/erode— use ±Infinity instead of the oldNumber.MIN_VALUEmin/max bug.getRangealso skips NaN.dilate/erodedo not: a NaN voxel turns its whole neighbourhood into NaN.Resampler.transform()— implemented (builds the transform matrix and resamples); no longer a no-op.- Cubic/Lanczos resampling at volume boundaries — boundary taps are linearly extrapolated, fixing the old outer-voxel bias.
temporalFilter()— performs real per-voxel temporal filtering; no longer a silent no-op.sphericalROI/ searchlight radius — radius is interpreted in mm and is spacing-aware per axis (anisotropy-correct).- Searchlight sphere membership — since 0.6.0, searchlights with
nonzero: true, andrandomSearchlight/bootstrapSearchlightby default, clip each sphere to the mask, not just its centre. splitReducegeometry — the output volume keeps the input's 3D geometry, including the affine.
Visualization
| Feature | Status | Notes |
|---|---|---|
mountSpace | supported | Embed subject-map comparison through neuroimjs/space. Requires same-origin frame assets and aligned axis-aligned RAS+ grids; analysis internals remain private. |
SimpleOrthogonalViewer | stable | Headline 3-view viewer. |
SingleSliceViewer + ViewSynchronizer | stable | Composable views for custom layouts. |
ColorMap / ColorMapFactory | stable | Presets + custom gradients; non-finite → transparent. |
VolLayer / VolStack | stable | Multi-layer compositing; layers on different grids are aligned in world space by default. See Multi-Layer Alignment. |
OrthogonalImageViewer | stable | Lower-level; prefer SimpleOrthogonalViewer. |
LayerControlPanel / OverlayReviewPanel | stable | Lit web components; covered by unit tests. Exported from neuroimjs/browser only. |
SubjectOverlayViewer | stable | Covered by unit tests; neuroimjs/browser only. See Overlay review. |
SimpleOrthogonalViewer.toDataURL() | stable | Renders the view before reading the canvas back, so exports are no longer blank. |
DepthEnhancedLayer | known issue | Experimental visual effect with no dedicated tests. It draws above the current slice, and its parallax is inactive because views do not dispatch pointer events to layers; see DepthEnhancedLayer. |
showIntensityReadout option | known issue | On SimpleOrthogonalViewer the option is accepted but ignored. On SingleSliceViewer it adds the readout layer, but nothing updates it, so it stays blank. Build a readout from onCoordChange + getValue. |
SliceLayer pointer handlers | known issue | onPointerMove / onPointerDown on a custom layer are never called; nothing dispatches pointer events to layers yet. Use SingleSliceViewer.onPointerMove / onPointerDown instead. |
onReady() / 'ready' event | known issue | Emitted inside create() before it returns, so a handler registered on the returned viewer never fires. Treat the resolved create() promise as "ready". |
Geometry & coordinates
| Feature | Status | Notes |
|---|---|---|
NeuroSpace grid ↔ world transforms | stable | |
NeuroSpace.reorient() | stable | Preserves world coordinates. |
getSliceAt / crosshair slicing | stable | Reoriented sampling. |
I/O (NIfTI)
| Feature | Status | Notes |
|---|---|---|
readVol / readVolList | stable | NIfTI-1 and NIfTI-2, .nii and .nii.gz. Applies intensity scaling, byte-swaps big-endian data, and takes space.spacing from the affine. readVolList takes a list of paths. Checked against nibabel-generated fixtures (npm run test:conformance). Unscaled UINT32 files are promoted losslessly to Float64NeuroVol by both decoders. Known discrepancies: the qform wins when qform_code > sform_code > 0 (nibabel prefers the sform); files with no transform get diag(pixdim), not nibabel's base affine; NIfTI-2 qform-only files throw. See the conformance notes. |
readHeader | stable | Raw header fields; spacing is the raw pixdim[1..3]. |
writeVol (3D) | stable | NIfTI-1 single file; round-trips covered by tests. Compression follows the extension (.nii.gz is gzipped, .nii is not); a compress or format option that contradicts it throws INVALID_ARGUMENT. |
writeVec (4D) | experimental | NIfTI-1 single file. Layout from the class: BigNeuroVec time-first, DenseNeuroVec/SparseNeuroVec time-last; other NeuroVecs are rejected. Full affine as qform and sform (sform only when sheared); pixdim[4] from the time spacing, which readVec reads back. Round-trips with both decoders are tested; npm run conformance:writers checks the output with nibabel. See I/O. |
readNiftiArrayBuffer (browser) | stable | NIfTI-1/2, .nii and .nii.gz, 3D or one frame of 4D. See Getting Started. Same scaling, byte-order and transform-selection rules as readVol, but scaled data are Float64 (not Float32). UINT16 files load as UInt16NeuroVol. UINT32 is promoted to Float64NeuroVol. |
NIfTI dual-file (.hdr/.img) | unavailable | A format descriptor exists, but readVol cannot read the pair and writeVol rejects the format. |
AFNI (.HEAD/.BRIK) | unavailable | Only a format descriptor (AFNIFormat) used for file-name matching. There is no AFNI reader or writer. |
Volumes & 4D/5D
| Feature | Status | Notes |
|---|---|---|
DenseNeuroVol & typed subclasses | stable | |
SparseNeuroVol, ClusteredNeuroVol, LogicalNeuroVol | stable | |
Volume arithmetic (addVol, meanVol, …) | stable | |
getRange() | stable | ±Infinity init, NaN-safe. |
NeuroVec (4D) + temporalFilter / detrend | stable | Preprocessing lives on the enhanced vec classes. |
BigNeuroVec / readVec | stable | readVec decodes the file once and holds it in memory (no temp file). It keeps the legacy time-first shape (dim = [T, X, Y, Z]), so vec.space does not describe the image grid; the 3D geometry, including the affine, is on volumeSpace and on every getVolume(t). mask is ignored. See Time Series. |
FileBackedNeuroVec | known issue | Loads volumes on demand through a callback you supply, with an LRU cache. It does not open files itself. |
MappedNeuroVec | known issue | Reads through a DataView over an ArrayBuffer you supply. It is not a memory-mapped file. |
NeuroHyperVec (5D+) core | known issue | Container, indexing and concat work. reduce() overflows the stack beyond a few thousand outputs; getSubVolume with two free named axes silently uses the first, and any spatial key (x, y, z) in the index spec throws. See Time Series. |
DenseNeuroHyperVec.glm, extractFeatures, save | unavailable | glm and extractFeatures throw "not yet implemented"; the async save returns a promise that rejects with it. |
Processing & analysis
| Feature | Status | Notes |
|---|---|---|
| Bilateral / guided filtering | stable | He et al.; numerically faithful. |
| Gaussian blur | stable | Separable (three 1-D passes). Sigma is in voxels, not mm. |
| Median / morphology / anisotropic diffusion | stable | Radii in voxels. dilate / erode propagate NaN to every neighbour. |
edgeDetection('canny') | known issue | Not implemented: warns and returns the Sobel magnitude. |
Resampler (nearest/linear) | stable | |
Resampler.transform() | stable | Implemented. |
Resampler cubic/Lanczos at volume boundaries | stable | Boundary taps are linearly extrapolated; edge-bias regression tests cover low/high edges and corners. |
Connected components / clusterTable / localMaxima | known issue | One-sided (>= threshold); mask voxels must equal 1; labels and sizes stored as Int16. In-mask NaN voxels seed clusters (and pull in suprathreshold neighbours, with maxValue NaN); remove them from the mask first. |
StatFunctions (mean/sum/min/max/std/median) | stable | mean/min/max/std skip NaN (std uses n − 1). sum returns NaN. median does not skip NaN, and NaN breaks its sort, so it returns NaN or a wrong value depending on where the NaN sits. |
Group statistics (computeSufficientStats, t / Welch / paired / consistency maps, OverlaySummaryService) | stable | Compensated two-pass sums; non-finite values skipped per voxel. See Group Statistics. |
OverlayReviewSession / createReviewVecFromVolumes | stable | Validates geometry across subjects and contrasts. |
sphericalROI / searchlight radius | stable | mm-based, spacing-aware (anisotropy-correct). |
| Searchlight sphere membership | stable | With nonzero: true, sphere members as well as centres are restricted to the mask; randomSearchlight and bootstrapSearchlight also clip by default. Pass clipToMask: false for geometric spheres. Without nonzero, every voxel is a centre and spheres are not clipped unless clipToMask: true. clusteredSearchlight spheres are never clipped. Window .data is all 1s. See Analysis. |
splitReduce | known issue | Returns one volume: the mean of the per-level reductions, not one map per level. The output keeps the input's 3D geometry, including the affine (a BigNeuroVec's volumeSpace). |
NeuroAtlas (loadGlasserAtlas, loadSchaeferAtlas, loadAtlas) | known issue | Glasser and Schaefer download their files from GitHub at run time. |
concat | known issue | Drops the input affine (keeps dims, spacing, origin only); use createReviewVecFromVolumes when geometry matters. |
Not yet available
These appear in some older README snippets but do not exist — use the alternatives:
| Doesn't exist | Use instead |
|---|---|
VolStack.fromNifti(url) | The loader in Getting Started → new VolStack(layer) |
NeuroVec.fromNifti(url) | readVec(fileName) in Node, or readNiftiArrayBuffer(buffer, { index }) per frame in the browser |
import … from 'neuroimjs/display' | The viewers come from neuroimjs/browser (or neuroimjs). The only sub-paths are neuroimjs/browser and the viewer-free neuroimjs/io, neuroimjs/slices and neuroimjs/geometry. |
Done since earlier drafts
Packaging and test-runner integrity (e2e excluded from the unit run, test files kept out of the published tarball, TestVolumeFactory no longer exported, types-first exports, lint restored), clustering determinism (k-means now uses a seeded k-means++ initialization), and the separable Gaussian blur are done.
Roadmap
The unit suite is green, but that does not mean only cleanup remains. In priority order:
- Fix the flagged known issues — the user-facing correctness bugs in the tables above:
NeuroHyperVec.reduce()stack overflow andgetSubVolumeindex specs,edgeDetection('canny'),splitReduceaveraging across levels,concatdropping the affine, connected components andStatFunctions.medianwith NaN, morphology with NaN, the NIfTI transform-selection discrepancies listed under I/O, and the vieweronReady/ layer pointer /showIntensityReadoutgaps. - Missing readers — dual-file NIfTI and AFNI.
- Consolidation — some duplicate NIfTI / viewer / ROI implementations from earlier accretion still coexist. They work; they'll be merged behind the current APIs. Two merges are breaking and deferred to a major version: unifying the
NeuroVecandINeuroVecinterfaces, and coordinatingSimpleOrthogonalViewer(MobX) andViewSynchronizer(events) the same way.readVec's legacy time-first shape is kept for compatibility. - Hygiene — dead-code removal.
Found something off?
If a feature marked stable misbehaves, please open an issue with a minimal repro — that's exactly what moves us to 1.0.