Skip to main content

Colormaps and contrast

Each volume maps its intensities onto a colormap. Two numbers control the mapping: calMin gets the first color and calMax gets the last. Changing them is how you set the contrast (the window) of an image.

import NiiVue from '@niivue/niivue'

const demo = async (canvas, { ui, asset }) => {
const nv = new NiiVue({ isColorbarVisible: true })
await nv.attachToCanvas(canvas)
await nv.loadVolumes([{ url: asset('volumes/mni152.nii.gz') }])

// A custom colormap (black, blue, white); it joins the continuous list as 'Icy'
nv.addColormap('icy', {
R: [0, 0, 255],
G: [0, 128, 255],
B: [0, 255, 255],
})

const vol = nv.volumes[0]
const lo = vol.globalMin
const hi = vol.globalMax
const step = (hi - lo) / 200

ui.select(
'Colormap',
nv.colormapNames('continuous'),
'Gray',
async (name) => {
await nv.setVolume(0, { colormap: name })
},
)
ui.slider(
'Min',
lo,
hi,
vol.calMin,
async (v) => {
await nv.setVolume(0, { calMin: v })
},
step,
)
ui.slider(
'Max',
lo,
hi,
vol.calMax,
async (v) => {
await nv.setVolume(0, { calMax: v })
},
step,
)
ui.checkbox('Invert', false, async (on) => {
await nv.setVolume(0, { isColormapInverted: on })
})
ui.checkbox('Colorbar', true, (on) => {
nv.isColorbarVisible = on
})
return nv
}

export default demo

Choosing a colormap​

nv.colormaps lists every public colormap name. Set one per volume, at load time or later:

console.log(nv.colormaps) // [..., 'Gray', 'Hot', ..., 'Viridis', ...]
await nv.setVolume(0, { colormap: 'hot' })

Stored names start with a capital letter, and a lookup capitalizes the first letter for you, so 'hot' and 'Hot' name the same map. The default is 'Gray'.

Window: calMin and calMax​

NiiVue picks a robust starting window when an image loads. You can override it in the load options or with setVolume:

await nv.loadVolumes([{ url: '/volumes/mni152.nii.gz', calMin: 30, calMax: 80 }])
await nv.setVolume(0, { calMin: 40, calMax: 90 })

nv.volumes[i] exposes the data range as globalMin and globalMax, which the demo uses to size its sliders. To go back to the automatic window, call await nv.recalculateCalMinMax(0).

By default, values below calMin are transparent on an overlay. Set isTransparentBelowCalMin: false to clamp them to the first color instead. For the background volume, nv.volumeIsAlphaClipDark = true makes voxels below calMin transparent, so dark air no longer hides what is behind it.

Users can set the window by dragging, too. On a 2D slice the right-button drag defaults to contrast mode (secondaryDragMode is DRAG_MODE.contrast). The Windowing example adds Otsu and reset buttons.

Inverting and negative values​

isColormapInverted reverses a volume's colormap. The 2D slices, the 3D render and the colorbar all follow.

Signed data, such as a statistical map, can use a second colormap for values below zero. colormapNegative names it, and calMinNeg / calMaxNeg set its window. Overlays and thresholds shows this in practice.

await nv.setVolume(1, { colormap: 'redyell', colormapNegative: 'winter' })

Colorbars​

nv.isColorbarVisible turns the colorbars on for the whole viewer. Each volume can opt out with its own isColorbarVisible: false, which is the usual setting for an anatomical background.

Custom colormaps​

Register your own with addColormap. Give it red, green and blue stops (0 to 255). A (alpha) and I (the intensity index of each stop) are optional. The call returns the stored name, with its first letter capitalized:

const name = nv.addColormap('icy', {
R: [0, 0, 255],
G: [0, 128, 255],
B: [0, 255, 255],
})
await nv.setVolume(0, { colormap: name }) // name is 'Icy'

The demo registers this map, so Icy appears in its list. addColormapFromUrl(url) fetches the same { R, G, B, A, I } shape as JSON and names it after the file. nv.hasColormap(name) checks whether a name is registered. nv.colormapNames('continuous') lists the ramps and nv.colormapNames('label') the label tables; nv.colormaps lists both.

Some built-in maps carry a display window of their own; ct_bones, for example, is meant for 180 to 600 HU. Picking such a map applies its window, whether you set it with setVolume, load the volume with it or stream it with loadChunkedVolume:

// Each line starts from a volume that is not using ct_bones yet.
await nv.setVolume(0, { colormap: 'ct_bones' }) // window becomes 180..600
await nv.setVolume(0, { colormap: 'ct_bones', calMin: -1024 }) // -1024..600
await nv.setVolume(0, { colormap: 'ct_bones', calMax: NaN }) // 180..image default

A bound you pass wins, and the map fills the other. NaN (or null, or any non-finite value) asks for the image's own default for that bound: the window in its header, or else the 2% to 98% range of its data (a volume streamed with loadChunkedVolume always uses the data range). setVolume applies the map's window only when the colormap changes, so sending the same colormap again keeps a window the user has adjusted. A volume added as an NVImage keeps its own window. nv.colormapWindow(name) returns a map's window as [min, max], or null for a map without one. lookupColorMap(name), a named export of @niivue/niivue, returns a colormap's stops along with that window as min and max.

A definition with a labels array is a label table instead of a ramp: each integer gets its own color and the display window is ignored. For label volumes, where each integer is a named region, see Atlases and labels.