From 5ea43768b6d95a9f28b7b853747fb4546344d83d Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 20 Mar 2023 14:21:29 -0400 Subject: [PATCH 01/21] Initialized example and function files, added function to dynamicVolume utilies index --- .../examples/generateImageFromTime/index.ts | 214 ++++++++++++++++++ .../dynamicVolume/generateImageFromTime.ts | 42 ++++ .../src/utilities/dynamicVolume/index.ts | 3 + 3 files changed, 259 insertions(+) create mode 100644 packages/tools/examples/generateImageFromTime/index.ts create mode 100644 packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts new file mode 100644 index 000000000..ef35e0b42 --- /dev/null +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -0,0 +1,214 @@ +import { + RenderingEngine, + Types, + Enums, + volumeLoader, + getRenderingEngine, + CONSTANTS, + utilities, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + setPetTransferFunctionForVolumeActor, + addSliderToToolbar, + addDropdownToToolbar, + addButtonToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +const { + segmentation, + SegmentationDisplayTool, + utilities: csToolsUtilities, + Enums: csToolsEnums, +} = cornerstoneTools; +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); +const { ViewportType } = Enums; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'CT_SAGITTAL_STACK'; +const orientations = [ + Enums.OrientationAxis.AXIAL, + Enums.OrientationAxis.SAGITTAL, + Enums.OrientationAxis.CORONAL, +]; +// ======== Set up page ======== // +setTitleAndDescription( + 'Volume 4D', + 'Displays a 4D DICOM series in a Volume viewport.' +); +const content = document.getElementById('content'); +const element = document.createElement('div'); +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; +content.appendChild(element); +// ============================= // +let volumeForButton; +addButtonToToolbar({ + title: 'Get Data In Time', + onClick: () => { + const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTime( + volumeForButton, + { + frameNumbers: [5, 6, 7], + // imageCoordinate: [-24, 24, -173], + maskVolumeId: segmentationId, + } + ); + }, +}); +addDropdownToToolbar({ + options: { + values: orientations, + defaultValue: orientations[0], + }, + onSelectedValueChange: (selectedValue) => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + // Get the volume viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + viewport.setOrientation(selectedValue); + viewport.render(); + }, +}); +function addTimePointSlider(volume) { + addSliderToToolbar({ + title: 'Time Point', + range: [0, volume.numTimePoints - 1], + defaultValue: 0, + onSelectedValueChange: (value) => { + const timePointIndex = Number(value); + volume.timePointIndex = timePointIndex; + }, + }); +} +// ==================================== // +// Define a unique id for the volume +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +// const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeLoaderScheme = 'cornerstoneStreamingDynamicImageVolume'; // 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'; +/** + * Adds two concentric circles to each axial slice of the demo segmentation. + */ +function createMockEllipsoidSegmentation(segmentationVolume) { + const scalarData = segmentationVolume.scalarData; + const { dimensions } = segmentationVolume; + const center = [72, 145, 117.5]; + const innerRadius = 20; + let voxelIndex = 0; + for (let z = 0; z < dimensions[2]; z++) { + for (let y = 0; y < dimensions[1]; y++) { + for (let x = 0; x < dimensions[0]; x++) { + const distanceFromCenter = Math.sqrt( + (x - center[0]) * (x - center[0]) + + (y - center[1]) * (y - center[1]) + + (z - center[2]) * (z - center[2]) + ); + if (distanceFromCenter < innerRadius) { + scalarData[voxelIndex] = 1; + } + voxelIndex++; + } + } + } +} +async function addSegmentationsToState() { + // Create a segmentation of the same resolution as the source data + // using volumeLoader.createAndCacheDerivedVolume. + const segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( + volumeId, + { + volumeId: segmentationId, + } + ); + // Add the segmentations to state + segmentation.addSegmentations([ + { + segmentationId, + representation: { + // The type of segmentation + type: csToolsEnums.SegmentationRepresentations.Labelmap, + // The actual segmentation data, in the case of labelmap this is a + // reference to the source volume of the segmentation. + data: { + volumeId: segmentationId, + }, + }, + }, + ]); + // Add some data to the segmentations + createMockEllipsoidSegmentation(segmentationVolume); +} +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + // Add tools to Cornerstone3D + cornerstoneTools.addTool(SegmentationDisplayTool); + // Define tool groups to add the segmentation display tool to + const toolGroup = + cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId); + toolGroup.addTool(SegmentationDisplayTool.toolName); + toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849', + SeriesInstanceUID: + '1.3.6.1.4.1.12842.1.1.22.4.20220915.124758.560.4125514885', + wadoRsRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', + }); + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + // Create a stack viewport + const viewportInput = { + viewportId, + type: ViewportType.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: Enums.OrientationAxis.ACQUISITION, + background: [0.2, 0, 0.2], + }, + }; + renderingEngine.enableElement(viewportInput); + // Add viewport to toolGroup + toolGroup.addViewport(viewportId, renderingEngineId); + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + // Add segmentation + await addSegmentationsToState(); + // Set the volume to load + volume.load(); + volumeForButton = volume; + addTimePointSlider(volume); + // Set the volume on the viewport + viewport.setVolumes([ + { volumeId, callback: setPetTransferFunctionForVolumeActor }, + ]); + await segmentation.addSegmentationRepresentations(toolGroupId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + }, + ]); + // Render the image + viewport.render(); +} +run(); diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts new file mode 100644 index 000000000..2517c0a0b --- /dev/null +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -0,0 +1,42 @@ +import { utilities, cache, Types } from '@cornerstonejs/core'; +import getDataInTime from './getDataInTime'; + +function generateImageFromTime( + dynamicVolume: Types.IDynamicImageVolume, + options: { + frameNumbers?; + maskVolumeId?; + imageCoordinate?; + } +): number[] | number[][] { + let dataInTime; + const frames = options.frameNumbers || [ + ...Array(dynamicVolume.numTimePoints).keys(), + ]; + + if (!options.maskVolumeId && !options.imageCoordinate) { + throw new Error('No ROI provided'); + } + + if (options.maskVolumeId && options.imageCoordinate) { + throw new Error('Please provide only one ROI'); + } + + if (options.maskVolumeId) { + dataInTime = getDataInTime(dynamicVolume, { + frameNumbers: frames, + maskVolumeId: options.maskVolumeId, + }); + } + + if (options.imageCoordinate) { + dataInTime = getDataInTime(dynamicVolume, { + frameNumbers: frames, + imageCoordinate: options.imageCoordinate, + }); + } + + return dataInTime; +} + +export default generateImageFromTime; diff --git a/packages/tools/src/utilities/dynamicVolume/index.ts b/packages/tools/src/utilities/dynamicVolume/index.ts index 48057ef2e..0f0e0e580 100644 --- a/packages/tools/src/utilities/dynamicVolume/index.ts +++ b/packages/tools/src/utilities/dynamicVolume/index.ts @@ -1,2 +1,5 @@ import getDataInTime from './getDataInTime'; +import generateImageFromTime from './generateImageFromTime'; + export { getDataInTime }; +export { generateImageFromTime }; From 03deb9e3d3b00e78b3a7b564aad01a2d692fa396 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 20 Mar 2023 15:37:26 -0400 Subject: [PATCH 02/21] added SUM, AVERAGE, and SUBTRACT operations --- .../examples/generateImageFromTime/index.ts | 7 +- .../dynamicVolume/generateImageFromTime.ts | 80 ++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index ef35e0b42..fce6d9a8d 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -53,10 +53,11 @@ addButtonToToolbar({ onClick: () => { const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTime( volumeForButton, + 'SUBTRACT', { - frameNumbers: [5, 6, 7], - // imageCoordinate: [-24, 24, -173], - maskVolumeId: segmentationId, + frameNumbers: [5, 40], + imageCoordinate: [-24, 24, -173], + // maskVolumeId: segmentationId, } ); }, diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index 2517c0a0b..702d37b9b 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -3,6 +3,7 @@ import getDataInTime from './getDataInTime'; function generateImageFromTime( dynamicVolume: Types.IDynamicImageVolume, + operation: string, options: { frameNumbers?; maskVolumeId?; @@ -10,10 +11,15 @@ function generateImageFromTime( } ): number[] | number[][] { let dataInTime; + let operationData; const frames = options.frameNumbers || [ ...Array(dynamicVolume.numTimePoints).keys(), ]; + if (frames.length <= 1) { + throw new Error('Please provide two or more time points'); + } + if (!options.maskVolumeId && !options.imageCoordinate) { throw new Error('No ROI provided'); } @@ -36,7 +42,79 @@ function generateImageFromTime( }); } - return dataInTime; + if (operation === 'SUM') { + operationData = _sumData(dataInTime, frames); + } + + if (operation === 'AVERAGE') { + operationData = _avgData(dataInTime, frames); + } + + if (operation === 'SUBTRACT') { + operationData = _subData(dataInTime, frames); + } + + // } + console.log(operationData); + console.log(dataInTime); + + return operationData; +} + +function _sumData(timeData, frames) { + const sumData = []; + if (Array.isArray(timeData[0])) { + for (let i = 0; i < timeData.length; i++) { + let voxelSum = 0; + for (let j = 0; j < frames.length; j++) { + voxelSum = voxelSum + timeData[i][j]; + } + sumData.push(voxelSum); + } + } else { + let voxelSum = 0; + for (let j = 0; j < frames.length; j++) { + voxelSum = voxelSum + timeData[j]; + } + sumData.push(voxelSum); + } + return sumData; +} + +function _avgData(timeData, frames) { + const avgData = []; + if (Array.isArray(timeData[0])) { + for (let i = 0; i < timeData.length; i++) { + let voxelSum = 0; + for (let j = 0; j < frames.length; j++) { + voxelSum = voxelSum + timeData[i][j]; + } + avgData.push(voxelSum / frames.length); + } + } else { + let voxelSum = 0; + for (let j = 0; j < frames.length; j++) { + voxelSum = voxelSum + timeData[j]; + } + avgData.push(voxelSum / frames.length); + } + return avgData; +} + +function _subData(timeData, frames) { + if (frames.length > 2) { + throw new Error('Please provide only 2 time points for subtraction.'); + } + const subData = []; + + if (Array.isArray(timeData[0])) { + for (let i = 0; i < timeData.length; i++) { + subData.push(timeData[i][0] - timeData[i][1]); + } + } else { + subData.push(timeData[0] - timeData[1]); + } + return subData; } export default generateImageFromTime; From c29f61a162d72b612d7d8fcbf6222f1709fe3b93 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 20 Mar 2023 15:59:17 -0400 Subject: [PATCH 03/21] fix for getDataInTime, uses forEach to only use the specified frames --- .../tools/examples/generateImageFromTime/index.ts | 6 +++--- .../utilities/dynamicVolume/generateImageFromTime.ts | 2 +- .../src/utilities/dynamicVolume/getDataInTime.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index fce6d9a8d..ec52d8aa7 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -55,9 +55,9 @@ addButtonToToolbar({ volumeForButton, 'SUBTRACT', { - frameNumbers: [5, 40], - imageCoordinate: [-24, 24, -173], - // maskVolumeId: segmentationId, + frameNumbers: [1, 40], + // imageCoordinate: [-24, 24, -173], + maskVolumeId: segmentationId, } ); }, diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index 702d37b9b..c20de0939 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -56,7 +56,7 @@ function generateImageFromTime( // } console.log(operationData); - console.log(dataInTime); + // console.log(dataInTime); return operationData; } diff --git a/packages/tools/src/utilities/dynamicVolume/getDataInTime.ts b/packages/tools/src/utilities/dynamicVolume/getDataInTime.ts index b9d46ab7c..9b360bf28 100644 --- a/packages/tools/src/utilities/dynamicVolume/getDataInTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/getDataInTime.ts @@ -83,11 +83,11 @@ function _getTimePointDataCoordinate(frames, coordinate, volume) { const allScalarData = volume.getScalarDataArrays(); const value = []; - for (let i = frames[0]; i < frames[0] + frames.length; i++) { - const activeScalarData = allScalarData[i]; + frames.forEach((frame) => { + const activeScalarData = allScalarData[frame]; const scalarIndex = index[2] * zMultiple + index[1] * yMultiple + index[0]; value.push(activeScalarData[scalarIndex]); - } + }); return value; } @@ -98,10 +98,10 @@ function _getTimePointDataMask(frames, indexArray, volume) { for (let i = 0; i < indexArray.length; i++) { const indexValues = []; - for (let j = frames[0]; j < frames[0] + frames.length; j++) { - const activeScalarData = allScalarData[j]; + frames.forEach((frame) => { + const activeScalarData = allScalarData[frame]; indexValues.push(activeScalarData[indexArray[i]]); - } + }); value.push(indexValues); } return value; From dd37de4dd2e3b7c0acf5320776fee92cf1b46dbe Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 20 Mar 2023 16:40:29 -0400 Subject: [PATCH 04/21] added drop down menu to select operations to perform on data --- .../examples/generateImageFromTime/index.ts | 21 +++++++++++++++---- .../dynamicVolume/generateImageFromTime.ts | 3 +-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index ec52d8aa7..f54361f78 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -35,6 +35,8 @@ const orientations = [ Enums.OrientationAxis.SAGITTAL, Enums.OrientationAxis.CORONAL, ]; +const operations = ['SUM', 'AVERAGE', 'SUBTRACT']; +let dataOperation = operations[0] // ======== Set up page ======== // setTitleAndDescription( 'Volume 4D', @@ -53,15 +55,26 @@ addButtonToToolbar({ onClick: () => { const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTime( volumeForButton, - 'SUBTRACT', + dataOperation, { - frameNumbers: [1, 40], - // imageCoordinate: [-24, 24, -173], - maskVolumeId: segmentationId, + frameNumbers: [1, 39], + imageCoordinate: [-24, 24, -173], + // maskVolumeId: segmentationId, } ); }, }); + +addDropdownToToolbar({ + options: { + values: operations, + defaultValue: operations[0], + }, + onSelectedValueChange: (selectedValue) => { + dataOperation = selectedValue as string; + }, +}); + addDropdownToToolbar({ options: { values: orientations, diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index c20de0939..8468ebff8 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -54,9 +54,8 @@ function generateImageFromTime( operationData = _subData(dataInTime, frames); } - // } console.log(operationData); - // console.log(dataInTime); + console.log(dataInTime); return operationData; } From 4ef18b936e1c466567944f1b4bc97e8d2a8f8404 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 21 Mar 2023 14:29:05 -0400 Subject: [PATCH 05/21] generateImageFromTime now outputs array of indexs for mask inputs --- .../examples/generateImageFromTime/index.ts | 32 +++++++++++++++++-- .../dynamicVolume/generateImageFromTime.ts | 22 ++++++------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index f54361f78..3c375b234 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -36,7 +36,7 @@ const orientations = [ Enums.OrientationAxis.CORONAL, ]; const operations = ['SUM', 'AVERAGE', 'SUBTRACT']; -let dataOperation = operations[0] +let dataOperation = operations[0]; // ======== Set up page ======== // setTitleAndDescription( 'Volume 4D', @@ -58,10 +58,11 @@ addButtonToToolbar({ dataOperation, { frameNumbers: [1, 39], - imageCoordinate: [-24, 24, -173], - // maskVolumeId: segmentationId, + // imageCoordinate: [-24, 24, -173], + maskVolumeId: segmentationId, } ); + createVolumeFromTimeData(dataInTime); }, }); @@ -109,6 +110,7 @@ const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix const volumeLoaderScheme = 'cornerstoneStreamingDynamicImageVolume'; // 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 computedVolumeId = 'MY_COMPUTED_ID'; const toolGroupId = 'MY_TOOLGROUP_ID'; /** * Adds two concentric circles to each axial slice of the demo segmentation. @@ -162,6 +164,30 @@ async function addSegmentationsToState() { // Add some data to the segmentations createMockEllipsoidSegmentation(segmentationVolume); } + +async function createVolumeFromTimeData(dataInTime) { + // Create a volume of the same resolution as the source data + // using volumeLoader.createAndCacheDerivedVolume. + // console.log('beep'); + // const computedVolume = await volumeLoader.createAndCacheDerivedVolume( + // volumeId, + // { + // volumeId: computedVolumeId, + // } + // ); + // // Add the segmentations to state + // const data = dataInTime.data; + // const index = dataInTime.index; + // let i = 0; + // index.forEach((voxelIndex) => { + // computedVolume.scalarData[voxelIndex] = data[i]; + // i++; + // }); + // // computedVolume.imageData.setPointData(); + // console.log(computedVolume); + // Add some data to the segmentations +} + /** * Runs the demo */ diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index 8468ebff8..3d13942e3 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -9,9 +9,10 @@ function generateImageFromTime( maskVolumeId?; imageCoordinate?; } -): number[] | number[][] { +) { let dataInTime; let operationData; + let indexArray; const frames = options.frameNumbers || [ ...Array(dynamicVolume.numTimePoints).keys(), ]; @@ -20,19 +21,16 @@ function generateImageFromTime( throw new Error('Please provide two or more time points'); } - if (!options.maskVolumeId && !options.imageCoordinate) { - throw new Error('No ROI provided'); - } - - if (options.maskVolumeId && options.imageCoordinate) { - throw new Error('Please provide only one ROI'); - } - if (options.maskVolumeId) { dataInTime = getDataInTime(dynamicVolume, { frameNumbers: frames, maskVolumeId: options.maskVolumeId, }); + const segmentationVolume = cache.getVolume(options.maskVolumeId); + indexArray = segmentationVolume + .getScalarData() + .map((_, i) => i) + .filter((i) => segmentationVolume.getScalarData()[i] !== 0); } if (options.imageCoordinate) { @@ -54,10 +52,10 @@ function generateImageFromTime( operationData = _subData(dataInTime, frames); } - console.log(operationData); - console.log(dataInTime); + // console.log(operationData); + // console.log(indexArray); - return operationData; + return { data: operationData, index: indexArray }; } function _sumData(timeData, frames) { From f333108c2e83666bce66354de93b298aabe22bea Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 22 Mar 2023 14:09:51 -0400 Subject: [PATCH 06/21] latest changes --- .../examples/generateImageFromTime/index.ts | 76 +++++++++++++------ .../dynamicVolume/generateImageFromTime.ts | 3 - 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index 3c375b234..b32a26224 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -17,6 +17,8 @@ import { addButtonToToolbar, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; +import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; + const { segmentation, SegmentationDisplayTool, @@ -30,6 +32,7 @@ console.warn( const { ViewportType } = Enums; const renderingEngineId = 'myRenderingEngine'; const viewportId = 'CT_SAGITTAL_STACK'; +let viewport; const orientations = [ Enums.OrientationAxis.AXIAL, Enums.OrientationAxis.SAGITTAL, @@ -39,8 +42,8 @@ const operations = ['SUM', 'AVERAGE', 'SUBTRACT']; let dataOperation = operations[0]; // ======== Set up page ======== // setTitleAndDescription( - 'Volume 4D', - 'Displays a 4D DICOM series in a Volume viewport.' + '3D Volume Generation From 4D Data', + 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators from a 4D time series.' ); const content = document.getElementById('content'); const element = document.createElement('div'); @@ -57,7 +60,7 @@ addButtonToToolbar({ volumeForButton, dataOperation, { - frameNumbers: [1, 39], + frameNumbers: [1, 19], // imageCoordinate: [-24, 24, -173], maskVolumeId: segmentationId, } @@ -110,7 +113,8 @@ const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix const volumeLoaderScheme = 'cornerstoneStreamingDynamicImageVolume'; // 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 computedVolumeId = 'MY_COMPUTED_ID'; +const computedVolumeName = 'PT_VOLUME_ID'; +const computedVolumeId = `cornerstoneStreamingImageVolume:${computedVolumeName}`; // VolumeId with loader id + volume id const toolGroupId = 'MY_TOOLGROUP_ID'; /** * Adds two concentric circles to each axial slice of the demo segmentation. @@ -169,23 +173,30 @@ async function createVolumeFromTimeData(dataInTime) { // Create a volume of the same resolution as the source data // using volumeLoader.createAndCacheDerivedVolume. // console.log('beep'); - // const computedVolume = await volumeLoader.createAndCacheDerivedVolume( - // volumeId, - // { - // volumeId: computedVolumeId, - // } - // ); + const computedVolume = await volumeLoader.createAndCacheDerivedVolume( + volumeId, + { + volumeId: computedVolumeId, + } + ); // // Add the segmentations to state - // const data = dataInTime.data; - // const index = dataInTime.index; - // let i = 0; - // index.forEach((voxelIndex) => { - // computedVolume.scalarData[voxelIndex] = data[i]; - // i++; - // }); + const data = dataInTime.data; + const index = dataInTime.index; + let i = 0; + index.forEach((voxelIndex) => { + computedVolume.scalarData[voxelIndex] = data[i]; + i++; + }); + volumeLoader.loadVolume(computedVolumeId); // // computedVolume.imageData.setPointData(); - // console.log(computedVolume); + // console.log(computedVolume.scalarData[index[0]]); + console.log(computedVolume); // Add some data to the segmentations + console.log(computedVolumeId); + viewport.setVolumes([ + { computedVolumeId, callback: setPetTransferFunctionForVolumeActor }, + ]); + // viewport.render; } /** @@ -201,14 +212,36 @@ async function run() { cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId); toolGroup.addTool(SegmentationDisplayTool.toolName); toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); + + const { metaDataManager } = cornerstoneWADOImageLoader.wadors; + // Get Cornerstone imageIds and fetch metadata into RAM - const imageIds = await createImageIdsAndCacheMetaData({ + let imageIds = await createImageIdsAndCacheMetaData({ StudyInstanceUID: '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849', SeriesInstanceUID: '1.3.6.1.4.1.12842.1.1.22.4.20220915.124758.560.4125514885', wadoRsRoot: 'https://d28o5kq0jsoob5.cloudfront.net/dicomweb', }); + + const MAX_NUM_TIMEPOINTS = 40; + const numTimePoints = 20; + const NUM_IMAGES_PER_TIME_POINT = 235; + const TOTAL_NUM_IMAGES = MAX_NUM_TIMEPOINTS * NUM_IMAGES_PER_TIME_POINT; + const numImagesToLoad = numTimePoints * NUM_IMAGES_PER_TIME_POINT; + + // Load the last N time points because they have a better image quality + // and first ones are white or contains only a few black pixels + const firstInstanceNumber = TOTAL_NUM_IMAGES - numImagesToLoad + 1; + + imageIds = imageIds.filter((imageId) => { + const instanceMetaData = metaDataManager.get(imageId); + const instanceTag = instanceMetaData['00200013']; + const instanceNumber = parseInt(instanceTag.Value[0]); + + return instanceNumber >= firstInstanceNumber; + }); + // Instantiate a rendering engine const renderingEngine = new RenderingEngine(renderingEngineId); // Create a stack viewport @@ -225,13 +258,12 @@ async function run() { // Add viewport to toolGroup toolGroup.addViewport(viewportId, renderingEngineId); // Get the stack viewport that was created - const viewport = ( - renderingEngine.getViewport(viewportId) - ); + viewport = renderingEngine.getViewport(viewportId); // Define a volume in memory const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds, }); + console.log(volume); // Add segmentation await addSegmentationsToState(); // Set the volume to load diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index 3d13942e3..1de8c7f36 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -52,9 +52,6 @@ function generateImageFromTime( operationData = _subData(dataInTime, frames); } - // console.log(operationData); - // console.log(indexArray); - return { data: operationData, index: indexArray }; } From 99c1ef65142a8760d83715ee57c732fd92527729 Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 23 Mar 2023 15:17:03 -0400 Subject: [PATCH 07/21] refactored operations for performance, added viewport for rendering generated image --- .../examples/generateImageFromTime/index.ts | 128 ++++++++++++------ .../dynamicVolume/generateImageFromTime.ts | 82 +++++++---- 2 files changed, 145 insertions(+), 65 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index b32a26224..197eeb4c6 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -6,6 +6,7 @@ import { getRenderingEngine, CONSTANTS, utilities, + cache, } from '@cornerstonejs/core'; import { initDemo, @@ -31,8 +32,9 @@ console.warn( ); const { ViewportType } = Enums; const renderingEngineId = 'myRenderingEngine'; -const viewportId = 'CT_SAGITTAL_STACK'; -let viewport; +const viewportId1 = 'CT_SAGITTAL_STACK'; +const viewportId2 = 'COMPUTED_STACK'; + const orientations = [ Enums.OrientationAxis.AXIAL, Enums.OrientationAxis.SAGITTAL, @@ -45,12 +47,33 @@ setTitleAndDescription( '3D Volume Generation From 4D Data', 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators from a 4D time series.' ); + +const size = '500px'; const content = document.getElementById('content'); -const element = document.createElement('div'); -element.id = 'cornerstone-element'; -element.style.width = '500px'; -element.style.height = '500px'; -content.appendChild(element); +const viewportGrid = document.createElement('div'); +// const element = document.createElement('div'); + +viewportGrid.style.display = 'flex'; +viewportGrid.style.flexDirection = 'row'; + +// element.id = 'cornerstone-element'; +// element.style.width = '500px'; +// element.style.height = '500px'; +const element1 = document.createElement('div'); +const element2 = document.createElement('div'); +element1.style.width = size; +element1.style.height = size; +element2.style.width = size; +element2.style.height = size; + +element1.oncontextmenu = (e) => e.preventDefault(); +element2.oncontextmenu = (e) => e.preventDefault(); + +// content.appendChild(element); +viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); + +content.appendChild(viewportGrid); // ============================= // let volumeForButton; addButtonToToolbar({ @@ -60,9 +83,9 @@ addButtonToToolbar({ volumeForButton, dataOperation, { - frameNumbers: [1, 19], - // imageCoordinate: [-24, 24, -173], - maskVolumeId: segmentationId, + frameNumbers: [1, 2], + // // imageCoordinate: [-24, 24, -173], + // maskVolumeId: segmentationId, } ); createVolumeFromTimeData(dataInTime); @@ -89,7 +112,7 @@ addDropdownToToolbar({ const renderingEngine = getRenderingEngine(renderingEngineId); // Get the volume viewport const viewport = ( - renderingEngine.getViewport(viewportId) + renderingEngine.getViewport(viewportId1) ); viewport.setOrientation(selectedValue); viewport.render(); @@ -116,6 +139,7 @@ const segmentationId = 'MY_SEGMENTATION_ID'; const computedVolumeName = 'PT_VOLUME_ID'; const computedVolumeId = `cornerstoneStreamingImageVolume:${computedVolumeName}`; // VolumeId with loader id + volume id const toolGroupId = 'MY_TOOLGROUP_ID'; +let renderingEngine; /** * Adds two concentric circles to each axial slice of the demo segmentation. */ @@ -180,23 +204,40 @@ async function createVolumeFromTimeData(dataInTime) { } ); // // Add the segmentations to state - const data = dataInTime.data; - const index = dataInTime.index; - let i = 0; - index.forEach((voxelIndex) => { - computedVolume.scalarData[voxelIndex] = data[i]; - i++; - }); + + const scalarData = computedVolume.getScalarData(); + for (let i = 0; i < dataInTime.length; i++) { + scalarData[i] = dataInTime[i]; + } + + const viewportInput2 = { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.ACQUISITION, + background: [0.2, 0, 0.2], + }, + }; + + const viewport2 = ( + renderingEngine.getViewport(viewportId2) + ); + renderingEngine.enableElement(viewportInput2); + volumeLoader.loadVolume(computedVolumeId); - // // computedVolume.imageData.setPointData(); - // console.log(computedVolume.scalarData[index[0]]); - console.log(computedVolume); - // Add some data to the segmentations - console.log(computedVolumeId); - viewport.setVolumes([ - { computedVolumeId, callback: setPetTransferFunctionForVolumeActor }, + + console.log(cache.getVolume(computedVolumeId)); + console.log(cache.getVolume(volumeId)); + console.log(cornerstone.cache._volumeCache); + debugger; + viewport2.setVolumes([ + { + volumeId: computedVolumeId, + callback: setPetTransferFunctionForVolumeActor, + }, ]); - // viewport.render; + viewport2.render; } /** @@ -225,7 +266,7 @@ async function run() { }); const MAX_NUM_TIMEPOINTS = 40; - const numTimePoints = 20; + const numTimePoints = 3; const NUM_IMAGES_PER_TIME_POINT = 235; const TOTAL_NUM_IMAGES = MAX_NUM_TIMEPOINTS * NUM_IMAGES_PER_TIME_POINT; const numImagesToLoad = numTimePoints * NUM_IMAGES_PER_TIME_POINT; @@ -243,43 +284,48 @@ async function run() { }); // Instantiate a rendering engine - const renderingEngine = new RenderingEngine(renderingEngineId); + renderingEngine = new RenderingEngine(renderingEngineId); // Create a stack viewport - const viewportInput = { - viewportId, + const viewportInput1 = { + viewportId: viewportId1, type: ViewportType.ORTHOGRAPHIC, - element, + element: element1, defaultOptions: { orientation: Enums.OrientationAxis.ACQUISITION, background: [0.2, 0, 0.2], }, }; - renderingEngine.enableElement(viewportInput); + renderingEngine.enableElement(viewportInput1); // Add viewport to toolGroup - toolGroup.addViewport(viewportId, renderingEngineId); + toolGroup.addViewport(viewportId1, renderingEngineId); + toolGroup.addViewport(viewportId2, renderingEngineId); // Get the stack viewport that was created - viewport = renderingEngine.getViewport(viewportId); + const viewport = ( + renderingEngine.getViewport(viewportId1) + ); // Define a volume in memory const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds, }); - console.log(volume); + // Add segmentation await addSegmentationsToState(); + // Set the volume to load volume.load(); volumeForButton = volume; addTimePointSlider(volume); + // Set the volume on the viewport viewport.setVolumes([ { volumeId, callback: setPetTransferFunctionForVolumeActor }, ]); - await segmentation.addSegmentationRepresentations(toolGroupId, [ - { - segmentationId, - type: csToolsEnums.SegmentationRepresentations.Labelmap, - }, - ]); + // await segmentation.addSegmentationRepresentations(toolGroupId, [ + // { + // segmentationId, + // type: csToolsEnums.SegmentationRepresentations.Labelmap, + // }, + // ]); // Render the image viewport.render(); } diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index 1de8c7f36..d885a9ae8 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -8,7 +8,7 @@ function generateImageFromTime( frameNumbers?; maskVolumeId?; imageCoordinate?; - } + } = {} ) { let dataInTime; let operationData; @@ -21,38 +21,72 @@ function generateImageFromTime( throw new Error('Please provide two or more time points'); } - if (options.maskVolumeId) { - dataInTime = getDataInTime(dynamicVolume, { - frameNumbers: frames, - maskVolumeId: options.maskVolumeId, - }); - const segmentationVolume = cache.getVolume(options.maskVolumeId); - indexArray = segmentationVolume - .getScalarData() - .map((_, i) => i) - .filter((i) => segmentationVolume.getScalarData()[i] !== 0); - } + const typedArrays = dynamicVolume.getScalarDataArrays(); - if (options.imageCoordinate) { - dataInTime = getDataInTime(dynamicVolume, { - frameNumbers: frames, - imageCoordinate: options.imageCoordinate, - }); - } + const arrayLength = typedArrays[0].length; + const finalArray = new Float32Array(arrayLength); // same type (you get this via create and cache) if (operation === 'SUM') { - operationData = _sumData(dataInTime, frames); + for (let i = 0; i < typedArrays.length; i++) { + const currentArray = typedArrays[i]; + for (let j = 0; j < arrayLength; j++) { + finalArray[j] += currentArray[j]; + } + } } - if (operation === 'AVERAGE') { - operationData = _avgData(dataInTime, frames); + if (operation === 'SUBTRACT') { + if (frames.length > 2) { + throw new Error('Please provide only 2 time points for subtraction.'); + } + for (let j = 0; j < arrayLength; j++) { + finalArray[j] += typedArrays[0][j] - typedArrays[1][j]; + } } - if (operation === 'SUBTRACT') { - operationData = _subData(dataInTime, frames); + if (operation === 'AVERAGE') { + for (let i = 0; i < typedArrays.length; i++) { + const currentArray = typedArrays[i]; + for (let j = 0; j < arrayLength; j++) { + finalArray[j] += currentArray[j]; + if (j === arrayLength - 1) { + finalArray[j] = finalArray[j] / typedArrays.length; + } + } + } } - return { data: operationData, index: indexArray }; + // Iterate through each array and sum the corresponding elements + + // if (options.maskVolumeId) { + // dataInTime = getDataInTime(dynamicVolume, { + // frameNumbers: frames, + // maskVolumeId: options.maskVolumeId, + // }); + // const segmentationVolume = cache.getVolume(options.maskVolumeId); + // indexArray = segmentationVolume + // .getScalarData() + // .map((_, i) => i) + // .filter((i) => segmentationVolume.getScalarData()[i] !== 0); + // } + + // if (options.imageCoordinate) { + // dataInTime = getDataInTime(dynamicVolume, { + // frameNumbers: frames, + // imageCoordinate: options.imageCoordinate, + // }); + // } + + // if (operation === 'SUM') { + // operationData = _sumData(dataInTime, frames); + // } + + // if (operation === 'AVERAGE') { + // operationData = _avgData(dataInTime, frames); + // } + + + return finalArray; } function _sumData(timeData, frames) { From bc28e96cc37f66964a5deb2f9b3772be675369df Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 24 Mar 2023 13:58:44 -0400 Subject: [PATCH 08/21] WIP updated work --- .../examples/generateImageFromTime/index.ts | 85 ++++++++++--------- .../dynamicVolume/generateImageFromTime.ts | 43 ++-------- 2 files changed, 53 insertions(+), 75 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index 197eeb4c6..402fa7026 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -140,6 +140,10 @@ const computedVolumeName = 'PT_VOLUME_ID'; const computedVolumeId = `cornerstoneStreamingImageVolume:${computedVolumeName}`; // VolumeId with loader id + volume id const toolGroupId = 'MY_TOOLGROUP_ID'; let renderingEngine; +let viewport; +let viewport2; +let computedVolume; +let volume; /** * Adds two concentric circles to each axial slice of the demo segmentation. */ @@ -196,47 +200,23 @@ async function addSegmentationsToState() { async function createVolumeFromTimeData(dataInTime) { // Create a volume of the same resolution as the source data // using volumeLoader.createAndCacheDerivedVolume. - // console.log('beep'); - const computedVolume = await volumeLoader.createAndCacheDerivedVolume( - volumeId, - { - volumeId: computedVolumeId, - } - ); - // // Add the segmentations to state + + // const localVolumeOptions = { + // scalarData: dataInTime, + // metadata: volume.metadata, // Just use the same metadata for the example. + // dimensions: volume.dimensions, + // spacing: volume.spacing, + // origin: volume.origin, + // direction: volume.direction, + // }; const scalarData = computedVolume.getScalarData(); for (let i = 0; i < dataInTime.length; i++) { scalarData[i] = dataInTime[i]; } - const viewportInput2 = { - viewportId: viewportId2, - type: ViewportType.ORTHOGRAPHIC, - element: element2, - defaultOptions: { - orientation: Enums.OrientationAxis.ACQUISITION, - background: [0.2, 0, 0.2], - }, - }; - - const viewport2 = ( - renderingEngine.getViewport(viewportId2) - ); - renderingEngine.enableElement(viewportInput2); - - volumeLoader.loadVolume(computedVolumeId); + console.log(computedVolume); - console.log(cache.getVolume(computedVolumeId)); - console.log(cache.getVolume(volumeId)); - console.log(cornerstone.cache._volumeCache); - debugger; - viewport2.setVolumes([ - { - volumeId: computedVolumeId, - callback: setPetTransferFunctionForVolumeActor, - }, - ]); viewport2.render; } @@ -266,7 +246,7 @@ async function run() { }); const MAX_NUM_TIMEPOINTS = 40; - const numTimePoints = 3; + const numTimePoints = 5; const NUM_IMAGES_PER_TIME_POINT = 235; const TOTAL_NUM_IMAGES = MAX_NUM_TIMEPOINTS * NUM_IMAGES_PER_TIME_POINT; const numImagesToLoad = numTimePoints * NUM_IMAGES_PER_TIME_POINT; @@ -295,24 +275,40 @@ async function run() { background: [0.2, 0, 0.2], }, }; + const viewportInput2 = { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.ACQUISITION, + background: [0.2, 0, 0.2], + }, + }; + renderingEngine.enableElement(viewportInput1); + renderingEngine.enableElement(viewportInput2); // Add viewport to toolGroup toolGroup.addViewport(viewportId1, renderingEngineId); - toolGroup.addViewport(viewportId2, renderingEngineId); + // toolGroup.addViewport(viewportId2, renderingEngineId); // Get the stack viewport that was created - const viewport = ( - renderingEngine.getViewport(viewportId1) - ); + viewport = renderingEngine.getViewport(viewportId1); + viewport2 = renderingEngine.getViewport(viewportId2); + // Define a volume in memory - const volume = await volumeLoader.createAndCacheVolume(volumeId, { + volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds, }); + computedVolume = await volumeLoader.createAndCacheDerivedVolume(volumeId, { + volumeId: computedVolumeId, + }); + // Add segmentation await addSegmentationsToState(); // Set the volume to load volume.load(); + // computedVolume.load(); volumeForButton = volume; addTimePointSlider(volume); @@ -320,13 +316,22 @@ async function run() { viewport.setVolumes([ { volumeId, callback: setPetTransferFunctionForVolumeActor }, ]); + viewport2.setVolumes([ + { + volumeId: computedVolumeId, + callback: setPetTransferFunctionForVolumeActor, + }, + ]); + // await segmentation.addSegmentationRepresentations(toolGroupId, [ // { // segmentationId, // type: csToolsEnums.SegmentationRepresentations.Labelmap, // }, // ]); + // Render the image viewport.render(); + viewport2.render(); } run(); diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index d885a9ae8..cd9cab78a 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -13,9 +13,11 @@ function generateImageFromTime( let dataInTime; let operationData; let indexArray; + const frames = options.frameNumbers || [ ...Array(dynamicVolume.numTimePoints).keys(), ]; + const numFrames = frames.length; if (frames.length <= 1) { throw new Error('Please provide two or more time points'); @@ -27,8 +29,9 @@ function generateImageFromTime( const finalArray = new Float32Array(arrayLength); // same type (you get this via create and cache) if (operation === 'SUM') { - for (let i = 0; i < typedArrays.length; i++) { - const currentArray = typedArrays[i]; + for (let i = 0; i < numFrames; i++) { + const currentArray = typedArrays[frames[i]]; + console.log(frames[i]); for (let j = 0; j < arrayLength; j++) { finalArray[j] += currentArray[j]; } @@ -45,47 +48,17 @@ function generateImageFromTime( } if (operation === 'AVERAGE') { - for (let i = 0; i < typedArrays.length; i++) { - const currentArray = typedArrays[i]; + for (let i = 0; i < numFrames; i++) { + const currentArray = typedArrays[frames[i]]; for (let j = 0; j < arrayLength; j++) { finalArray[j] += currentArray[j]; if (j === arrayLength - 1) { - finalArray[j] = finalArray[j] / typedArrays.length; + finalArray[j] = finalArray[j] / numFrames; } } } } - // Iterate through each array and sum the corresponding elements - - // if (options.maskVolumeId) { - // dataInTime = getDataInTime(dynamicVolume, { - // frameNumbers: frames, - // maskVolumeId: options.maskVolumeId, - // }); - // const segmentationVolume = cache.getVolume(options.maskVolumeId); - // indexArray = segmentationVolume - // .getScalarData() - // .map((_, i) => i) - // .filter((i) => segmentationVolume.getScalarData()[i] !== 0); - // } - - // if (options.imageCoordinate) { - // dataInTime = getDataInTime(dynamicVolume, { - // frameNumbers: frames, - // imageCoordinate: options.imageCoordinate, - // }); - // } - - // if (operation === 'SUM') { - // operationData = _sumData(dataInTime, frames); - // } - - // if (operation === 'AVERAGE') { - // operationData = _avgData(dataInTime, frames); - // } - - return finalArray; } From 46e4ba77bba307b2a6a1510e68964ce7730d0a27 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 24 Mar 2023 15:48:53 -0400 Subject: [PATCH 09/21] fix --- .../examples/generateImageFromTime/index.ts | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index 402fa7026..d3bbd0bb7 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -147,55 +147,55 @@ let volume; /** * Adds two concentric circles to each axial slice of the demo segmentation. */ -function createMockEllipsoidSegmentation(segmentationVolume) { - const scalarData = segmentationVolume.scalarData; - const { dimensions } = segmentationVolume; - const center = [72, 145, 117.5]; - const innerRadius = 20; - let voxelIndex = 0; - for (let z = 0; z < dimensions[2]; z++) { - for (let y = 0; y < dimensions[1]; y++) { - for (let x = 0; x < dimensions[0]; x++) { - const distanceFromCenter = Math.sqrt( - (x - center[0]) * (x - center[0]) + - (y - center[1]) * (y - center[1]) + - (z - center[2]) * (z - center[2]) - ); - if (distanceFromCenter < innerRadius) { - scalarData[voxelIndex] = 1; - } - voxelIndex++; - } - } - } -} -async function addSegmentationsToState() { - // Create a segmentation of the same resolution as the source data - // using volumeLoader.createAndCacheDerivedVolume. - const segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( - volumeId, - { - volumeId: segmentationId, - } - ); - // Add the segmentations to state - segmentation.addSegmentations([ - { - segmentationId, - representation: { - // The type of segmentation - type: csToolsEnums.SegmentationRepresentations.Labelmap, - // The actual segmentation data, in the case of labelmap this is a - // reference to the source volume of the segmentation. - data: { - volumeId: segmentationId, - }, - }, - }, - ]); - // Add some data to the segmentations - createMockEllipsoidSegmentation(segmentationVolume); -} +// function createMockEllipsoidSegmentation(segmentationVolume) { +// const scalarData = segmentationVolume.scalarData; +// const { dimensions } = segmentationVolume; +// const center = [72, 145, 117.5]; +// const innerRadius = 20; +// let voxelIndex = 0; +// for (let z = 0; z < dimensions[2]; z++) { +// for (let y = 0; y < dimensions[1]; y++) { +// for (let x = 0; x < dimensions[0]; x++) { +// const distanceFromCenter = Math.sqrt( +// (x - center[0]) * (x - center[0]) + +// (y - center[1]) * (y - center[1]) + +// (z - center[2]) * (z - center[2]) +// ); +// if (distanceFromCenter < innerRadius) { +// scalarData[voxelIndex] = 1; +// } +// voxelIndex++; +// } +// } +// } +// } +// async function addSegmentationsToState() { +// // Create a segmentation of the same resolution as the source data +// // using volumeLoader.createAndCacheDerivedVolume. +// const segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( +// volumeId, +// { +// volumeId: segmentationId, +// } +// ); +// // Add the segmentations to state +// segmentation.addSegmentations([ +// { +// segmentationId, +// representation: { +// // The type of segmentation +// type: csToolsEnums.SegmentationRepresentations.Labelmap, +// // The actual segmentation data, in the case of labelmap this is a +// // reference to the source volume of the segmentation. +// data: { +// volumeId: segmentationId, +// }, +// }, +// }, +// ]); +// // Add some data to the segmentations +// // createMockEllipsoidSegmentation(segmentationVolume); +// } async function createVolumeFromTimeData(dataInTime) { // Create a volume of the same resolution as the source data @@ -215,9 +215,14 @@ async function createVolumeFromTimeData(dataInTime) { scalarData[i] = dataInTime[i]; } - console.log(computedVolume); + viewport2.setVolumes([ + { + volumeId: computedVolumeId, + callback: setPetTransferFunctionForVolumeActor, + }, + ]); - viewport2.render; + viewport2.render(); } /** @@ -304,7 +309,7 @@ async function run() { }); // Add segmentation - await addSegmentationsToState(); + // await addSegmentationsToState(); // Set the volume to load volume.load(); @@ -316,12 +321,12 @@ async function run() { viewport.setVolumes([ { volumeId, callback: setPetTransferFunctionForVolumeActor }, ]); - viewport2.setVolumes([ - { - volumeId: computedVolumeId, - callback: setPetTransferFunctionForVolumeActor, - }, - ]); + // viewport2.setVolumes([ + // { + // volumeId: computedVolumeId, + // callback: setPetTransferFunctionForVolumeActor, + // }, + // ]); // await segmentation.addSegmentationRepresentations(toolGroupId, [ // { From 06d9b088a6a8d1ffa6d31f717b801ac195a8afb9 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 27 Mar 2023 10:41:57 -0400 Subject: [PATCH 10/21] Cleaned up code, refactored average operation, initialized clear image button --- .../examples/generateImageFromTime/index.ts | 124 +++++------------- .../dynamicVolume/generateImageFromTime.ts | 69 +--------- 2 files changed, 40 insertions(+), 153 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index d3bbd0bb7..9dcdd09f3 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -77,21 +77,45 @@ content.appendChild(viewportGrid); // ============================= // let volumeForButton; addButtonToToolbar({ - title: 'Get Data In Time', + title: 'Generate Image', onClick: () => { const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTime( volumeForButton, dataOperation, { - frameNumbers: [1, 2], - // // imageCoordinate: [-24, 24, -173], - // maskVolumeId: segmentationId, + frameNumbers: [0, 1, 3], } ); createVolumeFromTimeData(dataInTime); }, }); +addButtonToToolbar({ + title: 'Clear image', + onClick: () => { + const scalarData = computedVolume.getScalarData(); + for (let i = 0; i < scalarData.length; i++) { + scalarData[i] = 0; + } + console.log(computedVolume.getScalarData()[4160506]); + + viewport2.removeVolumeActors([computedVolumeId]); + + viewport2.render(); + // console.log('called render again'); + }, +}); + +addButtonToToolbar({ + title: 'Check computed volume', + onClick: () => { + console.log(viewport2.getActors()); + console.log(computedVolume.getScalarData()[4160506]); + + // console.log('called render again'); + }, +}); + addDropdownToToolbar({ options: { values: operations, @@ -118,6 +142,7 @@ addDropdownToToolbar({ viewport.render(); }, }); + function addTimePointSlider(volume) { addSliderToToolbar({ title: 'Time Point', @@ -135,7 +160,6 @@ const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix // const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use const volumeLoaderScheme = 'cornerstoneStreamingDynamicImageVolume'; // 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 computedVolumeName = 'PT_VOLUME_ID'; const computedVolumeId = `cornerstoneStreamingImageVolume:${computedVolumeName}`; // VolumeId with loader id + volume id const toolGroupId = 'MY_TOOLGROUP_ID'; @@ -143,78 +167,17 @@ let renderingEngine; let viewport; let viewport2; let computedVolume; -let volume; -/** - * Adds two concentric circles to each axial slice of the demo segmentation. - */ -// function createMockEllipsoidSegmentation(segmentationVolume) { -// const scalarData = segmentationVolume.scalarData; -// const { dimensions } = segmentationVolume; -// const center = [72, 145, 117.5]; -// const innerRadius = 20; -// let voxelIndex = 0; -// for (let z = 0; z < dimensions[2]; z++) { -// for (let y = 0; y < dimensions[1]; y++) { -// for (let x = 0; x < dimensions[0]; x++) { -// const distanceFromCenter = Math.sqrt( -// (x - center[0]) * (x - center[0]) + -// (y - center[1]) * (y - center[1]) + -// (z - center[2]) * (z - center[2]) -// ); -// if (distanceFromCenter < innerRadius) { -// scalarData[voxelIndex] = 1; -// } -// voxelIndex++; -// } -// } -// } -// } -// async function addSegmentationsToState() { -// // Create a segmentation of the same resolution as the source data -// // using volumeLoader.createAndCacheDerivedVolume. -// const segmentationVolume = await volumeLoader.createAndCacheDerivedVolume( -// volumeId, -// { -// volumeId: segmentationId, -// } -// ); -// // Add the segmentations to state -// segmentation.addSegmentations([ -// { -// segmentationId, -// representation: { -// // The type of segmentation -// type: csToolsEnums.SegmentationRepresentations.Labelmap, -// // The actual segmentation data, in the case of labelmap this is a -// // reference to the source volume of the segmentation. -// data: { -// volumeId: segmentationId, -// }, -// }, -// }, -// ]); -// // Add some data to the segmentations -// // createMockEllipsoidSegmentation(segmentationVolume); -// } async function createVolumeFromTimeData(dataInTime) { - // Create a volume of the same resolution as the source data - // using volumeLoader.createAndCacheDerivedVolume. - - // const localVolumeOptions = { - // scalarData: dataInTime, - // metadata: volume.metadata, // Just use the same metadata for the example. - // dimensions: volume.dimensions, - // spacing: volume.spacing, - // origin: volume.origin, - // direction: volume.direction, - // }; + // Fill the scalar data of the computed volume with the operation data const scalarData = computedVolume.getScalarData(); for (let i = 0; i < dataInTime.length; i++) { scalarData[i] = dataInTime[i]; } + console.log(computedVolume.getScalarData()[4160506]); + // Set computed volume to second viewport viewport2.setVolumes([ { volumeId: computedVolumeId, @@ -292,15 +255,16 @@ async function run() { renderingEngine.enableElement(viewportInput1); renderingEngine.enableElement(viewportInput2); + // Add viewport to toolGroup toolGroup.addViewport(viewportId1, renderingEngineId); - // toolGroup.addViewport(viewportId2, renderingEngineId); + // Get the stack viewport that was created viewport = renderingEngine.getViewport(viewportId1); viewport2 = renderingEngine.getViewport(viewportId2); // Define a volume in memory - volume = await volumeLoader.createAndCacheVolume(volumeId, { + const volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds, }); @@ -308,12 +272,9 @@ async function run() { volumeId: computedVolumeId, }); - // Add segmentation - // await addSegmentationsToState(); - // Set the volume to load volume.load(); - // computedVolume.load(); + volumeForButton = volume; addTimePointSlider(volume); @@ -321,19 +282,6 @@ async function run() { viewport.setVolumes([ { volumeId, callback: setPetTransferFunctionForVolumeActor }, ]); - // viewport2.setVolumes([ - // { - // volumeId: computedVolumeId, - // callback: setPetTransferFunctionForVolumeActor, - // }, - // ]); - - // await segmentation.addSegmentationRepresentations(toolGroupId, [ - // { - // segmentationId, - // type: csToolsEnums.SegmentationRepresentations.Labelmap, - // }, - // ]); // Render the image viewport.render(); diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts index cd9cab78a..90900b4c7 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts @@ -10,10 +10,6 @@ function generateImageFromTime( imageCoordinate?; } = {} ) { - let dataInTime; - let operationData; - let indexArray; - const frames = options.frameNumbers || [ ...Array(dynamicVolume.numTimePoints).keys(), ]; @@ -26,12 +22,11 @@ function generateImageFromTime( const typedArrays = dynamicVolume.getScalarDataArrays(); const arrayLength = typedArrays[0].length; - const finalArray = new Float32Array(arrayLength); // same type (you get this via create and cache) + const finalArray = new Float32Array(arrayLength); if (operation === 'SUM') { for (let i = 0; i < numFrames; i++) { const currentArray = typedArrays[frames[i]]; - console.log(frames[i]); for (let j = 0; j < arrayLength; j++) { finalArray[j] += currentArray[j]; } @@ -52,70 +47,14 @@ function generateImageFromTime( const currentArray = typedArrays[frames[i]]; for (let j = 0; j < arrayLength; j++) { finalArray[j] += currentArray[j]; - if (j === arrayLength - 1) { - finalArray[j] = finalArray[j] / numFrames; - } - } - } - } - - return finalArray; -} - -function _sumData(timeData, frames) { - const sumData = []; - if (Array.isArray(timeData[0])) { - for (let i = 0; i < timeData.length; i++) { - let voxelSum = 0; - for (let j = 0; j < frames.length; j++) { - voxelSum = voxelSum + timeData[i][j]; - } - sumData.push(voxelSum); - } - } else { - let voxelSum = 0; - for (let j = 0; j < frames.length; j++) { - voxelSum = voxelSum + timeData[j]; - } - sumData.push(voxelSum); - } - return sumData; -} - -function _avgData(timeData, frames) { - const avgData = []; - if (Array.isArray(timeData[0])) { - for (let i = 0; i < timeData.length; i++) { - let voxelSum = 0; - for (let j = 0; j < frames.length; j++) { - voxelSum = voxelSum + timeData[i][j]; } - avgData.push(voxelSum / frames.length); } - } else { - let voxelSum = 0; - for (let j = 0; j < frames.length; j++) { - voxelSum = voxelSum + timeData[j]; + for (let k = 0; k < arrayLength; k++) { + finalArray[k] = finalArray[k] / numFrames; } - avgData.push(voxelSum / frames.length); - } - return avgData; -} - -function _subData(timeData, frames) { - if (frames.length > 2) { - throw new Error('Please provide only 2 time points for subtraction.'); } - const subData = []; - if (Array.isArray(timeData[0])) { - for (let i = 0; i < timeData.length; i++) { - subData.push(timeData[i][0] - timeData[i][1]); - } - } else { - subData.push(timeData[0] - timeData[1]); - } - return subData; + return finalArray; } export default generateImageFromTime; From f45873a5350ffc2d76a1dbc6c367949910f00ed7 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 27 Mar 2023 11:06:53 -0400 Subject: [PATCH 11/21] added update for texture and image data so user can now switch between operations without having to refresh the page --- .../examples/generateImageFromTime/index.ts | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTime/index.ts index 9dcdd09f3..7807dee07 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTime/index.ts @@ -90,32 +90,6 @@ addButtonToToolbar({ }, }); -addButtonToToolbar({ - title: 'Clear image', - onClick: () => { - const scalarData = computedVolume.getScalarData(); - for (let i = 0; i < scalarData.length; i++) { - scalarData[i] = 0; - } - console.log(computedVolume.getScalarData()[4160506]); - - viewport2.removeVolumeActors([computedVolumeId]); - - viewport2.render(); - // console.log('called render again'); - }, -}); - -addButtonToToolbar({ - title: 'Check computed volume', - onClick: () => { - console.log(viewport2.getActors()); - console.log(computedVolume.getScalarData()[4160506]); - - // console.log('called render again'); - }, -}); - addDropdownToToolbar({ options: { values: operations, @@ -170,12 +144,18 @@ let computedVolume; async function createVolumeFromTimeData(dataInTime) { // Fill the scalar data of the computed volume with the operation data - const scalarData = computedVolume.getScalarData(); for (let i = 0; i < dataInTime.length; i++) { scalarData[i] = dataInTime[i]; } - console.log(computedVolume.getScalarData()[4160506]); + + const { imageData, vtkOpenGLTexture } = computedVolume; + const numSlices = imageData.getDimensions()[2]; + const slicesToUpdate = [...Array(numSlices).keys()]; + slicesToUpdate.forEach((i) => { + vtkOpenGLTexture.setUpdatedFrame(i); + }); + imageData.modified(); // Set computed volume to second viewport viewport2.setVolumes([ From 9f7f02dc9145d95c428c4e8632d11560db00eec3 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 27 Mar 2023 14:49:19 -0400 Subject: [PATCH 12/21] Added documentation, renamed files, improved parameters, cleaned up code --- common/reviews/api/tools.api.md | 6 +++- .../index.ts | 15 ++------- ...omTime.ts => generateImageFromTimeData.ts} | 31 ++++++++++++------- .../src/utilities/dynamicVolume/index.ts | 4 +-- 4 files changed, 28 insertions(+), 28 deletions(-) rename packages/tools/examples/{generateImageFromTime => generateImageFromTimeData}/index.ts (95%) rename packages/tools/src/utilities/dynamicVolume/{generateImageFromTime.ts => generateImageFromTimeData.ts} (57%) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 8be7b331c..8660532b1 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -1556,7 +1556,8 @@ function drawTextBox(svgDrawingHelper: SVGDrawingHelper, annotationUID: string, declare namespace dynamicVolume { export { - getDataInTime + getDataInTime, + generateImageFromTimeData } } @@ -1978,6 +1979,9 @@ class FrameOfReferenceSpecificAnnotationManager implements IAnnotationManager { readonly uid: string; } +// @public (undocumented) +function generateImageFromTimeData(dynamicVolume: Types_2.IDynamicImageVolume, operation: string, frameNumbers?: number[]): Float32Array; + // @public (undocumented) function getActiveSegmentationRepresentation(toolGroupId: string): ToolGroupSpecificRepresentation; diff --git a/packages/tools/examples/generateImageFromTime/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts similarity index 95% rename from packages/tools/examples/generateImageFromTime/index.ts rename to packages/tools/examples/generateImageFromTimeData/index.ts index 7807dee07..dad9623ad 100644 --- a/packages/tools/examples/generateImageFromTime/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -4,9 +4,6 @@ import { Enums, volumeLoader, getRenderingEngine, - CONSTANTS, - utilities, - cache, } from '@cornerstonejs/core'; import { initDemo, @@ -21,7 +18,6 @@ import * as cornerstoneTools from '@cornerstonejs/tools'; import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; const { - segmentation, SegmentationDisplayTool, utilities: csToolsUtilities, Enums: csToolsEnums, @@ -51,14 +47,10 @@ setTitleAndDescription( const size = '500px'; const content = document.getElementById('content'); const viewportGrid = document.createElement('div'); -// const element = document.createElement('div'); viewportGrid.style.display = 'flex'; viewportGrid.style.flexDirection = 'row'; -// element.id = 'cornerstone-element'; -// element.style.width = '500px'; -// element.style.height = '500px'; const element1 = document.createElement('div'); const element2 = document.createElement('div'); element1.style.width = size; @@ -79,12 +71,10 @@ let volumeForButton; addButtonToToolbar({ title: 'Generate Image', onClick: () => { - const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTime( + const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTimeData( volumeForButton, dataOperation, - { - frameNumbers: [0, 1, 3], - } + [0, 4] ); createVolumeFromTimeData(dataInTime); }, @@ -131,7 +121,6 @@ function addTimePointSlider(volume) { // ==================================== // // Define a unique id for the volume const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix -// const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use const volumeLoaderScheme = 'cornerstoneStreamingDynamicImageVolume'; // Loader id which defines which volume loader to use const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id const computedVolumeName = 'PT_VOLUME_ID'; diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts similarity index 57% rename from packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts rename to packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts index 90900b4c7..204cdae90 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTime.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts @@ -1,24 +1,31 @@ -import { utilities, cache, Types } from '@cornerstonejs/core'; -import getDataInTime from './getDataInTime'; +import { Types } from '@cornerstonejs/core'; -function generateImageFromTime( +/** + * Gets the scalar data for a series of time frames from a 4D volume, returns an + * array of scalar data after performing AVERAGE, SUM or SUBTRACT to be used to + * create a 3D volume + * + * @param dynamicVolume4D: volume to compute time frame data from + * @param operation: operation to perform on time frame data, operations include + * SUM, AVERAGE, and SUBTRACT (can only be used with 2 time frames provided) + * @param frameNumbers: an array of frame indexs to perform the operation on, if + * left empty, all frames will be used + * @returns + */ +function generateImageFromTimeData( dynamicVolume: Types.IDynamicImageVolume, operation: string, - options: { - frameNumbers?; - maskVolumeId?; - imageCoordinate?; - } = {} + frameNumbers?: number[] ) { - const frames = options.frameNumbers || [ - ...Array(dynamicVolume.numTimePoints).keys(), - ]; + // If no time frames provided, use all time frames + const frames = frameNumbers || [...Array(dynamicVolume.numTimePoints).keys()]; const numFrames = frames.length; if (frames.length <= 1) { throw new Error('Please provide two or more time points'); } + // Gets scalar data for all time frames const typedArrays = dynamicVolume.getScalarDataArrays(); const arrayLength = typedArrays[0].length; @@ -57,4 +64,4 @@ function generateImageFromTime( return finalArray; } -export default generateImageFromTime; +export default generateImageFromTimeData; diff --git a/packages/tools/src/utilities/dynamicVolume/index.ts b/packages/tools/src/utilities/dynamicVolume/index.ts index 0f0e0e580..588a4450b 100644 --- a/packages/tools/src/utilities/dynamicVolume/index.ts +++ b/packages/tools/src/utilities/dynamicVolume/index.ts @@ -1,5 +1,5 @@ import getDataInTime from './getDataInTime'; -import generateImageFromTime from './generateImageFromTime'; +import generateImageFromTimeData from './generateImageFromTimeData'; export { getDataInTime }; -export { generateImageFromTime }; +export { generateImageFromTimeData }; From c853c4519299b6b8326667a8b5940b734a3f6bec Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 09:00:12 -0400 Subject: [PATCH 13/21] changed time frames loaded --- .../generateImageFromTimeData/index.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index dad9623ad..92e5f9d5e 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -71,10 +71,11 @@ let volumeForButton; addButtonToToolbar({ title: 'Generate Image', onClick: () => { + console.time('Function #1'); const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTimeData( volumeForButton, dataOperation, - [0, 4] + // [0, 4] ); createVolumeFromTimeData(dataInTime); }, @@ -107,6 +108,19 @@ addDropdownToToolbar({ }, }); +function addTextInputBox() { + const id = 'myText' + const title = 'Enter time frames' + const button = document.createElement('textbox'); + + button.id = id; + button.innerHTML = title; + // button.onclick = onClick; + + const container = document.getElementById('demo-toolbar'); + container.append(button); +} + function addTimePointSlider(volume) { addSliderToToolbar({ title: 'Time Point', @@ -155,6 +169,7 @@ async function createVolumeFromTimeData(dataInTime) { ]); viewport2.render(); + console.timeEnd('Function #1'); } /** @@ -192,6 +207,7 @@ async function run() { // and first ones are white or contains only a few black pixels const firstInstanceNumber = TOTAL_NUM_IMAGES - numImagesToLoad + 1; + imageIds = imageIds.filter((imageId) => { const instanceMetaData = metaDataManager.get(imageId); const instanceTag = instanceMetaData['00200013']; @@ -199,6 +215,7 @@ async function run() { return instanceNumber >= firstInstanceNumber; }); + // imageIds = imageIds.slice(0, 1175) // Instantiate a rendering engine renderingEngine = new RenderingEngine(renderingEngineId); @@ -246,6 +263,7 @@ async function run() { volumeForButton = volume; addTimePointSlider(volume); + addTextInputBox(); // Set the volume on the viewport viewport.setVolumes([ From 1cdbdacf632059181d2bfb85cc924301ca92b245 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 13:38:57 -0400 Subject: [PATCH 14/21] User can now enter in which time frames to use for operation --- .../generateImageFromTimeData/index.ts | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index 92e5f9d5e..a9cbdf9c7 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -30,6 +30,7 @@ const { ViewportType } = Enums; const renderingEngineId = 'myRenderingEngine'; const viewportId1 = 'CT_SAGITTAL_STACK'; const viewportId2 = 'COMPUTED_STACK'; +let timeFrames; const orientations = [ Enums.OrientationAxis.AXIAL, @@ -41,7 +42,7 @@ let dataOperation = operations[0]; // ======== Set up page ======== // setTitleAndDescription( '3D Volume Generation From 4D Data', - 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators from a 4D time series.' + 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\n Enter in the time frames to use for the operator separated by spaces (ex: "0 1 3 4") then press "Set Time Frames", or leave blank to use all time frames. \nNote: the index for the time frames starts at 0' ); const size = '500px'; @@ -71,11 +72,10 @@ let volumeForButton; addButtonToToolbar({ title: 'Generate Image', onClick: () => { - console.time('Function #1'); const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTimeData( volumeForButton, dataOperation, - // [0, 4] + timeFrames ); createVolumeFromTimeData(dataInTime); }, @@ -108,17 +108,27 @@ addDropdownToToolbar({ }, }); +addButtonToToolbar({ + title: 'Set Time Frames', + onClick: () => { + const x = document.getElementById("myText").value.split(' '); + for (let i = 0; i < x.length; i++){ + x[i] = ~~x[i] + } + timeFrames = x; +}}); + function addTextInputBox() { const id = 'myText' const title = 'Enter time frames' - const button = document.createElement('textbox'); - - button.id = id; - button.innerHTML = title; - // button.onclick = onClick; + const textbox = document.createElement('input'); + const value = '' + textbox.id = id; + textbox.innerHTML = title; + textbox.value = value; const container = document.getElementById('demo-toolbar'); - container.append(button); + container.append(textbox); } function addTimePointSlider(volume) { @@ -132,6 +142,7 @@ function addTimePointSlider(volume) { }, }); } + // ==================================== // // Define a unique id for the volume const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix @@ -169,7 +180,6 @@ async function createVolumeFromTimeData(dataInTime) { ]); viewport2.render(); - console.timeEnd('Function #1'); } /** @@ -198,28 +208,25 @@ async function run() { }); const MAX_NUM_TIMEPOINTS = 40; - const numTimePoints = 5; + const firstTimePoint = 10; + const lastTimePoint = 14; const NUM_IMAGES_PER_TIME_POINT = 235; const TOTAL_NUM_IMAGES = MAX_NUM_TIMEPOINTS * NUM_IMAGES_PER_TIME_POINT; - const numImagesToLoad = numTimePoints * NUM_IMAGES_PER_TIME_POINT; - - // Load the last N time points because they have a better image quality - // and first ones are white or contains only a few black pixels - const firstInstanceNumber = TOTAL_NUM_IMAGES - numImagesToLoad + 1; - + const firstInstanceNumber = (firstTimePoint - 1) * NUM_IMAGES_PER_TIME_POINT + 1; + const lastInstanceNumber = lastTimePoint * NUM_IMAGES_PER_TIME_POINT; imageIds = imageIds.filter((imageId) => { const instanceMetaData = metaDataManager.get(imageId); const instanceTag = instanceMetaData['00200013']; const instanceNumber = parseInt(instanceTag.Value[0]); - return instanceNumber >= firstInstanceNumber; + return instanceNumber >= firstInstanceNumber && instanceNumber <= lastInstanceNumber; }); - // imageIds = imageIds.slice(0, 1175) // Instantiate a rendering engine renderingEngine = new RenderingEngine(renderingEngineId); - // Create a stack viewport + + // Create a viewport const viewportInput1 = { viewportId: viewportId1, type: ViewportType.ORTHOGRAPHIC, @@ -262,8 +269,8 @@ async function run() { volume.load(); volumeForButton = volume; - addTimePointSlider(volume); addTextInputBox(); + addTimePointSlider(volume); // Set the volume on the viewport viewport.setVolumes([ From 5c8174aa0fd22fe1ffe573876eade0ab082350b9 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 13:43:08 -0400 Subject: [PATCH 15/21] cleaning up code --- .../generateImageFromTimeData/index.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index a9cbdf9c7..1269f5f2a 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -111,18 +111,19 @@ addDropdownToToolbar({ addButtonToToolbar({ title: 'Set Time Frames', onClick: () => { - const x = document.getElementById("myText").value.split(' '); - for (let i = 0; i < x.length; i++){ - x[i] = ~~x[i] + const x = document.getElementById('myText').value.split(' '); + for (let i = 0; i < x.length; i++) { + x[i] = ~~x[i]; } timeFrames = x; -}}); + }, +}); function addTextInputBox() { - const id = 'myText' - const title = 'Enter time frames' + const id = 'myText'; + const title = 'Enter time frames'; const textbox = document.createElement('input'); - const value = '' + const value = ''; textbox.id = id; textbox.innerHTML = title; textbox.value = value; @@ -212,7 +213,8 @@ async function run() { const lastTimePoint = 14; const NUM_IMAGES_PER_TIME_POINT = 235; const TOTAL_NUM_IMAGES = MAX_NUM_TIMEPOINTS * NUM_IMAGES_PER_TIME_POINT; - const firstInstanceNumber = (firstTimePoint - 1) * NUM_IMAGES_PER_TIME_POINT + 1; + const firstInstanceNumber = + (firstTimePoint - 1) * NUM_IMAGES_PER_TIME_POINT + 1; const lastInstanceNumber = lastTimePoint * NUM_IMAGES_PER_TIME_POINT; imageIds = imageIds.filter((imageId) => { @@ -220,7 +222,10 @@ async function run() { const instanceTag = instanceMetaData['00200013']; const instanceNumber = parseInt(instanceTag.Value[0]); - return instanceNumber >= firstInstanceNumber && instanceNumber <= lastInstanceNumber; + return ( + instanceNumber >= firstInstanceNumber && + instanceNumber <= lastInstanceNumber + ); }); // Instantiate a rendering engine From 4625cd03351b171763e19f01780dd7658f111136 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 14:02:22 -0400 Subject: [PATCH 16/21] fixed spelling error --- packages/tools/examples/generateImageFromTimeData/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index 1269f5f2a..ffb690aa1 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -42,7 +42,7 @@ let dataOperation = operations[0]; // ======== Set up page ======== // setTitleAndDescription( '3D Volume Generation From 4D Data', - 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\n Enter in the time frames to use for the operator separated by spaces (ex: "0 1 3 4") then press "Set Time Frames", or leave blank to use all time frames. \nNote: the index for the time frames starts at 0' + 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\nEnter the time frames to use separated by spaces (ex: "0 1 3 4") then press "Set Time Frames", or leave blank to use all time frames. \nNote: the index for the time frames starts at 0' ); const size = '500px'; From 06a5dc86fb23e91d7e9fb85f4f9e60230f6b2199 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 14:06:51 -0400 Subject: [PATCH 17/21] editing description --- packages/tools/examples/generateImageFromTimeData/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index ffb690aa1..413ddc4fe 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -42,7 +42,7 @@ let dataOperation = operations[0]; // ======== Set up page ======== // setTitleAndDescription( '3D Volume Generation From 4D Data', - 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\nEnter the time frames to use separated by spaces (ex: "0 1 3 4") then press "Set Time Frames", or leave blank to use all time frames. \nNote: the index for the time frames starts at 0' + 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\nEnter the time frames to use separated by spaces (ex: "0 1 3 4") then press "Set Time Frames". \nNote: the index for the time frames starts at 0' ); const size = '500px'; From 181cd18e92bfc29af46e0787fb3f15085baaf779 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 15:36:14 -0400 Subject: [PATCH 18/21] added Enums type, addressed comments on PR --- packages/core/src/enums/DynamicOperatorType.ts | 14 ++++++++++++++ packages/core/src/enums/index.ts | 2 ++ .../examples/generateImageFromTimeData/index.ts | 2 +- .../dynamicVolume/generateImageFromTimeData.ts | 16 +++++++++------- utils/ExampleRunner/example-info.json | 4 ++++ 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/enums/DynamicOperatorType.ts diff --git a/packages/core/src/enums/DynamicOperatorType.ts b/packages/core/src/enums/DynamicOperatorType.ts new file mode 100644 index 000000000..b36e4ecfd --- /dev/null +++ b/packages/core/src/enums/DynamicOperatorType.ts @@ -0,0 +1,14 @@ +/** + * DynamicOperatorType enum for cornerstone-render which defines the operator to use for generateImageFromTimeData. + * It can be either SUM, AVERAGE or SUBTRACT. + */ +enum DynamicOperatorType { + /** For summing the time frames. */ + SUM = 'SUM', + /** For averaging the time frames. */ + AVERAGE = 'AVERAGE', + /** For subtracting two time frames */ + SUBTRACT = 'SUBTRACT', +} + +export default DynamicOperatorType; diff --git a/packages/core/src/enums/index.ts b/packages/core/src/enums/index.ts index 2286cec95..76cc95dd9 100644 --- a/packages/core/src/enums/index.ts +++ b/packages/core/src/enums/index.ts @@ -8,6 +8,7 @@ import SharedArrayBufferModes from './SharedArrayBufferModes'; import GeometryType from './GeometryType'; import ContourType from './ContourType'; import VOILUTFunctionType from './VOILUTFunctionType'; +import DynamicOperatorType from './DynamicOperatorType'; export { Events, @@ -20,4 +21,5 @@ export { GeometryType, ContourType, VOILUTFunctionType, + DynamicOperatorType, }; diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index 413ddc4fe..358c7ac03 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -74,7 +74,7 @@ addButtonToToolbar({ onClick: () => { const dataInTime = csToolsUtilities.dynamicVolume.generateImageFromTimeData( volumeForButton, - dataOperation, + dataOperation, timeFrames ); createVolumeFromTimeData(dataInTime); diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts index 204cdae90..37a75c928 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts @@ -1,4 +1,5 @@ -import { Types } from '@cornerstonejs/core'; +import { Enums, Types } from '@cornerstonejs/core'; +import { DynamicOperatorType } from 'core/src/enums'; /** * Gets the scalar data for a series of time frames from a 4D volume, returns an @@ -31,25 +32,27 @@ function generateImageFromTimeData( const arrayLength = typedArrays[0].length; const finalArray = new Float32Array(arrayLength); - if (operation === 'SUM') { + if (operation === Enums.DynamicOperatorType.SUM) { for (let i = 0; i < numFrames; i++) { const currentArray = typedArrays[frames[i]]; for (let j = 0; j < arrayLength; j++) { finalArray[j] += currentArray[j]; } } + return finalArray; } - if (operation === 'SUBTRACT') { + if (operation === Enums.DynamicOperatorType.SUBTRACT) { if (frames.length > 2) { throw new Error('Please provide only 2 time points for subtraction.'); } for (let j = 0; j < arrayLength; j++) { - finalArray[j] += typedArrays[0][j] - typedArrays[1][j]; + finalArray[j] += typedArrays[frames[0]][j] - typedArrays[frames[1]][j]; } + return finalArray; } - if (operation === 'AVERAGE') { + if (operation === Enums.DynamicOperatorType.AVERAGE) { for (let i = 0; i < numFrames; i++) { const currentArray = typedArrays[frames[i]]; for (let j = 0; j < arrayLength; j++) { @@ -59,9 +62,8 @@ function generateImageFromTimeData( for (let k = 0; k < arrayLength; k++) { finalArray[k] = finalArray[k] / numFrames; } + return finalArray; } - - return finalArray; } export default generateImageFromTimeData; diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 379f6be95..aa11dc16f 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -251,6 +251,10 @@ "scaleOverlayTool": { "name": "Scale Overlay Tool", "description": "Demonstrates the scale overlay tool for rendering a scale on a viewport showing the real world size of the image." + }, + "generateImageFromTimeData": { + "name": "Generate 3D Volume From 4D Data", + "description": "Demostrates generating a 3D volume from 4D data using subtract, average or sum." } } } From 4a8d64257c263120592458f3ff29a168e6312d4d Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 15:50:00 -0400 Subject: [PATCH 19/21] yarn update api --- common/reviews/api/core.api.md | 13 ++++++++++++- .../api/streaming-image-volume-loader.api.md | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 65a41688d..51fe1c8f5 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -515,6 +515,16 @@ interface CustomEvent_2 extends Event { // @public (undocumented) const deepMerge: (target?: {}, source?: {}, optionsArgument?: any) => any; +// @public (undocumented) +enum DynamicOperatorType { + // (undocumented) + AVERAGE = "AVERAGE", + // (undocumented) + SUBTRACT = "SUBTRACT", + // (undocumented) + SUM = "SUM" +} + // @public (undocumented) type ElementDisabledEvent = CustomEvent_2; @@ -546,7 +556,8 @@ declare namespace Enums { SharedArrayBufferModes, GeometryType, ContourType, - VOILUTFunctionType + VOILUTFunctionType, + DynamicOperatorType } } export { Enums } diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index a23c11239..4434588d7 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -400,6 +400,13 @@ interface CustomEvent_2 extends Event { ): void; } +// @public +enum DynamicOperatorType { + AVERAGE = 'AVERAGE', + SUBTRACT = 'SUBTRACT', + SUM = 'SUM', +} + // @public type ElementDisabledEvent = CustomEvent_2; From f05cb8f299c7953bfce6154ccf2df5ac35b19553 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 16:00:24 -0400 Subject: [PATCH 20/21] yarn build --- .../src/utilities/dynamicVolume/generateImageFromTimeData.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts b/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts index 37a75c928..569d47ca8 100644 --- a/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts +++ b/packages/tools/src/utilities/dynamicVolume/generateImageFromTimeData.ts @@ -1,5 +1,4 @@ import { Enums, Types } from '@cornerstonejs/core'; -import { DynamicOperatorType } from 'core/src/enums'; /** * Gets the scalar data for a series of time frames from a 4D volume, returns an From bcc4aab0b43a1abf919c8eff9b8dc7c4095a65ad Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 28 Mar 2023 16:28:38 -0400 Subject: [PATCH 21/21] added zoom, pan and stack scroll tools, time frames now separated by commas --- .../generateImageFromTimeData/index.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/tools/examples/generateImageFromTimeData/index.ts b/packages/tools/examples/generateImageFromTimeData/index.ts index 358c7ac03..e6f893943 100644 --- a/packages/tools/examples/generateImageFromTimeData/index.ts +++ b/packages/tools/examples/generateImageFromTimeData/index.ts @@ -21,7 +21,12 @@ const { SegmentationDisplayTool, utilities: csToolsUtilities, Enums: csToolsEnums, + PanTool, + StackScrollMouseWheelTool, + ZoomTool } = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; // This is for debugging purposes console.warn( 'Click on index.ts to open source code for this example --------->' @@ -42,7 +47,7 @@ let dataOperation = operations[0]; // ======== Set up page ======== // setTitleAndDescription( '3D Volume Generation From 4D Data', - 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\nEnter the time frames to use separated by spaces (ex: "0 1 3 4") then press "Set Time Frames". \nNote: the index for the time frames starts at 0' + 'Generates a 3D volume using the SUM, AVERAGE, or SUBTRACT operators for a 4D time series.\nEnter the time frames to use separated by commas (ex: 0,1,3,4) then press "Set Time Frames". \nNote: the index for the time frames starts at 0' ); const size = '500px'; @@ -111,7 +116,7 @@ addDropdownToToolbar({ addButtonToToolbar({ title: 'Set Time Frames', onClick: () => { - const x = document.getElementById('myText').value.split(' '); + const x = document.getElementById('myText').value.split(','); for (let i = 0; i < x.length; i++) { x[i] = ~~x[i]; } @@ -191,11 +196,32 @@ async function run() { await initDemo(); // Add tools to Cornerstone3D cornerstoneTools.addTool(SegmentationDisplayTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(ZoomTool); // Define tool groups to add the segmentation display tool to const toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId); toolGroup.addTool(SegmentationDisplayTool.toolName); + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + toolGroup.addTool(ZoomTool.toolName); toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, // Middle Click + }, + ], + }); + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, // Right Click + }, + ], + }); const { metaDataManager } = cornerstoneWADOImageLoader.wadors;