Skip to main content

embedded_dsp/
spatial.rs

1//! 2D Spatial and Image Processing routines (2D DCT/IDCT, 2D Convolution, 2D Nonlinear Filters, Sobel Edge Detection, 2D Histogram, MSE/PSNR).
2
3#[allow(unused_imports)]
4use crate::math::FloatMath;
5use crate::types::Status;
6
7/// Non-linear 2D spatial filter type.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum NonlinFilterType {
10    /// Minimum value in the sliding window.
11    Min,
12    /// Maximum value in the sliding window.
13    Max,
14    /// Median value in the sliding window.
15    Median,
16}
17
18/// Computes the 2D Discrete Cosine Transform (DCT-II) on a `rows x cols` image.
19///
20/// `src` and `dst` must have length at least `rows * cols`.
21pub fn dct2d_f32(src: &[f32], dst: &mut [f32], rows: usize, cols: usize) -> Status {
22    let total = rows * cols;
23    if rows == 0 || cols == 0 || src.len() < total || dst.len() < total {
24        return Status::LengthError;
25    }
26
27    let pi_over_2m = core::f32::consts::PI / (2.0 * rows as f32);
28    let pi_over_2n = core::f32::consts::PI / (2.0 * cols as f32);
29    let norm_row0 = (1.0 / rows as f32).sqrt();
30    let norm_rowk = (2.0 / rows as f32).sqrt();
31    let norm_col0 = (1.0 / cols as f32).sqrt();
32    let norm_colk = (2.0 / cols as f32).sqrt();
33
34    for u in 0..rows {
35        let cu = if u == 0 { norm_row0 } else { norm_rowk };
36        let u_f = u as f32;
37
38        for v in 0..cols {
39            let cv = if v == 0 { norm_col0 } else { norm_colk };
40            let v_f = v as f32;
41
42            let mut sum = 0.0f32;
43            for x in 0..rows {
44                let cos_u = ((2 * x + 1) as f32 * u_f * pi_over_2m).cos();
45                for y in 0..cols {
46                    let cos_v = ((2 * y + 1) as f32 * v_f * pi_over_2n).cos();
47                    sum += src[x * cols + y] * cos_u * cos_v;
48                }
49            }
50            dst[u * cols + v] = cu * cv * sum;
51        }
52    }
53
54    Status::Success
55}
56
57/// Computes the 2D Inverse Discrete Cosine Transform (IDCT-II) on a `rows x cols` coefficient matrix.
58pub fn idct2d_f32(src: &[f32], dst: &mut [f32], rows: usize, cols: usize) -> Status {
59    let total = rows * cols;
60    if rows == 0 || cols == 0 || src.len() < total || dst.len() < total {
61        return Status::LengthError;
62    }
63
64    let pi_over_2m = core::f32::consts::PI / (2.0 * rows as f32);
65    let pi_over_2n = core::f32::consts::PI / (2.0 * cols as f32);
66    let norm_row0 = (1.0 / rows as f32).sqrt();
67    let norm_rowk = (2.0 / rows as f32).sqrt();
68    let norm_col0 = (1.0 / cols as f32).sqrt();
69    let norm_colk = (2.0 / cols as f32).sqrt();
70
71    for x in 0..rows {
72        for y in 0..cols {
73            let mut sum = 0.0f32;
74            for u in 0..rows {
75                let cu = if u == 0 { norm_row0 } else { norm_rowk };
76                let cos_u = ((2 * x + 1) as f32 * u as f32 * pi_over_2m).cos();
77
78                for v in 0..cols {
79                    let cv = if v == 0 { norm_col0 } else { norm_colk };
80                    let cos_v = ((2 * y + 1) as f32 * v as f32 * pi_over_2n).cos();
81                    sum += cu * cv * src[u * cols + v] * cos_u * cos_v;
82                }
83            }
84            dst[x * cols + y] = sum;
85        }
86    }
87
88    Status::Success
89}
90
91/// Performs 2D spatial convolution of `src` image (`rows x cols`) with a `k_rows x k_cols` kernel.
92///
93/// If `normalize` is `true`, the convolution output is divided by the sum of the absolute kernel weights.
94pub fn convolve2d_f32(
95    src: &[f32],
96    dst: &mut [f32],
97    rows: usize,
98    cols: usize,
99    kernel: &[f32],
100    k_rows: usize,
101    k_cols: usize,
102    normalize: bool,
103) -> Status {
104    let total = rows * cols;
105    let k_total = k_rows * k_cols;
106    if rows == 0 || cols == 0 || k_rows == 0 || k_cols == 0 {
107        return Status::ArgumentError;
108    }
109    if src.len() < total || dst.len() < total || kernel.len() < k_total {
110        return Status::LengthError;
111    }
112
113    let dead_r = k_rows / 2;
114    let dead_c = k_cols / 2;
115
116    let norm_factor: f32 = if normalize {
117        let mut sum = 0.0f32;
118        for &v in kernel {
119            sum += v.abs();
120        }
121        if sum != 0.0 { sum } else { 1.0 }
122    } else {
123        1.0
124    };
125
126    for r in 0..rows {
127        for c in 0..cols {
128            let mut acc = 0.0f32;
129            for kr in 0..k_rows {
130                let ir = (r as isize + kr as isize - dead_r as isize).clamp(0, (rows - 1) as isize)
131                    as usize;
132                for kc in 0..k_cols {
133                    let ic = (c as isize + kc as isize - dead_c as isize)
134                        .clamp(0, (cols - 1) as isize) as usize;
135                    acc += src[ir * cols + ic] * kernel[kr * k_cols + kc];
136                }
137            }
138            dst[r * cols + c] = acc / norm_factor;
139        }
140    }
141
142    Status::Success
143}
144
145/// Performs 2D non-linear filtering (`Min`, `Max`, or `Median`) on a `rows x cols` image using a `k_size x k_size` square window.
146///
147/// `k_size` must be odd and $\le 7$ for embedded zero-allocation stack sorting.
148pub fn nonlin2d_filter_f32(
149    src: &[f32],
150    dst: &mut [f32],
151    rows: usize,
152    cols: usize,
153    k_size: usize,
154    filtype: NonlinFilterType,
155) -> Status {
156    let total = rows * cols;
157    if rows == 0 || cols == 0 || k_size == 0 || k_size % 2 == 0 || k_size > 7 {
158        return Status::ArgumentError;
159    }
160    if src.len() < total || dst.len() < total {
161        return Status::LengthError;
162    }
163
164    let half = k_size / 2;
165    let k_len = k_size * k_size;
166    let mut sort_buf = [0.0f32; 64];
167
168    for r in 0..rows {
169        for c in 0..cols {
170            let mut count = 0;
171            for kr in 0..k_size {
172                let ir = (r as isize + kr as isize - half as isize).clamp(0, (rows - 1) as isize)
173                    as usize;
174                for kc in 0..k_size {
175                    let ic = (c as isize + kc as isize - half as isize)
176                        .clamp(0, (cols - 1) as isize) as usize;
177                    sort_buf[count] = src[ir * cols + ic];
178                    count += 1;
179                }
180            }
181
182            match filtype {
183                NonlinFilterType::Min => {
184                    let mut min_v = sort_buf[0];
185                    for i in 1..k_len {
186                        if sort_buf[i] < min_v {
187                            min_v = sort_buf[i];
188                        }
189                    }
190                    dst[r * cols + c] = min_v;
191                }
192                NonlinFilterType::Max => {
193                    let mut max_v = sort_buf[0];
194                    for i in 1..k_len {
195                        if sort_buf[i] > max_v {
196                            max_v = sort_buf[i];
197                        }
198                    }
199                    dst[r * cols + c] = max_v;
200                }
201                NonlinFilterType::Median => {
202                    for a in 1..k_len {
203                        let mut b = a;
204                        while b > 0 && sort_buf[b - 1] > sort_buf[b] {
205                            sort_buf.swap(b - 1, b);
206                            b -= 1;
207                        }
208                    }
209                    dst[r * cols + c] = sort_buf[k_len / 2];
210                }
211            }
212        }
213    }
214
215    Status::Success
216}
217
218/// Applies Sobel edge detection to a 2D image, outputting gradient magnitude and binary edge detection.
219///
220/// Steps:
221/// 1. Convolve with horizontal Sobel operator $G_x$.
222/// 2. Convolve with vertical Sobel operator $G_y$.
223/// 3. Compute gradient magnitude $G = \sqrt{G_x^2 + G_y^2}$.
224/// 4. Threshold magnitude: values $\ge \text{threshold}$ become `1.0`, others `0.0`.
225pub fn sobel_edge_detection_f32(
226    src: &[f32],
227    dst_edges: &mut [f32],
228    rows: usize,
229    cols: usize,
230    threshold: f32,
231) -> Status {
232    let total = rows * cols;
233    if rows < 3 || cols < 3 || src.len() < total || dst_edges.len() < total {
234        return Status::LengthError;
235    }
236
237    let h_sobel: [f32; 9] = [-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0];
238    let v_sobel: [f32; 9] = [-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0];
239
240    for r in 0..rows {
241        for c in 0..cols {
242            let mut gx = 0.0f32;
243            let mut gy = 0.0f32;
244
245            for kr in 0..3 {
246                let ir = (r as isize + kr as isize - 1).clamp(0, (rows - 1) as isize) as usize;
247                for kc in 0..3 {
248                    let ic = (c as isize + kc as isize - 1).clamp(0, (cols - 1) as isize) as usize;
249                    let val = src[ir * cols + ic];
250                    gx += val * h_sobel[kr * 3 + kc];
251                    gy += val * v_sobel[kr * 3 + kc];
252                }
253            }
254
255            let mag = (gx * gx + gy * gy).sqrt();
256            dst_edges[r * cols + c] = if mag >= threshold { 1.0 } else { 0.0 };
257        }
258    }
259
260    Status::Success
261}
262
263/// Computes the histogram of a 2D image / matrix into `bins` spanning `[min_val, max_val]`.
264pub fn histogram_2d_f32(src: &[f32], bins: &mut [usize], min_val: f32, max_val: f32) -> Status {
265    let num_bins = bins.len();
266    if src.is_empty() || num_bins == 0 || max_val <= min_val {
267        return Status::ArgumentError;
268    }
269
270    bins.fill(0);
271    let span = max_val - min_val;
272    let scale = (num_bins as f32) / span;
273
274    for &val in src {
275        let clamped = val.clamp(min_val, max_val);
276        let mut idx = ((clamped - min_val) * scale) as usize;
277        if idx >= num_bins {
278            idx = num_bins - 1;
279        }
280        bins[idx] += 1;
281    }
282
283    Status::Success
284}
285
286/// Computes the Mean Squared Error (MSE) between two 2D images.
287pub fn mse_2d_f32(img_a: &[f32], img_b: &[f32]) -> f32 {
288    let len = img_a.len().min(img_b.len());
289    if len == 0 {
290        return 0.0;
291    }
292    let mut sum_sq = 0.0f32;
293    for i in 0..len {
294        let diff = img_a[i] - img_b[i];
295        sum_sq += diff * diff;
296    }
297    sum_sq / (len as f32)
298}
299
300/// Computes the Peak Signal-to-Noise Ratio (PSNR) in dB between two 2D images.
301pub fn psnr_2d_f32(img_a: &[f32], img_b: &[f32], max_val: f32) -> f32 {
302    let mse = mse_2d_f32(img_a, img_b);
303    if mse <= 1e-12 {
304        return 99.0; // Near identical
305    }
306    10.0 * ((max_val * max_val) / mse).log10()
307}