Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wobbly support #23

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9827800
Wobbly: Create base submodule
LightArrowsEXE Sep 25, 2023
d772fb3
Wibbly: Write most of the metric gathering and writing
LightArrowsEXE Sep 25, 2023
7a95f99
parse_wobbly, WobblyParsed: New function/class
LightArrowsEXE Sep 27, 2023
2a21076
Wobbly: Updates?
LightArrowsEXE Oct 22, 2023
1a86209
Wobbly: Add Types.Match
LightArrowsEXE Oct 22, 2023
14066b6
Wibbly: Implement setsu's old code
LightArrowsEXE Oct 22, 2023
ad00b16
Wibbly: `all_matches_to_c` helper method
LightArrowsEXE Oct 22, 2023
1fa1845
flake8
LightArrowsEXE Oct 22, 2023
7f2cb95
Wibbly: Add docstrings (anti-setsu reference)
LightArrowsEXE Oct 22, 2023
68dea36
WobblyParsed: Convert sections to Keyframes
LightArrowsEXE Nov 17, 2023
0547819
Update VapourSynth/Python versions
Setsugennoao Oct 20, 2023
69a33bd
Remove WobblyParser
LightArrowsEXE Aug 6, 2024
57d10b7
Wobbly/Wibbly: Big refactoring
LightArrowsEXE Aug 27, 2024
fc9e232
Merge remote-tracking branch 'origin' into py-wobbly
LightArrowsEXE Aug 27, 2024
39abe57
mypy/flake8
LightArrowsEXE Aug 27, 2024
0a3c826
Change __x -> _x, add `deinterlace_function` for orphan fields
LightArrowsEXE Aug 27, 2024
2569dd0
Further fixes, add preset/custom list support
LightArrowsEXE Aug 27, 2024
d1a1ce3
Custom decimate after clip is made progressive
LightArrowsEXE Aug 27, 2024
8638322
Fix freezeframes
LightArrowsEXE Aug 27, 2024
dba4c0f
deinterlace_orphans update
LightArrowsEXE Aug 27, 2024
b363b2d
Vinverse: add threshold (#35)
emotion3459 Sep 4, 2024
98866b7
FixInterlacedFades: Ensure YUV input
LightArrowsEXE Sep 4, 2024
c59821f
Vinverse: Allow passing of arbitrary clips to comb/contra blur (#36)
emotion3459 Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,10 @@ dmypy.json
.pytype/

# Cython debug symbols
cython_debug/
cython_debug/

# vscode and extensions
.vscode/

# test files
test/
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ vsrgtools>=1.7.0
vsmasktools>=1.3.0
vsaa>=1.10.0
vsscale>=2.1.0
vsdenoise>=2.6.0
vsdenoise>=2.6.0
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
install_requires=requirements,
python_requires='>=3.12',
packages=[
package_name
package_name,
f"{package_name}.wobbly"
],
package_data={
package_name: ['py.typed']
Expand Down
1 change: 1 addition & 0 deletions vsdeinterlace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from .funcs import * # noqa: F401, F403
from .ivtc import * # noqa: F401, F403
from .utils import * # noqa: F401, F403
from .wobbly import * # noqa: F401, F403
54 changes: 33 additions & 21 deletions vsdeinterlace/combing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from vsexprtools import ExprVars, complexpr_available, norm_expr
from vsrgtools import BlurMatrix
from vstools import (
ConvMode, CustomEnum, FuncExceptT, FunctionUtil, GenericVSFunction, KwargsT, PlanesT, core, scale_8bit, vs
)
from vstools import (ConvMode, CustomEnum, FormatsMismatchError, FuncExceptT,
FunctionUtil, GenericVSFunction, KwargsT, PlanesT, core,
scale_8bit, vs)

__all__ = [
'fix_interlaced_fades',
Expand Down Expand Up @@ -46,7 +46,7 @@ def __call__(
if not complexpr_available:
raise ExprVars._get_akarin_err()(func=func)

f = FunctionUtil(clip, func, planes, None, 32)
f = FunctionUtil(clip, func, planes, vs.YUV, 32)

fields = f.work_clip.std.Limiter().std.SeparateFields(tff=True)

Expand Down Expand Up @@ -84,9 +84,10 @@ def __call__(

def vinverse(
clip: vs.VideoNode,
comb_blur: GenericVSFunction | Sequence[int] = [1, 2, 1],
contra_blur: GenericVSFunction | Sequence[int] = [1, 4, 6, 4, 1],
contra_str: float = 2.7, amnt: int = 255, scl: float = 0.25, planes: PlanesT = None,
comb_blur: GenericVSFunction | vs.VideoNode | Sequence[int] = [1, 2, 1],
contra_blur: GenericVSFunction | vs.VideoNode | Sequence[int] = [1, 4, 6, 4, 1],
contra_str: float = 2.7, amnt: int = 255, scl: float = 0.25,
thr: int = 0, planes: PlanesT = None,
**kwargs: Any
) -> vs.VideoNode:
"""
Expand All @@ -96,31 +97,42 @@ def vinverse(
:param comb_blur: Filter used to remove combing.
:param contra_blur: Filter used to calculate contra sharpening.
:param contra_str: Strength of contra sharpening.
:param amnt: Change no pixel by more than this in 8bit (default is 255, unrestricted).
:param scl: Scale factor for vshrpD*vblurD < 0.
:param amnt: Change no pixel by more than this in 8bit.
:param thr: Skip processing if abs(clip - comb_blur(clip)) < thr
:param scl: Scale factor for vshrpD * vblurD < 0.
"""

func = FunctionUtil(clip, vinverse, planes, vs.YUV, 32)
func = FunctionUtil(clip, vinverse, planes)

def_k = KwargsT(mode=ConvMode.VERTICAL)

kwrg_a, kwrg_b = not callable(comb_blur), not callable(contra_blur)

if not callable(comb_blur):
comb_blur = BlurMatrix(comb_blur)
if isinstance(comb_blur, vs.VideoNode):
blurred = comb_blur
else:
if not callable(comb_blur):
comb_blur = BlurMatrix(comb_blur) # type:ignore

if not callable(contra_blur):
contra_blur = BlurMatrix(contra_blur)
blurred = comb_blur(func.work_clip, planes=planes, **((def_k | kwargs) if kwrg_a else kwargs))

blurred = comb_blur(func.work_clip, planes=planes, **((def_k | kwargs) if kwrg_a else kwargs))
blurred2 = contra_blur(blurred, planes=planes, **((def_k | kwargs) if kwrg_b else kwargs))
if isinstance(contra_blur, vs.VideoNode):
blurred2 = contra_blur
else:
if not callable(contra_blur):
contra_blur = BlurMatrix(contra_blur) # type:ignore

blurred2 = contra_blur(blurred, planes=planes, **((def_k | kwargs) if kwrg_b else kwargs))

FormatsMismatchError.check(func.func, func.work_clip, blurred, blurred2)

combed = norm_expr(
[func.work_clip, blurred, blurred2],
'y z - {sstr} * D1! x y - D2! D1@ abs D1A! D2@ abs D2A! '
'D1@ D2@ xor D1A@ D2A@ < D1@ D2@ ? {scl} * D1A@ D2A@ < D1@ D2@ ? ? y + '
'LIM! x {amnt} + LIM@ < x {amnt} + x {amnt} - LIM@ > x {amnt} - LIM@ ? ?',
planes, sstr=contra_str, amnt=scale_8bit(func.work_clip, amnt), scl=scl,
[func.work_clip, blurred, blurred2], # type:ignore
'x y - D1! D1@ abs D1A! D1A@ {thr} < x y z - {sstr} * D2! D2@ abs D2A! '
'D2@ D1@ xor D2A@ D1A@ < D2@ D1@ ? {scl} * D2A@ D1A@ < D2@ D1@ ? ? y + '
'LIM! x {amnt} + LIM@ < x {amnt} + x {amnt} - LIM@ > x {amnt} - LIM@ ? ? ?',
planes, sstr=contra_str, amnt=scale_8bit(func.work_clip, amnt),
scl=scl, thr=scale_8bit(func.work_clip, thr),
)

return func.return_clip(combed)
Expand Down
5 changes: 5 additions & 0 deletions vsdeinterlace/wobbly/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .exceptions import * # noqa: F401, F403
from .info import * # noqa: F401, F403
from .types import * # noqa: F401, F403
from .wibbly import * # noqa: F401, F403
from .wobbly import * # noqa: F401, F403
154 changes: 154 additions & 0 deletions vsdeinterlace/wobbly/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from functools import partial
from typing import Sequence

from vstools import (CustomKeyError, CustomValueError, DependencyNotFoundError,
FieldBased, FieldBasedT, FuncExceptT, VSFunction, core,
replace_ranges, vs)

from vsdeinterlace.combing import fix_interlaced_fades
from vsdeinterlace.wobbly.info import CustomList

from .info import FreezeFrame, InterlacedFade, OrphanField
from .types import CustomPostFiltering, Match


class _WobblyProcessBase:
"""A base class for Wobbly processing methods."""

def _check_plugin_installed(self, plugin: str, func_except: FuncExceptT | None = None) -> None:
"""Check if a plugin is installed."""

func = func_except or self._check_plugin_installed

if not hasattr(core, plugin):
raise DependencyNotFoundError(
f"Could not find the \"{plugin}\" plugin. Please install it!", func # type: ignore[arg-type]
)

def _apply_fieldmatches(
self, clip: vs.VideoNode, matches: Sequence[Match],
tff: FieldBasedT | None = None,
func_except: FuncExceptT | None = None
) -> vs.VideoNode:
"""Apply fieldmatches to a clip."""

self._check_plugin_installed('fh', func_except)

tff = FieldBased.from_param_or_video(tff, clip)

clip = clip.fh.FieldHint(None, tff, ''.join(matches))

match_clips = {match: clip.std.SetFrameProps(wobbly_match=match) for match in set(matches)}

return clip.std.FrameEval(lambda n: match_clips.get(matches[n]))

def _apply_freezeframes(
self, clip: vs.VideoNode, freezes: set[FreezeFrame],
func_except: FuncExceptT | None = None
) -> vs.VideoNode:
"""Apply freezeframes to a clip."""

start_frames, end_frames, replacements = list[int](), list[int](), list[int]()
freeze_props: dict[int, dict[str, int]] = {}

for freeze in freezes:
start_frames.append(freeze.start_frame)
end_frames.append(freeze.end_frame)
replacements.append(freeze.replacement)

freeze_props |= {
freeze.start_frame: {
'wobbly_freeze_start': freeze.start_frame,
'wobbly_freeze_end': freeze.end_frame,
'wobbly_freeze_replacement': freeze.replacement
}
}

try:
fclip = clip.std.FreezeFrames(start_frames, end_frames, replacements)
except vs.Error as e:
raise CustomValueError("Could not freeze frames!", func_except or self._apply_freezeframes) from e

def _set_props(n: int, clip: vs.VideoNode) -> vs.VideoNode:
return clip.std.SetFrameProps(**dict(freeze_props[n])) if n in freeze_props else clip

return fclip.std.FrameEval(partial(_set_props, clip=clip))

def _deinterlace_orphans(
self, clip: vs.VideoNode, orphans: Sequence[OrphanField],
deinterlacing_function: VSFunction = core.resize.Bob,
func_except: FuncExceptT | None = None
) -> vs.VideoNode:
"""Deinterlace orphaned fields."""

func = func_except or self._deinterlace_orphans # noqa

field_order = FieldBased.from_video(clip)

# TODO: implement good deinterlacing aimed at orphan fields.
# TODO: Try to be smart and freezeframe frames instead if they're literally identical to prev/next frame.
# TODO: Figure out what to actually pass here for custom deinterlacing functions.
deint_np = deinterlacing_function(clip, tff=not field_order.is_tff)[field_order.is_tff::2] # type:ignore
deint_bu = deinterlacing_function(clip, tff=field_order.is_tff)[field_order.is_tff::2] # type:ignore

frames_by_match: dict[str, list[int]] = {
k.__args__[0]: [] for k in Match.__args__ if k.__args__[0] != 'c' # type:ignore
}

try:
for o in orphans:
frames_by_match[o.match].append(o.framenum)
except KeyError:
raise CustomKeyError("Invalid orphan match!", func)

out_clip = replace_ranges(clip, deint_np, frames_by_match['n'] + frames_by_match['p'])
out_clip = replace_ranges(out_clip, deint_bu, frames_by_match['b'] + frames_by_match['u'])

# TODO: Put the match in the frame props.
out_clip = replace_ranges(
out_clip, out_clip.std.SetFrameProps(wobbly_orphan_deinterlace=True),
[f for frames in frames_by_match.values() for f in frames]
)

return out_clip

def _apply_combed_markers(self, clip: vs.VideoNode, combed_frames: set[int]) -> vs.VideoNode:
"""Apply combed markers to a clip."""

return replace_ranges(
clip.std.SetFrameProps(wobbly_combed=0),
clip.std.SetFrameProps(wobbly_combed=1),
list(combed_frames)
)

def _apply_interlaced_fades(
self, clip: vs.VideoNode, ifades: set[InterlacedFade],
func_except: FuncExceptT | None = None
) -> vs.VideoNode:
# TODO: Figure out how to get the right `color` param per frame with an eval.

func = func_except or self._apply_interlaced_fades

return replace_ranges(
clip.std.SetFrameProps(wobbly_fif=False),
fix_interlaced_fades(clip, colors=0, planes=0, func=func).std.SetFrameProps(wobbly_fif=True),
[f.framenum for f in ifades]
)

def _apply_custom_list(
self, clip: vs.VideoNode, custom_list: list[CustomList], pos: CustomPostFiltering
) -> vs.VideoNode:
"""Apply a list of custom functions to a clip based on the pos."""

for custom in custom_list:
if custom.position == pos:
custom_clip = custom.preset.apply_preset(clip)

custom_clip = custom_clip.std.SetFrameProps(
wobbly_custom_list_name=custom.name,
wobbly_custom_list_position=str(pos)
)

clip = replace_ranges(clip, custom_clip, custom.frames)

return clip
29 changes: 29 additions & 0 deletions vsdeinterlace/wobbly/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from vstools import CustomError

__all__: list[str] = [
"WobblyError",
"InvalidCycleError",
"InvalidMatchError",
"MatchMismatchError",
"SectionError",
]


class WobblyError(CustomError):
"""Thrown when an error related to Wobbly is thrown."""


class InvalidCycleError(WobblyError):
"""Raised when a wrong cycle is given."""


class InvalidMatchError(WobblyError, TypeError):
"""Thrown when an invalid fieldmatch value is given."""


class MatchMismatchError(WobblyError, ValueError):
"""Thrown when a fieldmatch value is given that is not allowed."""


class SectionError(WobblyError):
"""Raised when there's an issue with a section."""
Loading
Loading