Skip to content
lumalabs.ai
Images

Image layering

Decompose an image into ordered RGBA layers with the Luma Agents API — layer extraction with real transparency on uni-1.

Decompose an existing image into up to 10 ordered RGBA layers — each a standalone PNG with real transparency, stacked front-to-back so the original composition can be reassembled or re-edited layer by layer. The uni-1 model analyzes the image, plans the split automatically, and extracts each layer as a complete element. For modifying an image in place, see Image editing.

Use type: "layering" when you want to take an image apart — not change it. Each extracted layer is a complete element (the model fills in content hidden behind foreground objects), so layers can be moved, restyled, or recomposed independently.

ScenarioUseWhy
Separate a subject from its backgroundlayeringBoth come back as standalone RGBA images
Build animatable / parallax scenes from a stilllayeringOrdered depth layers, background last
Prepare design comps for per-element editinglayeringEach element is its own transparent PNG
Change something in the image (sky, color, style)image_edit + sourceEdits in place — see Image editing
  1. You submit a source image (and optionally a short prompt guiding how to split it).
  2. The server plans the layers automatically — a vision model analyzes the image before generating, deciding how many layers to extract and what goes in each.
  3. Each planned layer is generated as a full RGBA image. Occluded regions are completed, so every layer is a whole element rather than a cutout with holes behind it.
  4. The completed generation returns N outputs (up to 10), ordered front-to-back — the last output is always the opaque background. Each layer PNG is cropped to its visible content, and its layer.bounds carries the geometry to place it back on the composite canvas.

A layering request requires type: "layering" and a source image. The prompt field is required by the request schema — like every generation request — but for layering it may be an empty string.

from luma_agents import Luma
client = Luma()
generation = client.generations.create(
type="layering",
model="uni-1",
prompt="separate the subject from the background",
source={"url": "https://example.com/product-shot.jpg"},
)

The source field takes the same shape as image editing — a URL, inline base64 data, an uploaded file, or a prior generation reference.

From a URL:

{
"source": { "url": "https://example.com/photo.jpg" }
}

From base64 data:

{
"source": {
"data": "iVBORw0KGgoAAAANSUhEUgAAAAUA...",
"media_type": "image/jpeg"
}
}

From a prior generation — decompose an image you just generated by passing that completed generation’s top-level id as source.generation_id. The referenced generation must be owned by the same client:

{
"source": { "generation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
}

From an uploaded file — reference a file you uploaded via the Files API by its file_id:

{
"source": { "file_id": "f1e2d3c4-b5a6-7890-abcd-ef0123456789" }
}

The prompt field must be present on every request, but for layering its content is optional — pass "" to let the model choose the decomposition on its own. It is also shorter than elsewhere — at most 500 characters. It does not describe an image to generate; it guides how the planner splits the source.

{
"type": "layering",
"prompt": "keep the text and logo on their own layers, group the props together",
"source": { "url": "https://example.com/poster.jpg" }
}

State the rule by which you want things separated and let the planner apply it, rather than dictating a finished manifest:

  • Split by kind"one layer per product on the shelf, plus the shelf and the background", "split by material: metal parts, glass parts, everything else"
  • Steer coarse or fine"as few layers as possible", "the finest separation you can". Ten is the hard cap (nine foreground plus one background).
  • Name what to keep together — over-splitting is the more common complaint, and "keep the whole figure on one layer" is the fix.

Two things work against you. An exhaustive numbered manifest ("Layer 1: … Layer 2: … Layer 3: …") fights the planner instead of steering it. And instructions about rendering have no effect — the prompt shapes decomposition only; it does not restyle, recolor, or add content.

The layering prompt passes the same pre-flight content check as every other generation request. A rejection is a synchronous HTTP 422 with code: "content_moderated", returned before anything is queued or billed.

Two things commonly surprise layering callers:

  1. The check covers protected characters, not just unsafe content. A layer-by-layer breakdown of a character owned by a major rights holder is still a description of that character — often a more identifying one than a plain sentence, since it enumerates the exact visual construction that makes the character recognizable. Rewording will not clear it; that is a licensing boundary rather than a phrasing problem.
  2. Long part-by-part inventories detached from their subject read very differently to a text classifier than the same request with its subject attached. Prefer "the arms and legs of the yellow cartoon character" over a bare list of body parts and colors.

An empty prompt is always permitted, so submitting "" is a quick way to confirm whether a block is about your prompt text or your source image.

Sets the output resolution for every extracted layer. Defaults to "1k".

ValueDescription
1kFaster and lower cost — the default
2kRe-renders each layer at higher quality; flat 2× the 1k rate
generation = client.generations.create(
type="layering",
prompt="",
source={"url": "https://example.com/product-shot.jpg"},
layering={"resolution": "2k"},
)

These constraints are specific to type: "layering" requests.

RuleConstraint
typeMust be "layering"
modelMust be uni-1 — layering is not available on uni-1-max
sourceRequired. Provide exactly one of url, data, generation_id, or file_id
promptRequired field, may be empty. Maximum 500 characters (guides the split; does not describe a new image)
layering.resolution"1k" or "2k". Defaults to "1k"
layering.*Unknown keys in the layering block are rejected, not ignored
image_refRejected on layering requests
web_searchRejected on layering requests
videoRejected on layering requests

Common validation errors:

MistakeHTTP codeError
Missing source422"source image is required for layering"
Including image_ref422"image_ref is not supported for layering"
Including web_search422"web_search is not supported for layering"
Including a video block422"video options are not supported for layering"
prompt over 500 characters422"prompt must be at most 500 characters for layering"
layering block on a non-layering type422"layering options are only supported for type='layering'"

See Error handling for the full list of validation errors.

Some problems only surface after submission, when the generation is polled. These fail with state: "failed" and a standard failure_code — any hold placed at submit is released in full:

Situationfailure_code
The source image passed the submit checks but could not be used for layer extractioncorrupt_input
Layer planning failed for this image — retry, or adjust the prompt guidancegeneration_failed
The source image or an output was flagged by content moderationcontent_moderated

Layer planning failure is the one worth handling explicitly: the vision pass could not produce a usable decomposition for that particular image, and the server already retried once before reporting it. It shows up most on images with no clear foreground/background separation — a flat texture, an abstract pattern, an already-isolated object on plain white. Retrying the identical request rarely helps; adding prompt guidance does.

Layering follows the standard polling flow — submit returns HTTP 201 with state: "queued"; poll GET /v1/generations/{id} until the state reaches completed or failed. See the Quickstart for the polling pattern.

On completion, output contains one entry per extracted layer, ordered front-to-backoutput[0] is the frontmost element and the last entry is always the background. Each output carries a layer object describing what the layer contains:

{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"type": "layering",
"state": "completed",
"model": "uni-1",
"created_at": "2026-07-16T12:00:00Z",
"output": [
{
"type": "image",
"url": "https://storage.example.com/generations/d290f1ee/layer0.png?X-Amz-Expires=3600&...",
"layer": {
"index": 0,
"label": "subject",
"description": "A woman in a red coat, complete from head to toe",
"alpha_hint": "soft",
"bounds": {
"x": 312,
"y": 64,
"width": 420,
"height": 896,
"canvas_width": 1024,
"canvas_height": 1024
}
}
},
{
"type": "image",
"url": "https://storage.example.com/generations/d290f1ee/layer1.png?X-Amz-Expires=3600&...",
"layer": {
"index": 1,
"label": "background",
"description": "The full street scene behind the subject",
"alpha_hint": "none",
"bounds": {
"x": 0,
"y": 0,
"width": 1024,
"height": 1024,
"canvas_width": 1024,
"canvas_height": 1024
}
}
}
],
"failure_reason": null,
"failure_code": null
}
FieldDescription
indexLayer position, front-to-back, starting at 0. The last layer is the background
labelShort (1–2 word) layer name
descriptionComplete-element caption for the layer’s content
alpha_hintEdge treatment of the layer’s transparency — see below
boundsPlacement of this layer’s pixels on the composite canvas — see below

alpha_hint values:

ValueMeaning
softFeathered edges — hair, fur, glass, smoke
hardClean solid edges — objects, text, graphic elements
noneFully opaque — the background layer

Layers are cropped, and bounds puts them back

Section titled “Layers are cropped, and bounds puts them back”

Each layer is returned cropped to its own visible content, not padded to the size of the source image — a hat layer occupying a small corner comes back as a small PNG. This keeps payloads small, but it means the layer images in one response have different dimensions from each other and from your input. bounds carries the geometry needed to place each one:

FieldDescription
x, yWhere this layer’s top-left corner sits on the composite canvas
width, heightThe returned PNG’s real pixel size
canvas_width, canvas_heightThe full composite canvas — identical across every layer in one response

Guarantees you can build on:

  • bounds is present on every layering output — there is no need to derive placement from the alpha channel yourself.
  • A layer that already fills the frame (usually the background) reports the identity rect x=0, y=0, width=canvas_width, height=canvas_height, so you never need a has-crop/no-crop branch.

To reassemble the source image, composite back-to-front — draw the last output first (the background), then each preceding layer on top at its bounds offset:

import io, urllib.request
from PIL import Image
outputs = generation["output"]
b0 = outputs[0]["layer"]["bounds"]
canvas = Image.new("RGBA", (b0["canvas_width"], b0["canvas_height"]), (0, 0, 0, 0))
for out in reversed(outputs): # back-to-front
b = out["layer"]["bounds"]
layer = Image.open(io.BytesIO(urllib.request.urlopen(out["url"]).read())).convert("RGBA")
canvas.alpha_composite(layer, (b["x"], b["y"]))
canvas.save("recomposited.png")

Layering is priced per generation, by layering.resolution. The rate is flat — it does not depend on how many layers come back.

layering.resolutionPrice per generation
1k$0.2250
2k$0.4500

A 2-layer result and a 10-layer result cost the same at the same resolution, so you know the charge before you submit. The hold placed at submit equals the final charge — nothing is released on a successful generation. A failed generation releases the hold in full and is never charged.

Layering counts against your standard generation limits — see Rate limits and headers. Rejections return HTTP 429 with Retry-After and the standard X-RateLimit-* headers, before any hold is placed, so a 429 never charges you.

A layering request occupies far more capacity than its single request slot suggests: one submission fans out to as many as ten image generations. Pace accordingly — treat one layering submission as up to ten generations, and prefer a modest concurrency ceiling with retries over bursting. If your integration needs sustained layering throughput, contact support+luma-agents-api@lumalabs.ai so headroom can be sized for your account.

  • Image generation — Generate new images from text
  • Image editing — Modify existing images with text prompts
  • Files API — Upload a source once, reference it by file_id
  • Models — Model capabilities and limitations
  • Pricing — Pay-as-you-go and Provisioned Throughput plans
  • Error handling — Every error code with troubleshooting steps
  • API Reference — Complete endpoint specifications