用于渲染 DICOM Monochrome2 的像素映射

Pixel Mapping for Rendering DICOM Monochrome2

正在尝试将 dicom monochrome2 渲染到 HTML5 canvas

  1. 从灰度到 canvas rgb 的正确像素映射是什么?

    • 当前使用不正确映射

         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. 看了开源库,不是很清楚怎么渲染,能不能给个清晰的渲染介绍?

图 1. 不正确的映射

图2.正确映射,IrfanView中显示的dicom

这里有两个问题:您的单色数据比 RGB 显示的分辨率(例如值范围)更高,所以您不能直接将像素数据映射到 RGB 数据。
值范围取决于 Bits Stored 标签 - 对于典型值 12,数据范围为 4096。最简单的实现可以只缩小数字,在本例中为 16。

您的代码的第二个问题:要在 RGB 中表示单色值,您必须添加 3 个具有相同值的颜色分量:

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
}

为了获得更好的表示,您必须考虑 VOI LUT,而不仅仅是缩小比例。如果您定义了 Window Center / Window Width 标签,您可以计算最小值和最大值并从该范围获取比例因子:

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

编辑:正如@WilfRosenbaum 所观察到的:如果您没有 VOI LUT(如 WindowCenter 和 WindowWidth 的空值所示),您最好自己计算一个。为此,您必须计算像素数据的 min/max 值:

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

然后使用与 VOI LUT 所示相同的代码。

一些注意事项:

  • 如果你有模态LUT,你必须在VOI LUT之前应用它; CT图通常有一个(RescaleSlope/RescaleIntercept),虽然这个只有一个identity LUT,所以你可以忽略它
  • 你可以有多个 WindowCenter / WindowWindow 值对,或者可以有一个 VOI LUT 序列,这里也不考虑
  • 代码不在我的脑海中,所以它可能有错误

原来需要完成 4 件主要事情(阅读 fo-dicom 源代码以找出这些事情)

  1. 准备单色2 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. 将像素数据解释为无符号短整型

     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. 获取最小和最大像素值,然后计算 WindowWidth 以最终将每个像素映射到 Monochrome2 颜色图

    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. 终于画好了

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