soifunc 0.12.0__py3-none-any.whl → 0.14.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.
soifunc/deband.py CHANGED
@@ -2,7 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from vsdeband import f3k_deband
4
4
  from vsmasktools import dre_edgemask
5
- from vstools import InvalidVideoFormatError, check_variable, core, vs
5
+ from vstools import (
6
+ UnsupportedVideoFormatError,
7
+ VariableFormatError,
8
+ check_variable,
9
+ core,
10
+ vs,
11
+ )
6
12
 
7
13
  __all__ = [
8
14
  "retinex_deband",
@@ -14,33 +20,45 @@ def retinex_deband(
14
20
  threshold: int,
15
21
  showmask: bool = False,
16
22
  ) -> vs.VideoNode:
23
+ """Debanding using contrast-adaptive edge masking.
24
+
25
+ Args:
26
+ clip: Input video (8-16bit YUV required).
27
+ threshold: Debanding strength (0-255). Default ~16-48 recommended.
28
+ showmask: If True, return edge mask instead of debanded clip.
29
+
30
+ Returns:
31
+ Debanded video clip or edge mask.
32
+
33
+ Note:
34
+ Does not add grain. Use vsdeband.AddNoise for post-denoising.
17
35
  """
18
- Debanding using a contrast-adaptive edge mask to preserve details
19
- even in dark areas.
20
-
21
- "medium" `threshold` in f3kdb is 48. I think that's a bit strong.
22
- 16 might be a more sane starting point. Increase as needed.
23
-
24
- This function does not add grain on its own. Use another function like
25
- `vsdeband.AddNoise` to do that.
26
- """
27
- assert check_variable(clip, retinex_deband)
36
+ if threshold < 0 or threshold > 255:
37
+ raise ValueError(f"threshold must be between 0-255, got {threshold}")
38
+
39
+ if not check_variable(clip, retinex_deband):
40
+ raise VariableFormatError("clip must have constant format and fps")
28
41
 
29
42
  if (
30
43
  clip.format.color_family != vs.YUV
31
44
  or clip.format.sample_type != vs.INTEGER
32
45
  or clip.format.bits_per_sample > 16
33
46
  ):
34
- raise InvalidVideoFormatError(
47
+ raise UnsupportedVideoFormatError(
35
48
  retinex_deband,
36
49
  clip.format,
37
50
  "The format {format.name} is not supported! It must be an 8-16bit integer YUV bit format!",
38
51
  )
39
52
 
40
- mask = dre_edgemask.CLAHE(clip)
53
+ mask: vs.VideoNode = dre_edgemask.CLAHE(clip)
41
54
 
42
55
  if showmask:
43
56
  return mask
44
57
 
58
+ # The threshold value that `retinex_deband` takes is relative
59
+ # to 8-bit videos, but `f3kdb` changed their threshold
60
+ # values to be relative to 10-bit videos some time after this
61
+ # function was created. To keep this function compatible,
62
+ # we shift our threshold from 8-bit to 10-bit.
45
63
  deband = f3k_deband(clip, thr=(threshold << 2))
46
64
  return core.std.MaskedMerge(deband, clip, mask)
soifunc/denoise.py CHANGED
@@ -3,17 +3,18 @@ from __future__ import annotations
3
3
  from typing import Callable, Optional
4
4
 
5
5
  import vsdenoise
6
- from vsdenoise import DFTTest, bm3d, mc_degrain
7
- from vstools import core, vs
6
+ from vsdenoise import DFTTest, bm3d, mc_degrain, nl_means
7
+ from vstools import check_variable, core, join, split, vs
8
8
 
9
- __all__ = ["MCDenoise", "magic_denoise", "hqbm3d", "mc_dfttest"]
9
+ __all__ = ["MCDenoise", "magic_denoise", "hqbm3d", "mc_dfttest", "Stab"]
10
10
 
11
11
 
12
12
  def hqbm3d(
13
13
  clip: vs.VideoNode,
14
- luma_str: float = 0.45,
14
+ luma_str: float = 0.5,
15
15
  chroma_str: float = 0.4,
16
16
  profile: bm3d.Profile = bm3d.Profile.FAST,
17
+ tr: int = 1,
17
18
  ) -> vs.VideoNode:
18
19
  """
19
20
  High-quality presets for motion compensated denoising.
@@ -39,12 +40,24 @@ def hqbm3d(
39
40
  ),
40
41
  planes=None,
41
42
  )
42
- out = bm3d(clip, sigma=luma_str, tr=1, ref=mv, profile=profile, planes=0)
43
- return vsdenoise.nl_means(out, h=chroma_str, tr=1, ref=mv, planes=[1, 2])
43
+ [y, u, v] = split(clip)
44
+ [y_mv, u_mv, v_mv] = split(mv)
45
+ out_y = bm3d(y, sigma=luma_str, tr=tr, ref=y_mv, profile=profile)
46
+ if not hasattr(core, "nlm_cuda") and hasattr(core, "knlm"):
47
+ # the KNLMeansCL would force extra depth conversions
48
+ # and re-processing of the luma plane, so avoid it.
49
+ nlm_backend = nl_means.Backend.ISPC
50
+ else:
51
+ nlm_backend = nl_means.Backend.AUTO
52
+ out_u = nl_means(u, h=chroma_str, tr=tr, ref=u_mv, backend=nlm_backend)
53
+ out_v = nl_means(v, h=chroma_str, tr=tr, ref=v_mv, backend=nlm_backend)
54
+ return join(out_y, out_u, out_v, prop_src=clip)
44
55
 
45
56
 
46
57
  def mc_dfttest(
47
- clip: vs.VideoNode, thSAD: int = 75, noisy: bool = False
58
+ clip: vs.VideoNode,
59
+ thSAD: int = 75,
60
+ tr: int = 2,
48
61
  ) -> vs.VideoNode:
49
62
  """
50
63
  A motion-compensated denoiser using DFTTEST.
@@ -52,19 +65,14 @@ def mc_dfttest(
52
65
  Turn it up to 150 or more if you really need to nuke something.
53
66
  It does a decent job at preserving details, but not nearly as good
54
67
  as bm3d, so this is not recommended on clean, high-quality sources.
55
-
56
- The `noisy` parameter did help preserve more detail on high-quality but grainy sources.
57
- Currently it is deprecated, as the presets in `vsdenoise` changed,
58
- but it may be un-deprecated in the future.
59
68
  """
60
- # TODO: Do we need to tweak anything for the `noisy` param?
61
69
  blksize = select_block_size(clip)
62
70
  return mc_degrain(
63
71
  clip,
64
72
  prefilter=vsdenoise.Prefilter.DFTTEST,
65
73
  preset=vsdenoise.MVToolsPreset.HQ_SAD,
66
74
  thsad=thSAD,
67
- tr=2,
75
+ tr=tr,
68
76
  refine=3 if blksize > 16 else 2,
69
77
  blksize=blksize,
70
78
  )
@@ -210,3 +218,47 @@ def magic_denoise(clip: vs.VideoNode) -> vs.VideoNode:
210
218
  tbsize=3,
211
219
  ssystem=1,
212
220
  )
221
+
222
+
223
+ ##############################################################################
224
+ # Original script by g-force converted into a stand alone script by McCauley #
225
+ # then copied from havsfunc when they deleted it #
226
+ # latest version from December 10, 2008 #
227
+ ##############################################################################
228
+ def Stab(clp, dxmax=4, dymax=4, mirror=0):
229
+ if not isinstance(clp, vs.VideoNode):
230
+ raise vs.Error("Stab: this is not a clip")
231
+
232
+ clp = scdetect(clp, 25 / 255)
233
+ temp = clp.misc.AverageFrames([1] * 15, scenechange=True)
234
+ inter = core.std.Interleave(
235
+ [
236
+ core.rgvs.Repair(
237
+ temp, clp.misc.AverageFrames([1] * 3, scenechange=True), mode=[1]
238
+ ),
239
+ clp,
240
+ ]
241
+ )
242
+ mdata = inter.mv.DepanEstimate(trust=0, dxmax=dxmax, dymax=dymax)
243
+ last = inter.mv.DepanCompensate(data=mdata, offset=-1, mirror=mirror)
244
+ return last[::2]
245
+
246
+
247
+ def scdetect(clip: vs.VideoNode, threshold: float = 0.1) -> vs.VideoNode:
248
+ def _copy_property(n: int, f: list[vs.VideoFrame]) -> vs.VideoFrame:
249
+ fout = f[0].copy()
250
+ fout.props["_SceneChangePrev"] = f[1].props["_SceneChangePrev"]
251
+ fout.props["_SceneChangeNext"] = f[1].props["_SceneChangeNext"]
252
+ return fout
253
+
254
+ assert check_variable(clip, scdetect)
255
+
256
+ sc = clip
257
+ if clip.format.color_family == vs.RGB:
258
+ sc = clip.resize.Point(format=vs.GRAY8, matrix_s="709")
259
+
260
+ sc = sc.misc.SCDetect(threshold)
261
+ if clip.format.color_family == vs.RGB:
262
+ sc = clip.std.ModifyFrame([clip, sc], _copy_property)
263
+
264
+ return sc
soifunc/interpolate.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import platform
4
3
  from typing import TYPE_CHECKING
5
4
 
6
5
  import vstools
@@ -10,7 +9,7 @@ from vstools import vs
10
9
  if TYPE_CHECKING:
11
10
  from vsmlrt import backendT
12
11
 
13
- __all__ = ["rate_doubler", "decimation_fixer"]
12
+ __all__ = ["rate_doubler", "decimation_fixer", "replace_dupes"]
14
13
 
15
14
 
16
15
  def rate_doubler(
@@ -26,6 +25,7 @@ def rate_doubler(
26
25
 
27
26
  width = clip.width
28
27
  height = clip.height
28
+ format = clip.format
29
29
  matrix = vstools.Matrix.from_video(clip)
30
30
  transfer = vstools.Transfer.from_video(clip)
31
31
  primaries = vstools.Primaries.from_video(clip)
@@ -45,7 +45,7 @@ def rate_doubler(
45
45
  )
46
46
  # TODO: Handle other chroma samplings
47
47
  clip = clip.resize.Bicubic(
48
- format=vs.YUV420P16,
48
+ format=format,
49
49
  width=width,
50
50
  height=height,
51
51
  matrix=matrix,
@@ -55,6 +55,117 @@ def rate_doubler(
55
55
  return clip
56
56
 
57
57
 
58
+ def replace_dupes(
59
+ clip: vs.VideoNode,
60
+ max_length: int = 5,
61
+ backend: backendT | None = None,
62
+ threshold: float = 0.001,
63
+ ) -> vs.VideoNode:
64
+ """
65
+ Detects strings of duplicate frames in a video and replaces them
66
+ with interpolated frames from RIFE.
67
+
68
+ Max number of continuous duplicates to detect is determined by the `max_length` parameter.
69
+ `threshold` is the maximum average pixel difference (0-1 scale) to consider frames as duplicates.
70
+ Lower values are stricter (frames must be more similar to be considered duplicates).
71
+ """
72
+ import vsmlrt
73
+
74
+ # Store original properties
75
+ width = clip.width
76
+ height = clip.height
77
+ format = clip.format
78
+ matrix = vstools.Matrix.from_video(clip)
79
+ transfer = vstools.Transfer.from_video(clip)
80
+ primaries = vstools.Primaries.from_video(clip)
81
+
82
+ # Compute frame differences using PlaneStats
83
+ # This compares each frame with the previous one
84
+ diff_clip = clip.std.PlaneStats(clip[0] + clip)
85
+
86
+ # Prepare clip for RIFE (convert to RGBS and resize to multiple of 64)
87
+ rife_clip = clip.resize.Bicubic(
88
+ format=vs.RGBS,
89
+ width=next_multiple_of(64, width),
90
+ height=next_multiple_of(64, height),
91
+ )
92
+
93
+ # Create interpolated frames using RIFE (double the framerate)
94
+ interpolated = vsmlrt.RIFE(
95
+ rife_clip,
96
+ multi=2,
97
+ model=vsmlrt.RIFEModel.v4_25_heavy,
98
+ backend=(backend if backend else autoselect_backend()),
99
+ )
100
+
101
+ # Convert interpolated frames back to original format
102
+ interpolated = interpolated.resize.Bicubic(
103
+ format=format,
104
+ width=width,
105
+ height=height,
106
+ matrix=matrix,
107
+ transfer=transfer,
108
+ primaries=primaries,
109
+ )
110
+
111
+ # Track sequence state for lazy evaluation
112
+ state = {"prev_len": 0}
113
+
114
+ def select_frame(n):
115
+ """
116
+ Select interpolated frame if current frame is a duplicate,
117
+ otherwise use original. Copies PlaneStatsDiff property to output
118
+ to help users calibrate the threshold parameter.
119
+ """
120
+ if n == 0 or n == clip.num_frames - 1:
121
+ state["prev_len"] = 0
122
+ # Frame 0 and final frame are never duplicates
123
+ # (no previous frame for 0, no next frame for final)
124
+ output = clip[n : n + 1]
125
+ diff_val = (
126
+ 0.0
127
+ if n == 0
128
+ else diff_clip.get_frame(n).props.get("PlaneStatsDiff", 1.0)
129
+ )
130
+ return output.std.SetFrameProp(prop="PlaneStatsDiff", floatval=diff_val)
131
+
132
+ # Get difference from PlaneStats (lazy evaluation)
133
+ f = diff_clip.get_frame(n)
134
+ diff = f.props.get("PlaneStatsDiff", 1.0)
135
+
136
+ # Determine if this is a duplicate
137
+ if diff < threshold:
138
+ new_len = state["prev_len"] + 1
139
+ if new_len <= max_length:
140
+ state["prev_len"] = new_len
141
+ is_dupe = True
142
+ else:
143
+ state["prev_len"] = 0
144
+ is_dupe = False
145
+ else:
146
+ state["prev_len"] = 0
147
+ is_dupe = False
148
+
149
+ if is_dupe:
150
+ # Use interpolated frame between previous and current
151
+ # If the original sequence is 0 1 2 where 0 and 1 are dupes,
152
+ # the interpolated sequence will have 0 1 2 3 4 5
153
+ # where 3 is the interpolated frame we want to fetch
154
+ # to replace frame 1..
155
+
156
+ output = interpolated[n * 2 + 1 : n * 2 + 2]
157
+ else:
158
+ output = clip[n : n + 1]
159
+
160
+ # Attach PlaneStatsDiff property to output frame for threshold calibration
161
+ return output.std.SetFrameProp(prop="PlaneStatsDiff", floatval=diff)
162
+
163
+ # Apply frame selection with lazy evaluation
164
+ result = clip.std.FrameEval(select_frame)
165
+
166
+ return result
167
+
168
+
58
169
  def decimation_fixer(
59
170
  clip: vs.VideoNode, cycle: int, offset: int = 0, backend: backendT | None = None
60
171
  ) -> vs.VideoNode:
@@ -78,6 +189,7 @@ def decimation_fixer(
78
189
  width = clip.width
79
190
  height = clip.height
80
191
  fps = clip.fps
192
+ format = clip.format
81
193
  input_cycle = cycle - 1
82
194
  matrix = vstools.Matrix.from_video(clip)
83
195
  transfer = vstools.Transfer.from_video(clip)
@@ -115,9 +227,8 @@ def decimation_fixer(
115
227
  fpsnum=fps.numerator * cycle // input_cycle, fpsden=fps.denominator
116
228
  )
117
229
 
118
- # TODO: Handle other chroma samplings
119
230
  out_clip = out_clip.resize.Bicubic(
120
- format=vs.YUV420P16,
231
+ format=format,
121
232
  width=width,
122
233
  height=height,
123
234
  matrix=matrix,
soifunc/resize.py CHANGED
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from vsaa.deinterlacers import NNEDI3
4
- from vskernels import (
5
- Hermite,
6
- Spline36,
7
- )
4
+ from vskernels import Hermite, LeftShift, Spline36, TopShift
8
5
  from vsscale import ArtCNN
9
- from vstools import check_variable_format, is_gpu_available, join, vs
6
+ from vstools import (
7
+ VariableFormatError,
8
+ check_variable_format,
9
+ is_gpu_available,
10
+ join,
11
+ vs,
12
+ )
10
13
 
11
14
  __all__ = [
12
15
  "good_resize",
@@ -17,7 +20,7 @@ def good_resize(
17
20
  clip: vs.VideoNode,
18
21
  width: int,
19
22
  height: int,
20
- shift: tuple[float, float] = (0, 0),
23
+ shift: tuple[TopShift | list[TopShift], LeftShift | list[LeftShift]] = (0, 0),
21
24
  gpu: bool | None = None,
22
25
  anime: bool = False,
23
26
  ) -> vs.VideoNode:
@@ -55,10 +58,12 @@ def good_resize(
55
58
  else:
56
59
  luma_scaler = Hermite(sigmoid=True)
57
60
 
58
- assert check_variable_format(clip, "good_resize")
61
+ if not check_variable_format(clip, "good_resize"):
62
+ raise VariableFormatError("Invalid clip format for good_resize")
59
63
 
60
64
  luma = luma_scaler.scale(clip, width, height, shift)
61
65
 
66
+ # Grayscale doesn't need chroma processing
62
67
  if clip.format.num_planes == 1:
63
68
  return luma
64
69
 
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: soifunc
3
- Version: 0.12.0
3
+ Version: 0.14.1
4
4
  Summary: Soichiro's VapourSynth Functions Collection
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: Josh Holmer
7
8
  Author-email: jholmer.in@gmail.com
8
9
  Requires-Python: >=3.12,<4.0
@@ -10,8 +11,9 @@ Classifier: License :: OSI Approved :: MIT License
10
11
  Classifier: Programming Language :: Python :: 3
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Requires-Dist: vapoursynth (>=68)
14
- Requires-Dist: vsjetpack (>=0.7.1,<0.8.0)
16
+ Requires-Dist: vsjetpack (>=1.0.0,<2.0.0)
15
17
  Description-Content-Type: text/markdown
16
18
 
17
19
  ## soifunc
@@ -0,0 +1,10 @@
1
+ soifunc/__init__.py,sha256=H6BWoCLRW2ZD47wQtL72SIZvTpD6REH7cUqIYWvCn0k,174
2
+ soifunc/deband.py,sha256=8mT1FqKAteUAs1XJYi8WvmlcJtyQN2OxM5mfiujTKX4,1887
3
+ soifunc/denoise.py,sha256=pWmGbFU5IRU0AAPi56a57NNBq-zaNUd7IG-nfdCgTeA,8897
4
+ soifunc/interpolate.py,sha256=2Agg1N4GG9h6lkl7soy7G--ahVk3msbDfKGuRbj2Syo,7878
5
+ soifunc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ soifunc/resize.py,sha256=sOJpkovIEgaKvABEJTLfvYfKj6zvGf9pFRQMoWb7XdY,1998
7
+ soifunc-0.14.1.dist-info/METADATA,sha256=xVcBWO_TJ36Fx2Mc60QwRH3XFeKMVYjhPM1NxWZZ1Gc,1287
8
+ soifunc-0.14.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
9
+ soifunc-0.14.1.dist-info/licenses/LICENSE,sha256=vgEDSMEV1J2nMiCgXE5_sjNtw2VT7_lP7rkAnrFKOWI,1068
10
+ soifunc-0.14.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +0,0 @@
1
- soifunc/__init__.py,sha256=H6BWoCLRW2ZD47wQtL72SIZvTpD6REH7cUqIYWvCn0k,174
2
- soifunc/deband.py,sha256=MbI5OOG8JpWqM5B5ux6OU784-39PtCMQWrh9mk4k2xY,1273
3
- soifunc/denoise.py,sha256=ALTftqL9Q1cPLKtAU60mT3-RUkVHXBqgg2Pa9GwjGIY,6982
4
- soifunc/interpolate.py,sha256=dgYczCdn716afJ3sZgQgcF6KfAkaU2J_obJD-Kp7wP4,4091
5
- soifunc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- soifunc/resize.py,sha256=pSoQtrsxfWr44Wg4M9GRyMJnOsgnjsWEr6ZyoJW7Qkk,1781
7
- soifunc-0.12.0.dist-info/LICENSE,sha256=vgEDSMEV1J2nMiCgXE5_sjNtw2VT7_lP7rkAnrFKOWI,1068
8
- soifunc-0.12.0.dist-info/METADATA,sha256=cIZ9ZFeGwNCWcyiyKTStP51OvnaG7pLWRX3fTcv0vzo,1214
9
- soifunc-0.12.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
- soifunc-0.12.0.dist-info/RECORD,,