A command-line utility for searching DICOM studies on a remote SCP. Supports all query levels (PATIENT, STUDY, SERIES, IMAGE) and multiple output formats (table, JSON, CSV).
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
namespace {
constexpr const char* default_calling_ae = "QUERY_SCU";
constexpr auto default_timeout = std::chrono::milliseconds{30000};
struct options {
std::string host;
uint16_t port{0};
std::string called_ae;
std::string calling_ae{default_calling_ae};
std::string query_model{"study"};
std::string patient_name;
std::string patient_id;
std::string patient_birth_date;
std::string patient_sex;
std::string study_date;
std::string study_time;
std::string accession_number;
std::string study_uid;
std::string study_id;
std::string study_description;
std::string modality;
std::string series_uid;
std::string sop_instance_uid;
bool verbose{false};
size_t max_results{0};
};
void print_usage(const char* program_name) {
std::cout << R"(
Query SCU - DICOM C-FIND Client
Usage: )" << program_name << R"( <host> <port> <called_ae> [options]
Arguments:
host Remote host address (IP or hostname)
port Remote port number (typically 104 or 11112)
called_ae Called AE Title (remote SCP's AE title)
Query Options:
--level <level> Query level: PATIENT, STUDY, SERIES, IMAGE (default: STUDY)
--model <model> Query model: patient, study (default: study)
Search Criteria:
--patient-name <name> Patient name (wildcards: * ?)
--patient-id <id> Patient ID
--patient-birth-date <date> Patient birth date (YYYYMMDD)
--patient-sex <sex> Patient sex (M, F, O)
--study-date <date> Study date (YYYYMMDD or range YYYYMMDD-YYYYMMDD)
--study-time <time> Study time (HHMMSS or range)
--accession-number <num> Accession number
--study-uid <uid> Study Instance UID
--study-id <id> Study ID
--study-description <desc> Study description
--modality <mod> Modality (CT, MR, US, XR, etc.)
--series-uid <uid> Series Instance UID
--sop-instance-uid <uid> SOP Instance UID
Output Options:
--format <fmt> Output format: table, json, csv (default: table)
--max-results <n> Maximum results to display (default: unlimited)
--calling-ae <ae> Calling AE Title (default: QUERY_SCU)
--verbose, -v Show detailed progress
--help, -h Show this help message
Examples:
)" << program_name << R"( localhost 11112 PACS_SCP --level PATIENT --patient-name "Smith*"
)" << program_name << R"( localhost 11112 PACS_SCP --level STUDY --patient-id "12345" --study-date "20240101-20241231"
)" << program_name << R"( localhost 11112 PACS_SCP --level SERIES --study-uid "1.2.3.4.5" --format json
)" << program_name << R"( localhost 11112 PACS_SCP --modality CT --format csv > results.csv
Exit Codes:
0 Success - Query completed
1 Error - Query failed or no results
2 Error - Invalid arguments or connection failure
)";
}
std::optional<kcenon::pacs::services::query_level> parse_level(std::string_view level_str) {
if (level_str == "PATIENT" || level_str == "patient") {
}
if (level_str == "STUDY" || level_str == "study") {
}
if (level_str == "SERIES" || level_str == "series") {
}
if (level_str == "IMAGE" || level_str == "image" ||
level_str == "INSTANCE" || level_str == "instance") {
}
return std::nullopt;
}
bool parse_arguments(int argc, char* argv[], options& opts) {
if (argc < 4) {
return false;
}
opts.host = argv[1];
try {
int port_int = std::stoi(argv[2]);
if (port_int < 1 || port_int > 65535) {
std::cerr << "Error: Port must be between 1 and 65535\n";
return false;
}
opts.port = static_cast<uint16_t>(port_int);
} catch (const std::exception&) {
std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
return false;
}
opts.called_ae = argv[3];
if (opts.called_ae.length() > 16) {
std::cerr << "Error: Called AE title exceeds 16 characters\n";
return false;
}
for (int i = 4; i < argc; ++i) {
std::string arg = argv[i];
if ((arg == "--help" || arg == "-h")) {
return false;
}
if (arg == "--verbose" || arg == "-v") {
} else if (arg == "--level" && i + 1 < argc) {
auto level = parse_level(argv[++i]);
if (!level) {
std::cerr << "Error: Invalid query level '" << argv[i] << "'\n";
return false;
}
opts.level = *level;
} else if (arg == "--model" && i + 1 < argc) {
opts.query_model = argv[++i];
if (opts.query_model != "patient" && opts.query_model != "study") {
std::cerr << "Error: Invalid query model (use 'patient' or 'study')\n";
return false;
}
} else if (arg == "--patient-name" && i + 1 < argc) {
opts.
patient_name = argv[++i];
} else if (arg == "--patient-id" && i + 1 < argc) {
opts.
patient_id = argv[++i];
} else if (arg == "--patient-birth-date" && i + 1 < argc) {
opts.patient_birth_date = argv[++i];
} else if (arg == "--patient-sex" && i + 1 < argc) {
opts.patient_sex = argv[++i];
} else if (arg == "--study-date" && i + 1 < argc) {
opts.study_date = argv[++i];
} else if (arg == "--study-time" && i + 1 < argc) {
opts.study_time = argv[++i];
} else if (arg == "--accession-number" && i + 1 < argc) {
opts.accession_number = argv[++i];
} else if (arg == "--study-uid" && i + 1 < argc) {
opts.
study_uid = argv[++i];
} else if (arg == "--study-id" && i + 1 < argc) {
opts.study_id = argv[++i];
} else if (arg == "--study-description" && i + 1 < argc) {
opts.study_description = argv[++i];
} else if (arg == "--modality" && i + 1 < argc) {
opts.
modality = argv[++i];
} else if (arg == "--series-uid" && i + 1 < argc) {
opts.
series_uid = argv[++i];
} else if (arg == "--sop-instance-uid" && i + 1 < argc) {
opts.sop_instance_uid = argv[++i];
} else if (arg == "--format" && i + 1 < argc) {
} else if (arg == "--max-results" && i + 1 < argc) {
try {
opts.max_results = static_cast<size_t>(std::stoul(argv[++i]));
} catch (const std::exception&) {
std::cerr << "Error: Invalid max-results value\n";
return false;
}
} else if (arg == "--calling-ae" && i + 1 < argc) {
opts.calling_ae = argv[++i];
if (opts.calling_ae.length() > 16) {
std::cerr << "Error: Calling AE title exceeds 16 characters\n";
return false;
}
} else {
std::cerr << "Error: Unknown option '" << arg << "'\n";
return false;
}
}
return true;
}
std::string_view get_find_sop_class_uid(const std::string& model) {
if (model == "patient") {
}
}
int perform_query(const options& opts) {
auto sop_class_uid = get_find_sop_class_uid(opts.query_model);
if (opts.verbose) {
std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
std::cout << " Calling AE: " << opts.calling_ae << "\n";
std::cout << " Called AE: " << opts.called_ae << "\n";
std::cout << " Query Model: " << opts.query_model << " root\n";
std::cout << " Query Level: " << to_string(opts.level) << "\n\n";
}
1,
std::string(sop_class_uid),
{
"1.2.840.10008.1.2.1",
"1.2.840.10008.1.2"
}
});
auto start_time = std::chrono::steady_clock::now();
auto connect_result = association::connect(opts.host, opts.port, config, default_timeout);
if (connect_result.is_err()) {
std::cerr << "Failed to establish association: "
<< connect_result.error().message << "\n";
return 2;
}
auto& assoc = connect_result.value();
auto connect_time = std::chrono::steady_clock::now();
if (opts.verbose) {
auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
connect_time - start_time);
std::cout << "Association established in " << connect_duration.count() << " ms\n";
}
if (!assoc.has_accepted_context(sop_class_uid)) {
std::cerr << "Error: Query SOP Class not accepted by remote SCP\n";
assoc.abort();
return 2;
}
auto context_id_opt = assoc.accepted_context_id(sop_class_uid);
if (!context_id_opt) {
std::cerr << "Error: Could not get presentation context ID\n";
assoc.abort();
return 2;
}
uint8_t context_id = *context_id_opt;
auto find_rq = make_c_find_rq(1, sop_class_uid);
find_rq.set_dataset(std::move(query_ds));
if (opts.verbose) {
std::cout << "Sending C-FIND request...\n";
}
auto send_result = assoc.send_dimse(context_id, find_rq);
if (send_result.is_err()) {
std::cerr << "Failed to send C-FIND: " << send_result.error().message << "\n";
assoc.abort();
return 2;
}
std::vector<kcenon::pacs::core::dicom_dataset> results;
bool query_complete = false;
size_t pending_count = 0;
while (!query_complete) {
auto recv_result = assoc.receive_dimse(default_timeout);
if (recv_result.is_err()) {
std::cerr << "Failed to receive C-FIND response: "
<< recv_result.error().message << "\n";
assoc.abort();
return 2;
}
auto& [recv_context_id, find_rsp] = recv_result.value();
if (find_rsp.command() != command_field::c_find_rsp) {
std::cerr << "Error: Unexpected response (expected C-FIND-RSP)\n";
assoc.abort();
return 2;
}
auto status = find_rsp.status();
if (status == status_pending || status == status_pending_warning) {
++pending_count;
if (find_rsp.has_dataset()) {
if (opts.max_results == 0 || results.size() < opts.max_results) {
auto dataset_result = find_rsp.dataset();
if (dataset_result.is_ok()) {
results.push_back(dataset_result.value().get());
}
}
}
if (opts.verbose && pending_count % 10 == 0) {
std::cout << "\rReceived " << pending_count << " results..." << std::flush;
}
} else if (status == status_success) {
query_complete = true;
if (opts.verbose) {
std::cout << "\rQuery completed successfully.\n";
}
} else if (status == status_cancel) {
query_complete = true;
std::cerr << "Query was cancelled.\n";
} else {
query_complete = true;
std::cerr << "Query failed with status: 0x"
<< std::hex << status << std::dec << "\n";
}
}
if (opts.verbose) {
std::cout << "Releasing association...\n";
}
auto release_result = assoc.release(default_timeout);
if (release_result.is_err() && opts.verbose) {
std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
}
auto end_time = std::chrono::steady_clock::now();
auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time);
std::cout << formatter.
format(results);
std::cout << "\n========================================\n";
std::cout << " Summary\n";
std::cout << "========================================\n";
std::cout << " Query level: " << to_string(opts.level) << "\n";
std::cout << " Total results: " << results.size();
if (opts.max_results > 0 && pending_count > opts.max_results) {
std::cout << " (limited from " << pending_count << ")";
}
std::cout << "\n";
std::cout << " Query time: " << total_duration.count() << " ms\n";
std::cout << "========================================\n";
}
return results.empty() ? 1 : 0;
}
}
int main(
int argc,
char* argv[]) {
bool show_banner = true;
for (int i = 1; i < argc; ++i) {
if (std::string(argv[i]) == "--format" && i + 1 < argc) {
std::string fmt = argv[i + 1];
if (fmt == "json" || fmt == "csv") {
show_banner = false;
}
break;
}
}
if (show_banner) {
std::cout << R"(
___ _ _ _____ ______ __ ____ ____ _ _
/ _ \| | | | ____| _ \ \ / / / ___| / ___| | | |
| | | | | | | _| | |_) \ V / \___ \| | | | | |
| |_| | |_| | |___| _ < | | ___) | |___| |_| |
\__\_\\___/|_____|_| \_\|_| |____/ \____|\___/
DICOM C-FIND Client
)" << "\n";
}
options opts;
if (!parse_arguments(argc, argv, opts)) {
print_usage(argv[0]);
return 2;
}
return perform_query(opts);
}
DICOM Association management per PS3.8.
query_builder & modality(std::string_view mod)
Set modality criteria.
query_builder & patient_birth_date(std::string_view date)
Set patient birth date criteria.
query_builder & study_date(std::string_view date)
Set study date criteria (supports ranges)
query_builder & study_instance_uid(std::string_view uid)
Set study instance UID criteria.
query_builder & study_id(std::string_view id)
Set study ID criteria.
query_builder & sop_instance_uid(std::string_view uid)
Set SOP instance UID criteria.
query_builder & patient_sex(std::string_view sex)
Set patient sex criteria.
query_builder & patient_name(std::string_view name)
Set patient name search criteria (supports wildcards)
query_builder & series_instance_uid(std::string_view uid)
Set series instance UID criteria.
query_builder & accession_number(std::string_view accession)
Set accession number criteria.
query_builder & patient_id(std::string_view id)
Set patient ID search criteria.
kcenon::pacs::core::dicom_dataset build() const
Build the query dataset.
query_builder & study_description(std::string_view desc)
Set study description criteria.
query_builder & study_time(std::string_view time)
Set study time criteria.
query_builder & level(query_level lvl)
Set the query/retrieve level.
DIMSE message encoding and decoding.
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
constexpr std::string_view patient_root_find_sop_class_uid
Patient Root Query/Retrieve Information Model - FIND.
query_level
DICOM Query/Retrieve level enumeration.
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
output_format
Output format enumeration.
@ table
Human-readable table format.
output_format parse_output_format(std::string_view format_str)
Parse output format from string.
DICOM Query Dataset Builder.
DICOM Query SCP service (C-FIND handler)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::string implementation_class_uid
std::string implementation_version_name
std::vector< proposed_presentation_context > proposed_contexts