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 patool
psutil psutil
pyqt5 pyqt5
python-magic
python-magic-bin
pyunpack pyunpack
pyyaml pyyaml
requests requests

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# Name: Video2X Configuration File # Name: Video2X Configuration File
# Creator: K4YT3X # Creator: K4YT3X
# Date Created: October 23, 2018 # 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 # Values here are the default values. Change the value here to
# save the default value permanently. # save the default value permanently.
# Items commented out are parameters irrelevant to this context # Items commented out are parameters irrelevant to this context
@ -87,7 +87,7 @@ anime4kcpp:
zoomFactor: 2.0 # zoom factor for resizing (double [=2]) zoomFactor: 2.0 # zoom factor for resizing (double [=2])
threads: 16 # Threads count for video processing (unsigned int [=16]) threads: 16 # Threads count for video processing (unsigned int [=16])
fastMode: false # Faster but maybe low quality fastMode: false # Faster but maybe low quality
videoMode: true # Video process videoMode: false # Video process
preview: null # Preview image preview: null # Preview image
preprocessing: False # Enable pre processing preprocessing: False # Enable pre processing
postprocessing: False # Enable post processing postprocessing: False # Enable post processing
@ -101,9 +101,9 @@ anime4kcpp:
ffmpeg: ffmpeg:
ffmpeg_path: '%LOCALAPPDATA%\video2x\ffmpeg-latest-win64-static\bin' ffmpeg_path: '%LOCALAPPDATA%\video2x\ffmpeg-latest-win64-static\bin'
intermediate_file_name: 'intermediate.mkv' intermediate_file_name: 'intermediate.mkv'
# step 1: extract all frames from original video # step 1: extract all frames from original input
# into temporary directory # into temporary directory
video_to_frames: input_to_frames:
output_options: output_options:
'-qscale:v': null '-qscale:v': null
'-pix_fmt': rgba64be '-pix_fmt': rgba64be
@ -138,6 +138,17 @@ ffmpeg:
'-metadata': 'comment=Upscaled by Video2X' '-metadata': 'comment=Upscaled by Video2X'
'-hwaccel': auto '-hwaccel': auto
'-y': true '-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:
video2x_cache_directory: null # default: %TEMP%\video2x video2x_cache_directory: null # default: %TEMP%\video2x
image_format: png image_format: png

View File

@ -4,7 +4,7 @@
Creator: Video2X GUI Creator: Video2X GUI
Author: K4YT3X Author: K4YT3X
Date Created: May 5, 2020 Date Created: May 5, 2020
Last Modified: May 10, 2020 Last Modified: May 11, 2020
""" """
# local imports # local imports
@ -25,6 +25,7 @@ import yaml
from PyQt5 import QtGui, uic from PyQt5 import QtGui, uic
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *
import magic
# QObject, pyqtSlot, pyqtSignal, QRunnable, QThreadPool, QAbstractTableModel, Qt # QObject, pyqtSlot, pyqtSignal, QRunnable, QThreadPool, QAbstractTableModel, Qt
VERSION = '2.0.0' VERSION = '2.0.0'
@ -110,13 +111,34 @@ class InputTableModel(QAbstractTableModel):
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
file_path = self._data[index.row()]
if index.column() == 0: if index.column() == 0:
return str(self._data[index.row()].absolute()) return str(file_path.absolute())
else: else:
if self._data[index.row()].is_file():
return 'File' # determine file type
elif self._data[index.row()].is_dir(): # if path is a folder
if file_path.is_dir():
return 'Folder' 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: else:
return 'Unknown' return 'Unknown'
@ -373,6 +395,10 @@ class Video2XMainWindow(QMainWindow):
self.ffmpeg_settings = self.config['ffmpeg'] self.ffmpeg_settings = self.config['ffmpeg']
self.ffmpeg_settings['ffmpeg_path'] = str(pathlib.Path(os.path.expandvars(self.ffmpeg_settings['ffmpeg_path'])).absolute()) 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 # set cache directory path
if self.config['video2x']['video2x_cache_directory'] is None: if self.config['video2x']['video2x_cache_directory'] is None:
self.config['video2x']['video2x_cache_directory'] = str((pathlib.Path(tempfile.gettempdir()) / 'video2x').absolute()) self.config['video2x']['video2x_cache_directory'] = str((pathlib.Path(tempfile.gettempdir()) / 'video2x').absolute())
@ -585,7 +611,34 @@ class Video2XMainWindow(QMainWindow):
return return
if input_path.is_file(): 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(): elif input_path.is_dir():
output_path = input_path.parent / f'{input_path.stem}_output' output_path = input_path.parent / f'{input_path.stem}_output'
@ -593,7 +646,7 @@ class Video2XMainWindow(QMainWindow):
output_path_id = 0 output_path_id = 0
while output_path.exists() and output_path_id <= 1000: while output_path.exists() and output_path_id <= 1000:
if input_path.is_file(): 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(): elif input_path.is_dir():
output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}') output_path = input_path.parent / pathlib.Path(f'{input_path.stem}_output_{output_path_id}')
output_path_id += 1 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_upscaled,
self.upscaler.total_frames, self.upscaler.total_frames,
self.upscaler.total_processed, self.upscaler.total_processed,
self.upscaler.total_videos, self.upscaler.total_files,
self.upscaler.current_input_video, self.upscaler.current_input_file,
self.upscaler.last_frame_upscaled)) self.upscaler.last_frame_upscaled))
time.sleep(1) 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_upscaled = progress_information[1]
total_frames = progress_information[2] total_frames = progress_information[2]
total_processed = progress_information[3] total_processed = progress_information[3]
total_videos = progress_information[4] total_files = progress_information[4]
current_input_video = progress_information[5] current_input_file = progress_information[5]
last_frame_upscaled = progress_information[6] last_frame_upscaled = progress_information[6]
# calculate fields based on frames and time elapsed # 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_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.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.rate_label.setText('Rate (FPS): {}'.format(round(rate, 2)))
self.overall_progress_label.setText('Overall Progress: {}/{}'.format(total_processed, total_videos)) self.overall_progress_label.setText('Overall Progress: {}/{}'.format(total_processed, total_files))
self.overall_progress_bar.setMaximum(total_videos) self.overall_progress_bar.setMaximum(total_files)
self.overall_progress_bar.setValue(total_processed) 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 show frame is checked, show preview image
if self.frame_preview_show_preview_check_box.isChecked() and last_frame_upscaled.is_file(): 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, self.upscaler = Upscaler(input_path=input_directory,
output_path=output_directory, output_path=output_directory,
driver_settings=self.driver_settings, driver_settings=self.driver_settings,
ffmpeg_settings=self.ffmpeg_settings) ffmpeg_settings=self.ffmpeg_settings,
gifski_settings=self.gifski_settings)
# set optional options # set optional options
self.upscaler.driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()] self.upscaler.driver = AVAILABLE_DRIVERS[self.driver_combo_box.currentText()]

View File

@ -31,6 +31,7 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tarfile
import tempfile import tempfile
import time import time
import traceback import traceback
@ -42,12 +43,19 @@ import zipfile
# later in the script. # later in the script.
# import requests # import requests
VERSION = '1.8.0' VERSION = '2.0.0'
# global static variables # global static variables
LOCALAPPDATA = pathlib.Path(os.getenv('localappdata')) LOCALAPPDATA = pathlib.Path(os.getenv('localappdata'))
VIDEO2X_CONFIG = pathlib.Path(__file__).parent.absolute() / 'video2x.yaml' 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(): def parse_arguments():
@ -76,32 +84,21 @@ class Video2xSetup:
self.trash = [] self.trash = []
def run(self): def run(self):
# regardless of which driver to install
# always ensure Python modules are installed and up-to-date
if self.download_python_modules: if self.download_python_modules:
print('\nInstalling Python libraries') print('\nInstalling Python libraries')
self._install_python_requirements() self._install_python_requirements()
# if all drivers are to be installed
if self.driver == 'all': if self.driver == 'all':
self._install_ffmpeg() DRIVER_OPTIONS.remove('all')
self._install_waifu2x_caffe() for driver in DRIVER_OPTIONS:
self._install_waifu2x_converter_cpp() getattr(self, f'_install_{driver}')()
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()
print('\nGenerating Video2X configuration file') # install only the selected driver
self._generate_config() else:
getattr(self, f'_install_{self.driver}')()
print('\nCleaning up temporary files') print('\nCleaning up temporary files')
self._cleanup() self._cleanup()
@ -139,6 +136,22 @@ class Video2xSetup:
with zipfile.ZipFile(ffmpeg_zip) as zipf: with zipfile.ZipFile(ffmpeg_zip) as zipf:
zipf.extractall(LOCALAPPDATA / 'video2x') 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): def _install_waifu2x_caffe(self):
""" Install waifu2x_caffe """ Install waifu2x_caffe
""" """
@ -248,42 +261,6 @@ class Video2xSetup:
# rename the newly extracted directory # rename the newly extracted directory
(LOCALAPPDATA / 'video2x' / zipf.namelist()[0]).rename(srmd_ncnn_vulkan_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): def download(url, save_path, chunk_size=4096):
""" Download file to local with requests library """ 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['zoomFactor'] = upscaler.scale_ratio
self.driver_settings['threads'] = upscaler.processes 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): def upscale(self, input_file, output_file):
"""This is the core function for WAIFU2X class """This is the core function for WAIFU2X class
@ -90,14 +94,14 @@ class WrapperMain:
# list to be executed # list to be executed
# initialize the list with waifu2x binary path as the first element # 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(): for key in self.driver_settings.keys():
value = self.driver_settings[key] value = self.driver_settings[key]
# null or None means that leave this option out (keep default) # 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 continue
else: else:
if len(key) == 1: if len(key) == 1:

View File

@ -12,6 +12,7 @@ Description: This class handles all FFmpeg related operations.
# built-in imports # built-in imports
import json import json
import pathlib import pathlib
import shlex
import subprocess import subprocess
# third-party imports # third-party imports
@ -36,7 +37,7 @@ class Ffmpeg:
# video metadata # video metadata
self.image_format = image_format self.image_format = image_format
self.intermediate_file_name = pathlib.Path(self.ffmpeg_settings['intermediate_file_name']) 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): def get_pixel_formats(self):
""" Get a dictionary of supported pixel formats """ Get a dictionary of supported pixel formats
@ -49,8 +50,8 @@ class Ffmpeg:
""" """
execute = [ execute = [
self.ffmpeg_probe_binary, self.ffmpeg_probe_binary,
'-v', # '-v',
'quiet', # 'quiet',
'-pix_fmts' '-pix_fmts'
] ]
@ -74,7 +75,7 @@ class Ffmpeg:
return pixel_formats return pixel_formats
def get_video_info(self, input_video): def probe_file_info(self, input_video):
""" Gets input video information """ Gets input video information
This method reads input video information This method reads input video information
@ -104,31 +105,25 @@ class Ffmpeg:
# turn elements into str # turn elements into str
execute = [str(e) for e in execute] 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 json_str = subprocess.run(execute, check=True, stdout=subprocess.PIPE).stdout
return json.loads(json_str.decode('utf-8')) return json.loads(json_str.decode('utf-8'))
def extract_frames(self, input_video, extracted_frames): def extract_frames(self, input_file, extracted_frames):
"""Extract every frame from original videos """ extract frames from video or GIF file
This method extracts every frame from input video using FFmpeg
Arguments:
input_video {string} -- input video path
extracted_frames {string} -- video output directory
""" """
execute = [ execute = [
self.ffmpeg_binary self.ffmpeg_binary
] ]
execute.extend(self._read_configuration(phase='video_to_frames')) execute.extend(self._read_configuration(phase='input_to_frames'))
execute.extend([ execute.extend([
'-i', '-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([ execute.extend([
extracted_frames / f'extracted_%0d.{self.image_format}' extracted_frames / f'extracted_%0d.{self.image_format}'
@ -136,7 +131,7 @@ class Ffmpeg:
return(self._execute(execute)) return(self._execute(execute))
def assemble_video(self, framerate, resolution, upscaled_frames): def assemble_video(self, framerate, upscaled_frames):
"""Converts images into videos """Converts images into videos
This method converts a set of images into a video This method converts a set of images into a video
@ -149,9 +144,9 @@ class Ffmpeg:
execute = [ execute = [
self.ffmpeg_binary, self.ffmpeg_binary,
'-r', '-r',
str(framerate), str(framerate)
'-s', # '-s',
resolution # resolution
] ]
# read other options # read other options
@ -274,17 +269,9 @@ class Ffmpeg:
return configuration return configuration
def _execute(self, execute): 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 # turn all list elements into string to avoid errors
execute = [str(e) for e in execute] 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) 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 # list to be executed
# initialize the list with the binary path as the first element # 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(): for key in self.driver_settings.keys():
value = self.driver_settings[key] value = self.driver_settings[key]
# null or None means that leave this option out (keep default) # 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 continue
else: else:
if len(key) == 1: 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('-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('-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('-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('-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_file', type=str, help=argparse.SUPPRESS) # help='(required) path to input image file') parser.add_argument('-i', '--input_path', type=str, help=argparse.SUPPRESS) # help='(required) path to input image file')
return parser.parse_args(arguments) return parser.parse_args(arguments)
def load_configurations(self, upscaler): def load_configurations(self, upscaler):
@ -79,14 +79,14 @@ class WrapperMain:
# list to be executed # list to be executed
# initialize the list with waifu2x binary path as the first element # 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(): for key in self.driver_settings.keys():
value = self.driver_settings[key] value = self.driver_settings[key]
# null or None means that leave this option out (keep default) # 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 continue
else: else:
if len(key) == 1: if len(key) == 1:

View File

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

View File

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