From 18ac08ed860d119721c52e4ffc270332259100b6 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 29 May 2024 11:31:09 -0400 Subject: [PATCH] feat(hp): Add displayArea option for Hanging protocols and example with Mamo(#3808) --- README.md | 1 - babel.config.js | 1 + .../CornerstoneViewportService.ts | 11 + extensions/default/package.json | 2 +- .../default/src/getHangingProtocolModule.js | 14 +- .../src/{ => hangingprotocols}/hpCompare.ts | 4 +- .../src/{ => hangingprotocols}/hpMNGrid.ts | 0 .../default/src/hangingprotocols/hpMammo.ts | 201 ++++++++++++++++ .../default/src/hangingprotocols/hpScale.ts | 131 +++++++++++ .../default/src/hangingprotocols/index.ts | 15 ++ .../src/hangingprotocols/laterality.ts | 9 + .../mammoDisplaySetSelector.ts | 214 ++++++++++++++++++ .../registerHangingProtocolAttributes.ts | 8 + .../default/src/hangingprotocols/viewCode.ts | 11 + extensions/default/src/init.ts | 4 + modes/basic-dev-mode/package.json | 2 +- modes/basic-test-mode/package.json | 2 +- modes/longitudinal/package.json | 2 +- modes/segmentation/package.json | 2 +- modes/tmtv/package.json | 2 +- platform/app/.webpack/webpack.pwa.js | 3 +- .../integration/ImageConsistency.spec.js | 86 +++++++ platform/app/public/config/e2e.js | 28 +++ platform/core/src/types/HangingProtocol.ts | 13 ++ .../docs/configuration/configurationFiles.md | 1 + .../platform/extensions/modules/hpModule.md | 27 +-- platform/ui/package.json | 2 +- yarn.lock | 2 +- 28 files changed, 771 insertions(+), 27 deletions(-) rename extensions/default/src/{ => hangingprotocols}/hpCompare.ts (96%) rename extensions/default/src/{ => hangingprotocols}/hpMNGrid.ts (100%) create mode 100644 extensions/default/src/hangingprotocols/hpMammo.ts create mode 100644 extensions/default/src/hangingprotocols/hpScale.ts create mode 100644 extensions/default/src/hangingprotocols/index.ts create mode 100644 extensions/default/src/hangingprotocols/laterality.ts create mode 100644 extensions/default/src/hangingprotocols/mammoDisplaySetSelector.ts create mode 100644 extensions/default/src/hangingprotocols/registerHangingProtocolAttributes.ts create mode 100644 extensions/default/src/hangingprotocols/viewCode.ts create mode 100644 platform/app/cypress/integration/ImageConsistency.spec.js diff --git a/README.md b/README.md index 1c6fbdc53..f17031399 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ provided by the Open Health Imaging Foundation (OHIF | VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | | microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | - ## About The OHIF Viewer can retrieve diff --git a/babel.config.js b/babel.config.js index 32cc2d914..97d2663c1 100644 --- a/babel.config.js +++ b/babel.config.js @@ -32,6 +32,7 @@ module.exports = { '@babel/transform-destructuring', '@babel/plugin-transform-runtime', '@babel/plugin-transform-typescript', + '@babel/plugin-transform-class-static-block', ], }, production: { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 35c9d3e15..c5ae70cc2 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -573,6 +573,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; } + const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions(); + const properties = { ...presentations.lutPresentation?.properties }; if (!presentations.lutPresentation?.properties) { const { voi, voiInverted, colormap } = displaySetOptions[0]; @@ -598,6 +600,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); this.setPresentations(viewport.id, presentations); + if (displayArea) { + viewport.setDisplayArea(displayArea); + } + if (rotation) { + viewport.setProperties({ rotation }); + } + if (flipHorizontal) { + viewport.setCamera({ flipHorizontal: true }); + } }); } diff --git a/extensions/default/package.json b/extensions/default/package.json index 71fb7e1aa..07c3bdbd8 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -41,7 +41,7 @@ "react-dom": "^18.3.1", "react-i18next": "^12.2.2", "react-window": "^1.8.9", - "webpack": "^5.50.0", + "webpack": "^5.89.0", "webpack-merge": "^5.7.3" }, "dependencies": { diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index 596e3031d..f7fde44eb 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -1,5 +1,7 @@ -import hpMNGrid from './hpMNGrid'; -import hpMNCompare from './hpCompare'; +import hpMNGrid from './hangingprotocols/hpMNGrid'; +import hpMNCompare from './hangingprotocols/hpCompare'; +import hpMammography from './hangingprotocols/hpMammo'; +import hpScale from './hangingprotocols/hpScale'; const defaultProtocol = { id: 'default', @@ -114,6 +116,14 @@ function getHangingProtocolModule() { name: hpMNCompare.id, protocol: hpMNCompare, }, + { + name: hpMammography.id, + protocol: hpMammography, + }, + { + name: hpScale.id, + protocol: hpScale, + }, ]; } diff --git a/extensions/default/src/hpCompare.ts b/extensions/default/src/hangingprotocols/hpCompare.ts similarity index 96% rename from extensions/default/src/hpCompare.ts rename to extensions/default/src/hangingprotocols/hpCompare.ts index 7bd934be1..87a4f5e02 100644 --- a/extensions/default/src/hpCompare.ts +++ b/extensions/default/src/hangingprotocols/hpCompare.ts @@ -120,9 +120,9 @@ const hpMNCompare: Types.HangingProtocol.Protocol = { { id: 'Two Studies', weight: 1000, + // is there a second study or in another work the attribute + // studyInstanceUIDsIndex that we get from prior should not be null attribute: 'StudyInstanceUID', - // The 'from' attribute says where to get the 'attribute' value from. In this case - // prior means the second study in the study list. from: 'prior', required: true, constraint: { diff --git a/extensions/default/src/hpMNGrid.ts b/extensions/default/src/hangingprotocols/hpMNGrid.ts similarity index 100% rename from extensions/default/src/hpMNGrid.ts rename to extensions/default/src/hangingprotocols/hpMNGrid.ts diff --git a/extensions/default/src/hangingprotocols/hpMammo.ts b/extensions/default/src/hangingprotocols/hpMammo.ts new file mode 100644 index 000000000..8890b6843 --- /dev/null +++ b/extensions/default/src/hangingprotocols/hpMammo.ts @@ -0,0 +1,201 @@ +import { + RCC, + RMLO, + LCC, + LMLO, + RCCPrior, + LCCPrior, + RMLOPrior, + LMLOPrior, +} from './mammoDisplaySetSelector'; + +const rightDisplayArea = { + storeAsInitialCamera: true, + imageArea: [0.8, 0.8], + imageCanvasPoint: { + imagePoint: [0, 0.5], + canvasPoint: [0, 0.5], + }, +}; + +const leftDisplayArea = { + storeAsInitialCamera: true, + imageArea: [0.8, 0.8], + imageCanvasPoint: { + imagePoint: [1, 0.5], + canvasPoint: [1, 0.5], + }, +}; + +const hpMammography = { + id: '@ohif/hpMammo', + hasUpdatedPriorsInformation: false, + name: 'Mammography Breast Screening', + protocolMatchingRules: [ + { + id: 'Mammography', + weight: 150, + attribute: 'ModalitiesInStudy', + constraint: { + contains: 'MG', + }, + required: true, + }, + { + id: 'numberOfImages', + attribute: 'numberOfDisplaySetsWithImages', + constraint: { + greaterThan: 2, + }, + required: true, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + RCC, + LCC, + RMLO, + LMLO, + RCCPrior, + LCCPrior, + RMLOPrior, + LMLOPrior, + }, + + stages: [ + { + name: 'CC/MLO', + viewportStructure: { + type: 'grid', + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + // flipHorizontal: true, + // rotation: 180, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'RCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + // flipHorizontal: true, + displayArea: rightDisplayArea, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'LCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + // rotation: 180, + // flipHorizontal: true, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'RMLO', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: rightDisplayArea, + // flipHorizontal: true, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'LMLO', + }, + ], + }, + ], + }, + + // Compare CC current/prior top/bottom + { + name: 'CC compare', + viewportStructure: { + type: 'grid', + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + flipHorizontal: true, + rotation: 180, + }, + displaySets: [ + { + id: 'RCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + flipHorizontal: true, + displayArea: rightDisplayArea, + }, + displaySets: [ + { + id: 'LCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + flipHorizontal: true, + }, + displaySets: [ + { + id: 'RCCPrior', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: rightDisplayArea, + }, + displaySets: [ + { + id: 'LCCPrior', + }, + ], + }, + ], + }, + ], + // Indicates it is prior aware, but will work with no priors + numberOfPriorsReferenced: 0, +}; + +export default hpMammography; diff --git a/extensions/default/src/hangingprotocols/hpScale.ts b/extensions/default/src/hangingprotocols/hpScale.ts new file mode 100644 index 000000000..ee2a1e13a --- /dev/null +++ b/extensions/default/src/hangingprotocols/hpScale.ts @@ -0,0 +1,131 @@ +import { Types } from '@ohif/core'; + +const displayAreaScale1: Types.HangingProtocol.DisplayArea = { + type: 'SCALE', + scale: 1, + storeAsInitialCamera: true, +}; +const displayAreaScale15: Types.HangingProtocol.DisplayArea = { ...displayAreaScale1, scale: 15 }; + +/** + * This hanging protocol can be activated on the primary mode by directly + * referencing it in a URL or by directly including it within a mode, e.g.: + * `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL + * It is not included in the viewer mode by default. + */ +const hpScale: Types.HangingProtocol.Protocol = { + id: '@ohif/hpScale', + description: 'Has various hanging protocol grid layouts', + name: 'Scale Images', + protocolMatchingRules: [ + { + id: 'OneOrMoreSeries', + weight: 25, + attribute: 'numberOfDisplaySetsWithImages', + constraint: { + greaterThan: 0, + }, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + defaultDisplaySetId: { + seriesMatchingRules: [ + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + required: true, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 10, + constraint: { + equals: true, + }, + }, + ], + }, + }, + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + displayArea: displayAreaScale1, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + // A 1x1 stage - should be automatically activated if there is only 1 viewable instance + { + name: 'Scale 1:1', + stageActivation: { + enabled: { + minViewportsMatched: 1, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + allowUnmatchedView: true, + displayArea: displayAreaScale1, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'Scale 1:15', + stageActivation: { + enabled: { + minViewportsMatched: 1, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + allowUnmatchedView: true, + displayArea: displayAreaScale15, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + ], + numberOfPriorsReferenced: -1, +}; + +export default hpScale; diff --git a/extensions/default/src/hangingprotocols/index.ts b/extensions/default/src/hangingprotocols/index.ts new file mode 100644 index 000000000..ab5b11f23 --- /dev/null +++ b/extensions/default/src/hangingprotocols/index.ts @@ -0,0 +1,15 @@ +import viewCodeAttribute from './viewCode'; +import lateralityAttribute from './laterality'; +import registerHangingProtocolAttributes from './registerHangingProtocolAttributes'; +import hpMammography from './hpMammo'; +import hpMNGrid from './hpMNGrid'; +import hpCompare from './hpCompare'; + +export { + viewCodeAttribute, + lateralityAttribute, + hpMammography as hpMammo, + hpMNGrid, + hpCompare, + registerHangingProtocolAttributes, +}; diff --git a/extensions/default/src/hangingprotocols/laterality.ts b/extensions/default/src/hangingprotocols/laterality.ts new file mode 100644 index 000000000..59f8a4ebe --- /dev/null +++ b/extensions/default/src/hangingprotocols/laterality.ts @@ -0,0 +1,9 @@ +export default displaySet => { + const frameAnatomy = + displaySet?.images?.[0]?.SharedFunctionalGroupsSequence?.[0]?.FrameAnatomySequence?.[0]; + if (!frameAnatomy) { + return undefined; + } + const laterality = frameAnatomy?.FrameLaterality; + return laterality; +}; diff --git a/extensions/default/src/hangingprotocols/mammoDisplaySetSelector.ts b/extensions/default/src/hangingprotocols/mammoDisplaySetSelector.ts new file mode 100644 index 000000000..720d5a152 --- /dev/null +++ b/extensions/default/src/hangingprotocols/mammoDisplaySetSelector.ts @@ -0,0 +1,214 @@ +const priorStudyMatchingRules = [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 1 }, + }, + }, +]; + +const currentStudyMatchingRules = [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 0 }, + }, + }, +]; + +const LCCSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399162004', + }, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + contains: 'L', + }, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'L CC', + }, + }, +]; + +const RCCSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399162004', + }, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['P', 'L'], + }, + }, + { + attribute: 'PatientOrientation', + constraint: { + doesNotEqual: ['A', 'R'], + }, + required: true, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'CC', + }, + }, +]; + +const LMLOSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399368009', + }, + }, + { + weight: 0, + attribute: 'ViewCode', + constraint: { + doesNotEqual: 'SCT:399162004', + }, + required: true, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['A', 'R'], + }, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'L MLO', + }, + }, +]; + +const RMLOSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399368009', + }, + }, + { + attribute: 'ViewCode', + constraint: { + doesNotEqual: 'SCT:399162004', + }, + required: true, + }, + { + attribute: 'PatientOrientation', + constraint: { + doesNotContain: ['P', 'FL'], + }, + required: true, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['P', 'L'], + }, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['A', 'FR'], + }, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'R MLO', + }, + }, + { + attribute: 'SeriesDescription', + required: true, + constraint: { + doesNotContain: 'CC', + }, + }, + { + attribute: 'SeriesDescription', + required: true, + constraint: { + doesNotEqual: 'L MLO', + }, + required: true, + }, +]; + +const RCC = { + seriesMatchingRules: RCCSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const RCCPrior = { + seriesMatchingRules: RCCSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +const LCC = { + seriesMatchingRules: LCCSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const LCCPrior = { + seriesMatchingRules: LCCSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +const RMLO = { + seriesMatchingRules: RMLOSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const RMLOPrior = { + seriesMatchingRules: RMLOSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +const LMLO = { + seriesMatchingRules: LMLOSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const LMLOPrior = { + seriesMatchingRules: LMLOSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +export { RCC, LCC, RMLO, LMLO, RCCPrior, LCCPrior, RMLOPrior, LMLOPrior }; diff --git a/extensions/default/src/hangingprotocols/registerHangingProtocolAttributes.ts b/extensions/default/src/hangingprotocols/registerHangingProtocolAttributes.ts new file mode 100644 index 000000000..6c598009c --- /dev/null +++ b/extensions/default/src/hangingprotocols/registerHangingProtocolAttributes.ts @@ -0,0 +1,8 @@ +import viewCode from './viewCode'; +import laterality from './laterality'; + +export default function registerHangingProtocolAttributes({ servicesManager }) { + const { hangingProtocolService } = servicesManager.services; + hangingProtocolService.addCustomAttribute('ViewCode', 'View Code Designator:Value', viewCode); + hangingProtocolService.addCustomAttribute('Laterality', 'Laterality of object', laterality); +} diff --git a/extensions/default/src/hangingprotocols/viewCode.ts b/extensions/default/src/hangingprotocols/viewCode.ts new file mode 100644 index 000000000..0f2251e8e --- /dev/null +++ b/extensions/default/src/hangingprotocols/viewCode.ts @@ -0,0 +1,11 @@ +export default displaySet => { + const ViewCodeSequence = displaySet?.images[0]?.ViewCodeSequence[0]; + if (!ViewCodeSequence) { + return undefined; + } + const { CodingSchemeDesignator, CodeValue } = ViewCodeSequence; + if (!CodingSchemeDesignator || !CodeValue) { + return undefined; + } + return `${CodingSchemeDesignator}:${CodeValue}`; +}; diff --git a/extensions/default/src/init.ts b/extensions/default/src/init.ts index e1c77c911..2e5e831c8 100644 --- a/extensions/default/src/init.ts +++ b/extensions/default/src/init.ts @@ -2,6 +2,7 @@ import { DicomMetadataStore, classes } from '@ohif/core'; import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv'; import getPTImageIdInstanceMetadata from './getPTImageIdInstanceMetadata'; +import { registerHangingProtocolAttributes } from './hangingprotocols'; const metadataProvider = classes.MetadataProvider; @@ -60,6 +61,9 @@ export default function init({ // afterwards. stateSyncService.register('viewportsByPosition', { clearOnModeExit: true }); + // Adds extra custom attributes for use by hanging protocols + registerHangingProtocolAttributes({ servicesManager }); + // Function to process and subscribe to events for a given set of commands and listeners const subscribeToEvents = listeners => { Object.entries(listeners).forEach(([event, commands]) => { diff --git a/modes/basic-dev-mode/package.json b/modes/basic-dev-mode/package.json index 91c20f2e7..db1713f9a 100644 --- a/modes/basic-dev-mode/package.json +++ b/modes/basic-dev-mode/package.json @@ -43,7 +43,7 @@ "i18next": "^17.0.3" }, "devDependencies": { - "webpack": "^5.50.0", + "webpack": "^5.89.0", "webpack-merge": "^5.7.3" } } diff --git a/modes/basic-test-mode/package.json b/modes/basic-test-mode/package.json index faa3741f5..56399fa3b 100644 --- a/modes/basic-test-mode/package.json +++ b/modes/basic-test-mode/package.json @@ -48,7 +48,7 @@ "i18next": "^17.0.3" }, "devDependencies": { - "webpack": "^5.50.0", + "webpack": "^5.89.0", "webpack-merge": "^5.7.3" } } diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json index a813511a6..c8b67446c 100644 --- a/modes/longitudinal/package.json +++ b/modes/longitudinal/package.json @@ -49,7 +49,7 @@ "i18next": "^17.0.3" }, "devDependencies": { - "webpack": "^5.50.0", + "webpack": "^5.89.0", "webpack-merge": "^5.7.3" } } diff --git a/modes/segmentation/package.json b/modes/segmentation/package.json index 20f81a02a..81f64f093 100644 --- a/modes/segmentation/package.json +++ b/modes/segmentation/package.json @@ -70,7 +70,7 @@ "dotenv": "^14.1.0", "eslint": "^5.0.1", "eslint-loader": "^2.0.0", - "webpack": "^5.50.0", + "webpack": "^5.89.0", "webpack-cli": "^4.7.2", "webpack-merge": "^5.7.3" } diff --git a/modes/tmtv/package.json b/modes/tmtv/package.json index 52e8acb5d..17b73a449 100644 --- a/modes/tmtv/package.json +++ b/modes/tmtv/package.json @@ -47,7 +47,7 @@ "i18next": "^17.0.3" }, "devDependencies": { - "webpack": "^5.50.0", + "webpack": "^5.89.0", "webpack-merge": "^5.7.3" } } diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js index c07fdfb36..d25c59e8b 100644 --- a/platform/app/.webpack/webpack.pwa.js +++ b/platform/app/.webpack/webpack.pwa.js @@ -20,6 +20,7 @@ const PUBLIC_URL = process.env.PUBLIC_URL || '/'; const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js'; const PROXY_TARGET = process.env.PROXY_TARGET; const PROXY_DOMAIN = process.env.PROXY_DOMAIN; +const OHIF_PORT = Number(process.env.OHIF_PORT || 3000); const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`; const Dotenv = require('dotenv-webpack'); const writePluginImportFile = require('./writePluginImportsFile.js'); @@ -143,7 +144,7 @@ module.exports = (env, argv) => { // http2: true, // https: true, open: true, - port: 3000, + port: OHIF_PORT, client: { overlay: { errors: true, warnings: false }, }, diff --git a/platform/app/cypress/integration/ImageConsistency.spec.js b/platform/app/cypress/integration/ImageConsistency.spec.js new file mode 100644 index 000000000..45dea2e41 --- /dev/null +++ b/platform/app/cypress/integration/ImageConsistency.spec.js @@ -0,0 +1,86 @@ +import { utilities } from '@cornerstonejs/core'; + +/** + * Add tests to ensure image consistency and quality + */ + +const testPixel = (dx, dy, expectedPixel) => { + cy.get('.cornerstone-canvas').then(v => { + const canvas = v[0]; + cy.log( + 'testPixel canvas', + dx, + dy, + expectedPixel, + canvas.width, + canvas.height, + canvas.style.width, + canvas.style.height + ); + const ctx = canvas.getContext('2d'); + cy.window() + .its('cornerstone') + .then(cornerstone => { + const { viewport } = cornerstone.getEnabledElements()[0]; + const imageData = viewport.getImageData(); + // cy.log("imageData", imageData); + const origin = viewport.worldToCanvas(imageData.origin); + const orX = origin[0] * devicePixelRatio; + const orY = origin[1] * devicePixelRatio; + const x = Math.round(orX + dx); + const y = Math.round(orY + dy); + cy.log('testPixel origin x,y point x,y', orX, orY, x, y); + // cy.log('world origin', imageData.origin); + // cy.log('focal', viewport.getCamera().focalPoint, + // viewport.worldToCanvas(viewport.getCamera().focalPoint)); + const pixelData = ctx.getImageData(x, y, 1, 1); + + expect(pixelData.data[0]).closeTo(expectedPixel, 1); + }); + }); +}; + +describe('CS3D Image Consistency and Quality', () => { + const setupStudySeries = (studyUID, seriesUID) => { + cy.checkStudyRouteInViewer( + studyUID, + `&seriesInstanceUID=${seriesUID}&hangingProtocolId=@ohif/hpScale` + ); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + }; + + it('TG18 Resolution Test Displayed 1:1', () => { + setupStudySeries( + '2.16.124.113543.6004.101.103.20021117.061159.1', + '2.16.124.113543.6004.101.103.20021117.061159.1.004' + ); + testPixel(1018, 1028, 255); + // Horizontal and vertical delta from this should not be contaminated + // by values from center + testPixel(1019, 1028, 0); + testPixel(1018, 1029, 0); + testPixel(1017, 1028, 0); + testPixel(1018, 1027, 0); + }); + + // Missing test data - todo + it.skip('8 bit image displayable', () => { + setupStudySeries('1.3.46.670589.17.1.7.1.1.7', '1.3.46.670589.17.1.7.2.1.7'); + + // Compare with dcm2jpg generated values or by manually computing WL values + testPixel(258, 257, 171); + testPixel(259, 257, 166); + }); + + it.skip('12 bit image displayable and zoom with pixel spacing', () => { + setupStudySeries( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '1.3.6.1.4.1.25403.345050719074.3824.20170125113608.5' + ); + + // Compare with dcm2jpg generated values or by manually computing WL values + testPixel(258, 277, 120); + testPixel(259, 277, 122); + }); +}); diff --git a/platform/app/public/config/e2e.js b/platform/app/public/config/e2e.js index 896b07b4f..2eb52cbb5 100644 --- a/platform/app/public/config/e2e.js +++ b/platform/app/public/config/e2e.js @@ -111,6 +111,34 @@ window.config = { }, }, }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb2', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', + qidoRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', + wadoRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, { friendlyName: 'StaticWado default data', namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index e8d669545..16b846fb2 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -24,6 +24,18 @@ export type DisplaySetAndViewportOptions = { displaySetOptions: DisplaySetOptions; }; +export type DisplayArea = { + type?: 'SCALE' | 'FIT'; + scale?: number; + interpolationType?: any; + imageArea?: [number, number]; // areaX, areaY + imageCanvasPoint?: { + imagePoint: [number, number]; // imageX, imageY + canvasPoint?: [number, number]; // canvasX, canvasY + }; + storeAsInitialCamera?: boolean; +}; + export type SetProtocolOptions = { /** Used to provide a mapping of what keys are provided for which viewport. * For example, a Chest XRay might use have the display set selector id of @@ -155,6 +167,7 @@ export type ViewportOptions = { id?: string; orientation?: CustomOption; viewportId?: string; + displayArea?: DisplayArea; initialImageOptions?: CustomOption; syncGroups?: CustomOption[]; customViewportProps?: Record; diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md index 8201c0905..cbcdf8234 100644 --- a/platform/docs/docs/configuration/configurationFiles.md +++ b/platform/docs/docs/configuration/configurationFiles.md @@ -293,6 +293,7 @@ alternative data source (or even specify different default hotkeys). | `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` | | `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` | | `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` | +| `OHIF_PORT` | The port to run the webpack server on for PWA builds. | `3000` | You can also create a new config file and specify its path relative to the build output's root by setting the `APP_CONFIG` environment variable. You can set the diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md index cff0ac713..b7f0f15ed 100644 --- a/platform/docs/docs/platform/extensions/modules/hpModule.md +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -281,21 +281,22 @@ A list of criteria for the protocol along with the provided points for ranking. }, ``` -### `from` attribute -The from attribute allows getting the attribute to test from some other object -such as the prior study, the list of studies overall or another module provided -value. Some of the possible attributes are: +### `from` attribute (optional) +The `from` attribute allows you to retrieve the attribute to test from another object, such as the previous study, the overall list of studies, or another provided value from a module. -* `prior`: To get the value from the prior study. -* `activeStudy`: To match the active study -* `studies`: To match the list of studies to display -* `displaySets`: The display sets for the current study -* `allDisplaySets`: Alll available display sets -* `instance`: An instance from the current display set being tested -* `options`: Gets the options object itself, eg if you want a simple top level - value. +The values provided by OHIF which you can use are: -### displaySetSelectors +- `activeStudy`: to use the metadata of the active study to match +- `studies`: to use the metadata of the list of studies (all studies) to match +- `allDisplaySets`: all available display sets +- `displaySets`: if the selector has matched a study, these are the display sets for that study +- `prior`: the metadata of the first study in the list of studies that is not the active study +- `options`: during matching, we also provide an options object with the following information that you can use as the `from` value: + - `studyInstanceUIDsIndex`: the index of the study in the list of studies + - `instance`: the metadata of the instance being matched, which is exactly the displaySet.instance metadata. + + +### displaySetSelectors (mandatory) Defines the display sets that the protocol will use for arrangement. ```js diff --git a/platform/ui/package.json b/platform/ui/package.json index 593f8c59e..be362dba0 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -60,7 +60,7 @@ "react-window": "^1.8.9", "react-with-direction": "^1.3.1", "swiper": "^8.4.2", - "webpack": "^5.81.0" + "webpack": "^5.89.0" }, "devDependencies": { "@babel/core": "^7.23.2", diff --git a/yarn.lock b/yarn.lock index ed371cf13..e5c398220 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22557,7 +22557,7 @@ webpack-virtual-modules@^0.6.1: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz#ac6fdb9c5adb8caecd82ec241c9631b7a3681b6f" integrity sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg== -webpack@5, webpack@^5.50.0, webpack@^5.73.0, webpack@^5.81.0, webpack@^5.90.3: +webpack@5, webpack@^5.50.0, webpack@^5.73.0, webpack@^5.89.0, webpack@^5.90.3: version "5.91.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9" integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==