feat: Ultrasound Pleura B-line Annotation Mode & Extension (#5061)

This commit is contained in:
rodrigobasilio2022 authored and GitHub committed 2025-06-24 12:50:48 -04:00
1 parent 9b8eef9474
commit 226f9b0773
46 files changed
+7123 -3660

No files matched your search

+7 -7
View File
@@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.15.6",
"@cornerstonejs/dicom-image-loader": "^3.16.0",
"@ohif/core": "3.11.0-beta.60",
"@ohif/ui": "3.11.0-beta.60",
"dcmjs": "*",
@@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.15.6",
"@cornerstonejs/ai": "^3.15.6",
"@cornerstonejs/core": "^3.15.6",
"@cornerstonejs/labelmap-interpolation": "^3.15.6",
"@cornerstonejs/polymorphic-segmentation": "^3.15.6",
"@cornerstonejs/tools": "^3.15.6",
"@cornerstonejs/adapters": "^3.16.0",
"@cornerstonejs/ai": "^3.16.0",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/labelmap-interpolation": "^3.16.0",
"@cornerstonejs/polymorphic-segmentation": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { metaData, Enums, utilities, eventTarget } from '@cornerstonejs/core';
import { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui-next';
import type { InstanceMetadata } from '@ohif/core/src/types';
@@ -68,6 +69,7 @@ function CustomizableViewportOverlay({
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [annotationState, setAnnotationState] = useState(0);
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
const { imageIndex } = imageSliceData;
@@ -132,6 +134,20 @@ function CustomizableViewportOverlay({
};
}, [viewportId, viewportData, voi, element]);
const annotationModified = useCallback(evt => {
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
// Update the annotation state to trigger a re-render
setAnnotationState(prevState => prevState + 1);
}
}, []);
useEffect(() => {
eventTarget.addEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
return () => {
eventTarget.removeEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
};
}, [annotationModified]);
/**
* Updating the scale when the viewport changes its zoom
*/
@@ -209,6 +225,7 @@ function CustomizableViewportOverlay({
voi,
scale,
instanceNumber,
annotationState,
]
);
@@ -31,6 +31,7 @@ import {
TrackballRotateTool,
AdvancedMagnifyTool,
UltrasoundDirectionalTool,
UltrasoundPleuraBLineTool,
PlanarFreehandROITool,
PlanarFreehandContourSegmentationTool,
SplineROITool,
@@ -95,6 +96,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(ImageOverlayViewerTool);
addTool(AdvancedMagnifyTool);
addTool(UltrasoundDirectionalTool);
addTool(UltrasoundPleuraBLineTool);
addTool(PlanarFreehandROITool);
addTool(SplineROITool);
addTool(LivewireContourTool);
@@ -151,6 +153,7 @@ const toolNames = {
ImageOverlayViewer: ImageOverlayViewerTool.toolName,
AdvancedMagnify: AdvancedMagnifyTool.toolName,
UltrasoundDirectional: UltrasoundDirectionalTool.toolName,
UltrasoundAnnotation: UltrasoundPleuraBLineTool.toolName,
SplineROI: SplineROITool.toolName,
LivewireContour: LivewireContourTool.toolName,
PlanarFreehandROI: PlanarFreehandROITool.toolName,
@@ -38,6 +38,7 @@ const initMeasurementService = (
LivewireContour,
Probe,
UltrasoundDirectional,
UltrasoundPleuraBLine,
SegmentBidirectional,
} = measurementServiceMappingsFactory(
measurementService,
@@ -177,6 +178,14 @@ const initMeasurementService = (
UltrasoundDirectional.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'UltrasoundPleuraBLineTool',
UltrasoundPleuraBLine.matchingCriteria,
UltrasoundPleuraBLine.toAnnotation,
UltrasoundPleuraBLine.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'SegmentBidirectional',
@@ -0,0 +1,179 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
import { annotation } from '@cornerstonejs/tools';
const UltrasoundPleuraBLine = {
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 } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('UltrasoundPleuraBLine 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,
isLocked,
isVisible,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata } = annotation;
const { annotationType } = annotation.data;
const { referencedImageId } = metadata;
const annotations = [];
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;
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
annotationType,
});
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:UltrasoundPleuraBLine');
mappedAnnotations.forEach(annotation => {
const { annotationType } = annotation;
columns.push('AnnotationType');
values.push(annotationType);
});
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) {
const displayText = {
primary: [],
secondary: [],
};
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
const { annotationType, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.instances.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}`;
displayText.primary.push(`Annotation : ${annotationType}`);
displayText.secondary.push(seriesText);
return displayText;
}
export default UltrasoundPleuraBLine;
@@ -12,6 +12,7 @@ const supportedTools = [
'SplineROI',
'LivewireContour',
'UltrasoundDirectionalTool',
'UltrasoundPleuraBLineTool',
'SCOORD3DPoint',
'SegmentBidirectional',
];
@@ -13,6 +13,7 @@ import LivewireContour from './LivewireContour';
import Probe from './Probe';
import UltrasoundDirectional from './UltrasoundDirectional';
import SegmentBidirectional from './SegmentBidirectional';
import UltrasoundPleuraBLine from './UltrasoundPleuraBLine';
const measurementServiceMappingsFactory = (
measurementService: MeasurementService,
@@ -296,6 +297,23 @@ const measurementServiceMappingsFactory = (
},
],
},
UltrasoundPleuraBLine: {
toAnnotation: UltrasoundPleuraBLine.toAnnotation,
toMeasurement: csToolsAnnotation =>
UltrasoundPleuraBLine.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType,
customizationService
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
};
return factories;