diff --git a/src/ffx/cli.py b/src/ffx/cli.py index 74e28de..4ed0793 100755 --- a/src/ffx/cli.py +++ b/src/ffx/cli.py @@ -1194,7 +1194,15 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor): @click.option("--output-directory", type=str, default='') -@click.option("--deinterlace", is_flag=False, flag_value="default", default="none") +@click.option( + "--deinterlace", + type=click.Choice(("none", "default", "up_down", "hq_denoise")), + is_flag=False, + flag_value="default", + default="none", + show_default=True, + help="Deinterlace with bwdif, optionally followed by a named filter profile.", +) @click.option("--denoise", is_flag=False, flag_value="default", default="none") @click.option("--denoise-use-hw", is_flag=True, default=False) @@ -1524,7 +1532,7 @@ def convert(ctx, NlmeansFilter(**denoiseKwargs) if deinterlace != 'none' and not copyVideoEffective: - DeinterlaceFilter() + DeinterlaceFilter(mode=deinterlace) chainYield = list(qf.getChainYield()) diff --git a/src/ffx/filter/deinterlace_filter.py b/src/ffx/filter/deinterlace_filter.py index 668c992..087ad9f 100644 --- a/src/ffx/filter/deinterlace_filter.py +++ b/src/ffx/filter/deinterlace_filter.py @@ -7,6 +7,24 @@ class DeinterlaceFilter(Filter): IDENTIFIER = 'bwdif' + MODE_KEY = 'mode' + DEFAULT_MODE = 'default' + UP_DOWN_MODE = 'up_down' + HQ_DENOISE_MODE = 'hq_denoise' + + MODE_TOKENS = { + DEFAULT_MODE: ['bwdif=mode=1'], + UP_DOWN_MODE: [ + 'bwdif=mode=1', + 'scale=iw*2:ih*2:flags=lanczos', + 'scale=iw/2:ih/2:flags=lanczos', + ], + HQ_DENOISE_MODE: [ + 'bwdif=mode=1', + 'hqdn3d=1.5:1.5:3:3', + ], + } + # DEFAULT_STRENGTH: float = 2.8 # DEFAULT_PATCH_SIZE: int = 13 # DEFAULT_CHROMA_PATCH_SIZE: int = 9 @@ -21,7 +39,13 @@ class DeinterlaceFilter(Filter): def __init__(self, **kwargs): - + self.__mode = kwargs.get( + DeinterlaceFilter.MODE_KEY, + DeinterlaceFilter.DEFAULT_MODE, + ) + if self.__mode not in DeinterlaceFilter.MODE_TOKENS: + raise ValueError(f"Unknown deinterlace mode: {self.__mode}") + # self.__useHardware = kwargs.get('use_hardware', False) # self.__strengthList = [] @@ -123,10 +147,12 @@ class DeinterlaceFilter(Filter): # filterName = 'nlmeans_opencl' if self.__useHardware else 'nlmeans' payload = {'identifier': DeinterlaceFilter.IDENTIFIER, - 'parameters': {}, + 'parameters': { + DeinterlaceFilter.MODE_KEY: self.__mode, + }, 'suffices': suffices, 'variant': f"DEINT", - 'tokens': ['bwdif=mode=1']} + 'tokens': list(DeinterlaceFilter.MODE_TOKENS[self.__mode])} return payload diff --git a/tests/unit/test_deinterlace_filter.py b/tests/unit/test_deinterlace_filter.py new file mode 100644 index 0000000..a07d20c --- /dev/null +++ b/tests/unit/test_deinterlace_filter.py @@ -0,0 +1,64 @@ +import unittest + +from ffx.cli import convert +from ffx.filter.deinterlace_filter import DeinterlaceFilter +from ffx.filter.filter import Filter + + +class DeinterlaceFilterTest(unittest.TestCase): + + def setUp(self): + Filter.filterChain.clear() + + def tearDown(self): + Filter.filterChain.clear() + + def getPayload(self, mode=DeinterlaceFilter.DEFAULT_MODE): + deinterlaceFilter = DeinterlaceFilter(mode=mode) + payload, = deinterlaceFilter.getYield() + return payload + + def test_default_mode_yields_only_bwdif(self): + payload = self.getPayload() + + self.assertEqual(['bwdif=mode=1'], payload['tokens']) + + def test_up_down_mode_yields_lanczos_scales_after_bwdif(self): + payload = self.getPayload(DeinterlaceFilter.UP_DOWN_MODE) + + self.assertEqual( + [ + 'bwdif=mode=1', + 'scale=iw*2:ih*2:flags=lanczos', + 'scale=iw/2:ih/2:flags=lanczos', + ], + payload['tokens'], + ) + + def test_hq_denoise_mode_yields_hqdn3d_after_bwdif(self): + payload = self.getPayload(DeinterlaceFilter.HQ_DENOISE_MODE) + + self.assertEqual( + ['bwdif=mode=1', 'hqdn3d=1.5:1.5:3:3'], + payload['tokens'], + ) + + def test_unknown_mode_is_rejected(self): + with self.assertRaisesRegex(ValueError, 'Unknown deinterlace mode: other'): + DeinterlaceFilter(mode='other') + + def test_cli_accepts_named_modes_and_preserves_bare_flag_behavior(self): + upDownContext = convert.make_context( + 'convert', + ['--deinterlace', 'up_down'], + ) + bareFlagContext = convert.make_context('convert', ['--deinterlace']) + omittedContext = convert.make_context('convert', []) + + self.assertEqual('up_down', upDownContext.params['deinterlace']) + self.assertEqual('default', bareFlagContext.params['deinterlace']) + self.assertEqual('none', omittedContext.params['deinterlace']) + + +if __name__ == '__main__': + unittest.main()