From 94076f6263c6ecdfd5bd7595a73223bdcdcd19bf Mon Sep 17 00:00:00 2001 From: Weiwei Date: Mon, 8 Feb 2016 09:14:13 -0600 Subject: [PATCH] orthanc proxy --- Packages/orthanc-remote/package.js | 15 ++ Packages/orthanc-remote/server/lib.js | 115 +++++++++++++++ Packages/orthanc-remote/server/namespace.js | 4 + Packages/orthanc-remote/server/require.js | 1 + Packages/viewerbase/server/lib/namespace.js | 7 +- .../server/services/remote/instances.js | 82 +++++++++++ .../services/remote/retrieveMetadata.js | 133 ++++++++++++++++++ .../server/services/remote/studies.js | 47 +++++++ 8 files changed, 403 insertions(+), 1 deletion(-) create mode 100755 Packages/orthanc-remote/package.js create mode 100755 Packages/orthanc-remote/server/lib.js create mode 100755 Packages/orthanc-remote/server/namespace.js create mode 100755 Packages/orthanc-remote/server/require.js create mode 100755 Packages/viewerbase/server/services/remote/instances.js create mode 100755 Packages/viewerbase/server/services/remote/retrieveMetadata.js create mode 100755 Packages/viewerbase/server/services/remote/studies.js diff --git a/Packages/orthanc-remote/package.js b/Packages/orthanc-remote/package.js new file mode 100755 index 000000000..f840bc078 --- /dev/null +++ b/Packages/orthanc-remote/package.js @@ -0,0 +1,15 @@ +Package.describe({ + name: "orthancremote", + summary: "Orthanc Remote", + version: '0.0.1' +}); + +Package.onUse(function (api) { + api.use("http"); + + api.addFiles('server/require.js', 'server'); + api.addFiles('server/namespace.js', 'server'); + api.addFiles('server/lib.js', 'server'); + + api.export("OrthancRemote", 'server'); +}); diff --git a/Packages/orthanc-remote/server/lib.js b/Packages/orthanc-remote/server/lib.js new file mode 100755 index 000000000..773a5c74e --- /dev/null +++ b/Packages/orthanc-remote/server/lib.js @@ -0,0 +1,115 @@ + +OrthancRemote.prototype.findStudies = function(modality, params) { + return this.query(modality, 'Study', params); +} + +OrthancRemote.prototype.findSeries = function(modality, studyId, params) { + params = Object.assign({StudyInstanceUID : studyId || ''}, params); + + return this.query(modality, 'Series', params); +} + +OrthancRemote.prototype.findInstances = function(modality, studyId, seriesId, params) { + params = Object.assign({StudyInstanceUID : studyId || '', SeriesInstanceUID : seriesId || ''}, params); + + return this.query(modality, 'Instance', params); +} + +OrthancRemote.prototype.query = function(modality, level, params) { + var url = this.root + '/modalities/' + modality + '/query', + postData = {Level : level, Query : params || {}}; + + var result = HTTP.call('POST', url, {content : JSON.stringify(postData)}); + return this.getAnswersFromQuery(result.data['ID']); +} + +OrthancRemote.prototype.getAnswersFromQuery = function(id) { + var url = this.root + '/queries/' + id + '/answers', + result = HTTP.call('GET', url); + + var resultCount = result.data.length, answers = [], i = 0; + while (i < resultCount) { + var contentUrl = this.root + '/queries/' + id + '/answers/' + result.data[i] + '/content'; + //fetch the result + var contentResult = HTTP.call('GET', contentUrl); + answers.push(contentResult.data); + i++; + } + return { + query : id, + length : resultCount, + results : answers + }; +} + +OrthancRemote.prototype.retrieveAnswer = function(query, index) { + var retrieveUrl = this.root + '/queries/' + query + '/answers/' + index + '/retrieve'; + + var result = HTTP.call('POST', retrieveUrl, {content : this.localAE}); + + return result.data; +} + +OrthancRemote.prototype.retrieveStudy = function(modality, studyInstanceUID) { + var id = this.findStudyID(studyInstanceUID); + if (id === null) { + //retrieve the study + console.log('RetrieveStudy', studyInstanceUID); + var result = this.findStudies(modality, {"StudyInstanceUID" : studyInstanceUID}); + if (result.results.length < 1) { + throw new Meteor.Error(404, "No such study"); + } + + this.retrieveAnswer(result.query, 0); + //check again + id = this.findStudyID(studyInstanceUID); + if (id === null) { + throw new Meteor.Error(500, "Failed to retrieve remote study"); + } + } + return id; +} + +OrthancRemote.prototype.retrieveMetadata = function(modality, studyInstanceUID) { + var studyId = this.retrieveStudy(modality, studyInstanceUID), + instances = this.getInstancesByStudy(studyId); + + var results = [], length = instances.length; + for (var i = 0;i < length;i++) { + var instanceId = instances[i].ID, metadata = this.getInstanceMetadata(instanceId); + metadata['xxxx,0001'] = {Value : instanceId}; + + results.push(metadata); + } + + return results; +} + +OrthancRemote.prototype.getInstancesByStudy = function(studyId) { + var url = this.root + '/studies/' + studyId + '/instances'; + + var result = this._getCall(url); + return result; +} + +OrthancRemote.prototype.getInstanceMetadata = function(instanceId) { + var url = this.root + '/instances/' + instanceId + '/tags'; + + return this._getCall(url); +} + +OrthancRemote.prototype.findStudyID = function(uid) { + var url = this.root + '/tools/lookup'; + + var result = HTTP.call('POST', url, {content : uid}), len = result.data.length; + for (var i = 0;i < len;i++) { + if (result.data[i].Type == 'Study') { + return result.data[i].ID; + } + } + return null; +} + +OrthancRemote.prototype._getCall = function(url) { + return HTTP.call('GET', url).data; +} \ No newline at end of file diff --git a/Packages/orthanc-remote/server/namespace.js b/Packages/orthanc-remote/server/namespace.js new file mode 100755 index 000000000..f7b777743 --- /dev/null +++ b/Packages/orthanc-remote/server/namespace.js @@ -0,0 +1,4 @@ +OrthancRemote = function(root, localAE){ + this.root = root || 'http://localhost:8042'; + this.localAE = localAE || 'ORTHANCLOCAL'; +}; \ No newline at end of file diff --git a/Packages/orthanc-remote/server/require.js b/Packages/orthanc-remote/server/require.js new file mode 100755 index 000000000..8cbb4eced --- /dev/null +++ b/Packages/orthanc-remote/server/require.js @@ -0,0 +1 @@ +//http = Npm.require("http"); \ No newline at end of file diff --git a/Packages/viewerbase/server/lib/namespace.js b/Packages/viewerbase/server/lib/namespace.js index c99f7a3b6..8c8432d33 100644 --- a/Packages/viewerbase/server/lib/namespace.js +++ b/Packages/viewerbase/server/lib/namespace.js @@ -1,4 +1,9 @@ Services = {}; Services.QIDO = {}; Services.WADO = {}; -Services.DIMSE = {}; \ No newline at end of file +Services.DIMSE = {}; +Services.REMOTE = {}; + +remoteGetValue = function(obj) { + return obj ? obj.Value : null; +} diff --git a/Packages/viewerbase/server/services/remote/instances.js b/Packages/viewerbase/server/services/remote/instances.js new file mode 100755 index 000000000..ca20abe34 --- /dev/null +++ b/Packages/viewerbase/server/services/remote/instances.js @@ -0,0 +1,82 @@ +/** + * Parses data returned from a QIDO search and transforms it into + * an array of series that are present in the study + * + * @param server The DICOM server + * @param studyInstanceUid + * @param resultData + * @returns {Array} Series List + */ +function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { + var seriesMap = {}; + var seriesList = []; + + resultData.forEach(function(instance) { + // Use seriesMap to cache series data + // If the series instance UID has already been used to + // process series data, continue using that series + var seriesInstanceUid = remoteGetValue(instance['0020,000e']); + var series = seriesMap[seriesInstanceUid]; + + // If no series data exists in the seriesMap cache variable, + // process any available series data + if(!series) { + series = { + seriesInstanceUid : seriesInstanceUid, + seriesNumber : parseFloat(remoteGetValue(instance['0020,0011'])), + instances: [] + }; + + // Save this data in the seriesMap cache variable + seriesMap[seriesInstanceUid] = series; + seriesList.push(series); + } + + // The uri for the dicomweb + // NOTE: DCM4CHEE seems to return the data zipped + // NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't + // know how to parse yet + //var uri = DICOMWeb.getString(instance['00081190']); + //uri = uri.replace('wado-rs', 'dicom-web'); + + // manually create a WADO-URI from the UIDs + // NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured? + var sopInstanceUid = remoteGetValue(instance['0008,0018']); + var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom"; + + // Add this instance to the current series + series.instances.push({ + sopClassUid: remoteGetValue(instance['0008,0016']), + sopInstanceUid: sopInstanceUid, + uri: uri, + instanceNumber: parseFloat(remoteGetValue(instance['0020,0013'])) + }); + }); + return seriesList; +} + +/** + * Retrieve a set of instances using a QIDO call + * @param server + * @param studyInstanceUid + * @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}} + */ +Services.REMOTE.Instances = function(server, studyInstanceUid) { + var parameters = { + PatientName: "", + PatientID: "", + AccessionNumber: "", + SeriesInstanceUID: "", + SeriesNumber : "", + SOPClassUID : "", + InstanceNumber : "" + }; + + var remote = new OrthancRemote(server.root, server.sourceAE); + + return { + wadoUriRoot: server.wadoUriRoot, + studyInstanceUid: studyInstanceUid, + seriesList: resultDataToStudyMetadata(server, studyInstanceUid, remote.findInstances(server.modality, studyInstanceUid, null, parameters)) + }; +}; \ No newline at end of file diff --git a/Packages/viewerbase/server/services/remote/retrieveMetadata.js b/Packages/viewerbase/server/services/remote/retrieveMetadata.js new file mode 100755 index 000000000..3acc10184 --- /dev/null +++ b/Packages/viewerbase/server/services/remote/retrieveMetadata.js @@ -0,0 +1,133 @@ +/** + * 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 = remoteGetValue(instance['0008,2112']); + if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) { + return SourceImageSequence.Value[0]['0008,1155'].Value[0]; + } +} + +/** + * 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: *}} + */ +function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { + var seriesMap = {}; + var seriesList = []; + + if (!resultData.length) { + return; + } + + var anInstance = resultData[0]; + if (!anInstance) { + return; + } + + var studyData = { + seriesList: seriesList, + patientName: remoteGetValue(anInstance['0010,0010']), + patientId: remoteGetValue(anInstance['0010,0020']), + accessionNumber: remoteGetValue(anInstance['0008,0050']), + studyDate: remoteGetValue(anInstance['0008,0020']), + modalities: remoteGetValue(anInstance['0008,0061']), + studyDescription: remoteGetValue(anInstance['0008,1030']), + imageCount: remoteGetValue(anInstance['0020,1208']), + studyInstanceUid: remoteGetValue(anInstance['0020,000d']) + }; + + resultData.forEach(function(instance) { + var seriesInstanceUid = remoteGetValue(instance['0020,000e']); + var series = seriesMap[seriesInstanceUid]; + if (!series) { + series = { + seriesDescription: remoteGetValue(instance['0008,103e']), + modality: remoteGetValue(instance['0008,0060']), + seriesInstanceUid: seriesInstanceUid, + seriesNumber: parseFloat(remoteGetValue(instance['0020,0011'])), + instances: [] + }; + seriesMap[seriesInstanceUid] = series; + seriesList.push(series); + } + + var sopInstanceUid = remoteGetValue(instance['0008,0018']); + + var instanceSummary = { + imageType: remoteGetValue(instance['0008,0008']), + sopClassUid: remoteGetValue(instance['0008,0016']), + sopInstanceUid: sopInstanceUid, + instanceNumber: parseFloat(remoteGetValue(instance['0020,0013'])), + imagePositionPatient: remoteGetValue(instance['0020,0032']), + imageOrientationPatient: remoteGetValue(instance['0020,0037']), + frameOfReferenceUID: remoteGetValue(instance['0020,0052']), + sliceLocation: parseFloat(remoteGetValue(instance['0020,1041'])), + samplesPerPixel: parseFloat(remoteGetValue(instance['0028,0002'])), + photometricInterpretation: remoteGetValue(instance['0028,0004']), + rows: parseFloat(remoteGetValue(instance['0028,0010'])), + columns: parseFloat(remoteGetValue(instance['0028,0011'])), + pixelSpacing: remoteGetValue(instance['0028,0030']), + bitsAllocated: parseFloat(remoteGetValue(instance['0028,0100'])), + bitsStored: parseFloat(remoteGetValue(instance['0028,0101'])), + highBit: parseFloat(remoteGetValue(instance['0028,0102'])), + pixelRepresentation: parseFloat(remoteGetValue(instance['0028,0103'])), + windowCenter: remoteGetValue(instance['0028,1050']), + windowWidth: remoteGetValue(instance['0028,1051']), + rescaleIntercept: parseFloat(remoteGetValue(instance['0028,1052'])), + rescaleSlope: parseFloat(remoteGetValue(instance['0028,1053'])), + sourceImageInstanceUid: getSourceImageInstanceUid(instance), + laterality: remoteGetValue(instance['0020,0062']), + viewPosition: remoteGetValue(instance['0018,5101']), + numFrames: parseFloat(remoteGetValue(instance['0028,0008'])), + frameRate: parseFloat(remoteGetValue(instance['0018,1063'])) + }; + + var iid = instance['xxxx,0001'].Value; + if (server.imageRendering === 'wadouri') { + instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom"; + } else if (server.imageRendering == 'orthanc') { + instanceSummary.wadouri = server.root + '/instances/' + iid + '/file'; + } else { + instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1'; + } + + series.instances.push(instanceSummary); + }); +console.log(studyData.seriesList[0].instances); + return studyData; +} + +/** + * Retrieved Study MetaData from a DICOM server using a WADO call + * @param server + * @param studyInstanceUid + * @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}} + */ +Services.REMOTE.RetrieveMetadata = function(server, studyInstanceUid) { + var remote = new OrthancRemote(server.root, server.sourceAE); + + var study = resultDataToStudyMetadata(server, studyInstanceUid, remote.retrieveMetadata(server.modality, studyInstanceUid)); + if (!study) { + study = {}; + } + + study.wadoUriRoot = server.wadoUriRoot; + study.studyInstanceUid = studyInstanceUid; + + return study; +}; \ No newline at end of file diff --git a/Packages/viewerbase/server/services/remote/studies.js b/Packages/viewerbase/server/services/remote/studies.js new file mode 100755 index 000000000..f2ca3e5c2 --- /dev/null +++ b/Packages/viewerbase/server/services/remote/studies.js @@ -0,0 +1,47 @@ +function resultDataToStudies(resultData) { + var studies = []; + + resultData.forEach(function(study) { + studies.push({ + studyInstanceUid: remoteGetValue(study['0020,000d']), + // 00080005 = SpecificCharacterSet + studyDate: remoteGetValue(study['0008,0020']), + studyTime: remoteGetValue(study['0008,0030']), + accessionNumber: remoteGetValue(study['0008,0050']), + referringPhysicianName: remoteGetValue(study['0008,0090']), + // 00081190 = URL + patientName: remoteGetValue(study['0010,0010']), + patientId: remoteGetValue(study['0010,0020']), + patientBirthdate: remoteGetValue(study['0010,0030']), + patientSex: remoteGetValue(study['0010,0040']), + studyId: remoteGetValue(study['0020,0010']), + numberOfStudyRelatedSeries: parseFloat(remoteGetValue(study['0020,1206'])), + numberOfStudyRelatedInstances: parseFloat(remoteGetValue(study['0020,1208'])), + studyDescription: remoteGetValue(study['0008,1030']), + modalities: remoteGetValue(study['0008,0061']) + }); + }); + return studies; +} + +Services.REMOTE.Studies = function(server, filter) { + var parameters = { + PatientName: filter.patientName ? filter.patientName : "", + PatientID: filter.patientId, + AccessionNumber: filter.accessionNumber ? filter.accessionNumber : "", + StudyDescription: "", + StudyDate : "", + StudyTime : "", + ReferringPhysicianName : "", + PatientBirthDate : "", + PatientSex : "", + StudyID : "", + NumberOfStudyRelatedSeries : "", + NumberOfStudyRelatedInstances : "", + ModalitiesInStudy : "" + }; + var remote = new OrthancRemote(server.root, server.sourceAE); + var data = remote.findStudies(server.modality, parameters); + + return resultDataToStudies(data.results); +}; \ No newline at end of file