55enum class output_format {
73 uint16_t planar_configuration{0};
75 std::string photometric_interpretation;
76 size_t pixel_data_size{0};
77 bool has_pixel_data{
false};
84 std::filesystem::path input_path;
85 std::filesystem::path output_path;
86 output_format format{output_format::raw};
88 uint32_t frame_number{0};
89 bool extract_all_frames{
true};
90 bool info_only{
false};
91 bool recursive{
false};
92 bool overwrite{
false};
95 bool apply_window{
false};
103struct extraction_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) {
117DICOM Extract - Pixel Data Extraction Utility
119Usage: )" << program_name
120 << R"( <input> [output] [options]
123 input Input DICOM file or directory
124 output Output file or directory (optional for --info)
126Output Format Options:
127 --raw Raw pixel data (default)
128 --jpeg JPEG image (requires libjpeg)
129 --png PNG image (requires libpng)
130 --ppm PPM/PGM portable image format
133 -q, --quality <1-100> JPEG quality (default: 90)
136 --frame <n> Extract specific frame (0-indexed, default: all)
137 --all-frames Extract all frames (default)
139Windowing Options (for display):
140 --window <c> <w> Apply window center/width transformation
143 -r, --recursive Process directory recursively
144 --overwrite Overwrite existing output files
145 -v, --verbose Verbose output
146 --quiet Minimal output (errors only)
149 --info Show pixel data information only (no extraction)
150 -h, --help Show this help message
152Supported Transfer Syntaxes:
153 - Uncompressed: Implicit VR, Explicit VR (LE/BE)
154 - Compressed: Requires codec support (JPEG, JPEG2000, RLE)
158 << R"( image.dcm # Show pixel info
160 << R"( image.dcm output.raw --raw # Extract raw pixels
162 << R"( image.dcm output.jpg --jpeg # Extract as JPEG
164 << R"( image.dcm output.png --png # Extract as PNG
166 << R"( image.dcm output.ppm --ppm # Extract as PPM
168 << R"( image.dcm output.jpg --jpeg --frame 0
170 << R"( ./dicom/ ./images/ --recursive --jpeg
173 0 Success - All files extracted successfully
174 1 Error - Invalid arguments
175 2 Error - Extraction failed for one or more files
186bool parse_arguments(
int argc,
char* argv[], options& opts) {
191 for (
int i = 1; i < argc; ++i) {
192 std::string arg = argv[i];
194 if (arg ==
"--help" || arg ==
"-h") {
196 }
else if (arg ==
"--info") {
197 opts.info_only =
true;
198 }
else if (arg ==
"--raw") {
199 opts.format = output_format::raw;
200 }
else if (arg ==
"--jpeg") {
201 opts.format = output_format::jpeg;
202 }
else if (arg ==
"--png") {
203 opts.format = output_format::png;
204 }
else if (arg ==
"--ppm") {
205 opts.format = output_format::ppm;
206 }
else if ((arg ==
"-q" || arg ==
"--quality") && i + 1 < argc) {
208 opts.jpeg_quality = std::stoi(argv[++i]);
209 if (opts.jpeg_quality < 1 || opts.jpeg_quality > 100) {
210 std::cerr <<
"Error: Quality must be between 1 and 100\n";
214 std::cerr <<
"Error: Invalid quality value\n";
217 }
else if (arg ==
"--frame" && i + 1 < argc) {
219 opts.frame_number =
static_cast<uint32_t
>(std::stoul(argv[++i]));
220 opts.extract_all_frames =
false;
222 std::cerr <<
"Error: Invalid frame number\n";
225 }
else if (arg ==
"--all-frames") {
226 opts.extract_all_frames =
true;
227 }
else if (arg ==
"--window" && i + 2 < argc) {
229 opts.window_center = std::stod(argv[++i]);
230 opts.window_width = std::stod(argv[++i]);
231 opts.apply_window =
true;
233 std::cerr <<
"Error: Invalid window values\n";
236 }
else if (arg ==
"-r" || arg ==
"--recursive") {
237 opts.recursive =
true;
238 }
else if (arg ==
"--overwrite") {
239 opts.overwrite =
true;
240 }
else if (arg ==
"-v" || arg ==
"--verbose") {
242 }
else if (arg ==
"--quiet") {
244 }
else if (arg[0] ==
'-') {
245 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
247 }
else if (opts.input_path.empty()) {
248 opts.input_path = arg;
249 }
else if (opts.output_path.empty()) {
250 opts.output_path = arg;
252 std::cerr <<
"Error: Too many arguments\n";
257 if (opts.input_path.empty()) {
258 std::cerr <<
"Error: No input path specified\n";
262 if (opts.output_path.empty() && !opts.info_only) {
264 opts.info_only =
true;
269 opts.verbose =
false;
286 if (
auto val = dataset.
get_numeric<uint16_t>(tags::rows); val) {
289 if (
auto val = dataset.
get_numeric<uint16_t>(tags::columns); val) {
294 if (
auto val = dataset.
get_numeric<uint16_t>(dicom_tag{0x0028, 0x0100}); val) {
295 info.bits_allocated = *val;
297 if (
auto val = dataset.
get_numeric<uint16_t>(dicom_tag{0x0028, 0x0101}); val) {
298 info.bits_stored = *val;
300 if (
auto val = dataset.
get_numeric<uint16_t>(dicom_tag{0x0028, 0x0102}); val) {
301 info.high_bit = *val;
305 if (
auto val = dataset.
get_numeric<uint16_t>(tags::samples_per_pixel); val) {
306 info.samples_per_pixel = *val;
308 if (
auto val = dataset.
get_numeric<uint16_t>(dicom_tag{0x0028, 0x0103}); val) {
309 info.pixel_representation = *val;
311 if (
auto val = dataset.
get_numeric<uint16_t>(dicom_tag{0x0028, 0x0006}); val) {
312 info.planar_configuration = *val;
316 auto frames_str = dataset.
get_string(dicom_tag{0x0028, 0x0008});
317 if (!frames_str.empty()) {
319 info.number_of_frames =
static_cast<uint32_t
>(std::stoul(frames_str));
321 info.number_of_frames = 1;
326 info.photometric_interpretation =
327 dataset.
get_string(tags::photometric_interpretation);
331 if (pixel_data !=
nullptr) {
332 info.has_pixel_data =
true;
344void print_pixel_info(
const pixel_info& info,
345 const std::filesystem::path& file_path) {
347 std::cout <<
"========================================\n";
348 std::cout <<
" Pixel Data Information\n";
349 std::cout <<
"========================================\n";
350 std::cout <<
" File: " << file_path.filename().string() <<
"\n";
351 std::cout <<
" Dimensions: " <<
info.columns <<
" x " <<
info.rows <<
"\n";
352 std::cout <<
" Bits Allocated: " <<
info.bits_allocated <<
"\n";
353 std::cout <<
" Bits Stored: " <<
info.bits_stored <<
"\n";
354 std::cout <<
" High Bit: " <<
info.high_bit <<
"\n";
355 std::cout <<
" Samples/Pixel: " <<
info.samples_per_pixel <<
"\n";
356 std::cout <<
" Pixel Rep: "
357 << (
info.pixel_representation == 0 ?
"Unsigned" :
"Signed") <<
"\n";
358 std::cout <<
" Photometric: " <<
info.photometric_interpretation <<
"\n";
359 std::cout <<
" Number of Frames: " <<
info.number_of_frames <<
"\n";
360 std::cout <<
" Has Pixel Data: " << (
info.has_pixel_data ?
"Yes" :
"No") <<
"\n";
361 if (
info.has_pixel_data) {
362 std::cout <<
" Pixel Data Size: " <<
info.pixel_data_size <<
" bytes\n";
365 size_t expected =
static_cast<size_t>(
info.columns) *
info.rows *
366 info.samples_per_pixel * ((
info.bits_allocated + 7) / 8) *
367 info.number_of_frames;
368 std::cout <<
" Expected Size: " << expected <<
" bytes\n";
370 if (
info.pixel_data_size != expected) {
371 std::cout <<
" Note: Size mismatch - data may be compressed\n";
374 std::cout <<
"========================================\n";
384uint8_t apply_window_level(
int pixel,
double center,
double width) {
385 double lower = center - width / 2.0;
386 double upper = center + width / 2.0;
388 if (pixel <= lower)
return 0;
389 if (pixel >= upper)
return 255;
391 return static_cast<uint8_t
>(
392 ((pixel - lower) / width) * 255.0);
395#ifdef PACS_JPEG_FOUND
406bool write_jpeg(
const std::filesystem::path& output_path,
407 const std::vector<uint8_t>& pixels,
408 int width,
int height,
int components,
int quality) {
409 FILE* file = fopen(output_path.string().c_str(),
"wb");
410 if (file ==
nullptr) {
411 std::cerr <<
"Error: Cannot create file: " << output_path <<
"\n";
415 struct jpeg_compress_struct cinfo {};
416 struct jpeg_error_mgr jerr {};
418 cinfo.err = jpeg_std_error(&jerr);
419 jpeg_create_compress(&cinfo);
420 jpeg_stdio_dest(&cinfo, file);
422 cinfo.image_width =
static_cast<JDIMENSION
>(width);
423 cinfo.image_height =
static_cast<JDIMENSION
>(height);
424 cinfo.input_components = components;
425 cinfo.in_color_space = (components == 1) ? JCS_GRAYSCALE : JCS_RGB;
427 jpeg_set_defaults(&cinfo);
428 jpeg_set_quality(&cinfo, quality, TRUE);
429 jpeg_start_compress(&cinfo, TRUE);
431 JSAMPROW row_pointer[1];
432 int row_stride = width * components;
434 while (cinfo.next_scanline < cinfo.image_height) {
436 const_cast<JSAMPROW
>(&pixels[cinfo.next_scanline *
static_cast<size_t>(row_stride)]);
437 jpeg_write_scanlines(&cinfo, row_pointer, 1);
440 jpeg_finish_compress(&cinfo);
441 jpeg_destroy_compress(&cinfo);
458bool write_png(
const std::filesystem::path& output_path,
459 const std::vector<uint8_t>& pixels,
460 int width,
int height,
int components) {
461 FILE* file = fopen(output_path.string().c_str(),
"wb");
462 if (file ==
nullptr) {
463 std::cerr <<
"Error: Cannot create file: " << output_path <<
"\n";
468 png_structp png_ptr = png_create_write_struct(
469 PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
470 if (png_ptr ==
nullptr) {
471 std::cerr <<
"Error: Failed to create PNG write struct\n";
477 png_infop info_ptr = png_create_info_struct(png_ptr);
478 if (info_ptr ==
nullptr) {
479 std::cerr <<
"Error: Failed to create PNG info struct\n";
480 png_destroy_write_struct(&png_ptr,
nullptr);
486 if (setjmp(png_jmpbuf(png_ptr))) {
487 std::cerr <<
"Error: PNG write error\n";
488 png_destroy_write_struct(&png_ptr, &info_ptr);
494 png_init_io(png_ptr, file);
497 png_set_compression_level(png_ptr, 6);
500 int color_type = (components == 1) ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB;
501 png_set_IHDR(png_ptr, info_ptr,
502 static_cast<png_uint_32
>(width),
503 static_cast<png_uint_32
>(height),
507 PNG_COMPRESSION_TYPE_DEFAULT,
508 PNG_FILTER_TYPE_DEFAULT);
511 png_write_info(png_ptr, info_ptr);
514 int row_stride = width * components;
515 for (
int y = 0; y < height; ++y) {
516 png_bytep row =
const_cast<png_bytep
>(&pixels[
static_cast<size_t>(y) * row_stride]);
517 png_write_row(png_ptr, row);
521 png_write_end(png_ptr,
nullptr);
524 png_destroy_write_struct(&png_ptr, &info_ptr);
540bool write_ppm(
const std::filesystem::path& output_path,
541 const std::vector<uint8_t>& pixels,
542 int width,
int height,
int components) {
543 std::ofstream file(output_path, std::ios::binary);
545 std::cerr <<
"Error: Cannot create file: " << output_path <<
"\n";
550 if (components == 1) {
555 file << width <<
" " << height <<
"\n";
559 file.write(
reinterpret_cast<const char*
>(pixels.data()),
static_cast<std::streamsize
>(pixels.size()));
570bool write_raw(
const std::filesystem::path& output_path,
571 const std::vector<uint8_t>& pixels) {
572 std::ofstream file(output_path, std::ios::binary);
574 std::cerr <<
"Error: Cannot create file: " << output_path <<
"\n";
578 file.write(
reinterpret_cast<const char*
>(pixels.data()),
static_cast<std::streamsize
>(pixels.size()));
590std::vector<uint8_t> convert_to_8bit(
const std::vector<uint8_t>& data,
591 const pixel_info& info,
592 const options& opts) {
593 size_t num_pixels =
static_cast<size_t>(
info.columns) *
info.rows *
info.samples_per_pixel;
594 std::vector<uint8_t> result(num_pixels);
600 if (!opts.apply_window) {
602 int min_val = std::numeric_limits<int>::max();
603 int max_val = std::numeric_limits<int>::min();
605 for (
size_t i = 0; i < num_pixels; ++i) {
607 if (
info.pixel_representation == 0) {
608 pixel =
static_cast<int16_t
>(
609 data[i * 2] | (data[i * 2 + 1] << 8));
611 pixel =
static_cast<int16_t
>(
612 data[i * 2] | (data[i * 2 + 1] << 8));
614 min_val = std::min(min_val,
static_cast<int>(pixel));
615 max_val = std::max(max_val,
static_cast<int>(pixel));
619 window_center =
static_cast<double>(min_val + max_val) / 2.0;
625 for (
size_t i = 0; i < num_pixels; ++i) {
626 int16_t pixel =
static_cast<int16_t
>(
627 data[i * 2] | (data[i * 2 + 1] << 8));
628 result[i] = apply_window_level(pixel, window_center, window_width);
641bool extract_file(
const std::filesystem::path& input_path,
642 const std::filesystem::path& output_path,
643 const options& opts) {
647 auto result = dicom_file::open(input_path);
648 if (result.is_err()) {
649 std::cerr <<
"Error: Failed to open '" << input_path.string()
650 <<
"': " << result.error().message <<
"\n";
654 auto& file = result.value();
655 const auto& dataset = file.dataset();
658 auto info = get_pixel_info(dataset);
661 if (opts.info_only) {
662 print_pixel_info(info, input_path);
667 if (!
info.has_pixel_data) {
668 std::cerr <<
"Error: No pixel data in file: " << input_path <<
"\n";
673 if (std::filesystem::exists(output_path) && !opts.overwrite) {
675 std::cout <<
" Skipped (exists): " << output_path.filename().string()
682 auto* pixel_element = dataset.
get(tags::pixel_data);
683 if (pixel_element ==
nullptr) {
684 std::cerr <<
"Error: Cannot read pixel data: " << input_path <<
"\n";
692 std::cout <<
" Extracting: " << input_path.filename().string() <<
"\n";
693 std::cout <<
" Size: " <<
info.columns <<
" x " <<
info.rows <<
"\n";
694 std::cout <<
" Bits: " <<
info.bits_stored <<
"/" <<
info.bits_allocated
699 if (
info.bits_allocated == 16) {
700 pixels = convert_to_8bit(pixels, info, opts);
704 if (
info.photometric_interpretation ==
"MONOCHROME1") {
705 for (
auto& p : pixels) {
706 p =
static_cast<uint8_t
>(255 - p);
711 auto output_dir = output_path.parent_path();
712 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
713 std::filesystem::create_directories(output_dir);
719 switch (opts.format) {
720 case output_format::raw:
721 success = write_raw(output_path, pixels);
724 case output_format::jpeg:
725#ifdef PACS_JPEG_FOUND
727 info.samples_per_pixel, opts.jpeg_quality);
729 std::cerr <<
"Error: JPEG support not available. Install libjpeg-turbo.\n";
734 case output_format::png:
737 info.samples_per_pixel);
739 std::cerr <<
"Error: PNG support not available. Install libpng.\n";
744 case output_format::ppm:
746 write_ppm(output_path, pixels,
info.columns,
info.rows,
info.samples_per_pixel);
750 if (success && opts.verbose) {
751 std::cout <<
" Output: " << output_path.string() <<
"\n";
762std::string get_output_extension(output_format format) {
764 case output_format::raw:
766 case output_format::jpeg:
768 case output_format::png:
770 case output_format::ppm:
781bool is_dicom_file(
const std::filesystem::path& file_path) {
782 auto ext = file_path.extension().string();
783 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
784 return ext ==
".dcm" || ext ==
".dicom" || ext.empty();
794void process_directory(
const std::filesystem::path& input_dir,
795 const std::filesystem::path& output_dir,
797 extraction_stats& stats) {
798 auto process_file = [&](
const std::filesystem::path& file_path) {
799 if (!is_dicom_file(file_path)) {
806 auto relative_path = std::filesystem::relative(file_path, input_dir);
807 auto output_path = output_dir / relative_path;
808 output_path.replace_extension(get_output_extension(opts.format));
810 auto start = std::chrono::steady_clock::now();
812 if (extract_file(file_path, output_path, opts)) {
813 ++stats.success_count;
818 auto end = std::chrono::steady_clock::now();
820 std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
823 std::cout <<
"\rProcessed: " << stats.total_files
824 <<
" (Success: " << stats.success_count
825 <<
", Errors: " << stats.error_count <<
")" << std::flush;
829 if (opts.recursive) {
830 for (
const auto& entry :
831 std::filesystem::recursive_directory_iterator(input_dir)) {
832 if (entry.is_regular_file()) {
833 process_file(entry.path());
837 for (
const auto& entry : std::filesystem::directory_iterator(input_dir)) {
838 if (entry.is_regular_file()) {
839 process_file(entry.path());
853void print_summary(
const extraction_stats& stats) {
855 std::cout <<
"========================================\n";
856 std::cout <<
" Extraction Summary\n";
857 std::cout <<
"========================================\n";
858 std::cout <<
" Total files: " << stats.total_files <<
"\n";
859 std::cout <<
" Successful: " << stats.success_count <<
"\n";
860 std::cout <<
" Skipped: " << stats.skip_count <<
"\n";
861 std::cout <<
" Errors: " << stats.error_count <<
"\n";
862 std::cout <<
" Total time: " << stats.total_time.count() <<
" ms\n";
863 if (stats.total_files > 0) {
865 stats.total_time.count() /
static_cast<double>(stats.total_files);
866 std::cout <<
" Avg per file: " << std::fixed << std::setprecision(1)
867 << avg_time <<
" ms\n";
869 std::cout <<
"========================================\n";
874int main(
int argc,
char* argv[]) {
877 if (!parse_arguments(argc, argv, opts)) {
879 ____ ____ __ __ _______ _______ ____ _ ____ _____
880 | _ \ / ___| \/ | | ____\ \/ /_ _| _ \ / \ / ___|_ _|
881 | | | | | | |\/| | | _| \ / | | | |_) | / _ \| | | |
882 | |_| | |___| | | | | |___ / \ | | | _ < / ___ \ |___ | |
883 |____/ \____|_| |_| |_____/_/\_\ |_| |_| \_\/_/ \_\____| |_|
885 DICOM Pixel Data Extraction Utility
887 print_usage(argv[0]);
892 if (!std::filesystem::exists(opts.input_path)) {
893 std::cerr <<
"Error: Input path does not exist: " << opts.input_path.string()
901 ____ ____ __ __ _______ _______ ____ _ ____ _____
902 | _ \ / ___| \/ | | ____\ \/ /_ _| _ \ / \ / ___|_ _|
903 | | | | | | |\/| | | _| \ / | | | |_) | / _ \| | | |
904 | |_| | |___| | | | | |___ / \ | | | _ < / ___ \ |___ | |
905 |____/ \____|_| |_| |_____/_/\_\ |_| |_| \_\/_/ \_\____| |_|
907 DICOM Pixel Data Extraction Utility
911 extraction_stats stats;
912 auto start_time = std::chrono::steady_clock::now();
914 if (std::filesystem::is_directory(opts.input_path)) {
916 if (!opts.info_only) {
917 if (!std::filesystem::exists(opts.output_path)) {
918 std::filesystem::create_directories(opts.output_path);
923 std::cout <<
"Processing directory: " << opts.input_path.string() <<
"\n";
924 if (opts.recursive) {
925 std::cout <<
"Mode: Recursive\n\n";
929 process_directory(opts.input_path, opts.output_path, opts, stats);
934 if (extract_file(opts.input_path, opts.output_path, opts)) {
935 ++stats.success_count;
936 if (!opts.quiet && !opts.info_only) {
937 std::cout <<
"Extraction completed successfully.\n";
938 std::cout <<
" Output: " << opts.output_path.string() <<
"\n";
945 auto end_time = std::chrono::steady_clock::now();
947 std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
950 if (std::filesystem::is_directory(opts.input_path) && !opts.quiet &&
952 print_summary(stats);
955 return stats.error_count > 0 ? 2 : 0;
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
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.
@ jpeg
JPEG format (default)