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 2022-12-29 11:00:13 -05:00 committed by GitHub
parent 9a03bde0ae
commit 00e797801d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 212 additions and 3 deletions

View File

@ -8,6 +8,7 @@ const mpr = {
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [

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',

View File

@ -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();

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -97,12 +97,12 @@ support it yet, but it is gaining wider adoption.
If you have an existing archive and intend to host the OHIF Viewer at the same
domain name as your archive, then connecting the two is as simple as following
the steps layed out in our
[Configuration Essentials Guide](./../configuration/index.md).
[Configuration Essentials Guide](./../configuration/configurationFiles.md).
#### What if I don't have an imaging archive?
We provide some guidance on configuring a local image archive in our
[Data Source Essentials](./../configuration/index.md#set-up-a-local-DICOM-server)
[Data Source Essentials](./../configuration/dataSources/introduction.md)
guide. Hosting an archive remotely is a little trickier. You can check out some
of our [advanced recipes](#recipes) for modeled setups that may work for you.

View File

@ -219,6 +219,17 @@ matching), or provides and array of ids which will
make the ProtocolEngine to choose the best matching protocol (based on
protocolMatching rules, which is next section).
### imageLoadStrategy
The image load strategy specifies a function (by name) containing logic to re-order
the image load requests. This allows loading images viewed earlier to be done
sooner than those loaded later. The available strategies are:
* interleaveTopToBottom to start at the top and work towards the bottom, for all series being loaded
* interleaveCenter is like top to bottom but starts at the center
* nth is a strategy that loads every nth instance, starting with the center
and end points, and then filling in progressively all along the image. This results in partial
image view very quickly.
### protocolMatchingRules
A list of criteria for the protocol along with the provided points for ranking.