1mod blur;
2mod input;
3mod precompute;
4#[doc(hidden)]
6pub mod reference_data;
7mod simd_ops;
8mod xyb_simd;
9
10#[cfg(feature = "unsafe-simd")]
11mod xyb_unsafe_simd;
12
13#[cfg(feature = "unsafe-simd")]
14mod ssim_unsafe_simd;
15
16pub use blur::Blur;
17pub use input::{LinearRgbImage, ToLinearRgb};
18pub use precompute::Ssimulacra2Reference;
19pub use yuvxyb::{
21 ColorPrimaries, Frame, LinearRgb, MatrixCoefficients, Pixel, Plane, Rgb, TransferCharacteristic,
22 Yuv, YuvConfig,
23};
24
25pub use input::{srgb_to_linear, srgb_u16_to_linear, srgb_u8_to_linear};
27
28use yuvxyb::Xyb;
30
31#[cfg(all(feature = "unsafe-simd", target_arch = "x86_64"))]
32use safe_unaligned_simd::x86_64 as safe_simd;
33
34pub(crate) const NUM_SCALES: usize = 6;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum SimdImpl {
41 Scalar,
43 #[default]
45 Simd,
46 #[cfg(feature = "unsafe-simd")]
48 UnsafeSimd,
49}
50
51impl SimdImpl {
52 pub fn name(&self) -> &'static str {
54 match self {
55 SimdImpl::Scalar => "scalar",
56 SimdImpl::Simd => "simd (wide crate)",
57 #[cfg(feature = "unsafe-simd")]
58 SimdImpl::UnsafeSimd => "unsafe-simd (raw intrinsics)",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, Default)]
65pub struct Ssimulacra2Config {
66 pub impl_type: SimdImpl,
68}
69
70impl Ssimulacra2Config {
71 pub fn new(impl_type: SimdImpl) -> Self {
73 Self { impl_type }
74 }
75
76 pub fn simd() -> Self {
78 Self::new(SimdImpl::Simd)
79 }
80
81 #[cfg(feature = "unsafe-simd")]
83 pub fn unsafe_simd() -> Self {
84 Self::new(SimdImpl::UnsafeSimd)
85 }
86
87 pub fn scalar() -> Self {
89 Self::new(SimdImpl::Scalar)
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
95pub enum Ssimulacra2Error {
96 #[error("Failed to convert input image to linear RGB")]
98 LinearRgbConversionFailed,
99
100 #[error("Source and distorted image width and height must be equal")]
102 NonMatchingImageDimensions,
103
104 #[error("Images must be at least 8x8 pixels")]
106 InvalidImageSize,
107
108 #[error("Gaussian blur operation failed")]
110 GaussianBlurError,
111}
112
113pub fn compute_frame_ssimulacra2<T, U>(source: T, distorted: U) -> Result<f64, Ssimulacra2Error>
115where
116 LinearRgb: TryFrom<T> + TryFrom<U>,
117{
118 compute_frame_ssimulacra2_impl(source, distorted, Ssimulacra2Config::default())
119}
120
121pub fn compute_frame_ssimulacra2_with_config<T, U>(
123 source: T,
124 distorted: U,
125 config: Ssimulacra2Config,
126) -> Result<f64, Ssimulacra2Error>
127where
128 LinearRgb: TryFrom<T> + TryFrom<U>,
129{
130 compute_frame_ssimulacra2_impl(source, distorted, config)
131}
132
133pub fn compute_ssimulacra2<S, D>(source: S, distorted: D) -> Result<f64, Ssimulacra2Error>
155where
156 S: ToLinearRgb,
157 D: ToLinearRgb,
158{
159 compute_ssimulacra2_with_config(source, distorted, Ssimulacra2Config::default())
160}
161
162pub fn compute_ssimulacra2_with_config<S, D>(
164 source: S,
165 distorted: D,
166 config: Ssimulacra2Config,
167) -> Result<f64, Ssimulacra2Error>
168where
169 S: ToLinearRgb,
170 D: ToLinearRgb,
171{
172 let img1: LinearRgb = source.to_linear_rgb().into();
173 let img2: LinearRgb = distorted.to_linear_rgb().into();
174 compute_frame_ssimulacra2_impl(img1, img2, config)
175}
176
177fn compute_frame_ssimulacra2_impl<T, U>(
178 source: T,
179 distorted: U,
180 config: Ssimulacra2Config,
181) -> Result<f64, Ssimulacra2Error>
182where
183 LinearRgb: TryFrom<T> + TryFrom<U>,
184{
185 let Ok(mut img1) = LinearRgb::try_from(source) else {
186 return Err(Ssimulacra2Error::LinearRgbConversionFailed);
187 };
188
189 let Ok(mut img2) = LinearRgb::try_from(distorted) else {
190 return Err(Ssimulacra2Error::LinearRgbConversionFailed);
191 };
192
193 if img1.width() != img2.width() || img1.height() != img2.height() {
194 return Err(Ssimulacra2Error::NonMatchingImageDimensions);
195 }
196
197 if img1.width() < 8 || img1.height() < 8 {
198 return Err(Ssimulacra2Error::InvalidImageSize);
199 }
200
201 let mut width = img1.width();
202 let mut height = img1.height();
203 let impl_type = config.impl_type;
204
205 let alloc_plane = || vec![0.0f32; width * height];
207 let alloc_3planes = || [alloc_plane(), alloc_plane(), alloc_plane()];
208
209 let mut mul = alloc_3planes();
210 let mut sigma1_sq = alloc_3planes();
211 let mut sigma2_sq = alloc_3planes();
212 let mut sigma12 = alloc_3planes();
213 let mut mu1 = alloc_3planes();
214 let mut mu2 = alloc_3planes();
215 let mut img1_planar = alloc_3planes();
216 let mut img2_planar = alloc_3planes();
217
218 let mut blur = Blur::with_simd_impl(width, height, impl_type);
219 let mut msssim = Msssim::default();
220
221 for scale in 0..NUM_SCALES {
222 if width < 8 || height < 8 {
223 break;
224 }
225
226 if scale > 0 {
227 img1 = downscale_by_2(&img1);
228 img2 = downscale_by_2(&img2);
229 width = img1.width();
230 height = img2.height();
231 }
232
233 let size = width * height;
235 for buf in [
236 &mut mul,
237 &mut sigma1_sq,
238 &mut sigma2_sq,
239 &mut sigma12,
240 &mut mu1,
241 &mut mu2,
242 &mut img1_planar,
243 &mut img2_planar,
244 ] {
245 for c in buf.iter_mut() {
246 c.truncate(size);
247 }
248 }
249 blur.shrink_to(width, height);
250
251 let mut img1_xyb = linear_rgb_to_xyb(img1.clone(), impl_type);
252 let mut img2_xyb = linear_rgb_to_xyb(img2.clone(), impl_type);
253
254 make_positive_xyb(&mut img1_xyb);
255 make_positive_xyb(&mut img2_xyb);
256
257 xyb_to_planar_into(&img1_xyb, &mut img1_planar);
258 xyb_to_planar_into(&img2_xyb, &mut img2_planar);
259
260 image_multiply(&img1_planar, &img1_planar, &mut mul, impl_type);
261 blur.blur_into(&mul, &mut sigma1_sq);
262
263 image_multiply(&img2_planar, &img2_planar, &mut mul, impl_type);
264 blur.blur_into(&mul, &mut sigma2_sq);
265
266 image_multiply(&img1_planar, &img2_planar, &mut mul, impl_type);
267 blur.blur_into(&mul, &mut sigma12);
268
269 blur.blur_into(&img1_planar, &mut mu1);
270 blur.blur_into(&img2_planar, &mut mu2);
271
272 let avg_ssim = ssim_map(
273 width, height, &mu1, &mu2, &sigma1_sq, &sigma2_sq, &sigma12, impl_type,
274 );
275 let avg_edgediff = edge_diff_map(
276 width,
277 height,
278 &img1_planar,
279 &mu1,
280 &img2_planar,
281 &mu2,
282 impl_type,
283 );
284 msssim.scales.push(MsssimScale {
285 avg_ssim,
286 avg_edgediff,
287 });
288 }
289
290 Ok(msssim.score())
291}
292
293fn linear_rgb_to_xyb(linear_rgb: LinearRgb, impl_type: SimdImpl) -> Xyb {
295 match impl_type {
296 SimdImpl::Scalar => Xyb::from(linear_rgb),
297 SimdImpl::Simd => {
298 let width = linear_rgb.width();
299 let height = linear_rgb.height();
300 let mut data = linear_rgb.into_data();
301 xyb_simd::linear_rgb_to_xyb_simd(&mut data);
302 Xyb::new(data, width, height).expect("XYB construction should not fail")
303 }
304 #[cfg(feature = "unsafe-simd")]
305 SimdImpl::UnsafeSimd => {
306 let width = linear_rgb.width();
307 let height = linear_rgb.height();
308 let mut data = linear_rgb.into_data();
309 xyb_unsafe_simd::linear_rgb_to_xyb_unsafe(&mut data);
310 Xyb::new(data, width, height).expect("XYB construction should not fail")
311 }
312 }
313}
314
315pub(crate) fn linear_rgb_to_xyb_simd(linear_rgb: LinearRgb) -> Xyb {
317 linear_rgb_to_xyb(linear_rgb, SimdImpl::Simd)
318}
319
320pub(crate) fn make_positive_xyb(xyb: &mut Xyb) {
321 for pix in xyb.data_mut().iter_mut() {
322 pix[2] = (pix[2] - pix[1]) + 0.55;
323 pix[0] = (pix[0]).mul_add(14.0, 0.42);
324 pix[1] += 0.01;
325 }
326}
327
328pub(crate) fn xyb_to_planar(xyb: &Xyb) -> [Vec<f32>; 3] {
334 let size = xyb.width() * xyb.height();
335 let mut out = [vec![0.0f32; size], vec![0.0f32; size], vec![0.0f32; size]];
336 xyb_to_planar_into(xyb, &mut out);
337 out
338}
339
340pub(crate) fn xyb_to_planar_into(xyb: &Xyb, out: &mut [Vec<f32>; 3]) {
342 let [out0, out1, out2] = out;
343 for (((i, o0), o1), o2) in xyb
344 .data()
345 .iter()
346 .copied()
347 .zip(out0.iter_mut())
348 .zip(out1.iter_mut())
349 .zip(out2.iter_mut())
350 {
351 *o0 = i[0];
352 *o1 = i[1];
353 *o2 = i[2];
354 }
355}
356
357pub(crate) fn image_multiply(
358 img1: &[Vec<f32>; 3],
359 img2: &[Vec<f32>; 3],
360 out: &mut [Vec<f32>; 3],
361 impl_type: SimdImpl,
362) {
363 match impl_type {
364 SimdImpl::Scalar => image_multiply_scalar(img1, img2, out),
365 SimdImpl::Simd => simd_ops::image_multiply_simd(img1, img2, out),
366 #[cfg(feature = "unsafe-simd")]
367 SimdImpl::UnsafeSimd => {
368 #[cfg(target_arch = "x86_64")]
369 {
370 if is_x86_feature_detected!("avx2") {
371 unsafe { image_multiply_avx2(img1, img2, out) };
372 return;
373 }
374 }
375 simd_ops::image_multiply_simd(img1, img2, out);
377 }
378 }
379}
380
381fn image_multiply_scalar(img1: &[Vec<f32>; 3], img2: &[Vec<f32>; 3], out: &mut [Vec<f32>; 3]) {
382 for ((plane1, plane2), out_plane) in img1.iter().zip(img2.iter()).zip(out.iter_mut()) {
383 for ((&p1, &p2), o) in plane1.iter().zip(plane2.iter()).zip(out_plane.iter_mut()) {
384 *o = p1 * p2;
385 }
386 }
387}
388
389#[cfg(all(feature = "unsafe-simd", target_arch = "x86_64"))]
390#[target_feature(enable = "avx2")]
391unsafe fn image_multiply_avx2(img1: &[Vec<f32>; 3], img2: &[Vec<f32>; 3], out: &mut [Vec<f32>; 3]) {
392 use std::arch::x86_64::*;
393
394 for c in 0..3 {
395 let plane1 = &img1[c];
396 let plane2 = &img2[c];
397 let out_plane = &mut out[c];
398 let len = plane1.len();
399
400 let chunks_8 = len / 8;
401 for chunk in 0..chunks_8 {
402 let base = chunk * 8;
403 let v1 = safe_simd::_mm256_loadu_ps(plane1[base..].first_chunk::<8>().unwrap());
405 let v2 = safe_simd::_mm256_loadu_ps(plane2[base..].first_chunk::<8>().unwrap());
406 let result = _mm256_mul_ps(v1, v2);
407 safe_simd::_mm256_storeu_ps(out_plane[base..].first_chunk_mut::<8>().unwrap(), result);
409 }
410
411 for i in (chunks_8 * 8)..len {
413 out_plane[i] = plane1[i] * plane2[i];
414 }
415 }
416}
417
418pub(crate) fn downscale_by_2(in_data: &LinearRgb) -> LinearRgb {
419 const SCALE: usize = 2;
420 let in_w = in_data.width();
421 let in_h = in_data.height();
422 let out_w = in_w.div_ceil(SCALE);
423 let out_h = in_h.div_ceil(SCALE);
424 let mut out_data = vec![[0.0f32; 3]; out_w * out_h];
425
426 let in_data = &in_data.data();
427 for oy in 0..out_h {
428 for ox in 0..out_w {
429 for c in 0..3 {
430 let mut sum = 0f64;
431 for iy in 0..SCALE {
432 for ix in 0..SCALE {
433 let x = (ox * SCALE + ix).min(in_w - 1);
434 let y = (oy * SCALE + iy).min(in_h - 1);
435 let in_pix = in_data[y * in_w + x];
436 sum += f64::from(in_pix[c]);
437 }
438 }
439 let out_pix = &mut out_data[oy * out_w + ox];
440 out_pix[c] = (sum / (SCALE * SCALE) as f64) as f32;
441 }
442 }
443 }
444
445 LinearRgb::new(out_data, out_w, out_h).expect("Resolution and data size match")
446}
447
448#[allow(clippy::too_many_arguments)]
449pub(crate) fn ssim_map(
450 width: usize,
451 height: usize,
452 m1: &[Vec<f32>; 3],
453 m2: &[Vec<f32>; 3],
454 s11: &[Vec<f32>; 3],
455 s22: &[Vec<f32>; 3],
456 s12: &[Vec<f32>; 3],
457 impl_type: SimdImpl,
458) -> [f64; 3 * 2] {
459 match impl_type {
460 SimdImpl::Scalar => ssim_map_scalar(width, height, m1, m2, s11, s22, s12),
461 SimdImpl::Simd => simd_ops::ssim_map_simd(width, height, m1, m2, s11, s22, s12),
462 #[cfg(feature = "unsafe-simd")]
463 SimdImpl::UnsafeSimd => {
464 ssim_unsafe_simd::ssim_map_unsafe(width, height, m1, m2, s11, s22, s12)
465 }
466 }
467}
468
469fn ssim_map_scalar(
470 width: usize,
471 height: usize,
472 m1: &[Vec<f32>; 3],
473 m2: &[Vec<f32>; 3],
474 s11: &[Vec<f32>; 3],
475 s22: &[Vec<f32>; 3],
476 s12: &[Vec<f32>; 3],
477) -> [f64; 3 * 2] {
478 const C2: f32 = 0.0009f32;
479
480 let one_per_pixels = 1.0f64 / (width * height) as f64;
481 let mut plane_averages = [0f64; 3 * 2];
482
483 for c in 0..3 {
484 let mut sum1 = [0.0f64; 2];
485 for (row_m1, (row_m2, (row_s11, (row_s22, row_s12)))) in m1[c].chunks_exact(width).zip(
486 m2[c].chunks_exact(width).zip(
487 s11[c]
488 .chunks_exact(width)
489 .zip(s22[c].chunks_exact(width).zip(s12[c].chunks_exact(width))),
490 ),
491 ) {
492 for x in 0..width {
493 let mu1 = row_m1[x];
494 let mu2 = row_m2[x];
495 let mu11 = mu1 * mu1;
496 let mu22 = mu2 * mu2;
497 let mu12 = mu1 * mu2;
498 let mu_diff = mu1 - mu2;
499
500 let num_m = f64::from(mu_diff).mul_add(-f64::from(mu_diff), 1.0f64);
501 let num_s = 2f64.mul_add(f64::from(row_s12[x] - mu12), f64::from(C2));
502 let denom_s =
503 f64::from(row_s11[x] - mu11) + f64::from(row_s22[x] - mu22) + f64::from(C2);
504 let mut d = 1.0f64 - (num_m * num_s) / denom_s;
505 d = d.max(0.0);
506 sum1[0] += d;
507 sum1[1] += d.powi(4);
508 }
509 }
510 plane_averages[c * 2] = one_per_pixels * sum1[0];
511 plane_averages[c * 2 + 1] = (one_per_pixels * sum1[1]).sqrt().sqrt();
512 }
513
514 plane_averages
515}
516
517pub(crate) fn edge_diff_map(
518 width: usize,
519 height: usize,
520 img1: &[Vec<f32>; 3],
521 mu1: &[Vec<f32>; 3],
522 img2: &[Vec<f32>; 3],
523 mu2: &[Vec<f32>; 3],
524 impl_type: SimdImpl,
525) -> [f64; 3 * 4] {
526 match impl_type {
527 SimdImpl::Scalar => edge_diff_map_scalar(width, height, img1, mu1, img2, mu2),
528 SimdImpl::Simd => simd_ops::edge_diff_map_simd(width, height, img1, mu1, img2, mu2),
529 #[cfg(feature = "unsafe-simd")]
530 SimdImpl::UnsafeSimd => {
531 ssim_unsafe_simd::edge_diff_map_unsafe(width, height, img1, mu1, img2, mu2)
532 }
533 }
534}
535
536fn edge_diff_map_scalar(
537 width: usize,
538 height: usize,
539 img1: &[Vec<f32>; 3],
540 mu1: &[Vec<f32>; 3],
541 img2: &[Vec<f32>; 3],
542 mu2: &[Vec<f32>; 3],
543) -> [f64; 3 * 4] {
544 let one_per_pixels = 1.0f64 / (width * height) as f64;
545 let mut plane_averages = [0f64; 3 * 4];
546
547 for c in 0..3 {
548 let mut sum1 = [0.0f64; 4];
549 for (row1, (row2, (rowm1, rowm2))) in img1[c].chunks_exact(width).zip(
550 img2[c]
551 .chunks_exact(width)
552 .zip(mu1[c].chunks_exact(width).zip(mu2[c].chunks_exact(width))),
553 ) {
554 for x in 0..width {
555 let d1: f64 = (1.0 + f64::from((row2[x] - rowm2[x]).abs()))
556 / (1.0 + f64::from((row1[x] - rowm1[x]).abs()))
557 - 1.0;
558
559 let artifact = d1.max(0.0);
560 sum1[0] += artifact;
561 sum1[1] += artifact.powi(4);
562
563 let detail_lost = (-d1).max(0.0);
564 sum1[2] += detail_lost;
565 sum1[3] += detail_lost.powi(4);
566 }
567 }
568 plane_averages[c * 4] = one_per_pixels * sum1[0];
569 plane_averages[c * 4 + 1] = (one_per_pixels * sum1[1]).sqrt().sqrt();
570 plane_averages[c * 4 + 2] = one_per_pixels * sum1[2];
571 plane_averages[c * 4 + 3] = (one_per_pixels * sum1[3]).sqrt().sqrt();
572 }
573
574 plane_averages
575}
576
577#[derive(Debug, Clone, Default)]
578pub(crate) struct Msssim {
579 pub scales: Vec<MsssimScale>,
580}
581
582#[derive(Debug, Clone, Copy, Default)]
583pub(crate) struct MsssimScale {
584 pub avg_ssim: [f64; 3 * 2],
585 pub avg_edgediff: [f64; 3 * 4],
586}
587
588impl Msssim {
589 #[allow(clippy::too_many_lines)]
590 pub fn score(&self) -> f64 {
591 const WEIGHT: [f64; 108] = [
592 0.0,
593 0.000_737_660_670_740_658_6,
594 0.0,
595 0.0,
596 0.000_779_348_168_286_730_9,
597 0.0,
598 0.0,
599 0.000_437_115_573_010_737_9,
600 0.0,
601 1.104_172_642_665_734_6,
602 0.000_662_848_341_292_71,
603 0.000_152_316_327_837_187_52,
604 0.0,
605 0.001_640_643_745_659_975_4,
606 0.0,
607 1.842_245_552_053_929_8,
608 11.441_172_603_757_666,
609 0.0,
610 0.000_798_910_943_601_516_3,
611 0.000_176_816_438_078_653,
612 0.0,
613 1.878_759_497_954_638_7,
614 10.949_069_906_051_42,
615 0.0,
616 0.000_728_934_699_150_807_2,
617 0.967_793_708_062_683_3,
618 0.0,
619 0.000_140_034_242_854_358_84,
620 0.998_176_697_785_496_7,
621 0.000_319_497_559_344_350_53,
622 0.000_455_099_211_379_206_3,
623 0.0,
624 0.0,
625 0.001_364_876_616_324_339_8,
626 0.0,
627 0.0,
628 0.0,
629 0.0,
630 0.0,
631 7.466_890_328_078_848,
632 0.0,
633 17.445_833_984_131_262,
634 0.000_623_560_163_404_146_6,
635 0.0,
636 0.0,
637 6.683_678_146_179_332,
638 0.000_377_244_079_796_112_96,
639 1.027_889_937_768_264,
640 225.205_153_008_492_74,
641 0.0,
642 0.0,
643 19.213_238_186_143_016,
644 0.001_140_152_458_661_836_1,
645 0.001_237_755_635_509_985,
646 176.393_175_984_506_94,
647 0.0,
648 0.0,
649 24.433_009_998_704_76,
650 0.285_208_026_121_177_57,
651 0.000_448_543_692_383_340_8,
652 0.0,
653 0.0,
654 0.0,
655 34.779_063_444_837_72,
656 44.835_625_328_877_896,
657 0.0,
658 0.0,
659 0.0,
660 0.0,
661 0.0,
662 0.0,
663 0.0,
664 0.0,
665 0.000_868_055_657_329_169_8,
666 0.0,
667 0.0,
668 0.0,
669 0.0,
670 0.0,
671 0.000_531_319_187_435_874_7,
672 0.0,
673 0.000_165_338_141_613_791_12,
674 0.0,
675 0.0,
676 0.0,
677 0.0,
678 0.0,
679 0.000_417_917_180_325_133_6,
680 0.001_729_082_823_472_283_3,
681 0.0,
682 0.002_082_700_584_663_643_7,
683 0.0,
684 0.0,
685 8.826_982_764_996_862,
686 23.192_433_439_989_26,
687 0.0,
688 95.108_049_881_108_6,
689 0.986_397_803_440_068_2,
690 0.983_438_279_246_535_3,
691 0.001_228_640_504_827_849_3,
692 171.266_725_589_730_7,
693 0.980_785_887_243_537_9,
694 0.0,
695 0.0,
696 0.0,
697 0.000_513_006_458_899_067_9,
698 0.0,
699 0.000_108_540_578_584_115_37,
700 ];
701
702 let mut ssim = 0.0f64;
703
704 let mut i = 0usize;
705 for c in 0..3 {
706 for scale in &self.scales {
707 for n in 0..2 {
708 ssim = WEIGHT[i].mul_add(scale.avg_ssim[c * 2 + n].abs(), ssim);
709 i += 1;
710 ssim = WEIGHT[i].mul_add(scale.avg_edgediff[c * 4 + n].abs(), ssim);
711 i += 1;
712 ssim = WEIGHT[i].mul_add(scale.avg_edgediff[c * 4 + n + 2].abs(), ssim);
713 i += 1;
714 }
715 }
716 }
717
718 ssim *= 0.956_238_261_683_484_4_f64;
719 ssim = (6.248_496_625_763_138e-5 * ssim * ssim).mul_add(
720 ssim,
721 2.326_765_642_916_932f64.mul_add(ssim, -0.020_884_521_182_843_837 * ssim * ssim),
722 );
723
724 if ssim > 0.0f64 {
725 ssim = ssim
726 .powf(0.627_633_646_783_138_7)
727 .mul_add(-10.0f64, 100.0f64);
728 } else {
729 ssim = 100.0f64;
730 }
731
732 ssim
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use std::path::PathBuf;
739
740 use super::*;
741 use yuvxyb::{ColorPrimaries, Rgb, TransferCharacteristic};
742
743 #[test]
744 fn test_ssimulacra2() {
745 let source = image::open(
746 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
747 .join("test_data")
748 .join("tank_source.png"),
749 )
750 .unwrap();
751 let distorted = image::open(
752 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
753 .join("test_data")
754 .join("tank_distorted.png"),
755 )
756 .unwrap();
757 let source_data = source
758 .to_rgb32f()
759 .chunks_exact(3)
760 .map(|chunk| [chunk[0], chunk[1], chunk[2]])
761 .collect::<Vec<_>>();
762 let source_data = Xyb::try_from(
763 Rgb::new(
764 source_data,
765 source.width() as usize,
766 source.height() as usize,
767 TransferCharacteristic::SRGB,
768 ColorPrimaries::BT709,
769 )
770 .unwrap(),
771 )
772 .unwrap();
773 let distorted_data = distorted
774 .to_rgb32f()
775 .chunks_exact(3)
776 .map(|chunk| [chunk[0], chunk[1], chunk[2]])
777 .collect::<Vec<_>>();
778 let distorted_data = Xyb::try_from(
779 Rgb::new(
780 distorted_data,
781 distorted.width() as usize,
782 distorted.height() as usize,
783 TransferCharacteristic::SRGB,
784 ColorPrimaries::BT709,
785 )
786 .unwrap(),
787 )
788 .unwrap();
789 let result = compute_frame_ssimulacra2(source_data, distorted_data).unwrap();
790 let expected = 17.398_505_f64;
791 assert!(
792 (result - expected).abs() < 0.25f64,
793 "Result {result:.6} not equal to expected {expected:.6}",
794 );
795 }
796
797 #[test]
798 fn test_xyb_simd_vs_yuvxyb() {
799 use yuvxyb::{ColorPrimaries, TransferCharacteristic};
800
801 let source = image::open(
802 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
803 .join("test_data")
804 .join("tank_source.png"),
805 )
806 .unwrap();
807
808 let source_data: Vec<[f32; 3]> = source
809 .to_rgb32f()
810 .chunks_exact(3)
811 .map(|chunk| [chunk[0], chunk[1], chunk[2]])
812 .collect();
813
814 let width = source.width() as usize;
815 let height = source.height() as usize;
816
817 let rgb_for_yuvxyb = Rgb::new(
818 source_data.clone(),
819 width,
820 height,
821 TransferCharacteristic::SRGB,
822 ColorPrimaries::BT709,
823 )
824 .unwrap();
825 let lrgb_for_yuvxyb = yuvxyb::LinearRgb::try_from(rgb_for_yuvxyb).unwrap();
826 let xyb_yuvxyb = yuvxyb::Xyb::from(lrgb_for_yuvxyb);
827
828 let rgb_for_simd = Rgb::new(
829 source_data,
830 width,
831 height,
832 TransferCharacteristic::SRGB,
833 ColorPrimaries::BT709,
834 )
835 .unwrap();
836 let lrgb_for_simd = LinearRgb::try_from(rgb_for_simd).unwrap();
837 let xyb_simd = linear_rgb_to_xyb_simd(lrgb_for_simd);
838
839 let mut max_diff = [0.0f32; 3];
840 for (yuvxyb_pix, simd_pix) in xyb_yuvxyb.data().iter().zip(xyb_simd.data().iter()) {
841 for c in 0..3 {
842 let diff = (yuvxyb_pix[c] - simd_pix[c]).abs();
843 max_diff[c] = max_diff[c].max(diff);
844 }
845 }
846
847 assert!(
848 max_diff[0] < 1e-5 && max_diff[1] < 1e-5 && max_diff[2] < 1e-5,
849 "SIMD XYB differs from yuvxyb: max_diff={:?}",
850 max_diff
851 );
852 }
853}