2018-12-11 20:52:48 +00:00
|
|
|
#!/usr/bin/env python3
|
2019-07-27 17:39:40 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2018-12-11 20:52:48 +00:00
|
|
|
"""
|
|
|
|
Name: Video2X Upscaler
|
|
|
|
Author: K4YT3X
|
|
|
|
Date Created: December 10, 2018
|
2020-05-22 20:29:51 +00:00
|
|
|
Last Modified: May 22, 2020
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-03-23 13:05:24 +00:00
|
|
|
Description: This file contains the Upscaler class. Each
|
|
|
|
instance of the Upscaler class is an upscaler on an image or
|
|
|
|
a folder.
|
2018-12-11 20:52:48 +00:00
|
|
|
"""
|
|
|
|
|
2019-07-27 17:39:40 +00:00
|
|
|
# local imports
|
2018-12-21 23:42:03 +00:00
|
|
|
from exceptions import *
|
2019-03-26 20:21:58 +00:00
|
|
|
from image_cleaner import ImageCleaner
|
2020-05-07 19:58:22 +00:00
|
|
|
from progress_monitor import ProgressMonitor
|
2020-05-03 23:20:23 +00:00
|
|
|
from wrappers.ffmpeg import Ffmpeg
|
2020-05-12 00:24:18 +00:00
|
|
|
from wrappers.gifski import Gifski
|
2019-07-27 17:39:40 +00:00
|
|
|
|
|
|
|
# built-in imports
|
|
|
|
from fractions import Fraction
|
2019-08-26 03:03:37 +00:00
|
|
|
import contextlib
|
2019-06-14 03:49:18 +00:00
|
|
|
import copy
|
2020-05-06 08:45:48 +00:00
|
|
|
import gettext
|
2020-05-04 21:12:41 +00:00
|
|
|
import importlib
|
2020-05-06 08:45:48 +00:00
|
|
|
import locale
|
2020-05-22 20:29:51 +00:00
|
|
|
import mimetypes
|
2019-07-27 21:29:33 +00:00
|
|
|
import pathlib
|
2020-05-07 19:58:22 +00:00
|
|
|
import queue
|
2019-02-08 21:48:35 +00:00
|
|
|
import re
|
2018-12-11 20:52:48 +00:00
|
|
|
import shutil
|
2020-05-07 19:58:22 +00:00
|
|
|
import subprocess
|
2018-12-11 20:52:48 +00:00
|
|
|
import tempfile
|
2019-03-05 00:34:57 +00:00
|
|
|
import time
|
2019-07-27 21:29:33 +00:00
|
|
|
import traceback
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2019-07-27 17:39:40 +00:00
|
|
|
# third-party imports
|
|
|
|
from avalon_framework import Avalon
|
2020-05-12 00:24:18 +00:00
|
|
|
import magic
|
2019-07-27 17:39:40 +00:00
|
|
|
|
2020-05-06 08:45:48 +00:00
|
|
|
# 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
|
|
|
|
|
2020-05-16 01:28:22 +00:00
|
|
|
# version information
|
2020-05-16 11:12:25 +00:00
|
|
|
UPSCALER_VERSION = '4.1.0'
|
2020-05-16 01:28:22 +00:00
|
|
|
|
2020-05-04 21:12:41 +00:00
|
|
|
# these names are consistent for
|
|
|
|
# - driver selection in command line
|
|
|
|
# - driver wrapper file names
|
|
|
|
# - config file keys
|
|
|
|
AVAILABLE_DRIVERS = ['waifu2x_caffe',
|
|
|
|
'waifu2x_converter_cpp',
|
|
|
|
'waifu2x_ncnn_vulkan',
|
|
|
|
'srmd_ncnn_vulkan',
|
|
|
|
'anime4kcpp']
|
2019-08-16 03:57:24 +00:00
|
|
|
|
2018-12-11 20:52:48 +00:00
|
|
|
|
|
|
|
class Upscaler:
|
2019-03-19 17:10:38 +00:00
|
|
|
""" An instance of this class is a upscaler that will
|
2019-04-29 04:06:54 +00:00
|
|
|
upscale all images in the given directory.
|
2019-03-25 01:57:22 +00:00
|
|
|
|
2019-03-19 17:10:38 +00:00
|
|
|
Raises:
|
|
|
|
Exception -- all exceptions
|
|
|
|
ArgumentError -- if argument is not valid
|
|
|
|
"""
|
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
def __init__(self, input_path, output_path, driver_settings, ffmpeg_settings, gifski_settings):
|
2019-03-19 17:10:38 +00:00
|
|
|
# mandatory arguments
|
2020-05-08 21:37:16 +00:00
|
|
|
self.input = input_path
|
|
|
|
self.output = output_path
|
2019-10-20 01:52:11 +00:00
|
|
|
self.driver_settings = driver_settings
|
2019-03-09 17:50:54 +00:00
|
|
|
self.ffmpeg_settings = ffmpeg_settings
|
2020-05-12 00:24:18 +00:00
|
|
|
self.gifski_settings = gifski_settings
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2019-03-19 17:10:38 +00:00
|
|
|
# optional arguments
|
2020-05-03 23:20:23 +00:00
|
|
|
self.driver = 'waifu2x_caffe'
|
2019-03-30 18:01:36 +00:00
|
|
|
self.scale_ratio = None
|
2020-02-26 10:27:57 +00:00
|
|
|
self.processes = 1
|
2019-07-27 21:29:33 +00:00
|
|
|
self.video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x'
|
2019-03-30 18:01:36 +00:00
|
|
|
self.image_format = 'png'
|
|
|
|
self.preserve_frames = False
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# other internal members and signals
|
2020-05-09 00:28:46 +00:00
|
|
|
self.running = False
|
2020-05-07 19:58:22 +00:00
|
|
|
self.total_frames_upscaled = 0
|
|
|
|
self.total_frames = 0
|
2020-05-12 00:24:18 +00:00
|
|
|
self.total_files = 0
|
2020-05-09 00:28:46 +00:00
|
|
|
self.total_processed = 0
|
2020-05-12 00:24:18 +00:00
|
|
|
self.current_input_file = pathlib.Path()
|
2020-05-11 08:17:21 +00:00
|
|
|
self.last_frame_upscaled = pathlib.Path()
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2019-04-29 04:06:54 +00:00
|
|
|
def create_temp_directories(self):
|
2020-05-07 19:58:22 +00:00
|
|
|
"""create temporary directories
|
2019-04-18 18:56:30 +00:00
|
|
|
"""
|
2020-04-04 10:24:40 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# if cache directory unspecified, use %TEMP%\video2x
|
|
|
|
if self.video2x_cache_directory is None:
|
2020-04-04 10:24:40 +00:00
|
|
|
self.video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x'
|
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# 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')
|
2020-05-07 23:55:33 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# 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
|
|
|
|
|
2020-04-04 10:24:40 +00:00
|
|
|
# create temp directories for extracted frames and upscaled frames
|
2019-07-27 21:29:33 +00:00
|
|
|
self.extracted_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory))
|
2020-05-05 00:25:12 +00:00
|
|
|
Avalon.debug_info(_('Extracted frames are being saved to: {}').format(self.extracted_frames))
|
2019-07-27 21:29:33 +00:00
|
|
|
self.upscaled_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory))
|
2020-05-05 00:25:12 +00:00
|
|
|
Avalon.debug_info(_('Upscaled frames are being saved to: {}').format(self.upscaled_frames))
|
2019-02-08 19:04:43 +00:00
|
|
|
|
2019-04-29 04:06:54 +00:00
|
|
|
def cleanup_temp_directories(self):
|
2019-04-18 18:56:30 +00:00
|
|
|
"""delete temp directories when done
|
|
|
|
"""
|
2019-02-08 19:04:43 +00:00
|
|
|
if not self.preserve_frames:
|
2019-12-12 03:20:01 +00:00
|
|
|
for directory in [self.extracted_frames, self.upscaled_frames, self.video2x_cache_directory]:
|
2019-03-25 03:04:50 +00:00
|
|
|
try:
|
2019-04-18 18:56:30 +00:00
|
|
|
# avalon framework cannot be used if python is shutting down
|
|
|
|
# therefore, plain print is used
|
2020-05-05 00:25:12 +00:00
|
|
|
print(_('Cleaning up cache directory: {}').format(directory))
|
2019-03-25 03:04:50 +00:00
|
|
|
shutil.rmtree(directory)
|
2020-05-17 19:50:05 +00:00
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
|
|
|
except OSError:
|
2020-05-05 00:25:12 +00:00
|
|
|
print(_('Unable to delete: {}').format(directory))
|
2019-07-27 21:29:33 +00:00
|
|
|
traceback.print_exc()
|
2018-12-11 20:52:48 +00:00
|
|
|
|
|
|
|
def _check_arguments(self):
|
2020-05-08 21:37:16 +00:00
|
|
|
if isinstance(self.input, list):
|
|
|
|
if self.output.exists() and not self.output.is_dir():
|
|
|
|
Avalon.error(_('Input and output path type mismatch'))
|
|
|
|
Avalon.error(_('Input is multiple files but output is not directory'))
|
|
|
|
raise ArgumentError('input output path type mismatch')
|
|
|
|
for input_path in self.input:
|
|
|
|
if not input_path.is_file() and not input_path.is_dir():
|
2020-05-09 02:12:24 +00:00
|
|
|
Avalon.error(_('Input path {} is neither a file nor a directory').format(input_path))
|
2020-05-08 21:37:16 +00:00
|
|
|
raise FileNotFoundError(f'{input_path} is neither file nor directory')
|
2020-05-09 02:12:24 +00:00
|
|
|
with contextlib.suppress(FileNotFoundError):
|
|
|
|
if input_path.samefile(self.output):
|
|
|
|
Avalon.error(_('Input directory and output directory cannot be the same'))
|
|
|
|
raise FileExistsError('input directory and output directory are the same')
|
2020-05-08 21:37:16 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# if input is a file
|
2020-05-08 21:37:16 +00:00
|
|
|
elif self.input.is_file():
|
|
|
|
if self.output.is_dir():
|
2020-05-07 19:58:22 +00:00
|
|
|
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')
|
2020-05-12 00:24:18 +00:00
|
|
|
if self.output.suffix == '':
|
2020-05-07 19:58:22 +00:00
|
|
|
Avalon.error(_('No suffix found in output file path'))
|
2020-05-12 00:24:18 +00:00
|
|
|
Avalon.error(_('Suffix must be specified'))
|
|
|
|
raise ArgumentError('no output file suffix specified')
|
2020-05-07 19:58:22 +00:00
|
|
|
|
|
|
|
# if input is a directory
|
2020-05-08 21:37:16 +00:00
|
|
|
elif self.input.is_dir():
|
|
|
|
if self.output.is_file():
|
2020-05-07 19:58:22 +00:00
|
|
|
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')
|
2020-05-09 02:12:24 +00:00
|
|
|
with contextlib.suppress(FileNotFoundError):
|
|
|
|
if self.input.samefile(self.output):
|
|
|
|
Avalon.error(_('Input directory and output directory cannot be the same'))
|
|
|
|
raise FileExistsError('input directory and output directory are the same')
|
2020-05-07 19:58:22 +00:00
|
|
|
|
|
|
|
# if input is neither
|
|
|
|
else:
|
|
|
|
Avalon.error(_('Input path is neither a file nor a directory'))
|
2020-05-08 21:37:16 +00:00
|
|
|
raise FileNotFoundError(f'{self.input} is neither file nor directory')
|
2020-05-07 23:55:33 +00:00
|
|
|
|
2020-05-11 08:17:21 +00:00
|
|
|
# check FFmpeg settings
|
2020-05-07 19:58:22 +00:00
|
|
|
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'])
|
|
|
|
|
|
|
|
# check if driver settings
|
|
|
|
driver_settings = copy.deepcopy(self.driver_settings)
|
|
|
|
driver_path = driver_settings.pop('path')
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# 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():
|
2019-03-05 00:34:57 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
value = driver_settings[key]
|
2019-03-05 00:34:57 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
if value is None or value is False:
|
|
|
|
continue
|
2019-03-05 00:34:57 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
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))
|
2019-03-05 00:34:57 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
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
|
2019-03-05 00:34:57 +00:00
|
|
|
|
2020-05-11 08:17:21 +00:00
|
|
|
# waifu2x-caffe scale_ratio, scale_width and scale_height check
|
2020-05-11 08:33:38 +00:00
|
|
|
if self.driver == 'waifu2x_caffe':
|
|
|
|
if (driver_settings['scale_width'] != 0 and driver_settings['scale_height'] == 0 or
|
|
|
|
driver_settings['scale_width'] == 0 and driver_settings['scale_height'] != 0):
|
2020-05-11 08:17:21 +00:00
|
|
|
Avalon.error('Only one of scale_width and scale_height is specified for waifu2x-caffe')
|
|
|
|
raise AttributeError('only one of scale_width and scale_height is specified for waifu2x-caffe')
|
|
|
|
|
|
|
|
# if scale_width and scale_height are specified, ensure scale_ratio is None
|
|
|
|
elif self.driver_settings['scale_width'] != 0 and self.driver_settings['scale_height'] != 0:
|
|
|
|
self.driver_settings['scale_ratio'] = None
|
|
|
|
|
|
|
|
# if scale_width and scale_height not specified
|
|
|
|
# ensure they are None, not 0
|
|
|
|
else:
|
|
|
|
self.driver_settings['scale_width'] = None
|
|
|
|
self.driver_settings['scale_height'] = None
|
|
|
|
|
2019-06-14 03:49:18 +00:00
|
|
|
def _upscale_frames(self):
|
2018-12-11 20:52:48 +00:00
|
|
|
""" Upscale video frames with waifu2x-caffe
|
|
|
|
|
|
|
|
This function upscales all the frames extracted
|
|
|
|
by ffmpeg using the waifu2x-caffe binary.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
w2 {Waifu2x Object} -- initialized waifu2x object
|
|
|
|
"""
|
|
|
|
|
2019-06-14 23:19:12 +00:00
|
|
|
# initialize waifu2x driver
|
2020-05-03 23:20:23 +00:00
|
|
|
if self.driver not in AVAILABLE_DRIVERS:
|
2020-05-05 00:25:12 +00:00
|
|
|
raise UnrecognizedDriverError(_('Unrecognized driver: {}').format(self.driver))
|
2019-03-25 01:57:22 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
# list all images in the extracted frames
|
|
|
|
frames = [(self.extracted_frames / f) for f in self.extracted_frames.iterdir() if f.is_file]
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
# if we have less images than processes,
|
|
|
|
# create only the processes necessary
|
|
|
|
if len(frames) < self.processes:
|
|
|
|
self.processes = len(frames)
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
# create a directory for each process and append directory
|
|
|
|
# name into a list
|
|
|
|
process_directories = []
|
|
|
|
for process_id in range(self.processes):
|
|
|
|
process_directory = self.extracted_frames / str(process_id)
|
|
|
|
process_directories.append(process_directory)
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
# delete old directories and create new directories
|
|
|
|
if process_directory.is_dir():
|
|
|
|
shutil.rmtree(process_directory)
|
|
|
|
process_directory.mkdir(parents=True, exist_ok=True)
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
# waifu2x-converter-cpp will perform multi-threading within its own process
|
2020-05-09 08:54:28 +00:00
|
|
|
if self.driver in ['waifu2x_converter_cpp', 'waifu2x_ncnn_vulkan', 'srmd_ncnn_vulkan']:
|
2020-02-26 10:27:57 +00:00
|
|
|
process_directories = [self.extracted_frames]
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
else:
|
2019-06-25 23:25:27 +00:00
|
|
|
# evenly distribute images into each directory
|
|
|
|
# until there is none left in the directory
|
|
|
|
for image in frames:
|
|
|
|
# move image
|
2020-02-26 10:27:57 +00:00
|
|
|
image.rename(process_directories[0] / image.name)
|
2019-06-25 23:25:27 +00:00
|
|
|
# rotate list
|
2020-02-26 10:27:57 +00:00
|
|
|
process_directories = process_directories[-1:] + process_directories[:-1]
|
|
|
|
|
2020-05-11 08:17:21 +00:00
|
|
|
# create driver processes and start them
|
2020-02-26 10:27:57 +00:00
|
|
|
for process_directory in process_directories:
|
2020-05-11 08:17:21 +00:00
|
|
|
self.process_pool.append(self.driver_object.upscale(process_directory, self.upscaled_frames))
|
2020-04-27 00:01:37 +00:00
|
|
|
|
2020-02-26 10:27:57 +00:00
|
|
|
# start progress bar in a different thread
|
2020-05-07 19:58:22 +00:00
|
|
|
Avalon.debug_info(_('Starting progress monitor'))
|
|
|
|
self.progress_monitor = ProgressMonitor(self, process_directories)
|
|
|
|
self.progress_monitor.start()
|
2020-02-26 10:27:57 +00:00
|
|
|
|
|
|
|
# create the clearer and start it
|
2020-05-05 00:25:12 +00:00
|
|
|
Avalon.debug_info(_('Starting upscaled image cleaner'))
|
2020-05-07 19:58:22 +00:00
|
|
|
self.image_cleaner = ImageCleaner(self.extracted_frames, self.upscaled_frames, len(self.process_pool))
|
|
|
|
self.image_cleaner.start()
|
2020-02-26 10:27:57 +00:00
|
|
|
|
|
|
|
# wait for all process to exit
|
|
|
|
try:
|
2020-05-07 19:58:22 +00:00
|
|
|
self._wait()
|
|
|
|
except (Exception, KeyboardInterrupt, SystemExit) as e:
|
|
|
|
# cleanup
|
|
|
|
Avalon.debug_info(_('Killing progress monitor'))
|
|
|
|
self.progress_monitor.stop()
|
|
|
|
|
2020-05-05 00:25:12 +00:00
|
|
|
Avalon.debug_info(_('Killing upscaled image cleaner'))
|
2020-05-07 23:55:33 +00:00
|
|
|
self.image_cleaner.stop()
|
2020-05-07 19:58:22 +00:00
|
|
|
raise e
|
2020-02-26 10:27:57 +00:00
|
|
|
|
|
|
|
# if the driver is waifu2x-converter-cpp
|
|
|
|
# images need to be renamed to be recognizable for FFmpeg
|
2020-05-03 23:20:23 +00:00
|
|
|
if self.driver == 'waifu2x_converter_cpp':
|
2020-02-26 10:27:57 +00:00
|
|
|
for image in [f for f in self.upscaled_frames.iterdir() if f.is_file()]:
|
2020-05-07 19:58:22 +00:00
|
|
|
renamed = re.sub(f'_\\[.*\\]\\[x(\\d+(\\.\\d+)?)\\]\\.{self.image_format}',
|
|
|
|
f'.{self.image_format}',
|
|
|
|
str(image.name))
|
2020-02-26 10:27:57 +00:00
|
|
|
(self.upscaled_frames / image).rename(self.upscaled_frames / renamed)
|
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# upscaling done, kill helper threads
|
|
|
|
Avalon.debug_info(_('Killing progress monitor'))
|
|
|
|
self.progress_monitor.stop()
|
2019-06-25 23:25:27 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
Avalon.debug_info(_('Killing upscaled image cleaner'))
|
|
|
|
self.image_cleaner.stop()
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
def _terminate_subprocesses(self):
|
|
|
|
Avalon.warning(_('Terminating all processes'))
|
|
|
|
for process in self.process_pool:
|
|
|
|
process.terminate()
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
def _wait(self):
|
|
|
|
""" wait for subprocesses in process pool to complete
|
2018-12-11 20:52:48 +00:00
|
|
|
"""
|
2020-05-07 19:58:22 +00:00
|
|
|
Avalon.debug_info(_('Main process waiting for subprocesses to exit'))
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
try:
|
|
|
|
# while process pool not empty
|
|
|
|
while self.process_pool:
|
2020-05-07 23:55:33 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# if stop signal received, terminate all processes
|
2020-05-09 00:28:46 +00:00
|
|
|
if self.running is False:
|
2020-05-07 19:58:22 +00:00
|
|
|
raise SystemExit
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
for process in self.process_pool:
|
|
|
|
process_status = process.poll()
|
2020-05-05 05:11:50 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# if process finished
|
|
|
|
if process_status is None:
|
|
|
|
continue
|
2020-05-05 05:11:50 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# 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)
|
2018-12-11 20:52:48 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
else:
|
|
|
|
Avalon.debug_info(_('Subprocess {} exited with code {}').format(process.pid, process_status))
|
|
|
|
self.process_pool.remove(process)
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
time.sleep(0.1)
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
except (KeyboardInterrupt, SystemExit) as e:
|
|
|
|
Avalon.warning(_('Stop signal received'))
|
|
|
|
self._terminate_subprocesses()
|
|
|
|
raise e
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
except (Exception, subprocess.CalledProcessError) as e:
|
|
|
|
Avalon.error(_('Subprocess execution ran into an error'))
|
|
|
|
self._terminate_subprocesses()
|
|
|
|
raise e
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
def run(self):
|
|
|
|
""" Main controller for Video2X
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
This function controls the flow of video conversion
|
|
|
|
and handles all necessary functions.
|
|
|
|
"""
|
2020-05-07 23:55:33 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# external stop signal when called in a thread
|
2020-05-09 00:28:46 +00:00
|
|
|
self.running = True
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# define process pool to contain processes
|
|
|
|
self.process_pool = []
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-11 08:17:21 +00:00
|
|
|
# load driver modules
|
|
|
|
DriverWrapperMain = getattr(importlib.import_module(f'wrappers.{self.driver}'), 'WrapperMain')
|
|
|
|
self.driver_object = DriverWrapperMain(self.driver_settings)
|
|
|
|
|
|
|
|
# load options from upscaler class into driver settings
|
|
|
|
self.driver_object.load_configurations(self)
|
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# initialize FFmpeg object
|
2020-05-13 00:30:59 +00:00
|
|
|
self.ffmpeg_object = Ffmpeg(self.ffmpeg_settings, image_format=self.image_format)
|
2020-05-03 23:20:23 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# define processing queue
|
2020-05-09 00:28:46 +00:00
|
|
|
self.processing_queue = queue.Queue()
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-08 21:37:16 +00:00
|
|
|
# if input is a list of files
|
|
|
|
if isinstance(self.input, list):
|
|
|
|
# make output directory if it doesn't exist
|
|
|
|
self.output.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
for input_path in self.input:
|
|
|
|
|
|
|
|
if input_path.is_file():
|
2020-05-12 00:24:18 +00:00
|
|
|
output_path = self.output / input_path.name
|
|
|
|
self.processing_queue.put((input_path.absolute(), output_path.absolute()))
|
2020-05-08 21:37:16 +00:00
|
|
|
|
|
|
|
elif input_path.is_dir():
|
2020-05-12 00:24:18 +00:00
|
|
|
for input_path in [f for f in input_path.iterdir() if f.is_file()]:
|
|
|
|
output_path = self.output / input_path.name
|
|
|
|
self.processing_queue.put((input_path.absolute(), output_path.absolute()))
|
2020-05-08 21:37:16 +00:00
|
|
|
|
2020-05-07 19:58:22 +00:00
|
|
|
# if input specified is single file
|
2020-05-08 21:37:16 +00:00
|
|
|
elif self.input.is_file():
|
2020-05-12 00:24:18 +00:00
|
|
|
Avalon.info(_('Upscaling single file: {}').format(self.input))
|
2020-05-09 00:28:46 +00:00
|
|
|
self.processing_queue.put((self.input.absolute(), self.output.absolute()))
|
2020-05-07 19:58:22 +00:00
|
|
|
|
|
|
|
# if input specified is a directory
|
2020-05-08 21:37:16 +00:00
|
|
|
elif self.input.is_dir():
|
2020-05-07 19:58:22 +00:00
|
|
|
|
|
|
|
# make output directory if it doesn't exist
|
2020-05-08 21:37:16 +00:00
|
|
|
self.output.mkdir(parents=True, exist_ok=True)
|
2020-05-12 00:24:18 +00:00
|
|
|
for input_path in [f for f in self.input.iterdir() if f.is_file()]:
|
|
|
|
output_path = self.output / input_path.name
|
|
|
|
self.processing_queue.put((input_path.absolute(), output_path.absolute()))
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# check argument sanity before running
|
|
|
|
self._check_arguments()
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# record file count for external calls
|
|
|
|
self.total_files = self.processing_queue.qsize()
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
try:
|
|
|
|
while not self.processing_queue.empty():
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# reset current processing progress for new job
|
|
|
|
self.total_frames_upscaled = 0
|
|
|
|
self.total_frames = 0
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# get new job from queue
|
|
|
|
self.current_input_file, output_path = self.processing_queue.get()
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# get file type
|
|
|
|
input_file_mime_type = magic.from_file(str(self.current_input_file.absolute()), mime=True)
|
|
|
|
input_file_type = input_file_mime_type.split('/')[0]
|
|
|
|
input_file_subtype = input_file_mime_type.split('/')[1]
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-22 20:29:51 +00:00
|
|
|
# in case python-magic fails to detect file type
|
|
|
|
# try guessing file mime type with mimetypes
|
|
|
|
if input_file_type not in ['image', 'video']:
|
|
|
|
input_file_mime_type = mimetypes.guess_type(self.current_input_file.name)[0]
|
|
|
|
input_file_type = input_file_mime_type.split('/')[0]
|
|
|
|
input_file_subtype = input_file_mime_type.split('/')[1]
|
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# start handling input
|
|
|
|
# if input file is a static image
|
|
|
|
if input_file_type == 'image' and input_file_subtype != 'gif':
|
|
|
|
Avalon.info(_('Starting to upscale image'))
|
|
|
|
self.process_pool.append(self.driver_object.upscale(self.current_input_file, output_path))
|
|
|
|
self._wait()
|
2020-05-07 19:58:22 +00:00
|
|
|
Avalon.info(_('Upscaling completed'))
|
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# static images don't require GIF or video encoding
|
|
|
|
# go to the next task
|
|
|
|
self.processing_queue.task_done()
|
|
|
|
self.total_processed += 1
|
|
|
|
continue
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# if input file is a image/gif file or a video
|
|
|
|
elif input_file_mime_type == 'image/gif' or input_file_type == 'video':
|
|
|
|
|
2020-05-16 11:12:25 +00:00
|
|
|
self.create_temp_directories()
|
|
|
|
|
|
|
|
# get video information JSON using FFprobe
|
|
|
|
Avalon.info(_('Reading video information'))
|
|
|
|
video_info = self.ffmpeg_object.probe_file_info(self.current_input_file)
|
|
|
|
# analyze original video with FFprobe and retrieve framerate
|
|
|
|
# width, height = info['streams'][0]['width'], info['streams'][0]['height']
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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')
|
|
|
|
|
|
|
|
# get average frame rate of video stream
|
|
|
|
framerate = float(Fraction(video_info['streams'][video_stream_index]['r_frame_rate']))
|
|
|
|
Avalon.info(_('Framerate: {}').format(framerate))
|
|
|
|
# self.ffmpeg_object.pixel_format = video_info['streams'][video_stream_index]['pix_fmt']
|
|
|
|
|
|
|
|
# extract frames from video
|
|
|
|
self.process_pool.append((self.ffmpeg_object.extract_frames(self.current_input_file, self.extracted_frames)))
|
|
|
|
self._wait()
|
2020-05-12 00:24:18 +00:00
|
|
|
|
2020-05-16 11:12:25 +00:00
|
|
|
# if driver is waifu2x-caffe
|
|
|
|
# pass pixel format output depth information
|
|
|
|
if self.driver == 'waifu2x_caffe':
|
|
|
|
# get a dict of all pixel formats and corresponding bit depth
|
|
|
|
pixel_formats = self.ffmpeg_object.get_pixel_formats()
|
|
|
|
|
|
|
|
# try getting pixel format's corresponding bti depth
|
|
|
|
try:
|
|
|
|
self.driver_settings['output_depth'] = pixel_formats[self.ffmpeg_object.pixel_format]
|
|
|
|
except KeyError:
|
|
|
|
Avalon.error(_('Unsupported pixel format: {}').format(self.ffmpeg_object.pixel_format))
|
|
|
|
raise UnsupportedPixelError(f'unsupported pixel format {self.ffmpeg_object.pixel_format}')
|
|
|
|
|
|
|
|
# width/height will be coded width/height x upscale factor
|
|
|
|
# original_width = video_info['streams'][video_stream_index]['width']
|
|
|
|
# original_height = video_info['streams'][video_stream_index]['height']
|
|
|
|
# scale_width = int(self.scale_ratio * original_width)
|
|
|
|
# scale_height = int(self.scale_ratio * original_height)
|
|
|
|
|
|
|
|
# upscale images one by one using waifu2x
|
|
|
|
Avalon.info(_('Starting to upscale extracted frames'))
|
|
|
|
self._upscale_frames()
|
|
|
|
Avalon.info(_('Upscaling completed'))
|
2020-05-12 00:24:18 +00:00
|
|
|
|
|
|
|
# if file is none of: image, image/gif, video
|
|
|
|
# skip to the next task
|
|
|
|
else:
|
2020-05-22 20:29:51 +00:00
|
|
|
Avalon.error(_('File {} ({}) neither an image nor a video').format(self.current_input_file, input_file_mime_type))
|
2020-05-12 00:24:18 +00:00
|
|
|
Avalon.warning(_('Skipping this file'))
|
|
|
|
self.processing_queue.task_done()
|
|
|
|
self.total_processed += 1
|
|
|
|
continue
|
|
|
|
|
|
|
|
# start handling output
|
|
|
|
# output can be either GIF or video
|
|
|
|
|
|
|
|
# if the desired output is gif file
|
|
|
|
if output_path.suffix.lower() == '.gif':
|
|
|
|
Avalon.info(_('Converting extracted frames into GIF image'))
|
|
|
|
gifski_object = Gifski(self.gifski_settings)
|
|
|
|
self.process_pool.append(gifski_object.make_gif(self.upscaled_frames, output_path, framerate, self.image_format))
|
|
|
|
self._wait()
|
|
|
|
Avalon.info(_('Conversion completed'))
|
|
|
|
|
|
|
|
# if the desired output is video
|
|
|
|
else:
|
|
|
|
# frames to video
|
|
|
|
Avalon.info(_('Converting extracted frames into video'))
|
|
|
|
self.process_pool.append(self.ffmpeg_object.assemble_video(framerate, self.upscaled_frames))
|
2020-05-16 11:12:25 +00:00
|
|
|
# f'{scale_width}x{scale_height}'
|
2020-05-07 19:58:22 +00:00
|
|
|
self._wait()
|
|
|
|
Avalon.info(_('Conversion completed'))
|
|
|
|
|
2020-05-09 23:30:24 +00:00
|
|
|
try:
|
|
|
|
# migrate audio tracks and subtitles
|
2020-05-09 23:40:18 +00:00
|
|
|
Avalon.info(_('Migrating audio, subtitles and other streams to upscaled video'))
|
2020-05-12 00:24:18 +00:00
|
|
|
self.process_pool.append(self.ffmpeg_object.migrate_streams(self.current_input_file,
|
|
|
|
output_path,
|
|
|
|
self.upscaled_frames))
|
2020-05-09 23:30:24 +00:00
|
|
|
self._wait()
|
|
|
|
|
|
|
|
# if failed to copy streams
|
|
|
|
# use file with only video stream
|
|
|
|
except subprocess.CalledProcessError:
|
2020-05-12 23:25:39 +00:00
|
|
|
traceback.print_exc()
|
2020-05-09 23:30:24 +00:00
|
|
|
Avalon.error(_('Failed to migrate streams'))
|
|
|
|
Avalon.warning(_('Trying to output video without additional streams'))
|
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
if input_file_mime_type == 'image/gif':
|
2020-05-12 23:25:39 +00:00
|
|
|
# copy will overwrite destination content if exists
|
|
|
|
shutil.copy(self.upscaled_frames / self.ffmpeg_object.intermediate_file_name, output_path)
|
2020-05-09 23:30:24 +00:00
|
|
|
|
|
|
|
else:
|
2020-05-12 00:24:18 +00:00
|
|
|
# construct output file path
|
2020-05-12 23:25:39 +00:00
|
|
|
output_file_name = f'{output_path.stem}{self.ffmpeg_object.intermediate_file_name.suffix}'
|
|
|
|
output_video_path = output_path.parent / output_file_name
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 23:25:39 +00:00
|
|
|
# if output file already exists
|
|
|
|
# create temporary directory in output folder
|
|
|
|
# temporary directories generated by tempfile are guaranteed to be unique
|
|
|
|
# and won't conflict with other files
|
2020-05-12 00:24:18 +00:00
|
|
|
if output_video_path.exists():
|
2020-05-12 23:25:39 +00:00
|
|
|
Avalon.error(_('Output video file exists'))
|
2020-05-07 19:58:22 +00:00
|
|
|
|
2020-05-12 23:25:39 +00:00
|
|
|
temporary_directory = pathlib.Path(tempfile.mkdtemp(dir=output_path.parent))
|
|
|
|
output_video_path = temporary_directory / output_file_name
|
|
|
|
Avalon.info(_('Created temporary directory to contain file'))
|
|
|
|
|
2020-05-12 23:27:37 +00:00
|
|
|
# move file to new destination
|
2020-05-12 23:25:39 +00:00
|
|
|
Avalon.info(_('Writing intermediate file to: {}').format(output_video_path.absolute()))
|
2020-05-12 23:27:37 +00:00
|
|
|
shutil.move(self.upscaled_frames / self.ffmpeg_object.intermediate_file_name, output_video_path)
|
2020-05-09 00:28:46 +00:00
|
|
|
|
2020-05-12 00:24:18 +00:00
|
|
|
# increment total number of files processed
|
|
|
|
self.cleanup_temp_directories()
|
|
|
|
self.processing_queue.task_done()
|
|
|
|
self.total_processed += 1
|
|
|
|
|
|
|
|
except (Exception, KeyboardInterrupt, SystemExit) as e:
|
2020-05-12 01:01:09 +00:00
|
|
|
with contextlib.suppress(ValueError, AttributeError):
|
2020-05-12 00:24:18 +00:00
|
|
|
self.cleanup_temp_directories()
|
|
|
|
self.running = False
|
|
|
|
raise e
|
2020-05-09 00:28:46 +00:00
|
|
|
|
|
|
|
# signal upscaling completion
|
|
|
|
self.running = False
|