From 556bdcd14123a90b275f54d9b82e79e68a5d1334 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 24 Nov 2021 15:07:52 -0500 Subject: [PATCH] feat: Add TMTV calculation for segmentations --- .../src/util/math/vec3/isEqual.ts | 20 ++++--- .../src/util/segmentation/calculateTMTV.ts | 54 +++++++++++++++++++ .../src/util/segmentation/index.ts | 3 ++ .../demo/src/ExampleSegmentationRender.tsx | 28 ++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 packages/cornerstone-tools/src/util/segmentation/calculateTMTV.ts diff --git a/packages/cornerstone-tools/src/util/math/vec3/isEqual.ts b/packages/cornerstone-tools/src/util/math/vec3/isEqual.ts index 7f43564bf4..dc44fce1c9 100644 --- a/packages/cornerstone-tools/src/util/math/vec3/isEqual.ts +++ b/packages/cornerstone-tools/src/util/math/vec3/isEqual.ts @@ -11,13 +11,19 @@ import { Point3 } from '../../../types' * @returns {boolean} True if the two values are within the tolerance levels. */ export default function isEqual( - v1: Point3, - v2: Point3, + v1: Point3 | Float32Array, + v2: Point3 | Float32Array, tolerance = 1e-5 ): boolean { - return ( - Math.abs(v1[0] - v2[0]) < tolerance && - Math.abs(v1[1] - v2[1]) < tolerance && - Math.abs(v1[2] - v2[2]) < tolerance - ) + if (v1.length !== v2.length) { + return false + } + + for (let i = 0; i < v1.length; i++) { + if (Math.abs(v1[i] - v2[i]) > tolerance) { + return false + } + } + + return true } diff --git a/packages/cornerstone-tools/src/util/segmentation/calculateTMTV.ts b/packages/cornerstone-tools/src/util/segmentation/calculateTMTV.ts new file mode 100644 index 0000000000..57b60da5b9 --- /dev/null +++ b/packages/cornerstone-tools/src/util/segmentation/calculateTMTV.ts @@ -0,0 +1,54 @@ +import { IImageVolume } from '@precisionmetrics/cornerstone-render/src/types' +import isEqual from '../math/vec3/isEqual' + +/** + * Given a list of labelmaps (with the possitibility of overlapping regions), + * and a referenceVolume, it calculates the total metabolic turnover volume (TMTV) + * by flattening and rasterizing each segment into a single labelmap and summing + * the total number of volume voxels. It should be noted that for this calculation + * we do not double count voxels that are part of multiple labelmaps. + * @param {} labelmaps + * @param {number} segmentIndex + * @returns {number} TMTV + */ +function calculateTMTV( + labelmaps: Array, + segmentIndex = 1 +): number { + labelmaps.forEach(({ direction, dimensions, origin }) => { + if ( + !isEqual(dimensions, labelmaps[0].dimensions) || + !isEqual(direction, labelmaps[0].direction) || + !isEqual(origin, labelmaps[0].origin) + ) { + throw new Error('labelmaps must have the same size and shape') + } + }) + + const labelmap = labelmaps[0] + + const arrayType = labelmap.scalarData.constructor + const outputData = new arrayType(labelmap.scalarData.length) + + labelmaps.forEach((labelmap) => { + const { scalarData } = labelmap + for (let i = 0; i < scalarData.length; i++) { + if (scalarData[i] === segmentIndex) { + outputData[i] = segmentIndex + } + } + }) + + // count non-zero values inside the outputData, this would + // consider the overlapping regions to be only counted once + const tmtv = outputData.reduce((acc, curr) => { + if (curr > 0) { + return acc + 1 + } + return acc + }, 0) + + return tmtv +} + +export default calculateTMTV diff --git a/packages/cornerstone-tools/src/util/segmentation/index.ts b/packages/cornerstone-tools/src/util/segmentation/index.ts index 8cfc1644c2..59774719c9 100644 --- a/packages/cornerstone-tools/src/util/segmentation/index.ts +++ b/packages/cornerstone-tools/src/util/segmentation/index.ts @@ -2,12 +2,14 @@ import getBoundingBoxAroundShape from './getBoundingBoxAroundShape' import thresholdVolumeByRange from './thresholdVolumeByRange' import thresholdVolumeByRoiStats from './thresholdVolumeByRoiStats' +import calculateTMTV from './calculateTMTV' export { getBoundingBoxAroundShape, // fillOutsideBoundingBox, thresholdVolumeByRange, thresholdVolumeByRoiStats, + calculateTMTV, } export default { @@ -15,4 +17,5 @@ export default { // fillOutsideBoundingBox, thresholdVolumeByRange, thresholdVolumeByRoiStats, + calculateTMTV, } diff --git a/packages/demo/src/ExampleSegmentationRender.tsx b/packages/demo/src/ExampleSegmentationRender.tsx index c61ff9af03..6363240324 100644 --- a/packages/demo/src/ExampleSegmentationRender.tsx +++ b/packages/demo/src/ExampleSegmentationRender.tsx @@ -131,6 +131,7 @@ class SegmentationExample extends Component { thresholdMax: 100, numSlicesForThreshold: 1, selectedStrategy: '', + tmtv: null, } constructor(props) { @@ -536,6 +537,24 @@ class SegmentationExample extends Component { this.setState({ activeSegmentIndex: newIndex, segmentLocked }) } + calculateTMTV = () => { + const sceneUID = this.state.sceneForSegmentation + const scene = this.renderingEngine.getScene(sceneUID) + const { element } = scene.getViewports()[0] + const labelmapUIDs = SegmentationModule.getLabelmapUIDsForElement(element) + + const labelmaps = labelmapUIDs.map((uid) => cache.getVolume(uid)) + const segmentationIndex = 1 + const tmtv = csToolsUtils.segmentation.calculateTMTV( + labelmaps, + segmentationIndex + ) + this.setState((prevState) => ({ + ...prevState, + tmtv, + })) + } + executeThresholding = (mode) => { const ptVolume = cache.getVolume(ptVolumeUID) const labelmapVolume = cache.getVolume(this.state.selectedLabelmapUID) @@ -644,6 +663,15 @@ class SegmentationExample extends Component { > Execute Max Thresholding on Selected Annotation + + {this.state.tmtv !== null && ( + {` TMTV: ${this.state.tmtv} voxels`} + )} ) }