use crate::core::image::RgbImage;
use crate::processing::color::apply_gamma;
pub fn apply_tone_reproduction(image: &mut RgbImage, custom_gamma: Option<f32>) {
if let Some(gamma) = custom_gamma {
apply_gamma(image, gamma);
} else {
apply_tonemap(image, image.baseline_exposure());
}
}
pub fn apply_tonemap(image: &mut RgbImage, baseline_exposure: Option<f32>) {
let gain = baseline_exposure.map(|ev| 2.0f32.powf(ev)).unwrap_or(1.0);
let lut = build_lut(gain);
for pixel in &mut image.data {
*pixel = lut[*pixel as usize];
}
}
fn build_lut(gain: f32) -> Box<[u16; 65536]> {
let white_point = gain.max(1e-6_f32);
let white_out = hable(white_point);
let mut table = Box::new([0u16; 65536]);
for (i, entry) in table.iter_mut().enumerate() {
let linear = i as f32 / 65535.0;
let exposed = linear * gain;
let tonemapped = (hable(exposed) / white_out).clamp(0.0, 1.0);
let srgb = srgb_encode(tonemapped);
*entry = (srgb * 65535.0 + 0.5) as u16;
}
table
}
#[inline(always)]
fn hable(x: f32) -> f32 {
const A: f32 = 0.15; const B: f32 = 0.50; const C: f32 = 0.10; const D: f32 = 0.20; const E: f32 = 0.02; const F: f32 = 0.30; ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F
}
#[inline(always)]
pub(crate) fn srgb_encode(linear: f32) -> f32 {
if linear <= 0.003_130_8 {
linear * 12.92
} else {
1.055 * linear.powf(1.0 / 2.4) - 0.055
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::image::RgbImage;
fn make_image(values: &[u16]) -> RgbImage {
let n = values.len() as u32 / 3;
RgbImage::new(n, 1, values.to_vec())
}
#[test]
fn black_stays_black() {
let mut img = make_image(&[0, 0, 0]);
apply_tonemap(&mut img, None);
assert_eq!(img.data[0], 0);
assert_eq!(img.data[1], 0);
assert_eq!(img.data[2], 0);
}
#[test]
fn white_maps_to_white_no_exposure() {
let mut img = make_image(&[65535, 65535, 65535]);
apply_tonemap(&mut img, None);
assert_eq!(img.data[0], 65535);
}
#[test]
fn negative_baseline_exposure_maps_sensor_white_to_display_white() {
let mut img = make_image(&[65535, 65535, 65535]);
apply_tonemap(&mut img, Some(-0.83));
assert_eq!(img.data[0], 65535);
}
#[test]
fn negative_baseline_exposure_darkens_midtones() {
let mut img_no_exp = make_image(&[32768, 32768, 32768]);
let mut img_neg_exp = make_image(&[32768, 32768, 32768]);
apply_tonemap(&mut img_no_exp, None);
apply_tonemap(&mut img_neg_exp, Some(-0.83));
assert!(
img_neg_exp.data[0] < img_no_exp.data[0],
"negative EV {} should darken mid-grey {}",
img_neg_exp.data[0],
img_no_exp.data[0]
);
}
#[test]
fn positive_baseline_exposure_brightens_midtones() {
let mut img_no_exp = make_image(&[16384, 16384, 16384]);
let mut img_pos_exp = make_image(&[16384, 16384, 16384]);
apply_tonemap(&mut img_no_exp, None);
apply_tonemap(&mut img_pos_exp, Some(0.5));
assert!(
img_pos_exp.data[0] > img_no_exp.data[0],
"positive EV {} should brighten {}",
img_pos_exp.data[0],
img_no_exp.data[0]
);
}
#[test]
fn srgb_encode_extremes() {
assert!((srgb_encode(0.0)).abs() < 1e-6);
assert!((srgb_encode(1.0) - 1.0).abs() < 1e-4);
}
#[test]
fn tone_reproduction_uses_gamma_when_specified() {
let mut img_gamma = make_image(&[32768, 32768, 32768]);
let mut img_filmic = make_image(&[32768, 32768, 32768]);
apply_tone_reproduction(&mut img_gamma, Some(2.2));
apply_tone_reproduction(&mut img_filmic, None);
assert_ne!(img_gamma.data[0], img_filmic.data[0]);
}
#[test]
fn tone_reproduction_none_matches_filmic() {
let mut img_repro = make_image(&[32768, 32768, 32768]);
let mut img_filmic = make_image(&[32768, 32768, 32768]);
apply_tone_reproduction(&mut img_repro, None);
apply_tonemap(&mut img_filmic, None);
assert_eq!(img_repro.data[0], img_filmic.data[0]);
}
#[test]
fn test_apply_tone_reproduction_clamps() {
let mut img = make_image(&[65535, 65535, 65535]);
apply_tone_reproduction(&mut img, None);
assert_eq!(img.data[0], 65535, "white should stay at max after tonemap");
assert_eq!(img.data[1], 65535);
assert_eq!(img.data[2], 65535);
}
#[test]
fn test_linear_tone_reproduction() {
let values = [1000u16, 32768, 65535];
let mut img = make_image(&values);
apply_tone_reproduction(&mut img, Some(1.0));
assert_eq!(img.data[0], values[0]);
assert_eq!(img.data[1], values[1]);
assert_eq!(img.data[2], values[2]);
}
#[test]
fn test_tonemap_output_range() {
for ev in [-2.0f32, -0.83, 0.0, 0.5, 2.0] {
for val in [0u16, 1000, 32768, 65535] {
let mut img = make_image(&[val, val, val]);
apply_tonemap(&mut img, Some(ev));
assert!(
!img.data.is_empty(),
"EV={}, input={}: output should not be empty",
ev,
val
);
}
}
}
}