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 };
}