Skip to content

Commit

Permalink
feat(contour): improved performance and better configuration (#543)
Browse files Browse the repository at this point in the history
* refactor: Update contour rendering default configuration & remove console.debug call

This commit updates the Contour rendering settings to a more visually pleasing default configuration with a thinner active outline and a solid fill enabled. It also removes a `console.debug` call in the `removeSegmentationsFromToolGroup` function that is no longer needed.

* perf: Optimize getting and setting vtk polydata for contour sets

This commit refactors the `ContourSet` and `IContourSet` interfaces to include a new `getPolyData()` and `setPolyData()` function to get and set the vtk polydata object associated with the contour set for caching purposes. This allows improved performance when repeatedly accessing the polydata. Additionally, optimizations made to `updateContourSets()` ensure better and more consistent rendering of color and visibility updates. There is also cleanup of unused imports and variables.

* refactor(displayTools): optimize contour sets rendering with segment specific configs

Replace unnecessary loops with reduce function to create an accumulator holding the contour sets and segment specific configs. Then, process the contour sets and apply visibility updates according to any affected segments or custom segment settings that have a fillAlpha attribute.

* wip

* refactor: Remove redundant code for Contour display tools

This commit removes redundant code that has been replaced with simpler alternatives for adding contour sets to an element and updating contours sets in Contour display tools.

* feat: add computation for centroid of the contour set

This commit adds functionality to compute the centroid of a contour set. The centroid is calculated as a weighted average of all the contour points. Limitations are noted in the code. A new public function, `getCentroid()`, has also been added to retrieve the centroid.

* refactor: simplify centroid calculation for better contour rendering

Instead of calculating the centroid of all points in a ContourSet, this restructured logic picks the point closest to the centroid, as the centroid calculation can sometimes cause contours to be rendered incorrectly or not at all. Removed unused import vtkAppendPolyData.

fix: increase outline width of active contours

This change increases the outline width of active contours from 1 to 2 in contourConfig, for better visibility.

* update api

* update api

* apply review comments
  • Loading branch information
sedghi authored Apr 5, 2023
1 parent ebbb555 commit c69c58a
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 53 deletions.
2 changes: 2 additions & 0 deletions common/reviews/api/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,8 @@ interface IContourSet {
// (undocumented)
readonly frameOfReferenceUID: string;
// (undocumented)
getCentroid(): Point3;
// (undocumented)
getColor(): any;
// (undocumented)
getContours(): IContour[];
Expand Down
2 changes: 2 additions & 0 deletions common/reviews/api/streaming-image-volume-loader.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ interface IContourSet {
// (undocumented)
readonly frameOfReferenceUID: string;
// (undocumented)
getCentroid(): Point3;
// (undocumented)
getColor(): any;
getContours(): IContour[];
getFlatPointsArray(): Point3[];
Expand Down
2 changes: 2 additions & 0 deletions common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2339,6 +2339,8 @@ interface IContourSet {
// (undocumented)
readonly frameOfReferenceUID: string;
// (undocumented)
getCentroid(): Point3;
// (undocumented)
getColor(): any;
getContours(): IContour[];
getFlatPointsArray(): Point3[];
Expand Down
41 changes: 20 additions & 21 deletions packages/core/src/RenderingEngine/BaseVolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,27 +309,26 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport {
* @returns viewport properties including voi, interpolation type: TODO: slabThickness, invert, rotation, flip
*/
public getProperties = (): VolumeViewportProperties => {
const actorEntries = this.getActors();
const voiRanges = actorEntries.map((actorEntry) => {
const volumeActor = actorEntry.actor as vtkVolume;
const volumeId = actorEntry.uid;
const cfun = volumeActor.getProperty().getRGBTransferFunction(0);
let lower, upper;
if (this.VOILUTFunction === 'SIGMOID') {
[lower, upper] = getVoiFromSigmoidRGBTransferFunction(cfun);
} else {
// @ts-ignore: vtk d ts problem
[lower, upper] = cfun.getRange();
}
return {
volumeId,
voiRange: { lower, upper },
};
});
return {
voiRange: voiRanges[0].voiRange, // TODO: handle multiple actors instead of just first.
VOILUTFunction: this.VOILUTFunction,
};
const voiRanges = this.getActors()
.map((actorEntry) => {
const volumeActor = actorEntry.actor as vtkVolume;
const volumeId = actorEntry.uid;
const volume = cache.getVolume(volumeId);
if (!volume) return null;
const cfun = volumeActor.getProperty().getRGBTransferFunction(0);
const [lower, upper] =
this.VOILUTFunction === 'SIGMOID'
? getVoiFromSigmoidRGBTransferFunction(cfun)
: // @ts-ignore
cfun.getRange();
return { volumeId, voiRange: { lower, upper } };
})
.filter(Boolean);

const voiRange = voiRanges.length ? voiRanges[0].voiRange : null;
const VOILUTFunction = this.VOILUTFunction;

return { voiRange, VOILUTFunction };
};

/**
Expand Down
56 changes: 55 additions & 1 deletion packages/core/src/cache/classes/ContourSet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData';
import { Point3, IContourSet, IContour, ContourData } from '../../types';
import Contour from './Contour';

Expand All @@ -19,6 +20,8 @@ export class ContourSet implements IContourSet {
readonly frameOfReferenceUID: string;
private color: Point3 = [200, 0, 0]; // default color
private segmentIndex: number;
private polyData: vtkPolyData;
private centroid: Point3;
contours: IContour[];

constructor(props: ContourSetProps) {
Expand All @@ -27,7 +30,6 @@ export class ContourSet implements IContourSet {
this.color = props.color ?? this.color;
this.frameOfReferenceUID = props.frameOfReferenceUID;
this.segmentIndex = props.segmentIndex;

this._createEachContour(props.data);
this.sizeInBytes = this._getSizeInBytes();
}
Expand All @@ -50,6 +52,47 @@ export class ContourSet implements IContourSet {

this.contours.push(contour);
});

this._updateContourSetCentroid();
}

// Todo: this centroid calculation has limitation in which
// it will not work for MPR, the reason is that we are finding
// the centroid of all points but at the end we are picking the
// closest point to the centroid, which will not work for MPR
// The reason for picking the closest is a rendering issue since
// the centroid can be not exactly in the middle of the slice
// and it might cause the contour to be rendered in the wrong slice
// or not rendered at all
_updateContourSetCentroid(): void {
const numberOfPoints = this.getTotalNumberOfPoints();
const flatPointsArray = this.getFlatPointsArray();

const sumOfPoints = flatPointsArray.reduce(
(acc, point) => {
return [acc[0] + point[0], acc[1] + point[1], acc[2] + point[2]];
},
[0, 0, 0]
);

const centroid = [
sumOfPoints[0] / numberOfPoints,
sumOfPoints[1] / numberOfPoints,
sumOfPoints[2] / numberOfPoints,
];

const closestPoint = flatPointsArray.reduce((closestPoint, point) => {
const distanceToPoint = this._getDistance(centroid, point);
const distanceToClosestPoint = this._getDistance(centroid, closestPoint);

if (distanceToPoint < distanceToClosestPoint) {
return point;
} else {
return closestPoint;
}
}, flatPointsArray[0]);

this.centroid = closestPoint;
}

_getSizeInBytes(): number {
Expand All @@ -58,6 +101,10 @@ export class ContourSet implements IContourSet {
}, 0);
}

public getCentroid(): Point3 {
return this.centroid;
}

public getSegmentIndex(): number {
return this.segmentIndex;
}
Expand Down Expand Up @@ -138,6 +185,13 @@ export class ContourSet implements IContourSet {
return this.getPointsInContour(contourIndex).length;
}

private _getDistance(pointA, pointB) {
return Math.sqrt(
(pointA[0] - pointB[0]) ** 2 +
(pointA[1] - pointB[1]) ** 2 +
(pointA[2] - pointB[2]) ** 2
);
}
/**
public convertToClosedSurface(): ClosedSurface {
const flatPointsArray = this.getFlatPointsArray();
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/IContourSet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData';
import { ContourData, IContour, Point3 } from './';

/**
Expand All @@ -12,6 +13,7 @@ export interface IContourSet {
_createEachContour(data: ContourData[]): void;
getSizeInBytes(): number;
getSegmentIndex(): number;
getCentroid(): Point3;
getColor(): any;
/**
* This function returns the contours of the image
Expand Down
23 changes: 22 additions & 1 deletion packages/tools/examples/contourRenderingConfiguration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ addToggleButtonToToolbar({
});

addToggleButtonToToolbar({
title: 'Hide Segment',
title: 'Hide Red Segment',
onClick: (toggle) => {
const segmentIndex = 1;
[
Expand All @@ -121,6 +121,27 @@ addToggleButtonToToolbar({
},
});

addToggleButtonToToolbar({
title: 'Hide Green Segment',
onClick: (toggle) => {
const segmentIndex = 2;
[
{ representationUID: planarSegmentationRepresentationUID, toolGroupId },
{
representationUID: volumeSegmentationRepresentationUID,
toolGroupId: toolGroupId3d,
},
].forEach(({ representationUID, toolGroupId }) => {
segmentation.config.visibility.setSegmentVisibility(
toolGroupId,
representationUID,
segmentIndex,
!toggle
);
});
},
});

addSliderToToolbar({
title: 'Change Segments Thickness For left viewport',
range: [0.1, 10],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SegmentationRepresentations from '../../enums/SegmentationRepresentations';
import { labelmapDisplay } from '../../tools/displayTools/Labelmap';
import { contourDisplay } from '../../tools/displayTools/Contour';

import {
getSegmentationRepresentations,
Expand Down Expand Up @@ -76,7 +77,11 @@ function _removeSegmentation(
immediate
);
} else if (type === SegmentationRepresentations.Contour) {
console.debug('Contour representation is not supported yet, ignoring...');
contourDisplay.removeSegmentationRepresentation(
toolGroupId,
segmentationRepresentationUID,
immediate
);
} else {
throw new Error(`The representation ${type} is not supported yet`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const defaultContourConfig: ContourConfig = {
outlineWidthInactive: 2,
outlineOpacity: 1,
outlineOpacityInactive: 0.85,
renderFill: true,
fillAlpha: 1,
fillAlphaInactive: 0,
};

function getDefaultContourConfig(): ContourConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../../../types/SegmentationStateTypes';
import { addOrUpdateContourSets } from './addOrUpdateContourSets';
import removeContourFromElement from './removeContourFromElement';
import { deleteConfigCache } from './contourConfigCache';

/**
* It adds a new segmentation representation to the segmentation state
Expand Down Expand Up @@ -97,6 +98,8 @@ function removeSegmentationRepresentation(
segmentationRepresentationUID
);

deleteConfigCache(segmentationRepresentationUID);

if (renderImmediate) {
const viewportsInfo = getToolGroup(toolGroupId).getViewportsInfo();
viewportsInfo.forEach(({ viewportId, renderingEngineId }) => {
Expand Down
98 changes: 70 additions & 28 deletions packages/tools/src/tools/displayTools/Contour/updateContourSets.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { cache, Types } from '@cornerstonejs/core';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkAppendPolyData from '@kitware/vtk.js/Filters/General/AppendPolyData';
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';

import {
SegmentationRepresentationConfig,
ToolGroupSpecificContourRepresentation,
} from '../../../types';
import { getConfigCache, setConfigCache } from './contourConfigCache';
import { getPolyData } from './utils';
import { getSegmentSpecificConfig } from './utils';

export function updateContourSets(
viewport: Types.IVolumeViewport,
Expand Down Expand Up @@ -53,7 +51,7 @@ export function updateContourSets(
const segmentsToSetToVisible = [];

for (const segmentIndex of segmentsHidden) {
if (!cachedConfig?.segmentsHidden.has(segmentIndex)) {
if (!cachedConfig.segmentsHidden.has(segmentIndex)) {
segmentsToSetToInvisible.push(segmentIndex);
}
}
Expand All @@ -64,38 +62,82 @@ export function updateContourSets(
segmentsToSetToVisible.push(segmentIndex);
}
}
if (segmentsToSetToInvisible.length || segmentsToSetToVisible.length) {
const appendPolyData = vtkAppendPolyData.newInstance();

geometryIds.forEach((geometryId) => {
const mergedInvisibleSegments = Array.from(cachedConfig.segmentsHidden)
.filter((segmentIndex) => !segmentsToSetToVisible.includes(segmentIndex))
.concat(segmentsToSetToInvisible);

const { contourSets, segmentSpecificConfigs } = geometryIds.reduce(
(acc, geometryId) => {
const geometry = cache.getGeometry(geometryId);
const { data: contourSet } = geometry;
const segmentIndex = (contourSet as Types.IContourSet).getSegmentIndex();
const color = contourSet.getColor();
const visibility = segmentsToSetToInvisible.includes(segmentIndex)
? 0
: 255;
const polyData = getPolyData(contourSet);

const size = polyData.getPoints().getNumberOfPoints();

const scalars = vtkDataArray.newInstance({
size: size * 4,
numberOfComponents: 4,
dataType: 'Uint8Array',
});
for (let i = 0; i < size; ++i) {
scalars.setTuple(i, [...color, visibility]);
const segmentSpecificConfig = getSegmentSpecificConfig(
contourRepresentation,
geometryId,
segmentIndex
);

acc.contourSets.push(contourSet);
acc.segmentSpecificConfigs[segmentIndex] = segmentSpecificConfig ?? {};

return acc;
},
{ contourSets: [], segmentSpecificConfigs: {} }
);

const affectedSegments = [
...mergedInvisibleSegments,
...segmentsToSetToVisible,
];

const hasCustomSegmentSpecificConfig = Object.values(
segmentSpecificConfigs
).some((config) => Object.keys(config).length > 0);

let polyDataModified = false;

if (affectedSegments.length || hasCustomSegmentSpecificConfig) {
const appendPolyData = mapper.getInputData();
const appendScalars = appendPolyData.getPointData().getScalars();
const appendScalarsData = appendScalars.getData();
// below we will only manipulate the polyData of the contourSets that are affected
// by picking the correct offset in the scalarData array
let offset = 0;
contourSets.forEach((contourSet) => {
const segmentIndex = (contourSet as Types.IContourSet).getSegmentIndex();
const size = contourSet.getTotalNumberOfPoints();

if (
affectedSegments.includes(segmentIndex) ||
segmentSpecificConfigs[segmentIndex]?.fillAlpha // Todo: add others
) {
const color = contourSet.getColor();
let visibility = mergedInvisibleSegments.includes(segmentIndex)
? 0
: 255;

const segmentConfig = segmentSpecificConfigs[segmentIndex];
if (segmentConfig.fillAlpha !== undefined) {
visibility = segmentConfig.fillAlpha * 255;
}

for (let i = 0; i < size; ++i) {
appendScalarsData[offset + i * 4] = color[0];
appendScalarsData[offset + i * 4 + 1] = color[1];
appendScalarsData[offset + i * 4 + 2] = color[2];
appendScalarsData[offset + i * 4 + 3] = visibility;
}

polyDataModified = true;
}
polyData.getPointData().setScalars(scalars);

segmentIndex === 0
? appendPolyData.setInputData(polyData)
: appendPolyData.addInputData(polyData);
offset = offset + size * 4;
});

const polyDataOutput = appendPolyData.getOutputData();
mapper.setInputData(polyDataOutput);
if (polyDataModified) {
appendPolyData.modified();
}

setConfigCache(
segmentationRepresentationUID,
Expand Down
Loading

0 comments on commit c69c58a

Please sign in to comment.