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.

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.
| Sigma | Banding | Visible grain |
|---|---|---|
| 0.8 | Reduced a little | None |
| 1.5 | Mostly hidden | Barely at 100 percent |
| 3.0 | Hidden | Visible 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
- Fix the text in a reference image before it goes into an H3 clip
Spend one cheap Ideogram 4.5 edit on a reference image's text before you spend on a MiniMax H3 clip that copies it. The order on Sume, with a request.
- Godot MP3 loop: begin point only, so loop a Sume track as wav
Godot gives MP3 and Ogg only a loop begin point, no loop end. For a Sume BGM loop, cut a sample-exact wav with timeline audio split and set the loop on import.
- Graduation video music: a 2-minute anthem with a build
Brief a 2-minute graduation video bed with a build to a triumphant moment at 1:20 for the diploma reel: one generation and a render, $0.325 on Sume.
- Grayscale value check for AI product images: does it read at a glance?
Convert a Sume image to grayscale, measure the contrast between product and background with ImageStat, and flag low-value-contrast images before they ship.
Written by Sume