Skip to content

Commit

Permalink
Merge pull request #579 from Enet4/imp/fromimage/encapsulate
Browse files Browse the repository at this point in the history
[fromimage] Add --encapsulate option
  • Loading branch information
Enet4 authored Oct 24, 2024
2 parents f054566 + a8b2e9a commit 182db60
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 80 deletions.
56 changes: 43 additions & 13 deletions fromimage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,51 @@ This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project.
## Usage

```none
dicom-fromimage 0.1.0
Convert and replace a DICOM file's image with another image
Usage: dicom-fromimage [OPTIONS] <DCM_FILE> <IMG_FILE>
Arguments:
<DCM_FILE> Path to the base DICOM file to read
<IMG_FILE> Path to the image file to replace the DICOM file
Options:
-o, --out <OUTPUT>
Path to the output image (default is to replace input extension with `.new.dcm`)
--transfer-syntax <TRANSFER_SYNTAX>
Override the transfer syntax UID
--encapsulate
Encapsulate the image file raw data in a fragment sequence instead of writing native pixel data
--retain-implementation
Retain the implementation class UID and version name from base DICOM
-v, --verbose
Print more information about the image and the output file
-h, --help
Print help
-V, --version
Print version
```

### Example

USAGE:
dicom-fromimage.exe [FLAGS] [OPTIONS] <dcm-file> <img-file>
Given a template DICOM file `base.dcm`,
replace the image data with the image in `image.png`:

FLAGS:
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose Print more information about the image and the output file
```none
dicom-fromimage base.dcm image.png -o image.dcm
```

OPTIONS:
-o, --out <output> Path to the output image (default is to replace input extension with `.new.dcm`)
This will read the image file in the second argument
and save it as native pixel data in Explicit VR Little Endian to `image.dcm`.

ARGS:
<dcm-file> Path to the base DICOM file to read
<img-file> Path to the image file to replace the DICOM file
You can also encapsulate the image file into a pixel data fragment,
without converting to native pixel data.
This allows you to create a DICOM file in JPEG baseline:

```none
dicom-fromimage base.dcm image.jpg --transfer-syntax 1.2.840.10008.1.2.4.50 --encapsulate -o image.dcm
```

**Note:** `--transfer-syntax` is just a UID override,
it will not automatically transcode the pixel data
to conform to the given transfer syntax.
To transcode files between transfer syntaxes,
see [`dicom-transcode`](https://github.com/Enet4/dicom-rs/tree/master/pixeldata).
221 changes: 154 additions & 67 deletions fromimage/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
use std::path::PathBuf;

use clap::Parser;
use dicom_core::{value::PrimitiveValue, DataElement, VR};
use dicom_core::{
value::{PixelFragmentSequence, PrimitiveValue},
DataElement, DicomValue, VR,
};
use dicom_dictionary_std::tags;
use dicom_object::{open_file, FileMetaTableBuilder};
use dicom_object::{open_file, DefaultDicomObject, FileMetaTableBuilder};
use image::DynamicImage;

type Result<T, E = snafu::Whatever> = std::result::Result<T, E>;

/// Convert and replace a DICOM file's image with another image
#[derive(Debug, Parser)]
Expand All @@ -32,6 +38,13 @@ struct App {
/// (default is to replace input extension with `.new.dcm`)
#[arg(short = 'o', long = "out")]
output: Option<PathBuf>,
/// Override the transfer syntax UID (pixel data is not converted)
#[arg(long = "transfer-syntax", alias = "ts")]
transfer_syntax: Option<String>,
/// Encapsulate the image file raw data in a fragment sequence
/// instead of writing native pixel data
#[arg(long)]
encapsulate: bool,
/// Retain the implementation class UID and version name from base DICOM
#[arg(long)]
retain_implementation: bool,
Expand All @@ -50,6 +63,8 @@ fn main() {
dcm_file,
img_file,
output,
encapsulate,
transfer_syntax,
retain_implementation,
verbose,
} = App::parse();
Expand All @@ -65,11 +80,147 @@ fn main() {
std::process::exit(-1);
});

let img = image::open(img_file).unwrap_or_else(|e| {
if encapsulate {
inject_encapsulated(&mut obj, img_file, verbose)
} else {
inject_image(&mut obj, img_file, verbose)
}
.unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-2);
});

let class_uid = obj.meta().media_storage_sop_class_uid.clone();

let mut meta_builder = FileMetaTableBuilder::new()
// currently the tool will always decode the image's pixel data,
// so encode it as Explicit VR Little Endian
.transfer_syntax("1.2.840.10008.1.2.1")
.media_storage_sop_class_uid(class_uid);

if let Some(ts) = transfer_syntax {
meta_builder = meta_builder.transfer_syntax(ts);
}

// recover implementation class UID and version name from base object
if retain_implementation {
let implementation_class_uid = &obj.meta().implementation_class_uid;
meta_builder = meta_builder.implementation_class_uid(implementation_class_uid);

if let Some(implementation_version_name) = obj.meta().implementation_version_name.as_ref() {
meta_builder = meta_builder.implementation_version_name(implementation_version_name);
}
}

let obj = obj
.into_inner()
.with_meta(meta_builder)
.unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-3);
});

obj.write_to_file(&output).unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-4);
});

if verbose {
println!("DICOM file saved to {}", output.display());
}
}

fn inject_image(obj: &mut DefaultDicomObject, img_file: PathBuf, verbose: bool) -> Result<()> {
let image_reader = image::ImageReader::open(img_file).unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-1);
});

let img = image_reader.decode().unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-1);
});

let color = img.color();

let bits_stored: u16 = match color {
image::ColorType::L8 => 8,
image::ColorType::L16 => 16,
image::ColorType::Rgb8 => 8,
image::ColorType::Rgb16 => 16,
_ => {
eprintln!("Unsupported image format {:?}", color);
std::process::exit(-2);
}
};

update_from_img(obj, &img, verbose);

for tag in [
tags::NUMBER_OF_FRAMES,
tags::PIXEL_ASPECT_RATIO,
tags::SMALLEST_IMAGE_PIXEL_VALUE,
tags::LARGEST_IMAGE_PIXEL_VALUE,
tags::PIXEL_PADDING_RANGE_LIMIT,
tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DATA,
tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR,
tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA,
tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR,
tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA,
tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR,
tags::ICC_PROFILE,
tags::COLOR_SPACE,
tags::PIXEL_DATA_PROVIDER_URL,
tags::EXTENDED_OFFSET_TABLE,
tags::EXTENDED_OFFSET_TABLE_LENGTHS,
] {
obj.remove_element(tag);
}

let pixeldata = img.into_bytes();

obj.put(DataElement::new(
tags::PIXEL_DATA,
if bits_stored == 8 { VR::OB } else { VR::OW },
PrimitiveValue::from(pixeldata),
));

Ok(())
}

fn inject_encapsulated(
dcm: &mut DefaultDicomObject,
img_file: PathBuf,
verbose: bool,
) -> Result<()> {
let image_reader = image::ImageReader::open(&img_file).unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-1);
});

// collect img file data
let all_data = std::fs::read(img_file).unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-2);
});

if let Ok(img) = image_reader.decode() {
// insert attributes but not pixel data

update_from_img(&mut *dcm, &img, verbose);
}

// insert pixel data in a sequence
dcm.put(DataElement::new(
tags::PIXEL_DATA,
VR::OB,
DicomValue::PixelSequence(PixelFragmentSequence::new_fragments(vec![all_data])),
));

Ok(())
}

fn update_from_img(obj: &mut DefaultDicomObject, img: &DynamicImage, verbose: bool) {
let width = img.width();
let height = img.height();
let color = img.color();
Expand All @@ -85,8 +236,6 @@ fn main() {
}
};

let pixeldata = img.into_bytes();

if verbose {
println!("{}x{} {:?} image", width, height, color);
}
Expand Down Expand Up @@ -151,68 +300,6 @@ fn main() {
VR::US,
PrimitiveValue::from(0_u16),
));

for tag in [
tags::NUMBER_OF_FRAMES,
tags::PIXEL_ASPECT_RATIO,
tags::SMALLEST_IMAGE_PIXEL_VALUE,
tags::LARGEST_IMAGE_PIXEL_VALUE,
tags::PIXEL_PADDING_RANGE_LIMIT,
tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DATA,
tags::RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR,
tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA,
tags::GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR,
tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA,
tags::BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR,
tags::ICC_PROFILE,
tags::COLOR_SPACE,
tags::PIXEL_DATA_PROVIDER_URL,
tags::EXTENDED_OFFSET_TABLE,
tags::EXTENDED_OFFSET_TABLE_LENGTHS,
] {
obj.remove_element(tag);
}

obj.put(DataElement::new(
tags::PIXEL_DATA,
if bits_stored == 8 { VR::OB } else { VR::OW },
PrimitiveValue::from(pixeldata),
));

let class_uid = obj.meta().media_storage_sop_class_uid.clone();

let mut meta_builder = FileMetaTableBuilder::new()
// currently the tool will always decode the image's pixel data,
// so encode it as Explicit VR Little Endian
.transfer_syntax("1.2.840.10008.1.2.1")
.media_storage_sop_class_uid(class_uid);

// recover implementation class UID and version name from base object
if retain_implementation {
let implementation_class_uid = &obj.meta().implementation_class_uid;
meta_builder = meta_builder.implementation_class_uid(implementation_class_uid);

if let Some(implementation_version_name) = obj.meta().implementation_version_name.as_ref() {
meta_builder = meta_builder.implementation_version_name(implementation_version_name);
}
}

let obj = obj
.into_inner()
.with_meta(meta_builder)
.unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-3);
});

obj.write_to_file(&output).unwrap_or_else(|e| {
tracing::error!("{}", snafu::Report::from_error(e));
std::process::exit(-4);
});

if verbose {
println!("DICOM file saved to {}", output.display());
}
}

#[cfg(test)]
Expand Down
27 changes: 27 additions & 0 deletions pixeldata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,30 @@ and is responsible for decoding pixel data elements
into images or multi-dimensional arrays.

This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project.

## Binary

`dicom-pixeldata` also offers the `dicom-transcode` command-line tool
(enable Cargo feature `cli`).
You can use it to transcode a DICOM file to another transfer syntax,
transforming pixel data along the way.

```none
Usage: dicom-transcode [OPTIONS] <--ts <TS>|--expl-vr-le|--impl-vr-le|--jpeg-baseline> <FILE>
Arguments:
<FILE>
Options:
-o, --output <OUTPUT> The output file (default is to change the extension to .new.dcm)
--quality <QUALITY> The encoding quality (from 0 to 100)
--effort <EFFORT> The encoding effort (from 0 to 100)
--ts <TS> Transcode to the Transfer Syntax indicated by UID
--expl-vr-le Transcode to Explicit VR Little Endian
--impl-vr-le Transcode to Implicit VR Little Endian
--jpeg-baseline Transcode to JPEG baseline (8-bit)
--retain-implementation Retain the original implementation class UID and version name
-v, --verbose Verbose mode
-h, --help Print help
-V, --version Print version
```

0 comments on commit 182db60

Please sign in to comment.