Skip to content

Commit

Permalink
feat(multiframe): enhanced support for multiframe dicom (OHIF#3164)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
2 people authored and 徐忠元 committed Apr 1, 2023
1 parent e667496 commit 8ed75b1
Show file tree
Hide file tree
Showing 28 changed files with 228 additions and 62 deletions.
2 changes: 1 addition & 1 deletion 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 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) {
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
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 @@ -454,15 +482,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
3 changes: 3 additions & 0 deletions platform/viewer/public/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ window.config = {
showWarningMessageForCrossOrigin: true,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
maxNumRequests: {
interaction: 100,
thumbnail: 75,
Expand All @@ -45,10 +46,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',
Expand Down
1 change: 1 addition & 0 deletions platform/viewer/public/config/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Loading

0 comments on commit 8ed75b1

Please sign in to comment.