mirror of
https://github.com/k4yt3x/video2x.git
synced 2026-02-09 06:14:45 +08:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5db2cfd2a3 | ||
|
|
652b9f34bd | ||
|
|
f3c50ea728 | ||
|
|
880087241d | ||
|
|
067e17290a | ||
|
|
fc675e7899 | ||
|
|
2d81270e44 | ||
|
|
3ace2447c2 | ||
|
|
30b0e2c7bf | ||
|
|
d54fea0310 | ||
|
|
332055a4e5 | ||
|
|
19e17b1a8f | ||
|
|
9e745fb747 | ||
|
|
fd5edead7e | ||
|
|
aa7c0b3f12 | ||
|
|
65cc4c6afb | ||
|
|
bd2da021bd | ||
|
|
7e888db7b2 | ||
|
|
7726a86e35 | ||
|
|
a0595136ab | ||
|
|
5c93a5a73b | ||
|
|
7b9b87fa9b | ||
|
|
cb255da65c | ||
|
|
d503325a62 | ||
|
|
954233c238 | ||
|
|
95416f68a8 | ||
|
|
dce778b3bf | ||
|
|
c537dd726c | ||
|
|
b1918a4a8a | ||
|
|
7ae9618785 | ||
|
|
eb3d29103c | ||
|
|
a188f6ebda | ||
|
|
fd67dfca11 | ||
|
|
b1f29f1098 | ||
|
|
fe7c0c840d | ||
|
|
dc2410d4da | ||
|
|
91ac512d57 | ||
|
|
732288f075 | ||
|
|
e812c228c3 | ||
|
|
9841fa9577 | ||
|
|
5391f59847 | ||
|
|
6180ccd9b2 | ||
|
|
e10315f89e | ||
|
|
6521e071ce | ||
|
|
a685e91b14 | ||
|
|
9d72bad314 | ||
|
|
2b63b5f0a6 | ||
|
|
9c84228c17 | ||
|
|
f9bb191336 | ||
|
|
7a3af366f8 | ||
|
|
d4223f93ec | ||
|
|
c5c764a91f | ||
|
|
ba6472c9fc | ||
|
|
dc1244580f | ||
|
|
0347d05127 | ||
|
|
45f5bf91e6 | ||
|
|
5e78beabcf | ||
|
|
6c5b7ea17f | ||
|
|
d9610abae0 | ||
|
|
47471352ba | ||
|
|
969cfe5c0d | ||
|
|
e12db41213 | ||
|
|
7a69d58e28 | ||
|
|
ad6308d385 | ||
|
|
8c3a85ac42 | ||
|
|
db603062e9 | ||
|
|
807276fcc3 | ||
|
|
9be46f7d62 | ||
|
|
f42a8ab71f | ||
|
|
e3b2624977 | ||
|
|
685d894bef | ||
|
|
b0167e9f08 | ||
|
|
74767743bd | ||
|
|
ece345e531 | ||
|
|
47e787c80c | ||
|
|
4639555626 | ||
|
|
d423fa1b6e | ||
|
|
734cf7bb0e | ||
|
|
77ad212b01 | ||
|
|
a27bdb4b63 | ||
|
|
89dfd21f97 | ||
|
|
5151be4122 | ||
|
|
f8d3fa5c25 | ||
|
|
61abcc6a3a | ||
|
|
40a4d3a602 | ||
|
|
9429b04528 | ||
|
|
55c2f7abfb | ||
|
|
c7c83d35ed | ||
|
|
ca90c5be02 | ||
|
|
5a2b387b9d | ||
|
|
eda5d70773 | ||
|
|
8b845e35b3 | ||
|
|
e3b7110ff5 | ||
|
|
cefa2c4f30 | ||
|
|
4d9c0866ce | ||
|
|
25fca78eb4 | ||
|
|
4a0ffae6c8 | ||
|
|
f94f99eb1c | ||
|
|
a9586bf508 | ||
|
|
5f69a44bf7 | ||
|
|
1ee76c0224 | ||
|
|
0b99ac33a6 | ||
|
|
4e3565b3ac | ||
|
|
b40aee0cd7 | ||
|
|
5b27cf26e1 | ||
|
|
8fe6fd4009 | ||
|
|
d4d485e132 | ||
|
|
53f5192896 | ||
|
|
e6d865a80d | ||
|
|
d25857a178 | ||
|
|
81af7b1c1b | ||
|
|
81b8e45b6f | ||
|
|
e003cbc568 | ||
|
|
f96b95563e | ||
|
|
76144b589a | ||
|
|
15d2dbb07a | ||
|
|
9d5ad2f70c | ||
|
|
b5da9ebbf7 | ||
|
|
a1e1d98929 | ||
|
|
fafd5fadd3 | ||
|
|
1280a791ec | ||
|
|
8e85d6a6d3 | ||
|
|
8b0f82e95e | ||
|
|
a6666c3c3b | ||
|
|
585164a508 | ||
|
|
6b3e1b9768 | ||
|
|
0f4daa12d2 | ||
|
|
4cf83dc565 | ||
|
|
146044505b | ||
|
|
a7d41cafdf | ||
|
|
875572c75a | ||
|
|
c7353c4bf2 | ||
|
|
0af68bb39c | ||
|
|
19cb823591 | ||
|
|
6c1a714a1e | ||
|
|
837aca371e | ||
|
|
6538abd6e5 | ||
|
|
a57465e866 | ||
|
|
405c4b6636 | ||
|
|
3d46e1a822 | ||
|
|
7b501f2c67 | ||
|
|
98e2cbdb04 | ||
|
|
25156077f2 | ||
|
|
b984977227 |
5
.github/ISSUE_TEMPLATE/bug-report.md
vendored
5
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -11,10 +11,11 @@ assignees: K4YT3X
|
||||
|
||||
|Module|Version|
|
||||
|-|-|
|
||||
|`video2x`||
|
||||
|`ffmpeg`||
|
||||
|`Video2X`||
|
||||
|`FFmpeg`||
|
||||
|`waifu2x-caffe`||
|
||||
|`waifu2x-converter-cpp`||
|
||||
|`waifu2x-ncnn-vulkan`||
|
||||
|
||||
## Symptom
|
||||
|
||||
|
||||
122
README.md
122
README.md
@@ -2,36 +2,51 @@
|
||||
|
||||
### Official Discussion Group (Telegram): https://t.me/video2x
|
||||
|
||||
## Download Builds (Windows)
|
||||
|
||||
You can go to the [releases page](https://github.com/k4yt3x/video2x/releases) to download the latest builds of `Video2X`. The exe files will require no Python or Python module installation.
|
||||
|
||||
The **`full`** package provides all packages that will possibly be needed by `Video2X`, including `FFmpeg`, `waifu2x-caffe`, `waifu2x-converter-cpp`, `waifu2x-ncnn-vulkan`, and `Anime4K`. The config file (`video2x.json`) is also already configured for the environment. All you need to do is just to launch `video2x.exe`.
|
||||
|
||||
The **`light`** package provides only the most basic functions of `Video2X`. Only `video2x.exe`, `video2x_setup.exe` and `video2x.json` are included. To setup dependencies (e.g. `FFmpeg` and `Waifu2X`) automatically, simply launch `video2x_setup.exe`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Component names that are **bolded** are mandatory.
|
||||
Component names that are **bolded** can be automatically downloaded and configured with the `video2x_setup.py` script.
|
||||
|
||||
Component names that are *italicized* can be automatically downloaded and configured with the `video2x_setup.py` script.
|
||||
|
||||
1. Operating System: Windows
|
||||
1. AMD GPU / Nvidia GPU
|
||||
1. AMD GPU driver / Nvidia GPU driver / Nvidia CUDNN
|
||||
1. [***FFMPEG***](https://ffmpeg.zeranoe.com/builds/)
|
||||
1. [***waifu2x-caffe***](https://github.com/lltcggie/waifu2x-caffe/releases) / [***waifu2x-converter-cpp***](https://github.com/DeadSix27/waifu2x-converter-cpp/releases)
|
||||
1. Operating System: Windows / Linux
|
||||
2. AMD GPU / Nvidia GPU
|
||||
3. AMD GPU driver / Nvidia GPU driver / Nvidia CUDNN
|
||||
4. [**FFmpeg**](https://ffmpeg.zeranoe.com/builds/)
|
||||
5. One of the following drivers
|
||||
- [**waifu2x-caffe**](https://github.com/lltcggie/waifu2x-caffe/releases)
|
||||
- [**waifu2x-converter-cpp**](https://github.com/DeadSix27/waifu2x-converter-cpp/releases)
|
||||
- [**waifu2x-ncnn-vulkan**](https://github.com/nihui/waifu2x-ncnn-vulkan)
|
||||
- [**Anime4K**](https://github.com/bloc97/Anime4K)
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### 2.7.0 (March 30, 2019)
|
||||
### 3.0.0 (November 26, 2019)
|
||||
|
||||
- Added support for different extracted image formats.
|
||||
- Redesigned FFMPEG wrapper, FFMPEG settings are now customizable in the `video2x.json` config file.
|
||||
- Other minor enhancements and adjustments (e.g. argument -> method variable)
|
||||
- Linux compatibility
|
||||
- Configuration file changed to YAML format
|
||||
- You may still use a JSON-formatted config file. To do so, please specify `-c video2x.json`.
|
||||
- Other code clean-up and optimization
|
||||
|
||||
### Setup Script 1.2.0 (March 26, 2019)
|
||||
### 2.10.0 (August 16, 2019)
|
||||
|
||||
- `video2x_setup.py` script can now automatically download and configure `waifu2x-converter-cpp`.
|
||||
- replaced old progress indicator with progress bar.
|
||||
- **Added support for [Anime4K](https://github.com/bloc97/Anime4K)**
|
||||
|
||||
### 2.6.3 (March 24, 2019)
|
||||
### 2.9.0 (July 27, 2019)
|
||||
|
||||
- Added image cleaner by @BrianPetkovsek which removes upscaled frames.
|
||||
- Fixed some PEP8 issues.
|
||||
- Exceptions in waifu2x are now caught, and script will now stop on waifu2x error instead of keep going on to FFMPEG.
|
||||
- Changed file handling method from `os` to `pathlib`
|
||||
- Removed f_string dependency and support for legacy versions of Python
|
||||
- Organized file import statements
|
||||
|
||||
### Setup Script 1.6.0 (November 26, 2019)
|
||||
|
||||
- Added compatibility for new YAML configuration file
|
||||
- Added better exception handling
|
||||
|
||||
## Description
|
||||
|
||||
@@ -49,23 +64,29 @@ Clip is from trailer of animated movie "千と千尋の神隠し". Copyright bel
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Documentations
|
||||
|
||||
### [Video2X Wiki](https://github.com/K4YT3X/video2x/wiki)
|
||||
### [Video2X Wiki](https://github.com/k4yt3x/video2x/wiki)
|
||||
|
||||
You can find all detailed user-facing and developer-facing documentations in the [Video2X Wiki](https://github.com/K4YT3X/video2x/wiki). It covers everything from step-by-step instructions for beginners, to the code structure of this program for advanced users and developers. If this README page doesn't answer all your questions, the wiki page is where you should head to.
|
||||
You can find all detailed user-facing and developer-facing documentations in the [Video2X Wiki](https://github.com/k4yt3x/video2x/wiki). It covers everything from step-by-step instructions for beginners, to the code structure of this program for advanced users and developers. If this README page doesn't answer all your questions, the wiki page is where you should head to.
|
||||
|
||||
### [Step-By-Step Tutorial](https://github.com/K4YT3X/video2x/wiki/Step-By-Step-Tutorial) (Nvidia GPUs)
|
||||
### [Step-By-Step Tutorial](https://github.com/k4yt3x/video2x/wiki/Step-By-Step-Tutorial)
|
||||
|
||||
For those who want a detailed walk-through of how to use `Video2X`, you can head to the [Step-By-Step Tutorial](https://github.com/K4YT3X/video2x/wiki/Step-By-Step-Tutorial) wiki page. It includes almost every step you need to perform in order to enlarge your first video.
|
||||
For those who want a detailed walk-through of how to use `Video2X`, you can head to the [Step-By-Step Tutorial](https://github.com/k4yt3x/video2x/wiki/Step-By-Step-Tutorial) wiki page. It includes almost every step you need to perform in order to enlarge your first video.
|
||||
|
||||
### [Waifu2X Drivers](https://github.com/K4YT3X/video2x/wiki/Waifu2X-Drivers)
|
||||
### [Waifu2X Drivers](https://github.com/k4yt3x/video2x/wiki/Waifu2X-Drivers)
|
||||
|
||||
Go to the [Waifu2X Drivers](https://github.com/K4YT3X/video2x/wiki/Waifu2X-Drivers) wiki page if you want to see a detailed description on the different types of `waifu2x` drivers implemented by `Video2X`. This wiki page contains detailed difference between different drivers, and how to download and set each of them up for `Video2X`.
|
||||
Go to the [Waifu2X Drivers](https://github.com/k4yt3x/video2x/wiki/Waifu2X-Drivers) wiki page if you want to see a detailed description on the different types of `waifu2x` drivers implemented by `Video2X`. This wiki page contains detailed difference between different drivers, and how to download and set each of them up for `Video2X`.
|
||||
|
||||
### [Q&A](https://github.com/k4yt3x/video2x/wiki/Q&A)
|
||||
|
||||
If you have any questions, first try visiting our [Q&A](https://github.com/k4yt3x/video2x/wiki/Q&A) page to see if your question is answered there. If not, open an issue and we will respond to your questions ASAP.
|
||||
|
||||
---
|
||||
|
||||
@@ -73,27 +94,31 @@ Go to the [Waifu2X Drivers](https://github.com/K4YT3X/video2x/wiki/Waifu2X-Drive
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Python 3**
|
||||
- **Python 3**
|
||||
Download: https://www.python.org/downloads/windows/
|
||||
- **FFMPEG Windows Build**
|
||||
Download: https://ffmpeg.org/download.html
|
||||
- **waifu2x-caffe** (for Nvidia CUDA/CUDNN)
|
||||
- **FFmpeg Windows Build**
|
||||
Download: https://ffmpeg.org/download.html
|
||||
- **waifu2x-caffe** (for Nvidia CUDA/CUDNN)
|
||||
Download: https://github.com/lltcggie/waifu2x-caffe/releases
|
||||
- **waifu2x-converter-cpp** (required for AMD, OpenCL and OpenGL processing)
|
||||
- **waifu2x-converter-cpp** (required for AMD, OpenCL and OpenGL processing)
|
||||
Download: https://github.com/DeadSix27/waifu2x-converter-cpp/releases
|
||||
- **waifu2x-ncnn-vulkan**
|
||||
Download: https://github.com/nihui/waifu2x-ncnn-vulkan/releases
|
||||
- **Anime4K**
|
||||
Download: https://github.com/bloc97/Anime4K/releases
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
First, clone the video2x repository.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/K4YT3X/video2x.git
|
||||
git clone https://github.com/k4yt3x/video2x.git
|
||||
cd video2x/bin
|
||||
```
|
||||
|
||||
Then you may run the `video2x_setup.py` script to install and configure the dependencies automatically. This script is designed and tested on Windows 10.
|
||||
|
||||
This script will install the newest version of `ffmpeg`, either or both of `waifu2x-caffe` and `waifu2x-converter-cpp` to `%LOCALAPPDATA%\\video2x` and all required python libraries.
|
||||
This script will install the newest version of `ffmpeg`, any one or all `waifu2x-caffe`, `waifu2x-converter-cpp`, and `waifu2x-ncnn-vulkan` to `%LOCALAPPDATA%\\video2x` and all required python libraries.
|
||||
|
||||
```shell
|
||||
python video2x_setup.py
|
||||
@@ -128,7 +153,7 @@ Enlarge the video to 1920x1080 using CUDA. You may also use the `-r/--ratio` opt
|
||||
python video2x.py -i sample_input.mp4 -o sample_output.mp4 -m gpu --width=1920 --height=1080
|
||||
```
|
||||
|
||||
### Nvidia CNDNN
|
||||
### Nvidia CUDNN
|
||||
|
||||
Enlarge the video to 1920x1080 using CUDNN. You may also use the `-r/--ratio` option.
|
||||
|
||||
@@ -144,9 +169,23 @@ Enlarge the video by 2 times using OpenCL. Note that `waifu2x-converter-cpp` doe
|
||||
python video2x.py -i sample_input.mp4 -o sample_output.mp4 -m gpu -r 2 -d waifu2x_converter
|
||||
```
|
||||
|
||||
### AMD or Nvidia (waifu2x-ncnn-vulkan Vulkan)
|
||||
|
||||
```shell
|
||||
python video2x.py -i sample_input.mp4 -o sample_output.mp4 -m gpu -r 2 -d waifu2x_ncnn_vulkan
|
||||
```
|
||||
|
||||
### Anime4K
|
||||
|
||||
Enlarge the video by 2 times using Anime4K. **Remember to install and configure [JRE 12](https://www.oracle.com/technetwork/java/javase/downloads/java-archive-javase12-5440181.html) path in the configuration file.**
|
||||
|
||||
```shell
|
||||
python video2x.py -i sample_input.mp4 -o sample_output.mp4 -r 2 -d anime4k
|
||||
```
|
||||
|
||||
### CPU
|
||||
|
||||
Enlarge the video to 1920x1080 using the CPU. You may also use the `-r/--ratio` option. This is potentially much slower than using a GPU. The configuration file for this method is similar to the previous methods.
|
||||
Enlarge the video to 1920x1080 using the CPU. You may also use the `-r/--ratio` option. **waifu2x-based upscalers potentially run much slower than using a GPU, but Anime4K is more CPU-dependant**. The configuration file for this method is similar to the previous methods.
|
||||
|
||||
```shell
|
||||
python video2x.py -i sample_input.mp4 -o sample_output.mp4 -m cpu --width=1920 --height=1080
|
||||
@@ -209,16 +248,25 @@ https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||

|
||||
|
||||
(C) 2018-2019 K4YT3X
|
||||
(C) 2018-2020 K4YT3X
|
||||
|
||||
## Credits
|
||||
|
||||
This project relies on the following software and projects.
|
||||
|
||||
- [FFMPEG]('https://www.ffmpeg.org/')
|
||||
- [FFmpeg]('https://www.ffmpeg.org/')
|
||||
- [waifu2x-caffe](https://github.com/lltcggie/waifu2x-caffe)
|
||||
- [waifu2x-converter-cpp](https://github.com/DeadSix27/waifu2x-converter-cpp)
|
||||
- [waifu2x-ncnn-vulkan](https://github.com/nihui/waifu2x-ncnn-vulkan)
|
||||
- [Anime4K](https://github.com/bloc97/Anime4K)
|
||||
|
||||
## Special Thanks
|
||||
|
||||
Appreciations given to the following code contributors:
|
||||
|
||||
- @BrianPetkovsek
|
||||
- @SAT3LL
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Dandere2x](https://github.com/CardinalPanda/dandere2x): `Dandere2x` is a lossy video upscaler also built around `waifu2x`, but with video compression techniques to shorten the time needed to process a video.
|
||||
- [Dandere2x](https://github.com/CardinalPanda/dandere2x): `Dandere2x` is a lossy video upscaler also built around `waifu2x`, but with video compression techniques to shorten the time needed to process a video.
|
||||
|
||||
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-slate
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- code:utf-8 -*-
|
||||
"""
|
||||
Name: Video2X Exceptions
|
||||
Dev: K4YT3X
|
||||
Date Created: December 13, 2018
|
||||
Last Modified: March 19, 2019
|
||||
"""
|
||||
|
||||
|
||||
class ArgumentError(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
147
bin/ffmpeg.py
147
bin/ffmpeg.py
@@ -1,147 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: FFMPEG Class
|
||||
Author: K4YT3X
|
||||
Date Created: Feb 24, 2018
|
||||
Last Modified: March 30, 2019
|
||||
|
||||
Description: This class handles all FFMPEG related
|
||||
operations.
|
||||
"""
|
||||
from avalon_framework import Avalon
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
class Ffmpeg:
|
||||
"""This class communicates with ffmpeg
|
||||
|
||||
This class deals with ffmpeg. It handles extracitng
|
||||
frames, stripping audio, converting images into videos
|
||||
and inserting audio tracks to videos.
|
||||
"""
|
||||
|
||||
def __init__(self, ffmpeg_settings, image_format):
|
||||
self.ffmpeg_settings = ffmpeg_settings
|
||||
|
||||
self.ffmpeg_path = self.ffmpeg_settings['ffmpeg_path']
|
||||
# add a forward slash to directory if not present
|
||||
# otherwise there will be a format error
|
||||
if self.ffmpeg_path[-1] != '/' and self.ffmpeg_path[-1] != '\\':
|
||||
self.ffmpeg_path = '{}\\'.format(self.ffmpeg_path)
|
||||
|
||||
self.ffmpeg_binary = '{}ffmpeg.exe'.format(self.ffmpeg_path)
|
||||
self.ffmpeg_probe_binary = '{}ffprobe.exe'.format(self.ffmpeg_path)
|
||||
self.image_format = image_format
|
||||
|
||||
def get_video_info(self, input_video):
|
||||
""" Gets input video information
|
||||
|
||||
This method reads input video information
|
||||
using ffprobe in dictionary.
|
||||
|
||||
Arguments:
|
||||
input_video {string} -- input video file path
|
||||
|
||||
Returns:
|
||||
dictionary -- JSON text of input video information
|
||||
"""
|
||||
|
||||
# this execution command needs to be hard-coded
|
||||
# since video2x only strictly recignizes this one format
|
||||
execute = [
|
||||
self.ffmpeg_probe_binary,
|
||||
'-v',
|
||||
'quiet',
|
||||
'-print_format',
|
||||
'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
'-i',
|
||||
'{}'.format(input_video)
|
||||
]
|
||||
|
||||
Avalon.debug_info('Executing: {}'.format(' '.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 videoin
|
||||
using ffmpeg
|
||||
|
||||
Arguments:
|
||||
input_video {string} -- input video path
|
||||
extracted_frames {string} -- video output folder
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_binary,
|
||||
'-i',
|
||||
input_video,
|
||||
'{}\\extracted_%0d.{}'.format(extracted_frames, self.image_format)
|
||||
]
|
||||
self._execute(execute=execute, phase='video_to_frames')
|
||||
|
||||
def convert_video(self, framerate, resolution, upscaled_frames):
|
||||
"""Converts images into videos
|
||||
|
||||
This method converts a set of images into a
|
||||
video.
|
||||
|
||||
Arguments:
|
||||
framerate {float} -- target video framerate
|
||||
resolution {string} -- target video resolution
|
||||
upscaled_frames {string} -- source images folder
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_binary,
|
||||
'-r',
|
||||
str(framerate),
|
||||
'-s',
|
||||
resolution,
|
||||
'-i',
|
||||
'{}\\extracted_%d.{}'.format(upscaled_frames, self.image_format),
|
||||
'{}\\no_audio.mp4'.format(upscaled_frames)
|
||||
]
|
||||
self._execute(execute=execute, phase='frames_to_video')
|
||||
|
||||
def migrate_audio_tracks_subtitles(self, input_video, output_video, upscaled_frames):
|
||||
""" Migrates audio tracks and subtitles from input video to output video
|
||||
|
||||
Arguments:
|
||||
input_video {string} -- input video file path
|
||||
output_video {string} -- output video file path
|
||||
upscaled_frames {string} -- directory containing upscaled frames
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_binary,
|
||||
'-i',
|
||||
'{}\\no_audio.mp4'.format(upscaled_frames),
|
||||
'-i',
|
||||
input_video,
|
||||
output_video
|
||||
]
|
||||
self._execute(execute=execute, phase='migrating_tracks')
|
||||
|
||||
def _execute(self, execute, phase):
|
||||
|
||||
for key in self.ffmpeg_settings[phase].keys():
|
||||
|
||||
value = self.ffmpeg_settings[phase][key]
|
||||
|
||||
# null or None means that leave this option out (keep default)
|
||||
if value is None or value is False:
|
||||
continue
|
||||
else:
|
||||
execute.append(key)
|
||||
|
||||
# true means key is an option
|
||||
if value is True:
|
||||
continue
|
||||
|
||||
execute.append(str(value))
|
||||
|
||||
Avalon.debug_info('Executing: {}'.format(execute))
|
||||
return subprocess.run(execute, shell=True, check=True).returncode
|
||||
305
bin/upscaler.py
305
bin/upscaler.py
@@ -1,305 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Video2X Upscaler
|
||||
Author: K4YT3X
|
||||
Date Created: December 10, 2018
|
||||
Last Modified: March 30, 2019
|
||||
|
||||
Licensed under the GNU General Public License Version 3 (GNU GPL v3),
|
||||
available at: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
(C) 2018-2019 K4YT3X
|
||||
"""
|
||||
|
||||
from avalon_framework import Avalon
|
||||
from exceptions import *
|
||||
from ffmpeg import Ffmpeg
|
||||
from fractions import Fraction
|
||||
from image_cleaner import ImageCleaner
|
||||
from tqdm import tqdm
|
||||
from waifu2x_caffe import Waifu2xCaffe
|
||||
from waifu2x_converter import Waifu2xConverter
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class Upscaler:
|
||||
""" An instance of this class is a upscaler that will
|
||||
upscale all images in the given folder.
|
||||
|
||||
Raises:
|
||||
Exception -- all exceptions
|
||||
ArgumentError -- if argument is not valid
|
||||
"""
|
||||
|
||||
def __init__(self, input_video, output_video, method, waifu2x_settings, ffmpeg_settings):
|
||||
# mandatory arguments
|
||||
self.input_video = input_video
|
||||
self.output_video = output_video
|
||||
self.method = method
|
||||
self.waifu2x_settings = waifu2x_settings
|
||||
self.ffmpeg_settings = ffmpeg_settings
|
||||
|
||||
# optional arguments
|
||||
self.waifu2x_driver = 'waifu2x_caffe'
|
||||
self.scale_width = None
|
||||
self.scale_height = None
|
||||
self.scale_ratio = None
|
||||
self.model_dir = None
|
||||
self.threads = 5
|
||||
self.video2x_cache_folder = '{}\\video2x'.format(tempfile.gettempdir())
|
||||
self.image_format = 'png'
|
||||
self.preserve_frames = False
|
||||
|
||||
# create temporary folder/directories
|
||||
self.extracted_frames = tempfile.mkdtemp(dir=self.video2x_cache_folder)
|
||||
Avalon.debug_info('Extracted frames are being saved to: {}'.format(self.extracted_frames))
|
||||
self.upscaled_frames = tempfile.mkdtemp(dir=self.video2x_cache_folder)
|
||||
Avalon.debug_info('Upscaled frames are being saved to: {}'.format(self.upscaled_frames))
|
||||
|
||||
def cleanup(self):
|
||||
# delete temp directories when done
|
||||
# avalon framework cannot be used if python is shutting down
|
||||
# therefore, plain print is used
|
||||
if not self.preserve_frames:
|
||||
|
||||
for directory in [self.extracted_frames, self.upscaled_frames]:
|
||||
try:
|
||||
print('Cleaning up cache directory: {}'.format(directory))
|
||||
shutil.rmtree(directory)
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
def _check_arguments(self):
|
||||
# check if arguments are valid / all necessary argument
|
||||
# values are specified
|
||||
if not self.input_video:
|
||||
raise ArgumentError('You need to specify the video to process')
|
||||
elif (not self.scale_width or not self.scale_height) and not self.scale_ratio:
|
||||
raise ArgumentError('You must specify output video width and height or upscale factor')
|
||||
elif not self.output_video:
|
||||
raise ArgumentError('You need to specify the output video name')
|
||||
elif not self.method:
|
||||
raise ArgumentError('You need to specify the enlarging processing unit')
|
||||
|
||||
def _progress_bar(self, extracted_frames_folders):
|
||||
""" This method prints a progress bar
|
||||
|
||||
This method prints a progress bar by keeping track
|
||||
of the amount of frames in the input directory/folder
|
||||
and the output directory/folder. This is originally
|
||||
suggested by @ArmandBernard.
|
||||
"""
|
||||
# get number of extracted frames
|
||||
total_frames = 0
|
||||
for folder in extracted_frames_folders:
|
||||
total_frames += len([f for f in os.listdir(folder) if f[-4:] == '.png'])
|
||||
|
||||
with tqdm(total=total_frames, ascii=True, desc='Upscaling Progress') as progress_bar:
|
||||
|
||||
# tqdm update method adds the value to the progress
|
||||
# bar instead of setting the value. Therefore, a delta
|
||||
# needs to be calculated.
|
||||
previous_cycle_frames = 0
|
||||
while not self.progress_bar_exit_signal:
|
||||
|
||||
try:
|
||||
total_frames_upscaled = len([f for f in os.listdir(self.upscaled_frames) if f[-4:] == '.png'])
|
||||
delta = total_frames_upscaled - previous_cycle_frames
|
||||
previous_cycle_frames = total_frames_upscaled
|
||||
|
||||
# if upscaling is finished
|
||||
if total_frames_upscaled >= total_frames:
|
||||
return
|
||||
|
||||
# adds the detla into the progress bar
|
||||
progress_bar.update(delta)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def _upscale_frames(self, w2):
|
||||
""" 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
|
||||
"""
|
||||
|
||||
# progress bar thread exit signal
|
||||
self.progress_bar_exit_signal = False
|
||||
|
||||
# create a container for exceptions in threads
|
||||
# if this thread is not empty, then an exception has occured
|
||||
self.upscaler_exceptions = []
|
||||
|
||||
# it's easier to do multi-threading with waifu2x_converter
|
||||
# the number of threads can be passed directly to waifu2x_converter
|
||||
if self.waifu2x_driver == 'waifu2x_converter':
|
||||
|
||||
progress_bar = threading.Thread(target=self._progress_bar, args=([self.extracted_frames],))
|
||||
progress_bar.start()
|
||||
|
||||
w2.upscale(self.extracted_frames, self.upscaled_frames, self.scale_ratio, self.threads, self.image_format, self.upscaler_exceptions)
|
||||
for image in [f for f in os.listdir(self.upscaled_frames) if os.path.isfile(os.path.join(self.upscaled_frames, f))]:
|
||||
renamed = re.sub('_\[.*-.*\]\[x(\d+(\.\d+)?)\]\.{}'.format(self.image_format), '.{}'.format(self.image_format), image)
|
||||
shutil.move('{}\\{}'.format(self.upscaled_frames, image), '{}\\{}'.format(self.upscaled_frames, renamed))
|
||||
|
||||
self.progress_bar_exit_signal = True
|
||||
progress_bar.join()
|
||||
return
|
||||
|
||||
# create a container for all upscaler threads
|
||||
upscaler_threads = []
|
||||
|
||||
# list all images in the extracted frames
|
||||
frames = [os.path.join(self.extracted_frames, f) for f in os.listdir(self.extracted_frames) if os.path.isfile(os.path.join(self.extracted_frames, f))]
|
||||
|
||||
# if we have less images than threads,
|
||||
# create only the threads necessary
|
||||
if len(frames) < self.threads:
|
||||
self.threads = len(frames)
|
||||
|
||||
# create a folder for each thread and append folder
|
||||
# name into a list
|
||||
|
||||
thread_pool = []
|
||||
thread_folders = []
|
||||
for thread_id in range(self.threads):
|
||||
thread_folder = '{}\\{}'.format(self.extracted_frames, str(thread_id))
|
||||
thread_folders.append(thread_folder)
|
||||
|
||||
# delete old folders and create new folders
|
||||
if os.path.isdir(thread_folder):
|
||||
shutil.rmtree(thread_folder)
|
||||
os.mkdir(thread_folder)
|
||||
|
||||
# append folder path into list
|
||||
thread_pool.append((thread_folder, thread_id))
|
||||
|
||||
# evenly distribute images into each folder
|
||||
# until there is none left in the folder
|
||||
for image in frames:
|
||||
# move image
|
||||
shutil.move(image, thread_pool[0][0])
|
||||
# rotate list
|
||||
thread_pool = thread_pool[-1:] + thread_pool[:-1]
|
||||
|
||||
# create threads and start them
|
||||
for thread_info in thread_pool:
|
||||
# create thread
|
||||
if self.scale_ratio:
|
||||
thread = threading.Thread(target=w2.upscale, args=(thread_info[0], self.upscaled_frames, self.scale_ratio, False, False, self.image_format, self.upscaler_exceptions))
|
||||
else:
|
||||
thread = threading.Thread(target=w2.upscale, args=(thread_info[0], self.upscaled_frames, False, self.scale_width, self.scale_height, self.image_format, self.upscaler_exceptions))
|
||||
thread.name = thread_info[1]
|
||||
|
||||
# add threads into the pool
|
||||
upscaler_threads.append(thread)
|
||||
|
||||
# start progress bar in a different thread
|
||||
progress_bar = threading.Thread(target=self._progress_bar, args=(thread_folders,))
|
||||
progress_bar.start()
|
||||
|
||||
# create the clearer and start it
|
||||
Avalon.debug_info('Starting upscaled image cleaner')
|
||||
image_cleaner = ImageCleaner(self.extracted_frames, self.upscaled_frames, len(upscaler_threads))
|
||||
image_cleaner.start()
|
||||
|
||||
# start all threads
|
||||
for thread in upscaler_threads:
|
||||
thread.start()
|
||||
|
||||
# wait for threads to finish
|
||||
for thread in upscaler_threads:
|
||||
thread.join()
|
||||
|
||||
# upscaling done, kill the clearer
|
||||
Avalon.debug_info('Killing upscaled image cleaner')
|
||||
image_cleaner.stop()
|
||||
|
||||
self.progress_bar_exit_signal = True
|
||||
|
||||
if len(self.upscaler_exceptions) != 0:
|
||||
raise(self.upscaler_exceptions[0])
|
||||
|
||||
def run(self):
|
||||
"""Main controller for Video2X
|
||||
|
||||
This function controls the flow of video conversion
|
||||
and handles all necessary functions.
|
||||
"""
|
||||
|
||||
# parse arguments for waifu2x
|
||||
# check argument sanity
|
||||
self._check_arguments()
|
||||
|
||||
# convert paths to absolute paths
|
||||
self.input_video = os.path.abspath(self.input_video)
|
||||
self.output_video = os.path.abspath(self.output_video)
|
||||
|
||||
# initialize objects for ffmpeg and waifu2x-caffe
|
||||
fm = Ffmpeg(self.ffmpeg_settings, self.image_format)
|
||||
|
||||
# initialize waifu2x driver
|
||||
if self.waifu2x_driver == 'waifu2x_caffe':
|
||||
w2 = Waifu2xCaffe(self.waifu2x_settings, self.method, self.model_dir)
|
||||
elif self.waifu2x_driver == 'waifu2x_converter':
|
||||
w2 = Waifu2xConverter(self.waifu2x_settings, self.model_dir)
|
||||
else:
|
||||
raise Exception('Unrecognized waifu2x driver: {}'.format(self.waifu2x_driver))
|
||||
|
||||
# extract frames from video
|
||||
fm.extract_frames(self.input_video, self.extracted_frames)
|
||||
|
||||
Avalon.info('Reading video information')
|
||||
video_info = fm.get_video_info(self.input_video)
|
||||
# analyze original video with ffprobe and retrieve framerate
|
||||
# width, height = info['streams'][0]['width'], info['streams'][0]['height']
|
||||
|
||||
# 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')
|
||||
exit(1)
|
||||
|
||||
# get average frame rate of video stream
|
||||
framerate = float(Fraction(video_info['streams'][video_stream_index]['avg_frame_rate']))
|
||||
Avalon.info('Framerate: {}'.format(framerate))
|
||||
|
||||
# width/height will be coded width/height x upscale factor
|
||||
if self.scale_ratio:
|
||||
coded_width = video_info['streams'][video_stream_index]['coded_width']
|
||||
coded_height = video_info['streams'][video_stream_index]['coded_height']
|
||||
self.scale_width = int(self.scale_ratio * coded_width)
|
||||
self.scale_height = int(self.scale_ratio * coded_height)
|
||||
|
||||
# upscale images one by one using waifu2x
|
||||
Avalon.info('Starting to upscale extracted images')
|
||||
self._upscale_frames(w2)
|
||||
Avalon.info('Upscaling completed')
|
||||
|
||||
# frames to Video
|
||||
Avalon.info('Converting extracted frames into video')
|
||||
|
||||
# use user defined output size
|
||||
fm.convert_video(framerate, '{}x{}'.format(self.scale_width, self.scale_height), self.upscaled_frames)
|
||||
Avalon.info('Conversion completed')
|
||||
|
||||
# migrate audio tracks and subtitles
|
||||
Avalon.info('Migrating audio tracks and subtitles to upscaled video')
|
||||
fm.migrate_audio_tracks_subtitles(self.input_video, self.output_video, self.upscaled_frames)
|
||||
307
bin/video2x.py
307
bin/video2x.py
@@ -1,307 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
__ __ _ _ ___ __ __
|
||||
\ \ / / (_) | | |__ \ \ \ / /
|
||||
\ \ / / _ __| | ___ ___ ) | \ V /
|
||||
\ \/ / | | / _` | / _ \ / _ \ / / > <
|
||||
\ / | | | (_| | | __/ | (_) | / /_ / . \
|
||||
\/ |_| \__,_| \___| \___/ |____| /_/ \_\
|
||||
|
||||
|
||||
Name: Video2X Controller
|
||||
Author: K4YT3X
|
||||
Date Created: Feb 24, 2018
|
||||
Last Modified: March 30, 2019
|
||||
|
||||
Licensed under the GNU General Public License Version 3 (GNU GPL v3),
|
||||
available at: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
(C) 2018-2019 K4YT3X
|
||||
|
||||
Video2X is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Video2X is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Description: Video2X is an automation software based on waifu2x image
|
||||
enlarging engine. It extracts frames from a video, enlarge it by a
|
||||
number of times without losing any details or quality, keeping lines
|
||||
smooth and edges sharp.
|
||||
"""
|
||||
from avalon_framework import Avalon
|
||||
from upscaler import Upscaler
|
||||
import argparse
|
||||
import GPUtil
|
||||
import json
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
|
||||
VERSION = '2.7.0'
|
||||
|
||||
# each thread might take up to 2.5 GB during initialization.
|
||||
# (system memory, not to be confused with GPU memory)
|
||||
SYS_MEM_PER_THREAD = 2.5
|
||||
GPU_MEM_PER_THREAD = 3.5
|
||||
|
||||
|
||||
def process_arguments():
|
||||
"""Processes CLI arguments
|
||||
|
||||
This function parses all arguments
|
||||
This allows users to customize options
|
||||
for the output video.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
# video options
|
||||
file_options = parser.add_argument_group('File Options')
|
||||
file_options.add_argument('-i', '--input', help='Source video file/directory', action='store', required=True)
|
||||
file_options.add_argument('-o', '--output', help='Output video file/directory', action='store', required=True)
|
||||
|
||||
# upscaler options
|
||||
upscaler_options = parser.add_argument_group('Upscaler Options')
|
||||
upscaler_options.add_argument('-m', '--method', help='Upscaling method', action='store', default='gpu', choices=['cpu', 'gpu', 'cudnn'], required=True)
|
||||
upscaler_options.add_argument('-d', '--driver', help='Waifu2x driver', action='store', default='waifu2x_caffe', choices=['waifu2x_caffe', 'waifu2x_converter'])
|
||||
upscaler_options.add_argument('-y', '--model_dir', help='Folder containing model JSON files', action='store')
|
||||
upscaler_options.add_argument('-t', '--threads', help='Number of threads to use for upscaling', action='store', type=int, default=5)
|
||||
upscaler_options.add_argument('-c', '--config', help='Video2X config file location', action='store', default='{}\\video2x.json'.format(os.path.dirname(os.path.abspath(sys.argv[0]))))
|
||||
upscaler_options.add_argument('-b', '--batch', help='Enable batch mode (select all default values to questions)', action='store_true')
|
||||
|
||||
# scaling options
|
||||
scaling_options = parser.add_argument_group('Scaling Options')
|
||||
scaling_options.add_argument('--width', help='Output video width', action='store', type=int)
|
||||
scaling_options.add_argument('--height', help='Output video height', action='store', type=int)
|
||||
scaling_options.add_argument('-r', '--ratio', help='Scaling ratio', action='store', type=float)
|
||||
|
||||
# parse arguments
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def print_logo():
|
||||
print('__ __ _ _ ___ __ __')
|
||||
print('\\ \\ / / (_) | | |__ \\ \\ \\ / /')
|
||||
print(' \\ \\ / / _ __| | ___ ___ ) | \\ V /')
|
||||
print(' \\ \\/ / | | / _` | / _ \\ / _ \\ / / > <')
|
||||
print(' \\ / | | | (_| | | __/ | (_) | / /_ / . \\')
|
||||
print(' \\/ |_| \\__,_| \\___| \\___/ |____| /_/ \\_\\')
|
||||
print('\n Video2X Video Enlarger')
|
||||
spaces = ((44 - len("Version {}".format(VERSION))) // 2) * " "
|
||||
print('{}\n{} Version {}\n{}'.format(Avalon.FM.BD, spaces, VERSION, Avalon.FM.RST))
|
||||
|
||||
|
||||
def check_memory():
|
||||
""" Check usable system memory
|
||||
Warn the user if insufficient memory is available for
|
||||
the number of threads that the user have chosen.
|
||||
"""
|
||||
|
||||
memory_status = []
|
||||
# get system available memory
|
||||
system_memory_available = psutil.virtual_memory().available / (1024 ** 3)
|
||||
memory_status.append(('system', system_memory_available))
|
||||
|
||||
# check if Nvidia-smi is available
|
||||
# GPUtil requires nvidia-smi.exe to interact with GPU
|
||||
if args.method == 'gpu' or args.method == 'cudnn':
|
||||
if not os.path.isfile('C:\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe'):
|
||||
# Nvidia System Management Interface not available
|
||||
Avalon.warning('Nvidia-smi not available, skipping available memory check')
|
||||
Avalon.warning('If you experience error \"cudaSuccess out of memory\", try reducing number of threads you\'re using')
|
||||
else:
|
||||
try:
|
||||
# "0" is GPU ID. Both waifu2x drivers use the first GPU available, therefore only 0 makes sense
|
||||
gpu_memory_available = (GPUtil.getGPUs()[0].memoryTotal - GPUtil.getGPUs()[0].memoryUsed) / 1024
|
||||
memory_status.append(('GPU', gpu_memory_available))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# go though each checkable memory type and check availability
|
||||
for memory_type, memory_available in memory_status:
|
||||
|
||||
if memory_type == 'system':
|
||||
mem_per_thread = SYS_MEM_PER_THREAD
|
||||
else:
|
||||
mem_per_thread = GPU_MEM_PER_THREAD
|
||||
|
||||
# if user doesn't even have enough memory to run even one thread
|
||||
if memory_available < mem_per_thread:
|
||||
Avalon.warning('You might have insufficient amount of {} memory available to run this program ({} GB)'.format(memory_type, memory_available))
|
||||
Avalon.warning('Proceed with caution')
|
||||
if args.threads > 1:
|
||||
if Avalon.ask('Reduce number of threads to avoid crashing?', default=True, batch=args.batch):
|
||||
args.threads = 1
|
||||
# if memory available is less than needed, warn the user
|
||||
elif memory_available < (mem_per_thread * args.threads):
|
||||
Avalon.warning('Each waifu2x-caffe thread will require up to 2.5 GB of system memory')
|
||||
Avalon.warning('You demanded {} threads to be created, but you only have {} GB {} memory available'.format(args.threads, round(memory_available, 4), memory_type))
|
||||
Avalon.warning('{} GB of {} memory is recommended for {} threads'.format(mem_per_thread * args.threads, memory_type, args.threads))
|
||||
Avalon.warning('With your current amount of {} memory available, {} threads is recommended'.format(memory_type, int(memory_available // mem_per_thread)))
|
||||
|
||||
# ask the user if he / she wants to change to the recommended
|
||||
# number of threads
|
||||
if Avalon.ask('Change to the recommended value?', default=True, batch=args.batch):
|
||||
args.threads = int(memory_available // mem_per_thread)
|
||||
else:
|
||||
Avalon.warning('Proceed with caution')
|
||||
|
||||
|
||||
def read_config(config_file):
|
||||
""" Reads configuration file
|
||||
|
||||
Returns a dictionary read by JSON.
|
||||
"""
|
||||
with open(config_file, 'r') as raw_config:
|
||||
config = json.load(raw_config)
|
||||
return config
|
||||
|
||||
|
||||
# /////////////////// Execution /////////////////// #
|
||||
|
||||
# this is not a library
|
||||
if __name__ != '__main__':
|
||||
Avalon.error('This file cannot be imported')
|
||||
raise ImportError('{} cannot be imported'.format(__file__))
|
||||
|
||||
print_logo()
|
||||
|
||||
# process CLI arguments
|
||||
args = process_arguments()
|
||||
|
||||
# arguments sanity check
|
||||
if args.driver == 'waifu2x_converter' and args.width and args.height:
|
||||
Avalon.error('Waifu2x Converter CPP accepts only scaling ratio')
|
||||
exit(1)
|
||||
if (args.width or args.height) and args.ratio:
|
||||
Avalon.error('You can only specify either scaling ratio or output width and height')
|
||||
exit(1)
|
||||
if (args.width and not args.height) or (not args.width and args.height):
|
||||
Avalon.error('You must specify both width and height')
|
||||
exit(1)
|
||||
|
||||
# check available memory
|
||||
check_memory()
|
||||
|
||||
# read configurations from JSON
|
||||
config = read_config(args.config)
|
||||
|
||||
# load waifu2x configuration
|
||||
if args.driver == 'waifu2x_caffe':
|
||||
waifu2x_settings = config['waifu2x_caffe']
|
||||
if not os.path.isfile(waifu2x_settings['waifu2x_caffe_path']):
|
||||
Avalon.error('Specified waifu2x-caffe directory doesn\'t exist')
|
||||
Avalon.error('Please check the configuration file settings')
|
||||
raise FileNotFoundError(waifu2x_settings['waifu2x_caffe_path'])
|
||||
elif args.driver == 'waifu2x_converter':
|
||||
waifu2x_settings = config['waifu2x_converter']
|
||||
if not os.path.isdir(waifu2x_settings['waifu2x_converter_path']):
|
||||
Avalon.error('Specified waifu2x-conver-cpp directory doesn\'t exist')
|
||||
Avalon.error('Please check the configuration file settings')
|
||||
raise FileNotFoundError(waifu2x_settings['waifu2x_converter_path'])
|
||||
|
||||
# check if waifu2x path is valid
|
||||
|
||||
|
||||
# read FFMPEG configuration
|
||||
ffmpeg_settings = config['ffmpeg']
|
||||
|
||||
# load video2x settings
|
||||
video2x_cache_folder = config['video2x']['video2x_cache_folder']
|
||||
image_format = config['video2x']['image_format'].lower()
|
||||
preserve_frames = config['video2x']['preserve_frames']
|
||||
|
||||
# create temp directories if they don't exist
|
||||
if not video2x_cache_folder:
|
||||
video2x_cache_folder = '{}\\video2x'.format(tempfile.gettempdir())
|
||||
|
||||
if video2x_cache_folder and not os.path.isdir(video2x_cache_folder):
|
||||
if not os.path.isfile(video2x_cache_folder) and not os.path.islink(video2x_cache_folder):
|
||||
Avalon.warning('Specified cache folder/directory {} does not exist'.format(video2x_cache_folder))
|
||||
if Avalon.ask('Create folder/directory?', default=True, batch=args.batch):
|
||||
if os.mkdir(video2x_cache_folder) is None:
|
||||
Avalon.info('{} created'.format(video2x_cache_folder))
|
||||
else:
|
||||
Avalon.error('Unable to create {}'.format(video2x_cache_folder))
|
||||
Avalon.error('Aborting...')
|
||||
exit(1)
|
||||
else:
|
||||
Avalon.error('Specified cache folder/directory is a file/link')
|
||||
Avalon.error('Unable to continue, exiting...')
|
||||
exit(1)
|
||||
|
||||
|
||||
# start execution
|
||||
try:
|
||||
# start timer
|
||||
begin_time = time.time()
|
||||
|
||||
if os.path.isfile(args.input):
|
||||
""" Upscale single video file """
|
||||
Avalon.info('Upscaling single video file: {}'.format(args.input))
|
||||
upscaler = Upscaler(input_video=args.input, output_video=args.output, method=args.method, waifu2x_settings=waifu2x_settings, ffmpeg_settings=ffmpeg_settings)
|
||||
|
||||
# set optional options
|
||||
upscaler.waifu2x_driver = args.driver
|
||||
upscaler.scale_width = args.width
|
||||
upscaler.scale_height = args.height
|
||||
upscaler.scale_ratio = args.ratio
|
||||
upscaler.model_dir = args.model_dir
|
||||
upscaler.threads = args.threads
|
||||
upscaler.video2x_cache_folder = video2x_cache_folder
|
||||
upscaler.image_format = image_format
|
||||
upscaler.preserve_frames = preserve_frames
|
||||
|
||||
# run upscaler-
|
||||
upscaler.run()
|
||||
upscaler.cleanup()
|
||||
elif os.path.isdir(args.input):
|
||||
""" Upscale videos in a folder/directory """
|
||||
Avalon.info('Upscaling videos in folder/directory: {}'.format(args.input))
|
||||
for input_video in [f for f in os.listdir(args.input) if os.path.isfile(os.path.join(args.input, f))]:
|
||||
output_video = '{}\\{}'.format(args.output, input_video)
|
||||
upscaler = Upscaler(input_video=os.path.join(args.input, input_video), output_video=output_video, method=args.method, waifu2x_settings=waifu2x_settings, ffmpeg_settings=ffmpeg_settings)
|
||||
|
||||
# set optional options
|
||||
upscaler.waifu2x_driver = args.driver
|
||||
upscaler.scale_width = args.width
|
||||
upscaler.scale_height = args.height
|
||||
upscaler.scale_ratio = args.ratio
|
||||
upscaler.model_dir = args.model_dir
|
||||
upscaler.threads = args.threads
|
||||
upscaler.video2x_cache_folder = video2x_cache_folder
|
||||
upscaler.image_format = image_format
|
||||
upscaler.preserve_frames = preserve_frames
|
||||
|
||||
# run upscaler
|
||||
upscaler.run()
|
||||
upscaler.cleanup()
|
||||
else:
|
||||
Avalon.error('Input path is neither a file nor a folder/directory')
|
||||
raise FileNotFoundError('{} is neither file nor folder/directory'.format(args.input))
|
||||
|
||||
Avalon.info('Program completed, taking {} seconds'.format(round((time.time() - begin_time), 5)))
|
||||
except Exception:
|
||||
Avalon.error('An exception has occurred')
|
||||
traceback.print_exc()
|
||||
Avalon.warning('If you experience error \"cudaSuccess out of memory\", try reducing number of threads you\'re using')
|
||||
finally:
|
||||
# remove Video2X Cache folder
|
||||
try:
|
||||
if not preserve_frames:
|
||||
shutil.rmtree(video2x_cache_folder)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Video2X Setup Script
|
||||
Author: K4YT3X
|
||||
Date Created: November 28, 2018
|
||||
Last Modified: March 26, 2019
|
||||
|
||||
Licensed under the GNU General Public License Version 3 (GNU GPL v3),
|
||||
available at: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
(C) 2018-2019 K4YT3X
|
||||
|
||||
Description: This script helps installing all dependencies of video2x
|
||||
and generates a configuration for it.
|
||||
|
||||
Installation Details:
|
||||
- ffmpeg: %LOCALAPPDATA%\\video2x\\ffmpeg
|
||||
- waifu2x-caffe: %LOCALAPPDATA%\\video2x\\waifu2x-caffe
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import traceback
|
||||
import zipfile
|
||||
|
||||
# Requests doesn't come with windows, therefore
|
||||
# it will be installed as a dependency and imported
|
||||
# later in the script.
|
||||
# import requests
|
||||
|
||||
VERSION = '1.2.0'
|
||||
|
||||
|
||||
def process_arguments():
|
||||
"""Processes CLI arguments
|
||||
"""
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
# video options
|
||||
general_options = parser.add_argument_group('General Options')
|
||||
general_options.add_argument('-d', '--driver', help='driver to download and configure', action='store', choices=['all', 'waifu2x_caffe', 'waifu2x_converter'], default='all')
|
||||
|
||||
# parse arguments
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
class Video2xSetup:
|
||||
""" Install dependencies for video2x video enlarger
|
||||
|
||||
This library is meant to be executed as a stand-alone
|
||||
script. All files will be installed under %LOCALAPPDATA%\\video2x.
|
||||
"""
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
self.trash = []
|
||||
|
||||
def run(self):
|
||||
|
||||
print('\nInstalling Python libraries')
|
||||
self._install_python_requirements()
|
||||
|
||||
print('\nInstalling FFMPEG')
|
||||
self._install_ffmpeg()
|
||||
|
||||
if self.driver == 'all':
|
||||
self._install_waifu2x_caffe()
|
||||
self._install_waifu2x_converter_cpp()
|
||||
elif self.driver == 'waifu2x_caffe':
|
||||
self._install_waifu2x_caffe()
|
||||
elif self.driver == 'waifu2x_converter':
|
||||
self._install_waifu2x_converter_cpp()
|
||||
|
||||
print('\nGenerating Video2X configuration file')
|
||||
self._generate_config()
|
||||
|
||||
print('\nCleaning up temporary files')
|
||||
self._cleanup()
|
||||
|
||||
def _install_python_requirements(self):
|
||||
""" Read requirements.txt and return its content
|
||||
"""
|
||||
with open('requirements.txt', 'r') as req:
|
||||
for line in req:
|
||||
package = line.split('==')[0]
|
||||
pip_install(package)
|
||||
|
||||
def _cleanup(self):
|
||||
""" Cleanup all the temp files downloaded
|
||||
"""
|
||||
for file in self.trash:
|
||||
try:
|
||||
print('Deleting: {}'.format(file))
|
||||
os.remove(file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _install_ffmpeg(self):
|
||||
""" Install FFMPEG
|
||||
"""
|
||||
latest_release = 'https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-latest-win64-static.zip'
|
||||
|
||||
ffmpeg_zip = download(latest_release, tempfile.gettempdir())
|
||||
self.trash.append(ffmpeg_zip)
|
||||
|
||||
with zipfile.ZipFile(ffmpeg_zip) as zipf:
|
||||
zipf.extractall('{}\\video2x'.format(os.getenv('localappdata')))
|
||||
|
||||
def _install_waifu2x_caffe(self):
|
||||
""" Install waifu2x_caffe
|
||||
"""
|
||||
print('\nInstalling waifu2x-caffe')
|
||||
import requests
|
||||
|
||||
# Get latest release of waifu2x-caffe via GitHub API
|
||||
latest_release = json.loads(requests.get('https://api.github.com/repos/lltcggie/waifu2x-caffe/releases/latest').content)
|
||||
|
||||
for a in latest_release['assets']:
|
||||
if 'waifu2x-caffe.zip' in a['browser_download_url']:
|
||||
waifu2x_caffe_zip = download(a['browser_download_url'], tempfile.gettempdir())
|
||||
self.trash.append(waifu2x_caffe_zip)
|
||||
|
||||
with zipfile.ZipFile(waifu2x_caffe_zip) as zipf:
|
||||
zipf.extractall('{}\\video2x'.format(os.getenv('localappdata')))
|
||||
|
||||
def _install_waifu2x_converter_cpp(self):
|
||||
""" Install waifu2x_caffe
|
||||
"""
|
||||
print('\nInstalling waifu2x-converter-cpp')
|
||||
import re
|
||||
import requests
|
||||
|
||||
# Get latest release of waifu2x-caffe via GitHub API
|
||||
latest_release = json.loads(requests.get('https://api.github.com/repos/DeadSix27/waifu2x-converter-cpp/releases/latest').content)
|
||||
|
||||
for a in latest_release['assets']:
|
||||
if re.search(r'waifu2x-DeadSix27-win64_v[0-9]*\.zip', a['browser_download_url']):
|
||||
waifu2x_converter_cpp_zip = download(a['browser_download_url'], tempfile.gettempdir())
|
||||
self.trash.append(waifu2x_converter_cpp_zip)
|
||||
|
||||
with zipfile.ZipFile(waifu2x_converter_cpp_zip) as zipf:
|
||||
zipf.extractall('{}\\video2x\\waifu2x-converter-cpp'.format(os.getenv('localappdata')))
|
||||
|
||||
def _generate_config(self):
|
||||
""" Generate video2x config
|
||||
"""
|
||||
# Open current video2x.json file as template
|
||||
with open('video2x.json', 'r') as template:
|
||||
template_dict = json.load(template)
|
||||
template.close()
|
||||
|
||||
# configure only the specified drivers
|
||||
if self.driver == 'all':
|
||||
template_dict['waifu2x_caffe']['waifu2x_caffe_path'] = '{}\\video2x\\waifu2x-caffe\\waifu2x-caffe-cui.exe'.format(os.getenv('localappdata'))
|
||||
template_dict['waifu2x_converter']['waifu2x_converter_path'] = '{}\\video2x\\waifu2x-converter-cpp'.format(os.getenv('localappdata'))
|
||||
elif self.driver == 'waifu2x_caffe':
|
||||
template_dict['waifu2x_caffe']['waifu2x_caffe_path'] = '{}\\video2x\\waifu2x-caffe\\waifu2x-caffe-cui.exe'.format(os.getenv('localappdata'))
|
||||
elif self.driver == 'waifu2x_converter':
|
||||
template_dict['waifu2x_converter']['waifu2x_converter_path'] = '{}\\video2x\\waifu2x-converter-cpp'.format(os.getenv('localappdata'))
|
||||
|
||||
template_dict['ffmpeg']['ffmpeg_path'] = '{}\\video2x\\ffmpeg-latest-win64-static\\bin'.format(os.getenv('localappdata'))
|
||||
template_dict['ffmpeg']['ffmpeg_hwaccel'] = 'auto'
|
||||
template_dict['ffmpeg']['extra_arguments'] = []
|
||||
template_dict['video2x']['video2x_cache_folder'] = False
|
||||
template_dict['video2x']['preserve_frames'] = False
|
||||
|
||||
# Write configuration into file
|
||||
with open('video2x.json', 'w') as config:
|
||||
json.dump(template_dict, config, indent=2)
|
||||
config.close()
|
||||
|
||||
|
||||
def download(url, save_path, chunk_size=4096):
|
||||
""" Download file to local with requests library
|
||||
"""
|
||||
from tqdm import tqdm
|
||||
import requests
|
||||
|
||||
output_file = '{}\\{}'.format(save_path, url.split('/')[-1])
|
||||
print('Downloading: {}'.format(url))
|
||||
print('Chunk size: {}'.format(chunk_size))
|
||||
print('Saving to: {}'.format(output_file))
|
||||
|
||||
stream = requests.get(url, stream=True)
|
||||
total_size = int(stream.headers['content-length'])
|
||||
|
||||
# Write content into file
|
||||
with open(output_file, 'wb') as output:
|
||||
with tqdm(total=total_size, ascii=True) as progress_bar:
|
||||
for chunk in stream.iter_content(chunk_size=chunk_size):
|
||||
if chunk:
|
||||
output.write(chunk)
|
||||
progress_bar.update(len(chunk))
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def pip_install(package):
|
||||
""" Install python package via python pip module
|
||||
|
||||
pip.main() is not available after pip 9.0.1, thus
|
||||
pip module is not used in this case.
|
||||
"""
|
||||
return subprocess.run(['python', '-m', 'pip', 'install', '-U', package]).returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
args = process_arguments()
|
||||
print('Video2x Setup Script')
|
||||
print('Version: {}'.format(VERSION))
|
||||
setup = Video2xSetup(args.driver)
|
||||
setup.run()
|
||||
print('\n Script finished successfully')
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('An error has occurred')
|
||||
print('Video2X Automatic Setup has failed')
|
||||
exit(1)
|
||||
92
src/anime4k.py
Executable file
92
src/anime4k.py
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Anime4K Driver
|
||||
Author: K4YT3X
|
||||
Date Created: August 15, 2019
|
||||
Last Modified: November 15, 2019
|
||||
|
||||
Description: This class is a high-level wrapper
|
||||
for Anime4k.
|
||||
"""
|
||||
|
||||
# built-in imports
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
|
||||
|
||||
class Anime4k:
|
||||
"""This class communicates with Anime4K engine
|
||||
|
||||
An object will be created for this class, containing information
|
||||
about the binary address and the processing method. When being called
|
||||
by the main program, other detailed information will be passed to
|
||||
the upscale function.
|
||||
"""
|
||||
|
||||
def __init__(self, driver_settings):
|
||||
self.driver_settings = driver_settings
|
||||
self.print_lock = threading.Lock()
|
||||
|
||||
def upscale(self, input_directory, output_directory, scale_ratio, upscaler_exceptions, push_strength=None, push_grad_strength=None):
|
||||
""" Anime4K wrapper
|
||||
|
||||
Arguments:
|
||||
file_in {string} -- input file path
|
||||
file_out {string} -- output file path
|
||||
|
||||
Keyword Arguments:
|
||||
scale {int} -- scale ratio (default: {None})
|
||||
push_strength {int} -- residual push strength (default: {None})
|
||||
push_grad_strength {int} -- residual gradient push strength (default: {None})
|
||||
|
||||
Returns:
|
||||
subprocess.Popen.returncode -- command line return value of execution
|
||||
"""
|
||||
try:
|
||||
# return value is the sum of all execution return codes
|
||||
return_value = 0
|
||||
|
||||
# get a list lof all image files in input_directory
|
||||
extracted_frame_files = [f for f in input_directory.iterdir() if str(f).lower().endswith('.png') or str(f).lower().endswith('.jpg')]
|
||||
|
||||
# upscale each image in input_directory
|
||||
for image in extracted_frame_files:
|
||||
|
||||
execute = [
|
||||
self.driver_settings['java_path'],
|
||||
'-jar',
|
||||
self.driver_settings['path'],
|
||||
str(image.absolute()),
|
||||
str(output_directory / image.name),
|
||||
str(scale_ratio)
|
||||
]
|
||||
|
||||
# optional arguments
|
||||
kwargs = [
|
||||
'push_strength',
|
||||
'push_grad_strength'
|
||||
]
|
||||
|
||||
# if optional argument specified, append value to execution list
|
||||
for arg in kwargs:
|
||||
if locals()[arg] is not None:
|
||||
execute.extend([locals([arg])])
|
||||
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info(f'Executing: {execute}', )
|
||||
self.print_lock.release()
|
||||
return_value += subprocess.run(execute, check=True).returncode
|
||||
|
||||
# print thread exiting message
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info(f'[upscaler] Thread {threading.current_thread().name} exiting')
|
||||
self.print_lock.release()
|
||||
|
||||
# return command execution return code
|
||||
return return_value
|
||||
except Exception as e:
|
||||
upscaler_exceptions.append(e)
|
||||
28
src/exceptions.py
Executable file
28
src/exceptions.py
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Video2X Exceptions
|
||||
Dev: K4YT3X
|
||||
Date Created: December 13, 2018
|
||||
Last Modified: July 27, 2019
|
||||
"""
|
||||
|
||||
|
||||
class ArgumentError(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class StreamNotFoundError(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class UnrecognizedDriverError(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class UnsupportedPixelError(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
287
src/ffmpeg.py
Executable file
287
src/ffmpeg.py
Executable file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Video2X FFmpeg Controller
|
||||
Author: K4YT3X
|
||||
Date Created: Feb 24, 2018
|
||||
Last Modified: November 15, 2019
|
||||
|
||||
Description: This class handles all FFmpeg related operations.
|
||||
"""
|
||||
|
||||
# built-in imports
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
|
||||
|
||||
class Ffmpeg:
|
||||
"""This class communicates with FFmpeg
|
||||
|
||||
This class deals with FFmpeg. It handles extracting
|
||||
frames, stripping audio, converting images into videos
|
||||
and inserting audio tracks to videos.
|
||||
"""
|
||||
|
||||
def __init__(self, ffmpeg_settings, image_format):
|
||||
self.ffmpeg_settings = ffmpeg_settings
|
||||
|
||||
self.ffmpeg_path = pathlib.Path(self.ffmpeg_settings['ffmpeg_path'])
|
||||
self.ffmpeg_binary = self.ffmpeg_path / 'ffmpeg'
|
||||
self.ffmpeg_probe_binary = self.ffmpeg_path / 'ffprobe'
|
||||
self.image_format = image_format
|
||||
self.pixel_format = None
|
||||
|
||||
def get_pixel_formats(self):
|
||||
""" Get a dictionary of supported pixel formats
|
||||
|
||||
List all supported pixel formats and their
|
||||
corresponding bit depth.
|
||||
|
||||
Returns:
|
||||
dictionary -- JSON dict of all pixel formats to bit depth
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_probe_binary,
|
||||
'-v',
|
||||
'quiet',
|
||||
'-pix_fmts'
|
||||
]
|
||||
|
||||
# turn elements into str
|
||||
execute = [str(e) for e in execute]
|
||||
|
||||
Avalon.debug_info(f'Executing: {" ".join(execute)}')
|
||||
|
||||
# initialize dictionary to store pixel formats
|
||||
pixel_formats = {}
|
||||
|
||||
# record all pixel formats into dictionary
|
||||
for line in subprocess.run(execute, check=True, stdout=subprocess.PIPE).stdout.decode().split('\n'):
|
||||
try:
|
||||
pixel_formats[' '.join(line.split()).split()[1]] = int(' '.join(line.split()).split()[3])
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
|
||||
# print pixel formats for debugging
|
||||
Avalon.debug_info(str(pixel_formats))
|
||||
|
||||
return pixel_formats
|
||||
|
||||
def get_video_info(self, input_video):
|
||||
""" Gets input video information
|
||||
|
||||
This method reads input video information
|
||||
using ffprobe in dictionary
|
||||
|
||||
Arguments:
|
||||
input_video {string} -- input video file path
|
||||
|
||||
Returns:
|
||||
dictionary -- JSON text of input video information
|
||||
"""
|
||||
|
||||
# this execution command needs to be hard-coded
|
||||
# since video2x only strictly recignizes this one format
|
||||
execute = [
|
||||
self.ffmpeg_probe_binary,
|
||||
'-v',
|
||||
'quiet',
|
||||
'-print_format',
|
||||
'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
'-i',
|
||||
input_video
|
||||
]
|
||||
|
||||
# turn elements into str
|
||||
execute = [str(e) for e in execute]
|
||||
|
||||
Avalon.debug_info(f'Executing: {" ".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
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_binary
|
||||
]
|
||||
|
||||
execute.extend(self._read_configuration(phase='video_to_frames'))
|
||||
|
||||
execute.extend([
|
||||
'-i',
|
||||
input_video
|
||||
])
|
||||
|
||||
execute.extend(self._read_configuration(phase='video_to_frames', section='output_options'))
|
||||
|
||||
execute.extend([
|
||||
extracted_frames / f'extracted_%0d.{self.image_format}'
|
||||
])
|
||||
|
||||
self._execute(execute)
|
||||
|
||||
def convert_video(self, framerate, resolution, upscaled_frames):
|
||||
"""Converts images into videos
|
||||
|
||||
This method converts a set of images into a video
|
||||
|
||||
Arguments:
|
||||
framerate {float} -- target video framerate
|
||||
resolution {string} -- target video resolution
|
||||
upscaled_frames {string} -- source images directory
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_binary,
|
||||
'-r',
|
||||
str(framerate),
|
||||
'-s',
|
||||
resolution
|
||||
]
|
||||
|
||||
# read other options
|
||||
execute.extend(self._read_configuration(phase='frames_to_video'))
|
||||
|
||||
# read FFmpeg input options
|
||||
execute.extend(self._read_configuration(phase='frames_to_video', section='input_options'))
|
||||
|
||||
# WORKAROUND FOR WAIFU2X-NCNN-VULKAN
|
||||
# Dev: SAT3LL
|
||||
# rename all .png.png suffixes to .png
|
||||
import re
|
||||
regex = re.compile(r'\.png\.png$', re.IGNORECASE)
|
||||
for frame_name in upscaled_frames.iterdir():
|
||||
(upscaled_frames / frame_name).rename(upscaled_frames / regex.sub('.png', str(frame_name)))
|
||||
# END WORKAROUND
|
||||
|
||||
# append input frames path into command
|
||||
execute.extend([
|
||||
'-i',
|
||||
upscaled_frames / f'extracted_%d.{self.image_format}'
|
||||
])
|
||||
|
||||
# read FFmpeg output options
|
||||
execute.extend(self._read_configuration(phase='frames_to_video', section='output_options'))
|
||||
|
||||
# specify output file location
|
||||
execute.extend([
|
||||
upscaled_frames / 'no_audio.mp4'
|
||||
])
|
||||
|
||||
self._execute(execute)
|
||||
|
||||
def migrate_audio_tracks_subtitles(self, input_video, output_video, upscaled_frames):
|
||||
""" Migrates audio tracks and subtitles from input video to output video
|
||||
|
||||
Arguments:
|
||||
input_video {string} -- input video file path
|
||||
output_video {string} -- output video file path
|
||||
upscaled_frames {string} -- directory containing upscaled frames
|
||||
"""
|
||||
execute = [
|
||||
self.ffmpeg_binary
|
||||
]
|
||||
|
||||
execute.extend(self._read_configuration(phase='migrating_tracks'))
|
||||
|
||||
execute.extend([
|
||||
'-i',
|
||||
upscaled_frames / 'no_audio.mp4',
|
||||
'-i',
|
||||
input_video
|
||||
])
|
||||
|
||||
execute.extend(self._read_configuration(phase='migrating_tracks', section='output_options'))
|
||||
|
||||
execute.extend([
|
||||
output_video
|
||||
])
|
||||
|
||||
self._execute(execute)
|
||||
|
||||
def _read_configuration(self, phase, section=None):
|
||||
""" read configuration from JSON
|
||||
|
||||
Read the configurations (arguments) from the JSON
|
||||
configuration file and append them to the end of the
|
||||
FFmpeg command.
|
||||
|
||||
Arguments:
|
||||
execute {list} -- list of arguments to be executed
|
||||
phase {str} -- phase of operation
|
||||
"""
|
||||
|
||||
configuration = []
|
||||
|
||||
# if section is specified, read configurations or keys
|
||||
# from only that section
|
||||
if section:
|
||||
source = self.ffmpeg_settings[phase][section].keys()
|
||||
|
||||
# if pixel format is not specified, use the source pixel format
|
||||
try:
|
||||
if self.ffmpeg_settings[phase][section].get('-pix_fmt') is None:
|
||||
self.ffmpeg_settings[phase][section]['-pix_fmt'] = self.pixel_format
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
source = self.ffmpeg_settings[phase].keys()
|
||||
|
||||
for key in source:
|
||||
|
||||
if section:
|
||||
value = self.ffmpeg_settings[phase][section][key]
|
||||
else:
|
||||
value = self.ffmpeg_settings[phase][key]
|
||||
|
||||
# null or None means that leave this option out (keep default)
|
||||
if value is None or value is False or isinstance(value, dict):
|
||||
continue
|
||||
|
||||
# if the value is a list, append the same argument and all values
|
||||
elif isinstance(value, list):
|
||||
|
||||
for subvalue in value:
|
||||
configuration.append(key)
|
||||
if value is not True:
|
||||
configuration.append(str(subvalue))
|
||||
|
||||
# otherwise the value is typical
|
||||
else:
|
||||
configuration.append(key)
|
||||
|
||||
# true means key is an option
|
||||
if value is True:
|
||||
continue
|
||||
|
||||
configuration.append(str(value))
|
||||
|
||||
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}')
|
||||
|
||||
return subprocess.run(execute, check=True).returncode
|
||||
36
bin/image_cleaner.py → src/image_cleaner.py
Normal file → Executable file
36
bin/image_cleaner.py → src/image_cleaner.py
Normal file → Executable file
@@ -5,13 +5,13 @@ Name: Video2X Image Cleaner
|
||||
Author: BrianPetkovsek
|
||||
Author: K4YT3X
|
||||
Date Created: March 24, 2019
|
||||
Last Modified: March 24, 2019
|
||||
Last Modified: July 27, 2019
|
||||
|
||||
Description: This class is to remove the extracted frames
|
||||
that have already been upscaled.
|
||||
"""
|
||||
|
||||
import os
|
||||
# built-in imports
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -27,11 +27,11 @@ class ImageCleaner(threading.Thread):
|
||||
threading.Thread
|
||||
"""
|
||||
|
||||
def __init__(self, input_folder, output_folder, num_threads):
|
||||
def __init__(self, input_directory, output_directory, threads):
|
||||
threading.Thread.__init__(self)
|
||||
self.input_folder = input_folder
|
||||
self.output_folder = output_folder
|
||||
self.num_threads = num_threads
|
||||
self.input_directory = input_directory
|
||||
self.output_directory = output_directory
|
||||
self.threads = threads
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
@@ -53,24 +53,24 @@ class ImageCleaner(threading.Thread):
|
||||
""" remove frames that have already been upscaled
|
||||
|
||||
This method compares the files in the extracted frames
|
||||
folder with the upscaled frames folder, and removes
|
||||
directory with the upscaled frames directory, and removes
|
||||
the frames that has already been upscaled.
|
||||
"""
|
||||
|
||||
# list all images in the extracted frames
|
||||
output_frames = [f for f in os.listdir(self.output_folder) if os.path.isfile(os.path.join(self.output_folder, f))]
|
||||
output_frames = [f for f in self.output_directory.iterdir() if f.is_file()]
|
||||
|
||||
# compare and remove frames downscaled images that finished being upscaled
|
||||
# within each thread's extracted frames folder
|
||||
for i in range(self.num_threads):
|
||||
dir_path = os.path.join(self.input_folder, str(i))
|
||||
# within each thread's extracted frames directory
|
||||
for thread_id in range(self.threads):
|
||||
dir_path = self.input_directory / str(thread_id)
|
||||
|
||||
# for each file within all the folders
|
||||
for f in os.listdir(dir_path):
|
||||
file_path = os.path.join(dir_path, f)
|
||||
# for each file within all the directories
|
||||
for file in dir_path.iterdir():
|
||||
file_path = dir_path / file
|
||||
|
||||
# if file also exists in the output folder, then the file
|
||||
# if file also exists in the output directory, then the file
|
||||
# has already been processed, thus not needed anymore
|
||||
if os.path.isfile(file_path) and f in output_frames:
|
||||
os.remove(file_path)
|
||||
output_frames.remove(f)
|
||||
if file_path.is_file() and file in output_frames:
|
||||
file_path.unlink(file)
|
||||
output_frames.remove(file)
|
||||
@@ -2,5 +2,6 @@ avalon_framework
|
||||
colorama
|
||||
GPUtil
|
||||
psutil
|
||||
pyyaml
|
||||
requests
|
||||
tqdm
|
||||
369
src/upscaler.py
Executable file
369
src/upscaler.py
Executable file
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Video2X Upscaler
|
||||
Author: K4YT3X
|
||||
Date Created: December 10, 2018
|
||||
Last Modified: December 11, 2019
|
||||
|
||||
Dev: SAT3LL
|
||||
|
||||
Licensed under the GNU General Public License Version 3 (GNU GPL v3),
|
||||
available at: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
(C) 2018-2019 K4YT3X
|
||||
"""
|
||||
|
||||
# local imports
|
||||
from anime4k import Anime4k
|
||||
from exceptions import *
|
||||
from ffmpeg import Ffmpeg
|
||||
from image_cleaner import ImageCleaner
|
||||
from waifu2x_caffe import Waifu2xCaffe
|
||||
from waifu2x_converter import Waifu2xConverter
|
||||
from waifu2x_ncnn_vulkan import Waifu2xNcnnVulkan
|
||||
|
||||
# built-in imports
|
||||
from fractions import Fraction
|
||||
import contextlib
|
||||
import copy
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
from tqdm import tqdm
|
||||
|
||||
AVAILABLE_DRIVERS = ['waifu2x_caffe', 'waifu2x_converter', 'waifu2x_ncnn_vulkan', 'anime4k']
|
||||
|
||||
|
||||
class Upscaler:
|
||||
""" An instance of this class is a upscaler that will
|
||||
upscale all images in the given directory.
|
||||
|
||||
Raises:
|
||||
Exception -- all exceptions
|
||||
ArgumentError -- if argument is not valid
|
||||
"""
|
||||
|
||||
def __init__(self, input_video, output_video, method, driver_settings, ffmpeg_settings):
|
||||
# mandatory arguments
|
||||
self.input_video = input_video
|
||||
self.output_video = output_video
|
||||
self.method = method
|
||||
self.driver_settings = driver_settings
|
||||
self.ffmpeg_settings = ffmpeg_settings
|
||||
|
||||
# optional arguments
|
||||
self.waifu2x_driver = 'waifu2x_caffe'
|
||||
self.scale_width = None
|
||||
self.scale_height = None
|
||||
self.scale_ratio = None
|
||||
self.model_dir = None
|
||||
self.threads = 5
|
||||
self.video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x'
|
||||
self.image_format = 'png'
|
||||
self.preserve_frames = False
|
||||
|
||||
def create_temp_directories(self):
|
||||
"""create temporary directory
|
||||
"""
|
||||
self.extracted_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory))
|
||||
Avalon.debug_info(f'Extracted frames are being saved to: {self.extracted_frames}')
|
||||
self.upscaled_frames = pathlib.Path(tempfile.mkdtemp(dir=self.video2x_cache_directory))
|
||||
Avalon.debug_info(f'Upscaled frames are being saved to: {self.upscaled_frames}')
|
||||
|
||||
def cleanup_temp_directories(self):
|
||||
"""delete temp directories when done
|
||||
"""
|
||||
if not self.preserve_frames:
|
||||
for directory in [self.extracted_frames, self.upscaled_frames, self.video2x_cache_directory]:
|
||||
try:
|
||||
# avalon framework cannot be used if python is shutting down
|
||||
# therefore, plain print is used
|
||||
print(f'Cleaning up cache directory: {directory}')
|
||||
shutil.rmtree(directory)
|
||||
except (OSError, FileNotFoundError):
|
||||
print(f'Unable to delete: {directory}')
|
||||
traceback.print_exc()
|
||||
|
||||
def _check_arguments(self):
|
||||
# check if arguments are valid / all necessary argument
|
||||
# values are specified
|
||||
if not self.input_video:
|
||||
raise ArgumentError('You need to specify the video to process')
|
||||
elif (not self.scale_width or not self.scale_height) and not self.scale_ratio:
|
||||
raise ArgumentError('You must specify output video width and height or upscale factor')
|
||||
elif not self.output_video:
|
||||
raise ArgumentError('You need to specify the output video name')
|
||||
elif not self.method:
|
||||
raise ArgumentError('You need to specify the enlarging processing unit')
|
||||
|
||||
def _progress_bar(self, extracted_frames_directories):
|
||||
""" This method prints a progress bar
|
||||
|
||||
This method prints a progress bar by keeping track
|
||||
of the amount of frames in the input directory
|
||||
and the output directory. This is originally
|
||||
suggested by @ArmandBernard.
|
||||
"""
|
||||
|
||||
# get number of extracted frames
|
||||
self.total_frames = 0
|
||||
for directory in extracted_frames_directories:
|
||||
self.total_frames += len([f for f in directory.iterdir() if str(f)[-4:] == f'.{self.image_format}'])
|
||||
|
||||
with tqdm(total=self.total_frames, ascii=True, desc='Upscaling Progress') as progress_bar:
|
||||
|
||||
# tqdm update method adds the value to the progress
|
||||
# bar instead of setting the value. Therefore, a delta
|
||||
# needs to be calculated.
|
||||
previous_cycle_frames = 0
|
||||
while not self.progress_bar_exit_signal:
|
||||
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
self.total_frames_upscaled = len([f for f in self.upscaled_frames.iterdir() if str(f)[-4:] == f'.{self.image_format}'])
|
||||
delta = self.total_frames_upscaled - previous_cycle_frames
|
||||
previous_cycle_frames = self.total_frames_upscaled
|
||||
|
||||
# if upscaling is finished
|
||||
if self.total_frames_upscaled >= self.total_frames:
|
||||
return
|
||||
|
||||
# adds the delta into the progress bar
|
||||
progress_bar.update(delta)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def _upscale_frames(self):
|
||||
""" 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
|
||||
"""
|
||||
|
||||
# progress bar thread exit signal
|
||||
self.progress_bar_exit_signal = False
|
||||
|
||||
# create a container for exceptions in threads
|
||||
# if this thread is not empty, then an exception has occured
|
||||
self.upscaler_exceptions = []
|
||||
|
||||
# initialize waifu2x driver
|
||||
drivers = AVAILABLE_DRIVERS
|
||||
if self.waifu2x_driver not in drivers:
|
||||
raise UnrecognizedDriverError(f'Unrecognized waifu2x driver: {self.waifu2x_driver}')
|
||||
|
||||
# it's easier to do multi-threading with waifu2x_converter
|
||||
# the number of threads can be passed directly to waifu2x_converter
|
||||
if self.waifu2x_driver == 'waifu2x_converter':
|
||||
w2 = Waifu2xConverter(self.driver_settings, self.model_dir)
|
||||
|
||||
progress_bar = threading.Thread(target=self._progress_bar, args=([self.extracted_frames],))
|
||||
progress_bar.start()
|
||||
|
||||
w2.upscale(self.extracted_frames, self.upscaled_frames, self.scale_ratio, self.threads, self.image_format, self.upscaler_exceptions)
|
||||
for image in [f for f in self.upscaled_frames.iterdir() if f.is_file()]:
|
||||
renamed = re.sub(f'_\[.*-.*\]\[x(\d+(\.\d+)?)\]\.{self.image_format}', f'.{self.image_format}', str(image))
|
||||
(self.upscaled_frames / image).rename(self.upscaled_frames / renamed)
|
||||
|
||||
self.progress_bar_exit_signal = True
|
||||
progress_bar.join()
|
||||
return
|
||||
|
||||
# drivers that are to be multi-threaded by video2x
|
||||
else:
|
||||
# create a container for all upscaler threads
|
||||
upscaler_threads = []
|
||||
|
||||
# list all images in the extracted frames
|
||||
frames = [(self.extracted_frames / f) for f in self.extracted_frames.iterdir() if f.is_file]
|
||||
|
||||
# if we have less images than threads,
|
||||
# create only the threads necessary
|
||||
if len(frames) < self.threads:
|
||||
self.threads = len(frames)
|
||||
|
||||
# create a directory for each thread and append directory
|
||||
# name into a list
|
||||
|
||||
thread_pool = []
|
||||
thread_directories = []
|
||||
for thread_id in range(self.threads):
|
||||
thread_directory = self.extracted_frames / str(thread_id)
|
||||
thread_directories.append(thread_directory)
|
||||
|
||||
# delete old directories and create new directories
|
||||
if thread_directory.is_dir():
|
||||
shutil.rmtree(thread_directory)
|
||||
thread_directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# append directory path into list
|
||||
thread_pool.append((thread_directory, thread_id))
|
||||
|
||||
# evenly distribute images into each directory
|
||||
# until there is none left in the directory
|
||||
for image in frames:
|
||||
# move image
|
||||
image.rename(thread_pool[0][0] / image.name)
|
||||
# rotate list
|
||||
thread_pool = thread_pool[-1:] + thread_pool[:-1]
|
||||
|
||||
# create threads and start them
|
||||
for thread_info in thread_pool:
|
||||
|
||||
# create a separate w2 instance for each thread
|
||||
if self.waifu2x_driver == 'waifu2x_caffe':
|
||||
w2 = Waifu2xCaffe(copy.deepcopy(self.driver_settings), self.method, self.model_dir, self.bit_depth)
|
||||
if self.scale_ratio:
|
||||
thread = threading.Thread(target=w2.upscale,
|
||||
args=(thread_info[0],
|
||||
self.upscaled_frames,
|
||||
self.scale_ratio,
|
||||
False,
|
||||
False,
|
||||
self.image_format,
|
||||
self.upscaler_exceptions))
|
||||
else:
|
||||
thread = threading.Thread(target=w2.upscale,
|
||||
args=(thread_info[0],
|
||||
self.upscaled_frames,
|
||||
False,
|
||||
self.scale_width,
|
||||
self.scale_height,
|
||||
self.image_format,
|
||||
self.upscaler_exceptions))
|
||||
|
||||
# if the driver being used is waifu2x_ncnn_vulkan
|
||||
elif self.waifu2x_driver == 'waifu2x_ncnn_vulkan':
|
||||
w2 = Waifu2xNcnnVulkan(copy.deepcopy(self.driver_settings))
|
||||
thread = threading.Thread(target=w2.upscale,
|
||||
args=(thread_info[0],
|
||||
self.upscaled_frames,
|
||||
self.scale_ratio,
|
||||
self.upscaler_exceptions))
|
||||
|
||||
# if the driver being used is anime4k
|
||||
elif self.waifu2x_driver == 'anime4k':
|
||||
w2 = Anime4k(copy.deepcopy(self.driver_settings))
|
||||
thread = threading.Thread(target=w2.upscale,
|
||||
args=(thread_info[0],
|
||||
self.upscaled_frames,
|
||||
self.scale_ratio,
|
||||
self.upscaler_exceptions))
|
||||
|
||||
# create thread
|
||||
thread.name = thread_info[1]
|
||||
|
||||
# add threads into the pool
|
||||
upscaler_threads.append(thread)
|
||||
|
||||
# start progress bar in a different thread
|
||||
progress_bar = threading.Thread(target=self._progress_bar, args=(thread_directories,))
|
||||
progress_bar.start()
|
||||
|
||||
# create the clearer and start it
|
||||
Avalon.debug_info('Starting upscaled image cleaner')
|
||||
image_cleaner = ImageCleaner(self.extracted_frames, self.upscaled_frames, len(upscaler_threads))
|
||||
image_cleaner.start()
|
||||
|
||||
# start all threads
|
||||
for thread in upscaler_threads:
|
||||
thread.start()
|
||||
|
||||
# wait for threads to finish
|
||||
for thread in upscaler_threads:
|
||||
thread.join()
|
||||
|
||||
# upscaling done, kill the clearer
|
||||
Avalon.debug_info('Killing upscaled image cleaner')
|
||||
image_cleaner.stop()
|
||||
|
||||
self.progress_bar_exit_signal = True
|
||||
|
||||
if len(self.upscaler_exceptions) != 0:
|
||||
raise(self.upscaler_exceptions[0])
|
||||
|
||||
def run(self):
|
||||
"""Main controller for Video2X
|
||||
|
||||
This function controls the flow of video conversion
|
||||
and handles all necessary functions.
|
||||
"""
|
||||
|
||||
# parse arguments for waifu2x
|
||||
# check argument sanity
|
||||
self._check_arguments()
|
||||
|
||||
# convert paths to absolute paths
|
||||
self.input_video = self.input_video.absolute()
|
||||
self.output_video = self.output_video.absolute()
|
||||
|
||||
# initialize objects for ffmpeg and waifu2x-caffe
|
||||
fm = Ffmpeg(self.ffmpeg_settings, self.image_format)
|
||||
|
||||
# extract frames from video
|
||||
fm.extract_frames(self.input_video, self.extracted_frames)
|
||||
|
||||
Avalon.info('Reading video information')
|
||||
video_info = fm.get_video_info(self.input_video)
|
||||
# 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]['avg_frame_rate']))
|
||||
fm.pixel_format = video_info['streams'][video_stream_index]['pix_fmt']
|
||||
|
||||
# get a dict of all pixel formats and corresponding bit depth
|
||||
pixel_formats = fm.get_pixel_formats()
|
||||
|
||||
try:
|
||||
self.bit_depth = pixel_formats[fm.pixel_format]
|
||||
except KeyError:
|
||||
Avalon.error(f'Unsupported pixel format: {fm.pixel_format}')
|
||||
raise UnsupportedPixelError(f'unsupported pixel format {fm.pixel_format}')
|
||||
|
||||
Avalon.info(f'Framerate: {framerate}')
|
||||
|
||||
# width/height will be coded width/height x upscale factor
|
||||
if self.scale_ratio:
|
||||
original_width = video_info['streams'][video_stream_index]['width']
|
||||
original_height = video_info['streams'][video_stream_index]['height']
|
||||
self.scale_width = int(self.scale_ratio * original_width)
|
||||
self.scale_height = int(self.scale_ratio * original_height)
|
||||
|
||||
# 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')
|
||||
|
||||
# use user defined output size
|
||||
fm.convert_video(framerate, f'{self.scale_width}x{self.scale_height}', self.upscaled_frames)
|
||||
Avalon.info('Conversion completed')
|
||||
|
||||
# migrate audio tracks and subtitles
|
||||
Avalon.info('Migrating audio tracks and subtitles to upscaled video')
|
||||
fm.migrate_audio_tracks_subtitles(self.input_video, self.output_video, self.upscaled_frames)
|
||||
@@ -39,34 +39,63 @@
|
||||
"output": null,
|
||||
"input": null
|
||||
},
|
||||
"waifu2x_ncnn_vulkan": {
|
||||
"waifu2x_ncnn_vulkan_path": "C:\\Users\\K4YT3X\\AppData\\Local\\video2x\\waifu2x-ncnn-vulkan\\waifu2x-ncnn-vulkan.exe",
|
||||
"v": null,
|
||||
"i": null,
|
||||
"o": null,
|
||||
"n": 2,
|
||||
"s": 2,
|
||||
"t": 400,
|
||||
"m": "models-cunet",
|
||||
"g": 0,
|
||||
"j": "1:2:2"
|
||||
},
|
||||
"anime4k": {
|
||||
"anime4k_path": "C:\\Users\\K4YT3X\\AppData\\Local\\video2x\\anime4k\\Anime4K.jar",
|
||||
"java_path": "C:\\Program Files\\Java\\jdk-12.0.2\\bin\\java.exe"
|
||||
},
|
||||
"ffmpeg": {
|
||||
"ffmpeg_path": "C:\\Users\\K4YT3X\\AppData\\Local\\video2x\\ffmpeg-latest-win64-static\\bin",
|
||||
"video_to_frames": {
|
||||
"output_options": {
|
||||
"-qscale:v": null,
|
||||
"-hwaccel": "auto",
|
||||
"-y": true
|
||||
"-pix_fmt": "rgba64be"
|
||||
},
|
||||
"-hwaccel": "auto",
|
||||
"-y": true
|
||||
},
|
||||
"frames_to_video": {
|
||||
"input_options": {
|
||||
"-qscale:v": null,
|
||||
"-qscale:a": null,
|
||||
"-f": "image2",
|
||||
"-f": "image2"
|
||||
},
|
||||
"output_options": {
|
||||
"-vcodec": "libx264",
|
||||
"-crf": 25,
|
||||
"-pix_fmt": "yuv420p",
|
||||
"-hwaccel": "auto",
|
||||
"-y": true
|
||||
"-crf": 17,
|
||||
"-b:v": null,
|
||||
"-pix_fmt": null
|
||||
},
|
||||
"-hwaccel": "auto",
|
||||
"-y": true
|
||||
},
|
||||
"migrating_tracks": {
|
||||
"-map": "0:v:0?",
|
||||
"-map": "1?",
|
||||
"output_options": {
|
||||
"-map": [
|
||||
"0:v:0?",
|
||||
"1?",
|
||||
"-1:v?"
|
||||
],
|
||||
"-c": "copy",
|
||||
"-map": "-1:v?",
|
||||
"-hwaccel": "auto",
|
||||
"-y": true
|
||||
"-pix_fmt": null
|
||||
},
|
||||
"-hwaccel": "auto",
|
||||
"-y": true
|
||||
}
|
||||
},
|
||||
"video2x": {
|
||||
"video2x_cache_folder": null,
|
||||
"video2x_cache_directory": null,
|
||||
"image_format": "png",
|
||||
"preserve_frames": false
|
||||
}
|
||||
446
src/video2x.py
Executable file
446
src/video2x.py
Executable file
@@ -0,0 +1,446 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""
|
||||
|
||||
__ __ _ _ ___ __ __
|
||||
\ \ / / (_) | | |__ \ \ \ / /
|
||||
\ \ / / _ __| | ___ ___ ) | \ V /
|
||||
\ \/ / | | / _` | / _ \ / _ \ / / > <
|
||||
\ / | | | (_| | | __/ | (_) | / /_ / . \
|
||||
\/ |_| \__,_| \___| \___/ |____| /_/ \_\
|
||||
|
||||
|
||||
Name: Video2X Controller
|
||||
Creator: K4YT3X
|
||||
Date Created: Feb 24, 2018
|
||||
Last Modified: January 4, 2020
|
||||
|
||||
Editor: BrianPetkovsek
|
||||
Editor: SAT3LL
|
||||
|
||||
Licensed under the GNU General Public License Version 3 (GNU GPL v3),
|
||||
available at: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
(C) 2018 - 2020 K4YT3X
|
||||
|
||||
Video2X is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Video2X is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Description: Video2X is an automation software based on waifu2x image
|
||||
enlarging engine. It extracts frames from a video, enlarge it by a
|
||||
number of times without losing any details or quality, keeping lines
|
||||
smooth and edges sharp.
|
||||
"""
|
||||
|
||||
# local imports
|
||||
from exceptions import *
|
||||
from upscaler import AVAILABLE_DRIVERS
|
||||
from upscaler import Upscaler
|
||||
|
||||
# built-in imports
|
||||
import argparse
|
||||
import contextlib
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
import yaml
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
import GPUtil
|
||||
import psutil
|
||||
|
||||
|
||||
VERSION = '3.0.0'
|
||||
|
||||
LEGAL_INFO = f'''Video2X Version: {VERSION}
|
||||
Author: K4YT3X
|
||||
License: GNU GPL v3
|
||||
Github Page: https://github.com/k4yt3x/video2x
|
||||
Contact: k4yt3x@k4yt3x.com'''
|
||||
|
||||
LOGO = r'''
|
||||
__ __ _ _ ___ __ __
|
||||
\ \ / / (_) | | |__ \ \ \ / /
|
||||
\ \ / / _ __| | ___ ___ ) | \ V /
|
||||
\ \/ / | | / _` | / _ \ / _ \ / / > <
|
||||
\ / | | | (_| | | __/ | (_) | / /_ / . \
|
||||
\/ |_| \__,_| \___| \___/ |____| /_/ \_\
|
||||
'''
|
||||
|
||||
# each thread might take up to 2.5 GB during initialization.
|
||||
# (system memory, not to be confused with GPU memory)
|
||||
SYS_MEM_PER_THREAD = 2.5
|
||||
GPU_MEM_PER_THREAD = 3.5
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Processes CLI arguments
|
||||
|
||||
This function parses all arguments
|
||||
This allows users to customize options
|
||||
for the output video.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
# video options
|
||||
file_options = parser.add_argument_group('File Options')
|
||||
file_options.add_argument('-i', '--input', type=pathlib.Path, help='source video file/directory', action='store')
|
||||
file_options.add_argument('-o', '--output', type=pathlib.Path, help='output video file/directory', action='store')
|
||||
|
||||
# upscaler options
|
||||
upscaler_options = parser.add_argument_group('Upscaler Options')
|
||||
upscaler_options.add_argument('-m', '--method', help='upscaling method', action='store', default='gpu', choices=['cpu', 'gpu', 'cudnn'])
|
||||
upscaler_options.add_argument('-d', '--driver', help='upscaling driver', action='store', default='waifu2x_caffe', choices=AVAILABLE_DRIVERS)
|
||||
upscaler_options.add_argument('-y', '--model_dir', type=pathlib.Path, help='directory containing model JSON files', action='store')
|
||||
upscaler_options.add_argument('-t', '--threads', help='number of threads to use for upscaling', action='store', type=int, default=1)
|
||||
upscaler_options.add_argument('-c', '--config', type=pathlib.Path, help='video2x config file location', action='store', default=pathlib.Path(sys.argv[0]).parent.absolute() / 'video2x.yaml')
|
||||
upscaler_options.add_argument('-b', '--batch', help='enable batch mode (select all default values to questions)', action='store_true')
|
||||
|
||||
# scaling options
|
||||
scaling_options = parser.add_argument_group('Scaling Options')
|
||||
scaling_options.add_argument('--width', help='output video width', action='store', type=int)
|
||||
scaling_options.add_argument('--height', help='output video height', action='store', type=int)
|
||||
scaling_options.add_argument('-r', '--ratio', help='scaling ratio', action='store', type=float)
|
||||
|
||||
# extra options
|
||||
extra_options = parser.add_argument_group('Extra Options')
|
||||
extra_options.add_argument('-v', '--version', help='display version, lawful information and exit', action='store_true')
|
||||
|
||||
# parse arguments
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def print_logo():
|
||||
"""print video2x logo"""
|
||||
print(LOGO)
|
||||
print(f'\n{"Video2X Video Enlarger".rjust(40, " ")}')
|
||||
print(f'\n{Avalon.FM.BD}{f"Version {VERSION}".rjust(36, " ")}{Avalon.FM.RST}\n')
|
||||
|
||||
|
||||
def check_memory():
|
||||
""" Check usable system memory
|
||||
Warn the user if insufficient memory is available for
|
||||
the number of threads that the user have chosen.
|
||||
"""
|
||||
|
||||
memory_status = []
|
||||
# get system available memory
|
||||
system_memory_available = psutil.virtual_memory().available / (1024 ** 3)
|
||||
memory_status.append(('system', system_memory_available))
|
||||
|
||||
# check if Nvidia-smi is available
|
||||
# GPUtil requires nvidia-smi.exe to interact with GPU
|
||||
if args.method in ['gpu', 'cudnn']:
|
||||
if not (shutil.which('nvidia-smi') or
|
||||
pathlib.Path(r'C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe').is_file()):
|
||||
# Nvidia System Management Interface not available
|
||||
Avalon.warning('Nvidia-smi not available, skipping available memory check')
|
||||
Avalon.warning('If you experience error \"cudaSuccess out of memory\", try reducing number of threads you\'re using')
|
||||
else:
|
||||
with contextlib.suppress(ValueError):
|
||||
# "0" is GPU ID. Both waifu2x drivers use the first GPU available, therefore only 0 makes sense
|
||||
gpu_memory_available = (GPUtil.getGPUs()[0].memoryTotal - GPUtil.getGPUs()[0].memoryUsed) / 1024
|
||||
memory_status.append(('GPU', gpu_memory_available))
|
||||
|
||||
# go though each checkable memory type and check availability
|
||||
for memory_type, memory_available in memory_status:
|
||||
|
||||
if memory_type == 'system':
|
||||
mem_per_thread = SYS_MEM_PER_THREAD
|
||||
else:
|
||||
mem_per_thread = GPU_MEM_PER_THREAD
|
||||
|
||||
# if user doesn't even have enough memory to run even one thread
|
||||
if memory_available < mem_per_thread:
|
||||
Avalon.warning(f'You might have insufficient amount of {memory_type} memory available to run this program ({memory_available} GB)')
|
||||
Avalon.warning('Proceed with caution')
|
||||
if args.threads > 1:
|
||||
if Avalon.ask('Reduce number of threads to avoid crashing?', default=True, batch=args.batch):
|
||||
args.threads = 1
|
||||
# if memory available is less than needed, warn the user
|
||||
elif memory_available < (mem_per_thread * args.threads):
|
||||
Avalon.warning(f'Each waifu2x-caffe thread will require up to {SYS_MEM_PER_THREAD} GB of system memory')
|
||||
Avalon.warning(f'You demanded {args.threads} threads to be created, but you only have {round(memory_available, 4)} GB {memory_type} memory available')
|
||||
Avalon.warning(f'{mem_per_thread * args.threads} GB of {memory_type} memory is recommended for {args.threads} threads')
|
||||
Avalon.warning(f'With your current amount of {memory_type} memory available, {int(memory_available // mem_per_thread)} threads is recommended')
|
||||
|
||||
# ask the user if he / she wants to change to the recommended
|
||||
# number of threads
|
||||
if Avalon.ask('Change to the recommended value?', default=True, batch=args.batch):
|
||||
args.threads = int(memory_available // mem_per_thread)
|
||||
else:
|
||||
Avalon.warning('Proceed with caution')
|
||||
|
||||
|
||||
def read_config(config_file: pathlib.Path) -> dict:
|
||||
""" read video2x configurations from config file
|
||||
|
||||
Arguments:
|
||||
config_file {pathlib.Path} -- video2x configuration file pathlib.Path
|
||||
|
||||
Returns:
|
||||
dict -- dictionary of video2x configuration
|
||||
"""
|
||||
|
||||
with open(config_file, 'r') as config:
|
||||
return yaml.load(config, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def absolutify_paths(config):
|
||||
""" Check to see if paths to binaries are absolute
|
||||
|
||||
This function checks if paths to binary files are absolute.
|
||||
If not, then absolutify the path.
|
||||
|
||||
Arguments:
|
||||
config {dict} -- configuration file dictionary
|
||||
|
||||
Returns:
|
||||
dict -- configuration file dictionary
|
||||
"""
|
||||
current_directory = pathlib.Path(sys.argv[0]).parent.absolute()
|
||||
|
||||
# check waifu2x-caffe path
|
||||
if not re.match('^[a-z]:', config['waifu2x_caffe']['waifu2x_caffe_path'], re.IGNORECASE):
|
||||
config['waifu2x_caffe']['waifu2x_caffe_path'] = current_directory / config['waifu2x_caffe']['waifu2x_caffe_path']
|
||||
|
||||
# check waifu2x-converter-cpp path
|
||||
if not re.match('^[a-z]:', config['waifu2x_converter']['waifu2x_converter_path'], re.IGNORECASE):
|
||||
config['waifu2x_converter']['waifu2x_converter_path'] = current_directory / config['waifu2x_converter']['waifu2x_converter_path']
|
||||
|
||||
# check waifu2x_ncnn_vulkan path
|
||||
if not re.match('^[a-z]:', config['waifu2x_ncnn_vulkan']['waifu2x_ncnn_vulkan_path'], re.IGNORECASE):
|
||||
config['waifu2x_ncnn_vulkan']['waifu2x_ncnn_vulkan_path'] = current_directory / config['waifu2x_ncnn_vulkan']['waifu2x_ncnn_vulkan_path']
|
||||
|
||||
# check anime4k path
|
||||
if not re.match('^[a-z]:', config['anime4k']['anime4k_path'], re.IGNORECASE):
|
||||
config['anime4k']['anime4k_path'] = current_directory / config['anime4k']['anime4k_path']
|
||||
|
||||
# check ffmpeg path
|
||||
if not re.match('^[a-z]:', config['ffmpeg']['ffmpeg_path'], re.IGNORECASE):
|
||||
config['ffmpeg']['ffmpeg_path'] = current_directory / config['ffmpeg']['ffmpeg_path']
|
||||
|
||||
# check video2x cache path
|
||||
if config['video2x']['video2x_cache_directory']:
|
||||
if not re.match('^[a-z]:', config['video2x']['video2x_cache_directory'], re.IGNORECASE):
|
||||
config['video2x']['video2x_cache_directory'] = current_directory / config['video2x']['video2x_cache_directory']
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# /////////////////// Execution /////////////////// #
|
||||
|
||||
# this is not a library
|
||||
if __name__ != '__main__':
|
||||
Avalon.error('This file cannot be imported')
|
||||
raise ImportError(f'{__file__} cannot be imported')
|
||||
|
||||
# print video2x logo
|
||||
print_logo()
|
||||
|
||||
# parse command line arguments
|
||||
args = parse_arguments()
|
||||
|
||||
# display version and lawful informaition
|
||||
if args.version:
|
||||
print(LEGAL_INFO)
|
||||
sys.exit(0)
|
||||
|
||||
# arguments sanity check
|
||||
if not args.input:
|
||||
Avalon.error('You must specify input video file/directory path')
|
||||
raise ArgumentError('input video path not specified')
|
||||
if not args.output:
|
||||
Avalon.error('You must specify output video file/directory path')
|
||||
raise ArgumentError('output video path not specified')
|
||||
if (args.driver in ['waifu2x_converter', 'waifu2x_ncnn_vulkan', 'anime4k']) and args.width and args.height:
|
||||
Avalon.error('Selected driver accepts only scaling ratio')
|
||||
raise ArgumentError('selected driver supports only scaling ratio')
|
||||
if args.driver == 'waifu2x_ncnn_vulkan' and args.ratio is not None and (args.ratio > 2 or not args.ratio.is_integer()):
|
||||
Avalon.error('Scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan')
|
||||
raise ArgumentError('scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan')
|
||||
if (args.width or args.height) and args.ratio:
|
||||
Avalon.error('You can only specify either scaling ratio or output width and height')
|
||||
raise ArgumentError('both scaling ration and width/height specified')
|
||||
if (args.width and not args.height) or (not args.width and args.height):
|
||||
Avalon.error('You must specify both width and height')
|
||||
raise ArgumentError('only one of width or height is specified')
|
||||
|
||||
# check available memory if driver is waifu2x-based
|
||||
if args.driver in ['waifu2x_caffe', 'waifu2x_converter', 'waifu2x_ncnn_vulkan']:
|
||||
check_memory()
|
||||
|
||||
# anime4k runs significantly faster with more threads
|
||||
if args.driver == 'anime4k' and args.threads <= 1:
|
||||
Avalon.warning('Anime4K runs significantly faster with more threads')
|
||||
if Avalon.ask('Use more threads of Anime4K?', default=True, batch=args.batch):
|
||||
while True:
|
||||
try:
|
||||
threads = Avalon.gets('Amount of threads to use [5]: ', default=5, batch=args.batch)
|
||||
args.threads = int(threads)
|
||||
break
|
||||
except ValueError:
|
||||
if threads == '':
|
||||
args.threads = 5
|
||||
break
|
||||
Avalon.error(f'{threads} is not a valid integer')
|
||||
|
||||
# read configurations from configuration file
|
||||
config = read_config(args.config)
|
||||
|
||||
# config = absolutify_paths(config)
|
||||
|
||||
# load waifu2x configuration
|
||||
driver_settings = config[args.driver]
|
||||
|
||||
# check if driver path exists
|
||||
if not pathlib.Path(driver_settings['path']).is_file():
|
||||
if not pathlib.Path(f'{driver_settings["path"]}.exe').is_file():
|
||||
Avalon.error('Specified driver executable directory doesn\'t exist')
|
||||
Avalon.error('Please check the configuration file settings')
|
||||
raise FileNotFoundError(driver_settings['path'])
|
||||
|
||||
# if the driver is Anime4K, check if JDK 12 is installed
|
||||
if args.driver == 'anime4k':
|
||||
if not pathlib.Path('C:/Program Files/Java/jdk-12.0.2/bin/java.exe').is_file():
|
||||
Avalon.warning('Cannot find JDK 12 at its default installation location')
|
||||
Avalon.warning('Please ensure you have JDK 12 installed and configured')
|
||||
|
||||
# read FFmpeg configuration
|
||||
ffmpeg_settings = config['ffmpeg']
|
||||
|
||||
# load video2x settings
|
||||
image_format = config['video2x']['image_format'].lower()
|
||||
preserve_frames = config['video2x']['preserve_frames']
|
||||
|
||||
# load cache directory
|
||||
if isinstance(config['video2x']['video2x_cache_directory'], str):
|
||||
video2x_cache_directory = pathlib.Path(config['video2x']['video2x_cache_directory'])
|
||||
else:
|
||||
video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x'
|
||||
|
||||
if video2x_cache_directory.exists() and not video2x_cache_directory.is_dir():
|
||||
Avalon.error('Specified cache directory is a file/link')
|
||||
raise FileExistsError('Specified cache directory is a file/link')
|
||||
|
||||
elif not video2x_cache_directory.exists():
|
||||
# if destination file is a file or a symbolic link
|
||||
Avalon.warning(f'Specified cache directory {video2x_cache_directory} does not exist')
|
||||
|
||||
# try creating the cache directory
|
||||
if Avalon.ask('Create directory?', default=True, batch=args.batch):
|
||||
try:
|
||||
video2x_cache_directory.mkdir(parents=True, exist_ok=True)
|
||||
Avalon.info(f'{video2x_cache_directory} created')
|
||||
|
||||
# there can be a number of exceptions here
|
||||
# PermissionError, FileExistsError, etc.
|
||||
# therefore, we put a catch-them-all here
|
||||
except Exception as e:
|
||||
Avalon.error(f'Unable to create {video2x_cache_directory}')
|
||||
Avalon.error('Aborting...')
|
||||
raise e
|
||||
else:
|
||||
raise FileNotFoundError('Could not create cache directory')
|
||||
|
||||
|
||||
# start execution
|
||||
try:
|
||||
# start timer
|
||||
begin_time = time.time()
|
||||
|
||||
# if input specified is a single file
|
||||
if args.input.is_file():
|
||||
|
||||
# upscale single video file
|
||||
Avalon.info(f'Upscaling single video file: {args.input}')
|
||||
|
||||
# check for input output format mismatch
|
||||
if args.output.is_dir():
|
||||
Avalon.error('Input and output path type mismatch')
|
||||
Avalon.error('Input is single file but output is directory')
|
||||
raise Exception('input output path type mismatch')
|
||||
if not re.search(r'.*\..*$', str(args.output)):
|
||||
Avalon.error('No suffix found in output file path')
|
||||
Avalon.error('Suffix must be specified for FFmpeg')
|
||||
raise Exception('No suffix specified')
|
||||
|
||||
upscaler = Upscaler(input_video=args.input, output_video=args.output, method=args.method, driver_settings=driver_settings, ffmpeg_settings=ffmpeg_settings)
|
||||
|
||||
# set optional options
|
||||
upscaler.waifu2x_driver = args.driver
|
||||
upscaler.scale_width = args.width
|
||||
upscaler.scale_height = args.height
|
||||
upscaler.scale_ratio = args.ratio
|
||||
upscaler.model_dir = args.model_dir
|
||||
upscaler.threads = args.threads
|
||||
upscaler.video2x_cache_directory = video2x_cache_directory
|
||||
upscaler.image_format = image_format
|
||||
upscaler.preserve_frames = preserve_frames
|
||||
|
||||
# run upscaler
|
||||
upscaler.create_temp_directories()
|
||||
upscaler.run()
|
||||
upscaler.cleanup_temp_directories()
|
||||
|
||||
# if input specified is a directory
|
||||
elif args.input.is_dir():
|
||||
# upscale videos in a directory
|
||||
Avalon.info(f'Upscaling videos in directory: {args.input}')
|
||||
|
||||
# make output directory if it doesn't exist
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for input_video in [f for f in args.input.iterdir() if f.is_file()]:
|
||||
output_video = args.output / input_video.name
|
||||
upscaler = Upscaler(input_video=input_video, output_video=output_video, method=args.method, driver_settings=driver_settings, ffmpeg_settings=ffmpeg_settings)
|
||||
|
||||
# set optional options
|
||||
upscaler.waifu2x_driver = args.driver
|
||||
upscaler.scale_width = args.width
|
||||
upscaler.scale_height = args.height
|
||||
upscaler.scale_ratio = args.ratio
|
||||
upscaler.model_dir = args.model_dir
|
||||
upscaler.threads = args.threads
|
||||
upscaler.video2x_cache_directory = video2x_cache_directory
|
||||
upscaler.image_format = image_format
|
||||
upscaler.preserve_frames = preserve_frames
|
||||
|
||||
# run upscaler
|
||||
upscaler.create_temp_directories()
|
||||
upscaler.run()
|
||||
upscaler.cleanup_temp_directories()
|
||||
else:
|
||||
Avalon.error('Input path is neither a file nor a directory')
|
||||
raise FileNotFoundError(f'{args.input} is neither file nor directory')
|
||||
|
||||
Avalon.info(f'Program completed, taking {round((time.time() - begin_time), 5)} seconds')
|
||||
|
||||
except Exception:
|
||||
Avalon.error('An exception has occurred')
|
||||
traceback.print_exc()
|
||||
|
||||
# try cleaning up temp directories
|
||||
with contextlib.suppress(Exception):
|
||||
upscaler.cleanup_temp_directories()
|
||||
|
||||
finally:
|
||||
# remove Video2X cache directory
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
if not preserve_frames:
|
||||
shutil.rmtree(video2x_cache_directory)
|
||||
89
src/video2x.yaml
Normal file
89
src/video2x.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
waifu2x_caffe:
|
||||
path: C:\Users\K4YT3X\AppData\Local\video2x\waifu2x-caffe\waifu2x-caffe-cui
|
||||
input_extention_list:
|
||||
output_extention:
|
||||
mode: noise_scale
|
||||
scale_ratio:
|
||||
scale_width:
|
||||
scale_height:
|
||||
noise_level: 3
|
||||
process: gpu
|
||||
crop_size: 128
|
||||
output_quality: -1
|
||||
output_depth: 8
|
||||
batch_size: 1
|
||||
gpu: 0
|
||||
tta: 0
|
||||
input_path:
|
||||
output_path:
|
||||
model_dir:
|
||||
crop_w:
|
||||
crop_h:
|
||||
waifu2x_converter:
|
||||
path: C:\Users\K4YT3X\AppData\Local\video2x\waifu2x-converter-cpp
|
||||
output-format:
|
||||
png-compression:
|
||||
image-quality:
|
||||
block-size:
|
||||
disable-gpu:
|
||||
force-OpenCL:
|
||||
processor:
|
||||
jobs:
|
||||
model-dir:
|
||||
scale-ratio:
|
||||
noise-level: 3
|
||||
mode: noise-scale
|
||||
silent: true
|
||||
output:
|
||||
input:
|
||||
waifu2x_ncnn_vulkan:
|
||||
path: C:\Users\K4YT3X\AppData\Local\video2x\waifu2x-ncnn-vulkan\waifu2x-ncnn-vulkan
|
||||
v:
|
||||
i:
|
||||
o:
|
||||
n: 2
|
||||
s: 2
|
||||
t: 400
|
||||
m: models-cunet
|
||||
g: 0
|
||||
j: "1:2:2"
|
||||
anime4k:
|
||||
path: C:\Users\K4YT3X\AppData\Local\video2x\anime4k\Anime4K.jar
|
||||
java_path: C:\Program Files\Java\jdk-12.0.2\bin\java.exe
|
||||
ffmpeg:
|
||||
ffmpeg_path: C:\Users\K4YT3X\AppData\Local\video2x\ffmpeg-latest-win64-static\bin
|
||||
video_to_frames:
|
||||
output_options:
|
||||
"-qscale:v":
|
||||
"-pix_fmt": rgba64be
|
||||
"-hwaccel": auto
|
||||
"-y": true
|
||||
frames_to_video:
|
||||
input_options:
|
||||
"-qscale:v":
|
||||
"-qscale:a":
|
||||
"-f": image2
|
||||
output_options:
|
||||
"-vcodec": libx264
|
||||
"-crf": 17
|
||||
"-b:v":
|
||||
"-pix_fmt":
|
||||
"-hwaccel": auto
|
||||
"-y": true
|
||||
migrating_tracks:
|
||||
output_options:
|
||||
"-map":
|
||||
- 0:v?
|
||||
- 1:a?
|
||||
- 1:s?
|
||||
- 1:d?
|
||||
- 1:t?
|
||||
"-c": copy
|
||||
"-pix_fmt":
|
||||
"-hwaccel": auto
|
||||
"-y": true
|
||||
video2x:
|
||||
video2x_cache_directory:
|
||||
image_format: png
|
||||
preserve_frames: false
|
||||
462
src/video2x_gui.py
Executable file
462
src/video2x_gui.py
Executable file
@@ -0,0 +1,462 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Creator: Video2X GUI
|
||||
Author: K4YT3X
|
||||
Date Created: July 27, 2019
|
||||
Last Modified: December 11, 2019
|
||||
|
||||
Description: A simple GUI for Video2X made with tkinter.
|
||||
"""
|
||||
|
||||
# local imports
|
||||
from exceptions import *
|
||||
from upscaler import Upscaler
|
||||
|
||||
# built-in imports
|
||||
from tkinter import *
|
||||
from tkinter import messagebox
|
||||
from tkinter import ttk
|
||||
from tkinter.filedialog import *
|
||||
import contextlib
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import yaml
|
||||
|
||||
VERSION = '1.1.3'
|
||||
|
||||
VIDEO2X_CONFIG = pathlib.Path(sys.argv[0]).parent.absolute() / 'video2x.yaml'
|
||||
|
||||
LEGAL_INFO = f'''Video2X GUI Version: {VERSION}
|
||||
Author: K4YT3X
|
||||
License: GNU GPL v3
|
||||
Github Page: https://github.com/k4yt3x/video2x
|
||||
Contact: k4yt3x@k4yt3x.com'''
|
||||
|
||||
# global static variables
|
||||
AVAILABLE_METHODS = {
|
||||
'GPU': 'gpu',
|
||||
'CUDNN': 'cudnn',
|
||||
'CPU': 'cpu'
|
||||
}
|
||||
|
||||
AVAILABLE_DRIVERS = {
|
||||
'Waifu2X Caffe': 'waifu2x_caffe',
|
||||
'Waifu2X Converter CPP': 'waifu2x_converter',
|
||||
'Waifu2x NCNN Vulkan': 'waifu2x_ncnn_vulkan',
|
||||
'Anime4K': 'anime4k'
|
||||
}
|
||||
|
||||
IMAGE_FORMATS = {'PNG', 'JPG'}
|
||||
|
||||
DEMUXER_EXTENSIONS = {'3dostr', '4xm', 'aa', 'aac', 'ac3', 'acm', 'act',
|
||||
'adf', 'adp', 'ads', 'adx', 'aea', 'afc', 'aiff', 'aix', 'alaw',
|
||||
'alias_pix', 'alsa', 'amr', 'amrnb', 'amrwb', 'anm', 'apc', 'ape',
|
||||
'apng', 'aptx', 'aptx_hd', 'aqtitle', 'asf', 'asf_o', 'ass', 'ast',
|
||||
'au', 'avi', 'avisynth', 'avr', 'avs', 'avs2', 'bethsoftvid', 'bfi',
|
||||
'bfstm', 'bin', 'bink', 'bit', 'bmp_pipe', 'bmv', 'boa', 'brender_pix',
|
||||
'brstm', 'c93', 'caf', 'cavsvideo', 'cdg', 'cdxl', 'cine', 'codec2',
|
||||
'codec2raw', 'concat', 'dash', 'data', 'daud', 'dcstr', 'dds_pipe',
|
||||
'dfa', 'dirac', 'dnxhd', 'dpx_pipe', 'dsf', 'dsicin', 'dss', 'dts',
|
||||
'dtshd', 'dv', 'dvbsub', 'dvbtxt', 'dxa', 'ea', 'ea_cdata', 'eac3',
|
||||
'epaf', 'exr_pipe', 'f32be', 'f32le', 'f64be', 'f64le', 'fbdev',
|
||||
'ffmetadata', 'film_cpk', 'filmstrip', 'fits', 'flac', 'flic', 'flv',
|
||||
'frm', 'fsb', 'g722', 'g723_1', 'g726', 'g726le', 'g729', 'gdv', 'genh',
|
||||
'gif', 'gsm', 'gxf', 'h261', 'h263', 'h264', 'hevc', 'hls', 'applehttp',
|
||||
'hnm', 'ico', 'idcin', 'idf', 'iec61883', 'iff', 'ilbc', 'image2',
|
||||
'image2pipe', 'ingenient', 'ipmovie', 'ircam', 'iss', 'iv8', 'ivf',
|
||||
'ivr', 'j2k_pipe', 'jack', 'jacosub', 'jpeg_pipe', 'jpegls_pipe',
|
||||
'jv', 'kmsgrab', 'lavfi', 'libcdio', 'libdc1394', 'libgme', 'libopenmpt',
|
||||
'live_flv', 'lmlm4', 'loas', 'lrc', 'lvf', 'lxf', 'm4v', 'matroska', 'webm',
|
||||
'mgsts', 'microdvd', 'mjpeg', 'mjpeg_2000', 'mlp', 'mlv', 'mm', 'mmf',
|
||||
'mov', 'mp4', 'm4a', '3gp', '3g2', 'mj2', 'mp3', 'mpc', 'mpc8', 'mpeg',
|
||||
'mpegts', 'mpegtsraw', 'mpegvideo', 'mpjpeg', 'mpl2', 'mpsub', 'msf',
|
||||
'msnwctcp', 'mtaf', 'mtv', 'mulaw', 'musx', 'mv', 'mvi', 'mxf', 'mxg',
|
||||
'nc', 'nistsphere', 'nsp', 'nsv', 'nut', 'nuv', 'ogg', 'oma', 'openal',
|
||||
'oss', 'paf', 'pam_pipe', 'pbm_pipe', 'pcx_pipe', 'pgm_pipe', 'pgmyuv_pipe',
|
||||
'pictor_pipe', 'pjs', 'pmp', 'png_pipe', 'ppm_pipe', 'psd_pipe', 'psxstr',
|
||||
'pulse', 'pva', 'pvf', 'qcp', 'qdraw_pipe', 'r3d', 'rawvideo', 'realtext',
|
||||
'redspark', 'rl2', 'rm', 'roq', 'rpl', 'rsd', 'rso', 'rtp', 'rtsp',
|
||||
's16be', 's16le', 's24be', 's24le', 's32be', 's32le', 's337m', 's8',
|
||||
'sami', 'sap', 'sbc', 'sbg', 'scc', 'sdp', 'sdr2', 'sds', 'sdx', 'ser',
|
||||
'sgi_pipe', 'shn', 'siff', 'sln', 'smjpeg', 'smk', 'smush', 'sndio',
|
||||
'sol', 'sox', 'spdif', 'srt', 'stl', 'subviewer', 'subviewer1', 'sunrast_pipe',
|
||||
'sup', 'svag', 'svg_pipe', 'swf', 'tak', 'tedcaptions', 'thp', 'tiertexseq',
|
||||
'tiff_pipe', 'tmv', 'truehd', 'tta', 'tty', 'txd', 'ty', 'u16be', 'u16le',
|
||||
'u24be', 'u24le', 'u32be', 'u32le', 'u8', 'v210', 'v210x', 'vag', 'vc1',
|
||||
'vc1test', 'vidc', 'video4linux2', 'v4l2', 'vivo', 'vmd', 'vobsub', 'voc',
|
||||
'vpk', 'vplayer', 'vqf', 'w64', 'wav', 'wc3movie', 'webm_dash_manifest',
|
||||
'webp_pipe', 'webvtt', 'wsaud', 'wsd', 'wsvqa', 'wtv', 'wv', 'wve', 'x11grab',
|
||||
'xa', 'xbin', 'xmv', 'xpm_pipe', 'xvag', 'xwd_pipe', 'xwma', 'yop', 'yuv4mpegpipe'}
|
||||
|
||||
|
||||
class Video2xGui():
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.running = False
|
||||
|
||||
# create main window
|
||||
self.main_window = Tk()
|
||||
self.main_window.title(f'Video2X GUI {VERSION}')
|
||||
self.main_frame = Frame()
|
||||
self.main_frame.pack(fill=BOTH, expand=True)
|
||||
|
||||
# add menu bar
|
||||
self.menu_bar = Menu(self.main_frame)
|
||||
|
||||
# file menu
|
||||
self.file_menu = Menu(self.menu_bar, tearoff=0)
|
||||
self.file_menu.add_command(label='Exit', command=self.main_frame.quit)
|
||||
self.menu_bar.add_cascade(label='File', menu=self.file_menu)
|
||||
|
||||
# help menu
|
||||
self.help_menu = Menu(self.menu_bar, tearoff=0)
|
||||
self.help_menu.add_command(label='About', command=self._display_help)
|
||||
self.menu_bar.add_cascade(label='Help', menu=self.help_menu)
|
||||
|
||||
self.main_window.config(menu=self.menu_bar)
|
||||
|
||||
# file frame
|
||||
self.file_frame = Frame(self.main_frame)
|
||||
self.file_frame.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
|
||||
# input file
|
||||
self.input_file = StringVar()
|
||||
label_text = StringVar()
|
||||
label_text.set('Input File')
|
||||
Label(self.file_frame, textvariable=label_text, relief=RIDGE, width=10).grid(row=0, column=0, padx=5, pady=5, sticky=W)
|
||||
Entry(self.file_frame, textvariable=self.input_file, width=60).grid(row=0, column=1, padx=5, pady=5, sticky=W)
|
||||
Button(self.file_frame, text='Select', command=self._select_input).grid(row=0, column=2, padx=5, pady=5, sticky=W)
|
||||
|
||||
# output file
|
||||
self.output_file = StringVar()
|
||||
label_text = StringVar()
|
||||
label_text.set('Output File')
|
||||
Label(self.file_frame, textvariable=label_text, relief=RIDGE, width=10).grid(row=1, column=0, padx=5, pady=5, sticky=W)
|
||||
Entry(self.file_frame, textvariable=self.output_file, width=60).grid(row=1, column=1, padx=5, pady=5, sticky=W)
|
||||
Button(self.file_frame, text='Select', command=self._select_output).grid(row=1, column=2, padx=5, pady=5, sticky=W)
|
||||
|
||||
# options
|
||||
self.options_frame = Frame()
|
||||
# self.options_left.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
self.options_frame.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
|
||||
self.options_left = Frame(self.options_frame)
|
||||
# self.options_left.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
self.options_left.grid(row=0, column=0, padx=5, pady=5, sticky=N)
|
||||
|
||||
# width
|
||||
self.width = IntVar()
|
||||
# self.width.set(1920)
|
||||
Label(self.options_left, text='Width', relief=RIDGE, width=15).grid(row=0, column=0, padx=2, pady=3)
|
||||
width_field = Entry(self.options_left, textvariable=self.width)
|
||||
width_field.grid(row=0, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# height
|
||||
self.height = IntVar()
|
||||
# self.height.set(1080)
|
||||
Label(self.options_left, text='Height', relief=RIDGE, width=15).grid(row=1, column=0, padx=2, pady=3)
|
||||
height_field = Entry(self.options_left, textvariable=self.height)
|
||||
height_field.grid(row=1, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# scale ratio
|
||||
self.scale_ratio = DoubleVar()
|
||||
# self.scale_ratio.set(2.0)
|
||||
Label(self.options_left, text='Scale Ratio', relief=RIDGE, width=15).grid(row=2, column=0, padx=2, pady=3)
|
||||
scale_ratio_field = Entry(self.options_left, textvariable=self.scale_ratio)
|
||||
scale_ratio_field.grid(row=2, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# image format
|
||||
self.image_format = StringVar(self.options_left)
|
||||
self.image_format.set('PNG')
|
||||
Label(self.options_left, text='Image Format', relief=RIDGE, width=15).grid(row=3, column=0, padx=2, pady=3)
|
||||
image_format_menu = OptionMenu(self.options_left, self.image_format, *IMAGE_FORMATS)
|
||||
image_format_menu.grid(row=3, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# options
|
||||
self.options_right = Frame(self.options_frame)
|
||||
# self.options_left.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
self.options_right.grid(row=0, column=1, padx=5, pady=5, sticky=N)
|
||||
|
||||
# threads
|
||||
self.threads = IntVar()
|
||||
self.threads.set(1)
|
||||
Label(self.options_right, text='Threads', relief=RIDGE, width=15).grid(row=0, column=0, padx=2, pady=3)
|
||||
threads_field = Entry(self.options_right, textvariable=self.threads)
|
||||
threads_field.grid(row=0, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# method
|
||||
self.method = StringVar(self.options_left)
|
||||
self.method.set('GPU')
|
||||
Label(self.options_right, text='Method', relief=RIDGE, width=15).grid(row=1, column=0, padx=2, pady=3)
|
||||
method_menu = OptionMenu(self.options_right, self.method, *AVAILABLE_METHODS)
|
||||
method_menu.grid(row=1, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# driver
|
||||
self.driver = StringVar(self.options_left)
|
||||
self.driver.set('Waifu2X Caffe')
|
||||
Label(self.options_right, text='Driver', relief=RIDGE, width=15).grid(row=2, column=0, padx=2, pady=3)
|
||||
driver_menu = OptionMenu(self.options_right, self.driver, *AVAILABLE_DRIVERS)
|
||||
driver_menu.grid(row=2, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# preserve frames
|
||||
self.preserve_frames = BooleanVar(self.options_left)
|
||||
self.preserve_frames.set(False)
|
||||
Label(self.options_right, text='Preserve Frames', relief=RIDGE, width=15).grid(row=3, column=0, padx=2, pady=3)
|
||||
preserve_frames_menu = OptionMenu(self.options_right, self.preserve_frames, *{True, False})
|
||||
preserve_frames_menu.grid(row=3, column=1, padx=2, pady=3, sticky=W)
|
||||
|
||||
# progress bar
|
||||
self.progress_bar_frame = Frame()
|
||||
self.progress_bar_frame.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
|
||||
self.progress_bar = ttk.Progressbar(self.progress_bar_frame, orient='horizontal', length=100, mode='determinate')
|
||||
self.progress_bar.pack(fill=X)
|
||||
|
||||
# start button frame
|
||||
self.start_frame = Frame()
|
||||
self.start_frame.pack(fill=X, padx=5, pady=5, expand=True)
|
||||
|
||||
# start button
|
||||
self.start_button_text = StringVar()
|
||||
self.start_button_text.set('Start')
|
||||
Button(self.start_frame, textvariable=self.start_button_text, command=self._launch_upscaling, width=20).pack(side=RIGHT)
|
||||
|
||||
self.main_frame.mainloop()
|
||||
|
||||
def _display_help(self):
|
||||
messagebox.showinfo('About', LEGAL_INFO)
|
||||
|
||||
def _launch_upscaling(self):
|
||||
|
||||
# prevent launching multiple instances
|
||||
if self.running:
|
||||
messagebox.showerror('Error', 'Video2X is already running')
|
||||
return
|
||||
|
||||
# arguments sanity check
|
||||
if self.input_file.get() == '':
|
||||
messagebox.showerror('Error', 'You must specify input video file/directory path')
|
||||
return
|
||||
if self.output_file.get() == '':
|
||||
messagebox.showerror('Error', 'You must specify output video file/directory path')
|
||||
return
|
||||
if (self.driver.get() in ['Waifu2X Converter CPP', 'Waifu2x NCNN Vulkan', 'Anime4K']) and self.width.get() and self.height.get():
|
||||
messagebox.showerror('Error', f'Selected driver \"{self.driver.get()}\" accepts only scaling ratio')
|
||||
return
|
||||
if self.driver.get() == 'waifu2x_ncnn_vulkan' and (self.scale_ratio.get() > 2 or not self.scale_ratio.get().is_integer()):
|
||||
messagebox.showerror('Error', 'Scaling ratio must be 1 or 2 for waifu2x_ncnn_vulkan')
|
||||
return
|
||||
if (self.width.get() or self.height.get()) and self.scale_ratio.get():
|
||||
messagebox.showerror('Error', 'You can only specify either scaling ratio or output width and height')
|
||||
return
|
||||
if (self.width.get() and not self.height.get()) or (not self.width.get() and self.height.get()):
|
||||
messagebox.showerror('Error', 'You must specify both width and height')
|
||||
return
|
||||
if (not self.width.get() or not self.height.get()) and not self.scale_ratio.get():
|
||||
messagebox.showerror('Error', 'You must specify either output dimensions or scaling ratio')
|
||||
return
|
||||
|
||||
upscale = threading.Thread(target=self._upscale)
|
||||
upscale.start()
|
||||
self.running = True
|
||||
self.start_button_text.set('Running')
|
||||
|
||||
def _upscale(self):
|
||||
|
||||
try:
|
||||
# start timer
|
||||
begin_time = time.time()
|
||||
|
||||
# read configuration file
|
||||
config = read_config(VIDEO2X_CONFIG)
|
||||
config = absolutify_paths(config)
|
||||
|
||||
input_file = pathlib.Path(self.input_file.get())
|
||||
output_file = pathlib.Path(self.output_file.get())
|
||||
driver = AVAILABLE_DRIVERS[self.driver.get()]
|
||||
|
||||
# load specified driver's config into driver_settings
|
||||
driver_settings = config[driver]
|
||||
|
||||
# if executable doesn't exist, show warning
|
||||
if not pathlib.Path(driver_settings['path']).is_file() and not pathlib.Path(f'{driver_settings["path"]}.exe').is_file():
|
||||
messagebox.showerror('Error', 'Specified driver directory doesn\'t exist\nPlease check the configuration file settings')
|
||||
raise FileNotFoundError(driver_settings['path'])
|
||||
|
||||
# read FFmpeg configuration
|
||||
ffmpeg_settings = config['ffmpeg']
|
||||
|
||||
# load video2x settings
|
||||
image_format = config['video2x']['image_format'].lower()
|
||||
preserve_frames = config['video2x']['preserve_frames']
|
||||
|
||||
# load cache directory
|
||||
if isinstance(config['video2x']['video2x_cache_directory'], str):
|
||||
video2x_cache_directory = pathlib.Path(config['video2x']['video2x_cache_directory'])
|
||||
else:
|
||||
video2x_cache_directory = pathlib.Path(tempfile.gettempdir()) / 'video2x'
|
||||
|
||||
if video2x_cache_directory.exists() and not video2x_cache_directory.is_dir():
|
||||
messagebox.showerror('Error', 'Specified cache directory is a file/link')
|
||||
raise FileExistsError('Specified cache directory is a file/link')
|
||||
|
||||
elif not video2x_cache_directory.exists():
|
||||
# try creating the cache directory
|
||||
if messagebox.askyesno('Question', f'Specified cache directory {video2x_cache_directory} does not exist\nCreate directory?'):
|
||||
try:
|
||||
video2x_cache_directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# there can be a number of exceptions here
|
||||
# PermissionError, FileExistsError, etc.
|
||||
# therefore, we put a catch-them-all here
|
||||
except Exception as e:
|
||||
messagebox.showerror('Error', f'Unable to create {video2x_cache_directory}\nAborting...')
|
||||
raise e
|
||||
else:
|
||||
raise FileNotFoundError('Could not create cache directory')
|
||||
|
||||
# load more settings from gui
|
||||
width = self.width.get()
|
||||
height = self.height.get()
|
||||
scale_ratio = self.scale_ratio.get()
|
||||
image_format = self.image_format.get()
|
||||
threads = self.threads.get()
|
||||
method = AVAILABLE_METHODS[self.method.get()]
|
||||
preserve_frames = self.preserve_frames.get()
|
||||
|
||||
self.upscaler = Upscaler(input_video=input_file, output_video=output_file, method=method, driver_settings=driver_settings, ffmpeg_settings=ffmpeg_settings)
|
||||
|
||||
# set optional options
|
||||
self.upscaler.waifu2x_driver = driver
|
||||
self.upscaler.scale_width = width
|
||||
self.upscaler.scale_height = height
|
||||
self.upscaler.scale_ratio = scale_ratio
|
||||
self.upscaler.model_dir = None
|
||||
self.upscaler.threads = threads
|
||||
self.upscaler.video2x_cache_directory = video2x_cache_directory
|
||||
self.upscaler.image_format = image_format
|
||||
self.upscaler.preserve_frames = preserve_frames
|
||||
|
||||
# run upscaler
|
||||
self.upscaler.create_temp_directories()
|
||||
|
||||
# start progress bar
|
||||
progress_bar = threading.Thread(target=self._progress_bar)
|
||||
progress_bar.start()
|
||||
|
||||
# start upscaling
|
||||
self.upscaler.run()
|
||||
self.upscaler.cleanup_temp_directories()
|
||||
|
||||
# show message when upscaling completes
|
||||
messagebox.showinfo('Info', f'Upscaling Completed\nTime Taken: {round((time.time() - begin_time), 5)} seconds')
|
||||
self.progress_bar['value'] = 100
|
||||
self.running = False
|
||||
self.start_button_text.set('Start')
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror('Error', f'Upscaler ran into an error:\n{e}')
|
||||
|
||||
# try cleaning up temp directories
|
||||
with contextlib.suppress(Exception):
|
||||
self.upscaler.cleanup_temp_directories()
|
||||
|
||||
|
||||
def _progress_bar(self):
|
||||
""" This method prints a progress bar
|
||||
|
||||
This method prints a progress bar by keeping track
|
||||
of the amount of frames in the input directory
|
||||
and the output directory. This is originally
|
||||
suggested by @ArmandBernard.
|
||||
"""
|
||||
# initialize variables early
|
||||
self.upscaler.progress_bar_exit_signal = False
|
||||
self.upscaler.total_frames_upscaled = 0
|
||||
self.upscaler.total_frames = 1
|
||||
|
||||
# initialize progress bar values
|
||||
self.progress_bar['value'] = 0
|
||||
|
||||
while not self.upscaler.progress_bar_exit_signal:
|
||||
self.progress_bar['value'] = int(100 * self.upscaler.total_frames_upscaled / self.upscaler.total_frames)
|
||||
time.sleep(1)
|
||||
|
||||
def _select_input(self):
|
||||
self.input_file.set(askopenfilename(title='Select Input File'))
|
||||
|
||||
# remove input file extension
|
||||
input_filename = str(self.input_file.get())
|
||||
for extension in DEMUXER_EXTENSIONS:
|
||||
if input_filename.endswith(f'.{extension}'):
|
||||
input_filename = input_filename[:-1 - len(extension)]
|
||||
|
||||
# try to set an output file name automatically
|
||||
output_file = pathlib.Path(f'{input_filename}_output.mp4')
|
||||
|
||||
output_file_id = 0
|
||||
while output_file.is_file() and output_file_id <= 10:
|
||||
output_file = pathlib.Path(f'{input_filename}_output_{output_file_id}.mp4')
|
||||
output_file_id += 1
|
||||
|
||||
if not output_file.exists():
|
||||
self.output_file.set(str(output_file))
|
||||
|
||||
def _select_output(self):
|
||||
self.output_file.set(asksaveasfilename(title='Select Output File'))
|
||||
|
||||
|
||||
def read_config(config_file):
|
||||
""" Reads configuration file
|
||||
|
||||
Returns a dictionary read by parsing Video2X config.
|
||||
"""
|
||||
with open(config_file, 'r') as raw_config:
|
||||
config = yaml.load(raw_config, Loader=yaml.FullLoader)
|
||||
return config
|
||||
|
||||
|
||||
def absolutify_paths(config):
|
||||
""" Check to see if paths to binaries are absolute
|
||||
|
||||
This function checks if paths to binary files are absolute.
|
||||
If not, then absolutify the path.
|
||||
|
||||
Arguments:
|
||||
config {dict} -- configuration file dictionary
|
||||
|
||||
Returns:
|
||||
dict -- configuration file dictionary
|
||||
"""
|
||||
current_directory = pathlib.Path(sys.argv[0]).parent.absolute()
|
||||
|
||||
# check waifu2x-caffe path
|
||||
if not re.match('^[a-z]:', config['waifu2x_caffe']['path'], re.IGNORECASE):
|
||||
config['waifu2x_caffe']['path'] = current_directory / config['waifu2x_caffe']['path']
|
||||
|
||||
# check waifu2x-converter-cpp path
|
||||
if not re.match('^[a-z]:', config['waifu2x_converter']['path'], re.IGNORECASE):
|
||||
config['waifu2x_converter']['path'] = current_directory / config['waifu2x_converter']['path']
|
||||
|
||||
# check waifu2x_ncnn_vulkan path
|
||||
if not re.match('^[a-z]:', config['waifu2x_ncnn_vulkan']['path'], re.IGNORECASE):
|
||||
config['waifu2x_ncnn_vulkan']['path'] = current_directory / config['waifu2x_ncnn_vulkan']['path']
|
||||
|
||||
# check ffmpeg path
|
||||
if not re.match('^[a-z]:', config['ffmpeg']['ffmpeg_path'], re.IGNORECASE):
|
||||
config['ffmpeg']['ffmpeg_path'] = current_directory / config['ffmpeg']['ffmpeg_path']
|
||||
|
||||
# check video2x cache path
|
||||
if config['video2x']['video2x_cache_directory']:
|
||||
if not re.match('^[a-z]:', config['video2x']['video2x_cache_directory'], re.IGNORECASE):
|
||||
config['video2x']['video2x_cache_directory'] = current_directory / config['video2x']['video2x_cache_directory']
|
||||
|
||||
return config
|
||||
|
||||
|
||||
video2x_gui = Video2xGui()
|
||||
382
src/video2x_setup.py
Executable file
382
src/video2x_setup.py
Executable file
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Video2X Setup Script
|
||||
Creator: K4YT3X
|
||||
Date Created: November 28, 2018
|
||||
Last Modified: January 4, 2020
|
||||
|
||||
Editor: BrianPetkovsek
|
||||
Editor: SAT3LL
|
||||
|
||||
Description: This script helps installing all dependencies of video2x
|
||||
and generates a configuration for it.
|
||||
|
||||
Installation Details:
|
||||
- ffmpeg: %LOCALAPPDATA%\\video2x\\ffmpeg
|
||||
- waifu2x-caffe: %LOCALAPPDATA%\\video2x\\waifu2x-caffe
|
||||
- waifu2x-cpp-converter: %LOCALAPPDATA%\\video2x\\waifu2x-converter-cpp
|
||||
- waifu2x_ncnn_vulkan: %LOCALAPPDATA%\\video2x\\waifu2x-ncnn-vulkan
|
||||
- anime4k: %LOCALAPPDATA%\\video2x\\anime4k
|
||||
"""
|
||||
|
||||
# built-in imports
|
||||
from datetime import timedelta
|
||||
import argparse
|
||||
import contextlib
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
import urllib
|
||||
import zipfile
|
||||
|
||||
# Requests doesn't come with windows, therefore
|
||||
# it will be installed as a dependency and imported
|
||||
# later in the script.
|
||||
# import requests
|
||||
|
||||
VERSION = '1.6.1'
|
||||
|
||||
# global static variables
|
||||
LOCALAPPDATA = pathlib.Path(os.getenv('localappdata'))
|
||||
VIDEO2X_CONFIG = pathlib.Path(sys.argv[0]).parent.absolute() / 'video2x.yaml'
|
||||
DRIVER_OPTIONS = ['all', 'waifu2x_caffe', 'waifu2x_converter', 'waifu2x_ncnn_vulkan', 'anime4k']
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Processes CLI arguments
|
||||
"""
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
# video options
|
||||
general_options = parser.add_argument_group('General Options')
|
||||
general_options.add_argument('-d', '--driver', help='driver to download and configure', action='store', choices=DRIVER_OPTIONS, default='all')
|
||||
|
||||
# parse arguments
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
class Video2xSetup:
|
||||
""" Install dependencies for video2x video enlarger
|
||||
|
||||
This library is meant to be executed as a stand-alone
|
||||
script. All files will be installed under %LOCALAPPDATA%\\video2x.
|
||||
"""
|
||||
|
||||
def __init__(self, driver, download_python_modules):
|
||||
self.driver = driver
|
||||
self.download_python_modules = download_python_modules
|
||||
self.trash = []
|
||||
|
||||
def run(self):
|
||||
if self.download_python_modules:
|
||||
print('\nInstalling Python libraries')
|
||||
self._install_python_requirements()
|
||||
|
||||
print('\nInstalling FFmpeg')
|
||||
self._install_ffmpeg()
|
||||
|
||||
if self.driver == 'all':
|
||||
self._install_waifu2x_caffe()
|
||||
self._install_waifu2x_converter_cpp()
|
||||
self._install_waifu2x_ncnn_vulkan()
|
||||
self._install_anime4k()
|
||||
elif self.driver == 'waifu2x_caffe':
|
||||
self._install_waifu2x_caffe()
|
||||
elif self.driver == 'waifu2x_converter':
|
||||
self._install_waifu2x_converter_cpp()
|
||||
elif self.driver == 'waifu2x_ncnn_vulkan':
|
||||
self._install_waifu2x_ncnn_vulkan()
|
||||
elif self.driver == 'anime4k':
|
||||
self._install_anime4k()
|
||||
|
||||
print('\nGenerating Video2X configuration file')
|
||||
self._generate_config()
|
||||
|
||||
print('\nCleaning up temporary files')
|
||||
self._cleanup()
|
||||
|
||||
def _install_python_requirements(self):
|
||||
""" Read requirements.txt and return its content
|
||||
"""
|
||||
pip_install('requirements.txt')
|
||||
|
||||
def _cleanup(self):
|
||||
""" Cleanup all the temp files downloaded
|
||||
"""
|
||||
for file in self.trash:
|
||||
try:
|
||||
if file.is_dir():
|
||||
print(f'Deleting directory: {file}')
|
||||
shutil.rmtree(file)
|
||||
else:
|
||||
print(f'Deleting file: {file}')
|
||||
file.unlink()
|
||||
except Exception:
|
||||
print(f'Error deleting: {file}')
|
||||
traceback.print_exc()
|
||||
|
||||
def _install_ffmpeg(self):
|
||||
""" Install FFMPEG
|
||||
"""
|
||||
latest_release = 'https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-latest-win64-static.zip'
|
||||
|
||||
ffmpeg_zip = download(latest_release, tempfile.gettempdir())
|
||||
self.trash.append(ffmpeg_zip)
|
||||
|
||||
with zipfile.ZipFile(ffmpeg_zip) as zipf:
|
||||
zipf.extractall(LOCALAPPDATA / 'video2x')
|
||||
|
||||
def _install_waifu2x_caffe(self):
|
||||
""" Install waifu2x_caffe
|
||||
"""
|
||||
print('\nInstalling waifu2x-caffe')
|
||||
import requests
|
||||
|
||||
# Get latest release of waifu2x-caffe via GitHub API
|
||||
latest_release = requests.get('https://api.github.com/repos/lltcggie/waifu2x-caffe/releases/latest').json()
|
||||
|
||||
for a in latest_release['assets']:
|
||||
if 'waifu2x-caffe.zip' in a['browser_download_url']:
|
||||
waifu2x_caffe_zip = download(a['browser_download_url'], tempfile.gettempdir())
|
||||
self.trash.append(waifu2x_caffe_zip)
|
||||
|
||||
with zipfile.ZipFile(waifu2x_caffe_zip) as zipf:
|
||||
zipf.extractall(LOCALAPPDATA / 'video2x')
|
||||
|
||||
def _install_waifu2x_converter_cpp(self):
|
||||
""" Install waifu2x_caffe
|
||||
"""
|
||||
print('\nInstalling waifu2x-converter-cpp')
|
||||
import requests
|
||||
|
||||
# Get latest release of waifu2x-caffe via GitHub API
|
||||
latest_release = requests.get('https://api.github.com/repos/DeadSix27/waifu2x-converter-cpp/releases/latest').json()
|
||||
|
||||
for a in latest_release['assets']:
|
||||
if re.search(r'waifu2x-DeadSix27-win64_v[0-9]*\.zip', a['browser_download_url']):
|
||||
waifu2x_converter_cpp_zip = download(a['browser_download_url'], tempfile.gettempdir())
|
||||
self.trash.append(waifu2x_converter_cpp_zip)
|
||||
|
||||
with zipfile.ZipFile(waifu2x_converter_cpp_zip) as zipf:
|
||||
zipf.extractall(LOCALAPPDATA / 'video2x' / 'waifu2x-converter-cpp')
|
||||
|
||||
def _install_waifu2x_ncnn_vulkan(self):
|
||||
""" Install waifu2x-ncnn-vulkan
|
||||
"""
|
||||
print('\nInstalling waifu2x-ncnn-vulkan')
|
||||
import requests
|
||||
|
||||
# Get latest release of waifu2x-ncnn-vulkan via Github API
|
||||
latest_release = requests.get('https://api.github.com/repos/nihui/waifu2x-ncnn-vulkan/releases/latest').json()
|
||||
|
||||
for a in latest_release['assets']:
|
||||
if re.search(r'waifu2x-ncnn-vulkan-\d*\.zip', a['browser_download_url']):
|
||||
waifu2x_ncnn_vulkan_zip = download(a['browser_download_url'], tempfile.gettempdir())
|
||||
self.trash.append(waifu2x_ncnn_vulkan_zip)
|
||||
|
||||
# extract and rename
|
||||
waifu2x_ncnn_vulkan_directory = LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan'
|
||||
with zipfile.ZipFile(waifu2x_ncnn_vulkan_zip) as zipf:
|
||||
zipf.extractall(LOCALAPPDATA / 'video2x')
|
||||
|
||||
# if directory already exists, remove it
|
||||
if waifu2x_ncnn_vulkan_directory.exists():
|
||||
shutil.rmtree(waifu2x_ncnn_vulkan_directory)
|
||||
|
||||
# rename the newly extracted directory
|
||||
(LOCALAPPDATA / 'video2x' / zipf.namelist()[0]).rename(waifu2x_ncnn_vulkan_directory)
|
||||
|
||||
def _install_anime4k(self):
|
||||
""" Install Anime4K
|
||||
"""
|
||||
print('\nInstalling Anime4K')
|
||||
|
||||
"""
|
||||
import requests
|
||||
|
||||
# get latest release of Anime4K via Github API
|
||||
# at the time of writing this portion, Anime4K doesn't yet have a stable release
|
||||
# therefore releases/latest won't work
|
||||
latest_release = requests.get('https://api.github.com/repos/bloc97/Anime4K/releases').json()[0]
|
||||
|
||||
for a in latest_release['assets']:
|
||||
if 'Anime4K_Java.zip' in a['browser_download_url']:
|
||||
anime4k_zip = download(a['browser_download_url'], tempfile.gettempdir())
|
||||
self.trash.append(anime4k_zip)
|
||||
"""
|
||||
|
||||
# since Java pre-compiled release has been removed from download
|
||||
# page, we use this cached version as a temporary solution
|
||||
anime4k_zip = download('https://files.flexio.org/Resources/anime4k.zip', tempfile.gettempdir())
|
||||
self.trash.append(anime4k_zip)
|
||||
|
||||
# extract and rename
|
||||
with zipfile.ZipFile(anime4k_zip) as zipf:
|
||||
zipf.extractall(LOCALAPPDATA / 'video2x' / 'anime4k')
|
||||
|
||||
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.exe')
|
||||
template_dict['waifu2x_converter']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-converter-cpp')
|
||||
template_dict['waifu2x_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan' / 'waifu2x-ncnn-vulkan.exe')
|
||||
template_dict['anime4k']['path'] = str(LOCALAPPDATA / 'video2x' / 'anime4k' / 'Anime4K.jar')
|
||||
elif self.driver == 'waifu2x_caffe':
|
||||
template_dict['waifu2x_caffe']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-caffe' / 'waifu2x-caffe-cui.exe')
|
||||
elif self.driver == 'waifu2x_converter':
|
||||
template_dict['waifu2x_converter']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-converter-cpp')
|
||||
elif self.driver == 'waifu2x_ncnn_vulkan':
|
||||
template_dict['waifu2x_ncnn_vulkan']['path'] = str(LOCALAPPDATA / 'video2x' / 'waifu2x-ncnn-vulkan' / 'waifu2x-ncnn-vulkan.exe')
|
||||
elif self.driver == 'anime4k':
|
||||
template_dict['anime4k']['path'] = str(LOCALAPPDATA / 'video2x' / 'anime4k' / 'Anime4K.jar')
|
||||
|
||||
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
|
||||
"""
|
||||
from tqdm import tqdm
|
||||
import requests
|
||||
|
||||
save_path = pathlib.Path(save_path)
|
||||
|
||||
# create target folder if it doesn't exist
|
||||
save_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# create requests stream for steaming file
|
||||
stream = requests.get(url, stream=True, allow_redirects=True)
|
||||
|
||||
# get file name
|
||||
file_name = None
|
||||
if 'content-disposition' in stream.headers:
|
||||
disposition = stream.headers['content-disposition']
|
||||
with contextlib.suppress(IndexError):
|
||||
file_name = re.findall("filename=(.+)", disposition)[0].strip('"')
|
||||
|
||||
if file_name is None:
|
||||
# output_file = f'{save_path}\\{stream.url.split("/")[-1]}'
|
||||
output_file = save_path / stream.url.split('/')[-1]
|
||||
else:
|
||||
output_file = save_path / file_name
|
||||
|
||||
# decode url encoding
|
||||
output_file = pathlib.Path(urllib.parse.unquote(str(output_file)))
|
||||
|
||||
# get total size for progress bar if provided in headers
|
||||
total_size = 0
|
||||
if 'content-length' in stream.headers:
|
||||
total_size = int(stream.headers['content-length'])
|
||||
|
||||
# print download information summary
|
||||
print(f'Downloading: {url}')
|
||||
print(f'Total size: {total_size}')
|
||||
print(f'Chunk size: {chunk_size}')
|
||||
print(f'Saving to: {output_file}')
|
||||
|
||||
# Write content into file
|
||||
with open(output_file, 'wb') as output:
|
||||
with tqdm(total=total_size, ascii=True) as progress_bar:
|
||||
for chunk in stream.iter_content(chunk_size=chunk_size):
|
||||
if chunk:
|
||||
output.write(chunk)
|
||||
progress_bar.update(len(chunk))
|
||||
|
||||
# return the full path of saved file
|
||||
return output_file
|
||||
|
||||
|
||||
def pip_install(file):
|
||||
""" Install python package via python pip module
|
||||
|
||||
pip.main() is not available after pip 9.0.1, thus
|
||||
pip module is not used in this case.
|
||||
"""
|
||||
return subprocess.run([sys.executable, '-m', 'pip', 'install', '-U', '-r', file]).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
# set default exit code
|
||||
EXIT_CODE = 0
|
||||
|
||||
# get start time
|
||||
start_time = time.time()
|
||||
|
||||
# check platform
|
||||
if sys.platform != 'win32':
|
||||
print('This script is currently only compatible with Windows')
|
||||
EXIT_CODE = 1
|
||||
sys.exit(1)
|
||||
|
||||
# parse command line arguments
|
||||
args = parse_arguments()
|
||||
print('Video2X Setup Script')
|
||||
print(f'Version: {VERSION}')
|
||||
|
||||
# do not install pip modules if script
|
||||
# is packaged in exe format
|
||||
download_python_modules = True
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
print('\nScript is packaged as exe, skipping pip module download')
|
||||
download_python_modules = False
|
||||
|
||||
setup = Video2xSetup(args.driver, download_python_modules)
|
||||
setup.run()
|
||||
print('\nScript finished successfully')
|
||||
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
# if PermissionError is raised
|
||||
# user needs to run this with higher privilege
|
||||
except PermissionError:
|
||||
traceback.print_exc()
|
||||
print('You might have insufficient privilege for this script to run')
|
||||
print('Try running this script with Administrator privileges')
|
||||
EXIT_CODE = 1
|
||||
|
||||
# for any exception in the script
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('An error has occurred')
|
||||
print('Video2X Automatic Setup has failed')
|
||||
|
||||
# in case of a failure, try cleaning up temp files
|
||||
try:
|
||||
setup._cleanup()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('An error occurred while trying to cleanup files')
|
||||
|
||||
EXIT_CODE = 1
|
||||
|
||||
# regardless if script finishes successfully or not
|
||||
# print script execution summary
|
||||
finally:
|
||||
print('Script finished')
|
||||
print(f'Time taken: {timedelta(seconds=round(time.time() - start_time))}')
|
||||
input('Press [ENTER] to exit script')
|
||||
sys.exit(EXIT_CODE)
|
||||
55
bin/waifu2x_caffe.py → src/waifu2x_caffe.py
Normal file → Executable file
55
bin/waifu2x_caffe.py → src/waifu2x_caffe.py
Normal file → Executable file
@@ -4,15 +4,19 @@
|
||||
Name: Waifu2x Caffe Driver
|
||||
Author: K4YT3X
|
||||
Date Created: Feb 24, 2018
|
||||
Last Modified: March 30, 2019
|
||||
Last Modified: October 6, 2019
|
||||
|
||||
Description: This class is a high-level wrapper
|
||||
for waifu2x-caffe.
|
||||
"""
|
||||
from avalon_framework import Avalon
|
||||
|
||||
# built-in imports
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
|
||||
|
||||
class Waifu2xCaffe:
|
||||
"""This class communicates with waifu2x cui engine
|
||||
@@ -23,68 +27,69 @@ class Waifu2xCaffe:
|
||||
the upscale function.
|
||||
"""
|
||||
|
||||
def __init__(self, waifu2x_settings, process, model_dir):
|
||||
self.waifu2x_settings = waifu2x_settings
|
||||
self.waifu2x_settings['process'] = process
|
||||
self.waifu2x_settings['model_dir'] = model_dir
|
||||
def __init__(self, driver_settings, process, model_dir, bit_depth):
|
||||
self.driver_settings = driver_settings
|
||||
self.driver_settings['process'] = process
|
||||
self.driver_settings['model_dir'] = model_dir
|
||||
self.driver_settings['output_depth'] = bit_depth
|
||||
|
||||
# arguments passed through command line overwrites config file values
|
||||
self.process = process
|
||||
self.model_dir = model_dir
|
||||
self.print_lock = threading.Lock()
|
||||
|
||||
def upscale(self, input_folder, output_folder, scale_ratio, scale_width, scale_height, image_format, upscaler_exceptions):
|
||||
def upscale(self, input_directory, output_directory, scale_ratio, scale_width, scale_height, image_format, upscaler_exceptions):
|
||||
"""This is the core function for WAIFU2X class
|
||||
|
||||
Arguments:
|
||||
input_folder {string} -- source folder path
|
||||
output_folder {string} -- output folder path
|
||||
input_directory {string} -- source directory path
|
||||
output_directory {string} -- output directory path
|
||||
width {int} -- output video width
|
||||
height {int} -- output video height
|
||||
"""
|
||||
|
||||
try:
|
||||
# overwrite config file settings
|
||||
self.waifu2x_settings['input_path'] = input_folder
|
||||
self.waifu2x_settings['output_path'] = output_folder
|
||||
self.driver_settings['input_path'] = input_directory
|
||||
self.driver_settings['output_path'] = output_directory
|
||||
|
||||
if scale_ratio:
|
||||
self.waifu2x_settings['scale_ratio'] = scale_ratio
|
||||
self.driver_settings['scale_ratio'] = scale_ratio
|
||||
elif scale_width and scale_height:
|
||||
self.waifu2x_settings['scale_width'] = scale_width
|
||||
self.waifu2x_settings['scale_height'] = scale_height
|
||||
self.driver_settings['scale_width'] = scale_width
|
||||
self.driver_settings['scale_height'] = scale_height
|
||||
|
||||
self.waifu2x_settings['output_extention'] = image_format
|
||||
self.driver_settings['output_extention'] = image_format
|
||||
|
||||
# print thread start message
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info('[upscaler] Thread {} started'.format(threading.current_thread().name))
|
||||
Avalon.debug_info(f'[upscaler] Thread {threading.current_thread().name} started')
|
||||
self.print_lock.release()
|
||||
|
||||
# list to be executed
|
||||
execute = []
|
||||
# initialize the list with waifu2x binary path as the first element
|
||||
execute = [str(self.driver_settings['path'])]
|
||||
|
||||
execute.append(self.waifu2x_settings['waifu2x_caffe_path'])
|
||||
for key in self.waifu2x_settings.keys():
|
||||
for key in self.driver_settings.keys():
|
||||
|
||||
value = self.waifu2x_settings[key]
|
||||
value = self.driver_settings[key]
|
||||
|
||||
# is executable key or null or None means that leave this option out (keep default)
|
||||
if key == 'waifu2x_caffe_path' or value is None or value is False:
|
||||
if key == 'path' or value is None or value is False:
|
||||
continue
|
||||
else:
|
||||
if len(key) == 1:
|
||||
execute.append('-{}'.format(key))
|
||||
execute.append(f'-{key}')
|
||||
else:
|
||||
execute.append('--{}'.format(key))
|
||||
execute.append(f'--{key}')
|
||||
execute.append(str(value))
|
||||
|
||||
Avalon.debug_info('Executing: {}'.format(execute))
|
||||
Avalon.debug_info(f'Executing: {execute}')
|
||||
completed_command = subprocess.run(execute, check=True)
|
||||
|
||||
# print thread exiting message
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info('[upscaler] Thread {} exiting'.format(threading.current_thread().name))
|
||||
Avalon.debug_info(f'[upscaler] Thread {threading.current_thread().name} exiting')
|
||||
self.print_lock.release()
|
||||
|
||||
# return command execution return code
|
||||
63
bin/waifu2x_converter.py → src/waifu2x_converter.py
Normal file → Executable file
63
bin/waifu2x_converter.py → src/waifu2x_converter.py
Normal file → Executable file
@@ -4,15 +4,20 @@
|
||||
Name: Waifu2x Converter CPP Driver
|
||||
Author: K4YT3X
|
||||
Date Created: February 8, 2019
|
||||
Last Modified: March 30, 2019
|
||||
Last Modified: October 6, 2019
|
||||
|
||||
Description: This class is a high-level wrapper
|
||||
for waifu2x-converter-cpp.
|
||||
"""
|
||||
from avalon_framework import Avalon
|
||||
|
||||
# built-in imports
|
||||
import pathlib
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
|
||||
|
||||
class Waifu2xConverter:
|
||||
"""This class communicates with waifu2x cui engine
|
||||
@@ -23,68 +28,60 @@ class Waifu2xConverter:
|
||||
the upscale function.
|
||||
"""
|
||||
|
||||
def __init__(self, waifu2x_settings, model_dir):
|
||||
self.waifu2x_settings = waifu2x_settings
|
||||
self.waifu2x_settings['model_dir'] = model_dir
|
||||
def __init__(self, driver_settings, model_dir):
|
||||
self.driver_settings = driver_settings
|
||||
self.driver_settings['model_dir'] = model_dir
|
||||
self.print_lock = threading.Lock()
|
||||
|
||||
def upscale(self, input_folder, output_folder, scale_ratio, jobs, image_format, upscaler_exceptions):
|
||||
def upscale(self, input_directory, output_directory, scale_ratio, jobs, image_format, upscaler_exceptions):
|
||||
""" Waifu2x Converter Driver Upscaler
|
||||
This method executes the upscaling of extracted frames.
|
||||
|
||||
Arguments:
|
||||
input_folder {string} -- source folder path
|
||||
output_folder {string} -- output folder path
|
||||
input_directory {string} -- source directory path
|
||||
output_directory {string} -- output directory path
|
||||
scale_ratio {int} -- frames' scale ratio
|
||||
threads {int} -- number of threads
|
||||
"""
|
||||
|
||||
try:
|
||||
# overwrite config file settings
|
||||
self.waifu2x_settings['input'] = input_folder
|
||||
self.waifu2x_settings['output'] = output_folder
|
||||
|
||||
# temporary fix for https://github.com/DeadSix27/waifu2x-converter-cpp/issues/109
|
||||
"""
|
||||
self.waifu2x_settings['i'] = input_folder
|
||||
self.waifu2x_settings['o'] = output_folder
|
||||
self.waifu2x_settings['input'] = None
|
||||
self.waifu2x_settings['output'] = None
|
||||
"""
|
||||
|
||||
self.waifu2x_settings['scale-ratio'] = scale_ratio
|
||||
self.waifu2x_settings['jobs'] = jobs
|
||||
self.waifu2x_settings['output-format'] = image_format
|
||||
self.driver_settings['input'] = input_directory
|
||||
self.driver_settings['output'] = output_directory
|
||||
self.driver_settings['scale-ratio'] = scale_ratio
|
||||
self.driver_settings['jobs'] = jobs
|
||||
self.driver_settings['output-format'] = image_format
|
||||
|
||||
# models_rgb must be specified manually for waifu2x-converter-cpp
|
||||
# if it's not specified in the arguments, create automatically
|
||||
if self.waifu2x_settings['model-dir'] is None:
|
||||
self.waifu2x_settings['model-dir'] = '{}\\models_rgb'.format(self.waifu2x_settings['waifu2x_converter_path'])
|
||||
if self.driver_settings['model-dir'] is None:
|
||||
self.driver_settings['model-dir'] = pathlib.Path(self.driver_settings['waifu2x_converter_path']) / 'models_rgb'
|
||||
|
||||
# print thread start message
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info('[upscaler] Thread {} started'.format(threading.current_thread().name))
|
||||
Avalon.debug_info(f'[upscaler] Thread {threading.current_thread().name} started')
|
||||
self.print_lock.release()
|
||||
|
||||
# list to be executed
|
||||
execute = []
|
||||
# initialize the list with waifu2x binary path as the first element
|
||||
execute = [str(pathlib.Path(self.driver_settings['path']) / 'waifu2x-converter-cpp.exe')]
|
||||
|
||||
for key in self.waifu2x_settings.keys():
|
||||
for key in self.driver_settings.keys():
|
||||
|
||||
value = self.waifu2x_settings[key]
|
||||
value = self.driver_settings[key]
|
||||
|
||||
# the key doesn't need to be passed in this case
|
||||
if key == 'waifu2x_converter_path':
|
||||
execute.append('{}\\waifu2x-converter-cpp.exe'.format(str(value)))
|
||||
if key == 'path':
|
||||
continue
|
||||
|
||||
# null or None means that leave this option out (keep default)
|
||||
elif value is None or value is False:
|
||||
continue
|
||||
else:
|
||||
if len(key) == 1:
|
||||
execute.append('-{}'.format(key))
|
||||
execute.append(f'-{key}')
|
||||
else:
|
||||
execute.append('--{}'.format(key))
|
||||
execute.append(f'--{key}')
|
||||
|
||||
# true means key is an option
|
||||
if value is True:
|
||||
@@ -92,7 +89,7 @@ class Waifu2xConverter:
|
||||
|
||||
execute.append(str(value))
|
||||
|
||||
Avalon.debug_info('Executing: {}'.format(execute))
|
||||
Avalon.debug_info(f'Executing: {execute}')
|
||||
return subprocess.run(execute, check=True).returncode
|
||||
|
||||
except Exception as e:
|
||||
94
src/waifu2x_ncnn_vulkan.py
Executable file
94
src/waifu2x_ncnn_vulkan.py
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: Waifu2x NCNN Vulkan Driver
|
||||
Creator: SAT3LL
|
||||
Date Created: June 26, 2019
|
||||
Last Modified: November 15, 2019
|
||||
|
||||
Editor: K4YT3X
|
||||
Last Modified: January 4, 2020
|
||||
|
||||
Description: This class is a high-level wrapper
|
||||
for waifu2x_ncnn_vulkan.
|
||||
"""
|
||||
|
||||
# built-in imports
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
# third-party imports
|
||||
from avalon_framework import Avalon
|
||||
|
||||
|
||||
class Waifu2xNcnnVulkan:
|
||||
"""This class communicates with waifu2x ncnn vulkan engine
|
||||
|
||||
An object will be created for this class, containing information
|
||||
about the binary address and the processing method. When being called
|
||||
by the main program, other detailed information will be passed to
|
||||
the upscale function.
|
||||
"""
|
||||
|
||||
def __init__(self, driver_settings):
|
||||
self.driver_settings = driver_settings
|
||||
|
||||
# arguments passed through command line overwrites config file values
|
||||
|
||||
# waifu2x_ncnn_vulkan can't find its own model directory if its not in the current dir
|
||||
# so change to it
|
||||
os.chdir(os.path.join(self.driver_settings['path'], '..'))
|
||||
|
||||
self.print_lock = threading.Lock()
|
||||
|
||||
def upscale(self, input_directory, output_directory, scale_ratio, upscaler_exceptions):
|
||||
"""This is the core function for WAIFU2X class
|
||||
|
||||
Arguments:
|
||||
input_directory {string} -- source directory path
|
||||
output_directory {string} -- output directory path
|
||||
ratio {int} -- output video ratio
|
||||
"""
|
||||
|
||||
try:
|
||||
# overwrite config file settings
|
||||
self.driver_settings['i'] = input_directory
|
||||
self.driver_settings['o'] = output_directory
|
||||
self.driver_settings['s'] = int(scale_ratio)
|
||||
|
||||
# print thread start message
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info(f'[upscaler] Thread {threading.current_thread().name} started')
|
||||
self.print_lock.release()
|
||||
|
||||
# list to be executed
|
||||
# initialize the list with waifu2x binary path as the first element
|
||||
execute = [str(self.driver_settings['path'])]
|
||||
|
||||
for key in self.driver_settings.keys():
|
||||
|
||||
value = self.driver_settings[key]
|
||||
|
||||
# is executable key or null or None means that leave this option out (keep default)
|
||||
if key == 'path' or value is None or value is False:
|
||||
continue
|
||||
else:
|
||||
if len(key) == 1:
|
||||
execute.append(f'-{key}')
|
||||
else:
|
||||
execute.append(f'--{key}')
|
||||
execute.append(str(value))
|
||||
|
||||
Avalon.debug_info(f'Executing: {execute}')
|
||||
completed_command = subprocess.run(execute, check=True)
|
||||
|
||||
# print thread exiting message
|
||||
self.print_lock.acquire()
|
||||
Avalon.debug_info(f'[upscaler] Thread {threading.current_thread().name} exiting')
|
||||
self.print_lock.release()
|
||||
|
||||
# return command execution return code
|
||||
return completed_command.returncode
|
||||
except Exception as e:
|
||||
upscaler_exceptions.append(e)
|
||||
Reference in New Issue
Block a user