From 281c4dcf4c7bda840ce61434a7c7885de643d9ae Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 7 Jan 2025 21:57:39 -0500 Subject: [PATCH 1/7] fix: Start moving adapters to shared base file to allow for updates --- .../adapters/Cornerstone3D/BaseAdapter3D.ts | 86 ++++++++++++++ .../Cornerstone3D/{Length.js => Length.ts} | 32 ++---- .../src/adapters/Cornerstone3D/Probe.js | 106 ------------------ .../src/adapters/Cornerstone3D/Probe.ts | 68 +++++++++++ 4 files changed, 163 insertions(+), 129 deletions(-) create mode 100644 packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts rename packages/adapters/src/adapters/Cornerstone3D/{Length.js => Length.ts} (81%) delete mode 100644 packages/adapters/src/adapters/Cornerstone3D/Probe.js create mode 100644 packages/adapters/src/adapters/Cornerstone3D/Probe.ts diff --git a/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts new file mode 100644 index 0000000000..7cf91667bd --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts @@ -0,0 +1,86 @@ +import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; +import MeasurementReport from "./MeasurementReport"; + +/** + * This is a basic definition of adapters to be inherited for other adapters. + */ +export default class BaseAdapter3D { + public static toolType: string; + public static utilityToolType: string; + public static TID300Representation; + public static trackingIdentifierTextValue: string; + + /** + * @returns true if the tool is of the given tool type based on the tracking identifier + */ + public static isValidCornerstoneTrackingIdentifier = trackingIdentifier => { + if (!trackingIdentifier.includes(":")) { + return false; + } + + const [cornerstone3DTag, toolType] = trackingIdentifier.split(":"); + + if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { + return false; + } + + return toolType === this.toolType; + }; + + /** + * Returns annotation data for CS3D to use based on the underlying + * DICOM SR annotation data. + */ + public static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + _imageToWorldCoords, + metadata + ) { + const { defaultState: state, ReferencedFrameNumber } = + MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + this.toolType + ); + + state.annotation.data = { + cachedStats: {}, + frameNumber: ReferencedFrameNumber + }; + + return state; + } + + public static getTID300RepresentationArguments(tool, worldToImageCoords) { + const { data, metadata } = tool; + const { finding, findingSites } = tool; + const { referencedImageId } = metadata; + + if (!referencedImageId) { + throw new Error( + "Probe.getTID300RepresentationArguments: referencedImageId is not defined" + ); + } + + const { points } = data.handles; + + const pointsImage = points.map(point => { + const pointImage = worldToImageCoords(referencedImageId, point); + return { + x: pointImage[0], + y: pointImage[1] + }; + }); + + const TID300RepresentationArguments = { + points: pointsImage, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, + findingSites: findingSites || [], + finding + }; + + return TID300RepresentationArguments; + } +} diff --git a/packages/adapters/src/adapters/Cornerstone3D/Length.js b/packages/adapters/src/adapters/Cornerstone3D/Length.ts similarity index 81% rename from packages/adapters/src/adapters/Cornerstone3D/Length.js rename to packages/adapters/src/adapters/Cornerstone3D/Length.ts index 4d409ed264..9dd305a0d3 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Length.js +++ b/packages/adapters/src/adapters/Cornerstone3D/Length.ts @@ -1,13 +1,18 @@ import { utilities } from "dcmjs"; import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; const { Length: TID300Length } = utilities.TID300; const LENGTH = "Length"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${LENGTH}`; -class Length { +export default class Length extends BaseAdapter3D { + public static toolType = LENGTH; + public static utilityToolType = LENGTH; + public static TID300Representation = TID300Length; + public static trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${LENGTH}`; + // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport. static getMeasurementData( MeasurementGroup, @@ -20,7 +25,7 @@ class Length { MeasurementGroup, sopInstanceUIDToImageIdMap, metadata, - Length.toolType + this.toolType ); const referencedImageId = @@ -84,30 +89,11 @@ class Length { point1, point2, distance, - trackingIdentifierTextValue, + trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [] }; } } -Length.toolType = LENGTH; -Length.utilityToolType = LENGTH; -Length.TID300Representation = TID300Length; -Length.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === LENGTH; -}; - MeasurementReport.registerTool(Length); - -export default Length; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Probe.js b/packages/adapters/src/adapters/Cornerstone3D/Probe.js deleted file mode 100644 index 553ae3797c..0000000000 --- a/packages/adapters/src/adapters/Cornerstone3D/Probe.js +++ /dev/null @@ -1,106 +0,0 @@ -import { utilities } from "dcmjs"; -import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; -import MeasurementReport from "./MeasurementReport"; - -const { Point: TID300Point } = utilities.TID300; - -const PROBE = "Probe"; -const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PROBE}`; - -class Probe { - static getMeasurementData( - MeasurementGroup, - sopInstanceUIDToImageIdMap, - imageToWorldCoords, - metadata - ) { - const { defaultState, SCOORDGroup, ReferencedFrameNumber } = - MeasurementReport.getSetupMeasurementData( - MeasurementGroup, - sopInstanceUIDToImageIdMap, - metadata, - Probe.toolType - ); - - const referencedImageId = - defaultState.annotation.metadata.referencedImageId; - - const { GraphicData } = SCOORDGroup; - - const worldCoords = []; - for (let i = 0; i < GraphicData.length; i += 2) { - const point = imageToWorldCoords(referencedImageId, [ - GraphicData[i], - GraphicData[i + 1] - ]); - worldCoords.push(point); - } - - const state = defaultState; - - state.annotation.data = { - handles: { - points: worldCoords, - activeHandleIndex: null, - textBox: { - hasMoved: false - } - }, - frameNumber: ReferencedFrameNumber - }; - - return state; - } - - static getTID300RepresentationArguments(tool, worldToImageCoords) { - const { data, metadata } = tool; - let { finding, findingSites } = tool; - const { referencedImageId } = metadata; - - if (!referencedImageId) { - throw new Error( - "Probe.getTID300RepresentationArguments: referencedImageId is not defined" - ); - } - - const { points } = data.handles; - - const pointsImage = points.map(point => { - const pointImage = worldToImageCoords(referencedImageId, point); - return { - x: pointImage[0], - y: pointImage[1] - }; - }); - - const TID300RepresentationArguments = { - points: pointsImage, - trackingIdentifierTextValue, - findingSites: findingSites || [], - finding - }; - - return TID300RepresentationArguments; - } -} - -Probe.toolType = PROBE; -Probe.utilityToolType = PROBE; -Probe.TID300Representation = TID300Point; -Probe.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { - if (!TrackingIdentifier.includes(":")) { - return false; - } - - const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":"); - - if (cornerstone3DTag !== CORNERSTONE_3D_TAG) { - return false; - } - - return toolType === PROBE; -}; - -MeasurementReport.registerTool(Probe); - -export default Probe; diff --git a/packages/adapters/src/adapters/Cornerstone3D/Probe.ts b/packages/adapters/src/adapters/Cornerstone3D/Probe.ts new file mode 100644 index 0000000000..812b05c7c8 --- /dev/null +++ b/packages/adapters/src/adapters/Cornerstone3D/Probe.ts @@ -0,0 +1,68 @@ +import { utilities } from "dcmjs"; +import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; +import MeasurementReport from "./MeasurementReport"; +import BaseAdapter3D from "./BaseAdapter3D"; + +const { Point: TID300Point } = utilities.TID300; + +const PROBE = "Probe"; +const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PROBE}`; + +class Probe extends BaseAdapter3D { + public static toolType = PROBE; + public static utilityToolType = PROBE; + public static TID300Representation = TID300Point; + + static getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ) { + const state = super.getMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + imageToWorldCoords, + metadata + ); + + const { defaultState, SCOORDGroup } = + MeasurementReport.getSetupMeasurementData( + MeasurementGroup, + sopInstanceUIDToImageIdMap, + metadata, + Probe.toolType + ); + + const referencedImageId = + defaultState.annotation.metadata.referencedImageId; + + const { GraphicData } = SCOORDGroup; + + const worldCoords = []; + for (let i = 0; i < GraphicData.length; i += 2) { + const point = imageToWorldCoords(referencedImageId, [ + GraphicData[i], + GraphicData[i + 1] + ]); + worldCoords.push(point); + } + + state.annotation.data = { + ...state.annotation.data, + handles: { + points: worldCoords, + activeHandleIndex: null, + textBox: { + hasMoved: false + } + } + }; + + return state; + } +} + +MeasurementReport.registerTool(Probe); + +export default Probe; From 75f2ac6448045063294074d83bc672c5c8f4c9e5 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 9 Jan 2025 13:03:13 -0500 Subject: [PATCH 2/7] fix: Allow multiple selection types for key images. --- .../core/src/RenderingEngine/StackViewport.ts | 14 +- packages/core/src/types/IViewport.ts | 4 + .../core/src/utilities/frameRangeUtils.ts | 56 +++ packages/core/src/utilities/index.ts | 2 + packages/tools/examples/stackRange/index.ts | 368 ++++++++++++++++++ 5 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/utilities/frameRangeUtils.ts create mode 100644 packages/tools/examples/stackRange/index.ts diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 7f67af4d09..7c6f713217 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -93,6 +93,7 @@ import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import uuidv4 from '../utilities/uuidv4'; import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirection'; import getClosestImageId from '../utilities/getClosestImageId'; +import { frameRangeUtils } from '../utilities'; const EPSILON = 1; // Slice Thickness @@ -3122,10 +3123,21 @@ class StackViewport extends Viewport { ): ViewReference { const { sliceIndex = this.getCurrentImageIdIndex() } = viewRefSpecifier; const reference = super.getViewReference(viewRefSpecifier); - const referencedImageId = this.imageIds[sliceIndex as number]; + let referencedImageId = + this.imageIds[this.imageIds.length === 1 ? 0 : (sliceIndex as number)]; if (!referencedImageId) { return; } + const iSliceNumber = sliceIndex as number; + if (this.imageIds.length === 1 && iSliceNumber > 0) { + if (iSliceNumber >= this.getNumberOfSlices()) { + return; + } + referencedImageId = frameRangeUtils.n( + referencedImageId, + iSliceNumber + 1 + ); + } reference.referencedImageId = referencedImageId; if (this.getCurrentImageIdIndex() !== sliceIndex) { const referenceData = this.getImagePlaneReferenceData( diff --git a/packages/core/src/types/IViewport.ts b/packages/core/src/types/IViewport.ts index 365d68dd4e..e651cc7209 100644 --- a/packages/core/src/types/IViewport.ts +++ b/packages/core/src/types/IViewport.ts @@ -19,6 +19,10 @@ export interface ViewReferenceSpecifier { * volumes, or two viewports showing different orientations or slab thicknesses. */ sliceIndex?: number | [number, number]; + + /** The frame number for a multiframe */ + frameNumber?: number; + /** * Specifies to get a view reference that refers to the generic frame of * reference rather than to a specific volume or stack. Thus, the view diff --git a/packages/core/src/utilities/frameRangeUtils.ts b/packages/core/src/utilities/frameRangeUtils.ts new file mode 100644 index 0000000000..52f6cd5785 --- /dev/null +++ b/packages/core/src/utilities/frameRangeUtils.ts @@ -0,0 +1,56 @@ +import { triggerEvent, eventTarget } from '@cornerstonejs/core'; + +export type FramesRange = [number, number] | number; + +/** + * This class handles the annotation frame range values for multiframes. + * Mostly used for the Video viewport, it allows references to + * a range of frame values. + */ +export default class FrameRangeUtils { + protected static frameRangeExtractor = + /(\/frames\/|[&?]frameNumber=)([^/&?]*)/i; + + protected static imageIdToFrames(imageId: string): FramesRange { + const match = imageId.match(this.frameRangeExtractor); + if (!match || !match[2]) { + return null; + } + const range = match[2].split('-').map((it) => Number(it)); + if (range.length === 1) { + return range[0]; + } + return range as FramesRange; + } + + public static multiframeImageId(imageId: string, frameNumber = 1) { + const match = imageId.match(this.frameRangeExtractor); + if (!match || !match[2]) { + console.warn('Unable to extract frame from', imageId); + return imageId; + } + return imageId; + } + + public static framesToString(range) { + if (Array.isArray(range)) { + return `${range[0]}-${range[1]}`; + } + return String(range); + } + + protected static framesToImageId( + imageId: string, + range: FramesRange | string + ): string { + const match = imageId.match(this.frameRangeExtractor); + if (!match || !match[2]) { + return null; + } + const newRangeString = this.framesToString(range); + return imageId.replace( + this.frameRangeExtractor, + `${match[1]}${newRangeString}` + ); + } +} diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index a454ec81e9..36409609b3 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -79,6 +79,7 @@ import * as transferFunctionUtils from './transferFunctionUtils'; import * as color from './color'; import { deepEqual } from './deepEqual'; import type { IViewport } from '../types/IViewport'; +import frameRangeUtils from './frameRangeUtils'; // solving the circular dependency issue import { _getViewportModality } from './getViewportModality'; @@ -98,6 +99,7 @@ const getViewportModality = (viewport: IViewport, volumeId?: string) => _getViewportModality(viewport, volumeId, cache.getVolume); export { + frameRangeUtils, eventListener, csUtils as invertRgbTransferFunction, createSigmoidRGBTransferFunction, diff --git a/packages/tools/examples/stackRange/index.ts b/packages/tools/examples/stackRange/index.ts new file mode 100644 index 0000000000..97787040d8 --- /dev/null +++ b/packages/tools/examples/stackRange/index.ts @@ -0,0 +1,368 @@ +import type { Types } from '@cornerstonejs/core'; +import { RenderingEngine, Enums, eventTarget } from '@cornerstonejs/core'; +import { + addButtonToToolbar, + addToggleButtonToToolbar, + addDropdownToToolbar, + initDemo, + setTitleAndDescription, + createImageIdsAndCacheMetaData, + getLocalUrl, + addManipulationBindings, + addVideoTime, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { + KeyImageTool, + VideoRedactionTool, + + ToolGroupManager, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { annotationFrameRange } = cornerstoneTools.utilities; + +const { ViewportType } = Enums; +const { MouseBindings, KeyboardBindings, Events: toolsEvents } = csToolsEnums; + +const toolGroupId = 'STACK_GROUP_ID'; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Stack Range and Key Images Examples', + 'Show a stack viewport with controls to allow it to specify ranges and key images' +); + +const content = document.getElementById('content'); + +// Create a selection info element +const selectionDiv = document.createElement('div'); +selectionDiv.id = 'selection'; +selectionDiv.style.width = '90%'; +selectionDiv.style.height = '3em'; +content.appendChild(selectionDiv); + +// ************* Create the cornerstone element. +const element = document.createElement('div'); +// Disable right click context menu so we can have right click tools +element.oncontextmenu = (e) => e.preventDefault(); + +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); + +const instructions = document.createElement('p'); +instructions.innerText = `Clear Frame Range clears and selected from range on playback +Click the viewer to apply a key image (range if playing, frame if still). +Annotation navigation will choose next/previous annotation in the group +Select start/remove range/end range to set the start of the range and the end range, as well as to remove the range (make the key image apply to the current frame only) +`; + +content.append(instructions); +// ============================= // + +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'videoViewportId'; +const baseEventDetail = { + viewportId, + renderingEngineId, +}; + +let viewport; + +addButtonToToolbar({ + id: 'CreateKey', + title: 'Create Key Image', + onClick: () => { + KeyImageTool.createAndAddAnnotation(viewport, { + data: { label: 'Demo Key Image' }, + }); + }, +}); + +const toolsNames = [KeyImageTool.toolName, VideoRedactionTool.toolName]; +let selectedToolName = toolsNames[0]; + +addDropdownToToolbar({ + options: { values: toolsNames, defaultValue: selectedToolName }, + onSelectedValueChange: (newSelectedToolNameAsStringOrNumber) => { + const newSelectedToolName = String(newSelectedToolNameAsStringOrNumber); + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + + // Set the new tool active + toolGroup.setToolActive(newSelectedToolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + // Set the old tool passive + toolGroup.setToolPassive(selectedToolName); + + selectedToolName = newSelectedToolName; + }, +}); + +addButtonToToolbar({ + id: 'Previous', + title: '< Previous Annotation', + onClick() { + selectNextAnnotation(-1); + }, +}); + +addButtonToToolbar({ + id: 'Next', + title: 'Next Annotation >', + onClick() { + selectNextAnnotation(1); + }, +}); + +function togglePlay(toggle = undefined) { + if (toggle === undefined) { + toggle = viewport.togglePlayPause(); + } else if (toggle) { + viewport.play(); + } else { + viewport.pause(); + } +} + +addButtonToToolbar({ + id: 'Set Range [', + title: 'Start Range', + onClick() { + const annotation = getActiveAnnotation(); + if (annotation) { + const rangeSelection = annotationFrameRange.getFrameRange(annotation); + const frame = viewport.getFrameNumber(); + const range = Array.isArray(rangeSelection) + ? rangeSelection + : [rangeSelection, viewport.numberOfFrames]; + range[0] = frame; + range[1] = Math.max(frame, range[1]); + annotationFrameRange.setFrameRange( + annotation, + range as [number, number], + baseEventDetail + ); + viewport.setFrameRange(range); + viewport.render(); + } + }, +}); + +addButtonToToolbar({ + id: 'End Range', + title: 'End Range', + onClick() { + const annotation = getActiveAnnotation(); + if (annotation) { + const rangeSelection = annotationFrameRange.getFrameRange(annotation); + const frame = viewport.getFrameNumber(); + const range = Array.isArray(rangeSelection) + ? rangeSelection + : [rangeSelection, viewport.getNumberOfSlices()]; + range[1] = frame; + range[0] = Math.min(frame, range[0]); + annotationFrameRange.setFrameRange( + annotation, + range as [number, number], + baseEventDetail + ); + viewport.render(); + } + }, +}); + +addButtonToToolbar({ + id: 'Remove Range', + title: 'Remove Range', + onClick() { + const annotation = getActiveAnnotation(); + if (annotation) { + togglePlay(false); + annotationFrameRange.setFrameRange( + annotation, + viewport.getFrameNumber(), + baseEventDetail + ); + viewport.render(); + } + }, +}); + +function annotationModifiedListener(evt) { + updateAnnotationDiv( + evt.detail.annotation?.annotationUID || + evt.detail.annotationUID || + evt.detail.added?.[0] + ); +} + +const selectedAnnotation = { + annotationUID: '', +}; + +const activeGroup = new cornerstoneTools.annotation.AnnotationGroup(); + +function updateAnnotationDiv(uid) { + const annotation = cornerstoneTools.annotation.state.getAnnotation(uid); + if (!annotation) { + selectionDiv.innerHTML = ''; + selectedAnnotation.annotationUID = ''; + return; + } + selectedAnnotation.annotationUID = uid; + const { metadata, data } = annotation; + const { toolName } = metadata; + const range = annotationFrameRange.getFrameRange(annotation); + const rangeArr = Array.isArray(range) ? range : [range]; + const { fps } = viewport; + selectionDiv.innerHTML = ` + ${toolName} Annotation UID:${uid} Label:${ + data.label || data.text + } ${annotation.isVisible ? 'visible' : 'not visible'}
+ Range: Frames: ${rangeArr.join('-')} Times ${rangeArr + .map((it) => Math.round((it * 10) / fps) / 10) + .join('-')}
+ `; +} + +function getActiveAnnotation() { + return cornerstoneTools.annotation.state.getAnnotation( + selectedAnnotation.annotationUID + ); +} + +function addAnnotationListeners() { + eventTarget.addEventListener( + toolsEvents.ANNOTATION_SELECTION_CHANGE, + annotationModifiedListener + ); + eventTarget.addEventListener( + toolsEvents.ANNOTATION_MODIFIED, + annotationModifiedListener + ); + eventTarget.addEventListener( + toolsEvents.ANNOTATION_COMPLETED, + annotationModifiedListener + ); + eventTarget.addEventListener(toolsEvents.ANNOTATION_ADDED, (evt) => { + const { detail } = evt; + activeGroup.add(detail.annotation?.annotationUID || detail.annotationUID); + }); +} + +function selectNextAnnotation(direction) { + const uid = selectedAnnotation.annotationUID; + const nextUid = + activeGroup.findNearby(uid, direction) || + activeGroup.findNearby(null, direction); + updateAnnotationDiv(nextUid); + if (!nextUid) { + return; + } + const annotation = cornerstoneTools.annotation.state.getAnnotation(nextUid); + if (!annotation) { + return; + } + console.log( + 'Navigating to', + annotation.metadata.sliceIndex, + annotation.metadata.referencedImageId + ); + viewport.setViewReference(annotation.metadata); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '2.16.124.113643.100.10.2.97089913110630123934763297639331145050', + SeriesInstanceUID: + '2.16.124.113643.100.10.2.31433191110799088099930530803211617773', + wadoRsRoot: 'http://localhost:5000/dicomweb/', // getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }); + + const { StackScrollTool } = cornerstoneTools; + + addAnnotationListeners(); + // Add annotation tools to Cornerstone3D + cornerstoneTools.addTool(KeyImageTool); + cornerstoneTools.addTool(VideoRedactionTool); + + // Add tools to Cornerstone3D + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + addManipulationBindings(toolGroup); + + // Add tools to the tool group + toolGroup.addTool(KeyImageTool.toolName); + toolGroup.addTool(VideoRedactionTool.toolName); + + toolGroup.setToolActive(VideoRedactionTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, + modifierKey: KeyboardBindings.ShiftAlt, + }, + ], + }); + toolGroup.setToolActive(KeyImageTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Middle Click + }, + ], + }); + + // Get Cornerstone imageIds and fetch metadata into RAM + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create a stack viewport + + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Get the stack viewport that was created + viewport = renderingEngine.getViewport(viewportId); + + toolGroup.addViewport(viewport.id, renderingEngineId); + + // Set the video on the viewport + // Will be `/studies//series//instances//rendered?accept=video/mp4` + // on a compliant DICOMweb endpoint + viewport.setStack([imageIds[0]], 1); +} + +run(); From f18b52b22a5f1609167ff5da6b433367b77809e2 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 13 Jan 2025 13:35:19 -0500 Subject: [PATCH 3/7] fix: API check --- common/reviews/api/core.api.md | 48 +++++---- common/reviews/api/tools.api.md | 21 ++-- .../adapters/Cornerstone3D/BaseAdapter3D.ts | 5 +- .../core/src/RenderingEngine/StackViewport.ts | 91 ++++++++++------- .../core/src/RenderingEngine/VideoViewport.ts | 30 +++--- packages/core/src/RenderingEngine/Viewport.ts | 2 +- packages/core/src/types/IViewport.ts | 34 ++++++- packages/tools/examples/stackRange/index.ts | 98 +++++++------------ packages/tools/examples/videoRange/index.ts | 59 ++++------- packages/tools/src/enums/ChangeTypes.ts | 4 + .../src/tools/annotation/KeyImageTool.ts | 17 +--- .../tools/src/tools/annotation/ProbeTool.ts | 31 ++---- .../src/tools/annotation/RectangleROITool.ts | 71 +++++--------- .../tools/annotation/VideoRedactionTool.ts | 53 ++++------ .../tools/src/tools/base/AnnotationTool.ts | 7 +- .../src/utilities/annotationFrameRange.ts | 85 +++++++++++++--- 16 files changed, 335 insertions(+), 321 deletions(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 1f318988ff..f543f08f66 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -1187,6 +1187,20 @@ interface FlipDirection { flipVertical?: boolean; } +// @public (undocumented) +class FrameRangeUtils { + // (undocumented) + protected static frameRangeExtractor: RegExp; + // (undocumented) + protected static framesToImageId(imageId: string, range: FramesRange | string): string; + // (undocumented) + static framesToString(range: any): string; + // (undocumented) + protected static imageIdToFrames(imageId: string): FramesRange; + // (undocumented) + static multiframeImageId(imageId: string, frameNumber?: number): string; +} + // @public (undocumented) interface GeneralSeriesModuleMetadata { // (undocumented) @@ -3415,7 +3429,7 @@ export class StackViewport extends Viewport { // (undocumented) getCornerstoneImage: () => IImage; // (undocumented) - getCurrentImageId: () => string; + getCurrentImageId: (index?: number) => string; // (undocumented) getCurrentImageIdIndex: () => number; // (undocumented) @@ -3914,6 +3928,7 @@ function updateVTKImageDataWithCornerstoneImage(sourceImageData: vtkImageData, i declare namespace utilities { export { + FrameRangeUtils as frameRangeUtils, eventListener, invertRgbTransferFunction, createSigmoidRGBTransferFunction, @@ -4042,7 +4057,7 @@ export class VideoViewport extends Viewport { // (undocumented) getCamera(): ICamera; // (undocumented) - getCurrentImageId(): string; + getCurrentImageId(index?: number): string; // (undocumented) getCurrentImageIdIndex(): number; // (undocumented) @@ -4329,7 +4344,7 @@ export class Viewport { // (undocumented) _isInBounds(point: Point3, bounds: number[]): boolean; // (undocumented) - isReferenceViewable(viewRef: ViewReference, options?: ReferenceCompatibleOptions): boolean | unknown; + isReferenceViewable(viewRef: ViewReference, options?: ReferenceCompatibleOptions): boolean; // (undocumented) options: ViewportInputOptions; // (undocumented) @@ -4566,33 +4581,32 @@ interface ViewPresentationSelector { } // @public (undocumented) -interface ViewReference { - // (undocumented) - bounds?: BoundsLPS; - // (undocumented) - cameraFocalPoint?: Point3; - // (undocumented) +type ViewReference = { FrameOfReferenceUID?: string; - // (undocumented) referencedImageId?: string; - // (undocumented) - sliceIndex?: number | [number, number]; - // (undocumented) + referencedImageUri?: string; + cameraFocalPoint?: Point3; viewPlaneNormal?: Point3; - // (undocumented) viewUp?: Point3; - // (undocumented) + sliceIndex?: number; + sliceRangeEnd?: number; + sliceSet?: Set; volumeId?: string; -} + bounds?: BoundsLPS; +}; // @public (undocumented) interface ViewReferenceSpecifier { // (undocumented) forFrameOfReference?: boolean; // (undocumented) + frameNumber?: number; + // (undocumented) points?: Point3[]; // (undocumented) - sliceIndex?: number | [number, number]; + sliceIndex?: number; + // (undocumented) + sliceRangeEnd?: number; // (undocumented) volumeId?: string; } diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index f905f84a0f..b853c3ae0e 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -329,18 +329,23 @@ class AnnotationFrameRange { // (undocumented) protected static frameRangeExtractor: RegExp; // (undocumented) - protected static framesToImageId(imageId: string, range: FramesRange | string): string; + protected static framesToImageId(imageId: string, range: FramesRange_2 | string): string; // (undocumented) static framesToString(range: any): string; // (undocumented) static getFrameRange(annotation: Annotation): number | [number, number]; // (undocumented) - protected static imageIdToFrames(imageId: string): FramesRange; + protected static imageIdToFrames(imageId: string): FramesRange_2; // (undocumented) - static setFrameRange(annotation: Annotation, range: FramesRange | string, eventBase?: { - viewportId: any; - renderingEngineId: any; - }): void; + static setEndRange(viewport: any, annotation: any, endRange?: any): void; + // (undocumented) + static setRange(viewport: any, annotation: any, startRange?: number, endRange?: number): void; + // (undocumented) + static setSingle(viewport: any, annotation: any, current?: any): void; + // (undocumented) + static setStartRange(viewport: any, annotation: any, startRange?: any): void; + // (undocumented) + static setViewportFrameRange(viewport: any, specifier: any): void; } // @public (undocumented) @@ -507,7 +512,7 @@ export abstract class AnnotationTool extends AnnotationDisplayTool { // (undocumented) static createAnnotation(...annotationBaseData: any[]): Annotation; // (undocumented) - static createAnnotationForViewport(viewport: any, ...annotationBaseData: any[]): Annotation; + static createAnnotationForViewport(viewport: any, ...annotationBaseData: any[]): T; // (undocumented) static createAnnotationMemo(element: any, annotation: Annotation, options?: { newAnnotation?: boolean; @@ -980,6 +985,8 @@ enum ChangeTypes { // (undocumented) InterpolationUpdated = "InterpolationUpdated", // (undocumented) + MetadataReferenceModified = "MetadataReferenceModified", + // (undocumented) StatsUpdated = "StatsUpdated" } diff --git a/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts index 7cf91667bd..74b57a23a2 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/BaseAdapter3D.ts @@ -53,7 +53,10 @@ export default class BaseAdapter3D { return state; } - public static getTID300RepresentationArguments(tool, worldToImageCoords) { + public static getTID300RepresentationArguments( + tool, + worldToImageCoords + ): Record { const { data, metadata } = tool; const { finding, findingSites } = tool; const { referencedImageId } = metadata; diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 7c6f713217..cbcd320ea9 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -93,7 +93,7 @@ import type vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import uuidv4 from '../utilities/uuidv4'; import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirection'; import getClosestImageId from '../utilities/getClosestImageId'; -import { frameRangeUtils } from '../utilities'; +import frameRangeUtils from '../utilities/frameRangeUtils'; const EPSILON = 1; // Slice Thickness @@ -3036,48 +3036,67 @@ class StackViewport extends Viewport { viewRef: ViewReference, options: ReferenceCompatibleOptions = {} ): boolean { - if (!super.isReferenceViewable(viewRef, options)) { + const testIndex = this.getCurrentImageIdIndex(); + const currentImageId = this.imageIds[testIndex]; + if (!currentImageId || !viewRef) { return false; } + const { + referencedImageId, + sliceIndex, + sliceRangeEnd = sliceIndex, + } = viewRef; - const { referencedImageId, sliceIndex } = viewRef; - - if (viewRef.volumeId && !referencedImageId) { - return options.asVolume; - } + // Optimize the return for the exact match cases + if (referencedImageId && sliceIndex !== undefined) { + if ( + testIndex >= sliceIndex && + testIndex <= sliceRangeEnd && + this.imageIds[sliceIndex] === referencedImageId + ) { + return true; + } + if ( + options.withNavigation && + this.imageIds[sliceIndex] == referencedImageId + ) { + return true; + } - let testIndex = this.getCurrentImageIdIndex(); - let currentImageId = this.imageIds[testIndex]; + // Optimize the test for being viewable by defining the URI version + // This allows an endsWith test + viewRef.referencedImageUri ||= imageIdToURI(referencedImageId); + const { referencedImageUri } = viewRef; - if (options.withNavigation && typeof sliceIndex === 'number') { - testIndex = sliceIndex; - currentImageId = this.imageIds[testIndex]; - } + if ( + testIndex >= sliceIndex && + testIndex <= sliceRangeEnd && + this.imageIds[sliceIndex]?.endsWith(referencedImageUri) + ) { + return true; + } + if ( + options.withNavigation && + this.imageIds[sliceIndex]?.endsWith(referencedImageUri) + ) { + return true; + } + if ( + options.asOverlay && + this.matchImagesForOverlay(currentImageId, referencedImageId) + ) { + return true; + } - if (!currentImageId) { return false; } - if (options.asOverlay && referencedImageId) { - const matchedImageId = this.matchImagesForOverlay( - currentImageId, - referencedImageId - ); - if (matchedImageId) { - return true; - } + if (!super.isReferenceViewable(viewRef, options)) { + return false; } - let { imageURI } = options; - - if (!imageURI) { - // Remove the dataLoader scheme since that can change - imageURI = imageIdToURI(currentImageId); - } - const referencedImageURI = imageIdToURI(referencedImageId); - const matches = referencedImageURI === imageURI; - if (matches) { - return matches; + if (viewRef.volumeId) { + return options.asVolume; } // if camera focal point is provided, we can use that as a point @@ -3133,7 +3152,7 @@ class StackViewport extends Viewport { if (iSliceNumber >= this.getNumberOfSlices()) { return; } - referencedImageId = frameRangeUtils.n( + referencedImageId = frameRangeUtils.multiframeImageId( referencedImageId, iSliceNumber + 1 ); @@ -3211,8 +3230,10 @@ class StackViewport extends Viewport { * Returns the currently rendered imageId * @returns string for imageId */ - public getCurrentImageId = (): string => { - return this.imageIds[this.currentImageIdIndex]; + public getCurrentImageId = ( + index = this.getCurrentImageIdIndex() + ): string => { + return this.imageIds[index]; }; /** diff --git a/packages/core/src/RenderingEngine/VideoViewport.ts b/packages/core/src/RenderingEngine/VideoViewport.ts index b4cbcfa830..a96c7f21b3 100644 --- a/packages/core/src/RenderingEngine/VideoViewport.ts +++ b/packages/core/src/RenderingEngine/VideoViewport.ts @@ -771,13 +771,8 @@ class VideoViewport extends Viewport { * * @returns an imageID for video */ - public getCurrentImageId() { - const current = this.imageId.replace( - '/frames/1', - this.isPlaying - ? `/frames/${this.frameRange[0]}-${this.frameRange[1]}` - : `/frames/${this.getFrameNumber()}` - ); + public getCurrentImageId(index = this.getCurrentImageIdIndex()) { + const current = this.imageId.replace('/frames/1', `/frames/${index + 1}`); return current; } @@ -810,7 +805,7 @@ class VideoViewport extends Viewport { options: ReferenceCompatibleOptions = {} ): boolean { let { imageURI } = options; - const { referencedImageId, sliceIndex: sliceIndex } = viewRef; + const { referencedImageId, sliceIndex, sliceRangeEnd } = viewRef; if (!super.isReferenceViewable(viewRef)) { return false; } @@ -827,8 +822,8 @@ class VideoViewport extends Viewport { return true; } const currentIndex = this.getSliceIndex(); - if (Array.isArray(sliceIndex)) { - return currentIndex >= sliceIndex[0] && currentIndex <= sliceIndex[1]; + if (sliceRangeEnd) { + return currentIndex >= sliceIndex && currentIndex <= sliceRangeEnd; } if (sliceIndex !== undefined) { return currentIndex === sliceIndex; @@ -866,16 +861,17 @@ class VideoViewport extends Viewport { public getViewReference( viewRefSpecifier?: ViewReferenceSpecifier ): ViewReference { - let sliceIndex = viewRefSpecifier?.sliceIndex; - if (!sliceIndex) { - sliceIndex = this.isPlaying - ? [this.frameRange[0] - 1, this.frameRange[1] - 1] - : this.getCurrentImageIdIndex(); - } + const sliceIndex = + viewRefSpecifier?.sliceIndex ?? + (this.isPlaying ? this.frameRange[0] : this.getCurrentImageIdIndex()); + const sliceRangeEnd = + viewRefSpecifier?.sliceRangeEnd ?? + (this.isPlaying ? this.frameRange[1] - 1 : undefined); return { ...super.getViewReference(viewRefSpecifier), referencedImageId: this.getViewReferenceId(viewRefSpecifier), - sliceIndex: sliceIndex, + sliceIndex, + sliceRangeEnd, }; } diff --git a/packages/core/src/RenderingEngine/Viewport.ts b/packages/core/src/RenderingEngine/Viewport.ts index ce7ae1e569..f777f682a4 100644 --- a/packages/core/src/RenderingEngine/Viewport.ts +++ b/packages/core/src/RenderingEngine/Viewport.ts @@ -1756,7 +1756,7 @@ class Viewport { public isReferenceViewable( viewRef: ViewReference, options?: ReferenceCompatibleOptions - ): boolean | unknown { + ): boolean { if ( viewRef.FrameOfReferenceUID && viewRef.FrameOfReferenceUID !== this.getFrameOfReferenceUID() diff --git a/packages/core/src/types/IViewport.ts b/packages/core/src/types/IViewport.ts index e651cc7209..88e8eb55c6 100644 --- a/packages/core/src/types/IViewport.ts +++ b/packages/core/src/types/IViewport.ts @@ -18,7 +18,11 @@ export interface ViewReferenceSpecifier { * and cannot be shared across different view types such as stacks and * volumes, or two viewports showing different orientations or slab thicknesses. */ - sliceIndex?: number | [number, number]; + sliceIndex?: number; + /** + * The end index - this requires sliceIndex to be specified. + */ + sliceRangeEnd?: number; /** The frame number for a multiframe */ frameNumber?: number; @@ -88,7 +92,7 @@ export interface ReferenceCompatibleOptions { * to it later, as well as determining whether specific views should show annotations * or other overlay information. */ -export interface ViewReference { +export type ViewReference = { /** * The FrameOfReferenceUID */ @@ -101,9 +105,17 @@ export interface ViewReference { * * The naming of this particular attribute matches the DICOM SR naming for the * referenced image, as well as historical naming in CS3D. + * + * For range/selection, this must be the starting range referenced image id */ referencedImageId?: string; + /** + * An internal URI version of hte referencedImageId, used for performance + * while checking the referenced image id. + */ + referencedImageUri?: string; + /** * The focal point of the camera in world space. * The focal point is used for to define the stack positioning, but not the @@ -126,7 +138,7 @@ export interface ViewReference { */ viewUp?: Point3; /** - * The slice index or range for this view. + * The slice index for the image of interest * NOTE The slice index is relative to the volume or stack of images. * You cannot apply a slice index from one volume to another as they do NOT * apply. The referencedImageId should belong to the volume you are trying @@ -139,7 +151,19 @@ export interface ViewReference { * within the stack of images - subsequent slice indexes can be at opposite * ends or can be co-incident but separate types of images. */ - sliceIndex?: number | [number, number]; + sliceIndex?: number; + /** + * An end of a slice range. This is used to indicate the end of the slices + * where the sliceIndex is the first point. This is a positive value if defined, + * so can be tested for `sliceRangeEnd`. + */ + sliceRangeEnd?: number; + /** + * A set of slice indexes actually in this object. The values in this set + * must be in [sliceIndex, sliceRangeEnd], and if this is not set, then + * the entire range is considered in the set. + */ + sliceSet?: Set; /** * VolumeId that the referencedImageId was chosen from @@ -150,7 +174,7 @@ export interface ViewReference { * particular bounds or not. This will be in world coordinates. */ bounds?: BoundsLPS; -} +}; /** * A view presentation stores information about how the view is presented to the diff --git a/packages/tools/examples/stackRange/index.ts b/packages/tools/examples/stackRange/index.ts index 97787040d8..fbe454d708 100644 --- a/packages/tools/examples/stackRange/index.ts +++ b/packages/tools/examples/stackRange/index.ts @@ -2,14 +2,12 @@ import type { Types } from '@cornerstonejs/core'; import { RenderingEngine, Enums, eventTarget } from '@cornerstonejs/core'; import { addButtonToToolbar, - addToggleButtonToToolbar, addDropdownToToolbar, initDemo, setTitleAndDescription, createImageIdsAndCacheMetaData, getLocalUrl, addManipulationBindings, - addVideoTime, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; @@ -21,6 +19,9 @@ console.warn( const { KeyImageTool, VideoRedactionTool, + LengthTool, + ProbeTool, + RectangleROITool, ToolGroupManager, Enums: csToolsEnums, @@ -70,11 +71,7 @@ content.append(instructions); // ============================= // const renderingEngineId = 'myRenderingEngine'; -const viewportId = 'videoViewportId'; -const baseEventDetail = { - viewportId, - renderingEngineId, -}; +const viewportId = 'viewportId'; let viewport; @@ -88,7 +85,13 @@ addButtonToToolbar({ }, }); -const toolsNames = [KeyImageTool.toolName, VideoRedactionTool.toolName]; +const toolsNames = [ + KeyImageTool.toolName, + VideoRedactionTool.toolName, + LengthTool.toolName, + ProbeTool.toolName, + RectangleROITool.toolName, +]; let selectedToolName = toolsNames[0]; addDropdownToToolbar({ @@ -129,35 +132,13 @@ addButtonToToolbar({ }, }); -function togglePlay(toggle = undefined) { - if (toggle === undefined) { - toggle = viewport.togglePlayPause(); - } else if (toggle) { - viewport.play(); - } else { - viewport.pause(); - } -} - addButtonToToolbar({ id: 'Set Range [', title: 'Start Range', onClick() { const annotation = getActiveAnnotation(); if (annotation) { - const rangeSelection = annotationFrameRange.getFrameRange(annotation); - const frame = viewport.getFrameNumber(); - const range = Array.isArray(rangeSelection) - ? rangeSelection - : [rangeSelection, viewport.numberOfFrames]; - range[0] = frame; - range[1] = Math.max(frame, range[1]); - annotationFrameRange.setFrameRange( - annotation, - range as [number, number], - baseEventDetail - ); - viewport.setFrameRange(range); + annotationFrameRange.setStartRange(viewport, annotation); viewport.render(); } }, @@ -169,35 +150,31 @@ addButtonToToolbar({ onClick() { const annotation = getActiveAnnotation(); if (annotation) { - const rangeSelection = annotationFrameRange.getFrameRange(annotation); - const frame = viewport.getFrameNumber(); - const range = Array.isArray(rangeSelection) - ? rangeSelection - : [rangeSelection, viewport.getNumberOfSlices()]; - range[1] = frame; - range[0] = Math.min(frame, range[0]); - annotationFrameRange.setFrameRange( - annotation, - range as [number, number], - baseEventDetail - ); + annotationFrameRange.setEndRange(viewport, annotation); + viewport.render(); + } + }, +}); + +addButtonToToolbar({ + id: 'Select Series', + title: 'Select Series', + onClick() { + const annotation = getActiveAnnotation(); + if (annotation) { + annotationFrameRange.setRange(viewport, annotation); viewport.render(); } }, }); addButtonToToolbar({ - id: 'Remove Range', - title: 'Remove Range', + id: 'Select Current', + title: 'Select Current', onClick() { const annotation = getActiveAnnotation(); if (annotation) { - togglePlay(false); - annotationFrameRange.setFrameRange( - annotation, - viewport.getFrameNumber(), - baseEventDetail - ); + annotationFrameRange.setSingle(viewport, annotation); viewport.render(); } }, @@ -229,14 +206,11 @@ function updateAnnotationDiv(uid) { const { toolName } = metadata; const range = annotationFrameRange.getFrameRange(annotation); const rangeArr = Array.isArray(range) ? range : [range]; - const { fps } = viewport; selectionDiv.innerHTML = ` ${toolName} Annotation UID:${uid} Label:${ data.label || data.text } ${annotation.isVisible ? 'visible' : 'not visible'}
- Range: Frames: ${rangeArr.join('-')} Times ${rangeArr - .map((it) => Math.round((it * 10) / fps) / 10) - .join('-')}
+ Range: Frames: ${rangeArr.join('-')}
`; } @@ -296,18 +270,19 @@ async function run() { // Get Cornerstone imageIds and fetch metadata into RAM const imageIds = await createImageIdsAndCacheMetaData({ StudyInstanceUID: - '2.16.124.113643.100.10.2.97089913110630123934763297639331145050', + '1.3.6.1.4.1.53684.1.1.2.4037847388.8168.1651298318.32092078', SeriesInstanceUID: - '2.16.124.113643.100.10.2.31433191110799088099930530803211617773', + '1.3.6.1.4.1.53684.1.1.3.4037847388.8168.1651298319.32092097', wadoRsRoot: 'http://localhost:5000/dicomweb/', // getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', }); - const { StackScrollTool } = cornerstoneTools; - addAnnotationListeners(); // Add annotation tools to Cornerstone3D cornerstoneTools.addTool(KeyImageTool); cornerstoneTools.addTool(VideoRedactionTool); + cornerstoneTools.addTool(LengthTool); + cornerstoneTools.addTool(ProbeTool); + cornerstoneTools.addTool(RectangleROITool); // Add tools to Cornerstone3D @@ -319,6 +294,9 @@ async function run() { // Add tools to the tool group toolGroup.addTool(KeyImageTool.toolName); toolGroup.addTool(VideoRedactionTool.toolName); + toolGroup.addTool(ProbeTool.toolName); + toolGroup.addTool(LengthTool.toolName); + toolGroup.addTool(RectangleROITool.toolName); toolGroup.setToolActive(VideoRedactionTool.toolName, { bindings: [ @@ -362,7 +340,7 @@ async function run() { // Set the video on the viewport // Will be `/studies//series//instances//rendered?accept=video/mp4` // on a compliant DICOMweb endpoint - viewport.setStack([imageIds[0]], 1); + viewport.setStack(imageIds, 1); } run(); diff --git a/packages/tools/examples/videoRange/index.ts b/packages/tools/examples/videoRange/index.ts index 1ad0ea0a1a..d900a20b71 100644 --- a/packages/tools/examples/videoRange/index.ts +++ b/packages/tools/examples/videoRange/index.ts @@ -1,10 +1,5 @@ import type { Types } from '@cornerstonejs/core'; -import { - RenderingEngine, - Enums, - eventTarget, - triggerEvent, -} from '@cornerstonejs/core'; +import { RenderingEngine, Enums, eventTarget } from '@cornerstonejs/core'; import { addButtonToToolbar, addToggleButtonToToolbar, @@ -167,19 +162,7 @@ addButtonToToolbar({ onClick() { const annotation = getActiveAnnotation(); if (annotation) { - const rangeSelection = annotationFrameRange.getFrameRange(annotation); - const frame = viewport.getFrameNumber(); - const range = Array.isArray(rangeSelection) - ? rangeSelection - : [rangeSelection, viewport.numberOfFrames]; - range[0] = frame; - range[1] = Math.max(frame, range[1]); - annotationFrameRange.setFrameRange( - annotation, - range as [number, number], - baseEventDetail - ); - viewport.setFrameRange(range); + annotationFrameRange.setStartRange(viewport, annotation); viewport.render(); } }, @@ -191,36 +174,31 @@ addButtonToToolbar({ onClick() { const annotation = getActiveAnnotation(); if (annotation) { - const rangeSelection = annotationFrameRange.getFrameRange(annotation); - const frame = viewport.getFrameNumber(); - const range = Array.isArray(rangeSelection) - ? rangeSelection - : [rangeSelection, viewport.getNumberOfSlices()]; - range[1] = frame; - range[0] = Math.min(frame, range[0]); - annotationFrameRange.setFrameRange( - annotation, - range as [number, number], - baseEventDetail - ); - viewport.setFrameRange(range); + annotationFrameRange.setEndRange(viewport, annotation); + viewport.render(); + } + }, +}); + +addButtonToToolbar({ + id: 'Select Series', + title: 'Select Series', + onClick() { + const annotation = getActiveAnnotation(); + if (annotation) { + annotationFrameRange.setRange(viewport, annotation); viewport.render(); } }, }); addButtonToToolbar({ - id: 'Remove Range', - title: 'Remove Range', + id: 'Select Current', + title: 'Select Current', onClick() { const annotation = getActiveAnnotation(); if (annotation) { - togglePlay(false); - annotationFrameRange.setFrameRange( - annotation, - viewport.getFrameNumber(), - baseEventDetail - ); + annotationFrameRange.setSingle(viewport, annotation); viewport.render(); } }, @@ -252,6 +230,7 @@ function updateAnnotationDiv(uid) { const { toolName } = metadata; const range = annotationFrameRange.getFrameRange(annotation); const rangeArr = Array.isArray(range) ? range : [range]; + console.log('rangeArr=', range, rangeArr); const { fps } = viewport; selectionDiv.innerHTML = ` ${toolName} Annotation UID:${uid} Label:${ diff --git a/packages/tools/src/enums/ChangeTypes.ts b/packages/tools/src/enums/ChangeTypes.ts index d88e11b1bd..cfa862c4ad 100644 --- a/packages/tools/src/enums/ChangeTypes.ts +++ b/packages/tools/src/enums/ChangeTypes.ts @@ -35,6 +35,10 @@ enum ChangeTypes { * Occurs when an annotation is changed do to an undo or redo. */ History = 'History', + /** + * Send when the metadata selector changes + */ + MetadataReferenceModified = 'MetadataReferenceModified', } export default ChangeTypes; diff --git a/packages/tools/src/tools/annotation/KeyImageTool.ts b/packages/tools/src/tools/annotation/KeyImageTool.ts index ad74157a14..a344564efb 100644 --- a/packages/tools/src/tools/annotation/KeyImageTool.ts +++ b/packages/tools/src/tools/annotation/KeyImageTool.ts @@ -72,24 +72,11 @@ class KeyImageTool extends AnnotationTool { */ addNewAnnotation = (evt: EventTypes.InteractionEventType) => { const eventDetail = evt.detail; - const { currentPoints, element } = eventDetail; - const worldPos = currentPoints.world; + const { element } = eventDetail; const enabledElement = getEnabledElement(element); const { viewport } = enabledElement; - const camera = viewport.getCamera(); - const { viewPlaneNormal, viewUp } = camera; - - const referencedImageId = this.getReferencedImageId( - viewport, - worldPos, - viewPlaneNormal, - viewUp - ); - - const annotation = KeyImageTool.createAnnotation({ - metadata: { ...viewport.getViewReference(), referencedImageId }, - }); + const annotation = KeyImageTool.createAnnotationForViewport(viewport); addAnnotation(annotation, element); diff --git a/packages/tools/src/tools/annotation/ProbeTool.ts b/packages/tools/src/tools/annotation/ProbeTool.ts index 25846768ca..e31b25e78b 100644 --- a/packages/tools/src/tools/annotation/ProbeTool.ts +++ b/packages/tools/src/tools/annotation/ProbeTool.ts @@ -38,7 +38,6 @@ import type { EventTypes, ToolHandle, PublicToolProps, - ToolProps, SVGDrawingHelper, Annotation, } from '../../types'; @@ -215,34 +214,16 @@ class ProbeTool extends AnnotationTool { const { viewport } = enabledElement; this.isDrawing = true; - const camera = viewport.getCamera(); - const { viewPlaneNormal, viewUp } = camera; - const referencedImageId = this.getReferencedImageId( + const annotation = ProbeTool.createAnnotationForViewport( viewport, - worldPos, - viewPlaneNormal + { + data: { + handles: { points: [[...worldPos]] }, + }, + } ); - const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); - - const annotation = { - invalidated: true, - highlighted: true, - metadata: { - toolName: this.getToolName(), - viewPlaneNormal: [...viewPlaneNormal], - viewUp: [...viewUp], - FrameOfReferenceUID, - referencedImageId, - }, - data: { - label: '', - handles: { points: [[...worldPos]] }, - cachedStats: {}, - }, - }; - addAnnotation(annotation, element); const viewportIdsToRender = getViewportIdsWithToolToRender( diff --git a/packages/tools/src/tools/annotation/RectangleROITool.ts b/packages/tools/src/tools/annotation/RectangleROITool.ts index 1ccf3a5501..623c1a194d 100644 --- a/packages/tools/src/tools/annotation/RectangleROITool.ts +++ b/packages/tools/src/tools/annotation/RectangleROITool.ts @@ -148,57 +148,36 @@ class RectangleROITool extends AnnotationTool { const worldPos = currentPoints.world; const enabledElement = getEnabledElement(element); - const { viewport, renderingEngine } = enabledElement; + const { viewport } = enabledElement; this.isDrawing = true; - const camera = viewport.getCamera(); - const { viewPlaneNormal, viewUp } = camera; - - const referencedImageId = this.getReferencedImageId( - viewport, - worldPos, - viewPlaneNormal, - viewUp - ); - - const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); - - const annotation = { - invalidated: true, - highlighted: true, - metadata: { - toolName: this.getToolName(), - viewPlaneNormal: [...viewPlaneNormal], - viewUp: [...viewUp], - FrameOfReferenceUID, - referencedImageId, - ...viewport.getViewReference({ points: [worldPos] }), - }, - data: { - label: '', - handles: { - points: [ - [...worldPos], - [...worldPos], - [...worldPos], - [...worldPos], - ], - textBox: { - hasMoved: false, - worldPosition: [0, 0, 0], - worldBoundingBox: { - topLeft: [0, 0, 0], - topRight: [0, 0, 0], - bottomLeft: [0, 0, 0], - bottomRight: [0, 0, 0], + const annotation = + RectangleROITool.createAnnotationForViewport( + viewport, + { + data: { + handles: { + points: [ + [...worldPos], + [...worldPos], + [...worldPos], + [...worldPos], + ], + textBox: { + hasMoved: false, + worldPosition: [0, 0, 0], + worldBoundingBox: { + topLeft: [0, 0, 0], + topRight: [0, 0, 0], + bottomLeft: [0, 0, 0], + bottomRight: [0, 0, 0], + }, + }, }, }, - activeHandleIndex: null, - }, - cachedStats: {}, - }, - }; + } + ); addAnnotation(annotation, element); diff --git a/packages/tools/src/tools/annotation/VideoRedactionTool.ts b/packages/tools/src/tools/annotation/VideoRedactionTool.ts index 84fbba6d38..6904140b34 100644 --- a/packages/tools/src/tools/annotation/VideoRedactionTool.ts +++ b/packages/tools/src/tools/annotation/VideoRedactionTool.ts @@ -68,44 +68,25 @@ class VideoRedactionTool extends AnnotationTool { const worldPos = currentPoints.world; const enabledElement = getEnabledElement(element); - const { viewport, renderingEngine } = enabledElement; + const { viewport } = enabledElement; this.isDrawing = true; - - const camera = viewport.getCamera(); - const { viewPlaneNormal, viewUp } = camera; - const referencedImageId = this.getReferencedImageId( - viewport, - worldPos, - viewPlaneNormal, - viewUp - ); - - const annotation = { - metadata: { - // We probably just want a different type of data here, hacking this - // together for now. - viewPlaneNormal: [0, 0, 1], - viewUp: [0, 1, 0], - FrameOfReferenceUID: viewport.getFrameOfReferenceUID(), - referencedImageId, - toolName: this.getToolName(), - }, - data: { - invalidated: true, - handles: { - points: [ - [...worldPos], - [...worldPos], - [...worldPos], - [...worldPos], - ], - activeHandleIndex: null, - }, - cachedStats: {}, - active: true, - }, - }; + const annotation = + VideoRedactionTool.createAnnotationForViewport( + viewport, + { + data: { + handles: { + points: [ + [...worldPos], + [...worldPos], + [...worldPos], + [...worldPos], + ], + }, + }, + } + ); addAnnotation(annotation, element); diff --git a/packages/tools/src/tools/base/AnnotationTool.ts b/packages/tools/src/tools/base/AnnotationTool.ts index 346801a0c7..ab836bac7f 100644 --- a/packages/tools/src/tools/base/AnnotationTool.ts +++ b/packages/tools/src/tools/base/AnnotationTool.ts @@ -91,11 +91,14 @@ abstract class AnnotationTool extends AnnotationDisplayTool { * viewport reference data to the metadata, and otherwise returns the * static class createAnnotation data. */ - public static createAnnotationForViewport(viewport, ...annotationBaseData) { + public static createAnnotationForViewport( + viewport, + ...annotationBaseData + ): T { return this.createAnnotation( { metadata: viewport.getViewReference() }, ...annotationBaseData - ); + ) as T; } /** diff --git a/packages/tools/src/utilities/annotationFrameRange.ts b/packages/tools/src/utilities/annotationFrameRange.ts index 1a31da0623..bd644714c2 100644 --- a/packages/tools/src/utilities/annotationFrameRange.ts +++ b/packages/tools/src/utilities/annotationFrameRange.ts @@ -1,6 +1,7 @@ import { triggerEvent, eventTarget } from '@cornerstonejs/core'; import Events from '../enums/Events'; import type { Annotation } from '../types'; +import { ChangeTypes } from '../enums'; export type FramesRange = [number, number] | number; @@ -47,32 +48,88 @@ export default class AnnotationFrameRange { ); } + public static setStartRange( + viewport, + annotation, + startRange = viewport.getCurrentImageIdIndex() + ) { + this.setRange(viewport, annotation, startRange); + } + + public static setEndRange( + viewport, + annotation, + endRange = viewport.getCurrentImageIdIndex() + ) { + this.setRange(viewport, annotation, undefined, endRange); + } + /** - * Sets the range of frames to associate with the given annotation. - * The range can be a single frame number (1 based according to DICOM), - * or a range of values in the format `min-max` where min, max are inclusive - * Modifies the referencedImageID to specify the updated URL. + * Sets a range of images in the current viewport to be selected. + * This only works on stack and video viewports currently. */ - public static setFrameRange( - annotation: Annotation, - range: FramesRange | string, - eventBase?: { viewportId; renderingEngineId } + public static setRange( + viewport, + annotation, + startRange?: number, + endRange?: number ) { - const { referencedImageId } = annotation.metadata; - annotation.metadata.referencedImageId = this.framesToImageId( - referencedImageId, - range + const { metadata } = annotation; + + if (startRange === undefined) { + startRange = metadata.sliceIndex < endRange ? metadata.sliceIndex : 0; + if (endRange === undefined) { + endRange = viewport.getNumberOfSlices() - 1; + } + } + if (endRange === undefined) { + endRange = + metadata.sliceRangeEnd >= startRange + ? metadata.sliceRangeEnd + : viewport.getNumberOfSlices() - 1; + } + metadata.sliceRangeEnd = Math.max(startRange, endRange); + metadata.sliceIndex = Math.min(startRange, endRange); + metadata.referencedImageId = viewport.getCurrentImageId( + metadata.sliceIndex ); + metadata.referencedImageUri = undefined; + if (metadata.sliceRangeEnd === metadata.sliceIndex) { + metadata.sliceRangeEnd = undefined; + } + + // Send an event with metadata reference modified set to true so that + // any isReferenceViewable checks can be redone if needed. const eventDetail = { - ...eventBase, + viewportId: viewport.id, + renderingEngineId: viewport.renderingEngineId, + changeType: ChangeTypes.MetadataReferenceModified, annotation, }; + triggerEvent(eventTarget, Events.ANNOTATION_MODIFIED, eventDetail); + this.setViewportFrameRange(viewport, metadata); + } + + public static setSingle( + viewport, + annotation, + current = viewport.getCurrentImageIdIndex() + ) { + this.setRange(viewport, annotation, current, current); } public static getFrameRange( annotation: Annotation ): number | [number, number] { - return this.imageIdToFrames(annotation.metadata.referencedImageId); + const { metadata } = annotation; + const { sliceIndex, sliceRangeEnd } = metadata; + return sliceRangeEnd ? [sliceIndex + 1, sliceRangeEnd + 1] : sliceIndex + 1; + } + + public static setViewportFrameRange(viewport, specifier) { + if (viewport.setFrameRange && specifier.sliceRangeEnd) { + viewport.setFrameRange(specifier.sliceIndex, specifier.sliceRangeEnd); + } } } From 996711a8b120ec571a81a0fd0ada52d3b74567e3 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 14 Jan 2025 11:48:23 -0500 Subject: [PATCH 4/7] fix: Navigation of SR loaded studies --- .../core/src/RenderingEngine/StackViewport.ts | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index cbcd320ea9..a69bb001f7 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -131,11 +131,13 @@ interface SetVOIOptions { * the documentation section of this website. */ class StackViewport extends Viewport { - private imageIds: string[]; + private imageIds: string[] = []; + private imageIdsMap = new Map(); + // current imageIdIndex that is rendered in the viewport - private currentImageIdIndex: number; + private currentImageIdIndex = 0; // the imageIdIndex that is targeted to be loaded with scrolling but has not initiated loading yet - private targetImageIdIndex: number; + private targetImageIdIndex = 0; // setTimeout if the image is debounced to be loaded private debouncedTimeout: number; /** @@ -200,7 +202,6 @@ class StackViewport extends Viewport { ? this._resetCPUFallbackElement() : this._resetGPUViewport(); - this.imageIds = []; this.currentImageIdIndex = 0; this.targetImageIdIndex = 0; this.resetCamera(); @@ -1827,6 +1828,11 @@ class StackViewport extends Viewport { this._throwIfDestroyed(); this.imageIds = imageIds; + this.imageIdsMap.clear(); + imageIds.forEach((imageId, index) => { + this.imageIdsMap.set(imageId, index); + this.imageIdsMap.set(imageIdToURI(imageId), index); + }); this.currentImageIdIndex = currentImageIdIndex; this.targetImageIdIndex = currentImageIdIndex; const imageRetrieveConfiguration = metaData.get( @@ -3048,47 +3054,18 @@ class StackViewport extends Viewport { } = viewRef; // Optimize the return for the exact match cases - if (referencedImageId && sliceIndex !== undefined) { - if ( - testIndex >= sliceIndex && - testIndex <= sliceRangeEnd && - this.imageIds[sliceIndex] === referencedImageId - ) { + if (referencedImageId) { + if (referencedImageId === currentImageId) { return true; } - if ( - options.withNavigation && - this.imageIds[sliceIndex] == referencedImageId - ) { - return true; + const foundSliceIndex = this.imageIdsMap.get(referencedImageId); + if (foundSliceIndex === undefined) { + return false; } - - // Optimize the test for being viewable by defining the URI version - // This allows an endsWith test - viewRef.referencedImageUri ||= imageIdToURI(referencedImageId); - const { referencedImageUri } = viewRef; - - if ( - testIndex >= sliceIndex && - testIndex <= sliceRangeEnd && - this.imageIds[sliceIndex]?.endsWith(referencedImageUri) - ) { + if (options.withNavigation) { return true; } - if ( - options.withNavigation && - this.imageIds[sliceIndex]?.endsWith(referencedImageUri) - ) { - return true; - } - if ( - options.asOverlay && - this.matchImagesForOverlay(currentImageId, referencedImageId) - ) { - return true; - } - - return false; + return testIndex >= foundSliceIndex && testIndex <= sliceRangeEnd; } if (!super.isReferenceViewable(viewRef, options)) { @@ -3242,7 +3219,7 @@ class StackViewport extends Viewport { * @returns boolean if imageId is in viewport */ public hasImageId = (imageId: string): boolean => { - return this.imageIds.includes(imageId); + return this.imageIdsMap.has(imageId); }; /** @@ -3251,14 +3228,7 @@ class StackViewport extends Viewport { * @returns boolean if imageURI is in viewport */ public hasImageURI = (imageURI: string): boolean => { - const imageIds = this.imageIds; - for (let i = 0; i < imageIds.length; i++) { - if (imageIdToURI(imageIds[i]) === imageURI) { - return true; - } - } - - return false; + return this.imageIdsMap.has(imageIdToURI(imageURI)); }; private getCPUFallbackError(method: string): Error { From 5bff963c4221907ade7003afa220acb8543bf8fe Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 14 Jan 2025 11:52:10 -0500 Subject: [PATCH 5/7] Add example stackRange --- utils/ExampleRunner/example-info.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 5758ce150d..2ac9f6a1f8 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -175,6 +175,10 @@ "name": "Stack Annotation Tools", "description": "Demonstrates usage of various annotation tools (Probe, Rectangle ROI, Elliptical ROI, Bidirectional measurements) on a Stack Viewport." }, + "stackRange": { + "name": "Stack Range", + "description": "Demonstrates use of a selection range for key image and other tools" + }, "calibrationTools": { "name": "Calibration Tools", "description": "Demonstrates usage of calibration tools on a Stack Viewport." From c0069d2cf2c5f119de3fdab4624c3d6477ab159f Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 14 Jan 2025 19:47:50 -0500 Subject: [PATCH 6/7] fix: setViewReference needed to use the imageIdMap --- .../core/src/RenderingEngine/StackViewport.ts | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index a69bb001f7..8c14967203 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -3058,7 +3058,9 @@ class StackViewport extends Viewport { if (referencedImageId === currentImageId) { return true; } - const foundSliceIndex = this.imageIdsMap.get(referencedImageId); + viewRef.referencedImageUri ||= imageIdToURI(referencedImageId); + const { referencedImageUri } = viewRef; + const foundSliceIndex = this.imageIdsMap.get(referencedImageUri); if (foundSliceIndex === undefined) { return false; } @@ -3117,21 +3119,21 @@ class StackViewport extends Viewport { public getViewReference( viewRefSpecifier: ViewReferenceSpecifier = {} ): ViewReference { - const { sliceIndex = this.getCurrentImageIdIndex() } = viewRefSpecifier; + const { sliceIndex = this.getCurrentImageIdIndex(), sliceRangeEnd } = + viewRefSpecifier; const reference = super.getViewReference(viewRefSpecifier); let referencedImageId = - this.imageIds[this.imageIds.length === 1 ? 0 : (sliceIndex as number)]; + this.imageIds[this.imageIds.length === 1 ? 0 : sliceIndex]; if (!referencedImageId) { return; } - const iSliceNumber = sliceIndex as number; - if (this.imageIds.length === 1 && iSliceNumber > 0) { - if (iSliceNumber >= this.getNumberOfSlices()) { + if (this.imageIds.length === 1 && sliceIndex > 0) { + if (sliceIndex >= this.getNumberOfSlices()) { return; } referencedImageId = frameRangeUtils.multiframeImageId( referencedImageId, - iSliceNumber + 1 + sliceIndex + 1 ); } reference.referencedImageId = referencedImageId; @@ -3153,24 +3155,19 @@ class StackViewport extends Viewport { * Assumes that the slice index is correct for this viewport */ public setViewReference(viewRef: ViewReference): void { - if (!viewRef) { + if (!viewRef?.referencedImageId) { return; } - const { referencedImageId, sliceIndex } = viewRef; - if ( - typeof sliceIndex === 'number' && - referencedImageId && - referencedImageId === this.imageIds[sliceIndex] - ) { - this.scroll(sliceIndex - this.targetImageIdIndex); - } else { - const foundIndex = this.imageIds.indexOf(referencedImageId); - if (foundIndex !== -1) { - this.scroll(foundIndex - this.targetImageIdIndex); - } else { - throw new Error('Unsupported - referenced image id not found'); - } + const { referencedImageId } = viewRef; + viewRef.referencedImageUri ||= imageIdToURI(referencedImageId); + const { referencedImageUri } = viewRef; + const sliceIndex = this.imageIdsMap.get(referencedImageUri); + if (sliceIndex === undefined) { + console.error(`No image URI found for ${referencedImageUri}`); + return; } + + this.scroll(sliceIndex - this.targetImageIdIndex); } /** @@ -3178,10 +3175,7 @@ class StackViewport extends Viewport { * `imageId:` URN format. */ public getViewReferenceId(specifier: ViewReferenceSpecifier = {}): string { - const { sliceIndex: sliceIndex = this.currentImageIdIndex } = specifier; - if (Array.isArray(sliceIndex)) { - throw new Error('Use of slice ranges for stacks not supported'); - } + const { sliceIndex = this.currentImageIdIndex } = specifier; return `imageId:${this.imageIds[sliceIndex]}`; } @@ -3228,7 +3222,7 @@ class StackViewport extends Viewport { * @returns boolean if imageURI is in viewport */ public hasImageURI = (imageURI: string): boolean => { - return this.imageIdsMap.has(imageIdToURI(imageURI)); + return this.imageIdsMap.has(imageURI); }; private getCPUFallbackError(method: string): Error { From e162fb270f02e5e03eb7eb056d88d82465b87d18 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 15 Jan 2025 07:52:13 -0500 Subject: [PATCH 7/7] fix: Segmentation on stack viewport with fast isReferenceViewable --- .../core/src/RenderingEngine/StackViewport.ts | 24 +++++++++---------- packages/tools/examples/stackRange/index.ts | 12 ++++------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 8c14967203..b6aaa6ffec 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -3061,6 +3061,15 @@ class StackViewport extends Viewport { viewRef.referencedImageUri ||= imageIdToURI(referencedImageId); const { referencedImageUri } = viewRef; const foundSliceIndex = this.imageIdsMap.get(referencedImageUri); + if (options.asOverlay) { + const matchedImageId = this.matchImagesForOverlay( + currentImageId, + referencedImageId + ); + if (matchedImageId) { + return true; + } + } if (foundSliceIndex === undefined) { return false; } @@ -3119,23 +3128,12 @@ class StackViewport extends Viewport { public getViewReference( viewRefSpecifier: ViewReferenceSpecifier = {} ): ViewReference { - const { sliceIndex = this.getCurrentImageIdIndex(), sliceRangeEnd } = - viewRefSpecifier; + const { sliceIndex = this.getCurrentImageIdIndex() } = viewRefSpecifier; const reference = super.getViewReference(viewRefSpecifier); - let referencedImageId = - this.imageIds[this.imageIds.length === 1 ? 0 : sliceIndex]; + const referencedImageId = this.getCurrentImageId(sliceIndex); if (!referencedImageId) { return; } - if (this.imageIds.length === 1 && sliceIndex > 0) { - if (sliceIndex >= this.getNumberOfSlices()) { - return; - } - referencedImageId = frameRangeUtils.multiframeImageId( - referencedImageId, - sliceIndex + 1 - ); - } reference.referencedImageId = referencedImageId; if (this.getCurrentImageIdIndex() !== sliceIndex) { const referenceData = this.getImagePlaneReferenceData( diff --git a/packages/tools/examples/stackRange/index.ts b/packages/tools/examples/stackRange/index.ts index fbe454d708..827d7556b7 100644 --- a/packages/tools/examples/stackRange/index.ts +++ b/packages/tools/examples/stackRange/index.ts @@ -252,11 +252,6 @@ function selectNextAnnotation(direction) { if (!annotation) { return; } - console.log( - 'Navigating to', - annotation.metadata.sliceIndex, - annotation.metadata.referencedImageId - ); viewport.setViewReference(annotation.metadata); } @@ -270,10 +265,11 @@ async function run() { // Get Cornerstone imageIds and fetch metadata into RAM const imageIds = await createImageIdsAndCacheMetaData({ StudyInstanceUID: - '1.3.6.1.4.1.53684.1.1.2.4037847388.8168.1651298318.32092078', + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', SeriesInstanceUID: - '1.3.6.1.4.1.53684.1.1.3.4037847388.8168.1651298319.32092097', - wadoRsRoot: 'http://localhost:5000/dicomweb/', // getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: + getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', }); addAnnotationListeners();