Ffmpeg geq expr
Procedural and expression-based ffmpeg filters: geq (per-pixel YUV/RGB expressions), aeval (audio sample expressions), lut2 (two-input LUT expression), drawgraph (real-time metrics graph), drawvg (SVG rendering over video), random, feedback, lagfun. Use when the user asks to build a custom pixel-level filter, apply math directly to video samples, render SVG over video, make a real-time debug graph, simulate feedback delay, or write arbitrary expressions for creative effects.From its SKILL.md
npx -y skills add damionrashford/media-os --skill ffmpeg-geq-exprAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 17 stars17 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.5 KB, as published. Nobody here has run it
ffmpeg-geq-expr — procedural expression filters
Context: $ARGUMENTS
Power-user creative filters where you hand-write a math expression and ffmpeg evaluates it per-pixel (geq), per-sample (aeval), per-pair (lut2), or per-metric (drawgraph). Grammar is the same everywhere: see references/expressions.md.
Quick start
- Custom per-pixel effect: → Step 1 → pick
geq - Custom per-sample audio effect: → Step 1 → pick
aeval - Blend/combine two videos pixel-by-pixel with math: → Step 1 → pick
lut2 - Real-time metric graph burned into video: → Step 1 → pick
drawgraph - SVG overlay driven by frame metadata: → Step 1 → pick
drawvg - Light-on-dark persistence / phosphor trails: → Step 1 → pick
lagfun - Feedback-loop picture-in-picture: → Step 1 → pick
feedback - Shuffle frame order: → Step 1 → pick
random
When to use
- You need a per-pixel/per-sample effect that no named filter ships with (gradients, plasma, custom noise, chromatic aberration, vignettes).
- You want to wire filter metadata (
signalstats,ebur128,blockdetect) into a visible QC graph. - You want to combine two aligned inputs with arbitrary math, not
blend's fixed modes. - You want a one-off creative look without writing a C filter.
Do NOT use for things with a dedicated filter — geq is typically 10–100× slower than a native filter. If vignette, lut3d, colorchannelmixer, curves, etc. can do it, use those.
Step 1 — Pick the right filter
| Need | Filter | Domain |
|---|---|---|
| Per-pixel math on one video | geq | spatial (X,Y) + temporal (T,N) |
| Per-sample math on audio | aeval | per-sample per-channel |
| Two-video pixel math | lut2 | pairs of samples (x, y) from inputs |
| Graph metadata values | drawgraph | per-frame scalar from metadata keys |
| SVG / VGS vector overlay | drawvg | per-frame VGS script |
| Frame-order shuffle | random | temporal only |
| Feedback picture-in-picture | feedback | spatial box + time |
| Dark-decay afterimage | lagfun | per-pixel with decay |
Step 2 — Write the expression
Grammar highlights (full table in references/expressions.md):
- Built-in constants:
PI,E,PHI,TAU. - Functions:
sin,cos,tan,asin,acos,atan,atan2,exp,log,sqrt,hypot(a,b),pow(a,b)ora^b,mod(a,b),abs,floor,ceil,round,trunc,clip(v,a,b),between(v,a,b),if(c,t,e),ifnot(c,e),gt(a,b),lt(a,b),gte,lte,eq(a,b),not(x),random(seed),st(N,v)store,ld(N)load (26 slots, N=0..25),print(v)to stderr. - Operators:
+ - * / %,^power,&& || !logic,? :ternary is NOT supported (useif(c,t,e)).
geq per-plane YUV:
geq=lum='X+T*10':cb=128:cr=128 # moving luminance ramp, neutral chroma
geq=lum='255*(X/W)':cb='128+64*sin(T)':cr='128+64*cos(T)'
geq RGB (requires planar RGB pixfmt):
format=gbrp,geq=r='255*X/W':g='255*Y/H':b=0 # diagonal RG gradient
format=gbrp,geq=r='b(X,Y)':g=g:b='r(X,Y)' # BGR channel swap
Vignette via geq (hand-rolled):
format=gbrp,geq=r='r(X,Y)*clip(1-pow((X-W/2)/(W/2),2)-pow((Y-H/2)/(H/2),2),0,1)':g='g(X,Y)*clip(1-pow((X-W/2)/(W/2),2)-pow((Y-H/2)/(H/2),2),0,1)':b='b(X,Y)*clip(1-pow((X-W/2)/(W/2),2)-pow((Y-H/2)/(H/2),2),0,1)'
aeval (per-sample):
aeval=val(0)*0.5|val(1)*0.5:channel_layout=stereo # halve volume (note | between exprs)
aeval=val(ch)*sin(2*PI*5*t):c=same # 5 Hz tremolo
aevalsrc (synth):
aevalsrc='sin(2*PI*440*t)':channel_layout=mono:sample_rate=48000:duration=2
lut2 (two inputs, same size/pixfmt):
lut2=c0='(x+y)/2':c1='(x+y)/2':c2='(x+y)/2' # pixel average blend
drawgraph (metadata chart):
signalstats,drawgraph=m1=lavfi.signalstats.YAVG:fg1=0xff00ff:min=0:max=255:size=1280x200
drawvg (SVG/VGS overlay):
drawvg=cfg='<svg><rect x=20 y=20 width=200 height=60 fill="red"/></svg>'
random (frame order):
random=frames=30:seed=42
feedback (recursive PiP):
feedback=x=10:y=10:w=100:h=100
lagfun (dark decay / afterimage):
lagfun=decay=0.9
Step 3 — Run and debug
Always test on 2 seconds first:
ffmpeg -i in.mp4 -t 2 -vf "<expr-filter>" -y out_test.mp4
See parse errors: -loglevel debug prints tokens as they parse. A silent output or black frames without stderr output almost always means a typo or an operator ffmpeg's parser rejected.
Drop print() into any expression to dump a value per evaluation to stderr:
geq=lum='print(T)':cb=128:cr=128
Use the helper:
uv run ${CLAUDE_SKILL_DIR}/scripts/expr.py geq --input in.mp4 --output out.mp4 --lum 'X+T*10'
Available scripts
scripts/expr.py— subcommands:geq,aeval,lut2,drawgraph,feedback,lagfun. All support--dry-runand--verbose. Autodetects RGB vs YUV forgeqbased on which flags you pass.
Reference docs
- Read
references/expressions.mdfor the full grammar, geq constant table, store/load slot usage, 50+ worked recipes, and thedrawgraphmetric catalog.
Gotchas
geqcoordinates:X/Yare 0-based pixel coordinates,W/Hare plane dimensions,SW/SHare the horizontal/vertical subsampling factors (useSW/SHin chroma expressions on 4:2:0, where chroma planes are half-size).Tis time in seconds (float).Nis the frame number.p(x,y)inside a chroma (cb/cr) expression returns a chroma sample, not luma. If you want luma inside a chroma expression uselum(X,Y). Likewiselum(),cb(),cr(),alpha(),r(),g(),b()all exist.clip(v,a,b)clamps;st(N,v)stores in slotN(0..25),ld(N)reads. Use these when an expression appears multiple times.geqon RGB input silently operates on packed pixels unless you insertformat=gbrpfirst. Always prependformat=gbrpbefore a color-channelgeq.- Expressions are compiled but evaluated per pixel, per frame. Complex expressions can drop processing to sub-1 fps. Cache common sub-expressions with
st()/ld(). aevalevaluates per audio sample and is slow. For synth (not a real-time effect on a real stream), useaevalsrc— much faster because there's no input rate.aevalvariables:val(i)is input channeli's current sample,chis output channel index,nb_in_channels,nb_out_channels,ssample rate,ttime,nsample number.lut2: expression usesx(input 0 sample) andy(input 1 sample). Both inputs must have the same dimensions AND pixel format. Insertscale=+format=before the join.drawgraph: background defaults to transparent. If you want it on top of video, pipe throughoverlay. To output a standalone graph, addbg=blackor use thecolorsource as a backdrop.drawvgis new (post-FFmpeg 7.x). Checkffmpeg -filters 2>&1 | grep drawvgbefore relying on it.feedbackrequires all fourx,y,w,h— withoutw/hit silently fails.- When an expression has a parse error, ffmpeg commonly silently outputs black frames or stops without a loud error. Run with
-loglevel debugand search the output forError when evaluating. PI/E/PHI/TAUare built-in; don't redeclare them asst().- Comma
,separates filter options in ffmpeg syntax. Inside an expression, use\,(escaped) or wrap the whole expression in single quotes. - Semicolon
;separates filter-graph stages; use|to separate per-channel expressions insideaeval. - Always test expressions with
-t 2on a short clip first, then scale up.
Examples
Vertical gradient (RGB)
ffmpeg -f lavfi -i color=black:s=640x360:d=2 -vf "format=gbrp,geq=r='255*Y/H':g=0:b='255-255*Y/H'" -y grad.mp4
Moving plasma
ffmpeg -f lavfi -i color=black:s=640x360:d=3 \
-vf "format=gbrp,geq=r='128+127*sin(X/20+T)':g='128+127*sin(Y/20+T*1.3)':b='128+127*sin((X+Y)/20+T*0.7)'" \
-y plasma.mp4
Chromatic aberration
ffmpeg -i in.mp4 -vf "format=gbrp,geq=r='r(X+2,Y)':g='g(X,Y)':b='b(X-2,Y)'" -y ca.mp4
YAVG graph burn-in
ffmpeg -i in.mp4 -vf "signalstats,drawgraph=m1=lavfi.signalstats.YAVG:fg1=0xffff00:min=0:max=255:size=1920x120,format=yuv420p" -c:v libx264 -crf 18 -y graph.mp4
Lagfun phosphor trail
ffmpeg -i in.mp4 -vf "lagfun=decay=0.92" -y trail.mp4
Two-video lut2 average blend
ffmpeg -i a.mp4 -i b.mp4 -filter_complex "[0:v]scale=1280:720,format=yuv420p[a];[1:v]scale=1280:720,format=yuv420p[b];[a][b]lut2=c0='(x+y)/2':c1='(x+y)/2':c2='(x+y)/2'" -y blend.mp4
Troubleshooting
Black frames, no obvious error
Cause: expression parse error silently produced all-zero output.
Solution: add -loglevel debug, search stderr for Error when evaluating, check for unescaped commas in expression, or wrong variable name (e.g. x inside geq should be X).
geq on RGB but colors ignored / operating on packed bytes
Cause: input is packed RGB (rgb24, bgr24), not planar.
Solution: prepend format=gbrp before geq.
lut2 error: inputs dimensions differ
Cause: two inputs have different resolutions or pixel formats.
Solution: prepend scale=W:H,format=yuv420p on both inputs before joining.
geq runs at 0.3 fps
Cause: expression is complex, recomputed per-pixel.
Solution: cache repeated sub-expressions with st(0,...) then reuse with ld(0). Also: consider if a native filter (vignette, colorchannelmixer, curves) can do the same thing.
drawgraph shows nothing
Cause: metadata keys are wrong, or the producer filter wasn't upstream.
Solution: signalstats → drawgraph=m1=lavfi.signalstats.YAVG. Run ffprobe -show_frames to confirm the metadata keys.
aevalsrc produces clipped audio
Cause: expression output exceeds ±1.0.
Solution: scale the expression: sin(2*PI*440*t)*0.5.
feedback silently does nothing
Cause: missing one of x, y, w, h.
Solution: supply all four. Also check coords are inside the frame.