Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 58 additions & 122 deletions extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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;

const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];

let loadPromises = {};
Expand Down Expand Up @@ -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;
Expand All @@ -147,143 +149,77 @@ 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;

function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(
segDisplaySet.displaySetInstanceUID
);
}

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
);
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;

segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
segment.geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
{
percentComplete,
}
);
});

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
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much cleaner and easier to understand.

imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
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)
);
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
const { RecommendedDisplayCIELabValue: cielab } = data;
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
sedghi marked this conversation as resolved.
Show resolved Hide resolved
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,
};
data.rgba = rgba;
sedghi marked this conversation as resolved.
Show resolved Hide resolved
}
});

// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
segments[segmentNumber].functionalGroups.push(functionalGroup);
});
Object.assign(segDisplaySet, results);
}

return _getPixelData(dataset, segments);
function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(
segDisplaySet.displaySetInstanceUID
);
}

function getSopClassHandlerModule({ servicesManager, extensionManager }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,9 @@ function OHIFCornerstoneSEGViewport(props) {
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
thickness: SliceThickness
wayfarer3130 marked this conversation as resolved.
Show resolved Hide resolved
? `${parseFloat(SliceThickness).toFixed(2)}mm`
: '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
Expand Down
6 changes: 3 additions & 3 deletions extensions/cornerstone-dicom-sr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.7.1",
"@cornerstonejs/core": "^1.7.1",
"@cornerstonejs/tools": "^1.7.1",
"@cornerstonejs/adapters": "^1.9.3",
"@cornerstonejs/core": "^1.9.3",
"@cornerstonejs/tools": "^1.9.3",
"classnames": "^2.3.2"
}
}
10 changes: 5 additions & 5 deletions extensions/cornerstone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.7.1",
"@cornerstonejs/dicom-image-loader": "^1.9.3",
"@ohif/core": "3.7.0-beta.41",
"@ohif/ui": "3.7.0-beta.41",
"dcmjs": "^0.29.6",
Expand All @@ -52,10 +52,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.7.1",
"@cornerstonejs/core": "^1.7.1",
"@cornerstonejs/streaming-image-volume-loader": "^1.7.1",
"@cornerstonejs/tools": "^1.7.1",
"@cornerstonejs/adapters": "^1.9.3",
"@cornerstonejs/core": "^1.9.3",
"@cornerstonejs/streaming-image-volume-loader": "^1.9.3",
"@cornerstonejs/tools": "^1.9.3",
"@kitware/vtk.js": "27.3.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
Expand Down
Loading