Fix banding in an AI image gradient by adding light noise in Pillow

Visible steps in a sky or studio backdrop from a Sume image? Add one level of Gaussian noise before the JPEG save to break the bands. Code and amount table.

5 min readSume
All posts

Banding is steps in a smooth gradient, caused by only 256 levels per channel. Break the steps by adding Gaussian noise with a standard deviation of about 1 to 2 levels, using Image.effect_noise blended over the image, before you save to JPEG. The noise is hardly visible, and the eye stops seeing contour lines.

Where the steps come from

A long gradient, such as a studio backdrop that runs from dark to light, has to use whole numbers of brightness. A change of 20 levels across 1,000 pixels means one level per 50 pixels, so each level appears as a band 50 pixels wide. On a bright screen, that is visible.

Models draw smooth backdrops often, and the ask for soft light or a seamless studio sweep invites it. JPEG compression can make it worse, since it quantizes smooth areas hard.

Dither with noise

Image.effect_noise(size, sigma) makes a mode L image of Gaussian noise centered at 128. Subtract 128 and add the result to each channel. A sigma of 1.5 moves values by roughly one or two steps, enough to break the band edge without visible grain.

import os, io, requests
from PIL import Image
H = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}
def gen(**body):
    r = requests.post("https://api.sume.com/v1/images", json=body, headers=H, timeout=60)
    r.raise_for_status()
    if r.status_code == 202:
        raise SystemExit("queued, read /v1/jobs/{id}/result: " + r.text)
    return r.json()
def fetch(u):
    return Image.open(io.BytesIO(requests.get(u, timeout=60).content))
from PIL import ImageChops
out = gen(model="black-forest-labs/flux.2-pro", aspect_ratio="16:9",
          prompt="Seamless studio backdrop, smooth dark teal to light teal gradient, empty")
img = fetch(out["data"][0]["url"]).convert("RGB")
noise = Image.effect_noise(img.size, 1.5)
offset = Image.new("L", img.size, 128)
bands = []
for ch in img.split():
    up = ImageChops.add(ch, noise, scale=1, offset=-128)
    bands.append(up)
dithered = Image.merge("RGB", bands)
dithered.save("backdrop_dithered.jpg", quality=92)
img.save("backdrop_plain.jpg", quality=92)
print("cost", out["usage"]["cost"])

How much noise

More noise hides more banding and shows more grain. Start low and view at 100 percent zoom on a good display.

Noise sigma guide, values read 2026-10-05
SigmaBandingVisible grain
0.8Reduced a littleNone
1.5Mostly hiddenBarely at 100 percent
3.0HiddenVisible on flat areas

Notes

  • Add the noise before the final JPEG save, not after. JPEG will smooth part of it, so do not judge on a PNG copy.
  • Use a higher JPEG quality on gradient-heavy images. Quality 90 or more keeps the noise.
  • Do not add noise before an edit chain. Add it as the last step on the delivery copy.
  • WebP and AVIF also quantize smooth areas, so the same trick helps there.

Related

Contrast stretches can make bands worse, so run the autocontrast fix first and dither last. Keep lossless files between steps with PNG between passes. Print is a different problem, see the CMYK note, where banding shows as visible rings on smooth areas.

Sources

Related posts

More in Media tools

All Media tools posts

Written by Sume