vs-undistort 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vs_undistort-2.2.0/.github/workflows/publish.yml +76 -0
- vs_undistort-2.2.0/PKG-INFO +92 -0
- vs_undistort-2.2.0/README.md +74 -0
- vs_undistort-2.2.0/pyproject.toml +40 -0
- vs_undistort-2.2.0/vs_undistort/__init__.py +9 -0
- vs_undistort-2.2.0/vs_undistort/models/UNet3d_TMT.pth +0 -0
- vs_undistort-2.2.0/vs_undistort/models/UNet3d_TMT_arch.py +319 -0
- vs_undistort-2.2.0/vs_undistort/models/UNet3d_TMT_cub_op19_fp16.onnx +0 -0
- vs_undistort-2.2.0/vs_undistort/models/UNet3d_TMT_lin_op19_fp16.onnx +0 -0
- vs_undistort-2.2.0/vs_undistort/utils.py +146 -0
- vs_undistort-2.2.0/vs_undistort/vs_undistort.py +479 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build distributions
|
|
13
|
+
if: ${{ !github.event.release.prerelease }}
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Check out released tag
|
|
18
|
+
uses: actions/checkout@v6
|
|
19
|
+
with:
|
|
20
|
+
ref: ${{ github.event.release.tag_name }}
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
|
|
28
|
+
- name: Check tag matches package version
|
|
29
|
+
shell: bash
|
|
30
|
+
run: |
|
|
31
|
+
VERSION="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
|
32
|
+
TAG="${{ github.event.release.tag_name }}"
|
|
33
|
+
TAG_VERSION="${TAG#v}"
|
|
34
|
+
|
|
35
|
+
if [ "$TAG_VERSION" != "$VERSION" ]; then
|
|
36
|
+
echo "Release tag $TAG does not match pyproject.toml version $VERSION"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Install build tools
|
|
41
|
+
run: python -m pip install --upgrade build twine
|
|
42
|
+
|
|
43
|
+
- name: Build wheel and source distribution
|
|
44
|
+
run: python -m build
|
|
45
|
+
|
|
46
|
+
- name: Validate distributions
|
|
47
|
+
run: python -m twine check dist/*
|
|
48
|
+
|
|
49
|
+
- name: Upload distributions
|
|
50
|
+
uses: actions/upload-artifact@v5
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
if-no-files-found: error
|
|
55
|
+
|
|
56
|
+
publish:
|
|
57
|
+
name: Publish distributions to PyPI
|
|
58
|
+
needs: build
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
|
|
61
|
+
environment:
|
|
62
|
+
name: pypi
|
|
63
|
+
url: https://pypi.org/p/vs_undistort
|
|
64
|
+
|
|
65
|
+
permissions:
|
|
66
|
+
id-token: write
|
|
67
|
+
|
|
68
|
+
steps:
|
|
69
|
+
- name: Download distributions
|
|
70
|
+
uses: actions/download-artifact@v6
|
|
71
|
+
with:
|
|
72
|
+
name: python-package-distributions
|
|
73
|
+
path: dist/
|
|
74
|
+
|
|
75
|
+
- name: Publish to PyPI
|
|
76
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vs_undistort
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Vapoursynth function to remove video distortions, turbulence, wobble, warp, heat haze, or similar.
|
|
5
|
+
Project-URL: Homepage, https://github.com/pifroggi/vs_undistort
|
|
6
|
+
Project-URL: Repository, https://github.com/pifroggi/vs_undistort
|
|
7
|
+
Project-URL: Issues, https://github.com/pifroggi/vs_undistort/issues
|
|
8
|
+
Author: pifroggi
|
|
9
|
+
Keywords: distortion,filtering,heat haze,rectification,restoration,turbulence,vapoursynth,video,wobble
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia :: Video
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: tensorrt-cu13>=11.1.0.106
|
|
15
|
+
Requires-Dist: vapoursynth-mlrt-trt>=16.0
|
|
16
|
+
Requires-Dist: vapoursynth>=74
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Video Distortion Removal for VapourSynth
|
|
20
|
+
Also known as dewobble, warp stabilization, film or VHS distortion fix, rectification, atmospheric turbulence mitigation, or heat haze removal.
|
|
21
|
+
|
|
22
|
+
This is a partial implementation of the [Turbulence Mitigation Transformer](https://github.com/xg416/TMT) (only distortion removal, no deblurring). It does not do general video stabilization for shaky footage, only removes distortions within the frames. It is recommented to stabilize first if needed.
|
|
23
|
+
|
|
24
|
+
Check out stinkybread's comparisons [here](https://kaizoku.pw/c/tezc1iieeHympQGQeWKl) and [here](https://kaizoku.pw/c/4HsCLWDxuFMNpOIdSNI8).
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://github.com/xg416/TMT/blob/main/figs/video_22.gif"/>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<br />
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install -U vs_undistort
|
|
36
|
+
```
|
|
37
|
+
* To enable the CPU/CUDA backends, install [PyTorch with CUDA](https://pytorch.org/). *(optional)*
|
|
38
|
+
<br />
|
|
39
|
+
|
|
40
|
+
For older vapoursynth versions below R74, follow the manual installation steps [here](https://github.com/pifroggi/vs_temporalfix/wiki/Manual-Installation).
|
|
41
|
+
|
|
42
|
+
<br />
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from vs_undistort import vs_undistort
|
|
48
|
+
clip = vs_undistort(clip, temp_window=10, tiles=1, overlap=8, interpolation="bicubic", backend="tensorrt", num_streams=1, engine_folder=None)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
__*`clip`*__
|
|
52
|
+
Distorted clip. Must be in RGBH format.
|
|
53
|
+
|
|
54
|
+
__*`temp_window`*__
|
|
55
|
+
Temporal window length. How many frames are grouped together and processed as a single chunk. Larger means higher VRAM requirements, but better temporal averaging and slower distortions can be removed. If this is too small, some distortions may not get removed, small jumps/hitches may be visible between windows and seams from tiling may become more obvious.
|
|
56
|
+
|
|
57
|
+
__*`tiles`* (optional)__
|
|
58
|
+
Amount of tiles to split the frames into. A higher amount reduces VRAM requirements, but also worsens spatial averaging. Default tiles=1 uses the full frame.
|
|
59
|
+
|
|
60
|
+
__*`overlap`* (optional)__
|
|
61
|
+
Overlap from one tile to the next. Increase if seams between tiles are visible.
|
|
62
|
+
|
|
63
|
+
__*`interpolation`* (optional)__
|
|
64
|
+
The interpolation mode used to warp the frames:
|
|
65
|
+
* `bilinear` More blurry.
|
|
66
|
+
* `bicubic` No blur, but may oversharpen slightly.
|
|
67
|
+
|
|
68
|
+
__*`backend`* (optional)__
|
|
69
|
+
The backend used to run the model:
|
|
70
|
+
* `cpu` CPU mode using PyTorch *(very slow)*.
|
|
71
|
+
* `cuda` GPU mode using PyTorch with CUDA support. Requires any Nvidia GPU *(fast)*.
|
|
72
|
+
* `tensorrt` GPU mode using vs-mlrt with TensorRT support. Requires an Nvidia RTX GPU. On the first run, this mode will automatically build an engine, which may take a few minutes. Changing interpolation, temp_window, or input dimensions will trigger rebuilding, but previously build engines are stored *(very fast/low vram)*.
|
|
73
|
+
|
|
74
|
+
__*`num_streams`* (optional)__
|
|
75
|
+
Number of parallel TensorRT streams. For high end GPUs higher can be a bit faster, but requires more VRAM. Only affects the TensorRT backend.
|
|
76
|
+
|
|
77
|
+
__*`engine_folder`* (optional)__
|
|
78
|
+
Optional path to the TensorRT engine storage location. By default engines are stored in `vs_undistort/engines`. Only affects the TensorRT backend.
|
|
79
|
+
|
|
80
|
+
> [!TIP]
|
|
81
|
+
> * If you see jumps/hitches between temporal windows, you can crossfade them with [vs_tiletools](https://github.com/pifroggi/vs_tiletools?tab=readme-ov-file#fix-jumpshitches-on-chunktemporal-window-based-filters-via-crossfading).
|
|
82
|
+
> * If you have an undistorted reference clip, you can also try to align to it with [vs_align](https://github.com/pifroggi/vs_align).
|
|
83
|
+
|
|
84
|
+
<br />
|
|
85
|
+
|
|
86
|
+
## Benchmarks
|
|
87
|
+
|
|
88
|
+
| Hardware | Resolution | TensorRT | CUDA
|
|
89
|
+
|----------|------------|----------|---------
|
|
90
|
+
| RTX 4090 | 720x480 | ~35 fps | ~6.5 fps
|
|
91
|
+
| RTX 4090 | 1440x1080 | ~7.5 fps | ~1.5 fps
|
|
92
|
+
| RTX 4090 | 2880x2160 | ~2 fps | ~0.5 fps
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Video Distortion Removal for VapourSynth
|
|
2
|
+
Also known as dewobble, warp stabilization, film or VHS distortion fix, rectification, atmospheric turbulence mitigation, or heat haze removal.
|
|
3
|
+
|
|
4
|
+
This is a partial implementation of the [Turbulence Mitigation Transformer](https://github.com/xg416/TMT) (only distortion removal, no deblurring). It does not do general video stabilization for shaky footage, only removes distortions within the frames. It is recommented to stabilize first if needed.
|
|
5
|
+
|
|
6
|
+
Check out stinkybread's comparisons [here](https://kaizoku.pw/c/tezc1iieeHympQGQeWKl) and [here](https://kaizoku.pw/c/4HsCLWDxuFMNpOIdSNI8).
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="https://github.com/xg416/TMT/blob/main/figs/video_22.gif"/>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<br />
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install -U vs_undistort
|
|
18
|
+
```
|
|
19
|
+
* To enable the CPU/CUDA backends, install [PyTorch with CUDA](https://pytorch.org/). *(optional)*
|
|
20
|
+
<br />
|
|
21
|
+
|
|
22
|
+
For older vapoursynth versions below R74, follow the manual installation steps [here](https://github.com/pifroggi/vs_temporalfix/wiki/Manual-Installation).
|
|
23
|
+
|
|
24
|
+
<br />
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from vs_undistort import vs_undistort
|
|
30
|
+
clip = vs_undistort(clip, temp_window=10, tiles=1, overlap=8, interpolation="bicubic", backend="tensorrt", num_streams=1, engine_folder=None)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
__*`clip`*__
|
|
34
|
+
Distorted clip. Must be in RGBH format.
|
|
35
|
+
|
|
36
|
+
__*`temp_window`*__
|
|
37
|
+
Temporal window length. How many frames are grouped together and processed as a single chunk. Larger means higher VRAM requirements, but better temporal averaging and slower distortions can be removed. If this is too small, some distortions may not get removed, small jumps/hitches may be visible between windows and seams from tiling may become more obvious.
|
|
38
|
+
|
|
39
|
+
__*`tiles`* (optional)__
|
|
40
|
+
Amount of tiles to split the frames into. A higher amount reduces VRAM requirements, but also worsens spatial averaging. Default tiles=1 uses the full frame.
|
|
41
|
+
|
|
42
|
+
__*`overlap`* (optional)__
|
|
43
|
+
Overlap from one tile to the next. Increase if seams between tiles are visible.
|
|
44
|
+
|
|
45
|
+
__*`interpolation`* (optional)__
|
|
46
|
+
The interpolation mode used to warp the frames:
|
|
47
|
+
* `bilinear` More blurry.
|
|
48
|
+
* `bicubic` No blur, but may oversharpen slightly.
|
|
49
|
+
|
|
50
|
+
__*`backend`* (optional)__
|
|
51
|
+
The backend used to run the model:
|
|
52
|
+
* `cpu` CPU mode using PyTorch *(very slow)*.
|
|
53
|
+
* `cuda` GPU mode using PyTorch with CUDA support. Requires any Nvidia GPU *(fast)*.
|
|
54
|
+
* `tensorrt` GPU mode using vs-mlrt with TensorRT support. Requires an Nvidia RTX GPU. On the first run, this mode will automatically build an engine, which may take a few minutes. Changing interpolation, temp_window, or input dimensions will trigger rebuilding, but previously build engines are stored *(very fast/low vram)*.
|
|
55
|
+
|
|
56
|
+
__*`num_streams`* (optional)__
|
|
57
|
+
Number of parallel TensorRT streams. For high end GPUs higher can be a bit faster, but requires more VRAM. Only affects the TensorRT backend.
|
|
58
|
+
|
|
59
|
+
__*`engine_folder`* (optional)__
|
|
60
|
+
Optional path to the TensorRT engine storage location. By default engines are stored in `vs_undistort/engines`. Only affects the TensorRT backend.
|
|
61
|
+
|
|
62
|
+
> [!TIP]
|
|
63
|
+
> * If you see jumps/hitches between temporal windows, you can crossfade them with [vs_tiletools](https://github.com/pifroggi/vs_tiletools?tab=readme-ov-file#fix-jumpshitches-on-chunktemporal-window-based-filters-via-crossfading).
|
|
64
|
+
> * If you have an undistorted reference clip, you can also try to align to it with [vs_align](https://github.com/pifroggi/vs_align).
|
|
65
|
+
|
|
66
|
+
<br />
|
|
67
|
+
|
|
68
|
+
## Benchmarks
|
|
69
|
+
|
|
70
|
+
| Hardware | Resolution | TensorRT | CUDA
|
|
71
|
+
|----------|------------|----------|---------
|
|
72
|
+
| RTX 4090 | 720x480 | ~35 fps | ~6.5 fps
|
|
73
|
+
| RTX 4090 | 1440x1080 | ~7.5 fps | ~1.5 fps
|
|
74
|
+
| RTX 4090 | 2880x2160 | ~2 fps | ~0.5 fps
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vs_undistort"
|
|
7
|
+
version = "2.2.0"
|
|
8
|
+
description = "Vapoursynth function to remove video distortions, turbulence, wobble, warp, heat haze, or similar."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
authors = [{ name = "pifroggi" }]
|
|
12
|
+
|
|
13
|
+
keywords = [
|
|
14
|
+
"vapoursynth",
|
|
15
|
+
"video",
|
|
16
|
+
"distortion",
|
|
17
|
+
"turbulence",
|
|
18
|
+
"heat haze",
|
|
19
|
+
"wobble",
|
|
20
|
+
"rectification",
|
|
21
|
+
"filtering",
|
|
22
|
+
"restoration"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Topic :: Multimedia :: Video"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = [
|
|
31
|
+
"vapoursynth>=74",
|
|
32
|
+
"vapoursynth-mlrt-trt>=16.0",
|
|
33
|
+
"tensorrt-cu13>=11.1.0.106",
|
|
34
|
+
"numpy"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/pifroggi/vs_undistort"
|
|
39
|
+
Repository = "https://github.com/pifroggi/vs_undistort"
|
|
40
|
+
Issues = "https://github.com/pifroggi/vs_undistort/issues"
|
|
Binary file
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Code from "Turbulence Mitigation Transformer" https://github.com/xg416/TMT
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
import torch.nn.functional as F
|
|
7
|
+
|
|
8
|
+
class LayerNorm3D(nn.Module):
|
|
9
|
+
def __init__(self, dim, bias=True):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.LN = nn.LayerNorm(dim, elementwise_affine=bias)
|
|
12
|
+
|
|
13
|
+
def forward(self, x):
|
|
14
|
+
# x: (B, C, T, H, W)
|
|
15
|
+
x = x.permute(0, 2, 3, 4, 1) # (B, T, H, W, C)
|
|
16
|
+
x = self.LN(x) # normalize over last dim (C)
|
|
17
|
+
x = x.permute(0, 4, 1, 2, 3) # (B, C, T, H, W)
|
|
18
|
+
return x
|
|
19
|
+
|
|
20
|
+
class DetiltUNet3DS(nn.Module):
|
|
21
|
+
# __ __
|
|
22
|
+
# 1|__ ________________ __|1
|
|
23
|
+
# 2|__ ____________ __|2
|
|
24
|
+
# 3|__ ______ __|3
|
|
25
|
+
# 4|__ __ __|4
|
|
26
|
+
# The convolution operations on either side are residual subject to 1*1 Convolution for channel homogeneity
|
|
27
|
+
def __init__(self, num_channels=3, feat_channels=[64, 256, 256, 512], norm='BN', conv_type='normal', residual='conv', interpolation='bilinear'):
|
|
28
|
+
# residual: conv for residual input x through 1*1 conv across every layer for downsampling, None for removal of residuals
|
|
29
|
+
super(DetiltUNet3DS, self).__init__()
|
|
30
|
+
|
|
31
|
+
# Encoder downsamplers
|
|
32
|
+
self.pool1 = nn.MaxPool3d((1, 2, 2))
|
|
33
|
+
self.pool2 = nn.MaxPool3d((1, 2, 2))
|
|
34
|
+
self.pool3 = nn.MaxPool3d((1, 2, 2))
|
|
35
|
+
self.pool4 = nn.MaxPool3d((1, 2, 2))
|
|
36
|
+
|
|
37
|
+
# Encoder convolutions
|
|
38
|
+
if norm =='BN':
|
|
39
|
+
norm3d = nn.BatchNorm3d
|
|
40
|
+
elif norm == 'LN':
|
|
41
|
+
norm3d = LayerNorm3D
|
|
42
|
+
|
|
43
|
+
self.conv_blk1 = Conv3D_Block(num_channels, feat_channels[0], kernel=7, stride=1, padding=3, norm=norm3d, conv_type='normal', residual=residual, first=True)
|
|
44
|
+
self.conv_blk2 = Conv3D_Block(feat_channels[0], feat_channels[1], kernel=7, stride=1, padding=3, norm=norm3d, conv_type=conv_type, residual=residual)
|
|
45
|
+
self.conv_blk3 = Conv3D_Block(feat_channels[1], feat_channels[2], kernel=5, stride=1, padding=2, norm=norm3d, conv_type=conv_type, residual=residual)
|
|
46
|
+
self.conv_blk4 = Conv3D_Block(feat_channels[2], feat_channels[3], kernel=3, stride=1, padding=1, norm=norm3d, conv_type=conv_type, residual=residual)
|
|
47
|
+
|
|
48
|
+
# Decoder convolutions
|
|
49
|
+
self.dec_conv_blk3 = Conv3D_Block(2 * feat_channels[2], feat_channels[2], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
50
|
+
self.dec_conv_blk2 = Conv3D_Block(2 * feat_channels[1], feat_channels[1], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
51
|
+
self.dec_conv_blk1 = Conv3D_Block(2 * feat_channels[0], feat_channels[0], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
52
|
+
|
|
53
|
+
# Decoder upsamplers
|
|
54
|
+
self.deconv_blk3 = Deconv3D_Block(feat_channels[3], feat_channels[2], conv_type=conv_type)
|
|
55
|
+
self.deconv_blk2 = Deconv3D_Block(feat_channels[2], feat_channels[1], conv_type=conv_type)
|
|
56
|
+
self.deconv_blk1 = Deconv3D_Block(feat_channels[1], feat_channels[0], conv_type=conv_type)
|
|
57
|
+
|
|
58
|
+
# Final 1*1 Conv Segmentation map
|
|
59
|
+
self.out_conv3 = nn.Conv3d(feat_channels[2], 2, kernel_size=3, stride=1, padding=1, bias=True)
|
|
60
|
+
self.out_conv2 = nn.Conv3d(feat_channels[1], 2, kernel_size=3, stride=1, padding=1, bias=True)
|
|
61
|
+
self.out_conv1 = nn.Conv3d(feat_channels[0], 2, kernel_size=3, stride=1, padding=1, bias=True)
|
|
62
|
+
|
|
63
|
+
self.upsample3 = nn.Upsample(size=None, scale_factor=(1,4,4), mode='trilinear', align_corners=True)
|
|
64
|
+
self.upsample2 = nn.Upsample(size=None, scale_factor=(1,2,2), mode='trilinear', align_corners=True)
|
|
65
|
+
|
|
66
|
+
# TiltWarp settings
|
|
67
|
+
self.interpolation = interpolation
|
|
68
|
+
if interpolation == 'bilinear':
|
|
69
|
+
self.interp_mode = 'bilinear'
|
|
70
|
+
self.padding_mode = 'zeros'
|
|
71
|
+
elif interpolation == 'bicubic':
|
|
72
|
+
self.interp_mode = 'bicubic'
|
|
73
|
+
self.padding_mode = 'reflection'
|
|
74
|
+
|
|
75
|
+
def forward(self, x, scales=[True, True, True]):
|
|
76
|
+
# Encoder part
|
|
77
|
+
xin = x.permute(0,2,1,3,4)
|
|
78
|
+
x1 = self.conv_blk1(xin)
|
|
79
|
+
|
|
80
|
+
x_low1 = self.pool1(x1)
|
|
81
|
+
x2 = self.conv_blk2(x_low1)
|
|
82
|
+
|
|
83
|
+
x_low2 = self.pool2(x2)
|
|
84
|
+
x3 = self.conv_blk3(x_low2)
|
|
85
|
+
|
|
86
|
+
x_low3 = self.pool3(x3)
|
|
87
|
+
base = self.conv_blk4(x_low3)
|
|
88
|
+
|
|
89
|
+
# Decoder part
|
|
90
|
+
|
|
91
|
+
d3 = torch.cat([self.deconv_blk3(base), x3], dim=1)
|
|
92
|
+
d_high3 = self.dec_conv_blk3(d3)
|
|
93
|
+
d2 = torch.cat([self.deconv_blk2(d_high3), x2], dim=1)
|
|
94
|
+
d_high2 = self.dec_conv_blk2(d2)
|
|
95
|
+
d1 = torch.cat([self.deconv_blk1(d_high2), x1], dim=1)
|
|
96
|
+
d_high1 = self.dec_conv_blk1(d1)
|
|
97
|
+
|
|
98
|
+
s3, s2, s1 = scales
|
|
99
|
+
flow3 = self.out_conv3(d_high3)
|
|
100
|
+
flow2 = self.out_conv2(d_high2)
|
|
101
|
+
flow1 = self.out_conv1(d_high1)
|
|
102
|
+
|
|
103
|
+
# keep grid_sample in fp32 for stability
|
|
104
|
+
orig_dtype = x.dtype
|
|
105
|
+
x_warp = x.float()
|
|
106
|
+
|
|
107
|
+
# only do warp if needed
|
|
108
|
+
if s3:
|
|
109
|
+
flow3_up = self.upsample3(flow3).float()
|
|
110
|
+
out_3 = TiltWarp(x_warp, flow3_up, interp_mode=self.interp_mode, padding_mode=self.padding_mode)
|
|
111
|
+
else:
|
|
112
|
+
out_3 = x_warp
|
|
113
|
+
|
|
114
|
+
if s2:
|
|
115
|
+
flow2_up = self.upsample2(flow2).float()
|
|
116
|
+
out_2 = TiltWarp(out_3, flow2_up, interp_mode=self.interp_mode, padding_mode=self.padding_mode)
|
|
117
|
+
else:
|
|
118
|
+
out_2 = out_3
|
|
119
|
+
|
|
120
|
+
if s1:
|
|
121
|
+
flow1 = flow1.float()
|
|
122
|
+
out = TiltWarp(out_2, flow1, interp_mode=self.interp_mode, padding_mode=self.padding_mode)
|
|
123
|
+
else:
|
|
124
|
+
out = out_2
|
|
125
|
+
|
|
126
|
+
# cast back
|
|
127
|
+
return out.to(orig_dtype)
|
|
128
|
+
|
|
129
|
+
class UNet3D(nn.Module):
|
|
130
|
+
# __ __
|
|
131
|
+
# 1|__ ________________ __|1
|
|
132
|
+
# 2|__ ____________ __|2
|
|
133
|
+
# 3|__ ______ __|3
|
|
134
|
+
# 4|__ __ __|4
|
|
135
|
+
# The convolution operations on either side are residual subject to 1*1 Convolution for channel homogeneity
|
|
136
|
+
|
|
137
|
+
def __init__(self, num_channels=3, feat_channels=[64, 256, 256, 512, 1024], norm='BN', out_channels=2, conv_type='normal', residual='conv'):
|
|
138
|
+
# residual: conv for residual input x through 1*1 conv across every layer for downsampling, None for removal of residuals
|
|
139
|
+
|
|
140
|
+
super(UNet3D, self).__init__()
|
|
141
|
+
|
|
142
|
+
# Encoder downsamplers
|
|
143
|
+
self.pool1 = nn.MaxPool3d((1, 2, 2))
|
|
144
|
+
self.pool2 = nn.MaxPool3d((1, 2, 2))
|
|
145
|
+
self.pool3 = nn.MaxPool3d((1, 2, 2))
|
|
146
|
+
self.pool4 = nn.MaxPool3d((1, 2, 2))
|
|
147
|
+
|
|
148
|
+
# Encoder convolutions
|
|
149
|
+
if norm =='BN':
|
|
150
|
+
norm3d = nn.BatchNorm3d
|
|
151
|
+
elif norm == 'LN':
|
|
152
|
+
norm3d = LayerNorm3D
|
|
153
|
+
|
|
154
|
+
self.conv_blk1 = Conv3D_Block(num_channels, feat_channels[0], norm=norm3d, conv_type='normal', residual=residual)
|
|
155
|
+
self.conv_blk2 = Conv3D_Block(feat_channels[0], feat_channels[1], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
156
|
+
self.conv_blk3 = Conv3D_Block(feat_channels[1], feat_channels[2], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
157
|
+
self.conv_blk4 = Conv3D_Block(feat_channels[2], feat_channels[3], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
158
|
+
self.conv_blk5 = Conv3D_Block(feat_channels[3], feat_channels[4], norm=norm3d,conv_type=conv_type, residual=residual)
|
|
159
|
+
|
|
160
|
+
# Decoder convolutions
|
|
161
|
+
self.dec_conv_blk4 = Conv3D_Block(2 * feat_channels[3], feat_channels[3], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
162
|
+
self.dec_conv_blk3 = Conv3D_Block(2 * feat_channels[2], feat_channels[2], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
163
|
+
self.dec_conv_blk2 = Conv3D_Block(2 * feat_channels[1], feat_channels[1], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
164
|
+
self.dec_conv_blk1 = Conv3D_Block(2 * feat_channels[0], feat_channels[0], norm=norm3d, conv_type=conv_type, residual=residual)
|
|
165
|
+
|
|
166
|
+
# Decoder upsamplers
|
|
167
|
+
self.deconv_blk4 = Deconv3D_Block(feat_channels[4], feat_channels[3], conv_type=conv_type)
|
|
168
|
+
self.deconv_blk3 = Deconv3D_Block(feat_channels[3], feat_channels[2], conv_type=conv_type)
|
|
169
|
+
self.deconv_blk2 = Deconv3D_Block(feat_channels[2], feat_channels[1], conv_type=conv_type)
|
|
170
|
+
self.deconv_blk1 = Deconv3D_Block(feat_channels[1], feat_channels[0], conv_type=conv_type)
|
|
171
|
+
|
|
172
|
+
# Final output stage
|
|
173
|
+
self.out_conv = nn.Conv3d(feat_channels[0], out_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
|
174
|
+
|
|
175
|
+
def forward(self, x):
|
|
176
|
+
# Encoder part
|
|
177
|
+
x1 = self.conv_blk1(x)
|
|
178
|
+
|
|
179
|
+
x_low1 = self.pool1(x1)
|
|
180
|
+
x2 = self.conv_blk2(x_low1)
|
|
181
|
+
|
|
182
|
+
x_low2 = self.pool2(x2)
|
|
183
|
+
x3 = self.conv_blk3(x_low2)
|
|
184
|
+
|
|
185
|
+
x_low3 = self.pool3(x3)
|
|
186
|
+
x4 = self.conv_blk4(x_low3)
|
|
187
|
+
|
|
188
|
+
x_low4 = self.pool4(x4)
|
|
189
|
+
base = self.conv_blk5(x_low4)
|
|
190
|
+
|
|
191
|
+
# Decoder part
|
|
192
|
+
d4 = torch.cat([self.deconv_blk4(base), x4], dim=1)
|
|
193
|
+
d_high4 = self.dec_conv_blk4(d4)
|
|
194
|
+
d3 = torch.cat([self.deconv_blk3(d_high4), x3], dim=1)
|
|
195
|
+
d_high3 = self.dec_conv_blk3(d3)
|
|
196
|
+
d2 = torch.cat([self.deconv_blk2(d_high3), x2], dim=1)
|
|
197
|
+
d_high2 = self.dec_conv_blk2(d2)
|
|
198
|
+
d1 = torch.cat([self.deconv_blk1(d_high2), x1], dim=1)
|
|
199
|
+
d_high1 = self.dec_conv_blk1(d1)
|
|
200
|
+
|
|
201
|
+
# seg = self.tanh(self.one_conv(d_high1))
|
|
202
|
+
seg = self.out_conv(d_high1)
|
|
203
|
+
return seg
|
|
204
|
+
|
|
205
|
+
class DWconv3D(nn.Module):
|
|
206
|
+
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, bias=True):
|
|
207
|
+
super(DWconv3D, self).__init__()
|
|
208
|
+
|
|
209
|
+
self.conv = nn.Sequential(
|
|
210
|
+
nn.Conv3d(in_channels=in_channels, out_channels=in_channels, kernel_size=kernel_size, stride=stride, padding=padding, dilation=dilation,
|
|
211
|
+
groups=in_channels, bias=bias),
|
|
212
|
+
nn.Conv3d(in_channels=in_channels, out_channels=out_channels, kernel_size=1))
|
|
213
|
+
|
|
214
|
+
def forward(self, x):
|
|
215
|
+
return self.conv(x)
|
|
216
|
+
|
|
217
|
+
class DWconvTFL(nn.Module):
|
|
218
|
+
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, bias=True, n_frames=12):
|
|
219
|
+
super(DWconvTFL, self).__init__()
|
|
220
|
+
|
|
221
|
+
self.conv = nn.Sequential(
|
|
222
|
+
nn.Conv3d(in_channels=in_channels, out_channels=in_channels, kernel_size=(1,kernel_size,kernel_size),
|
|
223
|
+
stride=stride, padding=(0,padding,padding), dilation=dilation, groups=in_channels, bias=bias),
|
|
224
|
+
Rearrange('b c t h w -> b c h w t'),
|
|
225
|
+
nn.Linear(n_frames, n_frames),
|
|
226
|
+
Rearrange('b c h w t -> b c t h w'),
|
|
227
|
+
nn.Conv3d(in_channels=in_channels, out_channels=out_channels, kernel_size=1))
|
|
228
|
+
|
|
229
|
+
def forward(self, x):
|
|
230
|
+
return self.conv(x)
|
|
231
|
+
|
|
232
|
+
class Conv3D_Block(nn.Module):
|
|
233
|
+
def __init__(self, inp_feat, out_feat, kernel=3, stride=1, padding=1, norm=LayerNorm3D, conv_type='dw', residual=None, first=False):
|
|
234
|
+
super(Conv3D_Block, self).__init__()
|
|
235
|
+
if conv_type == 'normal' or first:
|
|
236
|
+
conv3d = nn.Conv3d
|
|
237
|
+
elif conv_type == 'dw':
|
|
238
|
+
conv3d = DWconv3D
|
|
239
|
+
|
|
240
|
+
self.conv1 = nn.Sequential(
|
|
241
|
+
conv3d(inp_feat, out_feat, kernel_size=kernel, stride=stride, padding=padding, bias=True),
|
|
242
|
+
norm(out_feat),
|
|
243
|
+
nn.GELU())
|
|
244
|
+
if first:
|
|
245
|
+
self.conv2 = nn.Sequential(
|
|
246
|
+
DWconv3D(out_feat, out_feat, kernel_size=kernel, stride=stride, padding=padding, bias=True),
|
|
247
|
+
norm(out_feat),
|
|
248
|
+
nn.GELU())
|
|
249
|
+
else:
|
|
250
|
+
self.conv2 = nn.Sequential(
|
|
251
|
+
conv3d(out_feat, out_feat, kernel_size=kernel, stride=stride, padding=padding, bias=True),
|
|
252
|
+
norm(out_feat),
|
|
253
|
+
nn.GELU())
|
|
254
|
+
self.residual = residual
|
|
255
|
+
if self.residual is not None:
|
|
256
|
+
self.residual_upsampler = conv3d(inp_feat, out_feat, kernel_size=1, bias=False)
|
|
257
|
+
|
|
258
|
+
def forward(self, x):
|
|
259
|
+
res = x
|
|
260
|
+
if not self.residual:
|
|
261
|
+
return self.conv2(self.conv1(x))
|
|
262
|
+
else:
|
|
263
|
+
return self.conv2(self.conv1(x)) + self.residual_upsampler(res)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Deconv3D_Block(nn.Module):
|
|
267
|
+
def __init__(self, inp_feat, out_feat, kernel=3, stride=2, padding=1, norm=LayerNorm3D, conv_type='dw'):
|
|
268
|
+
super(Deconv3D_Block, self).__init__()
|
|
269
|
+
if conv_type == 'normal':
|
|
270
|
+
self.deconv = nn.Sequential(
|
|
271
|
+
norm(inp_feat),
|
|
272
|
+
nn.ConvTranspose3d(inp_feat, out_feat, kernel_size=(kernel, kernel, kernel),
|
|
273
|
+
stride=(1, stride, stride), padding=(padding, padding, padding),
|
|
274
|
+
output_padding=(0,1,1), bias=True),
|
|
275
|
+
nn.GELU())
|
|
276
|
+
if conv_type == 'dw':
|
|
277
|
+
self.deconv = nn.Sequential(
|
|
278
|
+
norm(inp_feat),
|
|
279
|
+
nn.ConvTranspose3d(inp_feat, inp_feat, kernel_size=(kernel, kernel, kernel),
|
|
280
|
+
stride=(1, stride, stride), padding=(padding, padding, padding),
|
|
281
|
+
output_padding=(0,1,1), groups=inp_feat, bias=True),
|
|
282
|
+
nn.Conv3d(in_channels=inp_feat, out_channels=out_feat, kernel_size=1),
|
|
283
|
+
nn.GELU())
|
|
284
|
+
|
|
285
|
+
def forward(self, x):
|
|
286
|
+
return self.deconv(x)
|
|
287
|
+
|
|
288
|
+
def TiltWarp(x, flow, interp_mode='bilinear', padding_mode='zeros', align_corners=True, use_pad_mask=False):
|
|
289
|
+
"""
|
|
290
|
+
Args:
|
|
291
|
+
x (Tensor): Tensor with size (b, n, c, h, w) -> (b*n, c, h, w).
|
|
292
|
+
flow (Tensor): Tensor with size (b, 2, n, h, w) -> (b*n, h, w, 2), normal value.
|
|
293
|
+
interp_mode (str): 'nearest' or 'bilinear' or 'nearest4'. Default: 'bilinear'.
|
|
294
|
+
padding_mode (str): 'zeros' or 'border' or 'reflection'.
|
|
295
|
+
Default: 'zeros'.
|
|
296
|
+
align_corners (bool): Before pytorch 1.3, the default value is
|
|
297
|
+
align_corners=True. After pytorch 1.3, the default value is
|
|
298
|
+
align_corners=False. Here, we use the True as default.
|
|
299
|
+
use_pad_mask (bool): only used for PWCNet, x is first padded with ones along the channel dimension.
|
|
300
|
+
The mask is generated according to the grid_sample results of the padded dimension.
|
|
301
|
+
Returns:
|
|
302
|
+
Tensor: Warped image or feature map.
|
|
303
|
+
"""
|
|
304
|
+
B, N, C, H, W = x.shape
|
|
305
|
+
x_flat = x.reshape(B * N, C, H, W) # merge batch and time so grid_sample can work as (BN, C, H, W)
|
|
306
|
+
flow_flat = (flow.permute(0,2,3,4,1).reshape(B * N, H, W, 2)) # (BN, H, W, 2)
|
|
307
|
+
# create mesh grid
|
|
308
|
+
grid_y, grid_x = torch.meshgrid(torch.arange(H, dtype=x_flat.dtype, device=x_flat.device),
|
|
309
|
+
torch.arange(W, dtype=x_flat.dtype, device=x_flat.device), indexing='ij') # H, W are taken from the tensor's shape, so ONNX can track them.
|
|
310
|
+
grid = torch.stack((grid_x, grid_y), dim=2) # (H, W, 2)
|
|
311
|
+
vgrid = grid.unsqueeze(0) + flow_flat # (BN, H, W, 2)
|
|
312
|
+
|
|
313
|
+
vgrid_x = 2.0 * vgrid[..., 0] / (W - 1) - 1.0
|
|
314
|
+
vgrid_y = 2.0 * vgrid[..., 1] / (H - 1) - 1.0
|
|
315
|
+
vgrid_scaled = torch.stack((vgrid_x, vgrid_y), dim=-1) # (BN, H, W, 2)
|
|
316
|
+
output_flat = F.grid_sample(x_flat, vgrid_scaled, mode=interp_mode, padding_mode=padding_mode, align_corners=align_corners)
|
|
317
|
+
output = output_flat.reshape(B, N, C, H, W)
|
|
318
|
+
return output
|
|
319
|
+
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
|
|
2
|
+
# Script by pifroggi https://github.com/pifroggi/vs_undistort
|
|
3
|
+
# or tepete and pifroggi on Discord
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import vapoursynth as vs
|
|
7
|
+
|
|
8
|
+
core = vs.core
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def expression(clips, expr, format=None):
|
|
12
|
+
# backend for basic exprs supported by std.Expr
|
|
13
|
+
if hasattr(core, "akarin"):
|
|
14
|
+
return core.akarin.Expr(clips, expr, format=format)
|
|
15
|
+
else:
|
|
16
|
+
return core.std.Expr(clips, expr, format=format)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_window(clip, temp_window):
|
|
20
|
+
# how many pad frames to reach a multiple of temp_window
|
|
21
|
+
num_frames = clip.num_frames
|
|
22
|
+
pad = (-num_frames) % temp_window
|
|
23
|
+
|
|
24
|
+
if pad:
|
|
25
|
+
# pad black frames if too short
|
|
26
|
+
pad_clip = core.std.BlankClip(clip=clip, length=pad)
|
|
27
|
+
clip = core.std.Splice([clip, pad_clip])
|
|
28
|
+
|
|
29
|
+
# offset_clips[i] contains frames i, i+temp_window, i+2*temp_window, ...
|
|
30
|
+
return [core.std.SelectEvery(clip[i:], cycle=temp_window, offsets=[0]) for i in range(temp_window)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_tiles(clip_w, clip_h, tiles, overlap=0):
|
|
34
|
+
# calculate tile size and choose the most square layout
|
|
35
|
+
if tiles not in (1, 2, 4, 6, 8, 12, 16, 24, 32):
|
|
36
|
+
raise ValueError("vs_undistort: Tiles must be 1, 2, 4, 6, 8, 12, 16, 24, or 32.")
|
|
37
|
+
|
|
38
|
+
layouts = {
|
|
39
|
+
1: [(1, 1)],
|
|
40
|
+
2: [(2, 1), (1, 2)],
|
|
41
|
+
4: [(4, 1), (2, 2), (1, 4)],
|
|
42
|
+
6: [(6, 1), (3, 2), (2, 3), (1, 6)],
|
|
43
|
+
8: [(8, 1), (4, 2), (2, 4), (1, 8)],
|
|
44
|
+
12: [(12, 1), (6, 2), (4, 3), (3, 4), (2, 6), (1, 12)],
|
|
45
|
+
16: [(16, 1), (8, 2), (4, 4), (2, 8), (1, 16)],
|
|
46
|
+
24: [(24, 1), (12, 2), (8, 3), (6, 4), (4, 6), (3, 8), (2, 12), (1, 24)],
|
|
47
|
+
32: [(32, 1), (16, 2), (8, 4), (4, 8), (2, 16), (1, 32)],
|
|
48
|
+
}[tiles]
|
|
49
|
+
|
|
50
|
+
def _tile_size(layout):
|
|
51
|
+
cols, rows = layout
|
|
52
|
+
tile_w = math.ceil(math.ceil((clip_w + 2 * overlap * (cols - 1)) / cols) / 16) * 16
|
|
53
|
+
tile_h = math.ceil(math.ceil((clip_h + 2 * overlap * (rows - 1)) / rows) / 16) * 16
|
|
54
|
+
return tile_w, tile_h
|
|
55
|
+
|
|
56
|
+
def _layout_valid(layout):
|
|
57
|
+
# tiles must have a positive non overlapped stride
|
|
58
|
+
cols, rows = layout
|
|
59
|
+
tile_w, tile_h = _tile_size(layout)
|
|
60
|
+
if cols > 1 and tile_w <= 2 * overlap:
|
|
61
|
+
return False
|
|
62
|
+
if rows > 1 and tile_h <= 2 * overlap:
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
def _score(layout):
|
|
67
|
+
tile_w, tile_h = _tile_size(layout)
|
|
68
|
+
cols, rows = layout
|
|
69
|
+
tile_aspect = tile_w / tile_h
|
|
70
|
+
square_error = abs(math.log(tile_aspect))
|
|
71
|
+
orientation_penalty = rows if clip_w >= clip_h else cols
|
|
72
|
+
balance_penalty = abs(cols - rows)
|
|
73
|
+
return (square_error, orientation_penalty, balance_penalty)
|
|
74
|
+
|
|
75
|
+
valid_layouts = [layout for layout in layouts if _layout_valid(layout)]
|
|
76
|
+
if not valid_layouts:
|
|
77
|
+
raise ValueError("vs_undistort: Clip dimensions are too small for current tile amount. Reduce tiles or overlap.")
|
|
78
|
+
|
|
79
|
+
cols, rows = min(valid_layouts, key=_score)
|
|
80
|
+
tile_w, tile_h = _tile_size((cols, rows))
|
|
81
|
+
if tile_w > 2 * tile_h or tile_h > 2 * tile_w:
|
|
82
|
+
raise ValueError("vs_undistort: The current tile amount produces tiles that are too elongated. Try a different tile amount.")
|
|
83
|
+
|
|
84
|
+
return tile_w, tile_h
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_tile_positions(h, w, s_h, s_w, overlap=0):
|
|
88
|
+
if overlap is None:
|
|
89
|
+
overlap = 0
|
|
90
|
+
|
|
91
|
+
def _axis_positions(size, patch, overlap):
|
|
92
|
+
|
|
93
|
+
# single patch or smaller image = no tiling / overlap needed
|
|
94
|
+
if patch >= size:
|
|
95
|
+
return [0]
|
|
96
|
+
|
|
97
|
+
# overlap is removed from both sides of the tile stride to match vsmlrt
|
|
98
|
+
step = patch - 2 * overlap
|
|
99
|
+
max_start = size - patch
|
|
100
|
+
|
|
101
|
+
if step <= 0 or max_start <= 0:
|
|
102
|
+
return [0]
|
|
103
|
+
|
|
104
|
+
positions = [0]
|
|
105
|
+
while positions[-1] < max_start:
|
|
106
|
+
positions.append(min(positions[-1] + step, max_start))
|
|
107
|
+
|
|
108
|
+
return positions
|
|
109
|
+
|
|
110
|
+
hpos = _axis_positions(h, s_h, overlap)
|
|
111
|
+
wpos = _axis_positions(w, s_w, overlap)
|
|
112
|
+
return hpos, wpos
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def inference_tiled(input_blk, model_tilt, patch_height, patch_width, overlap=0, scales=[True, True, True], tile_device=None):
|
|
116
|
+
import torch
|
|
117
|
+
tile_device = torch.device("cpu") if tile_device is None else tile_device
|
|
118
|
+
model_device = next(model_tilt.parameters()).device
|
|
119
|
+
b, l, c, h, w = input_blk.shape
|
|
120
|
+
|
|
121
|
+
hpos, wpos = get_tile_positions(h, w, patch_height, patch_width, overlap)
|
|
122
|
+
out_spaces = torch.empty_like(input_blk, device=tile_device) # (B, L, C, H, W)
|
|
123
|
+
|
|
124
|
+
for hi in hpos:
|
|
125
|
+
y_crop_start = 0 if hi == 0 else overlap
|
|
126
|
+
y_crop_end = 0 if hi == h - patch_height else overlap
|
|
127
|
+
for wi in wpos:
|
|
128
|
+
x_crop_start = 0 if wi == 0 else overlap
|
|
129
|
+
x_crop_end = 0 if wi == w - patch_width else overlap
|
|
130
|
+
inp = input_blk[..., hi:hi + patch_height, wi:wi + patch_width] # (B, L, C, ph, pw)
|
|
131
|
+
|
|
132
|
+
# move only the tile to the model device
|
|
133
|
+
if inp.device != model_device:
|
|
134
|
+
non_blocking = (inp.device.type == "cpu" and model_device.type == "cuda" and inp.is_pinned())
|
|
135
|
+
inp = inp.to(model_device, non_blocking=non_blocking)
|
|
136
|
+
|
|
137
|
+
rectified = model_tilt(inp, scales=scales)
|
|
138
|
+
hs, ws = rectified.shape[-2:]
|
|
139
|
+
|
|
140
|
+
# move tile result back to tile device
|
|
141
|
+
if rectified.device != tile_device:
|
|
142
|
+
rectified = rectified.to(tile_device)
|
|
143
|
+
|
|
144
|
+
out_spaces[..., hi + y_crop_start:hi + hs - y_crop_end, wi + x_crop_start:wi + ws - x_crop_end].copy_(rectified[..., y_crop_start:hs - y_crop_end, x_crop_start:ws - x_crop_end])
|
|
145
|
+
|
|
146
|
+
return out_spaces
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# Script to run "Turbulence Mitigation Transformer" https://github.com/xg416/TMT
|
|
2
|
+
|
|
3
|
+
# Vapoursynth Implementation by pifroggi https://github.com/pifroggi/vs_undistort
|
|
4
|
+
# or tepete and pifroggi on Discord
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import logging
|
|
10
|
+
import subprocess
|
|
11
|
+
import vapoursynth as vs
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from .utils import get_window, get_tiles, expression
|
|
14
|
+
|
|
15
|
+
core = vs.core
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pytorch(clip, temp_window=10, tiles=1, overlap=8, interpolation="bicubic", device="cuda"):
|
|
19
|
+
import threading
|
|
20
|
+
import numpy as np
|
|
21
|
+
from collections import OrderedDict
|
|
22
|
+
|
|
23
|
+
if device == "cpu":
|
|
24
|
+
try:
|
|
25
|
+
import torch
|
|
26
|
+
except ImportError:
|
|
27
|
+
raise RuntimeError("vs_undistort: The CPU/CUDA backends require PyTorch. Please install it from https://pytorch.org/ or choose a different backend. For the CUDA backend specifically, install a version of PyTorch with CUDA support.") from None
|
|
28
|
+
|
|
29
|
+
if device == "cuda":
|
|
30
|
+
try:
|
|
31
|
+
import torch
|
|
32
|
+
except ImportError:
|
|
33
|
+
raise RuntimeError("vs_undistort: The CUDA backend requires PyTorch with CUDA. Please install a version of PyTorch with CUDA support from https://pytorch.org/ or choose a different backend.") from None
|
|
34
|
+
if not torch.cuda.is_available():
|
|
35
|
+
raise RuntimeError("vs_undistort: The CUDA backend requires PyTorch with CUDA, but the installed version has no CUDA support. Please upgrade to a version with CUDA support from https://pytorch.org/ or choose a different backend.")
|
|
36
|
+
|
|
37
|
+
from .utils import inference_tiled
|
|
38
|
+
from .models.UNet3d_TMT_arch import DetiltUNet3DS
|
|
39
|
+
os.environ["CUDA_MODULE_LOADING"] = "LAZY"
|
|
40
|
+
|
|
41
|
+
def _frames_to_tensor(frames, device, tile_device, fp16=False):
|
|
42
|
+
temp_window = len(frames)
|
|
43
|
+
h, w = frames[0].height, frames[0].width
|
|
44
|
+
num_planes = frames[0].format.num_planes
|
|
45
|
+
dtype = np.float16 if fp16 == True else np.float32
|
|
46
|
+
arr = np.empty((temp_window, num_planes, h, w), dtype=dtype)
|
|
47
|
+
for i, fr in enumerate(frames):
|
|
48
|
+
for p in range(num_planes):
|
|
49
|
+
arr[i, p, :, :] = np.asarray(fr[p])
|
|
50
|
+
tensor = torch.from_numpy(arr)
|
|
51
|
+
if tile_device.type != "cpu":
|
|
52
|
+
return tensor.to(tile_device).clamp_(0, 1).unsqueeze(0)
|
|
53
|
+
if device.type == "cuda":
|
|
54
|
+
tensor = tensor.pin_memory()
|
|
55
|
+
return tensor.clamp_(0, 1).unsqueeze(0)
|
|
56
|
+
|
|
57
|
+
def _tensor_to_frame(tensor, frame):
|
|
58
|
+
tensor_np = tensor.detach().cpu().numpy()
|
|
59
|
+
for p in range(frame.format.num_planes):
|
|
60
|
+
frame_arr = np.asarray(frame[p])
|
|
61
|
+
np.copyto(frame_arr, tensor_np[:, :, p])
|
|
62
|
+
|
|
63
|
+
def _load_model(device, interpolation, fp16=False):
|
|
64
|
+
model_tilt = DetiltUNet3DS(norm="LN", residual="pool", conv_type="dw", interpolation=interpolation)
|
|
65
|
+
current_folder = os.path.dirname(os.path.abspath(__file__))
|
|
66
|
+
path_tilt = os.path.join(current_folder, "models", "UNet3d_TMT.pth")
|
|
67
|
+
ckpt_tilt = torch.load(path_tilt, map_location=device, weights_only=True)
|
|
68
|
+
model_tilt.load_state_dict(ckpt_tilt["state_dict"] if "state_dict" in ckpt_tilt else ckpt_tilt)
|
|
69
|
+
model_tilt.to(device)
|
|
70
|
+
if fp16:
|
|
71
|
+
model_tilt.half()
|
|
72
|
+
model_tilt.eval()
|
|
73
|
+
return model_tilt
|
|
74
|
+
|
|
75
|
+
def _pytorch_inference(n, f):
|
|
76
|
+
with torch.inference_mode():
|
|
77
|
+
out = f[0].copy()
|
|
78
|
+
tensor = _frames_to_tensor(f[1:], device, tile_device, fp16=fp16) # (1, T, C, H, W)
|
|
79
|
+
tensor = inference_tiled(tensor, model_tilt, tile_h, tile_w, overlap=overlap, scales=[True, True, True], tile_device=tile_device)
|
|
80
|
+
stacked_tensor = tensor[0].permute(2, 0, 3, 1).contiguous().flatten(1, 2) # (H, T * W, C), output one large frame to let vs handle the caching
|
|
81
|
+
_tensor_to_frame(stacked_tensor, out)
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
# checks
|
|
85
|
+
if not isinstance(clip, vs.VideoNode):
|
|
86
|
+
raise TypeError("vs_undistort: Clip must be a vapoursynth clip.")
|
|
87
|
+
if clip.format.id == vs.PresetVideoFormat.NONE or clip.width == 0 or clip.height == 0:
|
|
88
|
+
raise TypeError("vs_undistort: Clip must have constant format and dimensions.")
|
|
89
|
+
if clip.format.color_family != vs.RGB:
|
|
90
|
+
raise ValueError("vs_undistort: Clip must be in RGB format.")
|
|
91
|
+
if not isinstance(temp_window, int) or isinstance(temp_window, bool):
|
|
92
|
+
raise TypeError("vs_undistort: Temporal window length must be an integer.")
|
|
93
|
+
if temp_window < 2:
|
|
94
|
+
raise ValueError("vs_undistort: Temporal window length must be at least 2.")
|
|
95
|
+
if not isinstance(tiles, int) or isinstance(tiles, bool):
|
|
96
|
+
raise TypeError("vs_undistort: Tiles must be an integer.")
|
|
97
|
+
if not isinstance(overlap, int) or isinstance(overlap, bool):
|
|
98
|
+
raise TypeError("vs_undistort: Overlap must be an integer.")
|
|
99
|
+
if overlap < 0:
|
|
100
|
+
raise ValueError("vs_undistort: Overlap can not be negative.")
|
|
101
|
+
if interpolation not in ["bilinear", "bicubic"]:
|
|
102
|
+
raise ValueError("vs_undistort: Warp interpolation mode must be 'bilinear' or 'bicubic'.")
|
|
103
|
+
|
|
104
|
+
orig_format = clip.format.id
|
|
105
|
+
device = torch.device(device)
|
|
106
|
+
fp16 = device.type == "cuda" and torch.cuda.get_device_capability()[0] >= 7
|
|
107
|
+
req_format = vs.RGBH if fp16 else vs.RGBS
|
|
108
|
+
model_tilt = _load_model(device, interpolation, fp16=fp16)
|
|
109
|
+
|
|
110
|
+
# decide tile size
|
|
111
|
+
tile_w, tile_h = get_tiles(clip_w=clip.width, clip_h=clip.height, tiles=tiles, overlap=overlap)
|
|
112
|
+
|
|
113
|
+
# pad if tile is larger than clip
|
|
114
|
+
pad_w = max(0, tile_w - clip.width)
|
|
115
|
+
pad_h = max(0, tile_h - clip.height)
|
|
116
|
+
if pad_w or pad_h:
|
|
117
|
+
clip = core.std.AddBorders(clip, right=pad_w, bottom=pad_h)
|
|
118
|
+
|
|
119
|
+
width = clip.width
|
|
120
|
+
height = clip.height
|
|
121
|
+
tile_device = device if tiles == 1 else torch.device("cpu")
|
|
122
|
+
|
|
123
|
+
# convert inputs to needed precision
|
|
124
|
+
if clip.format.id != req_format:
|
|
125
|
+
clip = core.resize.Point(clip, format=req_format)
|
|
126
|
+
if tile_w < 128 or tile_h < 128:
|
|
127
|
+
raise ValueError("vs_undistort: Tile size must be at least 128 x 128. Reduce tiles.")
|
|
128
|
+
if tiles > 1 and overlap > min(tile_w, tile_h) // 2:
|
|
129
|
+
raise ValueError("vs_undistort: Overlap can not be larger than half of tile size. Reduce overlap.")
|
|
130
|
+
|
|
131
|
+
# inference
|
|
132
|
+
offset_clips = get_window(clip, temp_window)
|
|
133
|
+
out_shape = core.std.BlankClip(clip=offset_clips[0], width=width * temp_window, height=height, keep=True)
|
|
134
|
+
stacked_clip = core.std.ModifyFrame(out_shape, clips=[out_shape, *offset_clips], selector=_pytorch_inference)
|
|
135
|
+
offset_clips = [core.std.Crop(stacked_clip, left=i * width, right=(temp_window - 1 - i) * width) for i in range(temp_window)]
|
|
136
|
+
unstacked_clip = core.std.Interleave(offset_clips)
|
|
137
|
+
|
|
138
|
+
if unstacked_clip.format.id != orig_format:
|
|
139
|
+
unstacked_clip = core.resize.Point(unstacked_clip, format=orig_format)
|
|
140
|
+
if pad_w or pad_h:
|
|
141
|
+
unstacked_clip = core.std.Crop(unstacked_clip, right=pad_w, bottom=pad_h)
|
|
142
|
+
if unstacked_clip.num_frames != clip.num_frames:
|
|
143
|
+
unstacked_clip = core.std.Trim(unstacked_clip, last=clip.num_frames - 1)
|
|
144
|
+
|
|
145
|
+
return core.std.CopyFrameProps(unstacked_clip, clip)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_builder(plugin_path, trt_version, cuda_major):
|
|
149
|
+
# finds compatible tensorrt engine builders
|
|
150
|
+
exe_name = "trtexec.exe" if os.name == "nt" else "trtexec"
|
|
151
|
+
builders = []
|
|
152
|
+
errors = []
|
|
153
|
+
|
|
154
|
+
# check for python tensorrt
|
|
155
|
+
try:
|
|
156
|
+
import tensorrt
|
|
157
|
+
package_version = list(map(int, tensorrt.__version__.split(".")[:3]))
|
|
158
|
+
if package_version == trt_version:
|
|
159
|
+
builders.append(["python", tensorrt])
|
|
160
|
+
else:
|
|
161
|
+
errors.append(f"Python TensorRT: Wrong version {'.'.join(map(str, package_version))}")
|
|
162
|
+
except ImportError:
|
|
163
|
+
errors.append("Python TensorRT: Not found.")
|
|
164
|
+
except Exception:
|
|
165
|
+
errors.append("Python TensorRT: Found but failed to check version.")
|
|
166
|
+
|
|
167
|
+
# check for bundled trtexec
|
|
168
|
+
bundled_trtexec = Path(plugin_path) / "vsmlrt-cuda" / exe_name
|
|
169
|
+
if bundled_trtexec.is_file() and os.access(str(bundled_trtexec), os.X_OK):
|
|
170
|
+
builders.append(["trtexec", bundled_trtexec])
|
|
171
|
+
else:
|
|
172
|
+
errors.append(f"Bundled trtexec: Not found.")
|
|
173
|
+
|
|
174
|
+
# check for system trtexec
|
|
175
|
+
system_trtexec = shutil.which("trtexec")
|
|
176
|
+
if system_trtexec is not None:
|
|
177
|
+
try:
|
|
178
|
+
trtexec_path = Path(system_trtexec)
|
|
179
|
+
help_output = subprocess.run([str(trtexec_path), "--help"], check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="locale", errors="replace")
|
|
180
|
+
help_output = f"{help_output.stdout}\n{help_output.stderr}"
|
|
181
|
+
|
|
182
|
+
trtexec_version = None
|
|
183
|
+
trtexec_version = re.search(r"\[TensorRT v(\d+)\]", help_output)
|
|
184
|
+
if trtexec_version is None:
|
|
185
|
+
raise RuntimeError("vs_undistort: Internal Error: Regex failed to find the version.")
|
|
186
|
+
|
|
187
|
+
trtexec_version = int(trtexec_version.group(1))
|
|
188
|
+
trtexec_version = [trtexec_version // 10000, (trtexec_version % 10000) // 100, trtexec_version % 100]
|
|
189
|
+
if trtexec_version == trt_version:
|
|
190
|
+
builders.append(["trtexec", trtexec_path])
|
|
191
|
+
else:
|
|
192
|
+
errors.append(f"System trtexec: Wrong version {'.'.join(map(str, trtexec_version))}")
|
|
193
|
+
except Exception:
|
|
194
|
+
errors.append("System trtexec: Found but failed to check version.")
|
|
195
|
+
else:
|
|
196
|
+
errors.append("System trtexec: Not found.")
|
|
197
|
+
|
|
198
|
+
# return first compatible builder
|
|
199
|
+
if builders:
|
|
200
|
+
return builders[0]
|
|
201
|
+
|
|
202
|
+
errors = "\n".join(f"{builder}" for builder in errors)
|
|
203
|
+
raise FileNotFoundError(f"vs_undistort: No compatible TensorRT engine builder found. Please install the python package 'tensorrt' or install trtexec. The required TensorRT version is {'.'.join(map(str, trt_version))}. The required CUDA version is {cuda_major}.\n{errors}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _build_engine_trtexec(onnx_path, engine_path, temp_window, engine_w, engine_h, interpolation, trt_version, trtexec_path):
|
|
207
|
+
# build engine using trtexec, supports trt 10 and 11
|
|
208
|
+
|
|
209
|
+
# settings
|
|
210
|
+
opt_shapes = f"input:1x{temp_window * 3}x{engine_h}x{engine_w}"
|
|
211
|
+
io_formats = f"fp16:chw" if trt_version[0] < 11 else "chw"
|
|
212
|
+
cmd = [
|
|
213
|
+
str(trtexec_path),
|
|
214
|
+
*(["--stronglyTyped"] if trt_version[0] < 11 else []),
|
|
215
|
+
*(["--markDebug=grid_sampler,grid_sampler_1"] if interpolation == "bicubic" else []), # part of gridsample bicubic workaround
|
|
216
|
+
"--skipInference",
|
|
217
|
+
"--memPoolSize=workspace:6144",
|
|
218
|
+
"--builderOptimizationLevel=3",
|
|
219
|
+
f"--inputIOFormats={io_formats}",
|
|
220
|
+
f"--outputIOFormats={io_formats}",
|
|
221
|
+
f"--onnx={onnx_path}",
|
|
222
|
+
f"--saveEngine={engine_path}",
|
|
223
|
+
f"--optShapes={opt_shapes}",
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# build
|
|
227
|
+
try:
|
|
228
|
+
result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="locale", errors="replace")
|
|
229
|
+
except subprocess.CalledProcessError as e:
|
|
230
|
+
msg = (
|
|
231
|
+
"vs_undistort: Internal Error: trtexec failed while building the TensorRT engine.\n"
|
|
232
|
+
f" Command: {' '.join(cmd)}\n"
|
|
233
|
+
f" Return code: {e.returncode}\n"
|
|
234
|
+
)
|
|
235
|
+
if e.stdout:
|
|
236
|
+
msg += f"\n=== trtexec stdout ===\n{e.stdout}"
|
|
237
|
+
if e.stderr:
|
|
238
|
+
msg += f"\n=== trtexec stderr ===\n{e.stderr}"
|
|
239
|
+
raise RuntimeError(msg) from e
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _build_engine_python(onnx_path, engine_path, temp_window, engine_w, engine_h, interpolation, trt_package):
|
|
243
|
+
# build engine using tensorrt python package, supports only trt 11 because of vapoursynth-mlrt-trt
|
|
244
|
+
trt = trt_package
|
|
245
|
+
|
|
246
|
+
# custom logger for errors
|
|
247
|
+
class _TrtLogger(trt.ILogger):
|
|
248
|
+
def __init__(self):
|
|
249
|
+
trt.ILogger.__init__(self)
|
|
250
|
+
self.messages = []
|
|
251
|
+
self.fatal = False
|
|
252
|
+
def log(self, severity, msg):
|
|
253
|
+
if severity <= trt.Logger.WARNING:
|
|
254
|
+
self.messages.append((severity, msg))
|
|
255
|
+
if self.fatal:
|
|
256
|
+
logging.critical(f" [{severity}] {msg}")
|
|
257
|
+
elif severity == trt.Logger.INTERNAL_ERROR: # print fatal errors immediately because python may not get control back
|
|
258
|
+
self.fatal = True
|
|
259
|
+
log = "\n".join(f" [{log_severity}] {log_msg}" for log_severity, log_msg in self.messages)
|
|
260
|
+
logging.critical(f"vs_undistort: Internal Error: TensorRT failed while building the TensorRT engine.\n=== TensorRT log ===\n{log}")
|
|
261
|
+
def get_log(self):
|
|
262
|
+
return "\n".join(f" [{severity}] {msg}" for severity, msg in self.messages)
|
|
263
|
+
|
|
264
|
+
# initialize trt and load model
|
|
265
|
+
logger = _TrtLogger()
|
|
266
|
+
builder = trt.Builder(logger)
|
|
267
|
+
network = builder.create_network()
|
|
268
|
+
config = builder.create_builder_config()
|
|
269
|
+
parser = trt.OnnxParser(network, logger)
|
|
270
|
+
if not parser.parse_from_file(str(onnx_path)):
|
|
271
|
+
errors = "\n".join(f" {parser.get_error(i)}" for i in range(parser.num_errors))
|
|
272
|
+
raise RuntimeError(f"vs_undistort: Internal Error: TensorRT failed while parsing the ONNX model.\n{errors}")
|
|
273
|
+
|
|
274
|
+
# mark debug
|
|
275
|
+
if interpolation == "bicubic":
|
|
276
|
+
for layer_index in range(network.num_layers):
|
|
277
|
+
layer = network.get_layer(layer_index)
|
|
278
|
+
for output_index in range(layer.num_outputs):
|
|
279
|
+
tensor = layer.get_output(output_index)
|
|
280
|
+
if tensor is not None and tensor.name in ("grid_sampler", "grid_sampler_1"): # part of gridsample bicubic workaround
|
|
281
|
+
network.mark_debug(tensor)
|
|
282
|
+
|
|
283
|
+
# settings
|
|
284
|
+
opt_shapes = (1, temp_window * 3, engine_h, engine_w) # optShapes
|
|
285
|
+
network.get_input(0).allowed_formats = network.get_output(0).allowed_formats = 1 << int(trt.TensorFormat.LINEAR) # IOFormats:chw
|
|
286
|
+
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 6144 << 20) # workspace
|
|
287
|
+
config.builder_optimization_level = 3 # builderOptimizationLevel
|
|
288
|
+
|
|
289
|
+
# build
|
|
290
|
+
profile = builder.create_optimization_profile()
|
|
291
|
+
profile.set_shape(network.get_input(0).name, opt_shapes, opt_shapes, opt_shapes)
|
|
292
|
+
config.add_optimization_profile(profile)
|
|
293
|
+
engine = builder.build_serialized_network(network, config)
|
|
294
|
+
if engine is None:
|
|
295
|
+
log = logger.get_log()
|
|
296
|
+
msg = "vs_undistort: Internal Error: TensorRT failed while building the TensorRT engine."
|
|
297
|
+
if log:
|
|
298
|
+
msg += f"\n=== TensorRT log ===\n{log}"
|
|
299
|
+
raise RuntimeError(msg)
|
|
300
|
+
|
|
301
|
+
# save engine
|
|
302
|
+
with open(engine_path, "wb") as f:
|
|
303
|
+
f.write(engine)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _get_engine(onnx_path, engine_dir, temp_window, engine_w, engine_h, interpolation, force_rebuild=False) -> str:
|
|
307
|
+
# get path to tensorrt engine
|
|
308
|
+
os.makedirs(engine_dir, exist_ok=True) # create engine folder if needed
|
|
309
|
+
engine_name = f"undistort_{interpolation}_t{temp_window}_h{engine_h}_w{engine_w}_fp16.engine"
|
|
310
|
+
engine_path = os.path.join(engine_dir, engine_name)
|
|
311
|
+
|
|
312
|
+
# check plugin version
|
|
313
|
+
try:
|
|
314
|
+
info = core.trt.Version()
|
|
315
|
+
except Exception as e:
|
|
316
|
+
raise RuntimeError("vs_undistort: Please install a version of vs-mlrt with TensorRT support or choose a different backend.") from e
|
|
317
|
+
|
|
318
|
+
# if engine file exist, return it
|
|
319
|
+
if not force_rebuild and os.path.isfile(engine_path) and os.path.getsize(engine_path) >= 512:
|
|
320
|
+
return engine_path
|
|
321
|
+
|
|
322
|
+
# get plugin info
|
|
323
|
+
plugin_path = os.path.dirname(info["path"].decode(errors="ignore"))
|
|
324
|
+
trt_version = int(info["tensorrt_version"].decode(errors="ignore"))
|
|
325
|
+
trt_version = [trt_version // 10000, (trt_version % 10000) // 100, trt_version % 100]
|
|
326
|
+
cuda_major = int(info["cuda_runtime_version"].decode(errors="ignore")) // 1000
|
|
327
|
+
|
|
328
|
+
# build new engine
|
|
329
|
+
logging.warning("vs_undistort: Building new TensorRT engine for interpolation='%s' with temp_window=%d, width=%d, and height=%d. This may take a few minutes.", interpolation, temp_window, engine_w, engine_h)
|
|
330
|
+
builder_info = _get_builder(plugin_path=plugin_path, trt_version=trt_version, cuda_major=cuda_major)
|
|
331
|
+
if builder_info[0] == "python":
|
|
332
|
+
_build_engine_python(onnx_path=onnx_path, engine_path=engine_path, temp_window=temp_window, engine_w=engine_w, engine_h=engine_h, interpolation=interpolation, trt_package=builder_info[1])
|
|
333
|
+
elif builder_info[0] == "trtexec":
|
|
334
|
+
_build_engine_trtexec(onnx_path=onnx_path, engine_path=engine_path, temp_window=temp_window, engine_w=engine_w, engine_h=engine_h, interpolation=interpolation, trt_version=trt_version, trtexec_path=builder_info[1])
|
|
335
|
+
else:
|
|
336
|
+
raise RuntimeError(f"vs_undistort: Internal Error: Unknown TensorRT engine builder: {builder_info[0]}")
|
|
337
|
+
logging.warning("vs_undistort: Engine building complete.")
|
|
338
|
+
return engine_path
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _tensorrt_inference(input_clips, onnx_path, engine_dir, temp_window, tile_w, tile_h, overlap, tiles, interpolation, num_streams, flex_out_prop, force_rebuild=False):
|
|
342
|
+
engine_path = _get_engine(onnx_path=onnx_path, engine_dir=engine_dir, temp_window=temp_window, engine_w=tile_w, engine_h=tile_h, interpolation=interpolation, force_rebuild=force_rebuild)
|
|
343
|
+
model_args = dict(engine_path=engine_path, num_streams=num_streams, flexible_output_prop=flex_out_prop, **(dict(tilesize=(tile_w, tile_h), overlap=(overlap, overlap)) if tiles > 1 else {}))
|
|
344
|
+
|
|
345
|
+
# try inference, rebuild engine if it fails
|
|
346
|
+
try:
|
|
347
|
+
out = core.trt.Model(input_clips, **model_args)
|
|
348
|
+
except vs.Error as e:
|
|
349
|
+
err_msg = str(e).lower()
|
|
350
|
+
serialization_keywords = ("serialize", "serialization", "deserialize", "deserialization")
|
|
351
|
+
if any(k in err_msg for k in serialization_keywords) and not force_rebuild:
|
|
352
|
+
logging.warning("vs_undistort: Engine loading failed. This may be due to a TensorRT or driver update. Rebuilding...")
|
|
353
|
+
model_args["engine_path"] = _get_engine(onnx_path=onnx_path, engine_dir=engine_dir, temp_window=temp_window, engine_w=tile_w, engine_h=tile_h, interpolation=interpolation, force_rebuild=True)
|
|
354
|
+
out = core.trt.Model(input_clips, **model_args)
|
|
355
|
+
else:
|
|
356
|
+
raise
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _tensorrt(clip, temp_window=10, tiles=1, overlap=8, interpolation="bicubic", num_streams=1, engine_folder=None):
|
|
361
|
+
|
|
362
|
+
# checks
|
|
363
|
+
if not isinstance(clip, vs.VideoNode):
|
|
364
|
+
raise TypeError("vs_undistort: Clip must be a vapoursynth clip.")
|
|
365
|
+
if clip.format.id == vs.PresetVideoFormat.NONE or clip.width == 0 or clip.height == 0:
|
|
366
|
+
raise TypeError("vs_undistort: Clip must have constant format and dimensions.")
|
|
367
|
+
if clip.format.id not in [vs.RGBH]:
|
|
368
|
+
raise ValueError("vs_undistort: Clip must be in RGBH format for the TensorRT backend.")
|
|
369
|
+
if not isinstance(temp_window, int) or isinstance(temp_window, bool):
|
|
370
|
+
raise TypeError("vs_undistort: Temporal window length must be an integer.")
|
|
371
|
+
if temp_window < 2:
|
|
372
|
+
raise ValueError("vs_undistort: Temporal window length must be at least 2.")
|
|
373
|
+
if not isinstance(tiles, int) or isinstance(tiles, bool):
|
|
374
|
+
raise TypeError("vs_undistort: Tiles must be an integer.")
|
|
375
|
+
if not isinstance(overlap, int) or isinstance(overlap, bool):
|
|
376
|
+
raise TypeError("vs_undistort: Overlap must be an integer.")
|
|
377
|
+
if overlap < 0:
|
|
378
|
+
raise ValueError("vs_undistort: Overlap can not be negative.")
|
|
379
|
+
if not isinstance(num_streams, int) or isinstance(num_streams, bool):
|
|
380
|
+
raise TypeError("vs_undistort: Number of parallel TensorRT streams (num_streams) must be an integer.")
|
|
381
|
+
if num_streams < 1:
|
|
382
|
+
raise ValueError("vs_undistort: Number of parallel TensorRT streams (num_streams) must be at least 1.")
|
|
383
|
+
|
|
384
|
+
# clamp
|
|
385
|
+
clip = expression(clip, expr=["x 0 max 1 min"])
|
|
386
|
+
|
|
387
|
+
# decide which dimensions to build the engine for
|
|
388
|
+
tile_w, tile_h = get_tiles(clip_w=clip.width, clip_h=clip.height, tiles=tiles, overlap=overlap)
|
|
389
|
+
|
|
390
|
+
# pad if tile is larger than clip
|
|
391
|
+
pad_w = max(0, tile_w - clip.width)
|
|
392
|
+
pad_h = max(0, tile_h - clip.height)
|
|
393
|
+
if pad_w or pad_h:
|
|
394
|
+
clip = core.std.AddBorders(clip, right=pad_w, bottom=pad_h)
|
|
395
|
+
|
|
396
|
+
if tile_w < 128 or tile_h < 128:
|
|
397
|
+
raise ValueError("vs_undistort: Tile size must be at least 128 x 128. Reduce tiles.")
|
|
398
|
+
if tiles > 1 and overlap > min(tile_w, tile_h) // 2:
|
|
399
|
+
raise ValueError("vs_undistort: Overlap can not be larger than half of tile size. Reduce overlap.")
|
|
400
|
+
|
|
401
|
+
# make sure the extremely slow engine isn't build due to some tensorrt tactic limitations
|
|
402
|
+
cur_pixels = temp_window * tile_w * tile_h
|
|
403
|
+
max_pixels = 4194304 #2^22
|
|
404
|
+
if cur_pixels >= max_pixels:
|
|
405
|
+
raise ValueError(f"vs_undistort: temp_window * tile width * tile height must be smaller than {max_pixels} (currently {cur_pixels}). Increase tiles or reduce temp_window or overlap.")
|
|
406
|
+
|
|
407
|
+
if interpolation == "bilinear":
|
|
408
|
+
model_name = "UNet3d_TMT_lin_op19_fp16.onnx"
|
|
409
|
+
elif interpolation == "bicubic":
|
|
410
|
+
model_name = "UNet3d_TMT_cub_op19_fp16.onnx"
|
|
411
|
+
else:
|
|
412
|
+
raise ValueError("vs_undistort: Warp interpolation mode must be 'bilinear' or 'bicubic'.")
|
|
413
|
+
|
|
414
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
415
|
+
engine_dir = os.path.join(current_dir, "engines") if engine_folder is None else os.path.abspath(engine_folder)
|
|
416
|
+
onnx_path = os.path.join(current_dir, "models", model_name)
|
|
417
|
+
flex_out_prop = "vs_undistort_output"
|
|
418
|
+
force_rebuild = False
|
|
419
|
+
|
|
420
|
+
# get inference window and do inference
|
|
421
|
+
input_clips = get_window(clip, temp_window)
|
|
422
|
+
stacked_clips = _tensorrt_inference(
|
|
423
|
+
input_clips=input_clips,
|
|
424
|
+
onnx_path=onnx_path,
|
|
425
|
+
engine_dir=engine_dir,
|
|
426
|
+
temp_window=temp_window,
|
|
427
|
+
tile_w=tile_w,
|
|
428
|
+
tile_h=tile_h,
|
|
429
|
+
overlap=overlap,
|
|
430
|
+
tiles=tiles,
|
|
431
|
+
interpolation=interpolation,
|
|
432
|
+
num_streams=num_streams,
|
|
433
|
+
flex_out_prop=flex_out_prop,
|
|
434
|
+
force_rebuild=force_rebuild,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# turn stacked output into normal clip
|
|
438
|
+
carrier_clip = stacked_clips["clip"]
|
|
439
|
+
num_planes = stacked_clips["num_planes"]
|
|
440
|
+
planes = [carrier_clip.std.PropToClip(prop=f"{flex_out_prop}{i}") for i in range(num_planes)] # turn planes back into normal clips
|
|
441
|
+
grouped_clips = [core.std.ShufflePlanes(planes[i:i+3], [0, 0, 0], vs.RGB) for i in range(0, num_planes, 3)] # group every 3 planes back into RGB clips
|
|
442
|
+
unstacked_clip = core.std.Interleave(grouped_clips) # interleave to restore chronological order
|
|
443
|
+
|
|
444
|
+
if pad_w or pad_h:
|
|
445
|
+
unstacked_clip = core.std.Crop(unstacked_clip, right=pad_w, bottom=pad_h)
|
|
446
|
+
if unstacked_clip.num_frames != clip.num_frames:
|
|
447
|
+
unstacked_clip = core.std.Trim(unstacked_clip, last=clip.num_frames - 1)
|
|
448
|
+
|
|
449
|
+
return core.std.CopyFrameProps(unstacked_clip, clip)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def vs_undistort(clip, temp_window=10, tiles=1, overlap=8, interpolation="bicubic", backend="tensorrt", num_streams=1, engine_folder=None):
|
|
453
|
+
"""Removes distortions. Also known as atmospheric turbulence mitigation, warp stabilization, film shrink or VHS distortion fix, heat haze removal.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
clip: Distorted clip. Must be in RGB format.
|
|
457
|
+
temp_window: Temporal window length. This is how many frames are grouped together and processed as a single chunk. Larger means
|
|
458
|
+
higher VRAM requirements, but better temporal averaging and slower distortions can be removed. If this is too small,
|
|
459
|
+
some distortions may not get removed, small jumps/hitches may be visible between windows and seams from tiling
|
|
460
|
+
may become more obvious.
|
|
461
|
+
tiles: Amount of tiles to split the frames into. A higher amount reduces VRAM requirements, but also worsens spatial averaging.
|
|
462
|
+
Default `tiles=1` uses the full frame.
|
|
463
|
+
overlap: Overlap from one tile to the next. Use if seams between tiles are visible.
|
|
464
|
+
interpolation: Interpolation mode used to warp the frames.
|
|
465
|
+
- `bilinear` = More blurry.
|
|
466
|
+
- `bicubic` = No blur, but may oversharpen slightly.
|
|
467
|
+
backend: The backend used to run the model.
|
|
468
|
+
- `cpu` = CPU mode using PyTorch (very slow).
|
|
469
|
+
- `cuda` = GPU mode using PyTorch with CUDA support. Requires any Nvidia GPU (fast).
|
|
470
|
+
- `tensorrt` = GPU mode using vs-mlrt with TensorRT support. Requires an Nvidia RTX GPU (very fast and lower vram usage).
|
|
471
|
+
num_streams: Number of parallel TensorRT streams. For high end GPUs higher can be a bit faster, but requires more VRAM. Only affects the TensorRT backend.
|
|
472
|
+
engine_folder: Optional path to the TensorRT engine storage location. By default engines are stored in `vs_undistort/engines`. Only affects the TensorRT backend.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
if backend in ["cpu", "cuda"]:
|
|
476
|
+
return _pytorch(clip, temp_window=temp_window, tiles=tiles, overlap=overlap, interpolation=interpolation, device=backend)
|
|
477
|
+
if backend in ["tensorrt", "trt"]:
|
|
478
|
+
return _tensorrt(clip, temp_window=temp_window, tiles=tiles, overlap=overlap, interpolation=interpolation, num_streams=num_streams, engine_folder=engine_folder)
|
|
479
|
+
raise ValueError("vs_undistort: Backend must be 'cpu', 'cuda', or 'tensorrt'.")
|