mod conv;
mod error;
mod map;
mod metric;
#[cfg(feature = "napi")]
mod napi;
mod network;
mod pyramid;
#[cfg(feature = "python")]
mod python;
mod simd;
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
mod wasm;
mod weights;
pub use error::MiloError;
pub use map::render_map;
pub const MIN_SIDE: usize = 16;
#[derive(Clone, Copy, Debug)]
pub struct Rgba8<'a> {
pub data: &'a [u8],
pub width: usize,
pub height: usize,
}
impl<'a> Rgba8<'a> {
pub fn new(data: &'a [u8], width: usize, height: usize) -> Self {
Self {
data,
width,
height,
}
}
fn validate(&self) -> Result<(), MiloError> {
let pixels = self.width * self.height;
if self.data.len() < pixels * 4 {
return Err(MiloError::Options(format!(
"image data is {} bytes, need {} for {}x{} RGBA",
self.data.len(),
pixels * 4,
self.width,
self.height
)));
}
Ok(())
}
fn validate_pair(&self, other: Rgba8<'_>) -> Result<(), MiloError> {
if self.width != other.width || self.height != other.height {
return Err(MiloError::SizeMismatch {
img1_width: self.width as u32,
img1_height: self.height as u32,
img2_width: other.width as u32,
img2_height: other.height as u32,
});
}
if self.width < MIN_SIDE || self.height < MIN_SIDE {
return Err(MiloError::InputTooSmall {
width: self.width as u32,
height: self.height as u32,
minimum: MIN_SIDE as u32,
});
}
self.validate()?;
other.validate()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MiloOptions {
pub threads: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct MiloOutcome {
pub raw_error: f64,
pub mos: f64,
pub mask: Vec<f32>,
pub error_map: Vec<f32>,
pub width: usize,
pub height: usize,
}
pub fn milo(
reference: Rgba8<'_>,
distorted: Rgba8<'_>,
options: &MiloOptions,
) -> Result<MiloOutcome, MiloError> {
metric::run(reference, distorted, options)
}
#[cfg(test)]
mod tests {
use super::*;
fn rgba(width: usize, height: usize, value: u8) -> Vec<u8> {
let mut data = vec![0u8; width * height * 4];
for pixel in data.chunks_exact_mut(4) {
pixel.copy_from_slice(&[value, value, value, 255]);
}
data
}
#[test]
fn identical_images_score_zero() {
let data = rgba(20, 17, 128);
let outcome = milo(
Rgba8::new(&data, 20, 17),
Rgba8::new(&data, 20, 17),
&MiloOptions::default(),
)
.unwrap();
assert_eq!(outcome.raw_error, 0.0);
assert!(outcome.error_map.iter().all(|v| *v == 0.0));
assert_eq!(outcome.mask.len(), 20 * 17);
assert!((outcome.mos - 4.346).abs() < 0.01);
}
#[test]
fn a_visible_change_scores_above_zero() {
let a = rgba(32, 32, 30);
let mut b = a.clone();
for pixel in b.chunks_exact_mut(4).skip(300).take(200) {
pixel[..3].copy_from_slice(&[220, 220, 220]);
}
let outcome = milo(
Rgba8::new(&a, 32, 32),
Rgba8::new(&b, 32, 32),
&MiloOptions::default(),
)
.unwrap();
assert!(outcome.raw_error > 0.0);
assert!(outcome.mos < 4.3);
assert!(outcome.error_map.iter().any(|v| *v > 0.0));
}
#[test]
fn the_thread_count_does_not_change_the_answer() {
let a = rgba(40, 70, 90);
let mut b = a.clone();
for (i, pixel) in b.chunks_exact_mut(4).enumerate() {
pixel[0] = ((i * 31) % 255) as u8;
}
let one = milo(
Rgba8::new(&a, 40, 70),
Rgba8::new(&b, 40, 70),
&MiloOptions { threads: Some(1) },
)
.unwrap();
let four = milo(
Rgba8::new(&a, 40, 70),
Rgba8::new(&b, 40, 70),
&MiloOptions { threads: Some(4) },
)
.unwrap();
assert_eq!(one.raw_error, four.raw_error);
assert_eq!(one.mask, four.mask);
assert_eq!(one.error_map, four.error_map);
}
#[test]
fn alpha_is_ignored() {
let a = rgba(16, 16, 100);
let mut b = a.clone();
for pixel in b.chunks_exact_mut(4) {
pixel[3] = 0;
}
let outcome = milo(
Rgba8::new(&a, 16, 16),
Rgba8::new(&b, 16, 16),
&MiloOptions::default(),
)
.unwrap();
assert_eq!(outcome.raw_error, 0.0);
}
#[test]
fn mismatched_sizes_are_rejected() {
let a = rgba(16, 16, 0);
let b = rgba(16, 17, 0);
assert!(matches!(
milo(
Rgba8::new(&a, 16, 16),
Rgba8::new(&b, 16, 17),
&MiloOptions::default()
),
Err(MiloError::SizeMismatch { .. })
));
}
#[test]
fn inputs_below_the_reference_floor_are_rejected() {
let a = rgba(15, 40, 0);
assert!(matches!(
milo(
Rgba8::new(&a, 15, 40),
Rgba8::new(&a, 15, 40),
&MiloOptions::default()
),
Err(MiloError::InputTooSmall { minimum: 16, .. })
));
}
#[test]
fn a_short_buffer_is_rejected() {
let a = rgba(16, 16, 0);
assert!(matches!(
milo(
Rgba8::new(&a, 32, 16),
Rgba8::new(&a, 32, 16),
&MiloOptions::default()
),
Err(MiloError::Options(_))
));
}
}