forgery_detection_zero/
vote.rs

1#![warn(missing_docs)]
2
3use std::f64::consts::{LN_10, PI};
4use std::ops::Index;
5use std::sync::Mutex;
6
7use bitvec::bitvec;
8
9#[cfg(feature = "image")]
10use image::{ImageBuffer, Luma};
11
12use itertools::Itertools;
13
14use libm::lgamma;
15
16#[cfg(feature = "rayon")]
17use rayon::prelude::{IntoParallelIterator, ParallelIterator};
18
19use crate::{ForgedRegion, Grid, LuminanceImage};
20
21/// The result of the vote to which grid a pixel is aligned with.
22#[derive(Debug, PartialEq, Eq, Clone, Copy)]
23pub enum Vote {
24    /// The pixel is aligned with a grid.
25    AlignedWith(Grid),
26
27    /// A vote is invalid when the pixel is within a 7 pixel wide region around the image border or when there is a tie.
28    Invalid,
29}
30
31impl Vote {
32    /// Converts a [`Vote`] to an [`Option`].
33    ///
34    /// It returns a grid if the vote is valid, `None` otherwise.
35    pub const fn grid(&self) -> Option<Grid> {
36        match self {
37            Self::AlignedWith(grid) => Some(*grid),
38            Self::Invalid => None,
39        }
40    }
41}
42
43/// The grid origin vote map of an image.
44///
45/// Each pixel in the image may belong to one of the 64 overlapping grids.
46/// This vote map contains the result of the vote for each pixel.
47#[derive(Clone)]
48pub struct Votes {
49    votes: Box<[Vote]>,
50
51    pub(crate) width: u32,
52    pub(crate) height: u32,
53    log_nt: f64,
54}
55
56impl Index<usize> for Votes {
57    type Output = Vote;
58
59    fn index(&self, index: usize) -> &Self::Output {
60        &self.votes[index]
61    }
62}
63
64impl Index<[u32; 2]> for Votes {
65    type Output = Vote;
66
67    fn index(&self, xy: [u32; 2]) -> &Self::Output {
68        &self.votes[(xy[0] + xy[1] * self.width) as usize]
69    }
70}
71
72impl Votes {
73    pub(crate) fn iter(&self) -> std::slice::Iter<'_, Vote> {
74        self.votes.iter()
75    }
76
77    pub(crate) fn iter_mut(&mut self) -> std::slice::IterMut<'_, Vote> {
78        self.votes.iter_mut()
79    }
80
81    /// Computes the votes for the given luminance image
82    pub(crate) fn from_luminance(image: &LuminanceImage) -> Self {
83        struct State {
84            zero: Vec<u32>,
85            votes: Vec<Vote>,
86        }
87        let cosine = cosine_table();
88        let zero = vec![0u32; (image.width() * image.height()) as usize];
89        let votes = vec![Vote::Invalid; (image.width() * image.height()) as usize];
90
91        let lock = Mutex::new(State { zero, votes });
92
93        let iter = 0..image.height() - 7;
94        #[cfg(feature = "rayon")]
95        let iter = iter.into_par_iter();
96        iter.for_each(|y| {
97            for x in 0..image.width() - 7 {
98                // SAFETY: The range of the loop makes `x` always less than `image.width() - 7` and `y` always less than `image.height() - 7`.
99                let number_of_zeroes = unsafe { compute_number_of_zeros(&cosine, image, x, y) };
100                // SAFETY: Same argument as above.
101                let const_along = unsafe { is_const_along_x_or_y(image, x, y) };
102
103                {
104                    let mut state = lock.lock().unwrap();
105
106                    // check all pixels in the block and update votes
107                    for xx in x..x + 8 {
108                        for yy in y..y + 8 {
109                            let index = (xx + yy * image.width()) as usize;
110                            // SAFETY: The loops iterate within the bounds of the image.
111                            // `state.zero` and `state.votes` have the same size as the image.
112                            // Thus, `index` is always within the bounds of the vec.
113                            unsafe {
114                                match number_of_zeroes.cmp(state.zero.get_unchecked(index)) {
115                                    std::cmp::Ordering::Equal => {
116                                        // if two grids are tied in number of zeros, do not vote
117                                        *state.votes.get_unchecked_mut(index) = Vote::Invalid;
118                                    }
119                                    std::cmp::Ordering::Greater => {
120                                        // update votes when the current grid has more zeros
121                                        *state.zero.get_unchecked_mut(index) = number_of_zeroes;
122                                        *state.votes.get_unchecked_mut(index) = if const_along {
123                                            Vote::Invalid
124                                        } else {
125                                            Vote::AlignedWith(Grid::from_xy(x, y))
126                                        };
127                                    }
128                                    std::cmp::Ordering::Less => (),
129                                }
130                            }
131                        }
132                    }
133                }
134            }
135        });
136
137        let mut votes = lock.into_inner().unwrap().votes;
138
139        // set pixels on the border to non valid votes - only pixels that
140        // belong to the 64 full 8x8 blocks inside the image can vote
141        for xx in 0..image.width() {
142            for yy in 0..7 {
143                let index = (xx + yy * image.width()) as usize;
144                votes[index] = Vote::Invalid;
145            }
146            for yy in (image.height() - 7)..image.height() {
147                let index = (xx + yy * image.width()) as usize;
148                votes[index] = Vote::Invalid;
149            }
150        }
151        for yy in 0..image.height() {
152            for xx in 0..7 {
153                let index = (xx + yy * image.width()) as usize;
154                votes[index] = Vote::Invalid;
155            }
156            for xx in (image.width() - 7)..image.width() {
157                let index = (xx + yy * image.width()) as usize;
158                votes[index] = Vote::Invalid;
159            }
160        }
161
162        Self {
163            votes: votes.into_boxed_slice(),
164            width: image.width(),
165            height: image.height(),
166            log_nt: 2.0f64.mul_add(
167                f64::from(image.height()).log10(),
168                2.0f64.mul_add(64.0f64.log10(), 2.0 * f64::from(image.width()).log10()),
169            ),
170        }
171    }
172
173    pub(crate) fn detect_global_grids(&self) -> (Option<Grid>, [f64; 64]) {
174        let mut grid_votes = [0u32; 64];
175        let p = 1.0 / 64.0;
176
177        // count votes per possible grid origin
178        // and keep track of the grid with the maximum of votes
179        let most_voted_grid = self.votes.iter().filter_map(Vote::grid).max_by_key(|grid| {
180            let votes = &mut grid_votes[grid.0 as usize];
181            *votes += 1;
182            *votes
183        });
184
185        // compute the NFA value for all the significant grids.  votes are
186        // correlated by irregular 8x8 blocks dividing by 64 gives a rough
187        // count of the number of independent votes
188        let n = self.width * self.height / 64;
189        let lnfa_grids: [f64; 64] = std::array::from_fn(|i| {
190            let k = grid_votes[i] / 64;
191
192            log_nfa(n, k, p, self.log_nt)
193        });
194
195        // meaningful grid -> main grid found!
196        if let Some(most_voted_grid) = most_voted_grid {
197            if lnfa_grids[most_voted_grid.0 as usize] < 0.0 {
198                return (Some(most_voted_grid), lnfa_grids);
199            }
200        }
201
202        (None, lnfa_grids)
203    }
204
205    /// Detects zones which are inconsistent with a given grid
206    pub(crate) fn detect_forgeries(
207        &self,
208        grid_to_exclude: Option<Grid>,
209        grid_max: Grid,
210    ) -> Box<[ForgedRegion]> {
211        let p = 1.0 / 64.0;
212
213        // Distance to look for neighbors in the region growing process.
214        // A meaningful forgery must have a density of votes of at least
215        // 1/64. Thus, its votes should not be in mean further away one
216        // from another than a distance of 8. One could use a little
217        // more to allow for some variation in the distribution.
218        let w = 9;
219
220        // minimal block size that can lead to a meaningful detection
221        let min_size = (64.0 * self.log_nt / 64.0f64.log10()).ceil() as usize;
222
223        let mut used = bitvec![0; self.votes.len()];
224
225        let mut forged_regions = Vec::new();
226
227        // region growing of zones that voted for other than the main grid
228        for x in 0..self.width {
229            for y in 0..self.height {
230                let index = (x + y * self.width) as usize;
231                if used[index] {
232                    continue;
233                }
234                let grid = self[index].grid();
235                if grid == grid_to_exclude {
236                    continue;
237                }
238                let grid = if let Some(grid) = grid {
239                    grid
240                } else {
241                    continue;
242                };
243                if grid.0 > grid_max.0 {
244                    continue;
245                }
246
247                let mut x0 = x; /* region bounding box */
248                let mut y0 = y;
249                let mut x1 = x;
250                let mut y1 = y;
251
252                used.set(index, true);
253
254                let mut regions_xy = vec![(x, y)];
255
256                // iteratively add neighbor pixel of pixels in the region
257                let mut i = 0;
258                while i < regions_xy.len() {
259                    let (reg_x, reg_y) = regions_xy[i];
260                    for xx in reg_x.saturating_sub(w)..=reg_x.saturating_add(w).min(self.width - 1)
261                    {
262                        for yy in
263                            reg_y.saturating_sub(w)..=reg_y.saturating_add(w).min(self.height - 1)
264                        {
265                            let index = (xx + yy * self.width) as usize;
266                            if used[index] {
267                                continue;
268                            }
269                            if self[index] != Vote::AlignedWith(grid) {
270                                continue;
271                            }
272
273                            used.set(index, true);
274                            regions_xy.push((xx, yy));
275                            if xx < x0 {
276                                x0 = xx;
277                            }
278                            if yy < y0 {
279                                y0 = yy;
280                            }
281                            if xx > x1 {
282                                x1 = xx;
283                            }
284                            if yy > y1 {
285                                y1 = yy;
286                            }
287                        }
288                    }
289                    i += 1;
290                }
291
292                // compute the number of false alarms (NFA) for the regions with at least the minimal size
293                if regions_xy.len() >= min_size {
294                    let n = (x1 - x0 + 1) * (y1 - y0 + 1) / 64;
295                    let k = regions_xy.len() / 64;
296                    let lnfa = log_nfa(n, k as u32, p, self.log_nt);
297                    if lnfa < 0.0 {
298                        // meaningful different grid found
299                        forged_regions.push(ForgedRegion {
300                            start: (x0, y0),
301                            end: (x1, y1),
302                            grid,
303                            lnfa,
304                            regions_xy: regions_xy.into_boxed_slice(),
305                        });
306                    }
307                }
308            }
309        }
310
311        forged_regions.into_boxed_slice()
312    }
313
314    /// Transforms the votes into a luminance image.
315    ///
316    /// When there is a tie, the pixel is black.
317    /// Otherwise, the pixel has a color between `0` and `63` to represent its vote.
318    #[cfg(feature = "image")]
319    pub fn to_luma_image(&self) -> ImageBuffer<Luma<u8>, Vec<u8>> {
320        ImageBuffer::from_fn(self.width, self.height, |x, y| {
321            let value = if let Vote::AlignedWith(grid) = self[[x, y]] {
322                grid.0
323            } else {
324                255
325            };
326
327            Luma([value])
328        })
329    }
330}
331
332fn cosine_table() -> [[f64; 8]; 8] {
333    let mut cosine = [[0.0; 8]; 8];
334
335    (0..8).cartesian_product(0..8).for_each(|(k, l)| {
336        cosine[k as usize][l as usize] =
337            (2.0f64.mul_add(f64::from(k), 1.0) * f64::from(l) * PI / 16.0).cos();
338    });
339
340    cosine
341}
342
343/// Computes the logarithm of the number of false alarms (NFA) to base 10.
344///
345/// `NFA = NT.b(n,k,p)` the return value is `log10(NFA)`
346///
347/// - `n,k,p` - binomial parameters.
348/// - `logNT` - logarithm of Number of Tests
349fn log_nfa(n: u32, k: u32, p: f64, log_nt: f64) -> f64 {
350    // an error of 10% in the result is accepted
351    let tolerance = 0.1;
352    let p_term = p / (1.0 - p);
353
354    debug_assert!(k <= n && (0.0..=1.0).contains(&p));
355
356    if n == 0 || k == 0 {
357        return log_nt;
358    } else if n == k {
359        return f64::from(n).mul_add(p.log10(), log_nt);
360    }
361
362    let log1term =
363        lgamma(f64::from(n) + 1.0) - lgamma(f64::from(k) + 1.0) - lgamma(f64::from(n - k) + 1.0)
364            + f64::from(k) * p.ln()
365            + f64::from(n - k) * (1.0 - p).ln();
366    let mut term = log1term.exp();
367    if term == 0.0 {
368        if f64::from(k) > f64::from(n) * p {
369            return log1term / LN_10 + log_nt;
370        }
371
372        return log_nt;
373    }
374
375    let mut bin_tail = term;
376    for i in (k + 1)..=n {
377        let bin_term = f64::from(n - i + 1) * 1.0 / f64::from(i);
378        let mult_term = bin_term * p_term;
379
380        term *= mult_term;
381        bin_tail += term;
382
383        if bin_term < 1.0 {
384            // when bin_term<1 then mult_term_j<mult_term_i for j>i.
385            // then, the error on the binomial tail when truncated at
386            // the i term can be bounded by a geometric serie of form
387            // term_i * sum mult_term_i^j.
388            let err =
389                term * ((1.0 - mult_term.powf(f64::from(n - i + 1))) / (1.0 - mult_term) - 1.0);
390
391            // one wants an error at most of tolerance*final_result, or:
392            // tolerance * abs(-log10(bin_tail)-logNT).
393            // now, the error that can be accepted on bin_tail is
394            // given by tolerance*final_result divided by the derivative
395            // of -log10(x) when x=bin_tail. that is:
396            // tolerance * abs(-log10(bin_tail)-logNT) / (1/bin_tail)
397            // finally, we truncate the tail if the error is less than:
398            // tolerance * abs(-log10(bin_tail)-logNT) * bin_tail
399            if err < tolerance * (-bin_tail.log10() - log_nt).abs() * bin_tail {
400                break;
401            }
402        }
403    }
404
405    bin_tail.log10() + log_nt
406}
407
408/// Computes DCT for 8x8 blocks staring at x,y and count its zeros
409///
410/// # Safety
411///
412/// `x` must be less than `image.width() - 7` and `y` must be less than `image.height() - 7`.
413unsafe fn compute_number_of_zeros(
414    cosine: &[[f64; 8]; 8],
415    image: &LuminanceImage,
416    x: u32,
417    y: u32,
418) -> u32 {
419    let vec = image.as_raw();
420    (0..8)
421        .cartesian_product(0..8)
422        .filter(|(i, j)| *i > 0 || *j > 0)
423        .map(|(i, j)| {
424            let normalization = 0.25
425                * (if i == 0 { 1.0 / 2.0f64.sqrt() } else { 1.0 })
426                * (if j == 0 { 1.0 / 2.0f64.sqrt() } else { 1.0 });
427            let dct_ij = (0..8)
428                .cartesian_product(0..8)
429                .map(|(xx, yy)| {
430                    let index = (x + xx + (y + yy) * image.width()) as usize;
431                    let pixel = vec.get_unchecked(index);
432                    pixel * cosine[xx as usize][i as usize] * cosine[yy as usize][j as usize]
433                })
434                .sum::<f64>()
435                * normalization;
436            // the finest quantization in JPEG is to integer values.
437            // in such case, the optimal threshold to decide if a
438            // coefficient is zero or not is the midpoint between
439            // 0 and 1, thus 0.5
440            u32::from(dct_ij.abs() < 0.5)
441        })
442        .sum()
443}
444
445/// Checks whether the block is constant along x or y axis
446///
447/// # Safety
448///
449/// `x` must be less than `image.width() - 7` and `y` must be less than `image.height() - 7`.
450unsafe fn is_const_along_x_or_y(image: &LuminanceImage, x: u32, y: u32) -> bool {
451    let along_y = || {
452        for yy in 0..8 {
453            let v1 = image.unsafe_get_pixel(x, y + yy);
454            for xx in 1..8 {
455                let v2 = image.unsafe_get_pixel(x + xx, y + yy);
456                if v1 != v2 {
457                    return false;
458                }
459            }
460        }
461        true
462    };
463    let along_x = || {
464        for xx in 0..8 {
465            let v1 = image.unsafe_get_pixel(x + xx, y);
466            for yy in 1..8 {
467                let v2 = image.unsafe_get_pixel(x + xx, y + yy);
468                if v1 != v2 {
469                    return false;
470                }
471            }
472        }
473        true
474    };
475    along_x() || along_y()
476}