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) — ~1.4× faster than the mvtools implementation it was ported from (measured 1.43× at matched settings, 1.46× on the UHDhalf path).

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 denoiseSuper → Analyse → (Recalculate) → Degrain on core.mvu, one generic Degrain over the whole [bw₁,fw₁,…] vector list.
  • Multi-pass motion refinementRefineMotion=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 v4+ 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. 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 SADMaskneeds mvutensils v4+, see below
DCTFlicker False recursive flicker-calming pass (requires LFR; same v4+ requirement)
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)

[!IMPORTANT] LFR / DCTFlicker need mvutensils v4 or newer. They gate the low-frequency restore with a motion-confidence mask from mvu.SADMask, which had a data race in mvutensils before v4: myrsloik/mvutensils#5 (fixed in v4). On the affected versions SADMask, VectorLengthMask and OcclusionMask all register as fmParallel but share one filter-instance zimg scratch buffer, so parallel frame requests scribble over each other — a multi-threaded render of real content dies with double free or corruption, a segfault, or std::system_error at nondeterministic frames. It is clean single-threaded, so there it hides from core.num_threads = 1 runs and from vspipe --info, surfacing only in a real encode.

On v4+ the race is fixed and LFR is safe. The package requires vapoursynth-mvutensils>=4, so a normal pip install is already safe — this only bites if you force an older mvutensils below that floor, in which case upgrade it or keep LFR=False (the default).

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. Supermvu.Super builds the hierarchical pyramid.
  6. Analyse → Recalculatemvu.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 scalemvuscale.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: llvmexprakarincranexpr, 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

smdegrain_bis-0.3.0.tar.gz (75.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

smdegrain_bis-0.3.0-py3-none-any.whl (57.8 kB view details)

Uploaded Python 3

File details

Details for the file smdegrain_bis-0.3.0.tar.gz.

File metadata

  • Download URL: smdegrain_bis-0.3.0.tar.gz
  • Upload date:
  • Size: 75.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for smdegrain_bis-0.3.0.tar.gz
Algorithm Hash digest
SHA256 61fcf43c51b17e799941c1613a9faf34f65f851a9652634cccba850547fec137
MD5 36d1f415d2966e47b5dd1af1a583db3d
BLAKE2b-256 1b847482741aec31dee37ca8b1973a1d0ca7790732d54c99be589a8ed654f77c

See more details on using hashes here.

Provenance

The following attestation bundles were made for smdegrain_bis-0.3.0.tar.gz:

Publisher: release.yml on FlorianGamper/vs-smdegrain-bis

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file smdegrain_bis-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: smdegrain_bis-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 57.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for smdegrain_bis-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e92ca393bac5bf1a399ff62363d6eef16cb302bacf463f556c16ae4ba01e4cb0
MD5 784a793d5d998740ff2e7b0be4221f5d
BLAKE2b-256 83a73e9c5329ac69d53cc2bd6efbc2113f1ed2e55c9627186809006b6e6cdbe2

See more details on using hashes here.

Provenance

The following attestation bundles were made for smdegrain_bis-0.3.0-py3-none-any.whl:

Publisher: release.yml on FlorianGamper/vs-smdegrain-bis

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.1

2 files

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 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