Skip to content

Commit

Permalink
Added support for rendering float pixel data (parametric maps)
Browse files Browse the repository at this point in the history
  • Loading branch information
PantelisGeorgiadis committed Mar 11, 2023
1 parent eae79c4 commit 79258b9
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 233 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This library was inspired by the rendering pipelines of [fo-dicom][fo-dicom-url]
### Features
- Renders single and multi-frame datasets with optional adjustment of window/level and color palette.
- Decodes all major transfer syntaxes using a native WebAssembly module.
- Handles color and grayscale datasets, from 1 to 32 bits allocated, with signed and unsigned pixel values.
- Handles color and grayscale datasets, from 1 to 32 bits allocated, with signed, unsigned and float pixel values.
- Outputs RGBA pixel arrays, suitable for use with HTML5 Canvas and WebGL, or other imaging libraries.
- Provides a common bundle for both Node.js and browser.

Expand Down
428 changes: 220 additions & 208 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dcmjs-imaging",
"version": "0.1.14",
"version": "0.1.15",
"description": "DICOM image and overlay rendering for Node.js and browser using dcmjs",
"main": "build/dcmjs-imaging.min.js",
"module": "build/dcmjs-imaging.min.js",
Expand Down Expand Up @@ -43,21 +43,21 @@
},
"homepage": "https://github.com/PantelisGeorgiadis/dcmjs-imaging",
"dependencies": {
"dcmjs": "^0.29.3",
"dcmjs": "^0.29.5",
"loglevel": "^1.8.1",
"loglevel-plugin-prefix": "^0.8.4"
},
"devDependencies": {
"@types/bmp-js": "^0.1.0",
"bmp-js": "^0.1.0",
"browserify": "^17.0.0",
"c8": "^7.12.0",
"c8": "^7.13.0",
"chai": "^4.3.7",
"clang-format": "^1.8.0",
"copy-webpack-plugin": "^11.0.0",
"docdash": "^2.0.0",
"eslint": "^8.31.0",
"jsdoc": "^4.0.0",
"docdash": "^2.0.1",
"eslint": "^8.36.0",
"jsdoc": "^4.0.2",
"karma": "^6.4.1",
"karma-browserify": "^8.1.0",
"karma-chai": "^0.1.0",
Expand All @@ -68,14 +68,14 @@
"mocha": "^10.2.0",
"open-cli": "^7.1.0",
"pako": "^2.1.0",
"prettier": "^2.8.1",
"prettier": "^2.8.4",
"shx": "^0.3.4",
"sinon": "^15.0.1",
"terser-webpack-plugin": "^5.3.6",
"terser-webpack-plugin": "^5.3.7",
"ts-node": "^10.9.1",
"tsd": "^0.25.0",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"tsd": "^0.27.0",
"typescript": "^4.9.5",
"webpack": "^5.76.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
}
Expand Down
6 changes: 5 additions & 1 deletion src/Lut.js
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,11 @@ class LutPipeline {
} else if (pixel.getBitsStored() <= 16) {
frameData = pixel.isSigned() ? pixel.getFrameDataS16(frame) : pixel.getFrameDataU16(frame);
} else if (pixel.getBitsStored() <= 32) {
frameData = pixel.isSigned() ? pixel.getFrameDataS32(frame) : pixel.getFrameDataU32(frame);
frameData = pixel.hasFloatPixelData()
? pixel.getFrameDataF32(frame)
: pixel.isSigned()
? pixel.getFrameDataS32(frame)
: pixel.getFrameDataU32(frame);
} else {
throw new Error(`Unsupported pixel data value for bits stored: ${pixel.getBitsStored()}`);
}
Expand Down
64 changes: 53 additions & 11 deletions src/Pixel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class Pixel {
this.frames = this._getElement(elements, 'NumberOfFrames') || 1;
this.width = this._getElement(elements, 'Columns');
this.height = this._getElement(elements, 'Rows');
this.bitsStored = this._getElement(elements, 'BitsStored') || 0;
this.bitsAllocated = this._getElement(elements, 'BitsAllocated') || 0;
this.bitsStored = this._getElement(elements, 'BitsStored') || this.bitsAllocated;
this.highBit = this._getElement(elements, 'HighBit') || this.bitsStored - 1;
this.samplesPerPixel = this._getElement(elements, 'SamplesPerPixel') || 1;
this.pixelRepresentation =
Expand All @@ -38,6 +38,7 @@ class Pixel {
this.smallestImagePixelValue = this._getElement(elements, 'SmallestImagePixelValue');
this.largestImagePixelValue = this._getElement(elements, 'LargestImagePixelValue');
this.pixelPaddingValue = this._getElement(elements, 'PixelPaddingValue');
this.floatPixelPaddingValue = this._getElement(elements, 'FloatPixelPaddingValue');
this.redPaletteColorLookupTableDescriptor = this._getElement(
elements,
'RedPaletteColorLookupTableDescriptor'
Expand All @@ -55,6 +56,7 @@ class Pixel {
'BluePaletteColorLookupTableData'
);
this.pixelData = this._getElement(elements, 'PixelData');
this.floatPixelData = this._getElement(elements, 'FloatPixelData');
}

/**
Expand Down Expand Up @@ -285,7 +287,7 @@ class Pixel {
* @returns {number} Pixel padding value.
*/
getPixelPaddingValue() {
return this.pixelPaddingValue;
return this.pixelPaddingValue || this.floatPixelPaddingValue;
}

/**
Expand Down Expand Up @@ -330,7 +332,16 @@ class Pixel {
* @returns {Array<ArrayBuffer>} Pixel data.
*/
getPixelData() {
return this.pixelData;
return this.pixelData || this.floatPixelData;
}

/**
* Checks whether the float pixel data exist.
* @method
* @returns {boolean} Whether the float pixel data exist.
*/
hasFloatPixelData() {
return this.floatPixelData !== undefined;
}

/**
Expand Down Expand Up @@ -410,6 +421,22 @@ class Pixel {
return s32;
}

/**
* Gets the pixel data as an array of float values.
* @method
* @param {number} frame - Frame index.
* @returns {Float32Array} Pixel data as an array of float values.
*/
getFrameDataF32(frame) {
const frameBuffer = this._getFrameBuffer(frame);

return new Float32Array(
frameBuffer.buffer,
frameBuffer.byteOffset,
frameBuffer.byteLength / Float32Array.BYTES_PER_ELEMENT
);
}

/**
* Gets the pixel description.
* @method
Expand Down Expand Up @@ -501,13 +528,24 @@ class Pixel {
framePixelBuffer[i + 1] = holder;
}
} else if (this.getBitsAllocated() === 32) {
for (let i = 0; i < framePixelBuffer.length; i += 4) {
let holder = framePixelBuffer[i];
framePixelBuffer[i] = framePixelBuffer[i + 1];
framePixelBuffer[i + 1] = holder;
holder = framePixelBuffer[i + 2];
framePixelBuffer[i + 2] = framePixelBuffer[i + 3];
framePixelBuffer[i + 3] = holder;
if (this.hasFloatPixelData()) {
for (let i = 0; i < framePixelBuffer.length; i += 4) {
let holder = framePixelBuffer[i];
framePixelBuffer[i] = framePixelBuffer[i + 3];
framePixelBuffer[i + 3] = holder;
holder = framePixelBuffer[i + 1];
framePixelBuffer[i + 1] = framePixelBuffer[i + 2];
framePixelBuffer[i + 2] = holder;
}
} else {
for (let i = 0; i < framePixelBuffer.length; i += 4) {
let holder = framePixelBuffer[i];
framePixelBuffer[i] = framePixelBuffer[i + 1];
framePixelBuffer[i + 1] = holder;
holder = framePixelBuffer[i + 2];
framePixelBuffer[i + 2] = framePixelBuffer[i + 3];
framePixelBuffer[i + 3] = holder;
}
}
}
}
Expand Down Expand Up @@ -725,7 +763,11 @@ class PixelPipeline {
pixel.getHeight(),
pixel.getMinimumPixelValue(),
pixel.getMaximumPixelValue(),
pixel.isSigned() ? pixel.getFrameDataS32(frame) : pixel.getFrameDataU32(frame)
pixel.hasFloatPixelData()
? pixel.getFrameDataF32(frame)
: pixel.isSigned()
? pixel.getFrameDataS32(frame)
: pixel.getFrameDataU32(frame)
);
} else {
throw new Error(`Unsupported pixel data value for bits stored: ${pixel.getBitsStored()}`);
Expand Down
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = '0.1.14';
module.exports = '0.1.15';
58 changes: 58 additions & 0 deletions test/DicomImage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,64 @@ describe('DicomImage', () => {
}
});

it('should correctly render a little and big endian float 32-bit grayscale frame (MONOCHROME2)', () => {
function floatToUint8Array(f, littleEndian) {
const fArray = new Float32Array(1);
fArray[0] = f;

return littleEndian === true
? new Uint8Array(fArray.buffer)
: new Uint8Array(fArray.buffer).reverse();
}

[true, false].forEach((littleEndian) => {
const width = 3;
const height = 3;
// prettier-ignore
const pixels = new Uint8Array([
...floatToUint8Array( 0.0, littleEndian), ...floatToUint8Array(1024.0, littleEndian), ...floatToUint8Array( 0.0, littleEndian),
...floatToUint8Array(1024.0, littleEndian), ...floatToUint8Array( 0.0, littleEndian), ...floatToUint8Array(1024.0, littleEndian),
...floatToUint8Array( 0.0, littleEndian), ...floatToUint8Array(1024.0, littleEndian), ...floatToUint8Array( 0.0, littleEndian)
]);
// prettier-ignore
const expectedRenderedPixels = Uint8Array.from([
0x00, 0xff, 0x00,
0xff, 0x00, 0xff,
0x00, 0xff, 0x00,
]);
const monoImage = new DicomImage(
{
Rows: height,
Columns: width,
BitsAllocated: 32,
SamplesPerPixel: 1,
PhotometricInterpretation: PhotometricInterpretation.Monochrome2,
FloatPixelData: [pixels],
},
littleEndian === true
? TransferSyntax.ExplicitVRLittleEndian
: TransferSyntax.ExplicitVRBigEndian
);

const renderingResult = monoImage.render();
expect(renderingResult.histograms).to.be.undefined;
expect(renderingResult.windowLevel).not.to.be.undefined;
expect(renderingResult.frame).to.be.eq(0);
expect(renderingResult.width).to.be.eq(width);
expect(renderingResult.height).to.be.eq(height);
expect(renderingResult.colorPalette).to.be.eq(StandardColorPalette.Grayscale);

const renderedPixels = new Uint8Array(renderingResult.pixels);
for (let i = 0, p = 0; i < 4 * width * height; i += 4) {
expect(renderedPixels[i]).to.be.eq(expectedRenderedPixels[p]);
expect(renderedPixels[i + 1]).to.be.eq(expectedRenderedPixels[p]);
expect(renderedPixels[i + 2]).to.be.eq(expectedRenderedPixels[p]);
expect(renderedPixels[i + 3]).to.be.eq(255);
p++;
}
});
});

it('should correctly render an RGB color frame (Interleaved)', () => {
[
TransferSyntax.ImplicitVRLittleEndian,
Expand Down
Binary file modified wasm/bin/native-pixel-decoder.wasm
Binary file not shown.

0 comments on commit 79258b9

Please sign in to comment.