Hide the seam in an AI image edit with a feathered composite in Pillow
A hard-edged paste of an AI edit leaves a visible line. Blur the mask a few pixels so the edit fades into the original. Short Pillow script and settings.

When you paste an AI-edited region back over your original, blur the mask edge by a few pixels. A hard rectangle leaves a visible line wherever the edited color or exposure differs even slightly from the original, and a feathered edge fades the two together. A Gaussian blur of 8 to 16 pixels on the mask, applied before compositing, is usually enough for a 1500 to 2500 pixel image.
FLUX 3 Image advertises edits that leave the rest of the picture unchanged. On a model that does not promise it, compositing the original back is how you make that guarantee yourself, and feathering is what keeps the guarantee from showing.
The script
composite_feathered loads the original and the edit, resizes the edit to the original's size if the API returned a different one, draws a white box on a black mask, blurs it, and composites with Pillow. The demo prints three pixel values: one far outside the box (original), one in the middle (edit) and one on the boundary, which lands in between.
from PIL import Image, ImageFilter
def composite_feathered(original_path, edited_path, box_px, out_path, feather=12):
"""Keep the original everywhere except box_px, blending the seam over `feather` pixels."""
orig = Image.open(original_path).convert("RGB")
edit = Image.open(edited_path).convert("RGB")
if orig.size != edit.size:
edit = edit.resize(orig.size, Image.LANCZOS)
mask = Image.new("L", orig.size, 0)
mask.paste(255, box_px)
mask = mask.filter(ImageFilter.GaussianBlur(feather))
Image.composite(edit, orig, mask).save(out_path)
if __name__ == "__main__":
Image.new("RGB", (400, 300), (200, 180, 160)).save("orig.png")
Image.new("RGB", (400, 300), (30, 30, 30)).save("edited.png")
composite_feathered("orig.png", "edited.png", (100, 100, 250, 200), "blend.png")
im = Image.open("blend.png")
print(im.getpixel((50, 50)), im.getpixel((175, 150)), im.getpixel((100, 150)))Choosing the feather
Feather scales with image size and with how different the two sides are. Too little and you see an edge; too much and the edit bleeds into areas you wanted to protect. Start from the table, then check on a real result at 100 percent.
| Image width | Starting feather (px) | Notes |
|---|---|---|
| 1024 | 6 to 8 | Small images need less |
| 2048 | 10 to 14 | Typical product still |
| 3840 | 16 to 24 | Large; verify the interior is untouched |
Where it breaks
Feathering softens a seam; it cannot fix a mismatch. If the edit changed the lighting direction or white balance across the whole frame, the blend will show a gradient of two color casts. In that case, either mask a larger area or ask for the edit with the original lighting named explicitly in the prompt. Size matters too: if the edit comes back at a different size, resizing it can shift the content by a pixel or two, so request the same aspect ratio as the source.
The shape of the mask need not be a rectangle. A hand-drawn or detected outline blurred the same way works better for round objects, since a rectangle crosses background the model may have subtly altered.
A quick visual check
Look at the result with a high-contrast overlay: subtract the composite from the original and amplify the difference. The difference should be zero outside the box, fade smoothly across the feather and show the edit inside. If you see a bright outline right at the feather's outer limit, the blur was too small for the color gap. If you see differences far from the box, the edit changed the whole frame, and you should return to the original lighting or mask more of the scene.
A note on alpha and bit depth
The script converts both images to RGB, which discards any alpha channel. If your edit must keep transparency, composite in RGBA instead and blur the mask the same way. Likewise, keep the master in PNG, not JPEG, so that the original side of the blend is not a re-compressed copy. Save the final blend as PNG as well, and convert to JPEG only for delivery.
Related steps
To confirm that nothing outside the box moved, diff the result against the original as in the composite-back post. For layered work from transparent PNGs, see editable layers with Pillow. Masks are accepted by ChatGPT Image 2.5 on Sume; see the Image API docs for mask_url.
Sources
Related posts
More in Developers
- Home Assistant response_variable: read a Sume job id and status
Home Assistant rest_command returns status, content and headers in response_variable. Here is how to pull the Sume job id from it and branch on the status read.
- Home Assistant rest_command 10-second timeout and Sume async jobs
rest_command times out at 10 seconds by default. Sume sync waits cap at 30. Submit with mode async, take the job id, and poll in a second command.
- How many characters is one minute of TTS audio? 1,200 s math
A vendor rule of thumb says a minute of speech is 750-800 characters. Here is what that means for a 1,200-second Sume TTS job and its 20,000-char cap.
- Idempotency-Key over 255 characters: hash long business keys
Sume accepts Idempotency-Key values up to 255 characters. Keep readable keys when short and fall back to a prefixed SHA-256 for long ones. Runnable Python.
Written by Sume