Two-color duotone poster from an AI image with ImageOps.colorize

Turn a Sume image into a duotone: grayscale, autocontrast, then ImageOps.colorize with a dark and a light brand color. Code and ink pairs inside.

5 min readSume
All posts

Convert the image to grayscale, stretch it with ImageOps.autocontrast, and map black and white to two brand colors with ImageOps.colorize(gray, black=..., white=...). That is a duotone. It makes a set of mixed AI images read as one campaign, because every image now uses exactly the same two inks regardless of what the model painted.

Why duotone suits generated images

Different prompts and different models give different palettes. A duotone throws that variation away and keeps only the brightness structure, so a portrait, a product, and a landscape all fit one poster series. It also hides small color errors, such as a slightly wrong skin tone, which the brief never wanted to be exact anyway.

Pick a dark ink and a light ink with enough luminance difference. If the two colors are close in brightness, the picture flattens into mud.

The code

colorize takes a grayscale image and linear-interpolates between black and white colors. An optional mid color adds a third stop, which gives a tritone. Hex strings and color names are accepted.

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
out = gen(model="black-forest-labs/flux.2-pro", aspect_ratio="3:4",
          prompt="Lone lighthouse on a cliff at dusk, dramatic clouds, high contrast")
gray = ImageOps.autocontrast(fetch(out["data"][0]["url"]).convert("L"), cutoff=1)
duo = ImageOps.colorize(gray, black="#1b1f4b", white="#ffd9a0")
duo.save("poster_duotone.png")
tri = ImageOps.colorize(gray, black="#1b1f4b", white="#ffd9a0", mid="#c8456a")
tri.save("poster_tritone.png")
print(duo.size, duo.mode, "cost", out["usage"]["cost"])

Picking the two inks

Test the pair on the light and dark ends before you commit. A rough rule is to keep the dark ink under about 20 percent brightness and the light ink above about 80 percent.

Duotone pairs and their role, colorize arguments read 2026-10-05
Dark inkLight inkMood
#1b1f4b#ffd9a0Dusk, warm highlights
#0b3d2e#e8f5c8Botanical, calm
#2a0a3d#ff9ec7Pop, high energy

Things to watch

  • Run autocontrast first. A flat, dull source makes a flat duotone.
  • Use cutoff=1 to ignore the extreme 1 percent of pixels so a single bright speck does not set the range.
  • A duotone discards hue, so a red product and a green product look alike. Use it for mood images, not product-accuracy shots.
  • Keep the original. Brand teams change their inks.

Check the output against your palette

Because the output only uses colors between your two inks, a delta E check against the endpoints is easy to run. Produce the poster at one size, then derive other widths with the srcset recipe. If you change models later, run your prompts through the regression test to see how the brightness structure shifts.

Sources

Related posts

More in Media tools

All Media tools posts

Written by Sume