Skip to content

Commit

Permalink
Merge pull request #416 from Enet4/imp/toimage/unwrap
Browse files Browse the repository at this point in the history
[toimage] Implement --unwrap option
  • Loading branch information
Enet4 authored Sep 25, 2023
2 parents 354fb06 + 5e72506 commit 2abef21
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 50 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion toimage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ default = ['dicom-object/inventory-registry', 'dicom-object/backtraces']

[dependencies]
clap = { version = "4.0.18", features = ["derive"] }
dicom-core = { version = "0.6.1", path = "../core" }
dicom-dictionary-std = { version = "0.6.0", path = "../dictionary-std" }
dicom-object = { path = "../object/", version = "0.6.1" }
dicom-pixeldata = { path = "../pixeldata/", version = "0.2.0", features = ["image"] }
snafu = "0.7.3"
snafu = { version = "0.7.3", features = ["rust_1_61"] }
tracing = "0.1.34"
tracing-subscriber = "0.3.11"
290 changes: 241 additions & 49 deletions toimage/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//! A CLI tool for converting a DICOM image file
//! into a general purpose image file (e.g. PNG).
use std::path::PathBuf;
use std::{borrow::Cow, path::PathBuf};

use clap::Parser;
use dicom_core::prelude::*;
use dicom_dictionary_std::{tags, uids};
use dicom_object::open_file;
use dicom_pixeldata::{ConvertOptions, PixelDecoder};
use snafu::{Report, ResultExt, Whatever};
use snafu::{OptionExt, Report, ResultExt, Snafu, Whatever};
use tracing::{error, Level};

/// Convert a DICOM file into an image file
Expand All @@ -32,80 +34,270 @@ struct App {
#[arg(long = "16bit", conflicts_with = "force_8bit")]
force_16bit: bool,

/// Output the raw pixel data instead of decoding it
#[arg(
long = "unwrap",
conflicts_with = "force_8bit",
conflicts_with = "force_16bit"
)]
unwrap: bool,

/// Print more information about the image and the output file
#[arg(short = 'v', long = "verbose")]
verbose: bool,
}

#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("could not read DICOM file {}", path.display()))]
ReadFile {
#[snafu(source(from(dicom_object::ReadError, Box::new)))]
source: Box<dicom_object::ReadError>,
path: PathBuf,
},
/// failed to decode pixel data
DecodePixelData {
#[snafu(source(from(dicom_pixeldata::Error, Box::new)))]
source: Box<dicom_pixeldata::Error>,
},
/// missing offset table entry for frame #{frame_number}
MissingOffsetEntry { frame_number: u32 },
/// missing key property {name}
MissingProperty { name: &'static str },
/// property {name} contains an invalid value
InvalidPropertyValue {
name: &'static str,
#[snafu(source(from(dicom_core::value::ConvertValueError, Box::new)))]
source: Box<dicom_core::value::ConvertValueError>,
},
/// pixel data of frame #{frame_number} is out of bounds
FrameOutOfBounds { frame_number: u32 },
/// failed to convert pixel data to image
ConvertImage {
#[snafu(source(from(dicom_pixeldata::Error, Box::new)))]
source: Box<dicom_pixeldata::Error>,
},
/// failed to save image to file
SaveImage {
#[snafu(source(from(dicom_pixeldata::image::ImageError, Box::new)))]
source: Box<dicom_pixeldata::image::ImageError>,
},
/// failed to save pixel data to file
SaveData { source: std::io::Error },
/// Unexpected DICOM pixel data as data set sequence
UnexpectedPixelData,
}

impl Error {
fn to_exit_code(&self) -> i32 {
match self {
Error::ReadFile { .. } => -1,
Error::DecodePixelData { .. }
| Error::MissingOffsetEntry { .. }
| Error::MissingProperty { .. }
| Error::InvalidPropertyValue { .. }
| Error::FrameOutOfBounds { .. } => -2,
Error::ConvertImage { .. } => -3,
Error::SaveData { .. } | Error::SaveImage { .. } => -4,
Error::UnexpectedPixelData => -7,
}
}
}

fn main() {
let App {
file,
output,
frame_number,
verbose,
force_8bit,
force_16bit,
} = App::parse();
let args = App::parse();

tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(if verbose { Level::DEBUG } else { Level::INFO })
.with_max_level(if args.verbose {
Level::DEBUG
} else {
Level::INFO
})
.finish(),
)
.whatever_context("Could not set up global logging subscriber")
.unwrap_or_else(|e: Whatever| {
eprintln!("[ERROR] {}", Report::from_error(e));
});

let output = output.unwrap_or_else(|| {
let mut path = file.clone();
path.set_extension("png");
path
});

let obj = open_file(&file).unwrap_or_else(|e| {
run(args).unwrap_or_else(|e| {
let code = e.to_exit_code();
error!("{}", Report::from_error(e));
std::process::exit(-1);
std::process::exit(code);
});
}

let pixel = obj.decode_pixel_data().unwrap_or_else(|e| {
error!("{}", Report::from_error(e));
std::process::exit(-2);
});
fn run(args: App) -> Result<(), Error> {
let App {
file,
output,
frame_number,
force_8bit,
force_16bit,
unwrap,
verbose,
} = args;

if verbose {
println!(
"{}x{}x{} image, {}-bit",
pixel.columns(),
pixel.rows(),
pixel.samples_per_pixel(),
pixel.bits_stored()
);
}
let obj = open_file(&file).with_context(|_| ReadFileSnafu { path: file.clone() })?;

let mut options = ConvertOptions::new();
if unwrap {
let output = output.unwrap_or_else(|| {
let mut path = file.clone();

if force_16bit {
options = options.force_16bit();
} else if force_8bit {
options = options.force_8bit();
}
// try to identify a better extension for this file
// based on transfer syntax
match obj.meta().transfer_syntax() {
uids::JPEG_BASELINE8_BIT
| uids::JPEG_EXTENDED12_BIT
| uids::JPEG_LOSSLESS
| uids::JPEG_LOSSLESS_SV1 => {
path.set_extension("jpg");
}
uids::JPEG2000
| uids::JPEG2000MC
| uids::JPEG2000MC_LOSSLESS
| uids::JPEG2000_LOSSLESS => {
path.set_extension("jp2");
}
_ => {
path.set_extension("data");
}
}
path
});

let image = pixel
.to_dynamic_image_with_options(frame_number, &options)
.unwrap_or_else(|e| {
error!("{}", Report::from_error(e));
std::process::exit(-3);
let pixeldata = obj.get(tags::PIXEL_DATA).unwrap_or_else(|| {
error!("DICOM file has no pixel data");
std::process::exit(-2);
});

image.save(&output).unwrap_or_else(|e| {
error!("{}", Report::from_error(e));
std::process::exit(-4);
});
let out_data = match pixeldata.value() {
DicomValue::PixelSequence(seq) => {
let number_of_frames = match obj.get(tags::NUMBER_OF_FRAMES) {
Some(elem) => elem.to_int::<u32>().unwrap_or_else(|e| {
tracing::warn!("Invalid Number of Frames: {}", e);
1
}),
None => 1,
};

if number_of_frames as usize == seq.fragments().len() {
// frame-to-fragment mapping is 1:1

// get fragment containing our frame
let fragment =
seq.fragments()
.get(frame_number as usize)
.unwrap_or_else(|| {
error!("Frame number {} is out of range", frame_number);
std::process::exit(-2);
});

Cow::Borrowed(&fragment[..])
} else {
// In this case we look up the basic offset table
// and gather all of the frame's fragments in a single vector.
// Note: not the most efficient way to do this,
// consider optimizing later with byte chunk readers
let offset_table = seq.offset_table();
let base_offset = offset_table.get(frame_number as usize).copied();
let base_offset = if frame_number == 0 {
base_offset.unwrap_or(0) as usize
} else {
base_offset.context(MissingOffsetEntrySnafu { frame_number })? as usize
};
let next_offset = offset_table.get(frame_number as usize + 1);

let mut offset = 0;
let mut frame_data = Vec::new();
for fragment in seq.fragments() {
// include it
if offset >= base_offset {
frame_data.extend_from_slice(fragment);
}
offset += fragment.len() + 8;
if let Some(&next_offset) = next_offset {
if offset >= next_offset as usize {
// next fragment is for the next frame
break;
}
}
}

Cow::Owned(frame_data)
}
}
DicomValue::Primitive(v) => {
// grab the intended slice based on image properties

let get_int_property = |tag, name| {
obj.get(tag)
.context(MissingPropertySnafu { name })?
.to_int::<usize>()
.context(InvalidPropertyValueSnafu { name })
};

if verbose {
println!("Image saved to {}", output.display());
let rows = get_int_property(tags::ROWS, "Rows")?;
let columns = get_int_property(tags::COLUMNS, "Columns")?;
let samples_per_pixel =
get_int_property(tags::SAMPLES_PER_PIXEL, "Samples Per Pixel")?;
let bits_allocated = get_int_property(tags::BITS_ALLOCATED, "Bits Allocated")?;
let frame_size = rows * columns * samples_per_pixel * ((bits_allocated + 7) / 8);

let frame = frame_number as usize;
Cow::Owned(
v.to_bytes()
.get((frame_size * frame)..(frame_size * (frame + 1)))
.context(FrameOutOfBoundsSnafu { frame_number })?
.to_vec(),
)
}
_ => {
return UnexpectedPixelDataSnafu.fail();
}
};

std::fs::write(output, out_data).context(SaveDataSnafu)?;
} else {
let output = output.unwrap_or_else(|| {
let mut path = file.clone();
path.set_extension("png");
path
});

let pixel = obj.decode_pixel_data().context(DecodePixelDataSnafu)?;

if verbose {
println!(
"{}x{}x{} image, {}-bit",
pixel.columns(),
pixel.rows(),
pixel.samples_per_pixel(),
pixel.bits_stored()
);
}

let mut options = ConvertOptions::new();

if force_16bit {
options = options.force_16bit();
} else if force_8bit {
options = options.force_8bit();
}

let image = pixel
.to_dynamic_image_with_options(frame_number, &options)
.context(ConvertImageSnafu)?;

image.save(&output).context(SaveImageSnafu)?;

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

Ok(())
}

#[cfg(test)]
Expand Down

0 comments on commit 2abef21

Please sign in to comment.