# Ffmpeg Video Filter

> Apply video filters with ffmpeg -vf / -filter_complex: scale, crop, pad, rotate/transpose, fps, setsar, deinterlace (yadif/bwdif), overlay, hstack/vstack, drawtext, drawbox, color (curves, eq, colorchannelmixer), denoise, sharpen, blur, fade, zoompan. Use when the user asks to resize, crop, rotate, flip, pad, overlay a watermark/logo, stack videos side-by-side, add text/captions on top, change fps, deinterlace, color correct, sharpen, blur, or apply any video effect.

- Skill: `damionrashford/ffmpeg-video-filter` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add damionrashford/ffmpeg-video-filter`
- Raw SKILL.md: https://api.skillmd.com/api/skills/damionrashford/ffmpeg-video-filter/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: damionrashford (https://skillmd.com/u/damionrashford)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/damionrashford/ffmpeg-video-filter

---


# Ffmpeg Video Filter

**Context:** $ARGUMENTS

## Quick start

- **Resize to 720p (keep aspect):** `-vf "scale=-2:720"` → Step 2
- **Letterbox to 1080p:** `-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"` → Example 2
- **Watermark (PNG corner):** `-filter_complex "[1:v]scale=200:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10"` → Example 1
- **Deinterlace:** `-vf "yadif=1"` → Example 5
- **Stack two videos:** `-filter_complex "[0:v][1:v]hstack=inputs=2"` → Example 3

## When to use

- Single-input transformation on one video stream (scale/crop/pad/fps/color) → use `-vf`.
- Combining two or more inputs (overlay, hstack, picture-in-picture) → use `-filter_complex`.
- Anything with `split`, multiple outputs, or named labels → `-filter_complex` + `-map`.

## Step 1 — Pick `-vf` vs `-filter_complex`

`-vf` (and `-af` for audio) is shorthand for a single-input, single-output chain. Every filter is comma-separated. Example: `-vf "scale=1280:720,eq=saturation=1.1,fps=30"`.

`-filter_complex` is the general graph. It accepts multiple inputs and outputs, uses `[in][out]` labels, and chains are separated by **semicolons** (filters within a chain are still comma-separated). When you use it you **must** `-map` every output stream you want in the container — default stream selection is disabled.

Rules of thumb:
- One input file, one video track, linear transform → `-vf`.
- Any time you write `[0:v]` or `[1:v]` → `-filter_complex`.
- You need the same source twice (split) → `-filter_complex`.

## Step 2 — Build the filter chain

Canonical filter order: **deinterlace → crop → scale → pad → color/eq → overlay → drawtext → fps → format**. Deviating has real cost: cropping after scaling wastes pixels; scaling before cropping blurs detail you were going to throw away.

Scale recipes (`scale` is the workhorse):

```text
scale=1280:720                    # exact
scale=-2:720                      # keep aspect, height 720, width rounded to even
scale=1920:-2                     # keep aspect, width 1920
scale=iw/2:ih/2                   # half size
scale=1920:1080:force_original_aspect_ratio=decrease   # fit inside box (letterbox prep)
scale=1920:1080:force_original_aspect_ratio=increase   # fill box (crop prep)
scale=1920:1080:flags=lanczos     # high-quality downscale
```

Use `-2` (not `-1`) for the auto dimension: most codecs (H.264/HEVC/AV1 with YUV 4:2:0) require even width and height. `-1` preserves aspect but can give odd numbers; `-2` preserves aspect AND snaps to a multiple of 2.

Color / eq quick picks:

```text
eq=brightness=0.05:saturation=1.2:contrast=1.1:gamma=1.0
curves=preset=increase_contrast
curves=preset=cross_process
hue=h=10:s=1.2                    # shift hue by 10 degrees, saturate
colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3   # luma-weighted grayscale
```

Overlay with alpha (PNG logo): pre-format the alpha plane so overlay doesn't silently drop it: `format=yuva420p`.

## Step 3 — Run + verify

```bash
ffmpeg -i in.mp4 -vf "scale=-2:720" -c:v libx264 -crf 20 -preset medium -c:a copy out.mp4
```

For `-filter_complex`, map outputs explicitly:

```bash
ffmpeg -i main.mp4 -i logo.png \
  -filter_complex "[1:v]scale=200:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10[v]" \
  -map "[v]" -map 0:a? -c:a copy out.mp4
```

Verify the output dimensions/fps with ffprobe:

```bash
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,pix_fmt out.mp4
```

## Available scripts

- **`scripts/vfilter.py`** — preset runner (`scale-720p`, `scale-1080p-letterbox`, `watermark`, `deinterlace`, `drawtext-timecode`, `hstack`, `vstack`, `2x-speed`) plus `--custom --filter-string "..."` for arbitrary `-vf` expressions. Use `--dry-run` to print the command without running.

## Workflow

```bash
uv run ${CLAUDE_SKILL_DIR}/scripts/vfilter.py --preset scale-720p --input in.mp4 --output out.mp4
uv run ${CLAUDE_SKILL_DIR}/scripts/vfilter.py --preset watermark --input in.mp4 --watermark logo.png --output out.mp4
uv run ${CLAUDE_SKILL_DIR}/scripts/vfilter.py --preset hstack --inputs a.mp4,b.mp4 --output side.mp4
uv run ${CLAUDE_SKILL_DIR}/scripts/vfilter.py --custom --filter-string "crop=iw-200:ih:100:0,scale=-2:720" --input in.mp4 --output out.mp4
```

## Reference docs

- Read [`references/filters.md`](references/filters.md) for exhaustive option tables, expression variables, `-filter_complex` label/map rules, drawtext escaping, and 50+ recipe snippets.

## Gotchas

- **Even dimensions.** Most encoders demand even width *and* height. Use `scale=-2:720`, not `scale=-1:720`. `libx264` with YUV 4:2:0 will hard-fail on odd sizes.
- **drawtext needs a fontfile on many builds.** If fontconfig is missing, omitting `fontfile=` throws "Fontconfig not found". Always specify a real path. macOS: `/System/Library/Fonts/Supplemental/Arial.ttf`. Linux: `/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf`. Windows: `C\:/Windows/Fonts/arial.ttf` (note the escaped colon).
- **drawtext has three escape levels.** `:` separates filter params → escape inside text as `\:`. `,` separates filters → escape as `\,`. `'` quotes the text → escape as `''` or wrap the whole arg in double quotes at the shell level. For % signs use `\%`.
- **`-vf` cannot take multiple inputs.** The moment you need `[1:v]`, switch to `-filter_complex`.
- **`-filter_complex` disables default stream selection.** You *must* `-map` every output label you want muxed (`-map "[v]" -map 0:a?`), or ffmpeg will not pick them up.
- **Filter order changes results.** Crop before scale to reduce compute and keep detail where it matters. Color ops go after scaling to avoid quantizing twice. drawtext/overlay go last so they render on the final pixel grid.
- **overlay silently drops alpha.** If your PNG has transparency, precede overlay with `format=yuva420p` on the overlay input: `[1:v]format=yuva420p,scale=...[wm]`.
- **`setsar=1` fixes aspect metadata.** Some sources (DV, DVD) have non-square pixels. After any scale you should append `setsar=1` to force square-pixel output.
- **`scale=flags=lanczos` matters when downscaling.** Default is bicubic; lanczos is sharper for 1080p→720p. Upscaling: bicubic/lanczos similar; gauss for blur-friendly upscale.
- **`hstack`/`vstack` require equal dimensions.** Both inputs must match the stacking axis (same height for hstack, same width for vstack). Scale first.
- **`fps` vs `minterpolate`.** `fps=30` drops/duplicates frames — cheap, no visual smoothing. `minterpolate=fps=60` motion-interpolates new frames — expensive, but smooth.
- **Deinterlace field order.** Check with `ffprobe -show_streams | grep field_order`. `yadif=1` sends one field per frame (keeps fps); `yadif=0` merges fields (halves fps, often what you want for 29.97i → 29.97p).

## Examples

### Example 1 — Watermark a logo in the bottom-right

Input: `main.mp4`, `logo.png` (with alpha).
Steps: scale logo to 200px wide, overlay with 10px margin.

```bash
ffmpeg -i main.mp4 -i logo.png \
  -filter_complex "[1:v]format=yuva420p,scale=200:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10[v]" \
  -map "[v]" -map 0:a? -c:v libx264 -crf 20 -c:a copy out.mp4
```

### Example 2 — Scale + letterbox to exact 1920x1080

Input: any aspect ratio.
Steps: fit inside 1920x1080, pad with black to center.

```bash
ffmpeg -i in.mp4 -vf \
  "scale=1920:1080:force_original_aspect_ratio=decrease:flags=lanczos,\
pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1" \
  -c:v libx264 -crf 18 -c:a copy out.mp4
```

### Example 3 — 2-up side-by-side (hstack)

Both inputs must share height. Normalize first.

```bash
ffmpeg -i a.mp4 -i b.mp4 -filter_complex \
  "[0:v]scale=-2:720[a];[1:v]scale=-2:720[b];[a][b]hstack=inputs=2[v]" \
  -map "[v]" -c:v libx264 -crf 20 out.mp4
```

### Example 4 — Timecode with drawtext

Burns running timecode at top-left with semi-transparent box.

```bash
ffmpeg -i in.mp4 -vf \
  "drawtext=fontfile=/System/Library/Fonts/Supplemental/Arial.ttf:\
timecode='00\:00\:00\:00':rate=25:fontsize=32:fontcolor=white:\
box=1:boxcolor=black@0.5:boxborderw=8:x=20:y=20" \
  -c:v libx264 -crf 20 -c:a copy out.mp4
```

Note the `\:` inside the `timecode=` value — the colons are escaped so ffmpeg doesn't interpret them as parameter separators.

### Example 5 — Deinterlace 1080i to 1080p

```bash
ffmpeg -i interlaced.mxf -vf "yadif=1,setsar=1" \
  -c:v libx264 -crf 18 -preset slow -c:a copy progressive.mp4
```

Use `yadif=0` if you actually want half the output framerate (merge fields, 50i → 25p).

## Troubleshooting

### Error: "width not divisible by 2"

Cause: `scale=-1:720` produced an odd width, or you used a codec that demands even dimensions.
Solution: replace `-1` with `-2`. Example: `scale=-2:720`.

### Error: "No such filter" / "Fontconfig not found"

Cause: your ffmpeg build lacks the filter or font system.
Solution: check `ffmpeg -filters | grep drawtext`. If present, pass `fontfile=/absolute/path/to/font.ttf` explicitly — do not rely on `font=Arial`.

### Error: "Output with label 'v' does not exist"

Cause: `-filter_complex` defined `[v]` but you forgot `-map "[v]"`, or you misspelled the label.
Solution: every bracketed label on the right side of the last filter must appear in a `-map` directive.

### Error: filtering produces frozen/black overlay

Cause: overlay input has alpha but no explicit pixel format, so it was negotiated to a format without alpha.
Solution: add `format=yuva420p` before `overlay` on the overlay chain: `[1:v]format=yuva420p,scale=200:-1[wm]`.

### Error: "Filter hstack:0 has an unconnected output" or size mismatch

Cause: inputs to hstack/vstack have different heights/widths.
Solution: scale both to a common dimension first. `[0:v]scale=-2:720[a];[1:v]scale=-2:720[b];[a][b]hstack`.

### drawtext text displays literally with backslashes visible

Cause: over-escaping. You only escape once at the filter-argument level.
Solution: for static text, use `text='Hello\, World'` (single-quoted, `\,` for commas). For timecodes, use the `timecode=` option instead of `text=`.

