diff --git a/docs/submodules/prefilters.rst b/docs/submodules/prefilters.rst index 0b511cd..3095d08 100644 --- a/docs/submodules/prefilters.rst +++ b/docs/submodules/prefilters.rst @@ -6,7 +6,6 @@ Prefilters vsdenoise.prefilters.Prefilter vsdenoise.prefilters.prefilter_to_full_range - vsdenoise.prefilters.PelType .. automodule:: vsdenoise.prefilters :members: diff --git a/vsdenoise/__init__.py b/vsdenoise/__init__.py index aaad58e..9a07961 100644 --- a/vsdenoise/__init__.py +++ b/vsdenoise/__init__.py @@ -6,8 +6,8 @@ from .deblock import * from .fft import * from .freqs import * -from .funcs import * -from .limit import * +#from .funcs import * +#from .limit import * from .mvtools import * from .nlm import * from .postprocess import * diff --git a/vsdenoise/mvtools/__init__.py b/vsdenoise/mvtools/__init__.py index bd3c0b1..5b57a9c 100644 --- a/vsdenoise/mvtools/__init__.py +++ b/vsdenoise/mvtools/__init__.py @@ -7,5 +7,5 @@ from .enums import * from .motion import * from .mvtools import * -from .presets import * +#from .presets import * from .utils import * diff --git a/vsdenoise/mvtools/enums.py b/vsdenoise/mvtools/enums.py index 1ad2222..e2fbbcd 100644 --- a/vsdenoise/mvtools/enums.py +++ b/vsdenoise/mvtools/enums.py @@ -1,129 +1,124 @@ from __future__ import annotations -from dataclasses import dataclass -from math import exp -from typing import Any, Literal, TypeVar, cast, overload +from typing import Any, cast from vstools import ( - MISSING, CustomIntEnum, CustomStrEnum, CustomValueError, MissingT, VSFunctionAllArgs, VSFunctionKwArgs, core, + CustomIntEnum, CustomValueError, VSFunctionAllArgs, VSFunctionKwArgs, core, fallback, vs ) __all__ = [ - 'MVDirection', - 'MVToolsPlugin', - 'SADMode', 'SearchMode', 'MotionMode', 'FlowMode', 'FinestMode' -] - - -class MVDirection(CustomStrEnum): - """Motion vector analyze direction.""" - - BACK = 'backward' - """Backwards motion detection.""" - - FWRD = 'forward' - """Forwards motion detection.""" - - @property - def isb(self) -> bool: - """Whether it's using backwards motion detection.""" + 'MVDirection', - return self is MVDirection.BACK + 'SharpMode', 'RFilterMode', 'SearchMode', 'SADMode', + 'MotionMode', 'PenaltyMode', 'FlowMode', 'MaskMode' +] class MVToolsPlugin(CustomIntEnum): - """Abstraction around the three versions of mvtools plugins that exist.""" + """Abstraction around all three mvtools plugin versions.""" INTEGER = 0 """Original plugin. Only accepts integer 8-16 bits clips.""" - FLOAT_OLD = 1 - """New plugin by IFeelBloated. Latest release. Only works with float single precision clips.""" - - FLOAT_NEW = 2 - """Latest git master of :py:attr:`FLOAT_OLD`. Must be compiled by yourself.""" + FLOAT = 1 + """Fork by IFeelBloated. Only works with float single precision clips.""" @property def namespace(self) -> Any: - return core.mv if self is MVToolsPlugin.INTEGER else core.mvsf + """Get the appropriate MVTools namespace based on plugin type.""" + + return core.lazy.mv if self is MVToolsPlugin.INTEGER else core.lazy.mvsf @property def Super(self) -> VSFunctionKwArgs: + """Get the Super function for creating motion vector clips.""" + return cast(VSFunctionKwArgs, self.namespace.Super) @property - def Analyse(self) -> VSFunctionKwArgs: + def Analyze(self) -> VSFunctionKwArgs: + """Get the Analyze function for analyzing motion vectors.""" + return cast( - VSFunctionKwArgs, self.namespace.Analyze if self is MVToolsPlugin.FLOAT_NEW else self.namespace.Analyse + VSFunctionKwArgs, self.namespace.Analyze if self is MVToolsPlugin.FLOAT else self.namespace.Analyse ) @property def Recalculate(self) -> VSFunctionAllArgs: + """Get the Recalculate function for refining motion vectors.""" + return cast(VSFunctionAllArgs, self.namespace.Recalculate) @property def Compensate(self) -> VSFunctionKwArgs: + """Get the Compensate function for motion compensation.""" + return cast(VSFunctionKwArgs, self.namespace.Compensate) @property def Flow(self) -> VSFunctionAllArgs: + """Get the Flow function for motion vector visualization.""" + return cast(VSFunctionAllArgs, self.namespace.Flow) @property def FlowInter(self) -> VSFunctionAllArgs: + """Get the FlowInter function for motion-compensated frame interpolation.""" + return cast(VSFunctionAllArgs, self.namespace.FlowInter) @property def FlowBlur(self) -> VSFunctionAllArgs: + """Get the FlowBlur function for motion-compensated frame blending.""" + return cast(VSFunctionAllArgs, self.namespace.FlowBlur) @property def FlowFPS(self) -> VSFunctionAllArgs: + """Get the FlowFPS function for motion-compensated frame rate conversion.""" + return cast(VSFunctionAllArgs, self.namespace.FlowFPS) @property def BlockFPS(self) -> VSFunctionAllArgs: + """Get the BlockFPS function for block-based frame rate conversion.""" + return cast(VSFunctionAllArgs, self.namespace.BlockFPS) @property def Mask(self) -> VSFunctionAllArgs: + """Get the Mask function for generating motion masks.""" + return cast(VSFunctionAllArgs, self.namespace.Mask) @property def SCDetection(self) -> VSFunctionAllArgs: + """Get the SCDetection function for scene change detection.""" + return cast(VSFunctionAllArgs, self.namespace.SCDetection) - @property - def Finest(self) -> VSFunctionAllArgs: - return cast(VSFunctionAllArgs, self.namespace.Finest) + def Degrain(self, tr: int | None = None) -> VSFunctionAllArgs: + """Get the Degrain function for motion vector denoising.""" - def Degrain(self, radius: int | None = None) -> VSFunctionAllArgs: - if radius is None and self is not MVToolsPlugin.FLOAT_NEW: - raise CustomValueError('This implementation needs a radius!', f'{self.name}.Degrain') - - if radius is not None and radius > 24 and self is not MVToolsPlugin.FLOAT_NEW: - raise ImportError( - f"{self.name}.Degrain: With the current settings, temporal radius > 24, you're gonna need the latest " - "master of mvsf and you're using an older version." - "\n\tPlease build it from: https://github.com/IFeelBloated/vapoursynth-mvtools-sf" - ) + if tr is None and self is not MVToolsPlugin.FLOAT: + raise CustomValueError('This implementation needs a temporal radius!', f'{self.name}.Degrain') try: - return cast(VSFunctionAllArgs, getattr(self.namespace, f"Degrain{fallback(radius, '')}")) + return cast(VSFunctionAllArgs, getattr(self.namespace, f"Degrain{fallback(tr, '')}")) except AttributeError: - raise CustomValueError(f'This radius isn\'t supported! ({radius})', f'{self.name}.Degrain') + raise CustomValueError(f'This temporal radius isn\'t supported! ({tr})', f'{self.name}.Degrain') @classmethod def from_video(cls, clip: vs.VideoNode) -> MVToolsPlugin: """ - Automatically select the appropriate plugin for the specified clip. + Automatically select the appropriate plugin based on the given clip. - :param clip: Clip you will use the plugin on. + :param clip: The clip to process. - :return: Correct MVToolsPlugin for the specified clip. + :return: The accompanying MVTools plugin for the clip. """ assert clip.format @@ -136,10 +131,8 @@ def from_video(cls, clip: vs.VideoNode) -> MVToolsPlugin: "\n\tPlease download it from: https://github.com/IFeelBloated/vapoursynth-mvtools-sf" ) - if hasattr(core.mvsf, 'Degrain'): - return MVToolsPlugin.FLOAT_NEW + return MVToolsPlugin.FLOAT - return MVToolsPlugin.FLOAT_OLD elif not hasattr(core, 'mv'): raise ImportError( "MVTools: You're missing mvtools." @@ -149,121 +142,73 @@ def from_video(cls, clip: vs.VideoNode) -> MVToolsPlugin: return MVToolsPlugin.INTEGER -class SADMode(CustomIntEnum): - """ - SAD Calculation mode for MVTools. - - Decides the using of block pure Spatial Data, DCT, SAD, or SATD for SAD calculation. - - SAD => Sum of Absolute Difference (The main parameter mvtools uses).\n - This is calculated over the 2 macroblocks that get compared. +class MVDirection(CustomIntEnum): + """Motion vector analyze direction.""" - DCT => Discrete Cosine Transform (Frequency Spectrum).\n - Transform both the current blocks and the reference block to frequency domain, - then calculate the sum of the absolute difference between each pair of transformed samples in that domain. + FWRD = 0 + """Forwards motion detection.""" - SATD => Sum of Hadamard Transformed Differences.\n - Get the difference block between the current blocks and the reference block, - and transform that difference block to frequency domain and calculate the sum of the absolute value - of each sample in that transformed difference block. + BACK = 1 + """Backwards motion detection.""" - You can read more about the algorithm SATD uses `here `_.\n - The actual implementation is a recursive Hadamard Ordered Walsh-Hadamard Transform. - The usage of DCT in particular, can improve motion vector estimation in the event of luma flicker and fades. +class SharpMode(CustomIntEnum): """ + Subpixel interpolation method for pel = 2 or 4. - SPATIAL = 0 - """Regular usage of spatial block data only, does not use DCT.""" - - DCT = 1 - """Use block DCT instead of spatial data (slow for block size of 8x8 and very slow for smaller sizes).""" - - MIXED_SPATIAL_DCT = 2 - """Mixed spatial and DCT data; weight is dependent on mean frame luma difference.""" - - ADAPTIVE_SPATIAL_MIXED = 3 - """Adaptive per-block switching from spatial to equal-weighted mixed mode.""" - - ADAPTIVE_SPATIAL_DCT = 4 - """Adaptive per-block switching from spatial to mixed mode with more weight given to DCT.""" - - SATD = 5 - """SATD instead of SAD for luma.""" - - MIXED_SATD_DCT = 6 - """Same as 2, except use SATD instead of SAD.""" - - ADAPTIVE_SATD_MIXED = 7 - """Same as 3, except use SATD instead of SAD.""" - - ADAPTIVE_SATD_DCT = 8 - """Same as 4, except use SATD instead of SAD.""" - - MIXED_SATEQSATD_DCT = 9 - """Similar to 2, use SATD and weight ranges from SAD only to equal SAD & SATD.""" - - ADAPTIVE_SATD_MAJLUMA = 10 - """Similar to 3 and 4, use SATD weight is on SAD, only on strong luma changes.""" - - @property - def is_satd(self) -> bool: - """Returns whether this SADMode uses SATD rather than SAD.""" - - return self >= SADMode.SATD - - @property - def same_recalc(self: SelfSADMode) -> tuple[SelfSADMode, SelfSADMode]: - return (self, self) + This enum controls the calculation of the first level only. + If pel=4, bilinear interpolation is always used to compute the second level. + """ + BILINEAR = 0 + """Soft bilinear interpolation.""" -SelfSADMode = TypeVar('SelfSADMode', bound=SADMode) + BICUBIC = 1 + """Bicubic interpolation (4-tap Catmull-Rom).""" + WIENER = 2 + """Sharper Wiener interpolation (6-tap, similar to Lanczos).""" -class SearchModeBase: - @dataclass - class Config: - """Dataclass to represent all the search related parameters.""" - mode: SearchMode - """SearchMode that decides which analysis mode to use for search of motion vectors.""" +class RFilterMode(CustomIntEnum): + """Hierarchical levels smoothing and reducing (halving) filter.""" - recalc_mode: SearchMode - """SearchMode that decides which analysis mode to use for recalculation of motion vectors.""" + AVERAGE = 0 + """Simple 4 pixels averaging.""" - param: int | None - """Parameter used by the search mode in analysis.""" + TRIANGLE_SHIFTED = 1 + """Triangle (shifted) filter for more smoothing (decrease aliasing).""" - param_recalc: int | None - """Parameter used by the search mode in recalculation.""" + TRIANGLE = 2 + """Triangle filter for even more smoothing.""" - pel: int | None - """Parameter used by search mode for subpixel accuracy.""" + QUADRATIC = 3 + """Quadratic filter for even more smoothing.""" + CUBIC = 4 + """Cubic filter for even more smoothing.""" -class SearchMode(SearchModeBase, CustomIntEnum): - """Decides the type of search at every level of the hierarchial analysis made while searching for motion vectors.""" - AUTO = -1 - """Automatically select a SearchMode.""" +class SearchMode(CustomIntEnum): + """Decides the type of search at every level.""" ONETIME = 0 """One time search.""" NSTEP = 1 - """N step searches. The most well-known of the MV search algorithms.""" + """N step searches.""" DIAMOND = 2 - """Logarithmic search, also known as Diamond Search.""" + """Logarithmic search, also named Diamond Search.""" + + EXHAUSTIVE = 3 + """Exhaustive search, square side is 2 * radius + 1. It's slow, but gives the best results SAD-wise.""" HEXAGON = 4 - """Hexagon search (similar to x264's).""" + """Hexagon search (similar to x264).""" UMH = 5 - """Uneven Multi Hexagon search (similar to x264's).""" - - EXHAUSTIVE = 3 - """Exhaustive search, square side is 2 * radius + 1. It's slow, but gives the best results SAD-wise.""" + """Uneven Multi Hexagon search (similar to x264).""" EXHAUSTIVE_H = 6 """Pure horizontal exhaustive search, width is 2 * radius + 1.""" @@ -271,304 +216,102 @@ class SearchMode(SearchModeBase, CustomIntEnum): EXHAUSTIVE_V = 7 """Pure vertical exhaustive search, height is 2 * radius + 1.""" - @overload - def __call__( # type: ignore - self: Literal[ONETIME], step: int | tuple[int, int] | None = ..., pel: int | None = ..., - recalc_mode: SearchMode = ..., /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param step: Step between each vector tried. If > 1, step will be progressively refined. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - @overload - def __call__( # type: ignore - self: Literal[NSTEP], times: int | tuple[int, int] | None = ..., pel: int | None = ..., - recalc_mode: SearchMode = ..., /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param times: Number of step for search. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - @overload - def __call__( # type: ignore - self: Literal[DIAMOND], init_step: int | tuple[int, int] | None = ..., pel: int | None = ..., - recalc_mode: SearchMode = ..., /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param init_step: Initial step search, then refined progressively. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - @overload - def __call__( # type: ignore - self: Literal[HEXAGON], range: int | tuple[int, int] | None = ..., pel: int | None = ..., - recalc_mode: SearchMode = ..., /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param range: Range of search. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - @overload - def __call__( # type: ignore - self: Literal[UMH], range: int | tuple[int, int] | None = ..., pel: int | None = ..., - recalc_mode: SearchMode = ..., /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param range: Radius of the multi hexagonal search. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - @overload - def __call__( # type: ignore - self: Literal[EXHAUSTIVE] | Literal[EXHAUSTIVE_H] | Literal[EXHAUSTIVE_V], - radius: int | tuple[int, int] | None = ..., pel: int | None = ..., recalc_mode: SearchMode = ..., - /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param radius: Radius of the exhaustive (tesa) search. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - @overload - def __call__( - self, param: int | tuple[int, int] | None = ..., pel: int | None = ..., recalc_mode: SearchMode = ..., - /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param param: Parameter used by the search mode. Purpose depends on the mode. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - def __call__( - self, param: int | tuple[int, int] | None | MissingT = MISSING, pel: int | None | MissingT = MISSING, - recalc_mode: SearchMode | MissingT = MISSING, /, **kwargs: Any - ) -> SearchMode.Config: - """ - Get the :py:class:`SearchMode.Config` from this mode and params. - - :param step: Parameter of the mvtools search mode. - :param pel: Search pixel enlargement, for subpixel precision. - - :return: :py:class:`SearchMode.Config` from this mode, param and accuracy. - """ - - is_uhd = kwargs.get('is_uhd', False) - refine = kwargs.get('refine', 3) - truemotion = kwargs.get('truemotion', False) - - if self is SearchMode.AUTO: - self = SearchMode.DIAMOND - - if recalc_mode is MISSING: - recalc_mode = SearchMode.ONETIME - - param_recalc: int | MissingT | None - - if isinstance(param, int): - param, param_recalc = param, MISSING - elif isinstance(param, tuple): - param, param_recalc = param - else: - param = param_recalc = param - - if param is MISSING: - param = (2 if is_uhd else 5) if (refine and truemotion) else (1 if is_uhd else 2) - - param_c = fallback(param, 2) - - if param_recalc is MISSING: - param_recalc = max(0, round(exp(0.69 * param_c - 1.79) - 0.67)) - if pel is MISSING: - pel = min(8, max(0, param_c * 2 - 2)) - - return SearchMode.Config(self, recalc_mode, param, param_recalc, pel) # type: ignore - - @property - def defaults(self) -> SearchMode.Config: - return self(None, None, self) - - -class MotionMode: +class SADMode(CustomIntEnum): """ - A preset or custom parameters values for truemotion/motion analysis modes of mvtools. - - Presets allows easy to switch default values of all "true motion" parameters at once. + Specifies how block differences (SAD) are calculated between frames. + Can use spatial data, DCT coefficients, SATD, or combinations to improve motion estimation. """ - @dataclass - class Config: - """Dataclass to represent all the "true motion" parameters.""" - - truemotion: bool - """Straight MVTools preset parameter.""" - - coherence: int - """ - Coherence of the field of vectors. The higher, the more coherent. + SPATIAL = 0 + """Calculate differences using raw pixel values in spatial domain.""" - However, if set too high, some best motion vectors can be missed. + DCT = 1 + """Calculate differences using DCT coefficients. Slower, especially for block sizes other than 8x8.""" - Values around 400 - 2000 (for block size 8) are strongly recommended. + MIXED_SPATIAL_DCT = 2 + """Use both spatial and DCT data, weighted based on the average luma difference between frames.""" - Internally it is coefficient for SAD penalty of vector squared - difference from predictors (neighbors), scaled by 256. + ADAPTIVE_SPATIAL_MIXED = 3 + """Adaptively choose between spatial data or an equal mix of spatial and DCT data for each block.""" - This is the "lambda" parameter of MVTools. - """ + ADAPTIVE_SPATIAL_DCT = 4 + """Adaptively choose between spatial data or DCT-weighted mixed mode for each block.""" - sad_limit: int - """ - SAD limit for coherence using. + SATD = 5 + """Use Sum of Absolute Transformed Differences (SATD) instead of SAD for luma comparison.""" - Local coherence is decreased if SAD value of vector predictor (formed from neighbor blocks) - is greater than the limit. It prevents bad predictors using but decreases the motion coherence. + MIXED_SATD_DCT = 6 + """Like MIXED_SPATIAL_DCT but uses SATD instead of SAD.""" - Values above 1000 (for block size=8) are recommended for true motion. + ADAPTIVE_SATD_MIXED = 7 + """Like ADAPTIVE_SPATIAL_MIXED but uses SATD instead of SAD.""" - This is the "lsad" parameter of MVTools. - """ + ADAPTIVE_SATD_DCT = 8 + """Like ADAPTIVE_SPATIAL_DCT but uses SATD instead of SAD.""" - pnew: int - """ - Relative penalty (scaled to 256) to SAD cost for new candidate vector. + MIXED_SADEQSATD_DCT = 9 + """Mix of SAD, SATD and DCT data. Weight varies from SAD-only to equal SAD/SATD mix.""" - New candidate vector will be accepted as new vector only if its SAD with penalty (SAD + SAD*pnew/256) - is lower then predictor cost (old SAD). + ADAPTIVE_SATD_LUMA = 10 + """Adaptively use SATD weighted by SAD, but only when there are significant luma changes.""" - It prevent replacing of quite good predictors by new vector - with a slightly better SAD but different length and direction. - """ - plevel: int - """ - Penalty factor coherence level scaling mode. - * 0 - No scaling. - * 1 - Linear. - * 2 - Quadratic dependence from hierarchical level size. +class MotionMode(CustomIntEnum): + """ + Controls how motion vectors are searched and selected. - Note that vector length is smaller at lower level. - """ + Provides presets that configure multiple motion estimation parameters like lambda, + LSAD threshold, and penalty values to optimize for either raw SAD scores or motion coherence. + """ - pglobal: int - """ - Relative penalty (scaled to 8 bit) to SAD cost for global predictor vector.\n - Coherence is not used for global vector. - """ + SAD = 0 + """Optimize purely for lowest SAD scores when searching motion vectors.""" - def block_coherence(self, block_size: int) -> int: - """Method to calculate coherence (lambda) based on blocksize.""" - return (self.coherence * block_size ** 2) // 64 - - HIGH_SAD = Config(False, 0, 400, 0, 0, False) - """Use to search motion vectors with best SAD.""" - - VECT_COHERENCE = Config(True, 1000, 1200, 50, 1, True) - """Use for true motion search (high vector coherence).""" - - VECT_NOSCALING = Config(True, 1000, 1200, 50, 0, True) - """Same as :py:attr:`VECT_COHERENCE` but with plevel set to no scaling (lower penalty factor).""" - - class _CustomConfig: - def __call__( - self, coherence: int | None = None, sad_limit: int | None = None, - pnew: int | None = None, plevel: int | None = None, pglobal: int | None = None, - truemotion: bool = True - ) -> MotionMode.Config: - """ - Create a custom :py:class:`MotionMode.Config`.\n - Default values will depend on `truemotion`. - - For parameters, please refer to :py:class:`MotionMode.Config` - """ - - ref = MotionMode.from_param(truemotion) - - return MotionMode.Config( - truemotion, - fallback(coherence, ref.coherence), - fallback(sad_limit, ref.sad_limit), - fallback(pnew, ref.pnew), - fallback(plevel, ref.plevel), - fallback(pglobal, ref.pglobal) - ) + COHERENCE = 1 + """Optimize for motion vector coherence, preferring vectors that align with surrounding blocks.""" - MANUAL = _CustomConfig() - """Construct a custom config.""" - @classmethod - def from_param(cls, truemotion: bool) -> Config: - """ - Get a default :py:class:`MotionMode.Config`. +class PenaltyMode(CustomIntEnum): + """Controls how motion estimation penalties scale with hierarchical levels.""" - :param truemotion: Whether to have a true motion config or not. + NONE = 0 + """Penalties remain constant across all hierarchical levels.""" - :return: A :py:class:`MotionMode.Config`. - """ + LINEAR = 1 + """Penalties scale linearly with hierarchical level size.""" - return MotionMode.VECT_COHERENCE if truemotion else MotionMode.HIGH_SAD + QUADRATIC = 2 + """Penalties scale quadratically with hierarchical level size.""" class FlowMode(CustomIntEnum): - """Decide where from the pixels be taken from the two frames when calculating the "flow" of the vector.""" + """Controls how motion vectors are applied to pixels.""" ABSOLUTE = 0 - """Fetch pixels to every place of destination.""" + """Motion vectors point directly to destination pixels.""" RELATIVE = 1 - """Shift pixels from every place of source""" + """Motion vectors describe how source pixels should be shifted.""" -class FinestMode(CustomIntEnum): - """Decide when to calculate Finest type of a vector""" +class MaskMode(CustomIntEnum): + """Defines the type of analysis mask to generate.""" - NONE = 0 - """Disabled""" + MOTION = 0 + """Generates a mask based on motion vector magnitudes.""" - ANALYZE = 1 - """After Analyze""" + SAD = 1 + """Generates a mask based on SAD (Sum of Absolute Differences) values.""" - RECALCULATE = 2 - """After recalculation""" + OCCLUSION = 2 + """Generates a mask highlighting areas where motion estimation fails due to occlusion.""" - BOTH = 3 - """Every step""" + HORIZONTAL = 3 + """Visualizes horizontal motion vector components. Values are in pixels + 128.""" - @property - def after_analyze(self) -> bool: - return self in {self.ANALYZE, self.BOTH} + VERTICAL = 4 + """Visualizes vertical motion vector components. Values are in pixels + 128.""" - @property - def after_recalculate(self) -> bool: - return self in {self.RECALCULATE, self.BOTH} + COLORMAP = 5 + """Creates a color visualization of motion vectors, mapping x/y components to U/V planes.""" diff --git a/vsdenoise/mvtools/motion.py b/vsdenoise/mvtools/motion.py index 4c4d380..efdc8f1 100644 --- a/vsdenoise/mvtools/motion.py +++ b/vsdenoise/mvtools/motion.py @@ -1,26 +1,24 @@ from __future__ import annotations -from typing import Any, NamedTuple +from typing import Any from vstools import vs -from .enums import MVDirection, MVToolsPlugin, FinestMode +from .enums import MVDirection __all__ = [ 'MotionVectors', - - 'SuperClips' ] class MotionVectors: - vmulti: vs.VideoNode - """Super analyzed clip.""" + """Class for storing and managing motion vectors for a video clip.""" - kwargs: dict[str, Any] + vmulti: vs.VideoNode + """Super-sampled clip used for motion vector analysis.""" temporal_vectors: dict[MVDirection, dict[int, vs.VideoNode]] - """Dict containing backwards and forwards motion vectors.""" + """Dictionary containing both backward and forward motion vectors.""" def __init__(self) -> None: self._init_vects() @@ -32,82 +30,39 @@ def _init_vects(self) -> None: @property def has_vectors(self) -> bool: - """Whether the instance uses bidirectional motion vectors.""" + """Check if motion vectors are available.""" return bool( (self.temporal_vectors[MVDirection.BACK] and self.temporal_vectors[MVDirection.FWRD]) or self.vmulti ) - def has_mv(self, direction: MVDirection, delta: int) -> bool: - """ - Returns whether the motion vector exists. - - :param direction: Which direction the motion vector was analyzed. - :param delta: Delta with which the motion vector was analyzed. - - :return: Whether the motion vector exists. - """ - - return delta in self.temporal_vectors[direction] - def get_mv(self, direction: MVDirection, delta: int) -> vs.VideoNode: """ - Get the motion vector. + Retrieve a specific motion vector. - :param direction: Which direction the motion vector was analyzed. - :param delta: Delta with which the motion vector was analyzed. + :param direction: Direction of the motion vector (forward or backward). + :param delta: Frame distance for the motion vector. - :return: Motion vector. + :return: The requested motion vector clip. """ return self.temporal_vectors[direction][delta] - def set_mv(self, direction: MVDirection, delta: int, vect: vs.VideoNode) -> None: + def set_mv(self, direction: MVDirection, delta: int, vector: vs.VideoNode) -> None: """ - Sets the motion vector. + Store a motion vector. - :param direction: Which direction the motion vector was analyzed. - :param delta: Delta with which the motion vector was analyzed. + :param direction: Direction of the motion vector (forward or backward). + :param delta: Frame distance for the motion vector. + :param vect: Motion vector clip to store. """ - self.temporal_vectors[direction][delta] = vect + self.temporal_vectors[direction][delta] = vector def clear(self) -> None: - """Deletes all :py:class:`vsdenoise.mvtools.MotionVectors` attributes.""" + """Clear all stored motion vectors and reset the instance.""" del self.vmulti self.kwargs.clear() self.temporal_vectors.clear() self._init_vects() - - def calculate_vectors( - self, delta: int, mvtools: MVToolsPlugin, supers: SuperClips, recalc: bool, finest: FinestMode, **kwargs: Any - ) -> None: - for direction in MVDirection: - if not recalc: - vect = mvtools.Analyse(supers.search, isb=direction.isb, delta=delta, **kwargs) - if finest.after_analyze: - vect = mvtools.Finest(vect) - else: - vect = mvtools.Recalculate( - supers.recalculate, self.get_mv(direction, delta), **kwargs - ) - if finest.after_recalculate: - vect = mvtools.Finest(vect) - - self.set_mv(direction, delta, vect) - - def finest(self, mvtools: MVToolsPlugin) -> None: - self.temporal_vectors = { - direction: { - delta: mvtools.Finest(vect) - for delta, vect in vectors.items() - } for direction, vectors in self.temporal_vectors.items() - } - - -class SuperClips(NamedTuple): - base: vs.VideoNode - render: vs.VideoNode - search: vs.VideoNode - recalculate: vs.VideoNode diff --git a/vsdenoise/mvtools/mvtools.py b/vsdenoise/mvtools/mvtools.py index d72dbce..a38956a 100644 --- a/vsdenoise/mvtools/mvtools.py +++ b/vsdenoise/mvtools/mvtools.py @@ -2,18 +2,21 @@ from fractions import Fraction from itertools import chain -from typing import Any, Callable, Concatenate, Sequence, Union, overload +from typing import Any, Sequence, overload from vstools import ( - ConstantFormatVideoNode, CustomOverflowError, CustomRuntimeError, FieldBased, FieldBasedT, FuncExceptT, - InvalidColorFamilyError, KwargsT, OutdatedPluginError, P, PlanesT, - check_ref_clip, check_variable, clamp, core, depth, disallow_variable_format, - disallow_variable_resolution, fallback, kwargs_fallback, normalize_planes, normalize_seq, vs + CustomRuntimeError, ColorRange, FieldBased, + InvalidColorFamilyError, KwargsT, VSFunction, + check_variable, core, depth, disallow_variable_format, scale_delta, + disallow_variable_resolution, fallback, normalize_planes, normalize_seq, vs ) +from vsexprtools import norm_expr -from ..prefilters import PelType, Prefilter, prefilter_to_full_range -from .enums import FinestMode, FlowMode, MotionMode, MVDirection, MVToolsPlugin, SADMode, SearchMode -from .motion import MotionVectors, SuperClips +from .enums import ( + MVToolsPlugin, MVDirection, SharpMode, RFilterMode, SearchMode, + SADMode, MotionMode, PenaltyMode, FlowMode, MaskMode +) +from .motion import MotionVectors from .utils import normalize_thscd, planes_to_mvtools __all__ = [ @@ -22,7 +25,7 @@ class MVTools: - """MVTools wrapper for motion analysis / degrain / compensation""" + """MVTools wrapper for motion analysis, degraining, compensation, interpolation, etc.""" super_args: KwargsT """Arguments passed to all the :py:attr:`MVToolsPlugin.Super` calls.""" @@ -36,6 +39,30 @@ class MVTools: compensate_args: KwargsT """Arguments passed to all the :py:attr:`MVToolsPlugin.Compensate` calls.""" + flow_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.Flow` calls.""" + + degrain_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.Degrain` calls.""" + + flow_interpolate_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.FlowInter` calls.""" + + flow_fps_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.FlowFPS` calls.""" + + block_fps_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.BlockFPS` calls.""" + + flow_blur_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.FlowBlur` calls.""" + + mask_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.Mask` calls.""" + + sc_detection_args: KwargsT + """Arguments passed to all the :py:attr:`MVToolsPlugin.SCDetection` calls.""" + vectors: MotionVectors """Motion vectors analyzed and used for all operations.""" @@ -45,13 +72,9 @@ class MVTools: @disallow_variable_format @disallow_variable_resolution def __init__( - self, clip: vs.VideoNode, - tr: int = 2, refine: int = 1, pel: int | None = None, + self, clip: vs.VideoNode, vectors: MotionVectors | MVTools | None = None, + tr: int = 1, pel: int | None = None, pelfilter: VSFunction | None = None, planes: int | Sequence[int] | None = None, - source_type: FieldBasedT | None = None, - high_precision: bool = False, - hpad: int | None = None, vpad: int | None = None, - vectors: MotionVectors | MVTools | None = None, *, # kwargs for mvtools calls super_args: KwargsT | None = None, @@ -59,381 +82,296 @@ def __init__( recalculate_args: KwargsT | None = None, compensate_args: KwargsT | None = None, flow_args: KwargsT | None = None, - # super kwargs - range_conversion: float | None = None, sharp: int | None = None, - rfilter: int | None = None, prefilter: Prefilter | vs.VideoNode | None = None, - pel_type: PelType | tuple[PelType, PelType] | None = None, - # analyze kwargs - block_size: int | None = None, overlap: int | None = None, - thSAD: int | None = None, search: SearchMode | SearchMode.Config | None = None, - sad_mode: SADMode | tuple[SADMode, SADMode] | None = None, - motion: MotionMode.Config | None = None, finest_mode: FinestMode = FinestMode.NONE + degrain_args: KwargsT | None = None, + flow_interpolate_args: KwargsT | None = None, + flow_fps_args: KwargsT | None = None, + block_fps_args: KwargsT | None = None, + flow_blur_args: KwargsT | None = None, + mask_args: KwargsT | None = None, + sc_detection_args: KwargsT | None = None ) -> None: """ - MVTools is a wrapper around the Motion Vector Tools plugin for VapourSynth, - used for estimation and compensation of object motion in video clips. - - This may be used for strong temporal denoising, degraining, - advanced framerate conversions, image restoration, and other similar tasks. - - The plugin uses block-matching method of motion estimation (similar methods are used in MPEG2, MPEG4, etc). - - Of course, the motion estimation and compensation is not ideal and precise.\n - In some complex cases (video with fading, ultra-fast motion, or periodic structures) - the motion estimation may be completely wrong, and the compensated frame will be blocky and(/or) ugly. - - Severe difficulty is also due to objects mutual screening (occlusion) or reverse opening.\n - Complex scripts with many motion compensation functions may eat huge amounts of memory - which results in very slow processing. - - It's not simple to use, but it's quite an advanced plugin. - The goal of this wrapper is to make it more accessible to your average user. - However, use it for appropriate cases only, and try tuning its (many) parameters. - - :param clip: Input clip to process. Must be either a GRAY or YUV format. - :param tr: Temporal radius of the processing. - :param refine: This represents the times the analyzed clip will be recalculated.\n - With every recalculation step, the block size will be further refined.\n - i.e. `refine=3` it will analyze at `block_size=32`, then refine at 16, 8, 4. - Set `refine=0` to disable recalculation completely. - :param pel: Pixel EnLargement value, a.k.a. subpixel accuracy of the motion estimation.\n - Value can only be 1, 2 or 4. - * 1 means a precision to the pixel. - * 2 means a precision to half a pixel. - * 4 means a precision to quarter a pixel. - `pel=4` is produced by spatial interpolation which is more accurate, - but slower and not always better due to big level scale step. - :param planes: Planes to process. - :param source_type: Source type of the input clip. - :param high_precision: Whether to process everything in float32 (very slow). - If set to False, it will process it in the input clip's bitdepth. - :param hpad: Horizontal padding added to source frame (both left and right).\n - Small padding is added for more correct motion estimation near frame borders. - :param vpad: Vertical padding added to source frame (both top and bottom). - :param vectors: Precalculated vectors, either a custom instance or another MVTools instance. - - :param super_args: Arguments passed to all the :py:attr:`MVToolsPlugin.Super` calls. - :param analyze_args: Arguments passed to all the :py:attr:`MVToolsPlugin.Analyze` calls. - :param recalculate_args: Arguments passed to all the :py:attr:`MVToolsPlugin.Recalculate` calls. - :param compensate_args: Arguments passed to all the :py:attr:`MVToolsPlugin.Compensate` calls. - :param flow_args: Arguments passed to all the :py:attr:`MVToolsPlugin.Flow` calls. - - :param block_size: Block size to be used as smallest portion of the picture for analysis. - :param overlap: N block overlap value. Must be even to or lesser than the block size.\n - The step between blocks for motion estimation is equal to `block_size - overlap`.\n - N blocks cover the size `(block_size - overlap) * N + overlap` on the frame.\n - Try using overlap value from `block_size / 4` to `block_size / 2`.\n - The greater the overlap, the higher the amount of blocks, - and the longer the processing will take.\n - However the default value of 0 may cause blocking-like artefacts.\n - :param thSAD: During the recalculation, only bad quality new vectors with SAD above this thSAD - will be re-estimated by search. thSAD value is scaled to 8x8 block size. - Good vectors are not changed, but their SAD will be re-calculated and updated. - :param range_conversion: If the input is limited, it will be converted to full range - to allow the motion analysis to use a wider array of information.\n - This is for deciding what range conversion method to use. - * >= 1.0 - Expansion with expr based on this coefficient. - * > 0.0 - Expansion with retinex. - * <= 0.0 - Simple conversion with resize plugin. - :param search: Decides the type of search at every level of the hierarchial - analysis made while searching for motion vectors. - :param sharp: Subpixel interpolation method for pel = 2 or 4. Possible values are 0, 1, 2.\n - * 0 - for soft interpolation (bilinear). - * 1 - for bicubic interpolation (4 tap Catmull-Rom). - * 2 - for sharper Wiener interpolation (6 tap, similar to Lanczos). - This parameter controls the calculation of the first level only. - When pel = 4, bilinear interpolation is always used to compute the second level. - :param rfilter: Hierarchical levels smoothing and reducing (halving) filter.\n - * 0 - Simple 4 pixels averaging. - * 1 - Triangle (shifted) for more smoothing (decrease aliasing). - * 2 - Triangle filter like Bilinear for even more smoothing. - * 3 - Quadratic filter for even more smoothing. - * 4 - Cubic filter like Bicubic(b=1, c=0) for even more smoothing. - :param sad_mode: SAD Calculation mode. - :param motion: A preset or custom parameters values for truemotion/motion analysis mode. - :param prefilter: Prefilter to use for motion estimation. Can be a prefiltered clip instead. - The ideal prefiltered clip will be one that has little to not - temporal instability or dynamic grain, but retains most of the detail. - :param pel_type: Type of interpolation to use for upscaling the pel clip. + MVTools is a collection of functions for motion estimation and compensation in video. + + Motion compensation may be used for strong temporal denoising, advanced framerate conversions, + image restoration, and other similar tasks. + + The plugin uses a block-matching method of motion estimation (similar methods as used in MPEG2, MPEG4, etc.). + During the analysis stage the plugin divides frames into smaller blocks and tries to find the most similar matching block + for every block in current frame in the second frame (which is either the previous or next frame). + The relative shift of these blocks is the motion vector. + + The main method of measuring block similarity is by calculating the sum of absolute differences (SAD) + of all pixels of these two blocks, which indicates how correct the motion estimation was. + + :param clip: The clip to process. + :param tr: The temporal radius. This determines how many frames are analyzed before/after the current frame. + Higher values will result in smoother motion vectors, but will also be much slower. Default: 1. + :param vectors: Pre-calculated motion vectors from another MVTools instance or custom implementation. + Default: None. + :param pel: Subpixel precision for motion estimation (1=pixel, 2=half-pixel, 4=quarter-pixel). + Default: 1. + :param pelfilter: Filter used for pel interpolation. Only applicable when pel > 1. Default: None. + :param fieldbased: Whether the clip is interlaced and its field order. Default: None. + :param planes: Which planes to process. Default: None (all planes). + :param super_args: Arguments passed to every :py:attr:`MVToolsPlugin.Super` calls. + :param analyze_args: Arguments passed to every :py:attr:`MVToolsPlugin.Analyze` calls. + :param recalculate_args: Arguments passed to every :py:attr:`MVToolsPlugin.Recalculate` calls. + :param compensate_args: Arguments passed to every :py:attr:`MVToolsPlugin.Compensate` calls. + :param flow_args: Arguments passed to every :py:attr:`MVToolsPlugin.Flow` calls. + :param degrain_args: Arguments passed to every :py:attr:`MVToolsPlugin.Degrain` calls. + :param flow_interpolate_args: Arguments passed to every :py:attr:`MVToolsPlugin.FlowInter` calls. + :param flow_fps_args: Arguments passed to every :py:attr:`MVToolsPlugin.FlowFPS` calls. + :param block_fps_args: Arguments passed to every :py:attr:`MVToolsPlugin.BlockFPS` calls. + :param flow_blur_args: Arguments passed to every :py:attr:`MVToolsPlugin.FlowBlur` calls. + :param mask_args: Arguments passed to every :py:attr:`MVToolsPlugin.Mask` calls. + :param sc_detection_args: Arguments passed to every :py:attr:`MVToolsPlugin.SCDetection` calls. """ assert check_variable(clip, self.__class__) - InvalidColorFamilyError.check(clip, (vs.GRAY, vs.YUV), self.__class__) + InvalidColorFamilyError.check(clip, (vs.YUV, vs.GRAY), self.__class__) - self.clip = clip - self.workclip = self.clip + if isinstance(vectors, MVTools): + self.vectors = vectors.vectors + elif isinstance(vectors, MotionVectors): + self.vectors = vectors + else: + self.vectors = MotionVectors() - self.source_type = FieldBased.from_param_or_video(source_type, self.clip, False, self.__class__) - self.is_hd = clip.width >= 1100 or clip.height >= 600 - self.is_uhd = self.clip.width >= 2600 or self.clip.height >= 1500 + self.mvtools = MVToolsPlugin.from_video(clip) + self.fieldbased = FieldBased.from_video(clip, False, self.__class__) + self.clip = clip.std.SeparateFields() if self.fieldbased.is_inter else clip self.tr = tr - self.refine = refine - self.pel = fallback(pel, 1 + int(not self.is_hd)) - self.planes = normalize_planes(self.clip, planes) + self.pel = pel + self.pelfilter = pelfilter - self.is_gray = self.planes == [0] + self.planes = normalize_planes(clip, planes) self.mv_plane = planes_to_mvtools(self.planes) self.chroma = self.mv_plane != 0 - self.high_precision = high_precision - - self.hpad = fallback(hpad, 8 if self.is_hd else 16) - self.hpad_uhd = self.hpad // 2 if self.is_uhd else self.hpad - self.vpad = fallback(vpad, 8 if self.is_hd else 16) - self.vpad_half = self.vpad // 2 if self.is_uhd else self.vpad + self.disable_compensate = False + + if self.mvtools is MVToolsPlugin.FLOAT and tr == 1: + self.disable_degrain = True + else: + self.disable_degrain = False self.super_args = fallback(super_args, KwargsT()) self.analyze_args = fallback(analyze_args, KwargsT()) self.recalculate_args = fallback(recalculate_args, KwargsT()) self.compensate_args = fallback(compensate_args, KwargsT()) + self.degrain_args = fallback(degrain_args, KwargsT()) self.flow_args = fallback(flow_args, KwargsT()) - - if self.refine > 6: - raise CustomOverflowError(f'Refine > 6 is not supported! ({refine})', self.__class__) - - if self.high_precision: - self.workclip = depth(self.workclip, 32) - - self.mvtools = MVToolsPlugin.from_video(self.workclip) - - if self.source_type.is_inter: - self.workclip = self.workclip.std.SeparateFields(self.source_type.is_tff) - - if self.mvtools is MVToolsPlugin.INTEGER: - if 'time' not in str(core.mv.Compensate.signature): - raise OutdatedPluginError(self.__class__, f'{self.__class__.__name__} {self.mvtools.name}') - elif self.mvtools in (MVToolsPlugin.FLOAT_OLD, MVToolsPlugin.FLOAT_NEW): - if not hasattr(self.mvtools.namespace, 'Flow'): - raise OutdatedPluginError(self.__class__, f'{self.__class__.__name__} {self.mvtools.name}') - - self.super_func_kwargs = dict( - rfilter=rfilter, range_conversion=range_conversion, sharp=sharp, - prefilter=prefilter, pel_type=pel_type - ) - - self.supers: SuperClips | None = None - - self.analyze_func_kwargs = dict( - overlap=overlap, search=search, block_size=block_size, sad_mode=sad_mode, - motion=motion, thSAD=thSAD - ) - - self.finest_mode = finest_mode - - if self.mvtools is MVToolsPlugin.INTEGER and self.finest_mode is not FinestMode.NONE: - raise CustomRuntimeError( - 'finest_mode != NONE is only supported in the float plugin!', reason=dict(finest_mode=self.finest_mode) - ) - - if isinstance(vectors, MVTools): - self.vectors = vectors.vectors - elif isinstance(vectors, MotionVectors): - self.vectors = vectors - else: - self.vectors = MotionVectors() + self.flow_interpolate_args = fallback(flow_interpolate_args, KwargsT()) + self.flow_fps_args = fallback(flow_fps_args, KwargsT()) + self.block_fps_args = fallback(block_fps_args, KwargsT()) + self.flow_blur_args = fallback(flow_blur_args, KwargsT()) + self.mask_args = fallback(mask_args, KwargsT()) + self.sc_detection_args = fallback(sc_detection_args, KwargsT()) def super( - self, range_conversion: float | None = None, sharp: int | None = None, - rfilter: int | None = None, prefilter: Prefilter | vs.VideoNode | None = None, - pel_type: PelType | tuple[PelType, PelType] | None = None, - *, ref: vs.VideoNode | None = None, inplace: bool = False - ) -> SuperClips: + self, clip: vs.VideoNode | None = None, pad: int | tuple[int | None, int | None] | None = None, + levels: int | None = None, sharp: SharpMode | None = None, rfilter: RFilterMode | None = None, + pelclip: vs.VideoNode | None = None, inplace: bool = False + ) -> vs.VideoNode: """ - Calculates Super clips for rendering, searching, and recalculating. - - :param range_conversion: If the input is limited, it will be converted to full range - to allow the motion analysis to use a wider array of information.\n - This is for deciding what range conversion method to use. - * >= 1.0 - Expansion with expr based on this coefficient. - * > 0.0 - Expansion with retinex. - * <= 0.0 - Simple conversion with resize plugin. - :param sharp: Subpixel interpolation method for pel = 2 or 4. Possible values are 0, 1, 2.\n - * 0 - for soft interpolation (bilinear). - * 1 - for bicubic interpolation (4 tap Catmull-Rom). - * 2 - for sharper Wiener interpolation (6 tap, similar to Lanczos). - This parameter controls the calculation of the first level only. - When pel = 4, bilinear interpolation is always used to compute the second level. - :param rfilter: Hierarchical levels smoothing and reducing (halving) filter.\n - * 0 - Simple 4 pixels averaging. - * 1 - Triangle (shifted) for more smoothing (decrease aliasing). - * 2 - Triangle filter like Bilinear for even more smoothing. - * 3 - Quadratic filter for even more smoothing. - * 4 - Cubic filter like Bicubic(b=1, c=0) for even more smoothing. - :param prefilter: Prefilter to use for motion estimation. Can be a prefiltered clip instead. - The ideal prefiltered clip will be one that has little to not - temporal instability or dynamic grain, but retains most of the detail. - :param pel_type: Type of interpolation to use for upscaling the pel clip. - :param ref: Reference clip to use for creating super clips. - - :return: SuperClips tuple containing the render, search, and recalculate super clips. + Get source clip and prepare special "super" clip with multilevel (hierarchical scaled) frames data. + The super clip is used by both :py:attr:`analyze` and motion compensation (client) functions. + + You can use different Super clip for generation vectors with :py:attr:`analyze` and a different super clip format for the actual action. + Source clip is appended to clip's frameprops, :py:attr:`get_super` can be used to extract the super clip if you wish to view it yourself. + + :param clip: The clip to process. If None, the :py:attr:`clip` attribute is used. + :param pad: How much padding to add to the source frame. + Small padding is added to help with motion estimation near frame borders. + :param levels: The number of hierarchical levels in super clip frames. + More levels are needed for :py:attr:`analyze` to get better vectors, + but fewer levels are needed for the actual motion compensation. + 0 = auto, all possible levels are produced. + :param sharp: Subpixel interpolation method if pel is 2 or 4. + For more information, see :py:class:`SharpMode`. + :param rfilter: Hierarchical levels smoothing and reducing (halving) filter. + For more information, see :py:class:`RFilterMode`. + :param pelclip: Optional upsampled source clip to use instead of internal subpixel interpolation (if pel > 1). + The clip must contain the original source pixels at positions that are multiples of pel + (e.g., positions 0, 2, 4, etc. for pel=2), with interpolated pixels in between. + The clip should not be padded. + :param inplace: Whether to store the super results in the current MVTools instance. + + :return: The original clip with the super clip attached as a frame property. """ - ref = self.get_ref_clip(ref, self.super) - rfilter = kwargs_fallback(rfilter, (self.super_func_kwargs, 'rfilter'), 3) - range_conversion = kwargs_fallback(range_conversion, (self.super_func_kwargs, 'range_conversion'), 5.0) - - sharp = kwargs_fallback(sharp, (self.super_func_kwargs, 'sharp'), 2) - - prefilter = kwargs_fallback( # type: ignore[assignment] - prefilter, (self.super_func_kwargs, 'prefilter'), Prefilter.AUTO - ) + clip = fallback(clip, self.clip) + hpad, vpad = normalize_seq(pad, 2) - pel_type = kwargs_fallback( # type: ignore[assignment] - pel_type, (self.super_func_kwargs, 'pel_type'), PelType.AUTO - ) + if self.pelfilter and not pelclip: + pelclip = self.pelfilter(clip) - if not isinstance(pel_type, tuple): - pel_type = (pel_type, pel_type) # type: ignore[assignment] - - if isinstance(prefilter, Prefilter): - prefilter = prefilter(ref, self.planes) - - prefilter = prefilter_to_full_range(prefilter, range_conversion, self.planes) - - assert prefilter is not None - - if self.high_precision: - prefilter = depth(prefilter, 32) - - check_ref_clip(ref, prefilter) - pelclip, pelclip2 = self.get_subpel_clips(prefilter, ref, pel_type) # type: ignore[arg-type] - - common_args = KwargsT( - sharp=sharp, pel=self.pel, vpad=self.vpad_half, hpad=self.hpad_uhd, chroma=self.chroma + super_args = KwargsT( + hpad=hpad, vpad=vpad, pel=self.pel, levels=levels, chroma=self.chroma, + sharp=sharp, rfilter=rfilter, pelclip=pelclip ) | self.super_args - super_render_args = common_args | dict(levels=1, hpad=self.hpad, vpad=self.vpad, chroma=not self.is_gray) - if pelclip or pelclip2: - common_args |= dict(pelclip=pelclip) # type: ignore - super_render_args |= dict(pelclip=pelclip2) # type: ignore + super_clip = self.mvtools.Super(clip, **super_args) - super_render = self.mvtools.Super(ref if inplace else self.workclip, **super_render_args) - super_search = self.mvtools.Super(ref, **(dict(rfilter=rfilter) | common_args)) - super_recalc = self.refine and self.mvtools.Super(prefilter, **(dict(levels=1) | common_args)) or super_render + super_clip = clip.std.ClipToProp(super_clip, prop='MSuper') - supers = SuperClips(ref, super_render, super_search, super_recalc) + if inplace: + self.clip = super_clip - if not inplace: - self.supers = supers - - return supers + return super_clip def analyze( - self, block_size: int | None = None, overlap: int | None = None, thSAD: int | None = None, - search: SearchMode | SearchMode.Config | None = None, - sad_mode: SADMode | tuple[SADMode, SADMode] | None = None, - motion: MotionMode.Config | None = None, supers: SuperClips | None = None, - *, ref: vs.VideoNode | None = None, inplace: bool = False + self, super: vs.VideoNode | None = None, blocksize: int | tuple[int | None, int | None] | None = None, + levels: int | None = None, search: SearchMode | None = None, searchparam: int | None = None, + pelsearch: int | None = None, lambda_: int | None = None, truemotion: MotionMode | None = None, + lsad: int | None = None, plevel: PenaltyMode | None = None, global_: bool | None = None, + pnew: int | None = None, pzero: int | None = None, pglobal: int | None = None, + overlap: int | tuple[int | None, int | None] | None = None, divide: bool | None = None, + badsad: int | None = None, badrange: int | None = None, meander: bool | None = None, + trymany: bool | None = None, dct: SADMode | None = None, inplace: bool = False, ) -> MotionVectors: """ - During the analysis stage, the plugin divides frames by small blocks and for every block in current frame - it tries to find the most similar (matching) block in the second frame (previous or next).\n - The relative shift of these blocks is represented by a motion vector. - - The main measure of block similarity is the sum of absolute differences (SAD) of all pixels - of the two compared blocks. SAD is a value which says how good the motion estimation was. - - :param block_size: Block size to be used as smallest portion of the picture for analysis. - :param overlap: N block overlap value. Must be even to or lesser than the block size.\n - The step between blocks for motion estimation is equal to `block_size - overlap`.\n - N blocks cover the size `(block_size - overlap) * N + overlap` on the frame.\n - Try using overlap value from `block_size / 4` to `block_size / 2`.\n - The greater the overlap, the higher the amount of blocks, - and the longer the processing will take.\n - However the default value of 0 may cause blocking-like artefacts.\n - :param thSAD: During the recalculation, only bad quality new vectors with SAD above this thSAD - will be re-estimated by search. thSAD value is scaled to 8x8 block size. - Good vectors are not changed, but their SAD will be re-calculated and updated. - :param search: Decides the type of search at every level of the hierarchial - analysis made while searching for motion vectors. - :param sad_mode: SAD Calculation mode. - :param motion: A preset or custom parameters values for truemotion/motion analysis mode. - :param supers: Custom super clips to be used for analyze. - :param ref: Reference clip to use for analyzes over the main clip. - :param inplace: Whether to save the analysis in the MVTools instance or not. - - :return: :py:class:`MotionVectors` object with the analyzed motion vectors. + Analyze motion vectors in a clip using block matching. + + Takes a prepared super clip (containing hierarchical frame data) and estimates motion by comparing blocks between frames. + Outputs motion vector data that can be used by other functions for motion compensation. + + The motion vector search is performed hierarchically, starting from a coarse image scale and progressively refining to finer scales. + For each block, the function first checks predictors like the zero vector and neighboring block vectors. + + This method calculates the Sum of Absolute Differences (SAD) for these predictors, + then iteratively tests new candidate vectors by adjusting the current best vector. + The vector with the lowest SAD value is chosen as the final motion vector, + with a penalty applied to maintain motion coherence between blocks. + + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param blocksize: Size of a block. Larger blocks are less sensitive to noise, are faster, but also less accurate. + :param levels: Number of levels used in hierarchical motion vector analysis. + A positive value specifies how many levels to use. + A negative or zero value specifies how many coarse levels to skip. + Lower values generally give better results since vectors of any length can be found. + Sometimes adding more levels can help prevent false vectors in CGI or similar content. + :param search: Search algorithm to use at the finest level. See :py:class:`SearchMode` for options. + :param searchparam: Additional parameter for the search algorithm. For NSTEP, this is the step size. + For EXHAUSTIVE, EXHAUSTIVE_H, EXHAUSTIVE_V, HEXAGON and UMH, this is the search radius. + :param lambda_: Controls the coherence of the motion vector field. + Higher values enforce more coherent/smooth motion between blocks. + Too high values may cause the algorithm to miss the optimal vectors. + :param truemotion: Preset that controls the default values of motion estimation parameters to optimize for true motion. + For more information, see :py:class:`MotionMode`. + :param lsad: SAD limit for lambda. + When the SAD value of a vector predictor (formed from neighboring blocks) exceeds this limit, + the local lambda value is decreased. This helps prevent the use of bad predictors, + but reduces motion coherence between blocks. + :param plevel: Controls how the penalty factor (lambda) scales with hierarchical levels. + For more information, see :py:class:`PenaltyMode`. + :param global_: Whether to estimate global motion at each level and use it as an additional predictor. + This can help with camera motion. + :param pnew: Penalty multiplier (relative to 256) applied to the SAD cost when evaluating new candidate vectors. + Higher values make the search more conservative. + :param pzero: Penalty multiplier (relative to 256) applied to the SAD cost for the zero motion vector. + Higher values discourage using zero motion. + :param pglobal: Penalty multiplier (relative to 256) applied to the SAD cost when using the global motion predictor. + :param overlap: Block overlap value. Can be a single integer for both dimensions or a tuple of (horizontal, vertical) overlap values. + Each value must be even and less than its corresponding block size dimension. + :param divide: Whether to divide each block into 4 subblocks during post-processing. + This may improve accuracy at the cost of performance. + :param badsad: SAD threshold above which a wider secondary search will be performed to find better motion vectors. + Higher values mean fewer blocks will trigger the secondary search. + :param badrange: Search radius for the secondary search when a block's SAD exceeds badsad. + :param meander: Whether to use a meandering scan pattern when processing blocks. + If True, alternates between left-to-right and right-to-left scanning between rows to improve motion coherence. + :param trymany: Whether to test multiple predictor vectors during the search process at coarser levels. + Enabling this can find better vectors but increases processing time. + :param dct: SAD calculation mode using block DCT (frequency spectrum) for comparing blocks. + For more information, see :py:class:`SADMode`. + :param inplace: Whether to store the analysis results in the current MVTools instance. + + :return: A :py:class:`MotionVectors` object containing the analyzed motion vectors for each frame. + These vectors describe the estimated motion between frames and can be used for motion compensation. """ - ref = self.get_ref_clip(ref, self.analyze) - - block_size = kwargs_fallback(block_size, (self.analyze_func_kwargs, 'block_size'), 16 if self.is_hd else 8) - blocksize = max(self.refine and 2 ** (self.refine + 1), block_size) - - halfblocksize = max(2, blocksize // 2) - halfoverlap = max(2, halfblocksize // 2) - - overlap = kwargs_fallback(overlap, (self.analyze_func_kwargs, 'overlap'), halfblocksize) - - thSAD = kwargs_fallback(thSAD, (self.analyze_func_kwargs, 'thSAD'), 300) - - search = kwargs_fallback( # type: ignore[assignment] - search, (self.analyze_func_kwargs, 'search'), - SearchMode.HEXAGON if self.refine else SearchMode.DIAMOND - ) - - motion = kwargs_fallback( - motion, (self.analyze_func_kwargs, 'motion'), - MotionMode.VECT_NOSCALING if ( - ref.format.bits_per_sample == 32 - ) else MotionMode.from_param(not self.is_hd) - ) - - if isinstance(search, SearchMode): - search = search(is_uhd=self.is_uhd, refine=self.refine, truemotion=motion.truemotion) - - assert search - - sad_mode = kwargs_fallback( # type: ignore[assignment] - sad_mode, (self.analyze_func_kwargs, 'sad_mode'), SADMode.SATD - ) + super_clip = self.get_super(fallback(super, self.clip)) vectors = MotionVectors() if inplace else self.vectors - if isinstance(sad_mode, tuple): - sad_mode, recalc_sad_mode = sad_mode - else: - sad_mode, recalc_sad_mode = sad_mode, SADMode.SATD - - supers = supers or self.get_supers(ref, inplace=inplace) - - thSAD_recalc = thSAD - - t2 = (self.tr * 2 if self.tr > 1 else self.tr) if self.source_type.is_inter else self.tr + blocksize, blocksizev = normalize_seq(blocksize, 2) + overlap, overlapv = normalize_seq(overlap, 2) analyze_args = KwargsT( - dct=sad_mode, pelsearch=search.pel, blksize=blocksize, overlap=overlap, search=search.mode, - truemotion=motion.truemotion, searchparam=search.param, chroma=self.chroma, - plevel=motion.plevel, pglobal=motion.pglobal, pnew=motion.pnew, - lambda_=motion.block_coherence(blocksize), lsad=motion.sad_limit, - fields=self.source_type.is_inter + blksize=blocksize, blksizev=blocksizev, levels=levels, + search=search, searchparam=searchparam, pelsearch=pelsearch, + lambda_=lambda_, chroma=self.chroma, truemotion=truemotion, + lsad=lsad, plevel=plevel, global_=global_, + pnew=pnew, pzero=pzero, pglobal=pglobal, + overlap=overlap, overlapv=overlapv, divide=divide, + badsad=badsad, badrange=badrange, meander=meander, + trymany=trymany, fields=self.fieldbased.is_inter, tff=self.fieldbased.is_tff, dct=dct ) | self.analyze_args - if self.mvtools is MVToolsPlugin.FLOAT_NEW: - vectors.vmulti = self.mvtools.Analyse(supers.search, radius=t2, **analyze_args) - else: - for i in range(1, t2 + 1): - vectors.calculate_vectors(i, self.mvtools, supers, False, self.finest_mode, **analyze_args) - - if self.refine: - self.recalculate( - self.refine, self.tr, blocksize, halfoverlap, thSAD_recalc, - search, recalc_sad_mode, motion, vectors, supers, ref=ref - ) + if self.mvtools is MVToolsPlugin.INTEGER and not any( + (analyze_args.get('overlap'), analyze_args.get('overlapv')) + ): + self.disable_compensate = True - vectors.kwargs.update(thSAD=thSAD) + if self.mvtools is MVToolsPlugin.FLOAT: + vectors.vmulti = self.mvtools.Analyze(super_clip, radius=self.tr, **analyze_args) + else: + for i in range(1, self.tr + 1): + for direction in MVDirection: + vector = self.mvtools.Analyze(super_clip, isb=direction, delta=i, **analyze_args) + vectors.set_mv(direction, i, vector) return vectors def recalculate( - self, refine: int = 1, tr: int | None = None, block_size: int | None = None, - overlap: int | None = None, thSAD: int | None = None, - search: SearchMode | SearchMode.Config | None = None, sad_mode: SADMode = SADMode.SATD, - motion: MotionMode.Config | None = None, vectors: MotionVectors | MVTools | None = None, - supers: SuperClips | None = None, *, ref: vs.VideoNode | None = None + self, super: vs.VideoNode | None = None, vectors: MotionVectors | MVTools | None = None, + blocksize: int | tuple[int | None, int | None] | None = None, search: SearchMode | None = None, + searchparam: int | None = None, lambda_: int | None = None, truemotion: MotionMode | None = None, + pnew: int | None = None, overlap: int | tuple[int | None, int | None] | None = None, + divide: bool | None = None, meander: bool | None = None, dct: SADMode | None = None ) -> None: - ref = self.get_ref_clip(ref, self.recalculate) + """ + Refines and recalculates motion vectors that were previously estimated, optionally using a different super clip or parameters. + This two-stage approach can provide more stable and robust motion estimation. + + The refinement only occurs at the finest hierarchical level. It uses the interpolated vectors from the original blocks + as predictors for the new vectors, and recalculates their SAD values. + + Only vectors with poor quality (SAD above threshold) will be re-estimated through a new search. + The SAD threshold is normalized to an 8x8 block size. Vectors with good quality are preserved, + though their SAD values are still recalculated and updated. + + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param blocksize: Size of blocks for motion estimation. Can be an int or tuple of (width, height). + Larger blocks are less sensitive to noise and faster to process, but will produce less accurate vectors. + :param search: Search algorithm to use at the finest level. See :py:class:`SearchMode` for options. + :param searchparam: Additional parameter for the search algorithm. For NSTEP, this is the step size. + For EXHAUSTIVE, EXHAUSTIVE_H, EXHAUSTIVE_V, HEXAGON and UMH, this is the search radius. + :param lambda_: Controls the coherence of the motion vector field. + Higher values enforce more coherent/smooth motion between blocks. + Too high values may cause the algorithm to miss the optimal vectors. + :param truemotion: Preset that controls the default values of motion estimation parameters to optimize for true motion. + For more information, see :py:class:`MotionMode`. + :param pnew: Penalty multiplier (relative to 256) applied to the SAD cost when evaluating new candidate vectors. + Higher values make the search more conservative. + :param overlap: Block overlap value. Can be a single integer for both dimensions or a tuple of (horizontal, vertical) overlap values. + Each value must be even and less than its corresponding block size dimension. + :param divide: Whether to divide each block into 4 subblocks during post-processing. + This may improve accuracy at the cost of performance. + :param meander: Whether to use a meandering scan pattern when processing blocks. + If True, alternates between left-to-right and right-to-left scanning between rows to improve motion coherence. + :param dct: SAD calculation mode using block DCT (frequency spectrum) for comparing blocks. + For more information, see :py:class:`SADMode`. + """ + + super_clip = self.get_super(fallback(super, self.clip)) if isinstance(vectors, MVTools): vectors = vectors.vectors @@ -441,594 +379,672 @@ def recalculate( vectors = self.vectors if not vectors.has_vectors: - raise CustomRuntimeError('You need to first run analyze before recalculating!', self.recalculate) - - tr = min(tr, self.tr) if tr else self.tr - t2 = (tr * 2 if tr > 1 else tr) if self.source_type.is_inter else tr - - blocksize = max(refine and 2 ** (refine + 1), fallback(block_size, 16 if self.is_hd else 8)) - halfblocksize = max(2, blocksize // 2) - - overlap = fallback(overlap, max(2, max(2, blocksize // 2) // 2)) + raise CustomRuntimeError('You must run `analyze` before `recalculate`!', self.recalculate) - search = kwargs_fallback( # type: ignore[assignment] - search, (self.analyze_func_kwargs, 'search'), - SearchMode.HEXAGON if self.refine else SearchMode.DIAMOND - ) + blocksize, blocksizev = normalize_seq(blocksize, 2) + overlap, overlapv = normalize_seq(overlap, 2) - motion = kwargs_fallback( - motion, (self.analyze_func_kwargs, 'motion'), - MotionMode.VECT_NOSCALING if ( - ref.format.bits_per_sample == 32 - ) else MotionMode.from_param(not self.is_hd) - ) - - if isinstance(search, SearchMode): - search = search(is_uhd=self.is_uhd, refine=self.refine, truemotion=motion.truemotion) - - assert search - - recalc_args = KwargsT( - search=search.recalc_mode, dct=sad_mode, thsad=thSAD, blksize=halfblocksize, - overlap=overlap, truemotion=motion.truemotion, searchparam=search.param_recalc, - chroma=self.chroma, pnew=motion.pnew, lambda_=motion.block_coherence(halfblocksize), - fields=self.source_type.is_inter + recalculate_args = KwargsT( + blksize=blocksize, blksizev=blocksizev, search=search, searchparam=searchparam, + lambda_=lambda_, chroma=self.chroma, truemotion=truemotion, pnew=pnew, + overlap=overlap, overlapv=overlapv, divide=divide, meander=meander, + fields=self.fieldbased.is_inter, tff=self.fieldbased.is_tff, dct=dct ) | self.recalculate_args - supers = supers or self.get_supers(ref, inplace=True) + if self.mvtools is MVToolsPlugin.INTEGER and not any( + (recalculate_args.get('overlap'), recalculate_args.get('overlapv')) + ): + self.disable_compensate = True - if self.mvtools is MVToolsPlugin.FLOAT_NEW: - for i in range(refine): - recalc_blksize = clamp(blocksize / 2 ** i, 4, 128) - - vectors.vmulti = self.mvtools.Recalculate( - supers.recalculate, vectors=vectors.vmulti, **(recalc_args | dict( - blksize=recalc_blksize, overlap=recalc_blksize / 2, - lambda_=motion.block_coherence(recalc_blksize) - )) - ) + if self.mvtools is MVToolsPlugin.FLOAT: + vectors.vmulti = self.mvtools.Recalculate(super_clip, vectors=vectors.vmulti, **recalculate_args) else: - for i in range(1, t2 + 1): - if not vectors.has_mv(MVDirection.BACK, i) or not vectors.has_mv(MVDirection.FWRD, i): - continue - - for j in range(0, refine): - recalc_blksize = clamp(blocksize / 2 ** j, 4, 128) - - vectors.calculate_vectors( - i, self.mvtools, supers, True, self.finest_mode, **(recalc_args | dict( - blksize=recalc_blksize, overlap=recalc_blksize // 2, - lambda_=motion.block_coherence(recalc_blksize) - )) - ) + for i in range(1, self.tr + 1): + for direction in MVDirection: + vector = self.mvtools.Recalculate(super_clip, vectors.get_mv(direction, i), **recalculate_args) + vectors.set_mv(direction, i, vector) @overload - def compensate( # type: ignore - self, func: Union[ - Callable[Concatenate[vs.VideoNode, P], vs.VideoNode], - Callable[Concatenate[list[vs.VideoNode], P], vs.VideoNode] - ] | None = None, - tr: int | None = None, thSAD: int = 10000, thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *args: P.args, ref: vs.VideoNode | None = None, - **kwargs: P.kwargs + def compensate( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None, scbehavior: bool | None = None, thsad: int | None = None, + time: float | None = None, thscd: int | tuple[int] | None = None, + func: VSFunction | None = None, interleave: bool = True ) -> vs.VideoNode: - """ - At compensation stage, the plugin client functions read the motion vectors and use them to move blocks - and form a motion compensated frame (or realize some other full- or partial motion compensation or - interpolation function). - - Every block in this fully-compensated frame is placed in the same position as this block in current frame. - - So, we may (for example) use strong temporal denoising even for quite fast moving objects without producing - annoying artefactes and ghosting (object's features and edges coincide if compensation is perfect). - - This function is for using compensated and original frames to create an interleaved clip, - denoising it with the external temporal filter `func`, and select central cleaned original frames for output. - - :param func: Temporal function to motion compensate. - :param thSAD: This is the SAD threshold for safe (dummy) compensation.\n - If block SAD is above thSAD, the block is bad, and we use source block - instead of the compensated block. - :param thSCD: The first value is a threshold for whether a block has changed - between the previous frame and the current one.\n - When a block has changed, it means that motion estimation for it isn't relevant. - It, for example, occurs at scene changes, and is one of the thresholds used to - tweak the scene changes detection engine.\n - Raising it will lower the number of blocks detected as changed.\n - It may be useful for noisy or flickered video. This threshold is compared to the SAD value.\n - For exactly identical blocks we have SAD = 0, but real blocks are always different - because of objects complex movement (zoom, rotation, deformation), - discrete pixels sampling, and noise.\n - Suppose we have two compared 8×8 blocks with every pixel different by 5.\n - It this case SAD will be 8×8×5 = 320 (block will not detected as changed for thSCD1 = 400).\n - Actually this parameter is scaled internally in MVTools, - and it is always relative to 8x8 block size.\n - The second value is a threshold of the percentage of how many blocks have to change for - the frame to be considered as a scene change. It ranges from 0 to 100 %. - :param supers: Custom super clips to be used for compensating. - :param wargs: Arguments passed to `func` to avoid using `partial`. - :param ref: Reference clip to use instead of main clip. - :param kwargs: Keyword arguments passed to `func` to avoid using `partial`. - - :return: Motion compensated output of `func`. - """ + ... @overload def compensate( - self, func: None = None, - tr: int | None = None, thSAD: int = 10000, thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, ref: vs.VideoNode | None = None + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None, scbehavior: bool | None = None, thsad: int | None = None, + time: float | None = None, thscd: int | tuple[int] | None = None, + func: VSFunction | None = None, interleave: bool = True ) -> tuple[vs.VideoNode, tuple[int, int]]: + ... + + def compensate( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None, scbehavior: bool | None = None, thsad: int | None = None, + time: float | None = None, thscd: int | tuple[int] | None = None, + func: VSFunction | None = None, interleave: bool = True + ) -> vs.VideoNode | tuple[vs.VideoNode, tuple[int, int]]: """ - At compensation stage, the plugin client functions read the motion vectors and use them to move blocks - and form a motion compensated frame (or realize some other full- or partial motion compensation or - interpolation function). - - Every block in this fully-compensated frame is placed in the same position as this block in current frame. - - So, we may (for example) use strong temporal denoising even for quite fast moving objects without producing - annoying artefactes and ghosting (object's features and edges coincide if compensation is perfect). - - This function is for using compensated and original frames to create an interleaved clip, - denoising it with the external temporal filter `func`, and select central cleaned original frames for output. - - :param thSAD: This is the SAD threshold for safe (dummy) compensation.\n - If block SAD is above thSAD, the block is bad, and we use source block - instead of the compensated block. - :param thSCD: The first value is a threshold for whether a block has changed - between the previous frame and the current one.\n - When a block has changed, it means that motion estimation for it isn't relevant. - It, for example, occurs at scene changes, and is one of the thresholds used to - tweak the scene changes detection engine.\n - Raising it will lower the number of blocks detected as changed.\n - It may be useful for noisy or flickered video. This threshold is compared to the SAD value.\n - For exactly identical blocks we have SAD = 0, but real blocks are always different - because of objects complex movement (zoom, rotation, deformation), - discrete pixels sampling, and noise.\n - Suppose we have two compared 8×8 blocks with every pixel different by 5.\n - It this case SAD will be 8×8×5 = 320 (block will not detected as changed for thSCD1 = 400).\n - Actually this parameter is scaled internally in MVTools, - and it is always relative to 8x8 block size.\n - The second value is a threshold of the percentage of how many blocks have to change for - the frame to be considered as a scene change. It ranges from 0 to 100 %. - :param supers: Custom super clips to be used for compensating. - :param ref: Reference clip to use instead of main clip. - - :return: A tuple of motion compensated clip, then a tuple of (cycle, offset) so that - compensated.std.SelectEvery(cycle, offsets) will give the original clip. + Perform motion compensation by moving blocks from reference frames to the current frame according to motion vectors. + This creates a prediction of the current frame by taking blocks from neighboring frames and moving them along their estimated motion paths. + + :param clip: The clip to process. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param direction: Motion vector direction to use. + :param tr: The temporal radius. This determines how many frames are analyzed before/after the current frame. + :param scbehavior: Whether to keep the current frame on scene changes. + If True, the frame is left unchanged. If False, the reference frame is copied. + :param thsad: SAD threshold for safe compensation. + If block SAD is above thsad, the source block is used instead of the compensated block. + :param time: Time position between frames as a percentage (0.0-100.0). + Controls the interpolation position between frames. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + :param func: Temporal function to apply to the motion compensated frames. + :param interleave: Whether to interleave the compensated frames with the input. + + :return: Motion compensated frames if func is provided, otherwise returns a tuple containing: + - The interleaved compensated frames + - A tuple of (total_frames, center_offset) for manual frame selection """ - def compensate( # type: ignore - self, func: Union[ - Callable[Concatenate[vs.VideoNode, P], vs.VideoNode], - Callable[Concatenate[list[vs.VideoNode], P], vs.VideoNode] - ] | None = None, - tr: int | None = None, thSAD: int = 10000, thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *args: P.args, ref: vs.VideoNode | None = None, - **kwargs: P.kwargs - ) -> vs.VideoNode | tuple[vs.VideoNode, tuple[int, int]]: - ref = self.get_ref_clip(ref, self.compensate) + if self.disable_compensate: + raise CustomRuntimeError('Motion analysis was performed without block overlap!', self.compensate) + + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) + + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors + tr = min(tr, self.tr) if tr else self.tr - thSCD1, thSCD2 = normalize_thscd(thSCD, self.compensate) - supers = supers or self.get_supers(ref, inplace=True) + vect_b, vect_f = self.get_vectors(self.vectors, direction=direction, tr=tr) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=tr) + thscd1, thscd2 = normalize_thscd(thscd) compensate_args = dict( - super=supers.render, thsad=thSAD, - thscd1=thSCD1, thscd2=thSCD2, - fields=self.source_type.is_inter, - tff=self.source_type.is_inter and self.source_type.is_tff or None + scbehavior=scbehavior, thsad=thsad, time=time, fields=self.fieldbased.is_inter, + thscd1=thscd1, thscd2=thscd2, tff=self.fieldbased.is_tff ) | self.compensate_args comp_back, comp_fwrd = [ - [self.mvtools.Compensate(ref, vectors=vect, **compensate_args) for vect in vectors] + [self.mvtools.Compensate(clip, super_clip, vectors=vect, **compensate_args) for vect in vectors] for vectors in (reversed(vect_b), vect_f) ] - comp_clips = [*comp_fwrd, ref, *comp_back] + if not interleave: + comp_clips = (comp_back, comp_fwrd) + if not direction == (MVDirection.BACK, MVDirection.FWRD): + comp_clips = comp_clips[direction - 1] + return comp_clips + + comp_clips = [*comp_fwrd, clip, *comp_back] n_clips = len(comp_clips) offset = (n_clips - 1) // 2 interleaved = core.std.Interleave(comp_clips) if func: - processed = func(interleaved, *args, **kwargs) # type: ignore + processed = func(interleaved) return processed.std.SelectEvery(n_clips, offset) return interleaved, (n_clips, offset) @overload - def flow( # type: ignore - self, func: Union[ - Callable[Concatenate[vs.VideoNode, P], vs.VideoNode], - Callable[Concatenate[list[vs.VideoNode], P], vs.VideoNode] - ] | None = None, - tr: int | None = None, time: float = 100, mode: FlowMode = FlowMode.ABSOLUTE, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *args: P.args, ref: vs.VideoNode | None = None, - **kwargs: P.kwargs + def flow( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None, time: float | None = None, mode: FlowMode | None = None, + thscd: int | tuple[int | None, int | None] | None = None, + func: VSFunction | None = None, interleave: bool = True ) -> vs.VideoNode: ... @overload - def flow( # type: ignore - self, func: None = None, - tr: int | None = None, time: float = 100, mode: FlowMode = FlowMode.ABSOLUTE, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *args: P.args, ref: vs.VideoNode | None = None, - **kwargs: P.kwargs + def flow( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None, time: float | None = None, mode: FlowMode | None = None, + thscd: int | tuple[int | None, int | None] | None = None, + func: VSFunction | None = None, interleave: bool = True ) -> tuple[vs.VideoNode, tuple[int, int]]: ... - def flow( # type: ignore - self, func: Union[ - Callable[Concatenate[vs.VideoNode, P], vs.VideoNode], - Callable[Concatenate[list[vs.VideoNode], P], vs.VideoNode] - ] | None = None, - tr: int | None = None, time: float = 100, mode: FlowMode = FlowMode.ABSOLUTE, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *args: P.args, ref: vs.VideoNode | None = None, - **kwargs: P.kwargs + def flow( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None, time: float | None = None, mode: FlowMode | None = None, + thscd: int | tuple[int | None, int | None] | None = None, + func: VSFunction | None = None, interleave: bool = True ) -> vs.VideoNode | tuple[vs.VideoNode, tuple[int, int]]: - ref = self.get_ref_clip(ref, self.flow) + """ + Performs motion compensation using pixel-level motion vectors interpolated from block vectors. + + Unlike block-based compensation, this calculates a unique motion vector for each pixel by bilinearly interpolating + between the motion vectors of the current block and its neighbors based on the pixel's position. + The pixels in the reference frame are then moved along these interpolated vectors to their estimated positions in the current frame. + + :param clip: The clip to process. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param direction: Motion vector direction to use. + :param tr: The temporal radius. This determines how many frames are analyzed before/after the current frame. + :param time: Time position between frames as a percentage (0.0-100.0). + Controls the interpolation position between frames. + :param mode: Method for positioning pixels during motion compensation. + See :py:class:`FlowMode` for options. + :param thscd: Scene change detection thresholds as a tuple of (threshold1, threshold2). + threshold1: SAD difference threshold between frames to consider a block changed + threshold2: Number of changed blocks needed to trigger a scene change + :param func: Optional function to process the motion compensated frames. + Takes the interleaved frames as input and returns processed frames. + :param interleave: Whether to interleave the compensated frames with the input. + + :return: Motion compensated frames if func is provided, otherwise returns a tuple containing: + - The interleaved compensated frames + - A tuple of (total_frames, center_offset) for manual frame selection + """ + + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) + + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors + tr = min(tr, self.tr) if tr else self.tr - thSCD1, thSCD2 = normalize_thscd(thSCD, self.flow) - supers = supers or self.get_supers(ref, inplace=True) + vect_b, vect_f = self.get_vectors(self.vectors, direction=direction, tr=tr) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=tr) + thscd1, thscd2 = normalize_thscd(thscd) - flow_args = KwargsT( # type: ignore - super=supers.render, time=time, mode=mode, - thscd1=thSCD1, thscd2=thSCD2, - fields=self.source_type.is_inter, - tff=self.source_type.is_inter and self.source_type.is_tff or None + flow_args = KwargsT( + time=time, mode=mode, fields=self.fieldbased.is_inter, + thscd1=thscd1, thscd2=thscd2, tff=self.fieldbased.is_tff ) | self.flow_args flow_back, flow_fwrd = [ - [self.mvtools.Flow(ref, vectors=vect, **flow_args) for vect in vectors] + [self.mvtools.Flow(clip, super_clip, vectors=vect, **flow_args) for vect in vectors] for vectors in (reversed(vect_b), vect_f) ] - flow_clips = [*flow_fwrd, ref, *flow_back] + if not interleave: + flow_clips = (flow_back, flow_fwrd) + if not direction == (MVDirection.BACK, MVDirection.FWRD): + flow_clips = flow_clips[direction - 1] + return flow_clips + + flow_clips = [*flow_fwrd, clip, *flow_back] n_clips = len(flow_clips) offset = (n_clips - 1) // 2 interleaved = core.std.Interleave(flow_clips) if func: - processed = func(interleaved, *args, **kwargs) # type: ignore + processed = func(interleaved) return processed.std.SelectEvery(n_clips, offset) return interleaved, (n_clips, offset) def degrain( - self, - tr: int | None = None, - thSAD: int | tuple[int | None, int | None] | None = None, - limit: int | tuple[int, int] | None = None, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, - *, vectors: MotionVectors | MVTools | None = None, ref: vs.VideoNode | None = None + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, tr: int | None = None, + thsad: int | tuple[int] | None = None, thsad2: int | tuple[int | None, int | None] | None = None, + limit: int | tuple[int | None, int | None] | None = None, + thscd: int | tuple[int | None, int | None] | None = None, ) -> vs.VideoNode: """ - Makes a temporal denoising with motion compensation. - - Blocks of previous and next frames are motion compensated and then averaged with current - frame with weigthing factors depended on block differences from current (SAD). - - :param thSAD: Defines the soft threshold of the block sum absolute differences.\n - If an int is specified, it will be used for luma and chroma will be a scaled value.\n - If a tuple is specified, the first value is for luma, second is for chroma.\n - If None, the same `thSAD` used in the `analyze` step will be used.\n - Block with SAD above threshold thSAD have a zero weight for averaging (denoising).\n - Block with low SAD has highest weight. Rest of weight is taken from pixels of source clip.\n - The provided thSAD value is scaled to a 8x8 blocksize.\n - Low values can result in staggered denoising, large values can result in ghosting and artifacts. - :param limit: Maximum change of pixel. This is post-processing to prevent some artifacts.\n - Value ranges from 0 to 255. - :param thSCD: The first value is a threshold for whether a block has changed - between the previous frame and the current one.\n - When a block has changed, it means that motion estimation for it isn't relevant. - It, for example, occurs at scene changes, and is one of the thresholds used to - tweak the scene changes detection engine.\n - Raising it will lower the number of blocks detected as changed.\n - It may be useful for noisy or flickered video. This threshold is compared to the SAD value.\n - For exactly identical blocks we have SAD = 0, but real blocks are always different - because of objects complex movement (zoom, rotation, deformation), - discrete pixels sampling, and noise.\n - Suppose we have two compared 8×8 blocks with every pixel different by 5.\n - It this case SAD will be 8×8×5 = 320 (block will not detected as changed for thSCD1 = 400).\n - Actually this parameter is scaled internally in MVTools, - and it is always relative to 8x8 block size.\n - The second value is a threshold of the percentage of how many blocks have to change for - the frame to be considered as a scene change. It ranges from 0 to 100 %. - :param ref: Reference clip to use rather than the main clip. If passed, - the degraining will be applied to the ref clip rather than the original input clip. - :param supers: Custom super clips to be used for degraining. - - :return: Degrained clip. + Perform temporal denoising using motion compensation. + + Motion compensated blocks from previous and next frames are averaged with the current frame. + The weighting factors for each block depend on their SAD from the current frame. + + :param clip: The clip to process. + If None, the :py:attr:`workclip` attribute is used. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param tr: The temporal radius. This determines how many frames are analyzed before/after the current frame. + If None, the :py:attr:`tr` attribute is used. + :param thsad: Defines the soft threshold of block sum absolute differences. + Blocks with SAD above this threshold have zero weight for averaging (denoising). + Blocks with low SAD have highest weight. + The remaining weight is taken from pixels of source clip. + :param thsad2: Define the SAD soft threshold for the furthest frames. + The actual SAD threshold for each reference frame is interpolated between thsad (close frames) + and thsad2 (far frames). + Only used with the FLOAT MVTools plugin. + :param limit: Maximum allowed change in pixel values. + :param thscd: Scene change detection thresholds: + - First value: SAD threshold for considering a block changed between frames + - Second value: Number of changed blocks needed to trigger a scene change + :param vectors: Pre-calculated motion vectors from another MVTools instance or custom implementation. + If None, the :py:attr:`vectors` attribute is used. + + :return: Motion compensated and temporally filtered clip with reduced noise. """ - ref = self.get_ref_clip(ref, self.degrain) - tr = min(tr, self.tr) if tr else self.tr + if self.disable_degrain: + raise CustomRuntimeError('Motion analysis was performed with a temporal radius of 1!', self.degrain) + + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) if isinstance(vectors, MVTools): vectors = vectors.vectors elif vectors is None: vectors = self.vectors - vect_b, vect_f = self.get_vectors_bf(vectors, supers=supers, ref=ref, tr=tr) - supers = supers or self.get_supers(ref, inplace=True) - - thSAD, thSADC = (thSAD if isinstance(thSAD, tuple) else (thSAD, None)) + tr = min(tr, self.tr) if tr else self.tr - thSAD = kwargs_fallback(thSAD, (vectors.kwargs, 'thSAD'), 300) - thSADC = fallback(thSADC, thSAD // 2) + thscd1, thscd2 = normalize_thscd(thscd) - limit, limitC = normalize_seq(limit, 2) + degrain_args = dict[str, Any](thscd1=thscd1, thscd2=thscd2, plane=self.mv_plane) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.degrain) + if self.mvtools is MVToolsPlugin.FLOAT: + degrain_args.update(thsad=thsad, thsad2=thsad2, limit=limit) + else: + vect_b, vect_f = self.get_vectors(vectors, tr=tr) - degrain_args = dict[str, Any](thscd1=thSCD1, thscd2=thSCD2, plane=self.mv_plane) + thsad, thsadc = normalize_seq(thsad, 2) + limit, limitc = normalize_seq(limit, 2) - if self.mvtools is MVToolsPlugin.INTEGER: - degrain_args.update(thsad=thSAD, thsadc=thSADC, limit=limit, limitc=limitC) - else: - degrain_args.update(thsad=[thSAD, thSADC, thSADC], limit=[limit, limitC]) + degrain_args.update(thsad=thsad, thsadc=thsadc, limit=limit, limitc=limitc) - if self.mvtools is MVToolsPlugin.FLOAT_NEW: - degrain_args.update(thsad2=[thSAD / 2, thSADC / 2]) + degrain_args = degrain_args | self.degrain_args - if self.mvtools is MVToolsPlugin.FLOAT_NEW: - output = self.mvtools.Degrain()(ref, supers.render, vectors.vmulti, **degrain_args) + if self.mvtools is MVToolsPlugin.FLOAT: + output = self.mvtools.Degrain()(clip, super_clip, vectors.vmulti, **degrain_args) else: output = self.mvtools.Degrain(tr)( - ref, supers.render, *chain.from_iterable(zip(vect_b, vect_f)), **degrain_args + clip, super_clip, *chain.from_iterable(zip(vect_b, vect_f)), **degrain_args ) - if not self.source_type.is_inter: - return output - - return output.std.DoubleWeave(self.source_type.is_tff)[::2] + return output def flow_interpolate( - self, - time: float = 50, mask_scale: float = 100, blend: bool = False, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *, ref: vs.VideoNode | None = None + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, time: float | None = None, + ml: float | None = None, blend: bool | None = None, thscd: int | tuple[int | None, int | None] | None = None ) -> vs.VideoNode: - ref = self.get_ref_clip(ref, self.flow_interpolate) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.flow_interpolate) + """ + Motion interpolation function that creates an intermediate frame between two frames. + + Uses both backward and forward motion vectors to estimate motion and create a frame at any time position between + the current and next frame. Occlusion masks are used to handle areas where motion estimation fails, and time + weighting ensures smooth blending between frames to minimize artifacts. + + :param clip: The clip to process. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param time: Time position between frames as a percentage (0.0-100.0). + Controls the interpolation position between frames. + :param ml: Mask scale parameter that controls occlusion mask strength. + Higher values produce weaker occlusion masks. + Used in MakeVectorOcclusionMaskTime for modes 3-5. + Used in MakeSADMaskTime for modes 6-8. + :param blend: Whether to blend frames at scene changes. + If True, frames will be blended. If False, frames will be copied. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + + :return: Motion interpolated clip with frames created + at the specified time position between input frames. + """ - supers = supers or self.get_supers(ref, inplace=True) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=1) + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) - return self.mvtools.FlowInter( - ref, supers.render, vect_b, vect_f, time, mask_scale, blend, thSCD1, thSCD2 - ) + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors - def flow_blur( - self, - blur: float = 50, pixel_precision: int = 1, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *, ref: vs.VideoNode | None = None - ) -> vs.VideoNode: - ref = self.get_ref_clip(ref, self.flow_blur) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.flow_blur) + vect_b, vect_f = self.get_vectors(self.vectors, tr=1) + + thscd1, thscd2 = normalize_thscd(thscd) - supers = supers or self.get_supers(ref, inplace=True) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=1) + flow_interpolate_args = KwargsT( + time=time, ml=ml, blend=blend, thscd1=thscd1, thscd2=thscd2 + ) | self.flow_interpolate_args - return self.mvtools.FlowBlur( - ref, supers.render, vect_b, vect_f, blur, pixel_precision, thSCD1, thSCD2 - ) + interpolated = self.mvtools.FlowInter(clip, super_clip, vect_b, vect_f, **flow_interpolate_args) + + if self.mvtools is MVToolsPlugin.INTEGER: + interpolated = norm_expr(interpolated, 'x {shift} +', shift=scale_delta(0.5, 8, interpolated)) + + return interpolated def flow_fps( - self, - fps: Fraction, mask_type: int = 2, mask_scale: float = 100, blend: bool = False, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *, ref: vs.VideoNode | None = None + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, fps: Fraction | None = None, + mask: int | None = None, ml: float | None = None, blend: bool | None = None, + thscd: int | tuple[int | None, int | None] | None = None ) -> vs.VideoNode: - ref = self.get_ref_clip(ref, self.flow_fps) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.flow_fps) + """ + Changes the framerate of the clip by interpolating frames between existing frames. + + Uses both backward and forward motion vectors to estimate motion and create frames at any time position between + the current and next frame. Occlusion masks are used to handle areas where motion estimation fails, and time + weighting ensures smooth blending between frames to minimize artifacts. + + :param clip: The clip to process. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param fps: Target output framerate as a Fraction. + :param mask: Processing mask mode for handling occlusions and motion failures. + :param ml: Mask scale parameter that controls occlusion mask strength. + Higher values produce weaker occlusion masks. + Used in MakeVectorOcclusionMaskTime for modes 3-5. + Used in MakeSADMaskTime for modes 6-8. + :param blend: Whether to blend frames at scene changes. + If True, frames will be blended. If False, frames will be copied. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + + :return: Clip with its framerate resampled. + """ - supers = supers or self.get_supers(ref, inplace=True) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=1) + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) - return self.mvtools.FlowFPS( - ref, supers.render, vect_b, vect_f, fps.numerator, fps.denominator, - mask_type, mask_scale, blend, thSCD1, thSCD2 - ) + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors - def block_fps( - self, - fps: Fraction, mask_type: int = 0, mask_scale: float = 100, blend: bool = False, - thSCD: int | tuple[int | None, int | None] | None = None, - supers: SuperClips | None = None, *, ref: vs.VideoNode | None = None - ) -> vs.VideoNode: - ref = self.get_ref_clip(ref, self.block_fps) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.block_fps) + vect_b, vect_f = self.get_vectors(self.vectors, tr=1) - supers = supers or self.get_supers(ref, inplace=True) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=1) + thscd1, thscd2 = normalize_thscd(thscd) - return self.mvtools.BlockFPS( - ref, supers.render, vect_b, vect_f, fps.numerator, fps.denominator, - mask_type, mask_scale, blend, thSCD1, thSCD2 - ) + flow_fps_args = KwargsT( + num=fps.numerator, den=fps.denominator, mask=mask, ml=ml, blend=blend, thscd1=thscd1, thscd2=thscd2 + ) | self.flow_fps_args - def mask( - self, - mask_type: int = 0, mask_scale: float = 100, gamma: float = 1.0, - scenechange_y: int = 0, time: float = 100, fwd: bool = True, - thSCD: int | tuple[int | None, int | None] | None = None, - *, ref: vs.VideoNode | None = None - ) -> vs.VideoNode: - ref = self.get_ref_clip(ref, self.mask) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.mask) + interpolated = self.mvtools.FlowFPS(clip, super_clip, vect_b, vect_f, **flow_fps_args) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=1) + if self.mvtools is MVToolsPlugin.INTEGER: + interpolated = norm_expr(interpolated, 'x {shift} +', shift=scale_delta(0.5, 8, interpolated)) - return self.mvtools.Mask( - ref, vect_f if fwd else vect_b, mask_scale, gamma, mask_type, - time, scenechange_y, thSCD1, thSCD2 - ) + return interpolated - def sc_detection( - self, - fwd: bool = True, - thSCD: int | tuple[int | None, int | None] | None = None, - *, ref: vs.VideoNode | None = None + def block_fps( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, fps: Fraction | None = None, + mode: int | None = None, ml: float | None = None, blend: bool | None = None, + thscd: int | tuple[int | None, int | None] | None = None ) -> vs.VideoNode: - ref = self.get_ref_clip(ref, self.sc_detection) - thSCD1, thSCD2 = normalize_thscd(thSCD, self.sc_detection) + """ + Changes the framerate of the clip by interpolating frames between existing frames + using block-based motion compensation. + + Uses both backward and forward motion vectors to estimate motion and create frames at any time position between + the current and next frame. Occlusion masks are used to handle areas where motion estimation fails, and time + weighting ensures smooth blending between frames to minimize artifacts. + + :param clip: The clip to process. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param fps: Target output framerate as a Fraction. + :param mode: Processing mask mode for handling occlusions and motion failures. + :param ml: Mask scale parameter that controls occlusion mask strength. + Higher values produce weaker occlusion masks. + Used in MakeVectorOcclusionMaskTime for modes 3-5. + Used in MakeSADMaskTime for modes 6-8. + :param blend: Whether to blend frames at scene changes. + If True, frames will be blended. If False, frames will be copied. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + + :return: Clip with its framerate resampled. + """ + + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) - vect_b, vect_f = self.get_vectors_bf(self.vectors, tr=1) + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors - sc_detect = self.mvtools.SCDetection(ref, vect_f if fwd else vect_b, thSCD1, thSCD2) + vect_b, vect_f = self.get_vectors(self.vectors, tr=1) - return sc_detect + thscd1, thscd2 = normalize_thscd(thscd) - def finest(self) -> None: - self.analyze().finest(self.mvtools) + block_fps_args = KwargsT( + num=fps.numerator, den=fps.denominator, mode=mode, ml=ml, blend=blend, thscd1=thscd1, thscd2=thscd2 + ) | self.block_fps_args - def get_supers(self, ref: vs.VideoNode, *, inplace: bool = False) -> SuperClips: - """ - Get the super clips for the specified ref clip. + interpolated = self.mvtools.BlockFPS(clip, super_clip, vect_b, vect_f, **block_fps_args) - If :py:attr:`analyze` wasn't previously called, - it will do so here with default values or kwargs specified in the constructor. + if self.mvtools is MVToolsPlugin.INTEGER: + interpolated = norm_expr(interpolated, 'x {shift} +', shift=scale_delta(0.5, 8, interpolated)) - :param inplace: Only return the SuperClips object, not modifying the internal state. + return interpolated - :return: SuperClips tuple. + def flow_blur( + self, clip: vs.VideoNode | None = None, super: vs.VideoNode | None = None, + vectors: MotionVectors | MVTools | None = None, blur: float | None = None, + prec: int | None = None, thscd: int | tuple[int | None, int | None] | None = None + ) -> vs.VideoNode: + """ + Creates a motion blur effect by simulating finite shutter time, similar to film cameras. + + Uses backward and forward motion vectors to create and overlay multiple copies of motion compensated pixels + at intermediate time positions within a blurring interval around the current frame. + + :param clip: The clip to process. + :param super: The multilevel super clip prepared by :py:attr:`super`. + If None, super will be obtained from clip. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param blur: Blur time interval between frames as a percentage (0.0-100.0). + Controls the simulated shutter time/motion blur strength. + :param prec: Blur precision in pixel units. Controls the accuracy of the motion blur. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + + :return: Motion blurred clip. """ - if self.supers and self.supers.base == ref: - return self.supers + clip = fallback(clip, self.clip) + super_clip = self.get_super(fallback(super, self.clip)) - return self.super(ref=ref, inplace=inplace) + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors - def get_vectors_bf( - self, vectors: MotionVectors, *, supers: SuperClips | None = None, - ref: vs.VideoNode | None = None, tr: int | None = None, inplace: bool = False - ) -> tuple[list[vs.VideoNode], list[vs.VideoNode]]: - """ - Get the backwards and forward vectors.\n + vect_b, vect_f = self.get_vectors(self.vectors, tr=1) - If :py:attr:`analyze` wasn't previously called, - it will do so here with default values or kwargs specified in the constructor. + thscd1, thscd2 = normalize_thscd(thscd) + + flow_blur_args = KwargsT( + blur=blur, prec=prec, thscd1=thscd1, thscd2=thscd2 + ) | self.flow_blur_args - :param inplace: Only return the list, not modifying the internal state.\n - (Useful if you haven't called :py:attr:`analyze` previously) + return self.mvtools.FlowBlur(clip, super_clip, vect_b, vect_f, **flow_blur_args) - :return: Two lists, respectively for backward and forwards, containing motion vectors. + def mask( + self, clip: vs.VideoNode | None = None, vectors: MotionVectors | MVTools | None = None, + direction: MVDirection = MVDirection.BACK, ml: float | None = None, gamma: float | None = None, + kind: MaskMode | None = None, time: float | None = None, ysc: int | None = None, + thscd: int | tuple[int | None, int | None] | None = None + ) -> vs.VideoNode: + """ + Creates a mask clip from motion vectors data. + + :param clip: The clip to process. + If None, the :py:attr:`workclip` attribute is used. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param direction: Motion vector direction to use. + :param ml: Motion length scale factor. When the vector's length (or other mask value) + is greater than or equal to ml, the output is saturated to 255. + :param gamma: Exponent for the relation between input and output values. + 1.0 gives a linear relation, 2.0 gives a quadratic relation. + :param kind: Type of mask to generate. See :py:class:`MaskMode` for options. + :param time: Time position between frames as a percentage (0.0-100.0). + :param ysc: Value assigned to the mask on scene changes. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + + :return: Motion mask clip. """ - if not vectors.has_vectors: - vectors = self.analyze(supers=supers, ref=ref, inplace=inplace) + clip = fallback(clip, self.clip) - tr = min(tr, self.tr) if tr else self.tr - t2 = (tr * 2 if tr > 1 else tr) if self.source_type.is_inter else tr + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors - vectors_backward = list[vs.VideoNode]() - vectors_forward = list[vs.VideoNode]() + vect = self.get_vectors(self.vectors, direction=direction, tr=1)[direction - 1] - if self.mvtools is MVToolsPlugin.FLOAT_NEW: - vmulti = vectors.vmulti + thscd1, thscd2 = normalize_thscd(thscd) - for i in range(0, t2 * 2, 2): - vectors_backward.append(vmulti.std.SelectEvery(t2 * 2, i)) - vectors_forward.append(vmulti.std.SelectEvery(t2 * 2, i + 1)) - else: - it = 1 + int(self.source_type.is_inter) + mask_args = KwargsT( + ml=ml, gamma=gamma, kind=kind, time=time, ysc=ysc, thscd1=thscd1, thscd2=thscd2 + ) | self.mask_args - for i in range(it, t2 + 1, it): - vectors_backward.append(vectors.get_mv(MVDirection.BACK, i)) - vectors_forward.append(vectors.get_mv(MVDirection.FWRD, i)) + mask_clip = depth(clip, 8) if self.mvtools is MVToolsPlugin.INTEGER else clip - return (vectors_backward, vectors_forward) + mask_clip = self.mvtools.Mask(mask_clip, vect, **mask_args) + + return depth(mask_clip, clip, range_in=ColorRange.FULL, range_out=ColorRange.FULL) - def get_ref_clip(self, ref: vs.VideoNode | None, func: FuncExceptT) -> ConstantFormatVideoNode: + def sc_detection( + self, clip: vs.VideoNode | None = None, vectors: MotionVectors | MVTools | None = None, + direction: MVDirection = MVDirection.BACK, thscd: int | tuple[int | None, int | None] | None = None + ) -> vs.VideoNode: + """ + Creates scene detection mask clip from motion vectors data. + + :param clip: The clip to process. + If None, the :py:attr:`workclip` attribute is used. + :param vectors: Motion vectors to use. Can be a MotionVectors object or another MVTools instance. + If None, uses the vectors from this instance. + :param direction: Motion vector direction to use. + :param thscd: Scene change detection thresholds. + First value is the block change threshold between frames. + Second value is the number of changed blocks needed for a scene change. + + :return: Clip with scene change properties set. """ - Utility for getting the ref clip and set it up with internal modifying. - :param ref: Input clip. If None, the workclip will be used. - :param func: Function this was called from. + clip = fallback(clip, self.clip) - :return: Clip to be used in this instance of MVTools. + if isinstance(vectors, MVTools): + vectors = vectors.vectors + elif vectors is None: + vectors = self.vectors + + vect = self.get_vectors(self.vectors, direction=direction, tr=1)[direction - 1] + + thscd1, thscd2 = normalize_thscd(thscd) + + sc_detection_args = KwargsT( + thscd1=thscd1, thscd2=thscd2 + ) | self.sc_detection_args + + return self.mvtools.SCDetection(clip, vect, **sc_detection_args) + + def get_super(self, clip: vs.VideoNode) -> vs.VideoNode: """ + Get the super clips from the specified clip. - ref = fallback(ref, self.workclip) + If :py:attr:`super` wasn't previously called, + it will do so here with default values or kwargs specified in the constructor. - if self.high_precision: - ref = depth(ref, 32) + :param clip: The clip to get the super clip from. - check_ref_clip(self.workclip, ref) + :return: VideoNode containing the super clip. + """ - assert check_variable(ref, func) + try: + super_clip = clip.std.PropToClip(prop='MSuper') + except vs.Error: + clip = self.super(clip) + super_clip = clip.std.PropToClip(prop='MSuper') - return ref + return super_clip - def get_subpel_clips( - self, pref: vs.VideoNode, ref: vs.VideoNode, pel_type: tuple[PelType, PelType] - ) -> tuple[vs.VideoNode | None, vs.VideoNode | None]: + def get_vectors( + self, vectors: MotionVectors, *, + direction: MVDirection | tuple[MVDirection] = (MVDirection.BACK, MVDirection.FWRD), + tr: int | None = None + ) -> list[vs.VideoNode] | tuple[list[vs.VideoNode], list[vs.VideoNode]]: """ - Get upscaled clips for the subpel param. + Get the backwards and forward vectors. - :param pref: Prefiltered clip. - :param ref: Input clip. - :param pel_type: :py:class:`PelType` to use for upscaling.\n - First is for the prefilter, the other is for normal clip. + :param vectors: The motion vectors to get the backwards and forward vectors from. + :param tr: The number of frames to get the vectors for. - :return: Two values. An upscaled clip or None if PelType.NONE. + :return: A tuple containing two lists of motion vectors. + The first list contains backward vectors and the second contains forward vectors. """ - return tuple( # type: ignore[return-value] - None if ptype is PelType.NONE else ptype( # type: ignore[misc] - clip, self.pel, PelType.WIENER if is_ref else PelType.BICUBIC - ) for is_ref, ptype, clip in zip((False, True), pel_type, (pref, ref)) - ) - - @classmethod - def denoise( - cls, clip: vs.VideoNode, thSAD: int | tuple[int, int | tuple[int, int]] | None = None, - tr: int = 2, refine: int = 1, block_size: int | None = None, overlap: int | None = None, - prefilter: Prefilter | vs.VideoNode | None = None, pel: int | None = None, - sad_mode: SADMode | tuple[SADMode, SADMode] | None = None, - search: SearchMode | SearchMode.Config | None = None, motion: MotionMode.Config | None = None, - pel_type: PelType | tuple[PelType, PelType] | None = None, - planes: PlanesT = None, source_type: FieldBasedT | None = None, high_precision: bool = False, - limit: int | tuple[int, int] | None = None, thSCD: int | tuple[int | None, int | None] | None = None, - *, super_args: KwargsT | None = None, analyze_args: KwargsT | None = None, - recalculate_args: KwargsT | None = None, compensate_args: KwargsT | None = None, - range_conversion: float | None = None, sharp: int | None = None, - hpad: int | None = None, vpad: int | None = None, - rfilter: int | None = None, vectors: MotionVectors | MVTools | None = None, - supers: SuperClips | None = None, ref: vs.VideoNode | None = None - ) -> vs.VideoNode: - mvtools = cls( - clip, tr, refine, pel, planes, source_type, high_precision, hpad, vpad, - vectors, super_args=super_args, analyze_args=analyze_args, - recalculate_args=recalculate_args, compensate_args=compensate_args - ) - - if not isinstance(thSAD, Sequence): - thSADA = thSADD = thSAD - else: - thSADA, thSADD = thSAD # type: ignore + if not vectors.has_vectors: + raise CustomRuntimeError('You need to run analyze before getting motion vectors!', self.get_vectors) + + if not isinstance(direction, tuple): + direction = (direction,) + + tr = min(tr, self.tr) if tr else self.tr - supers = supers or mvtools.super( - range_conversion, sharp, rfilter, prefilter, pel_type, inplace=True - ) + vectors_backward = list[vs.VideoNode]() + vectors_forward = list[vs.VideoNode]() + + if self.mvtools is MVToolsPlugin.FLOAT: + vmulti = vectors.vmulti - vectors = vectors or mvtools.analyze( - block_size, overlap, thSADA, search, sad_mode, motion, supers, inplace=True - ) + for i in range(0, tr * 2, 2): + if MVDirection.BACK in direction: + vectors_backward.append(vmulti.std.SelectEvery(tr * 2, i)) + if MVDirection.FWRD in direction: + vectors_forward.append(vmulti.std.SelectEvery(tr * 2, i + 1)) + else: + for i in range(1, tr + 1): + if MVDirection.BACK in direction: + vectors_backward.append(vectors.get_mv(MVDirection.BACK, i)) + if MVDirection.FWRD in direction: + vectors_forward.append(vectors.get_mv(MVDirection.FWRD, i)) - return mvtools.degrain(tr, thSADD, limit, thSCD, supers, vectors=vectors, ref=ref) + return (vectors_backward, vectors_forward) diff --git a/vsdenoise/mvtools/presets.py b/vsdenoise/mvtools/presets.py index f2612a7..ded1257 100644 --- a/vsdenoise/mvtools/presets.py +++ b/vsdenoise/mvtools/presets.py @@ -110,7 +110,7 @@ class MVToolsPresets: """Presets for MVTools analyzing/refining.""" CUSTOM = MVToolsPreset - """Create your own preset.""" + """Create your own custom preset.""" SMDE = MVToolsPreset( pel=2, prefilter=Prefilter.NONE, sharp=2, rfilter=4, @@ -119,7 +119,7 @@ class MVToolsPresets: hpad=property(fget=lambda x: x.block_size), vpad=property(fget=lambda x: x.block_size), range_conversion=1.0 ) - """SMDegrain by Caroliano & DogWay""" + """SMDegrain by Caroliano & DogWay.""" CMDE = MVToolsPreset( pel=1, prefilter=Prefilter.NONE, sharp=2, rfilter=4, @@ -136,11 +136,11 @@ class MVToolsPresets: sad_mode=SADMode.SPATIAL.same_recalc, search=SearchMode.DIAMOND, motion=MotionMode.HIGH_SAD, pel_type=PelType.BICUBIC, rfilter=2, sharp=2 ) - """Fast preset""" + """Fast preset.""" NOISY = MVToolsPreset( pel=2, thSAD=100, block_size=16, overlap=property(fget=lambda x: x.block_size // 2), motion=MotionMode.HIGH_SAD, prefilter=Prefilter.DFTTEST, sad_mode=(SADMode.ADAPTIVE_SPATIAL_MIXED, SADMode.ADAPTIVE_SATD_MIXED) ) - """Preset for accurate estimation""" + """Preset for accurate estimation.""" diff --git a/vsdenoise/mvtools/utils.py b/vsdenoise/mvtools/utils.py index 87e7d3a..897b7df 100644 --- a/vsdenoise/mvtools/utils.py +++ b/vsdenoise/mvtools/utils.py @@ -2,7 +2,7 @@ from typing import Sequence -from vstools import CustomValueError, FuncExceptT +from vstools import CustomValueError __all__ = [ 'planes_to_mvtools', @@ -13,11 +13,18 @@ def planes_to_mvtools(input_planes: Sequence[int]) -> int: """ - Util function to normalize planes, and converting them to mvtools planes param. + Convert a sequence of plane indices to MVTools' plane parameter value. - :param planes: Sequence of planes to be processed. + MVTools uses a single integer to represent which planes to process: + - 0: Process Y plane only + - 1: Process U plane only + - 2: Process V plane only + - 3: Process UV planes only + - 4: Process all planes - :return: Value of planes used by mvtools. + :param input_planes: Sequence of plane indices (0=Y, 1=U, 2=V) to process. + + :return: Integer value used by MVTools to specify which planes to process. """ planes = set(input_planes) @@ -31,17 +38,24 @@ def planes_to_mvtools(input_planes: Sequence[int]) -> int: if planes == {1, 2}: return 3 - raise CustomValueError("Invalid planes specified!", planes_to_mvtools) + raise CustomValueError('Invalid planes specified!', planes_to_mvtools) def normalize_thscd( - thSCD: int | tuple[int | None, int | None] | None, func: FuncExceptT | None = None, *, scale: bool = True + thscd: int | tuple[int | None, int | None] | None, scale: bool = False ) -> tuple[int, int]: - func = func or normalize_thscd + """ + Normalize and scale the thscd parameter. + + :param thscd: thscd value to scale and/or normalize. + :param scale: Whether to scale thscd2 from 0-100 percentage threshold to 0-255. + + :return: Scaled and/or normalized thscd tuple. + """ - thSCD1, thSCD2 = thSCD if isinstance(thSCD, tuple) else (thSCD, 51) + thscd1, thscd2 = thscd if isinstance(thscd, tuple) else (thscd, None) - if scale: - thSCD2 = round(thSCD2 / 100 * 255) + if scale and thscd2 is not None: + thscd2 = round(thscd2 / 100 * 255) - return (thSCD1, thSCD2) + return (thscd1, thscd2) diff --git a/vsdenoise/prefilters.py b/vsdenoise/prefilters.py index c6eaf95..99add2b 100644 --- a/vsdenoise/prefilters.py +++ b/vsdenoise/prefilters.py @@ -5,17 +5,16 @@ from __future__ import annotations from enum import EnumMeta -from math import ceil, sin +from math import sin from typing import TYPE_CHECKING, Any, Literal, cast, overload -from vsaa import Nnedi3 from vsexprtools import ExprOp, complexpr_available, norm_expr -from vskernels import Bicubic, Bilinear, Scaler, ScalerT +from vskernels import Bilinear, Scaler, ScalerT from vsmasktools import retinex from vsrgtools import bilateral, blur, gauss_blur, min_blur from vstools import ( - MISSING, ColorRange, ConvMode, CustomEnum, CustomIntEnum, CustomRuntimeError, MissingT, PlanesT, SingleOrArr, - SingleOrArrOpt, check_variable, clamp, core, depth, disallow_variable_format, disallow_variable_resolution, + MISSING, ColorRange, ConvMode, CustomIntEnum, MissingT, PlanesT, SingleOrArr, + SingleOrArrOpt, check_variable, clamp, core, depth, get_neutral_value, get_peak_value, get_y, join, normalize_planes, normalize_seq, scale_value, scale_delta, split, vs ) @@ -26,8 +25,7 @@ __all__ = [ 'Prefilter', 'prefilter_to_full_range', - 'MultiPrefilter', - 'PelType' + 'MultiPrefilter' ] __abstract__ = [ @@ -890,140 +888,3 @@ def prefilter_to_full_range(pref: vs.VideoNode, range_conversion: float, planes: return join(pref_full, *chroma, family=pref.format.color_family) return pref_full - - -if TYPE_CHECKING: - PelTypeBase = CustomEnum -else: - class PelTypeBase(CustomEnum): - ... - - class CUSTOM(Scaler): - def __init__(self, scaler: str | type[Scaler] | Scaler, **kwargs: Any) -> None: - self.scaler = Scaler.ensure_obj(scaler) - self.kwargs = kwargs - - @disallow_variable_format - @disallow_variable_resolution - def __call__( - self, clip: vs.VideoNode, pel: int, subpixel: int = 3, - default: ScalerT | None = None, **kwargs: Any - ) -> vs.VideoNode: - return PelType.__call__(self.scaler, clip, pel, default, **(self.kwargs | kwargs)) - - def scale( - self, clip: vs.VideoNode, width: int | None = None, height: int | None = None, - shift: tuple[float, float] = (0, 0), **kwargs: Any - ) -> vs.VideoNode: - width, height = Scaler._wh_norm(clip, width, height) - return self.scaler.scale(clip, width, height, shift, **kwargs) - - @property - def kernel_radius(self) -> int: - return self.scaler.kernel_radius - - BILINEAR = CUSTOM(Bilinear) - BICUBIC = CUSTOM(Bicubic) - WIENER = CUSTOM(Bicubic(b=-0.6, c=0.4)) - - PelTypeBase.CUSTOM = CUSTOM - PelTypeBase.BILINEAR = BILINEAR - PelTypeBase.BICUBIC = BICUBIC - PelTypeBase.WIENER = WIENER - - -class PelType(int, PelTypeBase): - AUTO = -1 - """Automatically decide what :py:class:`PelType` to use.""" - - NONE = 0 - """Don't perform any scaling.""" - - NNEDI3 = 4 - """Performs scaling with NNedi3, ZNedi3.""" - - if TYPE_CHECKING: - from .prefilters import PelType - - class CUSTOM(Scaler, PelType): # type: ignore - """Class for constructing your own :py:class:`PelType`.""" - - def __init__(self, scaler: str | type[Scaler] | Scaler, **kwargs: Any) -> None: - """ - Create custom :py:class`PelType` from a scaler. - - :param scaler: Scaler to be used for scaling and create a pel clip. - """ - - def scale( # type: ignore - self, clip: vs.VideoNode, width: int | None = None, height: int | None = None, - shift: tuple[float, float] = (0, 0), **kwargs: Any - ) -> vs.VideoNode: - ... - - BILINEAR: CUSTOM - """Performs scaling with the bilinear filter (:py:class:`vskernels.Bilinear`).""" - - BICUBIC: CUSTOM - """Performs scaling with default bicubic values (:py:class:`vskernels.Catrom`).""" - - WIENER: CUSTOM - """Performs scaling with the wiener filter (:py:class:`Bicubic(b=-0.6, c=0.4)`).""" - - def __new__(cls, value: int) -> PelType: - ... - - def __init__(self, value: int) -> None: - ... - - @disallow_variable_format - @disallow_variable_resolution - def __call__( - pel_type: Scaler | PelType, clip: vs.VideoNode, pel: int, - default: ScalerT | PelType | None = None, **kwargs: Any - ) -> vs.VideoNode: - """ - Scale a clip. Useful for motion interpolation. - - :param clip: Clip to be scaled. - :param pel: Rate of scaling. - :param subpixel: Precision used in mvtools calls.\n - Will be used with :py:attr:`PelType.AUTO`. - :param default: Specify a default :py:class:`PelType`/:py:class:`Scaler` top be used.\n - Will be used with :py:attr:`PelType.AUTO`. - :param kwargs: Keyword arguments passed to the scaler. - - :return: Upscaled clip. - """ - - assert clip.format - - if pel_type is PelType.NONE or pel <= 1: - return clip - - if pel_type is PelType.AUTO: - if default: - pel_type = default if isinstance(default, PelType) else Scaler.ensure_obj(default) - else: - val = 1 << 3 - ceil(clip.height / 1000) - - if val < 1: - pel_type = PelType.BILINEAR - elif val < 2: - pel_type = PelType.BICUBIC - elif val < 3: - pel_type = PelType.WIENER - else: - pel_type = PelType.NNEDI3 - - if pel_type == PelType.NNEDI3: - if not any((hasattr(core, ns) for ns in ('nnedi3cl', 'nnedi3'))): - raise CustomRuntimeError('Missing any nnedi3 implementation!', PelType.NNEDI3) - - kwargs |= {'nsize': 0, 'nns': clamp(((pel - 1) // 2) + 1, 0, 4), 'qual': clamp(pel - 1, 1, 3)} | kwargs - - pel_type = Nnedi3(**kwargs) - - assert isinstance(pel_type, Scaler) - - return pel_type.scale(clip, clip.width * pel, clip.height * pel, **kwargs)