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#[derive(Debug, PartialEq, Eq, Clone, Copy)]
23pub enum Vote {
24 AlignedWith(Grid),
26
27 Invalid,
29}
30
31impl Vote {
32 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#[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 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 let number_of_zeroes = unsafe { compute_number_of_zeros(&cosine, image, x, y) };
100 let const_along = unsafe { is_const_along_x_or_y(image, x, y) };
102
103 {
104 let mut state = lock.lock().unwrap();
105
106 for xx in x..x + 8 {
108 for yy in y..y + 8 {
109 let index = (xx + yy * image.width()) as usize;
110 unsafe {
114 match number_of_zeroes.cmp(state.zero.get_unchecked(index)) {
115 std::cmp::Ordering::Equal => {
116 *state.votes.get_unchecked_mut(index) = Vote::Invalid;
118 }
119 std::cmp::Ordering::Greater => {
120 *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 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 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 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 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 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 let w = 9;
219
220 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 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; 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 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 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 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 #[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
343fn log_nfa(n: u32, k: u32, p: f64, log_nt: f64) -> f64 {
350 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 let err =
389 term * ((1.0 - mult_term.powf(f64::from(n - i + 1))) / (1.0 - mult_term) - 1.0);
390
391 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
408unsafe 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 u32::from(dct_ij.abs() < 0.5)
441 })
442 .sum()
443}
444
445unsafe 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}