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:
parent
3184e5572f
commit
f3822f98a3
@ -162,6 +162,7 @@ const sopClassDictionary = {
|
|||||||
MRSpectroscopyStorage: '1.2.840.10008.5.1.4.1.1.4.2',
|
MRSpectroscopyStorage: '1.2.840.10008.5.1.4.1.1.4.2',
|
||||||
EnhancedMRColorImageStorage: '1.2.840.10008.5.1.4.1.1.4.3',
|
EnhancedMRColorImageStorage: '1.2.840.10008.5.1.4.1.1.4.3',
|
||||||
LegacyConvertedEnhancedMRImageStorage: '1.2.840.10008.5.1.4.1.1.4.4',
|
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',
|
UltrasoundImageStorage: '1.2.840.10008.5.1.4.1.1.6.1',
|
||||||
EnhancedUSVolumeStorage: '1.2.840.10008.5.1.4.1.1.6.2',
|
EnhancedUSVolumeStorage: '1.2.840.10008.5.1.4.1.1.6.2',
|
||||||
SecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7',
|
SecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7',
|
||||||
@ -288,6 +289,7 @@ const sopClassUids = [
|
|||||||
sopClassDictionary.EnhancedMRColorImageStorage,
|
sopClassDictionary.EnhancedMRColorImageStorage,
|
||||||
sopClassDictionary.LegacyConvertedEnhancedMRImageStorage,
|
sopClassDictionary.LegacyConvertedEnhancedMRImageStorage,
|
||||||
sopClassDictionary.UltrasoundImageStorage,
|
sopClassDictionary.UltrasoundImageStorage,
|
||||||
|
sopClassDictionary.UltrasoundImageStorageRET,
|
||||||
sopClassDictionary.SecondaryCaptureImageStorage,
|
sopClassDictionary.SecondaryCaptureImageStorage,
|
||||||
sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage,
|
sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage,
|
||||||
sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage,
|
sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import queryString from 'query-string';
|
|||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
||||||
import DicomMetadataStore from '../services/DicomMetadataStore';
|
import DicomMetadataStore from '../services/DicomMetadataStore';
|
||||||
|
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||||
|
|
||||||
class MetadataProvider {
|
class MetadataProvider {
|
||||||
constructor() {
|
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) {
|
addImageIdToUIDs(imageId, uids) {
|
||||||
// This method is a fallback for when you don't have WADO-URI or WADO-RS.
|
// 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);
|
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) {
|
_getInstance(imageId) {
|
||||||
const uids = this._getUIDsFromImageID(imageId);
|
const uids = this._getUIDsFromImageID(imageId);
|
||||||
|
|
||||||
@ -254,14 +181,14 @@ class MetadataProvider {
|
|||||||
bluePaletteColorLookupTableDescriptor: validNumber(
|
bluePaletteColorLookupTableDescriptor: validNumber(
|
||||||
instance.BluePaletteColorLookupTableDescriptor
|
instance.BluePaletteColorLookupTableDescriptor
|
||||||
),
|
),
|
||||||
redPaletteColorLookupTableData: validNumber(
|
redPaletteColorLookupTableData: fetchPaletteColorLookupTableData(
|
||||||
instance.RedPaletteColorLookupTableData
|
instance, "RedPaletteColorLookupTableData", "RedPaletteColorLookupTableDescriptor"
|
||||||
),
|
),
|
||||||
greenPaletteColorLookupTableData: validNumber(
|
greenPaletteColorLookupTableData: fetchPaletteColorLookupTableData(
|
||||||
instance.GreenPaletteColorLookupTableData
|
instance, "GreenPaletteColorLookupTableData", "GreenPaletteColorLookupTableDescriptor"
|
||||||
),
|
),
|
||||||
bluePaletteColorLookupTableData: validNumber(
|
bluePaletteColorLookupTableData: fetchPaletteColorLookupTableData(
|
||||||
instance.BluePaletteColorLookupTableData
|
instance, "BluePaletteColorLookupTableData", "BluePaletteColorLookupTableDescriptor"
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,165 +1,72 @@
|
|||||||
import { api } from 'dicomweb-client';
|
/**
|
||||||
import DICOMWeb from '../../DICOMWeb';
|
* Gets the palette color data for the specified tag - red/green/blue,
|
||||||
import str2ab from '../str2ab';
|
* either from the given UID or from the tag itself.
|
||||||
|
* Returns an array if the data is immediately available, or a promise
|
||||||
import errorHandler from '../../errorHandler';
|
* which resolves to the data if the data needs to be loaded.
|
||||||
|
* Returns undefined if the palette isn't specified.
|
||||||
export default async function fetchPaletteColorLookupTableData(
|
*
|
||||||
instance,
|
* @param {*} item containing the palette colour data and description
|
||||||
server
|
* @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 {
|
const { PaletteColorLookupTableUID } = item;
|
||||||
PaletteColorLookupTableUID,
|
const paletteData = item[tag];
|
||||||
RedPaletteColorLookupTableDescriptor,
|
if (paletteData === undefined && PaletteColorLookupTableUID === undefined) return;
|
||||||
GreenPaletteColorLookupTableDescriptor,
|
// performance optimization - read UID and cache by UID
|
||||||
BluePaletteColorLookupTableDescriptor,
|
return _getPaletteColor(
|
||||||
RedPaletteColorLookupTableData,
|
item[tag],
|
||||||
GreenPaletteColorLookupTableData,
|
item[descriptorTag]
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function _getPaletteColor(paletteColorLookupTableData, lutDescriptor) {
|
||||||
* 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) {
|
|
||||||
const numLutEntries = lutDescriptor[0];
|
const numLutEntries = lutDescriptor[0];
|
||||||
const bits = lutDescriptor[2];
|
const bits = lutDescriptor[2];
|
||||||
|
|
||||||
|
if (!paletteColorLookupTableData) return undefined;
|
||||||
|
|
||||||
const arrayBufferToPaletteColorLUT = arraybuffer => {
|
const arrayBufferToPaletteColorLUT = arraybuffer => {
|
||||||
const byteArray =
|
|
||||||
bits === 16 ? new Uint16Array(arraybuffer) : new Uint8Array(arraybuffer);
|
|
||||||
const lut = [];
|
const lut = [];
|
||||||
|
|
||||||
for (let i = 0; i < numLutEntries; i++) {
|
if (bits === 16) {
|
||||||
lut[i] = byteArray[i];
|
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;
|
return lut;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (paletteColorLookupTableData.BulkDataURI) {
|
if (paletteColorLookupTableData.palette) {
|
||||||
let uri = paletteColorLookupTableData.BulkDataURI;
|
return paletteColorLookupTableData.palette;
|
||||||
|
|
||||||
// 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.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`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1236,7 +1236,7 @@
|
|||||||
core-js-pure "^3.20.2"
|
core-js-pure "^3.20.2"
|
||||||
regenerator-runtime "^0.13.4"
|
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"
|
version "7.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user