is_photo/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3
4pub mod regression;
5
6use regression::RegressionModel;
7
8/// Pre-trained regression model with about 94% accuracy on my test set
9/// of 1500 images.
10pub const STANDARD_MODEL: RegressionModel = RegressionModel {
11    weights: [
12        -0.08685173,
13        -0.041326188,
14        -0.09524358,
15        -0.16007994,
16        -0.8713319,
17        0.08773025,
18        -0.00017104072,
19        -0.00025994,
20        -1.0389963,
21        -0.08185195,
22        -0.050398663,
23        0.30969876,
24        0.11085072,
25        -0.008571971,
26    ],
27    bias: 6.0718517,
28    lr: 0.01,
29    lambda: 0.01,
30};
31
32/// Image analysis results, most of which are used as features for the regression model
33/// to classify images as photos or vectors.
34#[derive(Debug)]
35pub struct Analysis {
36    /// Red-Green Covariance
37    pub rg_cov: f32,
38    /// Green-Blue Covariance
39    pub gb_cov: f32,
40    /// Blue-Red Covariance
41    pub br_cov: f32,
42    /// Image histogram entropy
43    pub entropy: f32,
44    //pub edginess: f32,
45    /// Low frequency energy
46    pub low_energy: f32,
47    /// High frequency energy
48    pub high_energy: f32,
49    /// Ratio of low to high frequency energy
50    pub spectral_energy_ratio: f32,
51    /// Mean spectral energy
52    pub spectral_mean: f32,
53    pub spectral_mean_square: f32,
54    /// Image spectral variance
55    pub spectral_variance: f32,
56    /// Skewness of the spectral energy distribution
57    pub spectral_skewness: f32,
58    /// Kurtosis of the spectral energy distribution
59    pub spectral_kurtosis: f32,
60    /// Amplitude of the peak frequency
61    pub peak_amplitude: f32,
62    /// Frequency of the peak amplitude, biased towards higher frequencies
63    pub peak_frequency: u8,
64    /// Ratio of peak amplitude to mean amplitude
65    pub peak_to_avg_ratio: f32,
66}
67
68impl Analysis {
69    pub fn std_dev(&self) -> f32 {
70        self.spectral_variance.sqrt()
71    }
72
73    /// Returns the raw probability that the image is a vector image.
74    pub fn raw_is_vector(&self, model: &RegressionModel) -> f32 {
75        // almost always a vector image
76        if self.peak_frequency > 24 || self.spectral_kurtosis < 0.0 || self.spectral_skewness < 0.0
77        {
78            return 1.0;
79        }
80
81        model.predict(self)
82    }
83
84    /// Returns the raw probability that the image is a photo.
85    pub fn raw_is_photo(&self, model: &RegressionModel) -> f32 {
86        1.0 - self.raw_is_vector(model).min(1.0)
87    }
88
89    /// Returns true if the image is likely a photo, false if it's likely a vector.
90    pub fn is_photo(&self, model: &RegressionModel) -> bool {
91        !self.is_vector(model)
92    }
93
94    /// Returns true if the image is likely a vector, false if it's likely a photo.
95    pub fn is_vector(&self, model: &RegressionModel) -> bool {
96        self.raw_is_vector(model) > 0.5
97    }
98}
99
100use image::{GenericImageView, Pixel};
101
102// /// Laplacian operator with diagonals
103// #[rustfmt::skip]
104// const LAPLACIAN: [f32; 9] = [
105//     -1.0, -1.0, -1.0,
106//     -1.0,  8.0, -1.0,
107//     -1.0, -1.0, -1.0,
108// ];
109
110// /// Fast 3x3 image convolution, same as image's filter3x3 but for RGB_8 only
111// /// and without any intermediate buffers, outputting to a callback function instead
112// #[inline]
113// fn apply_kernel<I, P, F>(image: &I, mut kernel: [f32; 9], mut cb: F)
114// where
115//     I: GenericImageView<Pixel = P>,
116//     P: Pixel<Subpixel = u8>,
117//     F: FnMut(u32, u32, [f32; 3]),
118// {
119//     #[rustfmt::skip]
120//     const TAPS: &[(isize, isize); 9] = &[
121//         (-1, -1), (0, -1), (1, -1),
122//         (-1,  0), (0,  0), (1,  0),
123//         (-1,  1), (0,  1), (1,  1),
124//     ];
125
126//     // apply u8 -> f32 weight here
127//     for k in &mut kernel {
128//         *k /= 255.0;
129//     }
130
131//     let (width, height) = image.dimensions();
132
133//     for y in 1..height - 1 {
134//         for x in 1..width - 1 {
135//             let mut t = [0.0f32; 3];
136
137//             for (&k, &(a, b)) in kernel.iter().zip(TAPS) {
138//                 let x0 = x as isize + a;
139//                 let y0 = y as isize + b;
140
141//                 let p = image.get_pixel(x0 as u32, y0 as u32);
142
143//                 for (&c, f) in p.channels().iter().zip(&mut t) {
144//                     *f += k * c as f32;
145//                 }
146//             }
147
148//             cb(x, y, t);
149//         }
150//     }
151// }
152
153#[rustfmt::skip]
154#[inline]
155fn luma([r, g, b]: [f32; 3]) -> f32 {
156    0.212671 * r +
157    0.715160 * g +
158    0.072169 * b
159}
160
161/// Analyze an image and return the results.
162///
163/// Returns `None` if the image is too small to analyze.
164pub fn analyze<I, P>(img: &I) -> Option<Analysis>
165where
166    I: GenericImageView<Pixel = P>,
167    P: Pixel<Subpixel = u8>,
168{
169    use easyfft::{const_size::FftMut, num_complex::Complex};
170
171    let (width, height) = img.dimensions();
172    let num_pixels = width as u64 * height as u64;
173
174    if num_pixels < 2 {
175        return None;
176    }
177
178    let inverse_n = 1.0 / num_pixels as f32;
179
180    // let mut edginess = 0.0;
181
182    // apply_kernel(img, LAPLACIAN, |_, _, edge| {
183    //     edginess += inverse_n * 2.0 * (luma(edge) - 0.5).max(0.0);
184    // });
185
186    let mut histogram = [Complex::<f32>::ZERO; 256];
187
188    let mut red = 0.0;
189    let mut green = 0.0;
190    let mut blue = 0.0;
191
192    let mut rg_cov = 0.0;
193    let mut gb_cov = 0.0;
194    let mut br_cov = 0.0;
195
196    let [r0, g0, b0] = img.get_pixel(0, 0).to_rgb().0.map(|c| c as f32 / 255.0);
197
198    for (_, _, pixel) in img.pixels() {
199        let [r, g, b] = pixel.to_rgb().0.map(|c| c as f32 / 255.0);
200
201        histogram[(luma([r, g, b]) * 255.0) as usize].re += 1.0;
202
203        let dr = r - r0;
204        let dg = g - g0;
205        let db = b - b0;
206
207        red += dr;
208        green += dg;
209        blue += db;
210
211        rg_cov += dr * dg;
212        gb_cov += dg * db;
213        br_cov += db * dr;
214    }
215
216    rg_cov = (rg_cov - red * green * inverse_n) * inverse_n;
217    gb_cov = (gb_cov - green * blue * inverse_n) * inverse_n;
218    br_cov = (br_cov - blue * green * inverse_n) * inverse_n;
219
220    // loop through the histogram to normalize and compute entropy
221    let mut entropy = 0.0;
222    for h in &mut histogram {
223        h.re *= inverse_n; // normalize
224
225        if h.re > 0.0 {
226            entropy -= h.re * h.re.ln();
227        }
228    }
229
230    // in-place real FFT
231    histogram.fft_mut();
232
233    let mut n = 0.0;
234    let mut mean = 0.0;
235    let mut m2 = 0.0;
236    let mut m3 = 0.0;
237    let mut m4 = 0.0;
238
239    let mut peak = (0, 0.0);
240
241    let mut low_sum = 0.0;
242    let mut high_sum = 0.0;
243
244    // iterate over positive frequencies, excluding the DC offset
245    for (idx, s) in histogram[1..128].iter().enumerate() {
246        let x = s.norm();
247
248        // allow equal to bias result towards higher frequencies
249        if x >= peak.1 {
250            peak = (idx, x);
251        }
252
253        if idx >= 32 {
254            high_sum += x;
255        } else {
256            low_sum += x;
257        }
258
259        let n1 = n;
260        n += 1.0;
261
262        let delta = x - mean;
263        let delta_n = delta / n;
264        let delta_n2 = delta_n * delta_n;
265        let term1 = delta * delta_n * n1;
266
267        mean += delta_n;
268        m4 += term1 * delta_n2 * (n * n - 3.0 * n + 3.0) + 6.0 * delta_n2 * m2 - 4.0 * delta_n * m3;
269        m3 += term1 * delta_n * (n - 2.0) - 3.0 * delta_n * m2;
270        m2 += term1;
271    }
272
273    let variance = m2 / (n - 1.0);
274    let skewness = n.sqrt() * m3 / (m2 * m2 * m2).sqrt();
275    let kurtosis = (n * m4) / (m2 * m2) - 3.0;
276
277    Some(Analysis {
278        rg_cov,
279        gb_cov,
280        br_cov,
281        spectral_mean: mean,
282        spectral_mean_square: m2,
283        spectral_variance: variance,
284        entropy,
285        //edginess,
286        low_energy: low_sum * inverse_n,
287        high_energy: high_sum * inverse_n,
288        spectral_energy_ratio: low_sum / high_sum,
289        spectral_skewness: skewness,
290        spectral_kurtosis: kurtosis,
291        peak_amplitude: peak.1,
292        peak_frequency: peak.0 as u8,
293        peak_to_avg_ratio: peak.1 / mean,
294    })
295}