From f3822f98a3fc3c2e7fc893a5c28d7adc6dc095c1 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 11 Mar 2022 11:18:56 -0500 Subject: [PATCH] Fix palette colour display in OHIF v3 (#2731) * fix(OHIFv3):Fix palette colour data display * PR comments - added params description --- .../default/src/getSopClassHandlerModule.js | 2 + platform/core/src/classes/MetadataProvider.js | 87 +------- .../fetchPaletteColorLookupTableData.js | 207 +++++------------- yarn.lock | 2 +- 4 files changed, 67 insertions(+), 231 deletions(-) diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index 7bf2d6e89..41a261a44 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -162,6 +162,7 @@ const sopClassDictionary = { MRSpectroscopyStorage: '1.2.840.10008.5.1.4.1.1.4.2', EnhancedMRColorImageStorage: '1.2.840.10008.5.1.4.1.1.4.3', LegacyConvertedEnhancedMRImageStorage: '1.2.840.10008.5.1.4.1.1.4.4', + UltrasoundImageStorageRET: '1.2.840.10008.5.1.4.1.1.6', UltrasoundImageStorage: '1.2.840.10008.5.1.4.1.1.6.1', EnhancedUSVolumeStorage: '1.2.840.10008.5.1.4.1.1.6.2', SecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7', @@ -288,6 +289,7 @@ const sopClassUids = [ sopClassDictionary.EnhancedMRColorImageStorage, sopClassDictionary.LegacyConvertedEnhancedMRImageStorage, sopClassDictionary.UltrasoundImageStorage, + sopClassDictionary.UltrasoundImageStorageRET, sopClassDictionary.SecondaryCaptureImageStorage, sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage, sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage, diff --git a/platform/core/src/classes/MetadataProvider.js b/platform/core/src/classes/MetadataProvider.js index 71c732e0e..a4b66f60c 100644 --- a/platform/core/src/classes/MetadataProvider.js +++ b/platform/core/src/classes/MetadataProvider.js @@ -2,6 +2,7 @@ import queryString from 'query-string'; import dicomParser from 'dicom-parser'; import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation'; import DicomMetadataStore from '../services/DicomMetadataStore'; +import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; class MetadataProvider { constructor() { @@ -20,46 +21,6 @@ class MetadataProvider { }); } - /*async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) { - let dicomJSONDataset; - - // If Arraybuffer, parse to DICOMJSON before naturalizing. - if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) { - const dicomData = DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer); - - dicomJSONDataset = dicomData.dict; - } else { - dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer; - } - - // Check if dataset is already naturalized. - - let naturalizedDataset; - - if (dicomJSONDataset['SeriesInstanceUID'] === undefined) { - naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset( - dicomJSONDataset - ); - } else { - naturalizedDataset = dicomJSONDataset; - } - - const { - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID, - } = naturalizedDataset; - - const study = this._getAndCacheStudy(StudyInstanceUID); - const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID); - const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID); - - Object.assign(instance, naturalizedDataset); - - await this._checkBulkDataAndInlineBinaries(instance, options.server); - - return instance; - }*/ addImageIdToUIDs(imageId, uids) { // This method is a fallback for when you don't have WADO-URI or WADO-RS. @@ -69,40 +30,6 @@ class MetadataProvider { this.imageIdToUIDs.set(imageId, uids); } - // _getAndCacheStudy(StudyInstanceUID) { - // const studies = this.studies; - - // let study = studies.get(StudyInstanceUID); - - // if (!study) { - // study = { series: new Map() }; - // studies.set(StudyInstanceUID, study); - // } - - // return study; - // } - // _getAndCacheSeriesFromStudy(study, SeriesInstanceUID) { - // let series = study.series.get(SeriesInstanceUID); - - // if (!series) { - // series = { instances: new Map() }; - // study.series.set(SeriesInstanceUID, series); - // } - - // return series; - // } - - // _getAndCacheInstanceFromStudy(series, SOPInstanceUID) { - // let instance = series.instances.get(SOPInstanceUID); - - // if (!instance) { - // instance = {}; - // series.instances.set(SOPInstanceUID, instance); - // } - - // return instance; - // } - _getInstance(imageId) { const uids = this._getUIDsFromImageID(imageId); @@ -254,14 +181,14 @@ class MetadataProvider { bluePaletteColorLookupTableDescriptor: validNumber( instance.BluePaletteColorLookupTableDescriptor ), - redPaletteColorLookupTableData: validNumber( - instance.RedPaletteColorLookupTableData + redPaletteColorLookupTableData: fetchPaletteColorLookupTableData( + instance, "RedPaletteColorLookupTableData", "RedPaletteColorLookupTableDescriptor" ), - greenPaletteColorLookupTableData: validNumber( - instance.GreenPaletteColorLookupTableData + greenPaletteColorLookupTableData: fetchPaletteColorLookupTableData( + instance, "GreenPaletteColorLookupTableData", "GreenPaletteColorLookupTableDescriptor" ), - bluePaletteColorLookupTableData: validNumber( - instance.BluePaletteColorLookupTableData + bluePaletteColorLookupTableData: fetchPaletteColorLookupTableData( + instance, "BluePaletteColorLookupTableData", "BluePaletteColorLookupTableDescriptor" ), }; diff --git a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js index b33a576d9..e79749958 100644 --- a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js +++ b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js @@ -1,165 +1,72 @@ -import { api } from 'dicomweb-client'; -import DICOMWeb from '../../DICOMWeb'; -import str2ab from '../str2ab'; - -import errorHandler from '../../errorHandler'; - -export default async function fetchPaletteColorLookupTableData( - instance, - server +/** + * Gets the palette color data for the specified tag - red/green/blue, + * either from the given UID or from the tag itself. + * Returns an array if the data is immediately available, or a promise + * which resolves to the data if the data needs to be loaded. + * Returns undefined if the palette isn't specified. + * + * @param {*} item containing the palette colour data and description + * @param {*} tag is the tag for the palette data + * @param {*} descriptorTag is the tag for the descriptor + * @returns Array view containing the palette data, or a promise to return one. + * Returns undefined if the palette data is absent. + */ +export default function fetchPaletteColorLookupTableData( + item, tag, descriptorTag ) { - const { - PaletteColorLookupTableUID, - RedPaletteColorLookupTableDescriptor, - GreenPaletteColorLookupTableDescriptor, - BluePaletteColorLookupTableDescriptor, - RedPaletteColorLookupTableData, - GreenPaletteColorLookupTableData, - BluePaletteColorLookupTableData, - } = instance; - - return new Promise((resolve, reject) => { - let entry; - if (_paletteColorCache.isValidUID(PaletteColorLookupTableUID)) { - entry = _paletteColorCache.get(PaletteColorLookupTableUID); - - if (entry) { - return resolve(entry); - } - } - - // no entry in cache... Fetch remote data. - const promises = [ - _getPaletteColor( - server, - RedPaletteColorLookupTableData, - RedPaletteColorLookupTableDescriptor - ), - _getPaletteColor( - server, - GreenPaletteColorLookupTableData, - GreenPaletteColorLookupTableDescriptor - ), - _getPaletteColor( - server, - BluePaletteColorLookupTableData, - BluePaletteColorLookupTableDescriptor - ), - ]; - - Promise.all(promises).then( - ([ - RedPaletteColorLookupTableData, - GreenPaletteColorLookupTableData, - BluePaletteColorLookupTableData, - ]) => { - // when PaletteColorLookupTableUID is present, the entry can be cached... - _paletteColorCache.add({ - RedPaletteColorLookupTableData, - GreenPaletteColorLookupTableData, - BluePaletteColorLookupTableData, - PaletteColorLookupTableUID, - }); - - instance.RedPaletteColorLookupTableData = RedPaletteColorLookupTableData; - instance.GreenPaletteColorLookupTableData = GreenPaletteColorLookupTableData; - instance.BluePaletteColorLookupTableData = BluePaletteColorLookupTableData; - - resolve(); - } - ); - }); + const { PaletteColorLookupTableUID } = item; + const paletteData = item[tag]; + if (paletteData === undefined && PaletteColorLookupTableUID === undefined) return; + // performance optimization - read UID and cache by UID + return _getPaletteColor( + item[tag], + item[descriptorTag] + ) } -/** - * Simple cache schema for retrieved color palettes. - */ -const _paletteColorCache = { - count: 0, - maxAge: 24 * 60 * 60 * 1000, // 24h cache? - entries: {}, - isValidUID: function(PaletteColorLookupTableUID) { - return ( - typeof PaletteColorLookupTableUID === 'string' && - PaletteColorLookupTableUID.length > 0 - ); - }, - get: function(PaletteColorLookupTableUID) { - let entry = null; - if (this.entries.hasOwnProperty(PaletteColorLookupTableUID)) { - entry = this.entries[PaletteColorLookupTableUID]; - // check how the entry is... - if (Date.now() - entry.time > this.maxAge) { - // entry is too old... remove entry. - delete this.entries[PaletteColorLookupTableUID]; - this.count--; - entry = null; - } - } - return entry; - }, - add: function(entry) { - if (this.isValidUID(entry.uid)) { - let PaletteColorLookupTableUID = entry.uid; - if (this.entries.hasOwnProperty(PaletteColorLookupTableUID) !== true) { - this.count++; // increment cache entry count... - } - entry.time = Date.now(); - this.entries[PaletteColorLookupTableUID] = entry; - // @TODO: Add logic to get rid of old entries and reduce memory usage... - } - }, -}; - -function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) { +function _getPaletteColor(paletteColorLookupTableData, lutDescriptor) { const numLutEntries = lutDescriptor[0]; const bits = lutDescriptor[2]; + if (!paletteColorLookupTableData) return undefined; + const arrayBufferToPaletteColorLUT = arraybuffer => { - const byteArray = - bits === 16 ? new Uint16Array(arraybuffer) : new Uint8Array(arraybuffer); const lut = []; - for (let i = 0; i < numLutEntries; i++) { - lut[i] = byteArray[i]; - } + if (bits === 16) { + let j = 0; + for (let i = 0; i < numLutEntries; i++) { + lut[i] = arraybuffer[j++] + arraybuffer[j++] << 8; + } + } else { + for (let i = 0; i < numLutEntries; i++) { + lut[i] = byteArray[i]; + } + } return lut; }; - if (paletteColorLookupTableData.BulkDataURI) { - let uri = paletteColorLookupTableData.BulkDataURI; - - // TODO: Workaround for dcm4chee behind SSL-terminating proxy returning - // incorrect bulk data URIs - if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) { - uri = uri.replace('http', 'https'); - } - - const config = { - url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used - headers: DICOMWeb.getAuthorizationHeader(server), - errorInterceptor: errorHandler.getHTTPErrorHandler(), - }; - const dicomWeb = new api.DICOMwebClient(config); - const options = { - BulkDataURI: uri, - }; - - return dicomWeb - .retrieveBulkData(options) - .then(result => result[0]) - .then(arrayBufferToPaletteColorLUT); - } else if (paletteColorLookupTableData.InlineBinary) { - const inlineBinaryData = atob(paletteColorLookupTableData.InlineBinary); - const arraybuffer = str2ab(inlineBinaryData); - - return new Promise(resolve => { - resolve(arrayBufferToPaletteColorLUT(arraybuffer)); - }); - } else { - return Promise.resolve( - arrayBufferToPaletteColorLUT(paletteColorLookupTableData) - ); + if (paletteColorLookupTableData.palette) { + return paletteColorLookupTableData.palette; } + + if (paletteColorLookupTableData.InlineBinary) { + try { + const arraybuffer = Uint8Array.from(atob(paletteColorLookupTableData.InlineBinary), c => + c.charCodeAt(0) + ); + return (paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(arraybuffer)); + } catch (e) { + console.log("Couldn't decode", paletteColorLookupTableData.InlineBinary, e); + return undefined; + } + } + + if (paletteColorLookupTableData.retrieveBulkData) { + return paletteColorLookupTableData.retrieveBulkData().then(val => + (paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(val))); + } + + throw new Error(`No data found for ${paletteColorLookupTableData} palette`) } diff --git a/yarn.lock b/yarn.lock index 31f275dac..ab03aa93b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1236,7 +1236,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@7.1.2", "@babel/runtime@7.16.3", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@7.1.2", "@babel/runtime@7.16.3", "@babel/runtime@7.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==