use std::{num::NonZeroU32, ops::RangeInclusive};
use crate::{math, Crop, Error, Image, ResizableImage, Score, ScoredCrop, RGB};
const PRESCALE: bool = true;
const PRESCALE_MIN: f64 = 400.00;
const MIN_SCALE: f64 = 1.0;
const MAX_SCALE: f64 = 1.0;
const STEP: f64 = 8.0;
const SCALE_STEP: f64 = 0.1;
const SCORE_DOWN_SAMPLE: f64 = 8.0;
const SKIN_WEIGHT: f64 = 1.8;
const DETAIL_WEIGHT: f64 = 0.2;
const SKIN_BRIGHTNESS_RANGE: RangeInclusive<f64> = 0.2..=1.0;
const SKIN_THRESHOLD: f64 = 0.8;
const SKIN_BIAS: f64 = 0.01;
const SATURATION_BRIGHTNESS_RANGE: RangeInclusive<f64> = 0.05..=0.9;
const SATURATION_THRESHOLD: f64 = 0.4;
const SATURATION_BIAS: f64 = 0.2;
const SATURATION_WEIGHT: f64 = 0.1;
#[derive(Debug)]
struct ImageMap {
width: u32,
height: u32,
pixels: Vec<Vec<RGB>>,
}
impl ImageMap {
fn new(width: u32, height: u32) -> ImageMap {
let white = RGB::new(255, 255, 255);
let pixels = vec![vec![white; height as usize]; width as usize];
ImageMap {
width,
height,
pixels,
}
}
fn set(&mut self, x: u32, y: u32, color: RGB) {
self.pixels[x as usize][y as usize] = color
}
fn get(&self, x: u32, y: u32) -> RGB {
self.pixels[x as usize][y as usize]
}
fn down_sample(self, factor: u32) -> Self {
let width = (self.width as f64 / factor as f64).floor() as u32;
let height = (self.height as f64 / factor as f64).floor() as u32;
let mut output = ImageMap::new(width, height);
let ifactor2: f64 = 1.0 / (factor as f64 * factor as f64);
let max = |a, b| {
if a > b {
a
} else {
b
}
};
for y in 0..height {
for x in 0..width {
let mut r: f64 = 0.0;
let mut g: f64 = 0.0;
let mut b: f64 = 0.0;
let mut mr: f64 = 0.0;
let mut mg: f64 = 0.0;
for v in 0..factor {
for u in 0..factor {
let ix = x * factor + u;
let iy = y * factor + v;
let icolor = self.get(ix, iy);
r += icolor.r as f64;
g += icolor.g as f64;
b += icolor.b as f64;
mr = max(mr, icolor.r as f64);
mg = max(mg, icolor.g as f64);
}
}
output.set(
x,
y,
RGB::new(
(r * ifactor2 * 0.5 + mr * 0.5).round() as u8,
(g * ifactor2 * 0.7 + mg * 0.3).round() as u8,
(b * ifactor2).round() as u8,
),
)
}
}
output
}
}
pub fn find_best_crop<I: Image + ResizableImage<RI>, RI: Image>(
img: &I,
width: NonZeroU32,
height: NonZeroU32,
) -> Result<ScoredCrop, Error> {
if img.width() == 0 || img.height() == 0 {
return Err(Error::ZeroSizedImage);
}
let width = width.get() as f64;
let height = height.get() as f64;
let scale = f64::min((img.width() as f64) / width, (img.height() as f64) / height);
if PRESCALE {
let f = PRESCALE_MIN / f64::min(img.width() as f64, img.height() as f64);
let prescalefactor = f.min(1.0);
let crop_width = (width * scale * prescalefactor).max(1.0).round() as u32;
let crop_height = (height * scale * prescalefactor).max(1.0).round() as u32;
let real_min_scale = calculate_real_min_scale(scale);
let new_width = ((img.width() as f64) * prescalefactor).round() as u32;
let new_height = (prescalefactor * img.height() as f64).round() as u32;
let old_width = img.width() as f64;
let old_height = img.height() as f64;
let img = img.resize(new_width, new_height);
assert!(img.width() == crop_width || img.height() == crop_height);
let top_crop = analyse(
&img,
NonZeroU32::new(crop_width).unwrap(),
NonZeroU32::new(crop_height).unwrap(),
real_min_scale,
);
let post_scale_w = img.width() as f64 / old_width;
let post_scale_h = img.height() as f64 / old_height;
let post_scale_factor = f64::max(post_scale_w, post_scale_h);
Ok(top_crop.scale(1.0 / post_scale_factor))
} else {
let crop_width = (width * scale).round() as u32;
let crop_height = (height * scale).round() as u32;
let real_min_scale = calculate_real_min_scale(scale);
assert!(img.width() == crop_width || img.height() == crop_height);
let top_crop = analyse(
img,
NonZeroU32::new(crop_width).unwrap(),
NonZeroU32::new(crop_height).unwrap(),
real_min_scale,
);
Ok(top_crop)
}
}
fn calculate_real_min_scale(scale: f64) -> f64 {
(1.0 / scale).clamp(MIN_SCALE, MAX_SCALE)
}
fn analyse<I: Image>(
img: &I,
crop_width: NonZeroU32,
crop_height: NonZeroU32,
real_min_scale: f64,
) -> ScoredCrop {
assert!(img.width() >= crop_width.get());
assert!(img.height() >= crop_height.get());
let mut o = ImageMap::new(img.width(), img.height());
edge_detect(img, &mut o);
skin_detect(img, &mut o);
saturation_detect(img, &mut o);
let cs: Vec<Crop> = crops(&o, crop_width.get(), crop_height.get(), real_min_scale);
assert!(!cs.is_empty());
let score_output = o.down_sample(SCORE_DOWN_SAMPLE as u32);
let top_crop: Option<ScoredCrop> = cs
.iter()
.map(|crop| ScoredCrop {
crop: crop.clone(),
score: score(&score_output, crop),
})
.fold(None, |result, scored_crop| {
Some(match result {
None => scored_crop,
Some(result) => {
if result.score.total > scored_crop.score.total {
result
} else {
scored_crop
}
}
})
});
top_crop.unwrap()
}
fn edge_detect<I: Image>(i: &I, o: &mut ImageMap) {
let w = i.width() as usize;
let h = i.height() as usize;
let cies = make_cies(i);
for y in 0..h {
for x in 0..w {
let color = i.get(x as u32, y as u32);
let lightness = if x == 0 || x >= w - 1 || y == 0 || y >= h - 1 {
cies[y * w + x]
} else {
cies[y * w + x] * 4.0
- cies[x + (y - 1) * w]
- cies[x - 1 + y * w]
- cies[x + 1 + y * w]
- cies[x + (y + 1) * w]
};
let g = math::bounds(lightness);
let nc = RGB { g, ..color };
o.set(x as u32, y as u32, nc)
}
}
}
fn make_cies<I: Image>(img: &I) -> Vec<f64> {
let w = img.width();
let h = img.height();
let size = w as u64 * h as u64;
let size = if size > usize::MAX as u64 {
None
} else {
Some(size as usize)
};
let mut cies = Vec::with_capacity(size.expect("Too big image dimensions"));
let mut i: usize = 0;
for y in 0..h {
for x in 0..w {
let color = img.get(x, y);
cies.insert(i, color.cie());
i += 1;
}
}
cies
}
fn crops(i: &ImageMap, crop_width: u32, crop_height: u32, real_min_scale: f64) -> Vec<Crop> {
let mut crops: Vec<Crop> = vec![];
let width = i.width as f64;
let height = i.height as f64;
let min_dimension = f64::min(width, height);
let crop_w = if crop_width != 0 {
crop_width as f64
} else {
min_dimension
};
let crop_h = if crop_height != 0 {
crop_height as f64
} else {
min_dimension
};
let y_step = STEP.min(height);
let x_step = STEP.min(width);
let mut scale = MAX_SCALE;
loop {
if scale < real_min_scale {
break;
};
let stepping = |step| (0..).map(f64::from).map(move |i| i * step);
for y in stepping(y_step).take_while(|y| y + crop_h * scale <= height) {
for x in stepping(x_step).take_while(|x| x + crop_w * scale <= width) {
crops.push(Crop {
x: x.round() as u32,
y: y.round() as u32,
width: (crop_w * scale).round() as u32,
height: (crop_h * scale).round() as u32,
});
}
}
scale -= SCALE_STEP;
}
crops
}
fn score(o: &ImageMap, crop: &Crop) -> Score {
let height = o.height as f64;
let width = o.width as f64;
let down_sample = SCORE_DOWN_SAMPLE;
let inv_down_sample = 1.0 / down_sample;
let output_height_down_sample = height * down_sample;
let output_width_down_sample = width * down_sample;
let mut skin = 0.0;
let mut detail = 0.0;
let mut saturation = 0.0;
for y in (0..)
.map(|i: u32| i as f64 * SCORE_DOWN_SAMPLE)
.take_while(|&y| y < output_height_down_sample)
{
for x in (0..)
.map(|i: u32| i as f64 * SCORE_DOWN_SAMPLE)
.take_while(|&x| x < output_width_down_sample)
{
let orig_x = (x * inv_down_sample).round() as u32;
let orig_y = (y * inv_down_sample).round() as u32;
let color = o.get(orig_x, orig_y);
let imp = math::importance(crop, x.round() as u32, y.round() as u32);
let det = color.g as f64 / 255.0;
skin += color.r as f64 / 255.0 * (det + SKIN_BIAS) * imp;
detail += det * imp;
saturation += color.b as f64 / 255.0 * (det + SATURATION_BIAS) * imp;
}
}
let total = (detail * DETAIL_WEIGHT + skin * SKIN_WEIGHT + saturation * SATURATION_WEIGHT)
/ crop.width as f64
/ crop.height as f64;
Score {
skin,
detail,
saturation,
total,
}
}
fn skin_detect<I: Image>(i: &I, o: &mut ImageMap) {
let w = i.width();
let h = i.height();
for y in 0..h {
for x in 0..w {
let lightness = i.get(x, y).cie() / 255.0;
let skin = math::skin_col(i.get(x, y));
let nc = if skin > SKIN_THRESHOLD && SKIN_BRIGHTNESS_RANGE.contains(&lightness) {
let r = (skin - SKIN_THRESHOLD) * (255.0 / (1.0 - SKIN_THRESHOLD));
let RGB { r: _, g, b } = o.get(x, y);
RGB {
r: math::bounds(r),
g,
b,
}
} else {
let RGB { r: _, g, b } = o.get(x, y);
RGB { r: 0, g, b }
};
o.set(x, y, nc);
}
}
}
fn saturation_detect<I: Image>(i: &I, o: &mut ImageMap) {
let w = i.width();
let h = i.height();
for y in 0..h {
for x in 0..w {
let color = i.get(x, y);
let lightness = color.cie() / 255.0;
let saturation = color.saturation();
let nc = if saturation > SATURATION_THRESHOLD
&& SATURATION_BRIGHTNESS_RANGE.contains(&lightness)
{
let b =
(saturation - SATURATION_THRESHOLD) * (255.0 / (1.0 - SATURATION_THRESHOLD));
let RGB { r, g, b: _ } = o.get(x, y);
RGB {
r,
g,
b: math::bounds(b),
}
} else {
let RGB { r, g, b: _ } = o.get(x, y);
RGB { r, g, b: 0 }
};
o.set(x, y, nc);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const WHITE: RGB = RGB {
r: 255,
g: 255,
b: 255,
};
const BLACK: RGB = RGB { r: 0, g: 0, b: 0 };
const RED: RGB = RGB { r: 255, g: 0, b: 0 };
const GREEN: RGB = RGB { r: 0, g: 255, b: 0 };
const BLUE: RGB = RGB { r: 0, g: 0, b: 255 };
const SKIN: RGB = RGB {
r: 255,
g: 200,
b: 159,
};
#[derive(Debug, Clone)]
struct TestImage {
w: u32,
h: u32,
pixels: Vec<Vec<RGB>>,
}
impl TestImage {
fn new(w: u32, h: u32, pixels: Vec<Vec<RGB>>) -> TestImage {
TestImage { w, h, pixels }
}
fn new_single_pixel(pixel: RGB) -> TestImage {
TestImage {
w: 1,
h: 1,
pixels: vec![vec![pixel]],
}
}
fn new_from_fn<G>(w: u32, h: u32, generate: G) -> TestImage
where
G: Fn(u32, u32) -> RGB,
{
let mut pixels = vec![vec![WHITE; h as usize]; w as usize];
for y in 0..h {
for x in 0..w {
pixels[x as usize][y as usize] = generate(x, y)
}
}
TestImage { w, h, pixels }
}
}
impl ImageMap {
fn from_image<I: Image>(image: &I) -> ImageMap {
let mut image_map = ImageMap::new(image.width(), image.height());
for y in 0..image.height() {
for x in 0..image.width() {
let color = image.get(x, y);
image_map.set(x, y, color);
}
}
image_map
}
}
impl Image for TestImage {
fn width(&self) -> u32 {
self.w
}
fn height(&self) -> u32 {
self.h
}
fn get(&self, x: u32, y: u32) -> RGB {
self.pixels[x as usize][y as usize]
}
}
impl ResizableImage<TestImage> for TestImage {
fn resize(&self, width: u32, _height: u32) -> TestImage {
if width == self.w {
return self.clone();
}
let height = (self.h as f64 * width as f64 / self.w as f64).round() as u32;
TestImage {
w: width,
h: height,
pixels: self.pixels.clone(),
}
}
fn crop_and_resize(&self, _crop: Crop, width: u32, height: u32) -> TestImage {
self.resize(width, height)
}
}
#[test]
fn saturation_tests() {
assert_eq!(0.0, BLACK.saturation());
assert_eq!(0.0, WHITE.saturation());
assert_eq!(1.0, RGB::new(255, 0, 0).saturation());
assert_eq!(1.0, RGB::new(0, 255, 0).saturation());
assert_eq!(1.0, RGB::new(0, 0, 255).saturation());
assert_eq!(1.0, RGB::new(0, 255, 255).saturation());
}
#[test]
fn image_map_test() {
let mut image_map = ImageMap::new(1, 2);
assert_eq!(image_map.width, 1);
assert_eq!(image_map.height, 2);
assert_eq!(image_map.get(0, 0), RGB::new(255, 255, 255));
assert_eq!(image_map.get(0, 1), RGB::new(255, 255, 255));
let red = RGB::new(255, 0, 0);
image_map.set(0, 0, red);
assert_eq!(image_map.get(0, 0), red);
let green = RGB::new(0, 255, 0);
image_map.set(0, 1, green);
assert_eq!(image_map.get(0, 1), green);
}
#[test]
fn crops_test() {
let real_min_scale = MIN_SCALE;
let crops = crops(&ImageMap::new(8, 8), 8, 8, real_min_scale);
assert_eq!(
crops[0],
Crop {
x: 0,
y: 0,
width: 8,
height: 8
}
)
}
#[test]
fn score_test_image_with_single_black_pixel_then_score_is_zero() {
let mut i = ImageMap::new(1, 1);
i.set(0, 0, RGB::new(0, 0, 0));
let s = score(
&i,
&Crop {
x: 0,
y: 0,
width: 1,
height: 1,
},
);
assert_eq!(
s,
Score {
detail: 0.0,
saturation: 0.0,
skin: 0.0,
total: 0.0
}
);
}
#[test]
fn score_test_image_with_single_white_pixel_then_score_is_the_same_as_for_js_version() {
let mut i = ImageMap::new(1, 1);
i.set(0, 0, RGB::new(255, 255, 255));
let s = score(
&i,
&Crop {
x: 0,
y: 0,
width: 1,
height: 1,
},
);
let js_version_score = Score {
detail: -6.404213562373096,
saturation: -7.685056274847715,
skin: -6.468255697996827,
total: -13.692208596353678,
};
assert_eq!(s, js_version_score);
}
#[test]
fn skin_detect_single_pixel_test() {
let detect_pixel = |color: RGB| {
let image = TestImage::new_single_pixel(color);
let mut o = ImageMap::new(1, 1);
o.set(0, 0, color);
skin_detect(&image, &mut o);
o.get(0, 0)
};
assert_eq!(detect_pixel(WHITE), RGB::new(0, 255, 255));
assert_eq!(detect_pixel(BLACK), RGB::new(0, 0, 0));
assert_eq!(detect_pixel(RED), RGB::new(0, 0, 0));
assert_eq!(detect_pixel(GREEN), RGB::new(0, 255, 0));
assert_eq!(detect_pixel(BLUE), RGB::new(0, 0, 255));
assert_eq!(detect_pixel(SKIN), RGB::new(159, 200, 159));
}
#[test]
fn edge_detect_single_pixel_image_test() {
let edge_detect_pixel = |color: RGB| {
let image = TestImage::new_single_pixel(color);
let mut o = ImageMap::new(1, 1);
o.set(0, 0, color);
edge_detect(&image, &mut o);
o.get(0, 0)
};
assert_eq!(edge_detect_pixel(BLACK), BLACK);
assert_eq!(edge_detect_pixel(WHITE), WHITE);
assert_eq!(edge_detect_pixel(RED), RGB::new(255, 18, 0));
assert_eq!(edge_detect_pixel(GREEN), RGB::new(0, 182, 0));
assert_eq!(edge_detect_pixel(BLUE), RGB::new(0, 131, 255));
assert_eq!(edge_detect_pixel(SKIN), RGB::new(255, 243, 159));
}
#[test]
fn edge_detect_3x3() {
let image = TestImage::new(
3,
3,
vec![
vec![RED, GREEN, BLUE],
vec![GREEN, BLUE, RED],
vec![BLUE, RED, GREEN],
],
);
let mut o = ImageMap::new(3, 3);
edge_detect(&image, &mut o);
assert_eq!(
o.get(0, 0),
RGB {
r: 255,
g: 18,
b: 0
}
);
assert_eq!(
o.get(0, 0),
RGB {
r: 255,
g: 18,
b: 0
}
);
assert_eq!(o.get(1, 0), RGB { r: 0, g: 182, b: 0 });
assert_eq!(
o.get(2, 0),
RGB {
r: 0,
g: 131,
b: 255
}
);
assert_eq!(o.get(0, 1), RGB { r: 0, g: 182, b: 0 });
assert_eq!(
o.get(1, 1),
RGB {
r: 0,
g: 121,
b: 255
}
);
assert_eq!(
o.get(2, 1),
RGB {
r: 255,
g: 18,
b: 0
}
);
assert_eq!(
o.get(0, 2),
RGB {
r: 0,
g: 131,
b: 255
}
);
assert_eq!(
o.get(1, 2),
RGB {
r: 255,
g: 18,
b: 0
}
);
assert_eq!(o.get(2, 2), RGB { r: 0, g: 182, b: 0 });
}
#[test]
fn saturation_detect_3x3() {
let image = TestImage::new(
3,
3,
vec![
vec![RED, GREEN, BLUE],
vec![WHITE, SKIN, BLACK],
vec![BLUE, RED, GREEN],
],
);
let mut o = ImageMap::from_image(&image);
saturation_detect(&image, &mut o);
assert_eq!(
o.get(0, 0),
RGB {
r: 255,
g: 0,
b: 255
}
);
assert_eq!(
o.get(0, 1),
RGB {
r: 0,
g: 255,
b: 255
}
);
assert_eq!(o.get(0, 2), RGB { r: 0, g: 0, b: 255 });
assert_eq!(
o.get(1, 0),
RGB {
r: 255,
g: 255,
b: 0
}
);
assert_eq!(
o.get(1, 1),
RGB {
r: 255,
g: 200,
b: 0
}
);
assert_eq!(o.get(1, 2), RGB { r: 0, g: 0, b: 0 });
assert_eq!(o.get(2, 0), RGB { r: 0, g: 0, b: 255 });
assert_eq!(
o.get(2, 1),
RGB {
r: 255,
g: 0,
b: 255
}
);
assert_eq!(
o.get(2, 2),
RGB {
r: 0,
g: 255,
b: 255
}
);
}
#[test]
fn analyze_test() {
let image = TestImage::new_from_fn(24, 24, |x, y| {
let center = 8..16;
if center.contains(&x) && center.contains(&y) {
SKIN
} else {
WHITE
}
});
let crop = analyse(
&image,
NonZeroU32::new(8).unwrap(),
NonZeroU32::new(8).unwrap(),
1.0,
);
assert_eq!(crop.crop.width, 8);
assert_eq!(crop.crop.height, 8);
assert_eq!(crop.crop.x, 8);
assert_eq!(crop.crop.y, 8);
assert_eq!(crop.score.saturation, 0.0);
assert_eq!(crop.score.detail, -1.7647058823529413);
assert_eq!(crop.score.skin, -0.03993215515362048);
assert_eq!(crop.score.total, -0.006637797746048519);
}
#[test]
fn crop_scale_test() {
let crop = Crop {
x: 2,
y: 4,
width: 8,
height: 16,
};
let scaled_crop = crop.scale(0.5);
assert_eq!(1, scaled_crop.x);
assert_eq!(2, scaled_crop.y);
assert_eq!(4, scaled_crop.width);
assert_eq!(8, scaled_crop.height);
}
#[test]
fn down_sample_test() {
let image = TestImage::new(
3,
3,
vec![
vec![RED, GREEN, BLUE],
vec![SKIN, BLUE, RED],
vec![BLUE, RED, GREEN],
],
);
let image_map = ImageMap::from_image(&image);
let result = image_map.down_sample(3);
assert_eq!(result.width, 1);
assert_eq!(result.height, 1);
assert_eq!(result.get(0, 0), RGB::new(184, 132, 103));
}
}