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,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,
|
||||
};
|
||||
Reference in new issue
Block a user