feat(SR): Measurement adapter mappings and save to same series (#3140)

* feat: Add adapter mappings for RectangleROI and Angle and save same

PR comments

* fix: Rehydrate check on non core measurement adapters
This commit is contained in:
Bill Wallace authored and GitHub committed 2023-02-17 16:44:21 -05:00
1 parent 926290f69e
commit 67fc3f733d
42 files changed
+1277 -218

No files matched your search

@@ -30,14 +30,12 @@ const _generateReport = (
const report = MeasurementReport.generateReport(
filteredToolState,
metaData,
utilities.worldToImageCoords
utilities.worldToImageCoords,
options
);
const { dataset } = report;
// Add in top level series options
Object.assign(dataset, options);
// Set the default character set as UTF-8
// https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005
if (typeof dataset.SpecificCharacterSet === 'undefined') {
@@ -89,7 +87,7 @@ const commandsModule = ({}) => {
additionalFindingTypes,
options = {},
}) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// Use the @cornerstonejs adapter for converting to/from DICOM
// But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements');
@@ -51,13 +51,17 @@ const RELATIONSHIP_TYPE = {
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* Basic SOPClassHandler:
* - For all Image types that are stackable, create
* a displaySet with a stack of images
* 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)
*
* @param {Array} sopClassHandlerModules List of SOP Class Modules
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @returns {Array} The list of display sets created for the given series object
* 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.
*
* @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
*/
function _getDisplaySetsFromSeries(
instances,
@@ -69,7 +73,8 @@ function _getDisplaySetsFromSeries(
throw new Error('No instances were provided');
}
const instance = instances[0];
utils.sortStudyInstances(instances);
const instance = instances[instances.length - 1];
const {
StudyInstanceUID,
@@ -85,10 +90,10 @@ function _getDisplaySetsFromSeries(
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport
CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
console.warn(
'Only support Imaging Measurement Report SRs (TID1500) for now'
console.log(
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
);
return [];
}
@@ -105,6 +110,9 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID,
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,
@@ -313,13 +321,13 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
MeasurementGroups
);
let measurements = [];
const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => {
const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
trackingUniqueIdentifier
];
const measurement = _processMeasurement(mergedContentSequence);
@@ -359,7 +367,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
if (
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
trackingUniqueIdentifier
] === undefined
) {
// Add the full ContentSequence
@@ -474,9 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const FindingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT &&
CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
CodeNameCodeSequenceValues.FindingSite
);
const measurement = {
@@ -493,7 +501,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
Finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
Finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
@@ -509,7 +517,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
@@ -1,29 +0,0 @@
import { addTool, annotation } from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SRLengthTool from './tools/tools/SRLength';
import SRBidirectionalTool from './tools/tools/SRBidirectional';
import SREllipticalROITool from './tools/tools/SREllipticalROI';
import SRArrowAnnotateTool from './tools/tools/SRArrowAnnotate';
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }) {
addTool(DICOMSRDisplayTool);
addTool(SRLengthTool);
addTool(SRBidirectionalTool);
addTool(SREllipticalROITool);
addTool(SRArrowAnnotateTool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
[SRLengthTool.toolName]: dashedLine,
[SRBidirectionalTool.toolName]: dashedLine,
[SREllipticalROITool.toolName]: dashedLine,
[SRArrowAnnotateTool.toolName]: dashedLine,
global: {},
});
}
@@ -0,0 +1,49 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
BidirectionalTool,
CobbAngleTool,
EllipticalROITool,
LengthTool,
PlanarFreehandROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
import { Types } from '@ohif/core';
import toolNames from './tools/toolNames';
/**
* @param {object} configuration
*/
export default function init({
configuration = {},
}: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
addToolInstance(toolNames.SRAngle, AngleTool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
// TODO - fix the rehydration of Freehand, as it throws an exception
// on a missing polyline. The fix is probably in CS3D
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
SRLength: dashedLine,
SRBidirectional: dashedLine,
SREllipticalROI: dashedLine,
SRArrowAnnotate: dashedLine,
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
global: {},
});
}
@@ -1,15 +1,15 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
import SRLengthTool from './tools/SRLength';
import SRBidirectional from './tools/SRBidirectional';
import SREllipticalROI from './tools/SREllipticalROI';
import SRArrowAnnotate from './tools/SRArrowAnnotate';
const toolNames = {
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
SRLength: SRLengthTool.toolName,
SRBidirectional: SRBidirectional.toolName,
SREllipticalROI: SREllipticalROI.toolName,
SRArrowAnnotate: SRArrowAnnotate.toolName,
SRLength: 'SRLength',
SRBidirectional: 'SRBidirectional',
SREllipticalROI: 'SREllipticalROI',
SRArrowAnnotate: 'SRArrowAnnotate',
SRAngle: 'SRAngle',
SRCobbAngle: 'SRCobbAngle',
SRRectangleROI: 'SRRectangleROI',
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
};
export default toolNames;
@@ -1,16 +0,0 @@
import { ArrowAnnotateTool } from '@cornerstonejs/tools';
/**
* The reason we are extending ArrowAnnotateTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have ArrowAnnotate
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a ArrowAnnotateTool mapper),
* but with extending and renaming it, there is not mapper for SRArrowAnnotateTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRArrowAnnotateTool extends ArrowAnnotateTool {
static toolName = 'SRArrowAnnotate';
}
export default SRArrowAnnotateTool;
@@ -1,16 +0,0 @@
import { BidirectionalTool } from '@cornerstonejs/tools';
/**
* The reason we are extending BidirectionalTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Bidirectional
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a BidirectionalTool mapper),
* but with extending and renaming it, there is not mapper for SRBidirectionalTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRBidirectional extends BidirectionalTool {
static toolName = 'SRBidirectional';
}
export default SRBidirectional;
@@ -1,16 +0,0 @@
import { EllipticalROITool } from '@cornerstonejs/tools';
/**
* The reason we are extending EllipticalROITool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have EllipticalROI
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a EllipticalROITool mapper),
* but with extending and renaming it, there is not mapper for SREllipticalROITool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SREllipticalROI extends EllipticalROITool {
static toolName = 'SREllipticalROI';
}
export default SREllipticalROI;
@@ -1,16 +0,0 @@
import { LengthTool } from '@cornerstonejs/tools';
/**
* The reason we are extending LengthTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Length
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a LengthTool mapper),
* but with extending and renaming it, there is not mapper for SRLengthTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRLengthTool extends LengthTool {
static toolName = 'SRLength';
}
export default SRLengthTool;
@@ -0,0 +1,12 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(
name: string,
toolClass,
configuration?
): void {
class InstanceClass extends toolClass {
static toolName = name;
}
addTool(InstanceClass);
}
@@ -1,6 +1,8 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const cornerstoneAdapters = adaptersSR.Cornerstone3D;
const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport
.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
@@ -37,7 +39,7 @@ export default function isRehydratable(displaySet, mappings) {
});
for (let i = 0; i < measurements.length; i++) {
const TrackingIdentifier = measurements[i].TrackingIdentifier;
const { TrackingIdentifier } = measurements[i] || {};
const hydratable = adapters.some(adapter => {
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
@@ -54,7 +56,13 @@ export default function isRehydratable(displaySet, mappings) {
if (hydratable) {
return true;
}
console.log(
'Measurement is not rehydratable',
TrackingIdentifier,
measurements[i]
);
}
console.log('No measurements found which were rehydratable');
return false;
}
@@ -284,7 +284,7 @@ function OHIFCornerstoneSRViewport(props) {
* isHydratable check, the outcome for the isHydrated state here is always FALSE
* since we don't do the hydration here. Todo: can't we just set it as false? why
* we are changing the state here? isHydrated is always false at this stage, and
* if it is hydrated we don't event use the SR viewport.
* if it is hydrated we don't even use the SR viewport.
*/
useEffect(() => {
if (!srDisplaySet.isLoaded) {