用于渲染 DICOM Monochrome2 的像素映射
Pixel Mapping for Rendering DICOM Monochrome2
正在尝试将 dicom monochrome2 渲染到 HTML5 canvas
从灰度到 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 )
看了开源库,不是很清楚怎么渲染,能不能给个清晰的渲染介绍?
图 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 源代码以找出这些事情)
准备单色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
}
将像素数据解释为无符号短整型
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
}
获取最小和最大像素值,然后计算 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 }
}
终于画好了
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 )
}
正在尝试将 dicom monochrome2 渲染到 HTML5 canvas
从灰度到 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 )
看了开源库,不是很清楚怎么渲染,能不能给个清晰的渲染介绍?
图 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 源代码以找出这些事情)
准备单色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 }
将像素数据解释为无符号短整型
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
}
获取最小和最大像素值,然后计算 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 } }
终于画好了
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 ) }