There's a
thread on the Avisynth forum about how to apply a filter using different arguments controlled by a gradient mask. The author asked to see my VapourSynth implementation of that idea so I'm posting it here.
Code:
import vapoursynth as vs
import adjust
from functools import partial
core = vs.core
# creates a 8-bit gray gradient mask
# set steps to a value 2-254 to have less than 255 different gray values
def makeMask(clip, steps=None):
format = vs.GRAY8
maxval = 255
stripes = []
height = steps or clip.height
for i in range(height):
alpha = maxval * i / (height-1)
stripes.append(core.std.BlankClip(format=format, color=alpha, width=clip.width, height=1))
gradientMask = core.std.StackVertical(stripes)
if gradientMask.height < clip.height:
gradientMask = core.resize.Point(gradientMask, width=clip.width, height=clip.height)
return gradientMask
# apply gradient mask + function, assumes 8-bit mask, returns result clip as YUV444P8
def doGradient(mask, clip, gradientFunc):
# convert video to YUV444P8 in order to access per pixel chroma
clip = core.resize.Bilinear(clip, clip.width, clip.height, format=vs.YUV444P8)
# this function runs for every frame
# n = frame number
# f = frame properties
# mask = mask clip
# src = source clip to apply the effect to
# i = current mask value (0-255)
def applyFunc(n, f, mask, src, i):
# skip if the mask has all zeroes (max value is zero)
if f.props['PlaneStatsMax'] > 0:
# calculate the effect and return it for the current frame, function gets called with argument ratio between 0.0 - 1.0
return gradientFunc(src, i/255)
# no effect applied, return source clip as is
return src
# create final "canvas" where final result will be composited to
final = core.std.BlankClip(clip, clip.width, clip.height)
# iterate through all possible mask values (0-255)
for i in range(256):
# create mask with 255 where original mask has value i, zero otherwise
m = core.std.Expr(mask, f'x {i} = 255 0 ?')
# calculate basic statistics for the mask (average, min and max brightness), will be used to skip unnecessary processing
m = core.std.PlaneStats(m, plane=0)
# evaluate applyFunc() for every frame, returns effect applied to whole frame if mask m is not all zeroes
gradient_clip = core.std.FrameEval(final, partial(applyFunc, mask=m, src=clip, i=i), prop_src=m)
# create a 3-channel mask to extract the parts where mask m has value 255
yuv_mask = core.std.ShufflePlanes(clips=[m,m,m], planes=[0,0,0], colorfamily=vs.YUV)
# draw the contribution of mask value i to canvas (take pixels from gradient_clip where mask = 255)
final = core.std.Expr([final, gradient_clip, yuv_mask], 'z 255 = y x ?')
# return final composition
return final
# example function: blur
def blur_func(clip, ratio):
clip = core.std.BoxBlur(clip, hradius=ratio*40, hpasses=1)
#clip = core.std.BoxBlur(clip, vradius=ratio*40, vpasses=1)
return clip
# example function: tweak (brightness, contrast)
def tweak_func(clip, ratio):
clip = adjust.Tweak(clip, bright=-ratio*100.0, cont=1.0 + ratio)
#clip = adjust.Tweak(clip, cont=1.0 + ratio*0.5)
return clip
# load source
src = core.ffms2.Source(source = 'source.avi')
# create a static gradient mask (you could use a video for a mask, too)
mask = makeMask(src, steps=None)
# apply mask + given function to source
result = doGradient(mask, src, blur_func)
# show results
result.set_output()
Hopefully there are some ideas that could be used in the Avisynth version (which currently has some memory issues).
This version runs about 1 fps on a 1080x576 source video using a mask with 256 different values and 5-6 fps using a mask with 16 different values. The mask could be created from a video clip as well, this example just creates a static gradient.