9 Commits

Author SHA1 Message Date
k4yt3x
6378a36d91 remove encoded images from memory to prevent exhaustion 2022-02-13 03:47:27 +00:00
k4yt3x
0d0fd70a24 redirected STDOUT and STDERR to prevent output from breaking the progress bar 2022-02-12 23:51:30 +00:00
K4YT3X
7c0e9c45d8 Merge pull request #598 from mayiflex/master
Update README.md
2022-02-12 20:24:07 +00:00
k4yt3x
7b60041529 replaced tqdm with rich.progress; enhanced error handling 2022-02-12 09:08:28 +00:00
k4yt3x
6ffd6282e0 removed tqdm files from project 2022-02-12 09:08:05 +00:00
mayiflex
c37e7f0d72 Update README.md
Fixed hyperlink to the Collab Notebook with instructions
2022-02-12 09:58:54 +01:00
k4yt3x
ca1e593874 let setup read version from init 2022-02-12 06:45:59 +00:00
k4yt3x
bfb0f339e2 fixed log format issue 2022-02-12 06:45:33 +00:00
k4yt3x
3690337092 fixed FFmpeg memory exhaustion issue by limiting the queue size 2022-02-12 05:14:00 +00:00
13 changed files with 199 additions and 143 deletions

View File

@@ -1,7 +1,7 @@
# Name: Video2X Dockerfile (CUDA)
# Creator: K4YT3X
# Date Created: February 3, 2022
# Last Modified: February 4, 2022
# Last Modified: February 12, 2022
# stage 1: build the python components into wheels
FROM docker.io/nvidia/cuda:11.6.0-runtime-ubuntu20.04 AS builder
@@ -11,7 +11,7 @@ COPY . /video2x
WORKDIR /video2x
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3-pip python3-opencv python3-pil python3-tqdm \
python3-pip python3-opencv python3-pil \
python3-dev libvulkan-dev glslang-dev glslang-tools \
build-essential swig git \
&& git config --global http.postBuffer 1048576000 \
@@ -33,7 +33,7 @@ COPY . /video2x
WORKDIR /video2x
RUN apt-get install -y --no-install-recommends \
python3-pip python3-dev \
python3-opencv python3-pil python3-tqdm \
python3-opencv python3-pil \
mesa-vulkan-drivers ffmpeg \
&& pip install --no-cache-dir --no-index -f /wheels . \
&& apt-get clean \

5
NOTICE
View File

@@ -41,8 +41,3 @@ https://github.com/python-pillow/Pillow.
This product depends on Rich, which is available under
the MIT License. The source code can be found at
https://github.com/Textualize/rich.
This product depends on tqdm, which is available under
the Mozilla Public License Version 2.0 and the MIT License.
The source code can be found at
https://github.com/tqdm/tqdm.

View File

@@ -18,7 +18,7 @@ The latest Windows update is built based on version 4.8.1. GUI is not available
## [📔 Google Colab](https://colab.research.google.com/drive/1gWEwcA9y57EsxwOjmLNmNMXPsafw0kGo)
You can use Video2X on [Google Colab](https://colab.research.google.com/) **for free** if you don't have a powerful GPU of your own. You can borrow a powerful GPU (Tesla K80, T4, P4, or P100) on Google's server for free for a maximum of 12 hours per session. **Please use the free resource fairly** and do not create sessions back-to-back and run upscaling 24/7. This might result in you getting banned. You can get [Colab Pro/Pro+](https://colab.research.google.com/signup/pricing) if you'd like to use better GPUs and get longer runtimes. Usage instructions are embedded in the [Colab Notebook](https://github.com/k4yt3x/video2x/actions/workflows/ci.yml).
You can use Video2X on [Google Colab](https://colab.research.google.com/) **for free** if you don't have a powerful GPU of your own. You can borrow a powerful GPU (Tesla K80, T4, P4, or P100) on Google's server for free for a maximum of 12 hours per session. **Please use the free resource fairly** and do not create sessions back-to-back and run upscaling 24/7. This might result in you getting banned. You can get [Colab Pro/Pro+](https://colab.research.google.com/signup/pricing) if you'd like to use better GPUs and get longer runtimes. Usage instructions are embedded in the [Colab Notebook](https://colab.research.google.com/drive/1gWEwcA9y57EsxwOjmLNmNMXPsafw0kGo).
## [🌙 Download Nightly Releases](https://github.com/k4yt3x/video2x/actions/workflows/ci.yml)
@@ -82,29 +82,29 @@ Copyright (c) 2018-2022 K4YT3X and contributors.
This project includes or depends on these following projects:
| Project | License |
| ------------------------------------------------------------------- | -------------------- |
| [FFmpeg](https://www.ffmpeg.org/) | LGPLv2.1, GPLv2 |
| [waifu2x-ncnn-vulkan](https://github.com/nihui/waifu2x-ncnn-vulkan) | MIT License |
| [srmd-ncnn-vulkan](https://github.com/nihui/srmd-ncnn-vulkan) | MIT License |
| [realsr-ncnn-vulkan](https://github.com/nihui/realsr-ncnn-vulkan) | MIT License |
| [rife-ncnn-vulkan](https://github.com/nihui/rife-ncnn-vulkan) | MIT License |
| [ffmpeg-python](https://github.com/kkroening/ffmpeg-python) | Apache-2.0 |
| [Loguru](https://github.com/Delgan/loguru) | MIT License |
| [opencv-python](https://github.com/opencv/opencv-python) | MIT License |
| [Pillow](https://github.com/python-pillow/Pillow) | HPND License |
| [Rich](https://github.com/Textualize/rich) | MIT License |
| [tqdm](https://github.com/tqdm/tqdm) | MPLv2.0, MIT License |
| Project | License |
| ------------------------------------------------------------------- | --------------- |
| [FFmpeg](https://www.ffmpeg.org/) | LGPLv2.1, GPLv2 |
| [waifu2x-ncnn-vulkan](https://github.com/nihui/waifu2x-ncnn-vulkan) | MIT License |
| [srmd-ncnn-vulkan](https://github.com/nihui/srmd-ncnn-vulkan) | MIT License |
| [realsr-ncnn-vulkan](https://github.com/nihui/realsr-ncnn-vulkan) | MIT License |
| [rife-ncnn-vulkan](https://github.com/nihui/rife-ncnn-vulkan) | MIT License |
| [ffmpeg-python](https://github.com/kkroening/ffmpeg-python) | Apache-2.0 |
| [Loguru](https://github.com/Delgan/loguru) | MIT License |
| [opencv-python](https://github.com/opencv/opencv-python) | MIT License |
| [Pillow](https://github.com/python-pillow/Pillow) | HPND License |
| [Rich](https://github.com/Textualize/rich) | MIT License |
Legacy versions of this project includes or depends on these following projects:
| Project | License |
| --------------------------------------------------------------------------- | ----------- |
| [waifu2x-caffe](https://github.com/lltcggie/waifu2x-caffe) | MIT License |
| [waifu2x-converter-cpp](https://github.com/DeadSix27/waifu2x-converter-cpp) | MIT License |
| [Anime4K](https://github.com/bloc97/Anime4K) | MIT License |
| [Anime4KCPP](https://github.com/TianZerL/Anime4KCPP) | MIT License |
| [Gifski](https://github.com/ImageOptim/gifski) | AGPLv3 |
| Project | License |
| --------------------------------------------------------------------------- | -------------------- |
| [waifu2x-caffe](https://github.com/lltcggie/waifu2x-caffe) | MIT License |
| [waifu2x-converter-cpp](https://github.com/DeadSix27/waifu2x-converter-cpp) | MIT License |
| [Anime4K](https://github.com/bloc97/Anime4K) | MIT License |
| [Anime4KCPP](https://github.com/TianZerL/Anime4KCPP) | MIT License |
| [Gifski](https://github.com/ImageOptim/gifski) | AGPLv3 |
| [tqdm](https://github.com/tqdm/tqdm) | MPLv2.0, MIT License |
More licensing information can be found in the [NOTICES](NOTICES) file.

View File

@@ -1,49 +0,0 @@
`tqdm` is a product of collaborative work.
Unless otherwise stated, all authors (see commit logs) retain copyright
for their respective work, and release the work under the MIT licence
(text below).
Exceptions or notable authors are listed below
in reverse chronological order:
* files: *
MPLv2.0 2015-2021 (c) Casper da Costa-Luis
[casperdcl](https://github.com/casperdcl).
* files: tqdm/_tqdm.py
MIT 2016 (c) [PR #96] on behalf of Google Inc.
* files: tqdm/_tqdm.py setup.py README.rst MANIFEST.in .gitignore
MIT 2013 (c) Noam Yorav-Raphael, original author.
[PR #96]: https://github.com/tqdm/tqdm/pull/96
Mozilla Public Licence (MPL) v. 2.0 - Exhibit A
-----------------------------------------------
This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this project,
You can obtain one at https://mozilla.org/MPL/2.0/.
MIT License (MIT)
-----------------
Copyright (c) 2013 noamraph
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,3 +1,3 @@
[build-system]
requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"]
requires = ["setuptools>=46.4", "wheel", "setuptools_scm[toml]>=3.4.3"]
build-backend = "setuptools.build_meta"

View File

@@ -1,7 +1,7 @@
# Name: Video2X PyPI setup file
# Creator: K4YT3X
# Date Created: June 17, 2021
# Last Modified: February 11, 2022
# Last Modified: February 12, 2022
# build & publish commands
# pip install --user -U setuptools wheel twine build
@@ -10,7 +10,7 @@
[metadata]
name = video2x
version = 5.0.0-beta1
version = attr: video2x.__version__
author = K4YT3X
author_email = i@k4yt3x.com
license = GNU Affero General Public License v3.0
@@ -39,7 +39,6 @@ install_requires =
opencv-python
pillow
rich
tqdm
realsr-ncnn-vulkan-python
rife-ncnn-vulkan-python
srmd-ncnn-vulkan-python

View File

@@ -22,6 +22,11 @@ Date Created: July 3, 2021
Last Modified: February 11, 2022
"""
# version assignment has to precede imports to
# prevent setup.cfg from producing import errors
__version__ = "5.0.0-beta2"
# local imports
from .video2x import Video2X
from .upscaler import Upscaler
from .interpolator import Interpolator

View File

@@ -22,6 +22,7 @@ Date Created: July 3, 2021
Last Modified: February 11, 2022
"""
# local imports
from .video2x import main

View File

@@ -19,13 +19,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Video Decoder
Author: K4YT3X
Date Created: June 17, 2021
Last Modified: June 17, 2021
Last Modified: February 12, 2022
"""
# built-in imports
import contextlib
import os
import pathlib
import queue
import signal
import subprocess
import threading
@@ -119,20 +121,29 @@ class VideoDecoder(threading.Thread):
"RGB", (self.input_width, self.input_height), buffer
)
self.processing_queue.put(
(
frame_index,
(previous_image, image),
self.processing_settings,
)
)
previous_image = image
# keep checking if the running flag is set to False
# while waiting to put the next image into the queue
while self.running:
with contextlib.suppress(queue.Full):
self.processing_queue.put(
(
frame_index,
(previous_image, image),
self.processing_settings,
),
timeout=0.1,
)
break
previous_image = image
frame_index += 1
# most likely "not enough image data"
except ValueError as e:
logger.exception(e)
# ignore queue closed
if not "is closed" in str(e):
logger.exception(e)
break
# send exceptions into the client connection pipe
@@ -141,9 +152,14 @@ class VideoDecoder(threading.Thread):
logger.exception(e)
break
# send SIGINT (2) to FFmpeg
# this instructs it to finalize and exit
if self.decoder.poll() is None:
self.decoder.send_signal(signal.SIGTERM)
# ensure the decoder has exited
self.decoder.wait()
logger.debug("Decoder thread exiting")
logger.info("Decoder thread exiting")
self.running = False
return super().run()

View File

@@ -133,8 +133,12 @@ class VideoEncoder(threading.Thread):
time.sleep(0.1)
continue
# send the image to FFmpeg for encoding
self.encoder.stdin.write(image.tobytes())
# remove the image from memory
self.processed_frames[frame_index] = None
with self.processed.get_lock():
self.processed.value += 1
@@ -156,7 +160,7 @@ class VideoEncoder(threading.Thread):
# wait for process to terminate
self.encoder.wait()
logger.debug("Encoder thread exiting")
logger.info("Encoder thread exiting")
self.running = False
return super().run()

View File

@@ -1,13 +0,0 @@
# *-ncnn-vulkan driver backends
realsr-ncnn-vulkan-python
rife-ncnn-vulkan-python
srmd-ncnn-vulkan-python
waifu2x-ncnn-vulkan-python
# regular Python packages from PyPI
ffmpeg-python
loguru
opencv-python
pillow
rich
tqdm

View File

@@ -66,7 +66,9 @@ class Upscaler(multiprocessing.Process):
def run(self):
self.running = True
logger.info(f"Upscaler process {self.name} initiating")
logger.opt(colors=True).info(
f"Upscaler process <blue>{self.name}</blue> initiating"
)
driver_objects = {}
while self.running:
try:
@@ -185,7 +187,9 @@ class Upscaler(multiprocessing.Process):
logger.exception(e)
break
logger.info(f"Upscaler process {self.name} terminating")
logger.opt(colors=True).info(
f"Upscaler process <blue>{self.name}</blue> terminating"
)
self.running = False
return super().run()

View File

@@ -27,7 +27,7 @@ __ __ _ _ ___ __ __
Name: Video2X
Creator: K4YT3X
Date Created: Feb 24, 2018
Last Modified: February 8, 2022
Last Modified: February 12, 2022
Editor: BrianPetkovsek
Last Modified: June 17, 2019
@@ -40,6 +40,7 @@ Last Modified: March 23, 2020
"""
# local imports
from . import __version__
from .decoder import VideoDecoder
from .encoder import VideoEncoder
from .interpolator import Interpolator
@@ -57,18 +58,27 @@ import time
# third-party imports
from loguru import logger
from rich import print
from tqdm import tqdm
from rich.console import Console
from rich.file_proxy import FileProxy
from rich.progress import (
BarColumn,
Progress,
ProgressColumn,
Task,
TimeElapsedColumn,
TimeRemainingColumn,
)
from rich.text import Text
import cv2
import ffmpeg
VERSION = "5.0.0"
LEGAL_INFO = """Video2X {}
Author: K4YT3X
License: GNU GPL v3
Github Page: https://github.com/k4yt3x/video2x
Contact: k4yt3x@k4yt3x.com""".format(
VERSION
__version__
)
UPSCALING_DRIVERS = [
@@ -87,6 +97,27 @@ DRIVER_FIXED_SCALING_RATIOS = {
"realsr": [4],
}
# progress bar labels for different modes
MODE_LABELS = {"upscale": "Upscaling", "interpolate": "Interpolating"}
# format string for Loguru loggers
LOGURU_FORMAT = (
"<green>{time:HH:mm:ss.SSSSSS!UTC}</green> | "
"<level>{level: <8}</level> | "
"<level>{message}</level>"
)
class ProcessingSpeedColumn(ProgressColumn):
"""Custom progress bar column that displays the processing speed"""
def render(self, task: Task) -> Text:
speed = task.finished_speed or task.speed
return Text(
f"{round(speed, 2) if isinstance(speed, float) else '?'} FPS",
style="progress.data.speed",
)
class Video2X:
"""
@@ -143,9 +174,25 @@ class Video2X:
processes: int,
processing_settings: tuple,
):
# record original STDOUT and STDERR for restoration
original_stdout = sys.stdout
original_stderr = sys.stderr
# create console for rich's Live display
console = Console()
# redirect STDOUT and STDERR to console
sys.stdout = FileProxy(console, sys.stdout)
sys.stderr = FileProxy(console, sys.stderr)
# re-add Loguru to point to the new STDERR
logger.remove()
logger.add(sys.stderr, colorize=True, format=LOGURU_FORMAT)
# initialize values
self.processor_processes = []
self.processing_queue = multiprocessing.Queue()
self.processing_queue = multiprocessing.Queue(maxsize=processes * 10)
processed_frames = multiprocessing.Manager().list([None] * total_frames)
self.processed = multiprocessing.Value("I", 0)
@@ -161,19 +208,11 @@ class Video2X:
)
self.decoder.start()
# in interpolate mode, the frame rate is doubled
if mode == "upscale":
progress = tqdm(total=total_frames, desc=f"UPSC {input_path.name}")
# elif mode == "interpolate":
else:
frame_rate *= 2
progress = tqdm(total=total_frames, desc=f"INTERP {input_path.name}")
# set up and start encoder thread
logger.info("Starting video encoder")
self.encoder = VideoEncoder(
input_path,
frame_rate,
frame_rate * 2 if mode == "interpolate" else frame_rate,
output_path,
output_width,
output_height,
@@ -183,7 +222,7 @@ class Video2X:
)
self.encoder.start()
# create upscaler processes
# create processor processes
for process_name in range(processes):
process = Processor(self.processing_queue, processed_frames)
process.name = str(process_name)
@@ -191,25 +230,54 @@ class Video2X:
process.start()
self.processor_processes.append(process)
# create progress bar
try:
# wait for jobs in queue to deplete
while self.encoder.is_alive() is True:
for process in self.processor_processes:
if not process.is_alive():
raise Exception("process died unexpectedly")
progress.n = self.processed.value
progress.refresh()
time.sleep(0.1)
# a temporary variable that stores the exception
exception = []
logger.info("Encoding has completed")
progress.n = self.processed.value
progress.refresh()
try:
# create progress bar
with Progress(
"[progress.description]{task.description}",
BarColumn(finished_style="green"),
"[progress.percentage]{task.percentage:>3.0f}%",
"[color(240)]({task.completed}/{task.total})",
ProcessingSpeedColumn(),
TimeElapsedColumn(),
"<",
TimeRemainingColumn(),
console=console,
disable=True,
) as progress:
task = progress.add_task(
f"[cyan]{MODE_LABELS.get(mode, 'Unknown')}", total=total_frames
)
# wait for jobs in queue to deplete
while self.processed.value < total_frames - 1:
time.sleep(0.5)
for process in self.processor_processes:
if not process.is_alive():
raise Exception("process died unexpectedly")
# show progress bar when upscale starts
if progress.disable is True and self.processed.value > 0:
progress.disable = False
progress.start()
# update progress
progress.update(task, completed=self.processed.value)
progress.update(task, completed=total_frames)
logger.info("Processing has completed")
# if SIGTERM is received or ^C is pressed
# TODO: pause and continue here
except (SystemExit, KeyboardInterrupt):
except (SystemExit, KeyboardInterrupt) as e:
logger.warning("Exit signal received, terminating")
exception.append(e)
except Exception as e:
logger.exception(e)
exception.append(e)
finally:
# mark processing queue as closed
@@ -230,6 +298,18 @@ class Video2X:
self.decoder.join()
self.encoder.join()
# raise the error if there is any
if len(exception) > 0:
raise exception[0]
# restore original STDOUT and STDERR
sys.stdout = original_stdout
sys.stderr = original_stderr
# re-add Loguru to point to the restored STDERR
logger.remove()
logger.add(sys.stderr, colorize=True, format=LOGURU_FORMAT)
def upscale(
self,
input_path: pathlib.Path,
@@ -414,17 +494,23 @@ def main():
if os.environ.get("LOGURU_LEVEL") is None:
os.environ["LOGURU_LEVEL"] = args.loglevel.upper()
# set logger format
if os.environ.get("LOGURU_FORMAT") is None:
os.environ[
"LOGURU_FORMAT"
] = "<fg 240>{time:HH:mm:ss!UTC}</fg 240> | <level>{level: <8}</level> | <level>{message}</level>"
# remove default handler
logger.remove()
# add new sink with custom handler
logger.add(sys.stderr, colorize=True, format=LOGURU_FORMAT)
# display version and lawful informaition
if args.version:
print(LEGAL_INFO)
sys.exit(0)
# print package version and copyright notice
logger.opt(colors=True).info(f"<magenta>Video2X {__version__}</magenta>")
logger.opt(colors=True).info(
"<magenta>Copyright (C) 2018-2022 K4YT3X and contributors.</magenta>"
)
# initialize upscaler object
video2x = Video2X()
@@ -448,5 +534,13 @@ def main():
args.threshold,
args.driver,
)
logger.success("Processing completed successfully")
# don't print the traceback for manual terminations
except (SystemExit, KeyboardInterrupt) as e:
raise SystemExit(e)
except Exception as e:
logger.exception(e)
raise SystemExit(e)