nv_flip/
lib.rs

1//! bindings to Nvidia Labs's [ꟻLIP] image comparison and error visualization library.
2//!
3//! This library allows you to visualize and reason about the human-noticable differences
4//! between rendered images. Especially when comparing images that are noisy or other small
5//! differences, FLIP's comparison can be more meaningful than a simple pixel-wise comparison.
6//!
7//! ![comp](https://raw.githubusercontent.com/NVlabs/flip/main/images/teaser.png)
8//!
9//! In order to keep a small dependency closure, this crate does not depend on `image`,
10//! but interop is simple.
11//!
12//! # Example
13//!
14//! ```rust
15//! // First we load the "reference image". This is the image we want to compare against.
16//! //
17//! // We make sure to turn the image into RGB8 as FLIP doesn't deal with alpha.
18//! let ref_image_data = image::open("../etc/tree-ref.png").unwrap().into_rgb8();
19//! let ref_image = nv_flip::FlipImageRgb8::with_data(
20//!     ref_image_data.width(),
21//!     ref_image_data.height(),
22//!     &ref_image_data
23//! );
24//!
25//! // We then load the "test image". This is the image we want to compare to the reference.
26//! let test_image_data = image::open("../etc/tree-test.png").unwrap().into_rgb8();
27//! let test_image = nv_flip::FlipImageRgb8::with_data(
28//!     test_image_data.width(),
29//!     test_image_data.height(),
30//!     &test_image_data
31//! );
32//!
33//! // We now run the comparison. This will produce a "error map" that that is the per-pixel
34//! // visual difference between the two images between 0 and 1.
35//! //
36//! // The last parameter is the number of pixels per degree of visual angle. This is used
37//! // to determine the size of imperfections that can be seen. See the `pixels_per_degree`
38//! // for more information. By default this value is 67.0.
39//! let error_map = nv_flip::flip(ref_image, test_image, nv_flip::DEFAULT_PIXELS_PER_DEGREE);
40//!
41//! // We can now visualize the error map using a LUT that maps the error value to a color.
42//! let visualized = error_map.apply_color_lut(&nv_flip::magma_lut());
43//!
44//! // Finally we can the final image into an `image` crate image and save it.
45//! let image = image::RgbImage::from_raw(
46//!     visualized.width(),
47//!     visualized.height(),
48//!     visualized.to_vec()
49//! ).unwrap();
50//! # let _ = image;
51//!
52//! // We can get statistics about the error map by using their "Pool" type,
53//! // which is essentially a weighted histogram.
54//! let mut pool = nv_flip::FlipPool::from_image(&error_map);
55//!
56//! // These are the same statistics shown by the command line.
57//! //
58//! // The paper's writers recommend that, if you are to use a single number to
59//! // represent the error, they recommend the mean.
60//! println!("Mean: {}", pool.mean());
61//! println!("Weighted median: {}", pool.get_percentile(0.5, true));
62//! println!("1st weighted quartile: {}", pool.get_percentile(0.25, true));
63//! println!("3rd weighted quartile: {}", pool.get_percentile(0.75, true));
64//! println!("Min: {}", pool.min_value());
65//! println!("Max: {}", pool.max_value());
66//! ```
67//! The result of this example looks like this:
68//!
69//! <!-- This table uses U+2800 BRAILLE PATTERN BLANK in the header make the images vaguely the same size. -->
70//!
71//! | Reference | ⠀⠀Test⠀⠀ | ⠀Result⠀ |
72//! |:---------:|:---------:|:---------:|
73//! | ![comp](https://raw.githubusercontent.com/gfx-rs/nv-flip-rs/trunk/etc/tree-ref.png) | ![comp](https://raw.githubusercontent.com/gfx-rs/nv-flip-rs/trunk/etc/tree-test.png)  | ![comp](https://raw.githubusercontent.com/gfx-rs/nv-flip-rs/trunk/etc/tree-comparison-cli.png) |
74//!
75//! # License
76//!
77//! The binding and rust interop code is tri-licensed under MIT, Apache-2.0, and ZLib.
78//!
79//! The ꟻLIP library itself is licensed under the BSD-3-Clause license.
80//!
81//! The example images used are licensed under the [Unsplash License].
82//!
83//! [ꟻLIP]: https://github.com/NVlabs/flip
84//! [Unsplash License]: https://unsplash.com/license
85
86use std::marker::PhantomData;
87
88pub use nv_flip_sys::{pixels_per_degree, DEFAULT_PIXELS_PER_DEGREE};
89
90/// 2D FLIP image that is accessed as Rgb8.
91///
92/// Internally this is Rgb32f, but the values are converted when read.
93pub struct FlipImageRgb8 {
94    inner: *mut nv_flip_sys::FlipImageColor3,
95    width: u32,
96    height: u32,
97}
98
99unsafe impl Send for FlipImageRgb8 {}
100unsafe impl Sync for FlipImageRgb8 {}
101
102impl Clone for FlipImageRgb8 {
103    fn clone(&self) -> Self {
104        let inner = unsafe { nv_flip_sys::flip_image_color3_clone(self.inner) };
105        assert!(!inner.is_null());
106        Self {
107            inner,
108            width: self.width,
109            height: self.height,
110        }
111    }
112}
113
114impl FlipImageRgb8 {
115    /// Create a new image with the given dimensions and zeroed contents.
116    pub fn new(width: u32, height: u32) -> Self {
117        let inner = unsafe { nv_flip_sys::flip_image_color3_new(width, height, std::ptr::null()) };
118        assert!(!inner.is_null());
119        Self {
120            inner,
121            width,
122            height,
123        }
124    }
125
126    /// Creates a new image with the given dimensions and copies the data into it.
127    ///
128    /// The data must be in Rgb8 format. Do not include alpha.
129    ///
130    /// Data is expected in row-major orderm from the top left, tightly packed.
131    ///
132    /// # Panics
133    ///
134    /// - If the data is not large enough to fill the image.
135    pub fn with_data(width: u32, height: u32, data: &[u8]) -> Self {
136        assert!(data.len() >= (width * height * 3) as usize);
137        let inner = unsafe { nv_flip_sys::flip_image_color3_new(width, height, data.as_ptr()) };
138        assert!(!inner.is_null());
139        Self {
140            inner,
141            width,
142            height,
143        }
144    }
145
146    /// Extracts the data from the image and returns it as a vector.
147    ///
148    /// Data is returned in row-major order, from the top left, tightly packed.
149    pub fn to_vec(&self) -> Vec<u8> {
150        let mut data = vec![0u8; (self.width * self.height * 3) as usize];
151        unsafe {
152            nv_flip_sys::flip_image_color3_get_data(self.inner, data.as_mut_ptr());
153        }
154        data
155    }
156
157    /// Returns the width of the image.
158    pub fn width(&self) -> u32 {
159        self.width
160    }
161
162    /// Returns the height of the image.
163    pub fn height(&self) -> u32 {
164        self.height
165    }
166}
167
168impl Drop for FlipImageRgb8 {
169    fn drop(&mut self) {
170        unsafe {
171            nv_flip_sys::flip_image_color3_free(self.inner);
172        }
173    }
174}
175
176/// 2D FLIP image that stores a single float per pixel.
177pub struct FlipImageFloat {
178    inner: *mut nv_flip_sys::FlipImageFloat,
179    width: u32,
180    height: u32,
181}
182
183unsafe impl Send for FlipImageFloat {}
184unsafe impl Sync for FlipImageFloat {}
185
186impl Clone for FlipImageFloat {
187    fn clone(&self) -> Self {
188        // SAFETY: The clone function does not mutate the image, despite taking a mutable pointer.
189        let inner = unsafe { nv_flip_sys::flip_image_float_clone(self.inner) };
190        assert!(!inner.is_null());
191        Self {
192            inner,
193            width: self.width,
194            height: self.height,
195        }
196    }
197}
198
199impl FlipImageFloat {
200    /// Create a new image with the given dimensions and zeroed contents.
201    pub fn new(width: u32, height: u32) -> Self {
202        let inner = unsafe { nv_flip_sys::flip_image_float_new(width, height, std::ptr::null()) };
203        assert!(!inner.is_null());
204        Self {
205            inner,
206            width,
207            height,
208        }
209    }
210
211    /// Creates a new image with the given dimensions and copies the data into it.
212    ///
213    /// Data is expected in row-major order, from the top left, tightly packed.
214    ///
215    /// # Panics
216    ///
217    /// - If the data is not large enough to fill the image.
218    pub fn with_data(width: u32, height: u32, data: &[f32]) -> Self {
219        assert!(data.len() >= (width * height) as usize);
220        let inner = unsafe { nv_flip_sys::flip_image_float_new(width, height, data.as_ptr()) };
221        assert!(!inner.is_null());
222        Self {
223            inner,
224            width,
225            height,
226        }
227    }
228
229    /// Applies the given 1D color lut to turn this single channel values into 3 channel values.
230    ///
231    /// Applies the following algorithm to each pixel:
232    ///
233    /// ```text
234    /// value_mapping[(pixel_value * 255).round() % value_mapping.width()]
235    /// ```
236    pub fn apply_color_lut(&self, value_mapping: &FlipImageRgb8) -> FlipImageRgb8 {
237        let output = FlipImageRgb8::new(self.width, self.height);
238        unsafe {
239            nv_flip_sys::flip_image_color3_color_map(output.inner, self.inner, value_mapping.inner);
240        }
241        output
242    }
243
244    /// Converts the image to a color image by copying the single channel value to all 3 channels.
245    pub fn to_color3(&self) -> FlipImageRgb8 {
246        let color3 = FlipImageRgb8::new(self.width, self.height);
247        unsafe {
248            nv_flip_sys::flip_image_float_copy_float_to_color3(self.inner, color3.inner);
249        }
250        color3
251    }
252
253    /// Extracts the data from the image and returns it as a vector.
254    ///
255    /// Data is returned in row-major order, from the top left, tightly packed.
256    pub fn to_vec(&self) -> Vec<f32> {
257        let mut data = vec![0f32; (self.width * self.height) as usize];
258        unsafe {
259            nv_flip_sys::flip_image_float_get_data(self.inner, data.as_mut_ptr());
260        }
261        data
262    }
263
264    /// Returns the width of the image.
265    pub fn width(&self) -> u32 {
266        self.width
267    }
268
269    /// Returns the height of the image.
270    pub fn height(&self) -> u32 {
271        self.height
272    }
273}
274
275impl Drop for FlipImageFloat {
276    fn drop(&mut self) {
277        unsafe {
278            nv_flip_sys::flip_image_float_free(self.inner);
279        }
280    }
281}
282
283/// Generates a 1D lut using the builtin magma colormap for mapping error values to colors.
284pub fn magma_lut() -> FlipImageRgb8 {
285    let inner = unsafe { nv_flip_sys::flip_image_color3_magma_map() };
286    assert!(!inner.is_null());
287    FlipImageRgb8 {
288        inner,
289        width: 256,
290        height: 1,
291    }
292}
293
294/// Performs a FLIP comparison between the two images.
295///
296/// The images must be the same size.
297///
298/// Images are assumed to be in the sRGB color space.
299///
300/// Returns an error map image, where each pixel represents the error between the two images
301/// at that location between 0 and 1.
302///
303/// The pixels_per_degree parameter is used to determine the sensitivity to differences. See the
304/// documentation for [`DEFAULT_PIXELS_PER_DEGREE`] and [`pixels_per_degree`] for more information.
305///
306/// Consumes both images as the algorithm uses them for scratch space. If you want to re-use
307/// the images, clone them while passing them in.
308///
309/// # Panics
310///
311/// - If the images are not the same size.
312pub fn flip(
313    reference_image: FlipImageRgb8,
314    test_image: FlipImageRgb8,
315    pixels_per_degree: f32,
316) -> FlipImageFloat {
317    assert_eq!(
318        reference_image.width(),
319        test_image.width(),
320        "Width mismatch between reference and test image"
321    );
322    assert_eq!(
323        reference_image.height(),
324        test_image.height(),
325        "Height mismatch between reference and test image"
326    );
327
328    let error_map = FlipImageFloat::new(reference_image.width(), reference_image.height());
329    unsafe {
330        nv_flip_sys::flip_image_float_flip(
331            error_map.inner,
332            reference_image.inner,
333            test_image.inner,
334            pixels_per_degree,
335        );
336    }
337    error_map
338}
339
340/// Bucket based histogram used internally by [`FlipPool`].
341///
342/// Generally you should not need to use this directly and any mutating
343/// operations are unsafe to prevent violating FlipPool invariants.
344pub struct FlipHistogram<'a> {
345    inner: *mut nv_flip_sys::FlipImageHistogramRef,
346    _phantom: PhantomData<&'a ()>,
347}
348
349unsafe impl Send for FlipHistogram<'_> {}
350unsafe impl Sync for FlipHistogram<'_> {}
351
352impl<'a> FlipHistogram<'a> {
353    /// Returns the difference between the maximum and minimum bucket values.
354    pub fn bucket_size(&self) -> usize {
355        unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_size(self.inner) }
356    }
357
358    /// Returns the index of the lowest bucket currently in use.
359    ///
360    /// If no buckets are in use, returns None.
361    pub fn bucket_id_min(&self) -> Option<usize> {
362        let value = unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_id_min(self.inner) };
363        if value == usize::MAX {
364            None
365        } else {
366            Some(value)
367        }
368    }
369
370    /// Returns the index of the highest bucket currently in use.
371    ///
372    /// If no buckets are in use, returns 0.
373    pub fn bucket_id_max(&self) -> usize {
374        unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_id_max(self.inner) }
375    }
376
377    /// Returns the amount of values contained within a given bucket.
378    ///
379    /// # Panics
380    ///
381    /// - If the bucket_id is out of bounds.
382    pub fn bucket_value_count(&self, bucket_id: usize) -> usize {
383        assert!(bucket_id < self.bucket_count());
384        unsafe { nv_flip_sys::flip_image_histogram_ref_get_bucket_value(self.inner, bucket_id) }
385    }
386
387    /// Returns the amount of buckets in the histogram.
388    pub fn bucket_count(&self) -> usize {
389        unsafe { nv_flip_sys::flip_image_histogram_ref_size(self.inner) }
390    }
391
392    /// Returns the smallest value the histogram can handle.
393    pub fn minimum_allowed_value(&self) -> f32 {
394        unsafe { nv_flip_sys::flip_image_histogram_ref_get_min_value(self.inner) }
395    }
396
397    /// Returns the largest value the histogram can handle.
398    pub fn maximum_allowed_value(&self) -> f32 {
399        unsafe { nv_flip_sys::flip_image_histogram_ref_get_max_value(self.inner) }
400    }
401
402    /// Clears the histogram of all values
403    ///
404    /// # Safety
405    ///
406    /// Due to the many invariants between the histogram and the pool,
407    /// we do not provide any safty guarentees when mutating the histogram.
408    pub unsafe fn clear(&mut self) {
409        unsafe {
410            nv_flip_sys::flip_image_histogram_ref_clear(self.inner);
411        }
412    }
413
414    /// Resizes the histogram to have `bucket_size` buckets.
415    ///
416    /// # Safety
417    ///
418    /// Due to the many invariants between the histogram and the pool,
419    /// we do not provide any safty guarentees when mutating the histogram.
420    pub unsafe fn resize(&mut self, bucket_size: usize) {
421        unsafe {
422            nv_flip_sys::flip_image_histogram_ref_resize(self.inner, bucket_size);
423        }
424    }
425
426    /// Returns which bucket a given value would fall into.
427    pub fn bucket_id(&self, value: f32) -> usize {
428        unsafe { nv_flip_sys::flip_image_histogram_ref_value_bucket_id(self.inner, value) }
429    }
430
431    /// Includes `count` instances of the following `value` in the histogram.
432    ///
433    /// # Safety
434    ///
435    /// Due to the many invariants between the histogram and the pool,
436    /// we do not provide any safty guarentees when mutating the histogram.
437    pub unsafe fn include_value(&mut self, value: f32, count: usize) {
438        unsafe {
439            nv_flip_sys::flip_image_histogram_ref_inc_value(self.inner, value, count);
440        }
441    }
442
443    /// Includes one instance of each value in the given image in the histogram.
444    ///
445    /// # Safety
446    ///
447    /// Due to the many invariants between the histogram and the pool,
448    /// we do not provide any safty guarentees when mutating the histogram.
449    pub unsafe fn include_image(&mut self, image: &FlipImageFloat) {
450        unsafe {
451            nv_flip_sys::flip_image_histogram_ref_inc_image(self.inner, image.inner);
452        }
453    }
454}
455
456impl Drop for FlipHistogram<'_> {
457    fn drop(&mut self) {
458        unsafe {
459            nv_flip_sys::flip_image_histogram_ref_free(self.inner);
460        }
461    }
462}
463
464/// Histogram-like value pool for determining if error map has significant differences.
465///
466/// This is how you can programmatically determine if images count as different.
467pub struct FlipPool {
468    inner: *mut nv_flip_sys::FlipImagePool,
469    values_added: usize,
470}
471
472unsafe impl Send for FlipPool {}
473unsafe impl Sync for FlipPool {}
474
475impl FlipPool {
476    /// Creates a new pool with 100 buckets.
477    pub fn new() -> Self {
478        Self::with_buckets(100)
479    }
480
481    /// Creates a new pool with the given amount of buckets.
482    pub fn with_buckets(bucket_count: usize) -> Self {
483        let inner = unsafe { nv_flip_sys::flip_image_pool_new(bucket_count) };
484        assert!(!inner.is_null());
485        Self {
486            inner,
487            values_added: 0,
488        }
489    }
490
491    /// Creates a new pool and initializes the buckets with the values given image.
492    pub fn from_image(image: &FlipImageFloat) -> Self {
493        let mut pool = Self::new();
494        pool.update_with_image(image);
495        pool
496    }
497
498    /// Accesses the internal histogram of the pool.
499    pub fn histogram(&mut self) -> FlipHistogram<'_> {
500        let inner = unsafe { nv_flip_sys::flip_image_pool_get_histogram(self.inner) };
501        assert!(!inner.is_null());
502        FlipHistogram {
503            inner,
504            _phantom: PhantomData,
505        }
506    }
507
508    /// Gets the minimum value stored in the pool.
509    ///
510    /// Returns 0.0 if no values have been added to the pool.
511    pub fn min_value(&self) -> f32 {
512        if self.values_added == 0 {
513            return 0.0;
514        }
515        unsafe { nv_flip_sys::flip_image_pool_get_min_value(self.inner) }
516    }
517
518    /// Gets the maximum value stored in the pool.
519    ///
520    /// Returns 0.0 if no values have been added to the pool.
521    pub fn max_value(&self) -> f32 {
522        if self.values_added == 0 {
523            return 0.0;
524        }
525        unsafe { nv_flip_sys::flip_image_pool_get_max_value(self.inner) }
526    }
527
528    /// Gets the mean value stored in the pool.
529    ///
530    /// Returns 0.0 if no values have been added to the pool.
531    pub fn mean(&self) -> f32 {
532        // Avoid div by zero in body.
533        if self.values_added == 0 {
534            return 0.0;
535        }
536        unsafe { nv_flip_sys::flip_image_pool_get_mean(self.inner) }
537    }
538
539    /// Gets the given weighted percentile [0.0, 1.0] from the pool.
540    ///
541    /// I currently do not understand the difference between this and [`Self::get_percentile`] with weighted = true,
542    /// except that this function uses doubles and doesn't require mutation of internal state.
543    ///
544    /// Returns 0.0 if no values have been added to the pool.
545    pub fn get_weighted_percentile(&self, percentile: f64) -> f64 {
546        if self.values_added == 0 {
547            return 0.0;
548        }
549        let bounds_percentile = f64::clamp(percentile, 0.0, next_f64_down(1.0));
550        unsafe {
551            nv_flip_sys::flip_image_pool_get_weighted_percentile(self.inner, bounds_percentile)
552        }
553    }
554
555    /// Get the value of the given percentile [0.0, 1.0] from the pool.
556    ///
557    /// If `weighted` is true, is almost equivalent to [`Self::get_weighted_percentile`].
558    ///
559    /// Returns 0.0 if no values have been added to the pool.
560    pub fn get_percentile(&mut self, percentile: f32, weighted: bool) -> f32 {
561        // Avoids a division by zero when bounds checking.
562        if self.values_added == 0 {
563            return 0.0;
564        }
565        // The implementaion doesn't actually do any bounds checking on the percentile,
566        // so we need to do it here, including tracking count of values added.
567        let bounds_percentile =
568            f32::clamp(percentile, 0.0, 1.0 - (self.values_added as f32).recip());
569        // Replicates the indexing behavior of the C++ implementation.
570        debug_assert!(
571            (f32::ceil(bounds_percentile * self.values_added as f32) as usize) < self.values_added
572        );
573        unsafe {
574            nv_flip_sys::flip_image_pool_get_percentile(self.inner, bounds_percentile, weighted)
575        }
576    }
577
578    /// Updates the given pool with the contents of the given image.
579    pub fn update_with_image(&mut self, image: &FlipImageFloat) {
580        unsafe {
581            nv_flip_sys::flip_image_pool_update_image(self.inner, image.inner);
582        }
583        self.values_added += image.width() as usize * image.height() as usize;
584    }
585
586    /// Clears the pool.
587    pub fn clear(&mut self) {
588        unsafe {
589            nv_flip_sys::flip_image_pool_clear(self.inner);
590        }
591        self.values_added = 0;
592    }
593}
594
595impl Default for FlipPool {
596    fn default() -> Self {
597        Self::new()
598    }
599}
600
601impl Drop for FlipPool {
602    fn drop(&mut self) {
603        unsafe {
604            nv_flip_sys::flip_image_pool_free(self.inner);
605        }
606    }
607}
608
609// This next_f64_down impl only works for positive, normal values that are
610// more than one ulp away from f64::MIN_POSITIVE.
611fn next_f64_down(value: f64) -> f64 {
612    f64::from_bits(value.to_bits() - 1)
613}
614
615#[cfg(test)]
616mod tests {
617    pub use super::*;
618    use float_eq::assert_float_eq;
619
620    #[test]
621    fn zeroed_init() {
622        assert_eq!(FlipImageRgb8::new(10, 10).to_vec(), vec![0u8; 10 * 10 * 3]);
623        assert_eq!(FlipImageFloat::new(10, 10).to_vec(), vec![0.0f32; 10 * 10]);
624    }
625
626    #[test]
627    fn zero_size_pool_ops() {
628        let mut pool = FlipPool::new();
629        assert_eq!(pool.min_value(), 0.0);
630        assert_eq!(pool.max_value(), 0.0);
631        assert_eq!(pool.mean(), 0.0);
632        assert_eq!(pool.get_percentile(0.0, false), 0.0);
633        assert_eq!(pool.get_percentile(0.0, true), 0.0);
634        assert_eq!(pool.get_weighted_percentile(0.0), 0.0);
635    }
636
637    #[test]
638    fn end_to_end() {
639        let reference_image = image::open("../etc/tree-ref.png").unwrap().into_rgb8();
640        let reference_image = FlipImageRgb8::with_data(
641            reference_image.width(),
642            reference_image.height(),
643            &reference_image,
644        );
645
646        let test_image = image::open("../etc/tree-test.png").unwrap().into_rgb8();
647        let test_image =
648            FlipImageRgb8::with_data(test_image.width(), test_image.height(), &test_image);
649
650        let error_map = flip(reference_image, test_image, DEFAULT_PIXELS_PER_DEGREE);
651
652        let mut pool = FlipPool::from_image(&error_map);
653
654        let magma_lut = magma_lut();
655        let color = error_map.apply_color_lut(&magma_lut);
656
657        let image =
658            image::RgbImage::from_raw(color.width(), color.height(), color.to_vec()).unwrap();
659
660        let reference = image::open("../etc/tree-comparison-cli.png")
661            .unwrap()
662            .into_rgb8();
663
664        for (a, b) in image.pixels().zip(reference.pixels()) {
665            assert!(a.0[0].abs_diff(b.0[0]) <= 3);
666            assert!(a.0[1].abs_diff(b.0[1]) <= 3);
667            assert!(a.0[2].abs_diff(b.0[2]) <= 3);
668        }
669
670        // These numbers pulled directly from the command line tool
671        const TOLERENCE: f32 = 0.000_1;
672        assert_float_eq!(pool.mean(), 0.133285, abs <= TOLERENCE);
673        assert_float_eq!(pool.get_percentile(0.25, true), 0.184924, abs <= TOLERENCE);
674        assert_float_eq!(pool.get_percentile(0.50, true), 0.333241, abs <= TOLERENCE);
675        assert_float_eq!(pool.get_percentile(0.75, true), 0.503441, abs <= TOLERENCE);
676        assert_float_eq!(pool.min_value(), 0.000000, abs <= TOLERENCE);
677        assert_float_eq!(pool.get_percentile(0.0, true), 0.000000, abs <= 0.001);
678        assert_float_eq!(pool.max_value(), 0.983044, abs <= TOLERENCE);
679        assert_float_eq!(pool.get_percentile(1.0, true), 0.983044, abs <= 0.001);
680        assert_float_eq!(
681            pool.get_weighted_percentile(0.25),
682            0.184586,
683            abs <= TOLERENCE as f64
684        );
685        assert_float_eq!(
686            pool.get_weighted_percentile(0.50),
687            0.333096,
688            abs <= TOLERENCE as f64
689        );
690        assert_float_eq!(
691            pool.get_weighted_percentile(0.75),
692            0.503230,
693            abs <= TOLERENCE as f64
694        );
695
696        let histogram = pool.histogram();
697        assert_float_eq!(histogram.minimum_allowed_value(), 0.0, abs <= TOLERENCE);
698        assert_float_eq!(histogram.maximum_allowed_value(), 1.0, abs <= TOLERENCE);
699        drop(histogram);
700
701        // Absurd values, trying to edge case the histogram
702        assert_float_eq!(pool.get_percentile(-10000.0, false), 0.0, abs <= TOLERENCE);
703        assert_float_eq!(pool.get_percentile(-10000.0, true), 0.0, abs <= TOLERENCE);
704        assert_float_eq!(
705            pool.get_percentile(10000.0, false),
706            0.983044,
707            abs <= TOLERENCE
708        );
709        assert_float_eq!(
710            pool.get_percentile(10000.0, true),
711            0.983044,
712            abs <= TOLERENCE
713        );
714        assert_float_eq!(
715            pool.get_weighted_percentile(-10000.0),
716            0.0,
717            abs <= TOLERENCE as _
718        );
719        assert_float_eq!(
720            pool.get_weighted_percentile(10000.0),
721            0.989999,
722            abs <= TOLERENCE as _
723        );
724    }
725}