Skip to main content

VapourSynth motion-compensated temporal denoiser (SMDegrain lineage, mvutensils backend)

Project description

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).

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

Status: pre-release. The filter is complete and validated across the pel × tr × UHD × LFR × interlaced grid. Packaging is in place; PyPI publishing is pending — until then, install from source (see Install).


Features

  • Motion-compensated temporal denoiseSuper → Analyse → (Recalculate) → Degrain on core.mvu, one generic Degrain over the whole [bw₁,fw₁,…] vector list.
  • 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.


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
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).


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 run a Recalculate refinement pass over the vectors
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
LFR False low-frequency detail restore, gated by a SADMask
DCTFlicker False recursive flicker-calming pass
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. Supermvu.Super builds the hierarchical pyramid.
  6. Analyse → Recalculatemvu.AnalyseMany(radius=tr) yields [bw₁,fw₁,…]; RefineMotion runs one mvu.Recalculate over the list.
  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
  • 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.


Requirements

VapourSynth plugins: mvutensils (motion backend), an Expr backend (akarin / llvmexpr / cranexpr), and — for UHDhalf — the mvuscale filter (the [uhdhalf] extra). Prefilter/finishing paths optionally use dfttest, ctmf, removegrain, zsmooth, znedi3, resize2, bm3dcpu/bm3dcuda, nlm_ispc/nlm_cuda, dgdecnv.

Python: vapoursynth; 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.

Project details


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.1.0rc2.tar.gz (68.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.1.0rc2-py3-none-any.whl (54.2 kB view details)

Uploaded Python 3

File details

Details for the file smdegrain_bis-0.1.0rc2.tar.gz.

File metadata

  • Download URL: smdegrain_bis-0.1.0rc2.tar.gz
  • Upload date:
  • Size: 68.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.1.0rc2.tar.gz
Algorithm Hash digest
SHA256 19806ee5df5509ae54bd055dde9dd0c9e45a16e019d27301bea980b1019673be
MD5 f26c96c2fe211f687a872ce13045c88e
BLAKE2b-256 07cca3269e9188cdf415a457ff88fcc48c97bdac1613811317ed81aa7a2e03a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for smdegrain_bis-0.1.0rc2.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.1.0rc2-py3-none-any.whl.

File metadata

File hashes

Hashes for smdegrain_bis-0.1.0rc2-py3-none-any.whl
Algorithm Hash digest
SHA256 ca8673f18ae00da7feb5c4092977a85d223917875bb21a2c8cc7f53e5b38c5b7
MD5 8e04e99dde0c86139caa342bf99ac185
BLAKE2b-256 1bdc79330ae41c6cc99c377740ece9b6a3e799a0f6e48f0159b9fc5de14d5963

See more details on using hashes here.

Provenance

The following attestation bundles were made for smdegrain_bis-0.1.0rc2-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.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page