diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml new file mode 100644 index 00000000000..d9a36ff5f31 --- /dev/null +++ b/.github/workflows/deploy_ghpages.yml @@ -0,0 +1,105 @@ +name: Deploy viewer to github pages + +on: + push: + branches: [ "gradienthealth/segmentation_mode_sheet_integration" ] + #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/segmentation_mode_sheet_integration + 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/segmentation_mode_sheet_integration + 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 cli link-mode ../GradientExtensionsAndModes/modes/breast-density-mode + 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/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 4a770d7e177..dd250d63379 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -19,8 +19,10 @@ import { getUpdatedViewportsForSegmentation, getTargetViewport, } from './utils/hydrationUtils'; -const { segmentation: segmentationUtils } = utilities; +import generateLabelmaps2DFromImageIdMap from './utils/generateLabelmaps2DFromImageIdMap'; +import getSegmentLabel from './utils/getSegmentLabel'; +const { segmentation: segmentationUtils } = utilities; const { datasetToBlob } = dcmjs.data; const { @@ -80,17 +82,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, @@ -113,7 +104,7 @@ const commandsModule = ({ toolGroupId, segmentIndex: 1, properties: { - label: 'Segment 1', + label: getSegmentLabel(segmentationService.getSegmentation(segmentationId)), }, }); @@ -245,14 +236,23 @@ const commandsModule = ({ */ generateSegmentation: ({ segmentationId, options = {} }) => { const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); + const segmentationLabelmapData = segmentation.representationData.LABELMAP; + + let referencedImages, labelmapObj; + if (segmentationLabelmapData.imageIdReferenceMap) { + const { imageIdReferenceMap } = segmentationLabelmapData; - const { referencedVolumeId } = segmentation.representationData.LABELMAP; + ({ referencedImages, labelmapObj } = + generateLabelmaps2DFromImageIdMap(imageIdReferenceMap)); + } else { + const { referencedVolumeId } = segmentationLabelmapData; - const segmentationVolume = cache.getVolume(segmentationId); - const referencedVolume = cache.getVolume(referencedVolumeId); - const referencedImages = referencedVolume.getCornerstoneImages(); + const segmentationVolume = cache.getVolume(segmentationId); + const referencedVolume = cache.getVolume(referencedVolumeId); + referencedImages = referencedVolume.getCornerstoneImages(); - const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); + labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); + } // Generate fake metadata as an example labelmapObj.metadata = []; @@ -326,28 +326,45 @@ const commandsModule = ({ * @returns {Object|void} Returns the naturalized report if successfully stored, * otherwise throws an error. */ - storeSegmentation: async ({ segmentationId, dataSource }) => { - const promptResult = await createReportDialogPrompt(uiDialogService, { - extensionManager, - }); - - if (promptResult.action !== 1 && promptResult.value) { - return; - } - + storeSegmentation: async ({ segmentationId, dataSource, skipLabelDialog = false }) => { const segmentation = segmentationService.getSegmentation(segmentationId); if (!segmentation) { throw new Error('No segmentation found'); } + const { label, displaySetInstanceUID } = segmentation; + + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + const shouldOverWrite = displaySet && displaySet.Modality === 'SEG'; + + let promptResult: { action?: number; value?: string } = {}; + + if (!(skipLabelDialog || shouldOverWrite)) { + promptResult = await createReportDialogPrompt(uiDialogService, { + extensionManager, + }); + + if (promptResult.action !== 1 && !promptResult.value) { + return; + } + } - const { label } = segmentation; const SeriesDescription = promptResult.value || label || 'Research Derived Series'; + segmentation.label = SeriesDescription; const generatedData = actions.generateSegmentation({ segmentationId, options: { SeriesDescription, + // Use SeriesInstanceUID, SOPInstanceUID, SeriesNumber, Manufacturer and SeriesDate + // if displaySet of the segmentation already exists. + // Study level and patient metadata will be used automatically. + ...(shouldOverWrite && { + SeriesInstanceUID: displaySet.SeriesInstanceUID, + SOPInstanceUID: displaySet.instances[0].SOPInstanceUID, + SeriesNumber: displaySet.SeriesNumber, + Manufacturer: displaySet.instances[0].Manufacturer, + }), }, }); @@ -366,8 +383,6 @@ const commandsModule = ({ // add the information for where we stored it to the instance as well naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot; - DicomMetadataStore.addInstances([naturalizedReport], true); - return naturalizedReport; }, /** diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js index d150201e06c..c7bf79e6643 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js @@ -142,20 +142,27 @@ 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); + if (!cachedReferencedVolume) { + throw new Error('Referenced Volume is missing for the SEG'); + } - const { imageIds } = cachedReferencedVolume; + imageIds = cachedReferencedVolume.imageIds || cachedReferencedVolume._imageIds; + } else { + imageIds = referencedDisplaySet.instances.map(instance => instance.imageId); + } // Todo: what should be defaults here const tolerance = 0.001; @@ -198,6 +205,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, }); } + /* Skip the warning message as it is annoying on auto segmentations loading. if (!usedRecommendedDisplayCIELabValue) { // Display a notification about the non-utilization of RecommendedDisplayCIELabValue uiNotificationService.show({ @@ -207,7 +215,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, type: 'warning', duration: 5000, }); - } + }*/ Object.assign(segDisplaySet, results); } diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 9dd15e8f29f..9504e09235d 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -1,11 +1,25 @@ import { createReportAsync } from '@ohif/extension-default'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useReducer } from 'react'; import PropTypes from 'prop-types'; import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui'; import { SegmentationPanelMode } from '../types/segmentation'; import callInputDialog from './callInputDialog'; import callColorPickerDialog from './colorPickerDialog'; import { useTranslation } from 'react-i18next'; +import getSegmentLabel from '../utils/getSegmentLabel'; + +const savedStatusReducer = (state, action) => { + return { + ...state, + ...action.payload, + }; +}; + +const SAVED_STATUS_ICON = { + SAVED: 'notifications-success', + MODIFIED: 'notifications-warning', + ERROR: 'notifications-error', +}; const components = { [SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded, @@ -23,18 +37,19 @@ export default function PanelSegmentation({ viewportGridService, uiDialogService, displaySetService, - cornerstoneViewportService, + userAuthenticationService, + CropDisplayAreaService, } = servicesManager.services; const { t } = useTranslation('PanelSegmentation'); const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); - const [addSegmentationClassName, setAddSegmentationClassName] = useState(''); const [segmentationConfiguration, setSegmentationConfiguration] = useState( segmentationService.getConfiguration() ); const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); + const [savedStatusStates, dispatch] = useReducer(savedStatusReducer, {}); useEffect(() => { // ~~ Subscription @@ -59,63 +74,111 @@ 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; + let changedSegmentations: any[] = [], + timerId; + const timoutInSeconds = 5; + + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + ({ segmentation }) => { + clearTimeout(timerId); + dispatch({ payload: { [segmentation.id]: SAVED_STATUS_ICON.MODIFIED } }); + + if ( + !changedSegmentations.find( + changedSegmentation => changedSegmentation.id === segmentation.id + ) + ) { + changedSegmentations.push(segmentation); + } + + timerId = setTimeout(() => { + const datasources = extensionManager.getActiveDataSource(); + + const promises = changedSegmentations.map(segmentation => + createReportAsync({ + servicesManager: servicesManager, + getReport: () => + commandsManager.runCommand('storeSegmentation', { + segmentationId: segmentation.id, + dataSource: datasources[0], + skipLabelDialog: true, + }), + reportType: 'Segmentation', + showLoadingModal: false, + throwErrors: true, + }) + ); + + Promise.allSettled(promises).then(results => { + const payload = results.reduce((acc, result, index) => { + if (result.value) { + changedSegmentations[index].displaySetInstanceUID = result.value[0]; + displaySetService.getDisplaySetByUID(result.value[0])?.getReferenceDisplaySet(); + } + + return { + ...acc, + [changedSegmentations[index].id]: + result.status === 'fulfilled' ? SAVED_STATUS_ICON.SAVED : SAVED_STATUS_ICON.ERROR, + }; + }, {}); + + dispatch({ payload }); + + const savedSegmentations = Object.keys(payload).filter( + id => payload[id] === SAVED_STATUS_ICON.SAVED + ); + changedSegmentations = changedSegmentations.filter( + cs => !savedSegmentations.includes(cs.id) + ); + }); + }, timoutInSeconds * 1000); } + ); - const isReconstructable = - displaySetUIDs?.some(displaySetUID => { - const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); - return displaySet?.isReconstructable; - }) || false; - - if (isReconstructable) { - setAddSegmentationClassName(''); - } else { - setAddSegmentationClassName('ohif-disabled'); - } + return () => { + unsubscribe(); }; + }, []); - // Handle initial state - handleActiveViewportChange(); - - const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED; - const ready = viewportGridService.EVENTS.VIEWPORTS_READY; + const setSegmentationActive = segmentationId => { + setReferencedDisplaySet(segmentationId); - const subsGrid = []; - [ready, changedGrid].forEach(evt => { - const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => { - handleActiveViewportChange(viewportId); - }); + const isSegmentationActive = segmentations.find(seg => seg.id === segmentationId)?.isActive; - subsGrid.push(unsubscribe); - }); + if (isSegmentationActive) { + return; + } - const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED; + segmentationService.setActiveSegmentationForToolGroup(segmentationId); + }; - const subsData = []; - [changedData].forEach(evt => { - const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => { - handleActiveViewportChange(); - }); + // Set referenced displaySet of the segmentation to the viewport + // if it is not displayed in any of the viewports. + const setReferencedDisplaySet = segmentationId => { + const segDisplayset = displaySetService.getDisplaySetByUID(segmentationId); + if (!segDisplayset) { + return; + } - subsData.push(unsubscribe); + const referencedDisplaySetInstanceUID = segDisplayset.referencedDisplaySetInstanceUID; + const { viewports, activeViewportId } = viewportGridService.getState(); + let referencedImageLoaded = false; + viewports.forEach(viewport => { + if (viewport.displaySetInstanceUIDs.includes(referencedDisplaySetInstanceUID)) { + referencedImageLoaded = true; + } }); - // Clean up - return () => { - subsGrid.forEach(unsub => unsub()); - subsData.forEach(unsub => unsub()); - }; - }, []); + if (!referencedImageLoaded) { + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [referencedDisplaySetInstanceUID], + }); + } + }; const getToolGroupIds = segmentationId => { const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); @@ -130,30 +193,35 @@ export default function PanelSegmentation({ }; const onSegmentationClick = (segmentationId: string) => { + setReferencedDisplaySet(segmentationId); segmentationService.setActiveSegmentationForToolGroup(segmentationId); }; const onSegmentationDelete = (segmentationId: string) => { + setSegmentationActive(segmentationId); segmentationService.remove(segmentationId); }; const onSegmentAdd = segmentationId => { - segmentationService.addSegment(segmentationId); + setSegmentationActive(segmentationId); + const label = getSegmentLabel(segmentations.find(seg => seg.id === segmentationId)); + segmentationService.addSegment(segmentationId, { properties: { label } }); }; const onSegmentClick = (segmentationId, segmentIndex) => { + setReferencedDisplaySet(segmentationId); segmentationService.setActiveSegment(segmentationId, segmentIndex); 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 +237,7 @@ export default function PanelSegmentation({ }; const onSegmentationEdit = segmentationId => { + setSegmentationActive(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const { label } = segmentation; @@ -189,6 +258,7 @@ export default function PanelSegmentation({ }; const onSegmentColorClick = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId); const segment = segmentation.segments[segmentIndex]; @@ -216,11 +286,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 +310,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,23 +346,34 @@ export default function PanelSegmentation({ ); const onSegmentationDownload = segmentationId => { + setSegmentationActive(segmentationId); commandsManager.runCommand('downloadSegmentation', { segmentationId, }); }; const storeSegmentation = async segmentationId => { + setSegmentationActive(segmentationId); const datasources = extensionManager.getActiveDataSource(); + let displaySetInstanceUIDs; + + try { + displaySetInstanceUIDs = await createReportAsync({ + servicesManager, + getReport: () => + commandsManager.runCommand('storeSegmentation', { + segmentationId, + dataSource: datasources[0], + }), + reportType: 'Segmentation', + throwErrors: true, + }); - const displaySetInstanceUIDs = await createReportAsync({ - servicesManager, - getReport: () => - commandsManager.runCommand('storeSegmentation', { - segmentationId, - dataSource: datasources[0], - }), - reportType: 'Segmentation', - }); + dispatch({ payload: { [segmentationId]: SAVED_STATUS_ICON.SAVED } }); + } catch (error) { + console.warn(error.message); + dispatch({ payload: { [segmentationId]: SAVED_STATUS_ICON.ERROR } }); + } // Show the exported report in the active viewport as read only (similar to SR) if (displaySetInstanceUIDs) { @@ -305,6 +390,7 @@ export default function PanelSegmentation({ }; const onSegmentationDownloadRTSS = segmentationId => { + setSegmentationActive(segmentationId); commandsManager.runCommand('downloadRTSS', { segmentationId, }); @@ -317,15 +403,19 @@ export default function PanelSegmentation({ configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function' ? configuration?.onSegmentationAdd : onSegmentationAdd; + const params = new URLSearchParams(window.location.search); + const showAddSegmentation = params.get('disableAddSegmentation') !== 'true'; + return ( _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) } + CropDisplayAreaService={CropDisplayAreaService} /> ); } 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/getSegmentLabel.ts b/extensions/cornerstone-dicom-seg/src/utils/getSegmentLabel.ts new file mode 100644 index 00000000000..7215361b598 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/getSegmentLabel.ts @@ -0,0 +1,8 @@ +const getSegmentLabel = (segmentation): string => { + const segmentationName = segmentation.label.includes('Vessel') ? 'Vessel' : 'Segment'; + const segmentCount = segmentation.segments.filter(segment => segment).length; + + return segmentationName + ' ' + (segmentCount + 1); +}; + +export default getSegmentLabel; 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/hps/mpr.ts b/extensions/cornerstone/src/hps/mpr.ts index fa54ffde6de..087874b5288 100644 --- a/extensions/cornerstone/src/hps/mpr.ts +++ b/extensions/cornerstone/src/hps/mpr.ts @@ -10,7 +10,7 @@ export const mpr: Types.HangingProtocol.Protocol = { modifiedDate: '2023-08-15', availableTo: {}, editableBy: {}, - // Unknown number of priors referenced - so just match any study + // Unknown number of priors referenced - so just match any study. numberOfPriorsReferenced: 0, protocolMatchingRules: [], imageLoadStrategy: 'nth', 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..517ea3bdfbe 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; @@ -798,15 +856,9 @@ class SegmentationService extends PubSubService { for (const [index, centroid] of centroids) { const count = centroid.count; const normalizedCentroid = { - x: centroid.x / count, - y: centroid.y / count, - z: centroid.z / count, + image: [centroid.x / count, centroid.y / count, centroid.z / count], }; - normalizedCentroid.world = imageData.indexToWorld([ - normalizedCentroid.x, - normalizedCentroid.y, - normalizedCentroid.z, - ]); + normalizedCentroid.world = imageData.indexToWorld(normalizedCentroid.image); result.set(index, normalizedCentroid); } @@ -819,7 +871,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 +1014,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 +1053,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 +1509,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); }; @@ -2007,6 +2088,16 @@ class SegmentationService extends PubSubService { cache.removeVolumeLoadObject(segmentationId); } + const segmentation = this.getSegmentation(segmentationId); + const segmentationImageMap = + segmentation.representationData[segmentation.type].imageIdReferenceMap; + if (removeFromCache && segmentationImageMap) { + segmentationImageMap.forEach( + segImageId => + cache.getVolumeLoadObject(segImageId) && cache.removeImageLoadObject(segImageId) + ); + } + return { updatedToolGroupIds: Array.from(updatedToolGroupIds) }; } 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..efa4150efd1 --- /dev/null +++ b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts @@ -0,0 +1,85 @@ +import { vec3, mat3 } 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 = [1, 1], + imagePositionPatient, + columns, + rows, + sliceThickness = 1, + } = imageMetaData; + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: 1, + values: image.getPixelData(), + }); + + const imageData = vtkImageData.newInstance(); + + let direction: csCoreTypes.Mat3 = mat3.fromValues(0, 1, 0, 0, 0, -1, -1, 0, 0), + origin: csCoreTypes.Point3 = [0, 0, 0]; + + 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.setDirection(direction); + imageData.setOrigin(origin); + 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/extensions/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx index d0f34a48a23..4006951c7d6 100644 --- a/extensions/default/src/Actions/createReportAsync.tsx +++ b/extensions/default/src/Actions/createReportAsync.tsx @@ -1,46 +1,82 @@ import React from 'react'; +import dcmjs from 'dcmjs'; import { DicomMetadataStore } from '@ohif/core'; +const { datasetToBlob } = dcmjs.data; + /** * * @param {*} servicesManager */ -async function createReportAsync({ servicesManager, getReport, reportType = 'measurement' }) { - const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services; - const loadingDialogId = uiDialogService.create({ - showOverlay: true, - isDraggable: false, - centralize: true, - content: Loading, - }); +async function createReportAsync({ + servicesManager, + getReport, + reportType = 'measurement', + showLoadingModal = true, + throwErrors = false, +}) { + const { displaySetService, uiNotificationService, uiDialogService, CacheAPIService } = + servicesManager.services; + const loadingDialogId = + showLoadingModal && + uiDialogService.create({ + showOverlay: true, + isDraggable: false, + centralize: true, + content: Loading, + }); + + let displaySetInstanceUID; try { const naturalizedReport = await getReport(); + const { SeriesInstanceUID, SOPInstanceUID } = naturalizedReport; + let displaySet = displaySetService + .getDisplaySetsForSeries(SeriesInstanceUID) + ?.find(ds => ds.instances.some(instance => instance.SOPInstanceUID === SOPInstanceUID)); + + const shouldOverWrite = displaySet && displaySet.Modality === 'SEG'; + // The "Mode" route listens for DicomMetadataStore changes // When a new instance is added, it listens and // automatically calls makeDisplaySets DicomMetadataStore.addInstances([naturalizedReport], true); - const displaySet = displaySetService.getMostRecentDisplaySet(); + if (!displaySet) { + // If there is no displayset before adding instances, it is a new series. + displaySet = displaySetService.getMostRecentDisplaySet(); + } - const displaySetInstanceUID = displaySet.displaySetInstanceUID; + displaySetInstanceUID = displaySet.displaySetInstanceUID; - uiNotificationService.show({ - title: 'Create Report', - message: `${reportType} saved successfully`, - type: 'success', - }); + showLoadingModal && + uiNotificationService.show({ + title: 'Create Report', + message: `${reportType} saved successfully`, + type: 'success', + }); + + reportType === 'Segmentation' && + CacheAPIService?.updateCachedFile(datasetToBlob(naturalizedReport), displaySet); + if (shouldOverWrite) { + return; + } return [displaySetInstanceUID]; } catch (error) { - uiNotificationService.show({ - title: 'Create Report', - message: error.message || `Failed to store ${reportType}`, - type: 'error', - }); + showLoadingModal && + uiNotificationService.show({ + title: 'Create Report', + message: error.message || `Failed to store ${reportType}`, + type: 'error', + }); + + if (throwErrors) { + throw error; + } } finally { - uiDialogService.dismiss({ id: loadingDialogId }); + showLoadingModal && uiDialogService.dismiss({ id: loadingDialogId }); } } diff --git a/extensions/default/src/Panels/PanelStudyBrowser.tsx b/extensions/default/src/Panels/PanelStudyBrowser.tsx index e1e6c30c0f2..3012bae8dfd 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.tsx @@ -213,7 +213,7 @@ function PanelStudyBrowser({ const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] + [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); 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/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 85a55a83052..219f7324cf1 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -26,6 +26,12 @@ const segmentation = { viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', }; +const gradienthealth = { + form: '@gradienthealth/ohif-gradienthealth-extension.panelModule.form', + thumbnailList: + '@gradienthealth/ohif-gradienthealth-extension.panelModule.seriesList-without-tracking', +}; + /** * Just two dependencies to be able to render a viewport with panels in order * to make sure that the mode is working. @@ -54,13 +60,23 @@ function modeFactory({ modeConfiguration }) { * Services and other resources. */ onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { - const { measurementService, toolbarService, toolGroupService } = servicesManager.services; + const { + measurementService, + toolbarService, + toolGroupService, + GoogleSheetsService, + CropDisplayAreaService, + CacheAPIService, + } = servicesManager.services; measurementService.clearMeasurements(); // Init Default and SR ToolGroups initToolGroups(extensionManager, toolGroupService, commandsManager); + GoogleSheetsService.init(); + CropDisplayAreaService.init(); + CacheAPIService.init(); toolbarService.addButtons(toolbarButtons); toolbarService.addButtons(segmentationButtons); @@ -85,6 +101,9 @@ function modeFactory({ modeConfiguration }) { cornerstoneViewportService, uiDialogService, uiModalService, + GoogleSheetsService, + CropDisplayAreaService, + CacheAPIService, } = servicesManager.services; uiDialogService.dismissAll(); @@ -93,6 +112,9 @@ function modeFactory({ modeConfiguration }) { syncGroupService.destroy(); segmentationService.destroy(); cornerstoneViewportService.destroy(); + GoogleSheetsService.destroy(); + CropDisplayAreaService.destroy(); + CacheAPIService.destroy(); }, /** */ validationTags: { @@ -133,11 +155,16 @@ function modeFactory({ modeConfiguration }) { { path: 'template', layoutTemplate: ({ location, servicesManager }) => { + const params = new URLSearchParams(location.search); + const rightPanels = [ + segmentation.panelTool, + ...(params.get('sheetId') ? [gradienthealth.form] : []), + ]; return { id: ohif.layout, props: { - leftPanels: [ohif.leftPanel], - rightPanels: [segmentation.panelTool], + leftPanels: [gradienthealth.thumbnailList], + rightPanels: rightPanels, viewports: [ { namespace: cornerstone.viewport, diff --git a/modes/segmentation/src/segmentationButtons.ts b/modes/segmentation/src/segmentationButtons.ts index 448267f0294..65adfa54070 100644 --- a/modes/segmentation/src/segmentationButtons.ts +++ b/modes/segmentation/src/segmentationButtons.ts @@ -21,10 +21,10 @@ const toolbarButtons: Button[] = [ name: 'Radius (mm)', id: 'brush-radius', type: 'range', - min: 0.5, - max: 99.5, - step: 0.5, - value: 25, + min: 2, + max: 3, + step: 0.01, + value: 2, commands: { commandName: 'setBrushSize', commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, @@ -56,10 +56,10 @@ const toolbarButtons: Button[] = [ name: 'Radius (mm)', id: 'eraser-radius', type: 'range', - min: 0.5, - max: 99.5, - step: 0.5, - value: 25, + min: 2, + max: 3, + step: 0.01, + value: 2, commands: { commandName: 'setBrushSize', commandOptions: { toolNames: ['CircularEraser', 'SphereEraser'] }, diff --git a/package.json b/package.json index c52519e0e8f..4491e0308c5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "scripts": { "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 5e6181094b0..0d99775b682 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -19,6 +19,8 @@ "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: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", @@ -50,6 +52,7 @@ ], "dependencies": { "@babel/runtime": "^7.20.13", + "@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 +87,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/pluginConfig.json b/platform/app/pluginConfig.json index a910bee9fbf..4a1a9124ec2 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,14 @@ "packageName": "@ohif/mode-basic-dev-mode", "default": false, "version": "3.0.0" + }, + { + "packageName": "@gradienthealth/cohort-mode", + "version": "0.1.2" + }, + { + "packageName": "@gradienthealth/breast-density-mode", + "version": "0.1.2" } ], "public": [ diff --git a/platform/app/public/assets/favicon.ico b/platform/app/public/assets/favicon.ico index faaa2cf9c10..411107554df 100644 Binary files a/platform/app/public/assets/favicon.ico and b/platform/app/public/assets/favicon.ico differ 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 00000000000..3df85c80080 Binary files /dev/null and b/platform/app/public/assets/loader.gif differ diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 88f7b872e12..8b7bd8b8a08 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -119,25 +119,24 @@ window.config = { // 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/customLogo.svg', - // className: 'w-8 h-8', - // } - // )) - // }, - // }, + 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', diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js new file mode 100644 index 00000000000..a4881fc5923 --- /dev/null +++ b/platform/app/public/config/gradient.js @@ -0,0 +1,229 @@ +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: false, + 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_write', + '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=', + 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', + requestTransferSyntaxUID: '*' + }, + },*/ + { + 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, + requestTransferSyntaxUID: '*' + } + }, + { + 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/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"] + }] +}; diff --git a/platform/app/public/html-templates/index.html b/platform/app/public/html-templates/index.html index dd61a8af9b2..948f242e484 100644 --- a/platform/app/public/html-templates/index.html +++ b/platform/app/public/html-templates/index.html @@ -200,7 +200,7 @@ src="<%= PUBLIC_URL %>init-service-worker.js" > - 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/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); }); }); 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/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js index b4c6098936c..1292df0dbc3 100644 --- a/platform/core/src/defaults/hotkeyBindings.js +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -173,6 +173,20 @@ const bindings = [ label: 'W/L Preset 5', keys: ['5'], }, + { + commandName: 'setToolActive', + commandOptions: { toolName: 'CircularBrush' }, + label: 'Segmentation Brush', + keys: ['b'], + isEditable: true, + }, + { + commandName: 'setToolActive', + commandOptions: { toolName: 'CircularEraser' }, + label: 'Segmentation Eraser', + keys: ['e'], + isEditable: true, + }, // These don't exist, so don't try applying them.... // { // commandName: 'setWindowLevel', 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/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 +
diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx index ff909483a10..189506271d9 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx @@ -4,35 +4,31 @@ import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; function SegmentationDropDownRow({ - segmentations = [], - activeSegmentation, - onActiveSegmentationChange, + segmentation, + savedStatusState, + 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; } return ( -
+
{ e.stopPropagation(); @@ -46,12 +42,12 @@ function SegmentationDropDownRow({ showBorders={false} maxCharactersPerLine={30} list={[ - ...(!disableEditing + ...(!disableEditing && showAddSegment ? [ { - title: t('Add new segmentation'), + title: t('Add new segment'), onClick: () => { - onSegmentationAdd(); + onSegmentAdd(segmentation.id); }, }, ] @@ -61,7 +57,7 @@ function SegmentationDropDownRow({ { title: t('Rename'), onClick: () => { - onSegmentationEdit(activeSegmentation.id); + onSegmentationEdit(segmentation.id); }, }, ] @@ -69,15 +65,15 @@ function SegmentationDropDownRow({ { title: t('Delete'), onClick: () => { - onSegmentationDelete(activeSegmentation.id); + onSegmentationDelete(segmentation.id); }, }, ...(!disableEditing ? [ { - title: t('Export DICOM SEG'), + title: t('Save'), onClick: () => { - storeSegmentation(activeSegmentation.id); + storeSegmentation(segmentation.id); }, }, ] @@ -86,15 +82,15 @@ 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 +99,28 @@ function SegmentationDropDownRow({
- {selectOptions?.length && ( -