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 | |
| 29 | use 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. |
| 42 | const 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. |
| 51 | const 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)] |
| 62 | pub 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. |
| 68 | pub 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. |
| 86 | pub 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? |
| 94 | fn 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. |
| 107 | fn 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. |
| 123 | fn 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. |
| 135 | fn 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. |
| 152 | fn 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. |
| 269 | fn 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. |
| 317 | const 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. |
| 330 | pub 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)] |
| 410 | mod 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 | } |