Edit one region of an AI image and keep every other pixel

A mask edit is guidance, not a guarantee. Paste the original back outside the mask with Pillow so untouched pixels stay identical. Python with Sume's mask_url.

5 min readSume
All posts

To change one part of an AI image and keep the rest identical, do not rely on the model to leave the rest alone. Run the masked edit, then paste the original image back everywhere outside the mask with an image library, so those pixels are the same by construction. OpenAI's guide says GPT Image treats a mask as guidance and "may not follow its exact shape with complete precision", which is why a model-only edit can drift outside the box.

The recipe below works on Sume's ChatGPT Image 2.5 rows, the ones with a mask_url. It needs the image and mask at public HTTPS URLs and about twenty lines of Python with Pillow.

Why does the rest of the picture change at all?

Image models regenerate the whole canvas. A mask tells the model where to concentrate, and OpenAI's wording is that masking is entirely prompt-based and that the mask is used as guidance. Press coverage of FLUX 3 Image says the new model's multi-step edits leave other parts of the image alone (The Decoder, reported); that is a model property you could test on a FLUX 3 host, and it is not something Sume's catalog offers today.

Compositing is the portable fix. It does not care which model made the edit, and it gives you an exact statement you can check: outside the mask, the output file equals the input file.

What does the mask need to look like?

OpenAI's guide says the mask must contain an alpha channel, and that the image and mask must be the same format and size and under 50 MB. If you send several images, the mask applies to the first one. Sume's docs list mask_url as a public HTTPS URL for ChatGPT Image 2.5 edits and say nothing more about its format. The guide excerpt I read does not say which polarity marks the edit area; the common convention is that fully transparent pixels mark the area to change. Run one cheap quality: low test and flip the script's EDIT_ALPHA constant if the wrong area changes.

Mask edit rules, read 2026-10-03
RuleSourceConsequence for the script
Mask has an alpha channelOpenAI image guideSave the mask as RGBA PNG
Image and mask same size and formatOpenAI image guideBuild the mask from the image's own size
Mask applies to the first imageOpenAI image guidePut the edited image first in input_references
mask_url is a public HTTPS URLSume Image API docsHost the mask before the call
Up to 16 reference imagesfal edit page, Sume catalogPlenty of room for extra references

The script: edit, then paste the original back

The code downloads the original and the mask, asks Sume for the edit, resizes the result to the original's size if the model returned a different one, and composites with the mask's own alpha as the blend weight. A two-pixel blur softens the seam; set it to zero for a hard edge. If Sume answers with a 202 job, the script stops and prints the status URL, because the result is not ready yet.

import io, os, requests
from PIL import Image, ImageFilter
ORIG = "https://example.com/photo.png"
MASK = "https://example.com/mask.png"
EDIT_ALPHA = 0  # alpha value that marks the area to change
load = lambda u: Image.open(io.BytesIO(requests.get(u, timeout=60).content))
r = requests.post(
    "https://api.sume.com/v1/images",
    headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
    json={
        "model": "openai/gpt-image-2.5",
        "prompt": "replace the mug with a blue enamel mug, same lighting",
        "input_references": [{"type": "image_url", "image_url": {"url": ORIG}}],
        "mask_url": MASK,
        "aspect_ratio": "auto",
    },
    timeout=90,
)
r.raise_for_status()
if r.status_code == 202:
    raise SystemExit("job running: " + r.json()["data"]["status_url"])
orig = load(ORIG).convert("RGBA")
edited = load(r.json()["data"][0]["url"]).convert("RGBA").resize(orig.size)
alpha = load(MASK).convert("RGBA").getchannel("A")
region = alpha.point(lambda a: 255 if a == EDIT_ALPHA else 0).filter(ImageFilter.GaussianBlur(2))
Image.composite(edited, orig, region).save("final.png")

Why not just ask for a tighter mask?

A tighter mask helps, and a perfect one still does not guarantee identical pixels, because the model returns a freshly generated image rather than a patch. The returned file may also come back at a different size from your input, which is why the script resizes it before blending. If you let an auto aspect ratio follow the reference, Sume's docs say the output matches the reference shape, but pixel dimensions are not promised, so the resize guard stays.

The practical rule is to treat the model output as raw material for the masked area only. Everything outside it is yours: the original file, untouched, plus the seam you chose.

How do I check that nothing else moved?

Subtract the final from the original and look only outside the mask. With the blur set to zero, every pixel where the mask was not marked should be exactly equal. With a two-pixel blur, expect a thin band at the seam to differ; that band is by design.

Compositing has a cost: if the edit changed global light, such as a new lamp that should cast a glow across the desk, the pasted-back original will not show it, and you will see a hard boundary. In that case widen the mask to include the area the change should affect, or accept the model's drift outside the box.

When is this the wrong tool?

Do not composite when the edit is meant to change the whole frame, such as relighting or a style change. For object removal the mask_url object remover post shows the same call with a removal prompt, and GPT Image mask leaks covers what OpenAI says about edits that spill. The mask preflight script checks size and alpha before you spend a call.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume