Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/src/hevc/filter.rs

23.1 KiB, 39 runs

created by r1870400018:20575, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The two passes over a finished picture, in the order they run.
2//!
3//! Both exist because a picture built block by block carries the seams of how it was built, and
4//! both are **in the loop**: the encoder ran them too, so a decoder that skips them is not simply
5//! showing a slightly rougher picture, it is showing a different one.
6//!
7//! **Deblocking** (§8.7.2) softens the boundary between adjacent blocks, and only where a boundary
8//! actually is — a transform or prediction edge that falls on the eight-sample grid. How hard it
9//! softens is set by the quantisation parameter either side: a coarsely quantised picture has
10//! bigger steps to hide. It decides, per four lines of edge, between a strong filter that reaches
11//! three samples in and a weak one that reaches one or two, and it declines entirely where the
12//! step across the boundary looks like a real edge in the photograph rather than an artefact of
13//! coding. That decision is the whole art of it: filtering a real edge is how a decoder turns a
14//! window frame into a smear.
15//!
16//! **The sample adaptive offset** (§8.7.3) then adds a small number to samples, chosen per coding
17//! tree block, in one of two ways. A *band* offset moves four adjacent slices of the range, which
18//! is how a gently graded sky is put back after quantisation stepped it. An *edge* offset compares
19//! each sample with two neighbours along a chosen direction and offsets peaks and valleys
20//! differently from slopes, which recovers detail the transform rounded away.
21//!
22//! Both run over the whole picture rather than block by block. The specification allows either --
23//! and says so -- and over a picture is what makes the ordering obvious: every vertical edge, then
24//! every horizontal one, then the offsets, each pass reading what the one before it wrote.
25//!
26//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
27//! Anthropic Claude
28
29use crate::hevc::decode::{
30 Picture,
31 Plane,
32 Sao,
33};
34
35// How far a sample may move, and how flat a boundary has to be before it is filtered at all
36// (§8.7.2.5.3, Table 8-12), indexed by the quantisation parameter.
37//
38// β is the flatness bar: a boundary whose second differences add up to less than this is taken to
39// be flat, and therefore a place where a step is an artefact rather than a subject. tC is how far
40// any one sample may be moved. Both are nought below a parameter of sixteen, which is why a finely
41// quantised picture is not filtered at all.
42const BETA: [i32; 52] = [
43 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
44 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24,
45 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56,
46 58, 60, 62, 64,
47];
48
49// The companion table, which runs two entries longer because the boundary strength adds to its
50// index.
51const TC: [i32; 54] = [
52 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
53 1, 1, 1, 1, 1, 1, 1, 1, 1,
54 2, 2, 2, 2,
55 3, 3, 3, 3,
56 4, 4, 4,
57 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24,
58];
59
60/// Where a boundary sits, and what a filter reads across it.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum Edge {
63 Vertical, // a block and the one to its left
64 Horizontal, // a block and the one above it
65}
66
67/// What the deblocking filter needs to know about a picture beyond its samples.
68pub struct Edges<'a> {
69 pub gw: usize, // the picture's width in four-sample blocks
70 pub gh: usize, // and its height
71 pub vertical: &'a [bool], // a transform or prediction boundary on each block's left?
72 pub horizontal: &'a [bool], // the same for its top
73 pub qp: &'a [i8], // luma quantisation parameter of the covering coding unit
74 pub chroma_offset: [i32; 2], // what the picture adds to the chroma parameter
75}
76
77/// Runs the deblocking filter over a whole picture (§8.7.2).
78///
79/// Every vertical edge first and then every horizontal one, which is the order the specification
80/// gives and the reason it gives it: the horizontal pass reads samples the vertical pass has
81/// already moved.
82///
83/// Every coding unit in a still picture is intra, so every boundary that is filtered at all is
84/// filtered at the strongest setting. That is what makes this shorter than a decoder that has to
85/// weigh motion vectors and reference indices to decide.
86pub fn deblock(pic: &mut Picture, edges: &Edges<'_>, depth: u32) {
87 for kind in [Edge::Vertical, Edge::Horizontal] {
88 luma(pic, edges, kind, depth);
89 chroma(pic, edges, kind, depth);
90 }
91}
92
93/// Does a boundary of the given kind sit at a luma position?
94fn boundary(edges: &Edges<'_>, kind: Edge, x: usize, y: usize) -> bool {
95 let (gx, gy) = (x / 4, y / 4);
96 if gx >= edges.gw || gy >= edges.gh {
97 return false;
98 }
99 let at = gy * edges.gw + gx;
100 match kind {
101 Edge::Vertical => edges.vertical[at],
102 Edge::Horizontal => edges.horizontal[at],
103 }
104}
105
106/// The quantisation parameter either side of a boundary, averaged as the filter wants it.
107fn across(edges: &Edges<'_>, kind: Edge, x: usize, y: usize) -> i32 {
108 let (gx, gy) = (x / 4, y / 4);
109 let (bx, by) = match kind {
110 Edge::Vertical => (gx.saturating_sub(1), gy),
111 Edge::Horizontal => (gx, gy.saturating_sub(1)),
112 };
113 let q = edges.qp[(gy.min(edges.gh - 1)) * edges.gw + gx.min(edges.gw - 1)] as i32;
114 let p = edges.qp[(by.min(edges.gh - 1)) * edges.gw + bx.min(edges.gw - 1)] as i32;
115 (q + p + 1) >> 1
116}
117
118/// One sample either side of a boundary, by how far from it and along it.
119///
120/// `i` counts away from the boundary -- negative into the block before it, nought and up into the
121/// block after -- and `k` counts along it. Writing the two directions this way is what lets one
122/// piece of arithmetic serve a vertical edge and a horizontal one.
123fn at(plane: &Plane, kind: Edge, x: usize, y: usize, i: i32, k: usize) -> i32 {
124 let (sx, sy) = match kind {
125 Edge::Vertical => (x as i32 + i, (y + k) as i32),
126 Edge::Horizontal => ((x + k) as i32, y as i32 + i),
127 };
128 if sx < 0 || sy < 0 {
129 return 0;
130 }
131 plane.at(sx as usize, sy as usize).unwrap_or(0) as i32
132}
133
134/// Writes one sample either side of a boundary, in the same coordinates.
135fn put(plane: &mut Plane, kind: Edge, x: usize, y: usize, i: i32, k: usize, v: i32, top: i32) {
136 let (sx, sy) = match kind {
137 Edge::Vertical => (x as i32 + i, (y + k) as i32),
138 Edge::Horizontal => ((x + k) as i32, y as i32 + i),
139 };
140 if sx < 0 || sy < 0 {
141 return;
142 }
143 let w = plane.w;
144 let h = plane.h;
145 let (sx, sy) = (sx as usize, sy as usize);
146 if sx < w && sy < h {
147 plane.px[sy * w + sx] = v.clamp(0, top) as u16;
148 }
149}
150
151/// The brightness plane, one four-line segment of edge at a time.
152fn luma(pic: &mut Picture, edges: &Edges<'_>, kind: Edge, depth: u32) {
153 let (w, h) = (pic.y.w, pic.y.h);
154 let top = (1i32 << depth) - 1;
155 let scale = 1i32 << (depth - 8);
156 // Along the edge in fours, across it in eights: only every eighth line of samples carries a
157 // boundary that is filtered.
158 let (xs, ys): (usize, usize) = match kind {
159 Edge::Vertical => (8, 4),
160 Edge::Horizontal => (4, 8),
161 };
162 let mut y = 0usize;
163 while y < h {
164 let mut x = 0usize;
165 while x < w {
166 let first = match kind {
167 Edge::Vertical => x == 0,
168 Edge::Horizontal => y == 0,
169 };
170 if first || !boundary(edges, kind, x, y) {
171 x += xs;
172 continue;
173 }
174 let qp = across(edges, kind, x, y);
175 let beta = BETA[qp.clamp(0, 51) as usize] * scale;
176 // The boundary strength is two everywhere in a still picture, which adds two to the
177 // index of the other table.
178 let tc = TC[(qp + 2).clamp(0, 53) as usize] * scale;
179 if beta == 0 {
180 x += xs;
181 continue;
182 }
183 // Every sample the segment needs, read before any of them is written -- which is how
184 // the specification states it, and it matters: the strong filter's new p2 must not
185 // feed the same line's new p0.
186 let mut ps = [[0i32; 4]; 4];
187 let mut qs = [[0i32; 4]; 4];
188 for k in 0..4 {
189 for i in 0..4 {
190 ps[k][i] = at(&pic.y, kind, x, y, -(i as i32) - 1, k);
191 qs[k][i] = at(&pic.y, kind, x, y, i as i32, k);
192 }
193 }
194 // The two outer lines of the four decide for all of them, which is what keeps the
195 // filter from following a diagonal edge line by line.
196 let p = |i: usize, k: usize| ps[k][i];
197 let q = |i: usize, k: usize| qs[k][i];
198 let second = |f: &dyn Fn(usize, usize) -> i32, k: usize| {
199 (f(2, k) - 2 * f(1, k) + f(0, k)).abs()
200 };
201 let (dp0, dp3) = (second(&p, 0), second(&p, 3));
202 let (dq0, dq3) = (second(&q, 0), second(&q, 3));
203 let (dpq0, dpq3) = (dp0 + dq0, dp3 + dq3);
204 let (dp, dq) = (dp0 + dp3, dq0 + dq3);
205 if dpq0 + dpq3 >= beta {
206 // The step across this boundary is too big to be an artefact of coding: it is
207 // something in the photograph, and smearing it is the one thing this must not do.
208 x += xs;
209 continue;
210 }
211 let flat = |k: usize, dpq: i32| {
212 dpq < (beta >> 2)
213 && ((p(3, k) - p(0, k)).abs() + (q(0, k) - q(3, k)).abs()) < (beta >> 3)
214 && (p(0, k) - q(0, k)).abs() < ((5 * tc + 1) >> 1)
215 };
216 let strong = flat(0, 2 * dpq0) && flat(3, 2 * dpq3);
217 let thin = (beta + (beta >> 1)) >> 3;
218 let (near_p, near_q) = (dp < thin, dq < thin);
219
220 for k in 0..4 {
221 let (p0, p1, p2, p3) = (p(0, k), p(1, k), p(2, k), p(3, k));
222 let (q0, q1, q2, q3) = (q(0, k), q(1, k), q(2, k), q(3, k));
223 if strong {
224 let clip = |v: i32, was: i32| v.clamp(was - 2 * tc, was + 2 * tc);
225 let _ = (p3, q3);
226 let np0 = clip((p2 + 2 * p1 + 2 * p0 + 2 * q0 + q1 + 4) >> 3, p0);
227 let np1 = clip((p2 + p1 + p0 + q0 + 2) >> 2, p1);
228 let np2 = clip((2 * p3 + 3 * p2 + p1 + p0 + q0 + 4) >> 3, p2);
229 let nq0 = clip((p1 + 2 * p0 + 2 * q0 + 2 * q1 + q2 + 4) >> 3, q0);
230 let nq1 = clip((p0 + q0 + q1 + q2 + 2) >> 2, q1);
231 let nq2 = clip((p0 + q0 + q1 + 3 * q2 + 2 * q3 + 4) >> 3, q2);
232 for (i, v) in [(0i32, np0), (1, np1), (2, np2)] {
233 put(&mut pic.y, kind, x, y, -i - 1, k, v, top);
234 }
235 for (i, v) in [(0i32, nq0), (1, nq1), (2, nq2)] {
236 put(&mut pic.y, kind, x, y, i, k, v, top);
237 }
238 } else {
239 let mut delta = (9 * (q0 - p0) - 3 * (q1 - p1) + 8) >> 4;
240 if delta.abs() >= tc * 10 {
241 continue;
242 }
243 delta = delta.clamp(-tc, tc);
244 put(&mut pic.y, kind, x, y, -1, k, p0 + delta, top);
245 put(&mut pic.y, kind, x, y, 0, k, q0 - delta, top);
246 if near_p {
247 let half = tc >> 1;
248 let d = (((p2 + p0 + 1) >> 1) - p1 + delta) >> 1;
249 put(&mut pic.y, kind, x, y, -2, k, p1 + d.clamp(-half, half), top);
250 }
251 if near_q {
252 let half = tc >> 1;
253 let d = (((q2 + q0 + 1) >> 1) - q1 - delta) >> 1;
254 put(&mut pic.y, kind, x, y, 1, k, q1 + d.clamp(-half, half), top);
255 }
256 }
257 }
258 x += xs;
259 }
260 y += ys;
261 }
262}
263
264/// The two colour planes, which take a simpler filter and only every second boundary.
265///
266/// Colour is coded at half resolution in both directions, so its own eight-sample grid falls on
267/// every sixteenth luma sample; and because a boundary in a still picture is always at full
268/// strength, no other test decides whether to filter.
269fn chroma(pic: &mut Picture, edges: &Edges<'_>, kind: Edge, depth: u32) {
270 let top = (1i32 << depth) - 1;
271 let scale = 1i32 << (depth - 8);
272 let (xs, ys): (usize, usize) = match kind {
273 Edge::Vertical => (16, 8),
274 Edge::Horizontal => (8, 16),
275 };
276 let (w, h) = (pic.y.w, pic.y.h);
277 for c in 0..2usize {
278 let mut y = 0usize;
279 while y < h {
280 let mut x = 0usize;
281 while x < w {
282 let first = match kind {
283 Edge::Vertical => x == 0,
284 Edge::Horizontal => y == 0,
285 };
286 if first || !boundary(edges, kind, x, y) {
287 x += xs;
288 continue;
289 }
290 let qpi = across(edges, kind, x, y) + edges.chroma_offset[c];
291 let tc = TC[(crate::hevc::decode::chroma_qp(qpi.clamp(0, 57)) + 2)
292 .clamp(0, 53) as usize] * scale;
293 if tc == 0 {
294 x += xs;
295 continue;
296 }
297 let plane = if c == 0 { &mut pic.cb } else { &mut pic.cr };
298 let (cx, cy) = (x / 2, y / 2);
299 for k in 0..4 {
300 let p0 = at(plane, kind, cx, cy, -1, k);
301 let p1 = at(plane, kind, cx, cy, -2, k);
302 let q0 = at(plane, kind, cx, cy, 0, k);
303 let q1 = at(plane, kind, cx, cy, 1, k);
304 let d = ((((q0 - p0) << 2) + p1 - q1 + 4) >> 3).clamp(-tc, tc);
305 put(plane, kind, cx, cy, -1, k, p0 + d, top);
306 put(plane, kind, cx, cy, 0, k, q0 - d, top);
307 }
308 x += xs;
309 }
310 y += ys;
311 }
312 }
313}
314
315// Which two neighbours an edge offset compares a sample with (§8.7.3.2, Table 8-13): across,
316// down, and the two diagonals.
317const NEIGHBOURS: [[(i32, i32); 2]; 4] = [
318 [(-1, 0), (1, 0)],
319 [(0, -1), (0, 1)],
320 [(-1, -1), (1, 1)],
321 [(1, -1), (-1, 1)],
322];
323
324/// Runs the sample adaptive offset over a whole picture (§8.7.3).
325///
326/// **It reads the picture as the deblocking filter left it and writes somewhere else.** A sample
327/// that has already been offset must not be what its neighbour is compared against, or the offsets
328/// walk across the picture; the copy is what stops that, and it is why this cannot be done in
329/// place however tempting the memory saving.
330pub fn sao(pic: &mut Picture, per_ctb: &[Sao], ctbs_w: usize, ctb: usize, depth: u32) {
331 let source = pic.clone();
332 let top = (1i32 << depth) - 1;
333 for c in 0..3usize {
334 let (src, dst) = match c {
335 0 => (&source.y, &mut pic.y),
336 1 => (&source.cb, &mut pic.cb),
337 _ => (&source.cr, &mut pic.cr),
338 };
339 // Colour is half size both ways, so its blocks are half as wide and half as tall.
340 let side = if c == 0 { ctb } else { ctb / 2 };
341 if side == 0 {
342 continue;
343 }
344 for (i, sao) in per_ctb.iter().enumerate() {
345 if sao.kind[c] == 0 {
346 continue;
347 }
348 let (rx, ry) = (i % ctbs_w, i / ctbs_w);
349 let (x0, y0) = (rx * side, ry * side);
350 for y in y0..(y0 + side).min(src.h) {
351 for x in x0..(x0 + side).min(src.w) {
352 let here = match src.at(x, y) {
353 Some(v) => v as i32,
354 None => continue,
355 };
356 let offset = if sao.kind[c] == 2 {
357 let pair = NEIGHBOURS[(sao.class[c] as usize).min(3)];
358 let mut idx = 2i32;
359 let mut outside = false;
360 for (dx, dy) in pair {
361 let (nx, ny) = (x as i32 + dx, y as i32 + dy);
362 if nx < 0 || ny < 0
363 || nx as usize >= src.w || ny as usize >= src.h
364 {
365 outside = true;
366 break;
367 }
368 let other = src.at(nx as usize, ny as usize).unwrap_or(0) as i32;
369 idx += (here - other).signum();
370 }
371 if outside {
372 continue;
373 }
374 // Two of the five categories are relabelled so that "the same as both
375 // neighbours" is the one with no offset.
376 let category = if idx <= 2 {
377 if idx == 2 { 0 } else { idx + 1 }
378 } else {
379 idx
380 };
381 if category == 0 {
382 continue;
383 }
384 sao.offset[c][(category - 1) as usize]
385 } else {
386 // A band offset moves four adjacent thirty-seconds of the range and
387 // nothing else.
388 let band = (here >> (depth - 5)) as usize & 31;
389 let start = sao.band[c] as usize;
390 let which = (band + 32 - start) & 31;
391 if which >= 4 {
392 continue;
393 }
394 sao.offset[c][which]
395 };
396 if offset == 0 {
397 continue;
398 }
399 let w = dst.w;
400 if x < w && y < dst.h {
401 dst.px[y * w + x] = (here + offset).clamp(0, top) as u16;
402 }
403 }
404 }
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use oxedyne_fe2o3_core::prelude::*;
413
414 /// A picture of one level throughout, at the given size.
415 fn flat(w: usize, h: usize, level: u16) -> Picture {
416 let plane = |w: usize, h: usize| Plane { w, h, px: vec![level; w * h] };
417 Picture { y: plane(w, h), cb: plane(w / 2, h / 2), cr: plane(w / 2, h / 2), depth: 8 }
418 }
419
420 /// Edges everywhere, at one quantisation parameter.
421 fn all_edges(w: usize, h: usize, qp: i8) -> (Vec<bool>, Vec<bool>, Vec<i8>) {
422 let (gw, gh) = (w / 4, h / 4);
423 (vec![true; gw * gh], vec![true; gw * gh], vec![qp; gw * gh])
424 }
425
426 #[test]
427 fn test_the_thresholds_are_the_published_ones_06() -> Outcome<()> {
428 // A hundred and six numbers out of a document, and one of them wrong by one place moves
429 // every filtering decision above that quantisation parameter. This was written with the
430 // second table shifted from index twenty-six on, and the fault it produced was a filter
431 // that smeared a real edge -- which the test above caught only because that edge was put
432 // there deliberately.
433 //
434 // HEVC_SPEC_TEXT=~/.cache/specs/h265.txt cargo test -p oxedyne_fe2o3_graphics hevc
435 let path = match std::env::var("HEVC_SPEC_TEXT") {
436 Ok(p) => p,
437 Err(_) => {
438 println!(" skipped: set HEVC_SPEC_TEXT to a text rendering of Rec. ITU-T H.265");
439 return Ok(());
440 },
441 };
442 let text = match std::fs::read_to_string(&path) {
443 Ok(t) => t,
444 Err(e) => {
445 println!(" skipped: {} would not read ({})", path, e);
446 return Ok(());
447 },
448 };
449 // The table is printed as three bands of Q, each with a row of β′ under it and a row of
450 // tC′ under that, so the numbers of each arrive in order across the bands.
451 let (mut beta, mut tc): (Vec<i32>, Vec<i32>) = (Vec::new(), Vec::new());
452 let mut found = false;
453 for line in text.lines() {
454 let t = line.trim();
455 if t.starts_with("Table 8-12") && t.contains("threshold") {
456 found = true;
457 continue;
458 }
459 if !found {
460 continue;
461 }
462 if t.starts_with("8.7.2.5.4") {
463 break;
464 }
465 // A row of the table starts with its name; a dash stands for an entry that does not
466 // exist, and there are two of those at the end of the first row.
467 let into = if t.starts_with("β′") {
468 &mut beta
469 } else if t.starts_with("t C′") || t.starts_with("tC′") {
470 &mut tc
471 } else {
472 continue;
473 };
474 for word in t.split_whitespace().skip(1) {
475 if word == "-" || word == "\u{2212}" {
476 continue;
477 }
478 if let Ok(n) = word.parse::<i32>() {
479 into.push(n);
480 }
481 }
482 }
483 if beta.is_empty() || tc.is_empty() {
484 println!(" skipped: Table 8-12 is not in {} in a shape this can read", path);
485 return Ok(());
486 }
487 req!(beta.len(), BETA.len(), "the document lists {} values of the flatness bar", beta.len());
488 req!(tc.len(), TC.len(), "the document lists {} values of the movement bound", tc.len());
489 for (q, want) in beta.iter().enumerate() {
490 req!(BETA[q], *want, "the flatness bar at a parameter of {}", q);
491 }
492 for (q, want) in tc.iter().enumerate() {
493 req!(TC[q], *want, "the movement bound at a parameter of {}", q);
494 }
495 Ok(())
496 }
497
498 #[test]
499 fn test_a_flat_picture_is_left_alone_00() -> Outcome<()> {
500 // The property that catches a filter applied where no step exists: there is nothing to
501 // soften in a picture of one level, so every sample must come back as it went in. A sign
502 // error in the weak filter's delta, or a clip written the wrong way round, shows here.
503 let mut pic = flat(32, 32, 128);
504 let (v, hz, qp) = all_edges(32, 32, 40);
505 let edges = Edges {
506 gw: 8, gh: 8, vertical: &v, horizontal: &hz, qp: &qp, chroma_offset: [0, 0],
507 };
508 deblock(&mut pic, &edges, 8);
509 for (i, s) in pic.y.px.iter().enumerate() {
510 req!(*s, 128u16, "sample {} of a flat picture moved", i);
511 }
512 Ok(())
513 }
514
515 #[test]
516 fn test_a_real_edge_survives_and_a_coding_step_does_not_01() -> Outcome<()> {
517 // The distinction the whole filter exists to make. A boundary with a big step across it is
518 // something in the photograph and must be left; one with a small step is an artefact of
519 // coding and must be softened. Both are put at the same place with the same settings, so
520 // nothing but the size of the step can account for the difference.
521 let mut real = flat(32, 32, 60);
522 for y in 0..32 {
523 for x in 16..32 {
524 real.y.px[y * 32 + x] = 200;
525 }
526 }
527 let mut coded = flat(32, 32, 60);
528 for y in 0..32 {
529 for x in 16..32 {
530 coded.y.px[y * 32 + x] = 64;
531 }
532 }
533 let (v, hz, qp) = all_edges(32, 32, 37);
534 let edges = Edges {
535 gw: 8, gh: 8, vertical: &v, horizontal: &hz, qp: &qp, chroma_offset: [0, 0],
536 };
537 let (was_real, was_coded) = (real.y.px.clone(), coded.y.px.clone());
538 deblock(&mut real, &edges, 8);
539 deblock(&mut coded, &edges, 8);
540
541 let kept = real.y.px[16 * 32 + 15] == was_real[16 * 32 + 15]
542 && real.y.px[16 * 32 + 16] == was_real[16 * 32 + 16];
543 req!(kept, true, "a step of 140 levels was filtered, which smears a real edge");
544 let softened = coded.y.px[16 * 32 + 15] != was_coded[16 * 32 + 15];
545 req!(softened, true, "a step of four levels was left, which is a visible block edge");
546 Ok(())
547 }
548
549 #[test]
550 fn test_nothing_is_filtered_where_there_is_no_boundary_02() -> Outcome<()> {
551 // Only a transform or prediction edge is a boundary. A filter that ran on the eight-sample
552 // grid regardless would soften the inside of every large block, which is a picture that has
553 // been quietly blurred.
554 let mut pic = flat(32, 32, 60);
555 for y in 0..32 {
556 for x in 16..32 {
557 pic.y.px[y * 32 + x] = 64;
558 }
559 }
560 let (gw, gh) = (8usize, 8usize);
561 let (v, hz, qp) = (vec![false; gw * gh], vec![false; gw * gh], vec![37i8; gw * gh]);
562 let edges = Edges {
563 gw, gh, vertical: &v, horizontal: &hz, qp: &qp, chroma_offset: [0, 0],
564 };
565 let was = pic.y.px.clone();
566 deblock(&mut pic, &edges, 8);
567 req!(pic.y.px, was, "a picture with no block boundaries in it was filtered anyway");
568 Ok(())
569 }
570
571 #[test]
572 fn test_a_finely_quantised_picture_is_not_filtered_03() -> Outcome<()> {
573 // Below a parameter of sixteen both thresholds are nought, which is the specification
574 // saying that a picture coded this finely has no steps worth hiding.
575 let mut pic = flat(32, 32, 60);
576 for y in 0..32 {
577 for x in 16..32 {
578 pic.y.px[y * 32 + x] = 64;
579 }
580 }
581 let (v, hz, qp) = all_edges(32, 32, 10);
582 let edges = Edges {
583 gw: 8, gh: 8, vertical: &v, horizontal: &hz, qp: &qp, chroma_offset: [0, 0],
584 };
585 let was = pic.y.px.clone();
586 deblock(&mut pic, &edges, 8);
587 req!(pic.y.px, was, "a picture at a quantisation parameter of ten was filtered");
588 Ok(())
589 }
590
591 #[test]
592 fn test_a_band_offset_moves_its_four_bands_and_no_others_04() -> Outcome<()> {
593 // Four adjacent thirty-seconds of the range move and the rest do not, which is what makes
594 // this a correction to a graded sky rather than a change of exposure.
595 let mut pic = flat(8, 8, 0);
596 for i in 0..64 {
597 // One sample in each of the first eight bands.
598 pic.y.px[i] = (i as u16 % 32) * 8;
599 }
600 let was = pic.y.px.clone();
601 let mut sao = Sao::default();
602 sao.kind[0] = 1;
603 sao.band[0] = 3;
604 sao.offset[0] = [5, 5, 5, 5];
605 super::sao(&mut pic, &[sao], 1, 8, 8);
606 for (i, (before, after)) in was.iter().zip(pic.y.px.iter()).enumerate() {
607 let band = (*before >> 3) & 31;
608 let moved = *after != *before;
609 let wanted = (3..7).contains(&band);
610 req!(moved, wanted, "sample {} in band {} moved {}", i, band, moved);
611 }
612 Ok(())
613 }
614
615 #[test]
616 fn test_an_edge_offset_leaves_a_slope_alone_05() -> Outcome<()> {
617 // The five categories an edge offset sorts samples into: a peak, a valley, the two sides of
618 // a step, and everything else. Everything else gets no offset at all, which is most of a
619 // photograph -- so a ramp must come back untouched while a single spike in it does not.
620 let mut pic = flat(8, 8, 0);
621 for y in 0..8 {
622 for x in 0..8 {
623 pic.y.px[y * 8 + x] = 40 + x as u16 * 5;
624 }
625 }
626 // One sample raised into a peak.
627 pic.y.px[3 * 8 + 4] = 200;
628 let was = pic.y.px.clone();
629 let mut sao = Sao::default();
630 sao.kind[0] = 2;
631 sao.class[0] = 0;
632 sao.offset[0] = [7, 3, -3, -7];
633 super::sao(&mut pic, &[sao], 1, 8, 8);
634 // The slope, away from the edges of the picture where the filter declines.
635 req!(pic.y.px[2 * 8 + 3], was[2 * 8 + 3], "a sample on a slope was offset");
636 let peak_moved = pic.y.px[3 * 8 + 4] != was[3 * 8 + 4];
637 req!(peak_moved, true, "a peak was not offset");
638 Ok(())
639 }
640}