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
@@ -36,7 +36,7 @@
|
||||
"@ohif/extension-cornerstone": "^3.0.0",
|
||||
"@ohif/extension-measurement-tracking": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"dcmjs": "0.29.4",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -45,7 +45,8 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
"@cornerstonejs/core": "0.30.1",
|
||||
"@cornerstonejs/tools": "0.39.0"
|
||||
"@cornerstonejs/adapters": "^0.3.1",
|
||||
"@cornerstonejs/core": "^0.30.1",
|
||||
"@cornerstonejs/tools": "^0.44.0"
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"cornerstone-wado-image-loader": "^4.2.1",
|
||||
"dcmjs": "0.29.4",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -43,10 +43,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "0.3.0",
|
||||
"@cornerstonejs/core": "0.30.1",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.11.2",
|
||||
"@cornerstonejs/tools": "0.39.0",
|
||||
"@cornerstonejs/adapters": "^0.3.1",
|
||||
"@cornerstonejs/core": "^0.30.1",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.8.2",
|
||||
"@cornerstonejs/tools": "^0.44.0",
|
||||
"@kitware/vtk.js": "26.4.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
|
||||
import { id } from './id';
|
||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@@ -62,10 +63,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Register the Cornerstone 3D services and set them up for use.
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
async preRegistration({
|
||||
servicesManager,
|
||||
@@ -151,3 +151,4 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
};
|
||||
|
||||
export default cornerstoneExtension;
|
||||
export { measurementMappingUtils };
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
DragProbeTool,
|
||||
ProbeTool,
|
||||
AngleTool,
|
||||
CobbAngleTool,
|
||||
PlanarFreehandROITool,
|
||||
MagnifyTool,
|
||||
CrosshairsTool,
|
||||
SegmentationDisplayTool,
|
||||
@@ -40,6 +42,8 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(ArrowAnnotateTool);
|
||||
addTool(DragProbeTool);
|
||||
addTool(AngleTool);
|
||||
addTool(CobbAngleTool);
|
||||
addTool(PlanarFreehandROITool);
|
||||
addTool(MagnifyTool);
|
||||
addTool(CrosshairsTool);
|
||||
addTool(SegmentationDisplayTool);
|
||||
@@ -76,6 +80,8 @@ const toolNames = {
|
||||
EllipticalROI: EllipticalROITool.toolName,
|
||||
Bidirectional: BidirectionalTool.toolName,
|
||||
Angle: AngleTool.toolName,
|
||||
CobbAngle: CobbAngleTool.toolName,
|
||||
PlanarFreehandROI: PlanarFreehandROITool.toolName,
|
||||
Magnify: MagnifyTool.toolName,
|
||||
Crosshairs: CrosshairsTool.toolName,
|
||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums, annotation } from '@cornerstonejs/tools';
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
import { DicomMetadataStore, MeasurementService } from '@ohif/core';
|
||||
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
@@ -23,6 +23,10 @@ const initMeasurementService = (
|
||||
Bidirectional,
|
||||
EllipticalROI,
|
||||
ArrowAnnotate,
|
||||
Angle,
|
||||
CobbAngle,
|
||||
RectangleROI,
|
||||
PlanarFreehandROI,
|
||||
} = measurementServiceMappingsFactory(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
@@ -66,6 +70,38 @@ const initMeasurementService = (
|
||||
ArrowAnnotate.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'CobbAngle',
|
||||
CobbAngle.matchingCriteria,
|
||||
CobbAngle.toAnnotation,
|
||||
CobbAngle.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Angle',
|
||||
Angle.matchingCriteria,
|
||||
Angle.toAnnotation,
|
||||
Angle.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'RectangleROI',
|
||||
RectangleROI.matchingCriteria,
|
||||
RectangleROI.toAnnotation,
|
||||
RectangleROI.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'PlanarFreehandROI',
|
||||
PlanarFreehandROI.matchingCriteria,
|
||||
PlanarFreehandROI.toAnnotation,
|
||||
PlanarFreehandROI.toMeasurement
|
||||
);
|
||||
|
||||
return csTools3DVer1MeasurementSource;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const Angle = {
|
||||
toAnnotation: measurement => { },
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
displaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Length tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { points } = data.handles;
|
||||
|
||||
const mappedAnnotations = getMappedAnnotations(
|
||||
annotation,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||
const getReport = () =>
|
||||
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||
const { metadata, data } = annotation;
|
||||
const { cachedStats } = data;
|
||||
const { referencedImageId } = metadata;
|
||||
const targets = Object.keys(cachedStats);
|
||||
|
||||
if (!targets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const annotations = [];
|
||||
Object.keys(cachedStats).forEach(targetId => {
|
||||
const targetStats = cachedStats[targetId];
|
||||
|
||||
if (!referencedImageId) {
|
||||
throw new Error(
|
||||
'Non-acquisition plane measurement mapping not supported'
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber,
|
||||
} = getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { angle } = targetStats;
|
||||
const unit = '\u00B0';
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
unit,
|
||||
angle,
|
||||
});
|
||||
});
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/*
|
||||
This function is used to convert the measurement data to a format that is
|
||||
suitable for the report generation (e.g. for the csv report). The report
|
||||
returns a list of columns and corresponding values.
|
||||
*/
|
||||
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
// Add Type
|
||||
columns.push('AnnotationType');
|
||||
values.push('Cornerstone:Angle');
|
||||
|
||||
mappedAnnotations.forEach(annotation => {
|
||||
const { angle, unit } = annotation;
|
||||
columns.push(`Angle (${unit})`);
|
||||
values.push(angle);
|
||||
});
|
||||
|
||||
if (FrameOfReferenceUID) {
|
||||
columns.push('FrameOfReferenceUID');
|
||||
values.push(FrameOfReferenceUID);
|
||||
}
|
||||
|
||||
if (points) {
|
||||
columns.push('points');
|
||||
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||
// so that it can be used in the csv report
|
||||
values.push(points.map(p => p.join(' ')).join(';'));
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayText(mappedAnnotations, displaySet) {
|
||||
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const displayText = [];
|
||||
|
||||
// Area is the same for all series
|
||||
const {
|
||||
angle,
|
||||
unit,
|
||||
SeriesNumber,
|
||||
SOPInstanceUID,
|
||||
frameNumber,
|
||||
} = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(
|
||||
image => image.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
if (angle === undefined) return displayText;
|
||||
const roundedAngle = utils.roundNumber(angle, 2);
|
||||
displayText.push(
|
||||
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||
);
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
export default Angle;
|
||||
@@ -0,0 +1,210 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const CobbAngle = {
|
||||
toAnnotation: measurement => { },
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
displaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Cobb Angle tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { points } = data.handles;
|
||||
|
||||
const mappedAnnotations = getMappedAnnotations(
|
||||
annotation,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||
const getReport = () =>
|
||||
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||
const { metadata, data } = annotation;
|
||||
const { cachedStats } = data;
|
||||
const { referencedImageId } = metadata;
|
||||
const targets = Object.keys(cachedStats);
|
||||
|
||||
if (!targets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const annotations = [];
|
||||
Object.keys(cachedStats).forEach(targetId => {
|
||||
const targetStats = cachedStats[targetId];
|
||||
|
||||
if (!referencedImageId) {
|
||||
throw new Error(
|
||||
'Non-acquisition plane measurement mapping not supported'
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber,
|
||||
} = getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { angle } = targetStats;
|
||||
const unit = '\u00B0';
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
unit,
|
||||
angle,
|
||||
});
|
||||
});
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/*
|
||||
This function is used to convert the measurement data to a format that is
|
||||
suitable for the report generation (e.g. for the csv report). The report
|
||||
returns a list of columns and corresponding values.
|
||||
*/
|
||||
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
// Add Type
|
||||
columns.push('AnnotationType');
|
||||
values.push('Cornerstone:CobbAngle');
|
||||
|
||||
mappedAnnotations.forEach(annotation => {
|
||||
const { angle, unit } = annotation;
|
||||
columns.push(`Angle (${unit})`);
|
||||
values.push(angle);
|
||||
});
|
||||
|
||||
if (FrameOfReferenceUID) {
|
||||
columns.push('FrameOfReferenceUID');
|
||||
values.push(FrameOfReferenceUID);
|
||||
}
|
||||
|
||||
if (points) {
|
||||
columns.push('points');
|
||||
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||
// so that it can be used in the csv report
|
||||
values.push(points.map(p => p.join(' ')).join(';'));
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayText(mappedAnnotations, displaySet) {
|
||||
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const displayText = [];
|
||||
|
||||
// Area is the same for all series
|
||||
const {
|
||||
angle,
|
||||
unit,
|
||||
SeriesNumber,
|
||||
SOPInstanceUID,
|
||||
frameNumber,
|
||||
} = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(
|
||||
image => image.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
if (angle === undefined) return displayText;
|
||||
const roundedAngle = utils.roundNumber(angle, 2);
|
||||
displayText.push(
|
||||
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||
);
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
export default CobbAngle;
|
||||
@@ -0,0 +1,148 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
|
||||
const PlanarFreehandROI = {
|
||||
toAnnotation: measurement => { },
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('PlanarFreehandROI tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { points } = data.handles;
|
||||
|
||||
const mappedAnnotations = getMappedAnnotations(
|
||||
annotation,
|
||||
DisplaySetService
|
||||
);
|
||||
|
||||
const displayText = getDisplayText(mappedAnnotations);
|
||||
const getReport = () =>
|
||||
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: { ...data, ...data.cachedStats },
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* It maps an imaging library annotation to a list of simplified annotation properties.
|
||||
*
|
||||
* @param {Object} annotationData
|
||||
* @param {Object} DisplaySetService
|
||||
* @returns
|
||||
*/
|
||||
function getMappedAnnotations(annotationData, DisplaySetService) {
|
||||
const { metadata, data } = annotationData;
|
||||
const { label } = data;
|
||||
const { referencedImageId } = metadata;
|
||||
|
||||
const annotations = [];
|
||||
|
||||
const {
|
||||
SOPInstanceUID: _SOPInstanceUID,
|
||||
SeriesInstanceUID: _SeriesInstanceUID,
|
||||
} = getSOPInstanceAttributes(referencedImageId) || {};
|
||||
|
||||
if (!_SOPInstanceUID || !_SeriesInstanceUID) {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
_SOPInstanceUID,
|
||||
_SeriesInstanceUID
|
||||
);
|
||||
|
||||
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SeriesNumber,
|
||||
label,
|
||||
data,
|
||||
});
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* TBD
|
||||
* This function is used to convert the measurement data to a format that is suitable for the report generation (e.g. for the csv report).
|
||||
* The report returns a list of columns and corresponding values.
|
||||
* @param {*} mappedAnnotations
|
||||
* @param {*} points
|
||||
* @param {*} FrameOfReferenceUID
|
||||
* @returns Object representing the report's content for this tool.
|
||||
*/
|
||||
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
return {
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayText(mappedAnnotations) {
|
||||
return '';
|
||||
}
|
||||
|
||||
export default PlanarFreehandROI;
|
||||
@@ -0,0 +1,228 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import getModalityUnit from './utils/getModalityUnit';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const RectangleROI = {
|
||||
toAnnotation: measurement => { },
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Rectangle ROI tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { points } = data.handles;
|
||||
|
||||
const mappedAnnotations = getMappedAnnotations(
|
||||
annotation,
|
||||
DisplaySetService
|
||||
);
|
||||
|
||||
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||
const getReport = () =>
|
||||
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||
const { metadata, data } = annotation;
|
||||
const { cachedStats } = data;
|
||||
const { referencedImageId } = metadata;
|
||||
const targets = Object.keys(cachedStats);
|
||||
|
||||
if (!targets.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const annotations = [];
|
||||
Object.keys(cachedStats).forEach(targetId => {
|
||||
const targetStats = cachedStats[targetId];
|
||||
|
||||
if (!referencedImageId) {
|
||||
// Todo: Non-acquisition plane measurement mapping not supported yet
|
||||
throw new Error(
|
||||
'Non-acquisition plane measurement mapping not supported'
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber,
|
||||
} = getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber
|
||||
);
|
||||
|
||||
const { SeriesNumber } = displaySet;
|
||||
const { mean, stdDev, max, area, Modality } = targetStats;
|
||||
const unit = getModalityUnit(Modality);
|
||||
|
||||
annotations.push({
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesNumber,
|
||||
frameNumber,
|
||||
Modality,
|
||||
unit,
|
||||
mean,
|
||||
stdDev,
|
||||
max,
|
||||
area,
|
||||
});
|
||||
});
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/*
|
||||
This function is used to convert the measurement data to a format that is
|
||||
suitable for the report generation (e.g. for the csv report). The report
|
||||
returns a list of columns and corresponding values.
|
||||
*/
|
||||
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||
const columns = [];
|
||||
const values = [];
|
||||
|
||||
// Add Type
|
||||
columns.push('AnnotationType');
|
||||
values.push('Cornerstone:EllipticalROI');
|
||||
|
||||
mappedAnnotations.forEach(annotation => {
|
||||
const { mean, stdDev, max, area, unit } = annotation;
|
||||
|
||||
if (!mean || !unit || !max || !area) {
|
||||
return;
|
||||
}
|
||||
|
||||
columns.push(
|
||||
`max (${unit})`,
|
||||
`mean (${unit})`,
|
||||
`std (${unit})`,
|
||||
`area (mm2)`
|
||||
);
|
||||
values.push(max, mean, stdDev, area);
|
||||
});
|
||||
|
||||
if (FrameOfReferenceUID) {
|
||||
columns.push('FrameOfReferenceUID');
|
||||
values.push(FrameOfReferenceUID);
|
||||
}
|
||||
|
||||
if (points) {
|
||||
columns.push('points');
|
||||
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||
// so that it can be used in the csv report
|
||||
values.push(points.map(p => p.join(' ')).join(';'));
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function getDisplayText(mappedAnnotations, displaySet) {
|
||||
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const displayText = [];
|
||||
|
||||
// Area is the same for all series
|
||||
const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(
|
||||
image => image.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
}
|
||||
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
|
||||
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
|
||||
const roundedArea = utils.roundNumber(area || 0, 2);
|
||||
displayText.push(`${roundedArea} mm<sup>2</sup>`);
|
||||
|
||||
// Todo: we need a better UI for displaying all these information
|
||||
mappedAnnotations.forEach(mappedAnnotation => {
|
||||
const { unit, max, SeriesNumber } = mappedAnnotation;
|
||||
|
||||
let maxStr = '';
|
||||
if (max) {
|
||||
const roundedMax = utils.roundNumber(max, 2);
|
||||
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
|
||||
}
|
||||
|
||||
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
|
||||
if (!displayText.includes(str)) {
|
||||
displayText.push(str);
|
||||
}
|
||||
});
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
export default RectangleROI;
|
||||
+11
-1
@@ -1 +1,11 @@
|
||||
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];
|
||||
export default [
|
||||
'Length',
|
||||
'EllipticalROI',
|
||||
'Bidirectional',
|
||||
'ArrowAnnotate',
|
||||
'Angle',
|
||||
'CobbAngle',
|
||||
'Probe',
|
||||
'RectangleROI',
|
||||
'PlanarFreehandROI',
|
||||
];
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as measurementMappingUtils from './utils';
|
||||
|
||||
export { measurementMappingUtils };
|
||||
+88
-11
@@ -1,19 +1,24 @@
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import Length from './Length';
|
||||
import Bidirectional from './Bidirectional';
|
||||
import EllipticalROI from './EllipticalROI';
|
||||
import ArrowAnnotate from './ArrowAnnotate';
|
||||
import CobbAngle from './CobbAngle';
|
||||
import Angle from './Angle';
|
||||
import PlanarFreehandROI from './PlanarFreehandROI';
|
||||
import RectangleROI from './RectangleROI';
|
||||
|
||||
const measurementServiceMappingsFactory = (
|
||||
measurementService,
|
||||
measurementService: MeasurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
) => {
|
||||
/**
|
||||
* Maps measurement service format object to cornerstone annotation object.
|
||||
*
|
||||
* @param {Measurement} measurement The measurement instance
|
||||
* @param {string} definition The source definition
|
||||
* @return {Object} Cornerstone annotation data
|
||||
* @param measurement The measurement instance
|
||||
* @param definition The source definition
|
||||
* @return Cornerstone annotation data
|
||||
*/
|
||||
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
@@ -23,7 +28,8 @@ const measurementServiceMappingsFactory = (
|
||||
RECTANGLE,
|
||||
BIDIRECTIONAL,
|
||||
POINT,
|
||||
} = measurementService.VALUE_TYPES;
|
||||
ANGLE,
|
||||
} = MeasurementService.VALUE_TYPES;
|
||||
|
||||
// TODO -> I get why this was attempted, but its not nearly flexible enough.
|
||||
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
||||
@@ -32,14 +38,17 @@ const measurementServiceMappingsFactory = (
|
||||
Length: POLYLINE,
|
||||
EllipticalROI: ELLIPSE,
|
||||
RectangleROI: RECTANGLE,
|
||||
PlanarFreehandROI: POLYLINE,
|
||||
Bidirectional: BIDIRECTIONAL,
|
||||
ArrowAnnotate: POINT,
|
||||
CobbAngle: ANGLE,
|
||||
Angle: ANGLE,
|
||||
};
|
||||
|
||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||
};
|
||||
|
||||
return {
|
||||
const factories = {
|
||||
Length: {
|
||||
toAnnotation: Length.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
@@ -51,7 +60,7 @@ const measurementServiceMappingsFactory = (
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
},
|
||||
],
|
||||
@@ -69,15 +78,16 @@ const measurementServiceMappingsFactory = (
|
||||
// TODO -> We should eventually do something like shortAxis + longAxis,
|
||||
// But its still a little unclear how these automatic interpretations will work.
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
},
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
EllipticalROI: {
|
||||
toAnnotation: EllipticalROI.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
@@ -89,10 +99,43 @@ const measurementServiceMappingsFactory = (
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.ELLIPSE,
|
||||
valueType: MeasurementService.VALUE_TYPES.ELLIPSE,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
RectangleROI: {
|
||||
toAnnotation: RectangleROI.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
RectangleROI.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
PlanarFreehandROI: {
|
||||
toAnnotation: PlanarFreehandROI.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
PlanarFreehandROI.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
ArrowAnnotate: {
|
||||
toAnnotation: ArrowAnnotate.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
@@ -104,12 +147,46 @@ const measurementServiceMappingsFactory = (
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.POINT,
|
||||
valueType: MeasurementService.VALUE_TYPES.POINT,
|
||||
points: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
CobbAngle: {
|
||||
toAnnotation: CobbAngle.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
CobbAngle.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: MeasurementService.VALUE_TYPES.ANGLE,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Angle: {
|
||||
toAnnotation: Angle.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
Angle.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: MeasurementService.VALUE_TYPES.ANGLE,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return factories;
|
||||
};
|
||||
|
||||
export default measurementServiceMappingsFactory;
|
||||
@@ -0,0 +1,17 @@
|
||||
import getHandlesFromPoints from './getHandlesFromPoints';
|
||||
import {
|
||||
isAnnotationSelected,
|
||||
setAnnotationSelected,
|
||||
getFirstAnnotationSelected,
|
||||
} from './selection';
|
||||
import getModalityUnit from './getModalityUnit';
|
||||
import getSOPInstanceAttributes from './getSOPInstanceAttributes';
|
||||
|
||||
export {
|
||||
getModalityUnit,
|
||||
getHandlesFromPoints,
|
||||
getSOPInstanceAttributes,
|
||||
isAnnotationSelected,
|
||||
setAnnotationSelected,
|
||||
getFirstAnnotationSelected,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { annotation as cs3dToolAnnotationUtils } from '@cornerstonejs/tools';
|
||||
|
||||
/**
|
||||
* Check whether an annotation from imaging library is selected or not.
|
||||
* @param {string} annotationUID uid of imaging library annotation
|
||||
* @returns boolean
|
||||
*/
|
||||
function isAnnotationSelected(annotationUID: string): boolean {
|
||||
return cs3dToolAnnotationUtils.selection.isAnnotationSelected(annotationUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change an annotation from imaging library's selected property.
|
||||
* @param annotationUID - uid of imaging library annotation
|
||||
* @param selected - new value for selected
|
||||
*/
|
||||
function setAnnotationSelected(annotationUID: string, selected: boolean): void {
|
||||
const isCurrentSelected = isAnnotationSelected(annotationUID);
|
||||
// branch cut, avoid invoking imaging library unnecessarily.
|
||||
if (isCurrentSelected !== selected) {
|
||||
cs3dToolAnnotationUtils.selection.setAnnotationSelected(
|
||||
annotationUID,
|
||||
selected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstAnnotationSelected(element) {
|
||||
const [selectedAnnotationUID] =
|
||||
cs3dToolAnnotationUtils.selection.getAnnotationsSelected() || [];
|
||||
|
||||
if (selectedAnnotationUID) {
|
||||
return cs3dToolAnnotationUtils.state.getAnnotation(
|
||||
selectedAnnotationUID,
|
||||
element
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isAnnotationSelected,
|
||||
setAnnotationSelected,
|
||||
getFirstAnnotationSelected,
|
||||
};
|
||||
@@ -32,8 +32,8 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/i18n": "^1.0.0",
|
||||
"dcmjs": "0.29.4",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicomweb-client": "^0.8.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils, ServicesManager } from '@ohif/core';
|
||||
import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
import createReportDialogPrompt, {
|
||||
CREATE_REPORT_DIALOG_RESPONSE,
|
||||
} from './createReportDialogPrompt';
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import getNextSRSeriesNumber from '../utils/getNextSRSeriesNumber';
|
||||
import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}) {
|
||||
}): React.FunctionComponent {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const { activeViewportIndex, viewports } = viewportGrid;
|
||||
const {
|
||||
@@ -25,7 +25,7 @@ export default function PanelMeasurementTable({
|
||||
uiDialogService,
|
||||
uiNotificationService,
|
||||
displaySetService,
|
||||
} = servicesManager.services;
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,7 +72,7 @@ export default function PanelMeasurementTable({
|
||||
measurementService.clearMeasurements();
|
||||
}
|
||||
|
||||
async function createReport() {
|
||||
async function createReport(): Promise<any> {
|
||||
// filter measurements that are added to the active study
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
const measurements = measurementService.getMeasurements();
|
||||
@@ -109,17 +109,19 @@ export default function PanelMeasurementTable({
|
||||
? 'Research Derived Series' // default
|
||||
: promptResult.value; // provided value
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
// Re-use an existing series having the same series description to avoid
|
||||
// creating too many series instances.
|
||||
const options = findSRWithSameSeriesDescription(
|
||||
SeriesDescription,
|
||||
displaySetService
|
||||
);
|
||||
|
||||
const displaySetInstanceUIDs = await createReportAsync(
|
||||
return createReportAsync(
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
dataSource,
|
||||
trackedMeasurements,
|
||||
{
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
}
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -233,16 +235,7 @@ export default function PanelMeasurementTable({
|
||||
}
|
||||
|
||||
PanelMeasurementTable.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
measurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||
};
|
||||
|
||||
function _getMappedMeasurements(measurementService) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DisplaySetService, Types } from '@ohif/core';
|
||||
|
||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||
|
||||
/**
|
||||
* Find an SR having the same series description.
|
||||
* This is used by the store service in order to store DICOM SR's having the
|
||||
* same Series Description into a single series under consecutive instance numbers
|
||||
* That way, they are all organized as a set and could have tools to view
|
||||
* "prior" SR instances.
|
||||
*
|
||||
* @param SeriesDescription - is the description to look for
|
||||
* @param displaySetService - the display sets to search for DICOM SR in
|
||||
* @returns SeriesMetadata from a DICOM SR having the same series description
|
||||
*/
|
||||
export default function findSRWithSameSeriesDescription(
|
||||
SeriesDescription: string,
|
||||
displaySetService: DisplaySetService
|
||||
): Types.SeriesMetadata {
|
||||
const activeDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
|
||||
const sameSeries = srDisplaySets.find(
|
||||
ds => ds.SeriesDescription === SeriesDescription
|
||||
);
|
||||
if (sameSeries) {
|
||||
console.log('Storing to same series', sameSeries);
|
||||
const { instance } = sameSeries;
|
||||
const {
|
||||
SeriesInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
Modality,
|
||||
} = instance;
|
||||
return {
|
||||
SeriesInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
Modality,
|
||||
InstanceNumber: sameSeries.others.length + 1,
|
||||
};
|
||||
}
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
return { SeriesDescription, SeriesNumber };
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"dcmjs": "0.29.4",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"dcmjs": "0.29.4",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"classnames": "^2.3.2",
|
||||
"@cornerstonejs/core": "0.30.1",
|
||||
"@cornerstonejs/tools": "0.39.0",
|
||||
"@cornerstonejs/core": "^0.30.1",
|
||||
"@cornerstonejs/tools": "^0.44.0",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
||||
"dcmjs": "0.29.4",
|
||||
"dcmjs": "^0.29.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"dcmjs": "0.29.4",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
Reference in new issue
Block a user