Skip to content

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 — readVol and readNiftiArrayBuffer apply scl_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 old Number.MIN_VALUE min/max bug. getRange also skips NaN. dilate / erode do 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, and randomSearchlight / bootstrapSearchlight by default, clip each sphere to the mask, not just its centre.
  • splitReduce geometry — the output volume keeps the input's 3D geometry, including the affine.

Visualization ​

FeatureStatusNotes
mountSpacesupportedEmbed subject-map comparison through neuroimjs/space. Requires same-origin frame assets and aligned axis-aligned RAS+ grids; analysis internals remain private.
SimpleOrthogonalViewerstableHeadline 3-view viewer.
SingleSliceViewer + ViewSynchronizerstableComposable views for custom layouts.
ColorMap / ColorMapFactorystablePresets + custom gradients; non-finite → transparent.
VolLayer / VolStackstableMulti-layer compositing; layers on different grids are aligned in world space by default. See Multi-Layer Alignment.
OrthogonalImageViewerstableLower-level; prefer SimpleOrthogonalViewer.
LayerControlPanel / OverlayReviewPanelstableLit web components; covered by unit tests. Exported from neuroimjs/browser only.
SubjectOverlayViewerstableCovered by unit tests; neuroimjs/browser only. See Overlay review.
SimpleOrthogonalViewer.toDataURL()stableRenders the view before reading the canvas back, so exports are no longer blank.
DepthEnhancedLayerknown issueExperimental 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 optionknown issueOn 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 handlersknown issueonPointerMove / onPointerDown on a custom layer are never called; nothing dispatches pointer events to layers yet. Use SingleSliceViewer.onPointerMove / onPointerDown instead.
onReady() / 'ready' eventknown issueEmitted inside create() before it returns, so a handler registered on the returned viewer never fires. Treat the resolved create() promise as "ready".

Geometry & coordinates ​

FeatureStatusNotes
NeuroSpace grid ↔ world transformsstable
NeuroSpace.reorient()stablePreserves world coordinates.
getSliceAt / crosshair slicingstableReoriented sampling.

I/O (NIfTI) ​

FeatureStatusNotes
readVol / readVolListstableNIfTI-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.
readHeaderstableRaw header fields; spacing is the raw pixdim[1..3].
writeVol (3D)stableNIfTI-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)experimentalNIfTI-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)stableNIfTI-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)unavailableA format descriptor exists, but readVol cannot read the pair and writeVol rejects the format.
AFNI (.HEAD/.BRIK)unavailableOnly a format descriptor (AFNIFormat) used for file-name matching. There is no AFNI reader or writer.

Volumes & 4D/5D ​

FeatureStatusNotes
DenseNeuroVol & typed subclassesstable
SparseNeuroVol, ClusteredNeuroVol, LogicalNeuroVolstable
Volume arithmetic (addVol, meanVol, …)stable
getRange()stable±Infinity init, NaN-safe.
NeuroVec (4D) + temporalFilter / detrendstablePreprocessing lives on the enhanced vec classes.
BigNeuroVec / readVecstablereadVec 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.
FileBackedNeuroVecknown issueLoads volumes on demand through a callback you supply, with an LRU cache. It does not open files itself.
MappedNeuroVecknown issueReads through a DataView over an ArrayBuffer you supply. It is not a memory-mapped file.
NeuroHyperVec (5D+) coreknown issueContainer, 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, saveunavailableglm and extractFeatures throw "not yet implemented"; the async save returns a promise that rejects with it.

Processing & analysis ​

FeatureStatusNotes
Bilateral / guided filteringstableHe et al.; numerically faithful.
Gaussian blurstableSeparable (three 1-D passes). Sigma is in voxels, not mm.
Median / morphology / anisotropic diffusionstableRadii in voxels. dilate / erode propagate NaN to every neighbour.
edgeDetection('canny')known issueNot implemented: warns and returns the Sobel magnitude.
Resampler (nearest/linear)stable
Resampler.transform()stableImplemented.
Resampler cubic/Lanczos at volume boundariesstableBoundary taps are linearly extrapolated; edge-bias regression tests cover low/high edges and corners.
Connected components / clusterTable / localMaximaknown issueOne-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)stablemean/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)stableCompensated two-pass sums; non-finite values skipped per voxel. See Group Statistics.
OverlayReviewSession / createReviewVecFromVolumesstableValidates geometry across subjects and contrasts.
sphericalROI / searchlight radiusstablemm-based, spacing-aware (anisotropy-correct).
Searchlight sphere membershipstableWith 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.
splitReduceknown issueReturns 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 issueGlasser and Schaefer download their files from GitHub at run time.
concatknown issueDrops 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 existUse 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:

  1. Fix the flagged known issues — the user-facing correctness bugs in the tables above: NeuroHyperVec.reduce() stack overflow and getSubVolume index specs, edgeDetection('canny'), splitReduce averaging across levels, concat dropping the affine, connected components and StatFunctions.median with NaN, morphology with NaN, the NIfTI transform-selection discrepancies listed under I/O, and the viewer onReady / layer pointer / showIntensityReadout gaps.
  2. Missing readers — dual-file NIfTI and AFNI.
  3. 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 NeuroVec and INeuroVec interfaces, and coordinating SimpleOrthogonalViewer (MobX) and ViewSynchronizer (events) the same way. readVec's legacy time-first shape is kept for compatibility.
  4. 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.

Released under the MIT License.