From 00e751933ac6d611a34773fa69594243f1b99082 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 21 Nov 2023 09:05:25 -0500 Subject: [PATCH] fix(DICOM Overlay): The overlay data wasn't being refreshed on change (#3793) --- extensions/cornerstone/src/index.tsx | 2 + .../src/tools/ImageOverlayViewerTool.tsx | 48 +++++++++---------- .../src/tools/OverlayPlaneModuleProvider.ts | 40 ++++++++++++++++ package.json | 2 +- platform/app/.webpack/webpack.pwa.js | 2 +- testdata | 2 +- 6 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 10b4ce026ba..ac53c8f0f27 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -29,6 +29,7 @@ import { id } from './id'; import * as csWADOImageLoader from './initWADOImageLoader.js'; import { measurementMappingUtils } from './utils/measurementServiceMappings'; import type { PublicViewportOptions } from './services/ViewportService/Viewport'; +import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'); @@ -140,5 +141,6 @@ export { CornerstoneExtensionTypes as Types, toolNames, getActiveViewportEnabledElement, + ImageOverlayViewerTool, }; export default cornerstoneExtension; diff --git a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx index 1815d52402a..0a3fa641faa 100644 --- a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx +++ b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx @@ -1,8 +1,8 @@ -import { VolumeViewport, metaData } from '@cornerstonejs/core'; -import { utilities } from '@cornerstonejs/core'; +import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core'; import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types'; import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools'; import { guid } from '@ohif/core/src/utils'; +import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider'; interface CachedStat { color: number[]; // [r, g, b, a] @@ -27,8 +27,12 @@ interface CachedStat { */ class ImageOverlayViewerTool extends AnnotationDisplayTool { static toolName = 'ImageOverlayViewer'; - private _cachedOverlayMetadata: Map = new Map(); - private _cachedStats: { [key: string]: CachedStat } = {}; + + /** + * The overlay plane module provider add method is exposed here to be used + * when updating the overlay for this tool to use for displaying data. + */ + public static addOverlayPlaneModule = OverlayPlaneModuleProvider.add; constructor( toolProps = {}, @@ -42,10 +46,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { super(toolProps, defaultToolProps); } - onSetToolDisabled = (): void => { - this._cachedStats = {}; - this._cachedOverlayMetadata = new Map(); - }; + onSetToolDisabled = (): void => { }; protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string { if (viewport instanceof VolumeViewport) { @@ -64,9 +65,8 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { return; } - const overlays = - this._cachedOverlayMetadata.get(imageId) ?? - metaData.get('overlayPlaneModule', imageId)?.overlays; + const overlayMetadata = metaData.get('overlayPlaneModule', imageId); + const overlays = overlayMetadata?.overlays; // no overlays if (!overlays?.length) { @@ -79,9 +79,10 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { overlay.y ||= 0; }); - this._cachedOverlayMetadata.set(imageId, overlays); + // Will clear cached stat data when the overlay data changes + ImageOverlayViewerTool.addOverlayPlaneModule(imageId, overlayMetadata); - this._getCachedStat(imageId, overlays, this.configuration.fillColor).then(cachedStat => { + this._getCachedStat(imageId, overlayMetadata, this.configuration.fillColor).then(cachedStat => { cachedStat.overlays.forEach(overlay => { this._renderOverlay(enabledElement, svgDrawingHelper, overlay); }); @@ -152,15 +153,18 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { private async _getCachedStat( imageId: string, - overlayMetadata: any[], + overlayMetadata, color: number[] ): Promise { - if (this._cachedStats[imageId] && this._isSameColor(this._cachedStats[imageId].color, color)) { - return this._cachedStats[imageId]; + const missingOverlay = overlayMetadata.overlays.filter( + overlay => overlay.pixelData && !overlay.dataUrl + ); + if (missingOverlay.length === 0) { + return overlayMetadata; } const overlays = await Promise.all( - overlayMetadata + overlayMetadata.overlays .filter(overlay => overlay.pixelData) .map(async (overlay, idx) => { let pixelData = null; @@ -178,7 +182,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { const dataUrl = this._renderOverlayToDataUrl( { width: overlay.columns, height: overlay.rows }, - color, + overlay.color || color, pixelData ); @@ -190,13 +194,9 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { }; }) ); + overlayMetadata.overlays = overlays; - this._cachedStats[imageId] = { - color: color, - overlays: overlays.filter(overlay => overlay), - }; - - return this._cachedStats[imageId]; + return overlayMetadata; } /** diff --git a/extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts b/extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts new file mode 100644 index 00000000000..d36b5437b41 --- /dev/null +++ b/extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts @@ -0,0 +1,40 @@ +import { metaData } from '@cornerstonejs/core'; + +const _cachedOverlayMetadata: Map = new Map(); + +/** + * Image Overlay Viewer tool is not a traditional tool that requires user interactin. + * But it is used to display Pixel Overlays. And it will provide toggling capability. + * + * The documentation for Overlay Plane Module of DICOM can be found in [C.9.2 of + * Part-3 of DICOM standard](https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_C.9.2.html) + * + * Image Overlay rendered by this tool can be toggled on and off using + * toolGroup.setToolEnabled() and toolGroup.setToolDisabled() + */ +const OverlayPlaneModuleProvider = { + /** Adds the metadata for overlayPlaneModule */ + add: (imageId, metadata) => { + if (_cachedOverlayMetadata.get(imageId) === metadata) { + // This is a no-op here as the tool re-caches the data + return; + } + _cachedOverlayMetadata.set(imageId, metadata); + }, + + /** Standard getter for metadata */ + get: (type: string, query: string | string[]) => { + if (Array.isArray(query)) { + return; + } + if (type !== 'overlayPlaneModule') { + return; + } + return _cachedOverlayMetadata.get(query); + }, +}; + +// Needs to be higher priority than default provider +metaData.addProvider(OverlayPlaneModuleProvider.get, 10_000); + +export default OverlayPlaneModuleProvider; diff --git a/package.json b/package.json index b542adbda3f..fa70a95e414 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "preinstall": "node preinstall.js", "start": "yarn run dev", "test": "yarn run test:unit", - "test:data": "git submodule update --init", + "test:data": "git submodule update --init -r", "test-watch": "jest --collectCoverage --watchAll", "test:unit": "jest --collectCoverage", "test:unit:ci": "lerna run test:unit:ci --parallel --stream", diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js index af0b70b445e..156b6445590 100644 --- a/platform/app/.webpack/webpack.pwa.js +++ b/platform/app/.webpack/webpack.pwa.js @@ -154,7 +154,7 @@ module.exports = (env, argv) => { { directory: '../../testdata', staticOptions: { - extensions: ['gz', 'br'], + extensions: ['gz', 'br', 'mht'], index: ['index.json.gz', 'index.mht.gz'], redirect: true, setHeaders, diff --git a/testdata b/testdata index 4d59660c288..c9892af83d5 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit 4d59660c2883ed749a680e5fb6d4624ab54c9422 +Subproject commit c9892af83d5dc4b06fac6ff1ca77246467571ec0