fix(sr-hydration): enable hydration and arrow navigation for 3D SR measurements (#5887)
Joe is away, so approving based on the code having the requested change, and otherwise looking good/passing tests. * fix(sr-hydration): enable hydration and arrows navigation for 3D SR measurements * test: add automated test for SR measurement navigation with arrows after hydration * add cross-study warning in the 3D branch * test: address reviewer feedback for the test * fix: support 3D and 2D annotations for SR hydration * test: improve navigation to first image --------- Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
parent
40a472a9b1
commit
7a38903b19
@ -80,7 +80,7 @@ export default function hydrateStructuredReport(
|
|||||||
const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement;
|
const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement;
|
||||||
const key = `${ReferencedSOPInstanceUID}:${frameNumber}`;
|
const key = `${ReferencedSOPInstanceUID}:${frameNumber}`;
|
||||||
|
|
||||||
if (!sopInstanceUIDToImageId[key]) {
|
if (imageId && !sopInstanceUIDToImageId[key]) {
|
||||||
sopInstanceUIDToImageId[key] = imageId;
|
sopInstanceUIDToImageId[key] = imageId;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -118,41 +118,17 @@ export default function hydrateStructuredReport(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the series touched as tracked.
|
|
||||||
const imageIds = [];
|
|
||||||
|
|
||||||
// TODO: notification if no hydratable?
|
|
||||||
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
|
||||||
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
|
||||||
|
|
||||||
toolDataForAnnotationType.forEach(toolData => {
|
|
||||||
// Add the measurement to toolState
|
|
||||||
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
|
||||||
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
|
||||||
// in the wrong value.
|
|
||||||
const frameNumber = toolData.annotation.data?.frameNumber || 1;
|
|
||||||
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
|
|
||||||
|
|
||||||
if (!imageIds.includes(imageId)) {
|
|
||||||
imageIds.push(imageId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let targetStudyInstanceUID;
|
let targetStudyInstanceUID;
|
||||||
const SeriesInstanceUIDs = [];
|
const SeriesInstanceUIDs = [];
|
||||||
|
|
||||||
for (let i = 0; i < imageIds.length; i++) {
|
// Set the series touched as tracked.
|
||||||
const imageId = imageIds[i];
|
const imageIds = getImageIds(hydratableMeasurementsInSR, sopInstanceUIDToImageId);
|
||||||
if (!imageId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
|
|
||||||
|
|
||||||
|
for (const imageId of imageIds) {
|
||||||
|
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
|
||||||
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
||||||
SeriesInstanceUIDs.push(SeriesInstanceUID);
|
SeriesInstanceUIDs.push(SeriesInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetStudyInstanceUID) {
|
if (!targetStudyInstanceUID) {
|
||||||
targetStudyInstanceUID = StudyInstanceUID;
|
targetStudyInstanceUID = StudyInstanceUID;
|
||||||
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
|
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
|
||||||
@ -160,6 +136,31 @@ export default function hydrateStructuredReport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For 3d annotations there are no image IDs,
|
||||||
|
// so we need to find the display sets by frame of reference to get the SeriesInstanceUIDs
|
||||||
|
const frameOfReferenceUIDs = getFrameOfReferenceUIDs(
|
||||||
|
hydratableMeasurementsInSR,
|
||||||
|
sopInstanceUIDToImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const FrameOfReferenceUID of frameOfReferenceUIDs) {
|
||||||
|
const displaySetsFOR = displaySetService.getDisplaySetsBy(
|
||||||
|
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID && !ds.isDerivedDisplaySet
|
||||||
|
);
|
||||||
|
const ds = chooseDisplaySet(displaySetsFOR, FrameOfReferenceUID);
|
||||||
|
if (!ds) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!SeriesInstanceUIDs.includes(ds.SeriesInstanceUID)) {
|
||||||
|
SeriesInstanceUIDs.push(ds.SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
if (!targetStudyInstanceUID) {
|
||||||
|
targetStudyInstanceUID = ds.StudyInstanceUID;
|
||||||
|
} else if (targetStudyInstanceUID !== ds.StudyInstanceUID) {
|
||||||
|
console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets reference data for what frame of reference and the referenced
|
* Gets reference data for what frame of reference and the referenced
|
||||||
* image id, or for 3d measurements, the volumeId to apply this annotation to.
|
* image id, or for 3d measurements, the volumeId to apply this annotation to.
|
||||||
@ -196,7 +197,7 @@ export default function hydrateStructuredReport(
|
|||||||
toolDataForAnnotationType.forEach(toolData => {
|
toolDataForAnnotationType.forEach(toolData => {
|
||||||
toolData.uid = guid();
|
toolData.uid = guid();
|
||||||
const referenceData = getReferenceData(toolData);
|
const referenceData = getReferenceData(toolData);
|
||||||
const { imageId } = referenceData;
|
const { referencedImageId } = referenceData;
|
||||||
|
|
||||||
const annotation = {
|
const annotation = {
|
||||||
annotationUID: toolData.annotation.annotationUID,
|
annotationUID: toolData.annotation.annotationUID,
|
||||||
@ -241,8 +242,8 @@ export default function hydrateStructuredReport(
|
|||||||
locking.setAnnotationLocked(newAnnotationUID, true);
|
locking.setAnnotationLocked(newAnnotationUID, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageId && !imageIds.includes(imageId)) {
|
if (referencedImageId && !imageIds.includes(referencedImageId)) {
|
||||||
imageIds.push(imageId);
|
imageIds.push(referencedImageId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -255,15 +256,64 @@ export default function hydrateStructuredReport(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the unique imageIds from hydratable measurements that have an imageId reference
|
||||||
|
* (i.e., 2D/SCOORD annotations).
|
||||||
|
*/
|
||||||
|
function getImageIds(hydratableMeasurementsInSR, sopInstanceUIDToImageId): string[] {
|
||||||
|
const imageIds: string[] = [];
|
||||||
|
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
||||||
|
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
||||||
|
|
||||||
|
toolDataForAnnotationType.forEach(toolData => {
|
||||||
|
// Add the measurement to toolState
|
||||||
|
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
||||||
|
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
||||||
|
// in the wrong value.
|
||||||
|
const frameNumber = toolData.annotation.data?.frameNumber || 1;
|
||||||
|
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
|
||||||
|
|
||||||
|
if (imageId && !imageIds.includes(imageId)) {
|
||||||
|
imageIds.push(imageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return imageIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the unique FrameOfReferenceUIDs from hydratable measurements that have no imageId reference
|
||||||
|
* (i.e., 3D/SCOORD3D annotations). This excludes annotations handled by the getImageIds function.
|
||||||
|
*/
|
||||||
|
function getFrameOfReferenceUIDs(hydratableMeasurementsInSR, sopInstanceUIDToImageId): string[] {
|
||||||
|
const frameOfReferenceUIDs: string[] = [];
|
||||||
|
|
||||||
|
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
||||||
|
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
||||||
|
toolDataForAnnotationType.forEach(toolData => {
|
||||||
|
const frameNumber = toolData.annotation.data?.frameNumber || 1;
|
||||||
|
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
const { FrameOfReferenceUID } = toolData.annotation.metadata;
|
||||||
|
if (FrameOfReferenceUID && !frameOfReferenceUIDs.includes(FrameOfReferenceUID)) {
|
||||||
|
frameOfReferenceUIDs.push(FrameOfReferenceUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return frameOfReferenceUIDs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For 3d annotations, there are often several display sets which could
|
* For 3d annotations, there are often several display sets which could
|
||||||
* be used to display the annotation. Choose the first annotation with the
|
* be used to display the annotation. Choose the first annotation with the
|
||||||
* same frame of reference that is reconstructable, or the first display set
|
* same frame of reference that is reconstructable, or the first display set
|
||||||
* otherwise.
|
* otherwise.
|
||||||
*/
|
*/
|
||||||
function chooseDisplaySet(displaySets, annotation) {
|
function chooseDisplaySet(displaySets, reference) {
|
||||||
if (!displaySets?.length) {
|
if (!displaySets?.length) {
|
||||||
console.warn('No display set found for', annotation);
|
console.warn('No display set found for', reference);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (displaySets.length === 1) {
|
if (displaySets.length === 1) {
|
||||||
|
|||||||
94
tests/SRHydrationArrowNavigation.spec.ts
Normal file
94
tests/SRHydrationArrowNavigation.spec.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { expect, navigateWithViewportArrow, test, visitStudy } from './utils';
|
||||||
|
import { expectRowSelected } from './utils/assertions';
|
||||||
|
import { press } from './utils/keyboardUtils';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7310.5101.860473186348887719777907797922';
|
||||||
|
const mode = 'viewer';
|
||||||
|
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate SR measurements with next/prev arrows after hydration for 3D SR', async ({
|
||||||
|
page,
|
||||||
|
DOMOverlayPageObject,
|
||||||
|
leftPanelPageObject,
|
||||||
|
rightPanelPageObject,
|
||||||
|
viewportPageObject,
|
||||||
|
}) => {
|
||||||
|
await rightPanelPageObject.toggle();
|
||||||
|
await rightPanelPageObject.measurementsPanel.select();
|
||||||
|
await leftPanelPageObject.loadSeriesByModality('SR');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const measurementCount = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
|
||||||
|
expect(measurementCount).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
// Navigate to first image
|
||||||
|
await viewportPageObject.active.pane.click();
|
||||||
|
await press({ page, key: 'Home' });
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// first arrow click should navigate to second measurement
|
||||||
|
await navigateWithViewportArrow(viewportPageObject, 'next');
|
||||||
|
|
||||||
|
await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(1));
|
||||||
|
|
||||||
|
await expect(viewportPageObject.active.svg('circle')).toBeVisible();
|
||||||
|
|
||||||
|
const secondAnnotation = viewportPageObject.active.nthAnnotation(0);
|
||||||
|
await expect(secondAnnotation.locator).toBeVisible();
|
||||||
|
await expect(secondAnnotation.text.locator).toBeVisible();
|
||||||
|
|
||||||
|
// navigate back to first measurement
|
||||||
|
await navigateWithViewportArrow(viewportPageObject, 'prev');
|
||||||
|
|
||||||
|
await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0));
|
||||||
|
|
||||||
|
await expect(viewportPageObject.active.svg('circle')).toBeVisible();
|
||||||
|
|
||||||
|
const firstAnnotation = viewportPageObject.active.nthAnnotation(0);
|
||||||
|
await expect(firstAnnotation.locator).toBeVisible();
|
||||||
|
await expect(firstAnnotation.text.locator).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should keep arrows visible and functional after clicking measurement in right panel', async ({
|
||||||
|
page,
|
||||||
|
DOMOverlayPageObject,
|
||||||
|
leftPanelPageObject,
|
||||||
|
rightPanelPageObject,
|
||||||
|
viewportPageObject,
|
||||||
|
}) => {
|
||||||
|
await rightPanelPageObject.toggle();
|
||||||
|
await rightPanelPageObject.measurementsPanel.select();
|
||||||
|
await leftPanelPageObject.loadSeriesByModality('SR');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const measurementCount = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
|
||||||
|
expect(measurementCount).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
// click on first measurement
|
||||||
|
await rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0).click();
|
||||||
|
|
||||||
|
await expect(viewportPageObject.active.nthAnnotation(0).locator).toBeVisible();
|
||||||
|
|
||||||
|
await expect(viewportPageObject.active.navigationArrows.next.button).toBeVisible();
|
||||||
|
await expect(viewportPageObject.active.navigationArrows.prev.button).toBeVisible();
|
||||||
|
|
||||||
|
// navigate to second measurement
|
||||||
|
await navigateWithViewportArrow(viewportPageObject, 'next');
|
||||||
|
|
||||||
|
await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(1));
|
||||||
|
await expect(viewportPageObject.active.nthAnnotation(0).locator).toBeVisible();
|
||||||
|
|
||||||
|
// navigate back to first measurement
|
||||||
|
await navigateWithViewportArrow(viewportPageObject, 'prev');
|
||||||
|
|
||||||
|
await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0));
|
||||||
|
await expect(viewportPageObject.active.nthAnnotation(0).locator).toBeVisible();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user