diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json index 4694aff802..f8df6cb0e0 100644 --- a/extensions/cornerstone-dicom-seg/package.json +++ b/extensions/cornerstone-dicom-seg/package.json @@ -46,8 +46,8 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.67.0", - "@cornerstonejs/core": "^1.67.0", + "@cornerstonejs/adapters": "^1.68.1", + "@cornerstonejs/core": "^1.68.1", "@kitware/vtk.js": "29.7.0", "react-color": "^2.19.3" } diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index a0d75ffb9b..da1c199346 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -46,9 +46,9 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.67.0", - "@cornerstonejs/core": "^1.67.0", - "@cornerstonejs/tools": "^1.67.0", + "@cornerstonejs/adapters": "^1.68.1", + "@cornerstonejs/core": "^1.68.1", + "@cornerstonejs/tools": "^1.68.1", "classnames": "^2.3.2" } } diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index fbeaa53164..be946bcfb1 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -1,6 +1,6 @@ import { SOPClassHandlerName, SOPClassHandlerId } from './id'; import { utils, classes, DisplaySetService, Types } from '@ohif/core'; -import addMeasurement from './utils/addMeasurement'; +import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation'; import isRehydratable from './utils/isRehydratable'; import { adaptersSR } from '@cornerstonejs/adapters'; @@ -150,13 +150,37 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) return [displaySet]; } -function _load(displaySet, servicesManager, extensionManager) { +async function _load(displaySet, servicesManager, extensionManager) { const { displaySetService, measurementService } = servicesManager.services; const dataSources = extensionManager.getDataSources(); const dataSource = dataSources[0]; const { ContentSequence } = displaySet.instance; + async function retrieveBulkData(obj, parentObj = null, key = null) { + for (const prop in obj) { + if (typeof obj[prop] === 'object' && obj[prop] !== null) { + await retrieveBulkData(obj[prop], obj, prop); + } else if (Array.isArray(obj[prop])) { + await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop))); + } else if (prop === 'BulkDataURI') { + const value = await dataSource.retrieve.bulkDataURI({ + BulkDataURI: obj[prop], + StudyInstanceUID: displaySet.instance.StudyInstanceUID, + SeriesInstanceUID: displaySet.instance.SeriesInstanceUID, + SOPInstanceUID: displaySet.instance.SOPInstanceUID, + }); + if (parentObj && key) { + parentObj[key] = new Float32Array(value); + } + } + } + } + + if (displaySet.isLoaded !== true) { + await retrieveBulkData(ContentSequence); + } + displaySet.referencedImages = _getReferencedImagesList(ContentSequence); displaySet.measurements = _getMeasurements(ContentSequence); @@ -211,7 +235,7 @@ function _checkIfCanAddMeasurementsToDisplaySet( return; } - if (!newDisplaySet instanceof ImageSet) { + if ((!newDisplaySet) instanceof ImageSet) { // This also filters out _this_ displaySet, as it is not an ImageSet. return; } @@ -275,7 +299,22 @@ function _checkIfCanAddMeasurementsToDisplaySet( } if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) { - addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID); + const frame = + (measurement.coords[0].ReferencedSOPSequence && + measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) || + 1; + + /** Add DICOMSRDisplay annotation for the SR viewport (only) */ + addDICOMSRDisplayAnnotation(measurement, imageId, frame); + + /** Update measurement properties */ + measurement.loaded = true; + measurement.imageId = imageId; + measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; + measurement.ReferencedSOPInstanceUID = + measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; + measurement.frameNumber = frame; + delete measurement.coords; unloadedMeasurements.splice(j, 1); } @@ -291,7 +330,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame // Standard. But for now, we will support only one ReferenceFrameNumber. const ReferencedFrameNumber = (measurement.coords[0].ReferencedSOPSequence && - measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) || + measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) || 1; if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) { diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx index 9b0700481e..b0b5e7abb1 100644 --- a/extensions/cornerstone-dicom-sr/src/index.tsx +++ b/extensions/cornerstone-dicom-sr/src/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; -import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule'; +import { srProtocol } from './getHangingProtocolModule'; import onModeEnter from './onModeEnter'; import getCommandsModule from './commandsModule'; import preRegistration from './init'; diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 1ab3300d77..949b4de2e6 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -1,5 +1,4 @@ import { - addTool, AngleTool, annotation, ArrowAnnotateTool, @@ -9,6 +8,7 @@ import { CircleROITool, LengthTool, PlanarFreehandROITool, + RectangleROITool, } from '@cornerstonejs/tools'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import addToolInstance from './utils/addToolInstance'; @@ -19,24 +19,25 @@ import toolNames from './tools/toolNames'; * @param {object} configuration */ export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void { - addTool(DICOMSRDisplayTool); - addToolInstance(toolNames.SRLength, LengthTool, {}); + addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool); + addToolInstance(toolNames.SRLength, LengthTool); addToolInstance(toolNames.SRBidirectional, BidirectionalTool); addToolInstance(toolNames.SREllipticalROI, EllipticalROITool); addToolInstance(toolNames.SRCircleROI, CircleROITool); addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool); addToolInstance(toolNames.SRAngle, AngleTool); + addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); + addToolInstance(toolNames.SRRectangleROI, RectangleROITool); + // TODO - fix the SR display of Cobb Angle, as it joins the two lines addToolInstance(toolNames.SRCobbAngle, CobbAngleTool); - // TODO - fix the rehydration of Freehand, as it throws an exception - // on a missing polyline. The fix is probably in CS3D - addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); // Modify annotation tools to use dashed lines on SR const dashedLine = { lineDash: '4,4', }; annotation.config.style.setToolGroupToolStyles('SRToolGroup', { + [toolNames.DICOMSRDisplay]: dashedLine, SRLength: dashedLine, SRBidirectional: dashedLine, SREllipticalROI: dashedLine, @@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP SRCobbAngle: dashedLine, SRAngle: dashedLine, SRPlanarFreehandROI: dashedLine, + SRRectangleROI: dashedLine, global: {}, }); } diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 7b14b107f5..278eed8288 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool { } _getTextBoxLinesFromLabels(labels) { - // TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this! + // TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this! - const labelLength = Math.min(labels.length, 3); + const labelLength = Math.min(labels.length, 5); const lines = []; for (let i = 0; i < labelLength; i++) { const labelEntry = labels[i]; - lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`); + lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`); } return lines; @@ -77,6 +77,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { toolName: this.getToolName(), viewportId: enabledElement.viewport.id, }; + const { style: annotationStyle } = annotation.config; for (let i = 0; i < filteredAnnotations.length; i++) { const annotation = filteredAnnotations[i]; @@ -87,6 +88,10 @@ export default class DICOMSRDisplayTool extends AnnotationTool { styleSpecifier.annotationUID = annotationUID; + const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[ + this.getToolName() + ]; + const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation); const lineDash = this.getStyle('lineDash', styleSpecifier, annotation); const color = @@ -98,6 +103,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { color, lineDash, lineWidth, + ...groupStyle, }; Object.keys(renderableData).forEach(GraphicType => { @@ -160,6 +166,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const drawingOptions = { color: options.color, width: options.lineWidth, + lineDash: options.lineDash, }; let allCanvasCoordinates = []; renderableData.map((data, index) => { @@ -307,6 +314,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool { { color: options.color, width: options.lineWidth, + lineDash: options.lineDash, } ); }); diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts similarity index 83% rename from extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts rename to extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts index 348a7a79d8..fc0eda76ca 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addDICOMSRDisplayAnnotation.ts @@ -1,15 +1,13 @@ import { vec3 } from 'gl-matrix'; import { Types, annotation } from '@cornerstonejs/tools'; import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core'; + import toolNames from '../tools/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; const EPSILON = 1e-4; -const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; - -export default function addMeasurement(measurement, imageId, displaySetInstanceUID) { - // TODO -> Render rotated ellipse . +export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) { const toolName = toolNames.DICOMSRDisplay; const measurementData = { @@ -27,26 +25,25 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU } measurementData.renderableData[GraphicType].push( - _getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier) + _getRenderableData(GraphicType, GraphicData, imageId) ); }); - // Use the metadata provider to grab its imagePlaneModule metadata const imagePlaneModule = metaData.get('imagePlaneModule', imageId); - const annotationManager = annotation.state.getAnnotationManager(); - - // Create Cornerstone3D Annotation from measurement - const frameNumber = - (measurement.coords[0].ReferencedSOPSequence && - measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) || - 1; - + /** + * This annotation (DICOMSRDisplay) is only used by the SR viewport. + * This is used before the annotation is hydrated. If hydrated the measurement will be added + * to the measurement service and will be available for the other viewports. + */ const SRAnnotation: Types.Annotation = { annotationUID: measurement.TrackingUniqueIdentifier, + highlighted: false, + isLocked: false, + invalidated: false, metadata: { - FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, toolName: toolName, + FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID, referencedImageId: imageId, }, data: { @@ -58,28 +55,14 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier, renderableData: measurementData.renderableData, }, - frameNumber: frameNumber, + frameNumber, }, }; - + const annotationManager = annotation.state.getAnnotationManager(); annotationManager.addAnnotation(SRAnnotation); - - measurement.loaded = true; - measurement.imageId = imageId; - measurement.displaySetInstanceUID = displaySetInstanceUID; - - // Remove the unneeded coord now its processed, but keep the SOPInstanceUID. - // NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame, - // It'd be super weird if it didn't anyway as a SCOORD. - measurement.ReferencedSOPInstanceUID = - measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; - measurement.frameNumber = frameNumber; - delete measurement.coords; } -function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) { - const [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); - +function _getRenderableData(GraphicType, GraphicData, imageId) { let renderableData: csTypes.Point3[]; switch (GraphicType) { diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 849813c57c..951644aa99 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -125,6 +125,10 @@ function OHIFCornerstoneSRViewport(props) { console.warn('More than one SOPClassUID in the same series is not yet supported.'); } + // if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) { + // return; + // } + _getViewportReferencedDisplaySetData( srDisplaySet, newMeasurementSelected, @@ -253,12 +257,16 @@ function OHIFCornerstoneSRViewport(props) { * if it is hydrated we don't even use the SR viewport. */ useEffect(() => { - if (!srDisplaySet.isLoaded) { - srDisplaySet.load(); - } - const numMeasurements = srDisplaySet.measurements.length; - setMeasurementCount(numMeasurements); - }, [srDisplaySet]); + const loadSR = async () => { + if (!srDisplaySet.isLoaded) { + await srDisplaySet.load(); + } + const numMeasurements = srDisplaySet.measurements.length; + setMeasurementCount(numMeasurements); + updateViewport(measurementSelected); + }; + loadSR(); + }, [dataSource, srDisplaySet]); /** * Hook to update the tracking identifiers when the selected measurement changes or @@ -275,19 +283,11 @@ function OHIFCornerstoneSRViewport(props) { * Todo: what is this, not sure what it does regarding the react aspect, * it is updating a local variable? which is not state. */ - let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0; + const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0); useEffect(() => { - isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0; + setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0); }, [trackedMeasurements]); - /** - * Data fetching for the SR displaySet, which updates the measurements and - * also gets the referenced image displaySet that SR is based on. - */ - useEffect(() => { - updateViewport(measurementSelected); - }, [dataSource, srDisplaySet]); - useEffect(() => { viewportActionCornersService.setComponents([ { diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index f832c0a0bf..e096b7d28f 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -38,7 +38,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.67.0", + "@cornerstonejs/dicom-image-loader": "^1.68.1", "@icr/polyseg-wasm": "^0.4.0", "@ohif/core": "3.8.0-beta.71", "@ohif/ui": "3.8.0-beta.71", @@ -55,10 +55,10 @@ }, "dependencies": { "@babel/runtime": "^7.20.13", - "@cornerstonejs/adapters": "^1.67.0", - "@cornerstonejs/core": "^1.67.0", - "@cornerstonejs/streaming-image-volume-loader": "^1.67.0", - "@cornerstonejs/tools": "^1.67.0", + "@cornerstonejs/adapters": "^1.68.1", + "@cornerstonejs/core": "^1.68.1", + "@cornerstonejs/streaming-image-volume-loader": "^1.68.1", + "@cornerstonejs/tools": "^1.68.1", "@kitware/vtk.js": "29.7.0", "html2canvas": "^1.4.1", "lodash.debounce": "4.0.8", diff --git a/extensions/cornerstone/src/initMeasurementService.js b/extensions/cornerstone/src/initMeasurementService.js index 2ab5cce55b..8801089c6c 100644 --- a/extensions/cornerstone/src/initMeasurementService.js +++ b/extensions/cornerstone/src/initMeasurementService.js @@ -357,6 +357,10 @@ const connectMeasurementServiceToTools = ( imageId = dataSource.getImageIdsForInstance({ instance }); } + /** + * This annotation is used by the cornerstone viewport. + * This is not the read-only annotation rendered by the SR viewport. + */ const annotationManager = annotation.state.getAnnotationManager(); annotationManager.addAnnotation({ annotationUID: measurement.uid, @@ -369,11 +373,16 @@ const connectMeasurementServiceToTools = ( referencedImageId: imageId, }, data: { + /** + * Don't remove this destructuring of data here. + * This is used to pass annotation specific data forward e.g. contour + */ + ...(data.annotation.data || {}), text: data.annotation.data.text, handles: { ...data.annotation.data.handles }, cachedStats: { ...data.annotation.data.cachedStats }, label: data.annotation.data.label, - frameNumber: frameNumber, + frameNumber, }, }); } diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts index d1322e4843..3820522b1a 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts @@ -1,14 +1,22 @@ import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { getDisplayUnit } from './utils'; +import { utils } from '@ohif/core'; +/** + * Represents a mapping utility for Planar Freehand ROI measurements. + */ const PlanarFreehandROI = { toAnnotation: measurement => {}, /** * Maps cornerstone annotation event data to measurement service format. * - * @param {Object} cornerstone Cornerstone event data - * @return {Measurement} Measurement instance + * @param {Object} csToolsEventDetail Cornerstone event data + * @param {DisplaySetService} DisplaySetService Service for managing display sets + * @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports + * @param {Function} getValueTypeFromToolType Function to get value type from tool type + * @returns {Measurement} Measurement instance */ toMeasurement: ( csToolsEventDetail, @@ -16,7 +24,7 @@ const PlanarFreehandROI = { CornerstoneViewportService, getValueTypeFromToolType ) => { - const { annotation, viewportId } = csToolsEventDetail; + const { annotation } = csToolsEventDetail; const { metadata, data, annotationUID } = annotation; if (!metadata || !data) { @@ -26,19 +34,14 @@ const PlanarFreehandROI = { const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; const validToolType = SUPPORTED_TOOLS.includes(toolName); - if (!validToolType) { - throw new Error('Tool not supported'); + throw new Error(`Tool ${toolName} not supported`); } - const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( - referencedImageId, - CornerstoneViewportService, - viewportId - ); + const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } = + getSOPInstanceAttributes(referencedImageId); let displaySet; - if (SOPInstanceUID) { displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, @@ -48,92 +51,151 @@ const PlanarFreehandROI = { displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points, textBox } = data.handles; - - const mappedAnnotations = getMappedAnnotations(annotation, DisplaySetService); - - const displayText = getDisplayText(mappedAnnotations); - const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID); - return { uid: annotationUID, SOPInstanceUID, FrameOfReferenceUID, - points, - textBox, + points: data.contour.polyline, + textBox: data.handles.textBox, metadata, + frameNumber, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, toolName: metadata.toolName, displaySetInstanceUID: displaySet.displaySetInstanceUID, label: data.label, - displayText: displayText, - data: { ...data, ...data.cachedStats }, + displayText: getDisplayText(annotation, displaySet), + data: data.cachedStats, type: getValueTypeFromToolType(toolName), - getReport, + getReport: () => getColumnValueReport(annotation), }; }, }; /** - * It maps an imaging library annotation to a list of simplified annotation properties. + * This function is used to convert the measurement data to a + * format that is suitable for report generation (e.g. for the csv report). + * The report returns a list of columns and corresponding values. * - * @param {Object} annotationData - * @param {Object} DisplaySetService - * @returns + * @param {object} annotation + * @returns {object} Report's content from this tool */ -function getMappedAnnotations(annotationData, DisplaySetService) { - const { metadata, data } = annotationData; - const { label } = data; - const { referencedImageId } = metadata; +function getColumnValueReport(annotation) { + const columns = []; + const values = []; - const annotations = []; + /** Add type */ + columns.push('AnnotationType'); + values.push('Cornerstone:PlanarFreehandROI'); + + /** Add cachedStats */ + const { metadata, data } = annotation; + const { mean, stdDev, max, area, unit, areaUnit, perimeter } = + data.cachedStats[`imageId:${metadata.referencedImageId}`]; + columns.push(`Maximum`, `Mean`, `Std Dev`, 'Pixel Unit', `Area`, 'Unit', 'Perimeter'); + values.push(max, mean, stdDev, unit, area, areaUnit, perimeter); + + /** Add FOR */ + if (metadata.FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(metadata.FrameOfReferenceUID); + } - const { SOPInstanceUID: _SOPInstanceUID, SeriesInstanceUID: _SeriesInstanceUID } = - getSOPInstanceAttributes(referencedImageId) || {}; + /** Add points */ + if (data.contour.polyline) { + /** + * 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 + */ + columns.push('points'); + values.push(data.contour.polyline.map(p => p.join(' ')).join(';')); + } - if (!_SOPInstanceUID || !_SeriesInstanceUID) { - return annotations; + return { columns, values }; +} + +/** + * Retrieves the display text for an annotation in a display set. + * + * @param {Object} annotation - The annotation object. + * @param {Object} displaySet - The display set object. + * @returns {string[]} - An array of display text. + */ +function getDisplayText(annotation, displaySet) { + const { metadata, data } = annotation; + + if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) { + return []; } - const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( - _SOPInstanceUID, - _SeriesInstanceUID - ); + const { mean, stdDev, max, area, modalityUnit, areaUnit, perimeter } = + data.cachedStats[`imageId:${metadata.referencedImageId}`]; - const { SeriesNumber, SeriesInstanceUID } = displaySet; + const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId); - annotations.push({ - SeriesInstanceUID, - SeriesNumber, - label, - data, - }); + const displayText = []; - return annotations; -} + const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } -/** - * 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 = []; + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; - return { - columns, - values, - }; -} + const { SeriesNumber } = displaySet; + if (SeriesNumber) { + displayText.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + } + + if (area) { + /** + * Add Area + * Area sometimes becomes undefined if `preventHandleOutsideImage` is off + */ + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.push(`Area: ${roundedArea} ${getDisplayUnit(areaUnit)}`); + } + + if (mean) { + if (Array.isArray(mean)) { + const meanValues = mean.map(value => utils.roundNumber(value)); + displayText.push(`Mean: ${meanValues.join(', ')} ${modalityUnit}`); + } else { + displayText.push(`Mean: ${utils.roundNumber(mean)} ${modalityUnit}`); + } + } + + if (max) { + if (Array.isArray(max)) { + const maxValues = max.map(value => utils.roundNumber(value, 2)); + displayText.push(`Max: ${maxValues.join(', ')} ${modalityUnit}`); + } else { + displayText.push(`Max: ${utils.roundNumber(max, 2)} ${modalityUnit}`); + } + } + + if (stdDev) { + if (Array.isArray(stdDev)) { + const stdDevValues = stdDev.map(value => utils.roundNumber(value)); + displayText.push(`Std Dev: ${stdDevValues.join(', ')} ${modalityUnit}`); + } else { + displayText.push(`Std Dev: ${utils.roundNumber(stdDev)} ${modalityUnit}`); + } + } + + if (perimeter) { + if (Array.isArray(perimeter)) { + const perimeterValues = perimeter.map(value => utils.roundNumber(value)); + displayText.push(`Perimeter: ${perimeterValues.join(', ')} ${modalityUnit}`); + } else { + displayText.push(`Perimeter: ${utils.roundNumber(perimeter)} ${modalityUnit}`); + } + } -function getDisplayText(mappedAnnotations) { - return ''; + return displayText; } export default PlanarFreehandROI; diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index aefca82ef8..eff286d817 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "start": "yarn run dev" }, "peerDependencies": { - "@cornerstonejs/core": "^1.67.0", - "@cornerstonejs/tools": "^1.67.0", + "@cornerstonejs/core": "^1.68.1", + "@cornerstonejs/tools": "^1.68.1", "@ohif/core": "3.8.0-beta.71", "@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.71", "@ohif/ui": "3.8.0-beta.71", diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx index 4aa2cee42d..74657e85d4 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -183,60 +183,69 @@ function TrackedMeasurementsContextProvider( // ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle useEffect(() => { - if (viewports.size > 0) { - const activeViewport = viewports.get(activeViewportId); + const triggerPromptHydrateFlow = async () => { + if (viewports.size > 0) { + const activeViewport = viewports.get(activeViewportId); - if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) { - return; - } + if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) { + return; + } - // Todo: Getting the first displaySetInstanceUID is wrong, but we don't have - // tracking fusion viewports yet. This should change when we do. - const { displaySetService } = servicesManager.services; - const displaySet = displaySetService.getDisplaySetByUID( - activeViewport.displaySetInstanceUIDs[0] - ); + // Todo: Getting the first displaySetInstanceUID is wrong, but we don't have + // tracking fusion viewports yet. This should change when we do. + const { displaySetService } = servicesManager.services; + const displaySet = displaySetService.getDisplaySetByUID( + activeViewport.displaySetInstanceUIDs[0] + ); - if (!displaySet) { - return; - } + if (!displaySet) { + return; + } - // If this is an SR produced by our SR SOPClassHandler, - // and it hasn't been loaded yet, do that now so we - // can check if it can be rehydrated or not. - // - // Note: This happens: - // - If the viewport is not currently an OHIFCornerstoneSRViewport - // - If the displaySet has never been hung - // - // Otherwise, the displaySet will be loaded by the useEffect handler - // listening to displaySet changes inside OHIFCornerstoneSRViewport. - // The issue here is that this handler in TrackedMeasurementsContext - // ends up occurring before the Viewport is created, so the displaySet - // is not loaded yet, and isRehydratable is undefined unless we call load(). - if ( - displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && - !displaySet.isLoaded && - displaySet.load - ) { - displaySet.load(); - } + // If this is an SR produced by our SR SOPClassHandler, + // and it hasn't been loaded yet, do that now so we + // can check if it can be rehydrated or not. + // + // Note: This happens: + // - If the viewport is not currently an OHIFCornerstoneSRViewport + // - If the displaySet has never been hung + // + // Otherwise, the displaySet will be loaded by the useEffect handler + // listening to displaySet changes inside OHIFCornerstoneSRViewport. + // The issue here is that this handler in TrackedMeasurementsContext + // ends up occurring before the Viewport is created, so the displaySet + // is not loaded yet, and isRehydratable is undefined unless we call load(). + if ( + displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && + !displaySet.isLoaded && + displaySet.load + ) { + await displaySet.load(); + } - // Magic string - // load function added by our sopClassHandler module - if ( - displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && - displaySet.isRehydratable === true - ) { - console.log('sending event...', trackedMeasurements); - sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', { - displaySetInstanceUID: displaySet.displaySetInstanceUID, - SeriesInstanceUID: displaySet.SeriesInstanceUID, - viewportId: activeViewportId, - }); + // Magic string + // load function added by our sopClassHandler module + if ( + displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && + displaySet.isRehydratable === true + ) { + console.log('sending event...', trackedMeasurements); + sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', { + displaySetInstanceUID: displaySet.displaySetInstanceUID, + SeriesInstanceUID: displaySet.SeriesInstanceUID, + viewportId: activeViewportId, + }); + } } - } - }, [activeViewportId, sendTrackedMeasurementsEvent, servicesManager.services, viewports]); + }; + triggerPromptHydrateFlow(); + }, [ + trackedMeasurements, + activeViewportId, + sendTrackedMeasurementsEvent, + servicesManager.services, + viewports, + ]); return ( { + if (m.referenceSeriesUID === displaySet.SeriesInstanceUID) { + measurementService.remove(m.uid); + } + }); }} onClickThumbnail={() => {}} onDoubleClickThumbnail={onDoubleClickThumbnailHandler} diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index cb1eb8c8bf..d1dfe3324b 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -129,6 +129,8 @@ function initSRToolGroup(extensionManager, toolGroupService) { { toolName: SRToolNames.SRBidirectional }, { toolName: SRToolNames.SREllipticalROI }, { toolName: SRToolNames.SRCircleROI }, + { toolName: SRToolNames.SRPlanarFreehandROI }, + { toolName: SRToolNames.SRRectangleROI }, ], enabled: [ { diff --git a/modes/longitudinal/src/moreTools.ts b/modes/longitudinal/src/moreTools.ts index d523418cd3..f9b1189c61 100644 --- a/modes/longitudinal/src/moreTools.ts +++ b/modes/longitudinal/src/moreTools.ts @@ -155,14 +155,6 @@ const moreTools = [ commands: setToolActiveToolbar, evaluate: 'evaluate.cornerstoneTool', }), - createButton({ - id: 'RectangleROI', - icon: 'tool-rectangle', - label: 'Rectangle', - tooltip: 'Rectangle', - commands: setToolActiveToolbar, - evaluate: 'evaluate.cornerstoneTool', - }), createButton({ id: 'CalibrationLine', icon: 'tool-calibration', diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts index 79d0297e92..7abe57f2c7 100644 --- a/modes/longitudinal/src/toolbarButtons.ts +++ b/modes/longitudinal/src/toolbarButtons.ts @@ -67,6 +67,22 @@ const toolbarButtons: Button[] = [ commands: setToolActiveToolbar, evaluate: 'evaluate.cornerstoneTool', }), + createButton({ + id: 'PlanarFreehandROI', + icon: 'tool-freehand-polygon', + label: 'Freehand', + tooltip: 'Freehand ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'RectangleROI', + icon: 'tool-rectangle', + label: 'Rectangle', + tooltip: 'Rectangle ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), createButton({ id: 'CircleROI', icon: 'tool-circle', diff --git a/platform/app/package.json b/platform/app/package.json index ccd67212cc..7285b4f376 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -53,7 +53,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", - "@cornerstonejs/dicom-image-loader": "^1.67.0", + "@cornerstonejs/dicom-image-loader": "^1.68.1", "@emotion/serialize": "^1.1.3", "@ohif/core": "3.8.0-beta.71", "@ohif/extension-cornerstone": "3.8.0-beta.71", diff --git a/platform/core/package.json b/platform/core/package.json index 2346a2f7d0..7769a624e9 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,7 +37,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "@cornerstonejs/dicom-image-loader": "^1.67.0", + "@cornerstonejs/dicom-image-loader": "^1.68.1", "@ohif/ui": "3.8.0-beta.71", "cornerstone-math": "0.1.9", "dicom-parser": "^1.8.21" diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 49ce726f08..7f8c974898 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -53,6 +53,7 @@ const MEASUREMENT_SCHEMA_KEYS = [ 'area', // TODO: Add concept names instead (descriptor) 'mean', 'stdDev', + 'perimeter', 'length', 'shortestDiameter', 'longestDiameter', diff --git a/yarn.lock b/yarn.lock index 414b9c706c..35b0f8631f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1582,13 +1582,13 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cornerstonejs/adapters@^1.67.0": - version "1.67.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.67.0.tgz#84c881a2b1c7bc3cec2ae55df869d82740cc6049" - integrity sha512-EnIpdMD7kx42eJYGHsqjahWTZj/ZTkP+5XCWXpLp0BY0p+O53oSV9aYZXvyjd/kWQPr+30L+010zPuE5oQzIxA== +"@cornerstonejs/adapters@^1.68.1": + version "1.68.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.68.1.tgz#9f4fa768f85759bb49e8f252c9a4e5b4acd09b1f" + integrity sha512-p4pKMX70/COLf1dSIDUonEBHhSPYklH2OkS9yOmdjOVYzvWxyaQWuczd1V+2cWYanD5WGqRzarl4KpJAK9W+zQ== dependencies: "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.67.0" + "@cornerstonejs/tools" "^1.68.1" buffer "^6.0.3" dcmjs "^0.29.8" gl-matrix "^3.4.3" @@ -1635,10 +1635,10 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0" integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== -"@cornerstonejs/core@^1.67.0": - version "1.67.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.67.0.tgz#711a915b70b5273d8838b59c8c06c553079b0ee7" - integrity sha512-t8VI5acGGu0wN51UzHyEXOjlBjoAJjIVcbBOQejC0yOWfGoy+I9y/h5de41pZyhfbXYNNXA1dHZDDtLi9sauBg== +"@cornerstonejs/core@^1.68.1": + version "1.68.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.68.1.tgz#f5d366924b791b117d77e2fe83cee82dced0f3fa" + integrity sha512-F+ptRQ4kYPLS1buDvpQhy3ckdJa4K4B7uz3Ujq3BaEw1AZPB01S6OFw6HtbYs8i+kIi8ZF1fdWvDheO0bpefBQ== dependencies: "@kitware/vtk.js" "29.7.0" comlink "^4.4.1" @@ -1646,34 +1646,34 @@ gl-matrix "^3.4.3" lodash.clonedeep "4.5.0" -"@cornerstonejs/dicom-image-loader@^1.67.0": - version "1.67.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.67.0.tgz#9e70858100821682105f88a927aa4dff96e75cda" - integrity sha512-ZhziYYKcci1X11iWA4qOzEVxtVUbDUKqx9ustsAD9vv9P7WfDOI4EHflUhW+X1hIAG5vl7xv6LAG0aRkVnwrrw== +"@cornerstonejs/dicom-image-loader@^1.68.1": + version "1.68.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.68.1.tgz#fd2cda6de4060ade8092a0fc887f32d4ce979684" + integrity sha512-xjmp2Izy4KMPUTZVutdnx0TXhFrLmlJP3ZZ54ypjJyKGdv4EIGyDvGmCHkD7G37dYROTbZJVBNtjukCrfvJ1Zg== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-openjpeg" "^1.2.2" "@cornerstonejs/codec-openjph" "^2.4.5" - "@cornerstonejs/core" "^1.67.0" + "@cornerstonejs/core" "^1.68.1" dicom-parser "^1.8.9" pako "^2.0.4" uuid "^9.0.0" -"@cornerstonejs/streaming-image-volume-loader@^1.67.0": - version "1.67.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.67.0.tgz#5e22c828e5a5c9932e728314edbfa95abf81982e" - integrity sha512-W5+CKSxkLI4BbcdXYVb/M+yeN6Z+G8AWOQISR8qX9NGnK3GR/Id7+9OH+HP6FmIZ4YBXew9/DOqRgEtTtyDZGQ== +"@cornerstonejs/streaming-image-volume-loader@^1.68.1": + version "1.68.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.68.1.tgz#93ab87a0b77717200c7ea35ddb832651f02882de" + integrity sha512-4n0oNuf+SNxVfZ6gr5CwSgKpu5f3S3+uFxzRcvUH5DMXwVGxwU1XF8CTFfvEnw9xBn3x6Dg4ckR1a682CyuwXA== dependencies: - "@cornerstonejs/core" "^1.67.0" + "@cornerstonejs/core" "^1.68.1" comlink "^4.4.1" -"@cornerstonejs/tools@^1.67.0": - version "1.67.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.67.0.tgz#47c6459859cc5122f780a7ec67c2b9d41b067b38" - integrity sha512-CPFWRF4IePxHV8KoXE6PBY0Q3jw8riJ7ZaJajAzXLpgvTSvIYu0m7tNuFG8iKVppq6KTgSywO4r9VbfcvZXOyw== +"@cornerstonejs/tools@^1.68.1": + version "1.68.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.68.1.tgz#17e177be628bd633391566d8110b2799be7d5068" + integrity sha512-fbXIqcgJKa2RUmwLvxbbISSvwkfwZBoKCjtI/j0AMowpV/gpm86Br6/MomNmPQvdfuF5HfQRdBHjZS71Vid1pQ== dependencies: - "@cornerstonejs/core" "^1.67.0" + "@cornerstonejs/core" "^1.68.1" "@icr/polyseg-wasm" "0.4.0" "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1"