Mirror-floor reflection under an AI product cutout, built in Pillow
Flip a transparent product PNG from Sume, fade it with a gradient mask, and stack it under the original for a glossy floor reflection. Code and fade settings.

Flip the cutout vertically with ImageOps.flip, multiply its alpha by a vertical gradient that goes from about 40 percent to 0, and paste it directly under the product on a dark or white backdrop. That gives a glossy-floor reflection from one model call, and you control the strength with a single number.
Why build it instead of prompting for it
A prompt that asks for a reflective floor makes the model invent the whole scene, and the reflection often disagrees with the object: wrong angle, wrong cut-off, a different product. Building it from the cutout guarantees the reflection is a true mirror of what is above it.
The source is a transparent PNG. ChatGPT Image 2.5 on Sume is the model that lists background in its supported parameters, so request transparent there.
The code
Make the gradient as a mode L image, then multiply it with the flipped alpha using ImageChops.multiply. The fade height is a fraction of the product height; 0.5 reflects the lower half only.
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, ImageOps
out = gen(model="openai/gpt-image-2.5", background="transparent", output_format="png",
prompt="Glass perfume bottle with gold cap, front view, studio product photo")
item = fetch(out["data"][0]["url"]).convert("RGBA")
w, h = item.size
refl = ImageOps.flip(item)
fade_h = int(h * 0.5)
grad = Image.new("L", (1, fade_h))
grad.putdata([int(255 * 0.4 * (1 - y / fade_h)) for y in range(fade_h)])
grad = grad.resize((w, fade_h))
mask = Image.new("L", (w, h), 0)
mask.paste(grad, (0, 0))
refl.putalpha(ImageChops.multiply(refl.getchannel("A"), mask))
canvas = Image.new("RGBA", (w, h + fade_h), (12, 12, 16, 255))
canvas.alpha_composite(item, (0, 0))
canvas.alpha_composite(refl.crop((0, 0, w, fade_h)), (0, h))
canvas.convert("RGB").save("bottle_reflection.jpg", quality=92)
print(canvas.size, "cost", out["usage"]["cost"])Settings that change the feel
Start at 40 percent and a half-height fade, then adjust. Glossy glass and black acrylic take more reflection, and matte cardboard takes almost none.
| Surface | Peak opacity | Fade height | Backdrop |
|---|---|---|---|
| Glossy black acrylic | 0.45 | 0.5 | Near black |
| Polished white tile | 0.25 | 0.35 | White |
| Matte table | 0.10 | 0.2 | Light gray |
What to watch
- Objects that sit on the floor should touch the reflection. Crop the cutout tight to the product so there is no empty alpha gap between the two.
- A transparent object such as glass shows the backdrop through it, which a flipped copy does not handle. Keep opacity low.
- Add a soft shadow too if the product stands on a matte area.
- Export JPEG only at the end.
Combine with other steps
A reflection goes well with the soft shadow recipe, where the shadow sits just under the contact line. Read the transparent PNG note first if edges look rough, and keep intermediates as PNG per the chain rule.
Sources
Related posts
More in Media tools
- Kling motion control reference over 30 seconds: trim it first
Sume's Kling motion control takes a duration_seconds of 1 to 30. For a longer dance or walk clip, cut a 30-second range with video trim, then submit that.
- Lyria music prompt rejected by policy: what to change, what to keep
After a policy rejection on a Sume music job, edit only the flagged content, keep the seven-axis brief, and retry in budget. A generic bed is the wrong fix.
- One-color stencil from an AI image: Pillow threshold for screen print
Make a one-ink stencil from a Sume image with autocontrast, a blur, and a threshold point() call. Pick the cutoff by pixel share, not by guesswork.
- One voice-over under ten AI shots: Timeline spine and start times
Put one narration track under ten AI video shots on Sume Timeline: video[0].start is 0, later starts increase, and the last shot may end up to 0.5 s early.
Written by Sume