Skip to content

Support bits_allocated == 1 #654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 4, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 63 additions & 11 deletions pixeldata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ pub enum InnerError {
#[snafu(display("PixelData attribute is not a primitive value or pixel sequence"))]
InvalidPixelData { backtrace: Backtrace },

#[snafu(display("Invalid BitsAllocated, must be 8 or 16"))]
#[snafu(display("Invalid BitsAllocated, must be 1, 8 or 16"))]
InvalidBitsAllocated { backtrace: Backtrace },

#[snafu(display("Unsupported PhotometricInterpretation `{}`", pi))]
Expand Down Expand Up @@ -468,7 +468,7 @@ impl DecodedPixelData<'_> {
/// Retrieve a slice of a frame's raw pixel data samples as bytes,
/// irrespective of the expected size of each sample.
pub fn frame_data(&self, frame: u32) -> Result<&[u8]> {
let bytes_per_sample = self.bits_allocated as usize / 8;
let bytes_per_sample = (self.bits_allocated as usize + 7) / 8;
let frame_length = self.rows as usize
* self.cols as usize
* self.samples_per_pixel as usize
Expand Down Expand Up @@ -1157,6 +1157,10 @@ impl DecodedPixelData<'_> {
}
}
}
1 => {
let data = self.frame_data(frame)?;
self.mono_image_with_extend(data.iter().copied(), *bit_depth)?
}
_ => InvalidBitsAllocatedSnafu.fail()?,
};
// Convert MONOCHROME1 => MONOCHROME2
Expand Down Expand Up @@ -2268,17 +2272,35 @@ where
}
DicomValue::Primitive(p) => {
// Non-encoded, just return the pixel data for a single frame
let frame_size = ((bits_allocated + 7) / 8) as usize
* samples_per_pixel as usize
* rows as usize
* cols as usize;
let frame_offset = frame_size * frame as usize;
let frame_pixels = (rows as usize) * (cols as usize);
let frame_samples = frame_pixels * (samples_per_pixel as usize);
let frame_size = if bits_allocated == 1 {
frame_samples / 8
} else {
frame_samples * ((bits_allocated as usize + 7) / 8)
};
let frame_offset = frame_size * (frame as usize);

let data = p.to_bytes();
data.get(frame_offset..frame_offset + frame_size)
.with_context(|| FrameOutOfRangeSnafu {

let frame_data = data.get(frame_offset..frame_offset + frame_size).context(
FrameOutOfRangeSnafu {
frame_number: frame,
})?
.to_vec()
},
)?;

let pixel_data = if bits_allocated == 1 {
// Map every bit in each byte to a separate byte of either 0 or 255
frame_data
.iter()
.flat_map(|&byte| (0..8).map(move |bit| ((byte >> bit) & 1) * 255))
.take(frame_pixels)
.collect()
} else {
frame_data.to_vec()
};

pixel_data
}
DicomValue::Sequence(..) => InvalidPixelDataSnafu.fail()?,
};
Expand Down Expand Up @@ -2910,4 +2932,34 @@ mod tests {
let interleaved: Vec<u8> = vec![1, 5, 9, 2, 6, 10, 3, 7, 11, 4, 8, 12];
assert_eq!(interleave(&planar), interleaved);
}

#[cfg(feature = "image")]
#[test]
fn test_1bit_image_decoding() {
use crate::PixelDecoder as _;
use std::path::Path;

let test_file =
dicom_test_files::path("pydicom/liver.dcm").expect("test DICOM file should exist");
println!("Parsing pixel data for {}", test_file.display());
let obj = dicom_object::open_file(test_file).unwrap();
let pixel_data = obj.decode_pixel_data_frame(0).unwrap();
let output_dir =
Path::new("../target/dicom_test_files/_out/test_1bit_image_decoding");
std::fs::create_dir_all(output_dir).unwrap();

assert_eq!(pixel_data.number_of_frames(), 1, "expected 1 frame only");

let image = pixel_data.to_dynamic_image(0).unwrap();
let image_path = output_dir.join(format!(
"{}-{}.png",
Path::new("pydicom/liver.dcm")
.file_stem()
.unwrap()
.to_str()
.unwrap(),
0,
));
image.save(image_path).unwrap();
}
}