diff --git a/vskernels/__init__.py b/vskernels/__init__.py index e4c6144..2061a32 100644 --- a/vskernels/__init__.py +++ b/vskernels/__init__.py @@ -1,4 +1,5 @@ from . import exceptions, kernels, util # noqa: F401, F403 from .exceptions import * # noqa: F401, F403 from .kernels import * # noqa: F401, F403 +from .types import * # noqa: F401, F403 from .util import * # noqa: F401, F403 diff --git a/vskernels/kernels/__init__.py b/vskernels/kernels/__init__.py index 2581235..a4f9325 100644 --- a/vskernels/kernels/__init__.py +++ b/vskernels/kernels/__init__.py @@ -1,10 +1,7 @@ from .abstract import * # noqa: F401, F403 from .bicubic import * # noqa: F401, F403 from .complex import * # noqa: F401, F403 -from .fmtconv import * # noqa: F401, F403 -from .impulse import * # noqa: F401, F403 +from .custom import * # noqa: F401, F403 from .placebo import * # noqa: F401, F403 -from .resize import * # noqa: F401, F403 from .spline import * # noqa: F401, F403 from .various import * # noqa: F401, F403 -from .zimg import * # noqa: F401, F403 diff --git a/vskernels/kernels/abstract.py b/vskernels/kernels/abstract.py index 355eeea..7bb4194 100644 --- a/vskernels/kernels/abstract.py +++ b/vskernels/kernels/abstract.py @@ -14,7 +14,10 @@ from vstools.enums.color import _norm_props_enums from ..exceptions import UnknownDescalerError, UnknownKernelError, UnknownResamplerError, UnknownScalerError -from ..types import BotFieldLeftShift, BotFieldTopShift, LeftShift, TopFieldLeftShift, TopFieldTopShift, TopShift +from ..types import ( + BorderHandling, BotFieldLeftShift, BotFieldTopShift, LeftShift, SampleGridModel, TopFieldLeftShift, + TopFieldTopShift, TopShift +) __all__ = [ 'Scaler', 'ScalerT', @@ -135,7 +138,7 @@ def __init_subclass__(cls) -> None: return from ..util import abstract_kernels - from .zimg import ZimgComplexKernel + from .complex import CustomComplexKernel if cls in abstract_kernels: return @@ -152,7 +155,7 @@ def __init_subclass__(cls) -> None: if 'kernel_radius' in cls.__dict__.keys(): return - mro = [cls, *({*cls.mro()} - {*ZimgComplexKernel.mro()})] + mro = [cls, *({*cls.mro()} - {*CustomComplexKernel.mro()})] for sub_cls in mro: if hasattr(sub_cls, '_static_kernel_radius'): @@ -189,7 +192,7 @@ def ensure_obj( cls, (mro := cls.mro())[mro.index(BaseScaler) - 1], scaler, cls._err_class, [], func_except ) - @inject_self.property + @inject_self.cached.property def kernel_radius(self) -> int: return _default_kernel_radius(__class__, self) # type: ignore @@ -272,25 +275,33 @@ def descale( # type: ignore[override] shift: tuple[TopShift, LeftShift] | tuple[ TopShift | tuple[TopFieldTopShift, BotFieldTopShift], LeftShift | tuple[TopFieldLeftShift, BotFieldLeftShift] - ] = (0, 0), **kwargs: Any + ] = (0, 0), *, + border_handling: BorderHandling = BorderHandling.MIRROR, + sample_grid_model: SampleGridModel = SampleGridModel.MATCH_EDGES, + field_based: FieldBased | None = None, + **kwargs: Any ) -> vs.VideoNode: width, height = self._wh_norm(clip, width, height) check_correct_subsampling(clip, width, height) - field_based = FieldBased.from_param_or_video(kwargs.pop('field_based', None), clip) + field_based = FieldBased.from_param_or_video(field_based, clip) clip, bits = expect_bits(clip, 32) de_base_args = (width, height // (1 + field_based.is_inter)) + kwargs |= dict(border_handling=border_handling) if field_based.is_inter: shift_y, shift_x = tuple[tuple[float, float], ...]( sh if isinstance(sh, tuple) else (sh, sh) for sh in shift ) - de_kwargs_tf = self.get_descale_args(clip, (shift_y[0], shift_x[0]), *de_base_args, **kwargs) - de_kwargs_bf = self.get_descale_args(clip, (shift_y[1], shift_x[1]), *de_base_args, **kwargs) + kwargs_tf, shift = sample_grid_model.for_descale(clip, width, height, (shift_y[0], shift_x[0]), **kwargs) + kwargs_bf, shift = sample_grid_model.for_descale(clip, width, height, (shift_y[1], shift_x[1]), **kwargs) + + de_kwargs_tf = self.get_descale_args(clip, (shift_y[0], shift_x[0]), *de_base_args, **kwargs_tf) + de_kwargs_bf = self.get_descale_args(clip, (shift_y[1], shift_x[1]), *de_base_args, **kwargs_bf) if height % 2: raise CustomIndexError('You can\'t descale to odd resolution when crossconverted!', self.descale) @@ -311,6 +322,8 @@ def descale( # type: ignore[override] if any(isinstance(sh, tuple) for sh in shift): raise CustomValueError('You can\'t descale per-field when the input is progressive!', self.descale) + kwargs, shift = sample_grid_model.for_descale(clip, width, height, shift, **kwargs) # type: ignore + de_kwargs = self.get_descale_args(clip, shift, *de_base_args, **kwargs) # type: ignore descaled = self.descale_function(clip, **_norm_props_enums(de_kwargs)) diff --git a/vskernels/kernels/bicubic.py b/vskernels/kernels/bicubic.py index beeff31..bbfd7db 100644 --- a/vskernels/kernels/bicubic.py +++ b/vskernels/kernels/bicubic.py @@ -3,9 +3,10 @@ from math import sqrt from typing import Any -from vstools import CustomValueError, core, inject_self, vs +from vstools import CustomValueError, inject_self -from .zimg import ZimgComplexKernel +from .complex import CustomComplexKernel +from .helpers import bic_vals, poly3 __all__ = [ 'Bicubic', @@ -15,6 +16,8 @@ 'Catrom', 'FFmpegBicubic', 'AdobeBicubic', + 'AdobeBicubicSharper', + 'AdobeBicubicSmoother', 'BicubicSharp', 'RobidouxSoft', 'Robidoux', @@ -23,37 +26,34 @@ ] -class Bicubic(ZimgComplexKernel): +class Bicubic(CustomComplexKernel): """ Built-in bicubic resizer. Default: b=0, c=0.5 - Dependencies: - - * VapourSynth-descale - :param b: B-param for bicubic kernel :param c: C-param for bicubic kernel """ - scale_function = resample_function = core.lazy.resize.Bicubic - descale_function = core.lazy.descale.Debicubic - def __init__(self, b: float = 0, c: float = 1 / 2, **kwargs: Any) -> None: self.b = b self.c = c super().__init__(**kwargs) - def get_params_args( - self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any - ) -> dict[str, Any]: - args = super().get_params_args(is_descale, clip, width, height, **kwargs) - if is_descale: - return args | dict(b=self.b, c=self.c) - return args | dict(filter_param_a=self.b, filter_param_b=self.c) + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + x, b, c = abs(x), self.b, self.c + + if (x < 1.0): + return poly3(x, bic_vals.p0(b, c), 0.0, bic_vals.p2(b, c), bic_vals.p3(b, c)) + + if (x < 2.0): + return poly3(x, bic_vals.q0(b, c), bic_vals.q1(b, c), bic_vals.q2(b, c), bic_vals.q3(b, c)) + + return 0.0 - @inject_self.property + @inject_self.cached.property def kernel_radius(self) -> int: # type: ignore if (self.b, self.c) == (0, 0): return 1 @@ -102,6 +102,20 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(b=0, c=3 / 4, **kwargs) +class AdobeBicubicSharper(Bicubic): + """Bicubic b=0, c=1, blur=1.05; Adobe's "Bicubic Sharper" interpolation preset.""" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(b=0, c=1, blur=1.05, **kwargs) + + +class AdobeBicubicSmoother(Bicubic): + """Bicubic b=0, c=0.625, blur=1.15; Adobe's "Bicubic Smoother" interpolation preset.""" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(b=0, c=5 / 8, blur=1.15, **kwargs) + + class BicubicSharp(Bicubic): """Bicubic b=0, c=1""" @@ -136,47 +150,27 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(b=b, c=c, **kwargs) -class BicubicAuto(ZimgComplexKernel): +class BicubicAuto(Bicubic): """ Kernel that follows the rule of: b + 2c = target """ - scale_function = resample_function = core.lazy.resize.Bicubic - descale_function = core.lazy.descale.Debicubic - - def __init__(self, b: float | None = None, c: float | None = None, target: float = 1.0, **kwargs: Any) -> None: + def __init__(self, b: float | None = None, c: float | None = None, **kwargs: Any) -> None: if None not in {b, c}: raise CustomValueError("You can't specify both b and c!", self.__class__) - self.b = b - self.c = c - self.target = target + self.b, self.c = self._get_bc_args(b, c) super().__init__(**kwargs) - def get_params_args( - self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any - ) -> dict[str, Any]: - args = super().get_params_args(is_descale, clip, width, height, **kwargs) + def _get_bc_args(self, b: float | None, c: float | None) -> tuple[float, float]: + autob = 0.0 if b is None else b + autoc = 0.5 if c is None else c - b, c = self._get_bc_args() - - if is_descale: - return args | dict(b=b, c=c) - return args | dict(filter_param_a=b, filter_param_b=c) - - def _get_bc_args(self) -> tuple[float, float]: - autob = 0.0 if self.b is None else self.b - autoc = 0.5 if self.c is None else self.c - - if self.c is not None and self.b is None: - autob = self.target - 2 * self.c - elif self.c is None and self.b is not None: - autoc = (self.target - self.b) / 2 + if c is not None and b is None: + autob = 1.0 - 2 * c + elif c is None and b is not None: + autoc = (1.0 - b) / 2 return autob, autoc - - @inject_self.property - def kernel_radius(self) -> int: # type: ignore - return Bicubic(*self._get_bc_args()).kernel_radius diff --git a/vskernels/kernels/complex.py b/vskernels/kernels/complex.py index fe1333f..0ca8f9c 100644 --- a/vskernels/kernels/complex.py +++ b/vskernels/kernels/complex.py @@ -1,62 +1,32 @@ from __future__ import annotations -from functools import lru_cache +from math import ceil from typing import TYPE_CHECKING, Any, SupportsFloat, TypeVar, Union, cast from stgpytools import inject_kwargs_params from vstools import ( - CustomIntEnum, Dar, KwargsT, Resolution, Sar, VSFunctionAllArgs, check_correct_subsampling, fallback, inject_self, - padder, vs + Dar, KwargsT, Resolution, Sar, VSFunctionAllArgs, check_correct_subsampling, fallback, inject_self, vs ) -from ..types import Center, LeftShift, Slope, TopShift +from ..types import BorderHandling, Center, LeftShift, SampleGridModel, Slope, TopShift from .abstract import Descaler, Kernel, Resampler, Scaler +from .custom import CustomKernel __all__ = [ - 'BorderHandling', - 'LinearScaler', 'LinearDescaler', 'KeepArScaler', 'ComplexScaler', 'ComplexScalerT', - 'ComplexKernel', 'ComplexKernelT' + 'ComplexKernel', 'ComplexKernelT', + + 'CustomComplexKernel', + 'CustomComplexTapsKernel' ] XarT = TypeVar('XarT', Sar, Dar) -class BorderHandling(CustomIntEnum): - MIRROR = 0 - ZERO = 1 - REPEAT = 2 - - def prepare_clip(self, clip: vs.VideoNode, min_pad: int = 2) -> vs.VideoNode: - pad_w, pad_h = ( - self.pad_amount(size, min_pad) for size in (clip.width, clip.height) - ) - - if pad_w == pad_h == 0: - return clip - - args = (clip, pad_w, pad_w, pad_h, pad_h) - - match self: - case BorderHandling.MIRROR: - return padder.MIRROR(*args) - case BorderHandling.ZERO: - return padder.COLOR(*args) - case BorderHandling.REPEAT: - return padder.REPEAT(*args) - - @lru_cache - def pad_amount(self, size: int, min_amount: int = 2) -> int: - if self is BorderHandling.MIRROR: - return 0 - - return (((size + min_amount) + 7) & -8) - size - - def _from_param(cls: type[XarT], value: XarT | bool | float | None, fallback: XarT) -> XarT | None: if value is False: return fallback @@ -88,7 +58,7 @@ def func( has_custom_op = hasattr(self, f'_linear_{op_name}') operation = cast( VSFunctionAllArgs, - getattr(self, f'_linear_{op_name}') if has_custom_op else getattr(super(), op_name) # type: ignore + getattr(self, f'_linear_{op_name}') if has_custom_op else getattr(super(), op_name) ) if sigmoid: @@ -159,7 +129,7 @@ def _get_kwargs_keep_ar( return kwargs - def _handle_crop_resize_kwargs( # type: ignore[override] + def _handle_crop_resize_kwargs( self, clip: vs.VideoNode, width: int, height: int, shift: tuple[TopShift, LeftShift], sar: Sar | bool | float | None, dar: Dar | bool | float | None, dar_in: Dar | bool | float | None, **kwargs: Any @@ -210,6 +180,7 @@ def scale( # type: ignore[override] self, clip: vs.VideoNode, width: int | None = None, height: int | None = None, shift: tuple[TopShift, LeftShift] = (0, 0), *, border_handling: BorderHandling = BorderHandling.MIRROR, + sample_grid_model: SampleGridModel = SampleGridModel.MATCH_EDGES, sar: Sar | float | bool | None = None, dar: Dar | float | bool | None = None, dar_in: Dar | bool | float | None = None, keep_ar: bool | None = None, **kwargs: Any @@ -225,9 +196,11 @@ def scale( # type: ignore[override] kwargs, shift, out_sar = self._handle_crop_resize_kwargs(clip, width, height, shift, **kwargs) + kwargs, shift = sample_grid_model.for_scale(clip, width, height, shift, **kwargs) + padded = border_handling.prepare_clip(clip, self.kernel_radius) - shift, clip = tuple( # type: ignore + shift, clip = tuple( s + ((p - c) // 2) for s, c, p in zip(shift, *((x.width, x.height) for x in (clip, padded))) ), padded @@ -247,6 +220,7 @@ def scale( # type: ignore[override] shift: tuple[TopShift, LeftShift] = (0, 0), *, border_handling: BorderHandling = BorderHandling.MIRROR, + sample_grid_model: SampleGridModel = SampleGridModel.MATCH_EDGES, sar: Sar | bool | float | None = None, dar: Dar | bool | float | None = None, keep_ar: bool | None = None, linear: bool = False, sigmoid: bool | tuple[Slope, Center] = False, **kwargs: Any @@ -255,7 +229,7 @@ def scale( # type: ignore[override] return super().scale( clip, width, height, shift, sar=sar, dar=dar, keep_ar=keep_ar, linear=linear, sigmoid=sigmoid, border_handling=border_handling, - **kwargs + sample_grid_model=sample_grid_model, **kwargs ) @@ -263,5 +237,29 @@ class ComplexKernel(Kernel, LinearDescaler, ComplexScaler): # type: ignore ... +class CustomComplexKernel(CustomKernel, ComplexKernel): # type: ignore + if TYPE_CHECKING: + @inject_self.cached + @inject_kwargs_params + def descale( # type: ignore[override] + self, clip: vs.VideoNode, width: int, height: int, shift: tuple[TopShift, LeftShift] = (0, 0), + *, blur: float = 1.0, border_handling: BorderHandling, + sample_grid_model: SampleGridModel = SampleGridModel.MATCH_EDGES, + ignore_mask: vs.VideoNode | None = None, linear: bool = False, + sigmoid: bool | tuple[Slope, Center] = False, **kwargs: Any + ) -> vs.VideoNode: + ... + + +class CustomComplexTapsKernel(CustomComplexKernel): + def __init__(self, taps: float, **kwargs: Any) -> None: + self.taps = taps + super().__init__(**kwargs) + + @inject_self.cached.property + def kernel_radius(self) -> int: # type: ignore + return ceil(self.taps) + + ComplexScalerT = Union[str, type[ComplexScaler], ComplexScaler] ComplexKernelT = Union[str, type[ComplexKernel], ComplexKernel] diff --git a/vskernels/kernels/custom.py b/vskernels/kernels/custom.py new file mode 100644 index 0000000..1687ed7 --- /dev/null +++ b/vskernels/kernels/custom.py @@ -0,0 +1,67 @@ +from __future__ import annotations +from stgpytools import inject_self +from inspect import Signature + +from vstools import vs, core +from typing import Any +from .abstract import Kernel + +from typing import TypeVar + + +__all__ = [ + 'CustomKernel' +] + + +class CustomKernel(Kernel): + @inject_self.cached + def kernel(self: CustomKernelT, *, x: float) -> float: + raise NotImplementedError + + def _modify_kernel_func(self, *, blur: float = 1.0, **kwargs: Any): + support = self.kernel_radius * blur + + if blur != 1.0: + def kernel(x: float) -> float: + return self.kernel(x=x / blur) + + return kernel, support + + return self.kernel, support + + @inject_self + def scale_function( + self, clip: vs.VideoNode, width: int | None = None, height: int | None = None, *args: Any, **kwargs: Any + ) -> vs.VideoNode: + clean_kwargs = { + k: v for k, v in kwargs.items() + if k not in Signature.from_callable(self._modify_kernel_func).parameters.keys() + } + return core.resize2.Custom(clip, *self._modify_kernel_func(**kwargs), width, height, *args, **clean_kwargs) + + resample_function = scale_function + + @inject_self + def descale_function( + self, clip: vs.VideoNode, width: int, height: int, *args: Any, **kwargs: Any + ) -> vs.VideoNode: + clean_kwargs = { + k: v for k, v in kwargs.items() + if k not in Signature.from_callable(self._modify_kernel_func).parameters.keys() + } + return core.descale.Decustom(clip, width, height, *self._modify_kernel_func(**kwargs), *args, **clean_kwargs) + + def get_params_args( + self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any + ) -> dict[str, Any]: + args = super().get_params_args(is_descale, clip, width, height, **kwargs) + + if not is_descale: + for key in ('border_handling', 'ignore_mask', 'force', 'force_h', 'force_v'): + args.pop(key, None) + + return args + + +CustomKernelT = TypeVar('CustomKernelT', bound=CustomKernel) diff --git a/vskernels/kernels/fmtconv.py b/vskernels/kernels/fmtconv.py deleted file mode 100644 index cb4606d..0000000 --- a/vskernels/kernels/fmtconv.py +++ /dev/null @@ -1,190 +0,0 @@ -from __future__ import annotations - -from math import ceil -from typing import Any, Callable, overload - -from stgpytools import inject_kwargs_params -from vstools import VideoFormatT, VSFunction, core, inject_self, vs - -from ..types import LeftShift, TopShift -from .abstract import Resampler -from .bicubic import Bicubic -from .complex import ComplexScaler - -__all__ = [ - 'FmtConv' -] - - -call_wrapT = Callable[..., VSFunction] - - -class FmtConv(Resampler, ComplexScaler): # type: ignore - """ - Abstract fmtconv's resizer. - - Dependencies: - - * fmtconv - """ - - def scale_function(self, clip: vs.VideoNode, **kwargs: Any) -> vs.VideoNode: - assert clip.format - - def _check_fmt(fmt: int | VideoFormatT) -> tuple[vs.VideoFormat, bool]: - fmt = core.get_video_format(fmt) - - return fmt, (( - fmt.bits_per_sample == 32 and fmt.sample_type == vs.FLOAT - ) or ( - fmt.bits_per_sample == 16 and fmt.sample_type == vs.INTEGER - )) - - in_fmt = clip.format - out_fmt = None - - csp = kwargs.get('csp', kwargs.get('format', None)) - bits = kwargs.get('bits', None) - flt = kwargs.get('flt', 0) - - if csp: - out_fmt, valid = _check_fmt(csp) - - if not valid: - kwargs.pop('csp') - else: - out_fmt = None - else: - out_fmt = in_fmt - - if bits: - if not out_fmt: - out_fmt = in_fmt - - out_fmt = out_fmt.replace(bits_per_sample=bits) - - if not _check_fmt(in_fmt)[1]: - clip = Bicubic.resample( - clip, in_fmt.replace(bits_per_sample=16 * (1 + flt), sample_type=vs.SampleType(flt)) - ) - if out_fmt: - out_fmt, valid = _check_fmt(out_fmt) - if valid: - kwargs['csp'] = out_fmt - out_fmt = None - - filtered = clip.fmtc.resample(**kwargs) - - if not out_fmt: - return filtered - - assert filtered.format - return Bicubic.resample(filtered, out_fmt) - - resample_function = scale_function - - _kernel: str - """Name of the fmtconv kernel""" - - _resize_fmtc_params_lut = { - 'sw': 'src_width', 'sx': 'src_left', - 'sh': 'src_height', 'sy': 'src_top' - } - - def __init__(self, taps: float = 4, **kwargs: Any) -> None: - self.taps = taps - super().__init__(**kwargs) - - def _clean_args(self, **kwargs: Any) -> dict[str, Any]: - for fmtc_param, res_param in self._resize_fmtc_params_lut.items(): - if res_param in kwargs: - kwargs[fmtc_param] = kwargs.pop(res_param) - - return kwargs - - @inject_kwargs_params - def get_scale_args( - self, clip: vs.VideoNode, shift: tuple[TopShift, LeftShift] = (0, 0), - width: int | None = None, height: int | None = None, - *funcs: Callable[..., Any], **kwargs: Any - ) -> dict[str, Any]: - return self._clean_args( - sx=shift[1], sy=shift[0], kernel=self._kernel, - **self.get_clean_kwargs(*funcs), - **self.get_params_args(False, clip, width, height, **kwargs) - ) - - def get_params_args( - self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any - ) -> dict[str, Any]: - kwargs |= dict(w=width, h=height, taps=self.taps) - - if is_descale: - kwargs |= dict(sw=width, sh=height) - return kwargs - - @overload # type: ignore - @inject_self.cached - @inject_kwargs_params - def shift(self, clip: vs.VideoNode, shift: tuple[TopShift, LeftShift] = (0, 0), **kwargs: Any) -> vs.VideoNode: - ... - - @overload # type: ignore - @inject_self.cached - @inject_kwargs_params - def shift( - self, clip: vs.VideoNode, - shift_top: float | list[float] = 0.0, shift_left: float | list[float] = 0.0, **kwargs: Any - ) -> vs.VideoNode: - ... - - @inject_self.cached # type: ignore - @inject_kwargs_params - def shift( - self, clip: vs.VideoNode, - shifts_or_top: float | tuple[float, float] | list[float] | None = None, - shift_left: float | list[float] | None = None, **kwargs: Any - ) -> vs.VideoNode: - assert clip.format - - n_planes = clip.format.num_planes - - def _shift(shift_top: float | list[float] = 0.0, shift_left: float | list[float] = 0.0) -> vs.VideoNode: - return self.scale_function( - clip, sy=shift_top, sx=shift_left, kernel=self._kernel, **self.get_clean_kwargs(), **kwargs - ) - - if not shifts_or_top and not shift_left: - return _shift() - elif isinstance(shifts_or_top, tuple): - return _shift(*shifts_or_top) - elif isinstance(shifts_or_top, float) and isinstance(shift_left, float): - return _shift(shifts_or_top, shift_left) - - shifts_top = shifts_or_top or 0.0 - if isinstance(shifts_top, list): - if not shifts_top: - shifts_top = [0.0] * n_planes - elif len(shifts_top) > n_planes: - shifts_top[:n_planes] - - shifts_left = shift_left or 0.0 - if isinstance(shifts_left, list): - if not shifts_left: - shifts_left = [0.0] * n_planes - elif len(shifts_left) > n_planes: - shifts_left = shifts_left[:n_planes] - - return _shift(shifts_top, shifts_left) - - @inject_self.property - def kernel_radius(self) -> int: # type: ignore - taps_hv: float | None = self.kwargs.get('taps_h', self.kwargs.get('taps_v', None)) - - if taps_hv is None: - taps_hv = self.taps - - return ceil(taps_hv) - - def get_implemented_funcs(self) -> tuple[Callable[..., Any]]: - return (self.shift, ) # type: ignore diff --git a/vskernels/kernels/helpers.py b/vskernels/kernels/helpers.py new file mode 100644 index 0000000..214fdbe --- /dev/null +++ b/vskernels/kernels/helpers.py @@ -0,0 +1,47 @@ +from math import floor, pi, sin + +__all__ = [ + 'sinc', 'poly3', 'round_halfup', 'bic_vals' +] + + +class bic_vals: + @staticmethod + def p0(b: float, c: float) -> float: + return (6.0 - 2.0 * b) / 6.0 + + @staticmethod + def p2(b: float, c: float) -> float: + return (-18.0 + 12.0 * b + 6.0 * c) / 6.0 + + @staticmethod + def p3(b: float, c: float) -> float: + return (12.0 - 9.0 * b - 6.0 * c) / 6.0 + + @staticmethod + def q0(b: float, c: float) -> float: + return (8.0 * b + 24.0 * c) / 6.0 + + @staticmethod + def q1(b: float, c: float) -> float: + return (-12.0 * b - 48.0 * c) / 6.0 + + @staticmethod + def q2(b: float, c: float) -> float: + return (6.0 * b + 30.0 * c) / 6.0 + + @staticmethod + def q3(b: float, c: float) -> float: + return (-b - 6.0 * c) / 6.0 + + +def sinc(x: float) -> float: + return 1.0 if x == 0.0 else sin(x * pi) / (x * pi) + + +def poly3(x: float, c0: float, c1: float, c2: float, c3: float) -> float: + return c0 + x * (c1 + x * (c2 + x * c3)) + + +def round_halfup(x: float) -> float: + return floor(x + 0.5) if x < 0 else floor(x + 0.49999999999999994) diff --git a/vskernels/kernels/impulse.py b/vskernels/kernels/impulse.py deleted file mode 100644 index fa7d13d..0000000 --- a/vskernels/kernels/impulse.py +++ /dev/null @@ -1,285 +0,0 @@ -from __future__ import annotations - -from typing import Any, Sequence - -from stgpytools import inject_kwargs_params -from vstools import inject_self, vs - -from ..types import LeftShift, TopShift -from .fmtconv import FmtConv - -__all__ = [ - 'Impulse', - 'Quadratic', - 'Wiener', - 'Hann', - 'Hamming', - 'BlackHarris', - 'BlackNuttall', - 'FlatTop', - 'MinSide', - 'Ginseng', - 'Welch', - 'Cosine', - 'Bessel', - 'Parzen', - 'Kaiser', - 'Bohman', -] - - -class Impulse(FmtConv): - """fmtconv's impulse resizer.""" - - _kernel = 'impulse' - - def get_params_args( - self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any - ) -> dict[str, Any]: - return super().get_params_args(is_descale, clip, width, height, **kwargs) - - def __init__(self, impulse: Sequence[float], oversample: int = 8, taps: float = 1, **kwargs: Any) -> None: - super().__init__(taps, impulse=[*impulse[::-1], *impulse[:-1]], kovrspl=oversample, **kwargs) - - @inject_self.cached - @inject_kwargs_params - def scale( # type: ignore[override] - self, clip: vs.VideoNode, width: int | None = None, height: int | None = None, - shift: tuple[TopShift, LeftShift] = (-0.125, -0.125), **kwargs: Any - ) -> vs.VideoNode: - width, height = self._wh_norm(clip, width, height) - return super().scale(clip, width, height, shift, **kwargs) - - -class Quadratic(Impulse): - """Quadratic kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 0.75, 0.746094, 0.734375, 0.714844, 0.6875, - 0.652344, 0.609375, 0.558594, 0.5, 0.439453, - 0.382813, 0.330078, 0.28125, 0.236328, 0.195313, - 0.158203, 0.125, 0.095703, 0.070313, 0.048828, - 0.03125, 0.017578, 0.007813, 0.001953 - ], 16, **kwargs) - - -class Wiener(Impulse): - """Wiener kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.928293, 0.847768, 0.749610, 0.625, 0.470033, - 0.300445, 0.136885, 0, -0.094068, -0.147204, - -0.165801, -0.15625, -0.1255, -0.082723, - -0.037647, 0, 0.022825, 0.032783, 0.034161, - 0.03125, 0.02491, 0.017308, 0.008864 - ], 8, **kwargs) - - -class Hann(Impulse): - """Hann kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9721491276507364, 0.8916666533754284, 0.7673293227933666, - 0.6123898750249921, 0.44274830753566713, 0.27481699575031854, - 0.12341360408603762, 0, -0.08848381495693607, -0.14005052617158592, - -0.1573484857988253, -0.14670726866144826, -0.11675289338122011, - -0.0768542368708979, -0.035667086889988917, 0, 0.025852297356152947, - 0.040259628673424544, 0.043939594567039456, 0.039299593276647324, - 0.029609836521073, 0.018187607754470616, 0.007745261904000362, - 0, -0.0044240051920867865, -0.005835794531242358, -0.005144384187084287, - -0.0034614139060841795, -0.0017466187017072979, -0.0005766441854451786, - -0.00007568486302246578 - ], 8, **kwargs) - - -class Hamming(Impulse): - """Hamming kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9723291304741598, 0.8923302555040387, 0.7686246617990514, - 0.6142487927491155, 0.4448795641717659, 0.27675712509487843, - 0.1246257815086694, 0, -0.0900023682175275, -0.1431203033588165, - -0.16168530396390082, -0.1517323766550889, -0.12167980632033411, - -0.0808254466432445, -0.0379149269102415, 0, 0.02826674933221172, - 0.044845606381107544, 0.05006826118507596, 0.0460528222676711, - 0.03593314767037512, 0.02307155155805429, 0.010401622971455375, - 0, -0.00707512686962141, -0.010701323742157228, -0.011434689771183611, - -0.010173206936358085, -0.00783656756150458, -0.005137221895242707, - -0.002481597155711647 - ], 8, **kwargs) - - -class BlackHarris(Impulse): - """Black-Harris kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9690685474542851, 0.8804147429536661, 0.7457038324654484, - 0.5820238498592893, 0.408897820926439, 0.24504664162741924, - 0.10556008266546878, 0, -0.06828556374209023, -0.10164720692199704, - -0.10668443324736704, -0.09229279162100884, -0.0676826118865924, - -0.040770939188391156, -0.01719366607497128, 0, 0.010072521093743276, - 0.013950171901372979, 0.013442938661666109, 0.010539924532807445, - 0.006912782290973393, 0.0036717082778625404, 0.001343980906503044, - 0, -0.0005599450449479095, -0.0006299356259012779, -0.00047599901604884737, - -0.0002782179944466215, -0.0001254132897686219, -0.00003940328671242334, - -0.000006272327028117027 - ], 8, **kwargs) - - -class BlackNuttall(Impulse): - """BlackNuttall kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9691956173858006, 0.8808777891436077, 0.7465903227462598, - 0.583261858851737, 0.41026820009726694, 0.2462414690601752, - 0.10626931490160989, 0, -0.06906778816628918, -0.10311203169060508, - -0.10858461954425613, -0.09429622625282544, -0.06945329948290131, - -0.04204507233452078, -0.0178310531150637, 0, 0.010590373575929674, - 0.014791046691013261, 0.0143917812832477, 0.01141108308851331, - 0.007582722772125722, 0.004090031528918704, 0.0015247187900517072, - 0, -0.0006668666328159042, -0.0007753811435345259, -0.0006111170869562098, - -0.0003782503724444364, -0.00018625686680715366, -0.00006947677421552765, - -0.00001734615796446616 - ], 8, **kwargs) - - -class FlatTop(Impulse): - """FlatTop kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9633262343597194, 0.859593850914732, 0.7061738614429447, - 0.5274619393571068, 0.3494083785508531, 0.1941326535234994, - 0.07599859616307823, 0, -0.03742790728859844, -0.045746423402100424, - -0.03680885348596081, -0.021591047728118973, -0.007969308213425627, - 0.00010539639481656004, 0.002167459237080162, 0, -0.003812769923009659, - -0.007053787251496119, -0.008498312606009256, -0.007982808809703638, - -0.0060878619550795154, -0.003677603986271232, -0.0015053481284463162, - 0, 0.0007565960905941711, 0.0009239058962157, 0.0007553677173860026, - 0.0004791071697109673, 0.00023850930336092043, 0.00008824527524187423, - 0.000021135402405053327 - ], 8, **kwargs) - - -class MinSide(Impulse): - """MinSide kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9677067397427601, 0.8754735424031383, 0.7363116111669247, - 0.5690387749427253, 0.3947100931903589, 0.2328726279464744, - 0.09846997346812622, 0, -0.060828473848914016, -0.08807741082408858, - -0.08963798222912361, -0.07495103916223587, -0.05294982732350461, - -0.030621864426414024, -0.012354100121804074, 0, 0.006550750370351611, - 0.008581357979868063, 0.007789505667884757, 0.00572824890289049, - 0.0035077922683009735, 0.001731217648211203, 0.0005857853596332941, - 0, -0.0002050370640200731, -0.0002095361090755842, -0.00014308227339085642, - -0.00007543851733140218, -0.000030927452169915216, -0.000009168350688844679, - -0.00000158174014270345 - ], 8, **kwargs) - - -class Ginseng(Impulse): - """Ginseng kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.9706924517023813, 0.8863037293563394, 0.7568846746026882, - 0.5974484350594943, 0.42568771143242046, 0.2593638071484244, - 0.1138155395729318, 0, -0.07663423509063601, -0.11630395745498025, - -0.12412241952630289, -0.10861320529064979, -0.07983137551487393, - -0.04746371544549015, -0.01925303598542219, 0, 0.008737966998564498, - 0.00827789347498015, 0.0019289633237434013, -0.006160413752293629, - -0.012153940393777712, -0.013434473237575344, -0.00910161101088654 - ], 8, **kwargs) - - -class Welch(Impulse): - """Welch kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.972804, 0.894064, 0.771960, 0.618936, 0.450106, 0.281349, - 0.127371, 0, -0.093051, -0.148802, -0.168948, -0.159155, -0.127874, - -0.0848512, -0.039589, 0, 0.028562, 0.0437654, 0.046219, 0.0389045, - 0.026257, 0.0130728, 0.003457 - ], 8, **kwargs) - - -class Cosine(Impulse): - """Cosine kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.972409, 0.892614, 0.769145, 0.614928, 0.445557, 0.277261, - 0.124857, -0, -0.090029, -0.142854, -0.160801, -0.150053, -0.119324, - -0.0782968, -0.036093, 0, 0.025353, 0.0382818, 0.039802, 0.0329539, - 0.021856, 0.0106832, 0.002771 - ], 8, **kwargs) - - -class Bessel(Impulse): - """Bessel kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1.5708, 1.525858, 1.39627, 1.196854, 0.94988, 0.681773, - 0.419361, 0.186267, 0, -0.129804, -0.201802, -0.222051, - -0.202077, -0.156337, -0.0995575, -0.044427, 0, 0.029012, - 0.0420623, 0.041868, 0.0330862, 0.020877, 0.00967627, 0.002371 - ], 8, **kwargs) - - -class Parzen(Impulse): - """Parzen kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 0.666667, 0.648727, 0.596804, 0.51624, 0.415088, 0.303095, 0.190509, - 0.086876, 0, -0.064824, -0.105205, -0.121655, -0.117225, -0.096848, - -0.0665116, -0.032382, 0, 0.026335, 0.0439404, 0.051856, 0.0506892, - 0.042271, 0.0291773, 0.014224, 0, -0.011489, -0.0190243, -0.022227, - -0.0214634, -0.017647, -0.0119891, -0.005745, 0, 0.004475, 0.00727292, - 0.008338, 0.00789788, 0.006366, 0.00423604, 0.001986, 0, -0.001471, - -0.00232189, -0.002577, -0.00235492, -0.001824, -0.00116114, -0.000518, - 0, 0.000341, 0.000502619, 0.000515, 0.000430408, 0.000301, 0.000169602, - 0.000066, 0, -0.00003, -0.0000341078, -0.000025, -0.0000138158, -0.000005, - -0.00000118185, 0 - ], 8, **kwargs) - - -class Kaiser(Impulse): - """Kaiser kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.973784, 0.897692, 0.779078, 0.629225, 0.462012, 0.29231, 0.134312, - 0, -0.102037, -0.167326, -0.195688, -0.190881, -0.159791, -0.111303, - -0.055016, 0, 0.046264, 0.0786285, 0.094631, 0.0944555, 0.080540, 0.0569238, - 0.028458, 0, -0.024290, -0.0414535, -0.050006, -0.0499464, -0.042553, -0.0300094, - -0.014951, 0, 0.012629, 0.0214054, 0.025619, 0.0253622, 0.021395, 0.0149254, - 0.007348, 0, -0.006043, -0.0100898, -0.011883, -0.0115622, -0.009575, -0.00654779, - -0.003156, 0, 0.002475, 0.00402406, 0.004606, 0.00434445, 0.003478, 0.00229289, - 0.001061, 0, -0.000757, -0.0011651, -0.001252, -0.00109846, -0.000808, -0.000481802, -0.000197 - ], 8, **kwargs) - - -class Bohman(Impulse): - """Bohman kernel.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__([ - 1, 0.973334, 0.896071, 0.77599, 0.624897, 0.457161, 0.287989, 0.131668, 0, -0.098853, - -0.161007, -0.186917, -0.180888, -0.150148, -0.103645, -0.050742, 0, 0.04179, 0.0702274, - 0.083521, 0.0823302, 0.069284, 0.0482961, 0.023797, 0, -0.019685, -0.0330319, -0.039145, - -0.0383729, -0.032053, -0.0221387, -0.010789, 0, 0.008687, 0.0143432, 0.016696, 0.0160458, - 0.013115, 0.00884562, 0.0042, 0, -0.003187, -0.00508814, -0.005708, -0.00526944, -0.004121, - -0.00264747, -0.001192, 0, 0.000798, 0.00118257, 0.001221, 0.00102559, 0.00072, 0.000408479, - 0.000159, 0, -0.000073, -0.0000834291, -0.000062, -0.000033957, -0.000013, -0.00000291302, 0 - ], 8, **kwargs) diff --git a/vskernels/kernels/placebo.py b/vskernels/kernels/placebo.py index 0dd24d3..d5c9b04 100644 --- a/vskernels/kernels/placebo.py +++ b/vskernels/kernels/placebo.py @@ -10,7 +10,15 @@ from .complex import LinearScaler __all__ = [ - 'Placebo' + 'Placebo', + 'EwaBicubic', + 'EwaJinc', + 'EwaLanczos', + 'EwaGinseng', + 'EwaHann', + 'EwaHannSoft', + 'EwaRobidoux', + 'EwaRobidouxSharp', ] @@ -79,7 +87,7 @@ def get_params_args( antiring=self.antiring, ) | kwargs - @inject_self.property + @inject_self.cached.property def kernel_radius(self) -> int: # type: ignore from .bicubic import Bicubic @@ -90,3 +98,66 @@ def kernel_radius(self) -> int: # type: ignore return Bicubic(fallback(self.b, 0), fallback(self.c, 0.5)).kernel_radius return 2 + + +class EwaBicubic(Placebo): + _kernel = 'ewa_robidoux' + + def __init__(self, b: float = 0.0, c: float = 0.5, radius: int | None = None, **kwargs: Any) -> None: + radius = kwargs.pop('taps', radius) + + if radius is None: + from .bicubic import Bicubic + + radius = Bicubic(b, c).kernel_radius + + super().__init__(radius, b, c, **kwargs) + + +class EwaLanczos(Placebo): + _kernel = 'ewa_lanczos' + + def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: + super().__init__(taps, None, None, **kwargs) + + +class EwaJinc(Placebo): + _kernel = 'ewa_jinc' + + def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: + super().__init__(taps, None, None, **kwargs) + + +class EwaGinseng(Placebo): + _kernel = 'ewa_ginseng' + + def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: + super().__init__(taps, None, None, **kwargs) + + +class EwaHann(Placebo): + _kernel = 'ewa_hann' + + def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: + super().__init__(taps, None, None, **kwargs) + + +class EwaHannSoft(Placebo): + _kernel = 'haasnsoft' + + def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: + super().__init__(taps, None, None, **kwargs) + + +class EwaRobidoux(Placebo): + _kernel = 'ewa_robidoux' + + def __init__(self, **kwargs: Any) -> None: + super().__init__(None, None, None, **kwargs) + + +class EwaRobidouxSharp(Placebo): + _kernel = 'ewa_robidouxsharp' + + def __init__(self, **kwargs: Any) -> None: + super().__init__(None, None, None, **kwargs) diff --git a/vskernels/kernels/resize.py b/vskernels/kernels/resize.py deleted file mode 100644 index c5b6e6a..0000000 --- a/vskernels/kernels/resize.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from math import ceil -from typing import Any - -from vstools import core, vs, inject_self - -from .zimg import ZimgComplexKernel - -__all__ = [ - 'Point', - 'Bilinear', - 'Lanczos', -] - - -class Point(ZimgComplexKernel): - """Built-in point resizer.""" - - scale_function = resample_function = descale_function = core.lazy.resize.Point - _static_kernel_radius = 1 - - -class Bilinear(ZimgComplexKernel): - """Built-in bilinear resizer.""" - - scale_function = resample_function = core.lazy.resize.Bilinear - descale_function = core.lazy.descale.Debilinear - _static_kernel_radius = 1 - - -class Lanczos(ZimgComplexKernel): - """ - Built-in lanczos resizer. - - Dependencies: - - * VapourSynth-descale - - :param taps: taps param for lanczos kernel - """ - - scale_function = resample_function = core.lazy.resize.Lanczos - descale_function = core.lazy.descale.Delanczos - - def __init__(self, taps: int = 3, **kwargs: Any) -> None: - self.taps = taps - super().__init__(**kwargs) - - def get_params_args( - self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any - ) -> dict[str, Any]: - args = super().get_params_args(is_descale, clip, width, height, **kwargs) - if is_descale: - return args | dict(taps=self.taps) - return args | dict(filter_param_a=self.taps) - - @inject_self.property - def kernel_radius(self) -> int: # type: ignore - return ceil(self.taps) diff --git a/vskernels/kernels/spline.py b/vskernels/kernels/spline.py index 280edb8..b1892cc 100644 --- a/vskernels/kernels/spline.py +++ b/vskernels/kernels/spline.py @@ -1,11 +1,12 @@ from __future__ import annotations +from math import comb from typing import Any -from vstools import core +from vstools import inject_self -from .fmtconv import FmtConv -from .zimg import ZimgComplexKernel +from .complex import CustomComplexTapsKernel +from .helpers import poly3 __all__ = [ 'Spline', @@ -15,52 +16,124 @@ ] -class Spline(FmtConv): - """fmtconv's spline resizer.""" - - _kernel = 'spline' +class Spline(CustomComplexTapsKernel): + """Spline resizer.""" def __init__(self, taps: float = 2, **kwargs: Any) -> None: - super().__init__(taps=taps, **kwargs) + super().__init__(taps, **kwargs) + if hasattr(self, '_static_coeffs'): + self._coefs = self._static_coeffs + else: + self._coefs = self._splineKernelCoeff() -class Spline16(ZimgComplexKernel): - """ - Built-in spline16 resizer. + def _naturalCubicSpline(self, values: list[int]) -> list[float]: + import numpy as np # type: ignore - Dependencies: + n = len(values) - 1 - * VapourSynth-descale - """ + rhs = values[:-1] + values[1:] + [0] * (2 * n) - scale_function = resample_function = core.lazy.resize.Spline16 - descale_function = core.lazy.descale.Despline16 - _static_kernel_radius = 2 + eqns = [] + # left value = sample + eqns += [[0] * (4 * i) + [i ** 3, i ** 2, i, 1] + [0] * (4 * (n - i - 1)) for i in range(n)] + # right value = sample + eqns += [[0] * (4 * i) + [(i + 1) ** 3, (i + 1) ** 2, i + 1, 1] + [0] * (4 * (n - i - 1)) for i in range(n)] + # derivatives match + eqns += [ + ( + [0] * (4 * i) + # noqa: W504 + [3 * (i + 1) ** 2, 2 * (i + 1), 1, 0] + # noqa: W504 + [-3 * (i + 1) ** 2, -2 * (i + 1), -1, 0] + # noqa: W504 + [0] * (4 * (n - i - 2)) + ) + for i in range(n - 1) + ] + # second derivatives match + eqns += [ + [0] * (4 * i) + [6 * (i + 1), 2, 0, 0] + [-6 * (i + 1), -2, 0, 0] + [0] * (4 * (n - i - 2)) + for i in range(n - 1) + ] + eqns += [[0, 2, 0, 0] + [0] * (4 * (n - 1))] + eqns += [[0] * (4 * (n - 1)) + [6 * n ** 2, 2 * n, 0, 0]] + assert (len(rhs) == len(eqns)) -class Spline36(ZimgComplexKernel): - """ - Built-in spline36 resizer. + return list(np.linalg.solve(np.array(eqns), np.array(rhs))) - Dependencies: + def _splineKernelCoeff(self) -> list[float]: + taps = self.kernel_radius - * VapourSynth-descale - """ + coeffs = list[float]() - scale_function = resample_function = core.lazy.resize.Spline36 - descale_function = core.lazy.descale.Despline36 - _static_kernel_radius = 3 + def _shiftPolynomial(coeffs: list[float], shift: float) -> list[float]: + return [ + sum(c * comb(k, m) * (-shift) ** max(0, k - m) for k, c in enumerate(coeffs[::-1])) + for m in range(len(coeffs)) + ][::-1] + + for i in range(taps): + samplept = taps - i - 1 + samples = [0] * samplept + [1] + [0] * (2 * taps - samplept - 1) + + assert len(samples) == 2 * taps + + coeffs += _shiftPolynomial( + self._naturalCubicSpline(samples)[4 * taps - 4:4 * taps], -(taps - 1) + i + ) + + return coeffs + + @inject_self.cached + def kernel(self, x: float) -> float: # type: ignore + x, taps = abs(x), self.kernel_radius + + if x >= taps: + return 0.0 + + tap = int(x) + + a, b, c, d = self._coefs[4 * tap:4 * tap + 4] + + return poly3(x, d, c, b, a) -class Spline64(ZimgComplexKernel): - """ - Built-in spline64 resizer. +class NaturalSpline(Spline): + def __init__(self, **kwargs: Any) -> None: + super().__init__(self._static_kernel_radius, **kwargs) # type: ignore - Dependencies: - * VapourSynth-descale - """ +class Spline16(NaturalSpline): + """Spline16 resizer.""" + + _static_kernel_radius = 2 + + _static_coeffs = [ + 0.9999999999999988, -1.799999999999999, -0.1999999999999993, 1.0000000000000004, + -0.333333333333333, 1.7999999999999985, -3.066666666666665, 1.5999999999999994 + ] + + +class Spline36(NaturalSpline): + """Spline36 resizer.""" + + _static_kernel_radius = 3 + + _static_coeffs = [ + 1.1818181818181834, -2.1674641148325353, -0.014354066985642788, 1.0, + -0.5454545454545451, 2.928229665071767, -4.9665071770334865, 2.583732057416266, + 0.09090909090909075, -0.760765550239233, 2.0765550239234405, -1.837320574162675 + ] + + +class Spline64(NaturalSpline): + """Spline64 resizer.""" - scale_function = resample_function = core.lazy.resize.Spline64 - descale_function = core.lazy.descale.Despline64 _static_kernel_radius = 4 + + _static_coeffs = [ + 1.195121951219515, -2.1940913775334927, -0.0010305736860232173, 0.9999999999999929, + -0.5853658536585364, 3.141188594984538, -5.326004809343863, 2.7701820680178617, + 0.1463414634146341, -1.2243215389900368, 3.3411198900721373, -2.9556853315011997, + -0.02439024390243902, 0.27722432153898985, -1.0381312263826854, 1.277911370663001 + ] diff --git a/vskernels/kernels/various.py b/vskernels/kernels/various.py index 2aa434f..2a60c60 100644 --- a/vskernels/kernels/various.py +++ b/vskernels/kernels/various.py @@ -1,163 +1,215 @@ from __future__ import annotations -from math import log, sqrt +from math import cos, exp, log, pi, sqrt from typing import Any -from vstools import CustomValueError, to_singleton +from vstools import inject_self -from .fmtconv import FmtConv -from .placebo import Placebo +from .complex import CustomComplexKernel, CustomComplexTapsKernel +from .helpers import sinc __all__ = [ + 'Point', + 'Bilinear', + 'Lanczos', + 'Gaussian', 'Box', 'BlackMan', 'BlackManMinLobe', 'Sinc', - 'Gaussian', - 'EwaBicubic', - 'EwaJinc', - 'EwaLanczos', - 'EwaGinseng', - 'EwaHann', - 'EwaHannSoft', - 'EwaRobidoux', - 'EwaRobidouxSharp', + 'Hann', + 'Hamming', + 'Welch', + 'Bohman', + 'Cosine', ] -class Box(FmtConv): - """fmtconv's box resizer.""" +class gauss_sigma(float): + def from_fmtc(self, curve: float) -> float: + if not curve: + return 0.0 + return sqrt(1.0 / (2.0 * (curve / 10.0) * log(2))) + + def to_fmtc(self, sigma: float) -> float: + if not sigma: + return 0.0 + return 10 / (2 * log(2) * (sigma ** 2)) + + def from_libplacebo(self, sigma: float) -> float: + if not sigma: + return 0.0 + return sqrt(sigma / 4) + + def to_libplacebo(self, sigma: float) -> float: + if not sigma: + return 0.0 + return 4 * (sigma ** 2) - _kernel = 'box' +class Point(CustomComplexKernel): + """Point resizer.""" -class BlackMan(FmtConv): - """fmtconv's blackman resizer.""" + _static_kernel_radius = 1 - _kernel = 'blackman' + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + return 1.0 -class BlackManMinLobe(FmtConv): - """fmtconv's blackmanminlobe resizer.""" +class Bilinear(CustomComplexKernel): + """Bilinear resizer.""" - _kernel = 'blackmanminlobe' + _static_kernel_radius = 1 + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + return max(1.0 - abs(x), 0.0) -class Sinc(FmtConv): - """fmtconv's sinc resizer.""" - _kernel = 'sinc' +class Lanczos(CustomComplexTapsKernel): + """ + Lanczos resizer. + :param taps: taps param for lanczos kernel + """ -class Gaussian(FmtConv): - """fmtconv's gaussian resizer.""" + def __init__(self, taps: int = 3, **kwargs: Any) -> None: + super().__init__(taps, **kwargs) - _kernel = 'gaussian' + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + x, taps = abs(x), self.kernel_radius + + return sinc(x) * sinc(x / taps) if x < taps else 0.0 + + +class Gaussian(CustomComplexTapsKernel): + """Gaussian resizer.""" def __init__(self, sigma: float = 0.5, taps: int = 2, **kwargs: Any) -> None: - """ - Sigma is imagemagick's sigma scaling. - This will internally be scaled to fmtc's curve. + """Sigma is the same as imagemagick's sigma scaling.""" + + self._sigma = sigma + + super().__init__(taps, **kwargs) + + @inject_self.property + def sigma(self) -> gauss_sigma: + return gauss_sigma(self._sigma) + + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + return 1 / (self._sigma * sqrt(2 * pi)) * exp(-x ** 2 / (2 * self._sigma ** 2)) + + +class Box(CustomComplexKernel): + """Box resizer.""" + + _static_kernel_radius = 1 + + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + return 1.0 if x >= -0.5 and x < 0.5 else 0.0 + + +class BlackMan(CustomComplexTapsKernel): + """Blackman resizer.""" - You can specify "curve" to override sigma and specify the original `a1` value. - """ - if 'curve' in kwargs: - a1 = kwargs.pop('curve') + def __init__(self, taps: int = 4, **kwargs: Any) -> None: + super().__init__(taps, **kwargs) - if a1 is not None: - if a1 < 1.0 or a1 > 100.0: - raise CustomValueError("curve must be in range 1-100! (inclusive)") - else: - a1 = self.sigma.to_fmtc(sigma) # type: ignore + def _win_coef(self, x: float) -> float: + w_x = x * (pi / self.kernel_radius) - low, up = self.sigma.from_fmtc(100), self.sigma.from_fmtc(1) # type: ignore + return 0.42 + 0.50 * cos(w_x) + 0.08 * cos(w_x * 2) - if a1 < 1.0 or a1 > 100.0: - raise CustomValueError(f"sigma must be in range {low:.4f}-{up:.4f}! (inclusive)") + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if x >= self.kernel_radius: + return 0.0 - super().__init__(taps, a1=a1, **kwargs) + return sinc(x) * self._win_coef(x) - @to_singleton - class sigma: - def from_fmtc(self, curve: float) -> float: - if not curve: - return 0.0 - return sqrt(1.0 / (2.0 * (curve / 10.0) * log(2))) - def to_fmtc(self, sigma: float) -> float: - if not sigma: - return 0.0 - return 10 / (2 * log(2) * (sigma ** 2)) +class BlackManMinLobe(BlackMan): + """Blackmanminlobe resizer.""" - def from_libplacebo(self, sigma: float) -> float: - if not sigma: - return 0.0 - return sqrt(sigma / 4) + def _win_coef(self, x: float) -> float: + w_x = x * (pi / self.kernel_radius) - def to_libplacebo(self, sigma: float) -> float: - if not sigma: - return 0.0 - return 4 * (sigma ** 2) + return 0.355768 + 0.487396 * cos(w_x) + 0.144232 * cos(w_x * 2) + 0.012604 * cos(w_x * 3) -class EwaBicubic(Placebo): - _kernel = 'ewa_robidoux' +class Sinc(CustomComplexTapsKernel): + """Sinc resizer.""" - def __init__(self, b: float = 0.0, c: float = 0.5, radius: int | None = None, **kwargs: Any) -> None: - radius = kwargs.pop('taps', radius) + def __init__(self, taps: int = 4, **kwargs: Any) -> None: + super().__init__(taps, **kwargs) - if radius is None: - from .bicubic import Bicubic + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if x >= self.kernel_radius: + return 0.0 - radius = Bicubic(b, c).kernel_radius + return sinc(x) - super().__init__(radius, b, c, **kwargs) +class Hann(CustomComplexTapsKernel): + """Hann kernel.""" -class EwaLanczos(Placebo): - _kernel = 'ewa_lanczos' + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if x >= self.kernel_radius: + return 0.0 - def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: - super().__init__(taps, None, None, **kwargs) + return 0.5 + 0.5 * cos(pi * x) -class EwaJinc(Placebo): - _kernel = 'ewa_jinc' +class Hamming(CustomComplexTapsKernel): + """Hamming kernel.""" - def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: - super().__init__(taps, None, None, **kwargs) + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if x >= self.kernel_radius: + return 0.0 + return 0.54 + 0.46 * cos(pi * x) -class EwaGinseng(Placebo): - _kernel = 'ewa_ginseng' - def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: - super().__init__(taps, None, None, **kwargs) +class Welch(CustomComplexTapsKernel): + """Welch kernel.""" + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if abs(x) >= 1.0: + return 0.0 -class EwaHann(Placebo): - _kernel = 'ewa_hann' + return 1.0 - x * x - def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: - super().__init__(taps, None, None, **kwargs) +class Cosine(CustomComplexTapsKernel): + """Cosine kernel.""" -class EwaHannSoft(Placebo): - _kernel = 'haasnsoft' + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if x >= self.kernel_radius: + return 0.0 - def __init__(self, taps: float = 3.2383154841662362076499, **kwargs: Any) -> None: - super().__init__(taps, None, None, **kwargs) + cosine = cos(pi * x) + return 0.34 + cosine * (0.5 + cosine * 0.16) -class EwaRobidoux(Placebo): - _kernel = 'ewa_robidoux' - def __init__(self, **kwargs: Any) -> None: - super().__init__(None, None, None, **kwargs) +class Bohman(CustomComplexTapsKernel): + """Bohman kernel.""" + @inject_self.cached + def kernel(self, *, x: float) -> float: # type: ignore + if x >= self.kernel_radius: + return 0.0 -class EwaRobidouxSharp(Placebo): - _kernel = 'ewa_robidouxsharp' + cosine = cos(pi * x) + sine = sqrt(1.0 - cosine * cosine) - def __init__(self, **kwargs: Any) -> None: - super().__init__(None, None, None, **kwargs) + return (1.0 - x) * cosine + (1.0 / pi) * sine diff --git a/vskernels/kernels/zimg.py b/vskernels/kernels/zimg.py deleted file mode 100644 index 4d21a4d..0000000 --- a/vskernels/kernels/zimg.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from stgpytools import inject_kwargs_params -from vstools import inject_self, vs - -from ..types import Center, LeftShift, Slope, TopShift -from .abstract import Descaler -from .complex import BorderHandling, ComplexKernel - -__all__ = [ - 'ZimgDescaler', - 'ZimgComplexKernel' -] - - -class ZimgDescaler(Descaler): - if TYPE_CHECKING: - @inject_self.cached - @inject_kwargs_params - def descale( # type: ignore[override] - self, clip: vs.VideoNode, width: int | None, height: int | None, shift: tuple[TopShift, LeftShift] = (0, 0), - *, blur: float = 1.0, border_handling: BorderHandling = BorderHandling.MIRROR, **kwargs: Any - ) -> vs.VideoNode: - ... - - -class ZimgComplexKernel(ComplexKernel, ZimgDescaler): # type: ignore - if TYPE_CHECKING: - @inject_self.cached - @inject_kwargs_params - def descale( # type: ignore[override] - self, clip: vs.VideoNode, width: int | None, height: int | None, shift: tuple[TopShift, LeftShift] = (0, 0), - *, blur: float = 1.0, border_handling: BorderHandling, ignore_mask: vs.VideoNode | None = None, - linear: bool = False, sigmoid: bool | tuple[Slope, Center] = False, **kwargs: Any - ) -> vs.VideoNode: - ... - - def get_params_args( - self, is_descale: bool, clip: vs.VideoNode, width: int | None = None, height: int | None = None, **kwargs: Any - ) -> dict[str, Any]: - args = super().get_params_args(is_descale, clip, width, height, **kwargs) - - if not is_descale: - for key in ('blur', 'border_handling', 'ignore_mask', 'force', 'force_h', 'force_v'): - args.pop(key, None) - - return args diff --git a/vskernels/types.py b/vskernels/types.py index e513d63..841a06c 100644 --- a/vskernels/types.py +++ b/vskernels/types.py @@ -1,16 +1,83 @@ from __future__ import annotations -from typing import TypeAlias +from functools import lru_cache +from typing import Any, TypeAlias + +from vstools import CustomIntEnum, KwargsT, padder, vs __all__ = [ - 'TopShift', - 'LeftShift', - 'TopFieldTopShift', - 'TopFieldLeftShift', - 'BotFieldTopShift', - 'BotFieldLeftShift', + 'BorderHandling', 'SampleGridModel' ] + +class BorderHandling(CustomIntEnum): + MIRROR = 0 + ZERO = 1 + REPEAT = 2 + + def prepare_clip(self, clip: vs.VideoNode, min_pad: int = 2) -> vs.VideoNode: + pad_w, pad_h = ( + self.pad_amount(size, min_pad) for size in (clip.width, clip.height) + ) + + if pad_w == pad_h == 0: + return clip + + args = (clip, pad_w, pad_w, pad_h, pad_h) + + match self: + case BorderHandling.MIRROR: + return padder.MIRROR(*args) + case BorderHandling.ZERO: + return padder.COLOR(*args) + case BorderHandling.REPEAT: + return padder.REPEAT(*args) + + @lru_cache + def pad_amount(self, size: int, min_amount: int = 2) -> int: + if self is BorderHandling.MIRROR: + return 0 + + return (((size + min_amount) + 7) & -8) - size + + +class SampleGridModel(CustomIntEnum): + MATCH_EDGES = 0 + MATCH_CENTERS = 1 + + def __call__( + self, width: int, height: int, src_width: int, src_height: int, shift: tuple[float, float] + ) -> tuple[KwargsT, tuple[float, float]]: + kwargs = KwargsT() + + if self is SampleGridModel.MATCH_CENTERS: + src_width = src_width * (width - 1) / (src_width - 1) + src_height = src_height * (height - 1) / (src_height - 1) + + kwargs |= dict(src_width=src_width, src_height=src_height) + shift = tuple[float, float]( + (x / 2 + y for x, y in zip(((height - src_height), (width - src_width)), shift)) + ) + + return kwargs, shift + + def for_scale( + self, clip: vs.VideoNode, width: int, height: int, shift: tuple[float, float], **kwargs: Any + ) -> tuple[KwargsT, tuple[float, float]]: + src_width = kwargs.pop('src_width', width) + src_height = kwargs.pop('src_height', height) + + return self(src_width, src_height, width, height, shift) + + def for_descale( + self, clip: vs.VideoNode, width: int, height: int, shift: tuple[float, float], **kwargs: Any + ) -> tuple[KwargsT, tuple[float, float]]: + src_width = kwargs.pop('src_width', clip.width) + src_height = kwargs.pop('src_height', clip.height) + + return self(width, height, src_width, src_height, shift) + + TopShift: TypeAlias = float LeftShift: TypeAlias = float TopFieldTopShift: TypeAlias = float diff --git a/vskernels/util.py b/vskernels/util.py index 4428b86..e09d9be 100644 --- a/vskernels/util.py +++ b/vskernels/util.py @@ -11,8 +11,8 @@ ) from .kernels import ( - Bicubic, BicubicAuto, Catrom, ComplexKernel, Descaler, FmtConv, Impulse, Kernel, KernelT, LinearDescaler, Placebo, - Point, Resampler, ResamplerT, Scaler, ZimgComplexKernel, ZimgDescaler + Bicubic, BicubicAuto, Catrom, ComplexKernel, CustomComplexKernel, Descaler, Kernel, KernelT, LinearDescaler, + Placebo, Point, Resampler, ResamplerT, Scaler ) from .types import Center, LeftShift, Slope, TopShift @@ -110,8 +110,7 @@ class inner_no_scale(kernel_t, NoScaleBase): # type: ignore abstract_kernels = list[type[Scaler | Descaler | Resampler | Kernel]]([ - Kernel, FmtConv, Impulse, Placebo, ComplexKernel, - ZimgDescaler, ZimgComplexKernel, LinearDescaler + Kernel, Placebo, ComplexKernel, CustomComplexKernel, LinearDescaler ])