diff --git a/src/locale/zh_CN/LC_MESSAGES/zh_CN.po b/src/locale/zh_CN/LC_MESSAGES/zh_CN.po index 0a09a25..e13b1df 100644 --- a/src/locale/zh_CN/LC_MESSAGES/zh_CN.po +++ b/src/locale/zh_CN/LC_MESSAGES/zh_CN.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2020-05-04 19:14-0400\n" -"PO-Revision-Date: 2020-05-04 19:16-0400\n" +"POT-Creation-Date: 2020-05-07 15:54-0400\n" +"PO-Revision-Date: 2020-05-07 15:55-0400\n" "Last-Translator: \n" "Language-Team: \n" "Language: zh_CN\n" @@ -17,107 +17,155 @@ msgstr "" "X-Generator: Poedit 2.3\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: upscaler.py:85 -msgid "Extracted frames are being saved to: {}" -msgstr "提取的帧将被保存到:{}" - -#: upscaler.py:87 -msgid "Upscaled frames are being saved to: {}" -msgstr "已放大的帧将被保存到:{}" - -#: upscaler.py:97 -msgid "Cleaning up cache directory: {}" -msgstr "清理缓存目录:{}" - -#: upscaler.py:100 -msgid "Unable to delete: {}" -msgstr "无法删除:{}" - -#: upscaler.py:107 -msgid "You must specify input video file/directory path" -msgstr "您必须指定输入视频文件/目录路径" - -#: upscaler.py:110 -msgid "You must specify output video file/directory path" -msgstr "您必须指定输出视频文件/目录路径" - -#: upscaler.py:113 -msgid "Selected driver accepts only scaling ratio" -msgstr "所选驱动程序仅接受缩放比率" - -#: upscaler.py:116 -msgid "Scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan" -msgstr "waifu2x_ncnn_vulkan 的缩放比必须为 1 或 2" - -#: upscaler.py:119 -msgid "Scaling ratio must be one of 2, 3 or 4 for srmd_ncnn_vulkan" -msgstr "srmd_ncnn_vulkan 的缩放比必须为 2、3 或 4" - -#: upscaler.py:122 -msgid "You can only specify either scaling ratio or output width and height" -msgstr "您只能指定缩放比或输出宽度和高度两者之一" - -#: upscaler.py:125 -msgid "You must specify both width and height" -msgstr "您必须同时指定宽度和高度" - -#: upscaler.py:142 +#: progress_monitor.py:42 msgid "Upscaling Progress" msgstr "放大进度" -#: upscaler.py:179 +#: upscaler.py:104 +msgid "Specified or default cache directory is a file/link" +msgstr "指定或默认的缓存目录是文件/链接" + +#: upscaler.py:110 +msgid "Creating cache directory {}" +msgstr "创建缓存目录 {}" + +#: upscaler.py:113 +msgid "Unable to create {}" +msgstr "无法创建 {}" + +#: upscaler.py:118 +msgid "Extracted frames are being saved to: {}" +msgstr "提取的帧将被保存到:{}" + +#: upscaler.py:120 +msgid "Upscaled frames are being saved to: {}" +msgstr "已放大的帧将被保存到:{}" + +#: upscaler.py:130 +msgid "Cleaning up cache directory: {}" +msgstr "清理缓存目录:{}" + +#: upscaler.py:133 +msgid "Unable to delete: {}" +msgstr "无法删除:{}" + +#: upscaler.py:140 upscaler.py:151 +msgid "Input and output path type mismatch" +msgstr "输入和输出路径类型不匹配" + +#: upscaler.py:141 +msgid "Input is single file but output is directory" +msgstr "所选的输入路径是单个文件,但输出路径是目录" + +#: upscaler.py:144 +msgid "No suffix found in output file path" +msgstr "在输出文件路径中未找到后缀" + +#: upscaler.py:145 +msgid "Suffix must be specified for FFmpeg" +msgstr "必须为 FFmpeg 指定后缀" + +#: upscaler.py:152 +msgid "Input is directory but output is existing single file" +msgstr "输入是目录,但输出是现有的单个文件" + +#: upscaler.py:157 +msgid "Input path is neither a file nor a directory" +msgstr "输入路径既不是文件也不是目录" + +#: upscaler.py:166 +msgid "FFmpeg or FFprobe cannot be found under the specified path" +msgstr "在指定的路径下找不到 FFmpeg 或 FFprobe" + +#: upscaler.py:167 upscaler.py:177 +msgid "Please check the configuration file settings" +msgstr "请检查配置文件设置" + +#: upscaler.py:176 +msgid "Specified driver executable directory doesn't exist" +msgstr "指定驱动的可执行文件不存在" + +#: upscaler.py:203 +msgid "Failed to parse driver argument: {}" +msgstr "解析驱动程序参数失败:{}" + +#: upscaler.py:218 msgid "Unrecognized driver: {}" msgstr "无法识别的驱动名称:{}" -#: upscaler.py:258 +#: upscaler.py:290 +msgid "Starting progress monitor" +msgstr "启动进度监视器" + +#: upscaler.py:295 msgid "Starting upscaled image cleaner" msgstr "启动已放大图像清理程序" -#: upscaler.py:264 -msgid "Main process waiting for subprocesses to exit" -msgstr "主进程开始等待子进程结束" +#: upscaler.py:304 upscaler.py:321 +msgid "Killing progress monitor" +msgstr "终结进度监视器" -#: upscaler.py:266 -msgid "Subprocess {} exited with code {}" -msgstr "子进程 {} 结束,返回码 {}" - -#: upscaler.py:274 upscaler.py:287 +#: upscaler.py:307 upscaler.py:324 msgid "Killing upscaled image cleaner" msgstr "终结已放大图像清理程序" -#: upscaler.py:313 upscaler.py:368 +#: upscaler.py:328 +msgid "Terminating all processes" +msgstr "正在终止所有进程" + +#: upscaler.py:335 +msgid "Main process waiting for subprocesses to exit" +msgstr "主进程开始等待子进程结束" + +#: upscaler.py:354 upscaler.py:358 +msgid "Subprocess {} exited with code {}" +msgstr "子进程 {} 结束,返回码 {}" + +#: upscaler.py:364 +msgid "Stop signal received" +msgstr "收到停止信号" + +#: upscaler.py:369 +msgid "Subprocess execution ran into an error" +msgstr "子进程执行遇到错误" + +#: upscaler.py:395 +msgid "Upscaling single video file: {}" +msgstr "放大单个视频文件:{}" + +#: upscaler.py:414 upscaler.py:477 msgid "Starting to upscale extracted images" msgstr "开始对提取的帧进行放大" -#: upscaler.py:316 upscaler.py:370 +#: upscaler.py:423 upscaler.py:479 msgid "Upscaling completed" msgstr "放大完成" -#: upscaler.py:324 +#: upscaler.py:432 msgid "Reading video information" msgstr "读取视频信息" -#: upscaler.py:338 +#: upscaler.py:446 msgid "Aborting: No video stream found" msgstr "程序中止:文件中未找到视频流" -#: upscaler.py:355 +#: upscaler.py:464 msgid "Unsupported pixel format: {}" msgstr "不支持的像素格式:{}" -#: upscaler.py:358 +#: upscaler.py:467 msgid "Framerate: {}" msgstr "帧率:{}" -#: upscaler.py:373 +#: upscaler.py:482 msgid "Converting extracted frames into video" msgstr "将提取的帧转换为视频" -#: upscaler.py:377 +#: upscaler.py:487 msgid "Conversion completed" msgstr "转换已完成" -#: upscaler.py:380 +#: upscaler.py:490 msgid "Migrating audio tracks and subtitles to upscaled video" msgstr "将音轨和字幕迁移到放大后的视频" @@ -187,58 +235,34 @@ msgstr "缩放比" msgid "This file cannot be imported" msgstr "此文件无法被当作模块导入" -#: video2x.py:193 -msgid "Specified driver executable directory doesn't exist" -msgstr "指定驱动的可执行文件不存在" - -#: video2x.py:194 -msgid "Please check the configuration file settings" -msgstr "请检查配置文件设置" - -#: video2x.py:211 -msgid "Specified cache directory is a file/link" -msgstr "指定的缓存目录是文件/链接" - -#: video2x.py:218 -msgid "Creating cache directory {}" -msgstr "创建缓存目录 {}" - #: video2x.py:224 -msgid "Unable to create {}" -msgstr "无法创建 {}" - -#: video2x.py:237 -msgid "Upscaling single video file: {}" -msgstr "放大单个视频文件:{}" - -#: video2x.py:241 -msgid "Input and output path type mismatch" -msgstr "输入和输出路径类型不匹配" - -#: video2x.py:242 -msgid "Input is single file but output is directory" -msgstr "所选的输入路径是单个文件,但输出路径是目录" - -#: video2x.py:245 -msgid "No suffix found in output file path" -msgstr "在输出文件路径中未找到后缀" - -#: video2x.py:246 -msgid "Suffix must be specified for FFmpeg" -msgstr "必须为 FFmpeg 指定后缀" - -#: video2x.py:270 -msgid "Upscaling videos in directory: {}" -msgstr "放大该文件夹中的所有视频:{}" - -#: video2x.py:295 -msgid "Input path is neither a file nor a directory" -msgstr "输入路径既不是文件也不是目录" - -#: video2x.py:298 msgid "Program completed, taking {} seconds" msgstr "程序执行完毕,总计花费 {} 秒" -#: video2x.py:301 +#: video2x.py:227 msgid "An exception has occurred" msgstr "发生了异常" + +#~ msgid "You must specify input video file/directory path" +#~ msgstr "您必须指定输入视频文件/目录路径" + +#~ msgid "You must specify output video file/directory path" +#~ msgstr "您必须指定输出视频文件/目录路径" + +#~ msgid "Selected driver accepts only scaling ratio" +#~ msgstr "所选驱动程序仅接受缩放比率" + +#~ msgid "Scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan" +#~ msgstr "waifu2x_ncnn_vulkan 的缩放比必须为 1 或 2" + +#~ msgid "Scaling ratio must be one of 2, 3 or 4 for srmd_ncnn_vulkan" +#~ msgstr "srmd_ncnn_vulkan 的缩放比必须为 2、3 或 4" + +#~ msgid "You can only specify either scaling ratio or output width and height" +#~ msgstr "您只能指定缩放比或输出宽度和高度两者之一" + +#~ msgid "You must specify both width and height" +#~ msgstr "您必须同时指定宽度和高度" + +#~ msgid "Upscaling videos in directory: {}" +#~ msgstr "放大该文件夹中的所有视频:{}" diff --git a/src/progress_monitor.py b/src/progress_monitor.py new file mode 100644 index 0000000..1a65fb2 --- /dev/null +++ b/src/progress_monitor.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Name: Video2X Upscale Progress Monitor +Author: BrianPetkovsek +Date Created: May 7, 2020 +Last Modified: May 7, 2020 +""" + +# built-in imports +import contextlib +import threading +import time + +# third-party imports +from tqdm import tqdm + + +class ProgressMonitor(threading.Thread): + """ progress monitor + + This class provides progress monitoring functionalities + by keeping track of the amount of frames in the input + directory and the output directory. This is originally + suggested by @ArmandBernard. + """ + + def __init__(self, upscaler, extracted_frames_directories): + threading.Thread.__init__(self) + self.upscaler = upscaler + self.extracted_frames_directories = extracted_frames_directories + self.running = False + + def run(self): + self.running = True + + # get number of extracted frames + self.upscaler.total_frames = 0 + for directory in self.extracted_frames_directories: + self.upscaler.total_frames += len([f for f in directory.iterdir() if str(f).lower().endswith(self.upscaler.image_format.lower())]) + + with tqdm(total=self.upscaler.total_frames, ascii=True, desc=_('Upscaling Progress')) as progress_bar: + # tqdm update method adds the value to the progress + # bar instead of setting the value. Therefore, a delta + # needs to be calculated. + previous_cycle_frames = 0 + while self.running: + + with contextlib.suppress(FileNotFoundError): + self.upscaler.total_frames_upscaled = len([f for f in self.upscaler.upscaled_frames.iterdir() if str(f).lower().endswith(self.upscaler.image_format.lower())]) + + # update progress bar + delta = self.upscaler.total_frames_upscaled - previous_cycle_frames + previous_cycle_frames = self.upscaler.total_frames_upscaled + progress_bar.update(delta) + + time.sleep(1) + + def stop(self): + self.running = False + self.join() diff --git a/src/upscaler.py b/src/upscaler.py index b308433..c5a4ee9 100755 --- a/src/upscaler.py +++ b/src/upscaler.py @@ -4,7 +4,7 @@ Name: Video2X Upscaler Author: K4YT3X Date Created: December 10, 2018 -Last Modified: May 6, 2020 +Last Modified: May 7, 2020 Description: This file contains the Upscaler class. Each instance of the Upscaler class is an upscaler on an image or @@ -14,6 +14,7 @@ a folder. # local imports from exceptions import * from image_cleaner import ImageCleaner +from progress_monitor import ProgressMonitor from wrappers.ffmpeg import Ffmpeg # built-in imports @@ -25,8 +26,10 @@ import importlib import locale import os import pathlib +import queue import re import shutil +import subprocess import sys import tempfile import threading @@ -35,7 +38,6 @@ import traceback # third-party imports from avalon_framework import Avalon -from tqdm import tqdm # internationalization constants DOMAIN = 'video2x' @@ -67,10 +69,10 @@ class Upscaler: ArgumentError -- if argument is not valid """ - def __init__(self, input_video, output_video, driver_settings, ffmpeg_settings): + def __init__(self, input_path, output_path, driver_settings, ffmpeg_settings): # mandatory arguments - self.input_video = input_video - self.output_video = output_video + self.input_path = input_path + self.output_path = output_path self.driver_settings = driver_settings self.ffmpeg_settings = ffmpeg_settings @@ -84,14 +86,33 @@ class Upscaler: self.image_format = 'png' self.preserve_frames = False + # other internal members and signals + self.stop_signal = False + self.total_frames_upscaled = 0 + self.total_frames = 0 + def create_temp_directories(self): - """create temporary directory + """create temporary directories """ - # create a new temp directory if the current one is not found - if not self.video2x_cache_directory.exists(): + # if cache directory unspecified, use %TEMP%\video2x + if self.video2x_cache_directory is None: self.video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x' + # if specified cache path exists and isn't a directory + if self.video2x_cache_directory.exists() and not self.video2x_cache_directory.is_dir(): + Avalon.error(_('Specified or default cache directory is a file/link')) + raise FileExistsError('Specified or default cache directory is a file/link') + + # if cache directory doesn't exist, try creating it + if not self.video2x_cache_directory.exists(): + try: + Avalon.debug_info(_('Creating cache directory {}').format(self.video2x_cache_directory)) + self.video2x_cache_directory.mkdir(parents=True, exist_ok=True) + except Exception as exception: + Avalon.error(_('Unable to create {}').format(self.video2x_cache_directory)) + raise exception + # create temp directories for extracted frames and upscaled frames self.extracted_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory)) Avalon.debug_info(_('Extracted frames are being saved to: {}').format(self.extracted_frames)) @@ -113,65 +134,74 @@ class Upscaler: traceback.print_exc() def _check_arguments(self): - # check if arguments are valid / all necessary argument - # values are specified - if not self.input_video: - Avalon.error(_('You must specify input video file/directory path')) - raise ArgumentError('input video path not specified') - if not self.output_video: - Avalon.error(_('You must specify output video file/directory path')) - raise ArgumentError('output video path not specified') - if (self.driver in ['waifu2x_converter', 'waifu2x_ncnn_vulkan', 'anime4k']) and self.scale_width and self.scale_height: - Avalon.error(_('Selected driver accepts only scaling ratio')) - raise ArgumentError('selected driver supports only scaling ratio') - if self.driver == 'waifu2x_ncnn_vulkan' and self.scale_ratio is not None and (self.scale_ratio > 2 or not self.scale_ratio.is_integer()): - Avalon.error(_('Scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan')) - raise ArgumentError('scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan') - if self.driver == 'srmd_ncnn_vulkan' and self.scale_ratio is not None and (self.scale_ratio not in [2, 3, 4]): - Avalon.error(_('Scaling ratio must be one of 2, 3 or 4 for srmd_ncnn_vulkan')) - raise ArgumentError('scaling ratio must be one of 2, 3 or 4 for srmd_ncnn_vulkan') - if (self.scale_width or self.scale_height) and self.scale_ratio: - Avalon.error(_('You can only specify either scaling ratio or output width and height')) - raise ArgumentError('both scaling ration and width/height specified') - if (self.scale_width and not self.scale_height) or (not self.scale_width and self.scale_height): - Avalon.error(_('You must specify both width and height')) - raise ArgumentError('only one of width or height is specified') + # if input is a file + if self.input_path.is_file(): + if self.output_path.is_dir(): + Avalon.error(_('Input and output path type mismatch')) + Avalon.error(_('Input is single file but output is directory')) + raise ArgumentError('input output path type mismatch') + if not re.search(r'.*\..*$', str(self.output_path)): + Avalon.error(_('No suffix found in output file path')) + Avalon.error(_('Suffix must be specified for FFmpeg')) + raise ArgumentError('no output video suffix specified') - def _progress_bar(self, extracted_frames_directories): - """ This method prints a progress bar + # if input is a directory + elif self.input_path.is_dir(): + if self.output_path.is_file(): + Avalon.error(_('Input and output path type mismatch')) + Avalon.error(_('Input is directory but output is existing single file')) + raise ArgumentError('input output path type mismatch') - This method prints a progress bar by keeping track - of the amount of frames in the input directory - and the output directory. This is originally - suggested by @ArmandBernard. - """ + # if input is neither + else: + Avalon.error(_('Input path is neither a file nor a directory')) + raise FileNotFoundError(f'{self.input_path} is neither file nor directory') + + # check Fmpeg settings + ffmpeg_path = pathlib.Path(self.ffmpeg_settings['ffmpeg_path']) + if not ((pathlib.Path(ffmpeg_path / 'ffmpeg.exe').is_file() and + pathlib.Path(ffmpeg_path / 'ffprobe.exe').is_file()) or + (pathlib.Path(ffmpeg_path / 'ffmpeg').is_file() and + pathlib.Path(ffmpeg_path / 'ffprobe').is_file())): + Avalon.error(_('FFmpeg or FFprobe cannot be found under the specified path')) + Avalon.error(_('Please check the configuration file settings')) + raise FileNotFoundError(self.ffmpeg_settings['ffmpeg_path']) - # get number of extracted frames - self.total_frames = 0 - for directory in extracted_frames_directories: - self.total_frames += len([f for f in directory.iterdir() if str(f).lower().endswith(self.image_format.lower())]) + # check if driver settings + driver_settings = copy.deepcopy(self.driver_settings) + driver_path = driver_settings.pop('path') - with tqdm(total=self.total_frames, ascii=True, desc=_('Upscaling Progress')) as progress_bar: + # check if driver path exists + if not (pathlib.Path(driver_path).is_file() or pathlib.Path(f'{driver_path}.exe').is_file()): + Avalon.error(_('Specified driver executable directory doesn\'t exist')) + Avalon.error(_('Please check the configuration file settings')) + raise FileNotFoundError(driver_path) - # tqdm update method adds the value to the progress - # bar instead of setting the value. Therefore, a delta - # needs to be calculated. - previous_cycle_frames = 0 - while not self.progress_bar_exit_signal: + # parse driver arguments using driver's parser + # the parser will throw AttributeError if argument doesn't satisfy constraints + try: + driver_arguments = [] + for key in driver_settings.keys(): - with contextlib.suppress(FileNotFoundError): - self.total_frames_upscaled = len([f for f in self.upscaled_frames.iterdir() if str(f).lower().endswith(self.image_format.lower())]) - delta = self.total_frames_upscaled - previous_cycle_frames - previous_cycle_frames = self.total_frames_upscaled + value = driver_settings[key] - # if upscaling is finished - if self.total_frames_upscaled >= self.total_frames: - return + if value is None or value is False: + continue - # adds the delta into the progress bar - progress_bar.update(delta) + else: + if len(key) == 1: + driver_arguments.append(f'-{key}') + else: + driver_arguments.append(f'--{key}') + # true means key is an option + if value is not True: + driver_arguments.append(str(value)) - time.sleep(1) + DriverWrapperMain = getattr(importlib.import_module(f'wrappers.{self.driver}'), 'WrapperMain') + DriverWrapperMain.parse_arguments(driver_arguments) + except AttributeError as e: + Avalon.error(_('Failed to parse driver argument: {}').format(e.args[0])) + raise e def _upscale_frames(self): """ Upscale video frames with waifu2x-caffe @@ -183,16 +213,10 @@ class Upscaler: w2 {Waifu2x Object} -- initialized waifu2x object """ - # progress bar process exit signal - self.progress_bar_exit_signal = False - # initialize waifu2x driver if self.driver not in AVAILABLE_DRIVERS: raise UnrecognizedDriverError(_('Unrecognized driver: {}').format(self.driver)) - # create a container for all upscaler processes - upscaler_processes = [] - # list all images in the extracted frames frames = [(self.extracted_frames / f) for f in self.extracted_frames.iterdir() if f.is_file] @@ -234,7 +258,7 @@ class Upscaler: # if the driver being used is waifu2x-caffe if self.driver == 'waifu2x_caffe': - upscaler_processes.append(driver.upscale(process_directory, + self.process_pool.append(driver.upscale(process_directory, self.upscaled_frames, self.scale_ratio, self.scale_width, @@ -244,7 +268,7 @@ class Upscaler: # if the driver being used is waifu2x-converter-cpp elif self.driver == 'waifu2x_converter_cpp': - upscaler_processes.append(driver.upscale(process_directory, + self.process_pool.append(driver.upscale(process_directory, self.upscaled_frames, self.scale_ratio, self.processes, @@ -252,55 +276,99 @@ class Upscaler: # if the driver being used is waifu2x-ncnn-vulkan elif self.driver == 'waifu2x_ncnn_vulkan': - upscaler_processes.append(driver.upscale(process_directory, + self.process_pool.append(driver.upscale(process_directory, self.upscaled_frames, self.scale_ratio)) # if the driver being used is srmd_ncnn_vulkan elif self.driver == 'srmd_ncnn_vulkan': - upscaler_processes.append(driver.upscale(process_directory, + self.process_pool.append(driver.upscale(process_directory, self.upscaled_frames, self.scale_ratio)) # start progress bar in a different thread - progress_bar = threading.Thread(target=self._progress_bar, args=(process_directories,)) - progress_bar.start() + Avalon.debug_info(_('Starting progress monitor')) + self.progress_monitor = ProgressMonitor(self, process_directories) + self.progress_monitor.start() # create the clearer and start it Avalon.debug_info(_('Starting upscaled image cleaner')) - image_cleaner = ImageCleaner(self.extracted_frames, self.upscaled_frames, len(upscaler_processes)) - image_cleaner.start() + self.image_cleaner = ImageCleaner(self.extracted_frames, self.upscaled_frames, len(self.process_pool)) + self.image_cleaner.start() # wait for all process to exit try: - Avalon.debug_info(_('Main process waiting for subprocesses to exit')) - for process in upscaler_processes: - Avalon.debug_info(_('Subprocess {} exited with code {}').format(process.pid, process.wait())) - except (KeyboardInterrupt, SystemExit): - Avalon.warning('Exit signal received') - Avalon.warning('Killing processes') - for process in upscaler_processes: - process.terminate() + self._wait() + except (Exception, KeyboardInterrupt, SystemExit) as e: + # cleanup + Avalon.debug_info(_('Killing progress monitor')) + self.progress_monitor.stop() - # cleanup and exit with exit code 1 Avalon.debug_info(_('Killing upscaled image cleaner')) - image_cleaner.stop() - self.progress_bar_exit_signal = True - sys.exit(1) + self.image_cleaner.stop() + raise e # if the driver is waifu2x-converter-cpp # images need to be renamed to be recognizable for FFmpeg if self.driver == 'waifu2x_converter_cpp': for image in [f for f in self.upscaled_frames.iterdir() if f.is_file()]: - renamed = re.sub(f'_\\[.*\\]\\[x(\\d+(\\.\\d+)?)\\]\\.{self.image_format}', f'.{self.image_format}', str(image.name)) + renamed = re.sub(f'_\\[.*\\]\\[x(\\d+(\\.\\d+)?)\\]\\.{self.image_format}', + f'.{self.image_format}', + str(image.name)) (self.upscaled_frames / image).rename(self.upscaled_frames / renamed) - # upscaling done, kill the clearer - Avalon.debug_info(_('Killing upscaled image cleaner')) - image_cleaner.stop() + # upscaling done, kill helper threads + Avalon.debug_info(_('Killing progress monitor')) + self.progress_monitor.stop() - # pass exit signal to progress bar thread - self.progress_bar_exit_signal = True + Avalon.debug_info(_('Killing upscaled image cleaner')) + self.image_cleaner.stop() + + def _terminate_subprocesses(self): + Avalon.warning(_('Terminating all processes')) + for process in self.process_pool: + process.terminate() + + def _wait(self): + """ wait for subprocesses in process pool to complete + """ + Avalon.debug_info(_('Main process waiting for subprocesses to exit')) + + try: + # while process pool not empty + while self.process_pool: + + # if stop signal received, terminate all processes + if self.stop_signal is True: + raise SystemExit + + for process in self.process_pool: + process_status = process.poll() + + # if process finished + if process_status is None: + continue + + # if return code is not 0 + elif process_status != 0: + Avalon.error(_('Subprocess {} exited with code {}').format(process.pid, process_status)) + raise subprocess.CalledProcessError(process_status, process.args) + + else: + Avalon.debug_info(_('Subprocess {} exited with code {}').format(process.pid, process_status)) + self.process_pool.remove(process) + + time.sleep(0.1) + + except (KeyboardInterrupt, SystemExit) as e: + Avalon.warning(_('Stop signal received')) + self._terminate_subprocesses() + raise e + + except (Exception, subprocess.CalledProcessError) as e: + Avalon.error(_('Subprocess execution ran into an error')) + self._terminate_subprocesses() + raise e def run(self): """ Main controller for Video2X @@ -308,94 +376,125 @@ class Upscaler: This function controls the flow of video conversion and handles all necessary functions. """ + + # external stop signal when called in a thread + self.stop_signal = False + + # define process pool to contain processes + self.process_pool = [] # parse arguments for waifu2x # check argument sanity self._check_arguments() - # convert paths to absolute paths - self.input_video = self.input_video.absolute() - self.output_video = self.output_video.absolute() + # define processing queue + processing_queue = queue.Queue() - # drivers that have native support for video processing - if self.driver == 'anime4kcpp': - # append FFmpeg path to the end of PATH - # Anime4KCPP will then use FFmpeg to migrate audio tracks - os.environ['PATH'] += f';{self.ffmpeg_settings["ffmpeg_path"]}' - Avalon.info(_('Starting to upscale extracted images')) + # if input specified is single file + if self.input_path.is_file(): + Avalon.info(_('Upscaling single video file: {}').format(self.input_path)) + processing_queue.put((self.input_path.absolute(), self.output_path.absolute())) - # import and initialize Anime4KCPP wrapper - DriverWrapperMain = getattr(importlib.import_module('wrappers.anime4kcpp'), 'WrapperMain') - driver = DriverWrapperMain(copy.deepcopy(self.driver_settings)) + # if input specified is a directory + elif self.input_path.is_dir(): - # run Anime4KCPP - driver.upscale(self.input_video, self.output_video, self.scale_ratio, self.processes).wait() - Avalon.info(_('Upscaling completed')) + # make output directory if it doesn't exist + self.output_path.mkdir(parents=True, exist_ok=True) + for input_video in [f for f in self.input_path.iterdir() if f.is_file()]: + output_video = self.output_path / input_video.name + processing_queue.put((input_video.absolute(), output_video.absolute())) - else: - self.create_temp_directories() + while not processing_queue.empty(): + input_video, output_video = processing_queue.get() + # drivers that have native support for video processing + if self.driver == 'anime4kcpp': + # append FFmpeg path to the end of PATH + # Anime4KCPP will then use FFmpeg to migrate audio tracks + os.environ['PATH'] += f';{self.ffmpeg_settings["ffmpeg_path"]}' + Avalon.info(_('Starting to upscale extracted images')) - # initialize objects for ffmpeg and waifu2x-caffe - fm = Ffmpeg(self.ffmpeg_settings, self.image_format) + # import and initialize Anime4KCPP wrapper + DriverWrapperMain = getattr(importlib.import_module('wrappers.anime4kcpp'), 'WrapperMain') + driver = DriverWrapperMain(copy.deepcopy(self.driver_settings)) - Avalon.info(_('Reading video information')) - video_info = fm.get_video_info(self.input_video) - # analyze original video with ffprobe and retrieve framerate - # width, height = info['streams'][0]['width'], info['streams'][0]['height'] + # run Anime4KCPP + self.process_pool.append(driver.upscale(input_video, output_video, self.scale_ratio, self.processes)) + self._wait() + Avalon.info(_('Upscaling completed')) - # find index of video stream - video_stream_index = None - for stream in video_info['streams']: - if stream['codec_type'] == 'video': - video_stream_index = stream['index'] - break + else: + try: + self.create_temp_directories() - # exit if no video stream found - if video_stream_index is None: - Avalon.error(_('Aborting: No video stream found')) - raise StreamNotFoundError('no video stream found') + # initialize objects for ffmpeg and waifu2x-caffe + fm = Ffmpeg(self.ffmpeg_settings, self.image_format) - # extract frames from video - fm.extract_frames(self.input_video, self.extracted_frames) + Avalon.info(_('Reading video information')) + video_info = fm.get_video_info(input_video) + # analyze original video with ffprobe and retrieve framerate + # width, height = info['streams'][0]['width'], info['streams'][0]['height'] - # get average frame rate of video stream - framerate = float(Fraction(video_info['streams'][video_stream_index]['avg_frame_rate'])) - fm.pixel_format = video_info['streams'][video_stream_index]['pix_fmt'] + # find index of video stream + video_stream_index = None + for stream in video_info['streams']: + if stream['codec_type'] == 'video': + video_stream_index = stream['index'] + break - # get a dict of all pixel formats and corresponding bit depth - pixel_formats = fm.get_pixel_formats() + # exit if no video stream found + if video_stream_index is None: + Avalon.error(_('Aborting: No video stream found')) + raise StreamNotFoundError('no video stream found') - # try getting pixel format's corresponding bti depth - try: - self.bit_depth = pixel_formats[fm.pixel_format] - except KeyError: - Avalon.error(_('Unsupported pixel format: {}').format(fm.pixel_format)) - raise UnsupportedPixelError(f'unsupported pixel format {fm.pixel_format}') + # extract frames from video + self.process_pool.append((fm.extract_frames(input_video, self.extracted_frames))) + self._wait() - Avalon.info(_('Framerate: {}').format(framerate)) + # get average frame rate of video stream + framerate = float(Fraction(video_info['streams'][video_stream_index]['avg_frame_rate'])) + fm.pixel_format = video_info['streams'][video_stream_index]['pix_fmt'] - # width/height will be coded width/height x upscale factor - if self.scale_ratio: - original_width = video_info['streams'][video_stream_index]['width'] - original_height = video_info['streams'][video_stream_index]['height'] - self.scale_width = int(self.scale_ratio * original_width) - self.scale_height = int(self.scale_ratio * original_height) + # get a dict of all pixel formats and corresponding bit depth + pixel_formats = fm.get_pixel_formats() - # upscale images one by one using waifu2x - Avalon.info(_('Starting to upscale extracted images')) - self._upscale_frames() - Avalon.info(_('Upscaling completed')) + # try getting pixel format's corresponding bti depth + try: + self.bit_depth = pixel_formats[fm.pixel_format] + except KeyError: + Avalon.error(_('Unsupported pixel format: {}').format(fm.pixel_format)) + raise UnsupportedPixelError(f'unsupported pixel format {fm.pixel_format}') - # frames to Video - Avalon.info(_('Converting extracted frames into video')) + Avalon.info(_('Framerate: {}').format(framerate)) - # use user defined output size - fm.convert_video(framerate, f'{self.scale_width}x{self.scale_height}', self.upscaled_frames) - Avalon.info(_('Conversion completed')) + # width/height will be coded width/height x upscale factor + if self.scale_ratio: + original_width = video_info['streams'][video_stream_index]['width'] + original_height = video_info['streams'][video_stream_index]['height'] + self.scale_width = int(self.scale_ratio * original_width) + self.scale_height = int(self.scale_ratio * original_height) - # migrate audio tracks and subtitles - Avalon.info(_('Migrating audio tracks and subtitles to upscaled video')) - fm.migrate_audio_tracks_subtitles(self.input_video, self.output_video, self.upscaled_frames) + # upscale images one by one using waifu2x + Avalon.info(_('Starting to upscale extracted images')) + self._upscale_frames() + Avalon.info(_('Upscaling completed')) - # destroy temp directories - self.cleanup_temp_directories() + # frames to Video + Avalon.info(_('Converting extracted frames into video')) + + # use user defined output size + self.process_pool.append(fm.convert_video(framerate, f'{self.scale_width}x{self.scale_height}', self.upscaled_frames)) + self._wait() + Avalon.info(_('Conversion completed')) + + # migrate audio tracks and subtitles + Avalon.info(_('Migrating audio tracks and subtitles to upscaled video')) + self.process_pool.append(fm.migrate_audio_tracks_subtitles(input_video, output_video, self.upscaled_frames)) + self._wait() + + # destroy temp directories + self.cleanup_temp_directories() + + except (Exception, KeyboardInterrupt, SystemExit) as e: + with contextlib.suppress(ValueError): + self.cleanup_temp_directories() + raise e diff --git a/src/video2x.py b/src/video2x.py index 609903c..1478d9c 100755 --- a/src/video2x.py +++ b/src/video2x.py @@ -13,7 +13,7 @@ __ __ _ _ ___ __ __ Name: Video2X Controller Creator: K4YT3X Date Created: Feb 24, 2018 -Last Modified: May 4, 2020 +Last Modified: May 7, 2020 Editor: BrianPetkovsek Last Modified: June 17, 2019 @@ -181,20 +181,6 @@ config = read_config(video2x_args.config) driver_settings = config[video2x_args.driver] driver_settings['path'] = os.path.expandvars(driver_settings['path']) -# overwrite driver_settings with driver_args -if driver_args is not None: - driver_args_dict = vars(driver_args) - for key in driver_args_dict: - if driver_args_dict[key] is not None: - driver_settings[key] = driver_args_dict[key] - -# check if driver path exists -if not pathlib.Path(driver_settings['path']).exists(): - if not pathlib.Path(f'{driver_settings["path"]}.exe').exists(): - Avalon.error(_('Specified driver executable directory doesn\'t exist')) - Avalon.error(_('Please check the configuration file settings')) - raise FileNotFoundError(driver_settings['path']) - # read FFmpeg configuration ffmpeg_settings = config['ffmpeg'] ffmpeg_settings['ffmpeg_path'] = os.path.expandvars(ffmpeg_settings['ffmpeg_path']) @@ -202,113 +188,41 @@ ffmpeg_settings['ffmpeg_path'] = os.path.expandvars(ffmpeg_settings['ffmpeg_path # load video2x settings image_format = config['video2x']['image_format'].lower() preserve_frames = config['video2x']['preserve_frames'] +video2x_cache_directory = config['video2x']['video2x_cache_directory'] -# load cache directory -if config['video2x']['video2x_cache_directory'] is not None: - video2x_cache_directory = pathlib.Path(config['video2x']['video2x_cache_directory']) -else: - video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x' - -if video2x_cache_directory.exists() and not video2x_cache_directory.is_dir(): - Avalon.error(_('Specified cache directory is a file/link')) - raise FileExistsError('Specified cache directory is a file/link') - -# if cache directory doesn't exist -# ask the user if it should be created -elif not video2x_cache_directory.exists(): - try: - Avalon.debug_info(_('Creating cache directory {}').format(video2x_cache_directory)) - video2x_cache_directory.mkdir(parents=True, exist_ok=True) - # there can be a number of exceptions here - # PermissionError, FileExistsError, etc. - # therefore, we put a catch-them-all here - except Exception as exception: - Avalon.error(_('Unable to create {}').format(video2x_cache_directory)) - raise exception - +# overwrite driver_settings with driver_args +if driver_args is not None: + driver_args_dict = vars(driver_args) + for key in driver_args_dict: + if driver_args_dict[key] is not None: + driver_settings[key] = driver_args_dict[key] # start execution try: # start timer begin_time = time.time() - # if input specified is a single file - if video2x_args.input.is_file(): + # initialize upscaler object + upscaler = Upscaler(input_path=video2x_args.input, + output_path=video2x_args.output, + driver_settings=driver_settings, + ffmpeg_settings=ffmpeg_settings) - # upscale single video file - Avalon.info(_('Upscaling single video file: {}').format(video2x_args.input)) + # set upscaler optional options + upscaler.driver = video2x_args.driver + upscaler.scale_width = video2x_args.width + upscaler.scale_height = video2x_args.height + upscaler.scale_ratio = video2x_args.ratio + upscaler.processes = video2x_args.processes + upscaler.video2x_cache_directory = video2x_cache_directory + upscaler.image_format = image_format + upscaler.preserve_frames = preserve_frames - # check for input output format mismatch - if video2x_args.output.is_dir(): - Avalon.error(_('Input and output path type mismatch')) - Avalon.error(_('Input is single file but output is directory')) - raise Exception('input output path type mismatch') - if not re.search(r'.*\..*$', str(video2x_args.output)): - Avalon.error(_('No suffix found in output file path')) - Avalon.error(_('Suffix must be specified for FFmpeg')) - raise Exception('No suffix specified') - - upscaler = Upscaler(input_video=video2x_args.input, - output_video=video2x_args.output, - driver_settings=driver_settings, - ffmpeg_settings=ffmpeg_settings) - - # set optional options - upscaler.driver = video2x_args.driver - upscaler.scale_width = video2x_args.width - upscaler.scale_height = video2x_args.height - upscaler.scale_ratio = video2x_args.ratio - upscaler.processes = video2x_args.processes - upscaler.video2x_cache_directory = video2x_cache_directory - upscaler.image_format = image_format - upscaler.preserve_frames = preserve_frames - - # run upscaler - upscaler.run() - - # if input specified is a directory - elif video2x_args.input.is_dir(): - # upscale videos in a directory - Avalon.info(_('Upscaling videos in directory: {}').format(video2x_args.input)) - - # make output directory if it doesn't exist - video2x_args.output.mkdir(parents=True, exist_ok=True) - - for input_video in [f for f in video2x_args.input.iterdir() if f.is_file()]: - output_video = video2x_args.output / input_video.name - upscaler = Upscaler(input_video=input_video, - output_video=output_video, - driver_settings=driver_settings, - ffmpeg_settings=ffmpeg_settings) - - # set optional options - upscaler.driver = video2x_args.driver - upscaler.scale_width = video2x_args.width - upscaler.scale_height = video2x_args.height - upscaler.scale_ratio = video2x_args.ratio - upscaler.processes = video2x_args.processes - upscaler.video2x_cache_directory = video2x_cache_directory - upscaler.image_format = image_format - upscaler.preserve_frames = preserve_frames - - # run upscaler - upscaler.run() - else: - Avalon.error(_('Input path is neither a file nor a directory')) - raise FileNotFoundError(f'{video2x_args.input} is neither file nor directory') + # run upscaler + upscaler.run() Avalon.info(_('Program completed, taking {} seconds').format(round((time.time() - begin_time), 5))) except Exception: Avalon.error(_('An exception has occurred')) traceback.print_exc() - - # try cleaning up temp directories - with contextlib.suppress(Exception): - upscaler.cleanup_temp_directories() - -finally: - # remove Video2X cache directory - with contextlib.suppress(FileNotFoundError): - if not preserve_frames: - shutil.rmtree(video2x_cache_directory) diff --git a/src/video2x_gui.py b/src/video2x_gui.py index 17d3604..b1fca62 100755 --- a/src/video2x_gui.py +++ b/src/video2x_gui.py @@ -15,7 +15,6 @@ import contextlib import os import pathlib import re -import shutil import sys import tempfile import time @@ -54,6 +53,7 @@ def resource_path(relative_path: str) -> pathlib.Path: class WorkerSignals(QObject): progress = pyqtSignal(tuple) error = pyqtSignal(str) + interrupted = pyqtSignal() finished = pyqtSignal() class ProgressBarWorker(QRunnable): @@ -89,11 +89,13 @@ class UpscalerWorker(QRunnable): # Retrieve args/kwargs here; and fire processing using them try: self.fn(*self.args, **self.kwargs) + except (KeyboardInterrupt, SystemExit): + self.signals.interrupted.emit() except Exception: error_message = traceback.format_exc() print(error_message, file=sys.stderr) self.signals.error.emit(error_message) - finally: + else: self.signals.finished.emit() class Video2XMainWindow(QtWidgets.QMainWindow): @@ -141,7 +143,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow): # express settings self.driver_combo_box = self.findChild(QtWidgets.QComboBox, 'driverComboBox') - self.driver_combo_box.currentTextChanged.connect(self.update_driver_constraints) + self.driver_combo_box.currentTextChanged.connect(self.update_gui_for_driver) self.processes_spin_box = self.findChild(QtWidgets.QSpinBox, 'processesSpinBox') self.scale_ratio_double_spin_box = self.findChild(QtWidgets.QDoubleSpinBox, 'scaleRatioDoubleSpinBox') self.preserve_frames_check_box = self.findChild(QtWidgets.QCheckBox, 'preserveFramesCheckBox') @@ -258,25 +260,10 @@ class Video2XMainWindow(QtWidgets.QMainWindow): self.ffmpeg_settings = self.config['ffmpeg'] self.ffmpeg_settings['ffmpeg_path'] = os.path.expandvars(self.ffmpeg_settings['ffmpeg_path']) - # load cache directory, create it if necessary - if self.config['video2x']['video2x_cache_directory'] is not None: - video2x_cache_directory = pathlib.Path(self.config['video2x']['video2x_cache_directory']) - else: - video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x' - - if video2x_cache_directory.exists() and not video2x_cache_directory.is_dir(): - self.show_error('Specified cache directory is a file/link') - raise FileExistsError('Specified cache directory is a file/link') - - # if cache directory doesn't exist - # ask the user if it should be created - elif not video2x_cache_directory.exists(): - try: - video2x_cache_directory.mkdir(parents=True, exist_ok=True) - except Exception as exception: - self.show_error(f'Unable to create cache directory: {video2x_cache_directory}') - raise exception - self.cache_line_edit.setText(str(video2x_cache_directory.absolute())) + # set cache directory path + if self.config['video2x']['video2x_cache_directory'] is None: + video2x_cache_directory = str((pathlib.Path(tempfile.gettempdir()) / 'video2x').absolute()) + self.cache_line_edit.setText(video2x_cache_directory) # load preserve frames settings self.preserve_frames_check_box.setChecked(self.config['video2x']['preserve_frames']) @@ -399,8 +386,10 @@ class Video2XMainWindow(QtWidgets.QMainWindow): self.config['anime4kcpp']['postprocessing'] = bool(self.anime4kcpp_post_processing_check_box.checkState()) self.config['anime4kcpp']['GPUMode'] = bool(self.anime4kcpp_gpu_mode_check_box.checkState()) - def update_driver_constraints(self): + def update_gui_for_driver(self): current_driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] + + # update scale ratio constraints if current_driver in ['waifu2x_caffe', 'waifu2x_converter_cpp', 'anime4kcpp']: self.scale_ratio_double_spin_box.setMinimum(0.0) self.scale_ratio_double_spin_box.setMaximum(999.0) @@ -413,14 +402,29 @@ class Video2XMainWindow(QtWidgets.QMainWindow): self.scale_ratio_double_spin_box.setMinimum(2.0) self.scale_ratio_double_spin_box.setMaximum(4.0) self.scale_ratio_double_spin_box.setValue(2.0) + + # update preferred processes/threads count + if current_driver == 'anime4kcpp': + self.processes_spin_box.setValue(16) + else: + self.processes_spin_box.setValue(1) + + def select_file(self, *args, **kwargs) -> pathlib.Path: + file_selected = QtWidgets.QFileDialog.getOpenFileName(self, *args, **kwargs) + if not isinstance(file_selected, tuple) or file_selected[0] == '': + return None + return pathlib.Path(file_selected[0]) + + def select_folder(self, *args, **kwargs) -> pathlib.Path: + folder_selected = QtWidgets.QFileDialog.getExistingDirectory(self, *args, **kwargs) + if folder_selected == '': + return None + return pathlib.Path(folder_selected) def select_input_file(self): - input_file = QtWidgets.QFileDialog.getOpenFileName(self, 'Select Input File') - if not isinstance(input_file, tuple) or input_file[0] == '': + + if (input_file := self.select_file('Select Input File')) is None: return - - input_file = pathlib.Path(input_file[0]) - self.input_line_edit.setText(str(input_file.absolute())) # try to set an output file name automatically @@ -435,11 +439,9 @@ class Video2XMainWindow(QtWidgets.QMainWindow): self.output_line_edit.setText(str(output_file.absolute())) def select_input_folder(self): - input_folder = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Input Folder') - if input_folder == '': - return - input_folder = pathlib.Path(input_folder) + if (input_folder := self.select_folder('Select Input Folder')) is None: + return self.input_line_edit.setText(str(input_folder.absolute())) @@ -455,40 +457,30 @@ class Video2XMainWindow(QtWidgets.QMainWindow): self.output_line_edit.setText(str(output_folder.absolute())) def select_output_file(self): - output_file = QtWidgets.QFileDialog.getOpenFileName(self, 'Select Output File') - if not isinstance(output_file, tuple): + if (output_file := self.select_file('Select Output File')) is None: return - - self.output_line_edit.setText(str(pathlib.Path(output_file[0]).absolute())) + self.output_line_edit.setText(str(output_file.absolute())) def select_output_folder(self): - output_folder = QtWidgets.QFileDialog.getSaveFileName(self, 'Select Output Folder') - if output_folder == '': + if (output_folder := self.select_folder('Select Output Folder')) is None: return - - self.output_line_edit.setText(str(pathlib.Path(output_folder).absolute())) + self.output_line_edit.setText(str(output_folder.absolute())) def select_cache_folder(self): - cache_folder = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Cache Folder') - if cache_folder == '': + if (cache_folder := self.select_folder('Select Cache Folder')) is None: return - - self.cache_line_edit.setText(str(pathlib.Path(cache_folder).absolute())) + self.cache_line_edit.setText(str(cache_folder.absolute())) def select_config_file(self): - config_file = QtWidgets.QFileDialog.getOpenFileName(self, 'Select Config File', filter='(YAML files (*.yaml))') - if not isinstance(config_file, tuple): + if (config_file := self.select_file('Select Config File', filter='(YAML files (*.yaml))')) is None: return - - self.config_line_edit.setText(str(pathlib.Path(config_file[0]).absolute())) + self.config_line_edit.setText(str(config_file.absolute())) self.load_configurations() def select_driver_binary_path(self, driver_line_edit): - driver_binary_path = QtWidgets.QFileDialog.getOpenFileName(self, 'Select Driver Binary File') - if not isinstance(driver_binary_path, tuple) or driver_binary_path[0] == '': + if (driver_binary_path := self.select_file('Select Driver Binary File')) is None: return - - driver_line_edit.setText(str(pathlib.Path(driver_binary_path[0]).absolute())) + driver_line_edit.setText(str(driver_binary_path.absolute())) def show_error(self, message: str): QtWidgets.QErrorMessage(self).showMessage(message.replace('\n', '
')) @@ -504,19 +496,24 @@ class Video2XMainWindow(QtWidgets.QMainWindow): message_box.exec_() def start_progress_bar(self, progress_callback): - - # initialize variables early - self.upscaler.progress_bar_exit_signal = False - self.upscaler.total_frames_upscaled = 0 - self.upscaler.total_frames = 1 + # wait for progress monitor to come online + while 'progress_monitor' not in self.upscaler.__dict__: + if self.upscaler.stop_signal: + return + time.sleep(0.1) # initialize progress bar values upscale_begin_time = time.time() progress_callback.emit((0, 0, 0, upscale_begin_time)) # keep querying upscaling process and feed information to callback signal - while not self.upscaler.progress_bar_exit_signal: - progress_callback.emit((int(100 * self.upscaler.total_frames_upscaled / self.upscaler.total_frames), + while self.upscaler.progress_monitor.running: + try: + progress_percentage = int(100 * self.upscaler.total_frames_upscaled / self.upscaler.total_frames) + except ZeroDivisionError: + progress_percentage = 0 + + progress_callback.emit((progress_percentage, self.upscaler.total_frames_upscaled, self.upscaler.total_frames, upscale_begin_time)) @@ -574,109 +571,60 @@ class Video2XMainWindow(QtWidgets.QMainWindow): # load driver settings for the current driver self.driver_settings = self.config[AVAILABLE_DRIVERS[self.driver_combo_box.currentText()]] - # if input specified is a single file - if input_directory.is_file(): + self.upscaler = Upscaler(input_path=input_directory, + output_path=output_directory, + driver_settings=self.driver_settings, + ffmpeg_settings=self.ffmpeg_settings) - # check for input output format mismatch - if output_directory.is_dir(): - self.show_error('Input and output path type mismatch\n\ - Input is single file but output is directory') - raise Exception('input output path type mismatch') - if not re.search(r'.*\..*$', str(output_directory)): - self.show_error('No suffix found in output file path\n\ - Suffix must be specified for FFmpeg') - raise Exception('No suffix specified') + # set optional options + self.upscaler.driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] + self.upscaler.scale_ratio = self.scale_ratio_double_spin_box.value() + self.upscaler.processes = self.processes_spin_box.value() + self.upscaler.video2x_cache_directory = pathlib.Path(os.path.expandvars(self.cache_line_edit.text())) + self.upscaler.image_format = self.config['video2x']['image_format'].lower() + self.upscaler.preserve_frames = bool(self.preserve_frames_check_box.checkState()) - self.upscaler = Upscaler(input_video=input_directory, - output_video=output_directory, - driver_settings=self.driver_settings, - ffmpeg_settings=self.ffmpeg_settings) + # start progress bar + if AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] != 'anime4kcpp': + progress_bar_worker = ProgressBarWorker(self.start_progress_bar) + progress_bar_worker.signals.progress.connect(self.set_progress) + self.threadpool.start(progress_bar_worker) - # set optional options - self.upscaler.driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] - self.upscaler.scale_ratio = self.scale_ratio_double_spin_box.value() - self.upscaler.processes = self.processes_spin_box.value() - self.upscaler.video2x_cache_directory = pathlib.Path(os.path.expandvars(self.cache_line_edit.text())) - self.upscaler.image_format = self.config['video2x']['image_format'].lower() - self.upscaler.preserve_frames = bool(self.preserve_frames_check_box.checkState()) - - # start progress bar - if AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] != 'anime4kcpp': - progress_bar_worker = ProgressBarWorker(self.start_progress_bar) - progress_bar_worker.signals.progress.connect(self.set_progress) - self.threadpool.start(progress_bar_worker) - - # run upscaler - worker = UpscalerWorker(self.upscaler.run) - worker.signals.error.connect(self.upscale_errored) - worker.signals.finished.connect(self.upscale_completed) - self.threadpool.start(worker) - self.start_button.setEnabled(False) - # self.stop_button.setEnabled(True) - - # if input specified is a directory - elif input_directory.is_dir(): - # upscale videos in a directory - - # make output directory if it doesn't exist - output_directory.mkdir(parents=True, exist_ok=True) - - for input_video in [f for f in input_directory.iterdir() if f.is_file()]: - output_video = output_directory / input_video.name - self.upscaler = Upscaler(input_video=input_video, - output_video=output_video, - driver_settings=self.driver_settings, - ffmpeg_settings=self.ffmpeg_settings) - - # set optional options - self.upscaler.driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] - self.upscaler.scale_ratio = self.scale_ratio_double_spin_box.value() - self.upscaler.processes = self.processes_spin_box.value() - self.upscaler.video2x_cache_directory = pathlib.Path(os.path.expandvars(self.cache_line_edit.text())) - self.upscaler.image_format = self.config['video2x']['image_format'].lower() - self.upscaler.preserve_frames = bool(self.preserve_frames_check_box.checkState()) - - # start progress bar - if AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] != 'anime4kcpp': - progress_bar_worker = ProgressBarWorker(self.start_progress_bar) - self.threadpool.start(progress_bar_worker) - - # run upscaler - worker = UpscalerWorker(self.upscaler.run) - worker.signals.error.connect(self.upscale_errored) - worker.signals.finished.connect(self.upscale_completed) - self.threadpool.start(worker) - self.start_button.setEnabled(False) - # self.stop_button.setEnabled(True) - else: - self.show_error('Input path is neither a file nor a directory') - raise FileNotFoundError(f'{input_directory} is neither file nor directory') + # run upscaler + worker = UpscalerWorker(self.upscaler.run) + worker.signals.error.connect(self.upscale_errored) + worker.signals.finished.connect(self.upscale_completed) + worker.signals.interrupted.connect(self.upscale_interrupted) + self.threadpool.start(worker) + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) except Exception: self.upscale_errored(traceback.format_exc()) - self.upscale_completed() def upscale_errored(self, error_message): self.show_error(f'Upscaler ran into an error:\n{error_message}') - # try cleaning up temp directories - with contextlib.suppress(Exception): - self.upscaler.progress_bar_exit_signal = True - self.upscaler.cleanup_temp_directories() - def upscale_completed(self): # if all threads have finished if self.threadpool.activeThreadCount() == 0: self.show_message('Program completed, taking {} seconds'.format(round((time.time() - self.begin_time), 5))) - # remove Video2X cache directory - with contextlib.suppress(FileNotFoundError): - if not bool(self.preserve_frames_check_box.checkState()): - shutil.rmtree(pathlib.Path(os.path.expandvars(self.cache_line_edit.text()))) self.start_button.setEnabled(True) + self.stop_button.setEnabled(False) + + def upscale_interrupted(self): + self.show_message('Upscale has been interrupted') + self.start_button.setEnabled(True) + self.stop_button.setEnabled(False) def stop(self): - # TODO unimplemented yet - pass + with contextlib.suppress(AttributeError): + self.upscaler.stop_signal = True + + def closeEvent(self, event): + # try cleaning up temp directories + self.stop() + event.accept() # this file shouldn't be imported diff --git a/src/video2x_gui.ui b/src/video2x_gui.ui index 7169102..46ff285 100644 --- a/src/video2x_gui.ui +++ b/src/video2x_gui.ui @@ -1206,6 +1206,16 @@ vp09 + + + hevc + + + + + av01 + + diff --git a/src/wrappers/anime4kcpp.py b/src/wrappers/anime4kcpp.py index 2aa1714..e71a593 100644 --- a/src/wrappers/anime4kcpp.py +++ b/src/wrappers/anime4kcpp.py @@ -13,6 +13,8 @@ for waifu2x-caffe. # built-in imports import argparse import os +import pathlib +import platform import shlex import subprocess import threading @@ -32,6 +34,7 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) parser.add_argument('--help', action='help', help='show this help message and exit') # parser.add_argument('-i', '--input', type=pathlib.Path, help='File for loading') # parser.add_argument('-o', '--output', type=pathlib.Path, help='File for outputting') @@ -70,6 +73,11 @@ class WrapperMain: self.driver_settings['output'] = output_file self.driver_settings['zoomFactor'] = zoom_factor self.driver_settings['threads'] = threads + + # Anime4KCPP will look for Anime4KCPPKernel.cl under the current working directory + # change the CWD to its containing directory so it will find it + if platform.system() == 'Windows': + os.chdir(pathlib.Path(self.driver_settings['path']).parent) # list to be executed # initialize the list with waifu2x binary path as the first element diff --git a/src/wrappers/ffmpeg.py b/src/wrappers/ffmpeg.py index 5387bc7..f4b3444 100644 --- a/src/wrappers/ffmpeg.py +++ b/src/wrappers/ffmpeg.py @@ -4,7 +4,7 @@ Name: Video2X FFmpeg Controller Author: K4YT3X Date Created: Feb 24, 2018 -Last Modified: November 15, 2019 +Last Modified: May 7, 2020 Description: This class handles all FFmpeg related operations. """ @@ -131,7 +131,7 @@ class Ffmpeg: extracted_frames / f'extracted_%0d.{self.image_format}' ]) - self._execute(execute) + return(self._execute(execute)) def convert_video(self, framerate, resolution, upscaled_frames): """Converts images into videos @@ -180,7 +180,7 @@ class Ffmpeg: upscaled_frames / 'no_audio.mp4' ]) - self._execute(execute) + return(self._execute(execute)) def migrate_audio_tracks_subtitles(self, input_video, output_video, upscaled_frames): """ Migrates audio tracks and subtitles from input video to output video @@ -209,7 +209,7 @@ class Ffmpeg: output_video ]) - self._execute(execute) + return(self._execute(execute)) def _read_configuration(self, phase, section=None): """ read configuration from JSON @@ -284,4 +284,4 @@ class Ffmpeg: Avalon.debug_info(f'Executing: {execute}') - return subprocess.run(execute, check=True).returncode + return subprocess.Popen(execute) diff --git a/src/wrappers/srmd_ncnn_vulkan.py b/src/wrappers/srmd_ncnn_vulkan.py index cd369a0..2be517d 100644 --- a/src/wrappers/srmd_ncnn_vulkan.py +++ b/src/wrappers/srmd_ncnn_vulkan.py @@ -4,7 +4,7 @@ Name: SRMD NCNN Vulkan Driver Creator: K4YT3X Date Created: April 26, 2020 -Last Modified: May 5, 2020 +Last Modified: May 7, 2020 Description: This class is a high-level wrapper for srmd_ncnn_vulkan. @@ -39,6 +39,7 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) parser.add_argument('--help', action='help', help='show this help message and exit') parser.add_argument('-v', action='store_true', help='verbose output') # parser.add_argument('-i', type=pathlib.Path, help='input image path (jpg/png) or directory') diff --git a/src/wrappers/waifu2x_caffe.py b/src/wrappers/waifu2x_caffe.py index 50df5ab..f67ba03 100644 --- a/src/wrappers/waifu2x_caffe.py +++ b/src/wrappers/waifu2x_caffe.py @@ -4,7 +4,7 @@ Name: Waifu2x Caffe Driver Author: K4YT3X Date Created: Feb 24, 2018 -Last Modified: May 4, 2020 +Last Modified: May 7, 2020 Description: This class is a high-level wrapper for waifu2x-caffe. @@ -37,6 +37,7 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) parser.add_argument('--help', action='help', help='show this help message and exit') parser.add_argument('-t', '--tta', type=int, choices=range(2), help='8x slower and slightly high quality') parser.add_argument('--gpu', type=int, help='gpu device no') diff --git a/src/wrappers/waifu2x_converter_cpp.py b/src/wrappers/waifu2x_converter_cpp.py index 1922b45..b3ea121 100644 --- a/src/wrappers/waifu2x_converter_cpp.py +++ b/src/wrappers/waifu2x_converter_cpp.py @@ -4,7 +4,7 @@ Name: Waifu2x Converter CPP Driver Author: K4YT3X Date Created: February 8, 2019 -Last Modified: May 4, 2020 +Last Modified: May 7, 2020 Description: This class is a high-level wrapper for waifu2x-converter-cpp. @@ -38,6 +38,7 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) parser.add_argument('--help', action='help', help='show this help message and exit') parser.add_argument('--list-supported-formats', action='store_true', help='dump currently supported format list') parser.add_argument('--list-opencv-formats', action='store_true', help='(deprecated. Use --list-supported-formats) dump opencv supported format list') diff --git a/src/wrappers/waifu2x_ncnn_vulkan.py b/src/wrappers/waifu2x_ncnn_vulkan.py index 31556c3..a62bf21 100644 --- a/src/wrappers/waifu2x_ncnn_vulkan.py +++ b/src/wrappers/waifu2x_ncnn_vulkan.py @@ -4,7 +4,7 @@ Name: Waifu2x NCNN Vulkan Driver Creator: SAT3LL Date Created: June 26, 2019 -Last Modified: May 5, 2020 +Last Modified: May 7, 2020 Editor: K4YT3X Last Modified: February 22, 2020 @@ -42,6 +42,7 @@ class WrapperMain: @staticmethod def parse_arguments(arguments): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) + parser.error = lambda message: (_ for _ in ()).throw(AttributeError(message)) parser.add_argument('--help', action='help', help='show this help message and exit') parser.add_argument('-v', action='store_true', help='verbose output') # parser.add_argument('-i', type=pathlib.Path, help='input image path (jpg/png) or directory')