Check an AI pattern tiles seamlessly with ImageChops.offset

Shift a Sume pattern image by half its size with ImageChops.offset and measure the seam stripe. A pass or fail score before you ship a repeating background.

5 min readSume
All posts

Wrap the image by half its width and height with ImageChops.offset(img, w // 2, h // 2), which moves the old edges to the center cross, then compare pixels across that cross with their neighbors. A tile that repeats cleanly has no visible step there. A tile that does not has a hard line, and the score below turns that line into a number.

Why prompts do not guarantee a tile

Asking for a seamless pattern makes an image model draw a pattern, but not one whose left edge matches its right edge. Some models come close, many do not. Sume's image models do not have a tile parameter in the catalog, so you check the result yourself.

The offset trick is the standard check in texture work: put the seam where you can see it. If you can see it, a visitor will, once the tile repeats across a page.

Score the seam

After the offset, the old edges sit on the middle row and middle column. The code takes a thin strip across each and compares it with a strip just beside it using ImageChops.difference, then reads the mean with ImageStat. The comparison strip is 4 pixels from the seam.

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, ImageStat
out = gen(model="black-forest-labs/flux.2-pro", aspect_ratio="1:1",
          prompt="Seamless repeating pattern of small blue paper boats on cream, flat design")
img = fetch(out["data"][0]["url"]).convert("L")
w, h = img.size
sh = ImageChops.offset(img, w // 2, h // 2)
def strip_diff(box_a, box_b):
    d = ImageChops.difference(sh.crop(box_a), sh.crop(box_b))
    return ImageStat.Stat(d).mean[0]
vert = strip_diff((w // 2 - 1, 0, w // 2 + 1, h), (w // 2 + 3, 0, w // 2 + 5, h))
horz = strip_diff((0, h // 2 - 1, w, h // 2 + 1), (0, h // 2 + 3, w, h // 2 + 5))
base = ImageStat.Stat(ImageChops.difference(sh.crop((0, 0, w - 4, h)), sh.crop((4, 0, w, h)))).mean[0]
print("seam v", round(vert, 1), "h", round(horz, 1), "baseline", round(base, 1), "cost", out["usage"]["cost"])
print("PASS" if max(vert, horz) < base * 1.5 + 2 else "SEAM VISIBLE")
sh.save("offset_view.png")

Reading the numbers

Compare the seam score with the baseline, which is how much neighboring columns differ elsewhere in the image. Busy patterns have a high baseline, so a fixed threshold would fail them wrongly.

Seam score interpretation, rule of thumb read 2026-10-05
Seam vs baselineMeaningAction
Under 1.5xNo visible stepShip the tile
1.5x to 3xFaint lineBlend the seam or regenerate
Over 3xHard cutRegenerate with a simpler pattern

If the tile fails

  • Ask for a simpler, evenly spaced motif. Large single objects and gradients cross edges badly.
  • Open offset_view.png and look. The score finds the seam, but your eyes decide if it matters.
  • Fix small seams by cloning: paste a blurred strip over the cross. For big seams, regenerate.
  • Try a different model before a different prompt, since the same text gives a different edge behavior on each.

Next steps

Add the check to your prompt regression test so a model swap flags new seams. Once a tile passes, export the widths you need with the srcset recipe, and save only lossless files during repair, per PNG between passes.

Sources

Related posts

More in Media tools

All Media tools posts

Written by Sume