Skip to content

test(rectangleROIThreshold+statistics): Modify rectangleStartEndROIThreshold example for threshold value + add test for testing rectangleROIThreshold with statistics result comparison with MIM #2061

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

Merged
merged 8 commits into from
May 28, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
createImageIdsAndCacheMetaData,
setTitleAndDescription,
addButtonToToolbar,
setCtTransferFunctionForVolumeActor,
addCheckboxToToolbar,
addInputToToolbar,
setPetTransferFunctionForVolumeActor,
getLocalUrl,
} from '../../../../utils/demo/helpers';
import * as cornerstoneTools from '@cornerstonejs/tools';
Expand All @@ -37,14 +39,86 @@ const { MouseBindings } = csToolsEnums;
const { ViewportType } = Enums;

// Define a unique id for the volume
const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix
const volumeName = 'PT_VOLUME_ID'; // Id of the volume less loader prefix
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id

const segmentationId = 'MY_SEGMENTATION_ID';
const toolGroupId = 'MY_TOOLGROUP_ID';

let segmentationRepresentationByUID;
let thresholdLower = 0.41; // Typical default
let thresholdByMaxRelative = true;

// Helper function - based off of getThresholdValue from OHIF Viewer
function getReliableAnnotationMaxValue(annotation, volume) {
if (!annotation) {
console.error('getReliableAnnotationMaxValue: Missing annotation.');
return -Infinity; // Or throw error
}

if (!volume) {
console.error('getReliableAnnotationMaxValue: Missing volume.');
return -Infinity; // Or throw error
}

if (!volume.imageData || typeof volume.imageData.get !== 'function') {
console.error(
`getReliableAnnotationMaxValue: Volume ${volume.volumeId} or its imageData/get method not found.`
);
return -Infinity;
}

const primaryVmData = volume.imageData.get('voxelManager');
if (!primaryVmData || !primaryVmData.voxelManager) {
console.error(
`getReliableAnnotationMaxValue: Nested voxelManager not found for volume ${volume.volumeId}.`
);
return -Infinity;
}
const actualVoxelManager = primaryVmData.voxelManager;

const boundsIJK =
cornerstoneTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(
[annotation], // Pass as an array
volume
);

if (!boundsIJK) {
console.warn(
`getReliableAnnotationMaxValue: Could not get boundsIJK for annotation ${annotation.annotationUID}`
);
return -Infinity;
}

let maxValue = -Infinity;
let iteratedCount = 0;
try {
actualVoxelManager.forEach(
({ value: voxelValue }) => {
iteratedCount++;
if (voxelValue > maxValue) {
maxValue = voxelValue;
}
},
{ boundsIJK }
);
} catch (e) {
console.error(
`Error during voxelManager.forEach in getReliableAnnotationMaxValue: ${e.message}`
);
return -Infinity; // Or rethrow if critical
}

if (iteratedCount === 0 && maxValue === -Infinity) {
console.warn(
`getReliableAnnotationMaxValue: forEach did not iterate or all values <= -Infinity for annotation ${
annotation.annotationUID
}. BoundsIJK: ${JSON.stringify(boundsIJK)}.`
);
}

return maxValue;
}

// ======== Set up page ======== //
setTitleAndDescription(
Expand Down Expand Up @@ -168,26 +242,82 @@ addButtonToToolbar({
onClick: () => {
const annotations = cornerstoneTools.annotation.state.getAllAnnotations();
const labelmapVolume = cache.getVolume(segmentationId);
const volume = cache.getVolume(volumeId);

annotations.map((annotation, i) => {
// @ts-ignore
const pointsInVolume = annotation.data.cachedStats.pointsInVolume;
for (let i = 0; i < pointsInVolume.length; i++) {
for (let j = 0; j < pointsInVolume[i].length; j++) {
if (pointsInVolume[i][j].value > 2) {
labelmapVolume.voxelManager.setAtIndex(
pointsInVolume[i][j].index,
1
);
}
}
}
const annotationUIDs = annotations.map((a) => {
return a.annotationUID;
});

const upper = Infinity;
let lower = thresholdLower;
if (thresholdByMaxRelative) {
// Works normally, but doesn't populate in time for playwright
//const annotationMaxValue = annotations[0].data.cachedStats.statistics.max;

// Alternative more reliable method
//const sourceVolume = cache.getVolume(volumeId);
const annotationMaxValue = getReliableAnnotationMaxValue(
annotations[0],
volume
);
lower = thresholdLower * annotationMaxValue;
console.log(annotationMaxValue);
console.log(lower);
}

//const volume = cache.getVolumes()[0]; // 0 is volume loaded

cornerstoneTools.utilities.segmentation.rectangleROIThresholdVolumeByRange(
annotationUIDs,
labelmapVolume,
[{ volume, lower, upper }],
{ overwrite: true, segmentationId }
);

cornerstoneTools.segmentation.triggerSegmentationEvents.triggerSegmentationDataModified(
labelmapVolume.volumeId
);
labelmapVolume.modified();

// example calculate TMTV
const segmentationIds = [segmentationId];
cornerstoneTools.utilities.segmentation
.computeMetabolicStats({
segmentationIds,
segmentIndex: 1,
})
.then((stats) => {
console.log(stats);
});

// Example get statistics over threshold region
cornerstoneTools.utilities.segmentation
.getStatistics({
segmentationId,
segmentIndices: 1,
mode: 'individual',
})
.then((stats) => {
console.log(stats);
});
},
});

addInputToToolbar({
id: 'thresholdSlider',
title: `Threshold:`,
defaultValue: thresholdLower,
onSelectedValueChange: (newVal) => {
thresholdLower = parseFloat(newVal);
},
});

addCheckboxToToolbar({
id: 'thresholdMaxRelative',
title: 'Relative to Max',
checked: thresholdByMaxRelative,
onChange: (newChecked) => {
thresholdByMaxRelative = newChecked;
},
});

Expand Down Expand Up @@ -271,10 +401,13 @@ async function run() {

// Get Cornerstone imageIds for the source data and fetch metadata into RAM
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID:
'1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463',
StudyInstanceUID: '1.2.840.113619.2.290.3.3767434740.226.1600859119.501', // Water phantom
//'1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', // Original

SeriesInstanceUID:
'1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561',
'2.16.840.1.114362.1.12114306.25269253871.642214905.509.634', // PT AC192
//'1.3.6.1.4.1.14519.5.2.1.7009.2403.879445243400782656317561081015', // Original

wadoRsRoot:
getLocalUrl() || 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
});
Expand All @@ -283,6 +416,7 @@ async function run() {
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds,
});

// Add some segmentations based on the source data volume
await addSegmentationsToState();

Expand All @@ -291,9 +425,9 @@ async function run() {
const renderingEngine = new RenderingEngine(renderingEngineId);

// Create the viewports
const viewportId1 = 'CT_AXIAL';
const viewportId2 = 'CT_SAGITTAL';
const viewportId3 = 'CT_CORONAL';
const viewportId1 = 'PT_AXIAL';
const viewportId2 = 'PT_SAGITTAL';
const viewportId3 = 'PT_CORONAL';

const viewportInputArray = [
{
Expand All @@ -302,7 +436,7 @@ async function run() {
element: element1,
defaultOptions: {
orientation: Enums.OrientationAxis.AXIAL,
background: <Types.Point3>[0, 0, 0],
background: <Types.Point3>[1, 1, 1],
},
},
{
Expand All @@ -311,7 +445,7 @@ async function run() {
element: element2,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
background: <Types.Point3>[0, 0, 0],
background: <Types.Point3>[1, 1, 1],
},
},
{
Expand All @@ -320,7 +454,7 @@ async function run() {
element: element3,
defaultOptions: {
orientation: Enums.OrientationAxis.CORONAL,
background: <Types.Point3>[0, 0, 0],
background: <Types.Point3>[1, 1, 1],
},
},
];
Expand All @@ -337,7 +471,7 @@ async function run() {
// Set volumes on the viewports
await setVolumesForViewports(
renderingEngine,
[{ volumeId, callback: setCtTransferFunctionForVolumeActor }],
[{ volumeId, callback: setPetTransferFunctionForVolumeActor }],
[viewportId1, viewportId2, viewportId3]
);

Expand Down
2 changes: 2 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default defineConfig({
'**/labelmapsegmentationtools.spec.ts',
'**/splineContourSegmentationTools.spec.ts',
'**/stackLabelmapSegmentation.spec.ts',
'**/rectangleROIThresholdStatisticsMIM.spec.ts',
'**/renderingPipeline.spec.ts',
'**/stackLabelmapSegmentation/**.spec.ts',
],
Expand All @@ -68,6 +69,7 @@ export default defineConfig({
'**/labelmapsegmentationtools.spec.ts',
'**/splineContourSegmentationTools.spec.ts',
'**/stackLabelmapSegmentation.spec.ts',
'**/rectangleROIThresholdStatisticsMIM.spec.ts',
'**/stackLabelmapSegmentation/**.spec.ts',
],
},
Expand Down
Loading
Loading