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:
35 files changed
+412
-69
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.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BidirectionalTool,
|
||||
CobbAngleTool,
|
||||
EllipticalROITool,
|
||||
CircleROITool,
|
||||
LengthTool,
|
||||
PlanarFreehandROITool,
|
||||
} from '@cornerstonejs/tools';
|
||||
@@ -24,6 +25,7 @@ export default function init({
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||
@@ -40,6 +42,7 @@ export default function init({
|
||||
SRLength: dashedLine,
|
||||
SRBidirectional: dashedLine,
|
||||
SREllipticalROI: dashedLine,
|
||||
SRCircleROI: dashedLine,
|
||||
SRArrowAnnotate: dashedLine,
|
||||
SRCobbAngle: dashedLine,
|
||||
SRAngle: dashedLine,
|
||||
|
||||
@@ -5,6 +5,7 @@ const toolNames = {
|
||||
SRLength: 'SRLength',
|
||||
SRBidirectional: 'SRBidirectional',
|
||||
SREllipticalROI: 'SREllipticalROI',
|
||||
SRCircleROI: 'SRCircleROI',
|
||||
SRArrowAnnotate: 'SRArrowAnnotate',
|
||||
SRAngle: 'SRAngle',
|
||||
SRCobbAngle: 'SRCobbAngle',
|
||||
|
||||
@@ -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',
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
+11
-21
@@ -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
@@ -1,6 +1,7 @@
|
||||
export default [
|
||||
'Length',
|
||||
'EllipticalROI',
|
||||
'CircleROI',
|
||||
'Bidirectional',
|
||||
'ArrowAnnotate',
|
||||
'Angle',
|
||||
|
||||
+19
@@ -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 =>
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/i18n": "^1.0.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicomweb-client": "^0.8.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^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.5",
|
||||
"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.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@cornerstonejs/core": "^0.40.0",
|
||||
"@cornerstonejs/tools": "^0.60.1",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dcmjs": "^0.29.5",
|
||||
"lodash.debounce": "^4.17.21",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^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.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
Reference in new issue
Block a user