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

@@ -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(