DICOM services -- client side ============================= .. highlight:: c++ :abbr:`SCUs (Service Class User)` (i.e. *clients*) are available for `C-ECHO`_, `C-FIND`_, `C-GET`_, `C-MOVE`_, and `C-STORE`_, with an abstract base class, `odil::SCU`_, implementing common features and concrete classes for each service (see table below). Every service requires an association which is correctly associated and has negotiated the corresponding abstract syntax. .. table:: DICOM services and Odil classes +----------+-------------------+ | Service | Odil class | +==========+===================+ | C-ECHO | `odil::EchoSCU`_ | +----------+-------------------+ | C-FIND | `odil::FindSCU`_ | +----------+-------------------+ | C-GET | `odil::GetSCU`_ | +----------+-------------------+ | C-MOVE | `odil::MoveSCU`_ | +----------+-------------------+ | C-STORE | `odil::StoreSCU`_ | +----------+-------------------+ Verifying DICOM communication ----------------------------- The simplest service is C-ECHO, used to verify that the two peers can exchange information using DICOM. :: #include #include #include int main() { odil::Association association; association.set_peer_host("www.dicomserver.co.uk"); association.set_peer_port(11112); association.update_parameters() .set_calling_ae_title("WORKSTATION") .set_called_ae_title("SERVER") .set_presentation_contexts({ { odil::registry::Verification, { odil::registry::ExplicitVRLittleEndian }, odil::AssociationParameters::PresentationContext::Role::SCU } }); association.associate(); odil::EchoSCU echo_scu(association); echo_scu.echo(); association.release(); } Querying a database ------------------- The query service, C-FIND, is parameterized by a query and either returns all the matching data sets or calls a function for each match. :: #include #include #include #include void print_informations(odil::DataSet const & response) { auto const name = (response.has("PatientName") && !response.empty("PatientName")) ?response.as_string("PatientName", 0):"(no name)"; auto const study = (response.has("StudyDescription") && !response.empty("StudyDescription")) ?response.as_string("StudyDescription", 0):"(no description)"; std::cout << name << ": " << study << "\n"; } int main() { auto const model = odil::registry::StudyRootQueryRetrieveInformationModelFind; odil::Association association; association.set_peer_host("www.dicomserver.co.uk"); association.set_peer_port(11112); association.update_parameters() .set_calling_ae_title("WORKSTATION") .set_called_ae_title("SERVER") .set_presentation_contexts({ { model, { odil::registry::ExplicitVRLittleEndian }, odil::AssociationParameters::PresentationContext::Role::SCU } }); association.associate(); odil::DataSet query; query.add("PatientName", { "*" }); query.add("QueryRetrieveLevel", { "STUDY" }); query.add("StudyDescription"); query.add("StudyDate"); odil::FindSCU find_scu(association); find_scu.set_affected_sop_class(model); auto const result = find_scu.find(query); for(auto const & dataset: result) { print_informations(dataset); } find_scu.find(query, print_informations); association.release(); } Retrieving data sets -------------------- The retrieval of data sets, using either C-GET or C-MOVE, is very similar to querying a database. The main difference is the additional presentation contexts required to transfer the data sets. :: #include #include #include #include #include #include #include odil::DataSet find( std::string const & host, unsigned int port, std::string const & calling_aet, std::string const & called_aet) { auto const model = odil::registry::StudyRootQueryRetrieveInformationModelFind; odil::Association association; association.set_peer_host(host); association.set_peer_port(port); association.update_parameters() .set_calling_ae_title(calling_aet).set_called_ae_title(called_aet) .set_presentation_contexts({ { model, { odil::registry::ExplicitVRLittleEndian }, odil::AssociationParameters::PresentationContext::Role::SCU } }); association.associate(); odil::DataSet query; query.add("QueryRetrieveLevel", { "STUDY" }); query.add("StudyInstanceUID"); query.add("SOPClassesInStudy"); query.add("StudyDate"); odil::FindSCU scu(association); scu.set_affected_sop_class(model); auto const studies = scu.find(query); association.release(); if(studies.empty()) { throw std::runtime_error("No matching studies"); } return studies[0]; } std::vector get_study( std::string const & host, unsigned int port, std::string const & calling_aet, std::string const & called_aet, odil::DataSet const & study) { auto const model = odil::registry::StudyRootQueryRetrieveInformationModelGet; odil::Association association; association.set_peer_host(host); association.set_peer_port(port); association.update_parameters() .set_calling_ae_title(calling_aet).set_called_ae_title(called_aet); std::vector contexts{ { model, { odil::registry::ExplicitVRLittleEndian }, odil::AssociationParameters::PresentationContext::Role::SCU } }; for(auto const & abstract_syntax: study.as_string("SOPClassesInStudy")) { contexts.push_back({ abstract_syntax, { odil::registry::ExplicitVRLittleEndian }, odil::AssociationParameters::PresentationContext::Role::SCP }); } association.update_parameters().set_presentation_contexts(contexts); association.associate(); odil::DataSet query; query.add("QueryRetrieveLevel", { "STUDY" }); query.add("StudyInstanceUID", study.as_string("StudyInstanceUID")); odil::GetSCU scu(association); scu.set_affected_sop_class(model); auto const data_sets = scu.get(query); association.release(); return data_sets; } int main() { std::string const host = "www.dicomserver.co.uk"; unsigned int port = 11112; std::string const calling_aet = "WORKSTATION"; std::string const called_aet = "SERVER"; auto const study = find(host, port, calling_aet, called_aet); auto const data_sets = get_study( host, port, calling_aet, called_aet, study); std::cout << data_sets.size() << " data set" << (data_sets.size()>0?"s":"") << " " << "received\n"; } Note that several presentation contexts must be specified: the C-GET context and one for each type of object returned by C-FIND in the *SOP Classes In Study* element. The C-GET SCU can also be called using one or two callbacks: one which will be called for each C-STORE operation initiated by the server and an optional one which will be called for each C-GET response. The latter one may for example be used for progress information. The C-MOVE SCU is similar to the C-GET SCU in terms of required presentation contexts (the *XXX Root Query Retrieve Information Model GET* being replaced by *XXX Root Query Retrieve Information Model MOVE*) and callbacks. The C-MOVE SCU has additional member functions used to specify where the remote peer will send the data sets: ``get_move_destination`` and ``set_move_destination``. If the move destination is the local peer and not a third party, the port of a temporary C-STORE SCP may be specified through ``set_incoming_port``. The default value of ``0`` means that no C-STORE SCP should be started. Storing data sets ----------------- The C-STORE SCU differs from the query/retrieve SCUs since it has no callback: each data set is either stored successfully or an exception is raised. An optional move origin (if the C-STORE SCU is created by a C-MOVE SCP) may be specified. .. _C-ECHO: http://dicom.nema.org/medical/dicom/current/output/chtml/part04/chapter_A.html .. _C-FIND: http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1 .. _C-GET: http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.3.html .. _C-MOVE: http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html .. _C-STORE: http://dicom.nema.org/medical/dicom/current/output/chtml/part04/chapter_B.html .. _odil::EchoSCU: ../../_static/doxygen/classodil_1_1EchoSCU.html .. _odil::FindSCU: ../../_static/doxygen/classodil_1_1FindSCU.html .. _odil::GetSCU: ../../_static/doxygen/classodil_1_1GetSCU.html .. _odil::MoveSCU: ../../_static/doxygen/classodil_1_1MoveSCU.html .. _odil::SCU: ../../_static/doxygen/classodil_1_1SCU.html .. _odil::StoreSCU: ../../_static/doxygen/classodil_1_1StoreSCU.html