1#[derive(Clone, Debug)]
25pub struct LinearRgbImage {
26 pub(crate) data: Vec<[f32; 3]>,
27 pub(crate) width: usize,
28 pub(crate) height: usize,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
33pub enum LinearRgbImageError {
34 #[error("LinearRgbImage dimensions must be nonzero")]
36 ZeroDimension,
37 #[error("LinearRgbImage dimensions overflow usize")]
39 DimensionOverflow,
40 #[error("LinearRgbImage data length {actual} does not match width * height = {expected}")]
42 DataLengthMismatch {
43 expected: usize,
45 actual: usize,
47 },
48}
49
50impl LinearRgbImage {
51 pub fn new(data: Vec<[f32; 3]>, width: usize, height: usize) -> Self {
59 Self::try_new(data, width, height)
60 .expect("LinearRgbImage::new: invalid dimensions or data length")
61 }
62
63 pub fn try_new(
68 data: Vec<[f32; 3]>,
69 width: usize,
70 height: usize,
71 ) -> Result<Self, LinearRgbImageError> {
72 if width == 0 || height == 0 {
73 return Err(LinearRgbImageError::ZeroDimension);
74 }
75 let expected = width
76 .checked_mul(height)
77 .ok_or(LinearRgbImageError::DimensionOverflow)?;
78 if data.len() != expected {
79 return Err(LinearRgbImageError::DataLengthMismatch {
80 expected,
81 actual: data.len(),
82 });
83 }
84 Ok(Self {
85 data,
86 width,
87 height,
88 })
89 }
90
91 pub fn width(&self) -> usize {
93 self.width
94 }
95
96 pub fn height(&self) -> usize {
98 self.height
99 }
100
101 pub fn data(&self) -> &[[f32; 3]] {
103 &self.data
104 }
105
106 pub fn data_mut(&mut self) -> &mut [[f32; 3]] {
108 &mut self.data
109 }
110}
111
112pub trait ToLinearRgb {
119 fn to_linear_rgb(&self) -> LinearRgbImage;
121
122 fn into_linear_rgb(self) -> LinearRgbImage
128 where
129 Self: Sized,
130 {
131 self.to_linear_rgb()
132 }
133}
134
135impl ToLinearRgb for LinearRgbImage {
137 fn to_linear_rgb(&self) -> LinearRgbImage {
138 self.clone()
139 }
140
141 fn into_linear_rgb(self) -> LinearRgbImage {
142 self
143 }
144}
145
146#[inline]
157pub fn srgb_to_linear(s: f32) -> f32 {
158 const THRESH: f32 = 0.04045;
159 const LOW_DIV_INV: f32 = 1.0 / 12.92;
160
161 const P: [f32; 5] = [
163 2.200_248_3e-4,
164 1.043_637_6e-2,
165 1.624_820_4e-1,
166 7.961_565e-1,
167 8.210_153e-1,
168 ];
169 const Q: [f32; 5] = [
170 2.631_847e-1,
171 1.076_976_5,
172 4.987_528_3e-1,
173 -5.512_498_3e-2,
174 6.521_209e-3,
175 ];
176
177 let x = s.abs();
178 if x <= THRESH {
179 x * LOW_DIV_INV
180 } else {
181 let num = P[4]
183 .mul_add(x, P[3])
184 .mul_add(x, P[2])
185 .mul_add(x, P[1])
186 .mul_add(x, P[0]);
187 let den = Q[4]
188 .mul_add(x, Q[3])
189 .mul_add(x, Q[2])
190 .mul_add(x, Q[1])
191 .mul_add(x, Q[0]);
192 num / den
193 }
194}
195
196#[inline]
198pub fn srgb_u8_to_linear(v: u8) -> f32 {
199 SRGB_TO_LINEAR_LUT[v as usize]
201}
202
203#[inline]
205pub fn srgb_u16_to_linear(v: u16) -> f32 {
206 srgb_to_linear(v as f32 / 65535.0)
207}
208
209static SRGB_TO_LINEAR_LUT: std::sync::LazyLock<[f32; 256]> = std::sync::LazyLock::new(|| {
212 let mut lut = [0.0f32; 256];
213 for (i, entry) in lut.iter_mut().enumerate() {
214 *entry = srgb_to_linear(i as f32 / 255.0);
215 }
216 lut
217});
218
219#[cfg(feature = "imgref")]
224mod imgref_impl {
225 use super::*;
226 use imgref::ImgRef;
227
228 impl ToLinearRgb for ImgRef<'_, [u8; 3]> {
230 fn to_linear_rgb(&self) -> LinearRgbImage {
231 let data: Vec<[f32; 3]> = self
232 .pixels()
233 .map(|[r, g, b]| {
234 [
235 srgb_u8_to_linear(r),
236 srgb_u8_to_linear(g),
237 srgb_u8_to_linear(b),
238 ]
239 })
240 .collect();
241 LinearRgbImage::new(data, self.width(), self.height())
242 }
243 }
244
245 impl ToLinearRgb for ImgRef<'_, [u16; 3]> {
247 fn to_linear_rgb(&self) -> LinearRgbImage {
248 let data: Vec<[f32; 3]> = self
249 .pixels()
250 .map(|[r, g, b]| {
251 [
252 srgb_u16_to_linear(r),
253 srgb_u16_to_linear(g),
254 srgb_u16_to_linear(b),
255 ]
256 })
257 .collect();
258 LinearRgbImage::new(data, self.width(), self.height())
259 }
260 }
261
262 impl ToLinearRgb for ImgRef<'_, [f32; 3]> {
264 fn to_linear_rgb(&self) -> LinearRgbImage {
265 let data: Vec<[f32; 3]> = self.pixels().collect();
266 LinearRgbImage::new(data, self.width(), self.height())
267 }
268 }
269
270 impl ToLinearRgb for ImgRef<'_, u8> {
272 fn to_linear_rgb(&self) -> LinearRgbImage {
273 let data: Vec<[f32; 3]> = self
274 .pixels()
275 .map(|v| {
276 let l = srgb_u8_to_linear(v);
277 [l, l, l]
278 })
279 .collect();
280 LinearRgbImage::new(data, self.width(), self.height())
281 }
282 }
283
284 impl ToLinearRgb for ImgRef<'_, f32> {
286 fn to_linear_rgb(&self) -> LinearRgbImage {
287 let data: Vec<[f32; 3]> = self.pixels().map(|v| [v, v, v]).collect();
288 LinearRgbImage::new(data, self.width(), self.height())
289 }
290 }
291}
292
293impl ToLinearRgb for yuvxyb::LinearRgb {
298 fn to_linear_rgb(&self) -> LinearRgbImage {
299 LinearRgbImage::new(
300 self.data().to_vec(),
301 self.width().get(),
302 self.height().get(),
303 )
304 }
305
306 fn into_linear_rgb(self) -> LinearRgbImage {
307 let width = self.width().get();
308 let height = self.height().get();
309 LinearRgbImage::new(self.into_data(), width, height)
310 }
311}
312
313impl From<LinearRgbImage> for yuvxyb::LinearRgb {
318 fn from(img: LinearRgbImage) -> Self {
319 use std::num::NonZeroUsize;
320 let width = NonZeroUsize::new(img.width)
326 .expect("LinearRgbImage width is nonzero (try_new invariant)");
327 let height = NonZeroUsize::new(img.height)
328 .expect("LinearRgbImage height is nonzero (try_new invariant)");
329 assert_eq!(
330 img.data.len(),
331 width.get().saturating_mul(height.get()),
332 "LinearRgbImage data length must equal width * height (try_new invariant)"
333 );
334 yuvxyb::LinearRgb::new(img.data, width, height)
335 .expect("LinearRgbImage dimensions are valid (try_new invariant)")
336 }
337}
338
339impl ToLinearRgb for yuvxyb::Rgb {
340 fn to_linear_rgb(&self) -> LinearRgbImage {
341 if self.transfer() == yuvxyb::TransferCharacteristic::SRGB {
342 let data: Vec<[f32; 3]> = self
345 .data()
346 .iter()
347 .map(|&[r, g, b]| [srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)])
348 .collect();
349 LinearRgbImage::new(data, self.width().get(), self.height().get())
350 } else {
351 let linear: yuvxyb::LinearRgb = yuvxyb::LinearRgb::try_from(self.clone())
353 .expect("Rgb to LinearRgb conversion should not fail");
354 linear.to_linear_rgb()
355 }
356 }
357
358 fn into_linear_rgb(self) -> LinearRgbImage {
359 let width = self.width().get();
360 let height = self.height().get();
361 if self.transfer() == yuvxyb::TransferCharacteristic::SRGB {
362 let mut data = self.into_data();
364 for pixel in &mut data {
365 pixel[0] = srgb_to_linear(pixel[0]);
366 pixel[1] = srgb_to_linear(pixel[1]);
367 pixel[2] = srgb_to_linear(pixel[2]);
368 }
369 LinearRgbImage::new(data, width, height)
370 } else {
371 let linear: yuvxyb::LinearRgb = yuvxyb::LinearRgb::try_from(self)
372 .expect("Rgb to LinearRgb conversion should not fail");
373 linear.into_linear_rgb()
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_srgb_to_linear_bounds() {
384 assert!((srgb_to_linear(0.0) - 0.0).abs() < 1e-6);
385 assert!((srgb_to_linear(1.0) - 1.0).abs() < 1e-6);
386 }
387
388 #[test]
389 fn test_srgb_to_linear_midpoint() {
390 let linear = srgb_to_linear(0.5);
392 assert!((linear - 0.214).abs() < 0.01);
393 }
394
395 #[test]
396 fn test_srgb_u8_to_linear() {
397 assert!((srgb_u8_to_linear(0) - 0.0).abs() < 1e-6);
398 assert!((srgb_u8_to_linear(255) - 1.0).abs() < 1e-6);
399 }
400
401 #[test]
402 fn test_linear_rgb_image_accessors() {
403 let data = vec![[0.5, 0.3, 0.1], [0.2, 0.4, 0.6]];
404 let img = LinearRgbImage::new(data.clone(), 2, 1);
405
406 assert_eq!(img.width(), 2);
407 assert_eq!(img.height(), 1);
408 assert_eq!(img.data(), &data[..]);
409 }
410
411 #[test]
412 fn test_try_new_rejects_zero_dimension() {
413 let err = LinearRgbImage::try_new(vec![], 0, 4).unwrap_err();
414 assert_eq!(err, LinearRgbImageError::ZeroDimension);
415 let err = LinearRgbImage::try_new(vec![], 4, 0).unwrap_err();
416 assert_eq!(err, LinearRgbImageError::ZeroDimension);
417 }
418
419 #[test]
420 fn test_try_new_rejects_dimension_overflow() {
421 let err = LinearRgbImage::try_new(vec![], usize::MAX, 2).unwrap_err();
423 assert_eq!(err, LinearRgbImageError::DimensionOverflow);
424 }
425
426 #[test]
427 fn test_try_new_rejects_data_length_mismatch() {
428 let err = LinearRgbImage::try_new(vec![[0.0; 3]; 3], 2, 2).unwrap_err();
429 assert!(matches!(
430 err,
431 LinearRgbImageError::DataLengthMismatch {
432 expected: 4,
433 actual: 3
434 }
435 ));
436 }
437
438 #[test]
439 fn test_try_new_accepts_valid_input() {
440 let img = LinearRgbImage::try_new(vec![[0.5, 0.3, 0.1]; 6], 3, 2).unwrap();
441 assert_eq!(img.width(), 3);
442 assert_eq!(img.height(), 2);
443 }
444
445 #[test]
446 #[should_panic(expected = "LinearRgbImage::new: invalid dimensions or data length")]
447 fn test_new_panics_on_zero_dimension_in_release() {
448 let _ = LinearRgbImage::new(vec![], 0, 4);
453 }
454
455 #[test]
456 fn test_yuvxyb_linearrgb_roundtrip() {
457 use std::num::NonZeroUsize;
458 let data = vec![[0.5, 0.3, 0.1]; 4];
459 let yuvxyb_img = yuvxyb::LinearRgb::new(
460 data.clone(),
461 NonZeroUsize::new(2).unwrap(),
462 NonZeroUsize::new(2).unwrap(),
463 )
464 .expect("valid dimensions");
465
466 let our_img = yuvxyb_img.to_linear_rgb();
467 assert_eq!(our_img.width(), 2);
468 assert_eq!(our_img.height(), 2);
469 assert_eq!(our_img.data(), &data[..]);
470
471 let back: yuvxyb::LinearRgb = our_img.into();
473 assert_eq!(back.data(), &data[..]);
474 }
475}
476
477#[cfg(all(test, feature = "imgref"))]
478mod imgref_tests {
479 use super::*;
480 use imgref::{Img, ImgVec};
481
482 #[test]
483 fn test_imgref_u8_srgb_conversion() {
484 let pixels: Vec<[u8; 3]> = vec![
486 [0, 0, 0], [255, 255, 255], [128, 128, 128], [255, 0, 0], ];
491 let img: ImgVec<[u8; 3]> = Img::new(pixels, 2, 2);
492
493 let linear = img.as_ref().to_linear_rgb();
494 assert_eq!(linear.width(), 2);
495 assert_eq!(linear.height(), 2);
496
497 assert!((linear.data()[0][0] - 0.0).abs() < 1e-6);
499 assert!((linear.data()[1][0] - 1.0).abs() < 1e-6);
501 assert!((linear.data()[1][1] - 1.0).abs() < 1e-6);
502 assert!((linear.data()[2][0] - 0.215).abs() < 0.01);
504 assert!((linear.data()[3][0] - 1.0).abs() < 1e-6);
506 assert!((linear.data()[3][1] - 0.0).abs() < 1e-6);
507 }
508
509 #[test]
510 fn test_imgref_f32_passthrough() {
511 let pixels: Vec<[f32; 3]> = vec![[0.5, 0.3, 0.1], [0.9, 0.8, 0.7]];
513 let img: ImgVec<[f32; 3]> = Img::new(pixels.clone(), 2, 1);
514
515 let linear = img.as_ref().to_linear_rgb();
516 assert_eq!(linear.data(), &pixels[..]);
517 }
518
519 #[test]
520 fn test_imgref_grayscale_u8_expansion() {
521 let pixels: Vec<u8> = vec![0, 255, 128];
523 let img: ImgVec<u8> = Img::new(pixels, 3, 1);
524
525 let linear = img.as_ref().to_linear_rgb();
526
527 let black = linear.data()[0];
529 assert!((black[0] - 0.0).abs() < 1e-6);
530 assert_eq!(black[0], black[1]);
531 assert_eq!(black[1], black[2]);
532
533 let white = linear.data()[1];
535 assert!((white[0] - 1.0).abs() < 1e-6);
536 assert_eq!(white[0], white[1]);
537
538 let gray = linear.data()[2];
540 assert!((gray[0] - 0.215).abs() < 0.01);
541 assert_eq!(gray[0], gray[1]);
542 }
543
544 #[test]
545 fn test_imgref_grayscale_f32_expansion() {
546 let pixels: Vec<f32> = vec![0.0, 1.0, 0.5];
548 let img: ImgVec<f32> = Img::new(pixels, 3, 1);
549
550 let linear = img.as_ref().to_linear_rgb();
551
552 assert_eq!(linear.data()[0], [0.0, 0.0, 0.0]);
553 assert_eq!(linear.data()[1], [1.0, 1.0, 1.0]);
554 assert_eq!(linear.data()[2], [0.5, 0.5, 0.5]);
555 }
556
557 #[test]
558 fn test_compute_ssimulacra2_with_imgref_u8() {
559 use crate::compute_ssimulacra2;
560
561 let pixels1: Vec<[u8; 3]> = vec![[128, 128, 128]; 16 * 16];
563 let pixels2: Vec<[u8; 3]> = vec![[130, 130, 130]; 16 * 16]; let img1: ImgVec<[u8; 3]> = Img::new(pixels1, 16, 16);
566 let img2: ImgVec<[u8; 3]> = Img::new(pixels2, 16, 16);
567
568 let score = compute_ssimulacra2(img1.as_ref(), img2.as_ref()).unwrap();
570 assert!(
572 score > 90.0,
573 "Score {score} should be > 90 for very similar images"
574 );
575 }
576
577 #[test]
578 fn test_compute_ssimulacra2_identical_imgref() {
579 use crate::compute_ssimulacra2;
580
581 let pixels: Vec<[u8; 3]> = vec![[100, 150, 200]; 16 * 16];
583 let img: ImgVec<[u8; 3]> = Img::new(pixels, 16, 16);
584
585 let score = compute_ssimulacra2(img.as_ref(), img.as_ref()).unwrap();
586 assert!(
587 (score - 100.0).abs() < 0.01,
588 "Identical images should score 100, got {score}"
589 );
590 }
591
592 #[test]
593 fn test_precompute_with_imgref() {
594 use crate::Ssimulacra2Reference;
595
596 let source_pixels: Vec<[u8; 3]> = vec![[128, 128, 128]; 32 * 32];
598 let distorted_pixels: Vec<[u8; 3]> = vec![[130, 128, 126]; 32 * 32];
599
600 let source: ImgVec<[u8; 3]> = Img::new(source_pixels, 32, 32);
601 let distorted: ImgVec<[u8; 3]> = Img::new(distorted_pixels, 32, 32);
602
603 let reference = Ssimulacra2Reference::new(source.as_ref()).unwrap();
605 let score = reference.compare(distorted.as_ref()).unwrap();
606
607 assert!(
609 score > 80.0,
610 "Score {score} should be > 80 for similar images"
611 );
612 }
613}