rawshift_image/transforms/
tonemap.rs1use crate::core::image::RgbImage;
16use crate::processing::color::apply_gamma;
17
18pub fn apply_tone_reproduction(image: &mut RgbImage, custom_gamma: Option<f32>) {
28 if let Some(gamma) = custom_gamma {
29 apply_gamma(image, gamma);
30 } else {
31 apply_tonemap(image, image.baseline_exposure());
32 }
33}
34
35pub fn apply_tonemap(image: &mut RgbImage, baseline_exposure: Option<f32>) {
44 let gain = baseline_exposure.map(|ev| 2.0f32.powf(ev)).unwrap_or(1.0);
45 let lut = build_lut(gain);
46 for pixel in &mut image.data {
47 *pixel = lut[*pixel as usize];
48 }
49}
50
51fn build_lut(gain: f32) -> Box<[u16; 65536]> {
60 let white_point = gain.max(1e-6_f32);
62 let white_out = hable(white_point);
63
64 let mut table = Box::new([0u16; 65536]);
65 for (i, entry) in table.iter_mut().enumerate() {
66 let linear = i as f32 / 65535.0;
67 let exposed = linear * gain;
68 let tonemapped = (hable(exposed) / white_out).clamp(0.0, 1.0);
69 let srgb = srgb_encode(tonemapped);
70 *entry = (srgb * 65535.0 + 0.5) as u16;
71 }
72 table
73}
74
75#[inline(always)]
79fn hable(x: f32) -> f32 {
80 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
87}
88
89#[inline(always)]
93pub(crate) fn srgb_encode(linear: f32) -> f32 {
94 if linear <= 0.003_130_8 {
95 linear * 12.92
96 } else {
97 1.055 * linear.powf(1.0 / 2.4) - 0.055
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::core::image::RgbImage;
105
106 fn make_image(values: &[u16]) -> RgbImage {
107 let n = values.len() as u32 / 3;
108 RgbImage::new(n, 1, values.to_vec())
109 }
110
111 #[test]
112 fn black_stays_black() {
113 let mut img = make_image(&[0, 0, 0]);
114 apply_tonemap(&mut img, None);
115 assert_eq!(img.data[0], 0);
116 assert_eq!(img.data[1], 0);
117 assert_eq!(img.data[2], 0);
118 }
119
120 #[test]
121 fn white_maps_to_white_no_exposure() {
122 let mut img = make_image(&[65535, 65535, 65535]);
124 apply_tonemap(&mut img, None);
125 assert_eq!(img.data[0], 65535);
126 }
127
128 #[test]
129 fn negative_baseline_exposure_maps_sensor_white_to_display_white() {
130 let mut img = make_image(&[65535, 65535, 65535]);
133 apply_tonemap(&mut img, Some(-0.83));
134 assert_eq!(img.data[0], 65535);
135 }
136
137 #[test]
138 fn negative_baseline_exposure_darkens_midtones() {
139 let mut img_no_exp = make_image(&[32768, 32768, 32768]);
142 let mut img_neg_exp = make_image(&[32768, 32768, 32768]);
143 apply_tonemap(&mut img_no_exp, None);
144 apply_tonemap(&mut img_neg_exp, Some(-0.83));
145 assert!(
146 img_neg_exp.data[0] < img_no_exp.data[0],
147 "negative EV {} should darken mid-grey {}",
148 img_neg_exp.data[0],
149 img_no_exp.data[0]
150 );
151 }
152
153 #[test]
154 fn positive_baseline_exposure_brightens_midtones() {
155 let mut img_no_exp = make_image(&[16384, 16384, 16384]);
156 let mut img_pos_exp = make_image(&[16384, 16384, 16384]);
157 apply_tonemap(&mut img_no_exp, None);
158 apply_tonemap(&mut img_pos_exp, Some(0.5));
159 assert!(
160 img_pos_exp.data[0] > img_no_exp.data[0],
161 "positive EV {} should brighten {}",
162 img_pos_exp.data[0],
163 img_no_exp.data[0]
164 );
165 }
166
167 #[test]
168 fn srgb_encode_extremes() {
169 assert!((srgb_encode(0.0)).abs() < 1e-6);
170 assert!((srgb_encode(1.0) - 1.0).abs() < 1e-4);
171 }
172
173 #[test]
174 fn tone_reproduction_uses_gamma_when_specified() {
175 let mut img_gamma = make_image(&[32768, 32768, 32768]);
176 let mut img_filmic = make_image(&[32768, 32768, 32768]);
177 apply_tone_reproduction(&mut img_gamma, Some(2.2));
178 apply_tone_reproduction(&mut img_filmic, None);
179 assert_ne!(img_gamma.data[0], img_filmic.data[0]);
181 }
182
183 #[test]
184 fn tone_reproduction_none_matches_filmic() {
185 let mut img_repro = make_image(&[32768, 32768, 32768]);
186 let mut img_filmic = make_image(&[32768, 32768, 32768]);
187 apply_tone_reproduction(&mut img_repro, None);
188 apply_tonemap(&mut img_filmic, None);
189 assert_eq!(img_repro.data[0], img_filmic.data[0]);
190 }
191
192 #[test]
193 fn test_apply_tone_reproduction_clamps() {
194 let mut img = make_image(&[65535, 65535, 65535]);
196 apply_tone_reproduction(&mut img, None);
197 assert_eq!(img.data[0], 65535, "white should stay at max after tonemap");
198 assert_eq!(img.data[1], 65535);
199 assert_eq!(img.data[2], 65535);
200 }
201
202 #[test]
203 fn test_linear_tone_reproduction() {
204 let values = [1000u16, 32768, 65535];
207 let mut img = make_image(&values);
208 apply_tone_reproduction(&mut img, Some(1.0));
209 assert_eq!(img.data[0], values[0]);
211 assert_eq!(img.data[1], values[1]);
212 assert_eq!(img.data[2], values[2]);
213 }
214
215 #[test]
216 fn test_tonemap_output_range() {
217 for ev in [-2.0f32, -0.83, 0.0, 0.5, 2.0] {
219 for val in [0u16, 1000, 32768, 65535] {
220 let mut img = make_image(&[val, val, val]);
221 apply_tonemap(&mut img, Some(ev));
222 assert!(
224 !img.data.is_empty(),
225 "EV={}, input={}: output should not be empty",
226 ev,
227 val
228 );
229 }
230 }
231 }
232}