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:
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');
|
||||
|
||||
|
||||
+25
-17
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user