37 return "image_display";
39 return "window_level";
41 return "patient_info";
53 if (str ==
"image_display") {
56 if (str ==
"window_level") {
59 if (str ==
"patient_info") {
62 if (str ==
"acquisition") {
65 if (str ==
"positioning") {
68 if (str ==
"multiframe") {
79 return "instance_number";
81 return "acquisition_time";
87 if (str ==
"position") {
90 if (str ==
"instance_number") {
93 if (str ==
"acquisition_time") {
109 std::ostringstream oss;
110 oss << std::hex << std::uppercase << std::setfill(
'0') << std::setw(4)
118std::optional<kcenon::pacs::core::dicom_tag> hex_to_tag(std::string_view hex) {
119 if (hex.size() != 8) {
125 static_cast<uint16_t
>(std::stoul(std::string(hex.substr(0, 4)),
nullptr, 16));
127 static_cast<uint16_t
>(std::stoul(std::string(hex.substr(4, 4)),
nullptr, 16));
137std::vector<double> parse_numeric_list(std::string_view str) {
138 std::vector<double> result;
140 std::istringstream iss(s);
142 while (std::getline(iss, token,
'\\')) {
144 result.push_back(std::stod(token));
155std::vector<std::string> parse_string_list(std::string_view str) {
156 std::vector<std::string> result;
158 std::istringstream iss(s);
160 while (std::getline(iss, token,
'\\')) {
161 result.push_back(token);
173 std::shared_ptr<storage::index_database> database)
174 : database_(std::move(database)) {}
186 std::unordered_set<std::string> tags;
192 tags.insert(tag_to_hex(rows));
193 tags.insert(tag_to_hex(columns));
194 tags.insert(tag_to_hex(bits_allocated));
195 tags.insert(tag_to_hex(bits_stored));
196 tags.insert(tag_to_hex(high_bit));
197 tags.insert(tag_to_hex(pixel_representation));
198 tags.insert(tag_to_hex(photometric_interpretation));
199 tags.insert(tag_to_hex(samples_per_pixel));
204 tags.insert(tag_to_hex(window_center));
205 tags.insert(tag_to_hex(window_width));
206 tags.insert(tag_to_hex(rescale_slope));
207 tags.insert(tag_to_hex(rescale_intercept));
209 tags.insert(
"00281055");
211 tags.insert(
"00283010");
216 tags.insert(tag_to_hex(patient_name));
217 tags.insert(tag_to_hex(patient_id));
218 tags.insert(tag_to_hex(patient_birth_date));
219 tags.insert(tag_to_hex(patient_sex));
220 tags.insert(tag_to_hex(patient_age));
227 tags.insert(
"00180060");
228 tags.insert(
"00181150");
229 tags.insert(
"00181151");
230 tags.insert(
"00180050");
231 tags.insert(
"00180088");
237 tags.insert(tag_to_hex(image_position_patient));
238 tags.insert(tag_to_hex(image_orientation_patient));
239 tags.insert(tag_to_hex(slice_location));
240 tags.insert(tag_to_hex(pixel_spacing));
246 tags.insert(
"00280008");
247 tags.insert(
"00280009");
248 tags.insert(
"00181063");
266 auto instance =
database_->find_instance(sop_instance_uid);
272 if (!std::filesystem::exists(instance->file_path)) {
277 std::unordered_set<std::string> requested_tags;
280 if (request.
preset.has_value()) {
282 requested_tags.insert(preset_tags.begin(), preset_tags.end());
286 for (
const auto& tag : request.
tags) {
287 requested_tags.insert(tag);
291 if (requested_tags.empty()) {
293 "No tags specified: provide 'tags' or 'preset' parameter");
304 std::string_view file_path,
305 const std::unordered_set<std::string>& requested_tags,
306 bool include_private) {
307 std::unordered_map<std::string, std::string> result;
311 if (file_result.is_err()) {
315 const auto& dataset = file_result.value().dataset();
318 for (
const auto& tag_hex : requested_tags) {
319 auto tag_opt = hex_to_tag(tag_hex);
320 if (!tag_opt.has_value()) {
324 auto tag = tag_opt.value();
332 const auto* elem = dataset.get(tag);
333 if (elem !=
nullptr) {
334 auto str_result = elem->as_string();
335 if (str_result.is_ok()) {
336 result[tag_hex] = str_result.value();
349 std::string_view sop_instance_uid) {
354 auto instance =
database_->find_instance(sop_instance_uid);
360 auto series =
database_->find_series_by_pk(instance->series_pk);
365 return series->series_uid;
369 std::string_view series_uid,
sort_order order,
bool ascending) {
375 auto instances_result =
database_->list_instances(series_uid);
376 if (instances_result.is_err()) {
380 auto& instances = instances_result.value();
381 if (instances.empty()) {
386 std::vector<sorted_instance> sorted;
387 sorted.reserve(instances.size());
389 for (
const auto& inst : instances) {
395 if (std::filesystem::exists(inst.file_path)) {
398 if (file_result.is_ok()) {
399 const auto& ds = file_result.value().dataset();
403 if (!slice_str.empty()) {
413 if (!pos_str.empty()) {
419 if (!time_str.empty()) {
425 sorted.push_back(std::move(si));
439 if (a.slice_location.has_value()) {
440 a_pos = a.slice_location.value();
441 }
else if (a.image_position_patient.has_value() &&
442 a.image_position_patient->size() >= 3) {
443 a_pos = (*a.image_position_patient)[2];
446 if (b.slice_location.has_value()) {
447 b_pos = b.slice_location.value();
448 }
else if (b.image_position_patient.has_value() &&
449 b.image_position_patient->size() >= 3) {
450 b_pos = (*b.image_position_patient)[2];
453 result = a_pos < b_pos;
458 int a_num = a.instance_number.value_or(0);
459 int b_num = b.instance_number.value_or(0);
460 result = a_num < b_num;
465 std::string a_time = a.acquisition_time.value_or(
"");
466 std::string b_time = b.acquisition_time.value_or(
"");
467 result = a_time < b_time;
472 return ascending ? result : !result;
475 std::sort(sorted.begin(), sorted.end(), compare);
481 std::string_view sop_instance_uid) {
488 if (!series_uid_opt.has_value()) {
495 if (!sorted_result.success) {
499 const auto& instances = sorted_result.instances;
500 if (instances.empty()) {
505 size_t current_index = 0;
507 for (
size_t i = 0; i < instances.size(); ++i) {
508 if (instances[i].sop_instance_uid == sop_instance_uid) {
521 nav.
index = current_index;
522 nav.
total = instances.size();
523 nav.
first = instances.front().sop_instance_uid;
524 nav.
last = instances.back().sop_instance_uid;
526 if (current_index > 0) {
527 nav.
previous = instances[current_index - 1].sop_instance_uid;
530 if (current_index < instances.size() - 1) {
531 nav.
next = instances[current_index + 1].sop_instance_uid;
542 std::string_view modality) {
543 std::vector<window_level_preset> presets;
545 if (modality ==
"CT") {
546 presets.push_back({
"Lung", -600, 1500});
547 presets.push_back({
"Bone", 300, 1500});
548 presets.push_back({
"Soft Tissue", 40, 400});
549 presets.push_back({
"Brain", 40, 80});
550 presets.push_back({
"Liver", 60, 150});
551 presets.push_back({
"Mediastinum", 50, 350});
552 }
else if (modality ==
"MR") {
553 presets.push_back({
"T1 Brain", 600, 1200});
554 presets.push_back({
"T2 Brain", 700, 1400});
555 presets.push_back({
"Spine", 500, 1000});
556 }
else if (modality ==
"CR" || modality ==
"DX") {
557 presets.push_back({
"Default", 2048, 4096});
558 presets.push_back({
"Bone", 1500, 3000});
559 presets.push_back({
"Soft Tissue", 1800, 3600});
560 }
else if (modality ==
"US") {
561 presets.push_back({
"Default", 128, 256});
564 presets.push_back({
"Default", 128, 256});
575 auto instance =
database_->find_instance(sop_instance_uid);
580 if (!std::filesystem::exists(instance->file_path)) {
586 if (file_result.is_err()) {
590 const auto& ds = file_result.value().dataset();
596 if (!wc_str.empty()) {
597 info.window_center = parse_numeric_list(wc_str);
602 if (!ww_str.empty()) {
603 info.window_width = parse_numeric_list(ww_str);
607 const auto* we_elem =
609 if (we_elem !=
nullptr) {
610 auto we_result = we_elem->as_string();
611 if (we_result.is_ok()) {
612 info.window_explanations = parse_string_list(we_result.value());
618 if (!rs_str.empty()) {
620 info.rescale_slope = std::stod(rs_str);
627 if (!ri_str.empty()) {
629 info.rescale_intercept = std::stod(ri_str);
646 auto instance =
database_->find_instance(sop_instance_uid);
651 if (!std::filesystem::exists(instance->file_path)) {
657 if (file_result.is_err()) {
661 const auto& ds = file_result.value().dataset();
667 if (nf_elem !=
nullptr) {
668 auto nf_result = nf_elem->as_string();
669 if (nf_result.is_ok()) {
671 info.total_frames =
static_cast<uint32_t
>(std::stoul(nf_result.value()));
673 info.total_frames = 1;
680 if (ft_elem !=
nullptr) {
681 auto ft_result = ft_elem->as_string();
682 if (ft_result.is_ok()) {
684 info.frame_time = std::stod(ft_result.value());
685 if (info.frame_time.has_value() && info.frame_time.value() > 0) {
686 info.frame_rate = 1000.0 / info.frame_time.value();
695 if (rows_opt.has_value()) {
696 info.rows = rows_opt.value();
701 if (cols_opt.has_value()) {
702 info.columns = cols_opt.value();
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.
constexpr auto is_private() const noexcept -> bool
Check if this is a private tag.
constexpr auto group() const noexcept -> uint16_t
Get the group number.
constexpr auto element() const noexcept -> uint16_t
Get the element number.
DICOM Dataset - ordered collection of Data Elements.
DICOM Data Element representation (Tag, VR, Value)
DICOM Part 10 file handling for reading/writing DICOM files.
DICOM Tag representation (Group, Element pairs)
Compile-time constants for commonly used DICOM tags.
PACS index database for metadata storage and retrieval.
sort_order
Sort order for series instances.
@ instance_number
Sort by InstanceNumber.
@ position
Sort by ImagePositionPatient/SliceLocation.
@ acquisition_time
Sort by AcquisitionTime.
std::string_view preset_to_string(metadata_preset preset)
Convert preset enum to string.
std::optional< sort_order > sort_order_from_string(std::string_view str)
Parse sort order from string.
std::optional< metadata_preset > preset_from_string(std::string_view str)
Parse preset from string.
metadata_preset
Metadata preset types for common use cases.
@ image_display
Rows, Columns, Bits, PhotometricInterpretation.
@ patient_info
Patient demographics.
@ acquisition
KVP, ExposureTime, SliceThickness.
@ positioning
ImagePosition, ImageOrientation, PixelSpacing.
@ multiframe
NumberOfFrames, FrameTime.
@ window_level
WindowCenter, WindowWidth, Rescale values.
std::string_view sort_order_to_string(sort_order order)
Convert sort order enum to string.
static frame_info ok()
Create a success result.
static frame_info error(std::string message)
Create an error result.
Navigation info for an instance.
std::string first
First instance UID.
static navigation_info error(std::string message)
Create an error result.
std::string next
Next instance UID (empty if last)
std::string last
Last instance UID.
size_t total
Total instances in series.
size_t index
Current index (0-based)
std::string previous
Previous instance UID (empty if first)
static navigation_info ok()
Create a success result.
Instance info for series navigation.
std::optional< std::vector< double > > image_position_patient
Image Position Patient (if available)
std::optional< double > slice_location
Slice location (if available)
std::string sop_instance_uid
SOP Instance UID.
std::optional< std::string > acquisition_time
Acquisition time (if available)
std::optional< int > instance_number
Instance number (if available)
Response for sorted instances query.
static sorted_instances_response ok(std::vector< sorted_instance > inst, size_t count)
Create a success result.
static sorted_instances_response error(std::string message)
Create an error result.
VOI LUT information from DICOM.
static voi_lut_info error(std::string message)
Create an error result.
static voi_lut_info ok()
Create a success result.