def simple_native_mask( clip: vs.VideoNode, descale_w: IntegerFloat, descale_h: IntegerFloat, blurh: IntegerFloat = 1.5, blurv: IntegerFloat = 1.5, iter_max: int = 3, no_resize: bool = False, ) -> vs.VideoNode: """ Create a native mask to make sure native content does not get descaled. Parameters ---------- clip: :class:`VideoNode` The video source. descale_w: :class:`Union[int, float]` Target descale width resolution for checking. descale_h: :class:`Union[int, float]` Target descale height resolution for checking. blurh: :class:`Union[int, float]` Horizontal blur strength. blurv: :class:`Union[int, float]` Vertical blur strength. iter_max: :class:`int` Iteration count that will expand the mask size. no_resize: :class:`bool` Don't resize to the descaled resolution (keep it at original resolution) Returns ------- :class:`VideoNode` The native mask. """ has_plugin_or_raise(["fmtc", "descale"]) clip_32 = fvf.Depth(clip, 32) y_32 = get_y(clip_32) clip_bits = clip.format.bits_per_sample target_w = clip.width target_h = clip.height descale_w = int(round(descale_w)) descale_h = int(round(descale_h)) down = core.descale.Debicubic(y_32, descale_w, descale_h) up = core.resize.Bicubic(down, target_w, target_h) dmask = core.std.Expr([y_32, up], "x y - abs 0.025 > 1 0 ?") dmask = iterate(dmask, core.std.Maximum, iter_max) if blurh > 0 and blurv > 0: dmask = core.std.BoxBlur(dmask, hradius=cast(int, blurh), vradius=cast(int, blurv)) if not no_resize: dmask = core.resize.Bicubic(dmask, descale_w, descale_h) return fvf.Depth(dmask, clip_bits)
def _select(n, y, debic_list, sraa_upscale, rfactor, f): # This simply descales to each of those and selects the most appropriate for each frame. errors = [x.props.PlaneStatsAverage for x in f] y_deb = debic_list[errors.index(min(errors))] dmask = core.std.Expr([y, y_deb.resize.Bicubic(clip.width, clip.height)], 'x y - abs 0.025 > 1 0 ?').std.Maximum().std.SetFrameProp("_descaled_resolution", intval=y_deb.height) if sraa_upscale: up = upscaled_sraa(y_deb, rfactor, h=clip.height).resize.Bicubic(clip.width, clip.height) else: up = fvf.Depth(nnedi3_rpow2(fvf.Depth(y_deb, 16), nns=4, correct_shift=True, width=clip.width, height=clip.height), 32) return core.std.ClipToProp(up, dmask)
def select_scale(n: int, f: List[vs.VideoFrame], descale_list: List[vs.VideoNode]): errors = [x.props["PlaneStatsAverage"] for x in f] y_deb = descale_list[errors.index(min(errors))] # type: ignore dmask = core.std.Expr( [y, global_clip_resizer(y_deb, target_w, target_h)], "x y - abs 0.025 > 1 0 ?").std.Maximum() y_deb16 = fvf.Depth(y_deb, 16) if rescale: y_scaled = upscale_nnedi3( y_deb16, nns=4, correct_shift=True, width=target_w, height=target_h, use_gpu=use_gpu # type: ignore ).fmtc.bitdepth(bits=32) else: y_scaled = global_clip_resizer(y_deb16, target_w, target_h).fmtc.bitdepth(bits=32) dmask = global_clip_resizer(dmask, target_w, target_h) if show_native_res and not show_mask: y_scaled = y_scaled.text.Text( f"Native resolution for this frame: {y_deb.height}") return core.std.ClipToProp(y_scaled, dmask)
def skip(n, f): if f.props.PlaneStatsMaximum == 0: return core.std.BlankClip(clip, format=vs.GRAYS, color=[0]) else: subedge = core.std.Expr(c444, 'x y z min min') diff = core.std.Expr([getY(clip).std.Convolution([1]*9), getY(ref).std.Convolution([1]*9)], 'x 0.8 > x 0.2 < or x y - abs 0.1 > and 1 0 ?').std.Maximum().std.Maximum() mask = core.misc.Hysteresis(subedge, diff) mask = iterate(mask, core.std.Maximum, expandN) return mask.std.Inflate().std.Inflate().std.Convolution([1]*9) clip = fvf.Depth(clip, 32) ref = fvf.Depth(ref, 32) right = core.resize.Point(clip, src_left=4) # right shift by 4 pixels subedge = core.std.Expr([clip, right], ['x y - abs 0.7 > 1 0 ?', 'x abs 0.1 < y abs 0.1 < and 1 0 ?']) c444 = split(subedge.resize.Bilinear(format=vs.YUV444PS)) luma = c444[0].std.PlaneStats() mask = core.std.FrameEval()
def conditional_descale(clip: vs.VideoNode, height: int, kernel: str = 'bicubic', b: float = 1 / 3, c: float = 1 / 3, taps: int = 4, threshold: float = 0.003, upscaler: str = None) -> vs.VideoNode: funcname = "conditional_descale" """ 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 either nnedi3_rpow2 or waifu2x-caffe. 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. :param height: int: Target descale height :param threshold: float: Threshold for deciding to descale or leave the original frame :param upscaler: str: What scaler is used to upscale (options: nnedi3_rpow2 (default), upscaled_sraa, waifu2x) :param replacement_clip: videoNode A clip to replace frames that were not descaled with. """ def _get_error(clip, height, kernel, b, c, taps): descale = fvf.Resize(clip, get_w(height), height, kernel=kernel, a1=b, a2=c, taps=taps, invks=True) upscale = fvf.Resize(clip, clip.width, clip.height, kernel=kernel, a1=b, a2=c, taps=taps) 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 = fvf.Depth(clip, 32, dither_type='none') planes = kgf.split(clip) descaled, diff = _get_error(planes[0], height=height, kernel=kernel, b=b, c=c, taps=taps) upscaler = upscaler or "nnedi3_rpow2" if upscaler in ['nnedi3_rpow2', 'nnedi3', 'nn3_rp2']: planes[0] = nnedi3_rpow2(descaled).resize.Spline36(clip.width, clip.height) elif upscaler in ['nnedi3_resample', 'nn3_res']: planes[0] = nnedi3_resample(descaled, clip.width, clip.height, kernel='gauss', invks=True, invkstaps=2, taps=1, a1=32, nns=4, qual=2, pscrn=4) elif upscaler in ['upscaled_sraa', 'up_sraa', 'sraa']: planes[0] = upscaled_sraa(descaled, h=clip.height, sharp_downscale=False).resize.Spline36(clip.width) elif upscaler in ['waifu2x', 'w2x']: planes[0] = core.caffe.Waifu2x(descaled, noise=-1, scale=2, model=6, cudnn=True, processor=0, tta=False).resize.Spline36(clip.width, clip.height) else: return error(funcname, f'"{upscaler}" is not a valid option for "upscaler". Please pick either "nnedi3_rpow2", "nnedi3_resample", "upscaled_sraa", or "waifu2x"') descaled = kgf.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 adaptive_grain(clip: vs.VideoNode, strength=0.25, static=True, luma_scaling=12, mask_bits=8, show_mask=False) -> vs.VideoNode: """ generates grain based on frame and pixel brightness. details can be found here: https://kageru.moe/article.php?p=adaptivegrain strength is the strength of the grain generated by AddGrain, static=True for static grain luma_scaling manipulates the grain alpha curve. Higher values will generate less grain (especially in brighter scenes) while lower values will generate more grain, even in brighter scenes Please note that 8 bit should be enough for the mask; 10, if you want to do everything in 10 bit. It is technically possible to set it to up to 16 (float does not work), but you won't gain anything. An 8 bit mask uses 1 MB of RAM, 10 bit need 4 MB, and 16 bit use 256 MB. Lookup times might also increase (they shouldn't, but you never know), as well as the initial generation time. """ import numpy as np def fill_lut(y): """ Using horner's method to compute this polynomial: (1 - (1.124 * x - 9.466 * x ** 2 + 36.624 * x ** 3 - 45.47 * x ** 4 + 18.188 * x ** 5)) ** ((y ** 2) * luma_scaling) * 255 Using the normal polynomial is about 2.5x slower during the initial generation. I know it doesn't matter as it only saves a few ms (or seconds at most), but god damn, just let me have some fun here, will ya? Just truncating (rather than rounding) the array would also half the processing time, but that would decrease the precision and is also just unnecessary. """ x = np.arange(0, 1, 1 / (1 << mask_bits)) z = (1 - (x * (1.124 + x * (-9.466 + x * (36.624 + x * (-45.47 + x * 18.188)))))) ** ((y ** 2) * luma_scaling) if clip.format.sample_type == vs.INTEGER: z = z * ((1 << mask_bits) - 1) z = np.rint(z).astype(int) return z.tolist() def generate_mask(n, f, clip): frameluma = round(f.props.PlaneStatsAverage * 999) table = lut[int(frameluma)] return core.std.Lut(clip, lut=table) clip8 = fvf.Depth(clip, mask_bits) bits = clip.format.bits_per_sample lut = [None] * 1000 for y in np.arange(0, 1, 0.001): lut[int(round(y * 1000))] = fill_lut(y) luma = core.std.ShufflePlanes(clip8, 0, vs.GRAY) luma = core.std.PlaneStats(luma) grained = core.grain.Add(clip, var=strength, constant=static) mask = core.std.FrameEval(luma, partial(generate_mask, clip=luma), prop_src=luma) mask = core.resize.Spline36(mask, clip.width, clip.height) if bits != mask_bits: mask = core.fmtc.bitdepth(mask, bits=bits, dmode=1) if show_mask: return mask return core.std.MaskedMerge(clip, grained, mask)
def fix_cr_tint(clip: vs.VideoNode, value: int = 128) -> vs.VideoNode: funcname = "fix_cr_tint" """ Tries to forcibly fix Crunchyroll's green tint by adding pixel values :param value: int: Values added to every pixel """ if get_depth(clip) != 16: clip = fvf.Depth(clip, 16) return core.std.Expr(clip, f'x {value} +')
def luma_histogram(src, plane=0): """ Helper function to grab a clip's plane and dither to 8-bit to use hist.Luma """ name = 'luma_histogram' if plane < 0 or src.format.num_planes <= plane: raise TypeError( name + ": 'plane' must be between 0 and the number of src clip planes") src = fvf.Depth(src, 8) if src.format.bits_per_sample != 8 else src plane = core.std.ShufflePlanes(src, planes=plane, colorfamily=vs.GRAY) return core.hist.Luma(plane)
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 NnEedi3(src: vs.VideoNode, strength=1, alpha=0.25, beta=0.5, gamma=40, nrad=2, mdis=20, nsize=3, nns=3, qual=1): """ Script written by Zastin. What it does is clamp the "change" done by eedi3 to the "change" of nnedi3. This should fix every issue created by eedi3. For example: https://i.imgur.com/hYVhetS.jpg """ clip = get_y(src) if clip.format.bits_per_sample != 16: clip = fvf.Depth(clip, 16) thr = strength * 256 strong = taa.TAAmbk(clip, aatype='Eedi3', alpha=alpha, beta=beta, gamma=gamma, nrad=nrad, mdis=mdis, mtype=0) weak = taa.TAAmbk(clip, aatype='Nnedi3', nsize=nsize, nns=nns, qual=qual, mtype=0) expr = 'x z - y z - * 0 < y x y {l} + min y {l} - max ?'.format(l=thr) aa = core.std.Expr([strong, weak, clip], expr) mask = clip.std.Prewitt().std.Binarize(50 >> 8).std.Maximum().std.Convolution([1] * 9) merged = core.std.MaskedMerge(clip, aa, mask) return clip if src.format.color_family == vs.GRAY else core.std.ShufflePlanes([clip, src], [0, 1, 2], vs.YUV)
def source(file: str, resample=False) -> vs.VideoNode: """ Just a stupid import script. There really is no reason to use this, but hey, it was fun to write. """ if file.startswith("file:///"): file = file[8::] if file.endswith(".d2v"): clip = core.d2v.Source(file) if is_image(file): clip = core.imwri.Read(file) else: if file.endswith(".m2ts"): clip = core.lsmas.LWLibavSource(file) else: clip = core.ffms2.Source(file) if resample: clip = fvf.Depth(clip, 16) return clip
def quick_denoise(clip: vs.VideoNode, mode='knlm', bm3d=True, sigma=3, h=1.0, refine_motion=True, sbsize=16, resample=True): """ Wrapper for generic denoising. Denoising is done by BM3D with a given denoisers being used for ref. Returns the denoised clip used as ref if BM3D=False. Mode 1 = KNLMeansCL Mode 2 = SMDegrain Mode 3 = DFTTest Will be removed eventuallyTM. """ if resample: if clip.format.bits_per_sample != 16: clip = fvf.Depth(clip, 16) clipY = core.std.ShufflePlanes(clip, 0, vs.GRAY) if mode in [1, 'knlm']: denoiseY = clipY.knlm.KNLMeansCL(d=3, a=2, h=h) elif mode in [2, 'SMD', 'SMDegrain']: denoiseY = haf.SMDegrain(clipY, prefilter=3, RefineMotion=refine_motion) elif mode in [3, 'DFT', 'dfttest']: denoiseY = clipY.dfttest.DFTTest(sigma=4.0, tbsize=1, sbsize=sbsize, sosize=sbsize*0.75) else: raise ValueError('denoise: unknown mode') if bm3d: denoisedY = mvf.BM3D(clipY, sigma=sigma, psample=0, radius1=1, ref=denoiseY) elif bm3d is False: denoisedY = denoiseY if clip.format.color_family is vs.GRAY: return denoisedY else: srcU = clip.std.ShufflePlanes(1, vs.GRAY) srcV = clip.std.ShufflePlanes(2, vs.GRAY) merged = core.std.ShufflePlanes([denoisedY, srcU, srcV], 0, vs.YUV) return merged
def edi_resample(src, w, h, edi=None, kernel='spline16', a1=None, a2=None, sx=None, sy=None, invks=False, taps=4, invkstaps=4, **kwargs): """ Edge-directed interpolation resampler Doubles the height with the given edge-directed interpolation filter as many times as needed and downsamples to the given w and h, fixing the chroma shift if necessary. Supports: - eedi2 - eedi3 - eedi3cl - nnedi3 (znedi3) - nnedi3cl Currently, it only correctly supports maintaining a similar aspect ratio because it always doubles both the width and height. """ name = 'edi_resample' valid_edis = { 'eedi2': [ 'mthresh', 'lthresh', 'vthresh', 'estr', 'dstr', 'maxd', 'map', 'nt', 'pp' ], 'eedi3': [ 'alpha', 'beta', 'gamma', 'nrad', 'mdis', 'hp', 'ucubic', 'cost3', 'vcheck', 'vthresh0', 'vthresh1', 'vthresh2', 'sclip', 'opt' ], 'eedi3cl': [ 'alpha', 'beta', 'gamma', 'nrad', 'mdis', 'hp', 'ucubic', 'cost3', 'vcheck', 'vthresh0', 'vthresh1', 'vthresh2', 'sclip', 'opt', 'device' ], 'nnedi3': [ 'nsize', 'nns', 'qual', 'etype', 'pscrn', 'opt', 'int16_prescreener', 'int16_predictor', 'exp' ], 'nnedi3cl': ['nsize', 'nns', 'qual', 'etype', 'pscrn', 'device'], } if not isinstance(src, vs.VideoNode): raise TypeError(name + ": 'src' must be a clip") if not isinstance(edi, str): raise TypeError( name + ": Must use a supported edge-directed interpolation filter string") edi = edi.lower() if edi not in valid_edis: raise TypeError( name + ": '" + edi + "' is not a supported edge-directed interpolation filter") # Check if kwargs are valid for given edi for arg in kwargs: if arg not in valid_edis[edi]: raise TypeError(name + ": '" + arg + "' is not a valid argument for " + edi) edifuncs = { 'eedi2': (lambda src: core.eedi2.EEDI2(src, field=1, **kwargs).std.Transpose()), 'eedi3': (lambda src: core.eedi3m.EEDI3( src, field=1, dh=True, **kwargs).std.Transpose()), 'eedi3cl': (lambda src: core.eedi3m.EEDI3CL( src, field=1, dh=True, **kwargs).std.Transpose()), 'nnedi3': (lambda src: core.znedi3.nnedi3( src, field=1, dh=True, **kwargs).std.Transpose()), 'nnedi3cl': (lambda src: core.nnedi3cl.NNEDI3CL( src, field=1, dh=True, dw=True, **kwargs)), } scale = h / src.height if scale == 1: return src double_count = ceil(log(scale, 2)) double_count = double_count * 2 if edi != 'nnedi3cl' else double_count doubled = src for _ in range(double_count): doubled = edifuncs[edi](doubled) if sx is None: sx = [-0.5, -0.5 * src.format.subsampling_w] if double_count >= 1 else 0 if sy is None: sy = [-0.5, -0.5 * src.format.subsampling_h] if double_count >= 1 else 0 down = core.fmtc.resample(doubled, w=w, h=h, sx=sx, sy=sy, kernel=kernel, a1=a1, a2=a2, taps=taps, invks=invks, invkstaps=invkstaps) return fvf.Depth(down, src.format.bits_per_sample)
def masked_f3kdb(clip: vs.VideoNode, mask: Optional[vs.VideoNode] = None, range: int = 15, y: int = 40, cb: Optional[int] = None, cr: Optional[int] = None, grainy: int = 0, grainc: int = 0, agrain: int = 0, luma_scaling: int = 12, sample_mode: int = 2, keep_tv_range: bool = True, output_depth: Optional[int] = None) -> vs.VideoNode: """Wrapper function for neo_f3kdb. Additional changes include sane defaults, the ability to merge with an external mask clip, and additionally add kagefunc's adaptive_grain to the final, debanded clip. grainy, grainc, and adaptive_grain are applied to the final, post-merged clip. Differing default behavior: - y defaults to 40 and cr/cb default to y//2. - output_depth defaults to the source bit depth. - grainy and grainc default to 0. - grain is always static. - keep_tv_range defaults to True (since typical sources are TV range). Args: Most f3kdb arguments: https://f3kdb.readthedocs.io/en/latest/index.html mask: Mask clip to use for merging with debanded clip. agrain: The strength arg for adaptive_grain. luma_scaling: The luma_scaling arg for adaptive_grain. """ name = 'masked_f3kdb' if not isinstance(clip, vs.VideoNode): raise TypeError(name + ": 'clip' must be a clip") if mask is not None and not isinstance(mask, vs.VideoNode): raise TypeError(name + ": 'mask' must be a clip") src_bits = clip.format.bits_per_sample if cb is None: cb = y // 2 if cr is None: cr = y // 2 if output_depth is None: output_depth = src_bits if mask is not None and mask.format.bits_per_sample != src_bits: mask = fvf.Depth(mask, src_bits, dither_type='none') debanded = core.neo_f3kdb.Deband(clip, range=range, y=y, cb=cb, cr=cr, grainy=0, grainc=0, sample_mode=sample_mode, keep_tv_range=keep_tv_range, output_depth=src_bits) if mask is not None: debanded = core.std.MaskedMerge(debanded, clip, mask) if grainy > 0 or grainc > 0: debanded = core.neo_f3kdb.Deband(debanded, range=0, y=0, cb=0, cr=0, grainy=grainy, grainc=grainc, sample_mode=sample_mode, keep_tv_range=keep_tv_range, output_depth=src_bits) if agrain > 0: debanded = kgf.adaptive_grain(debanded, strength=agrain, luma_scaling=luma_scaling) return fvf.Depth(debanded, output_depth)
def rescale(src, w=None, h=None, mask_detail=False, mask=None, thr=10, expand=2, inflate=2, descale_kernel='bicubic', b=1 / 3, c=1 / 3, descale_taps=3, kernel='spline16', taps=None, invks=False, invkstaps=3, a1=None, a2=None, nsize=4, nns=4, f=None, show_mask=False): """ Descale and re-upscale a clip This descales a clip's luma, nnedi3_resamples it back to its original resolution, and merges back in the original chroma if applicable. It can also mask detail that is greater than the 'native' resolution and merge it back into the final rescaled clip. Parameters: ----------- w: source clip's native width to descale to h: source clip's native height to descale to mask_detail (False): mask higher-than-native-resolution detail mask: external mask clip to use instead of built-in masking thr (10): threshold of detail to include in built-in mask expand (2): number of times to expand built-in mask inflate (2): number of times to inflate built-in mask descale_kernel ('bicubic'): kernel for descale b (1/3): b value for descale c (1/3): c value for descale descale_taps (3): taps value for descale kernel ('spline16'): kernel for nnedi3_resample rescale taps: taps value for nnedi3_resample rescale invks (False): invks for nnedi3_resample rescale invkstaps (3): invkstaps for nnedi3_resample a1: a1 for nnedi3_resample a2: a2 for nnedi3_resample nsize (4): nsize for nnedi3_resample nns (4): nns for nnedi3_resample f: function to perform on descaled luma before upscaling show_mask (False): output detail mask """ name = 'rescale' if not isinstance(src, vs.VideoNode): raise TypeError(name + ": 'src' must be a clip") if mask is not None and not isinstance(mask, vs.VideoNode): raise TypeError(name + ": 'mask' must be a clip") if h is None: raise TypeError(name + ": native height 'h' must be given") if show_mask and not mask_detail: raise TypeError(name + ": 'show_mask' can only be used with mask_detail=True") sw = src.width sh = src.height src_bits = src.format.bits_per_sample is_gray = src.format.color_family == vs.GRAY if w is None: w = h / sh * sw if mask is not None and mask.format.bits_per_sample != src_bits: mask = fvf.Depth(mask, src_bits, dither_type='none') y = src if is_gray else get_y(src) descaled = fvf.Resize(y, w, h, kernel=descale_kernel, a1=b, a2=c, taps=descale_taps, invks=True) # Built-in diff mask generation (from fvsfunc's DescaleM) if mask_detail and mask is None: peak = (1 << src_bits) - 1 thr = hvf.scale(thr, peak) up = fvf.Resize(descaled, sw, sh, kernel=descale_kernel, a1=b, a2=c, taps=descale_taps) diff_mask = core.std.Expr([y, up], 'x y - abs') diff_mask = fvf.Resize(diff_mask, w, h, kernel='bilinear') diff_mask = core.std.Binarize(diff_mask, threshold=thr) diff_mask = kgf.iterate(diff_mask, core.std.Maximum, expand) diff_mask = kgf.iterate(diff_mask, core.std.Inflate, inflate) diff_mask = core.resize.Spline36(diff_mask, sw, sh) elif mask_detail: diff_mask = mask if show_mask: return diff_mask if f is not None: descaled = f(descaled) rescaled = nnedi3_resample(descaled, sw, sh, nsize=nsize, nns=nns, kernel=kernel, a1=a1, a2=a2, taps=taps, invks=invks, invkstaps=invkstaps) if mask_detail: rescaled = core.std.MaskedMerge(rescaled, y, diff_mask) if is_gray: return rescaled return merge_chroma(rescaled, src)
def smarter_descale(src: vs.VideoNode, resolutions: List[int], descaler: Optional[Callable[[vs.VideoNode, int, int], vs.VideoNode]] = None, rescaler: Optional[Callable[[vs.VideoNode, int, int], vs.VideoNode]] = None, upscaler: Optional[Callable[[vs.VideoNode, int, int], vs.VideoNode]] = None, thr: float = 0.05, rescale: bool = True, to_src: bool = False) -> vs.VideoNode: """ An updated version of smart_descale, hence smart*er*_descale. Still in its experimental stage, and this is just a wrapper to also handle format conversions afterwards because x264 throws a fit otherwise. Example use: import functools import lvsfunc as lvf scaled = lvf.smarter_descale(src, range(840,849), functools.partial(core.descale.Debicubic, b=0, c=1/2), functools.partial(core.resize.Spline36)) scaled.set_output() :param resolutions: List[int]: A list of the resolutions to attempt to descale to. For example: "resolutions=[720, 810]" Range can help with easily getting a range of resolutions :param descaler: Descaler used. Default is descale.Bicubic :param rescaler: Rescaler used. Default is resize.Bicubic :param upscaler: Upscaler used. Default is resize.Bicubi. :param thr: float: Threshold for the descaling. Corrosponds directly to the same error rate as getscaler :param rescale: bool: To rescale to the highest-given height and handle conversion for x264 fuckery. Default is True :param to_src: bool: To upscale back to the src resolution with nnedi3_resample (inverse gauss) """ descaler = descaler or core.descale.Debicubic rescaler = rescaler or core.resize.Bicubic upscaler = upscaler or core.resize.Spline36 ScaleAttempt = namedtuple('ScaleAttempt', ['descaled', 'rescaled', 'resolution', 'diff']) src = fvf.Depth((get_y(src) if src.format.num_planes != 1 else src), 32) \ .std.SetFrameProp('descaleResolution', intval=src.height) def perform_descale(height: int) -> ScaleAttempt: resolution = Resolution(get_w(height, src.width / src.height), height) descaled = descaler(src, resolution.width, resolution.height) \ .std.SetFrameProp('descaleResolution', intval=height) rescaled = rescaler(descaled, src.width, src.height) diff = core.std.Expr([rescaled, src], 'x y - abs').std.PlaneStats() return ScaleAttempt(descaled, rescaled, resolution, diff) clips_by_resolution = {c.resolution.height: c for c in map(perform_descale, resolutions)} # If we pass a variable res clip as first argument to FrameEval, we’re also allowed to return one. variable_res_clip = core.std.Splice([ core.std.BlankClip(src, length=len(src)-1), core.std.BlankClip(src, length=1, width=src.width + 1) ], mismatch=True) def select_descale(n: int, f: List[vs.VideoFrame]): # TODO: higher resolutions tend to be lower. compensate for that print(f) print() print([fr.props.PlaneStatsAverage for fr in f]) best_res = max(f, key=lambda frame: math.log(src.height - frame.props.descaleResolution, 2) * round(1/frame.props.PlaneStatsAverage) ** 0.2) print('selected') print(clips_by_resolution) print(list(clips_by_resolution.keys())) print(best_res.props.descaleResolution) best_attempt = clips_by_resolution.get(best_res.props.descaleResolution) print('found') if threshold == 0: return best_attempt.descaled # No blending here because src and descaled have different resolutions. # The caller can use the frameProps to deal with that if they so desire. if best_res.props.PlaneStatsAverage > threshold: return src return best_attempt.descaled props = [c.diff for c in clips_by_resolution.values()] print(props) scaled = core.std.FrameEval(variable_res_clip, select_descale, prop_src=props) if rescale: # You MUST set the output format again, or else x264/x265 will throw a hissy fit scaled = upscaler(scaled, get_w(resolutions.sort()[-1]), resolutions.sort()[-1], format=src.format) if to_src: # This is done after scaling up because else nn3_rs returns an error scaled = nnedi3_resample(scaled, src.width, src.height, kernel='gauss', invks=True, invkstaps=2, taps=1, a1=32, nns=4, qual=2, pscrn=4) return scaled
from vapoursynth import core import vsTAAmbk as taa import fvsfunc as fvf apple = core.ffms2.Source('apple.mkv') apple = core.std.Crop(apple, 2, 2, 2, 2) apple = fvf.Depth(apple, 16) apple = core.resize.Spline36(apple, 720, 540) apple_d = core.placebo.Deband(apple, dither=False) apple_ds = core.placebo.Deband(apple, iterations=2, threshold=10, radius=20, dither=False) apple_dv = core.placebo.Deband(apple, iterations=4, threshold=23, radius=20, grain=0.7, dither=False) apple = taa.TAAmbk( apple_d[:814] + apple_ds[814:821] + apple_d[821:1709] + apple_dv[1709:1812] + apple_d[1812:2950] + apple_ds[2950:3320] + apple_d[3320:3786], 2) + taa.TAAmbk(apple_d[3786:3793], 1) + taa.TAAmbk( apple_d[3793:4250], 2) + taa.TAAmbk(apple_d[4250:4316], 1) + taa.TAAmbk( apple_d[4316:4447], 2) + taa.TAAmbk(apple_d[4447:4559], 1) + taa.TAAmbk(
def resample(clip): # Resampling to 8bit and RGB to properly display how it appears on your screen return fvf.Depth(mvf.ToRGB(clip), 8)
def smart_descale(clip: vs.VideoNode, res: List[int], b: float = 1/3, c: float = 1/3, thresh1: float = 0.03, thresh2: float = 0.7, no_mask: float = False, show_mask: bool = False, show_dmask: bool = False, sraa_upscale: bool = False, rfactor: float = 1.5, sraa_sharp: bool = False) -> vs.VideoNode: funcname = "smart_descale" """ Original function written by kageru and modified into a general function by me. For more information and comments I suggest you check out the original script: https://git.kageru.moe/kageru/vs-scripts/src/branch/master/abyss1.py A descaling function that compares relative errors between multiple resolutions and descales accordingly. Most of this code was leveraged from kageru's Made in Abyss script. As this is an incredibly complex function, I will offer only minimal support. :param res: List[int]: A list of resolutions to descale to. For example: [900, 871, 872, 877] :param thresh1: float: Threshold for when a frame will be descaled. :param thresh2: float: Threshold for when a frame will not be descaled. :param sraa_upscale: bool: Use upscaled_sraa to upscale the frames (warning: very slow!) :param rfactor: float: Image enlargement factor for upscaled_sraa """ def _descaling(clip: vs.VideoNode, h: int, b: float, c: float): # Descale and return a tuple of descaled clip and diff mask between that and the original. down = clip.descale.Debicubic(get_w(h), h, b, c) up = down.resize.Bicubic(clip.width, clip.height, filter_param_a=b, filter_param_b=c) diff = core.std.Expr([clip, up], 'x y - abs').std.PlaneStats() return down, diff def _select(n, y, debic_list, sraa_upscale, rfactor, f): # This simply descales to each of those and selects the most appropriate for each frame. errors = [x.props.PlaneStatsAverage for x in f] y_deb = debic_list[errors.index(min(errors))] dmask = core.std.Expr([y, y_deb.resize.Bicubic(clip.width, clip.height)], 'x y - abs 0.025 > 1 0 ?').std.Maximum().std.SetFrameProp("_descaled_resolution", intval=y_deb.height) if sraa_upscale: up = upscaled_sraa(y_deb, rfactor, h=clip.height).resize.Bicubic(clip.width, clip.height) else: up = fvf.Depth(nnedi3_rpow2(fvf.Depth(y_deb, 16), nns=4, correct_shift=True, width=clip.width, height=clip.height), 32) return core.std.ClipToProp(up, dmask) def _square(): top = core.std.BlankClip(length=len(y), format=vs.GRAYS, height=4, width=10, color=[1]) side = core.std.BlankClip(length=len(y), format=vs.GRAYS, height=2, width=4, color=[1]) center = core.std.BlankClip(length=len(y), format=vs.GRAYS, height=2, width=2, color=[0]) t1 = core.std.StackHorizontal([side, center, side]) return core.std.StackVertical([top, t1, top]) def _restore_original(n, f, clip: vs.VideoNode, orig: vs.VideoNode, thresh_a: float, thresh_b: float): # Just revert the entire scaling if the difference is too big. This should catch the 1080p scenes. if f.props.PlaneStatsAverage < thresh_a: return clip.std.SetFrameProp("_descaled", intval=1) elif f.props.PlaneStatsAverage > thresh_b: return orig.std.SetFrameProp("_descaled", intval=0) return core.std.Merge(clip, orig, (f.props.PlaneStatsAverage - thresh_a) * 20).std.SetFrameProp("_descaled", intval=2) # Error handling if len(res) < 2: return error(funcname, 'This function requires more than two resolutions to descale to') og = clip clip32 = fvf.Depth(clip, 32) if one_plane(clip32): y = get_y(clip32) else: y, u, v = split(clip32) debic_listp = [_descaling(y, h, b, c) for h in res] debic_list = [a[0] for a in debic_listp] debic_props = [a[1] for a in debic_listp] y_deb = core.std.FrameEval(y, partial(_select, y=y, debic_list=debic_list, sraa_upscale=sraa_upscale, rfactor=rfactor), prop_src=debic_props) # TO-DO: It returns a frame size error here for whatever reason. Need to figure out what causes it and fix it dmask = core.std.PropToClip(y_deb) if show_dmask: return dmask # TO-DO: Figure out how to make it properly round depending on resolution (although this should usually be 1080p anyway) line = core.std.StackHorizontal([_square()]*192) full_squares = core.std.StackVertical([line]*108) artifacts = core.misc.Hysteresis(dmask.resize.Bicubic(clip32.width, clip32.height, _format=vs.GRAYS), core.std.Expr([get_y(clip32).tcanny.TCanny(sigma=3), full_squares], 'x y min')) ret_raw = kgf.retinex_edgemask(fvf.Depth(clip, 16)) ret = ret_raw.std.Binarize(30).rgvs.RemoveGrain(3) mask = core.std.Expr([ret.resize.Point(_format=vs.GRAYS), kgf.iterate(artifacts, core.std.Maximum, 3)], 'x y -').std.Binarize(0.4) mask = mask.std.Inflate().std.Convolution(matrix=[1]*9).std.Convolution(matrix=[1]*9) if show_mask: return mask y = core.std.MaskedMerge(y, y_deb, mask) merged = join([y, u, v]) if not one_plane(og) else y merged = fvf.Depth(merged, get_depth(og)) if no_mask: return merged dmask = dmask.std.PlaneStats() # TO-DO: It returns a frame size error here for whatever reason. Need to figure out what causes it and fix it return merged.std.FrameEval(partial(_restore_original, clip=merged, orig=og, thresh_a=thresh1, thresh_b=thresh2), prop_src=dmask)#.std.SetFrameProp("_descaled_resolution", intval=y_deb.height)
from vapoursynth import core import awsmfunc as awf import havsfunc as haf import fvsfunc as fvf import lvsfunc as lvf import G41Fun ep = 2 source = core.lsmas.LWLibavSource( f"Code.Lyoko.S01E{ep:02}.DVD.Remux.Multi-Odd_HD.mkv")[1130:] source = source.std.AssumeFPS(fpsnum=25) source = source.std.Crop(top=6, bottom=2, left=10, right=8) lyoko = awf.fb(source, left=1, right=1) lyoko = lyoko.vinverse.Vinverse() lyoko = fvf.Depth(lyoko, 16) no_filter = lyoko lyoko = awf.bbmod(lyoko, top=4, bottom=4, thresh=4, blur=4) lyoko = fvf.AutoDeblock(lyoko, adb1=0.4, adb2=0.1, adb1d=0.3, adb2d=1.4) aa2 = lvf.aa.upscaled_sraa(lyoko, rfactor=2.0, rep=True) lyoko = lvf.aa.upscaled_sraa(lyoko, rfactor=3.0, rep=True) lyoko = G41Fun.MaskedDHA(lyoko, rx=1.4, ry=1.4, darkstr=0.0, lowsens=70) lyoko = awf.ReplaceFrames( lyoko, aa2, "[4189 4238] [4931 5386] [6796 6845] [9084 9133] [14670 14876] [16507 16556] [19215 19512] [19560 19662] [21924 22287] [22348 22486] [22577 22751] [23495 23843] [24354 25444] [25498 26806] [26950 27304] [27947 28648] [28751 29253] [29421 29768] [29824 29905] [30008 30104] [30250 30304] [30391 30419] [30506 30590] [30659 30699] [31071 31323]" ) lyoko = lyoko.f3kdb.Deband(range=2, grainy=16, grainc=16) lyoko = fvf.ReplaceFrames(haf.Deblock_QED(no_filter), lyoko, "[0 33399] [33650 33655]") lyoko = fvf.Depth(lyoko, 10)
def hardsubmask(clip: vs.VideoNode, ref: vs.VideoNode, mode='default', expandN=None, highpass=25) -> vs.VideoNode: """ Uses multiple techniques to mask the hardsubs in video streams like Anime on Demand or Wakanim. Might (should) work for other hardsubs, too, as long as the subs are somewhat close to black/white. It's kinda experimental, but I wanted to try something like this. It works by finding the edge of the subtitle (where the black border and the white fill color touch), and it grows these areas into a regular brightness + difference mask via hysteresis. This should (in theory) reliably find all hardsubs in the image with barely any false positives (or none at all). Output depth and processing precision are the same as the input It is not necessary for 'clip' and 'ref' to have the same bit depth, as 'ref' will be dithered to match 'clip' Most of this code was written by Zastin (https://github.com/Z4ST1N) Clean code soon(tm) """ clp_f = clip.format bits = clp_f.bits_per_sample st = clp_f.sample_type peak = 1 if st == vs.FLOAT else (1 << bits) - 1 if expandN is None: expandN = clip.width // 200 # if mode in ['default', None]: out_fmt = core.register_format(vs.GRAY, st, bits, 0, 0) YUV_fmt = core.register_format(clp_f.color_family, vs.INTEGER, 8, clp_f.subsampling_w, clp_f.subsampling_h) y_range = 219 << (bits - 8) if st == vs.INTEGER else 1 uv_range = 224 << (bits - 8) if st == vs.INTEGER else 1 offset = 16 << (bits - 8) if st == vs.INTEGER else 0 uv_abs = ' abs ' if st == vs.FLOAT else ' {} - abs '.format( (1 << bits) // 2) yexpr = 'x y - abs {thr} > 255 0 ?'.format(thr=y_range * 0.7) uvexpr = 'x {uv_abs} {thr} < y {uv_abs} {thr} < and 255 0 ?'.format( uv_abs=uv_abs, thr=uv_range * 0.1) difexpr = 'x {upper} > x {lower} < or x y - abs {mindiff} > and 255 0 ?'.format( upper=y_range * 0.8 + offset, lower=y_range * 0.2 + offset, mindiff=y_range * 0.1) # right shift by 4 pixels. # fmtc uses at least 16 bit internally, so it's slower for 8 bit, # but its behaviour when shifting/replicating edge pixels makes it faster otherwise if bits < 16: right = core.resize.Point(clip, src_left=4) else: right = core.fmtc.resample(clip, sx=4, flt=False) subedge = core.std.Expr([clip, right], [yexpr, uvexpr], YUV_fmt.id) c444 = split( subedge.resize.Bicubic(format=vs.YUV444P8, filter_param_a=0, filter_param_b=0.5)) subedge = core.std.Expr(c444, 'x y z min min') clip, ref = getY(clip), getY(ref) ref = ref if clip.format == ref.format else fvf.Depth(ref, bits) clips = [clip.std.Convolution([1] * 9), ref.std.Convolution([1] * 9)] diff = core.std.Expr(clips, difexpr, vs.GRAY8).std.Maximum().std.Maximum() mask = core.misc.Hysteresis(subedge, diff) mask = iterate(mask, core.std.Maximum, expandN) mask = mask.std.Inflate().std.Inflate().std.Convolution([1] * 9) mask = fvf.Depth(mask, bits, range=1, range_in=1) """ # needs some more testing elif mode == 'fast': highpass = highpass << (bits - 8) if st == vs.INTEGER else highpass / 255 clip, ref = getY(clip), getY(ref) ref = ref if clip.format == ref.format else fvf.Depth(ref, bits) edge = clip.std.Sobel() diff = core.std.Expr([clip, ref], 'x y - abs {:d} < 0 {:d} ?'.format(highpass, peak)) mask = core.misc.Hysteresis(edge, diff) mask = iterate(mask, core.std.Maximum, expandN) mask = mask.std.Convolution([1] * 9) else: raise ValueError('hardsubmask: Unknown mode') """ return mask
def faggotdb_mod(clip: vs.VideoNode, thrY=40, thrC=None, radiusY=15, radiusC=15, CbY=44, CrY=44, CbC=44, CrC=44, grainY=32, grainC=None, grainCC=0, sample_mode=2, neo=True, dynamic_grainY=False, dynamic_grainC=False, tv_range=True, mask="retinex", binarize=False, binarize_thr=70, grayscale=True, bitresamp=False, outbits=None, blurmask=True, horizblur=2, vertblur=2) -> vs.VideoNode: funcName = "faggotdb_mod" # name kept to be fallback compatible # Original Idea: Author who created Fag3kdb. Edited by AlucardSama04; additional modifications by l00t if not isinstance(clip, vs.VideoNode): raise TypeError(f"{funcName}: This is not a clip") if outbits is None: outbits = clip.format.bits_per_sample if bitresamp: if clip.format.bits_per_sample != outbits: clip = fvf.Depth( clip, bits=outbits) # instead of error, auto convert to 16 bits elif clip.format.bits_per_sample != outbits: raise TypeError(f"{funcName}: Input-output bitdepth mismatch") # if not isinstance(mask, vs.VideoNode): # raise vs.Error(f"{funcName}: mask' only clip inputs") if mask in [-1]: # more user friendly if we do the masking intentionally mask = clip blurmask = False elif mask in [0, "retinex"]: mask = kgf.retinex_edgemask(clip) elif mask in [1, "kirsch"]: mask = kgf.kirsch(clip) elif mask in [2, "Sobel"]: mask = core.std.Sobel(clip, scale=1) elif mask in [3, "Prewitt"]: mask = core.std.Prewitt(clip) elif mask in [4, "GF"]: mask = fvf.GradFun3(clip, mask=2, debug=1) else: raise ValueError(f"{funcName}: Unknown Mask Mode") if grayscale: mask = core.std.ShufflePlanes(mask, planes=0, colorfamily=vs.GRAY) if binarize: # binarize threshold should be adjusted according to bitdepth mask = core.std.Binarize(mask, threshold=binarize_thr) if blurmask: mask = core.std.BoxBlur(mask, hradius=horizblur, vradius=vertblur) if thrC is None: thrC = int(round(thrY / 2)) if grainC is None: grainC = int(round(grainY / 2)) if grainCC is None: grainCC = 0 f3kdb = core.neo_f3kdb.Deband if neo else core.f3kdb.Deband U = plane(clip, 1) V = plane(clip, 2) U = f3kdb(U, range=radiusC, y=thrC, cb=CbC, cr=CrC, grainy=grainC, grainc=0, sample_mode=sample_mode, dynamic_grain=dynamic_grainC, keep_tv_range=tv_range, output_depth=outbits) V = f3kdb(V, range=radiusC, y=thrC, cb=CbC, cr=CrC, grainy=grainC, grainc=0, sample_mode=sample_mode, dynamic_grain=dynamic_grainC, keep_tv_range=tv_range, output_depth=outbits) filtered = core.std.ShufflePlanes([clip, U, V], [0, 0, 0], vs.YUV) filtered = f3kdb(filtered, range=radiusY, y=thrY, cb=CbY, cr=CrY, grainy=grainY, grainc=grainCC, sample_mode=sample_mode, dynamic_grain=dynamic_grainY, keep_tv_range=tv_range, output_depth=outbits ) # if grainCC > 0 UV planes will be debanded once again return core.std.MaskedMerge(filtered, clip, mask)
def quick_output(clip: vs.VideoNode, bitdepth=8): """ A function for quickly preparing the clip for the output node. """ if vsutil.get_depth != bitdepth: return fvf.Depth(clip, bitdepth)
def simple_aa(src, aatype='nnedi3', aatypeuv=None, mask=None, kernel='spline36', nsize=3, nns=4, qual=2, alpha=0.5, beta=0.2, nrad=3, mdis=30): """ Basic nnedi3/eedi3 anti-aliasing with optional use of external mask Default values should be good for most anti-aliasing. By default it will use ocl for eedi3 and cpu for nnedi3 (znedi3). Parameters: ----------- aatype ('nnedi3'): type of anti-aliasing to use ('nnedi3', 'eedi3', 'combo') aatypeuv: type of anti-aliasing to use on chroma (disabled by default) mask: optional, external mask to use kernel ('spline36'): kernel to downsample interpolated clip nnedi3/eedi3 specific parameters """ def perform_aa(src, aatype): sw = src.width sh = src.height if aatype == 'nnedi3': aa = core.znedi3.nnedi3(src, field=1, dh=True, nsize=nsize, nns=nns, qual=qual).std.Transpose() aa = core.znedi3.nnedi3(aa, field=1, dh=True, nsize=nsize, nns=nns, qual=qual).std.Transpose() elif aatype == 'eedi3': aa = core.eedi3m.EEDI3CL(src, field=1, dh=True, alpha=alpha, beta=beta, nrad=nrad, mdis=mdis).std.Transpose() aa = core.eedi3m.EEDI3CL(aa, field=1, dh=True, alpha=alpha, beta=beta, nrad=nrad, mdis=mdis).std.Transpose() elif aatype == 'combo': aa = core.eedi3m.EEDI3CL(src, field=1, dh=True, alpha=alpha, beta=beta, nrad=nrad, mdis=mdis) aa = core.znedi3.nnedi3(aa, field=0, dh=True, nsize=nsize, nns=nns, qual=qual).std.Transpose() aa = core.eedi3m.EEDI3CL(aa, field=1, dh=True, alpha=alpha, beta=beta, nrad=nrad, mdis=mdis) aa = core.znedi3.nnedi3(aa, field=0, dh=True, nsize=nsize, nns=nns, qual=qual).std.Transpose() return fvf.Resize(aa, w=sw, h=sh, sx=-0.5, sy=-0.5, kernel=kernel) name = 'simple_aa' valid_aatypes = ['nnedi3', 'eedi3', 'combo'] if isinstance(aatype, str): aatype = aatype.lower() if isinstance(aatypeuv, str): aatypeuv = aatypeuv.lower() if aatype is not None and aatype not in valid_aatypes: raise TypeError(name + ": 'aatype' must be 'nnedi3', 'eedi3', or 'combo'") if aatypeuv is not None and aatypeuv not in valid_aatypes: raise TypeError(name + ": 'aatypeuv' must be 'nnedi3', 'eedi3', or 'combo'") if src.format.color_family not in [vs.GRAY, vs.YUV]: raise TypeError(name + ": src clip must be GRAY or YUV") src_bits = src.format.bits_per_sample is_gray = src.format.color_family == vs.GRAY if mask is not None and mask.format.bits_per_sample != src_bits: mask = fvf.Depth(mask, src_bits, dither_type='none') planes = [src] if is_gray else get_yuv(src) if aatype is not None: planes[0] = perform_aa(planes[0], aatype) if aatypeuv is not None: planes[1:] = [perform_aa(plane, aatypeuv) for plane in planes[1:]] aa = to_yuv(planes) if mask is not None: aa = core.std.MaskedMerge(src, aa, mask) return aa
def recursive_apply_mask( src_a: vs.VideoNode, src_b: vs.VideoNode, mask_folder: Union[str, Path], iter: int = 1, ) -> vs.VideoNode: """ Recursively check `mask_folder` for a .png or .ass file After it found all of them, it will use the mask to merge together the two clips. Acceptable filename format: - frameNum.png - frameStart-frameEnd.png - itsUpToYou.ass Example: - 2500.png - 2000-2004.png - maskep1.ass Parameters ---------- src_a: :class:`VideoNode` The first clip. src_b: :class:`VideoNode` The second clip. mask_folder: :class:`Union[str, Path]` The folder that contains the masks. iter: :class:`int` How many times we will need to iterate the mask. Returns ------- :class:`VideoNode` The merged clip. """ if isinstance(mask_folder, str): mask_folder = Path(mask_folder) has_plugin_or_raise(["fmtc", "imwri"]) imwri = core.imwri masks_png = mask_folder.glob("*.png") for mask in masks_png: frame = os_path.basename(mask).rsplit(".", 1)[0] frame = [int(i) for i in frame.split("-")][:2] if len(frame) < 2: frame = [frame[0], frame[0]] first_f, last_f = frame image = fvf.Depth( imwri.Read(str(mask), ).resize.Point(format=vs.GRAYS, matrix_s="709"), src_a.format.bits_per_sample, ).std.AssumeFPS( fpsnum=src_a.fps.numerator, fpsden=src_a.fps.denominator, ) src_a_n, src_b_n = src_a[first_f:last_f + 1], src_b[first_f:last_f + 1] image = image * ((last_f + 1) - first_f) image = get_y(image) src_masked = core.std.MaskedMerge(src_a_n, src_b_n, image) for _ in range(iter - 1): src_masked = src_masked.std.MaskedMerge(src_b_n, image) src_a = insert_clip(src_a, insert=src_masked, start_frame=first_f) masks_ass = mask_folder.glob("*.ass") for mask in masks_ass: blank_mask = src_a.std.BlankClip() ass_mask = get_y(blank_mask.sub.TextFile(str(mask))) src_a = core.std.MaskedMerge(src_a, src_b, ass_mask) return src_a
def adaptive_scaling( src: vs.VideoNode, target_w: Optional[IntegerFloat] = None, target_h: Optional[IntegerFloat] = None, descale_range: List[int] = [], kernel: DescaleKernel = "bicubic", b: IntegerFloat = 1 / 3, c: IntegerFloat = 1 / 3, taps: int = 3, iter_max: int = 3, rescale: bool = True, use_gpu: bool = False, show_native_res: bool = False, show_mask: bool = False, ): """ Descale a video within the range and upscale it back to the target_w and target_h. If target is not defined, it will use original resolution. Written originally by kageru, modified by N4O. Valid kernel: - bicubic - bilinear - lanczos - spline16 - spline36 - spline64 Default to ``bicubic`` kernel. Parameters ---------- src: :class:`VideoNode` The video source. target_w: :class:`Union[int, float]` Target final video width. target_h: :class:`Union[int, float]` Target final video height. descale_range: :class:`List[Union[int, float]]` List of number of descale target height. kernel: :class:`DescaleKernel` Kernel used for descaling. b: :class:`Union[int, float]` B-parameter for the kernel. c: :class:`Union[int, float]` C-parameter for the kernel. taps: :class:`int` Lanczos taps for the kernel. iter_max: :class:`int` Iteration count that will expand the mask size. rescale: :class:`bool` Rescale the video if ``True``. use_gpu: :class:`bool` Use GPU for rescaling (need nnedi3cl). show_native_res: :class:`bool` Show a text notifying what the native resolution are. show_mask: :class:`bool` Do you want to show the mask or not. """ target_w = src.width if target_w is None else target_w target_h = src.height if target_h is None else target_h kernel = kernel.lower() # type: ignore if kernel not in VALID_KERNELS: raise ValueError(f"adaptive_scaling: Invalid kernel: {kernel}") if not isinstance(src, vs.VideoNode): raise TypeError("adaptive_scaling: The source must be a clip.") if not isinstance(descale_range, (list, tuple)): raise TypeError("adaptive_scaling: descale_range must be a list.") descale_range = list(descale_range) if len(descale_range) != 2: raise ValueError( "adaptive_scaling: descale_range must have only 2 elements.") if descale_range[0] > descale_range[1]: raise ValueError( "adaptive_scaling: descale_range first value cannot be larger than second value" ) if rescale: if descale_range[0] > target_h or descale_range[1] > target_h: raise ValueError( "adaptive_scaling: One of the descale_range value cannot be larger than target_h" ) has_plugin_or_raise( ["retinex", "tcanny", "descale", "fmtc", "rgvs", "nnedi3"]) target_w = int(round(target_w)) target_h = int(round(target_h)) if target_w % 2 != 0: raise ValueError("adaptive_scaling: target_w must be even.") if target_h % 2 != 0: raise ValueError("adaptive_scaling: target_h must be even.") ref = src ref_d = ref.format.bits_per_sample clip32 = fvf.Depth(ref, 32) y = get_y(clip32) global_clip_resizer = _get_resizer(b, c, taps, kernel) global_clip_descaler = partial(core.descale.Descale, kernel=kernel, b=b, c=c, taps=taps) def simple_descale(y_clip: vs.VideoNode, h: int) -> Tuple[vs.VideoNode, vs.VideoNode]: ar = y_clip.width / y_clip.height down = global_clip_descaler(y_clip, get_w(h, ar), h) if rescale: up = global_clip_resizer(down, target_w, target_h) else: up = global_clip_resizer(down, y_clip.width, y_clip.height) diff = core.std.Expr([y, up], "x y - abs").std.PlaneStats() return down, diff descaled_clips_list = [ simple_descale(y, h) for h in range(descale_range[0], descale_range[1]) ] descaled_clips = [clip[0] for clip in descaled_clips_list] descaled_props = [clip[1] for clip in descaled_clips_list] if not rescale: y = global_clip_resizer(y, target_w, target_h) clip32 = global_clip_resizer(clip32, target_w, target_h) def select_scale(n: int, f: List[vs.VideoFrame], descale_list: List[vs.VideoNode]): errors = [x.props["PlaneStatsAverage"] for x in f] y_deb = descale_list[errors.index(min(errors))] # type: ignore dmask = core.std.Expr( [y, global_clip_resizer(y_deb, target_w, target_h)], "x y - abs 0.025 > 1 0 ?").std.Maximum() y_deb16 = fvf.Depth(y_deb, 16) if rescale: y_scaled = upscale_nnedi3( y_deb16, nns=4, correct_shift=True, width=target_w, height=target_h, use_gpu=use_gpu # type: ignore ).fmtc.bitdepth(bits=32) else: y_scaled = global_clip_resizer(y_deb16, target_w, target_h).fmtc.bitdepth(bits=32) dmask = global_clip_resizer(dmask, target_w, target_h) if show_native_res and not show_mask: y_scaled = y_scaled.text.Text( f"Native resolution for this frame: {y_deb.height}") return core.std.ClipToProp(y_scaled, dmask) y_deb = core.std.FrameEval(y, partial(select_scale, descale_list=descaled_clips), prop_src=descaled_props) dmask = core.std.PropToClip(y_deb) line = core.std.StackHorizontal([_square_clip(y)] * (target_w // 10)) full = core.std.StackVertical([line] * (target_h // 10)) line_mask = global_clip_resizer(full, target_w, target_h) artifacts = core.misc.Hysteresis( global_clip_resizer(dmask, target_w, target_h, format=vs.GRAYS), core.std.Expr([get_y(clip32).tcanny.TCanny(sigma=3), line_mask], "x y min"), ) ret_raw = _retinex_edgemask(ref) if not rescale: ret_raw = global_clip_resizer(ret_raw, target_w, target_h) ret = ret_raw.std.Binarize(30).rgvs.RemoveGrain(3) mask = core.std.Expr([ iterate(artifacts, core.std.Maximum, iter_max), ret.resize.Point(format=vs.GRAYS) ], "y x -").std.Binarize(0.4) mask = mask.std.Inflate().std.Convolution([1] * 9).std.Convolution([1] * 9) if show_mask: return mask merged = core.std.MaskedMerge(y, y_deb, mask) merged = core.std.ShufflePlanes([merged, clip32], [0, 1, 2], vs.YUV) return fvf.Depth(merged, ref_d)
import fvsfunc as fvf import lvsfunc as lvf import G41Fun lyokos = [] for ep in [3, 11, 12, 13, 14, 15, 17, 18, 19, 21, 22, 23, 24, 25, 26]: src = core.lsmas.LWLibavSource( f"Code.Lyoko.S01E{ep:02}.DVD.Remux.Multi-Odd_HD.mkv")[:1130] src = src.std.AssumeFPS(fpsnum=25) lyokos.append( fvf.Depth( awf.bbmod(fvf.AutoDeblock(src.vinverse.Vinverse(), adb1=0.2, adb2=0.8, adb1d=0.1, adb2d=1.4), top=6, bottom=4, thresh=4, blur=6), 16)) average = core.average.Mean(lyokos) average = average.std.Crop(top=2, bottom=2, left=2, right=2) lyoko = lvf.aa.upscaled_sraa(average, rfactor=2.0, rep=True) deband = lyoko.f3kdb.Deband(range=2, grainy=16, grainc=16) deband = awf.ReplaceFrames(deband, lyoko.f3kdb.Deband(range=15, grainy=32, grainc=32), "[417 425]")