Photo to halftone pop art: a $0.025 Qwen edit plus Pillow dots

Make a pop-art halftone from a photo: a flat-colour Qwen Image edit at $0.025 on Sume, then a short Pillow script that draws the dot grid in code.

5 min readSume
All posts

A halftone pop-art image is two jobs: flatten the photo into bold colour areas, then print it as dots. Do the first with a cheap edit model and the second in code. On Sume, Qwen Image (qwen/qwen-image) is tied with Grok Imagine for the lowest-priced edit-capable row, at $0.025 an image, and a Pillow script draws the dots for free.

Image models often draw dots irregularly, vary the dot size and lose the grid when you scale. A dot grid is simple arithmetic, so code does it better. Details here come from the Image API page and Sume's image catalog, read on 2026-10-11.

Step one: flatten the photo

Send the photo as a reference and ask for few colours, hard edges and no gradients. Qwen Image lists these ratios: 1:1, 16:9, 9:16, 4:3, 3:4, 4:5, 5:4, 3:2, 2:3, 21:9, 9:21, 1:2 and 2:1. It has no auto value, so send the ratio of your photo. Qwen Image accepts up to 10 references and n up to 4.

import os, requests

r = requests.post(
    "https://api.sume.com/v1/images",
    headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
    json={
        "model": "qwen/qwen-image",
        "prompt": "Pop art poster version of this photo: four flat colours, hard edges, high contrast, no gradients, keep the composition",
        "input_references": [
            {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
        ],
        "aspect_ratio": "4:5",
        "n": 2,
    },
    timeout=60,
)
r.raise_for_status()
body = r.json()
if r.status_code == 202:
    print("still running:", body["data"]["status_url"])
else:
    for img in body["data"]:
        print(img["url"])

Step two: the dots

The script below turns any image into black dots on white paper. Dot radius follows the darkness of each cell, which is how a newspaper halftone works. For a coloured version, run it once per channel with an offset, but start with greyscale.

from PIL import Image, ImageDraw

def halftone(src, dst, cell=14):
    gray = Image.open(src).convert("L")
    w, h = gray.size
    out = Image.new("L", (w, h), 255)
    draw = ImageDraw.Draw(out)
    for y in range(0, h, cell):
        for x in range(0, w, cell):
            data = list(gray.crop((x, y, x + cell, y + cell)).getdata())
            dark = 1 - sum(data) / len(data) / 255
            r = dark * cell * 0.72
            cx, cy = x + cell / 2, y + cell / 2
            draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=0)
    out.save(dst, "PNG")

halftone("flat.png", "halftone.png")

What it costs

Two takes of one photo cost 2 x $0.025 = $0.05; the dot script costs nothing. A set of 40 posters at two takes each is 80 x $0.025 = $2.00. Compare the other edit rows if the Qwen takes look weak:

Price per image of edit-capable Sume rows (catalog read 2026-10-11)
Sume model idPrice per imageRatio list has `auto`
qwen/qwen-image$0.025No
bytedance-seed/seedream-5-lite$0.04375No
black-forest-labs/flux.2-pro$0.0375No
google/nano-banana-2.1$0.10 at 1KYes

Tuning the look

The cell size sets the look more than the model does. A 14 px cell on a 1536 px edge gives about 110 dots across, a classic comic-book feel; 24 px gives a bold poster feel; below 8 px the dots turn into grain. If you print big, render the dot layer at the print size, not at the model's size, since dots drawn in code scale without softening.

Because the dot pass only reads brightness, a flat-colour edit with strong value contrast works better than a pastel one. If the output looks washed, raise the contrast in the prompt ("deep shadows, bright highlights") rather than in code.

Why the dots live in code

Doing the dot grid in code has practical benefits beyond looks. The output size is yours: render the halftone at 3000 px for print even if the model returned 1536 px, and the dots stay crisp because they are vector-like ellipses, not model pixels. The result is repeatable: the same input and cell size give byte-identical dot layers, so a series of posters matches. And it is free to iterate: changing cell from 14 to 24 costs nothing, while asking a model for "bigger dots" costs another generation.

For a two-colour print such as magenta dots on yellow paper, convert the L image into a mask and paste a solid colour through it, as the linocut script on this site does with ink and paper. For true CMYK-style separation, run the script on each channel of the flat-colour image with the cell grid rotated by a different angle (15, 75, 0 and 45 degrees are the traditional values), then merge the layers. That is more code, but still no extra model calls.

A response above 30 seconds comes back as a 202 job, which the sample code prints as a status URL; follow Jobs and results to fetch the images. Reference URLs must be public HTTPS.

One more practical point: a halftone only reads well when the source has strong value contrast, so check the flat-colour edit at thumbnail size first. If you cannot tell the subject at 200 px wide, the dots will not rescue it, and a second take costs $0.025 against the cost of printing a poster you do not like.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume