Fix palette colour display in OHIF v3 (#2731)

* fix(OHIFv3):Fix palette colour data display

* PR comments - added params description
This commit is contained in:
Bill Wallace 2022-03-11 11:18:56 -05:00 committed by GitHub
parent 3184e5572f
commit f3822f98a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 231 deletions

View File

@ -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,

View File

@ -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"
),
};

View File

@ -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`)
}

View File

@ -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==