perf(segmentation): make segmentation read 3x faster (#3577)

This commit is contained in:
Alireza authored and GitHub committed 2023-08-08 14:39:31 -04:00
1 parent ec76ea24dd
commit 156c1ba60b
17 files changed
+187 -315

No files matched your search

@@ -1,11 +1,14 @@
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
import { utils } from '@ohif/core';
import {
metaData,
cache,
triggerEvent,
eventTarget,
} from '@cornerstonejs/core';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import dcmjs from 'dcmjs';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
import { dicomlabToRGB } from './utils/dicomlabToRGB';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
@@ -122,13 +125,12 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
!segDisplaySet.segments ||
Object.keys(segDisplaySet.segments).length === 0
) {
const segments = await _loadSegments(
await _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers
);
segDisplaySet.segments = segments;
headers,
});
}
const suppressEvents = true;
@@ -147,28 +149,65 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
return loadPromises[SOPInstanceUID];
}
async function _loadSegments(extensionManager, segDisplaySet, headers) {
async function _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
}) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { segmentationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports;
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(
segDisplaySet,
null,
headers
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
const cachedReferencedVolume = cache.getVolume(
segDisplaySet.referencedVolumeId
);
if (!Array.isArray(dataset.SegmentSequence)) {
dataset.SegmentSequence = [dataset.SegmentSequence];
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
}
const segments = _getSegments(dataset);
return segments;
const { imageIds } = cachedReferencedVolume;
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
{
percentComplete,
}
);
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
}
});
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService) {
@@ -178,114 +217,6 @@ function _segmentationExists(segDisplaySet, segmentationService) {
);
}
function _getPixelData(dataset, segments) {
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
let nextOffset = 0;
Object.keys(segments).forEach(segmentKey => {
const segment = segments[segmentKey];
segment.numberOfFrames = segment.functionalGroups.length;
segment.size = segment.numberOfFrames * frameSize;
segment.offset = nextOffset;
nextOffset = segment.offset + segment.size;
const packedSegment = dataset.PixelData[0].slice(
segment.offset,
nextOffset
);
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
segment.geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
);
});
return segments;
}
function geometryFromFunctionalGroups(dataset, perFrame) {
let pixelMeasures =
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
let planeOrientation =
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
const geometry = {};
// NB: DICOM PixelSpacing is defined as Row then Column,
// unlike ImageOrientationPatient
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
if (!spacingBetweenSlices) {
if (pixelMeasures.SliceThickness) {
console.log('Using SliceThickness as SpacingBetweenSlices');
spacingBetweenSlices = pixelMeasures.SliceThickness;
}
}
geometry.spacing = [
pixelMeasures.PixelSpacing[1],
pixelMeasures.PixelSpacing[0],
spacingBetweenSlices,
].map(Number);
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
Number
);
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
const columnStepToPatient = orientation.slice(0, 3);
const rowStepToPatient = orientation.slice(3, 6);
geometry.planeNormal = [];
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
Number
);
let lastPosition = perFrame[
perFrame.length - 1
].PlanePositionSequence.ImagePositionPatient.map(Number);
geometry.sliceStep = [];
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
vtkMath.normalize(geometry.sliceStep);
geometry.direction = columnStepToPatient
.concat(rowStepToPatient)
.concat(geometry.sliceStep);
geometry.origin = planePosition.ImagePositionPatient.map(Number);
return geometry;
}
function _getSegments(dataset) {
const segments = {};
dataset.SegmentSequence.forEach(segment => {
const cielab = segment.RecommendedDisplayCIELabValue;
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
rgba.push(255);
const segmentNumber = segment.SegmentNumber;
segments[segmentNumber] = {
color: rgba,
functionalGroups: [],
offset: null,
size: null,
pixelData: null,
label: segment.SegmentLabel,
};
});
// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
segments[segmentNumber].functionalGroups.push(functionalGroup);
});
return _getPixelData(dataset, segments);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
@@ -0,0 +1,16 @@
import dcmjs from 'dcmjs';
/**
* Converts a CIELAB color to an RGB color using the dcmjs library.
* @param cielab - The CIELAB color to convert.
* @returns The RGB color as an array of three integers between 0 and 255.
*/
function dicomlabToRGB(cielab: number[]): number[] {
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
return rgb;
}
export { dicomlabToRGB };
@@ -317,7 +317,6 @@ function OHIFCornerstoneSEGViewport(props) {
setIsHydrated(isHydrated);
};
return (
<>
<ViewportActionBar
@@ -344,10 +343,13 @@ function OHIFCornerstoneSEGViewport(props) {
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
thickness: SliceThickness
? utils.roundNumber(SliceThickness, 2)
: '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
? utils.roundNumber(SpacingBetweenSlices, 2)
: '',
scanner: ManufacturerModelName || '',
},