diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index dde8f74aee..4fb719ca47 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -931,6 +931,8 @@ interface IContourSet { // (undocumented) readonly frameOfReferenceUID: string; // (undocumented) + getCentroid(): Point3; + // (undocumented) getColor(): any; // (undocumented) getContours(): IContour[]; diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index 219c0a7ce9..33d1247432 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -644,6 +644,8 @@ interface IContourSet { // (undocumented) readonly frameOfReferenceUID: string; // (undocumented) + getCentroid(): Point3; + // (undocumented) getColor(): any; getContours(): IContour[]; getFlatPointsArray(): Point3[]; diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 8b669db243..8c82e746cc 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -2339,6 +2339,8 @@ interface IContourSet { // (undocumented) readonly frameOfReferenceUID: string; // (undocumented) + getCentroid(): Point3; + // (undocumented) getColor(): any; getContours(): IContour[]; getFlatPointsArray(): Point3[]; diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index 5394823f62..966c923ca4 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -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 }; }; /** diff --git a/packages/core/src/cache/classes/ContourSet.ts b/packages/core/src/cache/classes/ContourSet.ts index dee5855ba5..ea53cd9fc2 100644 --- a/packages/core/src/cache/classes/ContourSet.ts +++ b/packages/core/src/cache/classes/ContourSet.ts @@ -1,3 +1,4 @@ +import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; import { Point3, IContourSet, IContour, ContourData } from '../../types'; import Contour from './Contour'; @@ -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) { @@ -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(); } @@ -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 { @@ -58,6 +101,10 @@ export class ContourSet implements IContourSet { }, 0); } + public getCentroid(): Point3 { + return this.centroid; + } + public getSegmentIndex(): number { return this.segmentIndex; } @@ -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(); diff --git a/packages/core/src/types/IContourSet.ts b/packages/core/src/types/IContourSet.ts index c6dcf43953..7f7fdf7dfd 100644 --- a/packages/core/src/types/IContourSet.ts +++ b/packages/core/src/types/IContourSet.ts @@ -1,3 +1,4 @@ +import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; import { ContourData, IContour, Point3 } from './'; /** @@ -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 diff --git a/packages/tools/examples/contourRenderingConfiguration/index.ts b/packages/tools/examples/contourRenderingConfiguration/index.ts index 90f2af8bcf..0cd5463a15 100644 --- a/packages/tools/examples/contourRenderingConfiguration/index.ts +++ b/packages/tools/examples/contourRenderingConfiguration/index.ts @@ -101,7 +101,7 @@ addToggleButtonToToolbar({ }); addToggleButtonToToolbar({ - title: 'Hide Segment', + title: 'Hide Red Segment', onClick: (toggle) => { const segmentIndex = 1; [ @@ -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], diff --git a/packages/tools/src/stateManagement/segmentation/removeSegmentationsFromToolGroup.ts b/packages/tools/src/stateManagement/segmentation/removeSegmentationsFromToolGroup.ts index c7cf12e0fa..24f2506e6d 100644 --- a/packages/tools/src/stateManagement/segmentation/removeSegmentationsFromToolGroup.ts +++ b/packages/tools/src/stateManagement/segmentation/removeSegmentationsFromToolGroup.ts @@ -1,5 +1,6 @@ import SegmentationRepresentations from '../../enums/SegmentationRepresentations'; import { labelmapDisplay } from '../../tools/displayTools/Labelmap'; +import { contourDisplay } from '../../tools/displayTools/Contour'; import { getSegmentationRepresentations, @@ -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`); } diff --git a/packages/tools/src/tools/displayTools/Contour/contourConfig.ts b/packages/tools/src/tools/displayTools/Contour/contourConfig.ts index 5ec7e8e65c..e8e128ffb2 100644 --- a/packages/tools/src/tools/displayTools/Contour/contourConfig.ts +++ b/packages/tools/src/tools/displayTools/Contour/contourConfig.ts @@ -6,6 +6,9 @@ const defaultContourConfig: ContourConfig = { outlineWidthInactive: 2, outlineOpacity: 1, outlineOpacityInactive: 0.85, + renderFill: true, + fillAlpha: 1, + fillAlphaInactive: 0, }; function getDefaultContourConfig(): ContourConfig { diff --git a/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts b/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts index a2eb563d4b..1add8e6315 100644 --- a/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts +++ b/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts @@ -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 @@ -97,6 +98,8 @@ function removeSegmentationRepresentation( segmentationRepresentationUID ); + deleteConfigCache(segmentationRepresentationUID); + if (renderImmediate) { const viewportsInfo = getToolGroup(toolGroupId).getViewportsInfo(); viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { diff --git a/packages/tools/src/tools/displayTools/Contour/updateContourSets.ts b/packages/tools/src/tools/displayTools/Contour/updateContourSets.ts index 52b985f9d4..34eed02245 100644 --- a/packages/tools/src/tools/displayTools/Contour/updateContourSets.ts +++ b/packages/tools/src/tools/displayTools/Contour/updateContourSets.ts @@ -1,6 +1,4 @@ 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 { @@ -8,7 +6,7 @@ import { ToolGroupSpecificContourRepresentation, } from '../../../types'; import { getConfigCache, setConfigCache } from './contourConfigCache'; -import { getPolyData } from './utils'; +import { getSegmentSpecificConfig } from './utils'; export function updateContourSets( viewport: Types.IVolumeViewport, @@ -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); } } @@ -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, diff --git a/packages/tools/src/tools/displayTools/Contour/utils.ts b/packages/tools/src/tools/displayTools/Contour/utils.ts index 375756c675..8708e874da 100644 --- a/packages/tools/src/tools/displayTools/Contour/utils.ts +++ b/packages/tools/src/tools/displayTools/Contour/utils.ts @@ -1,6 +1,5 @@ import { Enums, Types } from '@cornerstonejs/core'; import vtkCellArray from '@kitware/vtk.js/Common/Core/CellArray'; -import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkPoints from '@kitware/vtk.js/Common/Core/Points'; import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; import { ToolGroupSpecificContourRepresentation } from '../../../types'; diff --git a/packages/tools/src/types/ContourTypes.ts b/packages/tools/src/types/ContourTypes.ts index 2ba499629d..88c97a37d9 100644 --- a/packages/tools/src/types/ContourTypes.ts +++ b/packages/tools/src/types/ContourTypes.ts @@ -12,6 +12,12 @@ export type ContourConfig = { outlineOpacityInactive?: number; /** outline visibility */ renderOutline?: boolean; + /** render fill */ + renderFill?: boolean; + /** fill alpha */ + fillAlpha?: number; + /** fillAlphaInactive */ + fillAlphaInactive?: number; }; /**