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')