forgery_detection_zero/
lib.rs

1#![doc = include_str!("../README.md")]
2use bitvec::bitvec;
3use bitvec::vec::BitVec;
4
5#[cfg(feature = "image")]
6use image::{DynamicImage, ImageBuffer, Luma};
7
8#[cfg(feature = "image")]
9mod convert;
10mod vote;
11
12#[cfg(feature = "image")]
13use convert::ToLumaZero;
14
15pub use vote::{Vote, Votes};
16
17/// Represents the errors that can be raised when using [`Zero`].
18#[derive(thiserror::Error, Debug)]
19pub enum Error {
20    /// The dimensions given with the raw image are invalid (the size of the array is inconsistent with the width and height)
21    #[error("inconsistency between the raw image array and the width and height provided")]
22    InvalidRawDimensions,
23
24    /// The JPEG encoding failed
25    #[error("failed to encode the original image to a 99% quality JPEG: {0}")]
26    Encoding(#[from] jpeg_encoder::EncodingError),
27
28    /// The JPEG decoding failed
29    #[cfg(feature = "image")]
30    #[error("failed to decode the image: {0}")]
31    Decoding(#[from] image::ImageError),
32}
33
34type Result<T> = std::result::Result<T, Error>;
35
36/// A grid is an unsigned integer between `0` and `63`
37#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
38pub struct Grid(pub u8);
39
40impl Grid {
41    const fn from_xy(x: u32, y: u32) -> Self {
42        Self(((x % 8) + (y % 8) * 8) as u8)
43    }
44
45    pub const fn x(&self) -> u8 {
46        self.0 % 8
47    }
48
49    pub const fn y(&self) -> u8 {
50        self.0 / 8
51    }
52}
53
54/// An area of an image that has been forged.
55#[derive(Default, Clone, Debug)]
56pub struct ForgedRegion {
57    /// Bottom left of the bounding box
58    pub start: (u32, u32),
59
60    /// Top right of the bounding box
61    pub end: (u32, u32),
62
63    pub grid: Grid,
64    pub lnfa: f64,
65    regions_xy: Box<[(u32, u32)]>,
66}
67
68pub struct ForeignGridAreas {
69    luminance: LuminanceImage,
70    votes: Votes,
71    forged_regions: Box<[ForgedRegion]>,
72    lnfa_grids: [f64; 64],
73    main_grid: Option<Grid>,
74}
75
76impl ForeignGridAreas {
77    pub fn votes(&self) -> &Votes {
78        &self.votes
79    }
80
81    /// Builds a [forgery mask](`ForgeryMask`) that considers the pixels that are part of an area that have a grid different from the main one as forged
82    pub fn build_forgery_mask(&self) -> ForgeryMask {
83        ForgeryMask::from_regions(&self.forged_regions, self.votes.width, self.votes.height)
84    }
85
86    /// Get all the parts of the image where a JPEG grid is detected with its grid origin different from the main JPEG grid
87    pub fn forged_regions(&self) -> &[ForgedRegion] {
88        self.forged_regions.as_ref()
89    }
90
91    pub fn lnfa_grids(&self) -> [f64; 64] {
92        self.lnfa_grids
93    }
94
95    pub fn main_grid(&self) -> Option<Grid> {
96        self.main_grid
97    }
98
99    /// Whether it is likely that the image has been cropped.
100    ///
101    /// If the origin of the main grid is different from `(0, 0)`,
102    /// it is likely that the image has been cropped.
103    pub fn is_cropped(&self) -> bool {
104        self.main_grid.map_or(false, |grid| grid.0 > 0)
105    }
106
107    /// Detects the areas of the image that are missing a grid.
108    ///
109    /// # Errors
110    ///
111    /// It returns an error if it failed to encode the original image as 99% quality JPEG.
112    #[cfg(feature = "image")]
113    pub fn detect_missing_grid_areas(&self) -> Result<Option<MissingGridAreas>> {
114        let main_grid = if let Some(main_grid) = self.main_grid {
115            main_grid
116        } else {
117            return Ok(None);
118        };
119
120        let jpeg_99 = self.luminance.to_jpeg_99_luminance()?;
121
122        let mut jpeg_99_votes = Votes::from_luminance(&jpeg_99);
123        // update votemap by avoiding the votes for the main grid
124        for (&vote, vote_99) in self.votes.iter().zip(jpeg_99_votes.iter_mut()) {
125            if vote == Vote::AlignedWith(main_grid) {
126                *vote_99 = Vote::Invalid;
127            }
128        }
129
130        // Try to detect an imposed JPEG grid. No grid is to be excluded
131        // and we are interested only in grid with origin (0,0), so:
132        // grid_to_exclude = None and grid_max = 0
133        let jpeg_99_forged_regions = jpeg_99_votes.detect_forgeries(None, Grid(0));
134
135        Ok(Some(MissingGridAreas {
136            votes: jpeg_99_votes,
137            missing_regions: jpeg_99_forged_regions,
138        }))
139    }
140}
141
142impl IntoIterator for ForeignGridAreas {
143    type Item = ForgedRegion;
144
145    type IntoIter = std::vec::IntoIter<ForgedRegion>;
146
147    fn into_iter(self) -> Self::IntoIter {
148        self.forged_regions.into_vec().into_iter()
149    }
150}
151
152/// Contains the result for the detection of missing grid areas
153pub struct MissingGridAreas {
154    votes: Votes,
155
156    missing_regions: Box<[ForgedRegion]>,
157}
158
159impl MissingGridAreas {
160    pub fn votes(&self) -> &Votes {
161        &self.votes
162    }
163
164    /// Get all the parts of the image that have missing JPEG traces
165    pub fn forged_regions(&self) -> &[ForgedRegion] {
166        self.missing_regions.as_ref()
167    }
168
169    /// Builds a [forgery mask](`ForgeryMask`) that considers the pixels that are part of an area that have missing JPEG traces as forged
170    pub fn build_forgery_mask(self) -> ForgeryMask {
171        ForgeryMask::from_regions(&self.missing_regions, self.votes.width, self.votes.height)
172    }
173}
174
175impl IntoIterator for MissingGridAreas {
176    type Item = ForgedRegion;
177
178    type IntoIter = std::vec::IntoIter<ForgedRegion>;
179
180    fn into_iter(self) -> Self::IntoIter {
181        self.missing_regions.into_vec().into_iter()
182    }
183}
184
185/// JPEG grid detector applied to forgery detection.
186///
187/// # Examples
188///
189/// An easy way to detect all regions of an image that have been forged:
190///
191/// ```no_run
192/// # use forgery_detection_zero::Zero;
193/// # let jpeg = todo!();
194/// #
195/// for r in Zero::from_image(&jpeg).into_iter() {
196///     println!(
197///         "Forged region detected: from ({}, {}) to ({}, {})",
198///         r.start.0, r.start.1, r.end.0, r.end.1,
199///     )
200/// }
201/// ```
202pub struct Zero {
203    luminance: LuminanceImage,
204}
205
206impl Zero {
207    /// Initializes a forgery detection using the given image
208    #[cfg(feature = "image")]
209    pub fn from_image(image: &DynamicImage) -> Self {
210        let luminance = image.to_luma32f_zero();
211
212        Self { luminance }
213    }
214
215    /// Initializes a forgery detection using a raw luminance image
216    ///
217    /// # Errors
218    ///
219    /// It returns an error if the raw image array does not have a length consistent with the `width` and `height` parameters.
220    pub fn from_luminance_raw(luminance: Box<[f64]>, width: u32, height: u32) -> Result<Self> {
221        if luminance.len() != width.saturating_mul(height) as usize {
222            return Err(Error::InvalidRawDimensions);
223        }
224
225        Ok(Self {
226            luminance: LuminanceImage {
227                image: luminance,
228                width,
229                height,
230            },
231        })
232    }
233
234    /// Runs the forgery detection algorithm.
235    ///
236    /// This is the more advanced API.
237    /// If you just want to know the bounding box of each forged region in the image,
238    /// you can call [`IntoIterator::into_iter`] instead.
239    pub fn detect_forgeries(self) -> ForeignGridAreas {
240        let votes = Votes::from_luminance(&self.luminance);
241        let (main_grid, lnfa_grids) = votes.detect_global_grids();
242        let forged_regions = votes.detect_forgeries(main_grid, Grid(63));
243
244        ForeignGridAreas {
245            luminance: self.luminance,
246            votes,
247            forged_regions,
248            lnfa_grids,
249            main_grid,
250        }
251    }
252}
253
254#[cfg(feature = "image")]
255impl IntoIterator for Zero {
256    type Item = ForgedRegion;
257
258    type IntoIter = Box<dyn Iterator<Item = ForgedRegion>>;
259
260    fn into_iter(self) -> Self::IntoIter {
261        let foreign_grid_areas = self.detect_forgeries();
262        let missing_grid_regions = foreign_grid_areas
263            .detect_missing_grid_areas()
264            .ok()
265            .flatten()
266            .into_iter()
267            .flat_map(IntoIterator::into_iter);
268        let forged_regions = foreign_grid_areas.into_iter().chain(missing_grid_regions);
269
270        Box::new(forged_regions)
271    }
272}
273
274/// A mask that represents the pixels of an image that have been forged
275pub struct ForgeryMask {
276    mask: BitVec,
277    width: u32,
278    height: u32,
279}
280
281impl ForgeryMask {
282    /// Transforms the forgery mask into a luminance image.
283    ///
284    /// Each pixel considered forged is white, all the others are black.
285    #[cfg(feature = "image")]
286    pub fn into_luma_image(self) -> ImageBuffer<Luma<u8>, Vec<u8>> {
287        ImageBuffer::from_fn(self.width, self.height, |x, y| {
288            let index = (x + y * self.width) as usize;
289
290            Luma([u8::from(self.mask[index]) * 255])
291        })
292    }
293
294    /// Returns `true` if the pixel at `[x,y]` is considered forged
295    pub fn is_forged(&self, x: u32, y: u32) -> bool {
296        self.mask
297            .get((x + y * self.width) as usize)
298            .as_deref()
299            .copied()
300            .unwrap_or(false)
301    }
302
303    /// Returns the width of the forgery mask
304    pub fn width(&self) -> u32 {
305        self.width
306    }
307
308    /// Returns the height of the forgery mask
309    pub fn height(&self) -> u32 {
310        self.height
311    }
312
313    /// Builds a forgery mask for a set of forged regions.
314    ///
315    /// "Due to variations in the number of votes, the raw forgery mask contains holes.
316    /// To give a more useful forgery map,
317    /// these holes are filled by a mathematical morphology closing
318    /// operator with a square structuring element of size W
319    /// (the same as the neighborhood used in the region growing)."
320    fn from_regions(regions: &[ForgedRegion], width: u32, height: u32) -> Self {
321        let w = 9;
322        let mut mask_aux = bitvec![0; width as usize * height as usize];
323        let mut forgery_mask = bitvec![0; width as usize * height as usize];
324
325        for forged in regions {
326            for &(x, y) in forged.regions_xy.iter() {
327                for xx in (x - w)..=(x + w) {
328                    for yy in (y - w)..=(y + w) {
329                        let index = (xx + yy * width) as usize;
330                        mask_aux.set(index, true);
331                        forgery_mask.set(index, true);
332                    }
333                }
334            }
335        }
336
337        for x in w..width.saturating_sub(w) {
338            for y in w..height.saturating_sub(w) {
339                let index = (x + y * width) as usize;
340                if !mask_aux[index] {
341                    for xx in (x - w)..=(x + w) {
342                        for yy in (y - w)..=(y + w) {
343                            let index = (xx + yy * width) as usize;
344                            forgery_mask.set(index, false);
345                        }
346                    }
347                }
348            }
349        }
350
351        Self {
352            mask: forgery_mask,
353            width,
354            height,
355        }
356    }
357}
358
359pub(crate) struct LuminanceImage {
360    image: Box<[f64]>,
361    width: u32,
362    height: u32,
363}
364
365impl LuminanceImage {
366    pub(crate) fn width(&self) -> u32 {
367        self.width
368    }
369
370    pub(crate) fn height(&self) -> u32 {
371        self.height
372    }
373
374    pub(crate) fn as_raw(&self) -> &[f64] {
375        &self.image
376    }
377
378    /// Gets a pixel without doing bounds checking
379    ///
380    /// # Safety
381    ///
382    /// `x` must be less than `image.width()` and `y` must be less than `image.height()`.
383    pub(crate) unsafe fn unsafe_get_pixel(&self, x: u32, y: u32) -> &f64 {
384        self.image.get_unchecked((x + y * self.width) as usize)
385    }
386}