feat(multiframe): enhanced support for multiframe dicom (#3164)

* Changes in cswil version and multiframe

* Minor changes

* wip

* Adding support for NM multiframe images

* Applying PR suggestions

* fixing package versions

* Restoring default.js config file

* Check if NM subtype is reconstructable

* Restore default.js values

* refactore code

* feat: add flag for strict zspacing

---------

Co-authored-by: Alireza <ar.sedghi@gmail.com>
This commit is contained in:
rodrigobasilio2022andAlireza authored and GitHub committed 2023-03-31 22:21:16 -04:00
1 parent 5e42a42b5f
commit 226244a26c
31 files changed
+268 -86

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@
},
"peerDependencies": {
"cornerstone-math": "0.1.9",
"cornerstone-wado-image-loader": "^4.10.2",
"cornerstone-wado-image-loader": "^4.13.0",
"dicom-parser": "^1.8.9",
"@ohif/ui": "^2.0.0"
},
+30 -2
View File
@@ -412,6 +412,34 @@ class MetadataProvider {
return metadata;
}
/**
* Retrieves the frameNumber information, depending on the url style
* wadors /frames/1
* wadouri &frame=1
* @param {*} imageId
* @returns
*/
getFrameInformationFromURL(imageId) {
function getInformationFromURL(informationString, separator) {
let result = '';
const splittedStr = imageId.split(informationString)[1];
if (splittedStr.includes(separator)) {
result = splittedStr.split(separator)[0];
} else {
result = splittedStr;
}
return result;
}
if (imageId.includes('/frames')) {
return getInformationFromURL('/frames', '/');
}
if (imageId.includes('&frame=')) {
return getInformationFromURL('&frame=', '&');
}
return;
}
getUIDsFromImageID(imageId) {
// TODO: adding csiv here is not really correct. Probably need to use
// metadataProvider.addImageIdToUIDs(imageId, {
@@ -445,7 +473,7 @@ class MetadataProvider {
// check if the imageId starts with http:// or https:// using regex
// Todo: handle non http imageIds
let imageURI;
const urlRegex = /^(http|https):\/\//;
const urlRegex = /^(http|https|dicomfile):\/\//;
if (urlRegex.test(imageId)) {
imageURI = imageId;
} else {
@@ -453,7 +481,7 @@ class MetadataProvider {
}
const uids = this.imageURIToUIDs.get(imageURI);
const frameNumber = imageId.split(/\/frames\//)[1];
let frameNumber = this.getFrameInformationFromURL(imageId) || '1';
if (uids && frameNumber !== undefined) {
return { ...uids, frameNumber };
@@ -84,9 +84,9 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
}
function _getInstanceByImageId(imageId) {
for (let study of _model.studies) {
for (let series of study.series) {
for (let instance of series.instances) {
for (const study of _model.studies) {
for (const series of study.series) {
for (const instance of series.instances) {
if (instance.imageId === imageId) {
return instance;
}
@@ -236,7 +236,7 @@ const BaseImplementation = {
addStudy(study) {
const { StudyInstanceUID } = study;
let existingStudy = _model.studies.find(
const existingStudy = _model.studies.find(
study => study.StudyInstanceUID === StudyInstanceUID
);
@@ -30,12 +30,35 @@ const combineFrameInstance = (frame, instance) => {
.map(it => it[0])
.filter(it => it !== undefined && typeof it === 'object');
return Object.assign(
{ frameNumber: frameNumber },
instance,
...Object.values(shared),
...Object.values(perFrame)
);
// this is to fix NM multiframe datasets with position and orientation
// information inside DetectorInformationSequence
if (
!instance.ImageOrientationPatient &&
instance.DetectorInformationSequence
) {
instance.ImageOrientationPatient =
instance.DetectorInformationSequence[0].ImageOrientationPatient;
}
if (
!instance.ImagePositionPatient &&
instance.DetectorInformationSequence
) {
instance.ImagePositionPatient =
instance.DetectorInformationSequence[0].ImagePositionPatient;
}
const newInstance = Object.assign(instance, { frameNumber: frameNumber });
// merge the shared first then the per frame to override
[...shared, ...perFrame].forEach(item => {
Object.entries(item).forEach(([key, value]) => {
newInstance[key] = value;
});
});
// Todo: we should cache this combined instance somewhere, maybe add it
// back to the dicomMetaStore so we don't have to do this again.
return newInstance;
} else {
return instance;
}
@@ -30,62 +30,98 @@ export default function isDisplaySetReconstructable(instances) {
}
// Can't reconstruct if all instances don't have the ImagePositionPatient.
if (!instances.every(instance => !!instance.ImagePositionPatient)) {
if (
!isMultiframe &&
!instances.every(instance => instance.ImagePositionPatient)
) {
return { value: false };
}
const sortedInstances = sortInstancesByPosition(instances);
if (isMultiframe) {
return processMultiframe(sortedInstances[0]);
} else {
return processSingleframe(sortedInstances);
}
return isMultiframe
? processMultiframe(sortedInstances[0])
: processSingleframe(sortedInstances);
}
function hasPixelMeasurements(multiFrameInstance) {
const perFrameSequence =
multiFrameInstance.PerFrameFunctionalGroupsSequence?.[0];
const sharedSequence = multiFrameInstance.SharedFunctionalGroupsSequence;
return (
Boolean(perFrameSequence?.PixelMeasuresSequence) ||
Boolean(sharedSequence?.PixelMeasuresSequence) ||
Boolean(
multiFrameInstance.PixelSpacing &&
(multiFrameInstance.SliceThickness ||
multiFrameInstance.SpacingBetweenFrames)
)
);
}
function hasOrientation(multiFrameInstance) {
const sharedSequence = multiFrameInstance.SharedFunctionalGroupsSequence;
const perFrameSequence =
multiFrameInstance.PerFrameFunctionalGroupsSequence?.[0];
return (
Boolean(sharedSequence?.PlaneOrientationSequence) ||
Boolean(perFrameSequence?.PlaneOrientationSequence) ||
Boolean(
multiFrameInstance.ImageOrientationPatient ||
multiFrameInstance.DetectorInformationSequence?.[0]
?.ImageOrientationPatient
)
);
}
function hasPosition(multiFrameInstance) {
const perFrameSequence =
multiFrameInstance.PerFrameFunctionalGroupsSequence?.[0];
return (
Boolean(perFrameSequence?.PlanePositionSequence) ||
Boolean(perFrameSequence?.CTPositionSequence) ||
Boolean(
multiFrameInstance.ImagePositionPatient ||
multiFrameInstance.DetectorInformationSequence?.[0]
?.ImagePositionPatient
)
);
}
function isNMReconstructable(multiFrameInstance) {
const imageSubType = multiFrameInstance.ImageType?.[2];
return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO';
}
function processMultiframe(multiFrameInstance) {
const {
PerFrameFunctionalGroupsSequence,
SharedFunctionalGroupsSequence,
} = multiFrameInstance;
// If we don't have the PixelMeasuresSequence, then the pixel spacing and
// slice thickness isn't specified or is changing and we can't reconstruct
// the dataset.
if (!hasPixelMeasurements(multiFrameInstance)) {
return { value: false };
}
if (!hasOrientation(multiFrameInstance)) {
console.log('No image orientation information, not reconstructable');
return { value: false };
}
if (!hasPosition(multiFrameInstance)) {
console.log('No image position information, not reconstructable');
return { value: false };
}
if (
!SharedFunctionalGroupsSequence ||
!SharedFunctionalGroupsSequence[0].PixelMeasuresSequence
multiFrameInstance.Modality.includes('NM') &&
!isNMReconstructable(multiFrameInstance)
) {
return { value: false };
}
// Check that the orientation is either shared or with the allowed
// difference amount
const {
PlaneOrientationSequence: sharedOrientation,
} = SharedFunctionalGroupsSequence;
if (!sharedOrientation) {
const {
PlaneOrientationSequence: firstOrientation,
} = PerFrameFunctionalGroupsSequence[0];
if (!firstOrientation) {
console.log('No orientation information');
return { value: false };
}
// TODO - check orientation consistency
}
const frame0 = PerFrameFunctionalGroupsSequence[0];
const firstPosition =
frame0.PlanePositionSequence || frame0.CTPositionSequence;
if (!firstPosition) {
console.log('No image position information, not reconstructable');
return { value: false };
}
// TODO - check spacing consistency
return { value: true };
}
+1 -1
View File
@@ -65,7 +65,7 @@
"config-point": "^0.4.8",
"core-js": "^3.16.1",
"cornerstone-math": "^0.1.9",
"cornerstone-wado-image-loader": "^4.10.2",
"cornerstone-wado-image-loader": "^4.13.0",
"dcmjs": "^0.29.4",
"detect-gpu": "^4.0.16",
"dicom-parser": "^1.8.9",
+1
View File
@@ -9,6 +9,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
+3
View File
@@ -15,6 +15,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
maxNumRequests: {
interaction: 100,
thumbnail: 75,
@@ -34,10 +35,12 @@ window.config = {
// wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
// qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// new server
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
+1
View File
@@ -5,6 +5,7 @@ window.config = {
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
showWarningMessageForCrossOrigin: true,
strictZSpacingForVolumeViewport: true,
showCPUFallbackMessage: true,
servers: {
dicomWeb: [
@@ -9,6 +9,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
@@ -29,6 +30,20 @@ window.config = {
supportsWildcard: false,
},
},
{
friendlyName: 'dicom json',
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
sourceName: 'dicomjson',
configuration: {
name: 'json',
},
},
{
friendlyName: 'dicom local',
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
sourceName: 'dicomlocal',
configuration: {},
},
],
defaultDataSourceName: 'dicomweb',
};
@@ -10,6 +10,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
@@ -8,6 +8,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
dataSources: [
{
friendlyName: 'Orthanc Server',
@@ -23,6 +24,20 @@ window.config = {
thumbnailRendering: 'wadors',
},
},
{
friendlyName: 'dicom json',
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
sourceName: 'dicomjson',
configuration: {
name: 'json',
},
},
{
friendlyName: 'dicom local',
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
sourceName: 'dicomlocal',
configuration: {},
},
],
defaultDataSourceName: 'dicomweb',
};
@@ -6,6 +6,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
servers: {
// This is an array, but we'll only use the first entry for now
dicomWeb: [
@@ -8,6 +8,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
dataSources: [
{
friendlyName: 'Orthanc Server',
@@ -23,6 +24,20 @@ window.config = {
thumbnailRendering: 'wadors',
},
},
{
friendlyName: 'dicom json',
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
sourceName: 'dicomjson',
configuration: {
name: 'json',
},
},
{
friendlyName: 'dicom local',
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
sourceName: 'dicomlocal',
configuration: {},
},
],
defaultDataSourceName: 'dicomweb',
};
+1
View File
@@ -8,6 +8,7 @@ window.config = {
maxNumberOfWebWorkers: 3,
showWarningMessageForCrossOrigin: false,
showCPUFallbackMessage: false,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
+1
View File
@@ -6,6 +6,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// This is an array, but we'll only use the first entry for now
oidc: [
{
+1
View File
@@ -6,6 +6,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
servers: {
// This is an array, but we'll only use the first entry for now
dicomWeb: [],
@@ -8,6 +8,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
dataSources: [
{
friendlyName: 'DCM4CHEE Server',
@@ -27,6 +28,20 @@ window.config = {
},
},
},
{
friendlyName: 'dicom json',
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
sourceName: 'dicomjson',
configuration: {
name: 'json',
},
},
{
friendlyName: 'dicom local',
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
sourceName: 'dicomlocal',
configuration: {},
},
],
studyListFunctionsEnabled: true,
defaultDataSourceName: 'dicomweb',
@@ -8,6 +8,7 @@ window.config = {
showLoadingIndicator: true,
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
@@ -12,6 +12,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
@@ -20,6 +20,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
+1
View File
@@ -9,6 +9,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
@@ -6,6 +6,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
servers: {
dicomWeb: [
{
@@ -31,11 +31,6 @@ export default function buildModeRoutes({
hotkeysManager,
}) {
const routes = [];
// const dataSources = Object.keys(extensionManager.dataSourceMap).map(a =>
// extensionManager.getDataSources(a)
// );
const dataSourceNames = [];
dataSources.forEach(dataSource => {
@@ -61,7 +56,6 @@ export default function buildModeRoutes({
servicesManager={servicesManager}
commandsManager={commandsManager}
hotkeysManager={hotkeysManager}
commandsManager={commandsManager}
/>
);
@@ -86,7 +80,6 @@ export default function buildModeRoutes({
servicesManager={servicesManager}
commandsManager={commandsManager}
hotkeysManager={hotkeysManager}
commandsManager={commandsManager}
/>
);