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:
1 parent
5e42a42b5f
commit
226244a26c
31 files changed
+268
-86
No files matched your search
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,6 +9,7 @@ window.config = {
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
showCPUFallbackMessage: true,
|
||||
showLoadingIndicator: true,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
// filterQueryParam: false,
|
||||
dataSources: [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -8,6 +8,7 @@ window.config = {
|
||||
maxNumberOfWebWorkers: 3,
|
||||
showWarningMessageForCrossOrigin: false,
|
||||
showCPUFallbackMessage: false,
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
// filterQueryParam: false,
|
||||
dataSources: [
|
||||
{
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user