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.
@@ -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"
@@ -0,0 +1,9 @@
1
+ import sys
2
+ from types import ModuleType
3
+ from .vs_undistort import vs_undistort
4
+
5
+ class _CallableModule(ModuleType):
6
+ __call__ = staticmethod(vs_undistort)
7
+
8
+ sys.modules[__name__].__class__ = _CallableModule
9
+ __all__ = ["vs_undistort"]
@@ -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
+
@@ -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'.")