added image and GIF upscale support

This commit is contained in:
k4yt3x 2020-05-11 20:24:18 -04:00
parent 5cf3271aad
commit e305d0188e
13 changed files with 401 additions and 231 deletions

View File

@ -3,6 +3,8 @@ colorama
patool
psutil
pyqt5
python-magic
python-magic-bin
pyunpack
pyyaml
requests

View File

@ -4,7 +4,7 @@
Name: Video2X Upscaler
Author: K4YT3X
Date Created: December 10, 2018
Last Modified: May 10, 2020
Last Modified: May 11, 2020
Description: This file contains the Upscaler class. Each
instance of the Upscaler class is an upscaler on an image or
@ -16,6 +16,7 @@ from exceptions import *
from image_cleaner import ImageCleaner
from progress_monitor import ProgressMonitor
from wrappers.ffmpeg import Ffmpeg
from wrappers.gifski import Gifski
# built-in imports
from fractions import Fraction
@ -24,7 +25,6 @@ import copy
import gettext
import importlib
import locale
import os
import pathlib
import queue
import re
@ -36,6 +36,7 @@ import traceback
# third-party imports
from avalon_framework import Avalon
import magic
# internationalization constants
DOMAIN = 'video2x'
@ -67,12 +68,13 @@ class Upscaler:
ArgumentError -- if argument is not valid
"""
def __init__(self, input_path, output_path, driver_settings, ffmpeg_settings):
def __init__(self, input_path, output_path, driver_settings, ffmpeg_settings, gifski_settings):
# mandatory arguments
self.input = input_path
self.output = output_path
self.driver_settings = driver_settings
self.ffmpeg_settings = ffmpeg_settings
self.gifski_settings = gifski_settings
# optional arguments
self.driver = 'waifu2x_caffe'
@ -86,9 +88,9 @@ class Upscaler:
self.running = False
self.total_frames_upscaled = 0
self.total_frames = 0
self.total_videos = 0
self.total_files = 0
self.total_processed = 0
self.current_input_video = pathlib.Path()
self.current_input_file = pathlib.Path()
self.last_frame_upscaled = pathlib.Path()
def create_temp_directories(self):
@ -154,10 +156,10 @@ class Upscaler:
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)):
if self.output.suffix == '':
Avalon.error(_('No suffix found in output file path'))
Avalon.error(_('Suffix must be specified for FFmpeg'))
raise ArgumentError('no output video suffix specified')
Avalon.error(_('Suffix must be specified'))
raise ArgumentError('no output file suffix specified')
# if input is a directory
elif self.input.is_dir():
@ -238,6 +240,14 @@ class Upscaler:
self.driver_settings['scale_width'] = None
self.driver_settings['scale_height'] = None
# temporary file type check for Anime4KCPP
# it doesn't support GIF processing yet
if self.driver == 'anime4kcpp':
for task in self.processing_queue.queue:
if task[0].suffix.lower() == '.gif':
Avalon.error(_('Anime4KCPP doesn\'t yet support GIF processing'))
raise AttributeError('Anime4KCPP doesn\'t yet support GIF file processing')
def _upscale_frames(self):
""" Upscale video frames with waifu2x-caffe
@ -393,9 +403,8 @@ class Upscaler:
# load options from upscaler class into driver settings
self.driver_object.load_configurations(self)
# parse arguments for waifu2x
# check argument sanity
self._check_arguments()
# initialize FFmpeg object
self.ffmpeg_object = Ffmpeg(self.ffmpeg_settings, self.image_format)
# define processing queue
self.processing_queue = queue.Queue()
@ -408,17 +417,17 @@ class Upscaler:
for input_path in self.input:
if input_path.is_file():
output_video = self.output / input_path.name
self.processing_queue.put((input_path.absolute(), output_video.absolute()))
output_path = self.output / input_path.name
self.processing_queue.put((input_path.absolute(), output_path.absolute()))
elif input_path.is_dir():
for input_video in [f for f in input_path.iterdir() if f.is_file()]:
output_video = self.output / input_video.name
self.processing_queue.put((input_video.absolute(), output_video.absolute()))
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()))
# if input specified is single file
elif self.input.is_file():
Avalon.info(_('Upscaling single video file: {}').format(self.input))
Avalon.info(_('Upscaling single file: {}').format(self.input))
self.processing_queue.put((self.input.absolute(), self.output.absolute()))
# if input specified is a directory
@ -426,99 +435,147 @@ class Upscaler:
# make output directory if it doesn't exist
self.output.mkdir(parents=True, exist_ok=True)
for input_video in [f for f in self.input.iterdir() if f.is_file()]:
output_video = self.output / input_video.name
self.processing_queue.put((input_video.absolute(), output_video.absolute()))
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()))
# record video count for external calls
self.total_videos = self.processing_queue.qsize()
# check argument sanity before running
self._check_arguments()
while not self.processing_queue.empty():
self.current_input_video, output_video = self.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'))
# record file count for external calls
self.total_files = self.processing_queue.qsize()
# run Anime4KCPP
self.process_pool.append(self.driver_object.upscale(self.current_input_video, output_video))
self._wait()
Avalon.info(_('Upscaling completed'))
try:
while not self.processing_queue.empty():
else:
try:
self.create_temp_directories()
# reset current processing progress for new job
self.total_frames_upscaled = 0
self.total_frames = 0
# initialize objects for ffmpeg and waifu2x-caffe
fm = Ffmpeg(self.ffmpeg_settings, self.image_format)
# get new job from queue
self.current_input_file, output_path = self.processing_queue.get()
Avalon.info(_('Reading video information'))
video_info = fm.get_video_info(self.current_input_video)
# analyze original video with FFprobe and retrieve framerate
# width, height = info['streams'][0]['width'], info['streams'][0]['height']
# 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]
# 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')
# extract frames from video
self.process_pool.append((fm.extract_frames(self.current_input_video, self.extracted_frames)))
# 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()
# get average frame rate of video stream
framerate = float(Fraction(video_info['streams'][video_stream_index]['r_frame_rate']))
fm.pixel_format = video_info['streams'][video_stream_index]['pix_fmt']
if self.driver == 'waifu2x_caffe':
# get a dict of all pixel formats and corresponding bit depth
pixel_formats = fm.get_pixel_formats()
# try getting pixel format's corresponding bti depth
try:
self.driver_settings['output_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}')
Avalon.info(_('Framerate: {}').format(framerate))
# 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 images'))
self._upscale_frames()
Avalon.info(_('Upscaling completed'))
# frames to Video
Avalon.info(_('Converting extracted frames into video'))
# static images don't require GIF or video encoding
# go to the next task
self.processing_queue.task_done()
self.total_processed += 1
continue
# use user defined output size
self.process_pool.append(fm.assemble_video(framerate,
f'{scale_width}x{scale_height}',
self.upscaled_frames))
# if input file is a image/gif file or a video
elif input_file_mime_type == 'image/gif' or input_file_type == 'video':
# drivers that have native support for video processing
if input_file_type == 'video' and self.driver == 'anime4kcpp':
Avalon.info(_('Starting to upscale video with Anime4KCPP'))
# enable video processing mode for Anime4KCPP
self.driver_settings['videoMode'] = True
self.process_pool.append(self.driver_object.upscale(self.current_input_file, output_path))
self._wait()
Avalon.info(_('Upscaling completed'))
else:
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']))
# 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()
# 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}')
Avalon.info(_('Framerate: {}').format(framerate))
# 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'))
# if file is none of: image, image/gif, video
# skip to the next task
else:
Avalon.error(_('File {} ({}) neither an image of a video').format(self.current_input_file, input_file_mime_type))
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))
# f'{scale_width}x{scale_height}',
self._wait()
Avalon.info(_('Conversion completed'))
try:
# migrate audio tracks and subtitles
Avalon.info(_('Migrating audio, subtitles and other streams to upscaled video'))
self.process_pool.append(fm.migrate_streams(self.current_input_video,
output_video,
self.upscaled_frames))
self.process_pool.append(self.ffmpeg_object.migrate_streams(self.current_input_file,
output_path,
self.upscaled_frames))
self._wait()
# if failed to copy streams
@ -527,29 +584,32 @@ class Upscaler:
Avalon.error(_('Failed to migrate streams'))
Avalon.warning(_('Trying to output video without additional streams'))
# construct output file path
output_video_path = output_video.parent / f'{output_video.stem}{fm.intermediate_file_name.suffix}'
if input_file_mime_type == 'image/gif':
(self.upscaled_frames / self.ffmpeg_object.intermediate_file_name).replace(output_path)
# if output file already exists, cancel
if output_video_path.exists():
Avalon.error(_('Output video file exists, aborting'))
# otherwise, rename intermediate file to the output file
else:
Avalon.info(_('Writing intermediate file to: {}').format(output_video_path.absolute()))
(self.upscaled_frames / fm.intermediate_file_name).rename(output_video_path)
# construct output file path
output_video_path = output_path.parent / f'{output_path.stem}{self.ffmpeg_object.intermediate_file_name.suffix}'
# destroy temp directories
self.cleanup_temp_directories()
# if output file already exists, cancel
if output_video_path.exists():
Avalon.error(_('Output video file exists, aborting'))
except (Exception, KeyboardInterrupt, SystemExit) as e:
with contextlib.suppress(ValueError):
self.cleanup_temp_directories()
self.running = False
raise e
# otherwise, rename intermediate file to the output file
else:
Avalon.info(_('Writing intermediate file to: {}').format(output_video_path.absolute()))
(self.upscaled_frames / self.ffmpeg_object.intermediate_file_name).rename(output_video_path)
# increment total number of videos processed
self.total_processed += 1
# 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:
with contextlib.suppress(ValueError):
self.cleanup_temp_directories()
self.running = False
raise e
# signal upscaling completion
self.running = False

View File

@ -13,7 +13,7 @@ __ __ _ _ ___ __ __
Name: Video2X Controller
Creator: K4YT3X
Date Created: Feb 24, 2018
Last Modified: May 10, 2020
Last Modified: May 11, 2020
Editor: BrianPetkovsek
Last Modified: June 17, 2019
@ -181,6 +181,10 @@ driver_settings['path'] = os.path.expandvars(driver_settings['path'])
ffmpeg_settings = config['ffmpeg']
ffmpeg_settings['ffmpeg_path'] = os.path.expandvars(ffmpeg_settings['ffmpeg_path'])
# read Gifski configuration
gifski_settings = config['gifski']
gifski_settings['gifski_path'] = os.path.expandvars(gifski_settings['gifski_path'])
# load video2x settings
image_format = config['video2x']['image_format'].lower()
preserve_frames = config['video2x']['preserve_frames']
@ -213,7 +217,8 @@ try:
upscaler = Upscaler(input_path=video2x_args.input,
output_path=video2x_args.output,
driver_settings=driver_settings,
ffmpeg_settings=ffmpeg_settings)
ffmpeg_settings=ffmpeg_settings,
gifski_settings=gifski_settings)
# set upscaler optional options
upscaler.driver = video2x_args.driver

View File

@ -1,7 +1,7 @@
# Name: Video2X Configuration File
# Creator: K4YT3X
# Date Created: October 23, 2018
# Last Modified: May 9, 2020
# Last Modified: May 11, 2020
# Values here are the default values. Change the value here to
# save the default value permanently.
# Items commented out are parameters irrelevant to this context
@ -87,7 +87,7 @@ anime4kcpp:
zoomFactor: 2.0 # zoom factor for resizing (double [=2])
threads: 16 # Threads count for video processing (unsigned int [=16])
fastMode: false # Faster but maybe low quality
videoMode: true # Video process
videoMode: false # Video process
preview: null # Preview image
preprocessing: False # Enable pre processing
postprocessing: False # Enable post processing
@ -101,9 +101,9 @@ anime4kcpp:
ffmpeg:
ffmpeg_path: '%LOCALAPPDATA%\video2x\ffmpeg-latest-win64-static\bin'
intermediate_file_name: 'intermediate.mkv'
# step 1: extract all frames from original video
# step 1: extract all frames from original input
# into temporary directory
video_to_frames:
input_to_frames:
output_options:
'-qscale:v': null
'-pix_fmt': rgba64be
@ -138,6 +138,17 @@ ffmpeg:
'-metadata': 'comment=Upscaled by Video2X'
'-hwaccel': auto
'-y': true
gifski:
gifski_path: '%LOCALAPPDATA%\video2x\gifski\win\gifski'
# output: null # Destination file to write to
# fps: 20 # Animation frames per second (for PNG frames only) [default: 20]
fast: false # 3 times faster encoding, but 10% lower quality and bigger file
quality: 100 # Lower quality may give smaller file
width: null # Maximum width
height: null # Maximum height (if width is also set)
once: false # Do not loop the GIF
nosort: false # Use files exactly in the order given, rather than sorted
quiet: false # Do not show a progress bar
video2x:
video2x_cache_directory: null # default: %TEMP%\video2x
image_format: png

View File

@ -4,7 +4,7 @@
Creator: Video2X GUI
Author: K4YT3X
Date Created: May 5, 2020
Last Modified: May 10, 2020
Last Modified: May 11, 2020
"""
# local imports
@ -25,6 +25,7 @@ import yaml
from PyQt5 import QtGui, uic
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import magic
# QObject, pyqtSlot, pyqtSignal, QRunnable, QThreadPool, QAbstractTableModel, Qt
VERSION = '2.0.0'
@ -110,13 +111,34 @@ class InputTableModel(QAbstractTableModel):
def data(self, index, role):
if role == Qt.DisplayRole:
file_path = self._data[index.row()]
if index.column() == 0:
return str(self._data[index.row()].absolute())
return str(file_path.absolute())
else:
if self._data[index.row()].is_file():
return 'File'
elif self._data[index.row()].is_dir():
# determine file type
# if path is a folder
if file_path.is_dir():
return 'Folder'
# if path is single file
# determine file type
elif file_path.is_file():
input_file_mime_type = magic.from_file(str(file_path.absolute()), mime=True)
input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1]
if input_file_type == 'image':
if input_file_subtype == 'gif':
return 'GIF'
return 'Image'
elif input_file_type == 'video':
return 'Video'
else:
return 'Unknown'
else:
return 'Unknown'
@ -373,6 +395,10 @@ class Video2XMainWindow(QMainWindow):
self.ffmpeg_settings = self.config['ffmpeg']
self.ffmpeg_settings['ffmpeg_path'] = str(pathlib.Path(os.path.expandvars(self.ffmpeg_settings['ffmpeg_path'])).absolute())
# read Gifski configuration
self.gifski_settings = self.config['gifski']
self.gifski_settings['gifski_path'] = str(pathlib.Path(os.path.expandvars(self.gifski_settings['gifski_path'])).absolute())
# set cache directory path
if self.config['video2x']['video2x_cache_directory'] is None:
self.config['video2x']['video2x_cache_directory'] = str((pathlib.Path(tempfile.gettempdir()) / 'video2x').absolute())
@ -585,7 +611,34 @@ class Video2XMainWindow(QMainWindow):
return
if input_path.is_file():
output_path = input_path.parent / f'{input_path.stem}_output.mp4'
# generate suffix automatically
input_file_mime_type = magic.from_file(str(input_path.absolute()), mime=True)
input_file_type = input_file_mime_type.split('/')[0]
input_file_subtype = input_file_mime_type.split('/')[1]
# if input file is an image
if input_file_type == 'image':
# if file is a gif, use .gif
if input_file_subtype == 'gif':
suffix = '.gif'
# otherwise, use .png by default for all images
else:
suffix = '.png'
# if input is video, use .mp4 as output by default
elif input_file_type == 'video':
suffix = '.mp4'
# if failed to detect file type
# use input file's suffix
else:
suffix = input_path.suffix
output_path = input_path.parent / f'{input_path.stem}_output{suffix}'
elif input_path.is_dir():
output_path = input_path.parent / f'{input_path.stem}_output'
@ -593,7 +646,7 @@ class Video2XMainWindow(QMainWindow):
output_path_id = 0
while output_path.exists() and output_path_id <= 1000:
if input_path.is_file():
output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}.mp4')
output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}{suffix}')
elif input_path.is_dir():
output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}')
output_path_id += 1
@ -693,8 +746,8 @@ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new
self.upscaler.total_frames_upscaled,
self.upscaler.total_frames,
self.upscaler.total_processed,
self.upscaler.total_videos,
self.upscaler.current_input_video,
self.upscaler.total_files,
self.upscaler.current_input_file,
self.upscaler.last_frame_upscaled))
time.sleep(1)
@ -707,8 +760,8 @@ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new
total_frames_upscaled = progress_information[1]
total_frames = progress_information[2]
total_processed = progress_information[3]
total_videos = progress_information[4]
current_input_video = progress_information[5]
total_files = progress_information[4]
current_input_file = progress_information[5]
last_frame_upscaled = progress_information[6]
# calculate fields based on frames and time elapsed
@ -727,10 +780,10 @@ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new
self.time_elapsed_label.setText('Time Elapsed: {}'.format(time.strftime("%H:%M:%S", time.gmtime(time_elapsed))))
self.time_remaining_label.setText('Time Remaining: {}'.format(time.strftime("%H:%M:%S", time.gmtime(time_remaining))))
self.rate_label.setText('Rate (FPS): {}'.format(round(rate, 2)))
self.overall_progress_label.setText('Overall Progress: {}/{}'.format(total_processed, total_videos))
self.overall_progress_bar.setMaximum(total_videos)
self.overall_progress_label.setText('Overall Progress: {}/{}'.format(total_processed, total_files))
self.overall_progress_bar.setMaximum(total_files)
self.overall_progress_bar.setValue(total_processed)
self.currently_processing_label.setText('Currently Processing: {}'.format(str(current_input_video.name)))
self.currently_processing_label.setText('Currently Processing: {}'.format(str(current_input_file.name)))
# if show frame is checked, show preview image
if self.frame_preview_show_preview_check_box.isChecked() and last_frame_upscaled.is_file():
@ -798,7 +851,8 @@ You can [submit an issue on GitHub](https://github.com/k4yt3x/video2x/issues/new
self.upscaler = Upscaler(input_path=input_directory,
output_path=output_directory,
driver_settings=self.driver_settings,
ffmpeg_settings=self.ffmpeg_settings)
ffmpeg_settings=self.ffmpeg_settings,
gifski_settings=self.gifski_settings)
# set optional options
self.upscaler.driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()]

View File

@ -31,6 +31,7 @@ import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
import time
import traceback
@ -42,12 +43,19 @@ import zipfile
# later in the script.
# import requests
VERSION = '1.8.0'
VERSION = '2.0.0'
# global static variables
LOCALAPPDATA = pathlib.Path(os.getenv('localappdata'))
VIDEO2X_CONFIG = pathlib.Path(__file__).parent.absolute() / 'video2x.yaml'
DRIVER_OPTIONS = ['all', 'ffmpeg', 'waifu2x_caffe', 'waifu2x_converter_cpp', 'waifu2x_ncnn_vulkan', 'anime4kcpp', 'srmd_ncnn_vulkan']
DRIVER_OPTIONS = ['all',
'ffmpeg',
'gifski',
'waifu2x_caffe',
'waifu2x_converter_cpp',
'waifu2x_ncnn_vulkan',
'anime4kcpp',
'srmd_ncnn_vulkan']
def parse_arguments():
@ -76,32 +84,21 @@ class Video2xSetup:
self.trash = []
def run(self):
# regardless of which driver to install
# always ensure Python modules are installed and up-to-date
if self.download_python_modules:
print('\nInstalling Python libraries')
self._install_python_requirements()
# if all drivers are to be installed
if self.driver == 'all':
self._install_ffmpeg()
self._install_waifu2x_caffe()
self._install_waifu2x_converter_cpp()
self._install_waifu2x_ncnn_vulkan()
self._install_anime4kcpp()
self._install_srmd_ncnn_vulkan()
elif self.driver == 'ffmpeg':
self._install_ffmpeg()
elif self.driver == 'waifu2x_caffe':
self._install_waifu2x_caffe()
elif self.driver == 'waifu2x_converter_cpp':
self._install_waifu2x_converter_cpp()
elif self.driver == 'waifu2x_ncnn_vulkan':
self._install_waifu2x_ncnn_vulkan()
elif self.driver == 'anime4kcpp':
self._install_anime4kcpp()
elif self.driver == 'srmd_ncnn_vulkan':
self._install_srmd_ncnn_vulkan()
DRIVER_OPTIONS.remove('all')
for driver in DRIVER_OPTIONS:
getattr(self, f'_install_{driver}')()
print('\nGenerating Video2X configuration file')
self._generate_config()
# install only the selected driver
else:
getattr(self, f'_install_{self.driver}')()
print('\nCleaning up temporary files')
self._cleanup()
@ -139,6 +136,22 @@ class Video2xSetup:
with zipfile.ZipFile(ffmpeg_zip) as zipf:
zipf.extractall(LOCALAPPDATA / 'video2x')
def _install_gifski(self):
print('\nInstalling Gifski')
import requests
# Get latest release of waifu2x-ncnn-vulkan via Github API
latest_release = requests.get('https://api.github.com/repos/ImageOptim/gifski/releases/latest').json()
for a in latest_release['assets']:
if re.search(r'gifski-.*\.tar\.xz', a['browser_download_url']):
gifski_tar_gz = download(a['browser_download_url'], tempfile.gettempdir())
self.trash.append(gifski_tar_gz)
# extract and rename
with tarfile.open(gifski_tar_gz) as archive:
archive.extractall(LOCALAPPDATA / 'video2x' / 'gifski')
def _install_waifu2x_caffe(self):
""" Install waifu2x_caffe
"""
@ -248,42 +261,6 @@ class Video2xSetup:
# rename the newly extracted directory
(LOCALAPPDATA / 'video2x' / zipf.namelist()[0]).rename(srmd_ncnn_vulkan_directory)
def _generate_config(self):
""" Generate video2x config
"""
import yaml
# open current video2x configuration file as template
with open(VIDEO2X_CONFIG, 'r') as template:
template_dict = yaml.load(template, Loader=yaml.FullLoader)
template.close()
# configure only the specified drivers
if self.driver == 'all':
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' / 'waifu2x-converter-cpp')
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')
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')
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')
template_dict['ffmpeg']['ffmpeg_path'] = str(LOCALAPPDATA / 'video2x' / 'ffmpeg-latest-win64-static' / 'bin')
template_dict['video2x']['video2x_cache_directory'] = None
template_dict['video2x']['preserve_frames'] = False
# write configuration into file
with open(VIDEO2X_CONFIG, 'w') as config:
yaml.dump(template_dict, config)
def download(url, save_path, chunk_size=4096):
""" Download file to local with requests library

View File

@ -69,6 +69,10 @@ class WrapperMain:
self.driver_settings['zoomFactor'] = upscaler.scale_ratio
self.driver_settings['threads'] = upscaler.processes
# append FFmpeg path to the end of PATH
# Anime4KCPP will then use FFmpeg to migrate audio tracks
os.environ['PATH'] += f';{upscaler.ffmpeg_settings["ffmpeg_path"]}'
def upscale(self, input_file, output_file):
"""This is the core function for WAIFU2X class
@ -90,14 +94,14 @@ class WrapperMain:
# list to be executed
# initialize the list with waifu2x binary path as the first element
execute = [self.driver_settings.pop('path')]
execute = [self.driver_settings['path']]
for key in self.driver_settings.keys():
value = self.driver_settings[key]
# null or None means that leave this option out (keep default)
if value is None or value is False:
if key == 'path' or value is None or value is False:
continue
else:
if len(key) == 1:

View File

@ -12,6 +12,7 @@ Description: This class handles all FFmpeg related operations.
# built-in imports
import json
import pathlib
import shlex
import subprocess
# third-party imports
@ -36,7 +37,7 @@ class Ffmpeg:
# video metadata
self.image_format = image_format
self.intermediate_file_name = pathlib.Path(self.ffmpeg_settings['intermediate_file_name'])
self.pixel_format = None
self.pixel_format = self.ffmpeg_settings['input_to_frames']['output_options']['-pix_fmt']
def get_pixel_formats(self):
""" Get a dictionary of supported pixel formats
@ -49,8 +50,8 @@ class Ffmpeg:
"""
execute = [
self.ffmpeg_probe_binary,
'-v',
'quiet',
# '-v',
# 'quiet',
'-pix_fmts'
]
@ -74,7 +75,7 @@ class Ffmpeg:
return pixel_formats
def get_video_info(self, input_video):
def probe_file_info(self, input_video):
""" Gets input video information
This method reads input video information
@ -104,31 +105,25 @@ class Ffmpeg:
# turn elements into str
execute = [str(e) for e in execute]
Avalon.debug_info(f'Executing: {" ".join(execute)}')
Avalon.debug_info(f'Executing: {shlex.join(execute)}')
json_str = subprocess.run(execute, check=True, stdout=subprocess.PIPE).stdout
return json.loads(json_str.decode('utf-8'))
def extract_frames(self, input_video, extracted_frames):
"""Extract every frame from original videos
This method extracts every frame from input video using FFmpeg
Arguments:
input_video {string} -- input video path
extracted_frames {string} -- video output directory
def extract_frames(self, input_file, extracted_frames):
""" extract frames from video or GIF file
"""
execute = [
self.ffmpeg_binary
]
execute.extend(self._read_configuration(phase='video_to_frames'))
execute.extend(self._read_configuration(phase='input_to_frames'))
execute.extend([
'-i',
input_video
input_file
])
execute.extend(self._read_configuration(phase='video_to_frames', section='output_options'))
execute.extend(self._read_configuration(phase='input_to_frames', section='output_options'))
execute.extend([
extracted_frames / f'extracted_%0d.{self.image_format}'
@ -136,7 +131,7 @@ class Ffmpeg:
return(self._execute(execute))
def assemble_video(self, framerate, resolution, upscaled_frames):
def assemble_video(self, framerate, upscaled_frames):
"""Converts images into videos
This method converts a set of images into a video
@ -149,9 +144,9 @@ class Ffmpeg:
execute = [
self.ffmpeg_binary,
'-r',
str(framerate),
'-s',
resolution
str(framerate)
# '-s',
# resolution
]
# read other options
@ -274,17 +269,9 @@ class Ffmpeg:
return configuration
def _execute(self, execute):
""" execute command
Arguments:
execute {list} -- list of arguments to be executed
Returns:
int -- execution return code
"""
# turn all list elements into string to avoid errors
execute = [str(e) for e in execute]
Avalon.debug_info(f'Executing: {execute}')
Avalon.debug_info(f'Executing: {shlex.join(execute)}')
return subprocess.Popen(execute)

70
src/wrappers/gifski.py Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Name: Gifski Wrapper
Creator: K4YT3X
Date Created: May 11, 2020
Last Modified: May 11, 2020
Description: High-level wrapper for Gifski.
"""
# built-in imports
import pathlib
import subprocess
# third-party imports
from avalon_framework import Avalon
class Gifski:
def __init__(self, gifski_settings):
self.gifski_settings = gifski_settings
def make_gif(self, upscaled_frames: pathlib.Path, output_path: pathlib.Path, framerate: float, image_format: str) -> subprocess.Popen:
execute = [
self.gifski_settings['gifski_path'],
'-o',
output_path,
'--fps',
int(round(framerate, 0))
]
# load configurations from config file
execute.extend(self._load_configuration())
# append frames location
execute.extend([upscaled_frames / f'extracted_*.{image_format}'])
return(self._execute(execute))
def _load_configuration(self):
configuration = []
for key in self.gifski_settings.keys():
value = self.gifski_settings[key]
# null or None means that leave this option out (keep default)
if key == 'gifski_path' or value is None or value is False:
continue
else:
if len(key) == 1:
configuration.append(f'-{key}')
else:
configuration.append(f'--{key}')
# true means key is an option
if value is not True:
configuration.append(str(value))
return configuration
def _execute(self, execute: list) -> subprocess.Popen:
# turn all list elements into string to avoid errors
execute = [str(e) for e in execute]
Avalon.debug_info(f'Executing: {execute}')
return subprocess.Popen(execute)

View File

@ -77,14 +77,14 @@ class WrapperMain:
# list to be executed
# initialize the list with the binary path as the first element
execute = [self.driver_settings.pop('path')]
execute = [self.driver_settings['path']]
for key in self.driver_settings.keys():
value = self.driver_settings[key]
# null or None means that leave this option out (keep default)
if value is None or value is False:
if key == 'path' or value is None or value is False:
continue
else:
if len(key) == 1:

View File

@ -56,8 +56,8 @@ class WrapperMain:
parser.add_argument('-m', '--mode', choices=['noise', 'scale', 'noise_scale', 'auto_scale'], help='image processing mode')
parser.add_argument('-e', '--output_extention', type=str, help='extention to output image file when output_path is (auto) or input_path is folder')
parser.add_argument('-l', '--input_extention_list', type=str, help='extention to input image file when input_path is folder')
parser.add_argument('-o', '--output', type=str, help=argparse.SUPPRESS) # help='path to output image file (when input_path is folder, output_path must be folder)')
parser.add_argument('-i', '--input_file', type=str, help=argparse.SUPPRESS) # help='(required) path to input image file')
parser.add_argument('-o', '--output_path', type=str, help=argparse.SUPPRESS) # help='path to output image file (when input_path is folder, output_path must be folder)')
parser.add_argument('-i', '--input_path', type=str, help=argparse.SUPPRESS) # help='(required) path to input image file')
return parser.parse_args(arguments)
def load_configurations(self, upscaler):
@ -79,14 +79,14 @@ class WrapperMain:
# list to be executed
# initialize the list with waifu2x binary path as the first element
execute = [self.driver_settings.pop('path')]
execute = [self.driver_settings['path']]
for key in self.driver_settings.keys():
value = self.driver_settings[key]
# null or None means that leave this option out (keep default)
if value is None or value is False:
if key == 'path' or value is None or value is False:
continue
else:
if len(key) == 1:

View File

@ -93,14 +93,14 @@ class WrapperMain:
# list to be executed
# initialize the list with waifu2x binary path as the first element
execute = [self.driver_settings.pop('path')]
execute = [self.driver_settings['path']]
for key in self.driver_settings.keys():
value = self.driver_settings[key]
# null or None means that leave this option out (keep default)
if value is None or value is False:
if key == 'path' or value is None or value is False:
continue
else:
if len(key) == 1:

View File

@ -80,14 +80,14 @@ class WrapperMain:
# list to be executed
# initialize the list with waifu2x binary path as the first element
execute = [self.driver_settings.pop('path')]
execute = [self.driver_settings['path']]
for key in self.driver_settings.keys():
value = self.driver_settings[key]
# null or None means that leave this option out (keep default)
if value is None or value is False:
if key == 'path' or value is None or value is False:
continue
else:
if len(key) == 1: