fix(segmentation overlays): Allow for the addition of multiple segmentation overlays. (#5189)

This commit is contained in:
Joe Boccanfuso 2025-07-10 09:33:39 -04:00 committed by GitHub
parent 32ab291305
commit 48473625f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 187 additions and 44 deletions

View File

@ -650,26 +650,28 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
csToolsUtils.stackContextPrefetch.enable(element); csToolsUtils.stackContextPrefetch.enable(element);
}); });
let imageIdsToSet = imageIds; const overlayProcessingResults = this._processExtraDisplaySetsForViewport(viewport);
const overlayProcessingResult = this._processExtraDisplaySetsForViewport(viewport);
imageIdsToSet = overlayProcessingResult?.imageIds ?? imageIdsToSet;
const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId; const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId;
if (referencedImageId) { if (referencedImageId) {
initialImageIndexToUse = imageIdsToSet.indexOf(referencedImageId); initialImageIndexToUse = imageIds.indexOf(referencedImageId);
} }
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) { if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
} }
return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => { return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties }); viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo); this.setPresentations(viewport.id, presentations, viewportInfo);
if (overlayProcessingResults?.length) {
overlayProcessingResults.forEach(overlayProcessingResult => {
if (overlayProcessingResult?.addOverlayFn) { if (overlayProcessingResult?.addOverlayFn) {
overlayProcessingResult.addOverlayFn(); overlayProcessingResult.addOverlayFn();
} }
});
}
if (displayArea) { if (displayArea) {
viewport.setDisplayArea(displayArea); viewport.setDisplayArea(displayArea);
@ -874,9 +876,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}); });
// For SEG and RT viewports // For SEG and RT viewports
const { addOverlayFn, imageIds } = this._processExtraDisplaySetsForViewport(viewport) || {}; const overlayProcessingResults = this._processExtraDisplaySetsForViewport(viewport) || [];
if (!filteredVolumeInputArray.length && overlayProcessingResults?.length) {
if (!filteredVolumeInputArray.length && addOverlayFn) { overlayProcessingResults.forEach(({ imageIds, addOverlayFn }) => {
if (addOverlayFn) {
// if there is no volume input array, and there is an addOverlayFn, means we need to take // if there is no volume input array, and there is an addOverlayFn, means we need to take
// care of the background overlay display set first then the addOverlayFn will add the // care of the background overlay display set first then the addOverlayFn will add the
// SEG displaySet // SEG displaySet
@ -891,12 +894,18 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
throw new Error('Background display set not found'); throw new Error('Background display set not found');
} }
} }
});
}
await viewport.setVolumes(volumeInputArray); await viewport.setVolumes(volumeInputArray);
if (overlayProcessingResults?.length) {
overlayProcessingResults.forEach(({ addOverlayFn }) => {
if (addOverlayFn) { if (addOverlayFn) {
addOverlayFn(); addOverlayFn();
} }
});
}
viewport.render(); viewport.render();
volumesProperties.forEach(({ properties, volumeId }) => { volumesProperties.forEach(({ properties, volumeId }) => {
@ -934,18 +943,19 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
// Find overlay display sets (e.g. SEG, RTSTRUCT) // Find overlay display sets (e.g. SEG, RTSTRUCT)
const overlayDisplaySet = displaySetInstanceUIDs const overlayDisplaySets = displaySetInstanceUIDs
.map(displaySetService.getDisplaySetByUID) .map(displaySetService.getDisplaySetByUID)
.find(displaySet => displaySet?.isOverlayDisplaySet); .filter(displaySet => displaySet?.isOverlayDisplaySet);
// if it is only the overlay displaySet, then we need to get the reference // if it is only the overlay displaySet, then we need to get the reference
// displaySet imageIds and set them as the imageIds for the viewport, // displaySet imageIds and set them as the imageIds for the viewport,
// here we can do some logic if the reference is missing // here we can do some logic if the reference is missing
// then find the most similar match of displaySet instead // then find the most similar match of displaySet instead
if (!overlayDisplaySet) { if (!overlayDisplaySets?.length) {
return; return;
} }
return overlayDisplaySets.map(overlayDisplaySet => {
let imageIds; let imageIds;
if (overlayDisplaySet.referencedDisplaySetInstanceUID) { if (overlayDisplaySet.referencedDisplaySetInstanceUID) {
const referenceDisplaySet = displaySetService.getDisplaySetByUID( const referenceDisplaySet = displaySetService.getDisplaySetByUID(
@ -953,11 +963,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
); );
imageIds = referenceDisplaySet.images.map(image => image.imageId); imageIds = referenceDisplaySet.images.map(image => image.imageId);
} }
return { return {
imageIds, imageIds,
addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport), addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport),
}; };
});
} }
private addOverlayRepresentationForDisplaySet( private addOverlayRepresentationForDisplaySet(

View File

@ -41,6 +41,10 @@ class UINotificationService {
* @returns undefined * @returns undefined
*/ */
public hide(id: string) { public hide(id: string) {
if (process.env.TEST_ENV === 'true') {
return;
}
return serviceImplementation._hide(id); return serviceImplementation._hide(id);
} }
@ -105,6 +109,10 @@ class UINotificationService {
onClick: () => void; onClick: () => void;
}; };
}): string { }): string {
if (process.env.TEST_ENV === 'true') {
return;
}
if (promise && promiseMessages) { if (promise && promiseMessages) {
const loadingId = serviceImplementation._show({ const loadingId = serviceImplementation._show({
title, title,

View File

@ -21,7 +21,7 @@ export default defineConfig({
use: { use: {
baseURL: 'http://localhost:3335', baseURL: 'http://localhost:3335',
trace: 'on-first-retry', trace: 'on-first-retry',
video: 'on', video: 'on-first-retry',
testIdAttribute: 'data-cy', testIdAttribute: 'data-cy',
actionTimeout: 10_000, actionTimeout: 10_000,
}, },

View File

@ -35,9 +35,9 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
// Wait 5 seconds for RT to load. This is necessary in particular when screen shots are added or replaced. // Wait 5 seconds for RT to load. This is necessary in particular when screen shots are added or replaced.
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
await checkForScreenshot( await checkForScreenshot({
page, page,
page, screenshotPath: screenShotPaths.mprThenRTOverlayNoHydration.mprPostRTOverlayNoHydration,
screenShotPaths.mprThenRTOverlayNoHydration.mprPostRTOverlayNoHydration normalizedClip: { x: 0, y: 0, width: 1.0, height: 0.75 }, // clip to avoid any popups concerning surface creation and clipping
); });
}); });

View File

@ -0,0 +1,77 @@
import { test } from 'playwright-test-coverage';
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
import { press } from './utils/keyboardUtils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test('should display multiple segmentation overlays (both SEG and RT)', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click();
// Add multiple segmentation overlays and ensure the overlay menu reflects this change.
await page.getByTestId('dataOverlayMenu-default-btn').click();
await page.getByTestId('AddSegmentationDataOverlay-default').click();
await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('2d-tta_nnU-Net_Segmentation').click();
await page.getByTestId('AddSegmentationDataOverlay-default').click();
await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('Segmentation').click();
await page.getByTestId('AddSegmentationDataOverlay-default').click();
await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('3d_lowres-tta_nnU-Net_Segmentation').click();
await checkForScreenshot({
page,
screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.threeSegOverlaysInOverlayMenu,
});
// Hide the overlay menu and then show it again. The overlays from before should still be displayed.
await page.getByTestId('dataOverlayMenu-default-btn').click(); // hide
await page.getByTestId('dataOverlayMenu-default-btn').click(); // show
await checkForScreenshot({
page,
screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.threeSegOverlaysInOverlayMenu,
});
await page.getByTestId('dataOverlayMenu-default-btn').click(); // hide
// Navigate to image 56.
await press({ page, key: 'ArrowDown', nTimes: 55 });
await page.waitForTimeout(5000);
await checkForScreenshot({
page,
screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaysDisplayed,
});
// Now add the RT overlay
await page.getByTestId('dataOverlayMenu-default-btn').click();
await page.getByTestId('AddSegmentationDataOverlay-default').click();
await page.getByText('SELECT A SEGMENTATION').click();
await page.getByTestId('Series 3 - RTSTRUCT').click();
await page.waitForTimeout(5000);
await checkForScreenshot({
page,
screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaySEGsAndRTDisplayed,
});
// Hide the overlay menu and then show it again. The overlays from before should still be displayed.
await page.getByTestId('dataOverlayMenu-default-btn').click(); // hide
await page.getByTestId('dataOverlayMenu-default-btn').click(); // show
await checkForScreenshot({
page,
screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaySEGsAndRTDisplayed,
});
});

View File

@ -19,6 +19,8 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
// Hide the overlay menu. // Hide the overlay menu.
await page.getByTestId('dataOverlayMenu-default-btn').click(); await page.getByTestId('dataOverlayMenu-default-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot( await checkForScreenshot(
page, page,
page, page,
@ -28,6 +30,8 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
await page.waitForTimeout(5000);
await checkForScreenshot( await checkForScreenshot(
page, page,
page, page,

View File

@ -13,14 +13,20 @@ test('should hydrate an RTSTRUCT from MPR', async ({ page }) => {
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprBeforeRT); await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprBeforeRT);
await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRT); await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRT);
await page.getByTestId('yes-hydrate-btn').click(); await page.getByTestId('yes-hydrate-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRTHydrated); await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRTHydrated);
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();

View File

@ -13,11 +13,15 @@ test('should hydrate an RTSTRUCT and then launch MPR', async ({ page }) => {
await page.getByTestId('yes-hydrate-btn').click(); await page.getByTestId('yes-hydrate-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydrationThenMPR.rtPostHydration); await checkForScreenshot(page, page, screenShotPaths.rtHydrationThenMPR.rtPostHydration);
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('Axial Primary').click(); await page.getByTestId('Axial Primary').click();
await page.waitForTimeout(5000);
await checkForScreenshot( await checkForScreenshot(
page, page,
page, page,

View File

@ -11,10 +11,14 @@ test('should launch MPR with unhydrated RTSTRUCT', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click(); await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPreMPR); await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPreMPR);
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPostMPR); await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPostMPR);
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -9,6 +9,12 @@ type CheckForScreenshotProps = {
delay?: number; delay?: number;
maxDiffPixelRatio?: number; maxDiffPixelRatio?: number;
threshold?: number; threshold?: number;
normalizedClip?: {
x: number;
y: number;
width: number;
height: number;
};
}; };
const _checkForScreenshot = async (props: CheckForScreenshotProps) => { const _checkForScreenshot = async (props: CheckForScreenshotProps) => {
@ -20,15 +26,34 @@ const _checkForScreenshot = async (props: CheckForScreenshotProps) => {
delay = 500, delay = 500,
maxDiffPixelRatio = 0.02, maxDiffPixelRatio = 0.02,
threshold = 0.05, threshold = 0.05,
normalizedClip,
} = props; } = props;
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
for (let i = 0; i < attempts; i++) { for (let i = 0; i < attempts; i++) {
try { try {
let clip;
if (normalizedClip) {
let boundingBox;
if (locator === page) {
boundingBox = { x: 0, y: 0, ...(await page.viewportSize()) };
} else {
boundingBox = await (locator as Locator).boundingBox();
}
clip = {
x: normalizedClip.x * boundingBox.width,
y: normalizedClip.y * boundingBox.height,
width: normalizedClip.width * boundingBox.width,
height: normalizedClip.height * boundingBox.height,
};
}
await expect(locator).toHaveScreenshot(screenshotPath, { await expect(locator).toHaveScreenshot(screenshotPath, {
maxDiffPixelRatio, maxDiffPixelRatio,
threshold, threshold,
clip,
}); });
return true; return true;
} catch (error) { } catch (error) {

View File

@ -173,6 +173,11 @@ const screenShotPaths = {
overlaySegmentation: 'overlaySegmentation.png', overlaySegmentation: 'overlaySegmentation.png',
noOverlay: 'noOverlay.png', noOverlay: 'noOverlay.png',
}, },
multipleSegmentationDataOverlays: {
threeSegOverlaysInOverlayMenu: 'threeSegOverlaysInOverlayMenu.png',
overlaysDisplayed: 'overlaysDisplayed.png',
overlaySEGsAndRTDisplayed: 'overlaySEGsAndRTDisplayed.png',
},
}; };
export { screenShotPaths }; export { screenShotPaths };