31 Commits

Author SHA1 Message Date
k4yt3x
f17d75539c fixed dockerfile label syntax 2022-02-19 01:49:23 +00:00
k4yt3x
82512ef10c updated CI to build multiple versions of containers 2022-02-17 05:43:12 +00:00
k4yt3x
ad479e53b8 renamed Dockerfiles for CI 2022-02-17 05:42:48 +00:00
k4yt3x
a356bfeaff fixed workflow container image name 2022-02-17 05:08:22 +00:00
k4yt3x
dee8e23485 changed get tag command 2022-02-17 04:59:24 +00:00
k4yt3x
1fa0821057 changed container build pipeline to release 2022-02-17 04:36:26 +00:00
k4yt3x
b5ecffba81 added container build workflow 2022-02-17 04:22:27 +00:00
k4yt3x
672c9b8652 bumped version to 5.0.0-beta4 2022-02-17 03:52:04 +00:00
k4yt3x
9f73e75f17 added audio, subtitles, data, and attachments copying 2022-02-17 03:51:55 +00:00
k4yt3x
ef1a8f3e41 added extra args for run scripts 2022-02-17 03:50:38 +00:00
k4yt3x
04f409ef80 updated discussion group heading 2022-02-17 02:01:06 +00:00
k4yt3x
51c8693dce fixed legeal info print 2022-02-16 03:02:26 +00:00
k4yt3x
bb572e2468 fixed argparse help bug 2022-02-16 02:56:35 +00:00
k4yt3x
c07fafc0e9 bumped version to 5.0.0-beta3 2022-02-15 07:02:53 +00:00
k4yt3x
595b179d3c updated Library link 2022-02-15 05:49:24 +00:00
k4yt3x
c865d494a1 updated run source script paths 2022-02-15 02:30:22 +00:00
k4yt3x
f4acb2188d renamed container run scripts 2022-02-15 00:59:40 +00:00
k4yt3x
01d4006c75 changed -d to -a 2022-02-15 00:55:54 +00:00
k4yt3x
a7f0f34751 terminology change: driver -> algorithm 2022-02-15 00:54:17 +00:00
k4yt3x
b6b1bf9f0e added two container debugging scripts 2022-02-15 00:52:28 +00:00
k4yt3x
5d7a53a2fc added Python and shell examples 2022-02-15 00:52:21 +00:00
k4yt3x
b32e0ec132 updated containers documentation path 2022-02-15 00:06:56 +00:00
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
22 changed files with 476 additions and 242 deletions

61
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Release
on:
push:
branches:
- master
tags:
- "*"
jobs:
setup:
name: Setup
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.get_tag.outputs.tag }}
steps:
- name: Get tag
id: get_tag
run: echo ::set-output name=tag::${GITHUB_REF/refs\/tags\//}
create-release:
name: Create release
needs:
- setup
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.setup.outputs.tag }}
release_name: Video2X ${{ needs.setup.outputs.tag }}
draft: true
prerelease: false
container:
name: Build and upload container
needs:
- setup
- create-release
strategy:
matrix:
version:
- slim-alpine
- cuda
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: mr-smithers-excellent/docker-build-push@v5
name: Build & push Docker image
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
dockerfile: Dockerfile.${{ matrix.version }}
image: video2x
tags: latest, ${{ needs.setup.outputs.tag }}-${{ matrix.version }}

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 \

View File

@@ -18,7 +18,7 @@ RUN apk add --no-cache \
# stage 2: install wheels into final image
FROM docker.io/library/python:3.10.2-alpine3.15
LABEL maintainer="K4YT3X <i@k4yt3x.com>"
LABEL maintainer="K4YT3X <i@k4yt3x.com>" \
org.opencontainers.image.source="https://github.com/k4yt3x/video2x" \
org.opencontainers.image.description="A lossless video/GIF/image upscaler"

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

@@ -8,7 +8,7 @@
<img src="https://img.shields.io/badge/dynamic/json?color=%23e85b46&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F4507807&style=flat-square"/>
</p>
### Official [Telegram Discussion Group](https://t.me/video2x)
## [💬 Telegram Discussion Group](https://t.me/video2x)
Join our Telegram discussion group to ask any questions you have about Video2X, chat directly with the developers, or discuss about upscaling technologies and the future of Video2X in general.
@@ -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)
@@ -26,11 +26,11 @@ Nightly releases are automatically created by the GitHub Actions CI/CD pipelines
## [📦 Container Image](https://github.com/k4yt3x/video2x/pkgs/container/video2x)
Video2X container images are available on the GitHub Container Registry for easy deployment on Linux and macOS. If you already have Docker/Podman installed, only one command is needed to start upscaling a video. For more information on how to use Video2X's Docker image, please refer to the [documentations (outdated)](https://github.com/K4YT3X/video2x/wiki/Docker).
Video2X container images are available on the GitHub Container Registry for easy deployment on Linux and macOS. If you already have Docker/Podman installed, only one command is needed to start upscaling a video. For more information on how to use Video2X's Docker image, please refer to the [documentations](https://github.com/K4YT3X/video2x/wiki/Container).
## [📖 Documentations](https://github.com/k4yt3x/video2x/wiki)
Video2X's documentations are hosted on this repository's [Wiki page](https://github.com/k4yt3x/video2x/wiki). It includes comprehensive explanations for how to use the [GUI](https://github.com/k4yt3x/video2x/wiki/GUI), the [CLI](https://github.com/k4yt3x/video2x/wiki/CLI), the [container image](https://github.com/K4YT3X/video2x/wiki/Container), the library, and more. The Wiki is open to edits by the community, so you, yes you, can also correct errors or add new contents to the documentations.
Video2X's documentations are hosted on this repository's [Wiki page](https://github.com/k4yt3x/video2x/wiki). It includes comprehensive explanations for how to use the [GUI](https://github.com/k4yt3x/video2x/wiki/GUI), the [CLI](https://github.com/k4yt3x/video2x/wiki/CLI), the [container image](https://github.com/K4YT3X/video2x/wiki/Container), the [library](https://github.com/k4yt3x/video2x/wiki/Library), and more. The Wiki is open to edits by the community, so you, yes you, can also correct errors or add new contents to the documentations.
## Introduction
@@ -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

@@ -0,0 +1,10 @@
#!/bin/sh
set -euxo pipefail
sudo podman run \
-it --rm --gpus all -v /dev/dri:/dev/dri \
-v $PWD/data:/host \
ghcr.io/k4yt3x/video2x:5.0.0-beta3-cuda \
-i input.mp4 -o output.mp4 \
interpolate

View File

@@ -0,0 +1,11 @@
#!/bin/sh
set -euxo pipefail
sudo podman run \
-it --rm --gpus all -v /dev/dri:/dev/dri \
-v $PWD/data:/host \
ghcr.io/k4yt3x/video2x:5.0.0-beta3-cuda \
-i input.mp4 -o output.mp4 \
-p5 upscale \
-h 720 -a waifu2x -n3

View File

@@ -0,0 +1,21 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# built-in imports
import pathlib
# import video2x
from video2x import Video2X
# create video2x object
video2x = Video2X()
# run upscale
video2x.interpolate(
pathlib.Path("input.mp4"), # input video path
pathlib.Path("output.mp4"), # another
3, # processes: number of parallel processors
10, # threshold: adjacent frames with > n% diff won't be processed (100 == process all)
"rife", # algorithm: the algorithm to use to process the video
)

24
examples/run_upscale_waifu2x.py Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# built-in imports
import pathlib
# import video2x
from video2x import Video2X
# create video2x object
video2x = Video2X()
# run upscale
video2x.upscale(
pathlib.Path("input.mp4"), # input video path
pathlib.Path("output.mp4"), # another
None, # width: width of output, None == auto
720, # height: height of output, None == auto
3, # noise: noise level, algorithm-dependent
5, # processes: number of parallel processors
0, # threshold: adjacent frames with < n% diff won't be processed (0 == process all)
"waifu2x", # algorithm: the algorithm to use to process the video
)

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

@@ -0,0 +1,19 @@
#!/bin/sh
# mount the current (video2x repo root) directory into a container
# with drivers installed so the code can be debugged in the container
# this one launches an interactive shell instead of Python
set -euo pipefail
sudo podman run -it --rm \
--gpus all -v /dev/dri:/dev/dri \
-v $PWD:/host \
-m 15g \
--cpus 0.9 \
-v $HOME/projects/media2x/video2x:/video2x \
-e PYTHONPATH=/video2x \
-e PYTHONDONTWRITEBYTECODE=1 \
--entrypoint=/bin/bash \
ghcr.io/k4yt3x/video2x:5.0.0-beta4-cuda
# alias upscale='python3 -m video2x -i /host/input-large.mp4 -o /host/output-large.mp4 -p3 upscale -h 1440 -d waifu2x -n3'

View File

@@ -0,0 +1,19 @@
#!/bin/sh
# mount the current (video2x repo root) directory into a container
# with drivers installed so the code can be debugged in the container
set -euo pipefail
sudo podman run -it --rm \
--gpus all -v /dev/dri:/dev/dri \
-v $PWD:/host \
-m 15g \
--cpus 0.9 \
-v $HOME/projects/media2x/video2x:/video2x \
-e PYTHONPATH=/video2x \
-e PYTHONDONTWRITEBYTECODE=1 \
ghcr.io/k4yt3x/video2x:5.0.0-beta4-cuda \
-i data/input.mp4 -o data/output.mp4 \
-p3 \
upscale \
-h 1440 -a waifu2x -n3

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

@@ -19,9 +19,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Package Init
Author: K4YT3X
Date Created: July 3, 2021
Last Modified: February 11, 2022
Last Modified: February 16, 2022
"""
# version assignment has to precede imports to
# prevent setup.cfg from producing import errors
__version__ = "5.0.0-beta4"
# 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 16, 2022
"""
# built-in imports
import contextlib
import os
import pathlib
import queue
import signal
import subprocess
import threading
@@ -57,7 +59,7 @@ class VideoDecoder(threading.Thread):
processing_queue: queue.Queue,
processing_settings: tuple,
ignore_max_image_pixels=True,
):
) -> None:
threading.Thread.__init__(self)
self.running = False
self.input_path = input_path
@@ -89,7 +91,7 @@ class VideoDecoder(threading.Thread):
# stderr=subprocess.DEVNULL,
)
def run(self):
def run(self) -> None:
self.running = True
# the index of the frame
@@ -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,12 +152,17 @@ 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()
def stop(self):
def stop(self) -> None:
self.running = False

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Video Encoder
Author: K4YT3X
Date Created: June 17, 2021
Last Modified: June 30, 2021
Last Modified: February 16, 2022
"""
# built-in imports
@@ -61,7 +61,11 @@ class VideoEncoder(threading.Thread):
total_frames: int,
processed_frames: multiprocessing.managers.ListProxy,
processed: multiprocessing.sharedctypes.Synchronized,
):
copy_audio: bool = True,
copy_subtitle: bool = True,
copy_data: bool = False,
copy_attachments: bool = False,
) -> None:
threading.Thread.__init__(self)
self.running = False
self.input_path = input_path
@@ -82,31 +86,31 @@ class VideoEncoder(threading.Thread):
r=frame_rate,
)
# map additional streams from original file
"""
# copy additional streams from original file
# https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1
additional_streams = [
# self.original["v?"],
self.original["a?"],
self.original["s?"],
self.original["d?"],
self.original["t?"],
# self.original["1:v?"],
self.original["a?"] if copy_audio is True else None,
self.original["s?"] if copy_subtitle is True else None,
self.original["d?"] if copy_data is True else None,
self.original["t?"] if copy_attachments is True else None,
]
"""
# run FFmpeg and produce final output
self.encoder = subprocess.Popen(
ffmpeg.compile(
ffmpeg.output(
frames,
*[s for s in additional_streams if s is not None],
str(self.output_path),
pix_fmt="yuv420p",
vcodec="libx264",
acodec="copy",
# acodec="copy",
r=frame_rate,
crf=17,
vsync="1",
# map_metadata=1,
# metadata="comment=Upscaled with Video2X",
map_metadata=1,
metadata="comment=Processed with Video2X",
)
.global_args("-hide_banner")
.global_args("-nostats")
@@ -123,7 +127,7 @@ class VideoEncoder(threading.Thread):
# stderr=subprocess.DEVNULL,
)
def run(self):
def run(self) -> None:
self.running = True
frame_index = 0
while self.running and frame_index < self.total_frames:
@@ -133,8 +137,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,10 +164,10 @@ 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()
def stop(self):
def stop(self) -> None:
self.running = False

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Interpolator
Author: K4YT3X
Date Created: May 27, 2021
Last Modified: February 2, 2022
Last Modified: February 16, 2022
"""
# local imports
@@ -38,7 +38,7 @@ from PIL import ImageChops, ImageStat
from loguru import logger
DRIVER_CLASSES = {"rife": Rife}
ALGORITHM_CLASSES = {"rife": Rife}
class Interpolator(multiprocessing.Process):
@@ -46,7 +46,7 @@ class Interpolator(multiprocessing.Process):
self,
processing_queue: multiprocessing.Queue,
processed_frames: multiprocessing.managers.ListProxy,
):
) -> None:
multiprocessing.Process.__init__(self)
self.running = False
self.processing_queue = processing_queue
@@ -54,10 +54,10 @@ class Interpolator(multiprocessing.Process):
signal.signal(signal.SIGTERM, self._stop)
def run(self):
def run(self) -> None:
self.running = True
logger.info(f"Interpolator process {self.name} initiating")
driver_objects = {}
processor_objects = {}
while self.running:
try:
try:
@@ -65,7 +65,7 @@ class Interpolator(multiprocessing.Process):
(
frame_index,
(image0, image1),
(difference_threshold, driver),
(difference_threshold, algorithm),
) = self.processing_queue.get(False)
except queue.Empty:
time.sleep(0.1)
@@ -86,13 +86,13 @@ class Interpolator(multiprocessing.Process):
# process the interpolation
if difference_ratio < difference_threshold:
# select a driver object with the required settings
# select a processor object with the required settings
# create a new object if none are available
driver_object = driver_objects.get(driver)
if driver_object is None:
driver_object = DRIVER_CLASSES[driver](0)
driver_objects[driver] = driver_object
interpolated_image = driver_object.process(image0, image1)
processor_object = processor_objects.get(algorithm)
if processor_object is None:
processor_object = ALGORITHM_CLASSES[algorithm](0)
processor_objects[algorithm] = processor_object
interpolated_image = processor_object.process(image0, image1)
# if the difference is greater than threshold
# there's a change in camera angle, ignore
@@ -116,5 +116,5 @@ class Interpolator(multiprocessing.Process):
self.running = False
return super().run()
def _stop(self, _signal_number, _frame):
def _stop(self, _signal_number, _frame) -> None:
self.running = False

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

@@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Upscaler
Author: K4YT3X
Date Created: May 27, 2021
Last Modified: August 17, 2021
Last Modified: February 16, 2022
"""
# local imports
@@ -40,15 +40,15 @@ import time
from PIL import Image, ImageChops, ImageStat
from loguru import logger
# fixed scaling ratios supported by the drivers
# fixed scaling ratios supported by the algorithms
# that only support certain fixed scale ratios
DRIVER_FIXED_SCALING_RATIOS = {
ALGORITHM_FIXED_SCALING_RATIOS = {
"waifu2x": [1, 2],
"srmd": [2, 3, 4],
"realsr": [4],
}
DRIVER_CLASSES = {"waifu2x": Waifu2x, "srmd": Srmd, "realsr": Realsr}
ALGORITHM_CLASSES = {"waifu2x": Waifu2x, "srmd": Srmd, "realsr": Realsr}
class Upscaler(multiprocessing.Process):
@@ -56,7 +56,7 @@ class Upscaler(multiprocessing.Process):
self,
processing_queue: multiprocessing.Queue,
processed_frames: multiprocessing.managers.ListProxy,
):
) -> None:
multiprocessing.Process.__init__(self)
self.running = False
self.processing_queue = processing_queue
@@ -64,10 +64,12 @@ class Upscaler(multiprocessing.Process):
signal.signal(signal.SIGTERM, self._stop)
def run(self):
def run(self) -> None:
self.running = True
logger.info(f"Upscaler process {self.name} initiating")
driver_objects = {}
logger.opt(colors=True).info(
f"Upscaler process <blue>{self.name}</blue> initiating"
)
processor_objects = {}
while self.running:
try:
try:
@@ -80,7 +82,7 @@ class Upscaler(multiprocessing.Process):
output_height,
noise,
difference_threshold,
driver,
algorithm,
),
) = self.processing_queue.get(False)
@@ -121,9 +123,9 @@ class Upscaler(multiprocessing.Process):
# calculate required minimum scale ratio
output_scale = max(output_width / width, output_height / height)
# select the optimal driver scaling ratio to use
# select the optimal algorithm scaling ratio to use
supported_scaling_ratios = sorted(
DRIVER_FIXED_SCALING_RATIOS[driver]
ALGORITHM_FIXED_SCALING_RATIOS[algorithm]
)
remaining_scaling_ratio = math.ceil(output_scale)
@@ -161,17 +163,17 @@ class Upscaler(multiprocessing.Process):
for job in scaling_jobs:
# select a driver object with the required settings
# select a processor object with the required settings
# create a new object if none are available
driver_object = driver_objects.get((driver, job))
if driver_object is None:
driver_object = DRIVER_CLASSES[driver](
processor_object = processor_objects.get((algorithm, job))
if processor_object is None:
processor_object = ALGORITHM_CLASSES[algorithm](
scale=job, noise=noise
)
driver_objects[(driver, job)] = driver_object
processor_objects[(algorithm, job)] = processor_object
# process the image with the selected driver
image1 = driver_object.process(image1)
# process the image with the selected algorithm
image1 = processor_object.process(image1)
# downscale the image to the desired output size and save the image to disk
image1 = image1.resize((output_width, output_height), Image.LANCZOS)
@@ -185,9 +187,11 @@ 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()
def _stop(self, _signal_number, _frame):
def _stop(self, _signal_number, _frame) -> None:
self.running = False

View File

@@ -26,8 +26,8 @@ __ __ _ _ ___ __ __
Name: Video2X
Creator: K4YT3X
Date Created: Feb 24, 2018
Last Modified: February 8, 2022
Date Created: February 24, 2018
Last Modified: February 16, 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,35 +58,59 @@ 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
LEGAL_INFO = """Video2X\t\t{}
Author:\t\tK4YT3X
License:\tGNU AGPL v3
Github Page:\thttps://github.com/k4yt3x/video2x
Contact:\ti@k4yt3x.com""".format(
__version__
)
UPSCALING_DRIVERS = [
# algorithms available for upscaling tasks
UPSCALING_ALGORITHMS = [
"waifu2x",
"srmd",
"realsr",
]
INTERPOLATION_DRIVERS = ["rife"]
# algorithms available for frame interpolation tasks
INTERPOLATION_ALGORITHMS = ["rife"]
# fixed scaling ratios supported by the drivers
# that only support certain fixed scale ratios
DRIVER_FIXED_SCALING_RATIOS = {
"waifu2x": [1, 2],
"srmd": [2, 3, 4],
"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:
@@ -97,10 +122,10 @@ class Video2X:
- interpolate: perform motion interpolation on a file
"""
def __init__(self):
self.version = "5.0.0"
def __init__(self) -> None:
self.version = __version__
def _get_video_info(self, path: pathlib.Path):
def _get_video_info(self, path: pathlib.Path) -> tuple:
"""
get video file information with FFmpeg
@@ -142,10 +167,26 @@ class Video2X:
mode: str,
processes: int,
processing_settings: tuple,
):
) -> None:
# 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 +202,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 +216,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 +224,58 @@ 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)
# if no exceptions were produced
else:
logger.success("Processing completed successfully")
finally:
# mark processing queue as closed
@@ -230,6 +296,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,
@@ -239,7 +317,7 @@ class Video2X:
noise: int,
processes: int,
threshold: float,
driver: str,
algorithm: str,
) -> None:
# get basic video information
@@ -274,7 +352,7 @@ class Video2X:
output_height,
noise,
threshold,
driver,
algorithm,
),
)
@@ -284,7 +362,7 @@ class Video2X:
output_path: pathlib.Path,
processes: int,
threshold: float,
driver: str,
algorithm: str,
) -> None:
# get video basic information
@@ -306,7 +384,7 @@ class Video2X:
Interpolator,
"interpolate",
processes,
(threshold, driver),
(threshold, algorithm),
)
@@ -321,7 +399,7 @@ def parse_arguments() -> argparse.Namespace:
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-v", "--version", help="show version information and exit", action="store_true"
"--version", help="show version information and exit", action="store_true"
)
parser.add_argument(
"-i",
@@ -352,7 +430,12 @@ def parse_arguments() -> argparse.Namespace:
help="action to perform", dest="action", required=True
)
upscale = action.add_parser("upscale", help="upscale a file", add_help=False)
upscale = action.add_parser(
"upscale",
help="upscale a file",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
)
upscale.add_argument(
"--help", action="help", help="show this help message and exit"
)
@@ -360,52 +443,65 @@ def parse_arguments() -> argparse.Namespace:
upscale.add_argument("-h", "--height", type=int, help="output height")
upscale.add_argument("-n", "--noise", type=int, help="denoise level", default=3)
upscale.add_argument(
"-d",
"--driver",
choices=UPSCALING_DRIVERS,
help="driver to use for upscaling",
default=UPSCALING_DRIVERS[0],
"-a",
"--algorithm",
choices=UPSCALING_ALGORITHMS,
help="algorithm to use for upscaling",
default=UPSCALING_ALGORITHMS[0],
)
upscale.add_argument(
"-t",
"--threshold",
type=float,
help="skip if the % difference between two adjacent frames is below this value; set to 0 to process all frames",
help=(
"skip if the percent difference between two adjacent frames is below this"
" value; set to 0 to process all frames"
),
default=0,
)
# interpolator arguments
interpolate = action.add_parser(
"interpolate", help="interpolate frames for file", add_help=False
"interpolate",
help="interpolate frames for file",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
)
interpolate.add_argument(
"--help", action="help", help="show this help message and exit"
)
interpolate.add_argument(
"-d",
"--driver",
choices=UPSCALING_DRIVERS,
help="driver to use for upscaling",
default=INTERPOLATION_DRIVERS[0],
"-a",
"--algorithm",
choices=UPSCALING_ALGORITHMS,
help="algorithm to use for upscaling",
default=INTERPOLATION_ALGORITHMS[0],
)
interpolate.add_argument(
"-t",
"--threshold",
type=float,
help="skip if the % difference between two adjacent frames exceeds this value; set to 100 to interpolate all frames",
help=(
"skip if the percent difference between two adjacent frames exceeds this"
" value; set to 100 to interpolate all frames"
),
default=10,
)
return parser.parse_args()
def main():
def main() -> None:
"""
command line direct invocation
program entry point
"""
try:
# display version and lawful informaition
if "--version" in sys.argv:
print(LEGAL_INFO)
sys.exit(0)
# parse command line arguments
args = parse_arguments()
@@ -414,16 +510,17 @@ 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()
# display version and lawful informaition
if args.version:
print(LEGAL_INFO)
sys.exit(0)
# add new sink with custom handler
logger.add(sys.stderr, colorize=True, format=LOGURU_FORMAT)
# 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()
@@ -437,7 +534,7 @@ def main():
args.noise,
args.processes,
args.threshold,
args.driver,
args.algorithm,
)
elif args.action == "interpolate":
@@ -446,7 +543,13 @@ def main():
args.output,
args.processes,
args.threshold,
args.driver,
args.algorithm,
)
# don't print the traceback for manual terminations
except KeyboardInterrupt as e:
raise SystemExit(e)
except Exception as e:
logger.exception(e)
raise SystemExit(e)