1mod color;
36mod convolve;
37mod error;
38mod gaussian;
39mod grayscale;
40mod hitchhikers;
41mod map;
42mod multiscale;
43#[cfg(feature = "napi")]
44mod napi;
45mod perceptual;
46#[cfg(feature = "python")]
47mod python;
48mod simd;
49mod standard;
50mod stats;
51
52pub use error::SsimError;
53pub use hitchhikers::{hitchhikers_ssim, HitchhikersOptions};
54pub use map::render_map;
55pub use multiscale::{ms_ssim, MsSsimMethod, MsSsimOptions, DEFAULT_WEIGHTS};
56pub use perceptual::{perceptual_ssim, ColorSpace, PerceptualOptions, Pooling};
57pub use standard::ssim;
58
59#[derive(Clone, Copy, Debug)]
65pub struct Rgba8<'a> {
66 pub data: &'a [u8],
67 pub width: usize,
68 pub height: usize,
69}
70
71impl<'a> Rgba8<'a> {
72 pub fn new(data: &'a [u8], width: usize, height: usize) -> Self {
73 Self {
74 data,
75 width,
76 height,
77 }
78 }
79
80 fn validate(&self) -> Result<usize, SsimError> {
82 let pixels = self.width * self.height;
83 if self.data.len() < pixels * 4 {
84 return Err(SsimError::Options(format!(
85 "image data is {} bytes, need {} for {}x{} RGBA",
86 self.data.len(),
87 pixels * 4,
88 self.width,
89 self.height
90 )));
91 }
92 Ok(pixels)
93 }
94}
95
96#[derive(Clone, Debug)]
98pub struct Plane {
99 pub samples: Vec<f32>,
100 pub width: usize,
101 pub height: usize,
102}
103
104impl Plane {
105 pub fn from_rgba8(image: Rgba8<'_>) -> Result<Self, SsimError> {
107 let pixels = image.validate()?;
108 Ok(Self {
109 samples: grayscale::to_luma(&image.data[..pixels * 4]),
110 width: image.width,
111 height: image.height,
112 })
113 }
114
115 fn validate_pair(image1: &Plane, image2: &Plane) -> Result<(), SsimError> {
121 if image1.width != image2.width || image1.height != image2.height {
122 return Err(SsimError::SizeMismatch {
123 img1_width: image1.width as u32,
124 img1_height: image1.height as u32,
125 img2_width: image2.width as u32,
126 img2_height: image2.height as u32,
127 });
128 }
129 let expected = image1.width * image1.height;
130 if image1.samples.len() < expected || image2.samples.len() < expected {
131 return Err(SsimError::Options(format!(
132 "plane samples are shorter than {}x{}",
133 image1.width, image1.height
134 )));
135 }
136 Ok(())
137 }
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct SsimOptions {
143 pub window_size: usize,
145 pub k1: f64,
147 pub k2: f64,
149 pub bit_depth: u32,
152}
153
154impl Default for SsimOptions {
155 fn default() -> Self {
156 Self {
157 window_size: 11,
158 k1: 0.01,
159 k2: 0.03,
160 bit_depth: 8,
161 }
162 }
163}
164
165impl SsimOptions {
166 #[inline]
168 pub fn dynamic_range(&self) -> f64 {
169 2f64.powi(self.bit_depth as i32) - 1.0
170 }
171}
172
173#[derive(Clone, Debug)]
175pub struct SsimOutcome {
176 pub score: f64,
178 pub map: Vec<f32>,
180 pub map_width: usize,
181 pub map_height: usize,
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn rgba(width: usize, height: usize, value: u8) -> Vec<u8> {
189 let mut data = vec![0u8; width * height * 4];
190 for pixel in data.chunks_exact_mut(4) {
191 pixel.copy_from_slice(&[value, value, value, 255]);
192 }
193 data
194 }
195
196 #[test]
197 fn a_plane_takes_its_dimensions_from_the_view() {
198 let data = rgba(4, 3, 128);
199 let plane = Plane::from_rgba8(Rgba8::new(&data, 4, 3)).unwrap();
200 assert_eq!((plane.width, plane.height), (4, 3));
201 assert_eq!(plane.samples.len(), 12);
202 }
203
204 #[test]
205 fn a_short_buffer_is_rejected() {
206 let data = rgba(4, 3, 128);
207 assert!(matches!(
208 Plane::from_rgba8(Rgba8::new(&data, 8, 3)),
209 Err(SsimError::Options(_))
210 ));
211 }
212
213 #[test]
214 fn mismatched_planes_are_rejected() {
215 let plane = |width, height| Plane {
216 samples: vec![0.0; width * height],
217 width,
218 height,
219 };
220 assert!(matches!(
221 Plane::validate_pair(&plane(4, 4), &plane(2, 4)),
222 Err(SsimError::SizeMismatch { .. })
223 ));
224 }
225
226 #[test]
227 fn a_truncated_plane_is_rejected() {
228 let full = Plane {
229 samples: vec![0.0; 16],
230 width: 4,
231 height: 4,
232 };
233 let short = Plane {
234 samples: vec![0.0; 4],
235 width: 4,
236 height: 4,
237 };
238 assert!(matches!(
239 Plane::validate_pair(&full, &short),
240 Err(SsimError::Options(_))
241 ));
242 }
243
244 #[test]
245 fn the_dynamic_range_follows_the_bit_depth() {
246 assert_eq!(SsimOptions::default().dynamic_range(), 255.0);
247 assert_eq!(
248 SsimOptions {
249 bit_depth: 16,
250 ..Default::default()
251 }
252 .dynamic_range(),
253 65535.0
254 );
255 }
256}