49constexpr std::string_view kSecondaryCaptureUID =
"1.2.840.10008.5.1.4.1.1.7";
52constexpr std::string_view kSecondaryCaptureColorUID =
"1.2.840.10008.5.1.4.1.1.7.4";
60 auto now = std::chrono::system_clock::now();
61 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
62 now.time_since_epoch())
64 return std::string(
"1.2.826.0.1.3680043.8.1055.4.") + std::to_string(timestamp) +
65 "." + std::to_string(++counter);
72 std::vector<uint8_t> pixels;
78 std::string photometric_interpretation{
"MONOCHROME2"};
86 std::filesystem::path input_path;
87 std::filesystem::path output_path;
93 bool recursive{
false};
94 bool overwrite{
false};
97 std::string transfer_syntax;
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};
115void print_usage(
const char* program_name) {
117Image to DICOM - Image Conversion Utility
119Usage: )" << program_name
120 << R"( <input> <output> [options]
123 input Input image file (JPEG) or directory
124 output Output DICOM file or directory
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)
134 -r, --recursive Process directory recursively
135 --overwrite Overwrite existing output files
136 -v, --verbose Verbose output
137 -q, --quiet Minimal output (errors only)
139Transfer Syntax Options:
140 --explicit Explicit VR Little Endian (default)
141 --implicit Implicit VR Little Endian
144 -h, --help Show this help message
146Supported Input Formats:
147 - JPEG (.jpg, .jpeg) - Requires libjpeg-turbo
151 << R"( photo.jpg output.dcm
153 << R"( photo.jpg output.dcm --patient-name "DOE^JOHN" --patient-id "12345"
155 << R"( ./images/ ./dicom/ --recursive
158 0 Success - All files converted successfully
159 1 Error - Invalid arguments
160 2 Error - Conversion failed for one or more files
171bool parse_arguments(
int argc,
char* argv[], options& opts) {
179 opts.transfer_syntax = std::string(transfer_syntax::explicit_vr_little_endian.
uid());
181 for (
int i = 1; i < argc; ++i) {
182 std::string arg = argv[i];
184 if (arg ==
"--help" || arg ==
"-h") {
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") {
206 }
else if (arg ==
"-q" || arg ==
"--quiet") {
208 }
else if (arg[0] ==
'-') {
209 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
211 }
else if (opts.input_path.empty()) {
212 opts.input_path = arg;
213 }
else if (opts.output_path.empty()) {
214 opts.output_path = arg;
216 std::cerr <<
"Error: Too many arguments\n";
221 if (opts.input_path.empty()) {
222 std::cerr <<
"Error: No input path specified\n";
226 if (opts.output_path.empty()) {
227 std::cerr <<
"Error: No output path specified\n";
233 opts.verbose =
false;
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);
251 for (
int i = 0; i < 8; ++i) {
252 id += charset[dist(gen)];
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);
266 localtime_s(&tm_now, &time_t_now);
268 localtime_r(&time_t_now, &tm_now);
270 std::ostringstream oss;
271 oss << std::put_time(&tm_now,
"%Y%m%d");
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()) %
287 localtime_s(&tm_now, &time_t_now);
289 localtime_r(&time_t_now, &tm_now);
291 std::ostringstream oss;
292 oss << std::put_time(&tm_now,
"%H%M%S") <<
"." << std::setfill(
'0')
293 << std::setw(6) << ms.count();
297#ifdef PACS_JPEG_FOUND
303image_data read_jpeg(
const std::filesystem::path& file_path) {
306 FILE* file = fopen(file_path.string().c_str(),
"rb");
307 if (file ==
nullptr) {
308 std::cerr <<
"Error: Cannot open file: " << file_path <<
"\n";
312 struct jpeg_decompress_struct cinfo {};
313 struct jpeg_error_mgr jerr {};
315 cinfo.err = jpeg_std_error(&jerr);
316 jpeg_create_decompress(&cinfo);
317 jpeg_stdio_src(&cinfo, file);
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);
327 if (cinfo.num_components == 3) {
328 cinfo.out_color_space = JCS_RGB;
331 jpeg_start_decompress(&cinfo);
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;
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";
345 size_t row_stride =
static_cast<size_t>(cinfo.output_width) * cinfo.output_components;
346 result.pixels.resize(row_stride * cinfo.output_height);
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;
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);
358 jpeg_finish_decompress(&cinfo);
359 jpeg_destroy_decompress(&cinfo);
362 result.is_valid =
true;
369image_data read_jpeg(
const std::filesystem::path& file_path) {
371 std::cerr <<
"Error: JPEG support not available. Install libjpeg-turbo.\n";
372 std::cerr <<
" File: " << file_path <<
"\n";
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);
386 if (ext ==
".jpg" || ext ==
".jpeg") {
387 return read_jpeg(file_path);
390 std::cerr <<
"Error: Unsupported image format: " << ext <<
"\n";
391 std::cerr <<
" Supported formats: JPEG (.jpg, .jpeg)\n";
402 const options& opts) {
406 dicom_dataset dataset;
415 ? std::string(kSecondaryCaptureUID)
416 : std::string(kSecondaryCaptureColorUID);
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,
"");
423 dataset.set_string(dicom_tag{0x0010, 0x0040}, vr_type::CS,
"");
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,
"");
430 dataset.set_string(dicom_tag{0x0008, 0x0090}, vr_type::PN,
"");
431 dataset.set_string(dicom_tag{0x0020, 0x0010}, vr_type::SH,
"1");
432 dataset.set_string(tags::study_description, vr_type::LO, opts.study_description);
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");
438 dataset.set_string(tags::series_description, vr_type::LO, opts.series_description);
441 dataset.set_string(dicom_tag{0x0008, 0x0064}, vr_type::CS,
"DV");
444 dataset.set_string(dicom_tag{0x0020, 0x0013}, vr_type::IS,
"1");
445 dataset.set_string(dicom_tag{0x0020, 0x0020}, vr_type::CS,
"");
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);
459 if (img.samples_per_pixel > 1) {
460 dataset.set_numeric<uint16_t>(dicom_tag{0x0028, 0x0006}, vr_type::US, 0);
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);
468 dataset.insert(dicom_element(tags::pixel_data, vr_type::OW, img.pixels));
480bool convert_file(
const std::filesystem::path& input_path,
481 const std::filesystem::path& output_path,
482 const options& opts) {
487 if (std::filesystem::exists(output_path) && !opts.overwrite) {
489 std::cout <<
" Skipped (exists): " << output_path.filename().string() <<
"\n";
495 auto img = read_image(input_path);
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";
508 auto dataset = create_dicom_dataset(img, opts);
511 auto ts = transfer_syntax(opts.transfer_syntax);
512 auto dicom_file = dicom_file::create(std::move(dataset),
ts);
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);
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";
529 std::cout <<
" Output: " << output_path.string() <<
"\n";
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";
553void process_directory(
const std::filesystem::path& input_dir,
554 const std::filesystem::path& output_dir,
556 conversion_stats& stats) {
557 auto process_file = [&](
const std::filesystem::path& file_path) {
558 if (!is_supported_image(file_path)) {
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");
569 auto start = std::chrono::steady_clock::now();
571 if (convert_file(file_path, output_path, opts)) {
572 ++stats.success_count;
577 auto end = std::chrono::steady_clock::now();
579 std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
582 std::cout <<
"\rProcessed: " << stats.total_files
583 <<
" (Success: " << stats.success_count
584 <<
", Errors: " << stats.error_count <<
")" << std::flush;
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());
596 for (
const auto& entry : std::filesystem::directory_iterator(input_dir)) {
597 if (entry.is_regular_file()) {
598 process_file(entry.path());
612void print_summary(
const conversion_stats& stats) {
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) {
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";
628 std::cout <<
"========================================\n";
633int main(
int argc,
char* argv[]) {
636 if (!parse_arguments(argc, argv, opts)) {
638 ___ __ __ ____ ____ ____ ____ __ __
639 |_ _| \/ |/ ___| |___ \ | _ \ / ___| \/ |
640 | || |\/| | | _ __) | | | | | | | |\/| |
641 | || | | | |_| | / __/ | |_| | |___| | | |
642 |___|_| |_|\____| |_____| |____/ \____|_| |_|
644 Image to DICOM Conversion Utility
646 print_usage(argv[0]);
651 if (!std::filesystem::exists(opts.input_path)) {
652 std::cerr <<
"Error: Input path does not exist: " << opts.input_path.string()
660 ___ __ __ ____ ____ ____ ____ __ __
661 |_ _| \/ |/ ___| |___ \ | _ \ / ___| \/ |
662 | || |\/| | | _ __) | | | | | | | |\/| |
663 | || | | | |_| | / __/ | |_| | |___| | | |
664 |___|_| |_|\____| |_____| |____/ \____|_| |_|
666 Image to DICOM Conversion Utility
670 conversion_stats stats;
671 auto start_time = std::chrono::steady_clock::now();
673 if (std::filesystem::is_directory(opts.input_path)) {
675 if (!std::filesystem::exists(opts.output_path)) {
676 std::filesystem::create_directories(opts.output_path);
680 std::cout <<
"Processing directory: " << opts.input_path.string() <<
"\n";
681 if (opts.recursive) {
682 std::cout <<
"Mode: Recursive\n\n";
686 process_directory(opts.input_path, opts.output_path, opts, stats);
691 if (convert_file(opts.input_path, opts.output_path, opts)) {
692 ++stats.success_count;
694 std::cout <<
"Conversion completed successfully.\n";
695 std::cout <<
" Output: " << opts.output_path.string() <<
"\n";
702 auto end_time = std::chrono::steady_clock::now();
704 std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
707 if (std::filesystem::is_directory(opts.input_path) && !opts.quiet) {
708 print_summary(stats);
711 return stats.error_count > 0 ? 2 : 0;
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.
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)