Skip to content

Commit

Permalink
fix(SR): When loading DICOM SR, only one measurement is shown with no…
Browse files Browse the repository at this point in the history
… way to show others (#3228)

* fix: Make the cornerstone sR viewport show all measurements

* PR fixes

* PR fixes

* Add a DICOM SR hanging protocol

* Duplicate the hanging protocol for seg as well

* PR requested change

* PR requested changes

* PR fixes plus merge update fixes

* PR fixes and integration test fix

* PR - documentation
  • Loading branch information
wayfarer3130 authored Apr 25, 2023
1 parent dc61d87 commit 69d8e6a
Show file tree
Hide file tree
Showing 24 changed files with 456 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function _getDisplaySetsFromSeries(
segments: {},
sopClassUids,
instance,
instances: [instance],
wadoRoot,
wadoUriRoot,
wadoUri,
Expand Down
9 changes: 7 additions & 2 deletions extensions/cornerstone-dicom-seg/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';

import { Types } from '@ohif/core';

import getSopClassHandlerModule from './getSopClassHandlerModule';
import getSopClassHandlerModule, { protocols } from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation';
import getHangingProtocolModule from './getHangingProtocolModule';

Expand Down Expand Up @@ -37,7 +37,7 @@ const extension = {
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
getPanelModule: ({ servicesManager, commandsManager, extensionManager }: Types.Extensions.ExtensionParams): Types.Panel[] => {
const wrappedPanelSegmentation = () => {
return (
<PanelSegmentation
Expand All @@ -58,6 +58,7 @@ const extension = {
},
];
},

getViewportModule({ servicesManager, extensionManager }) {
const ExtendedOHIFCornerstoneSEGViewport = props => {
return (
Expand Down Expand Up @@ -85,3 +86,7 @@ const extension = {
};

export default extension;

// Export the protocols separately to allow for extending it at compile time
// in other modules
export { protocols };
4 changes: 2 additions & 2 deletions extensions/cornerstone-dicom-sr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
"@cornerstonejs/adapters": "^0.6.0",
"@cornerstonejs/core": "^0.40.0",
"@cornerstonejs/tools": "^0.60.1"
"@cornerstonejs/core": "^0.42.2",
"@cornerstonejs/tools": "^0.61.11"
}
}
21 changes: 18 additions & 3 deletions extensions/cornerstone-dicom-sr/src/commandsModule.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { metaData, utilities } from '@cornerstonejs/core';

import OHIF from '@ohif/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import dcmjs from 'dcmjs';
import { adaptersSR } from '@cornerstonejs/adapters';

Expand Down Expand Up @@ -41,7 +41,6 @@ const _generateReport = (
if (typeof dataset.SpecificCharacterSet === 'undefined') {
dataset.SpecificCharacterSet = 'ISO_IR 192';
}

return dataset;
};

Expand Down Expand Up @@ -104,14 +103,30 @@ const commandsModule = ({}) => {
additionalFindingTypes,
options
);
const { StudyInstanceUID } = naturalizedReport;

const { StudyInstanceUID, ContentSequence } = naturalizedReport;
// The content sequence has 5 or more elements, of which
// the `[4]` element contains the annotation data, so this is
// checking that there is some annotation data present.
if (!ContentSequence?.[4].ContentSequence?.length) {
console.log(
'naturalizedReport missing imaging content',
naturalizedReport
);
throw new Error('Invalid report, no content');
}

await dataSource.store.dicom(naturalizedReport);

if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
}

// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
DicomMetadataStore.addInstances([naturalizedReport], true);

return naturalizedReport;
} catch (error) {
console.warn(error);
Expand Down
110 changes: 75 additions & 35 deletions extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes } from '@ohif/core';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addMeasurement from './utils/addMeasurement';
import isRehydratable from './utils/isRehydratable';
import { adaptersSR } from '@cornerstonejs/adapters';

type InstanceMetadata = Types.InstanceMetadata;

const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;

const { ImageSet, MetadataProvider: metadataProvider } = classes;

// TODO ->
// Add SR thumbnail
// Make viewport
Expand All @@ -22,6 +25,17 @@ const sopClassUids = [
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';

const validateSameStudyUID = (uid: string, instances): void => {
instances.forEach(it => {
if (it.StudyInstanceUID !== uid) {
console.warn('Not all instances have the same UID', uid, it);
throw new Error(
`Instances ${it.SOPInstanceUID} does not belong to ${uid}`
);
}
});
};

const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
Expand Down Expand Up @@ -50,15 +64,38 @@ const RELATIONSHIP_TYPE = {

const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';

/**
* Adds instances to the DICOM SR series, rather than creating a new
* series, so that as SR's are saved, they append to the series, and the
* key image display set gets updated as well, containing just the new series.
* @param instances is a list of instances from THIS series that are not
* in this DICOM SR Display Set already.
*/
function addInstances(
instances: InstanceMetadata[],
displaySetService: DisplaySetService
) {
this.instances.push(...instances);
utils.sortStudyInstances(this.instances);
// The last instance is the newest one, so is the one most interesting.
// Eventually, the SR viewer should have the ability to choose which SR
// gets loaded, and to navigate among them.
this.instance = this.instances[this.instances.length - 1];
this.isLoaded = false;
if (this.keyImageDisplaySet) {
this.load();
this.keyImageDisplaySet.updateInstances();
displaySetService.setDisplaySetMetadataInvalidated(
this.keyImageDisplaySet.displaySetInstanceUID
);
}
return this;
}

/**
* DICOM SR SOP Class Handler
* For all referenced images in the TID 1500/300 sections, add an image to the
* display (this is TODO - it is not the actual behaviour below unfortunately)
*
* This will only display and rehydrate the latest DICOM SR in the given series
* It would be possible to add the ability to view older series rehydrations
* in the future.
*
* display.
* @param instances is a set of instances all from the same series
* @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object
Expand All @@ -74,6 +111,9 @@ function _getDisplaySetsFromSeries(
}

utils.sortStudyInstances(instances);
// The last instance is the newest one, so is the one most interesting.
// Eventually, the SR viewer should have the ability to choose which SR
// gets loaded, and to navigate among them.
const instance = instances[instances.length - 1];

const {
Expand All @@ -86,11 +126,12 @@ function _getDisplaySetsFromSeries(
ConceptNameCodeSequence,
SOPClassUID,
} = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);

if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport
CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
console.log(
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
Expand All @@ -111,14 +152,13 @@ function _getDisplaySetsFromSeries(
SOPClassHandlerId,
SOPClassUID,
instances,
// Others is a historical value used for instances which is deprecated and will be removed
others: instances,
referencedImages: null,
measurements: null,
isDerivedDisplaySet: true,
isLoaded: false,
sopClassUids,
instance,
addInstances,
};

displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
Expand Down Expand Up @@ -327,7 +367,7 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
trackingUniqueIdentifier => {
const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
trackingUniqueIdentifier
];

const measurement = _processMeasurement(mergedContentSequence);
Expand Down Expand Up @@ -367,7 +407,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(

if (
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
trackingUniqueIdentifier
] === undefined
) {
// Add the full ContentSequence
Expand Down Expand Up @@ -473,18 +513,18 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodeNameCodeSequenceValues.TrackingIdentifier
);

const Finding = mergedContentSequence.find(
const finding = mergedContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding
);

const FindingSites = mergedContentSequence.filter(
const findingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT &&
CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
CodeNameCodeSequenceValues.FindingSite
);

const measurement = {
Expand All @@ -496,28 +536,28 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
};

if (
Finding &&
finding &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
Finding.ConceptCodeSequence.CodingSchemeDesignator
finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
Finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: Finding.ConceptCodeSequence.CodeMeaning,
value: finding.ConceptCodeSequence.CodeMeaning,
});
}

// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
if (FindingSites.length) {
const cornerstoneFreeTextFindingSite = FindingSites.find(
if (findingSites.length) {
const cornerstoneFreeTextFindingSite = findingSites.find(
FindingSite =>
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
);

if (cornerstoneFreeTextFindingSite) {
Expand Down Expand Up @@ -633,24 +673,24 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {

_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
const { ReferencedSOPSequence } = item;

if (item.hasOwnProperty('ReferencedSOPClassUID')) {
const {
ReferencedSOPClassUID,
ReferencedSOPInstanceUID,
} = ReferencedSOPSequence;

referencedImages.push({
ReferencedSOPClassUID,
ReferencedSOPInstanceUID,
});
if (!ReferencedSOPSequence) return;
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
if (ref.ReferencedSOPClassUID) {
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;

referencedImages.push({
ReferencedSOPClassUID,
ReferencedSOPInstanceUID,
});
}
}
});

return referencedImages;
}

function _getSequenceAsArray(sequence) {
if (!sequence) return [];
return Array.isArray(sequence) ? sequence : [sequence];
}

Expand Down
5 changes: 2 additions & 3 deletions extensions/cornerstone-dicom-sr/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import preRegistration from './init';
import { id } from './id.js';
import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';

const Component = React.lazy(() => {
return import(
Expand Down Expand Up @@ -57,8 +58,6 @@ const dicomSRExtension = {
},
getCommandsModule,
getSopClassHandlerModule,
getHangingProtocolModule,

// Include dynmically computed values such as toolNames not known till instantiation
getUtilityModule({ servicesManager }) {
return [
Expand All @@ -75,4 +74,4 @@ const dicomSRExtension = {
export default dicomSRExtension;

// Put static exports here so they can be type checked
export { hydrateStructuredReport, srProtocol };
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
2 changes: 1 addition & 1 deletion extensions/cornerstone-dicom-sr/src/onModeEnter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function onModeEnter({ servicesManager }) {
const { displaySetService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();

const srDisplaySets = displaySetCache.filter(
const srDisplaySets = [...displaySetCache.values()].filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId
);

Expand Down
Loading

0 comments on commit 69d8e6a

Please sign in to comment.