From 04a85c1818aacfdb8825340de278ec6b19097662 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Tue, 2 Jan 2024 18:46:00 +0530 Subject: [PATCH 01/25] Added formpanel to segmentation mode and added form change listener in studybrowser panel --- .../default/src/Panels/PanelStudyBrowser.tsx | 42 +++++++++++++++---- modes/segmentation/src/index.tsx | 18 +++++++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/extensions/default/src/Panels/PanelStudyBrowser.tsx b/extensions/default/src/Panels/PanelStudyBrowser.tsx index e1b65d40197..fe9b5fec5a6 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.tsx @@ -17,7 +17,7 @@ function PanelStudyBrowser({ requestDisplaySetCreationForStudy, dataSource, }) { - const { hangingProtocolService, displaySetService, uiNotificationService } = + const { hangingProtocolService, displaySetService, uiNotificationService, GoogleSheetsService } = servicesManager.services; const navigate = useNavigate(); @@ -25,10 +25,11 @@ function PanelStudyBrowser({ // doesn't have to have such an intense shape. This works well enough for now. // Tabs --> Studies --> DisplaySets --> Thumbnails const { StudyInstanceUIDs } = useImageViewer(); + const [studyInstanceUIDs, setStudyInstanceUIDs] = useState([...StudyInstanceUIDs]); const [{ activeViewportId, viewports }, viewportGridService] = useViewportGrid(); const [activeTabName, setActiveTabName] = useState('primary'); const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ - ...StudyInstanceUIDs, + ...studyInstanceUIDs, ]); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); @@ -55,6 +56,11 @@ function PanelStudyBrowser({ viewportGridService.setDisplaySetsForViewports(updatedViewports); }; + useEffect(() => { + setStudyInstanceUIDs([...StudyInstanceUIDs]); + setExpandedStudyInstanceUIDs([...StudyInstanceUIDs]); + }, [StudyInstanceUIDs]); + // ~~ studyDisplayList useEffect(() => { // Fetch all studies for the patient in each primary study @@ -101,8 +107,8 @@ function PanelStudyBrowser({ }); } - StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); - }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); + studyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); + }, [studyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); // // ~~ Initial Thumbnails useEffect(() => { @@ -124,7 +130,7 @@ function PanelStudyBrowser({ return { ...prevState, ...newImageSrcEntry }; }); }); - }, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]); + }, [studyInstanceUIDs, dataSource, displaySetService, getImageSrc]); // ~~ displaySets useEffect(() => { @@ -134,7 +140,7 @@ function PanelStudyBrowser({ sortStudyInstances(mappedDisplaySets); setDisplaySets(mappedDisplaySets); - }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + }, [studyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); // ~~ subscriptions --> displaySets useEffect(() => { @@ -202,9 +208,29 @@ function PanelStudyBrowser({ SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; - }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + }, [studyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + + useEffect(() => { + const { unsubscribe } = GoogleSheetsService.subscribe( + GoogleSheetsService.EVENTS.GOOGLE_SHEETS_CHANGE, + () => { + const newStudyInstanceUID = Object.entries(GoogleSheetsService.studyUIDToIndex).filter( + ([key, val]) => val === GoogleSheetsService.index + )[0][0]; + + if (!studyInstanceUIDs.includes(newStudyInstanceUID)) { + setStudyInstanceUIDs([newStudyInstanceUID]); + setExpandedStudyInstanceUIDs([newStudyInstanceUID]); + } + } + ); + + return () => { + unsubscribe(); + }; + }); - const tabs = _createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); + const tabs = _createStudyBrowserTabs(studyInstanceUIDs, studyDisplayList, displaySets); // TODO: Should not fire this on "close" function _handleStudyClick(StudyInstanceUID) { diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 8b24196f161..b0036a0f7eb 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -22,6 +22,10 @@ const segmentation = { viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', }; +const gradienthealth = { + form: '@gradienthealth/ohif-gradienthealth-extension.panelModule.form', +}; + /** * Just two dependencies to be able to render a viewport with panels in order * to make sure that the mode is working. @@ -50,7 +54,14 @@ 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(); @@ -86,6 +97,9 @@ function modeFactory({ modeConfiguration }) { activateTool )); + GoogleSheetsService.init(); + CropDisplayAreaService.init(); + CacheAPIService.init(); toolbarService.init(extensionManager); toolbarService.addButtons(toolbarButtons); toolbarService.createButtonSection('primary', [ @@ -153,7 +167,7 @@ function modeFactory({ modeConfiguration }) { id: ohif.layout, props: { leftPanels: [ohif.leftPanel], - rightPanels: [segmentation.panelTool], + rightPanels: [segmentation.panelTool, gradienthealth.form], viewports: [ { namespace: cornerstone.viewport, From c232176d0e21f9f8f7f2d3c5e77bcd6d21b5cca7 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 10 Jan 2024 17:28:04 +0530 Subject: [PATCH 02/25] Jump to referenced displaySet if segmentation is selected. ALso changed the segmentation mode template panel from ohif to gradienthealth custom one. --- .../src/panels/PanelSegmentation.tsx | 34 ++++++++++++++- .../default/src/Panels/PanelStudyBrowser.tsx | 42 ++++--------------- modes/segmentation/src/index.tsx | 11 ++++- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 1b4c3a2d27d..50aa5adce6c 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -13,7 +13,13 @@ export default function PanelSegmentation({ extensionManager, configuration, }) { - const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services; + const { + segmentationService, + viewportGridService, + uiDialogService, + displaySetService, + userAuthenticationService, + } = servicesManager.services; const { t } = useTranslation('PanelSegmentation'); @@ -48,6 +54,8 @@ export default function PanelSegmentation({ }, []); const setSegmentationActive = segmentationId => { + setReferencedDisplaySet(segmentationId); + const isSegmentationActive = segmentations.find(seg => seg.id === segmentationId)?.isActive; if (isSegmentationActive) { @@ -57,6 +65,28 @@ export default function PanelSegmentation({ segmentationService.setActiveSegmentationForToolGroup(segmentationId); }; + // 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); + const referencedDisplaySetInstancesUID = segDisplayset.referencedDisplaySetInstanceUID; + const { viewports, activeViewportId } = viewportGridService.getState(); + let referencedImageLoaded = false; + viewports.forEach(viewport => { + if (viewport.displaySetInstanceUIDs.includes(referencedDisplaySetInstancesUID)) { + referencedImageLoaded = true; + } + }); + + if (!referencedImageLoaded) { + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [referencedDisplaySetInstancesUID, segmentationId], + }); + segDisplayset.load({ headers: userAuthenticationService.getAuthorizationHeader() }); + } + }; + const getToolGroupIds = segmentationId => { const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId); @@ -68,6 +98,7 @@ export default function PanelSegmentation({ }; const onSegmentationClick = (segmentationId: string) => { + setReferencedDisplaySet(segmentationId); segmentationService.setActiveSegmentationForToolGroup(segmentationId); }; @@ -82,6 +113,7 @@ export default function PanelSegmentation({ }; const onSegmentClick = (segmentationId, segmentIndex) => { + setReferencedDisplaySet(segmentationId); segmentationService.setActiveSegment(segmentationId, segmentIndex); const toolGroupIds = getToolGroupIds(segmentationId); diff --git a/extensions/default/src/Panels/PanelStudyBrowser.tsx b/extensions/default/src/Panels/PanelStudyBrowser.tsx index fe9b5fec5a6..e1b65d40197 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.tsx @@ -17,7 +17,7 @@ function PanelStudyBrowser({ requestDisplaySetCreationForStudy, dataSource, }) { - const { hangingProtocolService, displaySetService, uiNotificationService, GoogleSheetsService } = + const { hangingProtocolService, displaySetService, uiNotificationService } = servicesManager.services; const navigate = useNavigate(); @@ -25,11 +25,10 @@ function PanelStudyBrowser({ // doesn't have to have such an intense shape. This works well enough for now. // Tabs --> Studies --> DisplaySets --> Thumbnails const { StudyInstanceUIDs } = useImageViewer(); - const [studyInstanceUIDs, setStudyInstanceUIDs] = useState([...StudyInstanceUIDs]); const [{ activeViewportId, viewports }, viewportGridService] = useViewportGrid(); const [activeTabName, setActiveTabName] = useState('primary'); const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ - ...studyInstanceUIDs, + ...StudyInstanceUIDs, ]); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); @@ -56,11 +55,6 @@ function PanelStudyBrowser({ viewportGridService.setDisplaySetsForViewports(updatedViewports); }; - useEffect(() => { - setStudyInstanceUIDs([...StudyInstanceUIDs]); - setExpandedStudyInstanceUIDs([...StudyInstanceUIDs]); - }, [StudyInstanceUIDs]); - // ~~ studyDisplayList useEffect(() => { // Fetch all studies for the patient in each primary study @@ -107,8 +101,8 @@ function PanelStudyBrowser({ }); } - studyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); - }, [studyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); + StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); + }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); // // ~~ Initial Thumbnails useEffect(() => { @@ -130,7 +124,7 @@ function PanelStudyBrowser({ return { ...prevState, ...newImageSrcEntry }; }); }); - }, [studyInstanceUIDs, dataSource, displaySetService, getImageSrc]); + }, [StudyInstanceUIDs, dataSource, displaySetService, getImageSrc]); // ~~ displaySets useEffect(() => { @@ -140,7 +134,7 @@ function PanelStudyBrowser({ sortStudyInstances(mappedDisplaySets); setDisplaySets(mappedDisplaySets); - }, [studyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); // ~~ subscriptions --> displaySets useEffect(() => { @@ -208,29 +202,9 @@ function PanelStudyBrowser({ SubscriptionDisplaySetsChanged.unsubscribe(); SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; - }, [studyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); - - useEffect(() => { - const { unsubscribe } = GoogleSheetsService.subscribe( - GoogleSheetsService.EVENTS.GOOGLE_SHEETS_CHANGE, - () => { - const newStudyInstanceUID = Object.entries(GoogleSheetsService.studyUIDToIndex).filter( - ([key, val]) => val === GoogleSheetsService.index - )[0][0]; - - if (!studyInstanceUIDs.includes(newStudyInstanceUID)) { - setStudyInstanceUIDs([newStudyInstanceUID]); - setExpandedStudyInstanceUIDs([newStudyInstanceUID]); - } - } - ); - - return () => { - unsubscribe(); - }; - }); + }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); - const tabs = _createStudyBrowserTabs(studyInstanceUIDs, studyDisplayList, displaySets); + const tabs = _createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); // TODO: Should not fire this on "close" function _handleStudyClick(StudyInstanceUID) { diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index b0036a0f7eb..89d8162f703 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -24,6 +24,8 @@ const segmentation = { const gradienthealth = { form: '@gradienthealth/ohif-gradienthealth-extension.panelModule.form', + thumbnailList: + '@gradienthealth/ohif-gradienthealth-extension.panelModule.seriesList-without-tracking', }; /** @@ -163,11 +165,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, gradienthealth.form], + leftPanels: [gradienthealth.thumbnailList], + rightPanels: rightPanels, viewports: [ { namespace: cornerstone.viewport, From 9bd8ac3b63e237ceabd76ce4a19b8a6b4df03f60 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 10 Jan 2024 17:31:53 +0530 Subject: [PATCH 03/25] Removed stack segmentation cache when segmentation is removed. Included in the stack segmentation feature --- .../SegmentationService/SegmentationService.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 2b9707c5b92..30bf5b34024 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -2110,6 +2110,16 @@ class SegmentationService extends PubSubService { if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) { 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) + ); + } } private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) { From 9ae0a9d5225e35f832f0c27ccad71161910094de Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 10 Jan 2024 17:51:38 +0530 Subject: [PATCH 04/25] Fixed the colorLUT index issue. This Issue was found in the upstream. --- .../SegmentationService/SegmentationService.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 30bf5b34024..92c55541287 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -471,11 +471,15 @@ class SegmentationService extends PubSubService { // if first segmentation, we can use the default colorLUT, otherwise // we need to generate a new one and use a new colorLUT - const colorLUTIndex = 0; + let colorLUTIndex = 0; + const newColorLUT = this.generateNewColorLUT(); if (Object.keys(this.segmentations).length !== 0) { - const newColorLUT = this.generateNewColorLUT(); - const colorLUTIndex = this.getNextColorLUTIndex(); + colorLUTIndex = this.getNextColorLUTIndex(); cstSegmentation.config.color.addColorLUT(newColorLUT, colorLUTIndex); + } else if (!cstSegmentation.state.getColorLUT(0)) { + // If all loaded segmentations are removed and a new one is adding there may + // not be a colorLUT for index 0 as all of them should have been removed. + cstSegmentation.config.color.addColorLUT(newColorLUT, 0); } this.segmentations[segmentationId] = { From 4fb2196ae9fb8f3ae8a30d28f39baba98ffffbda Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 12 Jan 2024 19:17:50 +0530 Subject: [PATCH 05/25] Deployed to production --- .github/workflows/deploy_ghpages.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml index ba1cbdf78e6..8fbc930dba3 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/Stack-Segmentation" ] + branches: [ "gradienthealth/segmentation_mode_sheet_integration" ] #pull_request: #branches: [ "main" ] @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3 with: repository: gradienthealth/cornerstone3D-beta - ref: gradienthealth/stack-segmentation-support-with-zip-image-loader + ref: gradient_rebase_12212023 path: ./cornerstone3D - name: Build cornerstone3D @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v3 with: repository: gradienthealth/GradientExtensionsAndModes - ref: gradienthealth/Segmentation-with-DicomJSON + ref: gradienthealth/segmentation_mode_sheet_integration path: ./GradientExtensionsAndModes #- name: Build GradientExtensionsAndModes From 09ac407be2e8b50550e8ff8670ed3ab5dfaba7de Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 12 Jan 2024 22:55:39 +0530 Subject: [PATCH 06/25] Skipped CORS and segment default color usage warnings and reorganized segmentation drop down menu. --- .../cornerstone-dicom-seg/src/getSopClassHandlerModule.js | 3 ++- .../cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx | 4 ++++ platform/app/public/config/gradient.js | 2 +- .../SegmentationGroupTable/SegmentationDropDownRow.tsx | 6 +++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js index 8a3d095e9ed..a0043fefcda 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js @@ -192,6 +192,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({ @@ -201,7 +202,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 50aa5adce6c..f76ab9585cb 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -69,6 +69,10 @@ export default function PanelSegmentation({ // if it is not displayed in any of the viewports. const setReferencedDisplaySet = segmentationId => { const segDisplayset = displaySetService.getDisplaySetByUID(segmentationId); + if (!segDisplayset) { + return; + } + const referencedDisplaySetInstancesUID = segDisplayset.referencedDisplaySetInstanceUID; const { viewports, activeViewportId } = viewportGridService.getState(); let referencedImageLoaded = false; diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js index 0410534bf7b..a4881fc5923 100644 --- a/platform/app/public/config/gradient.js +++ b/platform/app/public/config/gradient.js @@ -13,7 +13,7 @@ window.config = { // below flag is for performance reasons, but it might not work for all servers omitQuotationForMultipartRequest: true, - showWarningMessageForCrossOrigin: true, + showWarningMessageForCrossOrigin: false, showCPUFallbackMessage: true, showLoadingIndicator: true, strictZSpacingForVolumeViewport: true, diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx index 25a3f88857f..6ee2bc1c83c 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx @@ -81,7 +81,7 @@ function SegmentationDropDownRow({ ...(!disableEditing ? [ { - title: t('Export DICOM SEG'), + title: t('Save'), onClick: () => { storeSegmentation(segmentation.id); }, @@ -95,12 +95,12 @@ function SegmentationDropDownRow({ onSegmentationDownload(segmentation.id); }, }, - { + /*{ title: t('Download DICOM RTSTRUCT'), onClick: () => { onSegmentationDownloadRTSS(segmentation.id); }, - }, + },*/ ], ]} > From 9a435f04d46ad0fec838a5a6ecda3f342a503f27 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Tue, 16 Jan 2024 17:31:02 +0530 Subject: [PATCH 07/25] save to the same segmentation file if modified. --- .../src/commandsModule.ts | 32 ++++++++++++------- .../default/src/Actions/createReportAsync.tsx | 23 ++++++++++++- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 98d8a241fd3..33a4e61ed1a 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -319,27 +319,39 @@ const commandsModule = ({ * otherwise throws an error. */ storeSegmentation: async ({ segmentationId, dataSource }) => { - const promptResult = await createReportDialogPrompt(uiDialogService, { - extensionManager, - }); - - if (promptResult.action !== 1 && promptResult.value) { - return; - } - 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 (!shouldOverWrite) { + promptResult = await createReportDialogPrompt(uiDialogService, { + extensionManager, + }); + + if (promptResult.action !== 1 && promptResult.value) { + return; + } + } - const { label } = segmentation; const SeriesDescription = promptResult.value || label || 'Research Derived Series'; const generatedData = actions.generateSegmentation({ segmentationId, options: { SeriesDescription, + // Use Series and SOP instancesUIDs if displaySet of the segmentation already exists. + ...(shouldOverWrite && { + SeriesInstanceUID: displaySet.SeriesInstanceUID, + SOPInstanceUID: displaySet.instances[0].SOPInstanceUID, + }), }, }); @@ -358,8 +370,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/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx index d0f34a48a23..8b54c772684 100644 --- a/extensions/default/src/Actions/createReportAsync.tsx +++ b/extensions/default/src/Actions/createReportAsync.tsx @@ -1,6 +1,10 @@ import React from 'react'; +import dcmjs from 'dcmjs'; +import { wadouri } from '@cornerstonejs/dicom-image-loader'; import { DicomMetadataStore } from '@ohif/core'; +const { datasetToBlob } = dcmjs.data; + /** * * @param {*} servicesManager @@ -17,12 +21,22 @@ async function createReportAsync({ servicesManager, getReport, reportType = 'mea 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; @@ -32,6 +46,13 @@ async function createReportAsync({ servicesManager, getReport, reportType = 'mea type: 'success', }); + if (shouldOverWrite) { + const fileUri = wadouri.fileManager.add(datasetToBlob(naturalizedReport)); + displaySet.instance.imageId = fileUri; + displaySet.instance.getImageId = () => fileUri; + return; + } + return [displaySetInstanceUID]; } catch (error) { uiNotificationService.show({ From 676347d401c75ee42736a8ce78eb088b06dd154f Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 17 Jan 2024 15:27:16 +0530 Subject: [PATCH 08/25] Changed the min size of segmentation brush and eraser to 0.01 --- .../src/panels/SegmentationToolbox.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx index f2d580cff46..e228e5c9697 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx @@ -249,10 +249,10 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { name: 'Radius (mm)', id: 'brush-radius', type: 'range', - min: 0.5, + min: 0.01, max: 99.5, value: state.Brush.brushSize, - step: 0.5, + step: 0.01, onChange: value => onBrushSizeChange(value, 'Brush'), }, { @@ -281,10 +281,10 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { name: 'Radius (mm)', type: 'range', id: 'eraser-radius', - min: 0.5, + min: 0.01, max: 99.5, value: state.Eraser.brushSize, - step: 0.5, + step: 0.01, onChange: value => onBrushSizeChange(value, 'Eraser'), }, { From bb31c0f9778bebfa02b7931ba3aa40ea14aa539b Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 17 Jan 2024 15:49:11 +0530 Subject: [PATCH 09/25] Removed the old segmentation file from fileManager when saving to same segmentation file. --- extensions/default/src/Actions/createReportAsync.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/extensions/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx index 8b54c772684..253ff8ee5ba 100644 --- a/extensions/default/src/Actions/createReportAsync.tsx +++ b/extensions/default/src/Actions/createReportAsync.tsx @@ -1,6 +1,5 @@ import React from 'react'; import dcmjs from 'dcmjs'; -import { wadouri } from '@cornerstonejs/dicom-image-loader'; import { DicomMetadataStore } from '@ohif/core'; const { datasetToBlob } = dcmjs.data; @@ -10,7 +9,8 @@ const { datasetToBlob } = dcmjs.data; * @param {*} servicesManager */ async function createReportAsync({ servicesManager, getReport, reportType = 'measurement' }) { - const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services; + const { displaySetService, uiNotificationService, uiDialogService, CacheAPIService } = + servicesManager.services; const loadingDialogId = uiDialogService.create({ showOverlay: true, isDraggable: false, @@ -47,9 +47,7 @@ async function createReportAsync({ servicesManager, getReport, reportType = 'mea }); if (shouldOverWrite) { - const fileUri = wadouri.fileManager.add(datasetToBlob(naturalizedReport)); - displaySet.instance.imageId = fileUri; - displaySet.instance.getImageId = () => fileUri; + CacheAPIService?.updateCachedFile(datasetToBlob(naturalizedReport), displaySet); return; } From 590da1a0515f6d0e45d97fcd637a432674a699ee Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 17 Jan 2024 17:48:07 +0530 Subject: [PATCH 10/25] updated the cornerstone deployment branch --- .github/workflows/deploy_ghpages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml index 8fbc930dba3..d9a36ff5f31 100644 --- a/.github/workflows/deploy_ghpages.yml +++ b/.github/workflows/deploy_ghpages.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3 with: repository: gradienthealth/cornerstone3D-beta - ref: gradient_rebase_12212023 + ref: gradienthealth/segmentation_mode_sheet_integration path: ./cornerstone3D - name: Build cornerstone3D From 54428b6faacd06800f9359f01c4a95dfe71d1432 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 2 Feb 2024 20:44:25 +0530 Subject: [PATCH 11/25] Implemented segment focus, modified segment brush unit and default size, added auto saving status icon, added auto segment and label naming. --- .../src/commandsModule.ts | 6 +- .../src/panels/PanelSegmentation.tsx | 125 +++++++++++++++--- .../src/panels/SegmentationToolbox.tsx | 58 ++++++-- .../src/utils/getSegmentLabel.ts | 8 ++ .../SegmentationService.ts | 10 +- .../src/utils/createImageDataForStackImage.ts | 11 +- .../default/src/Actions/createReportAsync.tsx | 57 +++++--- .../SegmentationDropDownRow.tsx | 31 ++--- .../SegmentationGroup.tsx | 8 +- .../SegmentationGroupSegment.tsx | 11 ++ .../SegmentationGroupTable.tsx | 6 + 11 files changed, 255 insertions(+), 76 deletions(-) create mode 100644 extensions/cornerstone-dicom-seg/src/utils/getSegmentLabel.ts diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 33a4e61ed1a..27de8e92aba 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -19,6 +19,7 @@ import { getTargetViewport, } from './utils/hydrationUtils'; import generateLabelmaps2DFromImageIdMap from './utils/generateLabelmaps2DFromImageIdMap'; +import getSegmentLabel from './utils/getSegmentLabel'; const { datasetToBlob } = dcmjs.data; @@ -100,7 +101,7 @@ const commandsModule = ({ toolGroupId, segmentIndex: 1, properties: { - label: 'Segment 1', + label: getSegmentLabel(segmentationService.getSegmentation(segmentationId)), }, }); @@ -336,12 +337,13 @@ const commandsModule = ({ extensionManager, }); - if (promptResult.action !== 1 && promptResult.value) { + if (promptResult.action !== 1 && !promptResult.value) { return; } } const SeriesDescription = promptResult.value || label || 'Research Derived Series'; + segmentation.label = SeriesDescription; const generatedData = actions.generateSegmentation({ segmentationId, diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index f76ab9585cb..9c0cf38ebad 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, LegacyButtonGroup, LegacyButton } from '@ohif/ui'; 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', +}; export default function PanelSegmentation({ servicesManager, @@ -19,6 +33,7 @@ export default function PanelSegmentation({ uiDialogService, displaySetService, userAuthenticationService, + CropDisplayAreaService, } = servicesManager.services; const { t } = useTranslation('PanelSegmentation'); @@ -29,6 +44,7 @@ export default function PanelSegmentation({ ); const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); + const [savedStatusStates, dispatch] = useReducer(savedStatusReducer, {}); useEffect(() => { // ~~ Subscription @@ -53,6 +69,74 @@ export default function PanelSegmentation({ }; }, []); + useEffect(() => { + 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], + }), + 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); + } + ); + + return () => { + unsubscribe(); + }; + }, []); + const setSegmentationActive = segmentationId => { setReferencedDisplaySet(segmentationId); @@ -73,11 +157,11 @@ export default function PanelSegmentation({ return; } - const referencedDisplaySetInstancesUID = segDisplayset.referencedDisplaySetInstanceUID; + const referencedDisplaySetInstanceUID = segDisplayset.referencedDisplaySetInstanceUID; const { viewports, activeViewportId } = viewportGridService.getState(); let referencedImageLoaded = false; viewports.forEach(viewport => { - if (viewport.displaySetInstanceUIDs.includes(referencedDisplaySetInstancesUID)) { + if (viewport.displaySetInstanceUIDs.includes(referencedDisplaySetInstanceUID)) { referencedImageLoaded = true; } }); @@ -85,9 +169,8 @@ export default function PanelSegmentation({ if (!referencedImageLoaded) { viewportGridService.setDisplaySetsForViewport({ viewportId: activeViewportId, - displaySetInstanceUIDs: [referencedDisplaySetInstancesUID, segmentationId], + displaySetInstanceUIDs: [referencedDisplaySetInstanceUID], }); - segDisplayset.load({ headers: userAuthenticationService.getAuthorizationHeader() }); } }; @@ -113,7 +196,8 @@ export default function PanelSegmentation({ const onSegmentAdd = segmentationId => { setSegmentationActive(segmentationId); - segmentationService.addSegment(segmentationId); + const label = getSegmentLabel(segmentations.find(seg => seg.id === segmentationId)); + segmentationService.addSegment(segmentationId, { properties: { label } }); }; const onSegmentClick = (segmentationId, segmentIndex) => { @@ -246,16 +330,25 @@ export default function PanelSegmentation({ 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) { @@ -284,6 +377,7 @@ export default function PanelSegmentation({ _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) } + CropDisplayAreaService={CropDisplayAreaService} /> diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx index e228e5c9697..750fecb5d1e 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState, useReducer } from 'react'; import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui'; import { Types } from '@ohif/extension-cornerstone'; import { utilities } from '@cornerstonejs/tools'; +import { EVENTS, eventTarget } from '@cornerstonejs/core'; const { segmentation: segmentationUtils } = utilities; @@ -105,6 +106,32 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { [toolbarService, dispatch] ); + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const defaultBrushSize = params.get('defaultBrushSize'); + const toolCategories = ['Brush', 'Eraser']; + + const elementEnabledHandler = evt => { + const setDefaultBrushSize = () => { + toolCategories.forEach(toolCategory => { + onBrushSizeChange(defaultBrushSize, toolCategory); + }); + + evt.detail.element.removeEventListener( + EVENTS.VOLUME_VIEWPORT_NEW_VOLUME, + setDefaultBrushSize + ); + eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, setDefaultBrushSize); + }; + + evt.detail.element.addEventListener(EVENTS.VOLUME_VIEWPORT_NEW_VOLUME, setDefaultBrushSize); + eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, setDefaultBrushSize); + eventTarget.removeEventListener(EVENTS.ELEMENT_ENABLED, setDefaultBrushSize); + }; + + eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler); + }, []); + /** * sets the tools enabled IF there are segmentations */ @@ -183,7 +210,11 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { const value = Number(valueAsStringOrNumber); _getToolNamesFromCategory(toolCategory).forEach(toolName => { - updateBrushSize(toolName, value); + const convertedValue = + toolCategory === 'Brush' || toolCategory === 'Eraser' + ? convertPixelToMM(value, servicesManager) + : value; + updateBrushSize(toolName, convertedValue); }); dispatch({ @@ -246,13 +277,13 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH), options: [ { - name: 'Radius (mm)', + name: 'Radius (px)', id: 'brush-radius', type: 'range', - min: 0.01, - max: 99.5, + min: 0.5, + max: 10000, value: state.Brush.brushSize, - step: 0.01, + step: 0.5, onChange: value => onBrushSizeChange(value, 'Brush'), }, { @@ -278,13 +309,13 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER), options: [ { - name: 'Radius (mm)', + name: 'Radius (px)', type: 'range', id: 'eraser-radius', - min: 0.01, - max: 99.5, + min: 0.5, + max: 10000, value: state.Eraser.brushSize, - step: 0.01, + step: 0.5, onChange: value => onBrushSizeChange(value, 'Eraser'), }, { @@ -402,4 +433,13 @@ function _getToolNamesFromCategory(category) { return toolNames; } +function convertPixelToMM(value, servicesManager) { + const { viewportGridService, cornerstoneViewportService } = servicesManager.services; + const { activeViewportId } = viewportGridService.getState(); + const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId); + const { spacing } = viewport.getImageData(); + + return Math.min(value * spacing[0], value * spacing[1], value * spacing[2]); +} + export default SegmentationToolbox; 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/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 92c55541287..0fd41b11d69 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -874,15 +874,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); } diff --git a/extensions/cornerstone/src/utils/createImageDataForStackImage.ts b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts index 74fa35d1e57..efa4150efd1 100644 --- a/extensions/cornerstone/src/utils/createImageDataForStackImage.ts +++ b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts @@ -1,4 +1,4 @@ -import { vec3 } from 'gl-matrix'; +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'; @@ -16,11 +16,11 @@ export default function createImageDataForStackImage(imageIdReferenceMap: Map { - if (segmentation.id === activeSegmentationId) { - onToggleShowSegments(!showSegments); - } else { - onSegmentationClick(segmentation.id); - - if (!showSegments) { - onToggleShowSegments(true); - } - } - }; - return (
-
+
onSegmentationClick(segmentation.id)} > {segmentation.label} +
onToggleSegmentationVisibility(segmentation.id)} > {segmentation.isVisible ? ( @@ -132,7 +125,10 @@ function SegmentationDropDownRow({ /> )}
-
+
onToggleShowSegments(showSegments => !showSegments)} + > {showSegments ? ( { const [showSegments, toggleShowSegments] = useState(true); @@ -35,6 +37,7 @@ const SegmentationGroup = ({
{showSegments && ( -
+
{segmentation?.segments?.map(segment => { if (!segment) { return null; @@ -82,6 +85,7 @@ const SegmentationGroup = ({ onColor={onSegmentColorClick} onToggleVisibility={onToggleSegmentVisibility} onToggleLocked={onToggleSegmentLock} + CropDisplayAreaService={CropDisplayAreaService} />
); @@ -107,6 +111,7 @@ SegmentationGroup.propTypes = { }) ), }), + savedStatusState: PropTypes.string, activeSegmentationId: PropTypes.string, disableEditing: PropTypes.bool, showAddSegment: PropTypes.bool, @@ -125,6 +130,7 @@ SegmentationGroup.propTypes = { onToggleSegmentVisibility: PropTypes.func.isRequired, onToggleSegmentLock: PropTypes.func.isRequired, onSegmentColorClick: PropTypes.func.isRequired, + CropDisplayAreaService: PropTypes.any, }; export default SegmentationGroup; diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx index 677f3b1246a..c404208cb63 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx @@ -20,9 +20,16 @@ const SegmentItem = ({ onColor, onToggleVisibility, onToggleLocked, + CropDisplayAreaService, }) => { const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false); + const onFocusClick = (segmentationId, segmentIndex) => { + CropDisplayAreaService.focusToSegment(segmentationId, segmentIndex).then(() => + onClick(segmentationId, segmentIndex) + ); + }; + const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`; return ( @@ -139,6 +146,7 @@ const SegmentItem = ({ onToggleVisibility={onToggleVisibility} segmentationId={segmentationId} segmentIndex={segmentIndex} + onFocusClick={onFocusClick} />
@@ -156,6 +164,7 @@ const HoveringIcons = ({ onToggleVisibility, segmentationId, segmentIndex, + onFocusClick, }) => { const iconClass = 'w-5 h-5 hover:cursor-pointer hover:opacity-60'; @@ -174,6 +183,7 @@ const HoveringIcons = ({ return (
+ {createIcon('tool-zoom', onFocusClick)} {!disableEditing && createIcon('row-edit', onEdit)} {!disableEditing && createIcon( @@ -205,6 +215,7 @@ SegmentItem.propTypes = { onDelete: PropTypes.func.isRequired, onToggleVisibility: PropTypes.func.isRequired, onToggleLocked: PropTypes.func, + CropDisplayAreaService: PropTypes.any, }; SegmentItem.defaultProps = { diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx index e3647081cb2..f5ca8b8b3a9 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx @@ -9,6 +9,7 @@ import SegmentationGroup from './SegmentationGroup'; const SegmentationGroupTable = ({ segmentations, + savedStatusStates, // segmentation initial config segmentationConfig, // UI show/hide @@ -41,6 +42,7 @@ const SegmentationGroupTable = ({ setRenderFill, setRenderInactiveSegmentations, setRenderOutline, + CropDisplayAreaService, }) => { const [isConfigOpen, setIsConfigOpen] = useState(false); const [activeSegmentationId, setActiveSegmentationId] = useState(null); @@ -109,6 +111,7 @@ const SegmentationGroupTable = ({ key={segmentation.id} activeSegmentationId={activeSegmentationId} segmentation={segmentation} + savedStatusState={savedStatusStates[segmentation.id]} disableEditing={disableEditing} showAddSegment={showAddSegment} onSegmentationClick={onSegmentationClick} @@ -126,6 +129,7 @@ const SegmentationGroupTable = ({ onToggleSegmentLock={onToggleSegmentLock} onSegmentColorClick={onSegmentColorClick} showDeleteSegment={showDeleteSegment} + CropDisplayAreaService={CropDisplayAreaService} /> )) )} @@ -151,6 +155,7 @@ SegmentationGroupTable.propTypes = { ), }) ), + savedStatusStates: PropTypes.object, segmentationConfig: PropTypes.object.isRequired, disableEditing: PropTypes.bool, showAddSegmentation: PropTypes.bool, @@ -178,6 +183,7 @@ SegmentationGroupTable.propTypes = { setRenderFill: PropTypes.func.isRequired, setRenderInactiveSegmentations: PropTypes.func.isRequired, setRenderOutline: PropTypes.func.isRequired, + CropDisplayAreaService: PropTypes.any, }; SegmentationGroupTable.defaultProps = { From e790e42f2b2642be1fd273408ad506159372e2e0 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Mon, 5 Feb 2024 11:30:50 +0530 Subject: [PATCH 12/25] Skiped label dialog in auto saving flow even for new segmentations. --- extensions/cornerstone-dicom-seg/src/commandsModule.ts | 4 ++-- .../cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 27de8e92aba..1748d24cbc1 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -319,7 +319,7 @@ const commandsModule = ({ * @returns {Object|void} Returns the naturalized report if successfully stored, * otherwise throws an error. */ - storeSegmentation: async ({ segmentationId, dataSource }) => { + storeSegmentation: async ({ segmentationId, dataSource, skipLabelDialog = false }) => { const segmentation = segmentationService.getSegmentation(segmentationId); if (!segmentation) { @@ -332,7 +332,7 @@ const commandsModule = ({ let promptResult: { action?: number; value?: string } = {}; - if (!shouldOverWrite) { + if (!(skipLabelDialog || shouldOverWrite)) { promptResult = await createReportDialogPrompt(uiDialogService, { extensionManager, }); diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 9c0cf38ebad..9104c3a15a0 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -98,6 +98,7 @@ export default function PanelSegmentation({ commandsManager.runCommand('storeSegmentation', { segmentationId: segmentation.id, dataSource: datasources[0], + skipLabelDialog: true, }), reportType: 'Segmentation', showLoadingModal: false, From 8659cf66ca48aad2f5aba8671bfa2ea4e6a7ee4d Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Thu, 22 Feb 2024 20:42:21 +0530 Subject: [PATCH 13/25] Made Min and max brush size configurable. Truncated segmentation label insegmentation panel and added tooltip. --- .../src/panels/SegmentationToolbox.tsx | 59 +++++++++++++------ .../generateLabelmaps2DFromImageIdMap.ts | 24 ++++---- .../SegmentationDropDownRow.tsx | 11 +++- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx index 750fecb5d1e..cb1eff3206e 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx @@ -70,6 +70,7 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { const [toolsEnabled, setToolsEnabled] = useState(false); const [state, dispatch] = useReducer(toolboxReducer, initialState); + const [brushProperties, setBrushProperties] = useState({ min: 1, max: 100, step: 1 }); const updateActiveTool = useCallback(() => { if (!viewports?.size || activeViewportId === undefined) { @@ -108,13 +109,35 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { useEffect(() => { const params = new URLSearchParams(window.location.search); - const defaultBrushSize = params.get('defaultBrushSize'); const toolCategories = ['Brush', 'Eraser']; const elementEnabledHandler = evt => { const setDefaultBrushSize = () => { + // Brush sizes are taken as radius, so taking half the value + const defaultBrushSize = (+params.get('defaultBrushSize') || 20) / 2; + const minBrushSize = (+params.get('minBrushSize') || 1) / 2; + const maxBrushSize = (+params.get('maxBrushSize') || 50) / 2; + + const defaultBrushSizeInMm = convertPixelToMM(defaultBrushSize, servicesManager); + let minBrushSizeInMm = convertPixelToMM(minBrushSize, servicesManager); + let maxBrushSizeInMm = convertPixelToMM(maxBrushSize, servicesManager); + const highestPixelSpacing = getPixelToMmConversionFactor(servicesManager); + const lowestBrushRadius = highestPixelSpacing / 2; + + if (minBrushSizeInMm < lowestBrushRadius) { + minBrushSizeInMm = lowestBrushRadius; + } + if (maxBrushSizeInMm < lowestBrushRadius) { + maxBrushSizeInMm = highestPixelSpacing; + } + + setBrushProperties({ + min: +minBrushSizeInMm.toFixed(2), + max: +maxBrushSizeInMm.toFixed(2), + step: +lowestBrushRadius.toFixed(2), + }); toolCategories.forEach(toolCategory => { - onBrushSizeChange(defaultBrushSize, toolCategory); + onBrushSizeChange(defaultBrushSizeInMm, toolCategory); }); evt.detail.element.removeEventListener( @@ -126,7 +149,7 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { evt.detail.element.addEventListener(EVENTS.VOLUME_VIEWPORT_NEW_VOLUME, setDefaultBrushSize); eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, setDefaultBrushSize); - eventTarget.removeEventListener(EVENTS.ELEMENT_ENABLED, setDefaultBrushSize); + eventTarget.removeEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler); }; eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler); @@ -210,11 +233,7 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { const value = Number(valueAsStringOrNumber); _getToolNamesFromCategory(toolCategory).forEach(toolName => { - const convertedValue = - toolCategory === 'Brush' || toolCategory === 'Eraser' - ? convertPixelToMM(value, servicesManager) - : value; - updateBrushSize(toolName, convertedValue); + updateBrushSize(toolName, value); }); dispatch({ @@ -277,13 +296,13 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH), options: [ { - name: 'Radius (px)', + name: 'Radius (mm)', id: 'brush-radius', type: 'range', - min: 0.5, - max: 10000, + min: brushProperties.min, + max: brushProperties.max, value: state.Brush.brushSize, - step: 0.5, + step: brushProperties.step, onChange: value => onBrushSizeChange(value, 'Brush'), }, { @@ -309,13 +328,13 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER), options: [ { - name: 'Radius (px)', + name: 'Radius (mm)', type: 'range', id: 'eraser-radius', - min: 0.5, - max: 10000, + min: brushProperties.min, + max: brushProperties.max, value: state.Eraser.brushSize, - step: 0.5, + step: brushProperties.step, onChange: value => onBrushSizeChange(value, 'Eraser'), }, { @@ -434,12 +453,16 @@ function _getToolNamesFromCategory(category) { } function convertPixelToMM(value, servicesManager) { + const conversionFactor = getPixelToMmConversionFactor(servicesManager); + return value * conversionFactor; +} + +function getPixelToMmConversionFactor(servicesManager) { const { viewportGridService, cornerstoneViewportService } = servicesManager.services; const { activeViewportId } = viewportGridService.getState(); const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId); const { spacing } = viewport.getImageData(); - - return Math.min(value * spacing[0], value * spacing[1], value * spacing[2]); + return Math.max(spacing[0], spacing[1]); } export default SegmentationToolbox; diff --git a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts index 14fffdae29a..2c739c9c438 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts @@ -19,18 +19,20 @@ const generateLabelmaps2DFromImageIdMap = imageIdReferenceMap => { } } - if (segmentsOnLabelmap.length) { - labelmaps2D[index] = { - segmentsOnLabelmap, - pixelData, - rows, - columns, - }; - - segmentsOnLabelmap.forEach(segmentIndex => { - segmentsOnLabelmap3D.add(segmentIndex); - }); + if (!segmentsOnLabelmap.length) { + segmentsOnLabelmap.push(1); } + + labelmaps2D[index] = { + segmentsOnLabelmap, + pixelData, + rows, + columns, + }; + + segmentsOnLabelmap.forEach(segmentIndex => { + segmentsOnLabelmap3D.add(segmentIndex); + }); }); const labelmapObj = { diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx index 6330a6a93ce..401a95b3505 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx @@ -99,13 +99,18 @@ function SegmentationDropDownRow({
onSegmentationClick(segmentation.id)} > - {segmentation.label} +
+ {segmentation.label} +
From e95d606a55ae0ed55438be8ff3c1819ea1226e3f Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 28 Feb 2024 13:21:49 +0530 Subject: [PATCH 14/25] Changed the configurable brush sizes unit to mm --- .../src/panels/SegmentationToolbox.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx index cb1eff3206e..1f59ccd0abd 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx @@ -113,14 +113,9 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { const elementEnabledHandler = evt => { const setDefaultBrushSize = () => { - // Brush sizes are taken as radius, so taking half the value - const defaultBrushSize = (+params.get('defaultBrushSize') || 20) / 2; - const minBrushSize = (+params.get('minBrushSize') || 1) / 2; - const maxBrushSize = (+params.get('maxBrushSize') || 50) / 2; - - const defaultBrushSizeInMm = convertPixelToMM(defaultBrushSize, servicesManager); - let minBrushSizeInMm = convertPixelToMM(minBrushSize, servicesManager); - let maxBrushSizeInMm = convertPixelToMM(maxBrushSize, servicesManager); + const defaultBrushSizeInMm = +params.get('defaultBrushSize') || 2; + let minBrushSizeInMm = +params.get('minBrushSize') || 2; + let maxBrushSizeInMm = +params.get('maxBrushSize') || 3; const highestPixelSpacing = getPixelToMmConversionFactor(servicesManager); const lowestBrushRadius = highestPixelSpacing / 2; @@ -134,7 +129,7 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { setBrushProperties({ min: +minBrushSizeInMm.toFixed(2), max: +maxBrushSizeInMm.toFixed(2), - step: +lowestBrushRadius.toFixed(2), + step: +((maxBrushSizeInMm - minBrushSizeInMm) / 100).toFixed(2), }); toolCategories.forEach(toolCategory => { onBrushSizeChange(defaultBrushSizeInMm, toolCategory); @@ -452,11 +447,6 @@ function _getToolNamesFromCategory(category) { return toolNames; } -function convertPixelToMM(value, servicesManager) { - const conversionFactor = getPixelToMmConversionFactor(servicesManager); - return value * conversionFactor; -} - function getPixelToMmConversionFactor(servicesManager) { const { viewportGridService, cornerstoneViewportService } = servicesManager.services; const { activeViewportId } = viewportGridService.getState(); From d2eca14ab3b7f6c89526034255e5de3d77c59734 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 1 Mar 2024 18:07:08 +0530 Subject: [PATCH 15/25] Fixed the issue of min/ max brush sizes not using configured values when the Toolbox component rerenders. --- .../src/panels/SegmentationToolbox.tsx | 85 +++++++++++++------ 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx index 1f59ccd0abd..e25e3045e42 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx @@ -25,11 +25,11 @@ const ACTIONS = { const initialState = { Brush: { - brushSize: 15, + brushSize: 2, mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush' }, Eraser: { - brushSize: 15, + brushSize: 2, mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser' }, Shapes: { @@ -70,7 +70,7 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { const [toolsEnabled, setToolsEnabled] = useState(false); const [state, dispatch] = useReducer(toolboxReducer, initialState); - const [brushProperties, setBrushProperties] = useState({ min: 1, max: 100, step: 1 }); + const [brushProperties, setBrushProperties] = useState({ min: 2, max: 3, step: 0.01 }); const updateActiveTool = useCallback(() => { if (!viewports?.size || activeViewportId === undefined) { @@ -107,33 +107,42 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { [toolbarService, dispatch] ); - useEffect(() => { + const setBrushSizesFromParams = () => { const params = new URLSearchParams(window.location.search); const toolCategories = ['Brush', 'Eraser']; + const defaultBrushSizeInMm = +params.get('defaultBrushSize') || 2; + let minBrushSizeInMm = +params.get('minBrushSize') || 2; + let maxBrushSizeInMm = +params.get('maxBrushSize') || 3; + + const highestPixelSpacing = getPixelToMmConversionFactor(servicesManager); + const lowestBrushRadius = highestPixelSpacing / 2; + + if (minBrushSizeInMm < lowestBrushRadius) { + minBrushSizeInMm = lowestBrushRadius; + } + if (maxBrushSizeInMm < lowestBrushRadius) { + maxBrushSizeInMm = highestPixelSpacing; + } + + setBrushProperties({ + min: +minBrushSizeInMm.toFixed(2), + max: +maxBrushSizeInMm.toFixed(2), + step: +((maxBrushSizeInMm - minBrushSizeInMm) / 100).toFixed(2), + }); + toolCategories.forEach(toolCategory => { + onBrushSizeChange(defaultBrushSizeInMm, toolCategory); + }); + }; + + useEffect(() => { + if (getPixelToMmConversionFactor(servicesManager)) { + setBrushSizesFromParams(); + return; + } const elementEnabledHandler = evt => { const setDefaultBrushSize = () => { - const defaultBrushSizeInMm = +params.get('defaultBrushSize') || 2; - let minBrushSizeInMm = +params.get('minBrushSize') || 2; - let maxBrushSizeInMm = +params.get('maxBrushSize') || 3; - const highestPixelSpacing = getPixelToMmConversionFactor(servicesManager); - const lowestBrushRadius = highestPixelSpacing / 2; - - if (minBrushSizeInMm < lowestBrushRadius) { - minBrushSizeInMm = lowestBrushRadius; - } - if (maxBrushSizeInMm < lowestBrushRadius) { - maxBrushSizeInMm = highestPixelSpacing; - } - - setBrushProperties({ - min: +minBrushSizeInMm.toFixed(2), - max: +maxBrushSizeInMm.toFixed(2), - step: +((maxBrushSizeInMm - minBrushSizeInMm) / 100).toFixed(2), - }); - toolCategories.forEach(toolCategory => { - onBrushSizeChange(defaultBrushSizeInMm, toolCategory); - }); + setBrushSizesFromParams(); evt.detail.element.removeEventListener( EVENTS.VOLUME_VIEWPORT_NEW_VOLUME, @@ -147,6 +156,12 @@ function SegmentationToolbox({ servicesManager, extensionManager }) { eventTarget.removeEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler); }; + const viewportElement = getActiveViewportElement(servicesManager, extensionManager); + if (viewportElement) { + elementEnabledHandler({ detail: { element: viewportElement } }); + return; + } + eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler); }, []); @@ -451,8 +466,26 @@ function getPixelToMmConversionFactor(servicesManager) { const { viewportGridService, cornerstoneViewportService } = servicesManager.services; const { activeViewportId } = viewportGridService.getState(); const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId); - const { spacing } = viewport.getImageData(); + const imageData = viewport?.getImageData(); + + if (!imageData) { + return; + } + + const { spacing } = imageData; return Math.max(spacing[0], spacing[1]); } +function getActiveViewportElement(servicesManager, extensionManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' + ); + const { getEnabledElement } = utilityModule.exports; + const { viewportGridService } = servicesManager.services; + + const { activeViewportId } = viewportGridService.getState(); + const { element } = getEnabledElement(activeViewportId) || {}; + return element; +} + export default SegmentationToolbox; From 3ea6d7d22e6bd60029b8cf712ac2e173a727498f Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 6 Mar 2024 19:52:05 +0530 Subject: [PATCH 16/25] Segmentation series description will be displayed using ImageLaterality and ViewPosition if available --- .../src/panels/PanelSegmentation.tsx | 8 ++++++-- .../src/utils/updateSegmentationLabels.ts | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 9104c3a15a0..d65cc5b5505 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -7,6 +7,7 @@ import callInputDialog from './callInputDialog'; import callColorPickerDialog from './colorPickerDialog'; import { useTranslation } from 'react-i18next'; import getSegmentLabel from '../utils/getSegmentLabel'; +import { updateSegmentationLabels } from '../utils/updateSegmentationLabels'; const savedStatusReducer = (state, action) => { return { @@ -43,7 +44,9 @@ export default function PanelSegmentation({ segmentationService.getConfiguration() ); - const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); + const [segmentations, setSegmentations] = useState(() => + updateSegmentationLabels(segmentationService.getSegmentations(), displaySetService) + ); const [savedStatusStates, dispatch] = useReducer(savedStatusReducer, {}); useEffect(() => { @@ -56,7 +59,8 @@ export default function PanelSegmentation({ [added, updated, removed].forEach(evt => { const { unsubscribe } = segmentationService.subscribe(evt, () => { const segmentations = segmentationService.getSegmentations(); - setSegmentations(segmentations); + const updatedSegmentations = updateSegmentationLabels(segmentations, displaySetService); + setSegmentations(updatedSegmentations); setSegmentationConfiguration(segmentationService.getConfiguration()); }); subscriptions.push(unsubscribe); diff --git a/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts b/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts new file mode 100644 index 00000000000..428d66489a8 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts @@ -0,0 +1,20 @@ +export function updateSegmentationLabels(segmentations, displaySetService) { + return segmentations.map(segmentation => { + const segDisplaySet = displaySetService.getDisplaySetByUID(segmentation.displaySetInstanceUID); + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + segDisplaySet.referencedDisplaySetInstanceUID + ); + const { ViewPosition, ImageLaterality } = referencedDisplaySet.instance; + + return { + ...segmentation, + ...(ImageLaterality && + ViewPosition && { + label: segmentation.label.replace( + /^.* - Vessel/, + `${ImageLaterality} ${ViewPosition} - Vessel` + ), + }), + }; + }); +} From 290fda3685a5ed533c275182883e4e6f362665ef Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 8 Mar 2024 20:32:54 +0530 Subject: [PATCH 17/25] Corrected the logic to access referenced displayset for newly created segmentations --- .../src/utils/updateSegmentationLabels.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts b/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts index 428d66489a8..b855230ae59 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts @@ -1,9 +1,17 @@ export function updateSegmentationLabels(segmentations, displaySetService) { return segmentations.map(segmentation => { - const segDisplaySet = displaySetService.getDisplaySetByUID(segmentation.displaySetInstanceUID); - const referencedDisplaySet = displaySetService.getDisplaySetByUID( - segDisplaySet.referencedDisplaySetInstanceUID - ); + const displaySet = displaySetService.getDisplaySetByUID(segmentation.displaySetInstanceUID); + + let referencedDisplaySet; + if (displaySet.Modality === 'SEG') { + referencedDisplaySet = displaySetService.getDisplaySetByUID( + displaySet.referencedDisplaySetInstanceUID + ); + } else { + // In case of newly created segmentations, displaySetInstanceUID in the segmentation is of the referenced displaySet. + referencedDisplaySet = displaySet; + } + const { ViewPosition, ImageLaterality } = referencedDisplaySet.instance; return { From cd593934d606852bd6c6ac5ea5c41b67bb364710 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 15 Mar 2024 20:25:15 +0530 Subject: [PATCH 18/25] Added more properties to retain to segmentation file when saving segmentation --- extensions/cornerstone-dicom-seg/src/commandsModule.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 1748d24cbc1..1b958b876fb 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -349,10 +349,15 @@ const commandsModule = ({ segmentationId, options: { SeriesDescription, - // Use Series and SOP instancesUIDs if displaySet of the segmentation already exists. + // 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, + SeriesDate: displaySet.SeriesDate, }), }, }); From 52b3dbe21b509e2bbd0a9e2bcc863c38d5ccc8dc Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Mon, 18 Mar 2024 19:58:14 +0530 Subject: [PATCH 19/25] Due to script update for modifying the segmentation SeriesDescription, this change is redundant and thus reverting. --- .../src/panels/PanelSegmentation.tsx | 8 ++---- .../generateLabelmaps2DFromImageIdMap.ts | 24 ++++++++-------- .../src/utils/updateSegmentationLabels.ts | 28 ------------------- 3 files changed, 13 insertions(+), 47 deletions(-) delete mode 100644 extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index d65cc5b5505..9104c3a15a0 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -7,7 +7,6 @@ import callInputDialog from './callInputDialog'; import callColorPickerDialog from './colorPickerDialog'; import { useTranslation } from 'react-i18next'; import getSegmentLabel from '../utils/getSegmentLabel'; -import { updateSegmentationLabels } from '../utils/updateSegmentationLabels'; const savedStatusReducer = (state, action) => { return { @@ -44,9 +43,7 @@ export default function PanelSegmentation({ segmentationService.getConfiguration() ); - const [segmentations, setSegmentations] = useState(() => - updateSegmentationLabels(segmentationService.getSegmentations(), displaySetService) - ); + const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); const [savedStatusStates, dispatch] = useReducer(savedStatusReducer, {}); useEffect(() => { @@ -59,8 +56,7 @@ export default function PanelSegmentation({ [added, updated, removed].forEach(evt => { const { unsubscribe } = segmentationService.subscribe(evt, () => { const segmentations = segmentationService.getSegmentations(); - const updatedSegmentations = updateSegmentationLabels(segmentations, displaySetService); - setSegmentations(updatedSegmentations); + setSegmentations(segmentations); setSegmentationConfiguration(segmentationService.getConfiguration()); }); subscriptions.push(unsubscribe); diff --git a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts index 2c739c9c438..14fffdae29a 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts @@ -19,20 +19,18 @@ const generateLabelmaps2DFromImageIdMap = imageIdReferenceMap => { } } - if (!segmentsOnLabelmap.length) { - segmentsOnLabelmap.push(1); + if (segmentsOnLabelmap.length) { + labelmaps2D[index] = { + segmentsOnLabelmap, + pixelData, + rows, + columns, + }; + + segmentsOnLabelmap.forEach(segmentIndex => { + segmentsOnLabelmap3D.add(segmentIndex); + }); } - - labelmaps2D[index] = { - segmentsOnLabelmap, - pixelData, - rows, - columns, - }; - - segmentsOnLabelmap.forEach(segmentIndex => { - segmentsOnLabelmap3D.add(segmentIndex); - }); }); const labelmapObj = { diff --git a/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts b/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts deleted file mode 100644 index b855230ae59..00000000000 --- a/extensions/cornerstone-dicom-seg/src/utils/updateSegmentationLabels.ts +++ /dev/null @@ -1,28 +0,0 @@ -export function updateSegmentationLabels(segmentations, displaySetService) { - return segmentations.map(segmentation => { - const displaySet = displaySetService.getDisplaySetByUID(segmentation.displaySetInstanceUID); - - let referencedDisplaySet; - if (displaySet.Modality === 'SEG') { - referencedDisplaySet = displaySetService.getDisplaySetByUID( - displaySet.referencedDisplaySetInstanceUID - ); - } else { - // In case of newly created segmentations, displaySetInstanceUID in the segmentation is of the referenced displaySet. - referencedDisplaySet = displaySet; - } - - const { ViewPosition, ImageLaterality } = referencedDisplaySet.instance; - - return { - ...segmentation, - ...(ImageLaterality && - ViewPosition && { - label: segmentation.label.replace( - /^.* - Vessel/, - `${ImageLaterality} ${ViewPosition} - Vessel` - ), - }), - }; - }); -} From 8d20452be9606c447f9c70a2ce4ae1b8ec9e61d0 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 20 Mar 2024 19:33:45 +0530 Subject: [PATCH 20/25] Disabled add segmentation using url param --- .../src/panels/PanelSegmentation.tsx | 4 ++++ .../SegmentationGroupTable.tsx | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 9104c3a15a0..141bb8f5935 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -372,6 +372,9 @@ export default function PanelSegmentation({ }); }; + const params = new URLSearchParams(window.location.search); + const showAddSegmentation = params.get('disableAddSegmentation') !== 'true'; + return ( <>
@@ -380,6 +383,7 @@ export default function PanelSegmentation({ segmentations={segmentations} savedStatusStates={savedStatusStates} disableEditing={configuration.disableEditing} + showAddSegmentation={showAddSegmentation} activeSegmentationId={selectedSegmentationId || ''} onSegmentationAdd={onSegmentationAdd} onSegmentationClick={onSegmentationClick} diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx index f5ca8b8b3a9..a7a22f23aeb 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx @@ -75,10 +75,14 @@ const SegmentationGroupTable = ({ title={t('Segmentations')} actionIcons={ activeSegmentation && [ - { - name: 'row-add', - onClick: () => onSegmentationAdd(), - }, + ...(showAddSegmentation && !disableEditing + ? [ + { + name: 'row-add', + onClick: () => onSegmentationAdd(), + }, + ] + : []), { name: 'settings-bars', onClick: () => setIsConfigOpen(isOpen => !isOpen), From 0ea434d75d1cc4203dd1e8f195d246b8a9afe8c2 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Mon, 25 Mar 2024 19:30:12 +0530 Subject: [PATCH 21/25] Added hotkeys to activate segmentation brush and eraser tools. --- platform/core/src/defaults/hotkeyBindings.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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', From cc13c72eeffd89ac132a39c024b08b75a0308438 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Tue, 26 Mar 2024 11:41:16 +0530 Subject: [PATCH 22/25] Updated SeriesDate when saving segmentations instead of using the previous one. --- extensions/cornerstone-dicom-seg/src/commandsModule.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts index 1b958b876fb..1d41748fcfc 100644 --- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -357,7 +357,6 @@ const commandsModule = ({ SOPInstanceUID: displaySet.instances[0].SOPInstanceUID, SeriesNumber: displaySet.SeriesNumber, Manufacturer: displaySet.instances[0].Manufacturer, - SeriesDate: displaySet.SeriesDate, }), }, }); From 68c1170546b64153a04f30df0646130c47f1f470 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Tue, 26 Mar 2024 18:34:07 +0530 Subject: [PATCH 23/25] Synced segmentation tools hotkeys with segmentation panel --- extensions/cornerstone/src/commandsModule.ts | 16 ++++++++++++++++ platform/core/src/defaults/hotkeyBindings.js | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 52645938125..5da90b32990 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -359,6 +359,19 @@ function commandsModule({ ], }); }, + recordSetToolActive: ({ toolName }) => { + toolbarService.recordInteraction({ + interactionType: 'tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName, + }, + }, + ], + }); + }, showDownloadViewportModal: () => { const { activeViewportId } = viewportGridService.getState(); @@ -653,6 +666,9 @@ function commandsModule({ setToolActive: { commandFn: actions.setToolActive, }, + recordSetToolActive: { + commandFn: actions.recordSetToolActive, + }, rotateViewportCW: { commandFn: actions.rotateViewport, options: { rotation: 90 }, diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js index 1292df0dbc3..c4b572b7049 100644 --- a/platform/core/src/defaults/hotkeyBindings.js +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -174,14 +174,14 @@ const bindings = [ keys: ['5'], }, { - commandName: 'setToolActive', + commandName: 'recordSetToolActive', commandOptions: { toolName: 'CircularBrush' }, label: 'Segmentation Brush', keys: ['b'], isEditable: true, }, { - commandName: 'setToolActive', + commandName: 'recordSetToolActive', commandOptions: { toolName: 'CircularEraser' }, label: 'Segmentation Eraser', keys: ['e'], From f4191a45b79021e22f06149b7938bb99fed73155 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Thu, 20 Jun 2024 14:12:23 +0530 Subject: [PATCH 24/25] Trigger deploy --- extensions/cornerstone/src/hps/mpr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cornerstone/src/hps/mpr.ts b/extensions/cornerstone/src/hps/mpr.ts index 4c625c73172..a26c509e445 100644 --- a/extensions/cornerstone/src/hps/mpr.ts +++ b/extensions/cornerstone/src/hps/mpr.ts @@ -8,7 +8,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', From 9485743a0eef609aef65d8fd97ae5bbaf0cf4c3c Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 4 Sep 2024 20:24:13 +0530 Subject: [PATCH 25/25] Auto flipping of disoriented Mammography images on stack viewport --- extensions/cornerstone/jest.config.js | 5 + extensions/cornerstone/src/commandsModule.ts | 13 ++ .../cornerstone/src/getCustomizationModule.ts | 3 + extensions/cornerstone/src/index.tsx | 2 + .../CornerstoneViewportService.ts | 51 +++++- .../src/utils/getImageFlips.test.js | 159 ++++++++++++++++++ .../cornerstone/src/utils/getImageFlips.ts | 123 ++++++++++++++ .../utils/isOrientationCorrectionNeeded.ts | 10 ++ platform/core/src/classes/MetadataProvider.ts | 10 +- platform/core/src/defaults/index.js | 5 +- .../defaults/orientationDirectionVectors.ts | 12 ++ .../core/src/types/OrientationDirections.ts | 1 + platform/core/src/types/index.ts | 1 + .../getDirectionsFromPatientOrientation.ts | 16 ++ platform/core/src/utils/index.js | 3 + platform/core/src/utils/index.test.js | 1 + 16 files changed, 403 insertions(+), 12 deletions(-) create mode 100644 extensions/cornerstone/src/utils/getImageFlips.test.js create mode 100644 extensions/cornerstone/src/utils/getImageFlips.ts create mode 100644 extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts create mode 100644 platform/core/src/defaults/orientationDirectionVectors.ts create mode 100644 platform/core/src/types/OrientationDirections.ts create mode 100644 platform/core/src/utils/getDirectionsFromPatientOrientation.ts diff --git a/extensions/cornerstone/jest.config.js b/extensions/cornerstone/jest.config.js index 2978b062ed1..9e0ecb3e4ec 100644 --- a/extensions/cornerstone/jest.config.js +++ b/extensions/cornerstone/jest.config.js @@ -5,6 +5,11 @@ module.exports = { ...base, name: pkg.name, displayName: pkg.name, + moduleNameMapper: { + ...base.moduleNameMapper, + '^@ohif/(.*)$': '/../../platform/$1/src', + '^@cornerstonejs/tools(.*)$': '/../../node_modules/@cornerstonejs/tools', + }, // rootDir: "../.." // testMatch: [ // //`/platform/${pack.name}/**/*.spec.js` diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 5da90b32990..44cc430a2ab 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -5,6 +5,7 @@ import { utilities as csUtils, Types as CoreTypes, BaseVolumeViewport, + metaData, } from '@cornerstonejs/core'; import { ToolGroupManager, @@ -21,6 +22,7 @@ import toggleStackImageSync from './utils/stackSync/toggleStackImageSync'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import { CornerstoneServices } from './types'; +import { getImageFlips } from './utils/getImageFlips'; function commandsModule({ servicesManager, @@ -35,6 +37,7 @@ function commandsModule({ cornerstoneViewportService, uiNotificationService, measurementService, + customizationService, } = servicesManager.services as CornerstoneServices; const { measurementServiceSource } = this; @@ -483,6 +486,16 @@ function commandsModule({ viewport.resetProperties?.(); viewport.resetCamera(); + const { criteria: isOrientationCorrectionNeeded } = customizationService.get( + 'orientationCorrectionCriterion' + ); + const instance = metaData.get('instance', viewport.getCurrentImageId()); + + if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) { + const { hFlip, vFlip } = getImageFlips(instance); + (hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip }); + } + viewport.render(); }, scaleViewport: ({ direction }) => { diff --git a/extensions/cornerstone/src/getCustomizationModule.ts b/extensions/cornerstone/src/getCustomizationModule.ts index acb06424462..9d22282e261 100644 --- a/extensions/cornerstone/src/getCustomizationModule.ts +++ b/extensions/cornerstone/src/getCustomizationModule.ts @@ -1,6 +1,7 @@ import { Enums } from '@cornerstonejs/tools'; import { toolNames } from './initCornerstoneTools'; import DicomUpload from './components/DicomUpload/DicomUpload'; +import isOrientationCorrectionNeeded from './utils/isOrientationCorrectionNeeded'; const tools = { active: [ @@ -37,6 +38,8 @@ function getCustomizationModule() { id: 'cornerstone.overlayViewportTools', tools, }, + // TODO: Move this customization to MG specific mode when introduced in OHIF. + { id: 'orientationCorrectionCriterion', criteria: isOrientationCorrectionNeeded }, ], }, ]; diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index ac53c8f0f27..531e979f67a 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -30,6 +30,7 @@ import * as csWADOImageLoader from './initWADOImageLoader.js'; import { measurementMappingUtils } from './utils/measurementServiceMappings'; import type { PublicViewportOptions } from './services/ViewportService/Viewport'; import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; +import { getImageFlips } from './utils/getImageFlips'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'); @@ -116,6 +117,7 @@ const cornerstoneExtension: Types.Extensions.Extension = { }, getEnabledElement, dicomLoaderService, + getImageFlips, }, }, { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 1180d8036f5..4882ca743cd 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -10,8 +10,9 @@ import { VolumeViewport3D, cache, Enums as csEnums, + metaData, + eventTarget, } from '@cornerstonejs/core'; - import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools'; import { IViewportService } from './IViewportService'; import { RENDERING_ENGINE_ID } from './constants'; @@ -20,6 +21,7 @@ import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCa import { Presentation, Presentations } from '../../types/Presentation'; import JumpPresets from '../../utils/JumpPresets'; +import { getImageFlips } from '../../utils/getImageFlips'; const EVENTS = { VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged', @@ -325,6 +327,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi viewportInfo: ViewportInfo, presentations: Presentations ): Promise { + const { segmentationService, displaySetService, customizationService } = + this.servicesManager.services; const displaySetOptions = viewportInfo.getDisplaySetOptions(); const { imageIds, initialImageIndex, displaySetInstanceUID } = viewportData.data; @@ -354,24 +358,48 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } } - const segmentations = this.servicesManager.services.segmentationService.getSegmentations(false); + const { criteria: isOrientationCorrectionNeeded } = customizationService.get( + 'orientationCorrectionCriterion' + ); + const instance = metaData.get('instance', imageIds[initialImageIndexToUse]); + + let hFlip = false, + vFlip = false; + if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) { + ({ hFlip, vFlip } = getImageFlips(instance)); + } + + const segmentations = segmentationService.getSegmentations(false); const toolgroupId = viewportInfo.getToolGroupId(); for (const segmentation of segmentations) { const toolGroupSegmentationRepresentations = - this.servicesManager.services.segmentationService.getSegmentationRepresentationsForToolGroup( - toolgroupId - ) || []; + segmentationService.getSegmentationRepresentationsForToolGroup(toolgroupId) || []; const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find( representation => representation.segmentationId === segmentation.id ); + const callback = evt => { + if (viewport.id !== evt.detail.viewportId) { + return; + } + + eventTarget.removeEventListener(csEnums.Events.STACK_VIEWPORT_IMAGES_ADDED, callback); + + const camera = presentations.positionPresentation?.camera; + if (isSegmentationInToolGroup && camera) { + viewport.setCamera(camera); + } else { + (hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip }); + } + }; + + eventTarget.addEventListener(csEnums.Events.STACK_VIEWPORT_IMAGES_ADDED, callback); + if (!isSegmentationInToolGroup) { - const segDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( - segmentation.id - ); + const segDisplaySet = displaySetService.getDisplaySetByUID(segmentation.id); segDisplaySet && - this.servicesManager.services.segmentationService.addSegmentationRepresentationToToolGroup( + segmentationService.addSegmentationRepresentationToToolGroup( toolgroupId, segmentation.id, segDisplaySet.isOverlayDisplaySet @@ -382,6 +410,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); const camera = presentations.positionPresentation?.camera; + + !camera && + (hFlip || vFlip) && + viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip }); + if (camera) { viewport.setCamera(camera); } diff --git a/extensions/cornerstone/src/utils/getImageFlips.test.js b/extensions/cornerstone/src/utils/getImageFlips.test.js new file mode 100644 index 00000000000..7f29b867cfa --- /dev/null +++ b/extensions/cornerstone/src/utils/getImageFlips.test.js @@ -0,0 +1,159 @@ +import { getImageFlips } from './getImageFlips'; + +const orientationDirectionVectorMap = { + L: [1, 0, 0], // Left + R: [-1, 0, 0], // Right + P: [0, 1, 0], // Posterior/ Back + A: [0, -1, 0], // Anterior/ Front + H: [0, 0, 1], // Head/ Superior + F: [0, 0, -1], // Feet/ Inferior +}; + +const getDirectionsFromPatientOrientation = patientOrientation => { + if (typeof patientOrientation === 'string') { + patientOrientation = patientOrientation.split('\\'); + } + + return { + rowDirection: patientOrientation[0], + columnDirection: patientOrientation[1][0], + }; +}; + +const getOrientationStringLPS = vector => { + const sampleVectorDirectionMap = { + '1,0,0': 'L', + '-1,0,0': 'R', + '0,1,0': 'P', + '0,-1,0': 'A', + '0,0,1': 'H', + '0,0,-1': 'F', + }; + + return sampleVectorDirectionMap[vector.toString()]; +}; + +jest.mock('@cornerstonejs/tools ', () => ({ + utilities: { orientation: { getOrientationStringLPS } }, +})); +jest.mock('@ohif/core', () => ({ + defaults: { orientationDirectionVectorMap }, + utils: { getDirectionsFromPatientOrientation }, +})); + +describe('getImageFlips', () => { + test('should return empty object if none of the parameters are provided', () => { + const flipsNeeded = getImageFlips({}); + expect(flipsNeeded).toEqual({}); + }); + + test('should return empty object if ImageOrientationPatient and PatientOrientation is not provided', () => { + const ImageLaterality = 'L'; + const flipsNeeded = getImageFlips({ + ImageLaterality, + }); + expect(flipsNeeded).toEqual({}); + }); + + test('should return empty object if ImageLaterality is not privided', () => { + const ImageOrientationPatient = [0, 1, 0, 0, 0, 1], + PatientOrientation = ['P', 'H']; + const flipsNeeded = getImageFlips({ + ImageOrientationPatient, + PatientOrientation, + }); + expect(flipsNeeded).toEqual({}); + }); + + test('should return { hFlip: false, vFlip: false } if ImageOrientationPatient is [0, 1, 0, 0, 0, -1] and ImageLaterality is R', () => { + const ImageOrientationPatient = [0, 1, 0, 0, 0, -1], + PatientOrientation = ['P', 'F'], + ImageLaterality = 'R'; + const flipsNeeded = getImageFlips({ + ImageOrientationPatient, + PatientOrientation, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: false, vFlip: false }); + }); + + test('should return { hFlip: false, vFlip: true } if ImageOrientationPatient is [0, -1, 0, 0, 0, 1] and ImageLaterality is L', () => { + const ImageOrientationPatient = [0, -1, 0, 0, 0, 1], + ImageLaterality = 'L'; + const flipsNeeded = getImageFlips({ + ImageOrientationPatient, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: false, vFlip: true }); + }); + + test('should return { hFlip: true, vFlip: true } if ImageOrientationPatient is [0, -1, 0, -1, 0, 0] and ImageLaterality is R', () => { + const ImageOrientationPatient = [0, -1, 0, -1, 0, 0], + ImageLaterality = 'R'; + const flipsNeeded = getImageFlips({ + ImageOrientationPatient, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: true, vFlip: true }); + }); + + test("should return { hFlip: true, vFlip: true } if ImageOrientationPatient is not present, PatientOrientation is ['P', 'H'] and ImageLaterality is L", () => { + const PatientOrientation = ['P', 'H'], + ImageLaterality = 'L'; + const flipsNeeded = getImageFlips({ + PatientOrientation, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: true, vFlip: true }); + }); + + test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient is not present, PatientOrientation is ['A', 'F'] and ImageLaterality is R", () => { + const PatientOrientation = ['A', 'F'], + ImageLaterality = 'R'; + const flipsNeeded = getImageFlips({ + PatientOrientation, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false }); + }); + + test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient is not present, PatientOrientation is ['A', 'FL'] and ImageLaterality is R", () => { + const PatientOrientation = ['A', 'FL'], + ImageLaterality = 'R'; + const flipsNeeded = getImageFlips({ + PatientOrientation, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false }); + }); + + test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient ans ImageLaterality is not present, PatientOrientation is ['P', 'FL'] and FrameLaterality is L", () => { + const PatientOrientation = ['P', 'FL'], + FrameLaterality = 'L'; + const flipsNeeded = getImageFlips({ + PatientOrientation, + FrameLaterality, + }); + expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false }); + }); + + test("should return empty object if ImageOrientationPatient is not present, PatientOrientation is ['H', 'R'] and ImageLaterality is R since the orientation is rotated, not flipped", () => { + const PatientOrientation = ['H', 'R'], + ImageLaterality = 'R'; + const flipsNeeded = getImageFlips({ + PatientOrientation, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({}); + }); + + test("should return empty object if ImageOrientationPatient is not present, PatientOrientation is ['F', 'L'] and ImageLaterality is L since the orientation is rotated, not flipped", () => { + const PatientOrientation = ['F', 'L'], + ImageLaterality = 'L'; + const flipsNeeded = getImageFlips({ + PatientOrientation, + ImageLaterality, + }); + expect(flipsNeeded).toEqual({}); + }); +}); diff --git a/extensions/cornerstone/src/utils/getImageFlips.ts b/extensions/cornerstone/src/utils/getImageFlips.ts new file mode 100644 index 00000000000..bbe95a5b0e2 --- /dev/null +++ b/extensions/cornerstone/src/utils/getImageFlips.ts @@ -0,0 +1,123 @@ +import { defaults, utils } from '@ohif/core'; +import { utilities } from '@cornerstonejs/tools'; +import { vec3 } from 'gl-matrix'; + +const { orientationDirectionVectorMap } = defaults; +const { getOrientationStringLPS } = utilities.orientation; + +type IOP = [number, number, number, number, number, number]; +type PO = [string, string] | string; +type IL = string; +type FL = string; +type Instance = { + ImageOrientationPatient?: IOP; + PatientOrientation?: PO; + ImageLaterality?: IL; + FrameLaterality?: FL; +}; + +/** + * A function to get required flipping to correct the image according to Orientation and Laterality. + * This function does not handle rotated images. + * @param instance Metadata instance of the image. + * @returns vertical and horizontal flipping needed to correct the image if possible. + */ +export function getImageFlips(instance: Instance): { vFlip?: boolean; hFlip?: boolean } { + const { ImageOrientationPatient, PatientOrientation, ImageLaterality, FrameLaterality } = + instance; + + if (!(ImageOrientationPatient || PatientOrientation) || !(ImageLaterality || FrameLaterality)) { + console.warn( + 'Skipping image orientation correction due to missing ImageOrientationPatient/ PatientOrientation or/and ImageLaterality/ FrameLaterality' + ); + return {}; + } + + let rowDirectionCurrent, columnDirectionCurrent, rowCosines, columnCosines; + if (ImageOrientationPatient) { + rowCosines = ImageOrientationPatient.slice(0, 3); + columnCosines = ImageOrientationPatient.slice(3, 6); + rowDirectionCurrent = getOrientationStringLPS(rowCosines); + columnDirectionCurrent = getOrientationStringLPS(columnCosines)[0]; + } else { + ({ rowDirection: rowDirectionCurrent, columnDirection: columnDirectionCurrent } = + utils.getDirectionsFromPatientOrientation(PatientOrientation)); + + rowCosines = orientationDirectionVectorMap[rowDirectionCurrent]; + columnCosines = orientationDirectionVectorMap[columnDirectionCurrent]; + } + + const scanAxisNormal = vec3.create(); + vec3.cross(scanAxisNormal, rowCosines, columnCosines); + + const scanAxisDirection = getOrientationStringLPS(scanAxisNormal as [number, number, number]); + + if (isImageRotated(rowDirectionCurrent, columnDirectionCurrent)) { + // TODO: Correcting images with rotation is not implemented. + console.warn('Correcting images by rotating is not implemented'); + return {}; + } + + let rowDirectionTarget, columnDirectionTarget; + switch (scanAxisDirection[0]) { + // Sagittal + case 'L': + case 'R': + if ((ImageLaterality || FrameLaterality) === 'L') { + rowDirectionTarget = 'A'; + } else { + rowDirectionTarget = 'P'; + } + columnDirectionTarget = 'F'; + break; + // Coronal + case 'A': + case 'P': + if ((ImageLaterality || FrameLaterality) === 'L') { + rowDirectionTarget = 'R'; + } else { + rowDirectionTarget = 'L'; + } + columnDirectionTarget = 'F'; + break; + // Axial + case 'H': + case 'F': + if ((ImageLaterality || FrameLaterality) === 'L') { + rowDirectionTarget = 'A'; + columnDirectionTarget = 'R'; + } else { + rowDirectionTarget = 'P'; + columnDirectionTarget = 'L'; + } + break; + } + + let hFlip = false, + vFlip = false; + if (rowDirectionCurrent !== rowDirectionTarget) { + hFlip = true; + } + if (columnDirectionCurrent !== columnDirectionTarget) { + vFlip = true; + } + + return { hFlip, vFlip }; +} + +function isImageRotated(rowDirection: string, columnDirection: string): boolean { + const possibleValues: { [key: string]: [string, string] } = { + xDirection: ['L', 'R'], + yDirection: ['P', 'A'], + zDirection: ['H', 'F'], + }; + + if ( + possibleValues.yDirection.includes(columnDirection) || + possibleValues.zDirection.includes(rowDirection) + ) { + return true; + } + + return false; +} diff --git a/extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts b/extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts new file mode 100644 index 00000000000..83e24545231 --- /dev/null +++ b/extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts @@ -0,0 +1,10 @@ +const DEFAULT_AUTO_FLIP_MODALITIES: string[] = ['MG']; + +export default function isOrientationCorrectionNeeded(instance) { + const { Modality } = instance; + + // Check Modality + const isModalityIncluded = DEFAULT_AUTO_FLIP_MODALITIES.includes(Modality); + + return isModalityIncluded; +} diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts index de12ff4e33c..4c52d80198f 100644 --- a/platform/core/src/classes/MetadataProvider.ts +++ b/platform/core/src/classes/MetadataProvider.ts @@ -6,6 +6,7 @@ import DicomMetadataStore from '../services/DicomMetadataStore'; import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; import toNumber from '../utils/toNumber'; import combineFrameInstance from '../utils/combineFrameInstance'; +import { orientationDirectionVectorMap } from '../defaults'; class MetadataProvider { constructor() { @@ -494,7 +495,7 @@ export default metadataProvider; const WADO_IMAGE_LOADER = { imagePlaneModule: instance => { - const { ImageOrientationPatient } = instance; + const { ImageOrientationPatient, PatientOrientation } = instance; // Fallback for DX images. // TODO: We should use the rest of the results of this function @@ -515,6 +516,13 @@ const WADO_IMAGE_LOADER = { if (ImageOrientationPatient) { rowCosines = ImageOrientationPatient.slice(0, 3); columnCosines = ImageOrientationPatient.slice(3, 6); + } else if (PatientOrientation) { + let patientOrientation = PatientOrientation; + if (typeof patientOrientation === 'string') { + patientOrientation = patientOrientation.split('\\'); + } + rowCosines = orientationDirectionVectorMap[patientOrientation[0]]; + columnCosines = orientationDirectionVectorMap[patientOrientation[1][0]]; } return { diff --git a/platform/core/src/defaults/index.js b/platform/core/src/defaults/index.js index f8c065d7b7f..ad4be530cfb 100644 --- a/platform/core/src/defaults/index.js +++ b/platform/core/src/defaults/index.js @@ -1,4 +1,5 @@ import hotkeyBindings from './hotkeyBindings'; import windowLevelPresets from './windowLevelPresets'; -export { hotkeyBindings, windowLevelPresets }; -export default { hotkeyBindings, windowLevelPresets }; +import orientationDirectionVectorMap from './orientationDirectionVectors'; +export { hotkeyBindings, windowLevelPresets, orientationDirectionVectorMap }; +export default { hotkeyBindings, windowLevelPresets, orientationDirectionVectorMap }; diff --git a/platform/core/src/defaults/orientationDirectionVectors.ts b/platform/core/src/defaults/orientationDirectionVectors.ts new file mode 100644 index 00000000000..2809673b26a --- /dev/null +++ b/platform/core/src/defaults/orientationDirectionVectors.ts @@ -0,0 +1,12 @@ +import { Types } from '@cornerstonejs/core'; + +const orientationDirectionVectorMap: { [key: string]: Types.Point3 } = { + L: [1, 0, 0], // Left + R: [-1, 0, 0], // Right + P: [0, 1, 0], // Posterior/ Back + A: [0, -1, 0], // Anterior/ Front + H: [0, 0, 1], // Head/ Superior + F: [0, 0, -1], // Feet/ Inferior +}; + +export default orientationDirectionVectorMap; diff --git a/platform/core/src/types/OrientationDirections.ts b/platform/core/src/types/OrientationDirections.ts new file mode 100644 index 00000000000..d44cec4d96d --- /dev/null +++ b/platform/core/src/types/OrientationDirections.ts @@ -0,0 +1 @@ +export type OrientationDirections = { rowDirection: string; columnDirection: string }; diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts index efcc8ec3ccf..088a5935f60 100644 --- a/platform/core/src/types/index.ts +++ b/platform/core/src/types/index.ts @@ -18,6 +18,7 @@ export type * from './StudyMetadata'; export type * from './PanelModule'; export type * from './IPubSub'; export type * from './Color'; +export type * from './OrientationDirections'; // Enum exports export * from './TimingEnum'; diff --git a/platform/core/src/utils/getDirectionsFromPatientOrientation.ts b/platform/core/src/utils/getDirectionsFromPatientOrientation.ts new file mode 100644 index 00000000000..33058df6f14 --- /dev/null +++ b/platform/core/src/utils/getDirectionsFromPatientOrientation.ts @@ -0,0 +1,16 @@ +import { OrientationDirections } from '../types'; + +export default function getDirectionsFromPatientOrientation( + patientOrientation: string | [string, string] +): OrientationDirections { + if (typeof patientOrientation === 'string') { + patientOrientation = patientOrientation.split('\\') as [string, string]; + } + + // TODO: We are only considering first direction in column orientation. + // ex: in ['P', 'HL'], we are only taking 'H' instead of 'HL' as column orientation. + return { + rowDirection: patientOrientation[0], + columnDirection: patientOrientation[1][0], + }; +} diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index d7862618e0b..860ddbf6eb5 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -37,6 +37,7 @@ import { } from './sortStudy'; import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange'; import { splitComma, getSplitParam } from './splitComma'; +import getDirectionsFromPatientOrientation from './getDirectionsFromPatientOrientation'; // Commented out unused functionality. // Need to implement new mechanism for derived displaySets using the displaySetManager. @@ -80,6 +81,7 @@ const utils = { splitComma, getSplitParam, generateAcceptHeader, + getDirectionsFromPatientOrientation, }; export { @@ -112,6 +114,7 @@ export { splitComma, getSplitParam, generateAcceptHeader, + getDirectionsFromPatientOrientation, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 30d8f70ef51..044df94a749 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -41,6 +41,7 @@ describe('Top level exports', () => { 'subscribeToNextViewportGridChange', 'uuidv4', 'addAccessors', + 'getDirectionsFromPatientOrientation', ].sort(); const exports = Object.keys(utils.default).sort();