Skip to content

Commit

Permalink
Issue 45: Add ultrasound directional support to measurement service
Browse files Browse the repository at this point in the history
  • Loading branch information
IbrahimCSAE committed Apr 16, 2024
1 parent cd5028f commit 4083f52
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
9 changes: 9 additions & 0 deletions extensions/cornerstone/src/initMeasurementService.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const initMeasurementService = (
SplineROI,
LivewireContour,
Probe,
UltrasoundDirectional,
} = measurementServiceMappingsFactory(
measurementService,
displaySetService,
Expand Down Expand Up @@ -166,6 +167,14 @@ const initMeasurementService = (
Probe.toMeasurement
);

measurementService.addMapping(
csTools3DVer1MeasurementSource,
'UltrasoundDirectionalTool',
UltrasoundDirectional.matchingCriteria,
UltrasoundDirectional.toAnnotation,
UltrasoundDirectional.toMeasurement
);

return csTools3DVer1MeasurementSource;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';

const UltrasoundDirectional = {
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,
customizationService
) => {
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);

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, customizationService);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);

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 { xValues, yValues, units, isUnitless, isHorizontal } = targetStats;

annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
xValues,
yValues,
units,
isUnitless,
isHorizontal,
});
});

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, customizationService) {
const columns = [];
const values = [];

// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:UltrasoundDirectional');

mappedAnnotations.forEach(annotation => {
const { xValues, yValues, units, isUnitless } = annotation;
if (isUnitless) {
columns.push('Length' + units[0]);
values.push(utils.roundNumber(xValues[0], 2));
} else {
const dist1 = Math.abs(xValues[1] - xValues[0]);
const dist2 = Math.abs(yValues[1] - yValues[0]);
columns.push('Time' + units[0]);
values.push(utils.roundNumber(dist1, 2));
columns.push('Length' + units[1]);
values.push(utils.roundNumber(dist2, 2));
}
});

if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}

if (points) {
columns.push('points');
values.push(points.map(p => p.join(' ')).join(';'));
}

return {
columns,
values,
};
}

function getDisplayText(mappedAnnotations, displaySet, customizationService) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}

const displayText = [];

const { xValues, yValues, units, isUnitless, 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}` : '';
const seriesText = `(S: ${SeriesNumber}${instanceText}${frameText})`;

if (xValues === undefined || yValues === undefined) {
return displayText;
}

if (isUnitless) {
displayText.push(`${utils.roundNumber(xValues[0], 2)} ${units[0]} ${seriesText}`);
} else {
const dist1 = Math.abs(xValues[1] - xValues[0]);
const dist2 = Math.abs(yValues[1] - yValues[0]);
displayText.push(`${utils.roundNumber(dist1)} ${units[0]} ${seriesText}`);
displayText.push(`${utils.roundNumber(dist2)} ${units[1]} ${seriesText}`);
}

return displayText;
}

export default UltrasoundDirectional;
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export default [
'SplineROI',
'LivewireContour',
'Probe',
'UltrasoundDirectionalTool',
];
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import RectangleROI from './RectangleROI';
import SplineROI from './SplineROI';
import LivewireContour from './LivewireContour';
import Probe from './Probe';
import UltrasoundDirectional from './UltrasoundDirectional';

const measurementServiceMappingsFactory = (
measurementService: MeasurementService,
Expand Down Expand Up @@ -46,6 +47,7 @@ const measurementServiceMappingsFactory = (
SplineROI: POLYLINE,
LivewireContour: POLYLINE,
Probe: POINT,
UltrasoundDirectional: POLYLINE,
};

return TOOL_TYPE_TO_VALUE_TYPE[toolType];
Expand Down Expand Up @@ -264,6 +266,23 @@ const measurementServiceMappingsFactory = (
},
],
},
UltrasoundDirectional: {
toAnnotation: UltrasoundDirectional.toAnnotation,
toMeasurement: csToolsAnnotation =>
UltrasoundDirectional.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType,
customizationService
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
};

return factories;
Expand Down

0 comments on commit 4083f52

Please sign in to comment.