---
title: "How to add a rotoscope with Claude (tested prompt and script)"
description: "At one second the orbit turns into a hand-traced animation: the holographic backdrop settles into flat patches of lavender, sky blue and cream, and thin brown-black pen lines trace the bottle, its label and the acrylic block, shivering slightly from one drawing to the next. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/rotoscope
---

# How to add a rotoscope with Claude

At one second the orbit turns into a hand-traced animation: the holographic backdrop settles into flat patches of lavender, sky blue and cream, and thin brown-black pen lines trace the bottle, its label and the acrylic block, shivering slightly from one drawing to the next.

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 rotoscope to input.mp4
   Second the footage turns into traced drawings: 1.0
   Frames each drawing stays on screen: 2
   ```

## 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 rotoscope 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                                                                                                            |
| -------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `START`  | `1.0`          | Second the footage turns into traced drawings.                                                                          |
| `HOLD`   | `2`            | Frames each drawing stays on screen. 2 at 30fps is animating on twos (15 drawings a second); 1 is smoother, 3 choppier. |
| `COLORS` | `16`           | Flat fill colors, picked once from the first drawing so they don't flicker. Fewer gives bolder, more graphic fills.     |
| `VIVID`  | `1.4`          | How much richer the fill colors are than the footage. 1 keeps them as they are.                                         |
| `LINES`  | `(20, 60)`     | Canny edge thresholds. Lower draws more lines, higher keeps only the strongest contours.                                |
| `WOBBLE` | `2.5`          | Pixels the lines drift, traced differently every drawing. 0 keeps them steady.                                          |
| `LINE`   | `3`            | Line thickness in pixels.                                                                                               |
| `INK`    | `(35, 25, 20)` | Line color, a dark brown-black.                                                                                         |
| `SEED`   | `7`            | Random seed for the palette and the wobble.                                                                             |

## Without the skill

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

```
Give input.mp4 (540x960, 30fps) a rotoscoped, hand-traced look from 1.0 seconds to the end; leave every frame before 1.0 s untouched. Use Python with OpenCV (opencv-python-headless) and numpy, reading and writing frames through ffmpeg. Animate on twos: from frame 30 (1.0 s) make a new drawing from every 2nd frame (frames 30, 32, 34...) and hold it for 2 frames. Palette: once, on frame 30, run a bilateral filter (diameter 9, sigma color 40, sigma space 7), then k-means with 16 clusters (k-means++ init, 3 attempts) on its pixels, and push each of the 16 colors away from its own gray (mean of R, G and B) by a factor of 1.4, clipped to 0-255; keep this palette for every drawing so the colors don't flicker. Each drawing: shrink the frame to 270x480 (area filter), run the same bilateral filter 3 times, replace every pixel with its nearest palette color, run a 5-pixel median blur and scale it back to 540x960 (bilinear) for the flat fills. Lines: Canny edges (thresholds 20 and 60) on the grayscale of the smoothed 270x480 frame, scaled up to 540x960 (bilinear) and thickened with a 3x3 dilate. Make the lines wobble: for each drawing, pick fresh random x and y offsets between -2.5 and +2.5 pixels on a grid 7 points across and 12 down, scale them up to 540x960 with bicubic interpolation and remap the line mask by them, then blur it by sigma 0.7 and use it to paint dark brown-black ink (35, 25, 20) over the fills. Draw any text with Pillow, not ffmpeg's drawtext. Keep the size (540x960), 30fps, the 6 second length and the audio, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `rotoscope.py` and run:

```
python3 rotoscope.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Rotoscope: at one second the shot turns hand-traced, with flat fills of color and thin ink lines that wobble from drawing to drawing.

Usage: python3 rotoscope.py input.mp4 output.mp4
Needs ffmpeg, OpenCV and numpy (pip install opencv-python-headless numpy).
"""
import json
import subprocess
import sys

import cv2
import numpy as np

START = 1.0              # second the footage turns into traced drawings
HOLD = 2                 # frames each drawing stays on screen ("on twos": 15 drawings a second at 30fps)
COLORS = 16              # flat fill colors, picked once from the first drawing so they don't flicker
VIVID = 1.4              # how much richer the fill colors are than the footage; 1 keeps them as they are
LINES = (20, 60)         # Canny edge thresholds; lower draws more lines
WOBBLE = 2.5             # pixels the lines drift, redrawn differently every drawing
LINE = 3                 # line thickness in pixels
INK = (35, 25, 20)       # line color, a dark brown-black
SEED = 7                 # random seed for the palette and the wobble

def pick_palette(img):
    """The flat fill colors: k-means on one smoothed frame, then made a little more vivid."""
    pixels = cv2.bilateralFilter(img, 9, 40, 7).reshape(-1, 3).astype(np.float32)
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
    cv2.setRNGSeed(SEED)
    colors = cv2.kmeans(pixels, COLORS, None, criteria, 3, cv2.KMEANS_PP_CENTERS)[2]
    gray = colors.mean(axis=1, keepdims=True)
    return np.clip(gray + (colors - gray) * VIVID, 0, 255)

def flatten(img):
    """Iron out texture at half size, then snap every pixel to the nearest palette color."""
    half = cv2.resize(img, (img.shape[1] // 2, img.shape[0] // 2), interpolation=cv2.INTER_AREA)
    for _ in range(3):
        half = cv2.bilateralFilter(half, 9, 40, 7)
    pixels = half.reshape(-1, 3).astype(np.float32)
    nearest = ((pixels[:, None] - palette[None]) ** 2).sum(axis=2).argmin(axis=1)
    flat = cv2.medianBlur(palette[nearest].astype(np.uint8).reshape(half.shape), 5)
    return half, flat

def wobble(mask):
    """Push a line mask around by a smooth random displacement, so each drawing is traced a little differently."""
    h, w = mask.shape
    dx, dy = (cv2.resize(rng.uniform(-WOBBLE, WOBBLE, (12, 7)).astype(np.float32), (w, h), interpolation=cv2.INTER_CUBIC)
              for _ in range(2))
    xs, ys = np.meshgrid(np.arange(w, dtype=np.float32), np.arange(h, dtype=np.float32))
    return cv2.remap(mask, xs + dx, ys + dy, cv2.INTER_LINEAR)

def trace(img):
    """One drawing: flat color fills with wobbly ink lines over them."""
    half, flat = flatten(img)
    color = cv2.resize(flat, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_LINEAR).astype(np.float32)
    edges = cv2.Canny(cv2.cvtColor(half, cv2.COLOR_RGB2GRAY), *LINES)
    edges = cv2.resize(edges, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_LINEAR)
    edges = cv2.dilate(edges, np.ones((LINE, LINE), np.uint8))
    ink = cv2.GaussianBlur(wobble(edges), (0, 0), 0.7).astype(np.float32)[..., None] / 255
    return (color * (1 - ink) + np.array(INK, np.float32) * ink).astype(np.uint8)

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate", "-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)

src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
rng = np.random.default_rng(SEED)
reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                          stdout=subprocess.PIPE)
writer = 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)
first = round(START * fps)
palette = drawing = None
i = 0
while raw := reader.stdout.read(w * h * 3):
    if i >= first:
        if (i - first) % HOLD == 0:
            img = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
            if palette is None:
                palette = pick_palette(img)
            drawing = trace(img).tobytes()
        raw = drawing
    writer.stdin.write(raw)
    i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
    sys.exit("ffmpeg failed")
```

## More stylize and art

- [Anime style **Anime style**](https://www.recharm.com/effects/anime-style)
- [ASCII art **ASCII art**](https://www.recharm.com/effects/ascii-art)
- [Blueprint **Blueprint**](https://www.recharm.com/effects/blueprint)
- [Charcoal drawing **Charcoal drawing**](https://www.recharm.com/effects/charcoal-drawing)
- [Claymation style **Claymation style**](https://www.recharm.com/effects/claymation-style)

## 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)
