<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MeiCut]]></title><description><![CDATA[MeiCut]]></description><link>https://meicut.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6adb8727c414037f96347b26/133ebc43-396e-4782-9cef-f7fa55a7e417.png</url><title>MeiCut</title><link>https://meicut.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 28 Oct 2026 10:06:39 GMT</lastBuildDate><atom:link href="https://meicut.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Video Sketch Without AI: A Deterministic Filter Chain That Can't Flicker]]></title><description><![CDATA[Why a deterministic FFmpeg filter chain (blur, invert, dodge, multiply) can't flicker like AI restyle — the math and four rules.]]></description><link>https://meicut.hashnode.dev/video-sketch-without-ai-a-deterministic-filter-chain-that-can-t-flicker</link><guid isPermaLink="true">https://meicut.hashnode.dev/video-sketch-without-ai-a-deterministic-filter-chain-that-can-t-flicker</guid><category><![CDATA[FFmpeg]]></category><category><![CDATA[webdev]]></category><category><![CDATA[video]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Tutu]]></dc:creator><pubDate>Fri, 23 Oct 2026 16:59:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adb8727c414037f96347b26/b5c47f7d-bfc6-49cf-b20f-7e5d1488ebc8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've tried any of the AI video-to-video restyle tools — "turn my clip into anime," "make this look hand-painted" — you've seen the artifact everyone complains about: the picture <em>crawls</em>. Hairlines shimmer, fabric textures boil, foliage sparkles. The effect looks great on a still frame and falls apart the moment you press play.</p>
<p>When we built the video sketch effect for our online video toolkit, we made a decision that felt almost old-fashioned in 2026: <strong>no neural network anywhere in the pipeline</strong>. The whole effect is a deterministic chain of classical image operations — Gaussian blur, inversion, color dodge, multiply — running in FFmpeg.</p>
<p>That choice is the entire reason our effect never flickers. Not "we tuned it until the flicker was acceptable." It <em>can't</em> flicker, for reasons that are mathematical rather than empirical. This post is about why — and Part 2 of this series gets into the genuinely painful part: making a browser <code>&lt;canvas&gt;</code> and FFmpeg produce pixel-identical output from the same algorithm.</p>
<h2>The recipe: dodge and burn, from the darkroom to OpenCV</h2>
<p>The pencil-sketch effect is a classic photo-editing trick, popularized in computer-vision circles by OpenCV's <code>pencilSketch</code>:</p>
<ol>
<li>Convert the frame to grayscale (or keep color, for a colored-pencil look)</li>
<li>Invert it</li>
<li>Blur the inverted copy with a Gaussian kernel</li>
<li><strong>Color-dodge</strong> the original against the blurred-inverted copy</li>
</ol>
<p>Color dodge is division in disguise: <code>dodge(A, B) = A · 255 / (255 − B)</code>. Where the blurred-inverted layer is bright (i.e., the original was dark), the denominator is small and the quotient blows up to white — that's the empty paper. Where the original had a sharp luminance transition, the blur lags behind the edge, the quotient lands in the mid-grays, and you get a pencil stroke. Two sliders fall out naturally: the blur radius controls stroke width, and a few cascaded multiply passes control how dark the strokes sit on the paper.</p>
<p>Here's the actual FFmpeg filtergraph we run (slightly reformatted for readability):</p>
<pre><code>[0:v]gblur=sigma=PRE,format=gray,split[a0][b];
[a0]split[a][ams];
[ams]lutyuv=y='if(eq(val,0),0,255)'[am];
[b]negate,gblur=sigma=SIGMA[inv];
[inv][a]blend=all_mode=dodge[d0];
[d0][am]blend=all_mode=multiply[d];
[d]split[p1][p2];[p1][p2]blend=all_mode=multiply:all_opacity=O1[m1];
[m1]split[q1][q2];[q1][q2]blend=all_mode=multiply:all_opacity=O2[m2];
[m2]split[r1][r2];[r1][r2]blend=all_mode=multiply:all_opacity=O3[outv]
</code></pre>
<p>(The strange-looking mask in the middle — <code>if(eq(val,0),0,255)</code> — exists solely to align FFmpeg with a browser quirk. That's a Part 2 story, and it's a good one.)</p>
<p>Nothing here is exotic. That's the point.</p>
<h2>Why this cannot flicker</h2>
<p>AI restyle flickers because every frame is an independent sample from a generative model. Frame 23 and frame 24 go in nearly identical and come out as two <em>different plausible drawings</em> — same content, different micro-texture. String 30 of those per second together and the micro-texture dances. The industry throws optical flow, temporal attention, and consistency losses at this, and it helps, but it's fighting the architecture.</p>
<p>Our chain has the opposite property, and it decomposes into four mechanisms:</p>
<p><strong>1. Every output pixel is a pure function of the current frame.</strong></p>
<p><code>output_t = f(input_t)</code>, with <code>f</code> fixed for the entire video. No state, no random seed, no learned weights. The temporal variation of the output is <em>bounded by</em> the temporal variation of the input: if two adjacent frames are nearly identical, their outputs are nearly identical. Walking people and moving cameras are continuous signals, so the sketch of a walking person is continuous too.</p>
<p><strong>2. There are no hard decisions in the chain.</strong></p>
<p>The classic way per-frame processing flickers is a <strong>threshold</strong>: a pixel hovering at a decision boundary flips between "edge" and "not edge" on alternating frames, and you get sparkling outlines. Edge-detector-based stylization (Sobel, Canny, <code>edgedetect</code>) suffers from exactly this. Our chain contains only continuous operations — blur is a weighted average; negate, dodge, and multiply are pointwise smooth monotone functions. Move the input a little, the output moves a little. There is nothing to flip.</p>
<p><strong>3. The amplifier sits <em>after</em> the low-pass filter.</strong></p>
<p>Color dodge is a division, and division is a noise amplifier. What saves us is ordering: the signal that gets amplified has already been through two Gaussian blurs (a light pre-blur and the main stroke-width blur). Sensor noise and H.264 compression artifacts — the stuff that genuinely jumps around frame to frame — live in the spatial high frequencies, and a Gaussian kernel is their natural predator. What survives the blur is large-scale structure, which is exactly the part of the signal that's temporally stable. Low-pass first, amplifier second — we treat that ordering as the one non-negotiable rule when we add new styles.</p>
<p><strong>4. Nothing adapts per frame.</strong></p>
<p>A sneakier flicker source is per-frame adaptation: auto-contrast, per-frame histogram equalization, anything that "looks at this frame and tunes itself." Those hunt — this frame a little brighter, next frame a little darker. Every parameter in our chain (blur sigma, the three multiply opacities) is locked for the whole render. The chain has no feedback loop, so it can't hunt.</p>
<h2>The rules we now follow</h2>
<p>These four mechanisms turned into house rules as we design new styles on the same pipeline (charcoal, ink wash, a cartoon look are on the roadmap):</p>
<ul>
<li><strong>No per-frame randomness, ever.</strong> Charcoal needs grain — but if you generate fresh random noise each frame, you've re-introduced the dice roll and the picture will "sand-shimmer." Use one fixed grain texture, looped or slowly panned.</li>
<li><strong>Be suspicious of thresholds.</strong> Hard edge detection is the flicker factory. If a style needs outlines, the threshold has to be soft, temporally smoothed, or applied at reduced resolution.</li>
<li><strong>Blur before you amplify.</strong> Any pointwise operation that expands contrast (dodge, burn, gamma &lt; 1) belongs downstream of a low-pass.</li>
</ul>
<h2>Why deterministic, not AI</h2>
<p>For this use case, the choice was low-risk across four axes:</p>
<ul>
<li><strong>Cost</strong>: the chain is a handful of blurs and blends. It runs on commodity CPU, no GPU fleet, no model hosting.</li>
<li><strong>Speed</strong>: faster than realtime on a modern server, fast enough in a browser to power a <em>live</em> preview while the user drags sliders.</li>
<li><strong>Determinism</strong>: the preview the user sees is the export they get. (Getting that literally pixel-identical across two rendering engines was its own war — that's Part 2.)</li>
<li><strong>Temporal stability</strong>: free, as shown above, as a property of the math rather than a tuning target.</li>
</ul>
<p>AI restyle is the right tool for open-ended restyling — a filter chain will never reach "in the style of Van Gogh." A deterministic chain is the right tool when you want one specific, stable, controllable look. Sketch is that second case, and choosing it removed an entire class of temporal artifacts by construction.</p>
<hr />
<p><em>This effect powers the sketch tool at <a href="https://meicut.com/tools/video-sketch-effect">MeiCut</a>, where it runs both server-side and entirely in the browser. Part 2 of this series: <a href="#">making Canvas 2D and FFmpeg render the same sketch, pixel for pixel</a> — including the operand-order bug that silently washed out every frame, and the divide-by-zero where FFmpeg says white and the browser says black.</em></p>
]]></content:encoded></item></channel></rss>