feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182)
This commit is contained in:
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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in new issue
Block a user