PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
28
29#include <algorithm>
30#include <chrono>
31#include <cstdio>
32#include <cstring>
33#include <filesystem>
34#include <fstream>
35#include <iomanip>
36#include <iostream>
37#include <random>
38#include <sstream>
39#include <string>
40#include <vector>
41
42#ifdef PACS_JPEG_FOUND
43#include <jpeglib.h>
44#endif
45
46namespace {
47
49constexpr std::string_view kSecondaryCaptureUID = "1.2.840.10008.5.1.4.1.1.7";
50
52constexpr std::string_view kSecondaryCaptureColorUID = "1.2.840.10008.5.1.4.1.1.7.4";
53
58std::string generate_uid() {
59 static uint64_t counter = 0;
60 auto now = std::chrono::system_clock::now();
61 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
62 now.time_since_epoch())
63 .count();
64 return std::string("1.2.826.0.1.3680043.8.1055.4.") + std::to_string(timestamp) +
65 "." + std::to_string(++counter);
66}
67
71struct image_data {
72 std::vector<uint8_t> pixels;
73 uint16_t width{0};
74 uint16_t height{0};
75 uint16_t bits_allocated{8};
76 uint16_t bits_stored{8};
77 uint16_t samples_per_pixel{1};
78 std::string photometric_interpretation{"MONOCHROME2"};
79 bool is_valid{false};
80};
81
85struct options {
86 std::filesystem::path input_path;
87 std::filesystem::path output_path;
88 std::string patient_name{"ANONYMOUS"};
89 std::string patient_id;
90 std::string study_description{"Imported Image"};
91 std::string series_description{"Secondary Capture"};
92 std::string modality{"OT"}; // Other
93 bool recursive{false};
94 bool overwrite{false};
95 bool verbose{false};
96 bool quiet{false};
97 std::string transfer_syntax;
98};
99
103struct conversion_stats {
104 size_t total_files{0};
105 size_t success_count{0};
106 size_t skip_count{0};
107 size_t error_count{0};
108 std::chrono::milliseconds total_time{0};
109};
110
115void print_usage(const char* program_name) {
116 std::cout << R"(
117Image to DICOM - Image Conversion Utility
118
119Usage: )" << program_name
120 << R"( <input> <output> [options]
121
122Arguments:
123 input Input image file (JPEG) or directory
124 output Output DICOM file or directory
125
126Patient/Study Options:
127 --patient-name <name> Patient name (default: ANONYMOUS)
128 --patient-id <id> Patient ID (auto-generated if not specified)
129 --study-description <desc> Study description (default: Imported Image)
130 --series-description <desc> Series description (default: Secondary Capture)
131 --modality <mod> Modality (default: OT)
132
133Processing Options:
134 -r, --recursive Process directory recursively
135 --overwrite Overwrite existing output files
136 -v, --verbose Verbose output
137 -q, --quiet Minimal output (errors only)
138
139Transfer Syntax Options:
140 --explicit Explicit VR Little Endian (default)
141 --implicit Implicit VR Little Endian
142
143Information:
144 -h, --help Show this help message
145
146Supported Input Formats:
147 - JPEG (.jpg, .jpeg) - Requires libjpeg-turbo
148
149Examples:
150 )" << program_name
151 << R"( photo.jpg output.dcm
152 )" << program_name
153 << R"( photo.jpg output.dcm --patient-name "DOE^JOHN" --patient-id "12345"
154 )" << program_name
155 << R"( ./images/ ./dicom/ --recursive
156
157Exit Codes:
158 0 Success - All files converted successfully
159 1 Error - Invalid arguments
160 2 Error - Conversion failed for one or more files
161)";
162}
163
171bool parse_arguments(int argc, char* argv[], options& opts) {
172 using namespace kcenon::pacs::encoding;
173
174 if (argc < 2) {
175 return false;
176 }
177
178 // Default to Explicit VR Little Endian
179 opts.transfer_syntax = std::string(transfer_syntax::explicit_vr_little_endian.uid());
180
181 for (int i = 1; i < argc; ++i) {
182 std::string arg = argv[i];
183
184 if (arg == "--help" || arg == "-h") {
185 return false;
186 } else if (arg == "--patient-name" && i + 1 < argc) {
187 opts.patient_name = argv[++i];
188 } else if (arg == "--patient-id" && i + 1 < argc) {
189 opts.patient_id = argv[++i];
190 } else if (arg == "--study-description" && i + 1 < argc) {
191 opts.study_description = argv[++i];
192 } else if (arg == "--series-description" && i + 1 < argc) {
193 opts.series_description = argv[++i];
194 } else if (arg == "--modality" && i + 1 < argc) {
195 opts.modality = argv[++i];
196 } else if (arg == "--explicit") {
197 opts.transfer_syntax = std::string(transfer_syntax::explicit_vr_little_endian.uid());
198 } else if (arg == "--implicit") {
199 opts.transfer_syntax = std::string(transfer_syntax::implicit_vr_little_endian.uid());
200 } else if (arg == "-r" || arg == "--recursive") {
201 opts.recursive = true;
202 } else if (arg == "--overwrite") {
203 opts.overwrite = true;
204 } else if (arg == "-v" || arg == "--verbose") {
205 opts.verbose = true;
206 } else if (arg == "-q" || arg == "--quiet") {
207 opts.quiet = true;
208 } else if (arg[0] == '-') {
209 std::cerr << "Error: Unknown option '" << arg << "'\n";
210 return false;
211 } else if (opts.input_path.empty()) {
212 opts.input_path = arg;
213 } else if (opts.output_path.empty()) {
214 opts.output_path = arg;
215 } else {
216 std::cerr << "Error: Too many arguments\n";
217 return false;
218 }
219 }
220
221 if (opts.input_path.empty()) {
222 std::cerr << "Error: No input path specified\n";
223 return false;
224 }
225
226 if (opts.output_path.empty()) {
227 std::cerr << "Error: No output path specified\n";
228 return false;
229 }
230
231 // Quiet mode overrides verbose
232 if (opts.quiet) {
233 opts.verbose = false;
234 }
235
236 return true;
237}
238
243std::string generate_patient_id() {
244 static const char charset[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
245 static std::random_device rd;
246 static std::mt19937 gen(rd());
247 static std::uniform_int_distribution<size_t> dist(0, sizeof(charset) - 2);
248
249 std::string id;
250 id.reserve(8);
251 for (int i = 0; i < 8; ++i) {
252 id += charset[dist(gen)];
253 }
254 return id;
255}
256
261std::string get_current_date() {
262 auto now = std::chrono::system_clock::now();
263 auto time_t_now = std::chrono::system_clock::to_time_t(now);
264 std::tm tm_now{};
265#ifdef _WIN32
266 localtime_s(&tm_now, &time_t_now);
267#else
268 localtime_r(&time_t_now, &tm_now);
269#endif
270 std::ostringstream oss;
271 oss << std::put_time(&tm_now, "%Y%m%d");
272 return oss.str();
273}
274
279std::string get_current_time() {
280 auto now = std::chrono::system_clock::now();
281 auto time_t_now = std::chrono::system_clock::to_time_t(now);
282 auto ms = std::chrono::duration_cast<std::chrono::microseconds>(
283 now.time_since_epoch()) %
284 1000000;
285 std::tm tm_now{};
286#ifdef _WIN32
287 localtime_s(&tm_now, &time_t_now);
288#else
289 localtime_r(&time_t_now, &tm_now);
290#endif
291 std::ostringstream oss;
292 oss << std::put_time(&tm_now, "%H%M%S") << "." << std::setfill('0')
293 << std::setw(6) << ms.count();
294 return oss.str();
295}
296
297#ifdef PACS_JPEG_FOUND
303image_data read_jpeg(const std::filesystem::path& file_path) {
304 image_data result;
305
306 FILE* file = fopen(file_path.string().c_str(), "rb");
307 if (file == nullptr) {
308 std::cerr << "Error: Cannot open file: " << file_path << "\n";
309 return result;
310 }
311
312 struct jpeg_decompress_struct cinfo {};
313 struct jpeg_error_mgr jerr {};
314
315 cinfo.err = jpeg_std_error(&jerr);
316 jpeg_create_decompress(&cinfo);
317 jpeg_stdio_src(&cinfo, file);
318
319 if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
320 std::cerr << "Error: Invalid JPEG header: " << file_path << "\n";
321 jpeg_destroy_decompress(&cinfo);
322 fclose(file);
323 return result;
324 }
325
326 // Force RGB output for color images
327 if (cinfo.num_components == 3) {
328 cinfo.out_color_space = JCS_RGB;
329 }
330
331 jpeg_start_decompress(&cinfo);
332
333 result.width = static_cast<uint16_t>(cinfo.output_width);
334 result.height = static_cast<uint16_t>(cinfo.output_height);
335 result.samples_per_pixel = static_cast<uint16_t>(cinfo.output_components);
336 result.bits_allocated = 8;
337 result.bits_stored = 8;
338
339 if (result.samples_per_pixel == 1) {
340 result.photometric_interpretation = "MONOCHROME2";
341 } else if (result.samples_per_pixel == 3) {
342 result.photometric_interpretation = "RGB";
343 }
344
345 size_t row_stride = static_cast<size_t>(cinfo.output_width) * cinfo.output_components;
346 result.pixels.resize(row_stride * cinfo.output_height);
347
348 std::vector<JSAMPROW> row_pointers(cinfo.output_height);
349 for (JDIMENSION row = 0; row < cinfo.output_height; ++row) {
350 row_pointers[row] = result.pixels.data() + row * row_stride;
351 }
352
353 while (cinfo.output_scanline < cinfo.output_height) {
354 jpeg_read_scanlines(&cinfo, &row_pointers[cinfo.output_scanline],
355 cinfo.output_height - cinfo.output_scanline);
356 }
357
358 jpeg_finish_decompress(&cinfo);
359 jpeg_destroy_decompress(&cinfo);
360 fclose(file);
361
362 result.is_valid = true;
363 return result;
364}
365#else
369image_data read_jpeg(const std::filesystem::path& file_path) {
370 image_data result;
371 std::cerr << "Error: JPEG support not available. Install libjpeg-turbo.\n";
372 std::cerr << " File: " << file_path << "\n";
373 return result;
374}
375#endif
376
382image_data read_image(const std::filesystem::path& file_path) {
383 auto ext = file_path.extension().string();
384 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
385
386 if (ext == ".jpg" || ext == ".jpeg") {
387 return read_jpeg(file_path);
388 }
389
390 std::cerr << "Error: Unsupported image format: " << ext << "\n";
391 std::cerr << " Supported formats: JPEG (.jpg, .jpeg)\n";
392 return {};
393}
394
401kcenon::pacs::core::dicom_dataset create_dicom_dataset(const image_data& img,
402 const options& opts) {
403 using namespace kcenon::pacs::core;
404 using namespace kcenon::pacs::encoding;
405
406 dicom_dataset dataset;
407
408 // Generate UIDs
409 std::string study_uid = generate_uid();
410 std::string series_uid = generate_uid();
411 std::string sop_instance_uid = generate_uid();
412
413 // Determine SOP Class based on image type
414 std::string sop_class_uid = (img.samples_per_pixel == 1)
415 ? std::string(kSecondaryCaptureUID)
416 : std::string(kSecondaryCaptureColorUID);
417
418 // Patient Module (M)
419 dataset.set_string(tags::patient_name, vr_type::PN, opts.patient_name);
420 dataset.set_string(tags::patient_id, vr_type::LO,
421 opts.patient_id.empty() ? generate_patient_id() : opts.patient_id);
422 dataset.set_string(dicom_tag{0x0010, 0x0030}, vr_type::DA, ""); // PatientBirthDate
423 dataset.set_string(dicom_tag{0x0010, 0x0040}, vr_type::CS, ""); // PatientSex
424
425 // General Study Module (M)
426 dataset.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
427 dataset.set_string(tags::study_date, vr_type::DA, get_current_date());
428 dataset.set_string(tags::study_time, vr_type::TM, get_current_time());
429 dataset.set_string(dicom_tag{0x0008, 0x0050}, vr_type::SH, ""); // AccessionNumber
430 dataset.set_string(dicom_tag{0x0008, 0x0090}, vr_type::PN, ""); // ReferringPhysicianName
431 dataset.set_string(dicom_tag{0x0020, 0x0010}, vr_type::SH, "1"); // StudyID
432 dataset.set_string(tags::study_description, vr_type::LO, opts.study_description);
433
434 // General Series Module (M)
435 dataset.set_string(tags::series_instance_uid, vr_type::UI, series_uid);
436 dataset.set_string(tags::modality, vr_type::CS, opts.modality);
437 dataset.set_string(dicom_tag{0x0020, 0x0011}, vr_type::IS, "1"); // SeriesNumber
438 dataset.set_string(tags::series_description, vr_type::LO, opts.series_description);
439
440 // SC Equipment Module (M)
441 dataset.set_string(dicom_tag{0x0008, 0x0064}, vr_type::CS, "DV"); // ConversionType
442
443 // General Image Module (M)
444 dataset.set_string(dicom_tag{0x0020, 0x0013}, vr_type::IS, "1"); // InstanceNumber
445 dataset.set_string(dicom_tag{0x0020, 0x0020}, vr_type::CS, ""); // PatientOrientation
446
447 // Image Pixel Module (M)
448 dataset.set_numeric<uint16_t>(tags::samples_per_pixel, vr_type::US, img.samples_per_pixel);
449 dataset.set_string(tags::photometric_interpretation, vr_type::CS, img.photometric_interpretation);
450 dataset.set_numeric<uint16_t>(tags::rows, vr_type::US, img.height);
451 dataset.set_numeric<uint16_t>(tags::columns, vr_type::US, img.width);
452 dataset.set_numeric<uint16_t>(dicom_tag{0x0028, 0x0100}, vr_type::US, img.bits_allocated);
453 dataset.set_numeric<uint16_t>(dicom_tag{0x0028, 0x0101}, vr_type::US, img.bits_stored);
454 dataset.set_numeric<uint16_t>(dicom_tag{0x0028, 0x0102}, vr_type::US,
455 static_cast<uint16_t>(img.bits_stored - 1));
456 dataset.set_numeric<uint16_t>(dicom_tag{0x0028, 0x0103}, vr_type::US, 0); // Unsigned
457
458 // Planar Configuration (only for color images)
459 if (img.samples_per_pixel > 1) {
460 dataset.set_numeric<uint16_t>(dicom_tag{0x0028, 0x0006}, vr_type::US, 0); // Interleaved
461 }
462
463 // SOP Common Module (M)
464 dataset.set_string(tags::sop_class_uid, vr_type::UI, sop_class_uid);
465 dataset.set_string(tags::sop_instance_uid, vr_type::UI, sop_instance_uid);
466
467 // Set pixel data
468 dataset.insert(dicom_element(tags::pixel_data, vr_type::OW, img.pixels));
469
470 return dataset;
471}
472
480bool convert_file(const std::filesystem::path& input_path,
481 const std::filesystem::path& output_path,
482 const options& opts) {
483 using namespace kcenon::pacs::core;
484 using namespace kcenon::pacs::encoding;
485
486 // Check if output exists and overwrite is disabled
487 if (std::filesystem::exists(output_path) && !opts.overwrite) {
488 if (opts.verbose) {
489 std::cout << " Skipped (exists): " << output_path.filename().string() << "\n";
490 }
491 return true;
492 }
493
494 // Read input image
495 auto img = read_image(input_path);
496 if (!img.is_valid) {
497 return false;
498 }
499
500 if (opts.verbose) {
501 std::cout << " Converting: " << input_path.filename().string() << "\n";
502 std::cout << " Size: " << img.width << " x " << img.height << "\n";
503 std::cout << " Components: " << img.samples_per_pixel << "\n";
504 std::cout << " Photometric: " << img.photometric_interpretation << "\n";
505 }
506
507 // Create DICOM dataset
508 auto dataset = create_dicom_dataset(img, opts);
509
510 // Create DICOM file with specified transfer syntax
511 auto ts = transfer_syntax(opts.transfer_syntax);
512 auto dicom_file = dicom_file::create(std::move(dataset), ts);
513
514 // Ensure output directory exists
515 auto output_dir = output_path.parent_path();
516 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
517 std::filesystem::create_directories(output_dir);
518 }
519
520 // Save output file
521 auto save_result = dicom_file.save(output_path);
522 if (save_result.is_err()) {
523 std::cerr << "Error: Failed to save '" << output_path.string()
524 << "': " << save_result.error().message << "\n";
525 return false;
526 }
527
528 if (opts.verbose) {
529 std::cout << " Output: " << output_path.string() << "\n";
530 }
531
532 return true;
533}
534
540bool is_supported_image(const std::filesystem::path& file_path) {
541 auto ext = file_path.extension().string();
542 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
543 return ext == ".jpg" || ext == ".jpeg";
544}
545
553void process_directory(const std::filesystem::path& input_dir,
554 const std::filesystem::path& output_dir,
555 const options& opts,
556 conversion_stats& stats) {
557 auto process_file = [&](const std::filesystem::path& file_path) {
558 if (!is_supported_image(file_path)) {
559 return;
560 }
561
562 ++stats.total_files;
563
564 // Calculate output path with .dcm extension
565 auto relative_path = std::filesystem::relative(file_path, input_dir);
566 auto output_path = output_dir / relative_path;
567 output_path.replace_extension(".dcm");
568
569 auto start = std::chrono::steady_clock::now();
570
571 if (convert_file(file_path, output_path, opts)) {
572 ++stats.success_count;
573 } else {
574 ++stats.error_count;
575 }
576
577 auto end = std::chrono::steady_clock::now();
578 stats.total_time +=
579 std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
580
581 if (!opts.quiet) {
582 std::cout << "\rProcessed: " << stats.total_files
583 << " (Success: " << stats.success_count
584 << ", Errors: " << stats.error_count << ")" << std::flush;
585 }
586 };
587
588 if (opts.recursive) {
589 for (const auto& entry :
590 std::filesystem::recursive_directory_iterator(input_dir)) {
591 if (entry.is_regular_file()) {
592 process_file(entry.path());
593 }
594 }
595 } else {
596 for (const auto& entry : std::filesystem::directory_iterator(input_dir)) {
597 if (entry.is_regular_file()) {
598 process_file(entry.path());
599 }
600 }
601 }
602
603 if (!opts.quiet) {
604 std::cout << "\n";
605 }
606}
607
612void print_summary(const conversion_stats& stats) {
613 std::cout << "\n";
614 std::cout << "========================================\n";
615 std::cout << " Conversion Summary\n";
616 std::cout << "========================================\n";
617 std::cout << " Total files: " << stats.total_files << "\n";
618 std::cout << " Successful: " << stats.success_count << "\n";
619 std::cout << " Skipped: " << stats.skip_count << "\n";
620 std::cout << " Errors: " << stats.error_count << "\n";
621 std::cout << " Total time: " << stats.total_time.count() << " ms\n";
622 if (stats.total_files > 0) {
623 auto avg_time =
624 stats.total_time.count() / static_cast<double>(stats.total_files);
625 std::cout << " Avg per file: " << std::fixed << std::setprecision(1)
626 << avg_time << " ms\n";
627 }
628 std::cout << "========================================\n";
629}
630
631} // namespace
632
633int main(int argc, char* argv[]) {
634 options opts;
635
636 if (!parse_arguments(argc, argv, opts)) {
637 std::cout << R"(
638 ___ __ __ ____ ____ ____ ____ __ __
639 |_ _| \/ |/ ___| |___ \ | _ \ / ___| \/ |
640 | || |\/| | | _ __) | | | | | | | |\/| |
641 | || | | | |_| | / __/ | |_| | |___| | | |
642 |___|_| |_|\____| |_____| |____/ \____|_| |_|
643
644 Image to DICOM Conversion Utility
645)" << "\n";
646 print_usage(argv[0]);
647 return 1;
648 }
649
650 // Check input exists
651 if (!std::filesystem::exists(opts.input_path)) {
652 std::cerr << "Error: Input path does not exist: " << opts.input_path.string()
653 << "\n";
654 return 2;
655 }
656
657 // Show banner
658 if (!opts.quiet) {
659 std::cout << R"(
660 ___ __ __ ____ ____ ____ ____ __ __
661 |_ _| \/ |/ ___| |___ \ | _ \ / ___| \/ |
662 | || |\/| | | _ __) | | | | | | | |\/| |
663 | || | | | |_| | / __/ | |_| | |___| | | |
664 |___|_| |_|\____| |_____| |____/ \____|_| |_|
665
666 Image to DICOM Conversion Utility
667)" << "\n";
668 }
669
670 conversion_stats stats;
671 auto start_time = std::chrono::steady_clock::now();
672
673 if (std::filesystem::is_directory(opts.input_path)) {
674 // Process directory
675 if (!std::filesystem::exists(opts.output_path)) {
676 std::filesystem::create_directories(opts.output_path);
677 }
678
679 if (!opts.quiet) {
680 std::cout << "Processing directory: " << opts.input_path.string() << "\n";
681 if (opts.recursive) {
682 std::cout << "Mode: Recursive\n\n";
683 }
684 }
685
686 process_directory(opts.input_path, opts.output_path, opts, stats);
687 } else {
688 // Process single file
689 ++stats.total_files;
690
691 if (convert_file(opts.input_path, opts.output_path, opts)) {
692 ++stats.success_count;
693 if (!opts.quiet) {
694 std::cout << "Conversion completed successfully.\n";
695 std::cout << " Output: " << opts.output_path.string() << "\n";
696 }
697 } else {
698 ++stats.error_count;
699 }
700 }
701
702 auto end_time = std::chrono::steady_clock::now();
703 stats.total_time =
704 std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
705
706 // Print summary for directory processing
707 if (std::filesystem::is_directory(opts.input_path) && !opts.quiet) {
708 print_summary(stats);
709 }
710
711 return stats.error_count > 0 ? 2 : 0;
712}
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.
int main()
Definition main.cpp:84
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag bits_allocated
Bits Allocated.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag bits_stored
Bits Stored.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag samples_per_pixel
Samples per Pixel.
constexpr dicom_tag series_description
Series Description.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag patient_name
Patient's Name.
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
@ counter
Monotonic increasing value.
@ id
Implant Displaced (alternate code)
Transfer Syntax UIDs.
Definition main.cpp:78
std::string_view uid