Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Apr 1, 2023
4 changes: 2 additions & 2 deletions extensions/cornerstone-dicom-sr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
"@cornerstonejs/adapters": "^0.6.0",
"@cornerstonejs/core": "^0.38.0",
"@cornerstonejs/tools": "^0.58.0"
"@cornerstonejs/core": "^0.40.0",
"@cornerstonejs/tools": "^0.60.1"
}
}
8 changes: 4 additions & 4 deletions extensions/cornerstone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"cornerstone-wado-image-loader": "^4.10.2",
"cornerstone-wado-image-loader": "^4.13.0",
"dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
Expand All @@ -44,9 +44,9 @@
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^0.6.0",
"@cornerstonejs/core": "^0.38.0",
"@cornerstonejs/streaming-image-volume-loader": "^0.15.13",
"@cornerstonejs/tools": "^0.58.0",
"@cornerstonejs/core": "^0.40.0",
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
"@cornerstonejs/tools": "^0.60.1",
"@kitware/vtk.js": "26.5.6",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
Expand Down
8 changes: 8 additions & 0 deletions extensions/cornerstone/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export default async function init({

// For debugging e2e tests that are failing on CI
cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering));
cornerstone.setConfiguration({
...cornerstone.getConfiguration(),
rendering: {
...cornerstone.getConfiguration().rendering,
strictZSpacingForVolumeViewport:
appConfig.strictZSpacingForVolumeViewport,
},
});

// For debugging large datasets
const MAX_CACHE_SIZE_1GB = 1073741824;
Expand Down
7 changes: 5 additions & 2 deletions extensions/default/src/DicomLocalDataSource/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ function createDicomLocalApi(dicomLocalConfig) {
study.series.forEach(aSeries => {
const { SeriesInstanceUID } = aSeries;

aSeries.instances.forEach(instance => {
const isMultiframe = aSeries.instances[0].NumberOfFrames > 1;

aSeries.instances.forEach((instance, index) => {
const {
url: imageId,
StudyInstanceUID,
Expand All @@ -153,6 +155,7 @@ function createDicomLocalApi(dicomLocalConfig) {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
frameIndex: isMultiframe ? index : 1,
});
});

Expand Down Expand Up @@ -185,7 +188,7 @@ function createDicomLocalApi(dicomLocalConfig) {
displaySet.images.forEach(instance => {
const NumberOfFrames = instance.NumberOfFrames;
if (NumberOfFrames > 1) {
for (let i = 0; i < NumberOfFrames; i++) {
for (let i = 1; i <= NumberOfFrames; i++) {
const imageId = this.getImageIdsForInstance({
instance,
frame: i,
Expand Down
4 changes: 2 additions & 2 deletions extensions/default/src/DicomWebDataSource/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
query: {
studies: {
mapParams: mapParams.bind(),
search: async function (origParams) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you aren't changing a file, please don't reformat it.

search: async function(origParams) {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
Expand All @@ -129,7 +129,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
},
series: {
// mapParams: mapParams.bind(),
search: async function (studyInstanceUid) {
search: async function(studyInstanceUid) {
const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
Expand Down
4 changes: 2 additions & 2 deletions extensions/measurement-tracking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"classnames": "^2.3.2",
"@cornerstonejs/core": "^0.38.0",
"@cornerstonejs/tools": "^0.58.0",
"@cornerstonejs/core": "^0.40.0",
"@cornerstonejs/tools": "^0.60.1",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "^0.29.4",
"lodash.debounce": "^4.17.21",
Expand Down
2 changes: 1 addition & 1 deletion platform/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
32 changes: 30 additions & 2 deletions platform/core/src/classes/MetadataProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -445,15 +473,15 @@ 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 {
imageURI = imageIdToURI(imageId);
}

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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
);

Expand Down
35 changes: 29 additions & 6 deletions platform/core/src/utils/combineFrameInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
108 changes: 72 additions & 36 deletions platform/core/src/utils/isDisplaySetReconstructable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 processMultiframe(multiFrameInstance) {
const {
PerFrameFunctionalGroupsSequence,
SharedFunctionalGroupsSequence,
} = multiFrameInstance;
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) {
// 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 (
!SharedFunctionalGroupsSequence ||
!SharedFunctionalGroupsSequence[0].PixelMeasuresSequence
) {
if (!hasPixelMeasurements(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
if (!hasOrientation(multiFrameInstance)) {
console.log('No image orientation information, not reconstructable');
return { value: false };
}

const frame0 = PerFrameFunctionalGroupsSequence[0];
const firstPosition =
frame0.PlanePositionSequence || frame0.CTPositionSequence;
if (!firstPosition) {
if (!hasPosition(multiFrameInstance)) {
console.log('No image position information, not reconstructable');
return { value: false };
}
// TODO - check spacing consistency

if (
multiFrameInstance.Modality.includes('NM') &&
!isNMReconstructable(multiFrameInstance)
) {
return { value: false };
}

// TODO - check spacing consistency
return { value: true };
}

Expand Down
2 changes: 1 addition & 1 deletion platform/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions platform/viewer/public/config/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
dataSources: [
{
Expand Down
Loading