################################################################################################################################ ## mvsfunc - mawen1250's VapourSynth functions ################################################################################################################################ ## Requirments: ## fmtconv ## BM3D ################################################################################################################################ ## Main functions: ## Depth ## ToRGB ## ToYUV ## BM3D ## VFRSplice ################################################################################################################################ ## Runtime functions: ## PlaneStatistics ## PlaneCompare ## ShowAverage ## FilterIf ## FilterCombed ################################################################################################################################ ## Utility functions: ## Min ## Max ## Avg ## MinFilter ## MaxFilter ## LimitFilter ## PointPower ## CheckMatrix ## postfix2infix ################################################################################################################################ ## Frame property functions: ## SetColorSpace ## AssumeFrame ## AssumeTFF ## AssumeBFF ## AssumeField ## AssumeCombed ################################################################################################################################ ## Helper functions: ## CheckVersion ## GetMatrix ## zDepth ## PlaneAverage ## GetPlane ## GrayScale ## Preview ## CheckColorFamily ## RemoveFrameProp ## RegisterFormat ################################################################################################################################ import vapoursynth as vs import functools import math from collections.abc import Sequence __version__ = '11' if not hasattr(vs, 'core'): core = vs.get_core() else: core = vs.core ################################################################################################################################ VSMaxPlaneNum = 3 ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Main functions below ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Main function: Depth() ################################################################################################################################ ## Bit depth conversion with dithering (if needed). ## It's a wrapper for fmtc.bitdepth and zDepth (core.resize/zimg). ## Only constant format is supported, frame properties of the input clip is mostly ignored (only available with zDepth). ################################################################################################################################ ## Basic parameters ## input {clip}: clip to be converted ## can be of YUV/RGB/Gray color family, can be of 8~16 bit integer or 16/32 bit float ## depth {int}: output bit depth, can be 1~16 bit integer or 16/32 bit float ## note that 1~7 bit content is still stored as 8 bit integer format ## default is the same as that of the input clip ## sample {int}: output sample type, can be 0 (vs.INTEGER) or 1 (vs.FLOAT) ## default is the same as that of the input clip ## fulls {bool}: define if input clip is of full range ## default: None, assume True for RGB/YCgCo input, assume False for Gray/YUV input ## fulld {bool}: define if output clip is of full range ## default is the same as "fulls" ################################################################################################################################ ## Advanced parameters ## dither {int|str}: dithering algorithm applied for depth conversion ## - {int}: same as "dmode" in fmtc.bitdepth, will be automatically converted if using zDepth ## - {str}: same as "dither_type" in zDepth, will be automatically converted if using fmtc.bitdepth ## - default: ## - output depth is 32, and conversions without quantization error: 1 | "none" ## - otherwise: 3 | "error_diffusion" ## useZ {bool}: prefer zDepth or fmtc.bitdepth for depth conversion ## When 11,13~15 bit integer or 16 bit float is involved, zDepth is always used. ## - False: prefer fmtc.bitdepth ## - True: prefer zDepth ## default: False ################################################################################################################################ ## Parameters of fmtc.bitdepth ## ampo, ampn, dyn, staticnoise, cpuopt, patsize, tpdfo, tpdfn, corplane: ## same as those in fmtc.bitdepth, ignored when useZ=True ## *NOTE* no positional arguments, only keyword arguments are accepted ################################################################################################################################ def Depth(input, depth=None, sample=None, fulls=None, fulld=None, dither=None, useZ=None, **kwargs): # input clip clip = input if not isinstance(input, vs.VideoNode): raise type_error('"input" must be a clip!') ## Default values for kwargs if 'ampn' not in kwargs: kwargs['ampn'] = None if 'ampo' not in kwargs: kwargs['ampo'] = None # Get properties of input clip sFormat = input.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsYUV = sColorFamily == vs.YUV sIsGRAY = sColorFamily == vs.GRAY sbitPS = sFormat.bits_per_sample sSType = sFormat.sample_type if fulls is None: # If not set, assume limited range for YUV and Gray input fulls = False if sIsYUV or sIsGRAY else True elif not isinstance(fulls, int): raise type_error('"fulls" must be a bool!') # Get properties of output clip lowDepth = False if depth is None: dbitPS = sbitPS elif not isinstance(depth, int): raise type_error('"depth" must be an int!') else: if depth < 8: dbitPS = 8 lowDepth = True else: dbitPS = depth if sample is None: if depth is None: dSType = sSType depth = dbitPS else: dSType = vs.FLOAT if dbitPS >= 32 else vs.INTEGER elif not isinstance(sample, int): raise type_error('"sample" must be an int!') elif sample != vs.INTEGER and sample != vs.FLOAT: raise value_error('"sample" must be either 0 (vs.INTEGER) or 1 (vs.FLOAT)!') else: dSType = sample if depth is None and sSType != vs.FLOAT and sample == vs.FLOAT: dbitPS = 32 elif depth is None and sSType != vs.INTEGER and sample == vs.INTEGER: dbitPS = 16 if dSType == vs.INTEGER and (dbitPS < 1 or dbitPS > 16): raise value_error(f'{dbitPS}-bit integer output is not supported!') if dSType == vs.FLOAT and (dbitPS != 16 and dbitPS != 32): raise value_error(f'{dbitPS}-bit float output is not supported!') if fulld is None: fulld = fulls elif not isinstance(fulld, int): raise type_error('"fulld" must be a bool!') # Low-depth support if lowDepth: if dither == "none" or dither == 1: clip = _quantization_conversion(clip, sbitPS, depth, vs.INTEGER, fulls, fulld, False, False, 8, 0) clip = _quantization_conversion(clip, depth, 8, vs.INTEGER, fulld, fulld, False, False, 8, 0) return clip else: full = fulld clip = _quantization_conversion(clip, sbitPS, depth, vs.INTEGER, fulls, full, False, False, 16, 1) sSType = vs.INTEGER sbitPS = 16 fulls = False fulld = False # Whether to use zDepth or fmtc.bitdepth for conversion # When 11,13~15 bit integer or 16 bit float is involved, force using zDepth if useZ is None: useZ = False elif not isinstance(useZ, int): raise type_error('"useZ" must be a bool!') if sSType == vs.INTEGER and (sbitPS == 13 or sbitPS == 15): useZ = True if dSType == vs.INTEGER and (dbitPS == 11 or 13 <= dbitPS <= 15): useZ = True if (sSType == vs.FLOAT and sbitPS < 32) or (dSType == vs.FLOAT and dbitPS < 32): useZ = True # Dithering type if kwargs['ampn'] is not None and not isinstance(kwargs['ampn'], (int, float)): raise type_error('"ampn" must be an int or a float!') if dither is None: if dbitPS == 32 or (dbitPS >= sbitPS and fulld == fulls and fulld == False): dither = "none" if useZ else 1 else: dither = "error_diffusion" if useZ else 3 elif not isinstance(dither, (int, str)): raise type_error('"dither" must be an int or a str!') else: if isinstance(dither, str): dither = dither.lower() if dither != "none" and dither != "ordered" and dither != "random" and dither != "error_diffusion": raise value_error('Unsupported "dither" specified!') else: if dither < 0 or dither > 9: raise value_error('Unsupported "dither" specified!') if useZ and isinstance(dither, int): if dither == 0: dither = "ordered" elif dither == 1 or dither == 2: if kwargs['ampn'] is not None and kwargs['ampn'] > 0: dither = "random" else: dither = "none" else: dither = "error_diffusion" elif not useZ and isinstance(dither, str): if dither == "none": dither = 1 elif dither == "ordered": dither = 0 elif dither == "random": if kwargs['ampn'] is None: dither = 1 kwargs['ampn'] = 1 elif kwargs['ampn'] > 0: dither = 1 else: dither = 3 else: dither = 3 if not useZ: if kwargs['ampo'] is None: kwargs['ampo'] = 1.5 if dither == 0 else 1 elif not isinstance(kwargs['ampo'], (int, float)): raise type_error('"ampo" must be an int or a float!') # Skip processing if not needed if dSType == sSType and dbitPS == sbitPS and (sSType == vs.FLOAT or fulld == fulls) and not lowDepth: return clip # Apply conversion if useZ: clip = zDepth(clip, sample=dSType, depth=dbitPS, range=fulld, range_in=fulls, dither_type=dither) else: clip = core.fmtc.bitdepth(clip, bits=dbitPS, flt=dSType, fulls=fulls, fulld=fulld, dmode=dither, **kwargs) clip = SetColorSpace(clip, ColorRange=0 if fulld else 1) # Low-depth support if lowDepth: clip = _quantization_conversion(clip, depth, 8, vs.INTEGER, full, full, False, False, 8, 0) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Main function: ToRGB() ################################################################################################################################ ## A wrapper of fmtconv to convert any color space to full range RGB. ## Thus, if input is limited range RGB, it will be converted to full range. ## If matrix is 10, "2020cl" or "bt2020c", the output is linear RGB. ## Only constant format is supported, frame properties of the input clip is mostly ignored. ## Note that you may get faster speed with core.resize, or not (for now, dither_type='error_diffusion' is slow). ## It's recommended to use Preview() for previewing now. ################################################################################################################################ ## Basic parameters ## input {clip}: clip to be converted ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## matrix {int|str}: color matrix of input clip, only makes sense for YUV input ## decides the conversion coefficients from YUV to RGB ## check GetMatrix() for available values ## default: None, guessed according to the color family and size of input clip ## depth {int}: output bit depth, can be 1-16 bit integer or 16/32 bit float ## note that 1-7 bit content is still stored as 8 bit integer format ## default is the same as that of the input clip ## sample {int}: output sample type, can be 0 (vs.INTEGER) or 1 (vs.FLOAT) ## default is the same as that of the input clip ## full {bool}: define if input clip is of full range ## default: guessed according to the color family of input clip and "matrix" ################################################################################################################################ ## Parameters of resampling ## kernel, taps, a1, a2, cplace: ## used for chroma re-sampling, same as those in fmtc.resample ## default: kernel="bicubic", a1=0, a2=0.5, also known as "Catmull-Rom". ################################################################################################################################ ## Parameters of depth conversion ## dither, useZ, ampo, ampn, dyn, staticnoise, cpuopt, patsize, tpdfo, tpdfn, corplane: ## same as those in Depth() ## *NOTE* no positional arguments, only keyword arguments are accepted ################################################################################################################################ def ToRGB(input, matrix=None, depth=None, sample=None, full=None, kernel=None, taps=None, a1=None, a2=None, cplace=None, **kwargs): # input clip clip = input if not isinstance(input, vs.VideoNode): raise type_error('"input" must be a clip!') # Get string format parameter "matrix" matrix = GetMatrix(input, matrix, True) # Get properties of input clip sFormat = input.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsRGB = sColorFamily == vs.RGB sIsYUV = sColorFamily == vs.YUV sIsGRAY = sColorFamily == vs.GRAY sbitPS = sFormat.bits_per_sample sSType = sFormat.sample_type sHSubS = 1 << sFormat.subsampling_w sVSubS = 1 << sFormat.subsampling_h if full is None: # If not set, assume limited range for YUV and Gray input # Assume full range for YCgCo and OPP input if (sIsGRAY or sIsYUV) and (matrix == "RGB" or matrix == "YCgCo" or matrix == "OPP"): fulls = True else: fulls = False if sIsYUV or sIsGRAY else True elif not isinstance(full, int): raise type_error('"full" must be a bool!') else: fulls = full # Get properties of output clip if depth is None: dbitPS = sbitPS elif not isinstance(depth, int): raise type_error('"depth" must be an int!') else: dbitPS = depth if sample is None: if depth is None: dSType = sSType else: dSType = vs.FLOAT if dbitPS >= 32 else vs.INTEGER elif not isinstance(sample, int): raise type_error('"sample" must be an int!') elif sample != vs.INTEGER and sample != vs.FLOAT: raise value_error('"sample" must be either 0 (vs.INTEGER) or 1 (vs.FLOAT)!') else: dSType = sample if depth is None and sSType != vs.FLOAT and sample == vs.FLOAT: dbitPS = 32 elif depth is None and sSType != vs.INTEGER and sample == vs.INTEGER: dbitPS = 16 if dSType == vs.INTEGER and (dbitPS < 1 or dbitPS > 16): raise value_error(f'{dbitPS}-bit integer output is not supported!') if dSType == vs.FLOAT and (dbitPS != 16 and dbitPS != 32): raise value_error(f'{dbitPS}-bit float output is not supported!') fulld = True # Get properties of internal processed clip pSType = max(sSType, dSType) # If float sample type is involved, then use float for conversion if pSType == vs.FLOAT: # For float sample type, only 32-bit is supported by fmtconv pbitPS = 32 else: # Apply conversion in the higher one of input and output bit depth pbitPS = max(sbitPS, dbitPS) # For integer sample type, only 8-, 9-, 10-, 12-, 16-bit is supported by fmtc.matrix if sHSubS != 1 or sVSubS != 1: # When chroma re-sampling is needed, always process in 16-bit for integer sample type pbitPS = 16 elif pbitPS == 11: pbitPS = 12 elif pbitPS > 12 and pbitPS < 16: pbitPS = 16 # fmtc.resample parameters if kernel is None: kernel = "bicubic" if a1 is None and a2 is None: a1 = 0 a2 = 0.5 elif not isinstance(kernel, str): raise type_error('"kernel" must be a str!') # Conversion if sIsRGB: # Skip matrix conversion for RGB input # Apply depth conversion for output clip clip = Depth(clip, dbitPS, dSType, fulls, fulld, **kwargs) elif sIsGRAY: # Apply depth conversion for output clip clip = Depth(clip, dbitPS, dSType, fulls, fulld, **kwargs) # Shuffle planes for Gray input clip = core.std.ShufflePlanes([clip,clip,clip], [0,0,0], vs.RGB) # Set output frame properties clip = SetColorSpace(clip, Matrix=0) else: # Apply chroma up-sampling if needed if sHSubS != 1 or sVSubS != 1: clip = core.fmtc.resample(clip, kernel=kernel, taps=taps, a1=a1, a2=a2, css="444", planes=[2,3,3], fulls=fulls, fulld=fulls, cplace=cplace, flt=pSType==vs.FLOAT) # Apply depth conversion for processed clip else: clip = Depth(clip, pbitPS, pSType, fulls, fulls, **kwargs) # Apply matrix conversion for YUV input if matrix == "OPP": clip = core.fmtc.matrix(clip, fulls=fulls, fulld=fulld, coef=[1,1,2/3,0, 1,0,-4/3,0, 1,-1,2/3,0], col_fam=vs.RGB) clip = SetColorSpace(clip, Matrix=0) elif matrix == "2020cl": clip = core.fmtc.matrix2020cl(clip, full=fulls) else: clip = core.fmtc.matrix(clip, mat=matrix, fulls=fulls, fulld=fulld, col_fam=vs.RGB) # Apply depth conversion for output clip clip = Depth(clip, dbitPS, dSType, fulld, fulld, **kwargs) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Main function: ToYUV() ################################################################################################################################ ## A wrapper of fmtconv to convert any color space to YUV with/without sub-sampling. ## If input is RGB, it's assumed to be of full range. ## Thus, limited range RGB clip should first be manually converted to full range before calling this function. ## If matrix is 10, "2020cl" or "bt2020c", the input should be linear RGB. ## Only constant format is supported, frame properties of the input clip is mostly ignored. ## Note that you may get faster speed with core.resize, or not (for now, dither_type='error_diffusion' is slow). ################################################################################################################################ ## Basic parameters ## input {clip}: clip to be converted ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## matrix {int|str}: color matrix of output clip ## decides the conversion coefficients from RGB to YUV ## check GetMatrix() for available values ## default: None, guessed according to the color family and size of input clip ## css {str}: chroma sub-sampling of output clip, similar to the one in fmtc.resample ## If two number is defined, then the first is horizontal sub-sampling and the second is vertical sub-sampling. ## For example, "11" is 4:4:4, "21" is 4:2:2, "22" is 4:2:0. ## preset values: ## - "444" | "4:4:4" | "11" ## - "440" | "4:4:0" | "12" ## - "422" | "4:2:2" | "21" ## - "420" | "4:2:0" | "22" ## - "411" | "4:1:1" | "41" ## - "410" | "4:1:0" | "42" ## default: 4:4:4 for RGB/Gray input, same as input is for YUV input ## depth {int}: output bit depth, can be 1-16 bit integer or 16/32 bit float ## note that 1-7 bit content is still stored as 8 bit integer format ## default is the same as that of the input clip ## sample {int}: output sample type, can be 0 (vs.INTEGER) or 1 (vs.FLOAT) ## default is the same as that of the input clip ## full {bool}: define if input/output Gray/YUV clip is of full range ## default: guessed according to the color family of input clip and "matrix" ################################################################################################################################ ## Parameters of resampling ## kernel, taps, a1, a2, cplace: ## used for chroma re-sampling, same as those in fmtc.resample ## default: kernel="bicubic", a1=0, a2=0.5, also known as "Catmull-Rom" ################################################################################################################################ ## Parameters of depth conversion ## dither, useZ, ampo, ampn, dyn, staticnoise, cpuopt, patsize, tpdfo, tpdfn, corplane: ## same as those in Depth() ## *NOTE* no positional arguments, only keyword arguments are accepted ################################################################################################################################ def ToYUV(input, matrix=None, css=None, depth=None, sample=None, full=None, kernel=None, taps=None, a1=None, a2=None, cplace=None, **kwargs): # input clip clip = input if not isinstance(input, vs.VideoNode): raise type_error('"input" must be a clip!') # Get string format parameter "matrix" matrix = GetMatrix(input, matrix, False) # Get properties of input clip sFormat = input.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsRGB = sColorFamily == vs.RGB sIsYUV = sColorFamily == vs.YUV sIsGRAY = sColorFamily == vs.GRAY sbitPS = sFormat.bits_per_sample sSType = sFormat.sample_type sHSubS = 1 << sFormat.subsampling_w sVSubS = 1 << sFormat.subsampling_h if sIsRGB: # Always assume full range for RGB input fulls = True elif full is None: # If not set, assume limited range for YUV and Gray input # Assume full range for YCgCo and OPP input if (sIsGRAY or sIsYUV) and (matrix == "RGB" or matrix == "YCgCo" or matrix == "OPP"): fulls = True else: fulls = False if sIsYUV or sIsGRAY else True elif not isinstance(full, int): raise type_error('"full" must be a bool!') else: fulls = full # Get properties of output clip if depth is None: dbitPS = sbitPS elif not isinstance(depth, int): raise type_error('"depth" must be an int!') else: dbitPS = depth if sample is None: if depth is None: dSType = sSType else: dSType = vs.FLOAT if dbitPS >= 32 else vs.INTEGER elif not isinstance(sample, int): raise type_error('"sample" must be an int!') elif sample != vs.INTEGER and sample != vs.FLOAT: raise value_error('"sample" must be either 0 (vs.INTEGER) or 1 (vs.FLOAT)!') else: dSType = sample if depth is None and sSType != vs.FLOAT and sample == vs.FLOAT: dbitPS = 32 elif depth is None and sSType != vs.INTEGER and sample == vs.INTEGER: dbitPS = 16 if dSType == vs.INTEGER and (dbitPS < 1 or dbitPS > 16): raise value_error(f'{dbitPS}-bit integer output is not supported!') if dSType == vs.FLOAT and (dbitPS != 16 and dbitPS != 32): raise value_error(f'{dbitPS}-bit float output is not supported!') if full is None: # If not set, assume limited range for YUV and Gray output # Assume full range for YCgCo and OPP output if matrix == "RGB" or matrix == "YCgCo" or matrix == "OPP": fulld = True else: fulld = False elif not isinstance(full, int): raise type_error('"full" must be a bool!') else: fulld = full # Chroma sub-sampling parameters if css is None: dHSubS = sHSubS dVSubS = sVSubS css = f'{dHSubS}{dVSubS}' elif not isinstance(css, str): raise type_error('"css" must be a str!') else: if css == "444" or css == "4:4:4": css = "11" elif css == "440" or css == "4:4:0": css = "12" elif css == "422" or css == "4:2:2": css = "21" elif css == "420" or css == "4:2:0": css = "22" elif css == "411" or css == "4:1:1": css = "41" elif css == "410" or css == "4:1:0": css = "42" dHSubS = int(css[0]) dVSubS = int(css[1]) # Get properties of internal processed clip pSType = max(sSType, dSType) # If float sample type is involved, then use float for conversion if pSType == vs.FLOAT: # For float sample type, only 32-bit is supported by fmtconv pbitPS = 32 else: # Apply conversion in the higher one of input and output bit depth pbitPS = max(sbitPS, dbitPS) # For integer sample type, only 8-, 9-, 10-, 12-, 16-bit is supported by fmtc.matrix if pbitPS == 11: pbitPS = 12 elif pbitPS > 12 and pbitPS < 16: pbitPS = 16 if dHSubS != sHSubS or dVSubS != sVSubS: # When chroma re-sampling is needed, always process in 16-bit for integer sample type pbitPS = 16 # fmtc.resample parameters if kernel is None: kernel = "bicubic" if a1 is None and a2 is None: a1 = 0 a2 = 0.5 elif not isinstance(kernel, str): raise type_error('"kernel" must be a str!') # Conversion if sIsYUV: # Skip matrix conversion for YUV input # Change chroma sub-sampling if needed if dHSubS != sHSubS or dVSubS != sVSubS: # Apply depth conversion for processed clip clip = Depth(clip, pbitPS, pSType, fulls, fulls, **kwargs) clip = core.fmtc.resample(clip, kernel=kernel, taps=taps, a1=a1, a2=a2, css=css, planes=[2,3,3], fulls=fulls, fulld=fulls, cplace=cplace) # Apply depth conversion for output clip clip = Depth(clip, dbitPS, dSType, fulls, fulld, **kwargs) elif sIsGRAY: # Apply depth conversion for output clip clip = Depth(clip, dbitPS, dSType, fulls, fulld, **kwargs) # Shuffle planes for Gray input widthc = input.width // dHSubS heightc = input.height // dVSubS UV = core.std.BlankClip(clip, width=widthc, height=heightc, color=1 << (dbitPS - 1) if dSType == vs.INTEGER else 0) clip = core.std.ShufflePlanes([clip,UV,UV], [0,0,0], vs.YUV) else: # Apply depth conversion for processed clip clip = Depth(clip, pbitPS, pSType, fulls, fulls, **kwargs) # Apply matrix conversion for RGB input if matrix == "OPP": clip = core.fmtc.matrix(clip, fulls=fulls, fulld=fulld, coef=[1/3,1/3,1/3,0, 1/2,0,-1/2,0, 1/4,-1/2,1/4,0], col_fam=vs.YUV) clip = SetColorSpace(clip, Matrix=2) elif matrix == "2020cl": clip = core.fmtc.matrix2020cl(clip, full=fulld) else: clip = core.fmtc.matrix(clip, mat=matrix, fulls=fulls, fulld=fulld, col_fam=vs.YUV) # Change chroma sub-sampling if needed if dHSubS != sHSubS or dVSubS != sVSubS: clip = core.fmtc.resample(clip, kernel=kernel, taps=taps, a1=a1, a2=a2, css=css, planes=[2,3,3], fulls=fulld, fulld=fulld, cplace=cplace) # Apply depth conversion for output clip clip = Depth(clip, dbitPS, dSType, fulld, fulld, **kwargs) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Main function: BM3D() ################################################################################################################################ ## A wrap function for easy using of BM3D/V-BM3D denoising filter. ## The BM3D filtering is always done in 16-bit int or 32-bit float opponent (OPP) color space internally. ## It can automatically convert any input color space to OPP and convert it back after filtering. ## Alternatively, you can specify "output" to force outputting RGB or OPP, and "css" to change chroma subsampling. ## For Gray input, no color space conversion is involved, thus "output" and "css" won't take effect. ## You can specify "refine" for any number of final estimate refinements. ################################################################################################################################ ## Basic parameters ## input {clip}: clip to be filtered ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## sigma {float[]}: same as "sigma" in BM3D, used for both basic estimate and final estimate ## the strength of filtering, should be carefully adjusted according to the noise ## set 0 to disable the filtering of corresponding plane ## default: [5.0,5.0,5.0] ## radius1 {int}: temporal radius of basic estimate ## - 0: BM3D (spatial denoising) ## - 1~16: temporal radius of V-BM3D (spatial-temporal denoising) ## default: 0 ## radius2 {int}: temporal radius of final estimate ## default is the same as "radius1" ## profile1 {str}: same as "profile" in BM3D basic estimate ## default: "fast" ## profile2 {str}: same as "profile" in BM3D final estimate ## default is the same as "profile1" ################################################################################################################################ ## Advanced parameters ## refine {int}: refinement times ## - 0: basic estimate only ## - 1: basic estimate + refined with final estimate, the original behavior of BM3D ## - n: basic estimate + refined with final estimate for n times ## each final estimate takes the filtered clip in previous estimate as reference clip to filter the input clip ## default: 1 ## pre {clip} (optional): pre-filtered clip for basic estimate ## must be of the same format and dimension as the input clip ## should be a clip better suited for block-matching than the input clip ## ref {clip} (optional): basic estimate clip ## must be of the same format and dimension as the input clip ## replace the basic estimate of BM3D and serve as the reference clip for final estimate ## psample {int}: internal processed precision ## - 0: 16-bit integer, less accuracy, less memory consumption ## - 1: 32-bit float, more accuracy, more memory consumption ## default: 1 ################################################################################################################################ ## Parameters of input properties ## matrix {int|str}: color matrix of input clip, only makes sense for YUV input ## check GetMatrix() for available values ## default: guessed according to the color family and size of input clip ## full {bool}: define if input clip is of full range ## default: guessed according to the color family of input clip and "matrix" ################################################################################################################################ ## Parameters of output properties ## output {int}: type of output clip, doesn't make sense for Gray input ## - 0: same format as input clip ## - 1: full range RGB (converted from input clip) ## - 2: full range OPP (converted from full range RGB, the color space where the filtering takes place) ## default: 0 ## css {str}: chroma subsampling of output clip, only valid when output=0 and input clip is YUV ## check ToYUV() for available values ## default is the same as that of the input clip ## depth {int}: bit depth of output clip, can be 1-16 for integer or 16/32 for float ## note that 1-7 bit content is still stored as 8 bit integer format ## default is the same as that of the input clip ## sample {int}: sample type of output clip, can be 0 (vs.INTEGER) or 1 (vs.FLOAT) ## default is the same as that of the input clip ################################################################################################################################ ## Parameters of resampling ## cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace: ## used for chroma up-sampling, same as those in fmtc.resample ## default: kernel="bicubic", a1=0, a2=0.5, also known as "Catmull-Rom" ## cd_kernel, cd_taps, cd_a1, cd_a2, cd_cplace: ## used for chroma down-sampling, same as those in fmtc.resample ## default: kernel="bicubic", a1=0, a2=0.5, also known as "Catmull-Rom" ################################################################################################################################ ## Parameters of BM3D basic estimate ## block_size1, block_step1, group_size1, bm_range1, bm_step1, ps_num1, ps_range1, ps_step1, th_mse1, hard_thr: ## same as those in bm3d.Basic/bm3d.VBasic ################################################################################################################################ ## Parameters of BM3D final estimate ## block_size2, block_step2, group_size2, bm_range2, bm_step2, ps_num2, ps_range2, ps_step2, th_mse2: ## same as those in bm3d.Final/bm3d.VFinal ################################################################################################################################ ## Parameters of depth conversion ## dither, useZ, ampo, ampn, dyn, staticnoise, cpuopt, patsize, tpdfo, tpdfn, corplane: ## same as those in Depth() ## *NOTE* no positional arguments, only keyword arguments are accepted ################################################################################################################################ def BM3D(input, sigma=None, radius1=None, radius2=None, profile1=None, profile2=None, refine=None, pre=None, ref=None, psample=None, matrix=None, full=None, output=None, css=None, depth=None, sample=None, cu_kernel=None, cu_taps=None, cu_a1=None, cu_a2=None, cu_cplace=None, cd_kernel=None, cd_taps=None, cd_a1=None, cd_a2=None, cd_cplace=None, block_size1=None, block_step1=None, group_size1=None, bm_range1=None, bm_step1=None, ps_num1=None, ps_range1=None, ps_step1=None, th_mse1=None, hard_thr=None, block_size2=None, block_step2=None, group_size2=None, bm_range2=None, bm_step2=None, ps_num2=None, ps_range2=None, ps_step2=None, th_mse2=None, **kwargs): # input clip clip = input if not isinstance(input, vs.VideoNode): raise type_error('"input" must be a clip!') # Get string format parameter "matrix" matrix = GetMatrix(input, matrix, True) # Get properties of input clip sFormat = input.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsRGB = sColorFamily == vs.RGB sIsYUV = sColorFamily == vs.YUV sIsGRAY = sColorFamily == vs.GRAY sbitPS = sFormat.bits_per_sample sSType = sFormat.sample_type sHSubS = 1 << sFormat.subsampling_w sVSubS = 1 << sFormat.subsampling_h if full is None: # If not set, assume limited range for YUV and Gray input # Assume full range for YCgCo and OPP input if (sIsGRAY or sIsYUV) and (matrix == "RGB" or matrix == "YCgCo" or matrix == "OPP"): fulls = True else: fulls = False if sIsYUV or sIsGRAY else True elif not isinstance(full, int): raise type_error('"full" must be a bool!') else: fulls = full # Get properties of internal processed clip if psample is None: psample = vs.FLOAT elif not isinstance(psample, int): raise type_error('"psample" must be an int!') elif psample != vs.INTEGER and psample != vs.FLOAT: raise value_error('"psample" must be either 0 (vs.INTEGER) or 1 (vs.FLOAT)!') pbitPS = 16 if psample == vs.INTEGER else 32 pSType = psample # Chroma sub-sampling parameters if css is None: dHSubS = sHSubS dVSubS = sVSubS css = f'{dHSubS}{dVSubS}' elif not isinstance(css, str): raise type_error('"css" must be a str!') else: if css == "444" or css == "4:4:4": css = "11" elif css == "440" or css == "4:4:0": css = "12" elif css == "422" or css == "4:2:2": css = "21" elif css == "420" or css == "4:2:0": css = "22" elif css == "411" or css == "4:1:1": css = "41" elif css == "410" or css == "4:1:0": css = "42" dHSubS = int(css[0]) dVSubS = int(css[1]) if cu_cplace is not None and cd_cplace is None: cd_cplace = cu_cplace # Parameters processing if sigma is None: sigma = [5.0,5.0,5.0] else: if isinstance(sigma, int): sigma = float(sigma) sigma = [sigma,sigma,sigma] elif isinstance(sigma, float): sigma = [sigma,sigma,sigma] elif isinstance(sigma, list): while len(sigma) < 3: sigma.append(sigma[len(sigma) - 1]) else: raise type_error('sigma must be a float[] or an int[]!') if sIsGRAY: sigma = [sigma[0],0,0] skip = sigma[0] <= 0 and sigma[1] <= 0 and sigma[2] <= 0 if radius1 is None: radius1 = 0 elif not isinstance(radius1, int): raise type_error('"radius1" must be an int!') elif radius1 < 0: raise value_error('valid range of "radius1" is [0, +inf)!') if radius2 is None: radius2 = radius1 elif not isinstance(radius2, int): raise type_error('"radius2" must be an int!') elif radius2 < 0: raise value_error('valid range of "radius2" is [0, +inf)!') if profile1 is None: profile1 = "fast" elif not isinstance(profile1, str): raise type_error('"profile1" must be a str!') if profile2 is None: profile2 = profile1 elif not isinstance(profile2, str): raise type_error('"profile2" must be a str!') if refine is None: refine = 1 elif not isinstance(refine, int): raise type_error('"refine" must be an int!') elif refine < 0: raise value_error('valid range of "refine" is [0, +inf)!') if output is None: output = 0 elif not isinstance(output, int): raise type_error('"output" must be an int!') elif output < 0 or output > 2: raise value_error('valid values of "output" are 0, 1 and 2!') if pre is not None: if not isinstance(pre, vs.VideoNode): raise type_error('"pre" must be a clip!') if pre.format.id != sFormat.id: raise value_error('clip "pre" must be of the same format as the input clip!') if pre.width != input.width or pre.height != input.height: raise value_error('clip "pre" must be of the same size as the input clip!') if ref is not None: if not isinstance(ref, vs.VideoNode): raise type_error('"ref" must be a clip!') if ref.format.id != sFormat.id: raise value_error('clip "ref" must be of the same format as the input clip!') if ref.width != input.width or ref.height != input.height: raise value_error('clip "ref" must be of the same size as the input clip!') # Get properties of output clip if depth is None: if output == 0: dbitPS = sbitPS else: dbitPS = pbitPS elif not isinstance(depth, int): raise type_error('"depth" must be an int!') else: dbitPS = depth if sample is None: if depth is None: if output == 0: dSType = sSType else: dSType = pSType else: dSType = vs.FLOAT if dbitPS >= 32 else vs.INTEGER elif not isinstance(sample, int): raise type_error('"sample" must be an int!') elif sample != vs.INTEGER and sample != vs.FLOAT: raise value_error('"sample" must be either 0 (vs.INTEGER) or 1 (vs.FLOAT)!') else: dSType = sample if depth is None and sSType != vs.FLOAT and sample == vs.FLOAT: dbitPS = 32 elif depth is None and sSType != vs.INTEGER and sample == vs.INTEGER: dbitPS = 16 if dSType == vs.INTEGER and (dbitPS < 1 or dbitPS > 16): raise value_error(f'{dbitPS}-bit integer output is not supported!') if dSType == vs.FLOAT and (dbitPS != 16 and dbitPS != 32): raise value_error(f'{dbitPS}-bit float output is not supported!') if output == 0: fulld = fulls else: # Always full range output when output=1|output=2 (full range RGB or full range OPP) fulld = True # Convert to processed format # YUV/RGB input is converted to opponent color space as full range YUV # Gray input is converted to full range Gray onlyY = False if sIsGRAY: onlyY = True # Convert Gray input to full range Gray in processed format clip = Depth(clip, pbitPS, pSType, fulls, True, **kwargs) if pre is not None: pre = Depth(pre, pbitPS, pSType, fulls, True, **kwargs) if ref is not None: ref = Depth(ref, pbitPS, pSType, fulls, True, **kwargs) else: # Convert input to full range RGB clip = ToRGB(clip, matrix, pbitPS, pSType, fulls, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) if pre is not None: pre = ToRGB(pre, matrix, pbitPS, pSType, fulls, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) if ref is not None: ref = ToRGB(ref, matrix, pbitPS, pSType, fulls, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) # Convert full range RGB to full range OPP clip = ToYUV(clip, "OPP", "444", pbitPS, pSType, True, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) if pre is not None: pre = ToYUV(pre, "OPP", "444", pbitPS, pSType, True, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) if ref is not None: ref = ToYUV(ref, "OPP", "444", pbitPS, pSType, True, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) # Convert OPP to Gray if only Y is processed srcOPP = clip if sigma[1] <= 0 and sigma[2] <= 0: onlyY = True clip = core.std.ShufflePlanes([clip], [0], vs.GRAY) if pre is not None: pre = core.std.ShufflePlanes([pre], [0], vs.GRAY) if ref is not None: ref = core.std.ShufflePlanes([ref], [0], vs.GRAY) # Basic estimate if ref is not None: # Use custom basic estimate specified by clip "ref" flt = ref elif skip: flt = clip elif radius1 < 1: # Apply BM3D basic estimate # Optional pre-filtered clip for block-matching can be specified by "pre" flt = core.bm3d.Basic(clip, ref=pre, profile=profile1, sigma=sigma, block_size=block_size1, block_step=block_step1, group_size=group_size1, bm_range=bm_range1, bm_step=bm_step1, th_mse=th_mse1, hard_thr=hard_thr, matrix=100) else: # Apply V-BM3D basic estimate # Optional pre-filtered clip for block-matching can be specified by "pre" flt = core.bm3d.VBasic(clip, ref=pre, profile=profile1, sigma=sigma, radius=radius1, block_size=block_size1, block_step=block_step1, group_size=group_size1, bm_range=bm_range1, bm_step=bm_step1, ps_num=ps_num1, ps_range=ps_range1, ps_step=ps_step1, th_mse=th_mse1, hard_thr=hard_thr, matrix=100).bm3d.VAggregate(radius=radius1, sample=pSType) # Shuffle Y plane back if not processed if not onlyY and sigma[0] <= 0: flt = core.std.ShufflePlanes([clip,flt,flt], [0,1,2], vs.YUV) # Final estimate for _ in range(0, refine): if skip: flt = clip elif radius2 < 1: # Apply BM3D final estimate flt = core.bm3d.Final(clip, ref=flt, profile=profile2, sigma=sigma, block_size=block_size2, block_step=block_step2, group_size=group_size2, bm_range=bm_range2, bm_step=bm_step2, th_mse=th_mse2, matrix=100) else: # Apply V-BM3D final estimate flt = core.bm3d.VFinal(clip, ref=flt, profile=profile2, sigma=sigma, radius=radius2, block_size=block_size2, block_step=block_step2, group_size=group_size2, bm_range=bm_range2, bm_step=bm_step2, ps_num=ps_num2, ps_range=ps_range2, ps_step=ps_step2, th_mse=th_mse2, matrix=100).bm3d.VAggregate(radius=radius2, sample=pSType) # Shuffle Y plane back if not processed if not onlyY and sigma[0] <= 0: flt = core.std.ShufflePlanes([clip,flt,flt], [0,1,2], vs.YUV) # Convert to output format if sIsGRAY: clip = Depth(flt, dbitPS, dSType, True, fulld, **kwargs) else: # Shuffle back to YUV if not all planes are processed if onlyY: clip = core.std.ShufflePlanes([flt,srcOPP,srcOPP], [0,1,2], vs.YUV) elif sigma[1] <= 0 or sigma[2] <= 0: clip = core.std.ShufflePlanes([flt, clip if sigma[1] <= 0 else flt, clip if sigma[2] <= 0 else flt], [0,1,2], vs.YUV) else: clip = flt # Convert to final output format if output <= 1: # Convert full range OPP to full range RGB clip = ToRGB(clip, "OPP", pbitPS, pSType, True, cu_kernel, cu_taps, cu_a1, cu_a2, cu_cplace, **kwargs) if output <= 0 and not sIsRGB: # Convert full range RGB to YUV clip = ToYUV(clip, matrix, css, dbitPS, dSType, fulld, cd_kernel, cd_taps, cd_a1, cd_a2, cd_cplace, **kwargs) else: # Depth conversion for RGB or OPP output clip = Depth(clip, dbitPS, dSType, True, fulld, **kwargs) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Main function: VFRSplice() ################################################################################################################################ ## Splice multiple clips with different frame rate, and output timecode file. ## Each input clip is of CFR (constant frame rate). ## The output clip is of VFR (variational frame rate). ################################################################################################################################ ## Basic parameters ## clips {clip}: any number of clips to splice ## each clip should be CFR ## tcfile {str}: timecode file output ## default: None ## v2 {bool}: timecode format ## True for v2 output and False for v1 output ## default: True ## precision {int}: precision of time and frame rate ## a decimal number indicating how many digits should be displayed after the decimal point for a fixed-point value ## default: 6 ################################################################################################################################ def VFRSplice(clips, tcfile=None, v2=None, precision=None): # Arguments if isinstance(clips, vs.VideoNode): clips = [clips] elif isinstance(clips, Sequence): for clip in clips: if not isinstance(clip, vs.VideoNode): raise type_error('each element in "clips" must be a clip!') if clip.fps_num == 0 or clip.fps_den == 0: raise value_error('each clip in "clips" must be CFR!') else: raise type_error('"clips" must be a clip or a sequence of clips!') if tcfile is not None and not isinstance(tcfile, str): raise type_error('"tcfile" must be a str!') if v2 is None: v2 = True elif not isinstance(v2, int): raise type_error('"v2" must be a bool!') if precision is None: precision = 6 elif not isinstance(precision, int): raise type_error('"precision" must be an int!') # Fraction to str function def frac2str(num, den, precision=6): return f'{num / den : <.{precision}F}' # Timecode file if tcfile is None: pass else: # Get timecode v1 list cur_frame = 0 tc_list = [] index = 0 for clip in clips: if index > 0 and clip.fps_num == tc_list[index - 1][2] and clip.fps_den == tc_list[index - 1][3]: tc_list[index - 1] = (tc_list[index - 1][0], cur_frame + clip.num_frames - 1, clip.fps_num, clip.fps_den) else: tc_list.append((cur_frame, cur_frame + clip.num_frames - 1, clip.fps_num, clip.fps_den)) index += 1 cur_frame += clip.num_frames # Write to timecode file ofile = open(tcfile, 'w') if v2: # timecode v2 olines = ['# timecode format v2\n'] time = 0 # ms for tc in tc_list: frame_duration = 1000 * tc[3] / tc[2] # ms for _ in range(tc[0], tc[1] + 1): olines.append(f'{time : <.{precision}F}\n') time += frame_duration else: # timecode v1 olines = [ '# timecode format v1\n', f'Assume {frac2str(tc_list[0][2], tc_list[0][3], precision)}\n' ] for tc in tc_list: olines.append(f'{tc[0]},{tc[1]},{frac2str(tc[2], tc[3], precision)}\n') try: ofile.writelines(olines) finally: ofile.close() # Output spliced clip return core.std.Splice(clips, mismatch=True) ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Runtime functions below ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Runtime function: PlaneStatistics() ################################################################################################################################ ## Calculate statistics of specific plane and store them as frame properties ## All the values are normalized (float that the peak-to-peak value is 1) ## Supported statistics: ## mean: stored as frame property 'PlaneMean' ## mean absolute deviation: stored as frame property 'PlaneMAD' ## variance: stored as frame property 'PlaneVar' ## standard deviation: stored as frame property 'PlaneSTD' ## root mean square: stored as frame property 'PlaneRMS' ################################################################################################################################ ## Basic parameters ## clip {clip}: clip to be evaluated ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 32 bit float ## plane {int}: specify which plane to evaluate ## default: 0 ## mean {bool}: whether to calculate mean ## default: True ## mad {bool}: whether to calculate mean absolute deviation ## default: True ## var {bool}: whether to calculate variance ## default: True ## std {bool}: whether to calculate standard deviation ## default: True ## rms {bool}: whether to calculate root mean square ## default: True ################################################################################################################################ def PlaneStatistics(clip, plane=None, mean=True, mad=True, var=True, std=True, rms=True): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sSType = sFormat.sample_type sbitPS = sFormat.bits_per_sample sNumPlanes = sFormat.num_planes valueRange = (1 << sbitPS) - 1 if sSType == vs.INTEGER else 1 # Parameters if plane is None: plane = 0 elif not isinstance(plane, int): raise type_error('"plane" must be an int!') elif plane < 0 or plane > sNumPlanes: raise value_error(f'valid range of "plane" is [0, {sNumPlanes})!') floatFormat = RegisterFormat(vs.GRAY, vs.FLOAT, 32, 0, 0) floatBlk = core.std.BlankClip(clip, format=floatFormat.id) clipPlane = GetPlane(clip, plane) # Plane Mean clip = PlaneAverage(clip, plane, "PlaneMean") # Plane MAD (mean absolute deviation) if mad: '''# always float precision def _PlaneADFrame(n, f, clip): mean = f.props.PlaneMean scale = 1 / valueRange expr = f"x {scale} * {mean} - abs" return core.std.Expr(clip, expr, floatFormat.id) ADclip = core.std.FrameEval(floatBlk, functools.partial(_PlaneADFrame, clip=clipPlane), clip)''' if hasattr(core, 'akarin'): ADclip = core.akarin.Expr([clipPlane, clip], f'x y.PlaneMean {valueRange} * - abs') else: def _PlaneADFrame(n, f, clip): mean = f.props.PlaneMean * valueRange expr = f"x {mean} - abs" return core.std.Expr(clip, expr) ADclip = core.std.FrameEval(clipPlane, functools.partial(_PlaneADFrame, clip=clipPlane), clip) ADclip = PlaneAverage(ADclip, 0, "PlaneMAD") def _PlaneMADTransfer(n, f): fout = f[0].copy() fout.props.PlaneMAD = f[1].props.PlaneMAD return fout clip = core.std.ModifyFrame(clip, [clip, ADclip], selector=_PlaneMADTransfer) # Plane Var (variance) and STD (standard deviation) if var or std: if hasattr(core, 'akarin'): SDclip = core.akarin.Expr([clipPlane, clip], f'x y.PlaneMean {valueRange} * - dup *', format=floatFormat.id) else: def _PlaneSDFrame(n, f, clip): mean = f.props.PlaneMean * valueRange expr = f"x {mean} - dup *" return core.std.Expr(clip, expr, floatFormat.id) SDclip = core.std.FrameEval(floatBlk, functools.partial(_PlaneSDFrame, clip=clipPlane), clip) SDclip = PlaneAverage(SDclip, 0, "PlaneVar") def _PlaneVarSTDTransfer(n, f): fout = f[0].copy() if var: fout.props.PlaneVar = f[1].props.PlaneVar / (valueRange * valueRange) if std: fout.props.PlaneSTD = math.sqrt(f[1].props.PlaneVar) / valueRange return fout clip = core.std.ModifyFrame(clip, [clip, SDclip], selector=_PlaneVarSTDTransfer) # Plane RMS (root mean square) if rms: expr = "x x *" squareClip = core.std.Expr(clipPlane, expr, floatFormat.id) squareClip = PlaneAverage(squareClip, 0, "PlaneMS") def _PlaneRMSTransfer(n, f): fout = f[0].copy() fout.props.PlaneRMS = math.sqrt(f[1].props.PlaneMS) / valueRange return fout clip = core.std.ModifyFrame(clip, [clip, squareClip], selector=_PlaneRMSTransfer) # Delete frame property "PlaneMean" if not needed if not mean: clip = RemoveFrameProp(clip, "PlaneMean") # Output return clip ################################################################################################################################ ################################################################################################################################ ## Runtime function: PlaneCompare() ################################################################################################################################ ## Compare specific plane of 2 clips and store the statistical results as frame properties ## All the values are normalized (float of which the peak-to-peak value is 1) except PSNR ## Supported statistics: ## mean absolute error: stored as frame property 'PlaneMAE', aka L1 distance ## root of mean squared error: stored as frame property 'PlaneRMSE', aka L2 distance(Euclidean distance) ## peak signal-to-noise ratio: stored as frame property 'PlanePSNR', maximum value is 100.0 (when 2 planes are identical) ## covariance: stored as frame property 'PlaneCov' ## correlation: stored as frame property 'PlaneCorr' ################################################################################################################################ ## Basic parameters ## clip1 {clip}: the first clip to be evaluated, will be copied to output ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 32 bit float ## clip2 {clip}: the second clip, to be compared with the first one ## must be of the same format and dimension as the "clip1" ## plane {int}: specify which plane to evaluate ## default: 0 ## mae {bool}: whether to calculate mean absolute error ## default: True ## rmse {bool}: whether to calculate root of mean squared error ## default: True ## psnr {bool}: whether to calculate peak signal-to-noise ratio ## default: True ## cov {bool}: whether to calculate covariance ## default: True ## corr {bool}: whether to calculate correlation ## default: True ################################################################################################################################ def PlaneCompare(clip1, clip2, plane=None, mae=True, rmse=True, psnr=True, cov=True, corr=True): # input clip if not isinstance(clip1, vs.VideoNode): raise type_error('"clip1" must be a clip!') if not isinstance(clip2, vs.VideoNode): raise type_error('"clip2" must be a clip!') # Get properties of input clip sFormat = clip1.format if sFormat.id != clip2.format.id: raise value_error('"clip1" and "clip2" must be of the same format!') if clip1.width != clip2.width or clip1.height != clip2.height: raise value_error('"clip1" and "clip2" must be of the same width and height!') sSType = sFormat.sample_type sbitPS = sFormat.bits_per_sample sNumPlanes = sFormat.num_planes valueRange = (1 << sbitPS) - 1 if sSType == vs.INTEGER else 1 # Parameters if plane is None: plane = 0 elif not isinstance(plane, int): raise type_error('"plane" must be an int!') elif plane < 0 or plane > sNumPlanes: raise value_error(f'valid range of "plane" is [0, {sNumPlanes})!') floatFormat = RegisterFormat(vs.GRAY, vs.FLOAT, 32, 0, 0) floatBlk = core.std.BlankClip(clip1, format=floatFormat.id) clip1Plane = GetPlane(clip1, plane) clip2Plane = GetPlane(clip2, plane) # Plane MAE (mean absolute error) if mae: expr = "x y - abs" ADclip = core.std.Expr([clip1Plane, clip2Plane], expr, floatFormat.id) ADclip = PlaneAverage(ADclip, 0, "PlaneMAE") def _PlaneMAETransfer(n, f): fout = f[0].copy() fout.props.PlaneMAE = f[1].props.PlaneMAE / valueRange return fout clip1 = core.std.ModifyFrame(clip1, [clip1, ADclip], selector=_PlaneMAETransfer) # Plane RMSE (root of mean squared error) and PSNR (peak signal-to-noise ratio) if rmse or psnr: expr = "x y - dup *" SDclip = core.std.Expr([clip1Plane, clip2Plane], expr, floatFormat.id) SDclip = PlaneAverage(SDclip, 0, "PlaneMSE") def _PlaneRMSEnPSNRTransfer(n, f): fout = f[0].copy() if rmse: fout.props.PlaneRMSE = math.sqrt(f[1].props.PlaneMSE) / valueRange if psnr: fout.props.PlanePSNR = min(10 * math.log(valueRange * valueRange / f[1].props.PlaneMSE, 10), 99.99999999999999) if f[1].props.PlaneMSE > 0 else 100.0 return fout clip1 = core.std.ModifyFrame(clip1, [clip1, SDclip], selector=_PlaneRMSEnPSNRTransfer) # Plane Cov (covariance) and Corr (correlation) if cov or corr: clip1Mean = PlaneAverage(clip1Plane, 0, "PlaneMean") clip2Mean = PlaneAverage(clip2Plane, 0, "PlaneMean") if hasattr(core, 'akarin'): CoDclip = core.akarin.Expr([clip1Plane, clip1Mean, clip2Plane, clip2Mean], f'x y.PlaneMean {valueRange} * - z a.PlaneMean {valueRange} * - *', format=floatFormat.id) else: def _PlaneCoDFrame(n, f, clip1, clip2): mean1 = f[0].props.PlaneMean * valueRange mean2 = f[1].props.PlaneMean * valueRange expr = f"x {mean1} - y {mean2} - *" return core.std.Expr([clip1, clip2], expr, floatFormat.id) CoDclip = core.std.FrameEval(floatBlk, functools.partial(_PlaneCoDFrame, clip1=clip1Plane, clip2=clip2Plane), [clip1Mean, clip2Mean]) CoDclip = PlaneAverage(CoDclip, 0, "PlaneCov") clips = [clip1, CoDclip] if corr: if hasattr(core, 'akarin'): SDclip1 = core.akarin.Expr([clip1Plane, clip1Mean], f'x y.PlaneMean {valueRange} * - dup *', format=floatFormat.id) SDclip2 = core.akarin.Expr([clip2Plane, clip2Mean], f'x y.PlaneMean {valueRange} * - dup *', format=floatFormat.id) else: def _PlaneSDFrame(n, f, clip): mean = f.props.PlaneMean * valueRange expr = f"x {mean} - dup *" return core.std.Expr(clip, expr, floatFormat.id) SDclip1 = core.std.FrameEval(floatBlk, functools.partial(_PlaneSDFrame, clip=clip1Plane), clip1Mean) SDclip2 = core.std.FrameEval(floatBlk, functools.partial(_PlaneSDFrame, clip=clip2Plane), clip2Mean) SDclip1 = PlaneAverage(SDclip1, 0, "PlaneVar") SDclip2 = PlaneAverage(SDclip2, 0, "PlaneVar") clips.append(SDclip1) clips.append(SDclip2) def _PlaneCovTransfer(n, f): fout = f[0].copy() if cov: fout.props.PlaneCov = f[1].props.PlaneCov / (valueRange * valueRange) if corr: std1std2 = math.sqrt(f[2].props.PlaneVar * f[3].props.PlaneVar) fout.props.PlaneCorr = f[1].props.PlaneCov / std1std2 if std1std2 > 0 else float("inf") return fout clip1 = core.std.ModifyFrame(clip1, clips, selector=_PlaneCovTransfer) # Output return clip1 ################################################################################################################################ ################################################################################################################################ ## Runtime function: ShowAverage() ################################################################################################################################ ## Display unnormalized average of each plane ################################################################################################################################ ## Basic parameters ## clip {clip}: clip to be evaluated ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ################################################################################################################################ ## Advanced parameters ## alignment {int}: same as the one in text.Text() ## default: 7 ################################################################################################################################ def ShowAverage(clip, alignment=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsYUV = sColorFamily == vs.YUV sSType = sFormat.sample_type sbitPS = sFormat.bits_per_sample sNumPlanes = sFormat.num_planes valueRange = (1 << sbitPS) - 1 if sSType == vs.INTEGER else 1 offset = [0, -0.5, -0.5] if sSType == vs.FLOAT and sIsYUV else [0, 0, 0] # Process and output def _ShowAverageFrame(n, f): text = "" if sNumPlanes == 1: average = f.props.PlaneAverage * valueRange + offset[0] text += f"PlaneAverage[{0}]={average}" else: for p in range(sNumPlanes): average = f[p].props.PlaneAverage * valueRange + offset[p] text += f"PlaneAverage[{p}]={average}\n" return core.text.Text(clip, text, alignment) avg = [] for p in range(sNumPlanes): avg.append(PlaneAverage(clip, p)) return core.std.FrameEval(clip, _ShowAverageFrame, prop_src=avg) ################################################################################################################################ ################################################################################################################################ ## Runtime function: FilterIf() ################################################################################################################################ ## Take the frames from clip "flt" that is marked to be filtered and the ones from clip "src" that is not. ## An arbitrary frame property named "prop_name" from clip "props" is evaluated to determine whether it should be filtered. ################################################################################################################################ ## Basic parameters ## src {clip}: the source clip ## can be of any constant format ## flt {clip}: the filtered clip ## must be of the same format and dimension as "src" ## prop_name {str} (mandatory): the frame property to be evaluated ## for each frame, if this property exists and is True, then take the frame from "flt", otherwise take the one from "src" ## props {clip} (optional): the clip from which the frame property is evaluated ## can be of any format, should have the same number of frames as "src" ## default: None (use "src") ################################################################################################################################ def FilterIf(src, flt, prop_name, props=None): # input clip if not isinstance(src, vs.VideoNode): raise type_error('"src" must be a clip!') if not isinstance(flt, vs.VideoNode): raise type_error('"flt" must be a clip!') if props is not None and not isinstance(props, vs.VideoNode): raise type_error('"props" must be a clip!') # Get properties of input clip sFormat = src.format if sFormat.id != flt.format.id: raise value_error('"src" and "flt" must be of the same format!') if src.width != flt.width or src.height != flt.height: raise value_error('"src" and "flt" must be of the same width and height!') if prop_name is None or not isinstance(prop_name, str): raise type_error('"prop_name" must be specified and must be a str!') else: prop_name = _check_arg_prop(prop_name, None, None, 'prop_name') if props is None: props = src # FrameEval function def _FilterIfFrame(n, f): try: if f.props.__getattr__(prop_name): return flt except KeyError: pass return src # Process return core.std.FrameEval(src, _FilterIfFrame, props) ################################################################################################################################ ################################################################################################################################ ## Runtime function: FilterCombed() ################################################################################################################################ ## Take the frames from clip "flt" that is marked as combed and the ones from clip "src" that is not. ## The frame property '_Combed' from clip "props" is evaluated to determine whether it's combed. ## This function is an instantiation of FilterIf() ################################################################################################################################ ## Basic parameters ## src {clip}: the source clip ## can be of any constant format ## flt {clip}: the filtered clip (de-interlaced) ## must be of the same format and dimension as "src" ## props {clip} (optional): the clip from which the frame property is evaluated ## can be of any format, should have the same number of frames as "src" ## default: None (use "src") ################################################################################################################################ def FilterCombed(src, flt, props=None): clip = FilterIf(src, flt, '_Combed', props) clip = RemoveFrameProp(clip, '_Combed') return AssumeFrame(clip) ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Utility functions below ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Utility function: Min() ################################################################################################################################ ## Take 2 clips and return pixel-wise minimum of them. ## With a special mode=2 for difference clip. ################################################################################################################################ ## Basic parameters ## clip1 {clip}: the first clip ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## clip2 {clip}: the second clip ## must be of the same format and dimension as "clip1" ## mode {int[]}: specify processing mode for each plane ## - 0: don't process, copy "clip1" to output ## - 1: normal minimum, output = min(clip1, clip2) ## - 2: difference minimum, output = abs(clip2 - neutral) < abs(clip1 - neutral) ? clip2 : clip1 ## default: [1,1,1] for YUV/RGB input, [1] for Gray input ################################################################################################################################ ## Advanced parameters ## neutral {int|float}: specfy the neutral value used for mode=2 ## default: 1 << (bits_per_sample - 1) for integer input, 0 for float input ################################################################################################################################ def Min(clip1, clip2, mode=None, neutral=None): return _operator2(clip1, clip2, mode, neutral, 'Min') ################################################################################################################################ ################################################################################################################################ ## Utility function: Max() ################################################################################################################################ ## Take 2 clips and return pixel-wise maximum of them. ## With a special mode=2 for difference clip. ################################################################################################################################ ## Basic parameters ## clip1 {clip}: the first clip ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## clip2 {clip}: the second clip ## must be of the same format and dimension as "clip1" ## mode {int[]}: specify processing mode for each plane ## - 0: don't process, copy "clip1" to output ## - 1: normal maximum, output = max(clip1, clip2) ## - 2: difference maximum, output = abs(clip2 - neutral) > abs(clip1 - neutral) ? clip2 : clip1 ## default: [1,1,1] for YUV/RGB input, [1] for Gray input ################################################################################################################################ ## Advanced parameters ## neutral {int|float}: specfy the neutral value used for mode=2 ## default: 1 << (bits_per_sample - 1) for integer input, 0 for float input ################################################################################################################################ def Max(clip1, clip2, mode=None, neutral=None): return _operator2(clip1, clip2, mode, neutral, 'Max') ################################################################################################################################ ################################################################################################################################ ## Utility function: Avg() ################################################################################################################################ ## Take 2 clips and return pixel-wise average of them. ################################################################################################################################ ## Basic parameters ## clip1 {clip}: the first clip ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## clip2 {clip}: the second clip ## must be of the same format and dimension as "clip1" ## mode {int[]}: specify processing mode for each plane ## - 0: don't process, copy "clip1" to output ## - 1: average, output = (clip1 + clip2) / 2 ## default: [1,1,1] for YUV/RGB input, [1] for Gray input ################################################################################################################################ def Avg(clip1, clip2, mode=None): return _operator2(clip1, clip2, mode, None, 'Avg') ################################################################################################################################ ################################################################################################################################ ## Utility function: MinFilter() ################################################################################################################################ ## Apply filtering with minimum difference of the 2 filtered clips. ################################################################################################################################ ## Basic parameters ## src {clip}: source clip ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## flt1 {clip}: filtered clip 1 ## must be of the same format and dimension as "src" ## flt2 {clip}: filtered clip 2 ## must be of the same format and dimension as "src" ## planes {int[]}: specify which planes to process ## unprocessed planes will be copied from "src" ## default: all planes will be processed, [0,1,2] for YUV/RGB input, [0] for Gray input ################################################################################################################################ def MinFilter(src, flt1, flt2, planes=None): return _min_max_filter(src, flt1, flt2, planes, 'MinFilter') ################################################################################################################################ ################################################################################################################################ ## Utility function: MaxFilter() ################################################################################################################################ ## Apply filtering with maximum difference of the 2 filtered clips. ################################################################################################################################ ## Basic parameters ## src {clip}: source clip ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## flt1 {clip}: filtered clip 1 ## must be of the same format and dimension as "src" ## flt2 {clip}: filtered clip 2 ## must be of the same format and dimension as "src" ## planes {int[]}: specify which planes to process ## unprocessed planes will be copied from "src" ## default: all planes will be processed, [0,1,2] for YUV/RGB input, [0] for Gray input ################################################################################################################################ def MaxFilter(src, flt1, flt2, planes=None): return _min_max_filter(src, flt1, flt2, planes, 'MaxFilter') ################################################################################################################################ ################################################################################################################################ ## Utility function: LimitFilter() ################################################################################################################################ ## Similar to the AviSynth function Dither_limit_dif16() and HQDeringmod_limit_dif16(). ## It acts as a post-processor, and is very useful to limit the difference of filtering while avoiding artifacts. ## Commonly used cases: ## de-banding ## de-ringing ## de-noising ## sharpening ## combining high precision source with low precision filtering: mvf.LimitFilter(src, flt, thr=1.0, elast=2.0) ################################################################################################################################ ## There are 2 implementations, default one with std.Expr, the other with std.Lut. ## The Expr version supports all mode, while the Lut version doesn't support float input and ref clip. ## Also the Lut version will truncate the filtering diff if it exceeds half the value range(128 for 8-bit, 32768 for 16-bit). ## The Lut version might be faster than Expr version in some cases, for example 8-bit input and brighten_thr != thr. ################################################################################################################################ ## Algorithm for Y/R/G/B plane (for chroma, replace "thr" and "brighten_thr" with "thrc") ## dif = flt - src ## dif_ref = flt - ref ## dif_abs = abs(dif_ref) ## thr_1 = brighten_thr if (dif > 0) else thr ## thr_2 = thr_1 * elast ## ## if dif_abs <= thr_1: ## final = flt ## elif dif_abs >= thr_2: ## final = src ## else: ## final = src + dif * (thr_2 - dif_abs) / (thr_2 - thr_1) ################################################################################################################################ ## Basic parameters ## flt {clip}: filtered clip, to compute the filtering diff ## can be of YUV/RGB/Gray color family, can be of 8-16 bit integer or 16/32 bit float ## src {clip}: source clip, to apply the filtering diff ## must be of the same format and dimension as "flt" ## ref {clip} (optional): reference clip, to compute the weight to be applied on filtering diff ## must be of the same format and dimension as "flt" ## default: None (use "src") ## thr {float}: threshold (8-bit scale) to limit filtering diff ## default: 1.0 ## elast {float}: elasticity of the soft threshold ## default: 2.0 ## planes {int[]}: specify which planes to process ## unprocessed planes will be copied from "flt" ## default: all planes will be processed, [0,1,2] for YUV/RGB input, [0] for Gray input ################################################################################################################################ ## Advanced parameters ## brighten_thr {float}: threshold (8-bit scale) for filtering diff that brightening the image (Y/R/G/B plane) ## set a value different from "thr" is useful to limit the overshoot/undershoot/blurring introduced in sharpening/de-ringing ## default is the same as "thr" ## thrc {float}: threshold (8-bit scale) for chroma (U/V/Co/Cg plane) ## default is the same as "thr" ## force_expr {bool} ## - True: force to use the std.Expr implementation ## - False: use the std.Lut implementation if available ## default: True ################################################################################################################################ def LimitFilter(flt, src, ref=None, thr=None, elast=None, brighten_thr=None, thrc=None, force_expr=None, planes=None): # input clip if not isinstance(flt, vs.VideoNode): raise type_error('"flt" must be a clip!') if not isinstance(src, vs.VideoNode): raise type_error('"src" must be a clip!') if ref is not None and not isinstance(ref, vs.VideoNode): raise type_error('"ref" must be a clip!') # Get properties of input clip sFormat = flt.format if sFormat.id != src.format.id: raise value_error('"flt" and "src" must be of the same format!') if flt.width != src.width or flt.height != src.height: raise value_error('"flt" and "src" must be of the same width and height!') if ref is not None: if sFormat.id != ref.format.id: raise value_error('"flt" and "ref" must be of the same format!') if flt.width != ref.width or flt.height != ref.height: raise value_error('"flt" and "ref" must be of the same width and height!') sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsYUV = sColorFamily == vs.YUV sSType = sFormat.sample_type sbitPS = sFormat.bits_per_sample sNumPlanes = sFormat.num_planes # Parameters if thr is None: thr = 1.0 elif isinstance(thr, int) or isinstance(thr, float): if thr < 0: raise value_error('valid range of "thr" is [0, +inf)') else: raise type_error('"thr" must be an int or a float!') if elast is None: elast = 2.0 elif isinstance(elast, int) or isinstance(elast, float): if elast < 1: raise value_error('valid range of "elast" is [1, +inf)') else: raise type_error('"elast" must be an int or a float!') if brighten_thr is None: brighten_thr = thr elif isinstance(brighten_thr, int) or isinstance(brighten_thr, float): if brighten_thr < 0: raise value_error('valid range of "brighten_thr" is [0, +inf)') else: raise type_error('"brighten_thr" must be an int or a float!') if thrc is None: thrc = thr elif isinstance(thrc, int) or isinstance(thrc, float): if thrc < 0: raise value_error('valid range of "thrc" is [0, +inf)') else: raise type_error('"thrc" must be an int or a float!') if force_expr is None: force_expr = True elif not isinstance(force_expr, int): raise type_error('"force_expr" must be a bool!') if ref is not None or sSType != vs.INTEGER: force_expr = True # planes process = [0 for i in range(VSMaxPlaneNum)] if planes is None: process = [1 for i in range(VSMaxPlaneNum)] elif isinstance(planes, int): if planes < 0 or planes >= VSMaxPlaneNum: raise value_error(f'valid range of "planes" is [0, {VSMaxPlaneNum})!') process[planes] = 1 elif isinstance(planes, Sequence): for p in planes: if not isinstance(p, int): raise type_error('"planes" must be a (sequence of) int!') elif p < 0 or p >= VSMaxPlaneNum: raise value_error(f'valid range of "planes" is [0, {VSMaxPlaneNum})!') process[p] = 1 else: raise type_error('"planes" must be a (sequence of) int!') # Process if thr <= 0 and brighten_thr <= 0: if sIsYUV: if thrc <= 0: return src else: return src if thr >= 255 and brighten_thr >= 255: if sIsYUV: if thrc >= 255: return flt else: return flt if thr >= 128 or brighten_thr >= 128: force_expr = True if force_expr: # implementation with std.Expr valueRange = (1 << sbitPS) - 1 if sSType == vs.INTEGER else 1 limitExprY = _limit_filter_expr(ref is not None, thr, elast, brighten_thr, valueRange) limitExprC = _limit_filter_expr(ref is not None, thrc, elast, thrc, valueRange) expr = [] for i in range(sNumPlanes): if process[i]: if i > 0 and (sIsYUV): expr.append(limitExprC) else: expr.append(limitExprY) else: expr.append("") if ref is None: clip = core.std.Expr([flt, src], expr) else: clip = core.std.Expr([flt, src, ref], expr) else: # implementation with std.MakeDiff, std.Lut and std.MergeDiff diff = core.std.MakeDiff(flt, src, planes=planes) if sIsYUV: if process[0]: diff = _limit_diff_lut(diff, thr, elast, brighten_thr, [0]) if process[1] or process[2]: _planes = [] if process[1]: _planes.append(1) if process[2]: _planes.append(2) diff = _limit_diff_lut(diff, thrc, elast, thrc, _planes) else: diff = _limit_diff_lut(diff, thr, elast, brighten_thr, planes) clip = core.std.MakeDiff(flt, diff, planes=planes) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Utility function: PointPower() ################################################################################################################################ ## Up-scaling by a power of 2 with nearest-neighborhood interpolation (point resize) ## Internally it involves std.Transpose, std.Interleave and std.DoubleWeave+std.SelectEvery for the scaling ## It will be faster than point-resizers for pure vertical scaling when the scaling ratio is not too large, especially for 8-bit input ################################################################################################################################ ## Basic parameters ## clip {clip}: clip to be scaled ## can be of any format ## vpow {float}: vertical scaling ratio is 2^vpow ## default: 1 ## hpow {float}: horizontal scaling ratio is 2^hpow ## default: 0 ################################################################################################################################ def PointPower(clip, vpow=None, hpow=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Parameters if vpow is None: vpow = 1 elif not isinstance(vpow, int): raise type_error('"vpow" must be an int!') elif vpow < 0: raise value_error('valid range of "vpow" is [0, +inf)!') if hpow is None: hpow = 0 elif not isinstance(hpow, int): raise type_error('"hpow" must be an int!') elif hpow < 0: raise value_error('valid range of "hpow" is [0, +inf)!') # Process if hpow > 0: clip = core.std.Transpose(clip) for _ in range(hpow): clip = core.std.Interleave([clip, clip]).std.DoubleWeave(True).std.SelectEvery(2, 0) clip = core.std.Transpose(clip) if vpow > 0: for _ in range(vpow): clip = core.std.Interleave([clip, clip]).std.DoubleWeave(True).std.SelectEvery(2, 0) # Output return AssumeFrame(clip) ################################################################################################################################ ################################################################################################################################ ## Utility function: CheckMatrix() ################################################################################################################################ ## *** EXPERIMENTAL *** I'm not sure whether it will work or not *** ################################################################################################################################ ## Check whether the input YUV clip matches specific color matrix ## Output is RGB24, out of range pixels will be marked as 255, indicating that the matrix may not be correct ## Additional plane mean values about the out of range pixels will be print on the frame ## Multiple matrices can be specified simultaneously, and multiple results will be stacked vertically ################################################################################################################################ ## Basic parameters ## clip {clip}: clip to evaluate ## must be of YUV color family ## matrices {str[]}: specify a (sequence of) matrix to test ## default: ['601', '709', '2020', '240', 'FCC', 'YCgCo'] ## full {bool}: define if input clip is of full range ## default: False for YUV input ## lower {float}: lower boundary for valid range (inclusive) ## default: -0.02 ## upper {float}: upper boundary for valid range (inclusive) ## default: 1.02 ################################################################################################################################ def CheckMatrix(clip, matrices=None, full=None, lower=None, upper=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily, ('YUV')) # Parameters if matrices is None: matrices = ['601', '709', '2020', '240', 'FCC', 'YCgCo'] elif isinstance(matrices, str): matrices = [matrices] elif not isinstance(matrices, Sequence): raise type_error('\'matrices\' must be a (sequence of) str!') if full is None: full = False elif not isinstance(full, int): raise type_error('\'full\' must be a bool!') if lower is None: lower = -0.02 elif not (isinstance(lower, float) or isinstance(lower, int)): raise type_error('\'lower\' must be an int or a float!') if upper is None: upper = 1.02 elif not (isinstance(upper, float) or isinstance(upper, int)): raise type_error('\'upper\' must be an int or a float!') # Process clip = ToYUV(clip, css='444', depth=32, full=full) props = ['RMean', 'GMean', 'BMean', 'TotalMean'] def _FrameProps(n, f): fout = f.copy() fout.props.__setattr__(props[3], (f.props.__getattr__(props[0]) + f.props.__getattr__(props[1]) + f.props.__getattr__(props[2])) / 3) return fout rgb_clips = [] for matrix in matrices: rgb_clip = ToRGB(clip, matrix=matrix) rgb_clip = rgb_clip.std.Expr(f'x {lower} < 1 x - x {upper} > x 0 ? ?') rgb_clip = PlaneAverage(rgb_clip, 0, props[0]) rgb_clip = PlaneAverage(rgb_clip, 1, props[1]) rgb_clip = PlaneAverage(rgb_clip, 2, props[2]) rgb_clip = core.std.ModifyFrame(rgb_clip, rgb_clip, selector=_FrameProps) rgb_clip = Depth(rgb_clip, depth=8, dither='none') rgb_clip = rgb_clip.text.FrameProps(props, alignment=7) rgb_clip = rgb_clip.text.Text(matrix, alignment=8) rgb_clips.append(rgb_clip) # Output return core.std.StackVertical(rgb_clips) ################################################################################################################################ ################################################################################################################################ ## Utility function: postfix2infix() ################################################################################################################################ ## Convert postfix expression (used by std.Expr) to infix expression ################################################################################################################################ ## Basic parameters ## expr {str}: the postfix expression to be converted ################################################################################################################################ def postfix2infix(expr): op1 = ['exp', 'log', 'sqrt', 'abs', 'not', 'dup'] op2 = ['+', '-', '*', '/', 'max', 'min', '>', '<', '=', '>=', '<=', 'and', 'or', 'xor', 'swap', 'pow'] op3 = ['?'] def remove_brackets(x): if x[0] == '(' and x[len(x) - 1] == ')': p = 1 for c in x[1:-1]: if c == '(': p += 1 elif c == ')': p -= 1 if p == 0: break if p == 1: return x[1:-1] return x if not isinstance(expr, str): raise type_error('\'expr\' must be a str!') expr_list = expr.split() stack = [] for item in expr_list: if op1.count(item) > 0: try: operand1 = stack.pop() except IndexError: raise value_error('Invalid expression, require operands.') if item == 'dup': stack.append(operand1) stack.append(operand1) else: stack.append(f'{item}({remove_brackets(operand1)})') elif op2.count(item) > 0: try: operand2 = stack.pop() operand1 = stack.pop() except IndexError: raise value_error('Invalid expression, require operands.') stack.append(f'({operand1} {item} {operand2})') elif op3.count(item) > 0: try: operand3 = stack.pop() operand2 = stack.pop() operand1 = stack.pop() except IndexError: raise value_error('Invalid expression, require operands.') stack.append(f'({operand1} {item} {operand2} : {operand3})') else: stack.append(item) if len(stack) > 1: raise value_error('Invalid expression, require operators.') return remove_brackets(stack[0]) ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Frame property functions below ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Frame property function: SetColorSpace() ################################################################################################################################ ## Modify the color space related frame properties in the given clip. ## Detailed descriptions of these properties: http://www.vapoursynth.com/doc/apireference.html ################################################################################################################################ ## Parameters ## %Any%: for the property named "_%Any%" ## - None: do nothing ## - True: do nothing ## - False: delete corresponding frame properties if exist ## - {int}: set to this value ################################################################################################################################ def SetColorSpace(clip, ChromaLocation=None, ColorRange=None, Primaries=None, Matrix=None, Transfer=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Modify frame properties if ChromaLocation is None: pass elif isinstance(ChromaLocation, bool): if ChromaLocation is False: clip = RemoveFrameProp(clip, '_ChromaLocation') elif isinstance(ChromaLocation, int): if ChromaLocation >= 0 and ChromaLocation <=5: clip = core.std.SetFrameProp(clip, prop='_ChromaLocation', intval=ChromaLocation) else: raise value_error('valid range of "ChromaLocation" is [0, 5]!') else: raise type_error('"ChromaLocation" must be an int or a bool!') if ColorRange is None: pass elif isinstance(ColorRange, bool): if ColorRange is False: clip = RemoveFrameProp(clip, '_ColorRange') elif isinstance(ColorRange, int): if ColorRange >= 0 and ColorRange <=1: clip = core.std.SetFrameProp(clip, prop='_ColorRange', intval=ColorRange) else: raise value_error('valid range of "ColorRange" is [0, 1]!') else: raise type_error('"ColorRange" must be an int or a bool!') if Primaries is None: pass elif isinstance(Primaries, bool): if Primaries is False: clip = RemoveFrameProp(clip, '_Primaries') elif isinstance(Primaries, int): clip = core.std.SetFrameProp(clip, prop='_Primaries', intval=Primaries) else: raise type_error('"Primaries" must be an int or a bool!') if Matrix is None: pass elif isinstance(Matrix, bool): if Matrix is False: clip = RemoveFrameProp(clip, '_Matrix') elif isinstance(Matrix, int): clip = core.std.SetFrameProp(clip, prop='_Matrix', intval=Matrix) else: raise type_error('"Matrix" must be an int or a bool!') if Transfer is None: pass elif isinstance(Transfer, bool): if Transfer is False: clip = RemoveFrameProp(clip, '_Transfer') elif isinstance(Transfer, int): clip = core.std.SetFrameProp(clip, prop='_Transfer', intval=Transfer) else: raise type_error('"Transfer" must be an int or a bool!') # Output return clip ################################################################################################################################ ################################################################################################################################ ## Frame property function: AssumeFrame() ################################################################################################################################ ## Set all the frames in the given clip to be frame-based(progressive). ## It can be used to prevent the field order set in de-interlace filters from being overridden by the frame property '_FieldBased'. ## Also it may be useful to be applied before upscaling or anti-aliasing scripts using EEDI3/nnedi3, etc.(whose field order should be specified explicitly) ################################################################################################################################ def AssumeFrame(clip): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Modify frame properties clip = core.std.SetFrameProp(clip, prop='_FieldBased', intval=0) clip = RemoveFrameProp(clip, '_Field') # Output return clip ################################################################################################################################ ################################################################################################################################ ## Frame property function: AssumeTFF() ################################################################################################################################ ## Set all the frames in the given clip to be top-field-first(interlaced). ## This frame property will override the field order set in those de-interlace filters. ################################################################################################################################ def AssumeTFF(clip): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Modify frame properties clip = core.std.SetFrameProp(clip, prop='_FieldBased', intval=2) clip = RemoveFrameProp(clip, '_Field') # Output return clip ################################################################################################################################ ################################################################################################################################ ## Frame property function: AssumeBFF() ################################################################################################################################ ## Set all the frames in the given clip to be bottom-field-first(interlaced). ## This frame property will override the field order set in those de-interlace filters. ################################################################################################################################ def AssumeBFF(clip): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Modify frame properties clip = core.std.SetFrameProp(clip, prop='_FieldBased', intval=1) clip = RemoveFrameProp(clip, '_Field') # Output return clip ################################################################################################################################ ################################################################################################################################ ## Frame property function: AssumeField() ################################################################################################################################ ## Set all the frames in the given clip to be field-based(derived from interlaced frame). ################################################################################################################################ ## Parameters ## top {bool}: ## - True: top-field-based ## - False: bottom-field-based ################################################################################################################################ def AssumeField(clip, top): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') if not isinstance(top, int): raise type_error('"top" must be a bool!') # Modify frame properties clip = RemoveFrameProp(clip, '_FieldBased') clip = core.std.SetFrameProp(clip, prop='_Field', intval=1 if top else 0) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Frame property function: AssumeCombed() ################################################################################################################################ ## Set all the frames in the given clip to be combed or not. ################################################################################################################################ ## Parameters ## combed {bool}: ## - None: delete property '_Combed' if exist ## - True: set property '_Combed' to 1 ## - False: set property '_Combed' to 0 ## default: True ################################################################################################################################ def AssumeCombed(clip, combed=True): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Modify frame properties if combed is None: clip = RemoveFrameProp(clip, '_Combed') elif not isinstance(combed, int): raise type_error('"combed" must be a bool!') else: clip = core.std.SetFrameProp(clip, prop='_Combed', intval=combed) # Output return clip ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Helper functions below ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Helper function: CheckVersion() ################################################################################################################################ ## Check if the version of mvsfunc matches the specified version requirements. ## For example, if you write a script requires at least mvsfunc-r5, use mvf.CheckVersion(5) to make sure r5 or later version is used. ################################################################################################################################ ## Parameters ## version {int} (mandatory): specify the required version number ## less {bool}: if False, raise error when this mvsfunc's version is less than the specified version ## default: False ## equal {bool}: if False, raise error when this mvsfunc's version is equal to the specified version ## default: True ## greater {bool}: if False, raise error when this mvsfunc's version is greater than the specified version ## default: True ################################################################################################################################ def CheckVersion(version, less=False, equal=True, greater=True): if not less and __version__ < version: raise ImportWarning(f'mvsfunc version ({__version__}) is less than the version ({version}) specified!') if not equal and __version__ == version: raise ImportWarning(f'mvsfunc version ({__version__}) is equal to the version ({version}) specified!') if not greater and __version__ > version: raise ImportWarning(f'mvsfunc version ({__version__}) is greater than the version ({version}) specified!') return True ################################################################################################################################ ################################################################################################################################ ## Helper function: GetMatrix() ################################################################################################################################ ## Return str or int format parameter "matrix". ################################################################################################################################ ## Parameters ## clip {clip}: the source clip ## matrix {int|str}: explicitly specify matrix in int or str format, not case-sensitive ## - 0 | "RGB" ## - 1 | "709" | "bt709" ## - 2 | "Unspecified": same as not specified (None) ## - 4 | "FCC" ## - 5 | "bt470bg": same as "601" ## - 6 | "601" | "smpte170m" ## - 7 | "240" | "smpte240m" ## - 8 | "YCgCo" | "YCoCg" ## - 9 | "2020" | "bt2020nc" ## - 10 | "2020cl" | "bt2020c" ## - 100 | "OPP" | "opponent": same as the opponent color space used in BM3D denoising filter ## default: guessed according to the color family and size of input clip ## dIsRGB {bool}: specify if the target is RGB ## If source and target are both RGB and "matrix" is not specified, then assume matrix="RGB". ## default: False for RGB input, otherwise True ## id {bool}: ## - False: output matrix name{str} ## - True: output matrix id{int} ## default: False ################################################################################################################################ def GetMatrix(clip, matrix=None, dIsRGB=None, id=False): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsRGB = sColorFamily == vs.RGB # Get properties of output clip if dIsRGB is None: dIsRGB = not sIsRGB elif not isinstance(dIsRGB, int): raise type_error('"dIsRGB" must be a bool!') # id if not isinstance(id, int): raise type_error('"id" must be a bool!') # Resolution level SD = False UHD = False if clip.width <= 1024 and clip.height <= 576: SD = True elif clip.width <= 2048 and clip.height <= 1536: pass # HD else: UHD = True # Convert to string format if matrix is None: matrix = "Unspecified" elif not isinstance(matrix, (int, str)): raise type_error('"matrix" must be an int or a str!') else: if isinstance(matrix, str): matrix = matrix.lower() if matrix == 0 or matrix == "rgb": # GBR matrix = 0 if id else "RGB" elif matrix == 1 or matrix == "709" or matrix == "bt709": # bt709 matrix = 1 if id else "709" elif matrix == 2 or matrix == "unspecified" or matrix == "unspec": # Unspecified matrix = 2 if id else "Unspecified" elif matrix == 4 or matrix == "fcc": # fcc matrix = 4 if id else "FCC" elif matrix == 5 or matrix == "bt470bg" or matrix == "470bg": # bt470bg matrix = 5 if id else "601" elif matrix == 6 or matrix == "601" or matrix == "smpte170m" or matrix == "170m": # smpte170m matrix = 6 if id else "601" elif matrix == 7 or matrix == "240" or matrix == "smpte240m": # smpte240m matrix = 7 if id else "240" elif matrix == 8 or matrix == "ycgco" or matrix == "ycocg": # YCgCo matrix = 8 if id else "YCgCo" elif matrix == 9 or matrix == "2020" or matrix == "bt2020nc" or matrix == "2020ncl": # bt2020nc matrix = 9 if id else "2020" elif matrix == 10 or matrix == "2020cl" or matrix == "bt2020c": # bt2020c matrix = 10 if id else "2020cl" elif matrix == 100 or matrix == "opp" or matrix == "opponent": # opponent color space matrix = 100 if id else "OPP" else: raise value_error('Unsupported matrix specified!') # If unspecified, automatically determine it based on color family and resolution level if matrix == 2 or matrix == "Unspecified": if dIsRGB and sIsRGB: matrix = 0 if id else "RGB" else: matrix = (6 if id else "601") if SD else (9 if id else "2020") if UHD else (1 if id else "709") # Output return matrix ################################################################################################################################ ################################################################################################################################ ## Helper function: zDepth() ################################################################################################################################ ## Smart function to utilize zimg depth conversion for both 1.0 and 2.0 API of vszimg as well as core.resize. ## core.resize is preferred now. ################################################################################################################################ def zDepth(clip, sample=None, depth=None, range=None, range_in=None, dither_type=None, cpu_type=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format # Get properties of output clip if sample is None: sample = sFormat.sample_type elif not isinstance(sample, int): raise type_error('"sample" must be an int!') if depth is None: depth = sFormat.bits_per_sample elif not isinstance(depth, int): raise type_error('"depth" must be an int!') format = RegisterFormat(sFormat.color_family, sample, depth, sFormat.subsampling_w, sFormat.subsampling_h) # Process zimgResize = hasattr(core, 'resize') zimgPlugin = hasattr(core, 'z') if zimgResize: # VapourSynth resizer clip = core.resize.Bicubic(clip, format=format.id, range=range, range_in=range_in, dither_type=dither_type, cpu_type=cpu_type) elif zimgPlugin and hasattr(core.z, 'Format'): # vszimg 2.0 clip = core.z.Format(clip, format=format.id, range=range, range_in=range_in, dither_type=dither_type, cpu_type=cpu_type) elif zimgPlugin and hasattr(core.z, 'Depth'): # vszimg 1.0 clip = core.z.Depth(clip, dither=dither_type, sample=sample, depth=depth, fullrange_in=range_in, fullrange_out=range) else: raise attribute_error('no available core.resize or zimg found!') # Output return clip ################################################################################################################################ ################################################################################################################################ ## Helper function: PlaneAverage() ################################################################################################################################ ## Evaluate normalized average value of the specified plane and store it as a frame property. ## Mainly as a wrap function to support both std.PlaneAverage and std.PlaneStats with the same interface. ################################################################################################################################ ## Parameters ## clip {clip}: the source clip ## can be of any constant format ## plane {int}: the plane to evaluate ## default: 0 ## prop {str}: the frame property name to be written ## default: 'PlaneAverage' ################################################################################################################################ def PlaneAverage(clip, plane=None, prop=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sNumPlanes = sFormat.num_planes # Parameters if plane is None: plane = 0 elif not isinstance(plane, int): raise type_error('"plane" must be an int!') elif plane < 0 or plane > sNumPlanes: raise value_error(f'valid range of "plane" is [0, {sNumPlanes})!') if prop is None: prop = 'PlaneAverage' elif not isinstance(prop, str): raise type_error('"prop" must be a str!') # Process if hasattr(core.std, 'PlaneAverage'): clip = core.std.PlaneAverage(clip, plane=plane, prop=prop) elif hasattr(core.std, 'PlaneStats'): clip = core.std.PlaneStats(clip, plane=plane, prop='PlaneStats') def _PlaneAverageTransfer(n, f): fout = f.copy() fout.props.__setattr__(prop, f.props.PlaneStatsAverage) del fout.props.PlaneStatsAverage del fout.props.PlaneStatsMinMax return fout clip = core.std.ModifyFrame(clip, clip, selector=_PlaneAverageTransfer) else: raise attribute_error('no available plane average function found!') # output return clip ################################################################################################################################ ################################################################################################################################ ## Helper function: GetPlane() ################################################################################################################################ ## Extract specific plane and store it in a Gray clip. ################################################################################################################################ ## Parameters ## clip {clip}: the source clip ## can be of any constant format ## plane {int}: the plane to extract ## default: 0 ################################################################################################################################ def GetPlane(clip, plane=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sNumPlanes = sFormat.num_planes # Parameters if plane is None: plane = 0 elif not isinstance(plane, int): raise type_error('"plane" must be an int!') elif plane < 0 or plane > sNumPlanes: raise value_error(f'valid range of "plane" is [0, {sNumPlanes})!') # Process return core.std.ShufflePlanes(clip, plane, vs.GRAY) ################################################################################################################################ ################################################################################################################################ ## Helper function: GrayScale() ################################################################################################################################ ## Convert the given clip to gray-scale. ################################################################################################################################ ## Parameters ## clip {clip}: the source clip ## can be of any constant format ## matrix {int|str}: for RGB input only, same as the one in ToYUV() ################################################################################################################################ def GrayScale(clip, matrix=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!') # Get properties of input clip sFormat = clip.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsRGB = sColorFamily == vs.RGB sIsGRAY = sColorFamily == vs.GRAY # Process if sIsGRAY: pass elif sIsRGB: clip = ToYUV(clip, matrix=matrix, full=True) clip = core.std.ShufflePlanes(clip, [0,0,0], sColorFamily) else: blank = clip.std.BlankClip() clip = core.std.ShufflePlanes([clip,blank,blank], [0,1,2], sColorFamily) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Helper function: Preview() ################################################################################################################################ ## Convert the given clip or clips to the same RGB format. ## When multiple clips is given, they will be interleaved together, and resized to the same dimension using "Catmull-Rom". ################################################################################################################################ ## Set "plane" if you want to output a specific plane. ################################################################################################################################ ## matrix, full, dither, kernel, a1, a2 correspond to matrix_in, range_in, dither_type, ## resample_filter_uv, filter_param_a_uv, filter_param_b_uv in resize.Bicubic. ## "matrix" is passed to GetMatrix(id=True) first. ## default dither: random ## default chroma resampler: kernel="bicubic", a1=0, a2=0.5, also known as "Catmull-Rom" ################################################################################################################################ def Preview(clips, plane=None, matrix=None, full=None, depth=None, dither=None, kernel=None, a1=None, a2=None): # input clip if isinstance(clips, vs.VideoNode): ref = clips elif isinstance(clips, Sequence): for c in clips: if not isinstance(c, vs.VideoNode): raise type_error('"clips" must be a clip or a sequence of clips!') ref = clips[0] else: raise type_error('"clips" must be a clip or a sequence of clips!') # Get properties of output clip if depth is None: depth = 8 elif not isinstance(depth, int): raise type_error('"depth" must be an int!') if depth >= 32: sample = vs.FLOAT else: sample = vs.INTEGER dFormat = RegisterFormat(vs.RGB, sample, depth, 0, 0).id # Parameters if dither is None: dither = "random" if kernel is None: kernel = "bicubic" if a1 is None and a2 is None: a1 = 0 a2 = 0.5 # Conversion def _Conv(clip): if plane is not None: clip = GetPlane(clip, plane) return core.resize.Bicubic(clip, ref.width, ref.height, format=dFormat, matrix_in=GetMatrix(clip, matrix, True, True), range_in=full, filter_param_a=0, filter_param_b=0.5, resample_filter_uv=kernel, filter_param_a_uv=a1, filter_param_b_uv=a2, dither_type=dither) if isinstance(clips, vs.VideoNode): clip = _Conv(clips) elif isinstance(clips, Sequence): clips = [_Conv(c) for c in clips] clip = core.std.Interleave(clips) # Output return clip ################################################################################################################################ ################################################################################################################################ ## Helper function: CheckColorFamily() ################################################################################################################################ def CheckColorFamily(color_family, valid_list=None, invalid_list=None): if valid_list is None: valid_list = ('RGB', 'YUV', 'GRAY') if invalid_list is None: invalid_list = ('COMPAT', 'UNDEFINED') # check invalid list for cf in invalid_list: if color_family == getattr(vs, cf, None): raise value_error(f'color family *{cf}* is not supported!') # check valid list if valid_list: if color_family not in [getattr(vs, cf, None) for cf in valid_list]: raise value_error(f'color family not supported, only {valid_list} are accepted') ################################################################################################################################ ################################################################################################################################ ## Helper function: RemoveFrameProp() ################################################################################################################################ def RemoveFrameProp(clip, prop): if hasattr(core.std, 'RemoveFrameProps'): # API >= 4 return core.std.RemoveFrameProps(clip, prop) return core.std.SetFrameProp(clip, prop, delete=True) ################################################################################################################################ ################################################################################################################################ ## Helper function: RegisterFormat() ################################################################################################################################ def RegisterFormat(color_family, sample_type, bits_per_sample, subsampling_w, subsampling_h): if hasattr(core, 'query_video_format'): # API >= 4 return core.query_video_format(color_family, sample_type, bits_per_sample, subsampling_w, subsampling_h) return core.register_format(color_family, sample_type, bits_per_sample, subsampling_w, subsampling_h) ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ## Internal used functions below ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ ################################################################################################################################ def get_func_name(num_of_call_stacks=1): import inspect frame = inspect.currentframe() for _ in range(num_of_call_stacks): frame = frame.f_back return frame.f_code.co_name ################################################################################################################################ def exception(obj1, *args, num_stacks=1): name = get_func_name(num_stacks + 1) return Exception(f'[mvsfunc.{name}] {obj1}', *args) ################################################################################################################################ def type_error(obj1, *args, num_stacks=1): name = get_func_name(num_stacks + 1) return TypeError(f'[mvsfunc.{name}] {obj1}', *args) ################################################################################################################################ def value_error(obj1, *args, num_stacks=1): name = get_func_name(num_stacks + 1) return ValueError(f'[mvsfunc.{name}] {obj1}', *args) ################################################################################################################################ def attribute_error(obj1, *args, num_stacks=1): name = get_func_name(num_stacks + 1) return AttributeError(f'[mvsfunc.{name}] {obj1}', *args) ################################################################################################################################ ################################################################################################################################ ## Internal used function to calculate quantization parameters ################################################################################################################################ def _quantization_parameters(sample=None, depth=None, full=None, chroma=None): qp = {} if sample is None: sample = vs.INTEGER if depth is None: depth = 8 elif depth < 1: raise value_error('"depth" should not be less than 1!', num_stacks=2) if full is None: full = True if chroma is None: chroma = False lShift = depth - 8 rShift = 8 - depth if sample == vs.INTEGER: if chroma: qp['floor'] = 0 if full else 16 << lShift if lShift >= 0 else 16 >> rShift qp['neutral'] = 128 << lShift if lShift >= 0 else 128 >> rShift qp['ceil'] = (1 << depth) - 1 if full else 240 << lShift if lShift >= 0 else 240 >> rShift qp['range'] = qp['ceil'] - qp['floor'] else: qp['floor'] = 0 if full else 16 << lShift if lShift >= 0 else 16 >> rShift qp['neutral'] = qp['floor'] qp['ceil'] = (1 << depth) - 1 if full else 235 << lShift if lShift >= 0 else 235 >> rShift qp['range'] = qp['ceil'] - qp['floor'] elif sample == vs.FLOAT: if chroma: qp['floor'] = -0.5 qp['neutral'] = 0.0 qp['ceil'] = 0.5 qp['range'] = qp['ceil'] - qp['floor'] else: qp['floor'] = 0.0 qp['neutral'] = qp['floor'] qp['ceil'] = 1.0 qp['range'] = qp['ceil'] - qp['floor'] else: raise value_error('Unsupported "sample" specified!', num_stacks=2) return qp ################################################################################################################################ ################################################################################################################################ ## Internal used function to do quantization conversion with std.Expr ################################################################################################################################ def _quantization_conversion(clip, depths=None, depthd=None, sample=None, fulls=None, fulld=None, chroma=None, clamp=None, dbitPS=None, mode=None): # input clip if not isinstance(clip, vs.VideoNode): raise type_error('"clip" must be a clip!', num_stacks=2) # Get properties of input clip sFormat = clip.format sColorFamily = sFormat.color_family CheckColorFamily(sColorFamily) sIsYUV = sColorFamily == vs.YUV sIsGRAY = sColorFamily == vs.GRAY sbitPS = sFormat.bits_per_sample sSType = sFormat.sample_type if depths is None: depths = sbitPS elif not isinstance(depths, int): raise type_error('"depths" must be an int!', num_stacks=2) if fulls is None: # If not set, assume limited range for YUV and Gray input fulls = False if sIsYUV or sIsGRAY else True elif not isinstance(fulls, int): raise type_error('"fulls" must be a bool!', num_stacks=2) if chroma is None: chroma = False elif not isinstance(chroma, int): raise type_error('"chroma" must be a bool!', num_stacks=2) elif not sIsGRAY: chroma = False # Get properties of output clip if depthd is None: pass elif not isinstance(depthd, int): raise type_error('"depthd" must be an int!', num_stacks=2) if sample is None: if depthd is None: dSType = sSType depthd = depths else: dSType = vs.FLOAT if dbitPS >= 32 else vs.INTEGER elif not isinstance(sample, int): raise type_error('"sample" must be an int!', num_stacks=2) elif sample != vs.INTEGER and sample != vs.FLOAT: raise value_error('"sample" must be either 0 (vs.INTEGER) or 1 (vs.FLOAT)!', num_stacks=2) else: dSType = sample if dSType == vs.INTEGER and (dbitPS < 1 or dbitPS > 16): raise value_error(f'{dbitPS}-bit integer output is not supported!', num_stacks=2) if dSType == vs.FLOAT and (dbitPS != 16 and dbitPS != 32): raise value_error(f'{dbitPS}-bit float output is not supported!', num_stacks=2) if fulld is None: fulld = fulls elif not isinstance(fulld, int): raise type_error('"fulld" must be a bool!', num_stacks=2) if clamp is None: clamp = dSType == vs.INTEGER elif not isinstance(clamp, int): raise type_error('"clamp" must be a bool!', num_stacks=2) if dbitPS is None: if depthd < 8: dbitPS = 8 else: dbitPS = depthd elif not isinstance(dbitPS, int): raise type_error('"dbitPS" must be an int!', num_stacks=2) if mode is None: mode = 0 elif not isinstance(mode, int): raise type_error('"mode" must be an int!', num_stacks=2) elif depthd >= 8: mode = 0 dFormat = RegisterFormat(sFormat.color_family, dSType, dbitPS, sFormat.subsampling_w, sFormat.subsampling_h) # Expression function def gen_expr(chroma, mode): if dSType == vs.INTEGER: exprLower = 0 exprUpper = 1 << (dFormat.bytes_per_sample * 8) - 1 else: exprLower = float('-inf') exprUpper = float('inf') sQP = _quantization_parameters(sSType, depths, fulls, chroma) dQP = _quantization_parameters(dSType, depthd, fulld, chroma) gain = dQP['range'] / sQP['range'] offset = dQP['neutral' if chroma else 'floor'] - sQP['neutral' if chroma else 'floor'] * gain if mode == 1: scale = 256 gain = gain * scale offset = offset * scale else: scale = 1 if gain != 1 or offset != 0 or clamp: expr = " x " if gain != 1: expr = expr + f" {gain} * " if offset != 0: expr = expr + f" {offset} + " if clamp: if dQP['floor'] * scale > exprLower: expr = expr + f" {dQP['floor'] * scale} max " if dQP['ceil'] * scale < exprUpper: expr = expr + f" {dQP['ceil'] * scale} min " else: expr = "" return expr # Process Yexpr = gen_expr(False, mode) Cexpr = gen_expr(True, mode) if sIsYUV: expr = [Yexpr, Cexpr] elif sIsGRAY and chroma: expr = Cexpr else: expr = Yexpr clip = core.std.Expr(clip, expr, format=dFormat.id) # Output clip = SetColorSpace(clip, ColorRange=0 if fulld else 1) return clip ################################################################################################################################ ################################################################################################################################ ## Internal used function to check the argument for frame property ################################################################################################################################ def _check_arg_prop(arg, default=None, defaultTrue=None, argName='arg'): if defaultTrue is None: defaultTrue = default if arg is None: arg = default elif isinstance(arg, int): if arg: arg = defaultTrue elif isinstance(arg, str): if arg: if not arg.isidentifier(): raise value_error(f'{argName}="{arg}" is not a valid identifier!', num_stacks=2) else: arg = False else: raise type_error(f'"{argName}" must be a str or a bool!', num_stacks=2) return arg ################################################################################################################################ ################################################################################################################################ ## Internal used function for Min(), Max() and Avg() ################################################################################################################################ def _operator2(clip1, clip2, mode, neutral, name): # input clip if not isinstance(clip1, vs.VideoNode): raise type_error('"clip1" must be a clip!', num_stacks=2) if not isinstance(clip2, vs.VideoNode): raise type_error('"clip2" must be a clip!', num_stacks=2) # Get properties of input clip sFormat = clip1.format if sFormat.id != clip2.format.id: raise value_error('"clip1" and "clip2" must be of the same format!', num_stacks=2) if clip1.width != clip2.width or clip1.height != clip2.height: raise value_error('"clip1" and "clip2" must be of the same width and height!', num_stacks=2) sSType = sFormat.sample_type sbitPS = sFormat.bits_per_sample sNumPlanes = sFormat.num_planes # mode if mode is None: mode = [1 for i in range(VSMaxPlaneNum)] elif isinstance(mode, int): mode = [mode for i in range(VSMaxPlaneNum)] elif isinstance(mode, list): for m in mode: if not isinstance(m, int): raise type_error('"mode" must be a (sequence of) int!', num_stacks=2) while len(mode) < VSMaxPlaneNum: mode.append(mode[len(mode) - 1]) else: raise type_error('"mode" must be a (sequence of) int!', num_stacks=2) # neutral if neutral is None: neutral = 1 << (sbitPS - 1) if sSType == vs.INTEGER else 0 elif not (isinstance(neutral, int) or isinstance(neutral, float)): raise type_error('"neutral" must be an int or a float!', num_stacks=2) # Process and output expr = [] for i in range(sNumPlanes): if name == 'Min': if mode[i] >= 2: expr.append(f"y {neutral} - abs x {neutral} - abs < y x ?") elif mode[i] == 1: expr.append("x y min") else: expr.append("") elif name == 'Max': if mode[i] >= 2: expr.append(f"y {neutral} - abs x {neutral} - abs > y x ?") elif mode[i] == 1: expr.append("x y max") else: expr.append("") elif name == 'Avg': if mode[i] >= 1: expr.append("x y + 2 /") else: expr.append("") else: raise value_error('Unknown "name" specified!', num_stacks=1) return core.std.Expr([clip1, clip2], expr) ################################################################################################################################ ################################################################################################################################ ## Internal used function for MinFilter() and MaxFilter() ################################################################################################################################ def _min_max_filter(src, flt1, flt2, planes, name): # input clip if not isinstance(src, vs.VideoNode): raise type_error('"src" must be a clip!', num_stacks=2) if not isinstance(flt1, vs.VideoNode): raise type_error('"flt1" must be a clip!', num_stacks=2) if not isinstance(flt2, vs.VideoNode): raise type_error('"flt2" must be a clip!', num_stacks=2) # Get properties of input clip sFormat = src.format if sFormat.id != flt1.format.id or sFormat.id != flt2.format.id: raise value_error('"src", "flt1" and "flt2" must be of the same format!', num_stacks=2) if src.width != flt1.width or src.height != flt1.height or src.width != flt2.width or src.height != flt2.height: raise value_error('"src", "flt1" and "flt2" must be of the same width and height!', num_stacks=2) sNumPlanes = sFormat.num_planes # planes process = [0 for i in range(VSMaxPlaneNum)] if planes is None: process = [1 for i in range(VSMaxPlaneNum)] elif isinstance(planes, int): if planes < 0 or planes >= VSMaxPlaneNum: raise value_error(f'valid range of "planes" is [0, {VSMaxPlaneNum})!', num_stacks=2) process[planes] = 1 elif isinstance(planes, Sequence): for p in planes: if not isinstance(p, int): raise type_error('"planes" must be a (sequence of) int!') elif p < 0 or p >= VSMaxPlaneNum: raise value_error(f'valid range of "planes" is [0, {VSMaxPlaneNum})!', num_stacks=2) process[p] = 1 else: raise type_error('"planes" must be a (sequence of) int!', num_stacks=2) # Process and output expr = [] for i in range(sNumPlanes): if process[i]: if name == 'MinFilter': expr.append("x z - abs x y - abs < z y ?") elif name == 'MaxFilter': expr.append("x z - abs x y - abs > z y ?") else: raise value_error('Unknown "name" specified!', num_stacks=1) else: expr.append("") return core.std.Expr([src, flt1, flt2], expr) ################################################################################################################################ ################################################################################################################################ ## Internal used functions for LimitFilter() ################################################################################################################################ def _limit_filter_expr(defref, thr, elast, largen_thr, value_range): flt = " x " src = " y " ref = " z " if defref else src dif = f" {flt} {src} - " dif_ref = f" {flt} {ref} - " dif_abs = dif_ref + " abs " thr = thr * value_range / 255 largen_thr = largen_thr * value_range / 255 if thr <= 0 and largen_thr <= 0: limitExpr = f" {src} " elif thr >= value_range and largen_thr >= value_range: limitExpr = "" else: if thr <= 0: limitExpr = f" {src} " elif thr >= value_range: limitExpr = f" {flt} " elif elast <= 1: limitExpr = f" {dif_abs} {thr} <= {flt} {src} ? " else: thr_1 = thr thr_2 = thr * elast thr_slope = 1 / (thr_2 - thr_1) # final = src + dif * (thr_2 - dif_abs) / (thr_2 - thr_1) limitExpr = f" {src} {dif} {thr_2} {dif_abs} - * {thr_slope} * + " limitExpr = f" {dif_abs} {thr_1} <= {flt} {dif_abs} {thr_2} >= {src} " + limitExpr + " ? ? " if largen_thr != thr: if largen_thr <= 0: limitExprLargen = f" {src} " elif largen_thr >= value_range: limitExprLargen = f" {flt} " elif elast <= 1: limitExprLargen = f" {dif_abs} {largen_thr} <= {flt} {src} ? " else: thr_1 = largen_thr thr_2 = largen_thr * elast thr_slope = 1 / (thr_2 - thr_1) # final = src + dif * (thr_2 - dif_abs) / (thr_2 - thr_1) limitExprLargen = f" {src} {dif} {thr_2} {dif_abs} - * {thr_slope} * + " limitExprLargen = f" {dif_abs} {thr_1} <= {flt} {dif_abs} {thr_2} >= {src} " + limitExprLargen + " ? ? " limitExpr = f" {flt} {ref} > " + limitExprLargen + " " + limitExpr + " ? " return limitExpr ################################################################################################################################ ################################################################################################################################ ## Internal used functions for LimitFilter() ################################################################################################################################ def _limit_diff_lut(diff, thr, elast, largen_thr, planes): # input clip if not isinstance(diff, vs.VideoNode): raise type_error('"diff" must be a clip!', num_stacks=2) # Get properties of input clip sFormat = diff.format sSType = sFormat.sample_type sbitPS = sFormat.bits_per_sample if sSType == vs.INTEGER: neutral = 1 << (sbitPS - 1) value_range = (1 << sbitPS) - 1 else: neutral = 0 value_range = 1 raise value_error('"diff" must be an int!', num_stacks=2) # Process thr = thr * value_range / 255 largen_thr = largen_thr * value_range / 255 ''' # for std.MergeDiff(src, limitedDiff) if thr <= 0 and largen_thr <= 0: def limitLut(x): return neutral return core.std.Lut(diff, planes=planes, function=limitLut) elif thr >= value_range / 2 and largen_thr >= value_range / 2: return diff elif elast <= 1: def limitLut(x): dif = x - neutral dif_abs = abs(dif) thr_1 = largen_thr if dif > 0 else thr return x if dif_abs <= thr_1 else neutral return core.std.Lut(diff, planes=planes, function=limitLut) else: def limitLut(x): dif = x - neutral dif_abs = abs(dif) thr_1 = largen_thr if dif > 0 else thr thr_2 = thr_1 * elast thr_slope = 1 / (thr_2 - thr_1) if dif_abs <= thr_1: return x elif dif_abs >= thr_2: return neutral else: # final = src - dif * ((dif_abs - thr_1) / (thr_2 - thr_1) - 1) return round(dif * (thr_2 - dif_abs) * thr_slope + neutral) ''' # for std.MakeDiff(flt, limitedDiff) if thr <= 0 and largen_thr <= 0: return diff elif thr >= value_range / 2 and largen_thr >= value_range / 2: def limitLut(x): return neutral return core.std.Lut(diff, planes=planes, function=limitLut) elif elast <= 1: def limitLut(x): dif = x - neutral dif_abs = abs(dif) thr_1 = largen_thr if dif > 0 else thr return neutral if dif_abs <= thr_1 else x return core.std.Lut(diff, planes=planes, function=limitLut) else: def limitLut(x): dif = x - neutral dif_abs = abs(dif) thr_1 = largen_thr if dif > 0 else thr thr_2 = thr_1 * elast if dif_abs <= thr_1: return neutral elif dif_abs >= thr_2: return x else: # final = flt - dif * (dif_abs - thr_1) / (thr_2 - thr_1) thr_slope = 1 / (thr_2 - thr_1) return round(dif * (dif_abs - thr_1) * thr_slope + neutral) return core.std.Lut(diff, planes=planes, function=limitLut) ################################################################################################################################