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).
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 denoise —
Super → Analyse → (Recalculate) → Degrainoncore.mvu, one genericDegrainover 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 fromthSAD. - 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
mvuscalefrom source (see below) and drop it into your VapourSynth plugins directory — it then autoloads andsmdegrain-bisuses 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):
- Frame-prop setup — read frame 0 for range / field / transfer; derive
thSADCandthSCD1. - Prefilter (motion search only) — optional denoise; HDR tone-mapped iff
tonemap_fnis given. - DitherLumaRebuild — TV→PC luma expansion so motion estimation sees more code values.
- UHDhalf (UHD only) — soft-cubic downscale to half-res for the search; render super stays full.
- Super —
mvu.Superbuilds the hierarchical pyramid. - Analyse → Recalculate —
mvu.AnalyseMany(radius=tr)yields[bw₁,fw₁,…];RefineMotionruns onemvu.Recalculateover the list. - UHDhalf vector scale —
mvuscale.ScaleVectmultiplies the half-res vectors up to full-res. - Degrain — one generic
mvu.Degrain(clip, super, vectors)does the compensated temporal average. - LFR / DCTFlicker (optional) —
mvu.SADMask-gated low-frequency restore + flicker calming. - 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 avsi | Added (VapourSynth-era, not in the avsi) | Not ported — still v3.1.2d |
|---|---|---|
|
|
|
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.Maskdetour is gone (mvu.SADMaskat native depth), and theUHDhalf+RefineMotion=FalseSIGSEGV cannot occur on mvu (soUHDhalfworks with refinement off, and themanipmvdependency 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file smdegrain_bis-0.1.0rc1.tar.gz.
File metadata
- Download URL: smdegrain_bis-0.1.0rc1.tar.gz
- Upload date:
- Size: 69.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d79851cab11760c66d3ee81c6316d33813c576601fba04cf3eec789db88a9851
|
|
| MD5 |
77862c5111d7e4a5ab92156de0c4e7cd
|
|
| BLAKE2b-256 |
324254c9d5817e9559f819b7d267f0b0ff0c1e3472b4047996b9f208fed4a9d1
|
Provenance
The following attestation bundles were made for smdegrain_bis-0.1.0rc1.tar.gz:
Publisher:
release.yml on FlorianGamper/vs-smdegrain-bis
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
smdegrain_bis-0.1.0rc1.tar.gz -
Subject digest:
d79851cab11760c66d3ee81c6316d33813c576601fba04cf3eec789db88a9851 - Sigstore transparency entry: 2145955192
- Sigstore integration time:
-
Permalink:
FlorianGamper/vs-smdegrain-bis@5f26287822ada8bcd0b9ef26f2adc1a656b88809 -
Branch / Tag:
refs/tags/v0.1.0rc1 - Owner: https://github.com/FlorianGamper
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5f26287822ada8bcd0b9ef26f2adc1a656b88809 -
Trigger Event:
push
-
Statement type:
File details
Details for the file smdegrain_bis-0.1.0rc1-py3-none-any.whl.
File metadata
- Download URL: smdegrain_bis-0.1.0rc1-py3-none-any.whl
- Upload date:
- Size: 54.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
744d0065d079cea5317430cde82288f1ee6461ca1f9b53e6eef5ef58cb07ee41
|
|
| MD5 |
d64c979291e2eac446503df3617ee5d2
|
|
| BLAKE2b-256 |
bdde03f876211afca30c14a1ae0edef85a950a23f68e2ea172c0c77ae315d9fa
|
Provenance
The following attestation bundles were made for smdegrain_bis-0.1.0rc1-py3-none-any.whl:
Publisher:
release.yml on FlorianGamper/vs-smdegrain-bis
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
smdegrain_bis-0.1.0rc1-py3-none-any.whl -
Subject digest:
744d0065d079cea5317430cde82288f1ee6461ca1f9b53e6eef5ef58cb07ee41 - Sigstore transparency entry: 2145955203
- Sigstore integration time:
-
Permalink:
FlorianGamper/vs-smdegrain-bis@5f26287822ada8bcd0b9ef26f2adc1a656b88809 -
Branch / Tag:
refs/tags/v0.1.0rc1 - Owner: https://github.com/FlorianGamper
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5f26287822ada8bcd0b9ef26f2adc1a656b88809 -
Trigger Event:
push
-
Statement type: