There are two problems here: your monochrome data has a higher resolution (e.g. value range) than can be shown in RGB, so you cannot just map the pixel data into the RGB data directly.
The value range depends on the Bits Stored
tag – for a typical value of 12 the data range would be 4096. The simplest implementation could just downscale the number, in this case by 16.
The second problem with your code: to represent a monochrome value in RGB, you have to add 3 color components with the same value:
let rgbaIdx = 0
let rgbIdx = 0
let pixelCount = 512 * 512
let scaleFactor = 16 // has to be calculated in real code
for ( let idx = 0; idx < pixelCount; idx++ ) {
# assume Little Endian
let pixelValue = pixelData[ rgbIdx ] + pixelData[ rgbIdx + 1 ] * 256
let displayValue = Math.round(pixelValue / scaleFactor)
imageData.data[ rgbaIdx ] = displayValue
imageData.data[ rgbaIdx + 1 ] = displayValue
imageData.data[ rgbaIdx + 2 ] = displayValue
imageData.data[ rgbaIdx + 3 ] = 255
rgbaIdx += 4
rgbIdx += 2
}
To get a better representation, you have to take the VOI LUT into account instead of just downscaling. In case you have the Window Center
/ Window Width
tags defined, you can calulate the minimum and maximum values and get the scale factor from that range:
let minValue = windowCenter - windowWidth / 2
let maxValue = windowCenter + windowWidth / 2
let scaleFactor = (maxValue - minValue) / 256
...
let pixelValue = pixelData[ rgbIdx ] + pixelData[ rgbIdx + 1 ] * 256
let displayValue = max((pixelValue - minValue) / scaleFactor), 255)
...
EDIT: As observed by @WilfRosenbaum: if you don’t have a VOI LUT (as suggested by the empty values of WindowCenter and WindowWidth) you best calculate your own one. To do this, you have to calculate the min/max values of your pixel data:
let minValue = 1 >> 16
let maxValue = 0
for ( let idx = 0; idx < pixelCount; idx++ ) {
let pixelValue = pixelData[ rgbIdx ] + pixelData[ rgbIdx + 1 ] * 256
minValue = min(minValue, pixelValue)
maxValue = max(maxValue, pixelValue)
}
let scaleFactor = (maxValue - minValue) / 256
and then use the same code as shown for the VOI LUT.
A few notes:
- if you have a modality LUT, you have to apply it before the VOI LUT; CT images usually have one (RescaleSlope/RescaleIntercept), though this one only has an identity LUT, so you can ignore it
- you can have more than one
WindowCenter
/ WindowWindow
value pairs, or could have a VOI LUT sequence, which is also not considered here
- the code is out of my head, so it may have bugs
Turned out 4 main things needed to be done (reading through fo-dicom source code to find these things out)
-
Prepare Monochrome2 LUT
export const LutMonochrome2 = () => {
let lut = []
for ( let idx = 0, byt = 255; idx < 256; idx++, byt-- ) {
// r, g, b, a
lut.push( [byt, byt, byt, 0xff] )
}
return lut
}
-
Interpret pixel data as unsigned short
export const bytesToShortSigned = (bytes) => {
let byteA = bytes[ 1 ]
let byteB = bytes[ 0 ]
let pixelVal
const sign = byteA & (1 << 7);
pixelVal = (((byteA & 0xFF) << 8) | (byteB & 0xFF));
if (sign) {
pixelVal = 0xFFFF0000 | pixelVal; // fill in most significant bits with 1's
}
return pixelVal
}
-
Get Minimum and Maximum Pixel Value and then compute WindowWidth to eventually map each pixel to Monochrome2 color map
export const getMinMax = ( pixelData ) => {
let pixelCount = pixelData.length
let min = 0, max = 0
for ( let idx = 0; idx < pixelCount; idx += 2 ) {
let pixelVal = bytesToShortSigned( [
pixelData[idx],
pixelData[idx+1]
] )
if (pixelVal < min)
min = pixelVal
if (pixelVal > max)
max = pixelVal
}
return { min, max }
}
-
Finally draw
export const draw = ( { dataSet, canvas } ) => {
const monochrome2 = LutMonochrome2()
const ctx = canvas.getContext( '2d' )
const imageData = ctx.createImageData( 512, 512 )
const pixelData = getPixelData( dataSet )
let pixelCount = pixelData.length
let { min: minPixel, max: maxPixel } = getMinMax( pixelData )
let windowWidth = Math.abs( maxPixel - minPixel );
let windowCenter = ( maxPixel + minPixel ) / 2.0;
console.debug( `minPixel: ${minPixel} , maxPixel: ${maxPixel}` )
let rgbaIdx = 0
for ( let idx = 0; idx < pixelCount; idx += 2 ) {
let pixelVal = bytesToShortSigned( [
pixelData[idx],
pixelData[idx+1]
] )
let binIdx = Math.floor( (pixelVal - minPixel) / windowWidth * 256 );
let displayVal = monochrome2[ binIdx ]
if ( displayVal == null )
displayVal = [ 0, 0, 0, 255]
imageData.data[ rgbaIdx ] = displayVal[0]
imageData.data[ rgbaIdx + 1 ] = displayVal[1]
imageData.data[ rgbaIdx + 2 ] = displayVal[2]
imageData.data[ rgbaIdx + 3 ] = displayVal[3]
rgbaIdx += 4
}
ctx.putImageData( imageData, 0, 0 )
}
Trying to render dicom monochrome2 onto HTML5 canvas
-
what is the correct pixel mapping from grayscale to canvas rgb ?
-
Currently using incorrect mapping of
const ctx = canvas.getContext( '2d' )
const imageData = ctx.createImageData( 512, 512 )
const pixelData = getPixelData( dataSet )
let rgbaIdx = 0
let rgbIdx = 0
let pixelCount = 512 * 512
for ( let idx = 0; idx < pixelCount; idx++ ) {
imageData.data[ rgbaIdx ] = pixelData[ rgbIdx ]
imageData.data[ rgbaIdx + 1 ] = pixelData[ rgbIdx + 1 ]
imageData.data[ rgbaIdx + 2 ] = 0
imageData.data[ rgbaIdx + 3 ] = 255
rgbaIdx += 4
rgbIdx += 2
}
ctx.putImageData( imageData, 0, 0 )
-
Reading through open source libraries, not very clear how, could you please suggest a clear introduction of how to render?

Fig 1. incorrect mapping

Fig 2. correct mapping, dicom displayed in IrfanView