Mercurial > hg > orthanc
changeset 1371:f528849ee9f7 query-retrieve
DICOM Query/Retrieve available from Orthanc Explorer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 27 May 2015 17:33:13 +0200 |
parents | 7b6f5115607f |
children | 1412ec22ee0b |
files | NEWS OrthancExplorer/explorer.html OrthancExplorer/query-retrieve.js OrthancServer/DicomProtocol/DicomUserConnection.cpp OrthancServer/DicomProtocol/DicomUserConnection.h OrthancServer/OrthancMoveRequestHandler.cpp |
diffstat | 6 files changed, 441 insertions(+), 10 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed May 27 12:32:43 2015 +0200 +++ b/NEWS Wed May 27 17:33:13 2015 +0200 @@ -4,6 +4,8 @@ Major ----- +* DICOM Query/Retrieve available from Orthanc Explorer +* C-Move SCU and C-Find SCU are accessible through the REST API * "?expand" flag for URIs "/patients", "/studies" and "/series" * "/tools/find" URI to search for DICOM resources from REST * Support of FreeBSD
--- a/OrthancExplorer/explorer.html Wed May 27 12:32:43 2015 +0200 +++ b/OrthancExplorer/explorer.html Wed May 27 17:33:13 2015 +0200 @@ -30,6 +30,7 @@ <link rel="stylesheet" href="explorer.css" /> <script src="file-upload.js"></script> <script src="explorer.js"></script> + <script src="query-retrieve.js"></script> <script src="../plugins/explorer.js"></script> </head> <body> @@ -37,7 +38,10 @@ <div data-role="header" > <h1><span class="orthanc-name"></span>Find a patient</h1> <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <ul id="all-patients" data-role="listview" data-inset="true" data-filter="true"> @@ -75,7 +79,10 @@ <div data-role="header" > <h1><span class="orthanc-name"></span>Patient</h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -129,7 +136,10 @@ Study </h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -178,7 +188,10 @@ </h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -228,7 +241,10 @@ Instance </h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -284,6 +300,90 @@ </div> </div> + <div data-role="page" id="query-retrieve" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>DICOM Query/Retrieve (1/3)</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + </div> + <div data-role="content"> + <form data-ajax="false"> + <div data-role="fieldcontain"> + <label for="qr-server">DICOM server:</label> + <select name="qr-server" id="qr-server"> + </select> + </div> + + <div data-role="fieldcontain" id="qr-fields"> + <fieldset data-role="controlgroup"> + <legend>Field of interest:</legend> + <input type="radio" name="qr-field" id="qr-patient-id" value="PatientID" checked="checked" /> + <label for="qr-patient-id">Patient ID</label> + <input type="radio" name="qr-field" id="qr-patient-name" value="PatientName" /> + <label for="qr-patient-name">Patient Name</label> + <input type="radio" name="qr-field" id="qr-accession-number" value="AccessionNumber" /> + <label for="qr-accession-number">Accession Number</label> + <input type="radio" name="qr-field" id="qr-study-description" value="StudyDescription" /> + <label for="qr-study-description">Study Description</label> + </fieldset> + </div> + + <div data-role="fieldcontain"> + <label for="qr-value">Value for this field:</label> + <input type="text" name="qr-value" id="qr-value" value="*" /> + </div> + + <div data-role="fieldcontain"> + <label for="qr-date">Study date:</label> + <select name="qr-date" id="qr-date"> + </select> + </div> + + <div data-role="fieldcontain" id="qr-modalities"> + <div data-role="fieldcontain"> + <fieldset data-role="controlgroup" data-type="horizontal"> + <legend>Modalities:</legend> + <input type="checkbox" name="CR" id="qr-cr" class="custom" /> <label for="qr-cr">CR</label> + <input type="checkbox" name="CT" id="qr-ct" class="custom" /> <label for="qr-ct">CT</label> + <input type="checkbox" name="MR" id="qr-mr" class="custom" /> <label for="qr-mr">MR</label> + <input type="checkbox" name="NM" id="qr-nm" class="custom" /> <label for="qr-nm">NM</label> + <input type="checkbox" name="PT" id="qr-pt" class="custom" /> <label for="qr-pt">PT</label> + <input type="checkbox" name="US" id="qr-us" class="custom" /> <label for="qr-us">US</label> + <input type="checkbox" name="XA" id="qr-xa" class="custom" /> <label for="qr-xa">XA</label> + </fieldset> + </div> + </div> + + <button id="qr-submit" type="submit" data-theme="b">Search studies</button> + </form> + </div> + </div> + + + <div data-role="page" id="query-retrieve-2" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>DICOM Query/Retrieve (2/3)</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a> + </div> + <div data-role="content"> + <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b"> + </ul> + </div> + </div> + + + <div data-role="page" id="query-retrieve-3" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>DICOM Query/Retrieve (3/3)</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a> + </div> + <div data-role="content"> + <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b"> + </ul> + </div> + </div> + <div id="peer-store" style="display:none;" class="ui-body-c"> <p align="center"><b>Sending to Orthanc peer...</b></p> @@ -295,6 +395,11 @@ <p><img src="libs/images/ajax-loader2.gif" alt="" /></p> </div> + <div id="info-retrieve" style="display:none;" class="ui-body-c"> + <p align="center"><b>Retrieving images from DICOM modality...</b></p> + <p><img src="libs/images/ajax-loader2.gif" alt="" /></p> + </div> + <div id="dialog" style="display:none" > </div> </body>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancExplorer/query-retrieve.js Wed May 27 17:33:13 2015 +0200 @@ -0,0 +1,265 @@ +function JavascriptDateToDicom(date) +{ + var s = date.toISOString(); + return s.substring(0, 4) + s.substring(5, 7) + s.substring(8, 10); +} + +function GenerateDicomDate(days) +{ + var today = new Date(); + var other = new Date(today); + other.setDate(today.getDate() + days); + return JavascriptDateToDicom(other); +} + + +$('#query-retrieve').live('pagebeforeshow', function() { + $.ajax({ + url: '../modalities', + dataType: 'json', + async: false, + cache: false, + success: function(modalities) { + var target = $('#qr-server'); + $('option', target).remove(); + + for (var i = 0; i < modalities.length; i++) { + var option = $('<option>').text(modalities[i]); + target.append(option); + } + + target.selectmenu('refresh'); + } + }); + + var target = $('#qr-date'); + $('option', target).remove(); + target.append($('<option>').attr('value', '*').text('Any date')); + target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today')); + target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday')); + target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days')); + target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days')); + target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months')); + target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year')); + target.selectmenu('refresh'); +}); + + +$('#qr-submit').live('click', function() { + var query = { + 'Level' : 'Study', + 'Query' : { + 'AccessionNumber' : '*', + 'PatientBirthDate' : '*', + 'PatientID' : '*', + 'PatientName' : '*', + 'PatientSex' : '*', + 'SpecificCharacterSet' : 'ISO_IR 192', // UTF-8 + 'StudyDate' : $('#qr-date').val(), + 'StudyDescription' : '*' + } + }; + + var field = $('#qr-fields input:checked').val(); + query['Query'][field] = $('#qr-value').val().toUpperCase(); + + var modalities = ''; + $('#qr-modalities input:checked').each(function() { + var s = $(this).attr('name'); + if (modalities == '*') + modalities = s; + else + modalities += '\\' + s; + }); + + if (modalities.length > 0) { + query['Query']['ModalitiesInStudy'] = modalities; + } + + + var server = $('#qr-server').val(); + $.ajax({ + url: '../modalities/' + server + '/query', + type: 'POST', + data: JSON.stringify(query), + dataType: 'json', + async: false, + error: function() { + alert('Error during query (C-Find)'); + }, + success: function(result) { + window.location.assign('explorer.html#query-retrieve-2?server=' + server + '&uuid=' + result['ID']); + } + }); + + return false; +}); + + + +function Retrieve(url) +{ + $.ajax({ + url: '../system', + dataType: 'json', + async: false, + success: function(system) { + $('<div>').simpledialog2({ + mode: 'button', + headerText: 'Retrieve destination', + headerClose: true, + buttonPrompt: 'Enter Application Entity Title (AET):', + buttonInput: true, + buttonInputDefault: system['DicomAet'], + buttons : { + 'OK': { + click: function () { + var aet = $.mobile.sdLastInput; + if (aet.length == 0) + aet = system['DicomAet']; + + $.ajax({ + url: url, + type: 'POST', + async: true, // Necessary to block UI + dataType: 'text', + data: aet, + beforeSend: function() { + $.blockUI({ message: $('#info-retrieve') }); + }, + complete: function(s) { + $.unblockUI(); + }, + error: function() { + alert('Error during retrieve'); + } + }); + } + } + } + }); + } + }); +} + + + + +$('#query-retrieve-2').live('pagebeforeshow', function() { + if ($.mobile.pageData) { + var uri = '../queries/' + $.mobile.pageData.uuid + '/answers'; + + $.ajax({ + url: uri, + dataType: 'json', + async: false, + success: function(answers) { + var target = $('#query-retrieve-2 ul'); + $('li', target).remove(); + + for (var i = 0; i < answers.length; i++) { + $.ajax({ + url: uri + '/' + answers[i] + '/content?simplify', + dataType: 'json', + async: false, + success: function(study) { + var series = '#query-retrieve-3?server=' + $.mobile.pageData.server + '&uuid=' + study['StudyInstanceUID']; + var info = $('<a>').attr('href', series).html( + ('<h3>{0} - {1}</h3>' + + '<p>Accession number: <b>{2}</b></p>' + + '<p>Birth date: <b>{3}</b></p>' + + '<p>Patient sex: <b>{4}</b></p>' + + '<p>Study description: <b>{5}</b></p>' + + '<p>Study date: <b>{6}</b></p>').format( + study['PatientID'], + study['PatientName'], + study['AccessionNumber'], + FormatDicomDate(study['PatientBirthDate']), + study['PatientSex'], + study['StudyDescription'], + FormatDicomDate(study['StudyDate']))); + + var studyUri = uri + '/' + answers[i] + '/retrieve'; + var retrieve = $('<a>').text('Retrieve').click(function() { + Retrieve(studyUri); + }); + + target.append($('<li>').append(info).append(retrieve)); + } + }); + } + + target.listview('refresh'); + } + }); + } +}); + + +$('#query-retrieve-3').live('pagebeforeshow', function() { + if ($.mobile.pageData) { + var query = { + 'Level' : 'Series', + 'Query' : { + 'Modality' : '*', + 'ProtocolName' : '*', + 'SeriesDescription' : '*', + 'SeriesInstanceUID' : '*', + 'StudyInstanceUID' : $.mobile.pageData.uuid + } + }; + + $.ajax({ + url: '../modalities/' + $.mobile.pageData.server + '/query', + type: 'POST', + data: JSON.stringify(query), + dataType: 'json', + async: false, + error: function() { + alert('Error during query (C-Find)'); + }, + success: function(answer) { + var uri = '../queries/' + answer['ID'] + '/answers'; + + $.ajax({ + url: uri, + dataType: 'json', + async: false, + success: function(answers) { + + var target = $('#query-retrieve-3 ul'); + $('li', target).remove(); + + for (var i = 0; i < answers.length; i++) { + $.ajax({ + url: uri + '/' + answers[i] + '/content?simplify', + dataType: 'json', + async: false, + success: function(series) { + var info = $('<a>').html( + ('<h3>{0}</h3>' + + '<p>Modality: <b>{1}</b></p>' + + '<p>Protocol name: <b>{2}</b></p>' + ).format( + series['SeriesDescription'], + series['Modality'], + series['ProtocolName'] + )); + + var seriesUri = uri + '/' + answers[i] + '/retrieve'; + var retrieve = $('<a>').text('Retrieve').click(function() { + Retrieve(seriesUri); + }); + + target.append($('<li>').append(info).append(retrieve)); + } + }); + } + + target.listview('refresh'); + } + }); + } + }); + } +});
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed May 27 12:32:43 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed May 27 17:33:13 2015 +0200 @@ -399,6 +399,13 @@ throw OrthancException(ErrorCode_InternalError); } + if (level == ResourceType_Study) + { + allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + } + + allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); + DicomArray query(fields); for (size_t i = 0; i < query.GetSize(); i++) { @@ -533,6 +540,7 @@ void DicomUserConnection::MoveInternal(const std::string& targetAet, + ResourceType level, const DicomMap& fields) { CheckIsOpen(); @@ -540,6 +548,39 @@ const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(fields)); + switch (level) + { + case ResourceType_Patient: + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT"); + break; + + case ResourceType_Study: + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY"); + break; + + case ResourceType_Series: + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES"); + break; + + case ResourceType_Instance: + if (manufacturer_ == ModalityManufacturer_ClearCanvas || + manufacturer_ == ModalityManufacturer_Dcm4Chee) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>. + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); if (presID == 0) @@ -880,7 +921,7 @@ throw OrthancException(ErrorCode_InternalError); } - MoveInternal(targetAet, move); + MoveInternal(targetAet, level, move); } @@ -889,7 +930,7 @@ { DicomMap query; query.SetValue(DICOM_TAG_PATIENT_ID, patientId); - MoveInternal(targetAet, query); + MoveInternal(targetAet, ResourceType_Patient, query); } void DicomUserConnection::MoveStudy(const std::string& targetAet, @@ -897,7 +938,7 @@ { DicomMap query; query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); - MoveInternal(targetAet, query); + MoveInternal(targetAet, ResourceType_Study, query); } void DicomUserConnection::MoveSeries(const std::string& targetAet, @@ -907,7 +948,7 @@ DicomMap query; query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid); - MoveInternal(targetAet, query); + MoveInternal(targetAet, ResourceType_Series, query); } void DicomUserConnection::MoveInstance(const std::string& targetAet, @@ -919,7 +960,7 @@ query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid); query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid); - MoveInternal(targetAet, query); + MoveInternal(targetAet, ResourceType_Instance, query); }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h Wed May 27 12:32:43 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Wed May 27 17:33:13 2015 +0200 @@ -65,6 +65,7 @@ void SetupPresentationContexts(const std::string& preferredTransferSyntax); void MoveInternal(const std::string& targetAet, + ResourceType level, const DicomMap& fields); void ResetStorageSOPClasses();
--- a/OrthancServer/OrthancMoveRequestHandler.cpp Wed May 27 12:32:43 2015 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Wed May 27 17:33:13 2015 +0200 @@ -36,6 +36,8 @@ #include <glog/logging.h> #include "OrthancInitialization.h" +#include "FromDcmtkBridge.h" +#include "../Core/DicomFormat/DicomArray.h" namespace Orthanc { @@ -132,6 +134,21 @@ { LOG(WARNING) << "Move-SCU request received for AET \"" << aet << "\""; + { + DicomArray query(input); + for (size_t i = 0; i < query.GetSize(); i++) + { + if (!query.GetElement(i).GetValue().IsNull()) + { + LOG(INFO) << " " << query.GetElement(i).GetTag() + << " " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag()) + << " = " << query.GetElement(i).GetValue().AsString(); + } + } + } + + + /** * Retrieve the query level. **/