Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contour): improved performance and better configuration #543

Merged
merged 10 commits into from
Apr 5, 2023
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