feat(measurements): add CircleROI Tool (#3211)

* [refactor] measurement service mapping files - rename to .ts files

* [fix] RectangleROI - measurement service mapping _getReport() function - tool name fix

* [feat] added CircleROI tool from updated cornerstone3D

* [refactor] fix for typescript strong typing warnings

* [feat] cornerstone-dicom-sr extension - added CircleROI tool

* [feat] added toolbar button for CircleROI tool

* [doc] doc updates for CircleROI tool

* [update] library - dcmjs to 0.29.5

* [fix] roundNumber() function when given strings

* [fix] refactor after upgrading conerstonejs library 0.49.0

* yarn.lock file change
This commit is contained in:
M.D authored and GitHub committed 2023-04-17 08:23:03 -04:00
1 parent 83b4606357
commit 47c25f4364
35 files changed
+412 -69

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"cornerstone-wado-image-loader": "^4.13.0",
"dcmjs": "^0.29.4",
"dcmjs": "^0.29.5",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -9,6 +9,7 @@ import {
LengthTool,
RectangleROITool,
EllipticalROITool,
CircleROITool,
BidirectionalTool,
ArrowAnnotateTool,
DragProbeTool,
@@ -41,6 +42,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(LengthTool);
addTool(RectangleROITool);
addTool(EllipticalROITool);
addTool(CircleROITool);
addTool(BidirectionalTool);
addTool(ArrowAnnotateTool);
addTool(DragProbeTool);
@@ -83,6 +85,7 @@ const toolNames = {
Probe: ProbeTool.toolName,
RectangleROI: RectangleROITool.toolName,
EllipticalROI: EllipticalROITool.toolName,
CircleROI: CircleROITool.toolName,
Bidirectional: BidirectionalTool.toolName,
Angle: AngleTool.toolName,
CobbAngle: CobbAngleTool.toolName,
@@ -24,6 +24,7 @@ const initMeasurementService = (
Length,
Bidirectional,
EllipticalROI,
CircleROI,
ArrowAnnotate,
Angle,
CobbAngle,
@@ -78,6 +79,14 @@ const initMeasurementService = (
EllipticalROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CircleROI',
CircleROI.matchingCriteria,
CircleROI.toAnnotation,
CircleROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'ArrowAnnotate',
@@ -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 CircleROI = {
toAnnotation: measurement => {},
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) {
// 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:CircleROI');
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 CircleROI;
@@ -1,6 +1,6 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { displaySetService, utils } from '@ohif/core';
import { utils } from '@ohif/core';
const Length = {
toAnnotation: measurement => {},
@@ -32,15 +32,12 @@ const Length = {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
cornerstoneViewportService,
viewportId
);
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
getSOPInstanceAttributes(
referencedImageId,
cornerstoneViewportService,
viewportId
);
let displaySet;
@@ -104,11 +101,8 @@ function getMappedAnnotations(annotation, displaySetService) {
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
getSOPInstanceAttributes(referencedImageId);
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
@@ -179,12 +173,8 @@ function getDisplayText(mappedAnnotations, displaySet) {
const displayText = [];
// Area is the same for all series
const {
length,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const { length, SeriesNumber, SOPInstanceUID, frameNumber } =
mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
@@ -4,7 +4,7 @@ import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const RectangleROI = {
toAnnotation: measurement => { },
toAnnotation: measurement => {},
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
@@ -26,15 +26,12 @@ const RectangleROI = {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
@@ -99,11 +96,8 @@ function getMappedAnnotations(annotation, DisplaySetService) {
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
@@ -143,7 +137,7 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:EllipticalROI');
values.push('Cornerstone:RectangleROI');
mappedAnnotations.forEach(annotation => {
const { mean, stdDev, max, area, unit } = annotation;
@@ -1,6 +1,7 @@
export default [
'Length',
'EllipticalROI',
'CircleROI',
'Bidirectional',
'ArrowAnnotate',
'Angle',
@@ -2,6 +2,7 @@ import { MeasurementService } from '@ohif/core';
import Length from './Length';
import Bidirectional from './Bidirectional';
import EllipticalROI from './EllipticalROI';
import CircleROI from './CircleROI';
import ArrowAnnotate from './ArrowAnnotate';
import CobbAngle from './CobbAngle';
import Angle from './Angle';
@@ -25,6 +26,7 @@ const measurementServiceMappingsFactory = (
const {
POLYLINE,
ELLIPSE,
CIRCLE,
RECTANGLE,
BIDIRECTIONAL,
POINT,
@@ -37,6 +39,7 @@ const measurementServiceMappingsFactory = (
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalROI: ELLIPSE,
CircleROI: CIRCLE,
RectangleROI: RECTANGLE,
PlanarFreehandROI: POLYLINE,
Bidirectional: BIDIRECTIONAL,
@@ -104,6 +107,22 @@ const measurementServiceMappingsFactory = (
],
},
CircleROI: {
toAnnotation: CircleROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
CircleROI.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.CIRCLE,
},
],
},
RectangleROI: {
toAnnotation: RectangleROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
@@ -13,8 +13,8 @@ import * as cornerstone from '@cornerstonejs/core';
*/
export default function getSOPInstanceAttributes(
imageId,
cornerstoneViewportService,
viewportId
cornerstoneViewportService = undefined,
viewportId = undefined
) {
if (imageId) {
return _getUIDFromImageID(imageId);