ohif-viewer/Packages/ohif-studies/imports/both/services/wado/retrieveMetadata.js
Egor Lezhnin 9486522552 feat(google-cloud): Add support for Google Cloud Healthcare API DICOM Store Switch (#325)
* NOTASK: PoC for loading data from Healthcare API

* NOTASK: PoC for loading data from Healthcare API fix

* Add sample dialog for loading test dicom-picker

* Fix incorrect promise resolving

* NOTASK: PoC for loading dicom store

* Add web-components as git submodules. (Probably will be changed later)

* Add rough dicomStore picker

* Add dataset selector

* NOTASK: QIDO metadata first draft

* NOTASK: Not Implemented error for color images

* NOTASK: clean-up

* Dicom files uploader intermediate version

* NOTASK: Accept header

* NOTASK: docker configs

* NOTASK: fix

* NOTASK: howTo

* Add draft of DICOM uploader

* Add missing files

* Fix error with missing clientId

* Update submodule

* NOTASK: HowTo

* NOTASK: config for dev

* NOTASK: new docker file

* clean-up

* Update GCP web-components

* Add integration with async web components

* Fix errors in nginx.conf

* Structured reports views basic implementation

* NOTASK: qido -> wado

* NOTASK: config fix

* NOTASK: dirty copy-paste implementation

* Add "Change dicom store" button.
Add an ability to clear date.
Fix bug, when OAuth dosn't work if URL is not /

* Fix studylist filtering

* Fix dockerfile

* NOTASK: meteor update

* NOTASK: fix of package structure

* NOTASK: merge fix

* improvements

Move check for now SR into SR modal
Use simple button to open SR modal
Remove button styling
cleanup

* copy SR data retrieval to wado

* make search of SR simpler

* Codestyle fix

* NOTASK: palette color error message

* Fix date filter.
Fix server settings on first loading.
Add SR and PS buttons

* NOTASK: PS is now hidden by default

* NOTASK: dirty solution

* NOTASK:  new wadoimage lib version

* Fix PS and SR buttons

* Make modal dialogs vertically centered

* Update web components. Quick UI fixes

* Add missing files to the previous commit

* NOTASK:  show-hide for PS

* NOTASK:  show-hide for PS

* NOTASK:  fix of min-max PixelValue bug

* Add "sign out" functionality for Google OAuth. Update web-components.

* intermediate commit

* Add demo signin page

* Beautifying demo sign in page

* NOTASK:  merge

* NOTASK:  clean-up

* Use npm for getting healthcare-api-adapter instead of git submodule.

* NOTASK:  clean-up

* meteor update

* clean-up

* clean-up

* Clean up code

* Update healthcare-api-adapter version. Add location to dicom store path. Fix buttons style.

* Downgrade meteor and packages to master version. Use meteor-build-client-fixed2
2019-03-22 16:42:26 +01:00

489 lines
18 KiB
JavaScript

import { OHIF } from 'meteor/ohif:core';
import DICOMwebClient from 'dicomweb-client';
import { parseFloatArray } from '../../lib/parseFloatArray';
const { DICOMWeb } = OHIF;
/**
* Simple cache schema for retrieved color palettes.
*/
const paletteColorCache = {
count: 0,
maxAge: 24 * 60 * 60 * 1000, // 24h cache?
entries: {},
isValidUID: function (paletteUID) {
return typeof paletteUID === 'string' && paletteUID.length > 0;
},
get: function (paletteUID) {
let entry = null;
if (this.entries.hasOwnProperty(paletteUID)) {
entry = this.entries[paletteUID];
// check how the entry is...
if ((Date.now() - entry.time) > this.maxAge) {
// entry is too old... remove entry.
delete this.entries[paletteUID];
this.count--;
entry = null;
}
}
return entry;
},
add: function (entry) {
if (this.isValidUID(entry.uid)) {
let paletteUID = entry.uid;
if (this.entries.hasOwnProperty(paletteUID) !== true) {
this.count++; // increment cache entry count...
}
entry.time = Date.now();
this.entries[paletteUID] = entry;
// @TODO: Add logic to get rid of old entries and reduce memory usage...
}
}
};
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`
}
function buildInstanceFrameWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) {
const baseWadoRsUri = buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server)
// 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: OHIF.DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const options = {
BulkDataURI: uri
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + (byteArray[position + 1] * 256);
}
const arrayBufferToPaletteColorLUT = (arraybuffer) =>{
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
}
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT)
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
if (paletteColorCache.isValidUID(paletteUID)) {
const entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);;
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);;
const promises = [r, g, b];
Promise.all(promises).then((args) => {
entry = {
red: args[0],
green: args[1],
blue: args[2]
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime'
};
if(!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if ((radiopharmaceuticalInfo === undefined) || !radiopharmaceuticalInfo.Value || !radiopharmaceuticalInfo.Value.length) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(firstPetRadiopharmaceuticalInfo['00181072']),
radionuclideTotalDose: DICOMWeb.getNumber(firstPetRadiopharmaceuticalInfo['00181074']),
radionuclideHalfLife: DICOMWeb.getNumber(firstPetRadiopharmaceuticalInfo['00181075'])
};
}
function getRelationshipString (data) {
const relationshipType = DICOMWeb.getString(data['0040A010']);
switch (relationshipType) {
case 'HAS CONCEPT MOD':
return 'Concept modifier: ';
case 'HAS OBS CONTEXT':
return 'Observation context: ';
default:
return '';
}
}
const getNestedObject = (data) => data.Value[0] || {};
const getMeaningString = (data) => (data['0040A043'] && `${DICOMWeb.getString(data['0040A043'].Value[0]['00080104'])} = `) || '';
function getValueString (data) {
switch (DICOMWeb.getString(data['0040A040'])) { // ValueType
case 'CODE':
const conceptCode = getNestedObject(data['0040A168']);
const conceptCodeValue = DICOMWeb.getString(conceptCode['00080100']);
const conceptCodeMeaning = DICOMWeb.getString(conceptCode['00080104']);
const schemeDesignator = DICOMWeb.getString(conceptCode['00080102']);
return `${conceptCodeMeaning} (${conceptCodeValue}, ${schemeDesignator})`;
case 'PNAME':
return DICOMWeb.getName(data['0040A123']);
case 'TEXT':
return DICOMWeb.getString(data['0040A160']);
case 'UIDREF':
return DICOMWeb.getString(data['0040A124']);
case 'NUM':
const numValue = DICOMWeb.getString(getNestedObject(data['0040A300'])['0040A30A']);
const codeValue = DICOMWeb.getString(getNestedObject(getNestedObject(data['0040A300'])['004008EA'])['00080100']);
return `${numValue} ${codeValue}`;
}
}
function constructPlainValue(data) {
const value = getValueString(data);
if (value) {
return getRelationshipString(data) + getMeaningString(data) + value;
}
}
function constructContentSequence(data, header) {
if (!data['0040A730'].Value) {
return;
}
const items = data['0040A730'].Value.map(item => parseContent(item)).filter(item => item);
if (items.length) {
const result = {
items
};
if (header) {
result.header = header;
}
return result;
}
}
/**
* Recursively parses content sequence for structured reports
*
* @param instance The instance
* @returns {Array} Series List
*/
function parseContent(instance) {
if (instance['0040A040']) { // ValueType
if (DICOMWeb.getString(instance['0040A040']) === 'CONTAINER') {
const header = DICOMWeb.getString(getNestedObject(instance['0040A043'])['00080104']); // TODO: check with real data
return constructContentSequence(instance, header);
}
return constructPlainValue(instance);
}
if (instance['0040A730']) { //ContentSequence
return constructContentSequence(instance);
}
}
function getModality(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
return modality || (!!instance['0040A730'] && 'SR') || undefined; // FIXME: dirty, dirty hack, we use
}
function getContentDateTime(instance) {
const date = DICOMWeb.getString(instance['00080023']);
const time = DICOMWeb.getString(instance['00080033']);
if (date && time) {
return `${date.substr(0, 4)}-${date.substr(4, 2)}-${date.substr(6, 2)} ${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}`;
}
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param server
* @param studyInstanceUid
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
if (!resultData.length) {
return;
}
const anInstance = resultData[0];
if (!anInstance) {
return;
}
const studyData = {
seriesList: [],
studyInstanceUid,
wadoUriRoot: server.wadoUriRoot,
patientName: DICOMWeb.getName(anInstance['00100010']),
patientId: DICOMWeb.getString(anInstance['00100020']),
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
studyDate: DICOMWeb.getString(anInstance['00080020']),
modalities: DICOMWeb.getString(anInstance['00080061']),
studyDescription: DICOMWeb.getString(anInstance['00081030']),
imageCount: DICOMWeb.getString(anInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
institutionName: DICOMWeb.getString(anInstance['00080080'])
};
const seriesMap = {};
await Promise.all(resultData.map(async function(instance) {
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = seriesMap[seriesInstanceUid];
const modality = getModality(instance);
if (!series) {
series = {
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality,
seriesInstanceUid: seriesInstanceUid,
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: []
};
seriesMap[seriesInstanceUid] = series;
studyData.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
const baseWadoRsUri = buildInstanceWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
const wadorsuri = buildInstanceFrameWadoRsUri(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
const instanceSummary = {
contentSequence: parseContent(instance),
completionFlag: DICOMWeb.getString(instance['0040A491']),
manufacturer: DICOMWeb.getString(instance['00080070']),
verificationFlag: DICOMWeb.getString(instance['0040A493']),
contentDateTime: getContentDateTime(instance),
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality,
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: wadouri,
wadorsuri: wadorsuri,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(DICOMWeb.getString(instance['00281101']));
const greenPaletteColorLookupTableDescriptor = parseFloatArray(DICOMWeb.getString(instance['00281102']));
const bluePaletteColorLookupTableDescriptor = parseFloatArray(DICOMWeb.getString(instance['00281103']));
const palettes = await getPaletteColors(server, instance, redPaletteColorLookupTableDescriptor);
if (palettes) {
if (palettes.uid) {
instanceSummary.paletteColorLookupTableUID = palettes.uid;
}
instanceSummary.redPaletteColorLookupTableData = palettes.red;
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(instanceSummary);
}));
return studyData;
}
/**
* Retrieve Study MetaData from a DICOM server using a WADO call
*
* @param server
* @param studyInstanceUid
* @returns {Promise}
*/
OHIF.studies.services.WADO.RetrieveMetadata = async function(server, studyInstanceUid) {
const config = {
url: server.wadoRoot,
headers: OHIF.DICOMWeb.getAuthorizationHeader()
};
const dicomWeb = new DICOMwebClient.api.DICOMwebClient(config);
const options = {
studyInstanceUID: studyInstanceUid
};
return dicomWeb.retrieveStudyMetadata(options).then(result => {
return resultDataToStudyMetadata(server, studyInstanceUid, result);
});
};