pineapple_core/cv/
features.rs

1// Copyright (c) 2025, Tom Ouellette
2// Licensed under the BSD 3-Clause License
3
4use std::ops::Deref;
5
6use num::{FromPrimitive, ToPrimitive};
7
8use crate::constant::{GLCM_ARRAY_SIZE, GLCM_LEVELS};
9use crate::im::PineappleViewBuffer;
10
11#[derive(Debug, Clone)]
12pub struct GLCM {
13    data: [f32; GLCM_ARRAY_SIZE],
14    rows: usize,
15    cols: usize,
16}
17
18impl GLCM {
19    /// Create a new normalized gray-level co-occurence matrix
20    ///
21    /// # Arguments
22    ///
23    /// * `pixels` - A row-major raw pixel buffer
24    /// * `width` - Width of image
25    /// * `height` - Height of image
26    /// * `channel` - Which channel to compute the comatrix
27    /// * `channels` - Number of channels in image
28    /// * `angle` - Angle (in degrees) for computing neighbour co-occurence
29    /// * `distance` - Number of pixels to neighbouring pixels
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use pineapple_core::cv::features::GLCM;
35    /// let buffer: Vec<f32> = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
36    /// let comatrix = GLCM::new(&buffer, 3, 3, 0, 1, 0.0, 1.0);
37    /// ```
38    pub fn new<T>(
39        pixels: &[T],
40        width: usize,
41        height: usize,
42        channel: usize,
43        channels: usize,
44        angle: f32,
45        distance: f32,
46    ) -> GLCM
47    where
48        T: ToPrimitive,
49    {
50        let radians = angle.to_radians();
51
52        let mut min_val = f32::MAX;
53        let mut max_val = f32::MIN;
54
55        let pixel_vec: Vec<f32> = pixels
56            .iter()
57            .skip(channel)
58            .step_by(channels)
59            .map(|p| {
60                let value = p.to_f32().unwrap();
61                min_val = min_val.min(value);
62                max_val = max_val.max(value);
63                value
64            })
65            .collect();
66
67        let (sa, sb, sc) = if max_val != GLCM_LEVELS as f32 - 1.0 || min_val != 0.0 {
68            (min_val, max_val, GLCM_LEVELS as f32 - 1.0)
69        } else if min_val == max_val {
70            // Homogeneous images are set to zero
71            (0.0, 1.0, 0.0)
72        } else {
73            (0.0, 1.0, 1.0)
74        };
75
76        let (w, h) = (width as i32, height as i32);
77        let offset_x = (radians.cos() * distance).round() as i32;
78        let offset_y = (radians.sin() * distance).round() as i32;
79
80        let mut comatrix = [0.0; GLCM_ARRAY_SIZE];
81
82        let scale_pixel =
83            |pixel: f32| -> usize { ((pixel - sa) / (sb - sa) * sc).round() as usize };
84
85        let mut comatrix_sum = 0f32;
86
87        for y in 0..h {
88            for x in 0..w {
89                let idx = (y * w + x) as usize;
90                let root = pixel_vec[idx];
91
92                let i_offset = x + offset_x;
93                let j_offset = y + offset_y;
94
95                if i_offset >= w || i_offset < 0 || j_offset >= h || j_offset < 0 {
96                    continue;
97                }
98
99                let neighbour_idx = (j_offset * w + i_offset) as usize;
100                let neighbour = pixel_vec[neighbour_idx];
101
102                let root_scaled = scale_pixel(root);
103                let neighbour_scaled = scale_pixel(neighbour);
104
105                comatrix[root_scaled * GLCM_LEVELS + neighbour_scaled] += 1.0;
106                comatrix[neighbour_scaled * GLCM_LEVELS + root_scaled] += 1.0;
107
108                comatrix_sum += 2.0;
109            }
110        }
111
112        comatrix.iter_mut().for_each(|v| *v /= comatrix_sum);
113
114        GLCM {
115            data: comatrix,
116            rows: GLCM_LEVELS,
117            cols: GLCM_LEVELS,
118        }
119    }
120
121    /// Create a new normalized gray-level co-occurence matrix from aa PineappleObjectBuffer
122    ///
123    /// # Arguments
124    ///
125    /// * `object` - A PineappleObjectBuffer
126    /// * `channel` - Which channel to compute the comatrix
127    /// * `angle` - Angle (in degrees) for computing neighbour co-occurence
128    /// * `distance` - Number of pixels to neighbouring pixels
129    ///
130    /// # Note
131    ///
132    /// This is pretty redunant with the default constructor. We could possibly
133    /// just accept a Vec<f32> instead and perform the other operations in the
134    /// glcm_multichannel function. This would avoid the need for object specific
135    /// functions.
136    pub fn new_from_object<T, Container>(
137        object: &PineappleViewBuffer<T, Container>,
138        channel: usize,
139        angle: f32,
140        distance: f32,
141    ) -> GLCM
142    where
143        T: ToPrimitive + FromPrimitive,
144        Container: Deref<Target = [T]>,
145    {
146        let radians = angle.to_radians();
147
148        let mut min_val = f32::MAX;
149        let mut max_val = f32::MIN;
150
151        let pixel_vec: Vec<f32> = object
152            .iter()
153            .skip(channel)
154            .step_by(object.channels())
155            .map(|p| {
156                let value = p.to_f32().unwrap();
157                min_val = min_val.min(value);
158                max_val = max_val.max(value);
159                value
160            })
161            .collect();
162
163        let (sa, sb, sc) = if max_val != GLCM_LEVELS as f32 - 1.0 || min_val != 0.0 {
164            (min_val, max_val, GLCM_LEVELS as f32 - 1.0)
165        } else if min_val == max_val {
166            // Homogeneous images are set to zero
167            (0.0, 1.0, 0.0)
168        } else {
169            (0.0, 1.0, 1.0)
170        };
171
172        let (w, h) = (object.width() as i32, object.height() as i32);
173        let offset_x = (radians.cos() * distance).round() as i32;
174        let offset_y = (radians.sin() * distance).round() as i32;
175
176        let mut comatrix = [0.0; GLCM_ARRAY_SIZE];
177
178        let scale_pixel =
179            |pixel: f32| -> usize { ((pixel - sa) / (sb - sa) * sc).round() as usize };
180
181        let mut comatrix_sum = 0f32;
182
183        for y in 0..h {
184            for x in 0..w {
185                let idx = (y * w + x) as usize;
186                let root = pixel_vec[idx];
187
188                let i_offset = x + offset_x;
189                let j_offset = y + offset_y;
190
191                if i_offset >= w || i_offset < 0 || j_offset >= h || j_offset < 0 {
192                    continue;
193                }
194
195                let neighbour_idx = (j_offset * w + i_offset) as usize;
196                let neighbour = pixel_vec[neighbour_idx];
197
198                let root_scaled = scale_pixel(root);
199                let neighbour_scaled = scale_pixel(neighbour);
200
201                comatrix[root_scaled * GLCM_LEVELS + neighbour_scaled] += 1.0;
202                comatrix[neighbour_scaled * GLCM_LEVELS + root_scaled] += 1.0;
203
204                comatrix_sum += 2.0;
205            }
206        }
207
208        comatrix.iter_mut().for_each(|v| *v /= comatrix_sum);
209
210        GLCM {
211            data: comatrix,
212            rows: GLCM_LEVELS,
213            cols: GLCM_LEVELS,
214        }
215    }
216
217    pub fn rows(&self) -> usize {
218        self.rows
219    }
220
221    pub fn cols(&self) -> usize {
222        self.cols
223    }
224
225    pub fn iter(&self) -> impl Iterator<Item = (usize, usize, f32)> + '_ {
226        self.data.iter().enumerate().map(|(index, &value)| {
227            let i = index / self.cols;
228            let j = index % self.cols;
229            (i, j, value)
230        })
231    }
232
233    pub fn margin_sums(&self) -> (Vec<f32>, Vec<f32>) {
234        let mut row_sums = vec![0.0; self.rows];
235        let mut col_sums = vec![0.0; self.cols];
236
237        for (i, j, value) in self.iter() {
238            row_sums[i] += value;
239            col_sums[j] += value;
240        }
241
242        (row_sums, col_sums)
243    }
244}
245
246/// Compute a normalized gray-level co-occurence matrix for each image channel
247///
248/// # Arguments
249///
250/// * `pixels` - A row-major raw pixel buffer
251/// * `width` - Width of image
252/// * `height` - Height of image
253/// * `channels` - Number of channels in image
254/// * `angle` - Angle (in degrees) for computing neighbour co-occurence
255/// * `distance` - Number of pixels to neighbouring pixels
256///
257/// # Examples
258///
259/// ```
260/// use pineapple_core::cv::features::glcm_multichannel;
261/// let buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
262/// let comatrices = glcm_multichannel(&buffer, 2, 2, 3, 0.0, 1.0);
263/// ```
264pub fn glcm_multichannel<T>(
265    pixels: &[T],
266    width: usize,
267    height: usize,
268    channels: usize,
269    angle: f32,
270    distance: f32,
271) -> Vec<GLCM>
272where
273    T: ToPrimitive,
274{
275    (0..channels)
276        .map(|channel| GLCM::new(pixels, width, height, channel, channels, angle, distance))
277        .collect()
278}
279
280/// Compute channel-wise normalized gray-level co-occurence matrix from a PineappleObjectBuffer
281///
282/// # Arguments
283///
284/// * `pixels` - A row-major raw pixel buffer
285/// * `width` - Width of image
286/// * `height` - Height of image
287/// * `channels` - Number of channels in image
288/// * `angle` - Angle (in degrees) for computing neighbour co-occurence
289/// * `distance` - Number of pixels to neighbouring pixels
290pub fn glcm_multichannel_object<T, Container>(
291    object: &PineappleViewBuffer<T, Container>,
292    angle: f32,
293    distance: f32,
294) -> Vec<GLCM>
295where
296    T: ToPrimitive + FromPrimitive,
297    Container: Deref<Target = [T]>,
298{
299    (0..object.channels())
300        .map(|channel| GLCM::new_from_object(object, channel, angle, distance))
301        .collect()
302}