Skip to main content

fits_io/image/compression/
dither.rs

1//! The dithering a floating point image is quantised with.
2//!
3//! Compressing a floating point image means turning it into integers first, and
4//! rounding every pixel to the nearest step of the same grid lays a visible
5//! pattern over a smooth background — contours where the sky crosses from one
6//! step to the next. The convention's answer is to add a known pseudo-random
7//! number to each value before rounding it and to take the same number off again
8//! on the way back, which spreads the rounding error out into noise the eye does
9//! not organise into shapes.
10//!
11//! "Known" is what makes it work: the sequence comes from a generator the
12//! convention fixes, so a reader reproduces exactly the numbers the writer used.
13//! A reader that ignores ZQUANTIZ gets an image that is wrong by up to half a
14//! quantisation step, in a pattern rather than at random.
15
16use std::error::Error;
17
18/// How many numbers the dithering sequence holds before it repeats.
19pub(crate) const SEQUENCE_LENGTH: usize = 10000;
20
21/// The quantised value that a `SUBTRACTIVE_DITHER_2` tile uses for an exact
22/// zero, which it stores rather than dithers so that zero stays zero.
23///
24/// It sits just above [`NULL_VALUE`], at the bottom of the range the convention
25/// reserves.
26const ZERO_VALUE: i64 = -2147483646;
27
28/// How a floating point image was turned into integers.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum Quantization {
31    /// The values were rounded to the nearest step, with nothing added.
32    #[default]
33    NoDither,
34    /// Every value was dithered, zeros included.
35    SubtractiveDither1,
36    /// As `SUBTRACTIVE_DITHER_1`, but a pixel that was exactly zero is stored
37    /// as such and comes back as exactly zero.
38    SubtractiveDither2,
39}
40
41impl Quantization {
42    /// Reads the method a ZQUANTIZ card names.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error for a method this crate does not implement: undithering
47    /// with the wrong sequence is worse than saying so, because the image it
48    /// produces looks right.
49    pub(crate) fn from_card(value: Option<&str>) -> Result<Self, Box<dyn Error + Send + Sync>> {
50        match value.map(str::trim) {
51            None | Some("") | Some("NONE") | Some("NO_DITHER") => Ok(Quantization::NoDither),
52            Some("SUBTRACTIVE_DITHER_1") => Ok(Quantization::SubtractiveDither1),
53            Some("SUBTRACTIVE_DITHER_2") => Ok(Quantization::SubtractiveDither2),
54            Some(other) => Err(format!(
55                "ZQUANTIZ {:?} is not a quantisation method this crate implements; it reads \
56                 NO_DITHER, SUBTRACTIVE_DITHER_1 and SUBTRACTIVE_DITHER_2",
57                other
58            )
59            .into()),
60        }
61    }
62
63    /// The name a ZQUANTIZ card writes this method under.
64    pub(crate) fn card_value(self) -> &'static str {
65        match self {
66            Quantization::NoDither => "NO_DITHER",
67            Quantization::SubtractiveDither1 => "SUBTRACTIVE_DITHER_1",
68            Quantization::SubtractiveDither2 => "SUBTRACTIVE_DITHER_2",
69        }
70    }
71
72    /// Whether this method adds anything to a value before rounding it.
73    pub(crate) fn dithers(self) -> bool {
74        !matches!(self, Quantization::NoDither)
75    }
76}
77
78/// The sequence of numbers the convention dithers with.
79///
80/// It is generated by the Park-Miller "minimal standard" generator from a seed
81/// of one, exactly as the reference implementation does, so that the numbers are
82/// the same ones the writer of a file used. Doing it any other way — a better
83/// generator, or the same one in a different arithmetic — undithers an image
84/// with numbers nobody added to it.
85pub(crate) fn sequence() -> &'static [f32; SEQUENCE_LENGTH] {
86    use std::sync::OnceLock;
87
88    static SEQUENCE: OnceLock<[f32; SEQUENCE_LENGTH]> = OnceLock::new();
89
90    SEQUENCE.get_or_init(|| {
91        let mut values = [0.0_f32; SEQUENCE_LENGTH];
92
93        for (value, seed) in values.iter_mut().zip(seeds()) {
94            // Single precision is what the reference implementation keeps the
95            // result in, and the numbers have to be the same ones to the last
96            // bit.
97            *value = (seed / MODULUS) as f32;
98        }
99
100        values
101    })
102}
103
104/// The seeds the generator passes through, in order.
105///
106/// Every product here is under 2^53, so the arithmetic is exact and the
107/// sequence is the same on every machine.
108fn seeds() -> impl Iterator<Item = f64> {
109    let mut seed = 1.0_f64;
110
111    std::iter::repeat_with(move || {
112        let temp = MULTIPLIER * seed;
113        seed = temp - MODULUS * (temp / MODULUS).floor();
114        seed
115    })
116}
117
118/// The multiplier and modulus of the generator the convention fixes.
119const MULTIPLIER: f64 = 16807.0;
120const MODULUS: f64 = 2147483647.0;
121
122/// Walks the dithering sequence for one tile.
123///
124/// Each tile starts at its own place in the sequence, worked out from the file's
125/// ZDITHER0 and the tile's number, so that tiles do not all dither alike.
126#[derive(Debug, Clone, Copy)]
127pub(crate) struct Dither {
128    /// Where in the sequence this tile's starting point was drawn from.
129    start: usize,
130    /// The index of the next number to use.
131    next: usize,
132}
133
134impl Dither {
135    /// The dithering for tile `tile` of a file seeded with `seed`, both counting
136    /// from one and zero respectively.
137    pub(crate) fn for_tile(seed: i64, tile: usize) -> Self {
138        // The seed and the tile number both step through the sequence, so that
139        // two files of the same shape do not dither identically and neither do
140        // two tiles of one file.
141        let start = (seed - 1).rem_euclid(SEQUENCE_LENGTH as i64) as usize;
142        let start = (start + tile) % SEQUENCE_LENGTH;
143
144        Self {
145            start,
146            next: Self::first(start),
147        }
148    }
149
150    /// Where in the sequence a tile starting at `start` takes its first number
151    /// from.
152    fn first(start: usize) -> usize {
153        (sequence()[start] * 500.0) as usize % SEQUENCE_LENGTH
154    }
155
156    /// The next number of the sequence, as the reference implementation's
157    /// single precision value widened.
158    fn next_value(&mut self) -> f64 {
159        self.take() as f64
160    }
161
162    /// The next number of the sequence.
163    fn take(&mut self) -> f32 {
164        let value = sequence()[self.next];
165
166        self.next += 1;
167        if self.next == SEQUENCE_LENGTH {
168            // The sequence has run out, so the tile draws a fresh starting point
169            // from the next entry of its own.
170            self.start = (self.start + 1) % SEQUENCE_LENGTH;
171            self.next = Self::first(self.start);
172        }
173
174        value
175    }
176}
177
178/// Turns a tile's quantised integers back into the values they stood for.
179///
180/// `blank` is the integer standing for a pixel the image does not define, which
181/// comes back as `NaN` rather than as whatever that integer scales to.
182pub(crate) fn unquantize(
183    values: &[f64],
184    scale: f64,
185    zero: f64,
186    method: Quantization,
187    blank: Option<f64>,
188    mut dither: Dither,
189) -> Vec<f64> {
190    values
191        .iter()
192        .map(|value| {
193            // A blank pixel still draws its number: the sequence has to stay in
194            // step with the one the writer used, whatever this pixel holds.
195            let random = if method.dithers() {
196                dither.next_value()
197            } else {
198                0.5
199            };
200
201            if Some(*value) == blank {
202                return f64::NAN;
203            }
204
205            if method == Quantization::SubtractiveDither2 && *value == ZERO_VALUE as f64 {
206                return 0.0;
207            }
208
209            zero + scale * (*value - random + 0.5)
210        })
211        .collect()
212}
213
214/// Turns a tile's values into the integers a compressor can work on, the inverse
215/// of [`unquantize`].
216pub(crate) fn quantize(
217    values: &[f64],
218    scale: f64,
219    zero: f64,
220    method: Quantization,
221    blank: Option<i64>,
222    mut dither: Dither,
223) -> Vec<i64> {
224    values
225        .iter()
226        .map(|value| {
227            let random = if method.dithers() {
228                dither.next_value()
229            } else {
230                0.5
231            };
232
233            if !value.is_finite() {
234                // An undefined pixel is stored as the blank value, and there is
235                // nowhere to put one if the caller reserved no such value.
236                return blank.unwrap_or(0);
237            }
238
239            if method == Quantization::SubtractiveDither2 && *value == 0.0 {
240                return ZERO_VALUE;
241            }
242
243            // Rounded the way the reference implementation rounds, so that a
244            // value on the boundary between two steps goes the same way here as
245            // it would there.
246            ((value - zero) / scale + random - 0.5).round() as i64
247        })
248        .collect()
249}
250
251#[cfg(test)]
252mod tests {
253    use super::{Dither, Quantization, SEQUENCE_LENGTH, quantize, seeds, sequence, unquantize};
254
255    /// The integers a compressor would hold, as the decompressor hands them
256    /// back: whatever the coding was, they arrive as numbers.
257    fn as_values(quantised: &[i64]) -> Vec<f64> {
258        quantised.iter().map(|value| *value as f64).collect()
259    }
260
261    #[test]
262    fn the_sequence_is_the_one_the_convention_fixes() {
263        let sequence = sequence();
264
265        // The first values of the Park-Miller generator seeded with one, as
266        // 16807/2147483647, 16807^2 mod m / m, and so on.
267        assert!((sequence[0] as f64 - 16807.0 / 2147483647.0).abs() < 1e-7);
268        assert!((sequence[1] as f64 - 282475249.0 / 2147483647.0).abs() < 1e-7);
269        assert!((sequence[2] as f64 - 1622650073.0 / 2147483647.0).abs() < 1e-7);
270
271        // The reference implementation checks itself against this: the seed
272        // behind the last number of the sequence is 1043618065, and an
273        // implementation that produces anything else is not producing the
274        // sequence the convention fixed.
275        assert_eq!(seeds().nth(SEQUENCE_LENGTH - 1), Some(1043618065.0));
276
277        // It never leaves the unit interval.
278        assert!(sequence.iter().all(|value| (0.0..1.0).contains(value)));
279    }
280
281    #[test]
282    fn a_quantisation_method_is_read_from_its_card() {
283        assert_eq!(
284            Quantization::from_card(None).unwrap(),
285            Quantization::NoDither
286        );
287        assert_eq!(
288            Quantization::from_card(Some("SUBTRACTIVE_DITHER_1")).unwrap(),
289            Quantization::SubtractiveDither1
290        );
291        assert!(Quantization::from_card(Some("SOMETHING_ELSE")).is_err());
292    }
293
294    #[test]
295    fn two_tiles_do_not_dither_alike() {
296        let values = [100.0_f64; 8];
297
298        let first = unquantize(
299            &values,
300            0.5,
301            0.0,
302            Quantization::SubtractiveDither1,
303            None,
304            Dither::for_tile(1, 0),
305        );
306        let second = unquantize(
307            &values,
308            0.5,
309            0.0,
310            Quantization::SubtractiveDither1,
311            None,
312            Dither::for_tile(1, 1),
313        );
314
315        assert_ne!(first, second);
316    }
317
318    #[test]
319    fn quantising_and_undoing_it_lands_within_one_step() {
320        let values: Vec<f64> = (0..64).map(|index| 10.0 + index as f64 * 0.017).collect();
321        let scale = 0.01;
322
323        for method in [
324            Quantization::NoDither,
325            Quantization::SubtractiveDither1,
326            Quantization::SubtractiveDither2,
327        ] {
328            let quantised = as_values(&quantize(
329                &values,
330                scale,
331                10.0,
332                method,
333                None,
334                Dither::for_tile(7, 3),
335            ));
336            let back = unquantize(
337                &quantised,
338                scale,
339                10.0,
340                method,
341                None,
342                Dither::for_tile(7, 3),
343            );
344
345            for (original, returned) in values.iter().zip(&back) {
346                assert!(
347                    (original - returned).abs() <= scale,
348                    "{original} came back as {returned}, further than one step of {scale}"
349                );
350            }
351        }
352    }
353
354    #[test]
355    fn dithering_keeps_the_average_of_a_flat_patch_where_it_was() {
356        // The point of dithering. A patch of sky sitting four tenths of a step
357        // above a quantisation level rounds, plainly, to that level in every
358        // pixel: the patch comes back four tenths of a step too dark, with no
359        // trace left that it was ever anywhere else. Dithered, the pixels fall
360        // on either side in the right proportion, and the patch keeps its
361        // brightness even though no single pixel does.
362        let scale = 0.01;
363        let value = 10.0 + 0.4 * scale;
364        let values = vec![value; 2000];
365
366        let mean = |method| {
367            let quantised = as_values(&quantize(
368                &values,
369                scale,
370                0.0,
371                method,
372                None,
373                Dither::for_tile(1, 0),
374            ));
375            let back = unquantize(&quantised, scale, 0.0, method, None, Dither::for_tile(1, 0));
376
377            back.iter().sum::<f64>() / back.len() as f64
378        };
379
380        let plain = mean(Quantization::NoDither);
381        let dithered = mean(Quantization::SubtractiveDither1);
382
383        assert!(
384            (plain - value).abs() > 0.3 * scale,
385            "plain rounding should lose the offset, got {plain}"
386        );
387        assert!(
388            (dithered - value).abs() < 0.05 * scale,
389            "dithering should keep the average at {value}, got {dithered}"
390        );
391    }
392
393    #[test]
394    fn dither_two_keeps_zero_exactly_zero() {
395        let quantised = as_values(&quantize(
396            &[0.0, 1.0],
397            0.5,
398            0.0,
399            Quantization::SubtractiveDither2,
400            None,
401            Dither::for_tile(1, 0),
402        ));
403        let back = unquantize(
404            &quantised,
405            0.5,
406            0.0,
407            Quantization::SubtractiveDither2,
408            None,
409            Dither::for_tile(1, 0),
410        );
411
412        assert_eq!(back[0], 0.0);
413    }
414
415    #[test]
416    fn a_blank_value_comes_back_undefined() {
417        let back = unquantize(
418            &[5.0, -32768.0, 7.0],
419            1.0,
420            0.0,
421            Quantization::NoDither,
422            Some(-32768.0),
423            Dither::for_tile(1, 0),
424        );
425
426        assert!(back[1].is_nan(), "got {back:?}");
427        assert!(back[0].is_finite() && back[2].is_finite(), "got {back:?}");
428    }
429}