Last active
August 15, 2025 23:41
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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