From 5e02fd85201023f3d30302d4d1e438e0a3a09e2a Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Mon, 10 Aug 2020 12:07:56 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Switch=20to=20using?= =?UTF-8?q?=20cornerstone=20prefetchers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switched to using cornerstone prefetchers so we don't send 1000 requests at the same time. This is a breaking change to the API as we switch to a sub model for the events. BREAKING CHANGE: The imageDataObject no-longer has insertPixelDataPromises, insteads the consumer may subscribe to an imageDataObject's onPixelDataInserted and/or onAllPixelDataInserted. --- examples/VTKCornerstonePaintingSyncExample.js | 8 +- examples/VTKCrosshairsExample.js | 6 +- examples/VTKFusionExample.js | 40 ++++----- examples/VTKLoadImageDataExample.js | 27 +++--- examples/VTKVolumeRenderingExample.js | 36 ++++---- package.json | 3 +- src/lib/getImageData.js | 33 +++++++ src/lib/loadImageData.js | 87 ++++++++++--------- 8 files changed, 142 insertions(+), 98 deletions(-) diff --git a/examples/VTKCornerstonePaintingSyncExample.js b/examples/VTKCornerstonePaintingSyncExample.js index 0fa6ab27..7f4bdf6a 100644 --- a/examples/VTKCornerstonePaintingSyncExample.js +++ b/examples/VTKCornerstonePaintingSyncExample.js @@ -145,7 +145,8 @@ class VTKCornerstonePaintingSyncExample extends Component { }; loadImageData(imageDataObject); - Promise.all(imageDataObject.insertPixelDataPromises).then(() => { + + const onAllPixelDataInsertedCallback = () => { const { actor } = createActorMapper(imageDataObject.vtkImageData); const rgbTransferFunction = actor @@ -166,7 +167,9 @@ class VTKCornerstonePaintingSyncExample extends Component { globalOpacity: segmentationModule.configuration.fillAlpha, outlineThickness: segmentationModule.configuration.outlineThickness, }); - }); + }; + + imageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback); }); } @@ -321,6 +324,7 @@ class VTKCornerstonePaintingSyncExample extends Component { colorLUT, globalOpacity, outlineThickness, + segmentsDefaultProperties: [], visible: true, renderOutline: true, }} diff --git a/examples/VTKCrosshairsExample.js b/examples/VTKCrosshairsExample.js index 5f8957f1..9f591ea1 100644 --- a/examples/VTKCrosshairsExample.js +++ b/examples/VTKCrosshairsExample.js @@ -77,7 +77,7 @@ class VTKCrosshairsExample extends Component { const ctImageDataObject = loadDataset(ctImageIds, 'ctDisplaySet'); - Promise.all(ctImageDataObject.insertPixelDataPromises).then(() => { + const onAllPixelDataInsertedCallback = () => { const ctImageData = ctImageDataObject.vtkImageData; const range = ctImageData @@ -97,7 +97,9 @@ class VTKCrosshairsExample extends Component { this.setState({ volumes: [ctVol], }); - }); + }; + + ctImageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback); } storeApi = viewportIndex => { diff --git a/examples/VTKFusionExample.js b/examples/VTKFusionExample.js index 334e88ae..d549660d 100644 --- a/examples/VTKFusionExample.js +++ b/examples/VTKFusionExample.js @@ -332,34 +332,32 @@ class VTKFusionExample extends Component { loadImageData(imageDataObject); - const { insertPixelDataPromises } = imageDataObject; + const numberOfFrames = imageIds.length; - const numberOfFrames = insertPixelDataPromises.length; - - // TODO -> Maybe the component itself should do this. - insertPixelDataPromises.forEach(promise => { - promise.then(numberProcessed => { - const percentComplete = Math.floor( - (numberProcessed * 100) / numberOfFrames - ); + const onPixelDataInsertedCallback = numberProcessed => { + const percentComplete = Math.floor( + (numberProcessed * 100) / numberOfFrames + ); - if (this.state.percentComplete !== percentComplete) { - const stateUpdate = {}; + if (this.state.percentComplete !== percentComplete) { + const stateUpdate = {}; - stateUpdate[percentageCompleteStateName] = percentComplete; + stateUpdate[percentageCompleteStateName] = percentComplete; - this.setState(stateUpdate); - } + this.setState(stateUpdate); + } - if (percentComplete % 20 === 0) { - this.rerenderAll(); - } - }); - }); + if (percentComplete % 20 === 0) { + this.rerenderAll(); + } + }; - Promise.all(insertPixelDataPromises).then(() => { + const onAllPixelDataInsertedCallback = () => { this.rerenderAll(); - }); + }; + + imageDataObject.onPixelDataInserted(onPixelDataInsertedCallback); + imageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback); return imageDataObject; } diff --git a/examples/VTKLoadImageDataExample.js b/examples/VTKLoadImageDataExample.js index bd69fa98..5b70dea3 100644 --- a/examples/VTKLoadImageDataExample.js +++ b/examples/VTKLoadImageDataExample.js @@ -109,16 +109,15 @@ class VTKLoadImageDataExample extends Component { loadImageData(imageDataObject); - const { insertPixelDataPromises } = imageDataObject; - insertPixelDataPromises.forEach(promise => { - promise.then(numberProcessed => { - const percentComplete = Math.floor( - (numberProcessed * 100) / insertPixelDataPromises.length - ); - - console.log(`Processing: ${percentComplete}%`); - }); - }); + const onPixelDataInsertedCallback = numberProcessed => { + const percentComplete = Math.floor( + (numberProcessed * 100) / imageIds.length + ); + + console.log(`Processing: ${percentComplete}%`); + }; + + imageDataObject.onPixelDataInserted(onPixelDataInsertedCallback); const { actor } = createActorMapper(imageDataObject.vtkImageData); @@ -154,9 +153,11 @@ class VTKLoadImageDataExample extends Component { istyle.setSliceOrientation(slicePlaneNormal, sliceViewUp); - this.imageDataObject.insertPixelDataPromises.forEach(promise => { - promise.then(() => renderWindow.render()); - }); + const onPixelDataInsertedCallback = () => { + renderWindow.render(); + }; + + this.imageDataObject.onPixelDataInserted(onPixelDataInsertedCallback); renderWindow.render(); }; diff --git a/examples/VTKVolumeRenderingExample.js b/examples/VTKVolumeRenderingExample.js index 44bbfa87..1445e47e 100644 --- a/examples/VTKVolumeRenderingExample.js +++ b/examples/VTKVolumeRenderingExample.js @@ -299,30 +299,28 @@ class VTKFusionExample extends Component { loadImageData(imageDataObject); - const { insertPixelDataPromises } = imageDataObject; + const numberOfFrames = imageIds.length; - const numberOfFrames = insertPixelDataPromises.length; + const onPixelDataInsertedCallback = numberProcessed => { + const percentComplete = Math.floor( + (numberProcessed * 100) / numberOfFrames + ); - // TODO -> Maybe the component itself should do this. - insertPixelDataPromises.forEach(promise => { - promise.then(numberProcessed => { - const percentComplete = Math.floor( - (numberProcessed * 100) / numberOfFrames - ); + if (this.state.percentComplete !== percentComplete) { + this.setState({ percentComplete }); + } - if (this.state.percentComplete !== percentComplete) { - this.setState({ percentComplete }); - } + if (percentComplete % 20 === 0) { + this.rerenderAll(); + } + }; - if (percentComplete % 20 === 0) { - this.rerenderAll(); - } - }); - }); - - Promise.all(insertPixelDataPromises).then(() => { + const onAllPixelDataInsertedCallback = () => { this.rerenderAll(); - }); + }; + + imageDataObject.onPixelDataInserted(onPixelDataInsertedCallback); + imageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback); return imageDataObject; } diff --git a/package.json b/package.json index 5c765015..eb360a2b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "peerDependencies": { "react": "^16.8.6", "react-dom": "^16.8.6", - "vtk.js": "^11.14.0" + "vtk.js": "^11.14.0", + "cornerstone-tools": "^4.11.0" }, "dependencies": { "date-fns": "^2.2.1", diff --git a/src/lib/getImageData.js b/src/lib/getImageData.js index 0d8b1ea4..2c1493b9 100644 --- a/src/lib/getImageData.js +++ b/src/lib/getImageData.js @@ -75,6 +75,27 @@ export default function getImageData(imageIds, displaySetInstanceUid) { imageData.setOrigin(...origin); imageData.getPointData().setScalars(scalarArray); + const _publishPixelDataInserted = count => { + imageDataObject.subscriptions.onPixelDataInserted.forEach(callback => { + callback(count); + }); + }; + + const _publishAllPixelDataInseted = () => { + imageDataObject.subscriptions.onAllPixelDataInserted.forEach(callback => { + callback(); + }); + imageDataObject.isLoading = false; + imageDataObject.loaded = true; + imageDataObject.vtkImageData.modified(); + + // Remove all subscriptions on completion. + imageDataObject.subscriptions = { + onPixelDataInserted: [], + onAllPixelDataInserted: [], + }; + }; + const imageDataObject = { imageIds, metaData0, @@ -87,6 +108,18 @@ export default function getImageData(imageIds, displaySetInstanceUid) { metaDataMap, sortedDatasets, loaded: false, + subscriptions: { + onPixelDataInserted: [], + onAllPixelDataInserted: [], + }, + onPixelDataInserted: callback => { + imageDataObject.subscriptions.onPixelDataInserted.push(callback); + }, + onAllPixelDataInserted: callback => { + imageDataObject.subscriptions.onAllPixelDataInserted.push(callback); + }, + _publishPixelDataInserted, + _publishAllPixelDataInseted, }; imageDataCache.set(displaySetInstanceUid, imageDataObject); diff --git a/src/lib/loadImageData.js b/src/lib/loadImageData.js index 87265cf4..388a7d36 100644 --- a/src/lib/loadImageData.js +++ b/src/lib/loadImageData.js @@ -1,4 +1,5 @@ import cornerstone from 'cornerstone-core'; +import { requestPoolManager } from 'cornerstone-tools'; import insertSlice from './data/insertSlice.js'; import getPatientWeightAndCorrectedDose from './data/getPatientWeightAndCorrectedDose.js'; @@ -16,7 +17,7 @@ export default function loadImageDataProgressively(imageDataObject) { metaDataMap, sortedDatasets, } = imageDataObject; - const loadImagePromises = imageIds.map(cornerstone.loadAndCacheImage); + const imageId0 = imageIds[0]; const seriesModule = cornerstone.metaData.get( @@ -55,60 +56,66 @@ export default function loadImageDataProgressively(imageDataObject) { let reRenderTarget = reRenderFraction; const insertPixelData = image => { - return new Promise(resolve => { - const { imagePositionPatient } = metaDataMap.get(image.imageId); + const { imagePositionPatient } = metaDataMap.get(image.imageId); - const sliceIndex = sortedDatasets.findIndex( - dataset => dataset.imagePositionPatient === imagePositionPatient - ); + const sliceIndex = sortedDatasets.findIndex( + dataset => dataset.imagePositionPatient === imagePositionPatient + ); - const { max, min } = insertSlice( - vtkImageData, - sliceIndex, - image, - modality, - modalitySpecificScalingParameters - ); + const { max, min } = insertSlice( + vtkImageData, + sliceIndex, + image, + modality, + modalitySpecificScalingParameters + ); - if (max > range.max) { - range.max = max; - } + if (max > range.max) { + range.max = max; + } - if (min < range.min) { - range.min = min; - } + if (min < range.min) { + range.min = min; + } - const dataArray = vtkImageData.getPointData().getScalars(); + const dataArray = vtkImageData.getPointData().getScalars(); - dataArray.setRange(range, 1); - numberProcessed++; + dataArray.setRange(range, 1); + numberProcessed++; - if (numberProcessed > reRenderTarget) { - reRenderTarget += reRenderFraction; + if (numberProcessed > reRenderTarget) { + reRenderTarget += reRenderFraction; - vtkImageData.modified(); + vtkImageData.modified(); + } - // rerender - } + imageDataObject._publishPixelDataInserted(numberProcessed); - resolve(numberProcessed); - }); + if (numberProcessed === numberOfFrames) { + // Done loading, publish complete and remove all subscriptions. + imageDataObject._publishAllPixelDataInseted(); + } }; - const insertPixelDataPromises = []; - - loadImagePromises.forEach(promise => { - const insertPixelDataPromise = promise.then(insertPixelData); + prefetchImageIds(imageIds, imageDataObject, insertPixelData); +} - insertPixelDataPromises.push(insertPixelDataPromise); - }); +const requestType = 'prefetch'; +const preventCache = false; - Promise.all(insertPixelDataPromises).then(() => { - imageDataObject.isLoading = false; - imageDataObject.loaded = true; +function prefetchImageIds(imageIds, imageDataObject, insertPixelData) { + const noop = () => {}; - vtkImageData.modified(); + imageIds.forEach(imageId => { + requestPoolManager.addRequest( + {}, + imageId, + requestType, + preventCache, + insertPixelData, + noop + ); }); - imageDataObject.insertPixelDataPromises = insertPixelDataPromises; + requestPoolManager.startGrabbing(); } From 4505337ff37ca3e4fc5003db6181bd7372129baf Mon Sep 17 00:00:00 2001 From: "James A. Petts" Date: Mon, 10 Aug 2020 13:28:09 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Use=20cornerstone=3D?= =?UTF-8?q?-tools=20requestPool=20to=20reduce=20pacs=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/getImageData.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/getImageData.js b/src/lib/getImageData.js index 2c1493b9..ce5afa4e 100644 --- a/src/lib/getImageData.js +++ b/src/lib/getImageData.js @@ -69,9 +69,7 @@ export default function getImageData(imageIds, displaySetInstanceUid) { imageData.setDimensions(xVoxels, yVoxels, zVoxels); imageData.setSpacing(xSpacing, ySpacing, zSpacing); - imageData.setDirection(direction); - imageData.setOrigin(...origin); imageData.getPointData().setScalars(scalarArray);