Skip to content

Build1 publisher2 min readPublished

A synthetic gradient and four fixed encodes turn banding into a measurement

The test costs nothing but disk time and ranks four debanding treatments on your own source, though it holds quality fixed, so the sweep that finds your weakest ladder rung is still yours to write.

The Engineer · Build desk

Illustration accompanying A synthetic gradient and four fixed encodes turn banding into a measurement

What happened

  • The test clip is synthesised rather than sourced: 10 seconds of a slow 1080p horizontal gradient from FFmpeg's lavfi gradients source, held as a lossless FFV1 master in 10-bit at 25 fps.
  • That master is then encoded four ways, naive 8-bit, 8-bit debanded, a 10-bit filter graph with explicit dither, and 10-bit AV1, all locally in FFmpeg with no assets to download.
  • Judgement is taken out of the loop by amplification, with one frame per encode pushed through eq=contrast=6.0 and stacked into a single comparison.png rather than eyeballed.
  • A loop prints each output's size in KiB, but the author withholds his own byte counts, arguing that a number copied from someone else's blog post is how bad ladder decisions get made.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • contradiction FFmpeg's documentation advises against gradfun before lossy compression, and the tutorial runs it that way deliberately, so the second encode is scripted to fail. Anyone lifting it as the recommended fix has read the demonstration backwards.
  • cost With no published byte counts, the price of each treatment is a measurement every team has to make on its own build, CRF and source before it can argue about ladder bitrates. The measuring is the cost of entry.
  • constraint Three encodes sit at CRF 23 and the AV1 one at CRF 32, with resolution untouched, so the harness ranks treatments at one quality point and cannot by itself name the rung that breaks first. The per-rung sweep is yours to write.
  • exposure Handing a 10-bit pixel format to libx264 quietly produces H.264 High 10, which the author warns not to ship to a browser audience, so the trap sits in a flag people copy without reading.

Banding is visible because the steps are wide, and the generator makes that arithmetic checkable. The master ramps blue from 0x20 to 0x60 across 1920 pixels [2], which is 64 code values in 8 bits, so a single step covers about 30 pixels of screen [18]. At 10 bits the same ramp has 256 codes to spend and the step falls to roughly 7.5 pixels [19]. Nothing in the clip is there to break up those flats, which is the point of synthesising it: real footage carries sensor noise that dithers banding away by accident [4]. The dark blues are deliberate, because banding is worst in the low end, which the author notes is also where nobody looks [3].

gradfun fits the gradient that should be in the flat region and dithers it back in [6]. Its `strength` parameter does two jobs at once. It is the maximum change per pixel and the flat-region detection threshold, on a range of 0.51 to 64 with a default of 1.2 [7]. `radius` is the neighbourhood used for the fit, 8 to 32, default 16, and larger values smooth more while making the filter more reluctant near detail [8]. So turning strength up to correct harder also widens what the filter is willing to call flat.

The third encode is the one worth reading closely, and the author is explicit that the filter is not carrying it. Running the graph at `yuv420p10le` gives gradfun somewhere to put intermediate values instead of re-quantising at every stage; the step back down to 8 bits is taken with `error_diffusion` rather than left as an implicit truncation; and `-tune grain` with `aq-mode=3` stops x264 smoothing away the low-amplitude noise the earlier stages just paid for [10]. According to the author, that last step is where most of the win is and it is the step people skip [11].

The AV1 encode carries no debanding filter at all, because keeping 10-bit output removes the quantisation problem instead of concealing it [12]. That is a difference in kind, not degree. Everything in the x264 chain is dither that has to survive a compressor built to discard exactly that class of detail, and FFmpeg's own documentation says the bands come back when it does not [9].

Two practical notes before you run it. The commands are written against FFmpeg 7.x and 8.x, and the author tells you to check yours with `ffmpeg -version` first [15]. And keep a second master cut from real footage, a fade to black say, because the synthetic clip finds the problem while the real clip tells you whether the fix survives contact with grain [16].

What to watch

  • Whether a follow-up publishes the same four treatments swept across each ladder rung's resolution and bitrate, which is what would actually locate the failing rung.
  • Whether your delivery audience can decode 10-bit AV1 at all, since that decides if the fourth encode is a shipping option or a lab result.
  • Any change to gradfun or to FFmpeg's guidance about using it before lossy compression, which would change what the second encode is for.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories