---
title: "How to add a 2.5D parallax photo with Claude (tested prompt and script)"
description: "A still shot of the serum bottle comes alive in 3D: the bottle and its acrylic block swing right and loom larger while the holographic backdrop behind them drifts left, as if the camera were circling the product. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/2-5d-parallax-photo
---

# How to add a 2.5D parallax photo with Claude

A still shot of the serum bottle comes alive in 3D: the bottle and its acrylic block swing right and loom larger while the holographic backdrop behind them drifts left, as if the camera were circling the product.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Add a 2.5D parallax photo to input.mp4
   Top of the stand, 0-1 down the frame; the product and everything below this line move as the near layer: 0.75
   Backdrop scale and slide (share of the frame width, negative is left) by the last frame: 1.08, -0.04
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Add a 2.5d parallax photo to **@clip1.mp4**, put a sale badge on **@clip2.mp4**, give **@clip3.mp4** a VHS look, then join all three with crossfades.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting               | Default       | What it does                                                                                                      |
| --------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
| `STAGE`               | `0.75`        | Top of the stand, 0-1 down the frame; the product and everything below this line move as the near layer.          |
| `FAR_ZOOM, FAR_PAN`   | `1.08, -0.04` | Backdrop scale and slide (share of the frame width, negative is left) by the last frame.                          |
| `NEAR_ZOOM, NEAR_PAN` | `1.16, 0.07`  | Product and stand scale and slide by the last frame. The bigger the gap from the far values, the deeper it looks. |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Turn the first frame of input.mp4 (540x960, 30fps, 6 seconds, a serum bottle standing on an acrylic block whose top edge is 75% down the frame) into a 2.5D parallax photo. Take the first frame as the photo and split it into two layers. Near layer: the bottle cut out with rembg (u2net model, its mask as the alpha), plus every row from 75% down to the bottom (the block and floor), fading in over 6 pixels centered on that line. Far layer: the photo with the bottle's hole filled: grow the rembg mask (pixels above 40) with a 15x15 dilation and fill it with OpenCV's Telea inpainting, radius 9. Animate with p = smoothstep(t / 6) = x * x * (3 - 2 * x), so frame 0 is the untouched photo and the move eases in and out over the whole 6 seconds. Far layer: scale by 1 + 0.08p about the frame center, then shift left by 0.04p of the frame width. Near layer: scale by 1 + 0.16p about the point at the horizontal center and 75% down, then shift right by 0.07p of the frame width. Draw the far layer, then the near layer over it, using bicubic resampling. Use Python with Pillow and numpy and ffmpeg to read and write the video, keep the size, 30fps, 6-second length and the input's audio, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `2-5d-parallax-photo.py` and run:

```
python3 2-5d-parallax-photo.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""2.5D parallax photo: a still product photo splits into a near and a far layer that slide and zoom by different amounts.

Usage: python3 2-5d-parallax-photo.py input.mp4 output.mp4
The photo is the input's first frame; the output keeps the input's length and audio.
Needs ffmpeg, Pillow, numpy, OpenCV (to fill the hole behind the product) and rembg (to cut the product out)
(pip install pillow numpy opencv-python-headless "rembg[cpu]"; rembg downloads its u2net model on first use).
"""
import json
import subprocess
import sys

import cv2
import numpy as np
from PIL import Image
from rembg import new_session, remove

STAGE = 0.75        # top of the stand, 0-1 down the frame: the product and everything below it form the near layer
FAR_ZOOM = 1.08     # backdrop scale on the last frame (about the frame center)
FAR_PAN = -0.04     # backdrop slide by the last frame, as a share of the frame width (negative = left)
NEAR_ZOOM = 1.16    # product and stand scale on the last frame (about the stand's top, centered)
NEAR_PAN = 0.07     # product and stand slide by the last frame, as a share of the frame width (positive = right)

def smoothstep(p):
    """0 -> 1, starting and stopping gently, like a camera move."""
    p = min(max(p, 0), 1)
    return p * p * (3 - 2 * p)

def split_layers(photo):
    """The near layer (product cut out, plus the stand and floor below STAGE) and the far layer (backdrop with the product's hole filled in)."""
    mask = np.asarray(remove(photo, session=new_session("u2net"), only_mask=True)).astype(np.float32)
    rows = np.clip((np.arange(h) - h * STAGE) / 6 + 0.5, 0, 1)[:, None] * 255  # 6-pixel fade at the stage line
    near = photo.convert("RGBA")
    near.putalpha(Image.fromarray(np.maximum(mask, rows).astype(np.uint8)))
    hole = cv2.dilate((mask > 40).astype(np.uint8) * 255, np.ones((15, 15), np.uint8))
    far = Image.fromarray(cv2.inpaint(np.asarray(photo), hole, 9, cv2.INPAINT_TELEA))
    return near, far

def move(layer, zoom, pan, cx, cy):
    """Scale a layer by zoom about (cx, cy), then slide it pan pixels across."""
    return layer.transform((w, h), Image.AFFINE, (1 / zoom, 0, cx - (cx + pan) / zoom, 0, 1 / zoom, cy - cy / zoom),
                           Image.BICUBIC)

def compose(t):
    """One output frame: the backdrop drifts one way while the nearer product and stand swing the other way and grow faster."""
    p = smoothstep(t / duration)
    frame = move(far, 1 + (FAR_ZOOM - 1) * p, FAR_PAN * w * p, w / 2, h / 2).convert("RGBA")
    frame.alpha_composite(move(near, 1 + (NEAR_ZOOM - 1) * p, NEAR_PAN * w * p, w / 2, h * STAGE))
    return frame.convert("RGB")

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])

def first_frame(path):
    raw = subprocess.check_output(["ffmpeg", "-v", "error", "-i", path, "-frames:v", "1",
                                   "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
    return Image.frombytes("RGB", (w, h), raw)

def render(src, out):
    """Write compose(t) for every frame of src's length to out, with src's audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error",
                           "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-i", src, "-map", "0:v", "-map", "1:a?", "-c:v", "libx264", "-crf", "18",
                           "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out], stdin=subprocess.PIPE)
    for i in range(round(duration * fps)):
        ff.stdin.write(compose(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
near, far = split_layers(first_frame(src))
render(src, out)
```

## More motion and camera

- [Back-and-forth loop **Back-and-forth loop**](https://www.recharm.com/effects/back-and-forth-loop)
- [Background blur **Background blur**](https://www.recharm.com/effects/background-blur)
- [Bullet time **Bullet time**](https://www.recharm.com/effects/bullet-time)
- [Cinemagraph **Cinemagraph**](https://www.recharm.com/effects/cinemagraph)
- [Clone effect **Clone effect**](https://www.recharm.com/effects/clone-effect)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
