From 3f5cd972b945d666a94bab2966a005a789aed119 Mon Sep 17 00:00:00 2001 From: Ouwen Huang Date: Mon, 10 Oct 2022 13:53:52 +0000 Subject: [PATCH 01/32] gradienthealth: whitelabel --- .../default/src/ViewerLayout/ViewerHeader.tsx | 2 +- platform/app/public/assets/favicon.ico | Bin 5430 -> 32038 bytes platform/app/public/assets/gradient.svg | 515 ++++++++++++++++++ platform/app/public/assets/loader.gif | Bin 0 -> 47186 bytes platform/app/public/config/default.js | 37 +- platform/app/public/html-templates/index.html | 2 +- .../app/public/html-templates/rollbar.html | 12 +- platform/app/src/routes/Local/Local.tsx | 7 +- .../src/components/AboutModal/AboutModal.tsx | 31 +- .../LoadingIndicatorProgress.tsx | 7 +- 10 files changed, 561 insertions(+), 52 deletions(-) create mode 100644 platform/app/public/assets/gradient.svg create mode 100644 platform/app/public/assets/loader.gif diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index 6dd130dd9ec..6cbee3cb770 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -51,7 +51,7 @@ function ViewerHeader({ hotkeysManager, extensionManager, servicesManager, appCo onClick: () => show({ content: AboutModal, - title: t('AboutModal:About OHIF Viewer'), + title: 'About Gradient OHIF Viewer', contentProps: { versionNumber, commitHash }, containerDimensions: 'max-w-4xl max-h-4xl', }), diff --git a/platform/app/public/assets/favicon.ico b/platform/app/public/assets/favicon.ico index faaa2cf9c10ac519bf5ab9a7d4f29247d076bc9d..411107554df1cc56b8bce5b6362039c50c2437f3 100644 GIT binary patch literal 32038 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 literal 5430 zcmeHJF=_)b5Zr`-OZx@ks*q=-aG8hX8F$>(euwcNT-o3oa69 zawet=hV10RXQ}=eFNuxDA^V`dsu_o?@508uk@ir0KiWh23E4H~TsjnA$j-fnPuc&- z`;)To18&J~C$inzLX5VMopq*yyg^uxN(*P8>$+#0_YLI2+A|hnw9&^mQwra3`|+Bt zK4R!yF{JYO-?$$1zHM7RT~;Fo``-9$EF%UJ@V|5JT3gpLF)+r9VycNTwpc#76rInf zeSSXtejcBn4=;Q>1HL@@aGNdS^GiKH6Y#U%ztsAX%I7|rIkME>`*Tvp|1SeM&tK~O zN#&z{WvxFG)X(z%m+zP8@3-9NFZcaRtskj;zBXo#^8Wwf(>s{f&-?jtcAOnfhtucP O{iBUO^i`hfSpF9%v4Y?L diff --git a/platform/app/public/assets/gradient.svg b/platform/app/public/assets/gradient.svg new file mode 100644 index 00000000000..b28424c2473 --- /dev/null +++ b/platform/app/public/assets/gradient.svg @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/app/public/assets/loader.gif b/platform/app/public/assets/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..3df85c800808cb62c373bfdbc690f00b947590c6 GIT binary patch literal 47186 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

n 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 - OHIF Viewer + Gradient Viewer - @@ -45,13 +45,13 @@ + />--> - + />--> @@ -195,7 +195,7 @@ src="<%= PUBLIC_URL %>init-service-worker.js" > - OHIF Viewer + Gradient Rollbar Viewer

-
diff --git a/platform/ui/src/components/AboutModal/AboutModal.tsx b/platform/ui/src/components/AboutModal/AboutModal.tsx index 1c027c87f1a..a21f1d18b56 100644 --- a/platform/ui/src/components/AboutModal/AboutModal.tsx +++ b/platform/ui/src/components/AboutModal/AboutModal.tsx @@ -78,14 +78,14 @@ const AboutModal = ({ buildNumber, versionNumber, commitHash }) => { {renderRowTitle(t('Important links'))}
- {t('Visit the forum')} + {t('Visit the forum')} {t('Report an issue')} @@ -93,10 +93,10 @@ const AboutModal = ({ buildNumber, versionNumber, commitHash }) => { - {t('More details')} + {t('More details')}
@@ -104,20 +104,15 @@ const AboutModal = ({ buildNumber, versionNumber, commitHash }) => { {renderRowTitle(t('Version information'))}
- - {/* */} + {/**/} - +
+ Gradient Health +
From 86c55072bc2c4317415d10564bd815473888e046 Mon Sep 17 00:00:00 2001 From: Ouwen Huang Date: Sun, 9 Apr 2023 07:55:43 +0000 Subject: [PATCH 02/32] config: bigquery --- platform/app/package.json | 5 +- platform/app/public/config/gradient.js | 206 ++++++++++++++++++ platform/ui/package.json | 5 + .../UserAuthenticationProvider.tsx | 8 +- 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 platform/app/public/config/gradient.js diff --git a/platform/app/package.json b/platform/app/package.json index 5e6181094b0..e26af3314d3 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -50,6 +50,8 @@ ], "dependencies": { "@babel/runtime": "^7.20.13", + "@gradienthealth/cohort-mode": "^0.1.2", + "@gradienthealth/ohif-gradienthealth-extension": "^0.3.0", "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", @@ -84,8 +86,9 @@ "history": "^5.3.0", "i18next": "^17.0.3", "i18next-browser-languagedetector": "^3.0.1", + "jszip": "^3.10.1", "lodash.isequal": "4.5.0", - "oidc-client": "1.11.5", + "oidc-client": "^1.11.5", "prop-types": "^15.7.2", "query-string": "^6.12.1", "react": "^17.0.2", diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js new file mode 100644 index 00000000000..7e52d24e52f --- /dev/null +++ b/platform/app/public/config/gradient.js @@ -0,0 +1,206 @@ +window.config = { + routerBasename: '/', + // whiteLabeling: {}, + extensions: [], + modes: [], + customizationService: { + // Shows a custom route -access via http://localhost:3000/custom + // helloPage: '@ohif/extension-default.customizationModule.helloPage', + }, + showStudyList: true, + // some windows systems have issues with more than 3 web workers + maxNumberOfWebWorkers: 3, + + // below flag is for performance reasons, but it might not work for all servers + omitQuotationForMultipartRequest: true, + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + use16BitDataType: true, + maxNumRequests: { + interaction: 100, + thumbnail: 75, + // Prefetch number is dependent on the http protocol. For http 2 or + // above, the number of requests can be go a lot higher. + prefetch: 25, + }, + oidc: [ + { + authority: 'https://accounts.google.com', + client_id: + '195181363105-h9e3uujhnd2t6c8dqrdcv01h4bn2fsva.apps.googleusercontent.com', + redirect_uri: '/callback', + response_type: 'id_token token', + scope: [ + 'email', + 'profile', + 'openid', + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/devstorage.read_only', + 'https://www.googleapis.com/auth/bigquery.readonly', + ].join(' '), + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + }, + ], + // filterQueryParam: false, + dataSources: [ + { + friendlyName: 'dcmjs DICOMWeb Server', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'aws', + // This is only here due to other deps, it is not actually used + wadoUriRoot: 'https://storage.cloud.google.com', + qidoRoot: 'https://storage.cloud.google.com', + wadoRoot: 'https://storage.cloud.google.com', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + }, + }, + { + friendlyName: 'dicom json', + namespace: + '@gradienthealth/ohif-gradienthealth-extension.dataSourcesModule.bq', + sourceName: 'bq', + configuration: { + name: 'json', + }, + }, + { + friendlyName: 'dicom local', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: {}, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + whiteLabeling: { + /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ + createLogoComponentFn: function (React) { + return React.createElement( + 'a', + { + target: '_self', + rel: 'noopener noreferrer', + className: 'text-purple-600 line-through', + href: '/', + }, + React.createElement('img', { + src: '/assets/gradient.svg', + }) + ); + }, + }, + defaultDataSourceName: 'dicomweb', + hotkeys: [ + { + commandName: 'incrementActiveViewport', + label: 'Next Viewport', + keys: ['right'], + }, + { + commandName: 'decrementActiveViewport', + label: 'Previous Viewport', + keys: ['left'], + }, + { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] }, + { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] }, + { commandName: 'invertViewport', label: 'Invert', keys: ['i'] }, + { + commandName: 'flipViewportHorizontal', + label: 'Flip Horizontally', + keys: ['h'], + }, + { + commandName: 'flipViewportVertical', + label: 'Flip Vertically', + keys: ['v'], + }, + { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] }, + { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] }, + { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] }, + { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, + { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, + { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + }, + // ~ Window level presets + { + commandName: 'windowLevelPreset1', + label: 'W/L Preset 1', + keys: ['1'], + }, + { + commandName: 'windowLevelPreset2', + label: 'W/L Preset 2', + keys: ['2'], + }, + { + commandName: 'windowLevelPreset3', + label: 'W/L Preset 3', + keys: ['3'], + }, + { + commandName: 'windowLevelPreset4', + label: 'W/L Preset 4', + keys: ['4'], + }, + { + commandName: 'windowLevelPreset5', + label: 'W/L Preset 5', + keys: ['5'], + }, + { + commandName: 'windowLevelPreset6', + label: 'W/L Preset 6', + keys: ['6'], + }, + { + commandName: 'windowLevelPreset7', + label: 'W/L Preset 7', + keys: ['7'], + }, + { + commandName: 'windowLevelPreset8', + label: 'W/L Preset 8', + keys: ['8'], + }, + { + commandName: 'windowLevelPreset9', + label: 'W/L Preset 9', + keys: ['9'], + }, + ], +}; diff --git a/platform/ui/package.json b/platform/ui/package.json index f26214c233e..367a524ff64 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -34,6 +34,11 @@ }, "dependencies": { "@testing-library/react-hooks": "^3.2.1", + "@emotion/react": "11.6.0", + "@emotion/styled": "11.6.0", + "@mui/icons-material": "5.8.4", + "@mui/lab": "5.0.0-alpha.92", + "@mui/material": "5.8.7", "browser-detect": "^0.2.28", "classnames": "^2.3.2", "d3-array": "3", diff --git a/platform/ui/src/contextProviders/UserAuthenticationProvider.tsx b/platform/ui/src/contextProviders/UserAuthenticationProvider.tsx index 185746fc8e0..bc6456d5d1f 100644 --- a/platform/ui/src/contextProviders/UserAuthenticationProvider.tsx +++ b/platform/ui/src/contextProviders/UserAuthenticationProvider.tsx @@ -1,8 +1,14 @@ import React, { createContext, useCallback, useContext, useEffect, useReducer } from 'react'; import PropTypes from 'prop-types'; +const user = JSON.parse( + sessionStorage.getItem( + 'oidc.user:https://accounts.google.com:195181363105-h9e3uujhnd2t6c8dqrdcv01h4bn2fsva.apps.googleusercontent.com' + ) +); + const DEFAULT_STATE = { - user: null, + user: (user && user?.expires_at * 1000 > Date.now()) ? user : null, enabled: false, }; From de7e56e1fe6ea06a212c0e05278986328a3a65ef Mon Sep 17 00:00:00 2001 From: Maya Mohan Date: Thu, 21 Sep 2023 17:15:06 +0400 Subject: [PATCH 03/32] fix: Ignored Series metadata retrieval promise handling for DicomJSONDataSource --- platform/app/pluginConfig.json | 8 ++++++++ platform/app/src/routes/Mode/defaultRouteInit.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json index a910bee9fbf..9ff95bd9c3c 100644 --- a/platform/app/pluginConfig.json +++ b/platform/app/pluginConfig.json @@ -56,6 +56,10 @@ "packageName": "@ohif/extension-cornerstone-dicom-rt", "default": false, "version": "3.0.0" + }, + { + "packageName": "@gradienthealth/ohif-gradienthealth-extension", + "version": "0.3.0" } ], "modes": [ @@ -83,6 +87,10 @@ "packageName": "@ohif/mode-basic-dev-mode", "default": false, "version": "3.0.0" + }, + { + "packageName": "@gradienthealth/cohort-mode", + "version": "0.1.2" } ], "public": [ diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index 863cb1946bd..db99d648ba0 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -90,7 +90,7 @@ export async function defaultRouteInit( // log the error if this fails, otherwise it's so difficult to tell what went wrong... allRetrieves.forEach(retrieve => { - retrieve.catch(error => { + retrieve?.catch(error => { console.error(error); }); }); From 570c7bce00248c323462695b8ab709557b23d286 Mon Sep 17 00:00:00 2001 From: maya-mohan Date: Wed, 27 Sep 2023 11:26:53 +0400 Subject: [PATCH 04/32] feat: Deploy viewer to github pages --- .github/workflows/deploy_ghpages.yml | 104 +++++++++++++++++++++++++++ package.json | 1 + platform/app/package.json | 2 +- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy_ghpages.yml diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml new file mode 100644 index 00000000000..57e37ce752c --- /dev/null +++ b/.github/workflows/deploy_ghpages.yml @@ -0,0 +1,104 @@ +name: Deploy viewer to github pages + +on: + push: + branches: [ "gradienthealth/zip_deployment" ] + #pull_request: + #branches: [ "main" ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Checkout cornerstone3D + uses: actions/checkout@v3 + with: + repository: gradienthealth/cornerstone3D-beta + ref: gradienthealth/dicom-zip-image-loader + path: ./cornerstone3D + + - name: Build cornerstone3D + run: | + cd ./cornerstone3D + yarn install + yarn build:all + + - name: Checkout GradientExtensionsAndModes + uses: actions/checkout@v3 + with: + repository: gradienthealth/GradientExtensionsAndModes + ref: gradienthealth/zip_deployment + path: ./GradientExtensionsAndModes + + #- name: Build GradientExtensionsAndModes + # run: | + # cd ./GradientExtensionsAndModes/extensions/ohif-gradienthealth-extension + # yarn install + # yarn build:package + # cd ./modes/cohort + # yarn install + # yarn build:package + + - name: Checkout Viewers + uses: actions/checkout@v3 + with: + repository: gradienthealth/Viewers + path: ./Viewers + + - name: Link + run: | + cd ./cornerstone3D/packages/adapters/dist + yarn link + cd ../../core/dist + yarn link + cd ../../dicomImageLoader/dist + yarn link + #cd ../../nifti-volume-loader/dist + #yarn link + cd ../../streaming-image-volume-loader/dist + yarn link + cd ../../tools/dist + yarn link + cd ../../../../Viewers + yarn link @cornerstonejs/adapters + yarn link @cornerstonejs/core + yarn link @cornerstonejs/dicom-image-loader + #yarn link @cornerstonejs/nifti-volume-loader + yarn link @cornerstonejs/streaming-image-volume-loader + yarn link @cornerstonejs/tools + yarn install + yarn run cli link-extension ../GradientExtensionsAndModes/extensions/ohif-gradienthealth-extension + yarn run cli link-mode ../GradientExtensionsAndModes/modes/cohort + yarn run build:gradient + + - name: Checkout gh page + uses: actions/checkout@v3 + with: + repository: gradienthealth/gradienthealth.github.io + path: ./gradienthealth.github.io + token: ${{ secrets.GH_DEPLOY_TOKEN }} + + - name: Copy + run: | + mv ./gradienthealth.github.io/.git /tmp/ + rm -r ./gradienthealth.github.io + cp -r ./Viewers/platform/app/dist/ ./gradienthealth.github.io + mv /tmp/.git ./gradienthealth.github.io + cd ./gradienthealth.github.io + cp index.html 404.html + git config --global user.name "maya-mohan" + git config --global user.email "maya@gradienthealth.io" + git remote set-url origin https://maya-mohan:${{ secrets.GH_DEPLOY_TOKEN }}@github.com/gradienthealth/gradienthealth.github.io + git add . + git commit -a -m "publishing viewer" + git push -u origin diff --git a/package.json b/package.json index c52519e0e8f..ab2686890d5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "scripts": { "cm": "npx git-cz", "build": "lerna run build:viewer --stream", + "build:gradient": "lerna run build:gradient --stream", "build:dev": "lerna run build:dev --stream", "build:ci": "lerna run build:viewer:ci --stream", "build:qa": "lerna run build:viewer:qa --stream", diff --git a/platform/app/package.json b/platform/app/package.json index e26af3314d3..47e8f5f3de4 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -19,6 +19,7 @@ "proxy": "http://localhost:8042", "scripts": { "build:viewer": "cross-env NODE_ENV=production yarn run build", + "build:gradient": "cross-env NODE_ENV=production APP_CONFIG=config/gradient.js yarn run build", "build:dev": "cross-env NODE_ENV=development yarn run build", "build:aws": "cross-env NODE_ENV=development APP_CONFIG=config/aws_static.js yarn run build && gzip -9 -r dist", "build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/ APP_CONFIG=config/netlify.js QUICK_BUILD=false yarn run build", @@ -50,7 +51,6 @@ ], "dependencies": { "@babel/runtime": "^7.20.13", - "@gradienthealth/cohort-mode": "^0.1.2", "@gradienthealth/ohif-gradienthealth-extension": "^0.3.0", "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", From 2cbbbd126b6d666c846b62e544a9db5a047e4252 Mon Sep 17 00:00:00 2001 From: maya-mohan Date: Thu, 28 Sep 2023 18:19:48 +0400 Subject: [PATCH 05/32] feat:Added icad config --- .github/workflows/deploy_ghpages.yml | 4 +- package.json | 1 + platform/app/package.json | 1 + platform/app/pluginConfig.json | 4 + platform/app/public/config/icad.js | 169 +++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 platform/app/public/config/icad.js diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml index 57e37ce752c..e2177d43404 100644 --- a/.github/workflows/deploy_ghpages.yml +++ b/.github/workflows/deploy_ghpages.yml @@ -79,7 +79,9 @@ jobs: yarn install yarn run cli link-extension ../GradientExtensionsAndModes/extensions/ohif-gradienthealth-extension yarn run cli link-mode ../GradientExtensionsAndModes/modes/cohort - yarn run build:gradient + yarn run cli link-mode ../GradientExtensionsAndModes/modes/breast-density-mode + #yarn run build:gradient + yarn run build:icad - name: Checkout gh page uses: actions/checkout@v3 diff --git a/package.json b/package.json index ab2686890d5..4491e0308c5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cm": "npx git-cz", "build": "lerna run build:viewer --stream", "build:gradient": "lerna run build:gradient --stream", + "build:icad": "lerna run build:icad --stream", "build:dev": "lerna run build:dev --stream", "build:ci": "lerna run build:viewer:ci --stream", "build:qa": "lerna run build:viewer:qa --stream", diff --git a/platform/app/package.json b/platform/app/package.json index 47e8f5f3de4..0d99775b682 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -20,6 +20,7 @@ "scripts": { "build:viewer": "cross-env NODE_ENV=production yarn run build", "build:gradient": "cross-env NODE_ENV=production APP_CONFIG=config/gradient.js yarn run build", + "build:icad": "cross-env NODE_ENV=production APP_CONFIG=config/icad.js yarn run build", "build:dev": "cross-env NODE_ENV=development yarn run build", "build:aws": "cross-env NODE_ENV=development APP_CONFIG=config/aws_static.js yarn run build && gzip -9 -r dist", "build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/ APP_CONFIG=config/netlify.js QUICK_BUILD=false yarn run build", diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json index 9ff95bd9c3c..4a1a9124ec2 100644 --- a/platform/app/pluginConfig.json +++ b/platform/app/pluginConfig.json @@ -91,6 +91,10 @@ { "packageName": "@gradienthealth/cohort-mode", "version": "0.1.2" + }, + { + "packageName": "@gradienthealth/breast-density-mode", + "version": "0.1.2" } ], "public": [ diff --git a/platform/app/public/config/icad.js b/platform/app/public/config/icad.js new file mode 100644 index 00000000000..9d173027022 --- /dev/null +++ b/platform/app/public/config/icad.js @@ -0,0 +1,169 @@ +window.config = { + routerBasename: "/", + extensions: [], + modes: [], + customizationService: {}, + showStudyList: !0, + maxNumberOfWebWorkers: 3, + omitQuotationForMultipartRequest: !0, + showLoadingIndicator: !0, + maxNumRequests: { + interaction: 100, + thumbnail: 75, + prefetch: 4 + }, + oidc: [{ + authority: "https://accounts.google.com", + client_id: "112149084621-0693qa0gtck3f9rd08qpbpuafq29r1n5.apps.googleusercontent.com", + redirect_uri: "/callback", + response_type: "id_token token", + scope: "email profile openid https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare", + post_logout_redirect_uri: "/logout-redirect.html", + revoke_uri: "https://accounts.google.com/o/oauth2/revoke?token=", + automaticSilentRenew: !0, + revokeAccessTokenOnSignout: !0 + }], + dataSources: [{ + friendlyName: "dcmjs DICOMWeb Server", + namespace: "@ohif/extension-default.dataSourcesModule.dicomweb", + sourceName: "dicomweb", + configuration: { + name: "GCP", + wadoUriRoot: "https://healthcare.googleapis.com/v1/projects/icad-med/locations/us-central1/datasets/mammo/dicomStores/breast_density/dicomWeb", + qidoRoot: "https://healthcare.googleapis.com/v1/projects/icad-med/locations/us-central1/datasets/mammo/dicomStores/breast_density/dicomWeb", + wadoRoot: "https://healthcare.googleapis.com/v1/projects/icad-med/locations/us-central1/datasets/mammo/dicomStores/breast_density/dicomWeb", + qidoSupportsIncludeField: !0, + imageRendering: "wadors", + thumbnailRendering: "wadors", + enableStudyLazyLoad: !0, + supportsFuzzyMatching: !0, + supportsWildcard: !1 + } + }, { + friendlyName: "dicom json", + namespace: "@ohif/extension-default.dataSourcesModule.dicomjson", + sourceName: "dicomjson", + configuration: { + name: "json" + } + }, { + friendlyName: "dicom local", + namespace: "@ohif/extension-default.dataSourcesModule.dicomlocal", + sourceName: "dicomlocal", + configuration: {} + }], + httpErrorHandler: e => { + console.warn(e.status), + console.warn("test, navigate to https://ohif.org/") + } + , + whiteLabeling: { + createLogoComponentFn: function (e) { + return e.createElement("a", { + target: "_self", + rel: "noopener noreferrer", + className: "text-purple-600 line-through", + href: "/" + }, e.createElement("img", { + src: "./assets/gradient.svg" + })) + } + }, + defaultDataSourceName: "dicomweb", + hotkeys: [{ + commandName: "incrementActiveViewport", + label: "Next Viewport", + keys: ["right"] + }, { + commandName: "decrementActiveViewport", + label: "Previous Viewport", + keys: ["left"] + }, { + commandName: "rotateViewportCW", + label: "Rotate Right", + keys: ["r"] + }, { + commandName: "rotateViewportCCW", + label: "Rotate Left", + keys: ["l"] + }, { + commandName: "invertViewport", + label: "Invert", + keys: ["i"] + }, { + commandName: "flipViewportHorizontal", + label: "Flip Horizontally", + keys: ["h"] + }, { + commandName: "flipViewportVertical", + label: "Flip Vertically", + keys: ["v"] + }, { + commandName: "scaleUpViewport", + label: "Zoom In", + keys: ["+"] + }, { + commandName: "scaleDownViewport", + label: "Zoom Out", + keys: ["-"] + }, { + commandName: "fitViewportToWindow", + label: "Zoom to Fit", + keys: ["="] + }, { + commandName: "resetViewport", + label: "Reset", + keys: ["space"] + }, { + commandName: "nextImage", + label: "Next Image", + keys: ["down"] + }, { + commandName: "previousImage", + label: "Previous Image", + keys: ["up"] + }, { + commandName: "setToolActive", + commandOptions: { + toolName: "Zoom" + }, + label: "Zoom", + keys: ["z"] + }, { + commandName: "windowLevelPreset1", + label: "W/L Preset 1", + keys: ["1"] + }, { + commandName: "windowLevelPreset2", + label: "W/L Preset 2", + keys: ["2"] + }, { + commandName: "windowLevelPreset3", + label: "W/L Preset 3", + keys: ["3"] + }, { + commandName: "windowLevelPreset4", + label: "W/L Preset 4", + keys: ["4"] + }, { + commandName: "windowLevelPreset5", + label: "W/L Preset 5", + keys: ["5"] + }, { + commandName: "windowLevelPreset6", + label: "W/L Preset 6", + keys: ["6"] + }, { + commandName: "windowLevelPreset7", + label: "W/L Preset 7", + keys: ["7"] + }, { + commandName: "windowLevelPreset8", + label: "W/L Preset 8", + keys: ["8"] + }, { + commandName: "windowLevelPreset9", + label: "W/L Preset 9", + keys: ["9"] + }] +}; From a27ca11ba5575c26840d1b6f4bd1e57ce04ff4f8 Mon Sep 17 00:00:00 2001 From: maya-mohan Date: Tue, 3 Oct 2023 14:34:28 +0400 Subject: [PATCH 06/32] Merged icad.js config to gradient.js config. --- .github/workflows/deploy_ghpages.yml | 3 +-- platform/app/public/config/gradient.js | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml index e2177d43404..19713e6ac16 100644 --- a/.github/workflows/deploy_ghpages.yml +++ b/.github/workflows/deploy_ghpages.yml @@ -80,8 +80,7 @@ jobs: yarn run cli link-extension ../GradientExtensionsAndModes/extensions/ohif-gradienthealth-extension yarn run cli link-mode ../GradientExtensionsAndModes/modes/cohort yarn run cli link-mode ../GradientExtensionsAndModes/modes/breast-density-mode - #yarn run build:gradient - yarn run build:icad + yarn run build:gradient - name: Checkout gh page uses: actions/checkout@v3 diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js index 7e52d24e52f..106abbcf9f9 100644 --- a/platform/app/public/config/gradient.js +++ b/platform/app/public/config/gradient.js @@ -39,6 +39,10 @@ window.config = { 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/devstorage.read_only', 'https://www.googleapis.com/auth/bigquery.readonly', + 'https://www.googleapis.com/auth/drive.metadata.readonly', + 'https://www.googleapis.com/auth/drive.readonly', + 'https://www.googleapis.com/auth/cloudplatformprojects.readonly', + 'https://www.googleapis.com/auth/cloud-healthcare', ].join(' '), post_logout_redirect_uri: '/logout-redirect.html', revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', @@ -48,7 +52,7 @@ window.config = { ], // filterQueryParam: false, dataSources: [ - { + /*{ friendlyName: 'dcmjs DICOMWeb Server', namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', @@ -68,6 +72,23 @@ window.config = { staticWado: true, singlepart: 'bulkdata,video,pdf', }, + },*/ + { + friendlyName: "dcmjs DICOMWeb Server", + namespace: "@ohif/extension-default.dataSourcesModule.dicomweb", + sourceName: "dicomweb", + configuration: { + name: "GCP", + wadoUriRoot: "https://healthcare.googleapis.com/v1/projects/icad-med/locations/us-central1/datasets/mammo/dicomStores/breast_density/dicomWeb", + qidoRoot: "https://healthcare.googleapis.com/v1/projects/icad-med/locations/us-central1/datasets/mammo/dicomStores/breast_density/dicomWeb", + wadoRoot: "https://healthcare.googleapis.com/v1/projects/icad-med/locations/us-central1/datasets/mammo/dicomStores/breast_density/dicomWeb", + qidoSupportsIncludeField: !0, + imageRendering: "wadors", + thumbnailRendering: "wadors", + enableStudyLazyLoad: !0, + supportsFuzzyMatching: !0, + supportsWildcard: !1 + } }, { friendlyName: 'dicom json', From 24aab3a1a87845f959cbdab1b9ed21dbf936eb87 Mon Sep 17 00:00:00 2001 From: maya-mohan Date: Tue, 3 Oct 2023 16:26:00 +0400 Subject: [PATCH 07/32] Added requestTransferSyntaxUID in datasource config --- platform/app/public/config/gradient.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js index 106abbcf9f9..8b4ec2f2ac4 100644 --- a/platform/app/public/config/gradient.js +++ b/platform/app/public/config/gradient.js @@ -71,6 +71,7 @@ window.config = { supportsWildcard: true, staticWado: true, singlepart: 'bulkdata,video,pdf', + requestTransferSyntaxUID: '*' }, },*/ { @@ -87,7 +88,8 @@ window.config = { thumbnailRendering: "wadors", enableStudyLazyLoad: !0, supportsFuzzyMatching: !0, - supportsWildcard: !1 + supportsWildcard: !1, + requestTransferSyntaxUID: '*' } }, { From 7d6db2ea2ae862b04ea0561d409a941a22d17a82 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Mon, 6 Nov 2023 15:26:03 +0530 Subject: [PATCH 08/32] feat: stack viewport and non-reconstructable segmentation --- .github/workflows/deploy_ghpages.yml | 6 +- .../src/commandsModule.ts | 33 ++-- .../src/getSopClassHandlerModule.js | 19 ++- .../src/panels/PanelSegmentation.tsx | 79 +++------ .../generateLabelmaps2DFromImageIdMap.ts | 44 +++++ .../src/utils/hydrationUtils.ts | 49 +++--- .../viewports/OHIFCornerstoneSEGViewport.tsx | 2 +- .../CornerstoneCacheService.ts | 17 +- .../SegmentationService.ts | 161 ++++++++++++++---- .../SegmentationServiceTypes.ts | 8 +- .../CornerstoneViewportService.ts | 25 +++ .../src/utils/createImageDataForStackImage.ts | 82 +++++++++ .../src/utils/dicomLoaderService.js | 6 + platform/app/public/config/gradient.js | 2 +- platform/app/src/service-worker.js | 27 +++ platform/core/src/classes/MetadataProvider.ts | 3 + .../SegmentationDropDownRow.tsx | 113 ++++++------ .../SegmentationGroup.tsx | 133 +++++++++++++++ .../SegmentationGroupTable.tsx | 80 +++------ 19 files changed, 629 insertions(+), 260 deletions(-) create mode 100644 extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts create mode 100644 extensions/cornerstone/src/utils/createImageDataForStackImage.ts create mode 100644 platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml index 19713e6ac16..ba1cbdf78e6 100644 --- a/.github/workflows/deploy_ghpages.yml +++ b/.github/workflows/deploy_ghpages.yml @@ -2,7 +2,7 @@ name: Deploy viewer to github pages on: push: - branches: [ "gradienthealth/zip_deployment" ] + branches: [ "gradienthealth/Stack-Segmentation" ] #pull_request: #branches: [ "main" ] @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3 with: repository: gradienthealth/cornerstone3D-beta - ref: gradienthealth/dicom-zip-image-loader + ref: gradienthealth/stack-segmentation-support-with-zip-image-loader path: ./cornerstone3D - name: Build cornerstone3D @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v3 with: repository: gradienthealth/GradientExtensionsAndModes - ref: gradienthealth/zip_deployment + ref: gradienthealth/Segmentation-with-DicomJSON path: ./GradientExtensionsAndModes #- name: Build GradientExtensionsAndModes diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 4a770d7e177..e0a385fd024 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -19,8 +19,9 @@ import { getUpdatedViewportsForSegmentation, getTargetViewport, } from './utils/hydrationUtils'; -const { segmentation: segmentationUtils } = utilities; +import generateLabelmaps2DFromImageIdMap from './utils/generateLabelmaps2DFromImageIdMap'; +const { segmentation: segmentationUtils } = utilities; const { datasetToBlob } = dcmjs.data; const { @@ -80,17 +81,6 @@ const commandsModule = ({ // Todo: add support for multiple display sets const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; - const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); - - if (!displaySet.isReconstructable) { - uiNotificationService.show({ - title: 'Segmentation', - message: 'Segmentation is not supported for non-reconstructible displaysets yet', - type: 'error', - }); - return; - } - updateViewportsForSegmentationRendering({ viewportId, servicesManager, @@ -245,14 +235,23 @@ const commandsModule = ({ */ generateSegmentation: ({ segmentationId, options = {} }) => { const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); + const segmentationLabelmapData = segmentation.representationData.LABELMAP; - const { referencedVolumeId } = segmentation.representationData.LABELMAP; + let referencedImages, labelmapObj; + if (segmentationLabelmapData.imageIdReferenceMap) { + const { imageIdReferenceMap } = segmentationLabelmapData; - const segmentationVolume = cache.getVolume(segmentationId); - const referencedVolume = cache.getVolume(referencedVolumeId); - const referencedImages = referencedVolume.getCornerstoneImages(); + ({ referencedImages, labelmapObj } = + generateLabelmaps2DFromImageIdMap(imageIdReferenceMap)); + } else { + const { referencedVolumeId } = segmentationLabelmapData; - const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); + const segmentationVolume = cache.getVolume(segmentationId); + const referencedVolume = cache.getVolume(referencedVolumeId); + referencedImages = referencedVolume.getCornerstoneImages(); + + labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); + } // Generate fake metadata as an example labelmapObj.metadata = []; diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js index d150201e06c..371efb158b5 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js @@ -142,21 +142,24 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, '@ohif/extension-cornerstone.utilityModule.common' ); - const { segmentationService, uiNotificationService } = servicesManager.services; + const { segmentationService, uiNotificationService, displaySetService } = + servicesManager.services; const { dicomLoaderService } = utilityModule.exports; const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); - const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId); + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + segDisplaySet.referencedDisplaySetInstanceUID + ); + let imageIds; - if (!cachedReferencedVolume) { - throw new Error( - 'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet' - ); + if (referencedDisplaySet.isReconstructable) { + const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId); + imageIds = cachedReferencedVolume.imageIds || cachedReferencedVolume._imageIds; + } else { + imageIds = referencedDisplaySet.instances.map(instance => instance.imageId); } - const { imageIds } = cachedReferencedVolume; - // Todo: what should be defaults here const tolerance = 0.001; const skipOverlapping = true; diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 9dd15e8f29f..9ddedd1eaa2 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -22,14 +22,11 @@ export default function PanelSegmentation({ segmentationService, viewportGridService, uiDialogService, - displaySetService, - cornerstoneViewportService, } = servicesManager.services; const { t } = useTranslation('PanelSegmentation'); const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); - const [addSegmentationClassName, setAddSegmentationClassName] = useState(''); const [segmentationConfiguration, setSegmentationConfiguration] = useState( segmentationService.getConfiguration() ); @@ -59,63 +56,15 @@ export default function PanelSegmentation({ }; }, []); - // temporary measure to not allow add segmentation when the selected viewport - // is stack viewport - useEffect(() => { - const handleActiveViewportChange = viewportId => { - const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport( - viewportId || viewportGridService.getActiveViewportId() - ); - - if (!displaySetUIDs) { - return; - } - - const isReconstructable = - displaySetUIDs?.some(displaySetUID => { - const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); - return displaySet?.isReconstructable; - }) || false; - - if (isReconstructable) { - setAddSegmentationClassName(''); - } else { - setAddSegmentationClassName('ohif-disabled'); - } - }; - - // Handle initial state - handleActiveViewportChange(); - - const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED; - const ready = viewportGridService.EVENTS.VIEWPORTS_READY; - - const subsGrid = []; - [ready, changedGrid].forEach(evt => { - const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => { - handleActiveViewportChange(viewportId); - }); + const setSegmentationActive = segmentationId => { + const isSegmentationActive = segmentations.find(seg => seg.id === segmentationId)?.isActive; - subsGrid.push(unsubscribe); - }); - - const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED; - - const subsData = []; - [changedData].forEach(evt => { - const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => { - handleActiveViewportChange(); - }); - - subsData.push(unsubscribe); - }); + if (isSegmentationActive) { + return; + } - // Clean up - return () => { - subsGrid.forEach(unsub => unsub()); - subsData.forEach(unsub => unsub()); - }; - }, []); + segmentationService.setActiveSegmentationForToolGroup(segmentationId); + }; const getToolGroupIds = segmentationId => { const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); @@ -134,10 +83,12 @@ export default function PanelSegmentation({ }; const onSegmentationDelete = (segmentationId: string) => { + setSegmentationActive(segmentationId); segmentationService.remove(segmentationId); }; const onSegmentAdd = segmentationId => { + setSegmentationActive(segmentationId); segmentationService.addSegment(segmentationId); }; @@ -147,13 +98,13 @@ export default function PanelSegmentation({ const toolGroupIds = getToolGroupIds(segmentationId); toolGroupIds.forEach(toolGroupId => { - // const toolGroupId = segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId); segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId); }); }; const onSegmentEdit = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const segment = segmentation.segments[segmentIndex]; @@ -169,6 +120,7 @@ export default function PanelSegmentation({ }; const onSegmentationEdit = segmentationId => { + setSegmentationActive(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const { label } = segmentation; @@ -189,6 +141,7 @@ export default function PanelSegmentation({ }; const onSegmentColorClick = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const segment = segmentation.segments[segmentIndex]; @@ -216,11 +169,13 @@ export default function PanelSegmentation({ }; const onSegmentDelete = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); segmentationService.removeSegment(segmentationId, segmentIndex); }; // segment hide const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const segmentInfo = segmentation.segments[segmentIndex]; const isVisible = !segmentInfo.isVisible; @@ -238,10 +193,12 @@ export default function PanelSegmentation({ }; const onToggleSegmentLock = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); segmentationService.toggleSegmentLocked(segmentationId, segmentIndex); }; const onToggleSegmentationVisibility = segmentationId => { + setSegmentationActive(segmentationId); segmentationService.toggleSegmentationVisibility(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const isVisible = segmentation.isVisible; @@ -272,12 +229,14 @@ export default function PanelSegmentation({ ); const onSegmentationDownload = segmentationId => { + setSegmentationActive(segmentationId); commandsManager.runCommand('downloadSegmentation', { segmentationId, }); }; const storeSegmentation = async segmentationId => { + setSegmentationActive(segmentationId); const datasources = extensionManager.getActiveDataSource(); const displaySetInstanceUIDs = await createReportAsync({ @@ -305,6 +264,7 @@ export default function PanelSegmentation({ }; const onSegmentationDownloadRTSS = segmentationId => { + setSegmentationActive(segmentationId); commandsManager.runCommand('downloadRTSS', { segmentationId, }); @@ -325,7 +285,6 @@ export default function PanelSegmentation({ disableEditing={configuration.disableEditing} activeSegmentationId={selectedSegmentationId || ''} onSegmentationAdd={onSegmentationAddWrapper} - addSegmentationClassName={addSegmentationClassName} showAddSegment={allowAddSegment} onSegmentationClick={onSegmentationClick} onSegmentationDelete={onSegmentationDelete} diff --git a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts new file mode 100644 index 00000000000..14fffdae29a --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts @@ -0,0 +1,44 @@ +import { cache } from '@cornerstonejs/core'; + +const generateLabelmaps2DFromImageIdMap = imageIdReferenceMap => { + const labelmaps2D = [], + referencedImages = [], + segmentsOnLabelmap3D = new Set(); + Array.from(imageIdReferenceMap.entries()).forEach((entry, index) => { + referencedImages.push(cache.getImage(entry[0])); + + const segmentationImage = cache.getImage(entry[1]); + const { rows, columns } = segmentationImage; + const pixelData = segmentationImage.getPixelData(); + const segmentsOnLabelmap = []; + + for (let i = 0; i < pixelData.length; i++) { + const segment = pixelData[i]; + if (!segmentsOnLabelmap.includes(segment) && segment !== 0) { + segmentsOnLabelmap.push(segment); + } + } + + if (segmentsOnLabelmap.length) { + labelmaps2D[index] = { + segmentsOnLabelmap, + pixelData, + rows, + columns, + }; + + segmentsOnLabelmap.forEach(segmentIndex => { + segmentsOnLabelmap3D.add(segmentIndex); + }); + } + }); + + const labelmapObj = { + segmentsOnLabelmap: Array.from(segmentsOnLabelmap3D), + labelmaps2D, + }; + + return { referencedImages, labelmapObj }; +}; + +export default generateLabelmaps2DFromImageIdMap; diff --git a/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts b/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts index 8726d9dc43e..d89c2b40cc0 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/hydrationUtils.ts @@ -1,4 +1,4 @@ -import { Enums, cache } from '@cornerstonejs/core'; +import { Enums, cache, eventTarget } from '@cornerstonejs/core'; /** * Updates the viewports in preparation for rendering segmentations. @@ -25,8 +25,12 @@ async function updateViewportsForSegmentationRendering({ servicesManager: any; referencedDisplaySetInstanceUID?: string; }) { - const { cornerstoneViewportService, segmentationService, viewportGridService } = - servicesManager.services; + const { + cornerstoneViewportService, + segmentationService, + viewportGridService, + displaySetService, + } = servicesManager.services; const viewport = getTargetViewport({ viewportId, viewportGridService }); const targetViewportId = viewport.viewportOptions.viewportId; @@ -42,7 +46,7 @@ async function updateViewportsForSegmentationRendering({ // create Segmentation callback which needs to be waited until // the volume is created (if coming from stack) - const createSegmentationForVolume = async () => { + const createSegmentation = async () => { const segmentationId = await loadFn(); segmentationService.hydrateSegmentation(segmentationId); }; @@ -53,10 +57,14 @@ async function updateViewportsForSegmentationRendering({ volumeId.includes(referencedDisplaySetInstanceUID) ); + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + updatedViewports.forEach(async viewport => { viewport.viewportOptions = { ...viewport.viewportOptions, - viewportType: 'volume', + viewportType: referencedDisplaySet.isReconstructable ? 'volume' : 'stack', needsRerendering: true, }; const viewportId = viewport.viewportId; @@ -64,14 +72,23 @@ async function updateViewportsForSegmentationRendering({ const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const prevCamera = csViewport.getCamera(); - // only run the createSegmentationForVolume for the targetViewportId + // only run the createSegmentation for the targetViewportId when volume cache is available // since the rest will get handled by cornerstoneViewportService if (volumeExists && viewportId === targetViewportId) { - await createSegmentationForVolume(); + await createSegmentation(); return; } + // TODO: Read from _imageCache and create segmentation when applicable + + const newViewportEvent = referencedDisplaySet.isReconstructable + ? Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME + : Enums.Events.STACK_VIEWPORT_NEW_STACK; - const createNewSegmentationWhenVolumeMounts = async evt => { + const eventTriggerer = referencedDisplaySet.isReconstructable + ? csViewport.element + : eventTarget; + + const createNewSegmentationOnNewViewport = async evt => { const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac => ac.uid.includes(referencedDisplaySetInstanceUID) ); @@ -82,25 +99,19 @@ async function updateViewportsForSegmentationRendering({ const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); volumeViewport.setCamera(prevCamera); - volumeViewport.element.removeEventListener( - Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - createNewSegmentationWhenVolumeMounts - ); + eventTriggerer.removeEventListener(newViewportEvent, createNewSegmentationOnNewViewport); - if (!isTheActiveViewportVolumeMounted) { + if (referencedDisplaySet.isReconstructable && !isTheActiveViewportVolumeMounted) { // it means it is one of those other updated viewports so just update the camera return; } if (viewportId === targetViewportId) { - await createSegmentationForVolume(); + await createSegmentation(); } }; - csViewport.element.addEventListener( - Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - createNewSegmentationWhenVolumeMounts - ); + eventTriggerer.addEventListener(newViewportEvent, createNewSegmentationOnNewViewport); }); // Set the displaySets for the viewports that require to be updated @@ -175,7 +186,7 @@ function getUpdatedViewportsForSegmentation({ viewportId, displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, viewportOptions: { - viewportType: 'volume', + viewportType: viewport.viewportType, needsRerendering: true, }, }); diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index b4e0f4528a2..887c26a49a6 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -106,7 +106,7 @@ function OHIFCornerstoneSEGViewport(props) { {...props} displaySets={[referencedDisplaySet, segDisplaySet]} viewportOptions={{ - viewportType: 'volume', + viewportType: referencedDisplaySet.isReconstructable ? 'volume' : 'stack', toolGroupId: toolGroupId, orientation: viewportOptions.orientation, viewportId: viewportOptions.viewportId, diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts index 74f5d7ac34e..88889e3e548 100644 --- a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -39,12 +39,11 @@ class CornerstoneCacheService { ): Promise { let viewportType = viewportOptions.viewportType as string; - // Todo: Since Cornerstone 3D currently doesn't support segmentation - // on stack viewport, we should check if whether the the displaySets + // For VolumeViewport, we should check if whether the the displaySets // that are about to be displayed are referenced in a segmentation // as a reference volume, if so, we should hang a volume viewport // instead of a stack viewport - if (this._shouldRenderSegmentation(displaySets)) { + if (displaySets[0].isReconstructable && this._shouldRenderSegmentation(displaySets)) { // if the viewport type is volume 3D, we should let it be as it is // Todo: in future here we should kick start the conversion of the // segmentation to closed surface @@ -129,12 +128,12 @@ class CornerstoneCacheService { return newViewportData; } - private _getStackViewportData( + private async _getStackViewportData( dataSource, displaySets, initialImageIndex, viewportType: Enums.ViewportType - ): StackViewportData { + ): Promise { // For Stack Viewport we don't have fusion currently const displaySet = displaySets[0]; @@ -157,6 +156,14 @@ class CornerstoneCacheService { }, }; + // Atmost two displaysets are expected here even when we load multiple segmentations + // over referenced series(displaySets[0]). + if (displaySets[1]?.load && displaySets[1].load instanceof Function) { + const { userAuthenticationService } = this.servicesManager.services; + const headers = userAuthenticationService.getAuthorizationHeader(); + await displaySets[1].load({ headers }); + } + if (typeof initialImageIndex === 'number') { StackViewportData.data.initialImageIndex = initialImageIndex; } diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 355bcd0f588..6bb4d9b17a5 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -7,6 +7,8 @@ import { getEnabledElementByIds, utilities as csUtils, volumeLoader, + imageLoader, + metaData, } from '@cornerstonejs/core'; import { Enums as csToolsEnums, @@ -19,6 +21,7 @@ import { Types as ohifTypes } from '@ohif/core'; import { easeInOutBell, reverseEaseInOutBell } from '../../utils/transitions'; import { Segment, Segmentation, SegmentationConfig } from './SegmentationServiceTypes'; import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData'; +import createImageDataForStackImage from '../../utils/createImageDataForStackImage'; const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap; const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour; @@ -226,10 +229,11 @@ class SegmentationService extends PubSubService { // Get volume and delete the labels // Todo: handle other segmentations other than labelmap - const labelmapVolume = this.getLabelmapVolume(segmentationId); + const labelmap = + this.getLabelmapVolume(segmentationId) || this.getLabelmapImageData(segmentationId); - const { dimensions } = labelmapVolume; - const scalarData = labelmapVolume.getScalarData(); + const { dimensions } = labelmap; + const scalarData = labelmap.getScalarData(); // Set all values of this segment to zero and get which frames have been edited. const frameLength = dimensions[0] * dimensions[1]; @@ -513,7 +517,8 @@ class SegmentationService extends PubSubService { }, }; - const labelmap = this.getLabelmapVolume(segmentationId); + const labelmap = + this.getLabelmapVolume(segmentationId) || this.getLabelmapImageData(segmentationId); const cachedSegmentation = this.getSegmentation(segmentationId); if (labelmap && cachedSegmentation) { // if the labelmap with the same segmentationId already exists, we can @@ -531,27 +536,79 @@ class SegmentationService extends PubSubService { throw new Error('No labelmapBufferArray or referencedVolumeId found for the SEG displaySet'); } + const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( + segDisplaySet.referencedDisplaySetInstanceUID + ); + + let indexToWorld; // if the labelmap doesn't exist, we need to create it first from the // DICOM SEG displaySet data - const referencedVolume = cache.getVolume(referencedVolumeId); + if (referencedDisplaySet.isReconstructable) { + const referencedVolume = cache.getVolume(referencedVolumeId); - if (!referencedVolume) { - throw new Error(`No volume found for referencedVolumeId: ${referencedVolumeId}`); - } + if (!referencedVolume) { + throw new Error(`No volume found for referencedVolumeId: ${referencedVolumeId}`); + } - // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so - // it is easily compressible in worker thread. - const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, { - volumeId: segmentationId, - targetBuffer: { - type: 'Uint8Array', - sharedArrayBuffer: window.SharedArrayBuffer, - }, - }); - const derivedVolumeScalarData = derivedVolume.getScalarData(); + // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so + // it is easily compressible in worker thread. + const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, { + volumeId: segmentationId, + targetBuffer: { + type: 'Uint8Array', + sharedArrayBuffer: window.SharedArrayBuffer, + }, + }); + const derivedVolumeScalarData = derivedVolume.getScalarData(); + derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0])); + + indexToWorld = derivedVolume.imageData.indexToWorld; + } else { + const getDerivedImageId = (imageId: string): string => `segimage:${segmentationId}:${imageId}`; + + const referencedImageIds = referencedDisplaySet.instances.reduce((imageIds, instance) => { + return [...imageIds, instance.imageId]; + }, []); + const imageIdReferenceMap = new Map(); + const segImageIds: string[] = []; + + referencedImageIds.forEach(referencedImageId => { + const segImageId = getDerivedImageId(referencedImageId); + imageIdReferenceMap.set(referencedImageId, segImageId); + segImageIds.push(segImageId); + + if (cache.getImage(segImageId)) { + return; + } + + imageLoader.createAndCacheDerivedImage(referencedImageId, { + imageId: segImageId, + targetBufferType: 'Uint8Array', + }); + }); + + // Change the segmentation labelmap representation to data to the Stack viewport one. + segmentation.representationData[LABELMAP] = { imageIdReferenceMap }; + + const { rows, columns } = metaData.get('imagePlaneModule', segImageIds[0]); + + const bytes = new Uint8Array(labelmapBufferArray[0]); + const singleSlicePixelSize = rows * columns; + for (let i = 0; i < referencedDisplaySet.instances.length; i++) { + const singleSlicePixelData = new Uint8Array( + bytes.slice(i * singleSlicePixelSize, (i + 1) * singleSlicePixelSize).buffer + ); + + const image = await cache.getImageLoadObject(segImageIds[i]).promise; + const pixelData = image.getPixelData(); + pixelData.set(singleSlicePixelData); + } + + const { imageData } = createImageDataForStackImage(imageIdReferenceMap); + indexToWorld = imageData.indexToWorld; + } const segmentsInfo = segDisplaySet.segMetadata.data; - derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0])); segmentation.segments = segmentsInfo.map((segmentInfo, segmentIndex) => { if (segmentIndex === 0) { @@ -569,7 +626,7 @@ class SegmentationService extends PubSubService { } = segmentInfo; const { x, y, z } = segDisplaySet.centroids.get(segmentIndex); - const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]); + const centerWorld = indexToWorld([x, y, z]); segmentation.cachedStats = { ...segmentation.cachedStats, @@ -761,9 +818,10 @@ class SegmentationService extends PubSubService { segmentIndex?: number ): Map => { const segmentation = this.getSegmentation(segmentationId); - const volume = this.getLabelmapVolume(segmentationId); - const { dimensions, imageData } = volume; - const scalarData = volume.getScalarData(); + const labelmap = + this.getLabelmapVolume(segmentationId) || this.getLabelmapImageData(segmentationId); + const { dimensions, imageData } = labelmap; + const scalarData = labelmap.getScalarData(); const [dimX, dimY, numFrames] = dimensions; const frameLength = dimX * dimY; @@ -819,7 +877,9 @@ class SegmentationService extends PubSubService { centroids: Map ): void => { const segmentation = this.getSegmentation(segmentationId); - const imageData = this.getLabelmapVolume(segmentationId).imageData; // Assuming this method returns imageData + const imageData = ( + this.getLabelmapVolume(segmentationId) || this.getLabelmapImageData(segmentationId) + ).imageData; // Assuming this method returns imageData if (!segmentation.cachedStats) { segmentation.cachedStats = { segmentCenter: {} }; @@ -960,15 +1020,31 @@ class SegmentationService extends PubSubService { const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`; - // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so - // it is easily compressible in worker thread. - await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, { - volumeId: segmentationId, - targetBuffer: { - type: 'Uint8Array', - sharedArrayBuffer: window.SharedArrayBuffer, - }, - }); + const imageIdReferenceMap = new Map(); + + if (displaySet.isReconstructable) { + // Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so + // it is easily compressible in worker thread. + await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, { + volumeId: segmentationId, + targetBuffer: { + type: 'Uint8Array', + sharedArrayBuffer: window.SharedArrayBuffer, + }, + }); + } else { + const getDerivedImageId = (imageId: string): string => `segimage:${segmentationId}:${imageId}`; + + const referencedImageIds = displaySet.instances.reduce((imageIds, instance) => { + return [...imageIds, instance.imageId]; + }, []); + + imageLoader.createAndCacheDerivedSegmentationImages(referencedImageIds, { getDerivedImageId}); + + referencedImageIds.forEach(imageId => + imageIdReferenceMap.set(imageId, getDerivedImageId(imageId)) + ); + } const defaultScheme = this._getDefaultSegmentationScheme(); @@ -983,10 +1059,12 @@ class SegmentationService extends PubSubService { FrameOfReferenceUID: options?.FrameOfReferenceUID || displaySet.instances?.[0]?.FrameOfReferenceUID, representationData: { - LABELMAP: { - volumeId: segmentationId, - referencedVolumeId: volumeId, // Todo: this is so ugly - }, + LABELMAP: displaySet.isReconstructable + ? { + volumeId: segmentationId, + referencedVolumeId: volumeId, // Todo: this is so ugly + } + : { imageIdReferenceMap }, }, description: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`, }; @@ -1437,6 +1515,15 @@ class SegmentationService extends PubSubService { return cache.getVolume(segmentationId); }; + public getLabelmapImageData = (segmentationId: string) => { + const segmentation = this.getSegmentation(segmentationId) as Segmentation; + if (segmentation?.representationData[LABELMAP].imageIdReferenceMap?.size) { + return createImageDataForStackImage( + segmentation?.representationData[LABELMAP].imageIdReferenceMap + ); + } + }; + public getSegmentationRepresentationsForToolGroup = toolGroupId => { return cstSegmentation.state.getSegmentationRepresentations(toolGroupId); }; diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts index cb65154d26d..c86b04c02c6 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationServiceTypes.ts @@ -60,11 +60,17 @@ type Segmentation = { representationData: SegmentationRepresentationData; }; -type LabelmapSegmentationData = { +type LabelmapVolumeData = { volumeId: string; referencedVolumeId?: string; }; +type LabelmapStackData = { + imageIdReferenceMap: Map; +}; + +type LabelmapSegmentationData = LabelmapVolumeData | LabelmapStackData; + type SegmentationRepresentationData = { LABELMAP?: LabelmapSegmentationData; }; diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 9fd4f3ce5f2..02c35dce861 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -588,6 +588,31 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } } + const segmentations = this.servicesManager.services.segmentationService.getSegmentations(false); + const toolgroupId = viewportInfo.getToolGroupId(); + for (const segmentation of segmentations) { + const toolGroupSegmentationRepresentations = + this.servicesManager.services.segmentationService.getSegmentationRepresentationsForToolGroup( + toolgroupId + ) || []; + const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find( + representation => representation.segmentationId === segmentation.id + ); + + if (!isSegmentationInToolGroup) { + const segDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( + segmentation.id + ); + + segDisplaySet && + this.servicesManager.services.segmentationService.addSegmentationRepresentationToToolGroup( + toolgroupId, + segmentation.id, + segDisplaySet.isOverlayDisplaySet + ); + } + } + return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); this.setPresentations(viewport.id, presentations); diff --git a/extensions/cornerstone/src/utils/createImageDataForStackImage.ts b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts new file mode 100644 index 00000000000..74fa35d1e57 --- /dev/null +++ b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts @@ -0,0 +1,82 @@ +import { vec3 } from 'gl-matrix'; +import { metaData, Types as csCoreTypes, cache } from '@cornerstonejs/core'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; + +export default function createImageDataForStackImage(imageIdReferenceMap: Map): { + dimensions: [number, number, number]; + direction: csCoreTypes.Mat3; + spacing: [number, number, number]; + origin: [number, number, number]; + getScalarData: () => csCoreTypes.PixelDataTypedArray; + imageData: vtkImageData; + metadata: Record; +} { + const image = cache.getImage(imageIdReferenceMap.values().next().value); + const imageMetaData = metaData.get('imagePlaneModule', image.imageId); + const { + imageOrientationPatient, + pixelSpacing, + imagePositionPatient, + columns, + rows, + sliceThickness, + } = imageMetaData; + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: 1, + values: image.getPixelData(), + }); + + const imageData = vtkImageData.newInstance(); + + let direction, origin; + + if (imageOrientationPatient?.length) { + const rowCosineVec = vec3.fromValues( + imageOrientationPatient[0], + imageOrientationPatient[1], + imageOrientationPatient[2] + ); + + const colCosineVec = vec3.fromValues( + imageOrientationPatient[3], + imageOrientationPatient[4], + imageOrientationPatient[5] + ); + + const scanAxisNormal = vec3.create(); + vec3.cross(scanAxisNormal, rowCosineVec, colCosineVec); + + direction = [ + ...Array.from(rowCosineVec), + ...Array.from(colCosineVec), + ...Array.from(scanAxisNormal), + ] as csCoreTypes.Mat3; + + imageData.setDirection(direction); + } + + if (imagePositionPatient?.length) { + origin = [...imagePositionPatient] as csCoreTypes.Point3; + imageData.setOrigin(imagePositionPatient); + } + + const spacing = [pixelSpacing[1], pixelSpacing[0], sliceThickness] as csCoreTypes.Point3; + const dimensions = [columns, rows, 1] as csCoreTypes.Point3; + + imageData.setDimensions(dimensions); + imageData.setSpacing(spacing); + imageData.getPointData().setScalars(scalarArray); + + return { + dimensions, + direction, + spacing, + origin, + getScalarData: () => image.getPixelData(), + imageData, + metadata: imageMetaData, + }; +} diff --git a/extensions/cornerstone/src/utils/dicomLoaderService.js b/extensions/cornerstone/src/utils/dicomLoaderService.js index b9d5ea6dd94..4615a85610b 100644 --- a/extensions/cornerstone/src/utils/dicomLoaderService.js +++ b/extensions/cornerstone/src/utils/dicomLoaderService.js @@ -194,6 +194,12 @@ class DicomLoaderService { // from it if it is not absolute. For instance it might be dicomweb:http://.... // and we need to remove the dicomweb: part const url = instance.url; + + if (url.startsWith('dicomzip')) { + const { url: uri } = dicomImageLoader.wadouri.parseImageId(url); + return dicomImageLoader.wadouri.loadZipRequest(uri, url); + } + const absoluteUrl = url.startsWith('http') ? url : url.substring(url.indexOf(':') + 1); return fetchIt(absoluteUrl, { headers: authorizationHeaders }); } diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js index 8b4ec2f2ac4..0410534bf7b 100644 --- a/platform/app/public/config/gradient.js +++ b/platform/app/public/config/gradient.js @@ -37,7 +37,7 @@ window.config = { 'profile', 'openid', 'https://www.googleapis.com/auth/spreadsheets', - 'https://www.googleapis.com/auth/devstorage.read_only', + 'https://www.googleapis.com/auth/devstorage.read_write', 'https://www.googleapis.com/auth/bigquery.readonly', 'https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/drive.readonly', diff --git a/platform/app/src/service-worker.js b/platform/app/src/service-worker.js index a235b8c7924..5e1f4cfbea3 100644 --- a/platform/app/src/service-worker.js +++ b/platform/app/src/service-worker.js @@ -62,6 +62,33 @@ self.addEventListener('message', event => { } }); +// FETCH HANDLER +self.addEventListener('fetch', function (event) { + if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { + return; + } + + event.respondWith( + fetch(event.request) + .then(function (response) { + const newHeaders = new Headers(response.headers); + newHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp'); + newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin'); + + const moddedResponse = new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }); + + return moddedResponse; + }) + .catch(function (e) { + console.error(e); + }) + ); +}); + workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); // TODO: Cache API diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts index 30525a2bc78..34f75423b32 100644 --- a/platform/core/src/classes/MetadataProvider.ts +++ b/platform/core/src/classes/MetadataProvider.ts @@ -471,6 +471,9 @@ class MetadataProvider { } getUIDsFromImageID(imageId) { + if (Array.isArray(imageId)) { + imageId = imageId[0]; + } if (!imageId) { throw new Error('MetadataProvider::Empty imageId'); } diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx index ff909483a10..b59212358e1 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx @@ -4,33 +4,40 @@ import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; function SegmentationDropDownRow({ - segmentations = [], - activeSegmentation, - onActiveSegmentationChange, + segmentation, + activeSegmentationId, disableEditing, + showAddSegment, onToggleSegmentationVisibility, + onSegmentationClick, onSegmentationEdit, onSegmentationDownload, onSegmentationDownloadRTSS, storeSegmentation, onSegmentationDelete, - onSegmentationAdd, addSegmentationClassName, + onSegmentAdd, + onToggleShowSegments, + showSegments, }) { - const handleChange = option => { - onActiveSegmentationChange(option.value); // Notify the parent - }; - - const selectOptions = segmentations.map(s => ({ - value: s.id, - label: s.label, - })); const { t } = useTranslation('SegmentationTable'); - if (!activeSegmentation) { + if (!segmentation) { return null; } + const segmentationClickHandler = () => { + if (segmentation.id === activeSegmentationId) { + onToggleShowSegments(!showSegments); + } else { + onSegmentationClick(segmentation.id); + + if (!showSegments) { + onToggleShowSegments(true); + } + } + }; + return (
{ - onSegmentationAdd(); + onSegmentAdd(segmentation.id); }, }, ] @@ -61,7 +68,7 @@ function SegmentationDropDownRow({ { title: t('Rename'), onClick: () => { - onSegmentationEdit(activeSegmentation.id); + onSegmentationEdit(segmentation.id); }, }, ] @@ -69,7 +76,7 @@ function SegmentationDropDownRow({ { title: t('Delete'), onClick: () => { - onSegmentationDelete(activeSegmentation.id); + onSegmentationDelete(segmentation.id); }, }, ...(!disableEditing @@ -77,7 +84,7 @@ function SegmentationDropDownRow({ { title: t('Export DICOM SEG'), onClick: () => { - storeSegmentation(activeSegmentation.id); + storeSegmentation(segmentation.id); }, }, ] @@ -86,13 +93,13 @@ function SegmentationDropDownRow({ { title: t('Download DICOM SEG'), onClick: () => { - onSegmentationDownload(activeSegmentation.id); + onSegmentationDownload(segmentation.id); }, }, { title: t('Download DICOM RTSTRUCT'), onClick: () => { - onSegmentationDownloadRTSS(activeSegmentation.id); + onSegmentationDownloadRTSS(segmentation.id); }, }, ], @@ -103,32 +110,19 @@ function SegmentationDropDownRow({
- {selectOptions?.length && ( -