From bd060d96506f4961990caded235c0a158d8de16c Mon Sep 17 00:00:00 2001 From: "Haasha.Atif" Date: Sun, 11 Feb 2024 23:05:27 +0500 Subject: [PATCH 1/6] feat(redaction): Redaction tool added --- packages/tools/src/index.ts | 2 + .../src/tools/annotation/RedactionTool.ts | 657 ++++++++++++++++++ packages/tools/src/tools/index.ts | 2 + 3 files changed, 661 insertions(+) create mode 100644 packages/tools/src/tools/annotation/RedactionTool.ts diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 5f7291b72c..20bd3a1caa 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -55,6 +55,7 @@ import { SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, + RedactionTool, SegmentationDisplayTool, BrushTool, AngleTool, @@ -126,6 +127,7 @@ export { ReferenceCursors, ReferenceLines, ScaleOverlayTool, + RedactionTool, // Segmentation Display SegmentationDisplayTool, // Segmentation Editing Tools diff --git a/packages/tools/src/tools/annotation/RedactionTool.ts b/packages/tools/src/tools/annotation/RedactionTool.ts new file mode 100644 index 0000000000..8e9d651950 --- /dev/null +++ b/packages/tools/src/tools/annotation/RedactionTool.ts @@ -0,0 +1,657 @@ +import { getEnabledElement, cache } from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; +import { AnnotationTool } from '../base'; +import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility'; +import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking'; + +import { + addAnnotation, + getAnnotations, + removeAnnotation, +} from '../../stateManagement'; +import { + drawHandles as drawHandlesSvg, + drawRedactionRect as drawRedactionRectSvg, +} from '../../drawingSvg'; +import { state } from '../../store'; +import { Events } from '../../enums'; +import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters'; +import * as rectangle from '../../utilities/math/rectangle'; +import { + resetElementCursor, + hideElementCursor, +} from '../../cursors/elementCursor'; +import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds'; +import { triggerAnnotationCompleted } from '../../stateManagement/annotation/helpers/state'; +import { + EventTypes, + ToolHandle, + TextBoxHandle, + ToolProps, + PublicToolProps, + SVGDrawingHelper, +} from '../../types'; +import { StyleSpecifier } from '../../types/AnnotationStyle'; +import { RedactionAnnotation } from '../../types/ToolSpecificAnnotationTypes'; + +/** + * RedactionTool lets you draw annotations that can redact the region of interest. + * You can use RedactionTool in all perpendicular views (axial, sagittal, coronal). + * Note: annotation tools in cornerstone3DTools exists in the exact location + * in the physical 3d space, as a result, by default, all annotations that are + * drawing in the same frameOfReference will get shared between viewports that + * are in the same frameOfReference. + * + * The resulting annotation's metadata (the + * state of the viewport while drawing was happening) will get added to the + * ToolState manager and can be accessed from the ToolState by calling getAnnotations + * or similar methods. + * + * ```js + * cornerstoneTools.addTool(RedactionTool) + * + * const toolGroup = ToolGroupManager.createToolGroup('toolGroupId') + * + * toolGroup.addTool(RedactionTool.toolName) + * + * toolGroup.addViewport('viewportId', 'renderingEngineId') + * + * toolGroup.setToolActive(RedactionTool.toolName, { + * bindings: [ + * { + * mouseButton: MouseBindings.Primary, // Left Click + * }, + * ], + * }) + * ``` + * + * Read more in the Docs section of the website. + */ +class RedactionTool extends AnnotationTool { + _throttledCalculateCachedStats: any; + editData: { + annotation: any; + viewportIdsToRender: string[]; + handleIndex?: number; + newAnnotation?: boolean; + hasMoved?: boolean; + } | null; + _configuration: any; + isDrawing: boolean; + isHandleOutsideImage: boolean; + + constructor( + toolProps: PublicToolProps = {}, + defaultToolProps: ToolProps = { + supportedInteractionTypes: ['Mouse', 'Touch'], + configuration: { + shadow: true, + preventHandleOutsideImage: false, + }, + } + ) { + super(toolProps, defaultToolProps); + + this._throttledCalculateCachedStats = undefined; + } + + /** + * Based on the current position of the mouse and the current imageId to create + * a Redaction Annotation and stores it in the annotationManager + * + * @param evt - EventTypes.NormalizedMouseEventType + * @returns The annotation object. + * + */ + addNewAnnotation = ( + evt: EventTypes.InteractionEventType + ): RedactionAnnotation => { + const eventDetail = evt.detail; + const { currentPoints, element } = eventDetail; + const worldPos = currentPoints.world; + const enabledElement = getEnabledElement(element); + const { viewport, renderingEngine } = 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, + }, + data: { + handles: { + points: [ + [...worldPos], + [...worldPos], + [...worldPos], + [...worldPos], + ], + activeHandleIndex: null, + }, + cachedStats: {}, + }, + }; + addAnnotation(annotation, element); + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + handleIndex: 3, + newAnnotation: true, + hasMoved: false, + }; + this._activateDraw(element); + hideElementCursor(element); + + evt.preventDefault(); + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); + + return annotation; + //******************************************** */ + }; + + /** + * It returns if the canvas point is near the provided annotation in the provided + * element or not. A proximity is passed to the function to determine the + * proximity of the point to the annotation in number of pixels. + * + * TO DO + * Configure this function to either use the line based or region based mechanism for dragging. + * + * @param element - HTML Element + * @param annotation - Annotation + * @param canvasCoords - Canvas coordinates + * @param proximity - Proximity to tool to consider + * @returns Boolean, whether the canvas point is near tool + */ + isPointNearTool = ( + element: HTMLDivElement, + annotation: RedactionAnnotation, + canvasCoords: Types.Point2, + proximity: number + ): boolean => { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + const { data } = annotation; + const { points } = data.handles; + + const canvasPoint1 = viewport.worldToCanvas(points[0]); + const canvasPoint2 = viewport.worldToCanvas(points[3]); + + const rect = this._getRectangleImageCoordinates([ + canvasPoint1, + canvasPoint2, + ]); + + const point = [canvasCoords[0], canvasCoords[1]] as Types.Point2; + const { left, top, width, height } = rect; + + const distanceToPoint = rectangle.distanceToPoint( + [left, top, width, height], + point + ); + + if (distanceToPoint <= proximity) { + return true; + } + + return false; + }; + + toolSelectedCallback = ( + evt: EventTypes.InteractionEventType, + annotation: RedactionAnnotation + ): void => { + const eventDetail = evt.detail; + const { element } = eventDetail; + + annotation.highlighted = true; + + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + }; + + this._activateModify(element); + + hideElementCursor(element); + + const enabledElement = getEnabledElement(element); + const { renderingEngine } = enabledElement; + + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); + + evt.preventDefault(); + }; + handleSelectedCallback = ( + evt: EventTypes.InteractionEventType, + annotation: RedactionAnnotation, + handle: ToolHandle + ): void => { + const eventDetail = evt.detail; + const { element } = eventDetail; + const { data } = annotation; + + annotation.highlighted = true; + + let movingTextBox = false; + let handleIndex; + + if ((handle as TextBoxHandle).worldPosition) { + movingTextBox = true; + } else { + handleIndex = data.handles.points.findIndex((p) => p === handle); + } + + // Find viewports to render on drag. + const viewportIdsToRender = getViewportIdsWithToolToRender( + element, + this.getToolName() + ); + + this.editData = { + annotation, + viewportIdsToRender, + handleIndex, + }; + this._activateModify(element); + + hideElementCursor(element); + + const enabledElement = getEnabledElement(element); + const { renderingEngine } = enabledElement; + + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); + + evt.preventDefault(); + }; + + _endCallback = (evt: EventTypes.InteractionEventType): void => { + const eventDetail = evt.detail; + const { element } = eventDetail; + + const { annotation, viewportIdsToRender, newAnnotation, hasMoved } = + this.editData; + const { data } = annotation; + + if (newAnnotation && !hasMoved) { + return; + } + + data.handles.activeHandleIndex = null; + + this._deactivateModify(element); + this._deactivateDraw(element); + + resetElementCursor(element); + + const { renderingEngine } = getEnabledElement(element); + + this.editData = null; + this.isDrawing = false; + + if ( + this.isHandleOutsideImage && + this.configuration.preventHandleOutsideImage + ) { + removeAnnotation(annotation.annotationUID); + } + + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); + + if (newAnnotation) { + triggerAnnotationCompleted(annotation); + } + }; + + _dragCallback = (evt: EventTypes.InteractionEventType): void => { + this.isDrawing = true; + + const eventDetail = evt.detail; + const { element } = eventDetail; + const { annotation, viewportIdsToRender, handleIndex } = this.editData; + const { data } = annotation; + if (handleIndex === undefined) { + // Drag mode - Moving tool, so move all points by the world points delta + const { deltaPoints } = eventDetail as EventTypes.MouseDragEventDetail; + const worldPosDelta = deltaPoints.world; + + const { points } = data.handles; + + points.forEach((point) => { + point[0] += worldPosDelta[0]; + point[1] += worldPosDelta[1]; + point[2] += worldPosDelta[2]; + }); + annotation.invalidated = true; + } else { + // Moving handle. + const { currentPoints } = eventDetail; + const enabledElement = getEnabledElement(element); + const { worldToCanvas, canvasToWorld } = enabledElement.viewport; + const worldPos = currentPoints.world; + + const { points } = data.handles; + + // Move this handle. + points[handleIndex] = [...worldPos]; + + let bottomLeftCanvas; + let bottomRightCanvas; + let topLeftCanvas; + let topRightCanvas; + + let bottomLeftWorld; + let bottomRightWorld; + let topLeftWorld; + let topRightWorld; + + switch (handleIndex) { + case 0: + case 3: + // Moving bottomLeft or topRight + + bottomLeftCanvas = worldToCanvas(points[0]); + topRightCanvas = worldToCanvas(points[3]); + + bottomRightCanvas = [topRightCanvas[0], bottomLeftCanvas[1]]; + topLeftCanvas = [bottomLeftCanvas[0], topRightCanvas[1]]; + + bottomRightWorld = canvasToWorld(bottomRightCanvas); + topLeftWorld = canvasToWorld(topLeftCanvas); + + points[1] = bottomRightWorld; + points[2] = topLeftWorld; + + break; + case 1: + case 2: + // Moving bottomRight or topLeft + bottomRightCanvas = worldToCanvas(points[1]); + topLeftCanvas = worldToCanvas(points[2]); + + bottomLeftCanvas = [ + topLeftCanvas[0], + bottomRightCanvas[1], + ]; + topRightCanvas = [ + bottomRightCanvas[0], + topLeftCanvas[1], + ]; + + bottomLeftWorld = canvasToWorld(bottomLeftCanvas); + topRightWorld = canvasToWorld(topRightCanvas); + + points[0] = bottomLeftWorld; + points[3] = topRightWorld; + + break; + } + annotation.invalidated = true; + } + this.editData.hasMoved = true; + const enabledElement = getEnabledElement(element); + const { renderingEngine } = enabledElement; + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); + }; + + cancel = (element: HTMLDivElement) => { + // If it is mid-draw or mid-modify + if (this.isDrawing) { + this.isDrawing = false; + this._deactivateDraw(element); + this._deactivateModify(element); + resetElementCursor(element); + + const { annotation, viewportIdsToRender, newAnnotation } = this.editData; + + const { data } = annotation; + + annotation.highlighted = false; + data.handles.activeHandleIndex = null; + + const { renderingEngine } = getEnabledElement(element); + + triggerAnnotationRenderForViewportIds( + renderingEngine, + viewportIdsToRender + ); + + if (newAnnotation) { + triggerAnnotationCompleted(annotation); + } + + this.editData = null; + return annotation.annotationUID; + } + }; + + /** + * Add event handlers for the modify event loop, and prevent default event prapogation. + */ + _activateDraw = (element) => { + state.isInteractingWithTool = true; + + element.addEventListener(Events.MOUSE_UP, this._endCallback); + element.addEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.addEventListener(Events.MOUSE_MOVE, this._dragCallback); + element.addEventListener(Events.MOUSE_CLICK, this._endCallback); + + element.addEventListener(Events.TOUCH_END, this._endCallback); + element.addEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.addEventListener(Events.TOUCH_TAP, this._endCallback); + }; + + /** + * Add event handlers for the modify event loop, and prevent default event prapogation. + */ + _deactivateDraw = (element) => { + state.isInteractingWithTool = false; + + element.removeEventListener(Events.MOUSE_UP, this._endCallback); + element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.removeEventListener(Events.MOUSE_MOVE, this._dragCallback); + element.removeEventListener(Events.MOUSE_CLICK, this._endCallback); + + element.removeEventListener(Events.TOUCH_END, this._endCallback); + element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.removeEventListener(Events.TOUCH_TAP, this._endCallback); + }; + + /** + * Add event handlers for the modify event loop, and prevent default event prapogation. + */ + _activateModify = (element) => { + state.isInteractingWithTool = true; + + element.addEventListener(Events.MOUSE_UP, this._endCallback); + element.addEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.addEventListener(Events.MOUSE_CLICK, this._endCallback); + + element.addEventListener(Events.TOUCH_END, this._endCallback); + element.addEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.addEventListener(Events.TOUCH_TAP, this._endCallback); + }; + + /** + * Remove event handlers for the modify event loop, and enable default event propagation. + */ + _deactivateModify = (element) => { + state.isInteractingWithTool = false; + + element.removeEventListener(Events.MOUSE_UP, this._endCallback); + element.removeEventListener(Events.MOUSE_DRAG, this._dragCallback); + element.removeEventListener(Events.MOUSE_CLICK, this._endCallback); + + element.removeEventListener(Events.TOUCH_END, this._endCallback); + element.removeEventListener(Events.TOUCH_DRAG, this._dragCallback); + element.removeEventListener(Events.TOUCH_TAP, this._endCallback); + }; + + /** + * it is used to draw the redaction annotation in each + * request animation frame. + * + * @param enabledElement - The Cornerstone's enabledElement. + * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing. + */ + renderAnnotation = ( + enabledElement: Types.IEnabledElement, + svgDrawingHelper: SVGDrawingHelper + ): boolean => { + let renderStatus = false; + const { viewport } = enabledElement; + const { element } = viewport; + + let annotations = getAnnotations(this.getToolName(), element); + + if (!annotations?.length) { + return renderStatus; + } + + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + + if (!annotations?.length) { + return renderStatus; + } + + const targetId = this.getTargetId(viewport); + const renderingEngine = viewport.getRenderingEngine(); + + const styleSpecifier: StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + + for (let i = 0; i < annotations.length; i++) { + const annotation = annotations[i] as RedactionAnnotation; + const { annotationUID, data } = annotation; + const { points, activeHandleIndex } = data.handles; + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); + + styleSpecifier.annotationUID = annotationUID; + const { color, lineWidth, lineDash } = this.getAnnotationStyle({ + annotation, + styleSpecifier, + }); + + const { viewPlaneNormal, viewUp } = viewport.getCamera(); + + const toolMetadata = annotation.metadata; + + // If rendering engine has been destroyed while rendering + if (!viewport.getRenderingEngine()) { + console.warn('Rendering Engine has been destroyed'); + return renderStatus; + } + + let activeHandleCanvasCoords; + + if (!isAnnotationVisible(annotationUID)) { + continue; + } + if ( + !isAnnotationLocked(annotation) && + !this.editData && + activeHandleIndex !== null + ) { + // Not locked or creating and hovering over handle, so render handle. + activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]]; + } + if (activeHandleCanvasCoords) { + const handleGroupUID = '0'; + drawHandlesSvg( + svgDrawingHelper, + annotationUID, + handleGroupUID, + activeHandleCanvasCoords, + { + color, + } + ); + } + + const rectangleUID = '0'; + drawRedactionRectSvg( + svgDrawingHelper, + annotationUID, + rectangleUID, + canvasCoordinates[0], + canvasCoordinates[3], + { + color, + lineDash, + lineWidth, + } + ); + + renderStatus = true; + } + return renderStatus; + }; + + _getRectangleImageCoordinates = ( + points: Array + ): { + left: number; + top: number; + width: number; + height: number; + } => { + const [point0, point1] = points; + + return { + left: Math.min(point0[0], point1[0]), + top: Math.min(point0[1], point1[1]), + width: Math.abs(point0[0] - point1[0]), + height: Math.abs(point0[1] - point1[1]), + }; + }; + + _getImageVolumeFromTargetUID(targetUID, renderingEngine) { + let imageVolume, viewport; + if (targetUID.startsWith('stackTarget')) { + const coloneIndex = targetUID.indexOf(':'); + const viewportUID = targetUID.substring(coloneIndex + 1); + const viewport = renderingEngine.getViewport(viewportUID); + imageVolume = viewport.getImageData(); + } else { + imageVolume = cache.getVolume(targetUID); + } + + return { imageVolume, viewport }; + } +} + +RedactionTool.toolName = 'Redaction'; +export default RedactionTool; diff --git a/packages/tools/src/tools/index.ts b/packages/tools/src/tools/index.ts index 3fd431f396..053e8aabc5 100644 --- a/packages/tools/src/tools/index.ts +++ b/packages/tools/src/tools/index.ts @@ -37,6 +37,7 @@ import AngleTool from './annotation/AngleTool'; import CobbAngleTool from './annotation/CobbAngleTool'; import UltrasoundDirectionalTool from './annotation/UltrasoundDirectionalTool'; import KeyImageTool from './annotation/KeyImageTool'; +import RedactionTool from './annotation/RedactionTool'; // Segmentation DisplayTool import SegmentationDisplayTool from './displayTools/SegmentationDisplayTool'; @@ -90,6 +91,7 @@ export { CobbAngleTool, UltrasoundDirectionalTool, KeyImageTool, + RedactionTool, // Segmentations Display SegmentationDisplayTool, // Segmentations Tools From 5d7ea7fd9391161b3a6e6963f1e5b89e50378b12 Mon Sep 17 00:00:00 2001 From: "Haasha.Atif" Date: Sun, 11 Feb 2024 23:06:28 +0500 Subject: [PATCH 2/6] feat(redaction): RedactionAnnotation added --- .../src/types/ToolSpecificAnnotationTypes.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index f59e70e853..8d1b7c26d0 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -439,4 +439,24 @@ export interface VideoRedactionAnnotation extends Annotation { }; } +//Identical to VideoRedaction Tool for now +export interface RedactionAnnotation extends Annotation { + metadata: { + viewPlaneNormal: Types.Point3; + viewUp: Types.Point3; + FrameOfReferenceUID: string; + referencedImageId: string; + toolName: string; + }; + data: { + handles: { + points: Types.Point3[]; + activeHandleIndex: number | null; + }; + cachedStats: { + [key: string]: any; // Can be more specific if the structure is known + }; + }; +} + export type { ContourAnnotation }; From 0448e9102e987b61fa3980c544080b5b77b9eaed Mon Sep 17 00:00:00 2001 From: "Haasha.Atif" Date: Sun, 11 Feb 2024 23:07:08 +0500 Subject: [PATCH 3/6] feat(redaction): frameRange added to AnnotationTypes --- packages/tools/src/types/AnnotationTypes.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tools/src/types/AnnotationTypes.ts b/packages/tools/src/types/AnnotationTypes.ts index aba60fec23..70b607e8a8 100644 --- a/packages/tools/src/types/AnnotationTypes.ts +++ b/packages/tools/src/types/AnnotationTypes.ts @@ -27,6 +27,8 @@ type Annotation = { invalidated?: boolean; /** If the annotation is auto generated from other annotations*/ autoGenerated?: boolean; + /** The range of annotation to be visible on for multiple images for Stack or Volume Viewport*/ + frameRange?: Types.Point2; /** Metadata for annotation */ metadata: Types.ViewReference & { /** From 4364bc8ba1e3bd5d13746a3b65305d13f0d8ab6b Mon Sep 17 00:00:00 2001 From: "Haasha.Atif" Date: Sun, 11 Feb 2024 23:07:35 +0500 Subject: [PATCH 4/6] feat(redaction): annotationFrameRange updated for StackViewport --- .../src/utilities/annotationFrameRange.ts | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/utilities/annotationFrameRange.ts b/packages/tools/src/utilities/annotationFrameRange.ts index 0499dffe9c..85a75e3d9a 100644 --- a/packages/tools/src/utilities/annotationFrameRange.ts +++ b/packages/tools/src/utilities/annotationFrameRange.ts @@ -1,7 +1,7 @@ import { triggerEvent, eventTarget } from '@cornerstonejs/core'; import Events from '../enums/Events'; import { Annotation } from '../types'; - +import type { Types } from '@cornerstonejs/core'; export type FramesRange = [number, number] | number; /** @@ -75,4 +75,48 @@ export default class AnnotationFrameRange { ): number | [number, number] { return this.imageIdToFrames(annotation.metadata.referencedImageId); } + + /** + * Sets the range of frames associated with the given annotation for Stack and Volume Viewport. + * The range can be a range of values in the format `min-max` where min, max are inclusive + * Updates the frameRange in annotation to specify updated range. + * */ + public static setStackFrameRange( + annotation: Annotation, + range: Types.Point2, + eventBase?: { viewportId; renderingEngineId } + ) { + annotation.frameRange = range; + const eventDetail = { + ...eventBase, + annotation + }; + triggerEvent(eventTarget, Events.ANNOTATION_MODIFIED, eventDetail); + } + + public static getStackFrameRange( + annotation: Annotation + ): Types.Point2 | null { + if (annotation.frameRange) { + return annotation.frameRange; + } + return null; + } + + /** + * Deletes the frameRange from annotation if exists. + */ + public static removeStackFrameRange( + annotation: Annotation, + eventBase?: { viewportId; renderingEngineId } + ) { + if (annotation.frameRange) { + delete annotation.frameRange; + } + const eventDetail = { + ...eventBase, + annotation + }; + triggerEvent(eventTarget, Events.ANNOTATION_MODIFIED, eventDetail); + } } From 3930733d1658d3621211e13640e5dd84f46ead6a Mon Sep 17 00:00:00 2001 From: "Haasha.Atif" Date: Sun, 11 Feb 2024 23:09:16 +0500 Subject: [PATCH 5/6] feat(redaction): example added and filterAnnotations updated --- .../stackAnnotationSeriesTools/index.ts | 323 ++++++++++++++++++ .../planar/filterAnnotationsForDisplay.ts | 4 + 2 files changed, 327 insertions(+) create mode 100644 packages/tools/examples/stackAnnotationSeriesTools/index.ts diff --git a/packages/tools/examples/stackAnnotationSeriesTools/index.ts b/packages/tools/examples/stackAnnotationSeriesTools/index.ts new file mode 100644 index 0000000000..10f2d39f85 --- /dev/null +++ b/packages/tools/examples/stackAnnotationSeriesTools/index.ts @@ -0,0 +1,323 @@ +import { + RenderingEngine, + Types, + Enums, + getRenderingEngine, + eventTarget, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addDropdownToToolbar, + addButtonToToolbar, +} 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 { + LengthTool, + ProbeTool, + RectangleROITool, + EllipticalROITool, + CircleROITool, + BidirectionalTool, + AngleTool, + CobbAngleTool, + ToolGroupManager, + ArrowAnnotateTool, + PlanarFreehandROITool, + KeyImageTool, + RedactionTool, + StackScrollMouseWheelTool, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { ViewportType, Events } = Enums; +const { MouseBindings, Events: toolsEvents } = csToolsEnums; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'CT_STACK'; + +const { annotationFrameRange } = cornerstoneTools.utilities; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Annotation Tools Stack Series', + 'Annotation tools for a stack viewport' +); + +const content = document.getElementById('content'); +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 info = document.createElement('div'); +content.appendChild(info); + +const instructions = document.createElement('p'); +instructions.innerText = 'Left Click to use selected tool'; +info.appendChild(instructions); + +const rotationInfo = document.createElement('div'); +info.appendChild(rotationInfo); + +const flipHorizontalInfo = document.createElement('div'); +info.appendChild(flipHorizontalInfo); + +const flipVerticalInfo = document.createElement('div'); +info.appendChild(flipVerticalInfo); + +element.addEventListener(Events.CAMERA_MODIFIED, (_) => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + if (!viewport) { + return; + } + + const { flipHorizontal, flipVertical } = viewport.getCamera(); + const { rotation } = viewport.getProperties(); + + rotationInfo.innerText = `Rotation: ${Math.round(rotation)}`; + flipHorizontalInfo.innerText = `Flip horizontal: ${flipHorizontal}`; + flipVerticalInfo.innerText = `Flip vertical: ${flipVertical}`; +}); +// ============================= // + +const toolGroupId = 'STACK_TOOL_GROUP_ID'; + +const cancelToolDrawing = (evt) => { + const { element, key } = evt.detail; + if (key === 'Escape') { + cornerstoneTools.cancelActiveManipulations(element); + } +}; + +element.addEventListener(csToolsEnums.Events.KEY_DOWN, (evt) => { + cancelToolDrawing(evt); +}); + +const toolsNames = [ + LengthTool.toolName, + ProbeTool.toolName, + RectangleROITool.toolName, + EllipticalROITool.toolName, + CircleROITool.toolName, + BidirectionalTool.toolName, + AngleTool.toolName, + CobbAngleTool.toolName, + ArrowAnnotateTool.toolName, + PlanarFreehandROITool.toolName, + RedactionTool.toolName, + KeyImageTool.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({ + title: 'Flip H', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + const { flipHorizontal } = viewport.getCamera(); + viewport.setCamera({ flipHorizontal: !flipHorizontal }); + + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Flip V', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + const { flipVertical } = viewport.getCamera(); + + viewport.setCamera({ flipVertical: !flipVertical }); + + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Rotate Delta 90', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + const { rotation } = viewport.getProperties(); + viewport.setProperties({ rotation: rotation + 90 }); + + viewport.render(); + }, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(LengthTool); + cornerstoneTools.addTool(ProbeTool); + cornerstoneTools.addTool(RectangleROITool); + cornerstoneTools.addTool(EllipticalROITool); + cornerstoneTools.addTool(CircleROITool); + cornerstoneTools.addTool(BidirectionalTool); + cornerstoneTools.addTool(AngleTool); + cornerstoneTools.addTool(CobbAngleTool); + cornerstoneTools.addTool(ArrowAnnotateTool); + cornerstoneTools.addTool(PlanarFreehandROITool); + cornerstoneTools.addTool(KeyImageTool); + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(RedactionTool); + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add the tools to the tool group + toolGroup.addTool(LengthTool.toolName); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + toolGroup.addTool(ProbeTool.toolName); + toolGroup.addTool(RectangleROITool.toolName); + toolGroup.addTool(EllipticalROITool.toolName); + toolGroup.addTool(CircleROITool.toolName); + toolGroup.addTool(BidirectionalTool.toolName); + toolGroup.addTool(AngleTool.toolName); + toolGroup.addTool(CobbAngleTool.toolName); + toolGroup.addTool(ArrowAnnotateTool.toolName); + toolGroup.addTool(PlanarFreehandROITool.toolName); + toolGroup.addTool(KeyImageTool.toolName); + toolGroup.addTool(RedactionTool.toolName); + + // Set the initial state of the tools, here we set one tool active on left click. + // This means left click will draw that tool. + toolGroup.setToolActive(toolsNames[0], { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + // We set all the other tools passive here, this means that any state is rendered, and editable + // But aren't actively being drawn (see the toolModes example for information) + toolGroup.setToolPassive(ProbeTool.toolName); + toolGroup.setToolPassive(RectangleROITool.toolName); + toolGroup.setToolPassive(EllipticalROITool.toolName); + toolGroup.setToolPassive(CircleROITool.toolName); + toolGroup.setToolPassive(BidirectionalTool.toolName); + toolGroup.setToolPassive(AngleTool.toolName); + toolGroup.setToolPassive(ArrowAnnotateTool.toolName); + toolGroup.setToolPassive(PlanarFreehandROITool.toolName); + toolGroup.setToolPassive(RedactionTool.toolName); + toolGroup.setToolConfiguration(PlanarFreehandROITool.toolName, { + calculateStats: true, + }); + eventTarget.addEventListener(toolsEvents.ANNOTATION_ADDED, (evt) => { + const { detail } = evt; + console.log(detail.annotation) + const length = viewport.getNumberOfSlices() + annotationFrameRange.setStackFrameRange(detail.annotation, [0,length]) + }) + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + }); + + // 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); + + // Set the tool group on the viewport + toolGroup.addViewport(viewportId, renderingEngineId); + + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Define a stack containing a single image + // const stack = [imageIds[0]]; + + // Set the stack on the viewport + viewport.setStack(imageIds); + + // Render the image + viewport.render(); +} + +run(); diff --git a/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts b/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts index 55a5c7c050..5aeca435f7 100644 --- a/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts +++ b/packages/tools/src/utilities/planar/filterAnnotationsForDisplay.ts @@ -47,6 +47,10 @@ export default function filterAnnotationsForDisplay( if (!annotation.isVisible) { return false; } + if (annotation.frameRange){ + const frameNumber = viewport.getCurrentImageIdIndex() + return frameNumber >= annotation.frameRange[0] && frameNumber <= annotation.frameRange[1]; + } return viewport.isReferenceViewable(annotation.metadata, filterOptions); }); } From cfcd0b522ce52324b8dc92fe722253e3204b6bd3 Mon Sep 17 00:00:00 2001 From: Haasha Bin Atif <50842417+Haasha@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:46:04 +0500 Subject: [PATCH 6/6] feat(redaction): Update tools.api.md --- common/reviews/api/tools.api.md | 93 +++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 2bee6c7eaf..cdb35761b6 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -236,6 +236,7 @@ type Annotation = { isVisible?: boolean; invalidated?: boolean; autoGenerated?: boolean; + frameRange?: Types_2.Point2; metadata: Types_2.ViewReference & { toolName: string; cameraPosition?: Types_2.Point3; @@ -321,12 +322,24 @@ class AnnotationFrameRange { // (undocumented) static getFrameRange(annotation: Annotation): number | [number, number]; // (undocumented) + static getStackFrameRange(annotation: Annotation): Types_2.Point2 | null; + // (undocumented) protected static imageIdToFrames(imageId: string): FramesRange; // (undocumented) + static removeStackFrameRange(annotation: Annotation, eventBase?: { + viewportId: any; + renderingEngineId: any; + }): void; + // (undocumented) static setFrameRange(annotation: Annotation, range: FramesRange | string, eventBase?: { viewportId: any; renderingEngineId: any; }): void; + // (undocumented) + static setStackFrameRange(annotation: Annotation, range: Types_2.Point2, eventBase?: { + viewportId: any; + renderingEngineId: any; + }): void; } // @public (undocumented) @@ -4237,6 +4250,85 @@ export class RectangleScissorsTool extends BaseTool { static toolName: any; } +// @public (undocumented) +interface RedactionAnnotation extends Annotation { + // (undocumented) + data: { + handles: { + points: Types_2.Point3[]; + activeHandleIndex: number | null; + }; + cachedStats: { + [key: string]: any; + }; + }; + // (undocumented) + metadata: { + viewPlaneNormal: Types_2.Point3; + viewUp: Types_2.Point3; + FrameOfReferenceUID: string; + referencedImageId: string; + toolName: string; + }; +} + +// @public (undocumented) +export class RedactionTool extends AnnotationTool { + constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps); + // (undocumented) + _activateDraw: (element: any) => void; + // (undocumented) + _activateModify: (element: any) => void; + // (undocumented) + addNewAnnotation: (evt: EventTypes_2.InteractionEventType) => RedactionAnnotation; + // (undocumented) + cancel: (element: HTMLDivElement) => any; + // (undocumented) + _configuration: any; + // (undocumented) + _deactivateDraw: (element: any) => void; + // (undocumented) + _deactivateModify: (element: any) => void; + // (undocumented) + _dragCallback: (evt: EventTypes_2.InteractionEventType) => void; + // (undocumented) + editData: { + annotation: any; + viewportIdsToRender: string[]; + handleIndex?: number; + newAnnotation?: boolean; + hasMoved?: boolean; + } | null; + // (undocumented) + _endCallback: (evt: EventTypes_2.InteractionEventType) => void; + // (undocumented) + _getImageVolumeFromTargetUID(targetUID: any, renderingEngine: any): { + imageVolume: any; + viewport: any; + }; + // (undocumented) + _getRectangleImageCoordinates: (points: Array) => { + left: number; + top: number; + width: number; + height: number; + }; + // (undocumented) + handleSelectedCallback: (evt: EventTypes_2.InteractionEventType, annotation: RedactionAnnotation, handle: ToolHandle) => void; + // (undocumented) + isDrawing: boolean; + // (undocumented) + isHandleOutsideImage: boolean; + // (undocumented) + isPointNearTool: (element: HTMLDivElement, annotation: RedactionAnnotation, canvasCoords: Types_2.Point2, proximity: number) => boolean; + // (undocumented) + renderAnnotation: (enabledElement: Types_2.IEnabledElement, svgDrawingHelper: SVGDrawingHelper) => boolean; + // (undocumented) + _throttledCalculateCachedStats: any; + // (undocumented) + toolSelectedCallback: (evt: EventTypes_2.InteractionEventType, annotation: RedactionAnnotation) => void; +} + // @public (undocumented) interface ReferenceCursor extends Annotation { // (undocumented) @@ -5417,6 +5509,7 @@ declare namespace ToolSpecificAnnotationTypes { ReferenceLineAnnotation, ScaleOverlayAnnotation, VideoRedactionAnnotation, + RedactionAnnotation, ContourAnnotation } }