diff --git a/extensions/cornerstone/src/getHangingProtocolModule.ts b/extensions/cornerstone/src/getHangingProtocolModule.ts index 2d37b96a1..c13a45d50 100644 --- a/extensions/cornerstone/src/getHangingProtocolModule.ts +++ b/extensions/cornerstone/src/getHangingProtocolModule.ts @@ -8,6 +8,7 @@ const mpr = { availableTo: {}, editableBy: {}, protocolMatchingRules: [], + imageLoadStrategy: 'nth', displaySetSelectors: { mprDisplaySet: { seriesMatchingRules: [ diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index fc19e384e..3b338a1b9 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -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', diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 82d53805f..227ff111f 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -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(); diff --git a/extensions/cornerstone/src/utils/getNthFrames.js b/extensions/cornerstone/src/utils/getNthFrames.js new file mode 100644 index 000000000..09bfc0b1e --- /dev/null +++ b/extensions/cornerstone/src/utils/getNthFrames.js @@ -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; +} diff --git a/extensions/cornerstone/src/utils/interleave.js b/extensions/cornerstone/src/utils/interleave.js new file mode 100644 index 000000000..05fe2cab1 --- /dev/null +++ b/extensions/cornerstone/src/utils/interleave.js @@ -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; +} diff --git a/extensions/cornerstone/src/utils/nthLoader.ts b/extensions/cornerstone/src/utils/nthLoader.ts new file mode 100644 index 000000000..a34066eb2 --- /dev/null +++ b/extensions/cornerstone/src/utils/nthLoader.ts @@ -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(); +const viewportIdVolumeInputArrayMap = new Map(); + +/** + * 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; +} diff --git a/platform/docs/docs/deployment/index.md b/platform/docs/docs/deployment/index.md index 384aefc77..824a645de 100644 --- a/platform/docs/docs/deployment/index.md +++ b/platform/docs/docs/deployment/index.md @@ -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. diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md index 57eebc239..625215b04 100644 --- a/platform/docs/docs/platform/extensions/modules/hpModule.md +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -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.