video2x GUI 2.0.0: completely redesigned and rewritten with PyQt5

This commit is contained in:
k4yt3x 2020-05-06 04:45:48 -04:00
parent f00a10aa91
commit 9b4fb3258a
4 changed files with 897 additions and 383 deletions

View File

@ -4,7 +4,7 @@
Name: Video2X Upscaler Name: Video2X Upscaler
Author: K4YT3X Author: K4YT3X
Date Created: December 10, 2018 Date Created: December 10, 2018
Last Modified: May 5, 2020 Last Modified: May 6, 2020
Description: This file contains the Upscaler class. Each Description: This file contains the Upscaler class. Each
instance of the Upscaler class is an upscaler on an image or instance of the Upscaler class is an upscaler on an image or
@ -20,7 +20,9 @@ from wrappers.ffmpeg import Ffmpeg
from fractions import Fraction from fractions import Fraction
import contextlib import contextlib
import copy import copy
import gettext
import importlib import importlib
import locale
import os import os
import pathlib import pathlib
import re import re
@ -35,6 +37,16 @@ import traceback
from avalon_framework import Avalon from avalon_framework import Avalon
from tqdm import tqdm from tqdm import tqdm
# internationalization constants
DOMAIN = 'video2x'
LOCALE_DIRECTORY = pathlib.Path(__file__).parent.absolute() / 'locale'
# getting default locale settings
default_locale, encoding = locale.getdefaultlocale()
language = gettext.translation(DOMAIN, LOCALE_DIRECTORY, [default_locale], fallback=True)
language.install()
_ = language.gettext
# these names are consistent for # these names are consistent for
# - driver selection in command line # - driver selection in command line
# - driver wrapper file names # - driver wrapper file names

View File

@ -1,34 +1,35 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Creator: Video2X GUI Creator: Video2X QT
Author: K4YT3X Author: K4YT3X
Date Created: July 27, 2019 Date Created: May 5, 2020
Last Modified: December 11, 2019 Last Modified: May 6, 2020
Description: A simple GUI for Video2X made with tkinter.
""" """
# local imports # local imports
from exceptions import *
from upscaler import Upscaler from upscaler import Upscaler
# built-in imports # built-in imports
from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from tkinter.filedialog import *
import contextlib
import pathlib import pathlib
import sys import sys
# built-in imports
import contextlib
import re
import shutil
import tempfile import tempfile
import threading import threading
import time import time
import traceback
import yaml import yaml
VERSION = '1.1.3' # third-party imports
from PyQt5 import QtWidgets, QtGui
from PyQt5 import uic
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QRunnable, QThreadPool
VIDEO2X_CONFIG = pathlib.Path(sys.argv[0]).parent.absolute() / 'video2x.yaml' VERSION = '2.0.0'
LEGAL_INFO = f'''Video2X GUI Version: {VERSION} LEGAL_INFO = f'''Video2X GUI Version: {VERSION}
Author: K4YT3X Author: K4YT3X
@ -36,427 +37,352 @@ License: GNU GPL v3
Github Page: https://github.com/k4yt3x/video2x Github Page: https://github.com/k4yt3x/video2x
Contact: k4yt3x@k4yt3x.com''' Contact: k4yt3x@k4yt3x.com'''
# global static variables
AVAILABLE_METHODS = {
'GPU': 'gpu',
'CUDNN': 'cudnn',
'CPU': 'cpu'
}
AVAILABLE_DRIVERS = { AVAILABLE_DRIVERS = {
'Waifu2X Caffe': 'waifu2x_caffe', 'Waifu2X Caffe': 'waifu2x_caffe',
'Waifu2X Converter CPP': 'waifu2x_converter', 'Waifu2X Converter CPP': 'waifu2x_converter_cpp',
'Waifu2x NCNN Vulkan': 'waifu2x_ncnn_vulkan', 'Waifu2x NCNN Vulkan': 'waifu2x_ncnn_vulkan',
'Anime4K': 'anime4k' 'SRMD NCNN Vulkan': 'srmd_ncnn_vulkan',
'Anime4KCPP': 'anime4kcpp'
} }
IMAGE_FORMATS = {'PNG', 'JPG'}
DEMUXER_EXTENSIONS = {'3dostr', '4xm', 'aa', 'aac', 'ac3', 'acm', 'act', class UpscalerSignals(QObject):
'adf', 'adp', 'ads', 'adx', 'aea', 'afc', 'aiff', 'aix', 'alaw', finished = pyqtSignal()
'alias_pix', 'alsa', 'amr', 'amrnb', 'amrwb', 'anm', 'apc', 'ape', error = pyqtSignal(tuple)
'apng', 'aptx', 'aptx_hd', 'aqtitle', 'asf', 'asf_o', 'ass', 'ast', result = pyqtSignal(object)
'au', 'avi', 'avisynth', 'avr', 'avs', 'avs2', 'bethsoftvid', 'bfi',
'bfstm', 'bin', 'bink', 'bit', 'bmp_pipe', 'bmv', 'boa', 'brender_pix',
'brstm', 'c93', 'caf', 'cavsvideo', 'cdg', 'cdxl', 'cine', 'codec2',
'codec2raw', 'concat', 'dash', 'data', 'daud', 'dcstr', 'dds_pipe',
'dfa', 'dirac', 'dnxhd', 'dpx_pipe', 'dsf', 'dsicin', 'dss', 'dts',
'dtshd', 'dv', 'dvbsub', 'dvbtxt', 'dxa', 'ea', 'ea_cdata', 'eac3',
'epaf', 'exr_pipe', 'f32be', 'f32le', 'f64be', 'f64le', 'fbdev',
'ffmetadata', 'film_cpk', 'filmstrip', 'fits', 'flac', 'flic', 'flv',
'frm', 'fsb', 'g722', 'g723_1', 'g726', 'g726le', 'g729', 'gdv', 'genh',
'gif', 'gsm', 'gxf', 'h261', 'h263', 'h264', 'hevc', 'hls', 'applehttp',
'hnm', 'ico', 'idcin', 'idf', 'iec61883', 'iff', 'ilbc', 'image2',
'image2pipe', 'ingenient', 'ipmovie', 'ircam', 'iss', 'iv8', 'ivf',
'ivr', 'j2k_pipe', 'jack', 'jacosub', 'jpeg_pipe', 'jpegls_pipe',
'jv', 'kmsgrab', 'lavfi', 'libcdio', 'libdc1394', 'libgme', 'libopenmpt',
'live_flv', 'lmlm4', 'loas', 'lrc', 'lvf', 'lxf', 'm4v', 'matroska', 'webm',
'mgsts', 'microdvd', 'mjpeg', 'mjpeg_2000', 'mlp', 'mlv', 'mm', 'mmf',
'mov', 'mp4', 'm4a', '3gp', '3g2', 'mj2', 'mp3', 'mpc', 'mpc8', 'mpeg',
'mpegts', 'mpegtsraw', 'mpegvideo', 'mpjpeg', 'mpl2', 'mpsub', 'msf',
'msnwctcp', 'mtaf', 'mtv', 'mulaw', 'musx', 'mv', 'mvi', 'mxf', 'mxg',
'nc', 'nistsphere', 'nsp', 'nsv', 'nut', 'nuv', 'ogg', 'oma', 'openal',
'oss', 'paf', 'pam_pipe', 'pbm_pipe', 'pcx_pipe', 'pgm_pipe', 'pgmyuv_pipe',
'pictor_pipe', 'pjs', 'pmp', 'png_pipe', 'ppm_pipe', 'psd_pipe', 'psxstr',
'pulse', 'pva', 'pvf', 'qcp', 'qdraw_pipe', 'r3d', 'rawvideo', 'realtext',
'redspark', 'rl2', 'rm', 'roq', 'rpl', 'rsd', 'rso', 'rtp', 'rtsp',
's16be', 's16le', 's24be', 's24le', 's32be', 's32le', 's337m', 's8',
'sami', 'sap', 'sbc', 'sbg', 'scc', 'sdp', 'sdr2', 'sds', 'sdx', 'ser',
'sgi_pipe', 'shn', 'siff', 'sln', 'smjpeg', 'smk', 'smush', 'sndio',
'sol', 'sox', 'spdif', 'srt', 'stl', 'subviewer', 'subviewer1', 'sunrast_pipe',
'sup', 'svag', 'svg_pipe', 'swf', 'tak', 'tedcaptions', 'thp', 'tiertexseq',
'tiff_pipe', 'tmv', 'truehd', 'tta', 'tty', 'txd', 'ty', 'u16be', 'u16le',
'u24be', 'u24le', 'u32be', 'u32le', 'u8', 'v210', 'v210x', 'vag', 'vc1',
'vc1test', 'vidc', 'video4linux2', 'v4l2', 'vivo', 'vmd', 'vobsub', 'voc',
'vpk', 'vplayer', 'vqf', 'w64', 'wav', 'wc3movie', 'webm_dash_manifest',
'webp_pipe', 'webvtt', 'wsaud', 'wsd', 'wsvqa', 'wtv', 'wv', 'wve', 'x11grab',
'xa', 'xbin', 'xmv', 'xpm_pipe', 'xvag', 'xwd_pipe', 'xwma', 'yop', 'yuv4mpegpipe'}
class Worker(QRunnable):
class Video2xGui(): def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
def __init__(self): # Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = UpscalerSignals()
self.running = False @pyqtSlot()
def run(self):
# create main window
self.main_window = Tk()
self.main_window.title(f'Video2X GUI {VERSION}')
self.main_frame = Frame()
self.main_frame.pack(fill=BOTH, expand=True)
# add menu bar
self.menu_bar = Menu(self.main_frame)
# file menu
self.file_menu = Menu(self.menu_bar, tearoff=0)
self.file_menu.add_command(label='Exit', command=self.main_frame.quit)
self.menu_bar.add_cascade(label='File', menu=self.file_menu)
# help menu
self.help_menu = Menu(self.menu_bar, tearoff=0)
self.help_menu.add_command(label='About', command=self._display_help)
self.menu_bar.add_cascade(label='Help', menu=self.help_menu)
self.main_window.config(menu=self.menu_bar)
# file frame
self.file_frame = Frame(self.main_frame)
self.file_frame.pack(fill=X, padx=5, pady=5, expand=True)
# input file
self.input_file = StringVar()
label_text = StringVar()
label_text.set('Input File')
Label(self.file_frame, textvariable=label_text, relief=RIDGE, width=10).grid(row=0, column=0, padx=5, pady=5, sticky=W)
Entry(self.file_frame, textvariable=self.input_file, width=60).grid(row=0, column=1, padx=5, pady=5, sticky=W)
Button(self.file_frame, text='Select', command=self._select_input).grid(row=0, column=2, padx=5, pady=5, sticky=W)
# output file
self.output_file = StringVar()
label_text = StringVar()
label_text.set('Output File')
Label(self.file_frame, textvariable=label_text, relief=RIDGE, width=10).grid(row=1, column=0, padx=5, pady=5, sticky=W)
Entry(self.file_frame, textvariable=self.output_file, width=60).grid(row=1, column=1, padx=5, pady=5, sticky=W)
Button(self.file_frame, text='Select', command=self._select_output).grid(row=1, column=2, padx=5, pady=5, sticky=W)
# options
self.options_frame = Frame()
# self.options_left.pack(fill=X, padx=5, pady=5, expand=True)
self.options_frame.pack(fill=X, padx=5, pady=5, expand=True)
self.options_left = Frame(self.options_frame)
# self.options_left.pack(fill=X, padx=5, pady=5, expand=True)
self.options_left.grid(row=0, column=0, padx=5, pady=5, sticky=N)
# width
self.width = IntVar()
# self.width.set(1920)
Label(self.options_left, text='Width', relief=RIDGE, width=15).grid(row=0, column=0, padx=2, pady=3)
width_field = Entry(self.options_left, textvariable=self.width)
width_field.grid(row=0, column=1, padx=2, pady=3, sticky=W)
# height
self.height = IntVar()
# self.height.set(1080)
Label(self.options_left, text='Height', relief=RIDGE, width=15).grid(row=1, column=0, padx=2, pady=3)
height_field = Entry(self.options_left, textvariable=self.height)
height_field.grid(row=1, column=1, padx=2, pady=3, sticky=W)
# scale ratio
self.scale_ratio = DoubleVar()
# self.scale_ratio.set(2.0)
Label(self.options_left, text='Scale Ratio', relief=RIDGE, width=15).grid(row=2, column=0, padx=2, pady=3)
scale_ratio_field = Entry(self.options_left, textvariable=self.scale_ratio)
scale_ratio_field.grid(row=2, column=1, padx=2, pady=3, sticky=W)
# image format
self.image_format = StringVar(self.options_left)
self.image_format.set('PNG')
Label(self.options_left, text='Image Format', relief=RIDGE, width=15).grid(row=3, column=0, padx=2, pady=3)
image_format_menu = OptionMenu(self.options_left, self.image_format, *IMAGE_FORMATS)
image_format_menu.grid(row=3, column=1, padx=2, pady=3, sticky=W)
# options
self.options_right = Frame(self.options_frame)
# self.options_left.pack(fill=X, padx=5, pady=5, expand=True)
self.options_right.grid(row=0, column=1, padx=5, pady=5, sticky=N)
# threads
self.threads = IntVar()
self.threads.set(1)
Label(self.options_right, text='Threads', relief=RIDGE, width=15).grid(row=0, column=0, padx=2, pady=3)
threads_field = Entry(self.options_right, textvariable=self.threads)
threads_field.grid(row=0, column=1, padx=2, pady=3, sticky=W)
# method
self.method = StringVar(self.options_left)
self.method.set('GPU')
Label(self.options_right, text='Method', relief=RIDGE, width=15).grid(row=1, column=0, padx=2, pady=3)
method_menu = OptionMenu(self.options_right, self.method, *AVAILABLE_METHODS)
method_menu.grid(row=1, column=1, padx=2, pady=3, sticky=W)
# driver
self.driver = StringVar(self.options_left)
self.driver.set('Waifu2X Caffe')
Label(self.options_right, text='Driver', relief=RIDGE, width=15).grid(row=2, column=0, padx=2, pady=3)
driver_menu = OptionMenu(self.options_right, self.driver, *AVAILABLE_DRIVERS)
driver_menu.grid(row=2, column=1, padx=2, pady=3, sticky=W)
# preserve frames
self.preserve_frames = BooleanVar(self.options_left)
self.preserve_frames.set(False)
Label(self.options_right, text='Preserve Frames', relief=RIDGE, width=15).grid(row=3, column=0, padx=2, pady=3)
preserve_frames_menu = OptionMenu(self.options_right, self.preserve_frames, *{True, False})
preserve_frames_menu.grid(row=3, column=1, padx=2, pady=3, sticky=W)
# progress bar
self.progress_bar_frame = Frame()
self.progress_bar_frame.pack(fill=X, padx=5, pady=5, expand=True)
self.progress_bar = ttk.Progressbar(self.progress_bar_frame, orient='horizontal', length=100, mode='determinate')
self.progress_bar.pack(fill=X)
# start button frame
self.start_frame = Frame()
self.start_frame.pack(fill=X, padx=5, pady=5, expand=True)
# start button
self.start_button_text = StringVar()
self.start_button_text.set('Start')
Button(self.start_frame, textvariable=self.start_button_text, command=self._launch_upscaling, width=20).pack(side=RIGHT)
self.main_frame.mainloop()
def _display_help(self):
messagebox.showinfo('About', LEGAL_INFO)
def _launch_upscaling(self):
# prevent launching multiple instances
if self.running:
messagebox.showerror('Error', 'Video2X is already running')
return
# arguments sanity check
if self.input_file.get() == '':
messagebox.showerror('Error', 'You must specify input video file/directory path')
return
if self.output_file.get() == '':
messagebox.showerror('Error', 'You must specify output video file/directory path')
return
if (self.driver.get() in ['Waifu2X Converter CPP', 'Waifu2x NCNN Vulkan', 'Anime4K']) and self.width.get() and self.height.get():
messagebox.showerror('Error', f'Selected driver \"{self.driver.get()}\" accepts only scaling ratio')
return
if self.driver.get() == 'waifu2x_ncnn_vulkan' and (self.scale_ratio.get() > 2 or not self.scale_ratio.get().is_integer()):
messagebox.showerror('Error', 'Scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan')
return
if (self.width.get() or self.height.get()) and self.scale_ratio.get():
messagebox.showerror('Error', 'You can only specify either scaling ratio or output width and height')
return
if (self.width.get() and not self.height.get()) or (not self.width.get() and self.height.get()):
messagebox.showerror('Error', 'You must specify both width and height')
return
if (not self.width.get() or not self.height.get()) and not self.scale_ratio.get():
messagebox.showerror('Error', 'You must specify either output dimensions or scaling ratio')
return
upscale = threading.Thread(target=self._upscale)
upscale.start()
self.running = True
self.start_button_text.set('Running')
def _upscale(self):
# Retrieve args/kwargs here; and fire processing using them
try: try:
# start timer result = self.fn(*self.args, **self.kwargs)
begin_time = time.time() except Exception:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
# read configuration file class Video2XMainWindow(QtWidgets.QMainWindow):
config = read_config(VIDEO2X_CONFIG)
config = absolutify_paths(config)
input_file = pathlib.Path(self.input_file.get()) def __init__(self, *args, **kwargs):
output_file = pathlib.Path(self.output_file.get()) super().__init__(*args, **kwargs)
driver = AVAILABLE_DRIVERS[self.driver.get()] uic.loadUi('video2x_gui.ui', self)
# load specified driver's config into driver_settings self.video2x_icon_path = str((pathlib.Path(__file__).parent / 'images' / 'video2x.png').absolute())
driver_settings = config[driver] self.setWindowTitle(f'Video2X GUI {VERSION}')
self.setWindowIcon(QtGui.QIcon(self.video2x_icon_path))
# if executable doesn't exist, show warning # menu bar
if not pathlib.Path(driver_settings['path']).is_file() and not pathlib.Path(f'{driver_settings["path"]}.exe').is_file(): self.action_exit = self.findChild(QtWidgets.QAction, 'actionExit')
messagebox.showerror('Error', 'Specified driver directory doesn\'t exist\nPlease check the configuration file settings') self.action_exit.triggered.connect(sys.exit)
raise FileNotFoundError(driver_settings['path'])
# read FFmpeg configuration self.action_about = self.findChild(QtWidgets.QAction, 'actionAbout')
ffmpeg_settings = config['ffmpeg'] self.action_about.triggered.connect(lambda: self.show_message(LEGAL_INFO, custom_icon=QtGui.QPixmap(self.video2x_icon_path)))
# load video2x settings # main tab
image_format = config['video2x']['image_format'].lower() # select input file/folder
preserve_frames = config['video2x']['preserve_frames'] self.input_line_edit = self.findChild(QtWidgets.QLineEdit, 'inputLineEdit')
self.input_select_file_button = self.findChild(QtWidgets.QPushButton, 'inputSelectFileButton')
self.input_select_file_button.clicked.connect(self.select_input_file)
self.input_select_folder_button = self.findChild(QtWidgets.QPushButton, 'inputSelectFolderButton')
self.input_select_folder_button.clicked.connect(self.select_input_folder)
# select output file/folder
self.output_line_edit = self.findChild(QtWidgets.QLineEdit, 'outputLineEdit')
self.output_select_file_button = self.findChild(QtWidgets.QPushButton, 'outputSelectFileButton')
self.output_select_file_button.clicked.connect(self.select_output_file)
self.output_select_folder_button = self.findChild(QtWidgets.QPushButton, 'outputSelectFolderButton')
self.output_select_folder_button.clicked.connect(self.select_output_folder)
# config file
self.config_line_edit = self.findChild(QtWidgets.QLineEdit, 'configLineEdit')
self.config_line_edit.setText(str((pathlib.Path(__file__).parent / 'video2x.yaml').absolute()))
self.config_select_file_button = self.findChild(QtWidgets.QPushButton, 'configSelectButton')
self.config_select_file_button.clicked.connect(self.select_config_file)
# cache directory
self.cache_line_edit = self.findChild(QtWidgets.QLineEdit, 'cacheLineEdit')
self.cache_select_folder_button = self.findChild(QtWidgets.QPushButton, 'cacheSelectFolderButton')
self.cache_select_folder_button.clicked.connect(self.select_cache_folder)
# express settings
self.driver_combo_box = self.findChild(QtWidgets.QComboBox, 'driverComboBox')
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')
# progress bar and start/stop controls
self.progress_bar = self.findChild(QtWidgets.QProgressBar, 'progressBar')
self.start_button = self.findChild(QtWidgets.QPushButton, 'startButton')
self.start_button.clicked.connect(self.upscale)
self.stop_button = self.findChild(QtWidgets.QPushButton, 'stopButton')
self.stop_button.clicked.connect(self.stop)
# load configurations
self.load_configurations()
@staticmethod
def read_config(config_file: pathlib.Path) -> dict:
""" read video2x configurations from config file
Arguments:
config_file {pathlib.Path} -- video2x configuration file pathlib.Path
Returns:
dict -- dictionary of video2x configuration
"""
with open(config_file, 'r') as config:
return yaml.load(config, Loader=yaml.FullLoader)
def load_configurations(self):
config_file_path = pathlib.Path(self.config_line_edit.text())
if not config_file_path.is_file():
QtWidgets.QErrorMessage(self).showMessage('Video2X configuration file not found, please specify manually.')
else:
self.config = self.read_config(config_file_path)
self.ffmpeg_settings = self.config['ffmpeg']
# load cache directory # load cache directory
if isinstance(config['video2x']['video2x_cache_directory'], str): if self.config['video2x']['video2x_cache_directory'] is not None:
video2x_cache_directory = pathlib.Path(config['video2x']['video2x_cache_directory']) video2x_cache_directory = pathlib.Path(self.config['video2x']['video2x_cache_directory'])
else: else:
video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x' video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x'
if video2x_cache_directory.exists() and not video2x_cache_directory.is_dir(): if video2x_cache_directory.exists() and not video2x_cache_directory.is_dir():
messagebox.showerror('Error', 'Specified cache directory is a file/link') self.show_error('Specified cache directory is a file/link')
raise FileExistsError('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(): elif not video2x_cache_directory.exists():
# try creating the cache directory try:
if messagebox.askyesno('Question', f'Specified cache directory {video2x_cache_directory} does not exist\nCreate directory?'): video2x_cache_directory.mkdir(parents=True, exist_ok=True)
try: except Exception as exception:
video2x_cache_directory.mkdir(parents=True, exist_ok=True) self.show_error(f'Unable to create cache directory: {video2x_cache_directory}')
raise exception
self.cache_line_edit.setText(str(video2x_cache_directory.absolute()))
# there can be a number of exceptions here self.preserve_frames_check_box.setChecked(self.config['video2x']['preserve_frames'])
# PermissionError, FileExistsError, etc. self.start_button.setEnabled(True)
# therefore, we put a catch-them-all here
except Exception as e:
messagebox.showerror('Error', f'Unable to create {video2x_cache_directory}\nAborting...')
raise e
else:
raise FileNotFoundError('Could not create cache directory')
# load more settings from gui def select_input_file(self):
width = self.width.get() input_file = QtWidgets.QFileDialog.getOpenFileName(self, 'Select Input File', )
height = self.height.get() if not isinstance(input_file, tuple) or input_file[0] == '':
scale_ratio = self.scale_ratio.get() return
image_format = self.image_format.get()
threads = self.threads.get()
method = AVAILABLE_METHODS[self.method.get()]
preserve_frames = self.preserve_frames.get()
self.upscaler = Upscaler(input_video=input_file, output_video=output_file, method=method, driver_settings=driver_settings, ffmpeg_settings=ffmpeg_settings) input_file = pathlib.Path(input_file[0])
# set optional options self.input_line_edit.setText(str(input_file.absolute()))
self.upscaler.waifu2x_driver = driver
self.upscaler.scale_width = width
self.upscaler.scale_height = height
self.upscaler.scale_ratio = scale_ratio
self.upscaler.model_dir = None
self.upscaler.threads = threads
self.upscaler.video2x_cache_directory = video2x_cache_directory
self.upscaler.image_format = image_format
self.upscaler.preserve_frames = preserve_frames
# run upscaler # try to set an output file name automatically
self.upscaler.create_temp_directories() output_file = input_file.parent / f'{input_file.stem}_output.mp4'
# start progress bar output_file_id = 0
progress_bar = threading.Thread(target=self._progress_bar) while output_file.is_file() and output_file_id <= 10:
progress_bar.start() output_file = input_file.parent / pathlib.Path(f'{input_file.stem}_output_{output_file_id}.mp4')
output_file_id += 1
# start upscaling if not output_file.exists():
self.upscaler.run() self.output_line_edit.setText(str(output_file.absolute()))
self.upscaler.cleanup_temp_directories()
# show message when upscaling completes def select_input_folder(self):
messagebox.showinfo('Info', f'Upscaling Completed\nTime Taken: {round((time.time() - begin_time), 5)} seconds') input_folder = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Input Folder')
self.progress_bar['value'] = 100 if input_folder == '':
self.running = False return
self.start_button_text.set('Start')
except Exception as e:
messagebox.showerror('Error', f'Upscaler ran into an error:\n{e}')
# try cleaning up temp directories input_folder = pathlib.Path(input_folder)
with contextlib.suppress(Exception):
self.upscaler.cleanup_temp_directories()
self.input_line_edit.setText(str(input_folder.absolute()))
def _progress_bar(self): # try to set an output file name automatically
""" This method prints a progress bar output_folder = input_folder.parent / f'{input_folder.stem}_output'
output_file_id = 0
while output_folder.is_dir() and output_file_id <= 10:
output_folder = input_folder.parent / pathlib.Path(f'{input_folder.stem}_output_{output_file_id}')
output_file_id += 1
if not output_folder.exists():
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):
return
self.output_line_edit.setText(str(pathlib.Path(output_file[0]).absolute()))
def select_output_folder(self):
output_folder = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Output Folder')
if output_folder == '':
return
self.output_line_edit.setText(str(pathlib.Path(output_folder).absolute()))
def select_cache_folder(self):
cache_folder = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Cache Folder')
if cache_folder == '':
return
self.cache_line_edit.setText(str(pathlib.Path(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):
return
self.config_line_edit.setText(str(pathlib.Path(config_file[0]).absolute()))
self.load_configurations()
def show_error(self, message: str):
QtWidgets.QErrorMessage(self).showMessage(message)
def show_message(self, message: str, custom_icon=None):
message_box = QtWidgets.QMessageBox()
message_box.setWindowTitle('Message')
if custom_icon:
message_box.setIconPixmap(custom_icon.scaled(64, 64))
else:
message_box.setIcon(QtWidgets.QMessageBox.Information)
message_box.setText(message)
message_box.exec_()
def start_progress_bar(self):
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.
"""
# initialize variables early # initialize variables early
self.upscaler.progress_bar_exit_signal = False self.upscaler.progress_bar_exit_signal = False
self.upscaler.total_frames_upscaled = 0 self.upscaler.total_frames_upscaled = 0
self.upscaler.total_frames = 1 self.upscaler.total_frames = 1
# initialize progress bar values # initialize progress bar values
self.progress_bar['value'] = 0 self.progress_bar.setValue(0)
while not self.upscaler.progress_bar_exit_signal: while not self.upscaler.progress_bar_exit_signal:
self.progress_bar['value'] = int(100 * self.upscaler.total_frames_upscaled / self.upscaler.total_frames) self.progress_bar.setValue(int(100 * self.upscaler.total_frames_upscaled / self.upscaler.total_frames))
time.sleep(1) time.sleep(1)
self.progress_bar.setValue(100)
def _select_input(self): def upscale(self):
self.input_file.set(askopenfilename(title='Select Input File'))
# remove input file extension # start execution
input_filename = str(self.input_file.get()) try:
for extension in DEMUXER_EXTENSIONS: # start timer
if input_filename.endswith(f'.{extension}'): self.begin_time = time.time()
input_filename = input_filename[:-1 - len(extension)]
# try to set an output file name automatically input_directory = pathlib.Path(self.input_line_edit.text())
output_file = pathlib.Path(f'{input_filename}_output.mp4') output_directory = pathlib.Path(self.output_line_edit.text())
output_file_id = 0 self.threadpool = QThreadPool()
while output_file.is_file() and output_file_id <= 10: self.driver_settings = self.config[AVAILABLE_DRIVERS[self.driver_combo_box.currentText()]]
output_file = pathlib.Path(f'{input_filename}_output_{output_file_id}.mp4')
output_file_id += 1
if not output_file.exists():
self.output_file.set(str(output_file))
def _select_output(self): # if input specified is a single file
self.output_file.set(asksaveasfilename(title='Select Output File')) if input_directory.is_file():
# upscale single video file
# 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')
self.upscaler = Upscaler(input_video=input_directory,
output_video=output_directory,
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(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 = threading.Thread(target=self.start_progress_bar)
progress_bar.start()
# run upscaler
worker = Worker(self.upscaler.run)
worker.signals.finished.connect(self.upscale_completed)
self.threadpool.start(worker)
# 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(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 = threading.Thread(target=self.start_progress_bar)
progress_bar.start()
# run upscaler
self.upscaler.run()
else:
self.show_error('Input path is neither a file nor a directory')
raise FileNotFoundError(f'{input_directory} is neither file nor directory')
except Exception as e:
self.show_error(f'Upscaler ran into an error:\n{e}')
# 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):
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(self.cache_line_edit.text()))
def stop(self):
# stop execution here
pass
def read_config(config_file): app = QtWidgets.QApplication(sys.argv)
""" Reads configuration file window = Video2XMainWindow()
window.show()
Returns a dictionary read by parsing Video2X config. app.exec_()
"""
with open(config_file, 'r') as raw_config:
config = yaml.load(raw_config, Loader=yaml.FullLoader)
return config
def absolutify_paths(config):
""" Check to see if paths to binaries are absolute
This function checks if paths to binary files are absolute.
If not, then absolutify the path.
Arguments:
config {dict} -- configuration file dictionary
Returns:
dict -- configuration file dictionary
"""
current_directory = pathlib.Path(sys.argv[0]).parent.absolute()
# check waifu2x-caffe path
if not re.match('^[a-z]:', config['waifu2x_caffe']['path'], re.IGNORECASE):
config['waifu2x_caffe']['path'] = current_directory / config['waifu2x_caffe']['path']
# check waifu2x-converter-cpp path
if not re.match('^[a-z]:', config['waifu2x_converter']['path'], re.IGNORECASE):
config['waifu2x_converter']['path'] = current_directory / config['waifu2x_converter']['path']
# check waifu2x_ncnn_vulkan path
if not re.match('^[a-z]:', config['waifu2x_ncnn_vulkan']['path'], re.IGNORECASE):
config['waifu2x_ncnn_vulkan']['path'] = current_directory / config['waifu2x_ncnn_vulkan']['path']
# check ffmpeg path
if not re.match('^[a-z]:', config['ffmpeg']['ffmpeg_path'], re.IGNORECASE):
config['ffmpeg']['ffmpeg_path'] = current_directory / config['ffmpeg']['ffmpeg_path']
# check video2x cache path
if config['video2x']['video2x_cache_directory']:
if not re.match('^[a-z]:', config['video2x']['video2x_cache_directory'], re.IGNORECASE):
config['video2x']['video2x_cache_directory'] = current_directory / config['video2x']['video2x_cache_directory']
return config
video2x_gui = Video2xGui()

View File

@ -0,0 +1,3 @@
{
"files": ["video2x_gui.ui","video2x_gui.py"]
}

573
src/video2x_gui.ui Normal file
View File

@ -0,0 +1,573 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Video2xQt</class>
<widget class="QMainWindow" name="Video2xQt">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>618</width>
<height>389</height>
</rect>
</property>
<property name="windowTitle">
<string>Video2xQt</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="mainTab">
<attribute name="title">
<string>Main</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<layout class="QVBoxLayout" name="fileSelectionVerticalLayout">
<item>
<layout class="QHBoxLayout" name="inputHorizontalLayout">
<item>
<widget class="QLabel" name="inputLabel">
<property name="minimumSize">
<size>
<width>63</width>
<height>0</height>
</size>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="text">
<string>Input</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="inputLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="inputSelectFileButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Select File</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="inputSelectFolderButton">
<property name="text">
<string>Select Folder</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="outputHorizontalLayout">
<item>
<widget class="QLabel" name="outputLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>63</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Output</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="outputLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="outputSelectFileButton">
<property name="text">
<string>Select File</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="outputSelectFolderButton">
<property name="text">
<string>Select Folder</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="configHorizontalLayout">
<item>
<widget class="QLabel" name="configLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>63</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Config</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="configLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="configSelectButton">
<property name="text">
<string>Select</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="cacheLabel">
<property name="minimumSize">
<size>
<width>63</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Cache Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="cacheLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="cacheSelectFolderButton">
<property name="text">
<string>Select Folder</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="expressSettingsGroupBox">
<property name="title">
<string>Express Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QVBoxLayout" name="expressSettingsVerticalLayout">
<item>
<layout class="QHBoxLayout" name="driverHorizontalLayout">
<item>
<widget class="QLabel" name="driverLabel">
<property name="text">
<string>Driver</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="driverComboBox">
<item>
<property name="text">
<string>Waifu2X Caffe</string>
</property>
</item>
<item>
<property name="text">
<string>Waifu2X Converter CPP</string>
</property>
</item>
<item>
<property name="text">
<string>Waifu2X NCNN Vulkan</string>
</property>
</item>
<item>
<property name="text">
<string>SRMD NCNN Vulkan</string>
</property>
</item>
<item>
<property name="text">
<string>Anime4KCPP</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="processesHorizontalLayout">
<item>
<widget class="QLabel" name="processesLabel">
<property name="text">
<string>Processes</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="processesSpinBox">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="scaleRatioHorizontalLayout">
<item>
<widget class="QLabel" name="scaleRatioLabel">
<property name="text">
<string>Scale Ratio</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="scaleRatioDoubleSpinBox">
<property name="value">
<double>2.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="preserveFramesCheckBox">
<property name="text">
<string>Preserve Frames</string>
</property>
<property name="tristate">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="driverSettingsTab">
<attribute name="title">
<string>Driver Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QTabWidget" name="driversTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="waifu2xCaffeTab">
<attribute name="title">
<string>Waifu2X Caffe</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<layout class="QVBoxLayout" name="waifu2xCaffeTabVerticalLayout">
<item>
<layout class="QHBoxLayout" name="waifu2xCaffePathHorizontalLayout">
<item>
<widget class="QLineEdit" name="waifu2xCaffePathLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="waifu2xCaffePathSelectButton">
<property name="text">
<string>Select Binary Path</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="waifu2xCaffeModeHorizontalLayout">
<item>
<widget class="QLabel" name="waifu2xCaffeModeLabel">
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="waifu2xCaffeModeComboBox">
<item>
<property name="text">
<string>Noise Scale</string>
</property>
</item>
<item>
<property name="text">
<string>Auto Scale</string>
</property>
</item>
<item>
<property name="text">
<string>Noise</string>
</property>
</item>
<item>
<property name="text">
<string>Scale</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="waifu2xCaffeNoiseLevelHorizontalLayout">
<item>
<widget class="QLabel" name="waifu2xCaffeNoiseLevelLabel">
<property name="text">
<string>Noise Level</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="waifu2xCaffeNoiseLevelComboBox">
<item>
<property name="text">
<string>3</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
<item>
<property name="text">
<string>1</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="waifu2xCaffeProcessHorizontalLayout">
<item>
<widget class="QLabel" name="waifu2xCaffeProcessLabel">
<property name="text">
<string>Process</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="waifu2xCaffeProcessComboBox">
<item>
<property name="text">
<string>GPU</string>
</property>
</item>
<item>
<property name="text">
<string>cuDNN</string>
</property>
</item>
<item>
<property name="text">
<string>CPU</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="waifu2xCaffeModelHorizontalLayout">
<item>
<widget class="QLabel" name="waifu2xCaffeModelLabel">
<property name="text">
<string>Model</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="waifu2xCaffeModelComboBox">
<item>
<property name="text">
<string>cunet</string>
</property>
</item>
<item>
<property name="text">
<string>anime_style_art</string>
</property>
</item>
<item>
<property name="text">
<string>anime_style_art_rgb</string>
</property>
</item>
<item>
<property name="text">
<string>photo</string>
</property>
</item>
<item>
<property name="text">
<string>ukbench</string>
</property>
</item>
<item>
<property name="text">
<string>upconv_7_anime_style_art_rgb</string>
</property>
</item>
<item>
<property name="text">
<string>upconv_7_photo</string>
</property>
</item>
<item>
<property name="text">
<string>upresnet10</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="waifu2xConverterCppTab">
<attribute name="title">
<string>Waifu2X Converter CPP</string>
</attribute>
</widget>
<widget class="QWidget" name="waifu2xNcnnVulkanTab">
<attribute name="title">
<string>Waifu2X NCNN Vulkan</string>
</attribute>
</widget>
<widget class="QWidget" name="srmdNcnnVulkanTab">
<attribute name="title">
<string>SRMD NCNN Vulkan</string>
</attribute>
</widget>
<widget class="QWidget" name="anime4kCppTab">
<attribute name="title">
<string>Anime4K CPP</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="controlsHorizontalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="startButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>618</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuAbout">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuAbout"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>