From e1099207779b64cfb26b3c8d5e086f51ba1b7a39 Mon Sep 17 00:00:00 2001 From: Celian-abd <101793092+Celian-abd@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:40:59 +0200 Subject: [PATCH] add CircleROIStartEndRhresholdTool + fix start/end command --- extensions/tmtv/src/commandsModule.ts | 47 ++++-------- extensions/tmtv/src/init.js | 13 +++- .../CircleROIStartEndThreshold.js | 67 +++++++++++++++++ .../measurementServiceMappingsFactory.js | 15 ++++ yarn.lock | 72 +++++++++++-------- 5 files changed, 150 insertions(+), 64 deletions(-) create mode 100644 extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js diff --git a/extensions/tmtv/src/commandsModule.ts b/extensions/tmtv/src/commandsModule.ts index 75b26407473..857ff94d410 100644 --- a/extensions/tmtv/src/commandsModule.ts +++ b/extensions/tmtv/src/commandsModule.ts @@ -12,9 +12,10 @@ import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructure import { getWebWorkerManager } from '@cornerstonejs/core'; const metadataProvider = classes.MetadataProvider; -const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = [ +const ROI_THRESHOLD_MANUAL_TOOL_IDS = [ 'RectangleROIStartEndThreshold', 'RectangleROIThreshold', + 'CircleROIStartEndThreshold' ]; const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap; @@ -204,7 +205,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const { referencedVolumeId } = cs.cache.getVolume(segVolumeId); const annotationUIDs = _getAnnotationsSelectedByToolNames( - RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS + ROI_THRESHOLD_MANUAL_TOOL_IDS ); if (annotationUIDs.length === 0) { @@ -291,7 +292,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const referencedVolume = cs.cache.getVolume(referencedVolumeId); const annotationUIDs = _getAnnotationsSelectedByToolNames( - RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS + ROI_THRESHOLD_MANUAL_TOOL_IDS ); const annotations = annotationUIDs.map(annotationUID => @@ -377,7 +378,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: meanValue: segmentationValues.reduce((a, b) => a + b, 0) / voxelCount, stdValue: Math.sqrt( segmentationValues.reduce((a, b) => a + b * b, 0) / voxelCount - - segmentationValues.reduce((a, b) => a + b, 0) / voxelCount ** 2 + segmentationValues.reduce((a, b) => a + b, 0) / voxelCount ** 2 ), volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3, }; @@ -468,38 +469,18 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: }, setStartSliceForROIThresholdTool: () => { const { viewport } = _getActiveViewportsEnabledElement(); - const { focalPoint, viewPlaneNormal } = viewport.getCamera(); + const { focalPoint } = viewport.getCamera(); const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames( - RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS + ROI_THRESHOLD_MANUAL_TOOL_IDS ); const annotationUID = selectedAnnotationUIDs[0]; const annotation = csTools.annotation.state.getAnnotation(annotationUID); - const { handles } = annotation.data; - const { points } = handles; - - // get the current slice Index - const sliceIndex = viewport.getCurrentImageIdIndex(); - annotation.data.startSlice = sliceIndex; - - // distance between camera focal point and each point on the rectangle - const newPoints = points.map(point => { - const distance = vec3.create(); - vec3.subtract(distance, focalPoint, point); - // distance in the direction of the viewPlaneNormal - const distanceInViewPlane = vec3.dot(distance, viewPlaneNormal); - // new point is current point minus distanceInViewPlane - const newPoint = vec3.create(); - vec3.scaleAndAdd(newPoint, point, viewPlaneNormal, distanceInViewPlane); - - return newPoint; - // - }); - - handles.points = newPoints; + // set the current focalpoint + annotation.data.startCoordinate = focalPoint; // IMPORTANT: invalidate the toolData for the cached stat to get updated // and re-calculate the projection points annotation.invalidated = true; @@ -509,16 +490,16 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: const { viewport } = _getActiveViewportsEnabledElement(); const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames( - RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS + ROI_THRESHOLD_MANUAL_TOOL_IDS ); const annotationUID = selectedAnnotationUIDs[0]; const annotation = csTools.annotation.state.getAnnotation(annotationUID); - // get the current slice Index - const sliceIndex = viewport.getCurrentImageIdIndex(); - annotation.data.endSlice = sliceIndex; + // get the current focalpoint + const focalPointToEnd = viewport.getCamera().focalPoint; + annotation.data.endCoordinate = focalPointToEnd; // IMPORTANT: invalidate the toolData for the cached stat to get updated // and re-calculate the projection points @@ -534,7 +515,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => { const forAnnotations = stateManager.annotations[frameOfReferenceUID]; - const ROIAnnotations = RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce( + const ROIAnnotations = ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce( (annotations, toolName) => [...annotations, ...(forAnnotations[toolName] ?? [])], [] ); diff --git a/extensions/tmtv/src/init.js b/extensions/tmtv/src/init.js index eaa46153327..57ca26cd50c 100644 --- a/extensions/tmtv/src/init.js +++ b/extensions/tmtv/src/init.js @@ -1,4 +1,4 @@ -import { addTool, RectangleROIStartEndThresholdTool } from '@cornerstonejs/tools'; +import { addTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool } from '@cornerstonejs/tools'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; @@ -15,8 +15,9 @@ export default function init({ servicesManager }) { servicesManager.services; addTool(RectangleROIStartEndThresholdTool); + addTool(CircleROIStartEndThresholdTool); - const { RectangleROIStartEndThreshold } = measurementServiceMappingsFactory( + const { RectangleROIStartEndThreshold, CircleROIStartEndThreshold } = measurementServiceMappingsFactory( measurementService, displaySetService, cornerstoneViewportService @@ -34,4 +35,12 @@ export default function init({ servicesManager }) { RectangleROIStartEndThreshold.toAnnotation, RectangleROIStartEndThreshold.toMeasurement ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'CircleROIStartEndThreshold', + CircleROIStartEndThreshold.matchingCriteria, + CircleROIStartEndThreshold.toAnnotation, + CircleROIStartEndThreshold.toMeasurement + ); } diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js new file mode 100644 index 00000000000..bcd94858978 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js @@ -0,0 +1,67 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const CircleROIStartEndThreshold = { + toAnnotation: (measurement, definition) => { }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => { + 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 { cachedStats } = data; + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + // points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: metadata.label, + // displayText: displayText, + data: data.cachedStats, + type: 'CircleROIStartEndThreshold', + // getReport, + }; + }, +}; + +export default CircleROIStartEndThreshold; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index f99b956a146..af19d43d967 100644 --- a/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -1,4 +1,5 @@ import RectangleROIStartEndThreshold from './RectangleROIStartEndThreshold'; +import CircleROIStartEndThreshold from './CircleROIStartEndThreshold' const measurementServiceMappingsFactory = ( measurementService, @@ -20,6 +21,20 @@ const measurementServiceMappingsFactory = ( }, ], }, + CircleROIStartEndThreshold: { + toAnnotation: CircleROIStartEndThreshold.toAnnotation, + toMeasurement: csToolsAnnotation => + CircleROIStartEndThreshold.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL, + }, + ], + }, }; }; diff --git a/yarn.lock b/yarn.lock index fcb857fa9af..daf8f3d9544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1493,12 +1493,12 @@ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@cornerstonejs/adapters@^1.78.1": - version "1.78.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.78.1.tgz#aad40ff55763811a4a12228c8a7ef554f0b7906a" - integrity sha512-y0cQRMK/b9CIYxkUq/d8yEqkkykAkZfu/a4gPzGI1vynoEqTefmRiLwSRutM8Uud4EVvNCw5KpJqV4ihjUzqcg== + version "1.81.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.81.3.tgz#b5e8ecbaf5036ee270f265911097f857f4e4439b" + integrity sha512-kUR2cppCrXVXMaCsEEMgV2t88atGSv4WPqzxTiqz2GwH8aHQWfYcpOokGmUrKcsJvZHvtlbs1rj4rK4Lzcl1ww== dependencies: "@babel/runtime-corejs2" "^7.17.8" - "@cornerstonejs/tools" "^1.78.1" + "@cornerstonejs/tools" "^1.81.3" buffer "^6.0.3" dcmjs "^0.29.8" gl-matrix "^3.4.3" @@ -1530,10 +1530,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.78.1": - version "1.78.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.78.1.tgz#e7db1afd573dd631e0ad4157706deb59844ca1b7" - integrity sha512-BfClVMEPM6IwU4TlAEh305jZOwK+xQBgf5jQywBea1DKf5KPYUFhnZcrxaWlZ44ldlpvsGBHZhDuTpdwjYszBg== +"@cornerstonejs/core@^1.78.1", "@cornerstonejs/core@^1.81.3": + version "1.81.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.81.3.tgz#76deaee4a2cce24db5695cb87857accea5645263" + integrity sha512-Uievs/wBpw20Xj4B+8UEjb/qe+cmSfz2oWfQzBANcWoqI5pXD77evXE0s6hORCMr4yFUmTIMbGnWwXT2fF2bYw== dependencies: "@kitware/vtk.js" "30.4.1" comlink "^4.4.1" @@ -1542,33 +1542,33 @@ lodash.clonedeep "4.5.0" "@cornerstonejs/dicom-image-loader@^1.77.6": - version "1.78.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.78.1.tgz#d8eef8f65e7069bef26a0f569a2d89bf7c48c9de" - integrity sha512-S37NlGD3FPrwE9qQcLVQA7pyzRlAIO+XBS7y2AgHWgXlHysRlJP+s4BWdLuwz7+Xh6cVh5g6mj/g+z51zep2kQ== + version "1.81.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.81.3.tgz#8d9165b351954999bbc566d811a9eedd2363d55c" + integrity sha512-QFqr0Jq9DWvoBB1rTwI6gL4fc3pawpnG6ebDSZpz+uPmG/QcGYaeGNT+Szrcu2V/q1ByOU/Jxn8ZG5Nuo+kD6A== 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.78.1" + "@cornerstonejs/core" "^1.81.3" dicom-parser "^1.8.9" pako "^2.0.4" uuid "^9.0.0" "@cornerstonejs/streaming-image-volume-loader@^1.78.1": - version "1.78.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.78.1.tgz#6139d0b6825391f0418d69081a6340fda01e2a15" - integrity sha512-YC/iyQ9/BWOVF0uF0ZFxJzJmHSrar+25x05X7TIRDCGWf52MpPQvFjqZfArCwwGIHZHajRG+WEh4TgYoqN4mWA== + version "1.81.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.81.3.tgz#01e464e42dd99abb456dab522acdbf15af2894eb" + integrity sha512-gv+a+MQ5//qUE8Zfcy3CihJu0dm5csg7iXuS0IAdtrdZ7C7eNhzFMn1FlDABLV5lGN4cbAv+BZgBxIRThVWcBA== dependencies: - "@cornerstonejs/core" "^1.78.1" + "@cornerstonejs/core" "^1.81.3" comlink "^4.4.1" -"@cornerstonejs/tools@^1.78.1": - version "1.78.1" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.78.1.tgz#d5b12f6526a5656af7fb76335fab1231603e509e" - integrity sha512-idkYlCNL4xhaWBkw9bqPBSJW5AuK4M5SyRVzp5GEVsBjsBAzRdoa7W+Ls7wmgMJqjv/4neI3E35FfxaXUXLOdA== +"@cornerstonejs/tools@^1.78.1", "@cornerstonejs/tools@^1.81.3": + version "1.81.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.81.3.tgz#79dd3fe6fa695a5b84a62188c5b166d7e0d5be70" + integrity sha512-GWRAXCANk8lr5iyP2h/9YGX6nb6uRmO26tsFQ/Zv0x6hAxlzFLVczD+xDdP8Xrx/wLsPmq2c/qPbT5T6lYic3g== dependencies: - "@cornerstonejs/core" "^1.78.1" + "@cornerstonejs/core" "^1.81.3" "@icr/polyseg-wasm" "0.4.0" "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1" @@ -5487,10 +5487,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^20.12.12": - version "20.14.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.5.tgz#fe35e3022ebe58b8f201580eb24e1fcfc0f2487d" - integrity sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA== +"@types/node@*": + version "20.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420" + integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg== dependencies: undici-types "~5.26.4" @@ -5511,6 +5511,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.12.12": + version "20.14.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.5.tgz#fe35e3022ebe58b8f201580eb24e1fcfc0f2487d" + integrity sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -5738,9 +5745,9 @@ source-map "^0.6.0" "@types/webxr@^0.5.5": - version "0.5.16" - resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.16.tgz#28955aa2d1197d1ef0b9826ae0f7e68f7eca0601" - integrity sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA== + version "0.5.19" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.19.tgz#463a27bc06ff1c0a0c997e86b48bf24c5f50a4af" + integrity sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw== "@types/ws@^8.2.2", "@types/ws@^8.5.5": version "8.5.10" @@ -12547,13 +12554,20 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.1.0, is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0, is-core-module@^2.8.1: +is-core-module@^2.1.0, is-core-module@^2.13.1, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: hasown "^2.0.0" +is-core-module@^2.13.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" + integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + dependencies: + hasown "^2.0.2" + is-data-view@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f"