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.
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) → Degrainoncore.mvu, one genericDegrainover the whole[bw₁,fw₁,…]vector list. - Multi-pass motion refinement —
RefineMotion=Nchains NRecalculatepasses, each halving the block size (e.g. refine a coarseblksize=32search down to 8) to recover detail that large blocks would otherwise blur.N=1reproduces Dogway's single refine pass;N≥2is asmdegrain_bisenhancement not present in the originalSMDegrain.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 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.
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):
- 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₁,…];RefineMotion=Nchains Nmvu.Recalculatepasses over the list, each halving the block size (N=1 is Dogway's single pass). - 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.
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-incore.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)
| File | Size | Uploaded | |
|---|---|---|---|
| smdegrain_bis-0.4.0.tar.gz | 77.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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