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}