1mod conv;
42mod error;
43mod map;
44mod metric;
45#[cfg(feature = "napi")]
46mod napi;
47mod network;
48mod pyramid;
49#[cfg(feature = "python")]
50mod python;
51mod simd;
52#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
53mod wasm;
54mod weights;
55
56pub use error::MiloError;
57pub use map::render_map;
58
59pub const MIN_SIDE: usize = 16;
62
63#[derive(Clone, Copy, Debug)]
66pub struct Rgba8<'a> {
67 pub data: &'a [u8],
68 pub width: usize,
69 pub height: usize,
70}
71
72impl<'a> Rgba8<'a> {
73 pub fn new(data: &'a [u8], width: usize, height: usize) -> Self {
74 Self {
75 data,
76 width,
77 height,
78 }
79 }
80
81 fn validate(&self) -> Result<(), MiloError> {
83 let pixels = self.width * self.height;
84 if self.data.len() < pixels * 4 {
85 return Err(MiloError::Options(format!(
86 "image data is {} bytes, need {} for {}x{} RGBA",
87 self.data.len(),
88 pixels * 4,
89 self.width,
90 self.height
91 )));
92 }
93 Ok(())
94 }
95
96 fn validate_pair(&self, other: Rgba8<'_>) -> Result<(), MiloError> {
99 if self.width != other.width || self.height != other.height {
100 return Err(MiloError::SizeMismatch {
101 img1_width: self.width as u32,
102 img1_height: self.height as u32,
103 img2_width: other.width as u32,
104 img2_height: other.height as u32,
105 });
106 }
107 if self.width < MIN_SIDE || self.height < MIN_SIDE {
108 return Err(MiloError::InputTooSmall {
109 width: self.width as u32,
110 height: self.height as u32,
111 minimum: MIN_SIDE as u32,
112 });
113 }
114 self.validate()?;
115 other.validate()
116 }
117}
118
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
121pub struct MiloOptions {
122 pub threads: Option<usize>,
126}
127
128#[derive(Clone, Debug)]
130pub struct MiloOutcome {
131 pub raw_error: f64,
134 pub mos: f64,
137 pub mask: Vec<f32>,
140 pub error_map: Vec<f32>,
144 pub width: usize,
145 pub height: usize,
146}
147
148pub fn milo(
152 reference: Rgba8<'_>,
153 distorted: Rgba8<'_>,
154 options: &MiloOptions,
155) -> Result<MiloOutcome, MiloError> {
156 metric::run(reference, distorted, options)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn rgba(width: usize, height: usize, value: u8) -> Vec<u8> {
164 let mut data = vec![0u8; width * height * 4];
165 for pixel in data.chunks_exact_mut(4) {
166 pixel.copy_from_slice(&[value, value, value, 255]);
167 }
168 data
169 }
170
171 #[test]
172 fn identical_images_score_zero() {
173 let data = rgba(20, 17, 128);
174 let outcome = milo(
175 Rgba8::new(&data, 20, 17),
176 Rgba8::new(&data, 20, 17),
177 &MiloOptions::default(),
178 )
179 .unwrap();
180 assert_eq!(outcome.raw_error, 0.0);
181 assert!(outcome.error_map.iter().all(|v| *v == 0.0));
182 assert_eq!(outcome.mask.len(), 20 * 17);
183 assert!((outcome.mos - 4.346).abs() < 0.01);
184 }
185
186 #[test]
187 fn a_visible_change_scores_above_zero() {
188 let a = rgba(32, 32, 30);
189 let mut b = a.clone();
190 for pixel in b.chunks_exact_mut(4).skip(300).take(200) {
191 pixel[..3].copy_from_slice(&[220, 220, 220]);
192 }
193 let outcome = milo(
194 Rgba8::new(&a, 32, 32),
195 Rgba8::new(&b, 32, 32),
196 &MiloOptions::default(),
197 )
198 .unwrap();
199 assert!(outcome.raw_error > 0.0);
200 assert!(outcome.mos < 4.3);
201 assert!(outcome.error_map.iter().any(|v| *v > 0.0));
202 }
203
204 #[test]
205 fn the_thread_count_does_not_change_the_answer() {
206 let a = rgba(40, 70, 90);
207 let mut b = a.clone();
208 for (i, pixel) in b.chunks_exact_mut(4).enumerate() {
209 pixel[0] = ((i * 31) % 255) as u8;
210 }
211 let one = milo(
212 Rgba8::new(&a, 40, 70),
213 Rgba8::new(&b, 40, 70),
214 &MiloOptions { threads: Some(1) },
215 )
216 .unwrap();
217 let four = milo(
218 Rgba8::new(&a, 40, 70),
219 Rgba8::new(&b, 40, 70),
220 &MiloOptions { threads: Some(4) },
221 )
222 .unwrap();
223 assert_eq!(one.raw_error, four.raw_error);
224 assert_eq!(one.mask, four.mask);
225 assert_eq!(one.error_map, four.error_map);
226 }
227
228 #[test]
229 fn alpha_is_ignored() {
230 let a = rgba(16, 16, 100);
231 let mut b = a.clone();
232 for pixel in b.chunks_exact_mut(4) {
233 pixel[3] = 0;
234 }
235 let outcome = milo(
236 Rgba8::new(&a, 16, 16),
237 Rgba8::new(&b, 16, 16),
238 &MiloOptions::default(),
239 )
240 .unwrap();
241 assert_eq!(outcome.raw_error, 0.0);
242 }
243
244 #[test]
245 fn mismatched_sizes_are_rejected() {
246 let a = rgba(16, 16, 0);
247 let b = rgba(16, 17, 0);
248 assert!(matches!(
249 milo(
250 Rgba8::new(&a, 16, 16),
251 Rgba8::new(&b, 16, 17),
252 &MiloOptions::default()
253 ),
254 Err(MiloError::SizeMismatch { .. })
255 ));
256 }
257
258 #[test]
259 fn inputs_below_the_reference_floor_are_rejected() {
260 let a = rgba(15, 40, 0);
261 assert!(matches!(
262 milo(
263 Rgba8::new(&a, 15, 40),
264 Rgba8::new(&a, 15, 40),
265 &MiloOptions::default()
266 ),
267 Err(MiloError::InputTooSmall { minimum: 16, .. })
268 ));
269 }
270
271 #[test]
272 fn a_short_buffer_is_rejected() {
273 let a = rgba(16, 16, 0);
274 assert!(matches!(
275 milo(
276 Rgba8::new(&a, 32, 16),
277 Rgba8::new(&a, 32, 16),
278 &MiloOptions::default()
279 ),
280 Err(MiloError::Options(_))
281 ));
282 }
283}