feat(imageLoader):Add an nth image loader strategy (#10) (#3044)

* feat(imageLoader):Add an nth image loader strategy (#10)

* Updates for PR for nth menu

* performance:Prior implementation was O(n^2), taking about 50 ms

* feat:Nth image loader, added docs as requested
This commit is contained in:
Bill Wallace authored and GitHub committed 2022-12-29 11:00:13 -05:00
1 parent 9a03bde0ae
commit 00e797801d
8 files changed
+212 -3

No files matched your search

@@ -8,6 +8,7 @@ const mpr = {
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
+2
View File
@@ -27,6 +27,7 @@ import { connectToolsToMeasurementService } from './initMeasurementService';
import callInputDialog from './utils/callInputDialog';
import initCineService from './initCineService';
import interleaveCenterLoader from './utils/interleaveCenterLoader';
import nthLoader from './utils/nthLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom';
const cs3DToolsEvents = Enums.Events;
@@ -121,6 +122,7 @@ export default async function init({
'interleaveTopToBottom',
interleaveTopToBottom
);
HangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
imageLoader.registerImageLoader(
'streaming-wadors',
@@ -136,7 +136,11 @@ class CornerstoneViewportService implements IViewportService {
public destroy() {
this._removeResizeObserver();
this.viewportGridResizeObserver = null;
this.renderingEngine?.destroy?.();
try {
this.renderingEngine?.destroy?.();
} catch (e) {
console.warn('Rendering engine not destroyed', e);
}
this.viewportsDisplaySets.clear();
this.renderingEngine = null;
cache.purgeCache();
@@ -0,0 +1,44 @@
/**
* Returns a re-ordered array consisting of, in order:
* 1. First few objects
* 2. Center objects
* 3. Last few objects
* 4. nth Objects (n=7), set 2
* 5. nth Objects set 5,
* 6. Remaining objects
* What this does is return the first/center/start objects, as those
* are often used first, then a selection of objects scattered over the
* instances in order to allow making requests over a set of image instances.
*
* @param {[]} imageIds
* @returns [] reordered to be an nth selection
*/
export default function getNthFrames(imageIds) {
const frames = [[], [], [], [], []];
const centerStart = imageIds.length / 2 - 3;
const centerEnd = centerStart + 6;
for (let i = 0; i < imageIds.length; i++) {
if (
i < 2 ||
i > imageIds.length - 4 ||
(i > centerStart && i < centerEnd)
) {
frames[0].push(imageIds[i]);
} else if (i % 7 === 2) {
frames[1].push(imageIds[i]);
} else if (i % 7 === 5) {
frames[2].push(imageIds[i]);
} else {
frames[(i % 2) + 3].push(imageIds[i]);
}
}
const ret = [
...frames[0],
...frames[1],
...frames[2],
...frames[3],
...frames[4],
];
return ret;
}
@@ -0,0 +1,26 @@
/**
* Interleave the items from all the lists so that the first items are first
* in the returned list, the second items are next etc.
* Does this in a O(n) fashion, and return lists[0] if there is only one list.
*
* @param {[]} lists
* @returns [] reordered to be breadth first traversal of lists
*/
export default function interleave(lists) {
if (!lists || !lists.length) return [];
if (lists.length === 1) return lists[0];
console.time('interleave');
const useLists = [...lists];
const ret = [];
for (let i = 0; useLists.length > 0; i++) {
for (const list of useLists) {
if (i >= list.length) {
useLists.splice(useLists.indexOf(list), 1);
continue;
}
ret.push(list[i]);
}
}
console.timeEnd('interleave');
return ret;
}
@@ -0,0 +1,121 @@
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
import getNthFrames from './getNthFrames';
import interleave from './interleave';
// Map of volumeId and SeriesInstanceId
const volumeIdMapsToLoad = new Map<string, string>();
const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
/**
* This function caches the volumeUIDs until all the volumes inside the
* hanging protocol are initialized. Then it goes through the requests and
* chooses a sub-selection starting the the first few objects, center objects
* and last objects, and then the remaining nth images until all instances are
* retrieved. This causes the image to have a progressive load order and looks
* visually much better.
* @param {Object} props image loading properties from Cornerstone ViewportService
*/
export default function interleaveNthLoader({
data: { viewportId, volumeInputArray },
displaySetsMatchDetails,
viewportMatchDetails: matchDetails,
}) {
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
// Based on the volumeInputs store the volumeIds and SeriesInstanceIds
// to keep track of the volumes being loaded
for (const volumeInput of volumeInputArray) {
const { volumeId } = volumeInput;
const volume = cache.getVolume(volumeId);
if (!volume) {
return;
}
// if the volumeUID is not in the volumeUIDs array, add it
if (!volumeIdMapsToLoad.has(volumeId)) {
const { metadata } = volume;
volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID);
}
}
/**
* The following is checking if all the viewports that were matched in the HP has been
* successfully created their cornerstone viewport or not. Todo: This can be
* improved by not checking it, and as soon as the matched DisplaySets have their
* volume loaded, we start the loading, but that comes at the cost of viewports
* not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one
* pt viewport are created we have a guarantee that the volumes are created in the cache
* but the rest of the viewports (fusion, mip etc.) are not created yet. So
* we can't initiate setting the volumes for those viewports. One solution can be
* to add an event when a viewport is created (not enabled element event) and then
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
return;
}
// Check if all the matched volumes are loaded
for (const [_, details] of displaySetsMatchDetails.entries()) {
const { SeriesInstanceUID } = details;
// HangingProtocol has matched, but don't have all the volumes created yet, so return
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
return;
}
}
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
// get volumes from cache
const volumes = volumeIds.map(volumeId => {
return cache.getVolume(volumeId);
});
// iterate over all volumes, and get their imageIds, and interleave
// the imageIds and save them in AllRequests for later use
const originalRequests = volumes
.map(volume => volume.getImageLoadRequests())
.filter(requests => requests?.[0]?.imageId);
const orderedRequests = originalRequests.map(request =>
getNthFrames(request)
);
// set the finalRequests to the imageLoadPoolManager
const finalRequests = interleave(orderedRequests);
const requestType = Enums.RequestType.Prefetch;
const priority = 0;
finalRequests.forEach(
({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => {
const callLoadImageBound = callLoadImage.bind(
null,
imageId,
imageIdIndex,
options
);
imageLoadPoolManager.addRequest(
callLoadImageBound,
requestType,
additionalDetails,
priority
);
}
);
// clear the volumeIdMapsToLoad
volumeIdMapsToLoad.clear();
// copy the viewportIdVolumeInputArrayMap
const viewportIdVolumeInputArrayMapCopy = new Map(
viewportIdVolumeInputArrayMap
);
// reset the viewportIdVolumeInputArrayMap
viewportIdVolumeInputArrayMap.clear();
return viewportIdVolumeInputArrayMapCopy;
}