Nodes/VizClick NormalCrafter/VizClick Normal Temporal Stabilizer
ComfyUI Node

VizClick Normal Temporal Stabilizer

Fixing the flicker your video normal maps still have

By zuanfilm·Created 17 days ago·Updated 17 days ago· 3
VizClick Normal Temporal Stabilizer
  • source_images
  • normal_video
  • stabilized_normals
◄temporal_strength0.65►
◄motion_strength1.00►
◄consistency_threshold0.12►

Even a model built for temporal consistency leaves residual shimmer - a pixel of angular wobble per frame that becomes crawling noise the moment anything downstream reads those normals. VizClick Normal Temporal Stabilizer is the cleanup pass: RGB frames plus the normal sequence in, normals that hold still out.

The interesting part is what it doesn't do. No second neural network, no checkpoint, no download - it's OpenCV optical flow and some vector math, and it's worth knowing about even if you never touch NormalCrafter itself.

Why not just average frames together

The obvious fix - blend each normal frame with the previous one - smears anything that moves: a shoulder leads the frame while the old one is still written into the vectors at its previous position. Trails read as geometry that isn't there, which is worse than flicker.

So the stabilizer motion-compensates first. It estimates dense optical flow between consecutive source RGB frames with Farnebäck flow, warps the previous stabilized normal map into the current frame's coordinates, and only then blends. The source video is strictly a motion guide - never mixed into the normals, which is the right call. Blending colour into a normal map doesn't give you a slightly blurred normal, it gives you a vector pointing where the surface isn't.

The second trick is that the blend isn't uniform across the image. Each pixel's warped history is compared against the current raw estimate by angle:

dot = (cur_normal * warped_prev).sum(dim=-1).clamp(-1, 1)
angular_error = torch.acos(dot) / np.pi
consistency = torch.exp(-((angular_error / consistency_threshold) ** 2))
alpha = temporal_strength * consistency

Where the two agree, alpha sits near temporal_strength and history dominates. Where they disagree - occlusion, real geometry change, a bad flow match - the gaussian falls off and the current frame wins. Then everything gets renormalized to unit length. It's a confidence-weighted blend, not a fixed opacity slider, which is why it can de-flicker a static wall and still let a hand move.

Inputs you actually set

source_images and normal_video are the package: the same RGB batch that went into NormalCrafter, and its normal_video output. Frame counts are truncated to whichever is shorter - silently - so if your two branches drifted apart you'll get a shorter result, not an error. Compare lengths if something looks off.

temporal_strength (0.65) is your flicker dial: the maximum weight the warped history can carry. 0 turns the node into a passthrough. Go too high and you get trails. The author's own starting range is 0.35–0.65, and I'd start at the bottom of that on any clip with fast motion.

motion_strength (1.0) scales the flow field before warping. This is your escape hatch when the flow estimate is bad - soft footage, motion blur, heavy grain. Dialing it to 0.6–0.8 means "trust the motion less, warp less," which trades a bit of stabilization for fewer warp artifacts.

consistency_threshold (0.12, range 0.01–0.50) controls how fast blending dies as the two estimates disagree. Smaller is more selective - it snaps to the current frame sooner, preserving real geometry changes. Larger blends across bigger disagreements and stabilizes harder, at the cost of reacting slowly. People get this wrong by treating it as another strength slider; it's a sharpness-of-trust knob, not a volume knob.

Out comes one output, stabilized_normals. Connect that to your preview and to VHS_VideoCombine.images for the MP4. Connecting VHS_VideoCombine straight to NormalCrafter's output instead is the single most common way to conclude "this node does nothing."

Installing

Same pack, same install - you don't install this separately:

cd ComfyUI/custom_nodes
git clone https://github.com/zuanfilm/ComfyUI-VizClick-NormalCrafter.git

Restart and look for VizClick Normal Temporal Stabilizer under VizClick/Video. No model files at all - just opencv-python>=4.8, which the pack declares. The package wraps each node import in its own try/except, so a missing cv2 prints a console message about the stabilizer failing while the NormalCrafter node still shows up. If the node is simply absent, read the console before reinstalling.

Tuning it, and where it fails

Start with the defaults, look at the raw normals first, then the stabilized ones. If the raw output is geometrically wrong, this node can't help - it reduces residual flicker, it doesn't correct geometry.

Trails or a laggy, rubbery feel mean temporal_strength is too high for the motion, or consistency_threshold too generous. Residual shimmer on a stable picture means the opposite. And on fast cuts, flashes, disocclusions, big camera moves or textureless areas, Farnebäck flow will guess wrong - the gaussian gate reduces the damage, it can't prevent it. Cutting the shot into separate runs beats any parameter.

One practical cost: dense flow on CPU, every frame pair. Fast next to the diffusion model it follows, so it won't be your bottleneck - but five minutes of 1080p isn't free either.

The honest framing: this node exists because the model underneath it is mostly temporally consistent, and "mostly" is where the ugly lives. If you're exporting normal sequences for relighting or compositing, it's the difference between a result you can use and one you can't.

CategoryVizClick/Video

Inputs (5)

NameTypeDefaultDescription
source_imagesIMAGE—
normal_videoIMAGE—
temporal_strengthFLOAT0.650–1—
motion_strengthFLOAT1.000–1—
consistency_thresholdFLOAT0.120.01–0.5—

Outputs (1)

NameTypeDescription
stabilized_normalsIMAGE—