Skip to main content

smdegrain_bis

A motion-compensated temporal denoiser for VapourSynth. A pure temporal denoiser in the lineage of Dogway's Avisynth SMDegrain — it estimates motion between frames and averages each pixel along its motion trajectory, so grain and noise wash out while detail and edges stay put. Runs on mvutensils (core.mvu), which makes it ~1.4× faster than the mvtools implementation it was ported from (measured 1.43–1.46× vs the same mvtools settings). On 4K, the optional UHDhalf half-res motion search is a further, separate speedup on top of that.

Licence: GPL-3.0-or-later VapourSynth: API4 Python: 3.8+ Backend: mvutensils

Status: released. The filter is complete and validated across the pel × tr × UHD × LFR × interlaced grid, and is on PyPI (see Install).


Features

  • Motion-compensated temporal denoise — Super → Analyse → (Recalculate) → Degrain on core.mvu, one generic Degrain over the whole [bw₁,fw₁,…] vector list.
  • Multi-pass motion refinement — RefineMotion=N chains N Recalculate passes, each halving the block size (e.g. refine a coarse blksize=32 search down to 8) to recover detail that large blocks would otherwise blur. N=1 reproduces Dogway's single refine pass; N≥2 is a smdegrain_bis enhancement not present in the original SMDegrain.avsi (which refines only once).
  • Frame-prop aware — auto-detects TV/PC range (_ColorRange/_Range, R74+ safe), interlacing (_FieldBased) and HDR transfer, and derives chroma-SAD / scene-change thresholds from thSAD.
  • UHD-aware — optional half-resolution motion search (UHDhalf) on 4K+ sources, with a native C++ vector scaler; search runs at half-res, the render stays full-res.
  • 8–16-bit integer YUV/GRAY, native depth throughout (no 8-bit detours).
  • Optional prefilters — MinBlur / DFTTest / KNLMeansCL / BM3D / DGDenoise, or your own clip.
  • Optional finishing — low-frequency restore (LFR), DCTFlicker, contra-sharpening, interlaced weave.
  • Bring-your-own tonemapper — HDR sources are tone-mapped for the motion search only if you pass a tonemap_fn; the filter never bundles a tonemapper.

Install

pip install smdegrain-bis                 # the filter (pure Python)
pip install smdegrain-bis[uhdhalf]        # + the native UHDhalf vector scaler

smdegrain-bis is pure Python; [uhdhalf] additionally pulls vapoursynth-mvuscale, a small per-platform native plugin used only when UHDhalf engages. You need a working VapourSynth install and the mvutensils plugin (pip install vapoursynth-mvutensils, pulled in automatically).

Optional acceleration/prefilter paths (vsrgtools, vs-dfttest2) come with pip install smdegrain-bis[extras]; they are imported lazily and not required.

Prefer to manage plugins yourself? Build mvuscale from source (see below) and drop it into your VapourSynth plugins directory — it then autoloads and smdegrain-bis uses it without the [uhdhalf] extra.

No pip? (StaxRip and other hosts that bundle their own VapourSynth). Use a portable drop-in zip — the pure-Python package plus the mvuscale plugin for your platform — and copy the two folders into the host's VapourSynth Python path and plugins directory. See packaging/ to build one (packaging/make_portable_zip.sh). vapoursynth-mvutensils v9+ is a prerequisite either way (it is not bundled).


Quick start

import vapoursynth as vs
from smdegrain_bis import SMDegrain

core = vs.core
clip = ...                                  # YUV/GRAY, 8–16-bit integer (float is rejected)

den = SMDegrain(clip, tr=2, thSAD=300)                      # basic temporal denoise
den = SMDegrain(clip, tr=3, thSAD=400, RefineMotion=True)   # stronger, refined motion (1 pass)
den = SMDegrain(clip, blksize=32, RefineMotion=2)           # refine coarse blocks 32→16→8 (detail)
den = SMDegrain(clip, tr=2, UHDhalf=True)                   # 4K: half-res motion search

UHDhalf (default True) engages only on sources above 2599×1499; below that it is a no-op. When it engages it requires the mvuscale plugin (the [uhdhalf] extra, or a self-built .so), and the source width and height must be divisible by 4: the half-res search size is forced to even, so on a non-mod-4 source the ×2-scaled vectors no longer match the full-res render super and graph construction fails cleanly with Degrain: The motion vectors passed are not compatible with the super clip. Crop/pad to mod-4 upstream, or set UHDhalf=False for such sources.


Key parameters

parameter default meaning
tr 2 temporal radius — frames each side used for the average
thSAD 300 degrain strength (luma SAD threshold); higher = stronger
thSADC auto chroma SAD threshold (derived from thSAD via the v4.x scaleCSAD table)
RefineMotion False motion-vector refinement. False/0 = off; True/1 = one Recalculate pass at half the block size (Dogway's behaviour); N = chain N passes, each halving the block size again (e.g. blksize=32, RefineMotion=2 → 32→16→8), down to the 4×4 floor. Asking for more passes than the block size allows is clamped (with a warning) to the deepest that fits, so one setting works across clip sizes. N≥2 is a smdegrain_bis enhancement — the original avsi refines once
pel auto motion precision (½/¼-pel); auto = 1 on UHD, 2 otherwise
prefilter -1 motion-search prefilter (-1 MinBlur … 5 BM3D, or a clip)
contrasharp auto contra-sharpen the result toward the source
UHDhalf True half-resolution motion search on >2599×1499 sources (dimensions must be mod-4)
LFR False low-frequency detail restore, gated by a SADMask — see below
DCTFlicker False recursive flicker-calming pass (requires LFR)
interlaced auto auto-detected from _FieldBased; set False to force progressive
tv_range auto auto-detected from the range frame-prop
tonemap_fn None caller-supplied HDR tonemapper (search only)

The full v3.1.2d keyword set (blksize, overlap, search, limit, thSCD1/2, Str/Amp, Globals, …) is accepted verbatim.


Examples

HDR sources — tonemap_fn

The filter never bundles a tonemapper. For HDR (PQ/HLG) sources you pass a tonemap_fn (callable(clip) -> clip); it is applied to the motion-search prefilter only, and only when the source's _Transfer prop is PQ (16) or HLG (18). The denoised output keeps your original HDR clip untouched — motion estimation just gets to work on perceptually-even values instead of raw PQ.

import vapoursynth as vs
from smdegrain_bis import SMDegrain
core = vs.core

def tonemap(clip):
    # Any clip -> clip callable. Typically libplacebo (vs-placebo); kwargs illustrative.
    return clip.placebo.Tonemap(src_csp=1, dst_csp=0)          # PQ -> SDR for the search

hdr = core.lsmas.LWLibavSource("uhd_hdr.mkv")                  # e.g. 3840x2160 PQ
den = SMDegrain(hdr, tr=2, thSAD=400, UHDhalf=True, tonemap_fn=tonemap)
# `den` is still HDR; only the internal motion search saw the tone-mapped clip.

Without a tonemap_fn, an HDR source is denoised as-is (motion search runs on raw PQ/HLG values). The same hook exists on the standalone helper: prefilter_clip(clip, mode, tonemap_fn=...).

Prefilters (motion search only)

prefilter selects the clip motion is estimated on; the result is always degrained from the original. Pass a mode (int or name) or your own clip:

den = SMDegrain(clip, prefilter=-1)          # auto MinBlur (the default)
den = SMDegrain(clip, prefilter="dfttest")   # DFTTest with a luma mask
den = SMDegrain(clip, prefilter="knlmeans")  # KNLMeansCL (CUDA / ISPC / OpenCL auto-pick)
den = SMDegrain(clip, prefilter="bm3d")      # BM3D
den = SMDegrain(clip, prefilter="dgdenoise") # DGDenoise (DGDecNV)

my_pref = core.bm3dcpu.BM3Dv2(clip, sigma=3.0)   # ... or any prebuilt clip
den = SMDegrain(clip, prefilter=my_pref)

Low-frequency restore & de-flicker (LFR / DCTFlicker)

Back-ported v4.x finishing for high-tr / high-thSAD (or truemotion) runs, where strong temporal averaging can eat low-frequency detail:

den = SMDegrain(clip, tr=4, thSAD=600, LFR=True)                  # restore low-freq detail
den = SMDegrain(clip, tr=4, thSAD=600, LFR=True, DCTFlicker=True) # + calm the restored detail
den = SMDegrain(clip, LFR=300)                                    # explicit Hz cutoff (0..1920)

Interlaced content

interlaced auto-detects from _FieldBased; set it explicitly (with field order) when the prop is absent:

den = SMDegrain(clip, tr=2, interlaced=True, tff=True)           # top-field-first

How it works

A single SMDegrain() call runs this pipeline (motion stages on core.mvu):

  1. Frame-prop setup — read frame 0 for range / field / transfer; derive thSADC and thSCD1.
  2. Prefilter (motion search only) — optional denoise; HDR tone-mapped iff tonemap_fn is given.
  3. DitherLumaRebuild — TV→PC luma expansion so motion estimation sees more code values.
  4. UHDhalf (UHD only) — soft-cubic downscale to half-res for the search; render super stays full.
  5. Super — mvu.Super builds the hierarchical pyramid.
  6. Analyse → Recalculate — mvu.AnalyseMany(radius=tr) yields [bw₁,fw₁,…]; RefineMotion=N chains N mvu.Recalculate passes over the list, each halving the block size (N=1 is Dogway's single pass).
  7. UHDhalf vector scale — mvuscale.ScaleVect multiplies the half-res vectors up to full-res.
  8. Degrain — one generic mvu.Degrain(clip, super, vectors) does the compensated temporal average.
  9. LFR / DCTFlicker (optional) — mvu.SADMask-gated low-frequency restore + flicker calming.
  10. Contra-sharpen / interlaced weave (optional) — output finishing.

Version & provenance — this is a mix, not full v4.7.0d

smdegrain_bis is Selur's VapourSynth v3.1.2d port with selected v4.7.0d features back-ported — deliberately not a full v4.7.0d port. Each item below was audited against the Avisynth source.

Back-ported from the v4.7.0d avsiAdded (VapourSynth-era, not in the avsi)Not ported — still v3.1.2d
  • UHDhalf trigger + half-res search
  • thSADC / scaleCSAD chroma table
  • thSCD2 = 51 %
  • recalc-thSAD exp(-101/(thSAD·0.83))·360
  • thSCD1 0.35·thSAD+260
  • LFR + DCTFlicker
  • BM3D / DGDenoise prefilters, contra-sharpen
  • multi-pass RefineMotion=N (chained Recalculate, 32→…→8; the avsi refines once)
  • frame-prop defaults (range/field/transfer)
  • caller-injected tonemap_fn (HDR, search-only)
  • public prefilter_clip() helper
  • lazily-imported optional deps
  • adaptive searchparam / pelsearch
  • explicit plevel=0
  • hpad/vpad = isHD?0:blksize
  • the ~25-mode preset system
  • mvtools2-only scaleCSAD, temporal

The full audit and upgrade roadmap live in the development notes.


The mvutensils port

Backend moved from core.mv (mvtools) to core.mvu (mvutensils):

  • ~1.43× faster (non-UHDhalf), ~1.46× faster (UHDhalf) — measured on a 3840×2160 10-bit source.
  • Output SSIM ≈ 0.9998 vs the frozen mvtools reference — not byte-identical (mvu's auto pyramid caps one level shallower), though the atomic SAD/interpolation/MC ops are bit-exact.
  • Two mvtools bugs fixed: the 10→8→10-bit mv.Mask detour is gone (mvu.SADMask at native depth), and the UHDhalf + RefineMotion=False SIGSEGV cannot occur on mvu (so UHDhalf works with refinement off, and the manipmv dependency is dropped).
  • native/mvuscale — a ~40-line API4 filter that scales mvu's vector frame-props for UHDhalf; ~6× faster than the pure-Python scaler it replaced.

Change history: CHANGELOG.md.


Vendored code

smdegrain_bis/vendor/ bundles one upstream module — sharpen.py (MinBlur, sbr, ContraSharpening, LSFmod, Padding, …) — as a byte-identical copy from Selur's VapoursynthScriptsInHybrid (original authors: LaTo INV., Didée, and the havsfunc lineage). It is vendored rather than declared as a dependency because it's a loose community .py file, not a PyPI package. Bundling keeps pip install self-contained and pins the exact code.

It is imported as a package submodule (smdegrain_bis.vendor.sharpen), never as a top-level sharpen, so a wheel install can't collide with a host's own sharpen. Authorship and provenance are in smdegrain_bis/vendor/NOTICE — don't edit it in place; re-vendor by copying.


Plugin requirements

smdegrain_bis degrades gracefully: the default call needs only the motion backend, and every other plugin is pulled in only by the feature that uses it (verified by running the default path with nothing but mvutensils loaded).

Always required

  • mvutensils (core.mvu) — the motion backend (pip install vapoursynth-mvutensils).

Required by a feature — only when you enable it (all common VS plugins):

feature plugin(s)
UHDhalf (auto on 4K+ sources) mvuscale — the [uhdhalf] extra / vapoursynth-mvuscale
contrasharp (auto only when CClip is given) a RemoveGrain provider: zsmooth or removegrain
prefilter ≥ 2 (MinBlur r≥2) ctmf
prefilter DFTTest dfttest (or akarin / vs-dfttest2)
prefilter BM3D bm3dcpu / bm3dcuda
prefilter DGDenoise dgdecnv
prefilter KNLMeans nlm_ispc / nlm_cuda / knlmeanscl
LFR resize2 + the vsrgtools Python package

Optional accelerators — used if present, silent fallback otherwise:

  • Expr backend: llvmexpr → akarin → cranexpr, else the built-in core.std.Expr. None is required — any just speeds up the small per-pixel expressions.

Never required: vszip, awarp / warp, rgsf (all hasattr-guarded, float-only, or unreached by default settings).

Python: a working vapoursynth; vapoursynth-mvutensils (a dependency); optionally vsrgtools and vs-dfttest2.

Building the native scaler

The mvuscale plugin ships as the per-platform vapoursynth-mvuscale wheel, or build it yourself (the VapourSynth API4 headers are vendored — no SDK needed):

pip wheel native/ -w dist/                 # a wheel (scikit-build-core + CMake)
CXX=g++ bash native/build.sh               # or just the .so → tests/_plugins/mvuscale/libmvuscale.so

Drop the built .so into your VapourSynth plugins directory (or set MVUSCALE_PLUGIN=/path/to/it).


Licence

GPL-3.0-or-later — see LICENSE. This is a derivative of Dogway's SMDegrain.avsi (GPL-3.0); the motion plugins it uses (mvutensils, and mvtools for the reference build) are GPL-2.0-or-later, which GPL-3 subsumes.

Credits: Dogway (SMDegrain — original mod and the v4.x algorithm), Caroliano (original idea), Selur (the VapourSynth smdegrain.py baseline and the vendored sharpen / nnedi3_resample / color helpers), and the mvtools / mvutensils authors (dubhater, myrsloik). See smdegrain_bis/vendor/NOTICE for the vendored files and native/include/NOTICE for the VapourSynth headers.

Metadata

Release files for smdegrain-bis 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for smdegrain-bis 0.4.0
File Size Uploaded
smdegrain_bis-0.4.0.tar.gz 77.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for smdegrain-bis 0.4.0
File Interpreter ABI Platform
smdegrain_bis-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 135.5 kB

Release files / smdegrain_bis-0.4.0.tar.gz

Download URL smdegrain_bis-0.4.0.tar.gz
Size 77.4 kB
Tags Source
SHA-256 checksum
How to use checksums
8ebd10082fa9352749e331c002b595b1c5aba7ee2b6331d01f72a60ce6a21a26
BLAKE2b-256 checksum
How to use checksums
19d44152e74f1e7d42830b7295be969d3a74505a04220578389f9a94a84ddd05
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / smdegrain_bis-0.4.0-py3-none-any.whl

Download URL smdegrain_bis-0.4.0-py3-none-any.whl
Size 58.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
37c3a1447b8afad2a488c1b768d55692aeef9c63031625a7d7832d70f7613bc8
BLAKE2b-256 checksum
How to use checksums
eb14ff7c0fa8094816c2f22ac654c14d69b009ff60d52000f2d0ae7183683316
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.2

2 release files

0.4.1

2 release files

This release

0.4.0 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page