diff --git a/packages/dicomImageLoader/.webpack/webpack-base.js b/packages/dicomImageLoader/.webpack/webpack-base.js index aa8b86c19..83232045e 100644 --- a/packages/dicomImageLoader/.webpack/webpack-base.js +++ b/packages/dicomImageLoader/.webpack/webpack-base.js @@ -69,6 +69,9 @@ module.exports = { exclude: [/(node_modules)/, /(codecs)/], use: { loader: 'babel-loader', + options: { + cacheDirectory: true, + }, }, }, { diff --git a/packages/dicomImageLoader/.webpack/webpack-bundle.js b/packages/dicomImageLoader/.webpack/webpack-bundle.js index cdacddf1b..f93ccb73a 100644 --- a/packages/dicomImageLoader/.webpack/webpack-bundle.js +++ b/packages/dicomImageLoader/.webpack/webpack-bundle.js @@ -90,7 +90,12 @@ module.exports = { }, ], }, - plugins: [new webpack.ProgressPlugin()], + plugins: [ + new webpack.ProgressPlugin(), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], experiments: { asyncWebAssembly: true, }, diff --git a/packages/dicomImageLoader/src/imageLoader/isNMReconstructable.ts b/packages/dicomImageLoader/src/imageLoader/isNMReconstructable.ts new file mode 100644 index 000000000..4a0909d23 --- /dev/null +++ b/packages/dicomImageLoader/src/imageLoader/isNMReconstructable.ts @@ -0,0 +1,3 @@ +export default function isNMReconstructable(imageSubType) { + return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO'; +} diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/combineFrameInstance.ts b/packages/dicomImageLoader/src/imageLoader/wadors/combineFrameInstance.ts index c7efcc841..258293361 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadors/combineFrameInstance.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadors/combineFrameInstance.ts @@ -69,12 +69,16 @@ function combineFrameInstance(frameNumber, instance) { frameNumber ); - return Object.assign( - rest, - { '00280008': NumberOfFrames }, - ...Object.values(shared), - ...Object.values(perFrame) - ); + const newInstance = Object.assign(instance, { frameNumber }); + + // merge the shared first then the per frame to override + [...shared, ...perFrame].forEach((item) => { + Object.entries(item).forEach(([key, value]) => { + newInstance[key] = value; + }); + }); + + return Object.assign(rest, { '00280008': NumberOfFrames }, newInstance); } return instance; diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/NMHelpers.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/NMHelpers.ts new file mode 100644 index 000000000..47ba74e93 --- /dev/null +++ b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/NMHelpers.ts @@ -0,0 +1,84 @@ +import getTagValue from '../getTagValue'; +import getValue from './getValue'; +import isNMReconstructable from '../../isNMReconstructable'; +import getNumberValues from './getNumberValues'; + +function isNMModality(metaData) { + const modality = getValue(metaData['00080060']); + + return modality.includes('NM'); +} + +/** + * Get a subpart of Image Type dicom tag defined by index + * @param {*} metaData + * @param {*} index 0 based index of the subtype + */ +function getImageTypeSubItemFromMetadata(metaData, index) { + const imageType = getTagValue(metaData['00080008'], false); + + if (imageType) { + // const subTypes = imageType.split('\\'); + + // if (subTypes.length > index) { + // return subTypes[index]; + // } + return imageType[index]; + } + + return undefined; +} +/** + * Extracts the orientation from NM multiframe metadata, if image type + * equal to RECON TOMO or RECON GATED TOMO + * @param {*} metaData + * @returns + */ +function extractOrientationFromNMMultiframeMetadata(metaData) { + let imageOrientationPatient; + const imageSubType = getImageTypeSubItemFromMetadata(metaData, 2); + + if (imageSubType && isNMReconstructable(imageSubType)) { + const detectorInformationSequence = getTagValue(metaData['00540022']); + + if (detectorInformationSequence) { + imageOrientationPatient = getNumberValues( + detectorInformationSequence['00200037'], + 6 + ); + } + } + + return imageOrientationPatient; +} + +/** + * Extracts the position from NM multiframe dataset, if image type + * equal to RECON TOMO or RECON GATED TOMO + * @param {*} metaData + * @returns + */ +function extractPositionFromNMMultiframeMetadata(metaData) { + let imagePositionPatient; + const imageSubType = getImageTypeSubItemFromMetadata(metaData, 2); + + if (imageSubType && isNMReconstructable(imageSubType)) { + const detectorInformationSequence = getTagValue(metaData['00540022']); + + if (detectorInformationSequence) { + imagePositionPatient = getNumberValues( + detectorInformationSequence['00200032'], + 3 + ); + } + } + + return imagePositionPatient; +} + +export { + extractOrientationFromNMMultiframeMetadata, + extractPositionFromNMMultiframeMetadata, + isNMModality, + getImageTypeSubItemFromMetadata, +}; diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/extractPositioningFromMetadata.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/extractPositioningFromMetadata.ts new file mode 100644 index 000000000..27c0d3b28 --- /dev/null +++ b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/extractPositioningFromMetadata.ts @@ -0,0 +1,49 @@ +import getNumberValues from './getNumberValues'; +import { + extractOrientationFromNMMultiframeMetadata, + extractPositionFromNMMultiframeMetadata, + isNMModality, +} from './NMHelpers'; + +/** + * Extract orientation information from a metadata. It tries to get the orientation + * from the Detector Information Sequence (for NM images) if image type equal + * to RECON TOMO or RECON GATED TOMO + * @param {*} metaData + * @returns + */ +function extractOrientationFromMetadata(metaData) { + let imageOrientationPatient = getNumberValues(metaData['00200037'], 6); + + // If orientation not valid to this point, trying to get the orientation + // from the Detector Information Sequence (for NM images) with image type + // equal to RECON TOMO or RECON GATED TOMO + + if (!imageOrientationPatient && isNMModality(metaData)) { + imageOrientationPatient = + extractOrientationFromNMMultiframeMetadata(metaData); + } + + return imageOrientationPatient; +} + +/** + * Extract position information from a metaData. It tries to get the position + * from the Detector Information Sequence (for NM images) if image type equal + * to RECON TOMO or RECON GATED TOMO + * @param {*} metaData + * @returns + */ +function extractPositionFromMetadata(metaData) { + let imagePositionPatient = getNumberValues(metaData['00200032'], 3); + + // If position not valid to this point, trying to get the position + // from the Detector Information Sequence (for NM images) + if (!imagePositionPatient && isNMModality(metaData)) { + imagePositionPatient = extractPositionFromNMMultiframeMetadata(metaData); + } + + return imagePositionPatient; +} + +export { extractOrientationFromMetadata, extractPositionFromMetadata }; diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/fixNMMetadata.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/fixNMMetadata.ts deleted file mode 100644 index 975981307..000000000 --- a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/fixNMMetadata.ts +++ /dev/null @@ -1,16 +0,0 @@ -import getTagValue from '../getTagValue'; - -export default function fixNMMetadata(metaData) { - if (!metaData['00200032'] || metaData['00200037']) { - // adjust metadata in case of multiframe NM data, as the dicom tags - // 00200032 and 00200037 could be found only in the dicom tag 00540022 - const detectorInformationSequence = getTagValue(metaData['00540022']); - - if (detectorInformationSequence) { - metaData['00200032'] = detectorInformationSequence['00200032']; - metaData['00200037'] = detectorInformationSequence['00200037']; - } - } - - return metaData; -} diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts index 611627274..9de5bdd53 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadors/metaData/metaDataProvider.ts @@ -4,12 +4,17 @@ import getNumberValue from './getNumberValue'; import getOverlayPlaneModule from './getOverlayPlaneModule'; import metaDataManager from '../metaDataManager'; import getValue from './getValue'; -//import fixNMMetadata from './fixNMMetadata'; import { getMultiframeInformation, getFrameInformation, } from '../combineFrameInstance'; import multiframeMetadata from '../retrieveMultiframeMetadata'; +import { + extractOrientationFromMetadata, + extractPositionFromMetadata, +} from './extractPositioningFromMetadata'; +import { getImageTypeSubItemFromMetadata } from './NMHelpers'; +import isNMReconstructable from '../../isNMReconstructable'; function metaDataProvider(type, imageId) { if (type === 'multiframeModule') { @@ -83,10 +88,28 @@ function metaDataProvider(type, imageId) { }; } + if (type === 'nmMultiframeGeometryModule') { + const modality = getValue(metaData['00080060']); + const imageSubType = getImageTypeSubItemFromMetadata(metaData, 2); + + return { + modality, + imageType: getValue(metaData['00080008']), + imageSubType, + imageOrientationPatient: extractOrientationFromMetadata(metaData), + imagePositionPatient: extractPositionFromMetadata(metaData), + sliceThickness: getNumberValue(metaData['00180050']), + pixelSpacing: getNumberValues(metaData['00280030'], 2), + numberOfFrames: getNumberValue(metaData['00280008']), + isNMReconstructable: + isNMReconstructable(imageSubType) && modality.includes('NM'), + }; + } + if (type === 'imagePlaneModule') { //metaData = fixNMMetadata(metaData); - const imageOrientationPatient = getNumberValues(metaData['00200037'], 6); - const imagePositionPatient = getNumberValues(metaData['00200032'], 3); + const imageOrientationPatient = extractOrientationFromMetadata(metaData); + const imagePositionPatient = extractPositionFromMetadata(metaData); const pixelSpacing = getNumberValues(metaData['00280030'], 2); let columnPixelSpacing = null; diff --git a/packages/dicomImageLoader/src/imageLoader/wadors/metaDataManager.ts b/packages/dicomImageLoader/src/imageLoader/wadors/metaDataManager.ts index d9126c2d7..919ccc354 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadors/metaDataManager.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadors/metaDataManager.ts @@ -4,6 +4,7 @@ import { combineFrameInstance } from './combineFrameInstance'; import multiframeMetadata from './retrieveMultiframeMetadata'; let metadataByImageURI = []; +let multiframeMetadataByImageURI = {}; function add(imageId: string, metadata: WADORSMetaData) { const imageURI = imageIdToURI(imageId); @@ -21,42 +22,51 @@ function add(imageId: string, metadata: WADORSMetaData) { function get(imageId: string): WADORSMetaData { const imageURI = imageIdToURI(imageId); - // dealing first with the non multiframe information - let metadata = metadataByImageURI[imageURI]; + // Check if the metadata is already available + const metadata = metadataByImageURI[imageURI]; - if (metadata) { - if (!metadata.isMultiframe) { - return metadata; - } + if (metadata && !metadata?.isMultiframe) { + // Return the metadata for single-frame images + return metadata; } - let frame = 1; + const cachedMetadata = multiframeMetadataByImageURI[imageURI]; - if (!metadata) { - // in this case it could indicate a multiframe imageid - // Try to get the first frame metadata, where is stored the multiframe info - const firstFrameInfo = - multiframeMetadata._retrieveMultiframeMetadata(imageURI); - - metadata = firstFrameInfo.metadata; - frame = firstFrameInfo.frame; + if (cachedMetadata) { + return cachedMetadata; } - if (metadata) { - metadata = combineFrameInstance(frame, metadata); + // Try to get the metadata for a specific frame of a multiframe image + const retrievedMetadata = + multiframeMetadata._retrieveMultiframeMetadata(imageURI); + + if (!retrievedMetadata || !retrievedMetadata.metadata) { + return; } - return metadata; + const { metadata: firstFrameMetadata, frame } = retrievedMetadata; + + if (firstFrameMetadata) { + // Combine the metadata from the first frame with the metadata from the specified frame + const combined = combineFrameInstance(frame, firstFrameMetadata); + + multiframeMetadataByImageURI[imageURI] = combined; + + return combined; + } } function remove(imageId) { const imageURI = imageIdToURI(imageId); metadataByImageURI[imageURI] = undefined; + + multiframeMetadataByImageURI[imageURI] = undefined; } function purge() { metadataByImageURI = []; + multiframeMetadataByImageURI = {}; } export { metadataByImageURI }; diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/combineFrameInstanceDataset.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/combineFrameInstanceDataset.ts index 0d40b521b..dadf9048c 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadouri/combineFrameInstanceDataset.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/combineFrameInstanceDataset.ts @@ -89,7 +89,6 @@ function combineFrameInstanceDataset(frameNumber, dataSet) { PerFrameFunctionalGroupsSequence, SharedFunctionalGroupsSequence, otherElements, - otherAttributtes, } = getMultiframeInformation(dataSet); if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) { @@ -100,8 +99,7 @@ function combineFrameInstanceDataset(frameNumber, dataSet) { ); // creating a new copy of the dataset to remove the two multiframe dicom tags - const newDataset = { - ...otherAttributtes, + const newElements = { elements: { ...otherElements, ...shared, @@ -109,6 +107,9 @@ function combineFrameInstanceDataset(frameNumber, dataSet) { }, }; + const clonedDataset = Object.create(dataSet); + const newDataset = Object.assign(clonedDataset, newElements); + return newDataset; } diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/extractPositioningFromDataset.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/extractPositioningFromDataset.ts new file mode 100644 index 000000000..be48d2ea4 --- /dev/null +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/extractPositioningFromDataset.ts @@ -0,0 +1,186 @@ +import getNumberValues from './getNumberValues'; +import isNMReconstructable from '../../isNMReconstructable'; + +/** + * Get a subpart of Image Type dicom tag defined by index + * @param {*} dataSet + * @param {*} index 0 based index of the subtype + */ +function getImageTypeSubItemFromDataset(dataSet, index) { + const imageType = dataSet.string('x00080008'); + + if (imageType) { + const subTypes = imageType.split('\\'); + + if (subTypes.length > index) { + return subTypes[index]; + } + } + + return undefined; +} +/** + * Extracts the orientation from NM multiframe dataset, if image type + * equal to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractOrientationFromNMMultiframeDataset(dataSet) { + let imageOrientationPatient; + const modality = dataSet.string('x00080060'); + + if (modality.includes('NM')) { + const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); + + if (imageSubType && isNMReconstructable(imageSubType)) { + if (dataSet.elements.x00540022) { + imageOrientationPatient = getNumberValues( + dataSet.elements.x00540022.items[0].dataSet, + 'x00200037', + 6 + ); + } + } + } + + return imageOrientationPatient; +} + +/** + * Extracts the position from NM multiframe dataset, if image type + * equal to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractPositionFromNMMultiframeDataset(dataSet) { + let imagePositionPatient; + const modality = dataSet.string('x00080060'); + + if (modality.includes('NM')) { + const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); + + if (imageSubType && isNMReconstructable(imageSubType)) { + if (dataSet.elements.x00540022) { + imagePositionPatient = getNumberValues( + dataSet.elements.x00540022.items[0].dataSet, + 'x00200032', + 3 + ); + } + } + } + + return imagePositionPatient; +} + +/** + * Extract orientation information from a dataset. It tries to get the orientation + * from the Detector Information Sequence (for NM images) if image type equal + * to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractOrientationFromDataset(dataSet) { + let imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); + + // Trying to get the orientation from the Plane Orientation Sequence + if (!imageOrientationPatient && dataSet.elements.x00209116) { + imageOrientationPatient = getNumberValues( + dataSet.elements.x00209116.items[0].dataSet, + 'x00200037', + 6 + ); + } + + // If orientation not valid to this point, trying to get the orientation + // from the Detector Information Sequence (for NM images) with image type + // equal to RECON TOMO or RECON GATED TOMO + + if (!imageOrientationPatient) { + imageOrientationPatient = + extractOrientationFromNMMultiframeDataset(dataSet); + } + + return imageOrientationPatient; +} + +/** + * Extract position information from a dataset. It tries to get the position + * from the Detector Information Sequence (for NM images) if image type equal + * to RECON TOMO or RECON GATED TOMO + * @param {*} dataSet + * @returns + */ +function extractPositionFromDataset(dataSet) { + let imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); + + // Trying to get the position from the Plane Position Sequence + if (!imagePositionPatient && dataSet.elements.x00209113) { + imagePositionPatient = getNumberValues( + dataSet.elements.x00209113.items[0].dataSet, + 'x00200032', + 3 + ); + } + + // If position not valid to this point, trying to get the position + // from the Detector Information Sequence (for NM images) + if (!imagePositionPatient) { + imagePositionPatient = extractPositionFromNMMultiframeDataset(dataSet); + } + + return imagePositionPatient; +} + +/** + * Extract the pixelSpacing information. If exists, extracts this information + * from Pixel Measures Sequence + * @param {*} dataSet + * @returns + */ +function extractSpacingFromDataset(dataSet) { + let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); + + // If pixelSpacing not valid to this point, trying to get the spacing + // from the Pixel Measures Sequence + if (!pixelSpacing && dataSet.elements.x00289110) { + pixelSpacing = getNumberValues( + dataSet.elements.x00289110.items[0].dataSet, + 'x00280030', + 2 + ); + } + + return pixelSpacing; +} + +/** + * Extract the sliceThickness information. If exists, extracts this information + * from Pixel Measures Sequence + * @param {*} dataSet + * @returns + */ +function extractSliceThicknessFromDataset(dataSet) { + let sliceThickness; + + if (dataSet.elements.x00180050) { + sliceThickness = dataSet.floatString('x00180050'); + } else if ( + dataSet.elements.x00289110 && + dataSet.elements.x00289110.items.length && + dataSet.elements.x00289110.items[0].dataSet.elements.x00180050 + ) { + sliceThickness = + dataSet.elements.x00289110.items[0].dataSet.floatString('x00180050'); + } + + return sliceThickness; +} + +export { + getImageTypeSubItemFromDataset, + extractOrientationFromDataset, + extractPositionFromDataset, + extractSpacingFromDataset, + extractSliceThicknessFromDataset, +}; diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts index c38ff5783..d63d35fc4 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts @@ -8,6 +8,14 @@ import getLUTs from './getLUTs'; import getModalityLUTOutputPixelRepresentation from './getModalityLUTOutputPixelRepresentation'; import { getDirectFrameInformation } from '../combineFrameInstanceDataset'; import multiframeDataset from '../retrieveMultiframeDataset'; +import { + getImageTypeSubItemFromDataset, + extractOrientationFromDataset, + extractPositionFromDataset, + extractSpacingFromDataset, + extractSliceThicknessFromDataset, +} from './extractPositioningFromDataset'; +import isNMReconstructable from '../../isNMReconstructable'; function metaDataProvider(type, imageId) { const { dicomParser } = external; @@ -30,7 +38,12 @@ function metaDataProvider(type, imageId) { return multiframeInfo; } - const dataSet = dataSetCacheManager.get(parsedImageId.url); + let url = parsedImageId.url; + + if (parsedImageId.frame) { + url = `${url}&frame=${parsedImageId.frame}`; + } + const dataSet = dataSetCacheManager.get(url); if (!dataSet) { return; @@ -58,9 +71,25 @@ function metaDataProvider(type, imageId) { } if (type === 'imagePlaneModule') { - const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); - const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); - const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2); + const imageOrientationPatient = extractOrientationFromDataset(dataSet); + + const imagePositionPatient = extractPositionFromDataset(dataSet); + + const pixelSpacing = extractSpacingFromDataset(dataSet); + + let frameOfReferenceUID; + + if (dataSet.elements.x00200052) { + frameOfReferenceUID = dataSet.string('x00200052'); + } + + const sliceThickness = extractSliceThicknessFromDataset(dataSet); + + let sliceLocation; + + if (dataSet.elements.x00201041) { + sliceLocation = dataSet.floatString('x00201041'); + } let columnPixelSpacing = null; @@ -95,21 +124,39 @@ function metaDataProvider(type, imageId) { } return { - frameOfReferenceUID: dataSet.string('x00200052'), + frameOfReferenceUID, rows: dataSet.uint16('x00280010'), columns: dataSet.uint16('x00280011'), imageOrientationPatient, rowCosines, columnCosines, imagePositionPatient, - sliceThickness: dataSet.floatString('x00180050'), - sliceLocation: dataSet.floatString('x00201041'), + sliceThickness, + sliceLocation, pixelSpacing, rowPixelSpacing, columnPixelSpacing, }; } + if (type === 'nmMultiframeGeometryModule') { + const modality = dataSet.string('x00080060'); + const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2); + + return { + modality, + imageType: dataSet.string('x00080008'), + imageSubType, + imageOrientationPatient: extractOrientationFromDataset(dataSet), + imagePositionPatient: extractPositionFromDataset(dataSet), + sliceThickness: extractSliceThicknessFromDataset(dataSet), + pixelSpacing: extractSpacingFromDataset(dataSet), + numberOfFrames: dataSet.uint16('x00280008'), + isNMReconstructable: + isNMReconstructable(imageSubType) && modality.includes('NM'), + }; + } + if (type === 'imagePixelModule') { return getImagePixelModule(dataSet); } diff --git a/packages/dicomImageLoader/test/imageLoader/wadouri/metaDataProvider_test.ts b/packages/dicomImageLoader/test/imageLoader/wadouri/metaDataProvider_test.ts index 006b532ba..50f75b09c 100644 --- a/packages/dicomImageLoader/test/imageLoader/wadouri/metaDataProvider_test.ts +++ b/packages/dicomImageLoader/test/imageLoader/wadouri/metaDataProvider_test.ts @@ -47,7 +47,7 @@ describe('#wadouri > metadataProvider', function () { }); }); - it('should return columnPixelSpacing undefined if pixelSpacing is undefined', function (done) { + it('should return rowPixelSpacing undefined if pixelSpacing is undefined', function (done) { this.timeout(5000); loadImage(imageId).promise.then(() => { const imagePlaneModule = external.cornerstone.metaData.get(