From 856c920579521b0b5b897acd524ffe787346d128 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Thu, 20 Jun 2024 18:40:54 +0530 Subject: [PATCH] Support for Tomosynthesis multiframe loading and segmentation. --- .../src/getSopClassHandlerModule.js | 2 +- .../src/panels/PanelSegmentation.tsx | 7 +++++++ .../generateLabelmaps2DFromImageIdMap.ts | 9 ++++----- .../SegmentationService.ts | 16 +++++++++++----- modes/segmentation/src/index.tsx | 2 +- .../src/utils/getImageIdsFromInstances.ts | 15 +++++++++++++++ platform/core/src/utils/index.js | 3 +++ .../src/utils/isDisplaySetReconstructable.js | 19 +++++++++++++++++++ .../SegmentationGroup.tsx | 3 +++ .../SegmentationGroupSegment.tsx | 10 +++------- .../SegmentationGroupTable.tsx | 3 +++ 11 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 platform/core/src/utils/getImageIdsFromInstances.ts diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js index a0043fefcda..6b0ee8bf2e7 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js @@ -157,7 +157,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId); imageIds = cachedReferencedVolume.imageIds || cachedReferencedVolume._imageIds; } else { - imageIds = referencedDisplaySet.instances.map(instance => instance.imageId); + imageIds = utils.getImageIdsFromInstances(referencedDisplaySet.instances); } // Todo: what should be defaults here diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 141bb8f5935..2720752eb62 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -201,6 +201,12 @@ export default function PanelSegmentation({ segmentationService.addSegment(segmentationId, { properties: { label } }); }; + const onSegmentFocusClick = (segmentationId, segmentIndex) => { + setSegmentationActive(segmentationId); + segmentationService.setActiveSegment(segmentationId, segmentIndex); + CropDisplayAreaService.focusToSegment(segmentationId, segmentIndex); + }; + const onSegmentClick = (segmentationId, segmentIndex) => { setReferencedDisplaySet(segmentationId); segmentationService.setActiveSegment(segmentationId, segmentIndex); @@ -392,6 +398,7 @@ export default function PanelSegmentation({ onSegmentationDownloadRTSS={onSegmentationDownloadRTSS} storeSegmentation={storeSegmentation} onSegmentationEdit={onSegmentationEdit} + onSegmentFocusClick={onSegmentFocusClick} onSegmentClick={onSegmentClick} onSegmentEdit={onSegmentEdit} onSegmentAdd={onSegmentAdd} diff --git a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts index 14fffdae29a..618ebeae4a8 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/generateLabelmaps2DFromImageIdMap.ts @@ -1,13 +1,12 @@ import { cache } from '@cornerstonejs/core'; -const generateLabelmaps2DFromImageIdMap = imageIdReferenceMap => { +const generateLabelmaps2DFromImageIdMap = (imageIdReferenceMap: Map) => { const labelmaps2D = [], referencedImages = [], segmentsOnLabelmap3D = new Set(); - Array.from(imageIdReferenceMap.entries()).forEach((entry, index) => { - referencedImages.push(cache.getImage(entry[0])); - - const segmentationImage = cache.getImage(entry[1]); + Array.from(imageIdReferenceMap.values()).forEach((segImageId, index) => { + const segmentationImage = cache.getImage(segImageId); + referencedImages.push(segmentationImage); const { rows, columns } = segmentationImage; const pixelData = segmentationImage.getPixelData(); const segmentsOnLabelmap = []; diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 0fd41b11d69..879f14b7d24 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -1,6 +1,6 @@ import cloneDeep from 'lodash.clonedeep'; -import { Types as OhifTypes, ServicesManager, PubSubService } from '@ohif/core'; +import { Types as OhifTypes, ServicesManager, PubSubService, utils } from '@ohif/core'; import { cache, Enums as csEnums, @@ -584,9 +584,7 @@ class SegmentationService extends PubSubService { } else { const getSegImageId = (imageId: string): string => `segimage:${segmentationId}:${imageId}`; - const referencedImageIds = referencedDisplaySet.instances.reduce((imageIds, instance) => { - return [...imageIds, instance.imageId]; - }, []); + const referencedImageIds = utils.getImageIdsFromInstances(referencedDisplaySet.instances); const imageIdReferenceMap = new Map(); const segImageIds: string[] = []; @@ -612,7 +610,7 @@ class SegmentationService extends PubSubService { const bytes = new Uint8Array(labelmapBufferArray[0]); const singleSlicePixelSize = rows * columns; - for (let i = 0; i < referencedDisplaySet.instances.length; i++) { + for (let i = 0; i < segImageIds.length; i++) { const singleSlicePixelData = new Uint8Array( bytes.slice(i * singleSlicePixelSize, (i + 1) * singleSlicePixelSize).buffer ); @@ -1048,6 +1046,14 @@ class SegmentationService extends PubSubService { const getSegImageId = (imageId: string): string => `segimage:${segmentationId}:${imageId}`; const referencedImageIds = displaySet.instances.reduce((imageIds, instance) => { + if (instance.NumberOfFrames > 1) { + const frameImageIds = []; + for (let frame = 1; frame <= instance.NumberOfFrames; frame++) { + frameImageIds.push(`${instance.imageId}&frame=${frame}`); + } + + return [...imageIds, ...frameImageIds]; + } return [...imageIds, instance.imageId]; }, []); diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx index 89d8162f703..256dd55662c 100644 --- a/modes/segmentation/src/index.tsx +++ b/modes/segmentation/src/index.tsx @@ -144,7 +144,7 @@ function modeFactory({ modeConfiguration }) { // that is not supported by the mode const modalitiesArray = modalities.split('\\'); if (modalitiesArray.length === 1) { - return !['SM', 'US', 'MG', 'OT', 'DOC', 'CR'].includes(modalitiesArray[0]); + return !['SM', 'US' /*,'MG'*/, 'OT', 'DOC', 'CR'].includes(modalitiesArray[0]); } return true; diff --git a/platform/core/src/utils/getImageIdsFromInstances.ts b/platform/core/src/utils/getImageIdsFromInstances.ts new file mode 100644 index 00000000000..fba1798b259 --- /dev/null +++ b/platform/core/src/utils/getImageIdsFromInstances.ts @@ -0,0 +1,15 @@ +export default function getImageIdsFromInstances(instances): string[] { + const isMultiFrame = instance => instance.NumberOfFrames > 1; + + return instances.reduce((imageIds, instance) => { + if (isMultiFrame(instance)) { + const frameImageIds = []; + for (let frame = 1; frame <= instance.NumberOfFrames; frame++) { + frameImageIds.push(`${instance.imageId}&frame=${frame}`); + } + + return [...imageIds, ...frameImageIds]; + } + return [...imageIds, instance.imageId]; + }, []); +} diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index d7862618e0b..e57b95e7928 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 getImageIdsFromInstances from './getImageIdsFromInstances'; // Commented out unused functionality. // Need to implement new mechanism for derived displaySets using the displaySetManager. @@ -80,6 +81,7 @@ const utils = { splitComma, getSplitParam, generateAcceptHeader, + getImageIdsFromInstances, }; export { @@ -112,6 +114,7 @@ export { splitComma, getSplitParam, generateAcceptHeader, + getImageIdsFromInstances, }; export default utils; diff --git a/platform/core/src/utils/isDisplaySetReconstructable.js b/platform/core/src/utils/isDisplaySetReconstructable.js index 2d36266c4fa..09f765a951a 100644 --- a/platform/core/src/utils/isDisplaySetReconstructable.js +++ b/platform/core/src/utils/isDisplaySetReconstructable.js @@ -82,6 +82,20 @@ function isNMReconstructable(multiFrameInstance) { return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO'; } +function hasUniquePositions(multiFrameInstance) { + const { PerFrameFunctionalGroupsSequence = [] } = multiFrameInstance; + + const uniqueImagePositionPatients = new Set(); + + for (let frame = 0; frame < PerFrameFunctionalGroupsSequence.length; frame++) { + const perFrameSequence = PerFrameFunctionalGroupsSequence[frame]; + const imagePositionPatient = perFrameSequence.PlanePositionSequence?.ImagePositionPatient; + uniqueImagePositionPatients.add(imagePositionPatient?.toString()); + } + + return uniqueImagePositionPatients.size === multiFrameInstance.NumberOfFrames; +} + function processMultiframe(multiFrameInstance) { // If we don't have the PixelMeasuresSequence, then the pixel spacing and // slice thickness isn't specified or is changing and we can't reconstruct @@ -100,6 +114,11 @@ function processMultiframe(multiFrameInstance) { return { value: false }; } + if (!hasUniquePositions(multiFrameInstance)) { + console.log('Positions of all the frames are not unique, not reconstructable'); + return { value: false }; + } + if (multiFrameInstance.Modality.includes('NM') && !isNMReconstructable(multiFrameInstance)) { return { value: false }; } diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx index cea71e8f14e..a04f7205fa3 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx @@ -18,6 +18,7 @@ const SegmentationGroup = ({ storeSegmentation, onSegmentAdd, onToggleSegmentationVisibility, + onSegmentFocusClick, onSegmentClick, onSegmentDelete, onSegmentEdit, @@ -78,6 +79,7 @@ const SegmentationGroup = ({ disableEditing={disableEditing} isLocked={isLocked} isVisible={isVisible} + onFocusClick={onSegmentFocusClick} onClick={onSegmentClick} onEdit={onSegmentEdit} onDelete={onSegmentDelete} @@ -122,6 +124,7 @@ SegmentationGroup.propTypes = { onSegmentationDownload: PropTypes.func.isRequired, onSegmentationDownloadRTSS: PropTypes.func.isRequired, storeSegmentation: PropTypes.func.isRequired, + onSegmentFocusClick: PropTypes.func, onSegmentClick: PropTypes.func.isRequired, onSegmentAdd: PropTypes.func.isRequired, onSegmentDelete: PropTypes.func.isRequired, diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx index c404208cb63..b182b5372bd 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx @@ -14,6 +14,7 @@ const SegmentItem = ({ showDelete, disableEditing, isLocked = false, + onFocusClick, onClick, onEdit, onDelete, @@ -24,12 +25,6 @@ const SegmentItem = ({ }) => { 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 ( @@ -183,7 +178,7 @@ const HoveringIcons = ({ return (
- {createIcon('tool-zoom', onFocusClick)} + {onFocusClick && createIcon('tool-zoom', onFocusClick)} {!disableEditing && createIcon('row-edit', onEdit)} {!disableEditing && createIcon( @@ -210,6 +205,7 @@ SegmentItem.propTypes = { isActive: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired, isLocked: PropTypes.bool, + onFocusClick: PropTypes.func, onClick: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx index a7a22f23aeb..5c585646262 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx @@ -26,6 +26,7 @@ const SegmentationGroupTable = ({ onSegmentationDownloadRTSS, storeSegmentation, // segment handlers + onSegmentFocusClick, onSegmentClick, onSegmentAdd, onSegmentDelete, @@ -126,6 +127,7 @@ const SegmentationGroupTable = ({ storeSegmentation={storeSegmentation} onSegmentAdd={onSegmentAdd} onToggleSegmentationVisibility={onToggleSegmentationVisibility} + onSegmentFocusClick={onSegmentFocusClick} onSegmentClick={onSegmentClick} onSegmentDelete={onSegmentDelete} onSegmentEdit={onSegmentEdit} @@ -172,6 +174,7 @@ SegmentationGroupTable.propTypes = { onSegmentationDownload: PropTypes.func.isRequired, onSegmentationDownloadRTSS: PropTypes.func.isRequired, storeSegmentation: PropTypes.func.isRequired, + onSegmentFocusClick: PropTypes.func, onSegmentClick: PropTypes.func.isRequired, onSegmentAdd: PropTypes.func.isRequired, onSegmentDelete: PropTypes.func.isRequired,