Skip to content
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

[pixeldata] Apply VOI LUT when rendering images #599

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
150 changes: 150 additions & 0 deletions pixeldata/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub enum AttributeName {
VoiLutFunction,
WindowCenter,
WindowWidth,
LutDescriptor,
LutData,
LutExplanation,
}

impl std::fmt::Display for AttributeName {
Expand Down Expand Up @@ -610,6 +613,153 @@ pub fn photometric_interpretation<D: DataDictionary + Clone>(
.into())
}

/// A decoded representation of the
/// DICOM _VOI LUT Sequence_ attribute.
///
/// See [section C.8.11.3.1.5][1] of the standard for more details.
///
/// [1]: https://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.8.html#sect_C.8.11.3.1.5
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a pretty old version of the standard. Search engines are often guilty of not providing pages to the latest versions.

Suggested change
/// [1]: https://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.8.html#sect_C.8.11.3.1.5
/// [1]: https://dicom.nema.org/medical/dicom/2024d/output/chtml/part03/sect_C.8.11.3.html#sect_C.8.11.3.1.5

#[derive(Clone, Debug)]
pub struct VoiLut {
/// Minimum pixel value to be mapped. All values below this should be mapped
/// to the first entry of the table.
///
/// The value can either be a signed or an unsigned 16-bit integer, an i32
/// accomodates both.
pub min_pixel_value: i32,
/// Number of bits stored for each LUT entry
pub bits_stored: u8,
/// LUT data. Pixels with value min_pixel_value or below get mapped to the
/// first entry, pixels with value min_pixel_value+1 to the second entry,
/// and so forth. Pixels with value higher or equal to min_pixel_value +
/// data.len() get mapped to the last entry.
pub data: Vec<u16>,
/// Free form text explanation of the meaning of the LUT.
pub explanation: Option<String>,
}

fn parse_voi_lut_entry<D: DataDictionary + Clone>(entry: &InMemDicomObject<D>) -> Result<VoiLut> {
let descriptor_elements: Vec<i32> = entry
.element_opt(tags::LUT_DESCRIPTOR)
.context(RetrieveSnafu {
name: AttributeName::LutDescriptor,
})?
.context(MissingRequiredSnafu {
name: AttributeName::LutDescriptor,
})?
.to_multi_int()
.context(ConvertValueSnafu {
name: AttributeName::LutDescriptor,
})?;
ensure!(
descriptor_elements.len() == 3,
InvalidValueSnafu {
name: AttributeName::LutDescriptor,
value: format!("value with multiplicity {}", descriptor_elements.len()),
}
);
ensure!(
descriptor_elements[0] > 0,
InvalidValueSnafu {
name: AttributeName::LutDescriptor,
value: format!("value with LUT length {}", descriptor_elements[0]),
}
);
let expected_lut_len: usize = descriptor_elements[0] as usize;

let min_pixel_value = descriptor_elements[1];

ensure!(
descriptor_elements[2] >= 0 && descriptor_elements[2] <= 16,
InvalidValueSnafu {
name: AttributeName::LutDescriptor,
value: format!("value with bits stored {}", descriptor_elements[2])
}
);
let bits_stored = descriptor_elements[2] as u8;

let lut_data = entry
.element_opt(tags::LUT_DATA)
.context(RetrieveSnafu {
name: AttributeName::LutData,
})?
.context(MissingRequiredSnafu {
name: AttributeName::LutData,
})?
.uint16_slice()
.context(CastValueSnafu {
name: AttributeName::LutData,
})?;
ensure!(
expected_lut_len == lut_data.len(),
InvalidValueSnafu {
name: AttributeName::LutData,
value: format!(
"sequence with {} elements (expected {expected_lut_len})",
lut_data.len()
),
}
);

let explanation = if let Some(val) =
entry
.element_opt(tags::LUT_EXPLANATION)
.context(RetrieveSnafu {
name: AttributeName::LutExplanation,
})? {
Some(
val.string()
.context(CastValueSnafu {
name: AttributeName::LutExplanation,
})?
.to_string(),
)
} else {
None
};

Ok(VoiLut {
min_pixel_value,
bits_stored,
data: lut_data.to_vec(),
explanation,
})
}

pub fn voi_lut_sequence<D: DataDictionary + Clone>(
obj: &FileDicomObject<InMemDicomObject<D>>,
) -> Option<Vec<VoiLut>> {
obj.get(tags::VOILUT_SEQUENCE)
.and_then(|e| {
e.items().and_then(|items| {
items
.iter()
.map(|e| parse_voi_lut_entry(e).ok())
.collect::<Option<Vec<VoiLut>>>()
})
})
.or_else(|| {
get_from_per_frame(obj, [tags::FRAME_VOILUT_SEQUENCE, tags::VOILUT_SEQUENCE]).and_then(
|v| {
v.into_iter()
.flat_map(|el| el.items())
.flat_map(|items| items.iter().map(|e| parse_voi_lut_entry(e).ok()))
.collect()
},
)
})
.or_else(|| {
get_from_shared(obj, [tags::FRAME_VOILUT_SEQUENCE, tags::VOILUT_SEQUENCE]).and_then(
|v| {
v.into_iter()
.flat_map(|el| el.items())
.flat_map(|items| items.iter().map(|e| parse_voi_lut_entry(e).ok()))
.collect()
},
)
})
}

#[cfg(test)]
mod tests {
use super::rescale_intercept;
Expand Down
4 changes: 4 additions & 0 deletions pixeldata/src/gdcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ where
.map(|v| VoiLutFunction::try_from((*v).as_str()).ok())
.collect()
});
let voi_lut_sequence = voi_lut_sequence(self);

ensure!(
rescale_intercept.len() == rescale_slope.len(),
Expand Down Expand Up @@ -178,6 +179,7 @@ where
rescale,
voi_lut_function,
window,
voi_lut_sequence,
enforce_frame_fg_vm_match: false,
})
}
Expand Down Expand Up @@ -236,6 +238,7 @@ where
.map(|v| VoiLutFunction::try_from((*v).as_str()).ok())
.collect()
});
let voi_lut_sequence = voi_lut_sequence(self);

let decoded_pixel_data = match pixel_data.value() {
DicomValue::PixelSequence(v) => {
Expand Down Expand Up @@ -356,6 +359,7 @@ where
rescale: rescale,
voi_lut_function,
window,
voi_lut_sequence,
enforce_frame_fg_vm_match: false,
})
}
Expand Down
Loading
Loading