93 Commits

Author SHA1 Message Date
k4yt3x
84b730497b Revert "added rich console patch for Google Colab"
This reverts commit db0b87597d.
2022-09-04 21:56:40 +00:00
k4yt3x
db0b87597d added rich console patch for Google Colab 2022-09-04 21:41:57 +00:00
k4yt3x
102340e2be updated Pillow version requirements 2022-09-04 21:32:27 +00:00
k4yt3x
85437a8481 removed the walrus operator to remain compatible with Python 3.7 2022-09-04 00:06:46 +00:00
k4yt3x
176ae90bbb removed fps_mode since it causes incompatibility issues 2022-08-28 19:23:49 +00:00
k4yt3x
44238aed35 bumped vulkan image version to 1.3-470 2022-08-28 19:18:37 +00:00
k4yt3x
045e643867 pinned opencv-python to 4.5.5.64 due to bug #676 in version 4.6.0.66 2022-08-28 19:18:17 +00:00
k4yt3x
c92805e7bc added realcugan-ncnn-vulkan's license 2022-08-28 16:02:12 +00:00
k4yt3x
899fe3ae2d added realcugan-ncnn-vulkan into licenses section 2022-08-28 16:01:23 +00:00
k4yt3x
a75c2a50ca bumped version to 5.0.0-beta6 2022-08-28 16:01:07 +00:00
K4YT3X
508d6ea4d0 Merge pull request #695 from k4yt3x/feat/decouple 2022-08-28 11:51:51 -04:00
K4YT3X
8976dd8199 Merge branch 'master' into feat/decouple 2022-08-28 11:17:39 -04:00
k4yt3x
29a55e633c fixed the issue where the encoder cannot exit 2022-08-28 15:03:16 +00:00
k4yt3x
f7d6dc41b3 changed FFmpeg vsync to fps_mode 2022-08-28 03:44:27 +00:00
K4YT3X
d236131134 Merge pull request #644 from plambe/master
Fixed interpolation algorithms list
2022-05-08 17:17:12 -04:00
plambeto
d669654142 Fixed interpolation algorithms list 2022-05-08 21:16:09 +03:00
k4yt3x
4b0ab5382c added kill method to encoder 2022-05-01 08:46:33 +00:00
k4yt3x
737646a248 made processor objects a class var 2022-05-01 08:45:56 +00:00
k4yt3x
9fc0aa787e moved main to __main__; changed _run to use the new encoder and processor 2022-05-01 08:44:33 +00:00
k4yt3x
a041a60d87 added processor skeleton class 2022-05-01 08:43:13 +00:00
k4yt3x
020fb2dc80 updated value unpacking 2022-05-01 08:42:13 +00:00
k4yt3x
9a27960bf7 added decoder thread 2022-05-01 08:34:31 +00:00
k4yt3x
862b811517 moved main to __main__ 2022-05-01 08:34:00 +00:00
k4yt3x
e01d24c164 broken the upscaler into descrete parts 2022-04-28 14:33:33 +00:00
k4yt3x
0a052a3a72 simplified the encoder 2022-04-27 22:28:14 +00:00
k4yt3x
f3eaa47ec6 changed decoder from a thread into an iterator 2022-04-27 22:27:48 +00:00
K4YT3X
3f457907b6 Merge pull request #634 from snixon/checking_output
Adding check for valid output path
2022-04-13 23:33:52 -04:00
K4YT3X
a1d750e7ca combined output dir check with the previous section 2022-04-14 03:33:22 +00:00
Steve Nixon
22f656b800 Adding check for valid output path 2022-04-11 22:47:50 -07:00
k4yt3x
8eeba71ece added configs for flake8 and isort 2022-04-09 18:43:36 +00:00
k4yt3x
afca10a17b fixed #627 env vars lost in Popen processes 2022-04-09 18:40:43 +00:00
K4YT3X
f976bdc1c9 Merge pull request #629 from snixon/ffmpeg_subs_crash
ffmpeg can crash if source title has subtitles
2022-04-09 05:16:27 -04:00
Steve Nixon
51c0c38b34 ffmpeg can crash when a title has subs 2022-04-05 23:26:31 -07:00
k4yt3x
f2b2e11c41 bumped RealCUGAN to 1.0.2 2022-04-06 03:54:14 +00:00
k4yt3x
865e3bd193 fixed Flake8 issues 2022-04-06 03:39:32 +00:00
k4yt3x
e0dc8237f5 pinned pdm-pep517 version to >=0.12 2022-04-02 18:04:47 +00:00
k4yt3x
bbc1b57445 changed license field to license-expression (PEP 639) 2022-04-02 05:36:22 +00:00
k4yt3x
ebbe4570d5 check bools explicitly 2022-04-02 05:29:54 +00:00
k4yt3x
bcb2e97f89 fixed console script error 2022-04-01 07:52:28 +00:00
k4yt3x
ba29349e65 added licensing info for pynput 2022-04-01 07:21:18 +00:00
k4yt3x
1b67f373d1 removed UESR nobody as it breaks permissions 2022-04-01 06:51:27 +00:00
k4yt3x
7215ee1921 fixed release pipeline ref quoting 2022-04-01 06:27:15 +00:00
k4yt3x
65c0d32a51 updated the Dockerfiles and the pipeline for 5.0.0-beta5 2022-04-01 06:23:46 +00:00
k4yt3x
268460fd17 added pynput as dependency; fixed hotkey in headless env; disabled FFmpeg stdin 2022-03-21 21:46:08 +00:00
k4yt3x
d72ecb332a fixed variable name error 2022-03-21 03:43:48 +00:00
k4yt3x
8ba56e58c2 added pause function 2022-03-21 03:28:19 +00:00
k4yt3x
fa485b6cdd restricted Vulkan ICD files in Dockerfile 2022-03-21 00:21:00 +00:00
k4yt3x
625b340f3d added RealCUGAN 2022-03-19 17:45:03 +00:00
k4yt3x
fa3bd38217 fixed minor flake8 formatting issues 2022-03-18 23:14:20 +00:00
k4yt3x
0fca5c8e9a added cuda drivers to cuda Dockerfile; updated slim Dockerfile for current version 2022-03-18 23:10:40 +00:00
k4yt3x
237f6336ec sorted encoder input and updated vsync to cft from 1 2022-03-05 21:30:01 +00:00
k4yt3x
083af60c6f sorted imports using isort 2022-03-01 21:11:50 +00:00
k4yt3x
f7756886e7 changed progress bar color 2022-02-28 17:33:16 +00:00
k4yt3x
6e72df66fc added colored threat id for interp 2022-02-28 06:56:16 +00:00
k4yt3x
2bfcb13976 fixed fail to exit upon ^C 2022-02-28 06:49:41 +00:00
k4yt3x
49e0375eee updated PDM dependencies 2022-02-28 06:45:56 +00:00
k4yt3x
4459f4d3be redirected subprocess outputs into Rich console 2022-02-28 01:54:52 +00:00
k4yt3x
c0fe81bd2e improved main function return handling 2022-02-27 06:21:46 +00:00
k4yt3x
8cb64d3b70 install rife directly from PyPI; changed setuptools to pdm 2022-02-27 02:39:48 +00:00
k4yt3x
0b154a715c changed builder from setuptools to pdm 2022-02-27 02:38:56 +00:00
k4yt3x
05031b2b5a fixed GHA release workflow hook 2022-02-19 04:59:45 +00:00
k4yt3x
2510c8fa28 bumped script versions to beta4 2022-02-19 03:21:47 +00:00
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
32 changed files with 2030 additions and 749 deletions

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

@@ -0,0 +1,78 @@
name: Release
on:
push:
tags:
- "*"
jobs:
setup:
if: github.event.base_ref == 'refs/heads/master'
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
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: mr-smithers-excellent/docker-build-push@v5
name: Build & push the Docker image
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
dockerfile: Dockerfile
image: video2x
tags: latest, ${{ needs.setup.outputs.tag }}
container-variants:
name: Build and upload variants of the container
needs:
- setup
- create-release
strategy:
matrix:
version:
- slim-alpine
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: mr-smithers-excellent/docker-build-push@v5
name: Build & push the Docker image
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
dockerfile: Dockerfile.${{ matrix.version }}
image: video2x
tags: ${{ needs.setup.outputs.tag }}-${{ matrix.version }}

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
# PDM
.pdm.toml
__pypackages__/
# test videos
*.mp4
*.mkv

View File

@@ -1,43 +1,44 @@
# Name: Video2X Dockerfile (CUDA)
# Name: Video2X Dockerfile
# Creator: K4YT3X
# Date Created: February 3, 2022
# Last Modified: February 4, 2022
# Last Modified: August 28, 2022
# stage 1: build the python components into wheels
FROM docker.io/nvidia/cuda:11.6.0-runtime-ubuntu20.04 AS builder
FROM docker.io/nvidia/vulkan:1.3-470 AS builder
ENV DEBIAN_FRONTEND=noninteractive
COPY . /video2x
WORKDIR /video2x
RUN gpg --keyserver=keyserver.ubuntu.com --receive-keys A4B469963BF863CC \
&& gpg --export A4B469963BF863CC > /etc/apt/trusted.gpg.d/cuda.gpg
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3-pip python3-opencv python3-pil python3-tqdm \
python3-dev libvulkan-dev glslang-dev glslang-tools \
build-essential swig git \
&& git config --global http.postBuffer 1048576000 \
&& git config --global https.postBuffer 1048576000 \
&& pip wheel -w /wheels \
wheel setuptools setuptools_scm \
rife-ncnn-vulkan-python@git+https://github.com/media2x/rife-ncnn-vulkan-python.git .
python3.8 python3-pip python3-opencv python3-pil \
python3.8-dev libvulkan-dev glslang-dev glslang-tools \
build-essential swig \
&& pip wheel -w /wheels wheel pdm-pep517 .
# stage 2: install wheels into the final image
FROM docker.io/nvidia/cuda:11.6.0-runtime-ubuntu20.04
FROM docker.io/nvidia/vulkan:1.3-470
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"
ENV DEBIAN_FRONTEND=noninteractive
ENV VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json\
:/usr/share/vulkan/icd.d/radeon_icd.x86_64.json\
:/usr/share/vulkan/icd.d/intel_icd.x86_64.json
COPY --from=builder /var/lib/apt/lists* /var/lib/apt/lists/
COPY --from=builder /wheels /wheels
COPY . /video2x
WORKDIR /video2x
RUN apt-get install -y --no-install-recommends \
python3-pip python3-dev \
python3-opencv python3-pil python3-tqdm \
mesa-vulkan-drivers ffmpeg \
python3-pip python3.8-dev \
python3-opencv python3-pil \
mesa-vulkan-drivers cuda-drivers ffmpeg \
&& pip install --no-cache-dir --no-index -f /wheels . \
&& apt-get clean \
&& rm -rf /wheels /video2x /var/lib/apt/lists/*
WORKDIR /host
ENTRYPOINT ["/usr/bin/python3", "-m", "video2x"]
ENTRYPOINT ["/usr/bin/python3.8", "-m", "video2x"]

View File

@@ -1,24 +1,22 @@
# Name: Video2X Dockerfile (Slim Alpine)
# Creator: K4YT3X
# Date Created: February 1, 2022
# Last Modified: February 4, 2022
# Last Modified: March 18, 2022
# stage: build python components into heels
FROM docker.io/library/python:3.10.2-alpine3.15 AS builder
FROM docker.io/library/python:3.10.4-alpine3.15 AS builder
COPY . /video2x
WORKDIR /video2x
RUN apk add --no-cache \
cmake g++ gcc git ninja swig \
cmake g++ gcc git ninja swig linux-headers \
ffmpeg-dev glslang-dev jpeg-dev vulkan-loader-dev zlib-dev \
linux-headers \
&& pip install -U pip \
&& CMAKE_ARGS="-DWITH_FFMPEG=YES" pip wheel -w /wheels opencv-python \
&& pip wheel -w /wheels wheel setuptools setuptools_scm \
rife-ncnn-vulkan-python@git+https://github.com/media2x/rife-ncnn-vulkan-python.git .
&& pip wheel -w /wheels wheel pdm-pep517 .
# stage 2: install wheels into final image
FROM docker.io/library/python:3.10.2-alpine3.15
LABEL maintainer="K4YT3X <i@k4yt3x.com>"
FROM docker.io/library/python:3.10.4-alpine3.15
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"

9
NOTICE
View File

@@ -3,7 +3,7 @@ Copyright (c) 2018-2022 K4YT3X and contributors.
This product depends on FFmpeg, which is available under
the GNU Lesser General Public License 2.1. The source code can be found at
https://www.gnu.org/licenses/agpl-3.0.txt.
https://github.com/FFmpeg/FFmpeg.
This product depends on waifu2x-ncnn-vulkan, which is available under
the MIT License. The source code can be found at
@@ -42,7 +42,6 @@ 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.
This product depends on pynput, which is available under
the GNU Lesser General Public License 3.0. The source code can be found at
https://github.com/moses-palmer/pynput.

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,31 @@ 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 |
| [realcugan-ncnn-vulkan](https://github.com/nihui/realcugan-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 |
| [pynput](https://github.com/moses-palmer/pynput) | LGPLv3.0 |
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.
@@ -117,6 +119,7 @@ Appreciations given to the following personnel who have contributed significantl
- [@ddouglas87](https://github.com/ddouglas87)
- [@lhanjian](https://github.com/lhanjian)
- [@ArchieMeng](https://github.com/archiemeng)
- [@nihui](https://github.com/nihui)
## Similar Projects

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-beta4-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-beta4-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

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 nihui
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,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.

165
licenses/lgpl-3.0.txt Normal file
View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

533
pdm.lock generated Normal file
View File

@@ -0,0 +1,533 @@
[[package]]
name = "cmake"
version = "3.22.3"
summary = "CMake is an open-source, cross-platform family of tools designed to build, test and package software"
[[package]]
name = "cmake-build-extension"
version = "0.5.0"
requires_python = ">=3.6"
summary = "Setuptools extension to build and package CMake projects."
dependencies = [
"GitPython",
"cmake",
"ninja",
"setuptools-scm",
]
[[package]]
name = "colorama"
version = "0.4.4"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
summary = "Cross-platform colored terminal text."
[[package]]
name = "commonmark"
version = "0.9.1"
summary = "Python parser for the CommonMark Markdown spec"
[[package]]
name = "evdev"
version = "1.4.0"
summary = "Bindings to the Linux input handling subsystem"
[[package]]
name = "ffmpeg-python"
version = "0.2.0"
summary = "Python bindings for FFmpeg - with complex filtering support"
dependencies = [
"future",
]
[[package]]
name = "future"
version = "0.18.2"
requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
summary = "Clean single-source support for Python 3 and 2"
[[package]]
name = "gitdb"
version = "4.0.9"
requires_python = ">=3.6"
summary = "Git Object Database"
dependencies = [
"smmap<6,>=3.0.1",
]
[[package]]
name = "gitpython"
version = "3.1.27"
requires_python = ">=3.7"
summary = "GitPython is a python library used to interact with Git repositories"
dependencies = [
"gitdb<5,>=4.0.1",
"typing-extensions>=3.7.4.3; python_version < \"3.8\"",
]
[[package]]
name = "loguru"
version = "0.6.0"
requires_python = ">=3.5"
summary = "Python logging made (stupidly) simple"
dependencies = [
"colorama>=0.3.4; sys_platform == \"win32\"",
"win32-setctime>=1.0.0; sys_platform == \"win32\"",
]
[[package]]
name = "ninja"
version = "1.10.2.3"
summary = "Ninja is a small build system with a focus on speed"
[[package]]
name = "numpy"
version = "1.22.3"
requires_python = ">=3.8"
summary = "NumPy is the fundamental package for array computing with Python."
[[package]]
name = "opencv-python"
version = "4.5.5.64"
requires_python = ">=3.6"
summary = "Wrapper package for OpenCV python bindings."
dependencies = [
"numpy>=1.14.5; python_version >= \"3.7\"",
"numpy>=1.17.3; python_version >= \"3.8\"",
"numpy>=1.19.3; python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\"",
"numpy>=1.19.3; python_version >= \"3.9\"",
"numpy>=1.21.2; python_version >= \"3.10\"",
"numpy>=1.21.2; python_version >= \"3.6\" and platform_system == \"Darwin\" and platform_machine == \"arm64\"",
]
[[package]]
name = "packaging"
version = "21.3"
requires_python = ">=3.6"
summary = "Core utilities for Python packages"
dependencies = [
"pyparsing!=3.0.5,>=2.0.2",
]
[[package]]
name = "pillow"
version = "9.0.1"
requires_python = ">=3.7"
summary = "Python Imaging Library (Fork)"
[[package]]
name = "pygments"
version = "2.11.2"
requires_python = ">=3.5"
summary = "Pygments is a syntax highlighting package written in Python."
[[package]]
name = "pynput"
version = "1.7.6"
summary = "Monitor and control user input devices"
dependencies = [
"evdev>=1.3; \"linux\" in sys_platform",
"pyobjc-framework-ApplicationServices>=8.0; sys_platform == \"darwin\"",
"pyobjc-framework-Quartz>=8.0; sys_platform == \"darwin\"",
"python-xlib>=0.17; \"linux\" in sys_platform",
"six",
]
[[package]]
name = "pyobjc-core"
version = "8.4.1"
requires_python = ">=3.6"
summary = "Python<->ObjC Interoperability Module"
[[package]]
name = "pyobjc-framework-applicationservices"
version = "8.4.1"
requires_python = ">=3.6"
summary = "Wrappers for the framework ApplicationServices on macOS"
dependencies = [
"pyobjc-core>=8.4.1",
"pyobjc-framework-Cocoa>=8.4.1",
"pyobjc-framework-Quartz>=8.4.1",
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "8.4.1"
requires_python = ">=3.6"
summary = "Wrappers for the Cocoa frameworks on macOS"
dependencies = [
"pyobjc-core>=8.4.1",
]
[[package]]
name = "pyobjc-framework-quartz"
version = "8.4.1"
requires_python = ">=3.6"
summary = "Wrappers for the Quartz frameworks on macOS"
dependencies = [
"pyobjc-core>=8.4.1",
"pyobjc-framework-Cocoa>=8.4.1",
]
[[package]]
name = "pyparsing"
version = "3.0.7"
requires_python = ">=3.6"
summary = "Python parsing module"
[[package]]
name = "python-xlib"
version = "0.31"
summary = "Python X Library"
dependencies = [
"six>=1.10.0",
]
[[package]]
name = "realcugan-ncnn-vulkan-python"
version = "1.0.0"
requires_python = ">=3.6"
summary = "A Python FFI of nihui/realcugan-ncnn-vulkan achieved with SWIG"
dependencies = [
"pillow",
]
[[package]]
name = "realsr-ncnn-vulkan-python"
version = "1.0.4"
requires_python = ">=3.6"
summary = "A Python FFI of nihui/realsr-ncnn-vulkan achieved with SWIG"
dependencies = [
"pillow",
]
[[package]]
name = "rich"
version = "12.0.0"
requires_python = ">=3.6.2,<4.0.0"
summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
dependencies = [
"commonmark<0.10.0,>=0.9.0",
"pygments<3.0.0,>=2.6.0",
"typing-extensions<5.0,>=3.7.4; python_version < \"3.8\"",
]
[[package]]
name = "rife-ncnn-vulkan-python"
version = "1.1.2.post3"
requires_python = ">=3.6"
summary = "A Python FFI of nihui/rife-ncnn-vulkan achieved with SWIG"
dependencies = [
"pillow",
]
[[package]]
name = "setuptools"
version = "60.10.0"
requires_python = ">=3.7"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
[[package]]
name = "setuptools-scm"
version = "6.4.2"
requires_python = ">=3.6"
summary = "the blessed package to manage your versions by scm tags"
dependencies = [
"packaging>=20.0",
"setuptools",
"tomli>=1.0.0",
]
[[package]]
name = "six"
version = "1.16.0"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
summary = "Python 2 and 3 compatibility utilities"
[[package]]
name = "smmap"
version = "5.0.0"
requires_python = ">=3.6"
summary = "A pure Python implementation of a sliding window memory map manager"
[[package]]
name = "srmd-ncnn-vulkan-python"
version = "1.0.2"
requires_python = ">=3.6"
summary = "A Python FFI of nihui/srmd-ncnn-vulkan achieved with SWIG"
dependencies = [
"pillow",
]
[[package]]
name = "tomli"
version = "2.0.1"
requires_python = ">=3.7"
summary = "A lil' TOML parser"
[[package]]
name = "typing-extensions"
version = "4.1.1"
requires_python = ">=3.6"
summary = "Backported and Experimental Type Hints for Python 3.6+"
[[package]]
name = "waifu2x-ncnn-vulkan-python"
version = "1.0.1"
requires_python = ">=3.6"
summary = "A Python FFI of nihui/waifu2x-ncnn-vulkan achieved with SWIG"
dependencies = [
"cmake",
"cmake-build-extension",
"pillow",
]
[[package]]
name = "win32-setctime"
version = "1.1.0"
requires_python = ">=3.5"
summary = "A small Python utility to set file creation time on Windows"
[metadata]
lock_version = "3.1"
content_hash = "sha256:f72fb9417f87c737f6a94710af1039611dc2d723d056e2479172fc6b695cab7c"
[metadata.files]
"cmake 3.22.3" = [
{file = "cmake-3.22.3-py2.py3-none-macosx_10_10_universal2.macosx_10_10_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:d387e4062541094d302c80b3beed08cfa4f2ef98454d04f34fd1071c595152d8"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff1a89c264a6645d5d222f73efe83ea47abc96d77ae96e3e4eb4580f6f396fc"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3adc8532f5029931e82c9ae6a07b5d866b216b422cedc72074155c80db6e27d7"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c46f0573b0455a234c397fff2f3679e18925eb3da32069cc41f1aab7cd968260"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c68b283223375ff942bce5b9f2b286fa5ac94b02f848e52fcec2369a407b22"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:452e53346d52241fbebdb9487e88786221c92d4abfa59fb002dc928ada69399c"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb5fbcb8b45c9f9823a1bffffe8eab673e463b8d36b258856035725e04baf446"},
{file = "cmake-3.22.3-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e615d29c2c26faf7e3876c47bafbab0eefe29683e01bec3a14736e9694bdff73"},
{file = "cmake-3.22.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:1d6096da730d2d0c8352926b647d0d340fc2ea3a6497dd13cff1c999d68d9d7b"},
{file = "cmake-3.22.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:d3be0f5256bfe76c81d2db30216c937a7f7cefd251fb63c1cb30c5e0a896bbad"},
{file = "cmake-3.22.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:d73961a801ea9e2fdda9afa3b9fc84539e368ecec30de1ef54045abfabce4ece"},
{file = "cmake-3.22.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:45e89f6ff37283ecff912c7fe65dc4bc6dfe2fb78b7747b95272dd7423a3847f"},
{file = "cmake-3.22.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c776ad422f7ce3866497f132af4ba7240cb81d554ee5eb801fd5e2d8c7cd67d6"},
{file = "cmake-3.22.3-py2.py3-none-win32.whl", hash = "sha256:8d32168fb3153313408befc7b8b5593130bad7d5fe0b8df764bb723b8c710bfc"},
{file = "cmake-3.22.3-py2.py3-none-win_amd64.whl", hash = "sha256:d8db320fb36560fcf2ca66481009bc1d1f8ed45fd756ce91e4dee97026a0e3a4"},
{file = "cmake-3.22.3.tar.gz", hash = "sha256:6e1d1991775915dac047af851f2feec7eb35ab4b4610d3e7fd94d93bce8faf58"},
]
"cmake-build-extension 0.5.0" = [
{file = "cmake_build_extension-0.5.0-py3-none-any.whl", hash = "sha256:b11b8c140ef0a7595576c8a8f807dbae30924e78f92cdecd5be8060087a84e88"},
{file = "cmake-build-extension-0.5.0.tar.gz", hash = "sha256:75a8e866544976e213aad31799f6a9b68a5135cf37cc41af29c1706446d56155"},
]
"colorama 0.4.4" = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
"commonmark 0.9.1" = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
]
"evdev 1.4.0" = [
{file = "evdev-1.4.0.tar.gz", hash = "sha256:8782740eb1a86b187334c07feb5127d3faa0b236e113206dfe3ae8f77fb1aaf1"},
]
"ffmpeg-python 0.2.0" = [
{file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"},
{file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"},
]
"future 0.18.2" = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
"gitdb 4.0.9" = [
{file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
{file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
"gitpython 3.1.27" = [
{file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
{file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
]
"loguru 0.6.0" = [
{file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
{file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
]
"ninja 1.10.2.3" = [
{file = "ninja-1.10.2.3-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:d5e0275d28997a750a4f445c00bdd357b35cc334c13cdff13edf30e544704fbd"},
{file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ea785bf6a15727040835256577239fa3cf5da0d60e618c307aa5efc31a1f0ce"},
{file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29570a18d697fc84d361e7e6330f0021f34603ae0fcb0ef67ae781e9814aae8d"},
{file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a1d84d4c7df5881bfd86c25cce4cf7af44ba2b8b255c57bc1c434ec30a2dfc"},
{file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ca8dbece144366d5f575ffc657af03eb11c58251268405bc8519d11cf42f113"},
{file = "ninja-1.10.2.3-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:279836285975e3519392c93c26e75755e8a8a7fafec9f4ecbb0293119ee0f9c6"},
{file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cc8b31b5509a2129e4d12a35fc21238c157038022560aaf22e49ef0a77039086"},
{file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:688167841b088b6802e006f911d911ffa925e078c73e8ef2f88286107d3204f8"},
{file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:840a0b042d43a8552c4004966e18271ec726e5996578f28345d9ce78e225b67e"},
{file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:84be6f9ec49f635dc40d4b871319a49fa49b8d55f1d9eae7cd50d8e57ddf7a85"},
{file = "ninja-1.10.2.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6bd76a025f26b9ae507cf8b2b01bb25bb0031df54ed685d85fc559c411c86cf4"},
{file = "ninja-1.10.2.3-py2.py3-none-win32.whl", hash = "sha256:740d61fefb4ca13573704ee8fe89b973d40b8dc2a51aaa4e9e68367233743bb6"},
{file = "ninja-1.10.2.3-py2.py3-none-win_amd64.whl", hash = "sha256:0560eea57199e41e86ac2c1af0108b63ae77c3ca4d05a9425a750e908135935a"},
{file = "ninja-1.10.2.3.tar.gz", hash = "sha256:e1b86ad50d4e681a7dbdff05fc23bb52cb773edb90bc428efba33fa027738408"},
]
"numpy 1.22.3" = [
{file = "numpy-1.22.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75"},
{file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"},
{file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e"},
{file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4"},
{file = "numpy-1.22.3-cp310-cp310-win32.whl", hash = "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430"},
{file = "numpy-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4"},
{file = "numpy-1.22.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce"},
{file = "numpy-1.22.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe"},
{file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5"},
{file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1"},
{file = "numpy-1.22.3-cp38-cp38-win32.whl", hash = "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62"},
{file = "numpy-1.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676"},
{file = "numpy-1.22.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123"},
{file = "numpy-1.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802"},
{file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d"},
{file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168"},
{file = "numpy-1.22.3-cp39-cp39-win32.whl", hash = "sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa"},
{file = "numpy-1.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a"},
{file = "numpy-1.22.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f"},
{file = "numpy-1.22.3.zip", hash = "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18"},
]
"opencv-python 4.5.5.64" = [
{file = "opencv_python-4.5.5.64-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:a512a0c59b6fec0fac3844b2f47d6ecb1a9d18d235e6c5491ce8dbbe0663eae8"},
{file = "opencv_python-4.5.5.64-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6138b6903910e384067d001763d40f97656875487381aed32993b076f44375"},
{file = "opencv_python-4.5.5.64-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b293ced62f4360d9f11cf72ae7e9df95320ff7bf5b834d87546f844e838c0c35"},
{file = "opencv_python-4.5.5.64-cp36-abi3-win32.whl", hash = "sha256:6247e584813c00c3b9ed69a795da40d2c153dc923d0182e957e1c2f00a554ac2"},
{file = "opencv_python-4.5.5.64-cp36-abi3-win_amd64.whl", hash = "sha256:408d5332550287aa797fd06bef47b2dfed163c6787668cc82ef9123a9484b56a"},
{file = "opencv_python-4.5.5.64-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7787bb017ae93d5f9bb1b817ac8e13e45dd193743cb648498fcab21d00cf20a3"},
{file = "opencv-python-4.5.5.64.tar.gz", hash = "sha256:f65de0446a330c3b773cd04ba10345d8ce1b15dcac3f49770204e37602d0b3f7"},
]
"packaging 21.3" = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
"pillow 9.0.1" = [
{file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"},
{file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"},
{file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"},
{file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"},
{file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"},
{file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"},
{file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"},
{file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"},
{file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"},
{file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"},
{file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"},
{file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"},
{file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"},
{file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"},
{file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"},
{file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"},
{file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"},
{file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"},
{file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"},
{file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"},
{file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"},
{file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"},
{file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"},
{file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"},
{file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"},
{file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"},
{file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"},
{file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"},
{file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"},
{file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"},
{file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"},
{file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"},
{file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"},
{file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"},
{file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"},
]
"pygments 2.11.2" = [
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
]
"pynput 1.7.6" = [
{file = "pynput-1.7.6-py2.py3-none-any.whl", hash = "sha256:19861b2a0c430d646489852f89500e0c9332e295f2c020e7c2775e7046aa2e2f"},
{file = "pynput-1.7.6.tar.gz", hash = "sha256:3a5726546da54116b687785d38b1db56997ce1d28e53e8d22fc656d8b92e533c"},
]
"pyobjc-core 8.4.1" = [
{file = "pyobjc_core-8.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a89cac910fbd64728fe7ec0c7a3a7cf20959bc1d7e2f41db4d7800556e47745"},
{file = "pyobjc_core-8.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2cf1d4348cb99fcba04fa38199a46e35263b2fe7bb66e6dfbd4f19bc2602998d"},
{file = "pyobjc_core-8.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a130324b25c0f5f4cfe30b6a28b8e70865d6e1eee158caababb603906ef431d2"},
{file = "pyobjc_core-8.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c31195d1a8f00da99abf79643f902d09c709dbbe9c9b6feb6b585303c57d720c"},
{file = "pyobjc_core-8.4.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:e7fd2aefb53a96a8f688c8bb36c6ebd5446250a7251bfa6b688a045e05afc60b"},
{file = "pyobjc_core-8.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3191173ce268e23c84d84f036fc94c3a8749a6726fc7fe73baf27dbac14f7d0"},
{file = "pyobjc-core-8.4.1.tar.gz", hash = "sha256:df98669e957adb33566d9ef46773a5ac876a81afe8849c282d6a80448e35dd74"},
]
"pyobjc-framework-applicationservices 8.4.1" = [
{file = "pyobjc_framework_ApplicationServices-8.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:362d178c624a617fc60c3a35397550193d82da9a59272b215cf1dc6fb68c011c"},
{file = "pyobjc_framework_ApplicationServices-8.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:41f0f6be6d343f91de92cab053be4a983e3936b364ca267a94c6e06bc34ff7fe"},
{file = "pyobjc_framework_ApplicationServices-8.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:73287b758a70a037b6e74a60036f4adb1677407dfeaddee94102074d92539e6e"},
{file = "pyobjc_framework_ApplicationServices-8.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:287b541b66c5c931a11dde90d7be69ddd2e5c3624c2e980e679f5242ec3ee0da"},
{file = "pyobjc_framework_ApplicationServices-8.4.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:780fbe2e3b02237f79e2f5780094dd95b7308ecdb265c062b78a507b9564eb4d"},
{file = "pyobjc_framework_ApplicationServices-8.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:092c4835e73e5dab1f3c498511b28b2e96503671fc7c1bc25f732c5e687b7214"},
{file = "pyobjc-framework-ApplicationServices-8.4.1.tar.gz", hash = "sha256:b058466266412d2bf24b0303785c91538961367f26db616bf2f9f45c498a83a3"},
]
"pyobjc-framework-cocoa 8.4.1" = [
{file = "pyobjc_framework_Cocoa-8.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cfbe038a0108ae1b45f8f7067af70af5811b8352d2dc3d86a7bcb4484ff5d56e"},
{file = "pyobjc_framework_Cocoa-8.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:118225562064d991bafb41d0913899d6b3d723984d1888cb7181e4dba63b22c2"},
{file = "pyobjc_framework_Cocoa-8.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d117a1eb24fd317e9f63792ac6a8703ed899de5d42e8a861c7bf885625668c31"},
{file = "pyobjc_framework_Cocoa-8.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3af8577acbd6b980d3b9270ec99bc0164f66ef8397351a72fcdee527f23c1a34"},
{file = "pyobjc_framework_Cocoa-8.4.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:91fdc964acb4dee4d37ae81fb603d48397739dbbfcc1eadbe0cdafaa8144b6e6"},
{file = "pyobjc_framework_Cocoa-8.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:197dd28668e786b55d7d4139afd85c285f780564ebbccc166e84a24be31de34f"},
{file = "pyobjc-framework-Cocoa-8.4.1.tar.gz", hash = "sha256:dc596bac0f5d424f67944e95b2d0d7c94a07c4166359d7b4a4d4ae4f8e112822"},
]
"pyobjc-framework-quartz 8.4.1" = [
{file = "pyobjc_framework_Quartz-8.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a82d9eab3b2a6fca46602465f731815026d06e4fd0ba65b5b5211f1a0472b861"},
{file = "pyobjc_framework_Quartz-8.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bd0a506385141b81330464b6e2537a7056cdca56deb98222b6926a04f72a3e6"},
{file = "pyobjc_framework_Quartz-8.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:64cc616386b7a387dce53d3adb8c12a8461c2720861ab36aeeb53768cd0ba7e4"},
{file = "pyobjc_framework_Quartz-8.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a2dff998c4d1286998390f58a3131b99bdc9dbf5c3d881562b33f168098bbe2e"},
{file = "pyobjc_framework_Quartz-8.4.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a98c58e1b265d5b413f5ecc6ec186b9e305fb6e37909d8b4b97fd681176f5f1c"},
{file = "pyobjc_framework_Quartz-8.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:39f55426ef883cbe12a53969f90886f71e2a94513c98cf3730efdf404cdc5c83"},
{file = "pyobjc-framework-Quartz-8.4.1.tar.gz", hash = "sha256:f8acebf0b526f0687e79ea6946e8f2528ced4ef02d0ed3fbf7116124b2749e52"},
]
"pyparsing 3.0.7" = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
"python-xlib 0.31" = [
{file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"},
{file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"},
]
"realcugan-ncnn-vulkan-python 1.0.0" = [
{file = "realcugan-ncnn-vulkan-python-1.0.0.tar.gz", hash = "sha256:f72764ffe0a0c53b13cdbe1ae328ff9b0eb5582b9d5d15a0ca0c0ac254d17410"},
]
"realsr-ncnn-vulkan-python 1.0.4" = [
{file = "realsr-ncnn-vulkan-python-1.0.4.tar.gz", hash = "sha256:bbfd9fc067cfea6ab28e8074f1d6aadcc7f927f166d90fab9dcc46380b7ef02d"},
]
"rich 12.0.0" = [
{file = "rich-12.0.0-py3-none-any.whl", hash = "sha256:fdcd2f8d416e152bcf35c659987038d1ae5a7bd336e821ca7551858a4c7e38a9"},
{file = "rich-12.0.0.tar.gz", hash = "sha256:14bfd0507edc633e021b02c45cbf7ca22e33b513817627b8de3412f047a3e798"},
]
"rife-ncnn-vulkan-python 1.1.2.post3" = [
{file = "rife-ncnn-vulkan-python-1.1.2.post3.tar.gz", hash = "sha256:df66d1f83bc6beacac33b0e384ee352b45fa39931b9e1ff8247b396d4d7a486d"},
]
"setuptools 60.10.0" = [
{file = "setuptools-60.10.0-py3-none-any.whl", hash = "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96"},
{file = "setuptools-60.10.0.tar.gz", hash = "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c"},
]
"setuptools-scm 6.4.2" = [
{file = "setuptools_scm-6.4.2-py3-none-any.whl", hash = "sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4"},
{file = "setuptools_scm-6.4.2.tar.gz", hash = "sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30"},
]
"six 1.16.0" = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
"smmap 5.0.0" = [
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
{file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
]
"srmd-ncnn-vulkan-python 1.0.2" = [
{file = "srmd-ncnn-vulkan-python-1.0.2.tar.gz", hash = "sha256:3c7f71cbba2b6310c4b14c2b11dae7737582ac004893f8c9cc3f7c305c7bbe49"},
]
"tomli 2.0.1" = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
"typing-extensions 4.1.1" = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]
"waifu2x-ncnn-vulkan-python 1.0.1" = [
{file = "waifu2x-ncnn-vulkan-python-1.0.1.tar.gz", hash = "sha256:18dbdd00141fc427a5ef8996f0a0197f1d6f3086a7c60bb88934e1d2a3004801"},
]
"win32-setctime 1.1.0" = [
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
]

View File

@@ -1,3 +1,59 @@
[project]
name = "video2x"
description = "A video/image upscaling and frame interpolation framework"
readme = "README.md"
requires-python = ">=3.7"
license-expression = "AGPL-3.0-or-later"
keywords = [
"super-resolution",
"upscaling",
"neural-network",
"machine-learning",
]
authors = [{ name = "K4YT3X", email = "i@k4yt3x.com" }]
classifiers = [
"Environment :: Console",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Multimedia :: Video",
"Topic :: Scientific/Engineering :: Image Processing",
]
dependencies = [
"ffmpeg-python>=0.2.0",
"loguru>=0.6.0",
"opencv-python==4.5.5.64",
"pillow>=9.1.0",
"pynput>=1.7.6",
"rich>=12.0.0",
"waifu2x-ncnn-vulkan-python>=1.0.2rc3",
"srmd-ncnn-vulkan-python>=1.0.2",
"realsr-ncnn-vulkan-python>=1.0.4",
"rife-ncnn-vulkan-python>=1.1.2.post3",
"realcugan-ncnn-vulkan-python>=1.0.2",
]
dynamic = ["version"]
[project.urls]
homepage = "https://github.com/k4yt3x/video2x/"
documentation = "https://github.com/k4yt3x/video2x/wiki"
repository = "https://github.com/k4yt3x/video2x.git"
changelog = "https://github.com/k4yt3x/video2x/releases"
[project.scripts]
video2x = "video2x:main"
[tool.isort]
profile = "black"
[tool.pdm]
version = { from = "video2x/__init__.py" }
[build-system]
requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"]
build-backend = "setuptools.build_meta"
requires = ["pdm-pep517>=0.12.0"]
build-backend = "pdm.pep517.api"

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,50 +0,0 @@
# Name: Video2X PyPI setup file
# Creator: K4YT3X
# Date Created: June 17, 2021
# Last Modified: February 11, 2022
# build & publish commands
# pip install --user -U setuptools wheel twine build
# python -m build .
# twine upload --repository pypi dist/*
[metadata]
name = video2x
version = 5.0.0-beta1
author = K4YT3X
author_email = i@k4yt3x.com
license = GNU Affero General Public License v3.0
description = A video/GIF/image upscaler and frame interpolator
url = https://github.com/k4yt3x/video2x
long_description = file: README.md
long_description_content_type = text/markdown
classifiers =
Environment :: Console
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Multimedia :: Video
Topic :: Scientific/Engineering :: Image Processing
[options]
packages = find:
install_requires =
ffmpeg-python
loguru
opencv-python
pillow
rich
tqdm
realsr-ncnn-vulkan-python
rife-ncnn-vulkan-python
srmd-ncnn-vulkan-python
waifu2x-ncnn-vulkan-python
python_requires = >=3.6
[options.entry_points]
console_scripts = video2x = video2x:main

BIN
tests/data/test_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

55
tests/test_upscaler.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import ctypes
import multiprocessing
import os
from pathlib import Path
import utils
from PIL import Image
from video2x import Upscaler, Video2X
def test_upscaling():
video2x = Video2X()
output_path = Path("data/test_video_output.mp4")
video2x.upscale(
Path("data/test_video.mp4"),
output_path,
None,
720,
3,
5,
0,
"waifu2x",
)
output_path.unlink()
def test_upscale_image():
# initialize upscaler instance
processing_queue = multiprocessing.Queue(maxsize=30)
processed_frames = multiprocessing.Manager().list([None])
pause = multiprocessing.Value(ctypes.c_bool, False)
upscaler = Upscaler(processing_queue, processed_frames, pause)
image = Image.open("data/test_image.png")
upscaled_image = upscaler.upscale_image(image, 1680, 960, "waifu2x", 3)
reference_image = Image.open("data/test_image_ref.png")
assert utils.get_image_diff(upscaled_image, reference_image) < 0.5
def test_get_scaling_tasks():
dimensions = [320, 240, 3840, 2160]
for algorithm, correct_answer in [
("waifu2x", [2, 2, 2, 2]),
["srmd", [3, 4]],
("realsr", [4, 4]),
("realcugan", [3, 4]),
]:
assert Upscaler._get_scaling_tasks(*dimensions, algorithm) == correct_answer

18
tests/utils.py Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from PIL import Image, ImageChops, ImageStat
def get_image_diff(image0: Image.Image, image1: Image.Image) -> float:
"""
calculate the percentage of differences between two images
:param image0 Image.Image: the first frame
:param image1 Image.Image: the second frame
:rtype float: the percent difference between the two images
"""
difference = ImageChops.difference(image0, image1)
difference_stat = ImageStat.Stat(difference)
percent_diff = sum(difference_stat.mean) / (len(difference_stat.mean) * 255) * 100
return percent_diff

View File

@@ -19,9 +19,16 @@ 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: August 28, 2022
"""
from .video2x import Video2X
from .upscaler import Upscaler
# version assignment has to precede imports to
# prevent setup.cfg from producing import errors
__version__ = "5.0.0-beta6"
# flake8: noqa
# let flake8 ignore this file to avoid F401 warnings
# generated by the following lines
from .interpolator import Interpolator
from .upscaler import Upscaler
from .video2x import Video2X

View File

@@ -19,11 +19,221 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Package Main
Author: K4YT3X
Date Created: July 3, 2021
Last Modified: February 11, 2022
Last Modified: February 26, 2022
"""
from .video2x import main
import argparse
import os
import pathlib
import sys
from loguru import logger
from rich import print as rich_print
from . import __version__
from .video2x import LOGURU_FORMAT, Video2X
LEGAL_INFO = f"""Video2X\t\t{__version__}
Author:\t\tK4YT3X
License:\tGNU AGPL v3
Github Page:\thttps://github.com/k4yt3x/video2x
Contact:\ti@k4yt3x.com"""
# algorithms available for upscaling tasks
UPSCALING_ALGORITHMS = [
"waifu2x",
"srmd",
"realsr",
"realcugan",
]
# algorithms available for frame interpolation tasks
INTERPOLATION_ALGORITHMS = ["rife"]
def parse_arguments() -> argparse.Namespace:
"""
parse command line arguments
:rtype argparse.Namespace: command parsing results
"""
parser = argparse.ArgumentParser(
prog="video2x",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--version", help="show version information and exit", action="store_true"
)
parser.add_argument(
"-i",
"--input",
type=pathlib.Path,
help="input file/directory path",
required=True,
)
parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
help="output file/directory path",
required=True,
)
parser.add_argument(
"-p", "--processes", type=int, help="number of processes to launch", default=1
)
parser.add_argument(
"-l",
"--loglevel",
choices=["trace", "debug", "info", "success", "warning", "error", "critical"],
default="info",
)
# upscaler arguments
action = parser.add_subparsers(
help="action to perform", dest="action", required=True
)
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"
)
upscale.add_argument("-w", "--width", type=int, help="output width")
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(
"-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 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",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
)
interpolate.add_argument(
"--help", action="help", help="show this help message and exit"
)
interpolate.add_argument(
"-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 percent difference between two adjacent frames exceeds this"
" value; set to 100 to interpolate all frames"
),
default=10,
)
return parser.parse_args()
def main() -> int:
"""
command line entrypoint for direct CLI invocation
:rtype int: 0 if completed successfully, else other int
"""
try:
# display version and lawful informaition
if "--version" in sys.argv:
rich_print(LEGAL_INFO)
return 0
# parse command line arguments
args = parse_arguments()
# check input/output file paths
if not args.input.exists():
logger.critical(f"Cannot find input file: {args.input}")
return 1
if not args.input.is_file():
logger.critical("Input path is not a file")
return 1
if not args.output.parent.exists():
logger.critical(f"Output directory does not exist: {args.output.parent}")
return 1
# set logger level
if os.environ.get("LOGURU_LEVEL") is None:
os.environ["LOGURU_LEVEL"] = args.loglevel.upper()
# remove default handler
logger.remove()
# 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 video2x object
video2x = Video2X()
if args.action == "upscale":
video2x.upscale(
args.input,
args.output,
args.width,
args.height,
args.noise,
args.processes,
args.threshold,
args.algorithm,
)
elif args.action == "interpolate":
video2x.interpolate(
args.input,
args.output,
args.processes,
args.threshold,
args.algorithm,
)
# don't print the traceback for manual terminations
except KeyboardInterrupt:
return 2
except Exception as error:
logger.exception(error)
return 1
# if no exceptions were produced
else:
logger.success("Processing completed successfully")
return 0
if __name__ == "__main__":
main()
sys.exit(main())

View File

@@ -19,21 +19,22 @@ 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: April 9, 2022
"""
# built-in imports
import contextlib
import os
import pathlib
import queue
import signal
import subprocess
import threading
from multiprocessing import Queue
from queue import Full
from threading import Thread
# third-party imports
from loguru import logger
from PIL import Image
import ffmpeg
from PIL import Image
from .pipe_printer import PipePrinter
# map Loguru log levels to FFmpeg log levels
LOGURU_FFMPEG_LOGLEVELS = {
@@ -47,36 +48,41 @@ LOGURU_FFMPEG_LOGLEVELS = {
}
class VideoDecoder(threading.Thread):
class VideoDecoder:
"""
A video decoder that generates frames read from FFmpeg.
:param input_path pathlib.Path: the input file's path
:param input_width int: the input file's width
:param input_height int: the input file's height
:param frame_rate float: the input file's frame rate
:param pil_ignore_max_image_pixels bool: setting this to True
disables PIL's "possible DDoS" warning
"""
def __init__(
self,
input_path: pathlib.Path,
input_width: int,
input_height: int,
frame_rate: float,
processing_queue: queue.Queue,
processing_settings: tuple,
ignore_max_image_pixels=True,
):
threading.Thread.__init__(self)
self.running = False
pil_ignore_max_image_pixels: bool = True,
) -> None:
self.input_path = input_path
self.input_width = input_width
self.input_height = input_height
self.processing_queue = processing_queue
self.processing_settings = processing_settings
# this disables the "possible DDoS" warning
if ignore_max_image_pixels:
if pil_ignore_max_image_pixels is True:
Image.MAX_IMAGE_PIXELS = None
self.exception = None
self.decoder = subprocess.Popen(
ffmpeg.compile(
ffmpeg.input(input_path, r=frame_rate)["v"]
.output("pipe:1", format="rawvideo", pix_fmt="rgb24", vsync="1")
.output("pipe:1", format="rawvideo", pix_fmt="rgb24")
.global_args("-hide_banner")
.global_args("-nostats")
.global_args("-nostdin")
.global_args(
"-loglevel",
LOGURU_FFMPEG_LOGLEVELS.get(
@@ -85,68 +91,87 @@ class VideoDecoder(threading.Thread):
),
overwrite_output=True,
),
env=dict(AV_LOG_FORCE_COLOR="TRUE", **os.environ),
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
# stderr=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
# start the PIPE printer to start printing FFmpeg logs
self.pipe_printer = PipePrinter(self.decoder.stderr)
self.pipe_printer.start()
def __iter__(self):
# continue yielding while FFmpeg continues to produce output
# it is possible to use := for this block to be more concise
# but it is purposefully avoided to remain compatible with Python 3.7
buffer = self.decoder.stdout.read(3 * self.input_width * self.input_height)
while len(buffer) > 0:
# convert raw bytes into image object
frame = Image.frombytes(
"RGB", (self.input_width, self.input_height), buffer
)
# return this frame
yield frame
# read the next frame
buffer = self.decoder.stdout.read(3 * self.input_width * self.input_height)
# automatically self-join and clean up after iterations are done
self.join()
def kill(self):
self.decoder.send_signal(signal.SIGKILL)
def join(self):
# close PIPEs to prevent process from getting stuck
self.decoder.stdout.close()
self.decoder.stderr.close()
# wait for process to exit
self.decoder.wait()
# wait for PIPE printer to exit
self.pipe_printer.stop()
self.pipe_printer.join()
class VideoDecoderThread(Thread):
def __init__(
self, tasks_queue: Queue, decoder: VideoDecoder, processing_settings: tuple
):
super().__init__()
self.tasks_queue = tasks_queue
self.decoder = decoder
self.processing_settings = processing_settings
self.running = False
def run(self):
self.running = True
previous_frame = None
for frame_index, frame in enumerate(self.decoder):
# the index of the frame
frame_index = 0
while True:
# create placeholder for previous frame
# used in interpolate mode
previous_image = None
# check for the stop signal
if self.running is False:
self.decoder.join()
return
# continue running until an exception occurs
# or all frames have been decoded
while self.running:
try:
buffer = self.decoder.stdout.read(
3 * self.input_width * self.input_height
)
# source depleted (decoding finished)
# after the last frame has been decoded
# read will return nothing
if len(buffer) == 0:
logger.debug("Decoding queue depleted")
with contextlib.suppress(Full):
self.tasks_queue.put(
(frame_index, previous_frame, frame, self.processing_settings),
timeout=0.1,
)
break
# convert raw bytes into image object
image = Image.frombytes(
"RGB", (self.input_width, self.input_height), buffer
)
self.processing_queue.put(
(
frame_index,
(previous_image, image),
self.processing_settings,
)
)
previous_image = image
frame_index += 1
# most likely "not enough image data"
except ValueError as e:
logger.exception(e)
break
# send exceptions into the client connection pipe
except Exception as e:
self.exception = e
logger.exception(e)
break
# ensure the decoder has exited
self.decoder.wait()
logger.debug("Decoder thread exiting")
self.running = False
return super().run()
previous_frame = frame
def stop(self):
self.running = False

View File

@@ -19,24 +19,18 @@ 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: August 28, 2022
"""
# built-in imports
import multiprocessing
import multiprocessing.managers
import multiprocessing.sharedctypes
import os
import pathlib
import signal
import subprocess
import threading
import time
# third-party imports
from loguru import logger
import ffmpeg
from PIL import Image
from .pipe_printer import PipePrinter
# map Loguru log levels to FFmpeg log levels
LOGURU_FFMPEG_LOGLEVELS = {
@@ -50,7 +44,7 @@ LOGURU_FFMPEG_LOGLEVELS = {
}
class VideoEncoder(threading.Thread):
class VideoEncoder:
def __init__(
self,
input_path: pathlib.Path,
@@ -58,55 +52,51 @@ class VideoEncoder(threading.Thread):
output_path: pathlib.Path,
output_width: int,
output_height: int,
total_frames: int,
processed_frames: multiprocessing.managers.ListProxy,
processed: multiprocessing.sharedctypes.Synchronized,
):
threading.Thread.__init__(self)
self.running = False
self.input_path = input_path
self.output_path = output_path
self.total_frames = total_frames
self.processed_frames = processed_frames
self.processed = processed
copy_audio: bool = True,
copy_subtitle: bool = True,
copy_data: bool = False,
copy_attachments: bool = False,
) -> None:
self.original = ffmpeg.input(input_path)
# create FFmpeg input for the original input video
original = ffmpeg.input(input_path)
# define frames as input
frames = ffmpeg.input(
"pipe:0",
format="rawvideo",
pix_fmt="rgb24",
vsync="1",
s=f"{output_width}x{output_height}",
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?"],
# original["1:v?"],
original["a?"] if copy_audio is True else None,
original["s?"] if copy_subtitle is True else None,
original["d?"] if copy_data is True else None,
original["t?"] if copy_attachments is True else None,
]
"""
# run FFmpeg and produce final output
self.encoder = subprocess.Popen(
ffmpeg.compile(
ffmpeg.output(
frames,
str(self.output_path),
pix_fmt="yuv420p",
*[s for s in additional_streams if s is not None],
str(output_path),
vcodec="libx264",
acodec="copy",
r=frame_rate,
scodec="copy",
pix_fmt="yuv420p",
crf=17,
vsync="1",
# map_metadata=1,
# metadata="comment=Upscaled with Video2X",
preset="veryslow",
# acodec="libfdk_aac",
# cutoff=20000,
r=frame_rate,
map_metadata=1,
metadata="comment=Processed with Video2X",
)
.global_args("-hide_banner")
.global_args("-nostats")
@@ -118,48 +108,42 @@ class VideoEncoder(threading.Thread):
),
overwrite_output=True,
),
env=dict(AV_LOG_FORCE_COLOR="TRUE", **os.environ),
stdin=subprocess.PIPE,
# stdout=subprocess.DEVNULL,
# stderr=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
def run(self):
self.running = True
frame_index = 0
while self.running and frame_index < self.total_frames:
try:
image = self.processed_frames[frame_index]
if image is None:
time.sleep(0.1)
continue
# start the PIPE printer to start printing FFmpeg logs
self.pipe_printer = PipePrinter(self.encoder.stderr)
self.pipe_printer.start()
self.encoder.stdin.write(image.tobytes())
def kill(self):
self.encoder.send_signal(signal.SIGKILL)
with self.processed.get_lock():
self.processed.value += 1
def write(self, frame: Image.Image) -> None:
"""
write a frame into FFmpeg encoder's STDIN
frame_index += 1
:param frame Image.Image: the Image object to use for writing
"""
self.encoder.stdin.write(frame.tobytes())
# send exceptions into the client connection pipe
except Exception as e:
logger.exception(e)
break
# flush the remaining data in STDIN and close PIPE
logger.debug("Encoding queue depleted")
def join(self) -> None:
"""
signal the encoder that all frames have been sent and the FFmpeg
should be instructed to wrap-up the processing
"""
# flush the remaining data in STDIN and STDERR
self.encoder.stdin.flush()
self.encoder.stderr.flush()
# close PIPEs to prevent process from getting stuck
self.encoder.stdin.close()
self.encoder.stderr.close()
# send SIGINT (2) to FFmpeg
# this instructs it to finalize and exit
self.encoder.send_signal(signal.SIGINT)
# wait for process to terminate
# wait for process to exit
self.encoder.wait()
logger.debug("Encoder thread exiting")
self.running = False
return super().run()
def stop(self):
self.running = False
# wait for PIPE printer to exit
self.pipe_printer.stop()
self.pipe_printer.join()

View File

@@ -19,53 +19,58 @@ 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: March 20, 2022
"""
# local imports
from rife_ncnn_vulkan_python.rife_ncnn_vulkan import Rife
# built-in imports
import multiprocessing
import multiprocessing.managers
import multiprocessing.sharedctypes
import queue
import signal
import time
from multiprocessing.managers import ListProxy
from multiprocessing.sharedctypes import Synchronized
# third-party imports
from PIL import ImageChops, ImageStat
from loguru import logger
from PIL import ImageChops, ImageStat
from rife_ncnn_vulkan_python.rife_ncnn_vulkan import Rife
DRIVER_CLASSES = {"rife": Rife}
ALGORITHM_CLASSES = {"rife": Rife}
class Interpolator(multiprocessing.Process):
def __init__(
self,
processing_queue: multiprocessing.Queue,
processed_frames: multiprocessing.managers.ListProxy,
):
processed_frames: ListProxy,
pause: Synchronized,
) -> None:
multiprocessing.Process.__init__(self)
self.running = False
self.processing_queue = processing_queue
self.processed_frames = processed_frames
self.pause = pause
self.running = False
self.processor_objects = {}
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 = {}
while self.running:
logger.opt(colors=True).info(
f"Interpolator process <blue>{self.name}</blue> initiating"
)
while self.running is True:
try:
# pause if pause flag is set
if self.pause.value is True:
time.sleep(0.1)
continue
try:
# get new job from queue
(
frame_index,
(image0, image1),
(difference_threshold, driver),
(difference_threshold, algorithm),
) = self.processing_queue.get(False)
except queue.Empty:
time.sleep(0.1)
@@ -76,6 +81,7 @@ class Interpolator(multiprocessing.Process):
if image0 is None:
continue
# calculate the %diff between the current frame and the previous frame
difference = ImageChops.difference(image0, image1)
difference_stat = ImageStat.Stat(difference)
difference_ratio = (
@@ -86,13 +92,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 = self.processor_objects.get(algorithm)
if processor_object is None:
processor_object = ALGORITHM_CLASSES[algorithm](0)
self.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
@@ -108,13 +114,14 @@ class Interpolator(multiprocessing.Process):
except (SystemExit, KeyboardInterrupt):
break
except Exception as e:
logger.exception(e)
except Exception as error:
logger.exception(error)
break
logger.info(f"Interpolator process {self.name} terminating")
self.running = False
logger.opt(colors=True).info(
f"Interpolator process <blue>{self.name}</blue> terminating"
)
return super().run()
def _stop(self, _signal_number, _frame):
def _stop(self, _signal_number, _frame) -> None:
self.running = False

63
video2x/pipe_printer.py Executable file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright (C) 2018-2022 K4YT3X and contributors.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: PIPE Printer
Author: K4YT3X
Date Created: February 27, 2022
Last Modified: February 28, 2022
"""
import os
import sys
import threading
import time
from typing import IO
class PipePrinter(threading.Thread):
def __init__(self, stderr: IO[bytes]) -> None:
threading.Thread.__init__(self)
self.stderr = stderr
self.running = False
# set read mode to non-blocking
os.set_blocking(self.stderr.fileno(), False)
def _print_output(self) -> None:
output = self.stderr.read()
if output is not None and len(output) != 0:
print(output.decode(), file=sys.stderr)
def run(self) -> None:
self.running = True
# keep printing contents in the PIPE
while self.running is True:
time.sleep(0.5)
try:
self._print_output()
# pipe closed
except ValueError:
break
return super().run()
def stop(self) -> None:
self.running = False

69
video2x/processor.py Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright (C) 2018-2022 K4YT3X and contributors.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Name: Processor Abstract Class
Author: K4YT3X
Date Created: April 9, 2022
Last Modified: April 9, 2022
"""
from abc import ABC, abstractmethod
from multiprocessing import Queue
from multiprocessing.managers import DictProxy
from multiprocessing.sharedctypes import Synchronized
from PIL import Image, ImageChops, ImageStat
class Processor(ABC):
def __init__(
self, tasks_queue: Queue, processed_frames: DictProxy, pause_flag: Synchronized
) -> None:
self.tasks_queue = tasks_queue
self.processed_frames = processed_frames
self.pause_flag = pause_flag
@abstractmethod
def process(self):
raise NotImplementedError
@staticmethod
def get_image_diff(image0: Image.Image, image1: Image.Image) -> float:
"""
get the percentage difference between two images
:param image0 Image.Image: the image to compare
:param image1 Image.Image: the image to compare against
:rtype float: precentage difference between two frames
"""
difference_stat = ImageStat.Stat(ImageChops.difference(image0, image1))
return sum(difference_stat.mean) / (len(difference_stat.mean) * 255) * 100
"""
def run(
self,
) -> None:
self.running = True
while self.running is True:
self.process()
self.running = False
return super().run()
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,175 +19,184 @@ 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: April 10, 2022
"""
# local imports
from realsr_ncnn_vulkan_python.realsr_ncnn_vulkan import Realsr
from srmd_ncnn_vulkan_python.srmd_ncnn_vulkan import Srmd
from waifu2x_ncnn_vulkan_python.waifu2x_ncnn_vulkan import Waifu2x
# built-in imports
import math
import multiprocessing
import multiprocessing.managers
import multiprocessing.sharedctypes
import queue
import signal
import time
# third-party imports
from PIL import Image, ImageChops, ImageStat
from loguru import logger
from PIL import Image
from realcugan_ncnn_vulkan_python import Realcugan
from realsr_ncnn_vulkan_python import Realsr
from srmd_ncnn_vulkan_python import Srmd
from waifu2x_ncnn_vulkan_python import Waifu2x
# 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],
}
DRIVER_CLASSES = {"waifu2x": Waifu2x, "srmd": Srmd, "realsr": Realsr}
from .processor import Processor
class Upscaler(multiprocessing.Process):
def __init__(
class Upscaler:
# fixed scaling ratios supported by the algorithms
# that only support certain fixed scale ratios
ALGORITHM_FIXED_SCALING_RATIOS = {
"waifu2x": [1, 2],
"srmd": [2, 3, 4],
"realsr": [4],
"realcugan": [1, 2, 3, 4],
}
ALGORITHM_CLASSES = {
"waifu2x": Waifu2x,
"srmd": Srmd,
"realsr": Realsr,
"realcugan": Realcugan,
}
processor_objects = {}
@staticmethod
def _get_scaling_tasks(
input_width: int,
input_height: int,
output_width: int,
output_height: int,
algorithm: str,
) -> list:
"""
Get the required tasks for upscaling the image until it is larger than
or equal to the desired output dimensions. For example, SRMD only supports
2x, 3x, and 4x, so upsclaing an image from 320x240 to 3840x2160 will
require the SRMD to run 3x then 4x. In this case, this function will
return [3, 4].
:param input_width int: input image width
:param input_height int: input image height
:param output_width int: desired output image width
:param output_height int: desired output image size
:param algorithm str: upsclaing algorithm
:rtype list: the list of upsclaing tasks required
"""
# calculate required minimum scale ratio
output_scale = max(output_width / input_width, output_height / input_height)
# select the optimal algorithm scaling ratio to use
supported_scaling_ratios = sorted(
Upscaler.ALGORITHM_FIXED_SCALING_RATIOS[algorithm]
)
remaining_scaling_ratio = math.ceil(output_scale)
# if the scaling ratio is 1.0
# apply the smallest scaling ratio available
if remaining_scaling_ratio == 1:
return [supported_scaling_ratios[0]]
scaling_jobs = []
while remaining_scaling_ratio > 1:
for ratio in supported_scaling_ratios:
if ratio >= remaining_scaling_ratio:
scaling_jobs.append(ratio)
remaining_scaling_ratio /= ratio
break
else:
found = False
for i in supported_scaling_ratios:
for j in supported_scaling_ratios:
if i * j >= remaining_scaling_ratio:
scaling_jobs.extend([i, j])
remaining_scaling_ratio /= i * j
found = True
break
if found is True:
break
if found is False:
scaling_jobs.append(supported_scaling_ratios[-1])
remaining_scaling_ratio /= supported_scaling_ratios[-1]
return scaling_jobs
def upscale_image(
self,
processing_queue: multiprocessing.Queue,
processed_frames: multiprocessing.managers.ListProxy,
):
multiprocessing.Process.__init__(self)
self.running = False
self.processing_queue = processing_queue
self.processed_frames = processed_frames
image: Image.Image,
output_width: int,
output_height: int,
algorithm: str,
noise: int,
) -> Image.Image:
"""
upscale an image
signal.signal(signal.SIGTERM, self._stop)
:param image Image.Image: the image to upscale
:param output_width int: the desired output width
:param output_height int: the desired output height
:param algorithm str: the algorithm to use
:param noise int: the noise level (available only for some algorithms)
:rtype Image.Image: the upscaled image
"""
width, height = image.size
for task in self._get_scaling_tasks(
width, height, output_width, output_height, algorithm
):
# select a processor object with the required settings
# create a new object if none are available
processor_object = self.processor_objects.get((algorithm, task))
if processor_object is None:
processor_object = self.ALGORITHM_CLASSES[algorithm](
noise=noise, scale=task
)
self.processor_objects[(algorithm, task)] = processor_object
# process the image with the selected algorithm
image = processor_object.process(image)
# downscale the image to the desired output size and
# save the image to disk
return image.resize((output_width, output_height), Image.Resampling.LANCZOS)
class UpscalerProcessor(Processor, Upscaler):
def process(self) -> None:
task = self.tasks_queue.get()
while task is not None:
def run(self):
self.running = True
logger.info(f"Upscaler process {self.name} initiating")
driver_objects = {}
while self.running:
try:
try:
# get new job from queue
(
frame_index,
(image0, image1),
(
output_width,
output_height,
noise,
difference_threshold,
driver,
),
) = self.processing_queue.get(False)
# destructure settings
except queue.Empty:
if self.pause_flag.value is True:
time.sleep(0.1)
continue
# unpack the task's values
(
frame_index,
previous_frame,
current_frame,
(output_width, output_height, algorithm, noise, threshold),
) = task
# calculate the %diff between the current frame and the previous frame
difference_ratio = 0
if image0 is not None:
difference = ImageChops.difference(image0, image1)
difference_stat = ImageStat.Stat(difference)
difference_ratio = (
sum(difference_stat.mean)
/ (len(difference_stat.mean) * 255)
* 100
if previous_frame is not None:
difference_ratio = self.get_image_diff(
previous_frame, current_frame
)
# if the difference is lower than threshold
# skip this frame
if difference_ratio < difference_threshold:
# make sure the previous frame has been processed
if frame_index > 0:
while self.processed_frames[frame_index - 1] is None:
time.sleep(0.1)
# if the difference is lower than threshold, skip this frame
if difference_ratio < threshold:
# make the current image the same as the previous result
self.processed_frames[frame_index] = self.processed_frames[
frame_index - 1
]
self.processed_frames[frame_index] = True
# if the difference is greater than threshold
# process this frame
else:
width, height = image1.size
# calculate required minimum scale ratio
output_scale = max(output_width / width, output_height / height)
# select the optimal driver scaling ratio to use
supported_scaling_ratios = sorted(
DRIVER_FIXED_SCALING_RATIOS[driver]
self.processed_frames[frame_index] = self.upscale_image(
current_frame, output_width, output_height, algorithm, noise
)
remaining_scaling_ratio = math.ceil(output_scale)
scaling_jobs = []
task = self.tasks_queue.get()
# if the scaling ratio is 1.0
# apply the smallest scaling ratio available
if remaining_scaling_ratio == 1:
scaling_jobs.append(supported_scaling_ratios[0])
else:
while remaining_scaling_ratio > 1:
for ratio in supported_scaling_ratios:
if ratio >= remaining_scaling_ratio:
scaling_jobs.append(ratio)
remaining_scaling_ratio /= ratio
break
else:
found = False
for i in supported_scaling_ratios:
for j in supported_scaling_ratios:
if i * j >= remaining_scaling_ratio:
scaling_jobs.extend([i, j])
remaining_scaling_ratio /= i * j
found = True
break
if found is True:
break
if found is False:
scaling_jobs.append(supported_scaling_ratios[-1])
remaining_scaling_ratio /= supported_scaling_ratios[
-1
]
for job in scaling_jobs:
# select a driver 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](
scale=job, noise=noise
)
driver_objects[(driver, job)] = driver_object
# process the image with the selected driver
image1 = driver_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)
self.processed_frames[frame_index] = image1
# send exceptions into the client connection pipe
except (SystemExit, KeyboardInterrupt):
except KeyboardInterrupt:
break
except Exception as e:
logger.exception(e)
break
logger.info(f"Upscaler process {self.name} terminating")
self.running = False
return super().run()
def _stop(self, _signal_number, _frame):
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: August 28, 2022
Editor: BrianPetkovsek
Last Modified: June 17, 2019
@@ -39,53 +39,69 @@ Editor: 28598519a
Last Modified: March 23, 2020
"""
# local imports
from .decoder import VideoDecoder
from .encoder import VideoEncoder
from .interpolator import Interpolator
from .upscaler import Upscaler
# built-in imports
import argparse
import ctypes
import math
import multiprocessing
import os
import pathlib
import signal
import sys
import time
from enum import Enum
from multiprocessing import Manager, Pool, Queue, Value
from pathlib import Path
# third-party imports
from loguru import logger
from rich import print
from tqdm import tqdm
import cv2
import ffmpeg
from cv2 import cv2
from loguru import logger
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
VERSION = "5.0.0"
from video2x.processor import Processor
LEGAL_INFO = """Video2X {}
Author: K4YT3X
License: GNU GPL v3
Github Page: https://github.com/k4yt3x/video2x
Contact: k4yt3x@k4yt3x.com""".format(
VERSION
from . import __version__
from .decoder import VideoDecoder, VideoDecoderThread
from .encoder import VideoEncoder
from .interpolator import Interpolator
from .upscaler import UpscalerProcessor
# for desktop environments only
# if pynput can be loaded, enable global pause hotkey support
try:
from pynput.keyboard import HotKey, Listener
except ImportError:
ENABLE_HOTKEY = False
else:
ENABLE_HOTKEY = True
# format string for Loguru loggers
LOGURU_FORMAT = (
"<green>{time:HH:mm:ss.SSSSSS!UTC}</green> | "
"<level>{level: <8}</level> | "
"<level>{message}</level>"
)
UPSCALING_DRIVERS = [
"waifu2x",
"srmd",
"realsr",
]
INTERPOLATION_DRIVERS = ["rife"]
class ProcessingSpeedColumn(ProgressColumn):
"""Custom progress bar column that displays the processing speed"""
# 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],
}
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 ProcessingMode(Enum):
UPSCALE = {"label": "Upscaling", "processor": UpscalerProcessor}
INTERPOLATE = {"label": "Interpolating", "processor": Interpolator}
class Video2X:
@@ -97,14 +113,15 @@ 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):
@staticmethod
def _get_video_info(path: Path) -> tuple:
"""
get video file information with FFmpeg
:param path pathlib.Path: video file path
:param path Path: video file path
:raises RuntimeError: raised when video stream isn't found
"""
# probe video file info
@@ -130,116 +147,227 @@ class Video2X:
def _run(
self,
input_path: pathlib.Path,
input_path: Path,
width: int,
height: int,
total_frames: int,
frame_rate: float,
output_path: pathlib.Path,
output_path: Path,
output_width: int,
output_height: int,
Processor: object,
mode: str,
mode: ProcessingMode,
processes: int,
processing_settings: tuple,
):
# initialize values
self.processor_processes = []
self.processing_queue = multiprocessing.Queue()
processed_frames = multiprocessing.Manager().list([None] * total_frames)
self.processed = multiprocessing.Value("I", 0)
) -> 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)
# TODO: add docs
tasks_queue = Queue(maxsize=processes * 10)
processed_frames = Manager().dict()
pause_flag = Value(ctypes.c_bool, False)
# set up and start decoder thread
logger.info("Starting video decoder")
self.decoder = VideoDecoder(
decoder = VideoDecoder(
input_path,
width,
height,
frame_rate,
self.processing_queue,
processing_settings,
)
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}")
decoder_thread = VideoDecoderThread(tasks_queue, decoder, processing_settings)
decoder_thread.start()
# set up and start encoder thread
logger.info("Starting video encoder")
self.encoder = VideoEncoder(
encoder = VideoEncoder(
input_path,
frame_rate,
frame_rate * 2 if mode == "interpolate" else frame_rate,
output_path,
output_width,
output_height,
total_frames,
processed_frames,
self.processed,
)
self.encoder.start()
# create upscaler processes
for process_name in range(processes):
process = Processor(self.processing_queue, processed_frames)
process.name = str(process_name)
process.daemon = True
process.start()
self.processor_processes.append(process)
# create a pool of processor processes to process the queue
processor: Processor = mode.value["processor"](
tasks_queue, processed_frames, pause_flag
)
processor_pool = Pool(processes, processor.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)
progress = Progress(
"[progress.description]{task.description}",
BarColumn(complete_style="blue", finished_style="green"),
"[progress.percentage]{task.percentage:>3.0f}%",
"[color(240)]({task.completed}/{task.total})",
ProcessingSpeedColumn(),
TimeElapsedColumn(),
"<",
TimeRemainingColumn(),
console=console,
speed_estimate_period=300.0,
disable=True,
)
task = progress.add_task(f"[cyan]{mode.value['label']}", total=total_frames)
logger.info("Encoding has completed")
progress.n = self.processed.value
progress.refresh()
def _toggle_pause(_signal_number: int = -1, _frame=None):
# allow the closure to modify external immutable flag
nonlocal pause_flag
# print console messages and update the progress bar's status
if pause_flag.value is False:
progress.update(
task, description=f"[cyan]{mode.value['label']} (paused)"
)
progress.stop_task(task)
logger.warning("Processing paused, press Ctrl+Alt+V again to resume")
# the lock is already acquired
elif pause_flag.value is True:
progress.update(task, description=f"[cyan]{mode.value['label']}")
logger.warning("Resuming processing")
progress.start_task(task)
# invert the flag
with pause_flag.get_lock():
pause_flag.value = not pause_flag.value
# allow sending SIGUSR1 to pause/resume processing
signal.signal(signal.SIGUSR1, _toggle_pause)
# enable global pause hotkey if it's supported
if ENABLE_HOTKEY is True:
# create global pause hotkey
pause_hotkey = HotKey(HotKey.parse("<ctrl>+<alt>+v"), _toggle_pause)
# create global keyboard input listener
keyboard_listener = Listener(
on_press=(
lambda key: pause_hotkey.press(keyboard_listener.canonical(key))
),
on_release=(
lambda key: pause_hotkey.release(keyboard_listener.canonical(key))
),
)
# start monitoring global key presses
keyboard_listener.start()
# a temporary variable that stores the exception
exceptions = []
try:
# let the context manager automatically stop the progress bar
with progress:
frame_index = 0
while frame_index < total_frames:
current_frame = processed_frames.get(frame_index)
if pause_flag.value is True or current_frame is None:
time.sleep(0.1)
continue
# show the progress bar after the processing starts
# reduces speed estimation inaccuracies and print overlaps
if frame_index == 0:
progress.disable = False
progress.start()
if current_frame is True:
encoder.write(processed_frames.get(frame_index - 1))
else:
encoder.write(current_frame)
if frame_index > 0:
del processed_frames[frame_index - 1]
progress.update(task, completed=frame_index + 1)
frame_index += 1
# if SIGTERM is received or ^C is pressed
# TODO: pause and continue here
except (SystemExit, KeyboardInterrupt):
logger.warning("Exit signal received, terminating")
except (SystemExit, KeyboardInterrupt) as error:
logger.warning("Exit signal received, exiting gracefully")
logger.warning("Press ^C again to force terminate")
exceptions.append(error)
except Exception as error:
logger.exception(error)
exceptions.append(error)
else:
logger.info("Processing has completed")
finally:
# mark processing queue as closed
self.processing_queue.close()
# stop upscaler processes
logger.info("Stopping upscaler processes")
for process in self.processor_processes:
process.terminate()
# stop keyboard listener
if ENABLE_HOTKEY is True:
keyboard_listener.stop()
keyboard_listener.join()
# wait for processes to finish
for process in self.processor_processes:
process.join()
# if errors have occurred, kill the FFmpeg processes
if len(exceptions) > 0:
decoder.kill()
encoder.kill()
# ensure both the decoder and the encoder have exited
self.decoder.stop()
self.encoder.stop()
self.decoder.join()
self.encoder.join()
# stop the decoder
decoder_thread.stop()
decoder_thread.join()
# clear queue and signal processors to exit
# multiprocessing.Queue has no Queue.queue.clear
while tasks_queue.empty() is not True:
tasks_queue.get()
for _ in range(processes):
tasks_queue.put(None)
# close and join the process pool
processor_pool.close()
processor_pool.join()
# stop the encoder
encoder.join()
# 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)
# raise the first collected error
if len(exceptions) > 0:
raise exceptions[0]
def upscale(
self,
input_path: pathlib.Path,
output_path: pathlib.Path,
input_path: Path,
output_path: Path,
output_width: int,
output_height: int,
noise: int,
processes: int,
threshold: float,
driver: str,
algorithm: str,
) -> None:
# get basic video information
@@ -266,25 +394,24 @@ class Video2X:
output_path,
output_width,
output_height,
Upscaler,
"upscale",
ProcessingMode.UPSCALE,
processes,
(
output_width,
output_height,
algorithm,
noise,
threshold,
driver,
),
)
def interpolate(
self,
input_path: pathlib.Path,
output_path: pathlib.Path,
input_path: Path,
output_path: Path,
processes: int,
threshold: float,
driver: str,
algorithm: str,
) -> None:
# get video basic information
@@ -303,150 +430,7 @@ class Video2X:
output_path,
width,
height,
Interpolator,
"interpolate",
ProcessingMode.INTERPOLATE,
processes,
(threshold, driver),
)
def parse_arguments() -> argparse.Namespace:
"""
parse command line arguments
:rtype argparse.Namespace: command parsing results
"""
parser = argparse.ArgumentParser(
prog="video2x",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-v", "--version", help="show version information and exit", action="store_true"
)
parser.add_argument(
"-i",
"--input",
type=pathlib.Path,
help="input file/directory path",
required=True,
)
parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
help="output file/directory path",
required=True,
)
parser.add_argument(
"-p", "--processes", type=int, help="number of processes to launch", default=1
)
parser.add_argument(
"-l",
"--loglevel",
choices=["trace", "debug", "info", "success", "warning", "error", "critical"],
default="info",
)
# upscaler arguments
action = parser.add_subparsers(
help="action to perform", dest="action", required=True
)
upscale = action.add_parser("upscale", help="upscale a file", add_help=False)
upscale.add_argument(
"--help", action="help", help="show this help message and exit"
)
upscale.add_argument("-w", "--width", type=int, help="output width")
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],
)
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",
default=0,
)
# interpolator arguments
interpolate = action.add_parser(
"interpolate", help="interpolate frames for file", 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],
)
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",
default=10,
)
return parser.parse_args()
def main():
"""
command line direct invocation
program entry point
"""
try:
# parse command line arguments
args = parse_arguments()
# set logger level
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>"
# display version and lawful informaition
if args.version:
print(LEGAL_INFO)
sys.exit(0)
# initialize upscaler object
video2x = Video2X()
if args.action == "upscale":
video2x.upscale(
args.input,
args.output,
args.width,
args.height,
args.noise,
args.processes,
args.threshold,
args.driver,
)
elif args.action == "interpolate":
video2x.interpolate(
args.input,
args.output,
args.processes,
args.threshold,
args.driver,
)
except Exception as e:
logger.exception(e)
(threshold, algorithm),
)