Skip to content

Commit

Permalink
Merge pull request #347 from Enet4/bug/346
Browse files Browse the repository at this point in the history
Change LUT to discard high bits of pixel data sample inputs
  • Loading branch information
Enet4 authored May 29, 2023
2 parents ce90d9f + 6b5522b commit 1ab8b09
Showing 1 changed file with 64 additions and 21 deletions.
85 changes: 64 additions & 21 deletions pixeldata/src/lut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ impl CreateLutError {
}
}

/// A look up table for pixel data sample value transformations.
/// A look up table (LUT) for pixel data sample value transformations.
///
/// All LUTs are guaranteed to have a size of a power of 2,
/// as defined by the constructor parameter `bits_stored`.
/// The type parameter `T` is the numeric type of the outputs produced.
/// Although the bit depths of the inputs may vary,
/// it is a run-time error to construct a `Lut<T>`
/// where `T` has less bits than `bits_stored`.
///
/// # Example
///
Expand Down Expand Up @@ -73,8 +80,8 @@ pub struct Lut<T> {
/// the table which maps an index to a transformed value,
/// of size 2 to the power of `bits_stored`
table: Vec<T>,
/// whether the input sample values are signed (Pixel Representation = 1)
signed: bool,
/// the mask to apply on all sample inputs
sample_mask: u32,
}

impl<T: 'static> Lut<T>
Expand Down Expand Up @@ -127,10 +134,10 @@ where
})
})
.collect();
Ok(Self {
table: table?,
signed,
})

let table = table?;
let sample_mask = table.len() as u32 - 1;
Ok(Self { table, sample_mask })
}

/// Create a new LUT containing only the modality rescale transformation.
Expand Down Expand Up @@ -281,25 +288,15 @@ where
/// this method works for signed sample values as well,
/// with the bits reinterpreted as their unsigned counterpart.
///
/// # Panics
///
/// Panics if `sample_value` is larger or equal to `2^bits_stored`.
/// The highest bits from the sample after `bits_stored` bits are discarded,
/// thus silently ignoring them.
pub fn get<I: 'static>(&self, sample_value: I) -> T
where
I: Copy,
I: Into<u32>,
{
let val = sample_value.into();
let index = if self.signed {
// adjust for signedness by masking out the extra sign bits
let mask = self.table.len() - 1;
val as usize & mask
} else {
val as usize
};
assert!((index as usize) < self.table.len());

self.table[index as usize]
let val = sample_value.into() & self.sample_mask;
self.table[val as usize]
}

/// Adapts an iterator of pixel data sample values
Expand Down Expand Up @@ -358,6 +355,52 @@ mod tests {
assert_eq!(lut.get(-1_i16 as u16), -1026);
assert_eq!(lut.get(-2_i16 as u16), -1028);
assert_eq!(lut.get(500 as u16), -24);

// input is truncated to fit
// (bit #10 won't fit in a 10-bit LUT)
assert_eq!(lut.get(1024 + 500_u16), -24);
}

#[test]
fn lut_unsigned_numbers() {
// 12-bit precision input, unsigned 16 bit output
let lut: Lut<u16> = Lut::new_rescale_and_window(
12,
false,
Rescale::new(1., -1024.),
WindowLevelTransform::linear(WindowLevel {
width: 300.,
center: 50.,
}),
)
.unwrap();

// < 0
let val: u16 = lut.get(824_u16);
assert_eq!(val, 0);

// > 200
let val: u16 = lut.get(1224_u16);
assert_eq!(val, 65_535);

// around the middle
let mid_i = 1024_u16 + 50;
let mid_val: u16 = lut.get(mid_i);
let expected_range = 32_600..=32_950;
assert!(
expected_range.contains(&mid_val),
"outcome was {}, expected to be in {:?}",
mid_val,
expected_range,
);

// input is truncated to fit
// (bits #12 and up won't fit in a 12-bit LUT)
assert_eq!(lut.get(0x1000 | mid_i), mid_val);
assert_eq!(lut.get(0x2000 | mid_i), mid_val);
assert_eq!(lut.get(0x4000 | mid_i), mid_val);
assert_eq!(lut.get(0x8000 | mid_i), mid_val);
assert_eq!(lut.get(0xFFFF_u16), 65_535);
}

#[test]
Expand Down

0 comments on commit 1ab8b09

Please sign in to comment.