#![warn(missing_docs)]
use std::f64::consts::{LN_10, PI};
use std::ops::Index;
use std::sync::Mutex;
use bitvec::bitvec;
#[cfg(feature = "image")]
use image::{ImageBuffer, Luma};
use itertools::Itertools;
use libm::lgamma;
#[cfg(feature = "rayon")]
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use crate::{ForgedRegion, Grid, LuminanceImage};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Vote {
AlignedWith(Grid),
Invalid,
}
impl Vote {
pub const fn grid(&self) -> Option<Grid> {
match self {
Self::AlignedWith(grid) => Some(*grid),
Self::Invalid => None,
}
}
}
#[derive(Clone)]
pub struct Votes {
votes: Box<[Vote]>,
pub(crate) width: u32,
pub(crate) height: u32,
log_nt: f64,
}
impl Index<usize> for Votes {
type Output = Vote;
fn index(&self, index: usize) -> &Self::Output {
&self.votes[index]
}
}
impl Index<[u32; 2]> for Votes {
type Output = Vote;
fn index(&self, xy: [u32; 2]) -> &Self::Output {
&self.votes[(xy[0] + xy[1] * self.width) as usize]
}
}
impl Votes {
pub(crate) fn iter(&self) -> std::slice::Iter<'_, Vote> {
self.votes.iter()
}
pub(crate) fn iter_mut(&mut self) -> std::slice::IterMut<'_, Vote> {
self.votes.iter_mut()
}
pub(crate) fn from_luminance(image: &LuminanceImage) -> Self {
struct State {
zero: Vec<u32>,
votes: Vec<Vote>,
}
let cosine = cosine_table();
let zero = vec![0u32; (image.width() * image.height()) as usize];
let votes = vec![Vote::Invalid; (image.width() * image.height()) as usize];
let lock = Mutex::new(State { zero, votes });
let iter = 0..image.height() - 7;
#[cfg(feature = "rayon")]
let iter = iter.into_par_iter();
iter.for_each(|y| {
for x in 0..image.width() - 7 {
let number_of_zeroes = unsafe { compute_number_of_zeros(&cosine, image, x, y) };
let const_along = unsafe { is_const_along_x_or_y(image, x, y) };
{
let mut state = lock.lock().unwrap();
for xx in x..x + 8 {
for yy in y..y + 8 {
let index = (xx + yy * image.width()) as usize;
unsafe {
match number_of_zeroes.cmp(state.zero.get_unchecked(index)) {
std::cmp::Ordering::Equal => {
*state.votes.get_unchecked_mut(index) = Vote::Invalid;
}
std::cmp::Ordering::Greater => {
*state.zero.get_unchecked_mut(index) = number_of_zeroes;
*state.votes.get_unchecked_mut(index) = if const_along {
Vote::Invalid
} else {
Vote::AlignedWith(Grid::from_xy(x, y))
};
}
std::cmp::Ordering::Less => (),
}
}
}
}
}
}
});
let mut votes = lock.into_inner().unwrap().votes;
for xx in 0..image.width() {
for yy in 0..7 {
let index = (xx + yy * image.width()) as usize;
votes[index] = Vote::Invalid;
}
for yy in (image.height() - 7)..image.height() {
let index = (xx + yy * image.width()) as usize;
votes[index] = Vote::Invalid;
}
}
for yy in 0..image.height() {
for xx in 0..7 {
let index = (xx + yy * image.width()) as usize;
votes[index] = Vote::Invalid;
}
for xx in (image.width() - 7)..image.width() {
let index = (xx + yy * image.width()) as usize;
votes[index] = Vote::Invalid;
}
}
Self {
votes: votes.into_boxed_slice(),
width: image.width(),
height: image.height(),
log_nt: 2.0f64.mul_add(
f64::from(image.height()).log10(),
2.0f64.mul_add(64.0f64.log10(), 2.0 * f64::from(image.width()).log10()),
),
}
}
pub(crate) fn detect_global_grids(&self) -> (Option<Grid>, [f64; 64]) {
let mut grid_votes = [0u32; 64];
let p = 1.0 / 64.0;
let most_voted_grid = self.votes.iter().filter_map(Vote::grid).max_by_key(|grid| {
let votes = &mut grid_votes[grid.0 as usize];
*votes += 1;
*votes
});
let n = self.width * self.height / 64;
let lnfa_grids: [f64; 64] = std::array::from_fn(|i| {
let k = grid_votes[i] / 64;
log_nfa(n, k, p, self.log_nt)
});
if let Some(most_voted_grid) = most_voted_grid {
if lnfa_grids[most_voted_grid.0 as usize] < 0.0 {
return (Some(most_voted_grid), lnfa_grids);
}
}
(None, lnfa_grids)
}
pub(crate) fn detect_forgeries(
&self,
grid_to_exclude: Option<Grid>,
grid_max: Grid,
) -> Box<[ForgedRegion]> {
let p = 1.0 / 64.0;
let w = 9;
let min_size = (64.0 * self.log_nt / 64.0f64.log10()).ceil() as usize;
let mut used = bitvec![0; self.votes.len()];
let mut forged_regions = Vec::new();
for x in 0..self.width {
for y in 0..self.height {
let index = (x + y * self.width) as usize;
if used[index] {
continue;
}
let grid = self[index].grid();
if grid == grid_to_exclude {
continue;
}
let grid = if let Some(grid) = grid {
grid
} else {
continue;
};
if grid.0 > grid_max.0 {
continue;
}
let mut x0 = x;
let mut y0 = y;
let mut x1 = x;
let mut y1 = y;
used.set(index, true);
let mut regions_xy = vec![(x, y)];
let mut i = 0;
while i < regions_xy.len() {
let (reg_x, reg_y) = regions_xy[i];
for xx in reg_x.saturating_sub(w)..=reg_x.saturating_add(w).min(self.width - 1)
{
for yy in
reg_y.saturating_sub(w)..=reg_y.saturating_add(w).min(self.height - 1)
{
let index = (xx + yy * self.width) as usize;
if used[index] {
continue;
}
if self[index] != Vote::AlignedWith(grid) {
continue;
}
used.set(index, true);
regions_xy.push((xx, yy));
if xx < x0 {
x0 = xx;
}
if yy < y0 {
y0 = yy;
}
if xx > x1 {
x1 = xx;
}
if yy > y1 {
y1 = yy;
}
}
}
i += 1;
}
if regions_xy.len() >= min_size {
let n = (x1 - x0 + 1) * (y1 - y0 + 1) / 64;
let k = regions_xy.len() / 64;
let lnfa = log_nfa(n, k as u32, p, self.log_nt);
if lnfa < 0.0 {
forged_regions.push(ForgedRegion {
start: (x0, y0),
end: (x1, y1),
grid,
lnfa,
regions_xy: regions_xy.into_boxed_slice(),
});
}
}
}
}
forged_regions.into_boxed_slice()
}
#[cfg(feature = "image")]
pub fn to_luma_image(&self) -> ImageBuffer<Luma<u8>, Vec<u8>> {
ImageBuffer::from_fn(self.width, self.height, |x, y| {
let value = if let Vote::AlignedWith(grid) = self[[x, y]] {
grid.0
} else {
255
};
Luma([value])
})
}
}
fn cosine_table() -> [[f64; 8]; 8] {
let mut cosine = [[0.0; 8]; 8];
(0..8).cartesian_product(0..8).for_each(|(k, l)| {
cosine[k as usize][l as usize] =
(2.0f64.mul_add(f64::from(k), 1.0) * f64::from(l) * PI / 16.0).cos();
});
cosine
}
fn log_nfa(n: u32, k: u32, p: f64, log_nt: f64) -> f64 {
let tolerance = 0.1;
let p_term = p / (1.0 - p);
debug_assert!(k <= n && (0.0..=1.0).contains(&p));
if n == 0 || k == 0 {
return log_nt;
} else if n == k {
return f64::from(n).mul_add(p.log10(), log_nt);
}
let log1term =
lgamma(f64::from(n) + 1.0) - lgamma(f64::from(k) + 1.0) - lgamma(f64::from(n - k) + 1.0)
+ f64::from(k) * p.ln()
+ f64::from(n - k) * (1.0 - p).ln();
let mut term = log1term.exp();
if term == 0.0 {
if f64::from(k) > f64::from(n) * p {
return log1term / LN_10 + log_nt;
}
return log_nt;
}
let mut bin_tail = term;
for i in (k + 1)..=n {
let bin_term = f64::from(n - i + 1) * 1.0 / f64::from(i);
let mult_term = bin_term * p_term;
term *= mult_term;
bin_tail += term;
if bin_term < 1.0 {
let err =
term * ((1.0 - mult_term.powf(f64::from(n - i + 1))) / (1.0 - mult_term) - 1.0);
if err < tolerance * (-bin_tail.log10() - log_nt).abs() * bin_tail {
break;
}
}
}
bin_tail.log10() + log_nt
}
unsafe fn compute_number_of_zeros(
cosine: &[[f64; 8]; 8],
image: &LuminanceImage,
x: u32,
y: u32,
) -> u32 {
let vec = image.as_raw();
(0..8)
.cartesian_product(0..8)
.filter(|(i, j)| *i > 0 || *j > 0)
.map(|(i, j)| {
let normalization = 0.25
* (if i == 0 { 1.0 / 2.0f64.sqrt() } else { 1.0 })
* (if j == 0 { 1.0 / 2.0f64.sqrt() } else { 1.0 });
let dct_ij = (0..8)
.cartesian_product(0..8)
.map(|(xx, yy)| {
let index = (x + xx + (y + yy) * image.width()) as usize;
let pixel = vec.get_unchecked(index);
pixel * cosine[xx as usize][i as usize] * cosine[yy as usize][j as usize]
})
.sum::<f64>()
* normalization;
u32::from(dct_ij.abs() < 0.5)
})
.sum()
}
unsafe fn is_const_along_x_or_y(image: &LuminanceImage, x: u32, y: u32) -> bool {
let along_y = || {
for yy in 0..8 {
let v1 = image.unsafe_get_pixel(x, y + yy);
for xx in 1..8 {
let v2 = image.unsafe_get_pixel(x + xx, y + yy);
if v1 != v2 {
return false;
}
}
}
true
};
let along_x = || {
for xx in 0..8 {
let v1 = image.unsafe_get_pixel(x + xx, y);
for yy in 1..8 {
let v2 = image.unsafe_get_pixel(x + xx, y + yy);
if v1 != v2 {
return false;
}
}
}
true
};
along_x() || along_y()
}