Skip to main content
Lesson 3 of 12 · Image Generation via API: GPT Image

Edit existing images (inpainting, outpainting)

Lesson 3 of 12 Updated November 2, 2026 Developer
On this page
  1. Edit existing images
  2. How the mask works
  3. Inpainting: Replace an element
  4. Outpainting: Extend the image
  5. Iterative editing
  6. Key constraints

Updated November 2, 2026 — Image models. gpt-image-1, the model this course was originally written for, was deprecated on October 23, 2026. Additionally, gpt-image-1.5, gpt-image-1-mini, and chatgpt-image-latest will be retired on December 1, 2026. Examples now use gpt-image-2.5-sunburst, which offers the highest precision for editing; gpt-image-2.5-flare is faster for general generation. Both models also support xhigh and max quality settings. GPT Image models always return images in base64: the response_format parameter used by older DALL·E models is no longer accepted and has been removed from the code. Source: Image generation (OpenAI).

Edit existing images

The client.images.edit() method modifies an existing image using a mask to define the area to regenerate. This leads to two distinct use cases, which are worth naming precisely because they require different masks. Inpainting fills or replaces an interior area—you might change the sky in a landscape, the screen on a computer, or the color of a wall. Outpainting, by contrast, extends the image beyond its original borders, such as turning a square portrait into a panoramic banner.

How the mask works

The mask is a PNG image with the same dimensions as the source image. Transparent areas (alpha = 0) indicate where the model should generate new content; opaque areas remain untouched. This convention is counterintuitive at first: you naturally want to “paint” what you want to change, but you actually need to cut a hole in the mask instead. The API call itself remains simple, requiring only the two files and a description of what should appear.

import base64
from openai import OpenAI

client = OpenAI()

response = client.images.edit(
    model="gpt-image-2.5-sunburst",
    image=open("desk_photo.png", "rb"),
    mask=open("desk_mask.png", "rb"),
    prompt="A computer screen displaying a modern analytics dashboard"
)

with open("edited_desk.png", "wb") as f:
    f.write(base64.b64decode(response.data[0].b64_json))

You don’t need a graphics editor to create this mask. Pillow can generate one in just a few lines: start with a fully opaque layer matching the source dimensions, then draw the shape you want to make transparent. The script below creates a rectangular window in the center of the image.

from PIL import Image, ImageDraw

# Load the source image to get its dimensions
source = Image.open("desk_photo.png")
width, height = source.size

# Create a fully opaque mask (black)
mask = Image.new("RGBA", (width, height), (0, 0, 0, 255))
draw = ImageDraw.Draw(mask)

# Draw a transparent area (the area to edit)
# Rectangle in the center of the image
x1, y1 = width // 4, height // 4
x2, y2 = 3 * width // 4, 3 * height // 4
draw.rectangle([x1, y1, x2, y2], fill=(0, 0, 0, 0))

mask.save("desk_mask.png")
print(f"Mask created: {width}x{height}, editable area in center")

Inpainting: Replace an element

Once you repeat this operation, it makes sense to encapsulate mask creation and the API call into a single function, parameterized by the region to modify. The prompt should then describe only the content expected within the masked area, never the rest of the image. The example below applies this function to a landscape photograph, modifying only the top 400 pixels—the sky—to give it a stormy atmosphere.

from openai import OpenAI
from PIL import Image, ImageDraw
import base64

client = OpenAI()

def inpaint_region(image_path, region, prompt, output_path):
    """
    Replace a rectangular region in an image.
    region: tuple (x1, y1, x2, y2) in pixels
    """
    # Create the mask
    source = Image.open(image_path)
    mask = Image.new("RGBA", source.size, (0, 0, 0, 255))
    draw = ImageDraw.Draw(mask)
    draw.rectangle(region, fill=(0, 0, 0, 0))
    mask.save("temp_mask.png")

    # Call the API
    response = client.images.edit(
        model="gpt-image-2.5-sunburst",
        image=open(image_path, "rb"),
        mask=open("temp_mask.png", "rb"),
        prompt=prompt
    )

    # Save the result
    with open(output_path, "wb") as f:
        f.write(base64.b64decode(response.data[0].b64_json))
    print(f"Edited image saved: {output_path}")


# Example: Replace the sky in a landscape photo
inpaint_region(
    image_path="landscape.png",
    region=(0, 0, 1024, 400),  # Top part = the sky
    prompt="Dramatic sky with purple storm clouds at sunset",
    output_path="landscape_sky_edited.png"
)

Outpainting: Extend the image

Enlarging an image requires an extra step because the API cannot generate content “next to” a file directly. You must first create a larger canvas, paste the original onto it, and provide a mask that protects exactly the area occupied by that original. The entire logic hinges on calculating the new dimensions and the paste position based on the requested direction.

from openai import OpenAI
from PIL import Image
import base64
from io import BytesIO

client = OpenAI()

def outpaint(image_path, direction, extra_pixels, prompt, output_path):
    """
    Extend an image in a given direction.
    direction: 'right', 'bottom', 'left', 'top'
    extra_pixels: number of pixels to add
    """
    source = Image.open(image_path).convert("RGBA")
    w, h = source.size

    # Calculate new dimensions
    if direction == "right":
        new_size = (w + extra_pixels, h)
        paste_pos = (0, 0)
    elif direction == "bottom":
        new_size = (w, h + extra_pixels)
        paste_pos = (0, 0)
    elif direction == "left":
        new_size = (w + extra_pixels, h)
        paste_pos = (extra_pixels, 0)
    elif direction == "top":
        new_size = (w, h + extra_pixels)
        paste_pos = (0, extra_pixels)

    # Create the extended canvas (transparent = area to generate)
    extended = Image.new("RGBA", new_size, (0, 0, 0, 0))
    extended.paste(source, paste_pos)

    # The mask: transparent everywhere except where the original image is placed
    mask = Image.new("RGBA", new_size, (0, 0, 0, 0))
    opaque = Image.new("RGBA", (w, h), (0, 0, 0, 255))
    mask.paste(opaque, paste_pos)

    # Save temporarily
    extended.save("temp_extended.png")
    mask.save("temp_mask_outpaint.png")

    response = client.images.edit(
        model="gpt-image-2.5-sunburst",
        image=open("temp_extended.png", "rb"),
        mask=open("temp_mask_outpaint.png", "rb"),
        prompt=prompt
    )

    with open(output_path, "wb") as f:
        f.write(base64.b64decode(response.data[0].b64_json))
    print(f"Extended image saved: {output_path}")


# Extend a portrait to the right
outpaint(
    image_path="portrait.png",
    direction="right",
    extra_pixels=512,
    prompt="Natural continuation of the scene, same lighting and background",
    output_path="portrait_extended.png"
)

Note the wording of the extension prompt: “natural continuation of the scene, same lighting and background.” In outpainting, you aren’t describing a new subject; you’re asking the model to extend what it already sees. An overly creative prompt would produce a visible seam between the original and the added area.

Iterative editing

Nothing stops you from chaining multiple edits on the same image, using each output as the input for the next step. This is the most reliable way to build a complex composition: handle the background first, then the foreground, with a dedicated mask for each pass, rather than hoping a single prompt will fix everything at once.

import base64
from openai import OpenAI

client = OpenAI()

def edit_step(image_path, mask_path, prompt, output_path):
    response = client.images.edit(
        model="gpt-image-2.5-sunburst",
        image=open(image_path, "rb"),
        mask=open(mask_path, "rb"),
        prompt=prompt
    )
    with open(output_path, "wb") as f:
        f.write(base64.b64decode(response.data[0].b64_json))
    return output_path

# Step 1: Modify the background
step1 = edit_step(
    "scene.png", "mask_background.png",
    "Lush tropical forest background",
    "scene_step1.png"
)

# Step 2: Add an element to the foreground
step2 = edit_step(
    step1, "mask_foreground.png",
    "A colorful macaw parrot perched on a branch",
    "scene_step2.png"
)

print(f"Final result: {step2}")

Key constraints

Four technical rules govern the success of the call. Violating any of them results in an immediate error rather than a degraded result.

  • The source image and mask must have the same dimensions
  • Both files must be in PNG format
  • The maximum size per file is 4 MB
  • The mask uses the alpha channel to define editable areas

One empirical observation adds to these constraints: results are more consistent when the masked area represents less than 50% of the image. Beyond that, the model has too little context to blend correctly, and you will often get better results by splitting your modification into two successive edits.

Put this into practice with a photo of your desk or any royalty-free image: write a script that automatically generates a mask over the top quarter of the image, replaces that area with a starry sky, and saves the result locally.