Last active
November 2, 2025 18:58
-
-
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 | |
| extra_clips = [extra_clip.std.CopyFrameProps(clip, '_FieldBased').std.SeparateFields() for extra_clip in extra_clips] | |
| clip = clip.std.SeparateFields() | |
| first *= 2 | |
| end *= 2 | |
| offset = 0 | |
| match triple_field_index: | |
| case 3: | |
| if delay_fields < 1 and first: | |
| # TODO: keep and fully process clip[first] if merge_expr is not None | |
| 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: | |
| # TODO: keep and fully process clip[first + 1] if merge_expr is not None | |
| 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: | |
| # TODO: keep and fully process clip[end - 1] if merge_expr is not None | |
| 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: | |
| # TODO: keep and fully process clip[end - 2] if merge_expr is not None | |
| 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)] | |
| splice = [] | |
| if first: | |
| splice.append(clip[:first]) | |
| splice.append(c.std.Interleave(cycle)) | |
| if end < clip.num_frames: | |
| splice.append(clip[end:]) | |
| return c.std.Splice(splice) | |
| 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: | |
| # 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: | |
| # 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