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.

5 min readSume
All posts

Convert to grayscale and compare the mean brightness of a center box, where the product usually sits, with the mean of the border strip around it. If the two means differ by less than about 40 out of 255, the product will blend into the background on a phone or in print, whatever its hue. The check takes under ten lines and finds problems that a color check misses.

Why value, not hue

Designers squint at an image to judge its value structure: light against dark. Two colors with very different hues, such as a red mug on a green cloth, can have the same brightness. In grayscale the mug vanishes. Color-blind viewers, thumbnails, and black-and-white print all see that grayscale version.

A brand color check measures color distance. It will happily pass the red-on-green image. A value check catches it.

A rough measure in code

The script takes the central 50 percent box as the subject area and the outer 10 percent frame as the background. It is a rough proxy, since the subject is not always in the center, so use it to flag and not to reject outright.

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 ImageOps, ImageStat
out = gen(model="google/nano-banana-2", aspect_ratio="1:1",
          prompt="Red ceramic mug on a green tablecloth, flat lay, product photo")
g = ImageOps.grayscale(fetch(out["data"][0]["url"]).convert("RGB"))
w, h = g.size
center = g.crop((w // 4, h // 4, 3 * w // 4, 3 * h // 4))
frame = [g.crop((0, 0, w, h // 10)), g.crop((0, 9 * h // 10, w, h)),
         g.crop((0, 0, w // 10, h)), g.crop((9 * w // 10, 0, w, h))]
bg = sum(ImageStat.Stat(f).mean[0] for f in frame) / 4
fg = ImageStat.Stat(center).mean[0]
gap = abs(fg - bg)
print("subject", round(fg), "background", round(bg), "gap", round(gap), "cost", out["usage"]["cost"])
print("LOW VALUE CONTRAST" if gap < 40 else "OK")

Reading the gap

Treat the thresholds as a starting point and tune them on images your team has already judged by eye.

Value gap guide, rule of thumb read 2026-10-05
Gap (0-255)Reads asAction
Under 25Blends inRegenerate with a lighter or darker backdrop in the prompt
25 to 40WeakReview by eye
Over 40ClearPass

Make the prompt help

  • Name the value relation: dark product on a light backdrop, or the reverse.
  • Avoid mid-tone backdrops with mid-tone products.
  • Add rim light or a backlight for dark-on-dark scenes.
  • Run the check on the thumbnail size your page uses, since small sizes lose subtle differences first.

Use it with the other checks

Value contrast is one gate. Add the delta E brand check for color, the duplicate finder to drop repeats, and keep them all in the regression test. A failed gate costs only the next call, which is a few cents on most Sume image models.

Sources

Related posts

More in Media tools

All Media tools posts

Written by Sume