use crate::header::{Bitpix, Header};
use std::error::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Normalizer {
zero_offset: f64,
scale: f64,
minimum: f64,
maximum: f64,
blank: Option<f64>,
}
impl Normalizer {
pub fn new(zero_offset: f64, scale: f64, minimum: f64, maximum: f64) -> Self {
Self {
zero_offset,
scale,
minimum: minimum.min(maximum),
maximum: minimum.max(maximum),
blank: None,
}
}
pub fn with_blank(mut self, blank: Option<i64>) -> Self {
self.blank = blank.map(|blank| blank as f64);
self
}
pub fn blank(&self) -> Option<f64> {
self.blank
}
pub fn is_blank(&self, raw: f64) -> bool {
self.blank == Some(raw)
}
pub fn for_bitpix(bitpix: Bitpix, zero_offset: f64, scale: f64) -> Option<Self> {
let (raw_min, raw_max) = bitpix.value_range()?;
let physical = |raw: f64| zero_offset + scale * raw;
Some(Self::new(
zero_offset,
scale,
physical(raw_min),
physical(raw_max),
))
}
pub fn from_samples(
zero_offset: f64,
scale: f64,
samples: impl IntoIterator<Item = f64>,
) -> Self {
let mut minimum = f64::INFINITY;
let mut maximum = f64::NEG_INFINITY;
for raw in samples {
let physical = zero_offset + scale * raw;
if physical.is_finite() {
minimum = minimum.min(physical);
maximum = maximum.max(physical);
}
}
if !minimum.is_finite() || !maximum.is_finite() {
minimum = 0.0;
maximum = 0.0;
}
Self::new(zero_offset, scale, minimum, maximum)
}
pub fn from_header(header: &Header) -> Result<Self, Box<dyn Error + Send + Sync>> {
let bitpix = header
.bitpix()
.ok_or("Cannot normalise an image from a header without a BITPIX card")?;
let zero_offset = header.bzero_or_default();
let scale = header.bscale_or_default();
let blank = blank_for(header, bitpix);
if let (Some(minimum), Some(maximum)) = (header.data_min(), header.data_max()) {
return Ok(Self::new(zero_offset, scale, minimum, maximum).with_blank(blank));
}
Self::for_bitpix(bitpix, zero_offset, scale)
.map(|normalizer| normalizer.with_blank(blank))
.ok_or_else(|| {
format!(
"Cannot normalise a {:?} image in a single pass: it carries neither a DATAMIN nor \
a DATAMAX card, so its black and white points are unknown",
bitpix
)
.into()
})
}
pub fn physical(&self, raw: f64) -> f64 {
if self.is_blank(raw) {
return f64::NAN;
}
self.zero_offset + self.scale * raw
}
pub fn normalize(&self, raw: f64) -> f64 {
if self.is_blank(raw) {
return f64::NAN;
}
let range = self.maximum - self.minimum;
if range <= 0.0 || !range.is_finite() {
return 0.0;
}
((self.physical(raw) - self.minimum) / range).clamp(0.0, 1.0)
}
}
fn blank_for(header: &Header, bitpix: Bitpix) -> Option<i64> {
match bitpix {
Bitpix::F32 | Bitpix::F64 => None,
Bitpix::U8 | Bitpix::I16 | Bitpix::I32 => header.blank(),
}
}
#[cfg(test)]
mod tests {
use super::Normalizer;
fn normalizer(zero_offset: f64, scale: f64, minimum: f64, maximum: f64) -> Normalizer {
Normalizer {
zero_offset,
scale,
minimum,
maximum,
blank: None,
}
}
#[test]
fn a_blank_pixel_has_no_physical_value_and_no_place_on_the_scale() {
let normalizer = normalizer(0.0, 1.0, 0.0, 100.0).with_blank(Some(-32768));
assert!(normalizer.normalize(-32768.0).is_nan());
assert!(normalizer.physical(-32768.0).is_nan());
assert_eq!(normalizer.normalize(50.0), 0.5);
assert_eq!(normalizer.physical(50.0), 50.0);
}
#[test]
fn a_blank_pixel_is_not_clamped_to_black() {
let plain = normalizer(0.0, 1.0, 0.0, 100.0);
assert_eq!(plain.normalize(-32768.0), 0.0);
let blanked = plain.with_blank(Some(-32768));
assert!(blanked.normalize(-32768.0).is_nan());
}
#[test]
fn unsigned_16_bit_samples_span_the_full_range() {
let normalizer = normalizer(32768.0, 1.0, 0.0, 65535.0);
assert_eq!(normalizer.normalize(i16::MIN as f64), 0.0);
assert_eq!(normalizer.normalize(i16::MAX as f64), 1.0);
assert!((normalizer.normalize(-1.0) - 0.5).abs() < 1e-4);
}
#[test]
fn raw_values_are_converted_to_physical_units() {
let normalizer = normalizer(32768.0, 2.0, 0.0, 65535.0);
assert_eq!(normalizer.physical(0.0), 32768.0);
assert_eq!(normalizer.physical(10.0), 32788.0);
}
#[test]
fn values_outside_the_range_are_clamped() {
let normalizer = normalizer(0.0, 1.0, 10.0, 20.0);
assert_eq!(normalizer.normalize(5.0), 0.0);
assert_eq!(normalizer.normalize(25.0), 1.0);
assert_eq!(normalizer.normalize(15.0), 0.5);
}
#[test]
fn a_degenerate_range_does_not_produce_nan_or_infinity() {
let normalizer = normalizer(0.0, 0.0, 7.0, 7.0);
assert_eq!(normalizer.normalize(1.0), 0.0);
assert_eq!(normalizer.normalize(f64::MAX), 0.0);
}
}