fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set (#5900)

- Add FrameOfReferenceUID to SEG and RTSTRUCT displaySet in SOP Class Handlers so the FOR is available for filtering
- Sync optimisticOverlayDisplaySets when background display set changes so the overlay menu reflects the correct state after a background switch
- Add FOR matching guard to the hydrate segmentation synchronizer to prevent the hydration synchronizer from blindly mirroring segmentations from a source viewport to a target viewport if their primary Frames of Reference do not align.
- fix segmentation overlay order reversal on viewport re-render
This commit is contained in:
Ghadeer Albattarni authored and GitHub committed 2026-04-02 08:52:23 -04:00
1 parent a875d7de1f
commit b9029ef6f8
15 files changed
+223 -56

No files matched your search

@@ -56,6 +56,7 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID,
SOPClassHandlerId,
SOPClassUID,
frameOfReferenceUID: null,
referencedImages: null,
referencedSeriesInstanceUID: null,
referencedDisplaySetInstanceUID: null,
@@ -95,6 +96,9 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.frameOfReferenceUID =
instance.ReferencedFrameOfReferenceSequence?.[0]?.FrameOfReferenceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets =
displaySetService.getDisplaySetsForReferences(referencedSeriesSequence);
@@ -113,6 +117,7 @@ function _getDisplaySetsFromSeries(
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
displaySet.frameOfReferenceUID = addedDisplaySet.frameOfReferenceUID;
unsubscribe();
}
}
@@ -121,6 +126,7 @@ function _getDisplaySetsFromSeries(
const [referencedDisplaySet] = referencedDisplaySets;
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
displaySet.frameOfReferenceUID = referencedDisplaySet.frameOfReferenceUID;
}
displaySet.load = ({ headers, createSegmentation = true }) =>
@@ -30,6 +30,7 @@ function _getDisplaySetsFromSeries(
SeriesDate,
StructureSetDate,
SOPClassUID,
FrameOfReferenceUID,
wadoRoot,
wadoUri,
wadoUriRoot,
@@ -49,6 +50,7 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID,
SOPClassHandlerId,
SOPClassUID,
frameOfReferenceUID: FrameOfReferenceUID,
referencedImages: null,
referencedSeriesInstanceUID: null,
referencedDisplaySetInstanceUID: null,
@@ -104,6 +106,7 @@ function _getDisplaySetsFromSeries(
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
displaySet.frameOfReferenceUID = addedDisplaySet.frameOfReferenceUID;
unsubscribe();
}
}
@@ -111,6 +114,7 @@ function _getDisplaySetsFromSeries(
} else {
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
displaySet.frameOfReferenceUID = referencedDisplaySet.frameOfReferenceUID;
}
displaySet.load = async ({ headers }) =>
@@ -230,7 +230,7 @@ async function _load(
function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
return (
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.frameOfReferenceUID
);
}
@@ -145,7 +145,7 @@ export default function hydrateStructuredReport(
for (const FrameOfReferenceUID of frameOfReferenceUIDs) {
const displaySetsFOR = displaySetService.getDisplaySetsBy(
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID && !ds.isDerivedDisplaySet
ds => ds.frameOfReferenceUID === FrameOfReferenceUID && !ds.isDerivedDisplaySet
);
const ds = chooseDisplaySet(displaySetsFOR, FrameOfReferenceUID);
if (!ds) {
@@ -335,7 +335,7 @@ function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) {
const { points } = toolData.annotation.data.handles;
const { displaySetService } = servicesManager.services;
const displaySetsFOR = displaySetService.getDisplaySetsBy(
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID
ds => ds.frameOfReferenceUID === FrameOfReferenceUID
);
if (!displaySetsFOR.length || !points?.length) {
return {
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
Icons,
@@ -43,6 +43,10 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
useEffect(() => {
setOptimisticOverlayDisplaySets(overlayDisplaySets);
}, [backgroundDisplaySet?.displaySetInstanceUID, overlayDisplaySets]);
/**
* Change the background display set
*/
@@ -482,7 +486,10 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
}
}}
>
<SelectTrigger className="flex-1">
<SelectTrigger
className="flex-1"
data-cy={`overlay-background-ds-select-${viewportId}`}
>
<SelectValue>
{(
backgroundDisplaySet?.SeriesDescription ||
@@ -80,8 +80,8 @@ export function getEnhancedDisplaySets({ viewportId, services }) {
// Check if Frame of Reference matches
if (
displaySet.FrameOfReferenceUID &&
displaySet.FrameOfReferenceUID !== backgroundDisplaySet.FrameOfReferenceUID
displaySet.frameOfReferenceUID &&
displaySet.frameOfReferenceUID !== backgroundDisplaySet.frameOfReferenceUID
) {
return {
...displaySet,
@@ -82,7 +82,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
FrameOfReferenceUID,
frameOfReferenceUID: FrameOfReferenceUID,
SOPClassHandlerId,
SOPClassUID,
SeriesDescription: SeriesDescription || 'Microscopy Data',
@@ -66,7 +66,15 @@ const segmentationRepresentationModifiedCallback = async (
const sharedDisplaySetExists = isAnyDisplaySetCommon(sourceDisplaySetUIDs, targetDisplaySetUIDs);
if (!sharedDisplaySetExists && !viewport.getFrameOfReferenceUID()) {
const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID();
const sourceFrameOfReferenceUID =
getEnabledElementByViewportId(sourceViewportId)?.viewport?.getFrameOfReferenceUID();
if (!sharedDisplaySetExists && !targetFrameOfReferenceUID) {
return;
}
if (!sharedDisplaySetExists && targetFrameOfReferenceUID !== sourceFrameOfReferenceUID) {
return;
}
@@ -870,28 +870,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
}
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo);
await viewport.setStack(imageIds, initialImageIndexToUse);
viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo);
if (overlayProcessingResults?.length) {
overlayProcessingResults.forEach(overlayProcessingResult => {
if (overlayProcessingResult?.addOverlayFn) {
overlayProcessingResult.addOverlayFn();
}
});
}
await this._addOverlayRepresentations(overlayProcessingResults);
if (displayArea) {
viewport.setDisplayArea(displayArea);
}
if (rotation) {
viewport.setProperties({ rotation });
}
if (flipHorizontal) {
viewport.setCamera({ flipHorizontal: true });
}
});
if (displayArea) {
viewport.setDisplayArea(displayArea);
}
if (rotation) {
viewport.setProperties({ rotation });
}
if (flipHorizontal) {
viewport.setCamera({ flipHorizontal: true });
}
}
private _getInitialImageIndexForViewport(
@@ -1120,14 +1113,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
await viewport.setVolumes(volumeInputArray);
if (overlayProcessingResults?.length) {
overlayProcessingResults.forEach(({ addOverlayFn }) => {
if (addOverlayFn) {
addOverlayFn();
}
});
}
await this._addOverlayRepresentations(overlayProcessingResults);
viewport.render();
volumesProperties.forEach(({ properties, volumeId }) => {
@@ -1193,7 +1179,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
private addOverlayRepresentationForDisplaySet(
displaySet: OhifTypes.DisplaySet,
viewport: Types.IViewport
) {
): Promise<void> {
const { segmentationService } = this.servicesManager.services;
const segmentationId = displaySet.displaySetInstanceUID;
@@ -1203,18 +1189,36 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
: csToolsEnums.SegmentationRepresentations.Contour;
const { predecessorImageId } = displaySet;
segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
predecessorImageId,
type: representationType,
config: {
blendMode:
viewport?.getBlendMode?.() === 1 ? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND : undefined,
},
});
const segmentationRepresentationPromise = segmentationService.addSegmentationRepresentation(
viewport.id,
{
segmentationId,
predecessorImageId,
type: representationType,
config: {
blendMode:
viewport?.getBlendMode?.() === 1
? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND
: undefined,
},
}
);
// store the segmentation presentation id in the viewport info
this.storePresentation({ viewportId: viewport.id });
return segmentationRepresentationPromise;
}
private async _addOverlayRepresentations(
overlayProcessingResults?: Array<{ addOverlayFn?: () => Promise<void> }>
): Promise<void> {
if (!overlayProcessingResults?.length) {
return;
}
for (const overlayProcessingResult of overlayProcessingResults) {
if (overlayProcessingResult?.addOverlayFn) {
await overlayProcessingResult.addOverlayFn();
}
}
}
// Todo: keepCamera is an interim solution until we have a better solution for
@@ -1272,7 +1276,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
if (viewport instanceof ECGViewport) {
return this._setEcgViewport(viewport as unknown as Types.IECGViewport, viewportData as StackViewportData);
return this._setEcgViewport(
viewport as unknown as Types.IECGViewport,
viewportData as StackViewportData
);
}
return this._setOtherViewport(
@@ -113,7 +113,7 @@ const makeDisplaySet = (instances, index) => {
label:
instance.SeriesDescription ||
`${i18n.t('Series')} ${instance.SeriesNumber} - ${i18n.t(instance.Modality)}`,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
frameOfReferenceUID: instance.FrameOfReferenceUID,
});
const imageIds = dataSource.getImageIdsForDisplaySet(imageSet);
@@ -329,16 +329,16 @@ export default class MicroscopyService extends PubSubService {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
const smDisplaySet = displaySets.find(ds => ds.Modality === 'SM');
const { FrameOfReferenceUID, othersFrameOfReferenceUID } = smDisplaySet;
const { frameOfReferenceUID, othersFrameOfReferenceUID } = smDisplaySet;
if (!studyMetadata) {
return;
}
let derivedDisplaySets = FrameOfReferenceUID
let derivedDisplaySets = frameOfReferenceUID
? displaySets.filter(
ds =>
ds.ReferencedFrameOfReferenceUID === FrameOfReferenceUID ||
ds.ReferencedFrameOfReferenceUID === frameOfReferenceUID ||
// sometimes each depth instance has the different FrameOfReferenceID
othersFrameOfReferenceUID.includes(ds.ReferencedFrameOfReferenceUID)
)
@@ -22,7 +22,7 @@ export default function getSourceDisplaySet(allDisplaySets, microscopySRDisplayS
const referencedDisplaySet = otherDisplaySets.find(
displaySet =>
displaySet.Modality === 'SM' &&
(displaySet.FrameOfReferenceUID === ReferencedFrameOfReferenceUID ||
(displaySet.frameOfReferenceUID === ReferencedFrameOfReferenceUID ||
// sometimes each depth instance has the different FrameOfReferenceID
displaySet.othersFrameOfReferenceUID.includes(ReferencedFrameOfReferenceUID))
);