1pub mod nlm;
2
3use crate::error::{IrisError, Result};
4use crate::image::Image;
5use burn::tensor::backend::Backend;
6
7pub struct Photo;
9
10impl Photo {
11 pub fn fast_nl_means_denoising<B: Backend>(
23 image: &Image<B>,
24 h: f32,
25 patch_radius: usize,
26 search_radius: usize,
27 ) -> Result<Image<B>> {
28 let dims = image.tensor.dims();
29 let c = dims[0];
30 let img_h = dims[1];
31 let img_w = dims[2];
32
33 let tensor_data = image.tensor.clone().into_data();
34 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
35 let mut out_vals = vec![0.0f32; c * img_h * img_w];
36
37 let h_sq_inv = 1.0 / (h * h * (2.0 * patch_radius as f32 + 1.0).powi(2));
38
39 for ch in 0..c {
40 let ch_offset = ch * img_h * img_w;
41 for cy in 0..img_h {
42 for cx in 0..img_w {
43 let center_idx = ch_offset + cy * img_w + cx;
44 let center_val = flat_vals[center_idx];
45
46 let mut weight_sum = 0.0f32;
47 let mut value_sum = 0.0f32;
48
49 let sy_start = cy.saturating_sub(search_radius);
51 let sy_end = (cy + search_radius + 1).min(img_h);
52 let sx_start = cx.saturating_sub(search_radius);
53 let sx_end = (cx + search_radius + 1).min(img_w);
54
55 for sy in sy_start..sy_end {
56 for sx in sx_start..sx_end {
57 let mut patch_dist = 0.0f32;
59 let mut valid = true;
60
61 for py in 0..=(2 * patch_radius) {
62 for px in 0..=(2 * patch_radius) {
63 let offy = py as isize - patch_radius as isize;
64 let offx = px as isize - patch_radius as isize;
65
66 let ny_c = cy as isize + offy;
67 let nx_c = cx as isize + offx;
68 let ny_s = sy as isize + offy;
69 let nx_s = sx as isize + offx;
70
71 if ny_c < 0
72 || ny_c >= img_h as isize
73 || nx_c < 0
74 || nx_c >= img_w as isize
75 || ny_s < 0
76 || ny_s >= img_h as isize
77 || nx_s < 0
78 || nx_s >= img_w as isize
79 {
80 valid = false;
81 break;
82 }
83
84 let val_c = flat_vals
85 [ch_offset + ny_c as usize * img_w + nx_c as usize];
86 let val_s = flat_vals
87 [ch_offset + ny_s as usize * img_w + nx_s as usize];
88 let diff = val_c - val_s;
89 patch_dist += diff * diff;
90 }
91 if !valid {
92 break;
93 }
94 }
95
96 if !valid {
97 continue;
98 }
99
100 let dx = (sx as f64 - cx as f64) as f32;
102 let dy = (sy as f64 - cy as f64) as f32;
103 let spatial_dist =
104 (dx * dx + dy * dy) / (search_radius as f32 * search_radius as f32);
105 let spatial_weight = (-spatial_dist * 2.0).exp();
106
107 let weight = (-patch_dist * h_sq_inv).exp() * spatial_weight;
108 value_sum += flat_vals[ch_offset + sy * img_w + sx] * weight;
109 weight_sum += weight;
110 }
111 }
112
113 out_vals[center_idx] = if weight_sum > 1e-10 {
114 (value_sum / weight_sum).clamp(0.0, 1.0)
115 } else {
116 center_val
117 };
118 }
119 }
120 }
121
122 let device = image.tensor.device();
123 let data = burn::tensor::TensorData::new(out_vals, [c, img_h, img_w]);
124 let tensor = burn::tensor::Tensor::<B, 3>::from_data(data, &device);
125 Ok(Image::new(tensor))
126 }
127}
128
129pub struct MergeMertens {
142 pub contrast_weight: f32,
143 pub saturation_weight: f32,
144 pub exposure_weight: f32,
145}
146
147impl MergeMertens {
148 #[must_use]
149 pub fn new() -> Self {
150 Self {
151 contrast_weight: 1.0,
152 saturation_weight: 1.0,
153 exposure_weight: 1.0,
154 }
155 }
156
157 #[must_use]
159 pub fn with_contrast_weight(mut self, w: f32) -> Self {
160 self.contrast_weight = w;
161 self
162 }
163
164 #[must_use]
166 pub fn with_saturation_weight(mut self, w: f32) -> Self {
167 self.saturation_weight = w;
168 self
169 }
170
171 #[must_use]
173 pub fn with_exposure_weight(mut self, w: f32) -> Self {
174 self.exposure_weight = w;
175 self
176 }
177
178 pub fn process<B: Backend>(&self, images: &[Image<B>]) -> Result<Image<B>> {
182 if images.is_empty() {
183 return Err(IrisError::InvalidParameter(
184 "Images list cannot be empty".into(),
185 ));
186 }
187
188 let dims = images[0].tensor.dims();
189 if dims[0] != 3 {
190 return Err(IrisError::InvalidParameter(
191 "Input images must be 3-channel RGB".into(),
192 ));
193 }
194 let h = dims[1];
195 let w = dims[2];
196 let n = images.len();
197
198 for img in images.iter().skip(1) {
200 let d = img.tensor.dims();
201 if d[0] != 3 || d[1] != h || d[2] != w {
202 return Err(IrisError::DimensionMismatch {
203 expected: vec![3, h, w],
204 actual: vec![d[0], d[1], d[2]],
205 });
206 }
207 }
208
209 let mut all_weights: Vec<Vec<f32>> = Vec::with_capacity(n);
211 let mut all_pixel_data: Vec<Vec<f32>> = Vec::with_capacity(n);
212
213 for img in images {
214 let data = img.tensor.clone().into_data();
215 let flat: Vec<f32> = data.iter::<f32>().collect();
216
217 let mut weights = vec![1.0f32; h * w];
218
219 let gray: Vec<f32> = (0..h * w)
221 .map(|i| 0.299 * flat[i] + 0.587 * flat[h * w + i] + 0.114 * flat[2 * h * w + i])
222 .collect();
223
224 for y in 0..h {
225 for x in 0..w {
226 let c = gray[y * w + x];
227 let mut laplacian = -4.0 * c;
228 if y > 0 {
229 laplacian += gray[(y - 1) * w + x];
230 }
231 if y + 1 < h {
232 laplacian += gray[(y + 1) * w + x];
233 }
234 if x > 0 {
235 laplacian += gray[y * w + x - 1];
236 }
237 if x + 1 < w {
238 laplacian += gray[y * w + x + 1];
239 }
240 let contrast = laplacian.abs();
241 let ci = y * w + x;
242 weights[ci] *= contrast.powf(self.contrast_weight);
243 }
244 }
245
246 for y in 0..h {
248 for x in 0..w {
249 let ci = y * w + x;
250 let r = flat[ci];
251 let g = flat[h * w + ci];
252 let b = flat[2 * h * w + ci];
253 let mean = (r + g + b) / 3.0;
254 let variance =
255 ((r - mean).powi(2) + (g - mean).powi(2) + (b - mean).powi(2)) / 3.0;
256 let saturation = variance.sqrt();
257 weights[ci] *= saturation.powf(self.saturation_weight);
258 }
259 }
260
261 for y in 0..h {
263 for x in 0..w {
264 let ci = y * w + x;
265 let lum = gray[ci];
266 let sigma = 0.2;
267 let diff = lum - 0.5f32;
268 let exposure_w = (-(diff * diff) / (2.0 * sigma * sigma)).exp();
269 weights[ci] *= exposure_w.powf(self.exposure_weight);
270 }
271 }
272
273 all_pixel_data.push(flat);
274 all_weights.push(weights);
275 }
276
277 let mut out_vals = vec![0.0f32; 3 * h * w];
279 for y in 0..h {
280 for x in 0..w {
281 let ci = y * w + x;
282 let mut total_weight = 0.0f32;
283
284 for w_map in &all_weights {
285 total_weight += w_map[ci];
286 }
287
288 if total_weight < 1e-10 {
289 for ch in 0..3 {
291 let mut sum = 0.0f32;
292 for pixel_data in &all_pixel_data {
293 sum += pixel_data[ch * h * w + ci];
294 }
295 out_vals[ch * h * w + ci] = sum / n as f32;
296 }
297 } else {
298 for ch in 0..3 {
299 let mut blended = 0.0f32;
300 for (idx, pixel_data) in all_pixel_data.iter().enumerate() {
301 blended +=
302 pixel_data[ch * h * w + ci] * all_weights[idx][ci] / total_weight;
303 }
304 out_vals[ch * h * w + ci] = blended.clamp(0.0, 1.0);
305 }
306 }
307 }
308 }
309
310 let device = images[0].tensor.device();
311 let data = burn::tensor::TensorData::new(out_vals, [3, h, w]);
312 let tensor = burn::tensor::Tensor::<B, 3>::from_data(data, &device);
313 Ok(Image::new(tensor))
314 }
315}
316
317impl Default for MergeMertens {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::test_helpers::{TestBackend, test_device};
327 use burn::tensor::{Tensor, TensorData};
328
329 #[test]
330 fn test_nlmeans_denoising() {
331 let device = test_device();
332 let flat_data = vec![0.5f32; 3 * 16 * 16];
333 let tensor =
334 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 16, 16]), &device);
335 let img = Image::new(tensor);
336
337 let denoised = Photo::fast_nl_means_denoising(&img, 12.0, 3, 5).unwrap();
338 assert_eq!(denoised.shape(), [3, 16, 16]);
339
340 let out_data = denoised.tensor.into_data();
342 let out_vals: Vec<f32> = out_data.iter::<f32>().collect();
343 for v in &out_vals {
344 assert!(
345 (*v - 0.5).abs() < 1e-5,
346 "Uniform image should stay uniform, got {}",
347 v
348 );
349 }
350 }
351
352 #[test]
353 fn test_mertens_exposure_fusion() {
354 let device = test_device();
355
356 let mut data1 = vec![0.3f32; 3 * 16 * 16];
359 let mut data2 = vec![0.7f32; 3 * 16 * 16];
361
362 for y in 0..16 {
364 for x in 0..16 {
365 let ci = y * 16 + x;
366 data1[ci] = 0.2 + 0.4 * (x as f32 / 16.0);
368 data1[16 * 16 + ci] = 0.2 + 0.3 * (y as f32 / 16.0);
369 data1[2 * 16 * 16 + ci] = 0.3;
370 data2[ci] = 0.5 + 0.3 * (x as f32 / 16.0);
372 data2[16 * 16 + ci] = 0.4 + 0.4 * (y as f32 / 16.0);
373 data2[2 * 16 * 16 + ci] = 0.6;
374 }
375 }
376
377 let img1 = Image::new(Tensor::<TestBackend, 3>::from_data(
378 TensorData::new(data1, [3, 16, 16]),
379 &device,
380 ));
381 let img2 = Image::new(Tensor::<TestBackend, 3>::from_data(
382 TensorData::new(data2, [3, 16, 16]),
383 &device,
384 ));
385
386 let mertens = MergeMertens::new();
387 let merged = mertens.process(&[img1, img2]).unwrap();
388 assert_eq!(merged.shape(), [3, 16, 16]);
389
390 let out_data = merged.tensor.into_data();
392 let out_vals: Vec<f32> = out_data.iter::<f32>().collect();
393 for v in &out_vals {
394 assert!(*v >= 0.0 && *v <= 1.0, "Output out of range: {}", v);
395 }
396 }
397
398 #[test]
399 fn test_mertens_empty_input() {
400 let mertens = MergeMertens::new();
401 let empty: Vec<Image<TestBackend>> = vec![];
402 assert!(mertens.process(&empty).is_err());
403 }
404}