1
votes

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?

incorrect mapping

Fig 1. incorrect mapping

correct monochrome2

Fig 2. correct mapping, dicom displayed in IrfanView

2

2 Answers

3
votes

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
1
votes

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 )
    
    }