feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182)

This commit is contained in:
Bill Wallace authored and GitHub committed 2024-08-15 14:36:53 -04:00
1 parent b12e0211d4
commit 7599ec9421
65 files changed
+781 -513

No files matched your search

@@ -190,6 +190,15 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
params
);
},
/**
* Provide direct access to the dicom web client for certain use cases
* where the dicom web client is used by an external library such as the
* microscopy viewer.
* Note this instance only needs to support the wado queries, and may not
* support any QIDO or STOW operations.
*/
getWadoDicomWebClient: () => wadoDicomWebClient,
bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
qidoDicomWebClient.headers = getAuthrorizationHeader();
const options = {
@@ -373,6 +382,32 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
dicomWebConfig
);
/**
* Adds the retrieve bulkdata function to naturalized DICOM data.
* This is done recursively, for sub-sequences.
*/
const addRetrieveBulkDataNaturalized = (naturalized, instance = naturalized) => {
for (const key of Object.keys(naturalized)) {
const value = naturalized[key];
if (Array.isArray(value) && typeof value[0] === 'object') {
// Fix recursive values
value.forEach(child => addRetrieveBulkDataNaturalized(child, instance));
continue;
}
// The value.Value will be set with the bulkdata read value
// in which case it isn't necessary to re-read this.
if (value && value.BulkDataURI && !value.Value) {
// handle the scenarios where bulkDataURI is relative path
fixBulkDataURI(value, instance, dicomWebConfig);
// Provide a method to fetch bulkdata
value.retrieveBulkData = retrieveBulkData.bind(qidoDicomWebClient, value);
}
}
return naturalized;
};
/**
* naturalizes the dataset, and adds a retrieve bulkdata method
* to any values containing BulkDataURI.
@@ -387,48 +422,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
return naturalized;
}
Object.keys(naturalized).forEach(key => {
const value = naturalized[key];
// The value.Value will be set with the bulkdata read value
// in which case it isn't necessary to re-read this.
if (value && value.BulkDataURI && !value.Value) {
// handle the scenarios where bulkDataURI is relative path
fixBulkDataURI(value, naturalized, dicomWebConfig);
// Provide a method to fetch bulkdata
value.retrieveBulkData = (options = {}) => {
const { mediaType } = options;
const useOptions = {
// The bulkdata fetches work with either multipart or
// singlepart, so set multipart to false to let the server
// decide which type to respond with.
multipart: false,
BulkDataURI: value.BulkDataURI,
// The study instance UID is required if the bulkdata uri
// is relative - that isn't disallowed by DICOMweb, but
// isn't well specified in the standard, but is needed in
// any implementation that stores static copies of the metadata
StudyInstanceUID: naturalized.StudyInstanceUID,
mediaTypes: mediaType
? [{ mediaType }, { mediaType: 'application/octet-stream' }]
: undefined,
...options,
};
return qidoDicomWebClient.retrieveBulkData(useOptions).then(val => {
// There are DICOM PDF cases where the first ArrayBuffer in the array is
// the bulk data and DICOM video cases where the second ArrayBuffer is
// the bulk data. Here we play it safe and do a find.
const ret =
(val instanceof Array && val.find(arrayBuffer => arrayBuffer?.byteLength)) ||
undefined;
value.Value = ret;
return ret;
});
};
}
});
return naturalized;
return addRetrieveBulkDataNaturalized(naturalized);
};
// Async load series, store as retrieved
@@ -558,4 +552,34 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
return IWebApiDataSource.create(implementation);
}
/**
* A bindable function that retrieves the bulk data against this as the
* dicomweb client, and on the given value element.
*
* @param value - a bind value that stores the retrieve value to short circuit the
* next retrieve instance.
* @param options - to allow specifying the content type.
*/
function retrieveBulkData(value, options = {}) {
const { mediaType } = options;
const useOptions = {
// The bulkdata fetches work with either multipart or
// singlepart, so set multipart to false to let the server
// decide which type to respond with.
multipart: false,
BulkDataURI: value.BulkDataURI,
mediaTypes: mediaType ? [{ mediaType }, { mediaType: 'application/octet-stream' }] : undefined,
...options,
};
return this.retrieveBulkData(useOptions).then(val => {
// There are DICOM PDF cases where the first ArrayBuffer in the array is
// the bulk data and DICOM video cases where the second ArrayBuffer is
// the bulk data. Here we play it safe and do a find.
const ret =
(val instanceof Array && val.find(arrayBuffer => arrayBuffer?.byteLength)) || undefined;
value.Value = ret;
return ret;
});
}
export { createDicomWebApi };
@@ -0,0 +1,78 @@
import { fixBulkDataURI } from './fixBulkDataURI';
function isPrimitive(v: any) {
return !(typeof v == 'object' || Array.isArray(v));
}
const vrNumerics = new Set([
'DS',
'FL',
'FD',
'IS',
'OD',
'OF',
'OL',
'OV',
'SL',
'SS',
'SV',
'UL',
'US',
'UV',
]);
/**
* Specialized for DICOM JSON format dataset cleaning.
* @param obj
* @returns
*/
export function cleanDenaturalizedDataset(
obj: any,
options?: {
StudyInstanceUID: string;
SeriesInstanceUID: string;
dataSourceConfig: unknown;
}
): any {
if (Array.isArray(obj)) {
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
return newAry;
}
if (isPrimitive(obj)) {
return obj;
}
Object.keys(obj).forEach(key => {
if (obj[key].Value === null && obj[key].vr) {
delete obj[key].Value;
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
if (options?.dataSourceConfig) {
// Not needed unless data source is directly used for loading data.
fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
}
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
// prevent mixed-content blockage
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
}
delete obj[key].Value;
} else if (vrNumerics.has(obj[key].vr)) {
obj[key].Value = obj[key].Value.map(v => +v);
} else {
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
}
}
});
return obj;
}
/**
* This is required to make the denaturalized data transferrable when it has
* added proxy values.
*/
export function transferDenaturalizedDataset(dataset) {
const noNull = cleanDenaturalizedDataset(dataset);
return JSON.parse(JSON.stringify(noNull));
}
@@ -23,6 +23,8 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
let { BulkDataURI } = value;
const { bulkDataURI: uriConfig = {} } = dicomWebConfig;
BulkDataURI = uriConfig.transform?.(BulkDataURI) || BulkDataURI;
// Handle incorrectly prefixed origins
const { startsWith, prefixWith = '' } = uriConfig;
if (startsWith && BulkDataURI.startsWith(startsWith)) {
@@ -36,7 +38,7 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
if (
BulkDataURI.startsWith('series/') ||
BulkDataURI.startsWith('bulkdata/') ||
(uriConfig.relativeResolution === 'studies' && isInstanceStart)
(uriConfig.relativeResolution === 'studies' && !isInstanceStart)
) {
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/${BulkDataURI}`;
} else if (
@@ -49,7 +51,6 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
return;
}
// in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http
// in the beginning and the first character is /) There are two scenarios, whether the wado root
// is absolute or relative. In case of absolute, we need to prepend the wado root to the bulkdata
@@ -0,0 +1,12 @@
/**
* Fix multi-valued keys so that those which are strings split by
* a backslash are returned as arrays.
*/
export function fixMultiValueKeys(naturalData, keys = ['ImageType']) {
for (const key of keys) {
if (typeof naturalData[key] === 'string') {
naturalData[key] = naturalData[key].split('\\');
}
}
return naturalData;
}
@@ -1,3 +1,9 @@
import { fixBulkDataURI } from './fixBulkDataURI';
import {
cleanDenaturalizedDataset,
transferDenaturalizedDataset,
} from './cleanDenaturalizedDataset';
export { fixBulkDataURI };
export { fixMultiValueKeys } from './fixMultiValueKeys';
export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset };
@@ -1,12 +1,12 @@
import { isImage } from '@ohif/core/src/utils/isImage';
import sopClassDictionary from '@ohif/core/src/utils/sopClassDictionary';
import ImageSet from '@ohif/core/src/classes/ImageSet';
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
import { utils, classes } from '@ohif/core';
import { id } from './id';
import getDisplaySetMessages from './getDisplaySetMessages';
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler';
const { isImage, sopClassDictionary, isDisplaySetReconstructable } = utils;
const { ImageSet } = classes;
const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
const sopClassHandlerName = 'stack';
@@ -178,7 +178,6 @@ function getDisplaySetsFromSeries(instances) {
displaySet.setAttributes({
sopClassUids,
isClip: true,
numImageFrames: instance.NumberOfFrames,
instanceNumber: instance.InstanceNumber,
acquisitionDatetime: instance.AcquisitionDateTime,
@@ -254,7 +253,8 @@ const sopClassUids = [
sopClassDictionary.OphthalmicPhotography8BitImageStorage,
sopClassDictionary.OphthalmicPhotography16BitImageStorage,
sopClassDictionary.OphthalmicTomographyImageStorage,
sopClassDictionary.VLWholeSlideMicroscopyImageStorage,
// Handled by another sop class module
// sopClassDictionary.VLWholeSlideMicroscopyImageStorage,
sopClassDictionary.PositronEmissionTomographyImageStorage,
sopClassDictionary.EnhancedPETImageStorage,
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
+2
View File
@@ -17,6 +17,7 @@ import * as dicomWebUtils from './DicomWebDataSource/utils';
import { createReportDialogPrompt } from './Panels';
import createReportAsync from './Actions/createReportAsync';
import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient';
import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils';
const defaultExtension: Types.Extensions.Extension = {
/**
@@ -56,4 +57,5 @@ export {
createReportDialogPrompt,
createReportAsync,
StaticWadoClient,
cleanDenaturalizedDataset,
};