Skip to content

Commit

Permalink
feat(contourSeg): polyline performance improvements (#1061)
Browse files Browse the repository at this point in the history
* feat(contourSeg): polyline simplication (Ramer–Douglas–Peucker)

* feat(contourSeg): polyline simplification (improvements)

* update api

* code review

* update api

* feat(contourSeg) polyline decimate :: code review
  • Loading branch information
lscoder authored Feb 8, 2024
1 parent 6e3d8ea commit 1df02d4
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 10 deletions.
9 changes: 9 additions & 0 deletions common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,9 @@ function debounce(func: Function, wait?: number, options?: {
trailing?: boolean;
}): Function;

// @public (undocumented)
function decimate(polyline: Types_2.Point2[], epsilon?: number): Types_2.Point2[];

// @public (undocumented)
const _default: {
filterAnnotationsWithinSlice: typeof filterAnnotationsWithinSlice;
Expand Down Expand Up @@ -3828,6 +3831,7 @@ declare namespace polyline {
getNormal3,
getNormal2,
intersectPolyline,
decimate,
getFirstLineSegmentIntersectionIndexes,
getLineSegmentIntersectionsIndexes,
getLineSegmentIntersectionsCoordinates,
Expand Down Expand Up @@ -5745,6 +5749,11 @@ function updateContourPolyline(annotation: ContourAnnotation, polylineData: {
targetWindingDirection?: ContourWindingDirection;
}, transforms: {
canvasToWorld: (point: Types_2.Point2) => Types_2.Point3;
}, options?: {
decimate?: {
enabled?: boolean;
epsilon?: number;
};
}): void;

declare namespace utilities {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ function getContourHolesData(
});
}

async function combinePolylines(
function combinePolylines(
viewport: Types.IViewport,
targetAnnotation: ContourSegmentationAnnotation,
targetPolyline: Types.Point2[],
Expand Down
17 changes: 15 additions & 2 deletions packages/tools/src/tools/annotation/LivewireContourTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { LivewireScissors } from '../../utilities/livewire/LivewireScissors';
import { LivewirePath } from '../../utilities/livewire/LiveWirePath';
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
import ContourSegmentationBaseTool from '../base/ContourSegmentationBaseTool';
import updateContourPolyline from '../../utilities/contours/updateContourPolyline';

const CLICK_CLOSE_CURVE_SQR_DIST = 10 ** 2; // px

Expand Down Expand Up @@ -108,6 +107,20 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
*/
showInterpolationPolyline: false,
},

/**
* The polyline may get processed in order to reduce the number of points
* for better performance and storage.
*/
decimate: {
enabled: false,
/** A maximum given distance 'epsilon' to decide if a point should or
* shouldn't be added the resulting polyline which will have a lower
* number of points for higher `epsilon` values.
*/
epsilon: 0.1,
},

actions: {
undo: {
method: 'undo',
Expand Down Expand Up @@ -922,7 +935,7 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
imagePoints = [...imagePoints, imagePoints[0]];
}

updateContourPolyline(
this.updateContourPolyline(
annotation,
{
points: imagePoints,
Expand Down
12 changes: 12 additions & 0 deletions packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
// interpolation is complete.
onInterpolationComplete: null,
},
/**
* The polyline may get processed in order to reduce the number of points
* for better performance and storage.
*/
decimate: {
enabled: false,
/** A maximum given distance 'epsilon' to decide if a point should or
* shouldn't be added the resulting polyline which will have a lower
* number of points for higher `epsilon` values.
*/
epsilon: 0.1,
},
calculateStats: false,
getTextLines: defaultGetTextLines,
statsCalculator: BasicStatsCalculator,
Expand Down
15 changes: 13 additions & 2 deletions packages/tools/src/tools/annotation/SplineROITool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import { LinearSpline } from './splines/LinearSpline';
import { CatmullRomSpline } from './splines/CatmullRomSpline';
import { BSpline } from './splines/BSpline';
import ContourSegmentationBaseTool from '../base/ContourSegmentationBaseTool';
import updateContourPolyline from '../../utilities/contours/updateContourPolyline';

const SPLINE_MIN_POINTS = 3;
const SPLINE_CLICK_CLOSE_CURVE_DIST = 10;
Expand Down Expand Up @@ -121,6 +120,18 @@ class SplineROITool extends ContourSegmentationBaseTool {
* modifier must be pressed when the first point of a new contour is added.
*/
contourHoleAdditionModifierKey: KeyboardBindings.Shift,
/**
* The polyline may get processed in order to reduce the number of points
* for better performance and storage.
*/
decimate: {
enabled: false,
/** A maximum given distance 'epsilon' to decide if a point should or
* shouldn't be added the resulting polyline which will have a lower
* number of points for higher `epsilon` values.
*/
epsilon: 0.1,
},
spline: {
configuration: {
[SplineTypesEnum.Cardinal]: {
Expand Down Expand Up @@ -693,7 +704,7 @@ class SplineROITool extends ContourSegmentationBaseTool {
const spline = this._updateSplineInstance(element, annotation);
const splinePolylineCanvas = spline.getPolylinePoints();

updateContourPolyline(
this.updateContourPolyline(
annotation,
{
points: splinePolylineCanvas,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { PlanarFreehandROIAnnotation } from '../../../types/ToolSpecificAnnotati
import findOpenUShapedContourVectorToPeak from './findOpenUShapedContourVectorToPeak';
import { polyline } from '../../../utilities/math';
import { removeAnnotation } from '../../../stateManagement/annotation/annotationState';
import { updateContourPolyline } from '../../../utilities/contours/';
import reverseIfAntiClockwise from '../../../utilities/contours/reverseIfAntiClockwise';

const {
Expand Down Expand Up @@ -238,7 +237,7 @@ function completeDrawClosedContour(
// contours. A future optimization if we use this for segmentation is to re-do
// this rendering with the GPU rather than SVG.

updateContourPolyline(
this.updateContourPolyline(
annotation,
{
points: updatedPoints,
Expand Down Expand Up @@ -315,7 +314,7 @@ function completeDrawOpenContour(
// contours. A future optimisation if we use this for segmentation is to re-do
// this rendering with the GPU rather than SVG.

updateContourPolyline(
this.updateContourPolyline(
annotation,
{
points: updatedPoints,
Expand Down
23 changes: 23 additions & 0 deletions packages/tools/src/tools/base/ContourBaseTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import type {
import { drawPath as drawPathSvg } from '../../drawingSvg';
import { StyleSpecifier } from '../../types/AnnotationStyle';
import AnnotationTool from './AnnotationTool';
import { updateContourPolyline } from '../../utilities/contours/';
import { getContourHolesDataCanvas } from '../../utilities/contours';
import { ContourWindingDirection } from '../../types/ContourAnnotation';

/**
* A contour base class responsible for rendering contour instances such as
Expand Down Expand Up @@ -210,6 +212,27 @@ abstract class ContourBaseTool extends AnnotationTool {
);
}

protected updateContourPolyline(
annotation: ContourAnnotation,
polylineData: {
points: Types.Point2[];
closed?: boolean;
targetWindingDirection?: ContourWindingDirection;
},
transforms: {
canvasToWorld: (point: Types.Point2) => Types.Point3;
}
) {
const decimateConfig = this.configuration?.decimate || {};

updateContourPolyline(annotation, polylineData, transforms, {
decimate: {
enabled: !!decimateConfig.enabled,
epsilon: decimateConfig.epsilon,
},
});
}

/**
* Get polyline points in world space.
* Just to give a chance for child classes to override it.
Expand Down
24 changes: 23 additions & 1 deletion packages/tools/src/utilities/contours/updateContourPolyline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {
* @param viewport - Viewport
* @param polylineData - Polyline data (points, winding direction and closed)
* @param transforms - Methods to convert points to/from canvas and world spaces
* @param options - Options
* - decimate: allow to set some parameters to decimate the polyline reducing
* the amount of points stored which also affects how fast it will draw the
* annotation in a viewport, compute the winding direction, append/remove
* contours and create holes. A higher `epsilon` value results in a polyline
* with less points.
*/
export default function updateContourPolyline(
annotation: ContourAnnotation,
Expand All @@ -24,11 +30,27 @@ export default function updateContourPolyline(
},
transforms: {
canvasToWorld: (point: Types.Point2) => Types.Point3;
},
options?: {
decimate?: {
enabled?: boolean;
epsilon?: number;
};
}
) {
const { canvasToWorld } = transforms;
const { data } = annotation;
const { points: polyline, targetWindingDirection } = polylineData;
const { targetWindingDirection } = polylineData;
let { points: polyline } = polylineData;

// Decimate the polyline to reduce tha amount of points
if (options?.decimate?.enabled) {
polyline = math.polyline.decimate(
polylineData.points,
options?.decimate?.epsilon
);
}

let { closed } = polylineData;
const numPoints = polyline.length;
const polylineWorldPoints = new Array(numPoints);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export default function distanceToPointSquaredInfo(
let closestPoint: Types.Point2;
const distanceSquared = math.point.distanceToPointSquared(lineStart, lineEnd);

// Check if lineStart is the same as lineEnd which means
// Check if lineStart equal to the lineEnd which means the closest point
// is any of these two points
if (lineStart[0] === lineEnd[0] && lineStart[1] === lineEnd[1]) {
closestPoint = lineStart;
}
Expand Down
105 changes: 105 additions & 0 deletions packages/tools/src/utilities/math/polyline/decimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Types } from '@cornerstonejs/core';
import * as mathLine from '../line';

const DEFAULT_EPSILON = 0.1;

/**
* Ramer–Douglas–Peucker algorithm implementation to decimate a polyline
* to a similar polyline with fewer points
*
* https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
* https://rosettacode.org/wiki/Ramer-Douglas-Peucker_line_simplification
* https://karthaus.nl/rdp/
*
* @param polyline - Polyline to decimate
* @param epsilon - A maximum given distance 'epsilon' to decide if a point
* should or shouldn't be added the decimated polyline version. In each
* iteration the polyline is split into two polylines and the distance of each
* point from those new polylines are checked against the line that connects
* the first and last points.
* @returns Decimated polyline
*/
export default function decimate(
polyline: Types.Point2[],
epsilon = DEFAULT_EPSILON
) {
const numPoints = polyline.length;

// The polyline must have at least a start and end points
if (numPoints < 3) {
return polyline;
}

const epsilonSquared = epsilon * epsilon;
const partitionQueue = [[0, numPoints - 1]];

// Used a boolean array to set each point that will be in the decimated polyline
// because pre-allocated arrays are 3-4x faster than thousands of push() calls
// to add all points to a new array.
const polylinePointFlags = new Array(numPoints).fill(false);

// Start and end points are always added to the decimated polyline
let numDecimatedPoints = 2;

// Add start and end points to the decimated polyline
polylinePointFlags[0] = true;
polylinePointFlags[numPoints - 1] = true;

// Iterative approach using a queue instead of recursion to reduce the number
// of function calls (performance)
while (partitionQueue.length) {
const [startIndex, endIndex] = partitionQueue.pop();

// Return if there is no point between the start and end points
if (endIndex - startIndex === 1) {
continue;
}

const startPoint = polyline[startIndex];
const endPoint = polyline[endIndex];
let maxDistSquared = -Infinity;
let maxDistIndex = -1;

// Search for the furthest point
for (let i = startIndex + 1; i < endIndex; i++) {
const currentPoint = polyline[i];
const distSquared = mathLine.distanceToPointSquared(
startPoint,
endPoint,
currentPoint
);

if (distSquared > maxDistSquared) {
maxDistSquared = distSquared;
maxDistIndex = i;
}
}

// Do not add any of the points because the fursthest one is very close to
// the line based on the epsilon value
if (maxDistSquared < epsilonSquared) {
continue;
}

// Update the flag for the furthest point because it will be added to the
// decimated polyline
polylinePointFlags[maxDistIndex] = true;
numDecimatedPoints++;

// Partition the points into two parts using maxDistIndex as the pivot point
// and process both sides
partitionQueue.push([maxDistIndex, endIndex]);
partitionQueue.push([startIndex, maxDistIndex]);
}

// A pre-allocated array is 3-4x faster then multiple push() calls
const decimatedPolyline: Types.Point2[] = new Array(numDecimatedPoints);

for (let srcIndex = 0, dstIndex = 0; srcIndex < numPoints; srcIndex++) {
if (polylinePointFlags[srcIndex]) {
decimatedPolyline[dstIndex++] = polyline[srcIndex];
}
}

return decimatedPolyline;
}
2 changes: 2 additions & 0 deletions packages/tools/src/utilities/math/polyline/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import getNormal3 from './getNormal3';
import getNormal2 from './getNormal2';
import { mergePolylines, subtractPolylines } from './combinePolyline';
import intersectPolyline from './intersectPolyline';
import decimate from './decimate';
import getFirstLineSegmentIntersectionIndexes from './getFirstLineSegmentIntersectionIndexes';
import getLineSegmentIntersectionsIndexes from './getLineSegmentIntersectionsIndexes';
import getLineSegmentIntersectionsCoordinates from './getLineSegmentIntersectionsCoordinates';
Expand All @@ -30,6 +31,7 @@ export {
getNormal3,
getNormal2,
intersectPolyline,
decimate,
getFirstLineSegmentIntersectionIndexes,
getLineSegmentIntersectionsIndexes,
getLineSegmentIntersectionsCoordinates,
Expand Down

0 comments on commit 1df02d4

Please sign in to comment.