vsflip 1.0.1__py3-none-any.whl
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.
- vsflip-1.0.1.dist-info/METADATA +90 -0
- vsflip-1.0.1.dist-info/RECORD +6 -0
- vsflip-1.0.1.dist-info/WHEEL +5 -0
- vsflip-1.0.1.dist-info/licenses/LICENSE +21 -0
- vsflip-1.0.1.dist-info/top_level.txt +1 -0
- vsflip.py +214 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vsflip
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A VapourSynth wrapper for the FLIP perceptual metric
|
|
5
|
+
Author-email: PingWer <r74michele@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 PingWer
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.13
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: numpy
|
|
32
|
+
Requires-Dist: matplotlib
|
|
33
|
+
Requires-Dist: flip_evaluator
|
|
34
|
+
Requires-Dist: vapoursynth
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# vsflip
|
|
38
|
+
|
|
39
|
+
**vsflip** is a Python wrapper for **VapourSynt** that enables perceptual video comparison using the [FLIP metric](https://github.com/NVlabs/flip) (Fidelity Loss Image Processing). It computes frame-by-frame error maps that quantify visual differences between a reference and a test video.
|
|
40
|
+
|
|
41
|
+
This wrapper integrates the `flip` Python library and works entirely within VapourSynth pipelines.
|
|
42
|
+
|
|
43
|
+
## 📦 Features
|
|
44
|
+
|
|
45
|
+
- Frame-by-frame FLIP comparison between two `VideoNode`.
|
|
46
|
+
- Outputs GrayscaleS error maps where darker pixels indicate better perceptual similarity.
|
|
47
|
+
- Supports both `LDR` and `HDR` evaluation modes ().
|
|
48
|
+
- Optionally exports error maps as images (`.png`).
|
|
49
|
+
|
|
50
|
+
## 🛠 Requirements
|
|
51
|
+
|
|
52
|
+
- Python ≥ 3.13
|
|
53
|
+
- [VapourSynth](https://www.vapoursynth.com/) R71 or newer
|
|
54
|
+
- [NumPy](https://numpy.org/)
|
|
55
|
+
- [Matplotlib](https://matplotlib.org/)
|
|
56
|
+
- [flip](https://github.com/NVlabs/flip)
|
|
57
|
+
|
|
58
|
+
## 🚀 Installation
|
|
59
|
+
|
|
60
|
+
You can install `vsflip` via pip:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install vsflip
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## ✏️ Basic Usage
|
|
67
|
+
```python
|
|
68
|
+
from vsflip import vsflip_frame, vsflip_video
|
|
69
|
+
|
|
70
|
+
# Compare the first frame of two clips
|
|
71
|
+
result = vsflip_frame(ref_clip, test_clip)
|
|
72
|
+
|
|
73
|
+
# Compare all frames (must be aligned and same length)
|
|
74
|
+
result = vsflip_video(ref_clip, test_clip)
|
|
75
|
+
```
|
|
76
|
+
- Both functions return a VideoNode in grayscale (GRAYS format)
|
|
77
|
+
|
|
78
|
+
- Refer to the Python docstrings for detailed parameter descriptions and options.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
This project is licensed under the **MIT License**. See the [LICENSE](./LICENSE) file for details.
|
|
82
|
+
|
|
83
|
+
### Third-party Dependencies
|
|
84
|
+
This project uses the [FLIP metric](https://github.com/NVlabs/flip) python wrapper by NVIDIA, which is licensed under the BSD 3-Clause License.
|
|
85
|
+
|
|
86
|
+
The flip library's source code or binaries are **not bundled** with this project.
|
|
87
|
+
It is installed separately as a required runtime dependency.
|
|
88
|
+
|
|
89
|
+
## 📬 Contact
|
|
90
|
+
For questions, bugs, or feature requests, feel free to open an issue or pull request.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
vsflip.py,sha256=hruhkbPwgQ7bdBTuQ4UjmbkikmshlmIxvnsQLvALRDs,7839
|
|
2
|
+
vsflip-1.0.1.dist-info/licenses/LICENSE,sha256=8fC0iEVo529xntZ3jNX833XuusyRcbOOu_lDOInjqDU,1064
|
|
3
|
+
vsflip-1.0.1.dist-info/METADATA,sha256=5u9h1jWtLG3SBmG_XcoNmotoXx9kPWfkoJv8jWO3DzI,3529
|
|
4
|
+
vsflip-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
vsflip-1.0.1.dist-info/top_level.txt,sha256=U4HstSPijwk8bINMXTp01dRh7lGdqAC5pr9lbYYq3sc,7
|
|
6
|
+
vsflip-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PingWer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vsflip
|
vsflip.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import vapoursynth as vs
|
|
3
|
+
except ImportError:
|
|
4
|
+
raise ImportError('Vapoursynth R71> is required')
|
|
5
|
+
|
|
6
|
+
import flip_evaluator as flip
|
|
7
|
+
from matplotlib.pyplot import imsave
|
|
8
|
+
import numpy as np
|
|
9
|
+
from weakref import WeakValueDictionary
|
|
10
|
+
|
|
11
|
+
core = vs.core
|
|
12
|
+
|
|
13
|
+
FLIPDICT = {
|
|
14
|
+
"vc": [0.5, 3840, 0.6],
|
|
15
|
+
"tonemapper": "ACES"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_f2c_cache = WeakValueDictionary[int, vs.VideoNode]()
|
|
19
|
+
|
|
20
|
+
def frame2clip(frame: vs.VideoFrame) -> vs.VideoNode:
|
|
21
|
+
"""
|
|
22
|
+
Original code from [vsjetpack](https://github.com/Jaded-Encoding-Thaumaturgy/vs-jetpack/blob/main/vstools/functions/utils.py#L369)
|
|
23
|
+
|
|
24
|
+
Convert a VideoFrame to a VideoNode.
|
|
25
|
+
|
|
26
|
+
:param frame: Input frame.
|
|
27
|
+
|
|
28
|
+
:return: 1-frame long VideoNode of the input frame.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
key = hash((frame.width, frame.height, frame.format.id))
|
|
32
|
+
|
|
33
|
+
if _f2c_cache.get(key, None) is None:
|
|
34
|
+
_f2c_cache[key] = blank_clip = vs.core.std.BlankClip(
|
|
35
|
+
None, frame.width, frame.height,
|
|
36
|
+
frame.format.id, 1, 1, 1,
|
|
37
|
+
[0] * frame.format.num_planes,
|
|
38
|
+
True
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
blank_clip = _f2c_cache[key]
|
|
42
|
+
|
|
43
|
+
frame_cp = frame.copy()
|
|
44
|
+
|
|
45
|
+
return vs.core.std.ModifyFrame(blank_clip, blank_clip, lambda n, f: frame_cp)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def frame_to_numpyArray(frame: vs.VideoNode) -> np.ndarray:
|
|
49
|
+
"""
|
|
50
|
+
Convert a VapourSynth 1-frame long VideoNode to a NumPy array.
|
|
51
|
+
|
|
52
|
+
:param frame: The VapourSynth 1-frame long VideoNode to convert.
|
|
53
|
+
:return: A NumPy array representation of the frame.
|
|
54
|
+
"""
|
|
55
|
+
frame = frame.get_frame(0)
|
|
56
|
+
|
|
57
|
+
return np.stack([np.asarray(frame[i]) for i in range(frame.format.num_planes)], axis=-1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def numpy_to_frame(np_array: np.ndarray, blank_clip1 : vs.VideoNode) -> vs.VideoFrame:
|
|
61
|
+
"""
|
|
62
|
+
Convert a 2D NumPy array (float32) to VapourSynth VideoFrame in GRAYS.
|
|
63
|
+
"""
|
|
64
|
+
assert np_array.ndim == 2 and np_array.dtype == np.float32, "Input must be a 2D NumPy array of type float32."
|
|
65
|
+
|
|
66
|
+
if blank_clip1 is None:
|
|
67
|
+
height, width = np_array.shape
|
|
68
|
+
blank_clip1 = core.std.BlankClip(format=vs.GRAYS, width=width, height=height, length=1)
|
|
69
|
+
|
|
70
|
+
frame = blank_clip1.get_frame(0).copy()
|
|
71
|
+
|
|
72
|
+
np.copyto(np.asarray(frame[0]), np_array)
|
|
73
|
+
|
|
74
|
+
return frame
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def vsflip_frame (
|
|
78
|
+
ref_clip:vs.VideoNode,
|
|
79
|
+
test_clip:vs.VideoNode,
|
|
80
|
+
range:str="LDR",
|
|
81
|
+
ref_frame: int = 0,
|
|
82
|
+
test_frame: int = 0,
|
|
83
|
+
parameters: dict = FLIPDICT,
|
|
84
|
+
save_flip_error_mask: bool = False,
|
|
85
|
+
debug: bool = False,
|
|
86
|
+
blank: vs.VideoNode = None
|
|
87
|
+
)-> vs.VideoNode:
|
|
88
|
+
"""
|
|
89
|
+
Compare two frames using the FLIP metric. Darker values indicate a better match.
|
|
90
|
+
|
|
91
|
+
:param ref_clip: The reference VapourSynth VideoNode.
|
|
92
|
+
:param test_clip: The test VapourSynth VideoNode.
|
|
93
|
+
:param range: The range of the video, either "LDR" or "HDR".
|
|
94
|
+
:param ref_frame: The frame number of the reference clip to compare. Default is 0.
|
|
95
|
+
:param test_frame: The frame number of the test clip to compare. Default is 0.
|
|
96
|
+
:param parameters: A dictionary of parameters for the FLIP evaluation. Default is {"vc": [0.5, 3840, 0.6], "tonemapper": "ACES"}).
|
|
97
|
+
:param save_flip_error_mask: If True, saves the FLIP error mask as png in the script folder. Default is False.
|
|
98
|
+
:param debug: If True, prints debug information. Default is False.
|
|
99
|
+
:param blank: Only for devoloper use.
|
|
100
|
+
:return: A VapourSynth 1-frame long VideoNode containing the FLIP error map in GrayScaleS.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
if range not in ["LDR", "HDR"]:
|
|
104
|
+
raise ValueError("Range must be either 'LDR' or 'HDR'.")
|
|
105
|
+
|
|
106
|
+
frame_test=test_clip[test_frame]
|
|
107
|
+
frame_ref=ref_clip[ref_frame]
|
|
108
|
+
|
|
109
|
+
if debug:
|
|
110
|
+
print("Reference Frame Properties:\n")
|
|
111
|
+
print(frame_ref)
|
|
112
|
+
print("\nTest Frame Properties:\n")
|
|
113
|
+
print(frame_test)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if frame_ref.format.id != vs.RGBS or frame_test.format.id != vs.RGBS:
|
|
117
|
+
frame_ref= core.resize.Bicubic(frame_ref, format=vs.RGBS)
|
|
118
|
+
frame_test = core.resize.Bicubic(frame_test, format=vs.RGBS)
|
|
119
|
+
|
|
120
|
+
np_ref = frame_to_numpyArray(frame_ref)
|
|
121
|
+
np_test = frame_to_numpyArray(frame_test)
|
|
122
|
+
|
|
123
|
+
flipErrorMap, meanFLIPError, parameters = flip.evaluate(np_ref, np_test, range, applyMagma=False, parameters=parameters)
|
|
124
|
+
flipErrorMap = np.squeeze(flipErrorMap)
|
|
125
|
+
|
|
126
|
+
if debug:
|
|
127
|
+
print("Mean FLIP error: ", round(meanFLIPError, 6), "\n")
|
|
128
|
+
|
|
129
|
+
print("The following parameters were used:")
|
|
130
|
+
for key in parameters:
|
|
131
|
+
val = parameters[key]
|
|
132
|
+
if isinstance(val, float):
|
|
133
|
+
val = round(val, 4)
|
|
134
|
+
print("\t%s: %s" % (key, str(val)))
|
|
135
|
+
|
|
136
|
+
if save_flip_error_mask:
|
|
137
|
+
imsave(f"vsflip_error_map_{ref_frame}_{test_frame}.png", flipErrorMap, cmap="gray")
|
|
138
|
+
|
|
139
|
+
if debug:
|
|
140
|
+
print("numpy array max and min values (correct value should be between 1-0):")
|
|
141
|
+
print(flipErrorMap.max(), flipErrorMap.min())
|
|
142
|
+
|
|
143
|
+
frame= numpy_to_frame(flipErrorMap, blank_clip1=blank)
|
|
144
|
+
|
|
145
|
+
# Add meanFLIPError to frame properties
|
|
146
|
+
frame.props["meanFLIPError"] = float(meanFLIPError)
|
|
147
|
+
|
|
148
|
+
return frame2clip(frame)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"Error processing frame {ref_frame} and {test_frame}: {e}")
|
|
152
|
+
return blank # Return a blank frame in case of error
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def vsflip_video(
|
|
156
|
+
ref_clip: vs.VideoNode,
|
|
157
|
+
test_clip: vs.VideoNode,
|
|
158
|
+
range: str = "LDR",
|
|
159
|
+
parameters: dict = FLIPDICT,
|
|
160
|
+
debug: bool = False,
|
|
161
|
+
allignment_to_ref: int = 0
|
|
162
|
+
) -> vs.VideoNode:
|
|
163
|
+
"""
|
|
164
|
+
Compare two videos using the FLIP metric frame by frame. Darker values indicate a better match.
|
|
165
|
+
Both clips must have the same number of frames or at least be perfectly aligned.
|
|
166
|
+
|
|
167
|
+
:param ref_clip: The reference VapourSynth VideoNode.
|
|
168
|
+
:param test_clip: The test VapourSynth VideoNode.
|
|
169
|
+
:param range: The range of the video, either "LDR" or "HDR".
|
|
170
|
+
:param parameters: A dictionary of parameters for the FLIP evaluation. Default is {"vc": [0.5, 3840, 0.6], "tonemapper": "ACES"}).
|
|
171
|
+
:param debug: If True, prints debug information. Default is False.
|
|
172
|
+
:param allignment_to_ref: The number of frames to align the test clip to the reference clip. Default is 0.
|
|
173
|
+
Ensure both clip are perfectly aligned, because using this offset could lead to error.
|
|
174
|
+
:return: A VapourSynth VideoNode containing the FLIP error map for each frame in GrayScaleS.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
# just in case the user is tard
|
|
178
|
+
min_length = min(ref_clip.num_frames, test_clip.num_frames - allignment_to_ref)
|
|
179
|
+
|
|
180
|
+
blank = core.std.BlankClip(
|
|
181
|
+
format=vs.GRAYS,
|
|
182
|
+
width=ref_clip.width,
|
|
183
|
+
height=ref_clip.height,
|
|
184
|
+
length=min_length,
|
|
185
|
+
fpsnum=ref_clip.fps_num,
|
|
186
|
+
fpsden=ref_clip.fps_den
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
blank1 = core.std.BlankClip(
|
|
190
|
+
format=vs.GRAYS,
|
|
191
|
+
width=ref_clip.width,
|
|
192
|
+
height=ref_clip.height,
|
|
193
|
+
length=1,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def select_flip(n: int) -> vs.VideoNode:
|
|
197
|
+
try:
|
|
198
|
+
return vsflip_frame(
|
|
199
|
+
ref_clip,
|
|
200
|
+
test_clip,
|
|
201
|
+
range=range,
|
|
202
|
+
ref_frame=n,
|
|
203
|
+
test_frame=n+allignment_to_ref,
|
|
204
|
+
parameters=parameters,
|
|
205
|
+
save_flip_error_mask=False,
|
|
206
|
+
debug=debug,
|
|
207
|
+
blank=blank1
|
|
208
|
+
)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"Error processing frame {n}: {e}")
|
|
211
|
+
return blank1 # Return a blank frame in case of error
|
|
212
|
+
|
|
213
|
+
return core.std.FrameEval(clip=blank, eval=select_flip)
|
|
214
|
+
|