1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
//! Image quality metrics: how far one image is from another.
//!
//! Every function here takes **two images of the same pixel type** and
//! reports how much they differ. That is the one thing the rest of
//! [`analyze`](crate::analyze) cannot do: a histogram, a statistic and a
//! moment each describe a single image, so none of them can answer "did this
//! pipeline change the picture, and by how much".
//!
//! | Question | Use | Output |
//! |---|---|---|
//! | "How far apart are these pixel values?" | [`squared_error`] | [`SquaredError`], per channel and pooled |
//! | "What is the MSE / RMSE / PSNR?" | accessors on [`ChannelSquaredError`] | [`f64`], or [`None`] with no samples |
//! | "Do they *look* alike, structurally?" | [`ssim`] / [`ssim_map`] | One score, or a per-position map |
//!
//! # Why both images must be the same pixel type
//!
//! An error between a `Mono8` and a `MonoF32` needs a range convention — is
//! `255` the same brightness as `1.0`? — and this crate deliberately does not
//! have one: float pixels carry no intrinsic full scale. So the pixel type is
//! unified in the signature and the *conversion* is the caller's, made by name
//! through [`convert_image`](crate::transform::convert_image). The two *image*
//! types stay free, so comparing an [`Image`](crate::image::Image) against a
//! region of view of another image works.
//!
//! # The full-scale value is a parameter, sometimes a type-level one
//!
//! PSNR and SSIM both need to know what "full scale" means: PSNR divides by
//! it, SSIM's stabilizing constants are fractions of it. For integer pixels
//! the pixel type knows the answer, so [`PeakValue::of_pixel`] reads it off
//! the type, and reads `1023`, not `65535`, for `Mono<10>`. Float pixels have
//! no intrinsic full scale in this crate, so they do not implement
//! [`WhiteChannel`] and cannot use that
//! constructor; a float caller names the peak with [`peak!`](crate::peak) and owns
//! the assumption.
//!
//! # Example
//!
//! ```
//! use fovea::analyze::quality::{PeakValue, SsimParams, squared_error, ssim};
//! use fovea::image::{Image, ImageViewMut};
//! use fovea::pixel::Mono8;
//!
//! // A reference frame and the same frame with one pixel knocked out.
//! let reference = Image::generate(32, 32, |x, y| Mono8::new(((x * 8) ^ (y * 4)) as u8));
//! let mut degraded = reference.clone();
//! *degraded.pixel_at_mut(16, 16) = Mono8::new(0);
//!
//! let peak = PeakValue::of_pixel::<Mono8>();
//! assert_eq!(peak.get(), 255.0);
//!
//! // One pixel of 1024 dropped by 192 levels: MSE = 192² / 1024 = 36, so
//! // PSNR = 10·log10(255² / 36) ≈ 32.6 dB.
//! let error = squared_error(&reference, °raded)?.pooled();
//! assert_eq!(error.count, 32 * 32);
//! assert_eq!(error.mean_squared_error(), Some(36.0));
//! assert_eq!(error.max_absolute_error(), Some(192.0));
//! assert!(error.peak_signal_to_noise_ratio(peak).unwrap() > 32.0);
//!
//! // Structurally the two are still nearly the same image.
//! let score = ssim(&reference, °raded, SsimParams::reference(peak))?;
//! assert!(score > 0.9, "{score}");
//!
//! // An image compared against itself is exactly 1.0, and has no error.
//! assert_eq!(ssim(&reference, &reference, SsimParams::reference(peak))?, 1.0);
//! assert_eq!(
//! squared_error(&reference, &reference)?.pooled().mean_squared_error(),
//! Some(0.0),
//! );
//! # Ok::<(), fovea::Error>(())
//! ```
pub use ;
pub use ;
use crateError;
use crateStatisticsChannel;
use crateWhiteChannel;
/// The value a quality metric treats as full scale: finite and strictly
/// positive.
///
/// PSNR's numerator and SSIM's `C1` / `C2` are both expressed in units of the
/// signal's dynamic range, conventionally written `L`. `PeakValue` is that
/// number as an invariant-carrying parameter type, the same discipline as
/// [`Sigma`](crate::Sigma) and [`Tolerance`](crate::Tolerance): validation
/// happens once, where the value is born, and every metric taking a
/// `PeakValue` is total in it.
///
/// Three ways in, in decreasing order of how much the type system helps:
///
/// - [`of_pixel`](Self::of_pixel) reads the pixel type's own saturated value.
/// Correct by construction, and correct for reduced-range pixels: `Mono<10>`
/// reports `1023`.
/// - [`peak!`](crate::peak) takes a literal and checks it at compile time.
/// - [`try_new`](Self::try_new) takes a computed value and reports
/// [`Error::InvalidParameter`].
///
/// [`new`](Self::new) is the checked `const fn` under the macro; it returns
/// [`Option`], so reaching for it by name cannot abort.
///
/// # This is a range, not a maximum sample
///
/// `L` is the dynamic range of the *representation*, not the largest value
/// either image happens to contain. An 8-bit frame whose brightest pixel is
/// 100 still has `L = 255`, and passing `100` would report a PSNR about 8 dB
/// better than every other library would. If you genuinely want the
/// data-derived range, that is
/// [`ChannelStatistics::max`](crate::analyze::statistics::ChannelStatistics::max)
/// minus `min`, and it is a different, non-comparable figure.
///
/// # Example
///
/// ```
/// use fovea::analyze::quality::PeakValue;
/// use fovea::pixel::{Mono, Mono8, Mono16};
///
/// // Read off the pixel type, including the reduced-range families.
/// assert_eq!(PeakValue::of_pixel::<Mono8>().get(), 255.0);
/// assert_eq!(PeakValue::of_pixel::<Mono16>().get(), 65_535.0);
/// assert_eq!(PeakValue::of_pixel::<Mono<10>>().get(), 1023.0);
///
/// // Float pixels have no intrinsic full scale, so the caller names it.
/// use fovea::peak;
/// const UNIT: PeakValue = peak!(1.0);
/// assert_eq!(UNIT.get(), 1.0);
/// ```
///
/// A float pixel type is rejected at compile time rather than guessed:
///
/// ```compile_fail
/// use fovea::analyze::quality::PeakValue;
/// use fovea::pixel::MonoF32;
///
/// // ERROR: `MonoF32: WhiteChannel` is not satisfied.
/// let _peak = PeakValue::of_pixel::<MonoF32>();
/// ```
;
/// A [`PeakValue`](crate::analyze::quality::PeakValue) literal, checked at
/// compile time.
///
/// For integer pixel types prefer
/// [`PeakValue::of_pixel`](crate::analyze::quality::PeakValue::of_pixel),
/// which reads the full scale off the type and cannot disagree with it. This
/// macro is for the float case, where the range is a call-site convention and
/// somebody has to name it.
///
/// The [`sigma!`](crate::sigma) macro's counterpart for full-scale values; see
/// it for why this is a macro and not a `const fn`. A value that is not a
/// constant expression does not compile (`error[E0435]`); use
/// [`PeakValue::try_new`](crate::analyze::quality::PeakValue::try_new) there.
///
/// # Example
///
/// ```
/// use fovea::analyze::quality::PeakValue;
/// use fovea::peak;
///
/// const UNIT: PeakValue = peak!(1.0); // the usual float convention
/// assert_eq!(UNIT.get(), 1.0);
/// ```
///
/// A full scale of zero makes every metric degenerate, so it does not build:
///
/// ```compile_fail
/// use fovea::peak;
/// // ERROR: evaluation panicked: peak value must be finite and strictly
/// // positive
/// let _ = peak!(0.0);
/// ```