otf-pixels-codec-avif 0.5.0

AVIF codec for otf-pixels, implemented from scratch.
Documentation
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! A whole AV1 key frame: headers, tiles, OBUs.
//!
//! The headers are written, then parsed back with the decoder's own parsers,
//! and the decoder's tile state is built from what it read, so the encoder
//! codes exactly the frame a decoder will see.

use super::super::bits::BitReader;
use super::super::frame::FrameHeader;
use super::super::seq::SequenceHeader;
use super::super::tile::{TileState, tile_bounds};
use super::super::transform::ac_q;
use super::headers::{
    CdefStrength, FrameParams, OBU_FRAME, OBU_SEQUENCE_HEADER, SequenceParams, frame_header, obu,
    sequence_header,
};
use super::tile::{ALL_MODES, SourcePlane, TileEncoder, Tuning};
use otf_pixels_core::{PixelsError, Result};

/// An 8-bit picture to code: one luma plane, or luma and two chroma planes,
/// each row-major at its own size.
pub(crate) struct Picture<'a> {
    pub width: u32,
    pub height: u32,
    /// Y, then U and V unless monochrome: half size (rounded up) when
    /// `subsampled`, full size otherwise.
    pub planes: &'a [&'a [u16]],
    /// Chroma at 4:2:0 rather than 4:4:4.
    pub subsampled: bool,
    /// `(colour_primaries, transfer, matrix)`.
    pub cicp: (u8, u8, u8),
    pub full_range: bool,
}

/// A coded still: the sequence header OBU (for `av1C`) and the full temporal
/// unit (for the item's data).
pub(crate) struct CodedStill {
    /// `seq_level_idx[0]`, which `av1C` repeats.
    pub level: u8,
    /// `seq_profile`, which `av1C` repeats: 0, or 1 for 4:4:4.
    pub profile: u8,
    /// Whether chroma is 4:2:0 (or the picture is monochrome, which `av1C`
    /// also flags as subsampled).
    pub subsampled: bool,
    pub sequence_header_obu: Vec<u8>,
    pub data: Vec<u8>,
}

/// `base_q_idx` for a 0 (worst) to 100 (best) quality. Never 0, which would
/// make the frame lossless and change its whole coding.
pub(crate) fn qindex_for_quality(quality: u8) -> u8 {
    let q = (f64::from(100 - quality.min(100)) * 2.55).round();
    q.clamp(1.0, 255.0) as u8
}

/// The deblocking level libaom estimates for an 8-bit key frame at `qindex`.
fn loop_filter_level(qindex: u8) -> u8 {
    let q = ac_q(8, i32::from(qindex));
    let guess = ((q * 17_563 - 421_574 + (1 << 17)) >> 18) - 4;
    guess.clamp(0, 63) as u8
}

fn cdef_strength(qindex: u8) -> CdefStrength {
    let y_pri = (qindex / 32).min(8);
    let y_sec = u8::from(qindex >= 80);
    CdefStrength {
        y_pri,
        y_sec,
        uv_pri: y_pri / 2,
        uv_sec: y_sec,
    }
}

/// Encode `picture` at `qindex` as a reduced-still-picture AV1 stream, at
/// the default effort.
#[cfg(test)]
pub(crate) fn encode_still(picture: &Picture<'_>, qindex: u8) -> Result<CodedStill> {
    encode_still_with(picture, qindex, DEFAULT_EFFORT)
}

/// sharp's default AVIF effort, and the search this encoder always made.
pub(crate) const DEFAULT_EFFORT: u8 = 4;

/// [`encode_still`] at `effort`, 0..=9: below 3 the intra mode search tries
/// only the four cheapest predictors, DC, vertical, horizontal and Paeth.
pub(crate) fn encode_still_with(
    picture: &Picture<'_>,
    qindex: u8,
    effort: u8,
) -> Result<CodedStill> {
    let mono_chrome = picture.planes.len() == 1;
    if picture.width == 0
        || picture.height == 0
        || (picture.planes.len() != 1 && picture.planes.len() != 3)
    {
        return Err(PixelsError::invalid_argument(
            "picture",
            "an AV1 picture needs a size and one or three planes",
        ));
    }
    let seq_params = SequenceParams {
        width: picture.width,
        height: picture.height,
        mono_chrome,
        subsampled: picture.subsampled,
        cicp: picture.cicp,
        full_range: picture.full_range,
    };
    let level = loop_filter_level(qindex);
    let frame_params = FrameParams {
        base_q_idx: qindex,
        loop_filter: [level, level, level, level],
        sharpness: 0,
        cdef: (1, vec![cdef_strength(qindex)]),
        tx_mode_select: false,
        tile_log2: (0, 0),
    };
    let seq_payload = sequence_header(&seq_params);
    let frame_payload = frame_header(&seq_params, &frame_params);
    let seq = SequenceHeader::parse(&mut BitReader::new(&seq_payload))?;
    let frame = FrameHeader::parse(&mut BitReader::new(&frame_payload), &seq, 0, 0)?;
    let mut state = TileState::for_frame(&seq, &frame)?;

    // The source, padded to the decoder's plane sizes.
    let mut source = Vec::with_capacity(picture.planes.len());
    for (index, samples) in picture.planes.iter().enumerate() {
        let sub = u32::from(index > 0 && picture.subsampled);
        let (w, h) = (
            ((picture.width + sub) >> sub) as usize,
            ((picture.height + sub) >> sub) as usize,
        );
        if samples.len() < w * h {
            return Err(PixelsError::invalid_argument(
                "picture",
                "an AV1 plane is smaller than the picture",
            ));
        }
        let plane = state.planes.get(index).ok_or_else(|| {
            PixelsError::invalid_argument("picture", "the frame has fewer planes than given")
        })?;
        source.push(SourcePlane::padded(
            samples,
            w,
            h,
            plane.width(),
            plane.height(),
        ));
    }

    let tuning = Tuning {
        ac_q: ac_q(8, i32::from(qindex)),
        dc_bias: 0.5,
        ac_bias: 0.36,
        modes: if effort < 3 {
            &[0, 1, 2, 12]
        } else {
            &ALL_MODES
        },
    };
    let info = &frame.tile_info;
    let (mi_rows, mi_cols) = state.mi_dims();
    let mut tiles = Vec::with_capacity(info.count() as usize);
    for number in 0..info.count() {
        let bounds = tile_bounds(info, 4, mi_rows, mi_cols, number);
        let mut coder = TileEncoder::new(&source, tuning);
        state.code_tile(&mut coder, bounds)?;
        tiles.push(coder.finish());
    }

    // OBU_FRAME: the header, then the tile group (§5.11.1).
    let mut payload = frame_payload;
    if tiles.len() > 1 {
        payload.push(0); // tile_start_and_end_present_flag = 0, byte-aligned
    }
    let last = tiles.len().saturating_sub(1);
    for (i, tile) in tiles.iter().enumerate() {
        if i < last {
            let size = u32::try_from(tile.len() - 1).map_err(|_| {
                PixelsError::invalid_argument("picture", "a tile larger than 4 GiB")
            })?;
            payload.extend_from_slice(&size.to_le_bytes());
        }
        payload.extend_from_slice(tile);
    }
    let sequence_header_obu = obu(OBU_SEQUENCE_HEADER, &seq_payload);
    let mut data = sequence_header_obu.clone();
    data.extend_from_slice(&obu(OBU_FRAME, &payload));
    Ok(CodedStill {
        level: seq_params.level(),
        profile: u8::from(!mono_chrome && !picture.subsampled),
        subsampled: mono_chrome || picture.subsampled,
        sequence_header_obu,
        data,
    })
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::indexing_slicing,
    reason = "tests operate on known-good values"
)]
mod tests {
    use super::super::super::still::StillPicture;
    use super::super::super::tile::decode_still;
    use super::*;

    /// A picture with smooth gradients, edges and texture.
    fn scene(width: u32, height: u32) -> Vec<Vec<u16>> {
        let (w, h) = (width as usize, height as usize);
        let mut y = vec![0_u16; w * h];
        for j in 0..h {
            for i in 0..w {
                let ramp = (i * 160 / w.max(1) + j * 60 / h.max(1)) as i32;
                let disc = if (i as i32 - w as i32 / 2).pow(2) + (j as i32 - h as i32 / 3).pow(2)
                    < (w as i32 / 4).pow(2)
                {
                    50
                } else {
                    0
                };
                let stripes = if (i / 3 + j / 5) % 2 == 0 && j > h * 2 / 3 {
                    30
                } else {
                    0
                };
                y[j * w + i] = (ramp + disc + stripes).clamp(0, 255) as u16;
            }
        }
        let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
        let u = (0..cw * ch)
            .map(|k| (128 + (k % cw) * 40 / cw.max(1)) as u16)
            .collect();
        let v = (0..cw * ch)
            .map(|k| (100 + (k / cw) * 60 / ch.max(1)) as u16)
            .collect();
        vec![y, u, v]
    }

    fn psnr(a: &[u16], b: &[u16]) -> f64 {
        let mse = a
            .iter()
            .zip(b)
            .map(|(&x, &y)| f64::from(x as i32 - y as i32).powi(2))
            .sum::<f64>()
            / a.len() as f64;
        if mse == 0.0 {
            99.0
        } else {
            10.0 * (255.0 * 255.0 / mse).log10()
        }
    }

    fn round_trip(
        width: u32,
        height: u32,
        planes: &[Vec<u16>],
        qindex: u8,
    ) -> (Vec<Vec<u16>>, usize) {
        let refs: Vec<&[u16]> = planes.iter().map(Vec::as_slice).collect();
        let picture = Picture {
            subsampled: true,
            width,
            height,
            planes: &refs,
            cicp: (1, 13, 6),
            full_range: true,
        };
        let coded = encode_still(&picture, qindex).unwrap();
        let still = StillPicture::parse(&coded.sequence_header_obu, &coded.data).unwrap();
        let groups: Vec<&[u8]> = still
            .tile_groups
            .iter()
            .map(|r| &coded.data[r.clone()])
            .collect();
        let decoded = decode_still(&still.sequence, &still.frame, &groups).unwrap();
        let out = decoded
            .planes
            .iter()
            .enumerate()
            .map(|(i, plane)| {
                let sub = u32::from(i > 0);
                let (w, h) = (
                    ((width + sub) >> sub) as usize,
                    ((height + sub) >> sub) as usize,
                );
                (0..w * h)
                    .map(|k| plane.get(k % w, k / w).unwrap())
                    .collect()
            })
            .collect();
        (out, coded.data.len())
    }

    /// With `OTF_EMIT_DIR` set, writes coded streams (`.obu`, Annex-less with
    /// a temporal delimiter) and our decode of them (`.yuv`, I420 or Y only)
    /// for `scripts/check-avif-interop.sh` to compare against libaom.
    #[test]
    fn emit_streams_for_interop() {
        let Some(dir) = std::env::var_os("OTF_EMIT_DIR") else {
            return;
        };
        let dir = std::path::PathBuf::from(dir);
        for (width, height, qindex, mono) in [
            (64, 64, 40, false),
            (100, 75, 120, false),
            (130, 33, 1, false),
            (1, 1, 255, false),
            (300, 200, 80, false),
            (48, 40, 60, true),
        ] {
            let mut planes = scene(width, height);
            if mono {
                planes.truncate(1);
            }
            let refs: Vec<&[u16]> = planes.iter().map(Vec::as_slice).collect();
            let picture = Picture {
                subsampled: true,
                width,
                height,
                planes: &refs,
                cicp: (1, 13, 6),
                full_range: true,
            };
            let coded = encode_still(&picture, qindex).unwrap();
            let mut obus = vec![0x12, 0x00];
            obus.extend_from_slice(&coded.data);
            let name = format!(
                "{width}x{height}_q{qindex}{}",
                if mono { "_mono" } else { "" }
            );
            std::fs::write(dir.join(format!("{name}.obu")), obus).unwrap();
            let (out, _) = round_trip(width, height, &planes, qindex);
            let bytes: Vec<u8> = out.iter().flatten().map(|&v| v as u8).collect();
            std::fs::write(dir.join(format!("{name}.yuv")), bytes).unwrap();
        }
    }

    #[test]
    fn a_coded_picture_decodes_close_to_its_source() {
        for (width, height) in [(64, 64), (100, 75), (8, 8), (1, 1), (130, 33)] {
            let planes = scene(width, height);
            let (out, _) = round_trip(width, height, &planes, 40);
            for (plane, (a, b)) in out.iter().zip(&planes).enumerate() {
                let p = psnr(a, b);
                assert!(p > 36.0, "{width}x{height} plane {plane}: {p:.1} dB");
            }
        }
    }

    #[test]
    fn quality_trades_size_for_fidelity() {
        let planes = scene(128, 96);
        let (fine, fine_size) = round_trip(128, 96, &planes, 20);
        let (coarse, coarse_size) = round_trip(128, 96, &planes, 200);
        assert!(coarse_size < fine_size, "{coarse_size} >= {fine_size}");
        assert!(psnr(&fine[0], &planes[0]) > psnr(&coarse[0], &planes[0]));
        assert!(psnr(&coarse[0], &planes[0]) > 24.0);
    }

    #[test]
    fn a_monochrome_picture_codes_one_plane() {
        let planes = vec![scene(48, 40).swap_remove(0)];
        let (out, _) = round_trip(48, 40, &planes, 60);
        assert_eq!(out.len(), 1);
        assert!(psnr(&out[0], &planes[0]) > 34.0);
    }

    #[test]
    fn extreme_samples_survive_the_finest_quantizer() {
        // Full-swing checkerboard at qindex 1: large coefficients, Golomb tails.
        let (w, h) = (32_u32, 32_u32);
        let y: Vec<u16> = (0..w * h)
            .map(|k| if (k % w + k / w) % 2 == 0 { 0 } else { 255 })
            .collect();
        let c = vec![255_u16; 256];
        let planes = vec![y, c.clone(), vec![0; 256]];
        let (out, _) = round_trip(w, h, &planes, 1);
        assert!(
            psnr(&out[0], &planes[0]) > 40.0,
            "{}",
            psnr(&out[0], &planes[0])
        );
        assert_eq!(out[1], planes[1]);
    }

    #[test]
    fn quality_maps_onto_nonzero_quantizers() {
        assert_eq!(qindex_for_quality(100), 1);
        assert_eq!(qindex_for_quality(0), 255);
        assert!(qindex_for_quality(50) > qindex_for_quality(80));
        assert_eq!(loop_filter_level(1), 0);
        assert!(loop_filter_level(200) > loop_filter_level(60));
    }

    #[test]
    fn rejects_a_malformed_picture() {
        let y = vec![0_u16; 4];
        let refs: Vec<&[u16]> = vec![&y, &y];
        let picture = Picture {
            subsampled: true,
            width: 2,
            height: 2,
            planes: &refs,
            cicp: (1, 13, 6),
            full_range: true,
        };
        assert!(encode_still(&picture, 50).is_err());
        let refs: Vec<&[u16]> = vec![&y];
        let picture = Picture {
            subsampled: true,
            width: 3,
            height: 2,
            planes: &refs,
            cicp: (1, 13, 6),
            full_range: true,
        };
        assert!(encode_still(&picture, 50).is_err());
    }
}