Skip to content

Commit

Permalink
feat(adapters): Add adapter to generate segments & geometry from SEG …
Browse files Browse the repository at this point in the history
…for easier use in VTKjs (migrated from vtkDisplay example)
  • Loading branch information
swederik committed Feb 27, 2019
1 parent bc79040 commit 398b74d
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 0 deletions.
217 changes: 217 additions & 0 deletions packages/adapters/src/adapters/VTKjs/Segmentation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,220 @@
import Colors from "../../colors.js";
import BitArray from "../../bitArray.js";

// TODO: Is there a better name for this? RGBAInt?
// Should we move it to Colors.js
function dicomlab2RGBA(cielab) {
const rgba = Colors.dicomlab2RGB(cielab).map(x => Math.round(x * 255));
rgba.push(255);

return rgba;
}

// TODO: Copied these functions in from VTK Math so we don't need a dependency.
// I guess we should put them somewhere
// https://github.com/Kitware/vtk-js/blob/master/Sources/Common/Core/Math/index.js
function cross(x, y, out) {
const Zx = x[1] * y[2] - x[2] * y[1];
const Zy = x[2] * y[0] - x[0] * y[2];
const Zz = x[0] * y[1] - x[1] * y[0];
out[0] = Zx;
out[1] = Zy;
out[2] = Zz;
}

function norm(x, n = 3) {
switch (n) {
case 1:
return Math.abs(x);
case 2:
return Math.sqrt(x[0] * x[0] + x[1] * x[1]);
case 3:
return Math.sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
default: {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += x[i] * x[i];
}
return Math.sqrt(sum);
}
}
}

function normalize(x) {
const den = norm(x);
if (den !== 0.0) {
x[0] /= den;
x[1] /= den;
x[2] /= den;
}
return den;
}

function subtract(a, b, out) {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
out[2] = a[2] - b[2];
}

// TODO: This is a useful utility on its own. We should move it somewhere?
// dcmjs.adapters.vtk.Multiframe? dcmjs.utils?
function geometryFromFunctionalGroups(dataset, PerFrameFunctionalGroups) {
const geometry = {};
const pixelMeasures =
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
const planeOrientation =
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;

// Find the origin of the volume from the PerFrameFunctionalGroups' ImagePositionPatient values
//
// TODO: assumes sorted frames. This should read the ImagePositionPatient from each frame and
// sort them to obtain the first and last position along the acquisition axis.
const firstFunctionalGroup = PerFrameFunctionalGroups[0];
const lastFunctionalGroup =
PerFrameFunctionalGroups[PerFrameFunctionalGroups.length - 1];
const firstPosition = firstFunctionalGroup.PlanePositionSequence.ImagePositionPatient.map(
Number
);
const lastPosition = lastFunctionalGroup.PlanePositionSequence.ImagePositionPatient.map(
Number
);

geometry.origin = firstPosition;

// NB: DICOM PixelSpacing is defined as Row then Column,
// unlike ImageOrientationPatient
geometry.spacing = [
pixelMeasures.PixelSpacing[1],
pixelMeasures.PixelSpacing[0],
pixelMeasures.SpacingBetweenSlices
].map(Number);

geometry.dimensions = [
dataset.Columns,
dataset.Rows,
PerFrameFunctionalGroups.length
].map(Number);

const orientation = planeOrientation.ImageOrientationPatient.map(Number);
const columnStepToPatient = orientation.slice(0, 3);
const rowStepToPatient = orientation.slice(3, 6);

geometry.planeNormal = [];

cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);

geometry.sliceStep = [];
subtract(lastPosition, firstPosition, geometry.sliceStep);
normalize(geometry.sliceStep);
geometry.direction = columnStepToPatient
.concat(rowStepToPatient)
.concat(geometry.sliceStep);

return geometry;
}

export default class Segmentation {
constructor() {}

/**
* Produces an array of Segments from an input DICOM Segmentation dataset
*
* Segments are returned with Geometry values that can be used to create
* VTK Image Data objects.
*
* @example Example usage to create VTK Volume actors from each segment:
*
* const actors = [];
* const segments = generateToolState(dataset);
* segments.forEach(segment => {
* // now make actors using the segment information
* const scalarArray = vtk.Common.Core.vtkDataArray.newInstance({
* name: "Scalars",
* numberOfComponents: 1,
* values: segment.pixelData,
* });
*
* const imageData = vtk.Common.DataModel.vtkImageData.newInstance();
* imageData.getPointData().setScalars(scalarArray);
* imageData.setDimensions(geometry.dimensions);
* imageData.setSpacing(geometry.spacing);
* imageData.setOrigin(geometry.origin);
* imageData.setDirection(geometry.direction);
*
* const mapper = vtk.Rendering.Core.vtkVolumeMapper.newInstance();
* mapper.setInputData(imageData);
* mapper.setSampleDistance(2.);
*
* const actor = vtk.Rendering.Core.vtkVolume.newInstance();
* actor.setMapper(mapper);
*
* actors.push(actor);
* });
*
* @param dataset
* @return {{}}
*/
static generateSegments(dataset) {
if (dataset.SegmentSequence.constructor.name !== "Array") {
dataset.SegmentSequence = [dataset.SegmentSequence];
}

dataset.SegmentSequence.forEach(segment => {
// TODO: other interesting fields could be extracted from the segment
// TODO: Read SegmentsOverlay field
// http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.8.20.2.html

// TODO: Looks like vtkColor only wants RGB in 0-1 values.
// Why was this example converting to RGBA with 0-255 values?
const color = dicomlab2RGBA(segment.RecommendedDisplayCIELabValue);

segments[segment.SegmentNumber] = {
color,
functionalGroups: [],
offset: null,
size: null,
pixelData: null
};
});

// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence
.ReferencedSegmentNumber;

segments[segmentNumber].functionalGroups.push(functionalGroup);
});

// determine per-segment index into the pixel data
// TODO: only handles one-bit-per pixel
const frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
let nextOffset = 0;

Object.keys(segments).forEach(segmentNumber => {
const segment = segments[segmentNumber];

segment.numberOfFrames = segment.functionalGroups.length;
segment.size = segment.numberOfFrames * frameSize;
segment.offset = nextOffset;

nextOffset = segment.offset + segment.size;

const packedSegment = dataset.PixelData.slice(
segment.offset,
nextOffset
);

segment.pixelData = BitArray.unpack(packedSegment);

const geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
);

segment.geometry = geometry;
});

return segments;
}
}
1 change: 1 addition & 0 deletions packages/adapters/src/bitArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const BitArray = {
};

export { BitArray };
export default BitArray;

function getBytesForBinaryFrame(numPixels) {
// Check whether the 1-bit pixels exactly fit into bytes
Expand Down
1 change: 1 addition & 0 deletions packages/adapters/src/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,4 @@ class Colors {
}

export { Colors };
export default Colors;

0 comments on commit 398b74d

Please sign in to comment.