use std::collections::VecDeque;
use std::fmt;
use rayon::prelude::*;
use crate::cost::{CostMap, CostProvider};
use crate::image_io::buffer::{CoverSource, ImageBuffer};
use crate::image_io::jpeg_detect::luminance_plane;
const HIGH_PASS_KERNEL: [[f32; 3]; 3] = [[0.0, -1.0, 0.0], [-1.0, 4.0, -1.0], [0.0, -1.0, 0.0]];
const FIRST_SMOOTHING_SIGMA: f32 = 1.0;
const SECOND_SMOOTHING_SIGMA: f32 = 1.5;
const INVERSION_EPSILON: f32 = 1e-6;
const RED_CHANNEL_PENALTY: f32 = 1.20;
const SMOOTH_PERCENTILE: f32 = 0.05;
const MAX_SMOOTH_RATIO: f32 = 0.30;
const MAX_SMOOTH_REGION_RATIO: f32 = 0.10;
const TEXTURE_PERCENTILE: f32 = 0.95;
const MIN_TEXTURE_COST: f32 = 0.10;
#[derive(Debug)]
pub enum CostError {
ExcessiveSmoothRegions {
ratio: f32,
},
LargeSmoothRegion {
size: usize,
},
InsufficientGlobalTexture,
}
impl fmt::Display for CostError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CostError::ExcessiveSmoothRegions { ratio } => write!(
f,
"{:.1}% of the image is too smooth to hide anything; choose a container with \
more texture",
ratio * 100.0
),
CostError::LargeSmoothRegion { size } => write!(
f,
"the image contains a single flat area of {size} pixels; choose a container \
without large uniform surfaces"
),
CostError::InsufficientGlobalTexture => write!(
f,
"the image has no textured region anywhere; choose a container with more detail"
),
}
}
}
impl std::error::Error for CostError {}
#[derive(Debug, Default, Clone, Copy)]
pub struct HillCostProvider;
impl HillCostProvider {
pub fn new() -> Self {
Self
}
}
impl CostProvider for HillCostProvider {
type Error = CostError;
fn compute<'img>(&self, image: &'img ImageBuffer) -> Result<CostMap<'img>, CostError> {
let (width, height) = image.dimensions();
let width = width as usize;
let height = height as usize;
let color_space = image.color_space();
let luma = luminance_plane(image.pixels(), width * height, color_space);
let residual = high_pass(&luma, width, height);
drop(luma);
let smoothed = convolve_separable(
&residual,
width,
height,
&gaussian_kernel(FIRST_SMOOTHING_SIGMA),
);
drop(residual);
let second = convolve_separable(
&smoothed,
width,
height,
&gaussian_kernel(SECOND_SMOOTHING_SIGMA),
);
drop(smoothed);
let mut costs: Vec<f32> = second
.into_par_iter()
.map(|texture| 1.0 / (texture + INVERSION_EPSILON))
.collect();
if color_space.has_explicit_red_channel() {
costs
.par_iter_mut()
.for_each(|cost| *cost *= RED_CHANNEL_PENALTY);
}
validate(&costs, width, height)?;
Ok(CostMap::new(image, costs))
}
}
fn reflect(coord: isize, len: usize) -> usize {
if len <= 1 {
return 0;
}
let len = len as isize;
let period = 2 * (len - 1);
let mut folded = coord % period;
if folded < 0 {
folded += period;
}
if folded >= len {
folded = period - folded;
}
folded as usize
}
fn high_pass(luma: &[f32], width: usize, height: usize) -> Vec<f32> {
let mut output = vec![0.0f32; luma.len()];
if width == 0 || height == 0 {
return output;
}
output
.par_chunks_mut(width)
.enumerate()
.for_each(|(y, row)| {
for (x, value) in row.iter_mut().enumerate() {
let mut accumulator = 0.0f32;
for (ky, weights) in HIGH_PASS_KERNEL.iter().enumerate() {
let sy = reflect(y as isize + ky as isize - 1, height);
for (kx, &weight) in weights.iter().enumerate() {
if weight == 0.0 {
continue;
}
let sx = reflect(x as isize + kx as isize - 1, width);
accumulator += weight * luma.get(sy * width + sx).copied().unwrap_or(0.0);
}
}
*value = accumulator.abs();
}
});
output
}
fn gaussian_kernel(sigma: f32) -> Vec<f32> {
let radius = (2.0 * sigma).ceil().max(1.0) as usize;
let mut taps: Vec<f32> = (0..=2 * radius)
.map(|tap| {
let offset = tap as f32 - radius as f32;
(-(offset * offset) / (2.0 * sigma * sigma)).exp()
})
.collect();
let sum: f32 = taps.iter().sum();
if sum > 0.0 {
for tap in &mut taps {
*tap /= sum;
}
}
taps
}
fn convolve_separable(input: &[f32], width: usize, height: usize, taps: &[f32]) -> Vec<f32> {
let horizontal = convolve_axis(input, width, height, taps, Axis::Horizontal);
convolve_axis(&horizontal, width, height, taps, Axis::Vertical)
}
#[derive(Clone, Copy)]
enum Axis {
Horizontal,
Vertical,
}
fn convolve_axis(input: &[f32], width: usize, height: usize, taps: &[f32], axis: Axis) -> Vec<f32> {
let mut output = vec![0.0f32; input.len()];
if width == 0 || height == 0 || taps.is_empty() {
return output;
}
let radius = (taps.len() / 2) as isize;
output
.par_chunks_mut(width)
.enumerate()
.for_each(|(y, row)| {
for (x, value) in row.iter_mut().enumerate() {
let mut accumulator = 0.0f32;
for (tap_index, &tap) in taps.iter().enumerate() {
let offset = tap_index as isize - radius;
let (sx, sy) = match axis {
Axis::Horizontal => (reflect(x as isize + offset, width), y),
Axis::Vertical => (x, reflect(y as isize + offset, height)),
};
accumulator += tap * input.get(sy * width + sx).copied().unwrap_or(0.0);
}
*value = accumulator;
}
});
output
}
fn percentile(sorted: &[f32], fraction: f32) -> f32 {
if sorted.is_empty() {
return 0.0;
}
let rank = (fraction * sorted.len() as f32).ceil() as usize;
let index = rank.saturating_sub(1).min(sorted.len() - 1);
sorted.get(index).copied().unwrap_or(0.0)
}
fn largest_smooth_region(costs: &[f32], width: usize, height: usize, threshold: f32) -> usize {
if width == 0 || height == 0 {
return 0;
}
let is_smooth = |index: usize| costs.get(index).is_some_and(|&cost| cost < threshold);
let mut visited = vec![false; costs.len()];
let mut queue: VecDeque<usize> = VecDeque::new();
let mut largest = 0usize;
for start in 0..costs.len() {
if visited[start] || !is_smooth(start) {
continue;
}
visited[start] = true;
queue.push_back(start);
let mut size = 0usize;
while let Some(index) = queue.pop_front() {
size += 1;
let x = index % width;
let y = index / width;
let neighbours = [
(x > 0).then(|| index - 1),
(x + 1 < width).then(|| index + 1),
(y > 0).then(|| index - width),
(y + 1 < height).then(|| index + width),
];
for neighbour in neighbours.into_iter().flatten() {
if !visited[neighbour] && is_smooth(neighbour) {
visited[neighbour] = true;
queue.push_back(neighbour);
}
}
}
largest = largest.max(size);
}
largest
}
fn validate(costs: &[f32], width: usize, height: usize) -> Result<(), CostError> {
if costs.is_empty() {
return Err(CostError::InsufficientGlobalTexture);
}
let mut sorted = costs.to_vec();
sorted.par_sort_unstable_by(f32::total_cmp);
let theta_smooth = percentile(&sorted, SMOOTH_PERCENTILE);
let smooth_pixels = costs
.par_iter()
.filter(|&&cost| cost < theta_smooth)
.count();
let ratio = smooth_pixels as f32 / costs.len() as f32;
if ratio > MAX_SMOOTH_RATIO {
return Err(CostError::ExcessiveSmoothRegions { ratio });
}
let largest = largest_smooth_region(costs, width, height, theta_smooth);
if largest as f32 > MAX_SMOOTH_REGION_RATIO * costs.len() as f32 {
return Err(CostError::LargeSmoothRegion { size: largest });
}
if percentile(&sorted, TEXTURE_PERCENTILE) < MIN_TEXTURE_COST {
return Err(CostError::InsufficientGlobalTexture);
}
Ok(())
}