Skip to content

Instantly share code, notes, and snippets.

@astiob
Last active August 15, 2025 23:41
Show Gist options
  • Save astiob/9e687c04d2e712fed930318b1a9b00b5 to your computer and use it in GitHub Desktop.
Save astiob/9e687c04d2e712fed930318b1a9b00b5 to your computer and use it in GitHub Desktop.
VapourSynth script for converting interlaced fades over telecined content to pure-progressive fades
import vapoursynth as vs
from vsdeinterlace import vinverse
__all__ = (
'smooth_stable_fade',
'unfade_unique_into_repeating',
'unfade_repeating_into_unique',
'fade_unique_into_repeating',
'fade_repeating_into_unique',
'fade_repeating_into_repeating',
)
c = vs.core
def smooth_stable_fade_fields(clip, first, last, triple_field_index, *, delay_fields: float = 0, extra_clips=[], merge_expr=None, unique_expr=None):
end = last + 1
clip = clip.std.SeparateFields()
first *= 2
end *= 2
offset = 0
match triple_field_index:
case 3:
if delay_fields < 1 and first:
clip = c.std.Splice([clip[:first], clip[first - 2], clip[first + 1:]])
offset = -1
case 4 if not first:
offset = -2
case 4:
match delay_fields:
case 0:
offset = 1
case 1:
clip = c.std.Splice([clip[:first + 1], clip[first - 1], clip[first + 2:]])
offset = -2
case _:
# TODO: replace clip[first-1] and clip[first+1] by their weighted average matching clip[first]
raise ValueError(f'{delay_fields=} is not yet implemented for fades starting in the middle of a pulled-down frame')
first -= offset
triple_field_index = (triple_field_index + offset) % 5
n = end - first
match (n - triple_field_index) % 5:
case 1:
if delay_fields > 0 and end < clip.num_frames:
clip = c.std.Splice([clip[:end - 1], clip[end + 1], clip[end:]])
end -= 1
case 2 if end >= clip.num_frames:
end -= 2
case 2:
match delay_fields:
case 1:
end += 1
case 0:
clip = c.std.Splice([clip[:end - 2], clip[end], clip[end - 1:]])
end -= 2
case _:
# TODO: replace clip[end-2] and clip[end] by their weighted average matching clip[end-1]
raise ValueError(f'{delay_fields=} is not yet implemented for fades ending in the middle of a pulled-down frame')
fade = clip[first:end].akarin.PropExpr(lambda: {'FadeFieldIndex': f'N {offset} -'})
arg_clips = [fade] + [extra_clip[first:end] for extra_clip in extra_clips]
a = triple_field_index
b = a + 2
if merge_expr is None:
mean = c.std.Merge(fade[a::5], fade[b::5])
else:
mean = c.akarin.Expr([arg_clip[i::5] for arg_clip in arg_clips for i in (a, b)], merge_expr)
mean = mean.akarin.PropExpr(lambda: {'FadeFieldIndex': f'N 5 * {a + 1 - offset} +'})
if unique_expr is None:
cycle = [mean if i in (a, b) else fade[i::5] for i in range(5)]
else:
cycle = [mean if i in (a, b) else c.akarin.Expr([arg_clip[i::5] for arg_clip in arg_clips], unique_expr) for i in range(5)]
return c.std.Splice([
clip[:first],
c.std.Interleave(cycle),
clip[end:],
])
def smooth_stable_fade(clip, first, last, triple_field_index, delay_fields: float = 0):
return smooth_stable_fade_fields(clip, first, last, triple_field_index, delay_fields=delay_fields).std.DoubleWeave()[::2]
def unfade_unique_into_repeating(clip, first, last, repeating_clip, delay_fields: float = 0, triple_field_index=None,
*, mask: vs.VideoNode | None = None, premultiplied: bool = False):
end = last + 1
n = (end - first) * 2
right = repeating_clip
right = (right * (clip.num_frames // right.num_frames + 2))[-end % right.num_frames:][:clip.num_frames]
mask = [(mask * (clip.num_frames // mask.num_frames + 2))[-end % mask.num_frames:][:clip.num_frames]] if mask else []
divide_coeff = ('z *', 'b *', 'c *') if mask else ('', '', '')
subtract_coeff = ('', '', '') if premultiplied else divide_coeff
if triple_field_index is None:
return c.akarin.Expr([clip, right, *mask], f'N {first} - 2 * {1 - delay_fields} + Y 2 % + I! x {n} * y I@ {subtract_coeff[0]} * - {n} I@ {divide_coeff[0]} - /')
else:
right = right.std.CopyFrameProps(clip, '_FieldBased')
right = right.std.SeparateFields()
# NB: may alter one additional field preceding {first} or succeeding {last} depending on delay_fields
clip = smooth_stable_fade_fields(clip, first, last, triple_field_index,
delay_fields=delay_fields,
extra_clips=[right, *mask],
merge_expr=f'''
x.FadeFieldIndex {1 - delay_fields} + 0 {n} clamp I!
y.FadeFieldIndex {1 - delay_fields} + 0 {n} clamp J!
{n} x * I@ {subtract_coeff[1]} z * - {n} I@ {divide_coeff[1]} - *
{n} y * J@ {subtract_coeff[2]} a * - {n} J@ {divide_coeff[2]} - * +
{n} I@ {divide_coeff[1]} - dup *
{n} J@ {divide_coeff[2]} - dup * +
/
''',
unique_expr=f'x.FadeFieldIndex {1 - delay_fields} + I! x {n} * y I@ {subtract_coeff[0]} * - {n} I@ {divide_coeff[0]} - /',
)
return clip.std.DoubleWeave()[::2]
def unfade_repeating_into_unique(clip, first, last, repeating_clip, delay_fields: float = 0, triple_field_index=None,
*, mask: vs.VideoNode | None = None, premultiplied: bool = False):
end = last + 1
n = (end - first) * 2
left = repeating_clip
left = (left * (clip.num_frames // left.num_frames + 2))[-first % left.num_frames:][:clip.num_frames]
mask = [(mask * (clip.num_frames // mask.num_frames + 2))[-end % mask.num_frames:][:clip.num_frames]] if mask else []
divide_coeff = ('z *', 'b *', 'c *') if mask else ('', '', '')
subtract_coeff = ('', '', '') if premultiplied else divide_coeff
if triple_field_index is None:
return c.akarin.Expr([clip, left, *mask], f'{n} N {first} - 2 * {1 - delay_fields} + Y 2 % + - I! x {n} * y I@ {subtract_coeff[0]} * - {n} I@ {divide_coeff[0]} - /')
else:
left = left.std.CopyFrameProps(clip, '_FieldBased')
left = left.std.SeparateFields()
# NB: may alter one additional field preceding {first} or succeeding {last} depending on delay_fields
clip = smooth_stable_fade_fields(clip, first, last, triple_field_index,
delay_fields=delay_fields,
extra_clips=[left, *mask],
merge_expr=f'''
{n} x.FadeFieldIndex {1 - delay_fields} + 0 {n} clamp - I!
{n} y.FadeFieldIndex {1 - delay_fields} + 0 {n} clamp - J!
{n} x * I@ {subtract_coeff[1]} z * - {n} I@ {divide_coeff[1]} - *
{n} y * J@ {subtract_coeff[2]} a * - {n} J@ {divide_coeff[2]} - * +
{n} I@ {divide_coeff[1]} - dup *
{n} J@ {divide_coeff[2]} - dup * +
/
''',
unique_expr=f'{n} x.FadeFieldIndex {1 - delay_fields} + - I! x {n} * y I@ {subtract_coeff[0]} * - {n} I@ {divide_coeff[0]} - /',
)
return clip.std.DoubleWeave()[::2]
def fade_unique_into_repeating(clip, first, last, repeating_clip, delay: float, apply_vinverse=False, protect_top=0, protect_bottom=0,
*, mask: vs.VideoNode | None = None, premultiplied: bool = False):
end = last + 1
right = repeating_clip
right = (right * (clip.num_frames // right.num_frames + 2))[-end % right.num_frames:][:clip.num_frames]
mask = [(mask * (clip.num_frames // mask.num_frames + 2))[-end % mask.num_frames:][:clip.num_frames]] if mask else []
if apply_vinverse:
# Some top & bottom pixels of this flashback are 2px-per-band gradients.
# Vinverse happily smooths them out to 1px per band,
# but that contrasts with the non-crossfade frames, so avoid that.
stack = []
if protect_top:
stack.append(clip.std.Crop(bottom = clip.height - protect_top))
stack.append(vinverse(clip).std.Crop(top=protect_top, bottom=protect_bottom))
if protect_bottom:
stack.append(clip.std.Crop(top = clip.height - protect_bottom))
clip = c.std.StackVertical(stack)
multiply_coeff = 'z *' if mask else ''
add_coeff = '' if premultiplied else multiply_coeff
return c.akarin.Expr([clip, right, *mask], f'N {first - 1 + delay} - {end - first} / A! x 1 A@ {multiply_coeff} - * y A@ {add_coeff} * +')
def fade_repeating_into_unique(clip, first, last, repeating_clip, delay: float, apply_vinverse=False, protect_top=0, protect_bottom=0,
*, mask: vs.VideoNode | None = None, premultiplied: bool = False):
end = last + 1
left = repeating_clip
left = (left * (clip.num_frames // left.num_frames + 2))[-first % left.num_frames:][:clip.num_frames]
mask = [(mask * (clip.num_frames // mask.num_frames + 2))[-end % mask.num_frames:][:clip.num_frames]] if mask else []
if apply_vinverse:
# Some top & bottom pixels of this flashback are 2px-per-band gradients.
# Vinverse happily smooths them out to 1px per band,
# but that contrasts with the non-crossfade frames, so avoid that.
stack = []
if protect_top:
stack.append(clip.std.Crop(bottom = clip.height - protect_top))
stack.append(vinverse(clip).std.Crop(top=protect_top, bottom=protect_bottom))
if protect_bottom:
stack.append(clip.std.Crop(top = clip.height - protect_bottom))
clip = c.std.StackVertical(stack)
multiply_coeff = 'z *' if mask else ''
add_coeff = '' if premultiplied else multiply_coeff
return c.akarin.Expr([clip, left, *mask], f'1 N {first - 1 + delay} - {end - first} / - A! x 1 A@ {multiply_coeff} - * y A@ {add_coeff} * +')
def fade_repeating_into_repeating(clip, first, last, left, right, delay: float):
end = last + 1
left = (left * (clip.num_frames // left.num_frames + 2))[-first % left.num_frames:][:clip.num_frames]
right = (right * (clip.num_frames // right.num_frames + 2))[-end % right.num_frames:][:clip.num_frames]
return c.akarin.Expr([left, right], f'N {first - 1 + delay} - {end - first} / A! x 1 A@ - * y A@ * +')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment