def DebandReader(clip, csvfile, range=30, delimiter=' ', mask=None, luma_scaling=15): """ DebandReader, read a csv file to apply a f3kdb filter for given strengths and frames. From awsmfunc. > Usage: DebandReader(clip, csvfile, grain, range) * csvfile is the path to a csv file containing in each row: <startframe> <endframe> <<strength_y>,**<strength_b>,**<strength_r>> <grain strength> <mask> * mask is the mask list you want to apply. it should be in a list * range is passed as range in the f3kdb filter """ import csv filtered = clip if get_depth(clip) <= 16 else Depth(clip, 16) depth = get_depth(clip) with open(csvfile) as debandcsv: csvzones = csv.reader(debandcsv, delimiter=delimiter) for row in csvzones: clip_mask = int(row[4]) strength = row[2].split(',') while len(strength) < 3: strength.append(strength[-1]) grain_strength = float(row[3]) db = core.f3kdb.Deband(clip, y=strength[0], cb=strength[1], cr=strength[2], grainy=0, grainc=0, range=range, output_depth=depth) db = agm.adptvgrnMod(db, luma_scaling=luma_scaling, strength=grain_strength) filtered = awf.ReplaceFrames(filtered, db, mappings="[" + row[0] + " " + row[1] + "]") if mask: filtered = core.std.MaskedMerge(filtered, clip, mask[clip_mask]) return filtered
def lfdeband(clip: vs.VideoNode) -> vs.VideoNode: """A simple debander ported from AviSynth by Zastin from debandshit Args: clip (vs.VideoNode): Source clip Returns: vs.VideoNode: Debanded clip. """ if clip.format is None: raise ValueError("lfdeband: 'Variable-format clips not supported'") bits = get_depth(clip) wss, hss = 1 << clip.format.subsampling_w, 1 << clip.format.subsampling_h w, h = clip.width, clip.height dw, dh = round(w / 2), round(h / 2) clip = depth(clip, 16) dsc = core.resize.Spline64(clip, dw - dw % wss, dh - dh % hss) d3kdb = dumb3kdb(dsc, radius=30, threshold=80, grain=0) ddif = core.std.MakeDiff(d3kdb, dsc) dif = core.resize.Spline64(ddif, w, h) out = core.std.MergeDiff(clip, dif) return depth(out, bits)
def scaled_grain(clip: vs.VideoNode, var: float = 0.25, uvar: float = 0, grain_h: Optional[int] = None, grain_w: Optional[int] = None, static: bool = True, adaptive: bool = False, luma_scaling: int = 12, kernel: str = 'bicubic', b: float = 0, c: float = 1 / 2, taps: int = 3) -> vs.VideoNode: """Grains a clip in the given dimensions and merges it with the source clip. This is useful for making larger grain patterns when using a grain resolution smaller than the source clip's resolution. It supports static and dynamic grain with optional adaptive brightness masking to grain darker areas more than brighter areas. Args: clip: The source clip. Assumes YUV format. var: Luma grain variance (strength). uvar: Chroma grain variance (strength). grain_h: Height of the grained clip. grain_w: Width of the grained clip. static: Determines whether static (constant) or dynamic grain is used. adaptive: Determines whether adaptive brightness masking is used. luma_scaling: The scaling factor for adaptive brightness masking. Lower values increase the graining of brighter areas. kernel: The scaling kernel used to scale the grain clip to the source clip's dimensions. b, c, taps: Parameters for tweaking the kernel. """ grain_h = fallback(grain_h, clip.height / 2) grain_w = fallback(grain_w, get_w(grain_h, clip.width / clip.height)) blank_value = (1 << get_depth(clip)) / 2 if is_integer(clip) else 0 blank_clip = core.std.BlankClip( clip, width=grain_w, height=grain_h, color=[blank_value, blank_value, blank_value]) grained = core.grain.Add(blank_clip, var=var, uvar=uvar, constant=static) grained = fvf.Resize(grained, clip.width, clip.height, kernel=kernel, a1=b, a2=c, taps=taps) if adaptive: src_res_blank = core.resize.Point(blank_clip, clip.width, clip.height) adaptive_mask = core.adg.Mask(core.std.PlaneStats(clip), luma_scaling) grained = core.std.MaskedMerge(src_res_blank, grained, adaptive_mask) merged = core.std.MergeDiff(clip, grained) return clamp(merged)
def luma_mask(clip: vs.VideoNode, thr_lo: float, thr_hi: float, invert: bool = True) -> vs.VideoNode: """Mask each pixel according to its luma value. From debandshit. Args: clip (vs.VideoNode): Source clip. thr_lo (float): All pixels below this threshold will be binary thr_hi (float): All pixels above this threshold will be binary All pixels in-between will be scaled from black to white invert (bool, optional): When true, masks dark areas (pixels below lo will be white, and vice versa). Defaults to True. Returns: vs.VideoNode: Luma mask. """ bits = get_depth(clip) is_float = get_sample_type(clip) == vs.FLOAT peak = 1.0 if is_float else (1 << bits) - 1 mask = pick_px_op( is_float, (f'x {thr_lo} < 0 x {thr_hi} > {peak} x {thr_lo} - {thr_lo} {thr_hi} - / {peak} * ? ?', lambda x: round(0 if x < thr_lo else peak if x > thr_hi else (x - thr_lo) / (thr_hi - thr_lo) * peak)))(get_y(clip)) return mask.std.Invert() if invert else mask
def _get_bits(clip: vs.VideoNode, expected_depth: int = 16) -> Tuple[int, vs.VideoNode]: from vsutil import get_depth bits = get_depth(clip) return bits, depth(clip, expected_depth) if bits != expected_depth else clip
def Deband(clip: vs.VideoNode, radius: int = 17, threshold: float = 4, iterations: int = 1, grain: float = 4, chroma: bool = True) -> vs.VideoNode: """Wrapper for placebo.Deband because at the moment, processing one plane is faster. Args: clip (vs.VideoNode): radius (int, optional): Defaults to 17. threshold (float, optional): Defaults to 4. iterations (int, optional): Defaults to 1. grain (float, optional): Defaults to 4. chroma (bool, optional): Defaults to True. Returns: vs.VideoNode """ if get_depth(clip) != 16: clip = depth(clip, 16) if chroma is True: clip = join([ core.placebo.Deband(x, 1, iterations, threshold, radius, grain) for x in split(clip) ]) else: clip = core.placebo.Deband(clip, 1, iterations, threshold, radius, grain) return clip
def _get_bits(clip: vs.VideoNode, expected_depth: int = 16) -> Tuple[int, vs.VideoNode]: """Checks bitdepth, set bitdepth if necessary, and sends original clip's bitdepth back with the clip""" from vsutil import depth, get_depth bits = get_depth(clip) return bits, depth(clip, expected_depth) if bits != expected_depth else clip
def test_descale(clip: vs.VideoNode, height: int, kernel: str = 'bicubic', b: Union[float, Fraction] = Fraction(1, 3), c: Union[float, Fraction] = Fraction(1, 3), taps: int = 3, show_error: bool = True) -> vs.VideoNode: """ Generic function to test descales with; descales and reupscales a given clip, allowing you to compare the two easily. When comparing, it is recommended to do atleast a 4x zoom using Nearest Neighbor. I also suggest using 'compare' (py:func:lvsfunc.comparison.compare), as that will make comparing the output with the source clip a lot easier. Some of this code was leveraged from DescaleAA found in fvsfunc. Dependencies: vapoursynth-descale :param clip: Input clip :param height: Target descaled height. :param kernel: Kernel used to descale (see :py:func:`lvsfunc.util.get_scale_filter`) :param b: B-param for bicubic kernel (Default: 1 / 3) :param c: C-param for bicubic kernel (Default: 1 / 3) :param taps: Taps param for lanczos kernel (Default: 3) :param show_error: Show diff between the original clip and the reupscaled clip (Default: True) :return: A clip re-upscaled with the same kernel """ try: from descale import get_filter except ModuleNotFoundError: raise ModuleNotFoundError("test_descale: missing dependency 'descale'") b = float(b) c = float(c) if get_depth(clip) != 32: clip = util.resampler(clip, 32) clip_y = get_y(clip) desc = get_filter(b, c, taps, kernel)(clip_y, get_w(height, clip.width / clip.height), height) upsc = util.get_scale_filter(kernel, b=b, c=c, taps=taps)(desc, clip.width, clip.height) upsc = core.std.PlaneStats(clip_y, upsc) if clip is vs.GRAY: return core.text.FrameProps(upsc, "PlaneStatsDiff") if show_error else upsc merge = core.std.ShufflePlanes([upsc, clip], [0, 1, 2], vs.YUV) return core.text.FrameProps(merge, "PlaneStatsDiff") if show_error else merge
def linemask(clip: vs.VideoNode, strength: int = 200, protection: int = 2, luma_cap: int = 224, threshold: float = 3) -> Tuple[vs.VideoNode, vs.VideoNode]: """ Lineart mask from havsfunc.FastLineDarkenMod, using the very same syntax. Furthermore, it checks the overall planestatsaverage of the frame to determine if it's a super grainy scene or not. """ import math from functools import partial from typing import List from lvsfunc.misc import get_prop from vsutil import depth, get_depth, get_y def _reduce_grain(n: int, f: vs.VideoFrame, clips: List[vs.VideoNode]) -> vs.VideoNode: return clips[1] if get_prop(f, 'PlaneStatsAverage', float) > 0.032 else clips[0] def _cround(x: float) -> float: return math.floor(x + 0.5) if x > 0 else math.ceil(x - 0.5) assert clip.format clip_y = get_y(depth(clip, 8)) bits = clip.format.bits_per_sample peak = (1 << get_depth(clip_y)) - 1 strngth = strength / 128 lum = _cround(luma_cap * peak / 255) if peak != 1 else luma_cap / 255 thr = _cround(threshold * peak / 255) if peak != 1 else threshold / 255 maxed = clip_y.std.Maximum(threshold=peak / (protection + 1)).std.Minimum() dark = core.std.Expr( [clip_y, maxed], expr= f'y {lum} < y {lum} ? x {thr} + > x y {lum} < y {lum} ? - 0 ? {strngth} * x +' ) extr = core.std.Lut2(clip_y, dark, function=lambda x, y: 255 if abs(x - y) else 0) dedot = extr.rgvs.RemoveGrain(6) blur = dedot.std.Convolution(matrix=[1, 2, 1, 2, 0, 2, 1, 2, 1]) degrain = core.std.FrameEval(dedot, partial(_reduce_grain, clips=[dedot, blur]), dedot.std.PlaneStats()) return dark, depth(degrain, bits)
def rescale(self, clip: vs.VideoNode, height: int = 720, kernel: lvsfunc.kernels.Kernel = lvsfunc.kernels.Catrom(), thr: Union[int, float] = 55, expand: int = 2) -> vs.VideoNode: """Makes a mask based on rescaled difference. Modified version of Atomchtools. Args: clip (vs.VideoNode): Source clip. Can be Gray, YUV or RGB. Keep in mind that descale plugin will descale all planes after conversion to GRAYS, YUV444PS and RGBS respectively. height (int, optional): Height to descale to. Defaults to 720. kernel (lvsfunc.kernels.Kernel, optional): Kernel used to descale. Defaults to lvsfunc.kernels.Bicubic(b=0, c=0.5). thr (Union[int, float], optional): Binarization threshold. Defaults to 55. expand (int, optional): Growing/shrinking shape. 0 is allowed. Defaults to 2. Returns: vs.VideoNode: Rescaled mask. """ if clip.format is None: raise FormatError('diff_rescale_mask: Variable format not allowed!') bits = get_depth(clip) gray_only = clip.format.num_planes == 1 thr = scale_value(thr, bits, 32, scale_offsets=True) pre = core.resize.Bicubic( clip, format=clip.format.replace( bits_per_sample=32, sample_type=vs.FLOAT, subsampling_w=0, subsampling_h=0 ).id ) descale = kernel.descale(pre, get_w(height), height) rescale = kernel.scale(descale, clip.width, clip.height) diff = core.std.Expr(split(pre) + split(rescale), mae_expr(gray_only)) mask = iterate(diff, lambda x: core.rgsf.RemoveGrain(x, 2), 2) mask = core.std.Expr(mask, f'x 2 4 pow * {thr} < 0 1 ?') mask = self._minmax(mask, 2 + expand, True) mask = mask.std.Deflate() return mask.resize.Point( format=clip.format.replace(color_family=vs.GRAY, subsampling_w=0, subsampling_h=0).id, dither_type='none' )
def fix_cr_tint(clip: vs.VideoNode, value: int = 128) -> vs.VideoNode: """ Tries to forcibly fix Crunchyroll's green tint by adding pixel values. :param clip: Input clip :param value: Value added to every pixel (Default: 128) :return: Clip with CR tint fixed """ if get_depth(clip) != 16: clip = depth(clip, 16) return core.std.Expr(clip, f'x {value} +')
def clamp_integer(c): depth = get_depth(c) depth_max = (1 << depth) - 1 is_limited = is_limited_range(clip) max_luma = 235 * (1 << (depth - 8)) max_chroma = 240 * (1 << (depth - 8)) minimum = 16 * (1 << (depth - 8)) if is_limited else 0 maximum = [max_luma, max_chroma] if is_limited else depth_max return core.std.Limiter(c, min=minimum, max=maximum)
def chroma_reconstruct(clip: vs.VideoNode, radius: int = 2, i444: bool = False) -> vs.VideoNode: """ A function to demangle messed-up chroma, like for example chroma that was downscaled using Nearest Neighbour, or the chroma found on DVDs. This function should be used with care, and not blindly applied to anything. This function can also return a 4:4:4 clip. This is not recommended except for very specific cases, like for example where you're dealing with a razor-sharp 1080p source with a lot of bright colours. Otherwise, have it return the 4:2:0 clip instead. Original function by shane, modified by Ichunjo and LightArrowsEXE. Aliases for this function are `lvsfunc.demangle` and `lvsfunc.crecon`. :param clip: Input clip :param radius: Boxblur radius :param i444: Return a 4:4:4 clip :return: Clip with demangled chroma in either 4:2:0 or 4:4:4 """ if clip.format is None: raise ValueError("recon: 'Variable-format clips not supported'") def dmgl(clip: vs.VideoNode) -> vs.VideoNode: return core.resize.Bicubic(clip, w, h, src_left=0.25) w, h = clip.width, clip.height clipb = depth(clip, 32) planes = split(clipb) clip_y = planes[0] planes[0] = planes[0].resize.Bicubic(planes[1].width, planes[1].height, src_left=-.5, filter_param_a=1 / 3, filter_param_b=1 / 3) planes[0], planes[1], planes[2] = map(dmgl, (planes[0], planes[1], planes[2])) y_fix = core.std.MakeDiff(clip_y, planes[0]) yu, yv = _Regress(planes[0], planes[1], planes[2], radius=radius) u_fix = _ReconstructMulti(y_fix, yu, radius=radius) planes[1] = core.std.MergeDiff(planes[1], u_fix) v_fix = _ReconstructMulti(y_fix, yv, radius=radius) planes[2] = core.std.MergeDiff(planes[2], v_fix) merged = join([clip_y, planes[1], planes[2]]) return core.resize.Bicubic(merged, format=clip.format.id) if not i444 \ else depth(merged, get_depth(clip))
def test_descale(clip: vs.VideoNode, width: Optional[int] = None, height: int = 720, kernel: kernels.Kernel = kernels.Bicubic(b=0, c=1 / 2), show_error: bool = True) -> Tuple[vs.VideoNode, ScaleAttempt]: """ Generic function to test descales with; descales and reupscales a given clip, allowing you to compare the two easily. Also returns a :py:class:`lvsfunc.scale.ScaleAttempt` with additional information. When comparing, it is recommended to do atleast a 4x zoom using Nearest Neighbor. I also suggest using 'compare' (:py:func:`lvsfunc.comparison.compare`), as that will make comparing the output with the source clip a lot easier. Some of this code was leveraged from DescaleAA found in fvsfunc. Dependencies: * vapoursynth-descale :param clip: Input clip :param width: Target descale width. If None, determine from `height` :param height: Target descale height (Default: 720) :param kernel: Kernel used to descale (see :py:class:`lvsfunc.kernels.Kernel`, Default: kernels.Bicubic(b=0, c=1/2)) :param show_error: Render PlaneStatsDiff on the reupscaled frame (Default: True) :return: A tuple containing a clip re-upscaled with the same kernel and a ScaleAttempt tuple. """ if clip.format is None: raise ValueError("test_descale: 'Variable-format clips not supported'") width = width or get_w(height, clip.width / clip.height) clip_y = depth(get_y(clip), 32) descale = _perform_descale(Resolution(width, height), clip_y, kernel) rescaled = depth(core.std.PlaneStats(descale.rescaled, clip_y), get_depth(clip)) if clip.format.num_planes == 1: rescaled = core.text.FrameProps( rescaled, "PlaneStatsDiff") if show_error else rescaled else: merge = core.std.ShufflePlanes([rescaled, clip], [0, 1, 2], vs.YUV) rescaled = core.text.FrameProps( merge, "PlaneStatsDiff") if show_error else merge return rescaled, descale
def _retinex_edgemask(src: vs.VideoNode, sigma: int = 1) -> vs.VideoNode: """ Use retinex to greatly improve the accuracy of the edge detection in dark scenes. sigma is the sigma of tcanny From kagefunc.py, moved here so I don't need to import it. """ luma = get_y(src) max_value = 1 if src.format.sample_type == vs.FLOAT else ( 1 << get_depth(src)) - 1 ret = core.retinex.MSRCP(luma, sigma=[50, 200, 350], upper_thr=0.005) tcanny = ret.tcanny.TCanny( mode=1, sigma=sigma).std.Minimum(coordinates=[1, 0, 1, 0, 0, 1, 0, 1]) return core.std.Expr([_kirsch(luma), tcanny], f"x y + {max_value} min")
def get_mask(self, clip: vs.VideoNode, lthr: float = 0.0, hthr: Optional[float] = None, multi: float = 1.0) -> vs.VideoNode: """Makes edge mask based on convolution kernel. The resulting mask can be thresholded with lthr, hthr and multiplied with multi. Args: clip (vs.VideoNode): Source clip. lthr (float, optional): Low threshold. Anything below lthr will be set to 0. Defaults to 0. hthr (Optional[float], optional): High threshold. Anything above hthr will be set to the range max. Defaults to None. multi (float, optional): Multiply all pixels by this before thresholding. Defaults to 1.0. Returns: vs.VideoNode: Mask clip. """ assert clip.format is not None bits = get_depth(clip) is_float = get_sample_type(clip) == vs.FLOAT peak = 1.0 if is_float else (1 << bits) - 1 hthr = peak if hthr is None else hthr clip_p = self._preprocess(clip) mask = self._compute_mask(clip_p) mask = depth(mask, bits, range=Range.FULL, range_in=Range.FULL) if multi != 1: mask = pick_px_op( is_float, (f'x {multi} *', lambda x: round(max(min(x * multi, peak), 0))) )(mask) if lthr > 0 or hthr < peak: mask = pick_px_op( is_float, (f'x {hthr} > {peak} x {lthr} <= 0 x ? ?', lambda x: peak if x > hthr else 0 if x <= lthr else x) )(mask) return mask
def quick_import(file: str, force_lsmas=False, resample=True): """ A function to quickly import and resample a file. If the file does not have a chroma subsampling of 4:2:0, it'll automatically be converted to such. """ src = lvf.src(file, force_lsmas=force_lsmas) depth = vsutil.get_depth(src) if vsutil.get_subsampling != '420': src = multi_resample(src, depth=depth) if resample: return fvf.Depth(src, 16) else: return src
def shader(clip: vs.VideoNode, width: int, height: int, shader_file: str, luma_only: bool = True, **kwargs) -> vs.VideoNode: """Wrapper for placebo.Resample https://github.com/Lypheo/vs-placebo#vs-placebo Args: clip (vs.VideoNode): Source clip/ width (int): Destination width. height (int): Destination height. shader_file (str): Path to shader file used into placebo.Shader. luma_only (bool, optional): If process the luma only. Defaults to True. Returns: vs.VideoNode: Shader'd clip. """ if get_depth(clip) != 16: clip = depth(clip, 16) if luma_only is True: filter_shader = 'box' if clip.format.num_planes == 1: if width > clip.width or height > clip.height: clip = clip.resize.Point(format=vs.YUV444P16) else: blank = core.std.BlankClip(clip, clip.width / 4, clip.height / 4, vs.GRAY16) clip = join([clip, blank, blank]) else: filter_shader = 'ewa_lanczos' clip = core.placebo.Shader(clip, shader_file, width, height, filter=filter_shader, **kwargs) return get_y(clip) if luma_only is True else clip
def rescaler(clip: vs.VideoNode, height: int, shader_file: Optional[str] = None, **kwargs: Any ) -> Tuple[vs.VideoNode, vs.VideoNode]: """ Multi-descaling + reupscaling function. Compares multiple descales and takes darkest/brightest pixels from clips as necessary """ import lvsfunc as lvf import muvsfunc as muf from vardefunc.mask import FDOG from vardefunc.scale import fsrcnnx_upscale, nnedi3_upscale bits = get_depth(clip) clip = depth(clip, 32) clip_y = get_y(clip) scalers: List[Callable[[vs.VideoNode, int, int], vs.VideoNode]] = [ lvf.kernels.Spline36().descale, lvf.kernels.Catrom().descale, lvf.kernels.BicubicSharp().descale, lvf.kernels.Catrom().scale ] descale_clips = [scaler(clip_y, get_w(height), height) for scaler in scalers] descale_clip = core.std.Expr(descale_clips, 'x y z a min max min y z a max min max z a min max') if shader_file: rescale = fsrcnnx_upscale(descale_clip, shader_file=shader_file, downscaler=None) else: rescale = nnedi3_upscale(descale_clip) rescale = muf.SSIM_downsample(rescale, clip.width, clip.height, smooth=((3 ** 2 - 1) / 12) ** 0.5, sigmoid=True, filter_param_a=0, filter_param_b=0) l_mask = FDOG().get_mask(clip_y, lthr=0.065, hthr=0.065).std.Maximum().std.Minimum() l_mask = l_mask.std.Median().std.Convolution([1] * 9) # stolen from varde xd masked_rescale = core.std.MaskedMerge(clip_y, rescale, l_mask) scaled = join([masked_rescale, plane(clip, 1), plane(clip, 2)]) upscale = lvf.kernels.Spline36().scale(descale_clips[0], clip.width, clip.height) detail_mask = lvf.scale.descale_detail_mask(clip_y, upscale, threshold=0.04) scaled_down = scaled if bits == 32 else depth(scaled, bits) mask_down = detail_mask if bits == 32 else depth(detail_mask, 16, range_in=Range.FULL, range=Range.LIMITED) return scaled_down, mask_down
def get_neutral_value(clip: vs.VideoNode, chroma: bool = False) -> float: """ Taken from vsutil. This isn't in any new versions yet, so mypy complains. Will remove once vsutil does another version bump. Returns the neutral value for the combination of the plane type and bit depth/type of the clip as float. :param clip: Input clip. :param chroma: Whether to get luma or chroma plane value :return: Neutral value. """ check_variable(clip, "get_neutral_value") assert clip.format is_float = clip.format.sample_type == vs.FLOAT return (0. if chroma else 0.5) if is_float else float(1 << (get_depth(clip) - 1))
def IVTC(clip: vs.VideoNode) -> vs.VideoNode: """" Experimental script for inverse telecining and deinterlacing. Forked from <https://github.com/LightArrowsEXE/dotfiles/blob/master/mpv/.config/mpv/vs/ivtc.vpy> Requires VapourSynth <http://www.vapoursynth.com/doc/about.html> Additional dependencies: * vsutil <https://pypi.org/project/vsutil/> * TIVTC <https://github.com/dubhater/vapoursynth-tivtc> TIVTC's documentation <http://avisynth.nl/index.php/TIVTC/TFM> :param clip: Input clip :return: IVTC'd clip """ down = depth(clip, 8) tfm = core.tivtc.TFM(down) return depth(tfm, get_depth(clip))
def gamma2linear(clip: vs.VideoNode, curve: CURVES, gcor: float = 1.0, sigmoid: bool = False, thr: float = 0.5, cont: float = 6.5, epsilon: float = 1e-6) -> vs.VideoNode: check_variable(clip, "gamma2linear") assert clip.format if get_depth(clip) != 32 and clip.format.sample_type != vs.FLOAT: raise ValueError("gamma2linear: 'Only 32 bits float is allowed'") c = get_coefs(curve) expr = f'x {c.k0} <= x {c.phi} / x {c.alpha} + 1 {c.alpha} + / {c.gamma} pow ? {gcor} pow' if sigmoid: x0 = f'1 1 {cont} {thr} * exp + /' x1 = f'1 1 {cont} {thr} 1 - * exp + /' expr = f'{thr} 1 {expr} {x1} {x0} - * {x0} + {epsilon} max / 1 - {epsilon} max log {cont} / -' expr = f'{expr} 0.0 max 1.0 min' return core.std.Expr(clip, expr).std.SetFrameProps(_Transfer=8)
def linear2gamma(clip: vs.VideoNode, curve: CURVES, gcor: float = 1.0, sigmoid: bool = False, thr: float = 0.5, cont: float = 6.5, ) -> vs.VideoNode: check_variable(clip, "linear2gamma") assert clip.format if get_depth(clip) != 32 and clip.format.sample_type != vs.FLOAT: raise ValueError("linear2gamma: 'Only 32 bits float is allowed'") c = get_coefs(curve) expr = 'x' if sigmoid: x0 = f'1 1 {cont} {thr} * exp + /' x1 = f'1 1 {cont} {thr} 1 - * exp + /' expr = f'1 1 {cont} {thr} {expr} - * exp + / {x0} - {x1} {x0} - /' expr += f' {gcor} pow' expr = f'{expr} {c.k0} {c.phi} / <= {expr} {c.phi} * {expr} 1 {c.gamma} / pow {c.alpha} 1 + * {c.alpha} - ?' expr = f'{expr} 0.0 max 1.0 min' return core.std.Expr(clip, expr).std.SetFrameProps(_Transfer=curve)
def shift_tint(clip: vs.VideoNode, values: Union[int, Sequence[int]] = 16) -> vs.VideoNode: """ A function for forcibly adding pixel values to a clip. Can be used to fix green tints in Crunchyroll sources, for example. Only use this if you know what you're doing! This function accepts a single integer or a list of integers. Values passed should mimic those of an 8bit clip. If your clip is not 8bit, they will be scaled accordingly. If you only pass 1 value, it will copied to every plane. If you pass 2, the 2nd one will be copied over to the 3rd. Don't pass more than three. :param clip: Input clip :param values: Value added to every pixel, scales accordingly to your clip's depth (Default: 16) :return: Clip with pixel values added """ val: Tuple[float, float, float] if isinstance(values, int): val = (values, values, values) elif len(values) == 2: val = (values[0], values[1], values[1]) elif len(values) == 3: val = (values[0], values[1], values[2]) else: raise ValueError("shift_tint: 'Too many values supplied'") if any(v > 255 or v < -255 for v in val): raise ValueError("shift_tint: 'Every value in \"values\" must be below 255'") cdepth = get_depth(clip) cv: List[float] = [scale_value(v, 8, cdepth) for v in val] if cdepth != 8 else list(val) return core.std.Expr(clip, expr=[f'x {cv[0]} +', f'x {cv[1]} +', f'x {cv[2]} +'])
def shift_tint(clip: vs.VideoNode, values: Union[int, List[int]] = 16) -> vs.VideoNode: """ A function for forcibly adding pixel values to a clip. Can be used to fix green tints in CrunchyRoll sources, for example. Only use this if you know what you're doing! Values passed should mimic those of an 8bit clip. If your clip is not 8bit, they will be scaled accordingly. If you only pass 1 value, it will copied to every plane. If you pass 2, the 2nd one will be copied over to the 3rd. Alias for this function is `lvsfunc.misc.fix_cr_tint`. :param clip: Input clip :param value: Value added to every pixel, scales accordingly to your clip's depth (Default: 16) :return: Clip with pixel values added """ if isinstance(values, int): values = [values, values, values] elif len(values) == 2: values = [values[0], values[1], values[1]] if any(v > 255 or v < -255 for v in values): raise ValueError( "shift_tint: 'Every value in \"values\" must be below 255'") cdepth = get_depth(clip) if cdepth != 8: values: List[float, int] = [scale_value(v, 8, cdepth) for v in values] return core.std.Expr( clip, expr=[f'x {values[0]} +', f'x {values[1]} +', f'x {values[2]} +'])
def descale(clip: vs.VideoNode, upscaler: Optional[Callable[[vs.VideoNode, int, int], vs.VideoNode]] = reupscale, width: Union[int, List[int], None] = None, height: Union[int, List[int]] = 720, kernel: kernels.Kernel = kernels.Bicubic(b=0, c=1 / 2), threshold: float = 0.0, mask: Optional[Callable[[vs.VideoNode, vs.VideoNode], vs.VideoNode]] = descale_detail_mask, src_left: float = 0.0, src_top: float = 0.0, show_mask: bool = False) -> vs.VideoNode: """ A unified descaling function. Includes support for handling fractional resolutions (experimental), multiple resolutions, detail masking, and conditional scaling. If you want to descale to a fractional resolution, set src_left and src_top and round up the target height. If the source has multiple native resolutions, specify ``height`` as a list. If you want to conditionally descale, specify a non-zero threshold. Dependencies: vapoursynth-descale, znedi3 :param clip: Clip to descale :param upscaler: Callable function with signature upscaler(clip, width, height) -> vs.VideoNode to be used for reupscaling. Must be capable of handling variable res clips for multiple heights and conditional scaling. If a single height is given and upscaler is None, a constant resolution GRAY clip will be returned instead. Note that if upscaler is None, no upscaling will be performed and neither detail masking nor proper fractional descaling can be preformed. (Default: :py:func:`lvsfunc.scale.reupscale`) :param width: Width to descale to (if None, auto-calculated) :param height: Height(s) to descale to. List indicates multiple resolutions, the function will determine the best. (Default: 720) :param kernel: Kernel used to descale (see :py:class:`lvsfunc.kernels.Kernel`, (Default: kernels.Bicubic(b=0, c=1/2)) :param threshold: Error threshold for conditional descaling (Default: 0.0, always descale) :param mask: Function used to mask detail. If ``None``, no masking. Function must accept a clip and a reupscaled clip and return a mask. (Default: :py:func:`lvsfunc.scale.descale_detail_mask`) :param src_left: Horizontal shifting for fractional resolutions (Default: 0.0) :param src_top: Vertical shifting for fractional resolutions (Default: 0.0) :param show_mask: Return detail mask :return: Descaled and re-upscaled clip with float bitdepth """ if clip.format is None: raise ValueError("descale: 'Variable-format clips not supported'") if type(height) is int: height = [cast(int, height)] height = cast(List[int], height) if type(width) is int: width = [cast(int, width)] elif width is None: width = [ get_w(h, aspect_ratio=clip.width / clip.height) for h in height ] width = cast(List[int], width) if len(width) != len(height): raise ValueError( "descale: Asymmetric number of heights and widths specified") resolutions = [Resolution(*r) for r in zip(width, height)] clip = depth(clip, 32) assert clip.format is not None # clip was modified by depth, but that wont make it variable clip_y = get_y(clip) \ .std.SetFrameProp('descaleResolution', intval=clip.height) variable_res_clip = core.std.Splice([ core.std.BlankClip(clip_y, length=len(clip) - 1), core.std.BlankClip(clip_y, length=1, width=clip.width + 1) ], mismatch=True) descale_partial = partial(_perform_descale, clip=clip_y, kernel=kernel) clips_by_resolution = { c.resolution.height: c for c in map(descale_partial, resolutions) } props = [c.diff for c in clips_by_resolution.values()] select_partial = partial(_select_descale, threshold=threshold, clip=clip_y, clips_by_resolution=clips_by_resolution) descaled = core.std.FrameEval(variable_res_clip, select_partial, prop_src=props) if src_left != 0 or src_top != 0: descaled = core.resize.Bicubic(descaled, src_left=src_left, src_top=src_top) if upscaler is None: upscaled = descaled if len(height) == 1: upscaled = core.resize.Point(upscaled, width[0], height[0]) else: return upscaled else: upscaled = upscaler(descaled, clip.width, clip.height) if src_left != 0 or src_top != 0: upscaled = core.resize.Bicubic(descaled, src_left=-src_left, src_top=-src_top) if upscaled.format is None: raise RuntimeError( "descale: 'Upscaler cannot return variable-format clips'") if mask: clip_y = clip_y.resize.Point(format=upscaled.format.id) rescaled = kernel.scale(descaled, clip.width, clip.height, (src_left, src_top)) rescaled = rescaled.resize.Point(format=clip.format.id) dmask = mask(clip_y, rescaled) if upscaler is None: dmask = core.resize.Spline36(dmask, upscaled.width, upscaled.height) clip_y = core.resize.Spline36(clip_y, upscaled.width, upscaled.height) if show_mask: return dmask upscaled = core.std.MaskedMerge(upscaled, clip_y, dmask) upscaled = depth(upscaled, get_depth(clip)) if clip.format.num_planes == 1 or upscaler is None: return upscaled return join([upscaled, plane(clip, 1), plane(clip, 2)])
def fsrcnnx_upscale( clip: vs.VideoNode, width: int = None, height: int = 1080, shader_file: str = None, # noqa: PLR0912 downscaler: Callable[[vs.VideoNode, int, int], vs.VideoNode] = core.resize.Bicubic, upscaled_smooth: Optional[vs.VideoNode] = None, strength: float = 100.0, profile: str = 'slow', lmode: int = 1, overshoot: float = None, undershoot: float = None, sharpener: Callable[[vs.VideoNode], vs.VideoNode] = partial(z4usm, radius=2, strength=65) ) -> vs.VideoNode: """ Upscale the given luma source clip with FSRCNNX to a given width / height while preventing FSRCNNX artifacts by limiting them. Args: source (vs.VideoNode): Source clip, assuming this one is perfectly descaled. width (int): Target resolution width (if None, auto-calculated). Defaults to None. height (int): Target resolution height. Defaults to 1080. shader_file (str): Path to the FSRCNNX shader file. Defaults to None. downscaler (Callable[[vs.VideoNode, int, int], vs.VideoNode], optional): Resizer used to downscale the upscaled clip. Defaults to core.resize.Bicubic. upscaled_smooth (Optional[vs.VideoNode]): Smooth doubled clip. If not provided, will use nnedi3_upscale(source). strength (float): Only for profile='slow'. Strength between the smooth upscale and the fsrcnnx upscale where 0.0 means the full smooth clip and 100.0 means the full fsrcnnx clip. Negative and positive values are possible, but not recommended. profile (str): Profile settings. Possible strings: "fast", "old", "slow" or "zastin". – "fast" is the old draft mode (the plain fsrcnnx clip returned). – "old" is the old mode to deal with the bright pixels. – "slow" is the new mode, more efficient, using clamping. – "zastin" is a combination between a sharpened nnedi3 upscale and a fsrcnnx upscale. The sharpener prevents the interior of lines from being brightened and fsrnncx (as a clamping clip without nnedi3) prevents artifacting (halos) from the sharpening. lmode (int): Only for profile='slow': – (< 0): Limit with rgvs.Repair (ex: lmode=-1 --> rgvs.Repair(1), lmode=-5 --> rgvs.Repair(5) ...) – (= 0): No limit. – (= 1): Limit to over/undershoot. overshoot (int): Only for profile='slow'. Limit for pixels that get brighter during upscaling. undershoot (int): Only for profile='slow'. Limit for pixels that get darker during upscaling. sharpener (Callable[[vs.VideoNode, Any], vs.VideoNode], optional): Only for profile='zastin'. Sharpening function used to replace the sharped smoother nnedi3 upscale. Defaults to partial(z4USM, radius=2, strength=65) Returns: vs.VideoNode: Upscaled luma clip. """ bits = get_depth(clip) clip = get_y(clip) clip = depth(clip, 16) if width is None: width = get_w(height, clip.width / clip.height) if overshoot is None: overshoot = strength / 100 if undershoot is None: undershoot = overshoot profiles = ['fast', 'old', 'slow', 'zastin'] if profile not in profiles: raise ValueError( 'fsrcnnx_upscale: "profile" must be "fast", "old", "slow" or "zastin"' ) num = profiles.index(profile.lower()) if not shader_file: raise ValueError( 'fsrcnnx_upscale: You must set a string path for "shader_file"') fsrcnnx = shader(clip, clip.width * 2, clip.height * 2, shader_file) if num >= 1: # old or slow profile smooth = depth(get_y(upscaled_smooth), bits) if upscaled_smooth else nnedi3_upscale(clip) if num == 1: # old profile limit = core.std.Expr([fsrcnnx, smooth], 'x y min') elif num == 2: # slow profile upscaled = core.std.Expr( [fsrcnnx, smooth], 'x {strength} * y 1 {strength} - * +'.format( strength=strength / 100)) if lmode < 0: limit = core.rgvs.Repair(upscaled, smooth, abs(lmode)) elif lmode == 0: limit = upscaled elif lmode == 1: dark_limit = core.std.Minimum(smooth) bright_limit = core.std.Maximum(smooth) overshoot = scale_value(overshoot, 8, 16, range_in=Range.FULL, range=Range.FULL) undershoot = scale_value(undershoot, 8, 16, range_in=Range.FULL, range=Range.FULL) limit = core.std.Expr([ upscaled, bright_limit, dark_limit ], f'x y {overshoot} + > y {overshoot} + x ? z {undershoot} - < z {undershoot} - x y {overshoot} + > y {overshoot} + x ? ?' ) else: raise ValueError( 'fsrcnnx_upscale: "lmode" must be < 0, 0 or 1') else: # zastin profile smooth_sharp = sharpener(smooth) limit = core.std.Expr([smooth, fsrcnnx, smooth_sharp], 'x y z min max y z max min') else: limit = fsrcnnx if downscaler: scaled = downscaler(limit, width, height) else: scaled = limit return depth(scaled, bits)
def conditional_descale(clip: vs.VideoNode, height: int, upscaler: Callable[[vs.VideoNode, int, int], vs.VideoNode], kernel: str = 'bicubic', b: Union[float, Fraction] = Fraction(1, 3), c: Union[float, Fraction] = Fraction(1, 3), taps: int = 4, threshold: float = 0.003) -> vs.VideoNode: """ Descales and reupscales a clip; if the difference exceeds the threshold, the frame will not be descaled. If it does not exceed the threshold, the frame will upscaled using the caller-supplied upscaler function. Useful for bad BDs that have additional post-processing done on some scenes, rather than all of them. Currently only works with bicubic, and has no native 1080p masking. Consider scenefiltering OP/EDs with a different descale function instead. The code for _get_error was mostly taken from kageru's Made in Abyss script. Special thanks to Lypheo for holding my hand as this was written. Dependencies: vapoursynth-descale :param clip: Input clip :param upscaler: Callable function with signature upscaler(clip, width, height) -> vs.VideoNode to be used for reupscaling. Example for nnedi3_rpow2: `lambda clip, width, height: nnedi3_rpow2(clip).resize.Spline36(width, height)` :param height: Target descale height :param kernel: Kernel used to descale (see :py:func:`lvsfunc.util.get_scale_filter`, Default: bicubic) :param b: B-param for bicubic kernel (Default: 1 / 3) :param c: C-param for bicubic kernel (Default: 1 / 3) :param taps: Taps param for lanczos kernel (Default: 4) :param threshold: Threshold for deciding to descale or leave the original frame (Default: 0.003) :return: Constant-resolution rescaled clip """ try: from descale import get_filter except ModuleNotFoundError: raise ModuleNotFoundError( "conditional_descale: missing dependency 'descale'") b = float(b) c = float(c) def _get_error(clip, height, kernel, b, c, taps): descale = get_filter(b, c, taps, kernel)(clip, get_w(height, clip.width / clip.height), height) upscale = util.get_scale_filter(kernel, b=b, c=c, taps=taps)(clip, clip.width, clip.height) diff = core.std.PlaneStats(upscale, clip) return descale, diff def _diff(n, f, clip_a, clip_b, threshold): return clip_a if f.props.PlaneStatsDiff > threshold else clip_b if get_depth(clip) != 32: clip = util.resampler(clip, 32) planes = split(clip) descaled, diff = _get_error(planes[0], height=height, kernel=kernel, b=b, c=c, taps=taps) try: planes[0] = upscaler(descaled, clip.width, clip.height) except: raise Exception("conditional_descale: upscale function misbehaved") descaled = join(planes).resize.Spline36(format=clip.format) descaled = descaled.std.SetFrameProp("_descaled", intval=1) clip = clip.std.SetFrameProp("_descaled", intval=0) return core.std.FrameEval( clip, partial(_diff, clip_a=clip, clip_b=descaled, threshold=threshold), diff)
def prep(*clips: vs.VideoNode, w: int = 1280, h: int = 720, dith: bool = True, yuv444: bool = True, static: bool = True) \ -> Union[vs.VideoNode, List[vs.VideoNode]]: """Prepares multiple clips of diff sizes/bit-depths to be compared. Can optionally be used as a simplified resize/ftmc wrapper for one clip. Clips MUST be either YUV420 or YUV444. Transforms all planes to w x h using Bicubic: Hermite 0,0 for downscale / Mitchell 1/3,1/3 for upscale. :param clips: clip(s) to process :bit depth: ANY :color family: YUV :float precision: ANY :sample type: ANY :subsampling: 420, 444 :param w: target width in px :param h: target height in px :param dith: whether or not to dither clips down to 8-bit (Default value = True) :param yuv444: whether or not to convert all clips to 444 chroma subsampling (Default value = True) :param static: changes dither mode based on clip usage (Default value = True) True will use Floyd-Steinberg error diffusion (good for static screenshots) False will use Sierra's Filter Lite error diffusion (faster) :returns: processed clip(s) """ outclips = [] for clip in clips: if get_subsampling(clip) == '444': if clip.height > h: clip_scaled = core.resize.Bicubic(clip, w, h, filter_param_a=0, filter_param_b=0) elif clip.height < h: clip_scaled = core.resize.Bicubic(clip, w, h, filter_param_a=0.33, filter_param_b=0.33) else: clip_scaled = clip elif get_subsampling(clip) == '420' and yuv444: if clip.height > h: if clip.height >= (2 * h): # this downscales chroma with Hermite instead of Mitchell clip_scaled = core.resize.Bicubic( clip, w, h, filter_param_a=0, filter_param_b=0, filter_param_a_uv=0, filter_param_b_uv=0, format=clip.format.replace(subsampling_w=0, subsampling_h=0)) else: clip_scaled = core.resize.Bicubic( clip, w, h, filter_param_a=0, filter_param_b=0, filter_param_a_uv=0.33, filter_param_b_uv=0.33, format=clip.format.replace(subsampling_w=0, subsampling_h=0)) elif clip.height < h: clip_scaled = core.resize.Bicubic(clip, w, h, filter_param_a=0.33, filter_param_b=0.33, format=clip.format.replace( subsampling_w=0, subsampling_h=0)) else: clip_scaled = core.resize.Bicubic(clip, filter_param_a=0.33, filter_param_b=0.33, format=clip.format.replace( subsampling_w=0, subsampling_h=0)) else: if clip.height > h: clip_scaled = core.resize.Bicubic(clip, w, h, filter_param_a=0, filter_param_b=0) elif clip.height < h: clip_scaled = core.resize.Bicubic(clip, w, h, filter_param_a=0.33, filter_param_b=0.33) else: clip_scaled = clip if get_depth(clip_scaled) > 8: if dith: if static: # Floyd-Steinberg error diffusion clip_dith = core.fmtc.bitdepth(clip_scaled, bits=8, dmode=6) else: # Sierra-2-4A "Filter Lite" error diffusion clip_dith = core.fmtc.bitdepth(clip_scaled, bits=8, dmode=3) else: # No dither, round to the closest value clip_dith = core.fmtc.bitdepth(clip_scaled, bits=8, dmode=1) else: clip_dith = clip_scaled outclips.append(clip_dith) if len(outclips) == 1: return clip_dith return outclips
def f3kbilateral(clip: vs.VideoNode, radius: int = 16, threshold: Union[int, List[int]] = 65, grain: Union[int, List[int]] = 0, f3kdb_args: Dict[str, Any] = {}, lf_args: Dict[str, Any] = {}) -> vs.VideoNode: """f3kdb multistage bilateral-esque filter from debandshit. This function is more of a last resort for extreme banding. Args: clip (vs.VideoNode): Source clip. radius (int, optional): Same as F3kdb constructor. Defaults to 16. threshold (Union[int, List[int]], optional): Same as F3kdb constructor. Defaults to 65. grain (Union[int, List[int]], optional): Same as F3kdb constructor. It happens after mvsfunc.LimitFilter and call another instance of F3kdb if != 0. Defaults to 0. f3kdb_args (Dict[str, Any], optional): Same as F3kdb constructor. Defaults to {}. lf_args (Dict[str, Any], optional): Arguments passed to mvsfunc.LimitFilter. Defaults to {}. Returns: vs.VideoNode: Debanded clip. """ try: from mvsfunc import LimitFilter except ModuleNotFoundError as mod_err: raise ModuleNotFoundError( "f3kbilateral: missing dependency 'mvsfunc'") from mod_err if clip.format is None: raise FormatError( "f3kbilateral: 'Variable-format clips not supported'") bits = get_depth(clip) limflt_args: Dict[str, Any] = dict(thr=0.6, elast=3.0, thrc=None) limflt_args.update(lf_args) # Radius rad1 = round(radius * 4 / 3) rad2 = round(radius * 2 / 3) rad3 = round(radius / 3) # F3kdb objects db1 = F3kdb(rad1, threshold, 0, **f3kdb_args) db2 = F3kdb(rad2, threshold, 0, **f3kdb_args) db3 = F3kdb(rad3, threshold, 0, **f3kdb_args) # Edit the thr of first f3kdb object db1.thy, db1.thcb, db1.thcr = db1.thy // 2, db1.thcb // 2, db1.thcr // 2 # Perform deband clip = depth(clip, 16) flt1 = db1.deband(clip) flt2 = db2.deband(flt1) flt3 = db3.deband(flt2) # Limit limit = LimitFilter(flt3, flt2, ref=clip, **lf_args) # Grain if grain != 0 or grain is not None: grained = F3kdb(grain=grain, **f3kdb_args).grain(limit) else: grained = limit return depth(grained, bits)