Get this effect
Switch to Code in your Claude Desktop App
Install the skill (once)
Show the prompt
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/skillsAsk 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.
Show the prompt
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.mp4Show the script
#!/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")





