13 Commits

16 changed files with 779 additions and 650 deletions

View File

@@ -72,6 +72,8 @@ Watch for the sharper edges in this screenshot around the shadows:
![preview](https://user-images.githubusercontent.com/21986859/49412428-65083280-f73a-11e8-8237-bb34158a545e.png)
*Upscale Comparison Demonstration*
**You can also watch the YouTube video Demo: https://www.youtube.com/watch?v=PG94iPoeoZk**
Clip is from trailer of animated movie "千と千尋の神隠し". Copyright belongs to "株式会社スタジオジブリ (STUDIO GHIBLI INC.)". Will delete immediately if use of clip is in violation of copyright.
@@ -80,12 +82,20 @@ Clip is from trailer of animated movie "千と千尋の神隠し". Copyright bel
### Video2X GUI
![Video2X GUI Screenshot](https://user-images.githubusercontent.com/21986859/81157039-19346b00-8f76-11ea-965f-4d7edf6a818e.png)
![Video2X GUI Main Tab Screenshot](https://user-images.githubusercontent.com/21986859/81241806-dcf71e00-8ffa-11ea-88ed-50b062d9bb5e.png)
*Video2X GUI Main Tab Screenshot*
![Video2X GUI Driver Settings Screenshot](https://user-images.githubusercontent.com/21986859/81241837-f9935600-8ffa-11ea-8b89-5ae098b63de4.png)
*Video2X GUI Driver Settings Screenshot*
### Video2X CLI
![Video2X CLI Screenshot](https://user-images.githubusercontent.com/21986859/81039711-4fe88380-8e99-11ea-9846-175f72100a76.png)
*Video2X CLI Screenshot*
---
## Documentations
@@ -158,6 +168,8 @@ If you can't find a video clip to begin with, or if you want to see a before-aft
![sample_video](https://user-images.githubusercontent.com/21986859/52905766-d5512b00-3236-11e9-9aea-077636539679.png)
*Sample Upscale Videos*
- [Sample Video Original (240P) 1.7MB](https://files.k4yt3x.com/Resources/Videos/sample_input.mp4)
- [Sample Video Upscaled (1080P) 4.8MB](https://files.k4yt3x.com/Resources/Videos/sample_output.mp4)

View File

@@ -2,7 +2,7 @@
Name: Video2X Build Script
Creator: K4YT3X
Date Created: May 6, 2020
Last Modified: May 6, 2020
Last Modified: May 7, 2020
Description: A PowerShell script that will build Video2X
executable (PE) releases automatically using PyInstaller.
@@ -13,8 +13,8 @@ powershell ExecutionPolicy Bypass
#>
# version number
$SCRIPT_VERSION = "1.0.0"
$VIDEO2X_VERSION = "4.0.0"
$SCRIPT_VERSION = "1.0.1"
$VIDEO2X_VERSION = "4.0.0_beta2"
Write-Host -ForegroundColor White "Video2X Building Script Version $($SCRIPT_VERSION)
Starting to build Video2X release packages"
@@ -46,33 +46,33 @@ pyinstaller --noconfirm --log-level=WARN `
video2x_setup.py
# remove old builds if found
if (Test-Path "video2x-builds" -PathType any) {
Remove-Item -path "video2x-builds" -recurse
if (Test-Path "$($VIDEO2X_VERSION)" -PathType any) {
Remove-Item -path "$($VIDEO2X_VERSION)" -recurse
}
# create build directory
New-Item "video2x-builds" -ItemType Directory
New-Item "$($VIDEO2X_VERSION)" -ItemType Directory
# copy files into corresponding builds
# full edition
Write-Host -ForegroundColor White "`nCreating full package"
New-Item "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-full" -ItemType Directory
Copy-Item "dist\video2x.exe" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-full\"
Copy-Item "dist\video2x_gui.exe" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-full\"
Copy-Item -Path "$env:LOCALAPPDATA\video2x" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-full\dependencies" -Recurse
New-Item "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-full" -ItemType Directory
Copy-Item "dist\video2x.exe" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-full\"
Copy-Item "dist\video2x_gui.exe" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-full\"
Copy-Item -Path "$env:LOCALAPPDATA\video2x" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-full\dependencies" -Recurse
# overwrite paths to relative paths
(Get-Content "video2x.yaml").replace("C:\Users\K4YT3X\AppData\Local\video2x\", "dependencies\") | Set-Content "video2x.yaml.relative"
Move-Item "video2x.yaml.relative" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-full\video2x.yaml"
(Get-Content "video2x.yaml").replace("%LOCALAPPDATA%\video2x", "dependencies") | Set-Content "video2x.yaml.relative"
Move-Item "video2x.yaml.relative" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-full\video2x.yaml"
# light edition
Write-Host -ForegroundColor White "`nCreating light package"
New-Item "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-light" -ItemType Directory
Copy-Item "dist\video2x.exe" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "dist\video2x_gui.exe" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "dist\video2x_setup.exe" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "video2x.yaml" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "requirements.txt" -Destination "video2x-builds\video2x-$($VIDEO2X_VERSION)-win32-light\"
New-Item "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-light" -ItemType Directory
Copy-Item "dist\video2x.exe" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "dist\video2x_gui.exe" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "dist\video2x_setup.exe" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "video2x.yaml" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-light\"
Copy-Item "requirements.txt" -Destination "$($VIDEO2X_VERSION)\video2x-$($VIDEO2X_VERSION)-win32-light\"
# clean up temporary files
Write-Host -ForegroundColor White "`nDeleting temporary files"

View File

@@ -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 "放大该文件夹中的所有视频:{}"

61
src/progress_monitor.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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
@@ -58,6 +58,7 @@ import contextlib
import gettext
import importlib
import locale
import os
import pathlib
import re
import shutil
@@ -178,6 +179,16 @@ config = read_config(video2x_args.config)
# load waifu2x configuration
driver_settings = config[video2x_args.driver]
driver_settings['path'] = os.path.expandvars(driver_settings['path'])
# read FFmpeg configuration
ffmpeg_settings = config['ffmpeg']
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']
# overwrite driver_settings with driver_args
if driver_args is not None:
@@ -186,126 +197,32 @@ if driver_args is not None:
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']
# load video2x settings
image_format = config['video2x']['image_format'].lower()
preserve_frames = config['video2x']['preserve_frames']
# 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
# 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)

View File

@@ -1,53 +1,59 @@
# Name: Video2X Configuration File
# Creator: K4YT3X
# Date Created: October 23, 2018
# Last Modified: May 7, 2020
# Items commented out are parameters handled by Video2x.
waifu2x_caffe:
path: 'C:\Users\K4YT3X\AppData\Local\video2x\waifu2x-caffe\waifu2x-caffe-cui'
input_extention_list: null
output_extention: null
mode: noise_scale
scale_ratio: null
scale_width: null
scale_height: null
noise_level: 3
process: gpu
crop_size: 128
output_quality: -1
output_depth: 8
batch_size: 1
gpu: 0
tta: 0
input_path: null
output_path: null
model_dir: null
crop_w: null
crop_h: null
path: '%LOCALAPPDATA%\video2x\waifu2x-caffe\waifu2x-caffe-cui'
tta: 0 # <0|1> 8x slower and slightly high quality
gpu: 0 # gpu device no
batch_size: 1 # input batch size
crop_h: null # input image split size(height)
crop_w: null # input image split size(width)
crop_size: 128 # input image split size
output_depth: 8 # output image chaneel depth bit
output_quality: -1 # output image quality
process: gpu # <cpu|gpu|cudnn> process mode
model_dir: null # path to custom model directory (don't append last / )
#scale_height: null # custom scale height
#scale_width: null # custom scale width
#scale_ratio: null # custom scale ratio
noise_level: 3 # <0|1|2|3> noise reduction level
mode: noise_scale # <noise|scale|noise_scale|auto_scale> image processing mode
output_extention: null # extention to output image file when output_path is (auto) or input_path is folder
input_extention_list: null # extention to input image file when input_path is folder
#output_path: null # path to output image file (when input_path is folder, output_path must be folder)
#input_path: null # (required) path to input image file
waifu2x_converter_cpp:
path: 'C:\Users\K4YT3X\AppData\Local\video2x\waifu2x-converter-cpp\waifu2x-converter-cpp'
# list-supported-formats: null
# list-opencv-formats: null
# list-processor
output-format: progress_bar_exit_signal
png-compression: 5
image-quality: -1
block-size: 0
disable-gpu: false
force-OpenCL: false
processor: -1
jobs: 0
model-dir: null # models_rgb
scale-ratio: 2.0
noise-level: 1
mode: noise-scale
log-level: 1
silent: null
tta: 0
# generate-subdir: 0
# recursive-directory: 0
output: null
input: null
path: '%LOCALAPPDATA%\video2x\waifu2x-converter-cpp\waifu2x-converter-cpp'
#list-supported-formats: null # dump currently supported format list
#list-opencv-formats: null # (deprecated. Use --list-supported-formats) dump opencv supported format list
#list-processor # dump processor list
output-format: null # The format used when running in recursive/folder mode
png-compression: 5 # Set PNG compression level (0-9), 9 = Max compression (slowest & smallest)
image-quality: -1 # JPEG & WebP Compression quality (0-101, 0 being smallest size and lowest quality), use 101 for lossless WebP
block-size: 0 # block size
disable-gpu: false # disable GPU
force-OpenCL: false # force to use OpenCL on Intel Platform
processor: -1 # set target processor
jobs: 0 # number of threads launching at the same time
model-dir: null # path to custom model directory (don't append last / ) default: models_rgb
#scale-ratio: 2.0 # custom scale ratio
noise-level: 1 # <0|1|2|3> noise reduction level
mode: noise-scale # <noise|scale|noise-scale> image processing mode
log-level: 1 # <0|1|2|3|4> Set log level
silent: true # Enable silent mode. (same as --log-level 1)
tta: 0 # Enable Test-Time Augmentation mode. (0 or 1)
#generate-subdir: 0 # Generate sub folder when recursive directory is enabled.
#auto-naming: 0 # Add postfix to output name when output path is not specified.
#recursive-directory: 0 # Search recursively through directories to find more images to process.
#output: null # path to output image file or directory (you should use the full path)
#input: null # (required) path to input image file or directory (you should use the full path)
waifu2x_ncnn_vulkan:
path: 'C:\Users\K4YT3X\AppData\Local\video2x\waifu2x-ncnn-vulkan\waifu2x-ncnn-vulkan'
path: '%LOCALAPPDATA%\video2x\waifu2x-ncnn-vulkan\waifu2x-ncnn-vulkan'
v: null # verbose output
i: null # input-path: input image path (jpg/png) or directory
o: null # output-path: output image path (png) or directory
#i: null # input-path: input image path (jpg/png) or directory
#o: null # output-path: output image path (png) or directory
'n': 2 # noise-level: denoise level (-1/0/1/2/3, default=0)
s: 2 # scale: upscale ratio (1/2, default=2)
t: 400 # tile-size: tile size (>=32, default=400)
@@ -56,10 +62,10 @@ waifu2x_ncnn_vulkan:
j: '1:2:2' # thread count for load/proc/save (default=1:2:2)
x: false # enable tta mode
srmd_ncnn_vulkan:
path: 'C:\Users\K4YT3X\AppData\Local\video2x\srmd-ncnn-vulkan\srmd-ncnn-vulkan'
path: '%LOCALAPPDATA%\video2x\srmd-ncnn-vulkan\srmd-ncnn-vulkan'
v: null # verbose output
i: null # input-path: input image path (jpg/png) or directory
o: null # output-path: output image path (png) or directory
#i: null # input-path: input image path (jpg/png) or directory
#o: null # output-path: output image path (png) or directory
'n': 3 # noise-level: denoise level (-1/0/1/2/3/4/5/6/7/8/9/10, default=3)
s: 2 # upscale ratio (2/3/4, default=2)
t: 400 # tile-size: tile size (>=32, default=400)
@@ -68,9 +74,9 @@ srmd_ncnn_vulkan:
j: '1:2:2' # thread count for load/proc/save (default=1:2:2)
x: false # enable tta mode
anime4kcpp:
path: 'C:\Users\K4YT3X\AppData\Local\video2x\anime4kcpp\CLI\Anime4KCPP_CLI\Anime4KCPP_CLI'
input: null # File for loading (string [=./pic/p1.png])
output: null # File for outputting (string [=output.png])
path: '%LOCALAPPDATA%\video2x\anime4kcpp\CLI\Anime4KCPP_CLI\Anime4KCPP_CLI'
#input: null # File for loading (string [=./pic/p1.png])
#output: null # File for outputting (string [=output.png])
passes: 2 # Passes for processing (int [=2])
pushColorCount: 2 # Limit the number of color pushes (int [=2])
strengthColor: 0.3 # Strength for pushing color,range 0 to 1,higher for thinner (double [=0.3])
@@ -80,16 +86,17 @@ anime4kcpp:
fastMode: false # Faster but maybe low quality
videoMode: true # Video process
preview: null # Preview image
preProcessing: False # Enable pre processing
postProcessing: False # Enable post processing
preprocessing: False # Enable pre processing
postprocessing: False # Enable post processing
preFilters: 4 # Enhancement filter, only working when preProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D) (unsigned int [=4])
postFilters: 40 # Enhancement filter, only working when postProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D), so you can put 40 to enable Gaussian blur weak and Bilateral filter, which also is what I recommend for image that < 1080P, 48 for image that >= 1080P, and for performance I recommend to use 72 for video that < 1080P, 80 for video that >=1080P (unsigned int [=40])
GPUMode: False # Enable GPU acceleration
listGPUs: null # list GPUs
platformID: 0 # Specify the platform ID (unsigned int [=0])
deviceID: 0 # Specify the device ID (unsigned int [=0])
codec: mp4v # Specify the codec for encoding from mp4v(recommended in Windows), dxva(for Windows), avc1(H264, recommended in Linux), vp09(very slow), hevc(not support in Windowds), av01(not support in Windowds) (string [=mp4v])
ffmpeg:
ffmpeg_path: 'C:\Users\K4YT3X\AppData\Local\video2x\ffmpeg-latest-win64-static\bin'
ffmpeg_path: '%LOCALAPPDATA%\video2x\ffmpeg-latest-win64-static\bin'
video_to_frames:
output_options:
'-qscale:v': null

View File

@@ -12,9 +12,9 @@ from upscaler import Upscaler
# built-in imports
import contextlib
import os
import pathlib
import re
import shutil
import sys
import tempfile
import time
@@ -45,7 +45,7 @@ AVAILABLE_DRIVERS = {
def resource_path(relative_path: str) -> pathlib.Path:
try:
base_path = pathlib.Path(sys._MEIPASS)
except Exception:
except AttributeError:
base_path = pathlib.Path(__file__).parent
return base_path / relative_path
@@ -53,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):
@@ -88,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):
@@ -140,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')
@@ -217,6 +220,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.anime4kcpp_post_filters_spin_box = self.findChild(QtWidgets.QSpinBox, 'anime4kCppPostFiltersSpinBox')
self.anime4kcpp_platform_id_spin_box = self.findChild(QtWidgets.QSpinBox, 'anime4kCppPlatformIdSpinBox')
self.anime4kcpp_device_id_spin_box = self.findChild(QtWidgets.QSpinBox, 'anime4kCppDeviceIdSpinBox')
self.anime4kcpp_codec_combo_box = self.findChild(QtWidgets.QComboBox, 'anime4kCppCodecComboBox')
self.anime4kcpp_fast_mode_check_box = self.findChild(QtWidgets.QCheckBox, 'anime4kCppFastModeCheckBox')
self.anime4kcpp_pre_processing_check_box = self.findChild(QtWidgets.QCheckBox, 'anime4kCppPreProcessingCheckBox')
self.anime4kcpp_post_processing_check_box = self.findChild(QtWidgets.QCheckBox, 'anime4kCppPostProcessingCheckBox')
@@ -242,7 +246,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
def load_configurations(self):
# get config file path from line edit
config_file_path = pathlib.Path(self.config_line_edit.text())
config_file_path = pathlib.Path(os.path.expandvars(self.config_line_edit.text()))
# if file doesn't exist, return
if not config_file_path.is_file():
@@ -254,26 +258,12 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
# load FFmpeg settings
self.ffmpeg_settings = self.config['ffmpeg']
self.ffmpeg_settings['ffmpeg_path'] = str(pathlib.Path(os.path.expandvars(self.ffmpeg_settings['ffmpeg_path'])).absolute())
# 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'])
@@ -281,7 +271,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
# waifu2x-caffe
settings = self.config['waifu2x_caffe']
self.waifu2x_caffe_path_line_edit.setText(str(pathlib.Path(settings['path']).absolute()))
self.waifu2x_caffe_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute()))
self.waifu2x_caffe_mode_combo_box.setCurrentText(settings['mode'])
self.waifu2x_caffe_noise_level_spin_box.setValue(settings['noise_level'])
self.waifu2x_caffe_process_combo_box.setCurrentText(settings['process'])
@@ -294,7 +284,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
# waifu2x-converter-cpp
settings = self.config['waifu2x_converter_cpp']
self.waifu2x_converter_cpp_path_line_edit.setText(str(pathlib.Path(settings['path']).absolute()))
self.waifu2x_converter_cpp_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute()))
self.waifu2x_converter_cpp_png_compression_spin_box.setValue(settings['png-compression'])
self.waifu2x_converter_cpp_processor_spin_box.setValue(settings['processor'])
self.waifu2x_converter_cpp_mode_combo_box.setCurrentText(settings['mode'])
@@ -303,7 +293,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
# waifu2x-ncnn-vulkan
settings = self.config['waifu2x_ncnn_vulkan']
self.waifu2x_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(settings['path']).absolute()))
self.waifu2x_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute()))
self.waifu2x_ncnn_vulkan_noise_level_spin_box.setValue(settings['n'])
self.waifu2x_ncnn_vulkan_tile_size_spin_box.setValue(settings['t'])
self.waifu2x_ncnn_vulkan_gpu_id_spin_box.setValue(settings['g'])
@@ -312,7 +302,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
# srmd-ncnn-vulkan
settings = self.config['srmd_ncnn_vulkan']
self.srmd_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(settings['path']).absolute()))
self.srmd_ncnn_vulkan_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute()))
self.srmd_ncnn_vulkan_noise_level_spin_box.setValue(settings['n'])
self.srmd_ncnn_vulkan_tile_size_spin_box.setValue(settings['t'])
self.srmd_ncnn_vulkan_gpu_id_spin_box.setValue(settings['g'])
@@ -321,7 +311,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
# anime4k
settings = self.config['anime4kcpp']
self.anime4kcpp_path_line_edit.setText(str(pathlib.Path(settings['path']).absolute()))
self.anime4kcpp_path_line_edit.setText(str(pathlib.Path(os.path.expandvars(settings['path'])).absolute()))
self.anime4kcpp_passes_spin_box.setValue(settings['passes'])
self.anime4kcpp_push_color_count_spin_box.setValue(settings['pushColorCount'])
self.anime4kcpp_strength_color_spin_box.setValue(settings['strengthColor'])
@@ -331,15 +321,16 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.anime4kcpp_post_filters_spin_box.setValue(settings['postFilters'])
self.anime4kcpp_platform_id_spin_box.setValue(settings['platformID'])
self.anime4kcpp_device_id_spin_box.setValue(settings['deviceID'])
self.anime4kcpp_codec_combo_box.setCurrentText(settings['codec'])
self.anime4kcpp_fast_mode_check_box.setChecked(settings['fastMode'])
self.anime4kcpp_pre_processing_check_box.setChecked(settings['preProcessing'])
self.anime4kcpp_post_processing_check_box.setChecked(settings['postProcessing'])
self.anime4kcpp_pre_processing_check_box.setChecked(settings['preprocessing'])
self.anime4kcpp_post_processing_check_box.setChecked(settings['postprocessing'])
self.anime4kcpp_gpu_mode_check_box.setChecked(settings['GPUMode'])
def resolve_driver_settings(self):
# waifu2x-caffe
self.config['waifu2x_caffe']['path'] = self.waifu2x_caffe_path_line_edit.text()
self.config['waifu2x_caffe']['path'] = os.path.expandvars(self.waifu2x_caffe_path_line_edit.text())
self.config['waifu2x_caffe']['mode'] = self.waifu2x_caffe_mode_combo_box.currentText()
self.config['waifu2x_caffe']['noise_level'] = self.waifu2x_caffe_noise_level_spin_box.value()
self.config['waifu2x_caffe']['process'] = self.waifu2x_caffe_process_combo_box.currentText()
@@ -352,7 +343,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.config['waifu2x_caffe']['tta'] = int(self.waifu2x_caffe_tta_check_box.checkState())
# waifu2x-converter-cpp
self.config['waifu2x_converter_cpp']['path'] = self.waifu2x_converter_cpp_path_line_edit.text()
self.config['waifu2x_converter_cpp']['path'] = os.path.expandvars(self.waifu2x_converter_cpp_path_line_edit.text())
self.config['waifu2x_converter_cpp']['png-compression'] = self.waifu2x_converter_cpp_png_compression_spin_box.value()
self.config['waifu2x_converter_cpp']['processor'] = self.waifu2x_converter_cpp_processor_spin_box.value()
self.config['waifu2x_converter_cpp']['model-dir'] = str((pathlib.Path(self.config['waifu2x_converter_cpp']['path']).parent / self.waifu2x_converter_cpp_model_combo_box.currentText()).absolute())
@@ -361,7 +352,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.config['waifu2x_converter_cpp']['tta'] = int(self.waifu2x_converter_cpp_tta_check_box.checkState())
# waifu2x-ncnn-vulkan
self.config['waifu2x_ncnn_vulkan']['path'] = self.waifu2x_ncnn_vulkan_path_line_edit.text()
self.config['waifu2x_ncnn_vulkan']['path'] = os.path.expandvars(self.waifu2x_ncnn_vulkan_path_line_edit.text())
self.config['waifu2x_ncnn_vulkan']['n'] = self.waifu2x_ncnn_vulkan_noise_level_spin_box.value()
self.config['waifu2x_ncnn_vulkan']['t'] = self.waifu2x_ncnn_vulkan_tile_size_spin_box.value()
self.config['waifu2x_ncnn_vulkan']['m'] = str((pathlib.Path(self.config['waifu2x_ncnn_vulkan']['path']).parent / self.waifu2x_ncnn_vulkan_model_combo_box.currentText()).absolute())
@@ -370,7 +361,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.config['waifu2x_ncnn_vulkan']['x'] = self.waifu2x_ncnn_vulkan_tta_check_box.checkState()
# srmd-ncnn-vulkan
self.config['srmd_ncnn_vulkan']['path'] = self.srmd_ncnn_vulkan_path_line_edit.text()
self.config['srmd_ncnn_vulkan']['path'] = os.path.expandvars(self.srmd_ncnn_vulkan_path_line_edit.text())
self.config['srmd_ncnn_vulkan']['n'] = self.srmd_ncnn_vulkan_noise_level_spin_box.value()
self.config['srmd_ncnn_vulkan']['t'] = self.srmd_ncnn_vulkan_tile_size_spin_box.value()
self.config['srmd_ncnn_vulkan']['m'] = str((pathlib.Path(self.config['srmd_ncnn_vulkan']['path']).parent / self.srmd_ncnn_vulkan_model_combo_box.currentText()).absolute())
@@ -379,7 +370,7 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.config['srmd_ncnn_vulkan']['x'] = self.srmd_ncnn_vulkan_tta_check_box.checkState()
# anime4k
self.config['anime4kcpp']['path'] = self.anime4kcpp_path_line_edit.text()
self.config['anime4kcpp']['path'] = os.path.expandvars(self.anime4kcpp_path_line_edit.text())
self.config['anime4kcpp']['passes'] = self.anime4kcpp_passes_spin_box.value()
self.config['anime4kcpp']['pushColorCount'] = self.anime4kcpp_push_color_count_spin_box.value()
self.config['anime4kcpp']['strengthColor'] = self.anime4kcpp_strength_color_spin_box.value()
@@ -389,18 +380,17 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.config['anime4kcpp']['postFilters'] = self.anime4kcpp_post_filters_spin_box.value()
self.config['anime4kcpp']['platformID'] = self.anime4kcpp_platform_id_spin_box.value()
self.config['anime4kcpp']['deviceID'] = self.anime4kcpp_device_id_spin_box.value()
self.config['anime4kcpp']['codec'] = self.anime4kcpp_codec_combo_box.currentText()
self.config['anime4kcpp']['fastMode'] = bool(self.anime4kcpp_fast_mode_check_box.checkState())
self.config['anime4kcpp']['preProcessing'] = bool(self.anime4kcpp_pre_processing_check_box.checkState())
self.config['anime4kcpp']['postProcessing'] = bool(self.anime4kcpp_post_processing_check_box.checkState())
self.config['anime4kcpp']['preprocessing'] = bool(self.anime4kcpp_pre_processing_check_box.checkState())
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()]
if current_driver == 'waifu2x_caffe':
self.scale_ratio_double_spin_box.setMinimum(0.0)
self.scale_ratio_double_spin_box.setMaximum(999.0)
self.scale_ratio_double_spin_box.setValue(2.0)
elif current_driver == 'waifu2x_converter_cpp':
# 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)
self.scale_ratio_double_spin_box.setValue(2.0)
@@ -412,18 +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)
elif current_driver == 'anime4kcpp':
self.scale_ratio_double_spin_box.setMinimum(0)
self.scale_ratio_double_spin_box.setMaximum(999.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
@@ -438,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()))
@@ -458,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', '<br>'))
@@ -507,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))
@@ -565,8 +559,8 @@ class Video2XMainWindow(QtWidgets.QMainWindow):
self.show_error('Output path not specified')
return
input_directory = pathlib.Path(self.input_line_edit.text())
output_directory = pathlib.Path(self.output_line_edit.text())
input_directory = pathlib.Path(os.path.expandvars(self.input_line_edit.text()))
output_directory = pathlib.Path(os.path.expandvars(self.output_line_edit.text()))
# create thread pool for upscaler workers
self.threadpool = QThreadPool()
@@ -577,111 +571,65 @@ 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(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(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(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
app = QtWidgets.QApplication(sys.argv)
window = Video2XMainWindow()
window.show()
app.exec_()
def closeEvent(self, event):
# try cleaning up temp directories
self.stop()
event.accept()
# this file shouldn't be imported
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Video2XMainWindow()
window.show()
app.exec_()

View File

@@ -1175,6 +1175,51 @@
</item>
</layout>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="anime4kCppCodecHorizontalLayout">
<item>
<widget class="QLabel" name="anime4kCppCodecLabel">
<property name="text">
<string>Codec</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="anime4kCppCodecComboBox">
<item>
<property name="text">
<string>mp4v</string>
</property>
</item>
<item>
<property name="text">
<string>dxva</string>
</property>
</item>
<item>
<property name="text">
<string>avc1</string>
</property>
</item>
<item>
<property name="text">
<string>vp09</string>
</property>
</item>
<item>
<property name="text">
<string>hevc</string>
</property>
</item>
<item>
<property name="text">
<string>av01</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>

View File

@@ -4,7 +4,7 @@
Name: Video2X Setup Script
Creator: K4YT3X
Date Created: November 28, 2018
Last Modified: May 5, 2020
Last Modified: May 7, 2020
Editor: BrianPetkovsek
Editor: SAT3LL
@@ -260,16 +260,19 @@ class Video2xSetup:
# configure only the specified drivers
if self.driver == 'all':
template_dict['waifu2x_caffe']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-caffe' / 'waifu2x-caffe-cui.exe')
template_dict['waifu2x_caffe']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-caffe' / 'waifu2x-caffe-cui')
template_dict['waifu2x_converter_cpp']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-converter-cpp')
template_dict['waifu2x_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan' / 'waifu2x-ncnn-vulkan.exe')
template_dict['waifu2x_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan' / 'waifu2x-ncnn-vulkan')
template_dict['srmd_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'srmd-ncnn-vulkan' / 'srmd-ncnn-vulkan')
template_dict['anime4kcpp']['path'] = str(LOCALAPPDATA / 'video2x' / 'anime4kcpp' / 'CLI' / 'Anime4KCPP_CLI' / 'Anime4KCPP_CLI')
elif self.driver == 'waifu2x_caffe':
template_dict['waifu2x_caffe']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-caffe' / 'waifu2x-caffe-cui.exe')
template_dict['waifu2x_caffe']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-caffe' / 'waifu2x-caffe-cui')
elif self.driver == 'waifu2x_converter_cpp':
template_dict['waifu2x_converter_cpp']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-converter-cpp' / 'waifu2x-converter-cpp')
elif self.driver == 'waifu2x_ncnn_vulkan':
template_dict['waifu2x_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan' / 'waifu2x-ncnn-vulkan.exe')
template_dict['waifu2x_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan' / 'waifu2x-ncnn-vulkan')
elif self.driver == 'srmd_ncnn_vulkan':
template_dict['srmd_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'srmd-ncnn-vulkan' / 'srmd-ncnn-vulkan')
elif self.driver == 'anime4kcpp':
template_dict['anime4kcpp']['path'] = str(LOCALAPPDATA / 'video2x' / 'anime4kcpp' / 'CLI' / 'Anime4KCPP_CLI' / 'Anime4KCPP_CLI')

View File

@@ -4,7 +4,7 @@
Name: Waifu2x Caffe Driver
Author: K4YT3X
Date Created: May 3, 2020
Last Modified: May 4, 2020
Last Modified: May 7, 2020
Description: This class is a high-level wrapper
for waifu2x-caffe.
@@ -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')
@@ -44,14 +47,15 @@ class WrapperMain:
parser.add_argument('-f', '--fastMode', action='store_true', help='Faster but maybe low quality')
# parser.add_argument('-v', '--videoMode', action='store_true', help='Video process')
parser.add_argument('-s', '--preview', action='store_true', help='Preview image')
parser.add_argument('-b', '--preProcessing', action='store_true', help='Enable pre processing')
parser.add_argument('-a', '--postProcessing', action='store_true', help='Enable post processing')
parser.add_argument('-b', '--preprocessing', action='store_true', help='Enable pre processing')
parser.add_argument('-a', '--postprocessing', action='store_true', help='Enable post processing')
parser.add_argument('-r', '--preFilters', type=int, help='Enhancement filter, only working when preProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D)')
parser.add_argument('-e', '--postFilters', type=int, help='Enhancement filter, only working when postProcessing is true,there are 5 options by binary:Median blur=0000001, Mean blur=0000010, CAS Sharpening=0000100, Gaussian blur weak=0001000, Gaussian blur=0010000, Bilateral filter=0100000, Bilateral filter faster=1000000, you can freely combine them, eg: Gaussian blur weak + Bilateral filter = 0001000 | 0100000 = 0101000 = 40(D), so you can put 40 to enable Gaussian blur weak and Bilateral filter, which also is what I recommend for image that < 1080P, 48 for image that >= 1080P, and for performance I recommend to use 72 for video that < 1080P, 80 for video that >=1080P')
parser.add_argument('-q', '--GPUMode', action='store_true', help='Enable GPU acceleration')
parser.add_argument('-l', '--listGPUs', action='store_true', help='list GPUs')
parser.add_argument('-h', '--platformID', type=int, help='Specify the platform ID')
parser.add_argument('-d', '--deviceID', type=int, help='Specify the device ID')
parser.add_argument('-C', '--codec', type=str, help='Specify the codec for encoding from mp4v(recommended in Windows), dxva(for Windows), avc1(H264, recommended in Linux), vp09(very slow), hevc(not support in Windowds), av01(not support in Windowds) (string [=mp4v])')
return parser.parse_args(arguments)
def upscale(self, input_file, output_file, zoom_factor, threads):
@@ -69,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

View File

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

View File

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

View File

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

View File

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

View File

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