Skip to content

Commit

Permalink
feat: Planar freehand roi tool (#89)
Browse files Browse the repository at this point in the history
* WIP, made SVG render + starting draw interaction.

* WIP cross on create to trigger stop and later edit

* Drawing complete + resamping and breaking on cross.

* Implement open contour drawing

* WIP open contour + adding editing paths

* Edit open end loop

* WIP contour editing

* Find line to snap to

* Snap + register second cross + cropping of edit line so that we have clear criteria for the preview contour

* Rendering preview bug fixed.

* Finish closed contour edit and implement most of open contour edit logic

* WIP open contour editing drag over end

* Fix open contour edit placement of handles and write open edit line drag to draw transition.

* Stable for demo

* Implement edit on cross.

* WIP find other snap index if first will break crossing rules

* Fix more edge cases.

* Reconcile changes with upstream.

* Event emission

* Fix start of edits when you draw along the line

* Cleanup unused variables + simplify some segments to just the important index for calculations

* Implement missing spacing calculation for volume viewports.

* WIP update all tsdoc + refactor

* Cleanup + doc + type

* try to fix type issues

* WIP review comments.

* WIP more comments

* Cleanup rendering logic.

* Fix issues

* fix build issues

Co-authored-by: Erik Ziegler <[email protected]>
Co-authored-by: Alireza <[email protected]>
  • Loading branch information
3 people authored May 3, 2022
1 parent d743a45 commit 0067339
Show file tree
Hide file tree
Showing 25 changed files with 3,309 additions and 3 deletions.
125 changes: 124 additions & 1 deletion common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type ActorEntry = {
// @public (undocumented)
function addAnnotation(element: HTMLDivElement, annotation: Annotation): void;

// @public (undocumented)
const addCanvasPointsToArray: (element: HTMLDivElement, canvasPoints: Types_2.Point2[], newCanvasPoint: Types_2.Point2, commonData: PlanarFreehandROICommonData) => number;

// @public (undocumented)
function addColorLUT(colorLUT: ColorLUT, index: number): void;

Expand Down Expand Up @@ -80,6 +83,7 @@ type Annotation = {
};
[key: string]: any;
};
[key: string]: any;
cachedStats?: unknown;
};
};
Expand Down Expand Up @@ -426,6 +430,9 @@ export class BrushTool extends BaseTool {
static toolName: string;
}

// @public (undocumented)
function calculateAreaOfPoints(points: Types_2.Point2[]): number;

// @public (undocumented)
function calibrateImageSpacing(imageId: string, renderingEngine: Types_2.IRenderingEngine, rowPixelSpacing: number, columnPixelSpacing: number): void;

Expand Down Expand Up @@ -1023,6 +1030,7 @@ declare namespace drawing {
drawEllipse,
drawHandles,
drawLine,
drawPolyline,
drawLinkedTextBox,
drawRect,
drawTextBox,
Expand All @@ -1043,6 +1051,15 @@ function drawLine(svgDrawingHelper: any, annotationUID: string, lineUID: string,
// @public (undocumented)
function drawLinkedTextBox(svgDrawingHelper: Record<string, unknown>, annotationUID: string, textBoxUID: string, textLines: Array<string>, textBoxPosition: Types_2.Point2, annotationAnchorPoints: Array<Types_2.Point2>, textBox: unknown, options?: {}): SVGRect;

// @public (undocumented)
function drawPolyline(svgDrawingHelper: any, annotationUID: string, polylineUID: string, points: Types_2.Point2[], options: {
color?: string;
width?: number;
lineWidth?: number;
lineDash?: string;
connectLastToFirst?: boolean;
}): void;

// @public (undocumented)
function drawRect(svgDrawingHelper: any, annotationUID: string, rectangleUID: string, start: Types_2.Point2, end: Types_2.Point2, options?: {}): void;

Expand Down Expand Up @@ -1400,6 +1417,12 @@ function getBoundingBoxAroundShape(vertices: Types_2.Point3[], dimensions?: Type
// @public (undocumented)
function getCanvasEllipseCorners(ellipseCanvasPoints: canvasCoordinates): Array<Types_2.Point2>;

// @public (undocumented)
function getClosestIntersectionWithPolyline(points: Types_2.Point2[], p1: Types_2.Point2, q1: Types_2.Point2, closed?: boolean): {
segment: Types_2.Point2;
distance: number;
} | undefined;

// @public (undocumented)
function getColorForSegmentIndex(toolGroupId: string, segmentationRepresentationUID: string, segmentIndex: number): Color;

Expand All @@ -1415,6 +1438,9 @@ function getDefaultRepresentationConfig(segmentation: Segmentation): LabelmapCon
// @public (undocumented)
function getDefaultSegmentationStateManager(): SegmentationStateManager;

// @public (undocumented)
function getFirstIntersectionWithPolyline(points: Types_2.Point2[], p1: Types_2.Point2, q1: Types_2.Point2, closed?: boolean): Types_2.Point2 | undefined;

// @public (undocumented)
function getFont(settings?: Settings, state?: AnnotationStyleStates, mode?: ToolModes): string;

Expand Down Expand Up @@ -1464,6 +1490,13 @@ function getState(annotation?: Annotation): AnnotationStyleStates;
// @public (undocumented)
function getStyle(toolName?: string, annotation?: Record<string, unknown>): Settings;

// @public (undocumented)
const getSubPixelSpacingAndXYDirections: (viewport: Types_2.IStackViewport | Types_2.IVolumeViewport, subPixelResolution: number) => {
spacing: Types_2.Point2;
xDir: Types_2.Point3;
yDir: Types_2.Point3;
};

// @public (undocumented)
function getSynchronizer(synchronizerId: string): Synchronizer | void;

Expand Down Expand Up @@ -2312,7 +2345,8 @@ declare namespace math {
vec2,
ellipse,
lineSegment,
rectangle
rectangle,
polyline
}
}

Expand Down Expand Up @@ -2533,6 +2567,76 @@ type PlanarBoundingBox = {
height: number;
};

// @public (undocumented)
interface PlanarFreehandROIAnnotation extends Annotation {
// (undocumented)
data: {
polyline: Types_2.Point3[];
label?: string;
isOpenContour?: boolean;
handles: {
points: Types_2.Point3[];
activeHandleIndex: number | null;
textBox: {
hasMoved: boolean;
worldPosition: Types_2.Point3;
worldBoundingBox: {
topLeft: Types_2.Point3;
topRight: Types_2.Point3;
bottomLeft: Types_2.Point3;
bottomRight: Types_2.Point3;
};
};
};
};
// (undocumented)
metadata: {
cameraPosition?: Types_2.Point3;
cameraFocalPoint?: Types_2.Point3;
viewPlaneNormal?: Types_2.Point3;
viewUp?: Types_2.Point3;
annotationUID?: string;
FrameOfReferenceUID: string;
referencedImageId?: string;
toolName: string;
};
}

// @public (undocumented)
export class PlanarFreehandROITool extends AnnotationTool {
constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
// (undocumented)
addNewAnnotation: (evt: EventTypes_2.MouseDownActivateEventType) => PlanarFreehandROIAnnotation;
// (undocumented)
cancel: (element: HTMLDivElement) => void;
// (undocumented)
handleSelectedCallback: (evt: EventTypes_2.MouseDownEventType, annotation: PlanarFreehandROIAnnotation, handle: ToolHandle, interactionType?: string) => void;
// (undocumented)
isDrawing: boolean;
// (undocumented)
isEditingClosed: boolean;
// (undocumented)
isEditingOpen: boolean;
// (undocumented)
isPointNearTool: (element: HTMLDivElement, annotation: PlanarFreehandROIAnnotation, canvasCoords: Types_2.Point2, proximity: number) => boolean;
// (undocumented)
mouseDragCallback: any;
// (undocumented)
renderAnnotation: (enabledElement: Types_2.IEnabledElement, svgDrawingHelper: any) => void;
// (undocumented)
_throttledCalculateCachedStats: any;
// (undocumented)
static toolName: string;
// (undocumented)
toolSelectedCallback: (evt: EventTypes_2.MouseDownEventType, annotation: PlanarFreehandROIAnnotation, interactionType: InteractionTypes) => void;
// (undocumented)
touchDragCallback: any;
// (undocumented)
triggerAnnotationCompleted: (annotation: PlanarFreehandROIAnnotation) => void;
// (undocumented)
triggerAnnotationModified: (annotation: PlanarFreehandROIAnnotation, enabledElement: Types_2.IEnabledElement) => void;
}

// @public
type Plane = [number, number, number, number];

Expand All @@ -2545,6 +2649,9 @@ type Point3 = [number, number, number];
// @public
type Point4 = [number, number, number, number];

// @public (undocumented)
const pointCanProjectOnLine: (p: Types_2.Point2, p1: Types_2.Point2, p2: Types_2.Point2, proximity: number) => boolean;

// @public (undocumented)
function pointInEllipse(ellipse: Ellipse, pointLPS: Types_2.Point3): boolean;

Expand All @@ -2554,6 +2661,21 @@ function pointInShapeCallback(imageData: vtkImageData | Types_2.CPUImageData, po
// @public (undocumented)
function pointInSurroundingSphereCallback(viewport: Types_2.IVolumeViewport, imageData: vtkImageData, circlePoints: [Types_2.Point3, Types_2.Point3], callback: PointInShapeCallback): void;

// @public (undocumented)
const pointsAreWithinCloseContourProximity: (p1: Types_2.Point2, p2: Types_2.Point2, closeContourProximity: number) => boolean;

declare namespace polyline {
export {
getFirstIntersectionWithPolyline,
getClosestIntersectionWithPolyline,
getSubPixelSpacingAndXYDirections,
pointsAreWithinCloseContourProximity,
addCanvasPointsToArray,
pointCanProjectOnLine,
calculateAreaOfPoints
}
}

// @public
type PreStackNewImageEvent = CustomEvent_2<PreStackNewImageEventDetail>;

Expand Down Expand Up @@ -3511,6 +3633,7 @@ declare namespace ToolSpecificAnnotationTypes {
BidirectionalAnnotation,
RectangleROIThresholdAnnotation,
RectangleROIStartEndThresholdAnnotation,
PlanarFreehandROIAnnotation,
ArrowAnnotation
}
}
Expand Down
160 changes: 160 additions & 0 deletions packages/tools/examples/planarFreehandROITool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { RenderingEngine, Types, Enums } from '@cornerstonejs/core';
import {
initDemo,
createImageIdsAndCacheMetaData,
setTitleAndDescription,
} 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 {
PlanarFreehandROITool,
PanTool,
StackScrollMouseWheelTool,
ZoomTool,
ToolGroupManager,
Enums: csToolsEnums,
} = cornerstoneTools;

const { ViewportType } = Enums;
const { MouseBindings } = csToolsEnums;

// ======== Set up page ======== //
setTitleAndDescription(
'Planar Freehand Annotation Tool',
'Here we demonstrate how to use the Planar Freehand Annotation Tool to draw 2D open and closed ROIs'
);

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 instructions = document.createElement('p');
instructions.innerText = `
Drawing:
- Left click and drag to draw a contour.
-- If you join the contour together it will be closed, otherwise releasing the mouse will create an open contour (freehand line)
Editing:
- Left click and drag on the line of an existing contour to edit it:
-- Closed Contours:
--- Drag the line and a preview of the edit will be displayed. Release the mouse to complete the edit. You can cross the original contour multiple times in one drag to do a complicated edit in one movement.
-- Open Contours:
--- Hover over an end and you will see a handle appear, drag this handle to pull out the polyline further. You can join this handle back round to the other end if you wish to close the contour (say you made a mistake making an open contour).
--- Drag the line and a preview of the edit will be displayed. Release the mouse to complete the edit. You can cross the original contour multiple times in one drag to do a complicated edit in one movement.
--- If You drag the line past the end of the of the open contour, the edit will snap to make your edit the new end, and allow you to continue drawing.
`;

content.append(instructions);
// ============================= //

const toolGroupId = 'STACK_TOOL_GROUP_ID';

/**
* Runs the demo
*/
async function run() {
// Init Cornerstone and related libraries
await initDemo();

// Add tools to Cornerstone3D
cornerstoneTools.addTool(PlanarFreehandROITool);
cornerstoneTools.addTool(PanTool);
cornerstoneTools.addTool(StackScrollMouseWheelTool);
cornerstoneTools.addTool(ZoomTool);

// 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(PlanarFreehandROITool.toolName);
toolGroup.addTool(PanTool.toolName);
toolGroup.addTool(StackScrollMouseWheelTool.toolName);
toolGroup.addTool(ZoomTool.toolName);

// Set the initial state of the tools.
toolGroup.setToolActive(PlanarFreehandROITool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Primary, // Left Click
},
],
});
toolGroup.setToolActive(PanTool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Auxiliary, // Middle Click
},
],
});
toolGroup.setToolActive(ZoomTool.toolName, {
bindings: [
{
mouseButton: MouseBindings.Secondary, // Right Click
},
],
});
// As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
// hook instead of mouse buttons, it does not need to assign any mouse button.
toolGroup.setToolActive(StackScrollMouseWheelTool.toolName);

// 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://d1qmxk7r72ysft.cloudfront.net/dicomweb',
type: 'STACK',
});

// Instantiate a rendering engine
const renderingEngineId = 'myRenderingEngine';
const renderingEngine = new RenderingEngine(renderingEngineId);

// Create a stack viewport
const viewportId = 'CT_STACK';
const viewportInput = {
viewportId,
type: ViewportType.STACK,
element,
defaultOptions: {
background: <Types.Point3>[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 = <Types.IStackViewport>(
renderingEngine.getViewport(viewportId)
);

// Define a stack containing a single image
const stack = [imageIds[0], imageIds[1]];

// Set the stack on the viewport
viewport.setStack(stack);

// Render the image
viewport.render();
}

run();
Loading

0 comments on commit 0067339

Please sign in to comment.