Nov 20, 2022

Solution 1 :

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

Solution 2 :

Turned out 4 main things needed to be done (reading through fo-dicom source code to find these things out)

1. 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
}
``````
2. 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
``````

}

3. 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 }
}
``````
4. 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 )

}
``````

Problem :

Trying to render dicom monochrome2 onto HTML5 canvas

1. 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 )
``````
2. 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