vsdirty 0.1.0__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.
- vsdirty/__init__.py +8 -0
- vsdirty/adfunc.py +554 -0
- vsdirty/admask.py +480 -0
- vsdirty/adutils.py +208 -0
- vsdirty/dirtyfixer.py +113 -0
- vsdirty-0.1.0.dist-info/METADATA +174 -0
- vsdirty-0.1.0.dist-info/RECORD +10 -0
- vsdirty-0.1.0.dist-info/WHEEL +5 -0
- vsdirty-0.1.0.dist-info/licenses/LICENSE +21 -0
- vsdirty-0.1.0.dist-info/top_level.txt +1 -0
vsdirty/admask.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import vapoursynth as vs
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
core = vs.core
|
|
5
|
+
|
|
6
|
+
if not (hasattr(vs.core, 'cas') or hasattr(vs.core, 'fmtc') or hasattr(vs.core, 'akarin')):
|
|
7
|
+
raise ImportError("'cas', 'fmtc' and 'akarin' are mandatory. Make sure the DLLs are present in the plugins folder.")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_stdev(avg: float, sq_avg: float) -> float:
|
|
11
|
+
return abs(sq_avg - avg ** 2) ** 0.5
|
|
12
|
+
|
|
13
|
+
#fatta dalla IA e anche male, ma fa il suo la cambierò in futuro forse
|
|
14
|
+
def _soft_threshold(clip: vs.VideoNode, thr: float, steepness: float = 20.0) -> vs.VideoNode:
|
|
15
|
+
"""
|
|
16
|
+
Applies a soft threshold to the clip.
|
|
17
|
+
Values below thr accept a penalty that decays exponentially based on the distance from thr.
|
|
18
|
+
formula: x < thr ? x * exp((x - thr) * steepness) : x
|
|
19
|
+
"""
|
|
20
|
+
from .adutils import scale_binary_value
|
|
21
|
+
|
|
22
|
+
core = vs.core
|
|
23
|
+
|
|
24
|
+
thr_scaled = scale_binary_value(clip, thr, return_int=False)
|
|
25
|
+
|
|
26
|
+
if clip.format.sample_type == vs.INTEGER:
|
|
27
|
+
max_val = (1 << clip.format.bits_per_sample) - 1
|
|
28
|
+
diff_expr = f"x {thr_scaled} - {max_val} /"
|
|
29
|
+
else:
|
|
30
|
+
diff_expr = f"x {thr_scaled} -"
|
|
31
|
+
|
|
32
|
+
return core.akarin.Expr(
|
|
33
|
+
[clip],
|
|
34
|
+
f"x {thr_scaled} >= x "
|
|
35
|
+
f"{diff_expr} {steepness} * exp x * "
|
|
36
|
+
f"?"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def auto_thr_high(stddev):
|
|
40
|
+
if stddev >0.900:
|
|
41
|
+
stdev_min, stdev_max = 0.800, 1.000
|
|
42
|
+
else:
|
|
43
|
+
stdev_min, stdev_max = 0.400, 1.000
|
|
44
|
+
thr_high_min, thr_high_max = 0.005, 1.000
|
|
45
|
+
norm = min(max((stddev - stdev_min) / (stdev_max - stdev_min), 0.000), 1.000)
|
|
46
|
+
return (thr_high_max * ((thr_high_min / thr_high_max) ** norm))
|
|
47
|
+
|
|
48
|
+
def luma_mask (
|
|
49
|
+
clip: vs.VideoNode,
|
|
50
|
+
min_value: float = 17500,
|
|
51
|
+
sthmax: float = 0.95,
|
|
52
|
+
sthmin: float = 1.4,
|
|
53
|
+
)-> vs.VideoNode :
|
|
54
|
+
from .adutils import plane
|
|
55
|
+
|
|
56
|
+
luma = plane(clip, 0)
|
|
57
|
+
lumamask = core.std.Expr(
|
|
58
|
+
[luma],
|
|
59
|
+
"x {0} < x {2} * x {0} - 0.0001 + log {1} * exp x + ?".format(min_value, sthmax, sthmin)
|
|
60
|
+
)
|
|
61
|
+
lumamask = lumamask.std.Invert()
|
|
62
|
+
|
|
63
|
+
return lumamask
|
|
64
|
+
|
|
65
|
+
def luma_mask_man (
|
|
66
|
+
clip: vs.VideoNode,
|
|
67
|
+
t: float = 0.3,
|
|
68
|
+
s: float = 5,
|
|
69
|
+
a: float = 0.3,
|
|
70
|
+
)-> vs.VideoNode :
|
|
71
|
+
"""
|
|
72
|
+
Custom luma mask that uses a different approach to calculate the mask (Made By Mhanz).
|
|
73
|
+
This mask is sensitive to the brightness of the image producing a smooth transition between dark and bright areas of th clip based on brightness levels.
|
|
74
|
+
The mask exalt bright areas and darkens dark areas, inverting them.
|
|
75
|
+
|
|
76
|
+
Curve graph https://www.geogebra.org/calculator/cqnfnqyk
|
|
77
|
+
|
|
78
|
+
:param clip: Clip to process (only the first plane will be processed).
|
|
79
|
+
:param s:
|
|
80
|
+
:param t: Threshold that determines what is considered light and what is dark.
|
|
81
|
+
:param a:
|
|
82
|
+
:return: Luma mask.
|
|
83
|
+
"""
|
|
84
|
+
from .adutils import plane
|
|
85
|
+
|
|
86
|
+
luma = plane(clip, 0)
|
|
87
|
+
f=1/3
|
|
88
|
+
|
|
89
|
+
maxvalue = (1 << clip.format.bits_per_sample) - 1
|
|
90
|
+
normx = f"x 2 * {maxvalue} / "
|
|
91
|
+
|
|
92
|
+
lumamask = core.std.Expr(
|
|
93
|
+
[luma],
|
|
94
|
+
f"x " # Mettiamo x sullo stack per la moltiplicazione finale
|
|
95
|
+
f"{normx} {t} < " # x < b ?
|
|
96
|
+
# - Ramo TRUE → f(x):
|
|
97
|
+
f"{normx} {t} - {normx} {t} - 2 pow {s} * {a} + {f} pow / 1 + "
|
|
98
|
+
# - Ramo FALSE → h(x):
|
|
99
|
+
f"{normx} {t} - {normx} {t} - 2 pow {s} * 5 * {a} + {f} pow / 1 + "
|
|
100
|
+
# - Operatore ternario:
|
|
101
|
+
f"? "
|
|
102
|
+
# - moltiplica per x:
|
|
103
|
+
f"*"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
lumamask = lumamask.std.Invert()
|
|
107
|
+
|
|
108
|
+
return lumamask
|
|
109
|
+
|
|
110
|
+
def luma_mask_ping(
|
|
111
|
+
clip: vs.VideoNode,
|
|
112
|
+
low_amp: float = 0.8,
|
|
113
|
+
thr: float = 0.196,
|
|
114
|
+
) -> vs.VideoNode:
|
|
115
|
+
"""
|
|
116
|
+
Custom luma mask that uses a different approach to calculate the mask (Made By PingWer).
|
|
117
|
+
This mask is sensitive to the brightness of the image, producing a constant dark mask for bright areas,
|
|
118
|
+
a constant white mask for very dark areas, and a exponential transition between these extremes based on brightness levels.
|
|
119
|
+
|
|
120
|
+
Curve graph https://www.geogebra.org/calculator/fxbrx4s4
|
|
121
|
+
|
|
122
|
+
:param clip: Clip to process (only the first plane will be processed).
|
|
123
|
+
:param low_amp: General preamplification value, but more sensitive for values lower than thr.
|
|
124
|
+
:param thr: Threshold that determines what is considered bright and what is dark.
|
|
125
|
+
:return: Luma mask.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
core = vs.core
|
|
129
|
+
import math
|
|
130
|
+
from .adutils import scale_binary_value, plane
|
|
131
|
+
|
|
132
|
+
bit_depth = clip.format.bits_per_sample
|
|
133
|
+
max_val = (1 << bit_depth) - 1
|
|
134
|
+
|
|
135
|
+
thr_scaled = scale_binary_value(clip, value=thr, bit=bit_depth, return_int=False)
|
|
136
|
+
|
|
137
|
+
high_amp = (math.exp(low_amp - 1) + low_amp * math.exp(low_amp)) / (math.exp(low_amp) - 1)
|
|
138
|
+
|
|
139
|
+
expr = (
|
|
140
|
+
f"x {max_val} / "
|
|
141
|
+
f"dup {thr_scaled} < "
|
|
142
|
+
f"{thr_scaled} 1 + - exp {low_amp} + "
|
|
143
|
+
f"{high_amp} {high_amp} dup {thr_scaled} 1 - - log {high_amp} * exp {low_amp} + / - "
|
|
144
|
+
f"? "
|
|
145
|
+
f"x *"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
cc = core.akarin.Expr([plane(clip, 0)], expr)
|
|
149
|
+
|
|
150
|
+
cc = core.std.Invert(cc)
|
|
151
|
+
|
|
152
|
+
return cc
|
|
153
|
+
|
|
154
|
+
def unbloat_retinex(
|
|
155
|
+
clip: vs.VideoNode,
|
|
156
|
+
sigma: list[float] = [25, 80, 250],
|
|
157
|
+
lower_thr: float = 0.001,
|
|
158
|
+
upper_thr: float = 0.005,
|
|
159
|
+
fast: bool = True
|
|
160
|
+
) -> vs.VideoNode:
|
|
161
|
+
"""
|
|
162
|
+
Multi-Scale Retinex (MSR) optimized for edge enhancement and dynamic range compression.
|
|
163
|
+
|
|
164
|
+
Uses Gaussian blur (via vsrgtools.gauss_blur) with optional downscaling optimization
|
|
165
|
+
for large sigma values. The output is a normalized [0, 1] Float32 grayscale clip
|
|
166
|
+
suitable for edge detection or as a preprocessing step for masking.
|
|
167
|
+
|
|
168
|
+
:param clip: Input clip. Must be Grayscale (GRAY format).
|
|
169
|
+
:param sigma: List of sigma values for multi-scale blur. Default: [25, 80, 250].
|
|
170
|
+
Larger values capture coarser illumination variations.
|
|
171
|
+
:param lower_thr: Quantile threshold for black level (0-1). Default: 0.001 (0.1%).
|
|
172
|
+
Used to ignore dark outliers during final normalization.
|
|
173
|
+
:param upper_thr: Quantile threshold for white level (0-1). Default: 0.001 (0.1%).
|
|
174
|
+
Used to ignore bright outliers during final normalization.
|
|
175
|
+
:param fast: Enable downscaling optimization for large sigmas. Default: True.
|
|
176
|
+
Significantly faster with minimal quality loss.
|
|
177
|
+
:return: Processed Float32 Grayscale clip with enhanced local contrast.
|
|
178
|
+
|
|
179
|
+
:raises ValueError: If input clip is not Grayscale.
|
|
180
|
+
"""
|
|
181
|
+
from vstools import depth
|
|
182
|
+
from vsrgtools import gauss_blur
|
|
183
|
+
|
|
184
|
+
if clip.format.color_family != vs.GRAY:
|
|
185
|
+
raise ValueError("unbloat_retinex: Input must be a Grayscale clip.")
|
|
186
|
+
|
|
187
|
+
if clip.format.sample_type != vs.FLOAT:
|
|
188
|
+
luma = depth(clip, 32)
|
|
189
|
+
else:
|
|
190
|
+
luma = clip
|
|
191
|
+
|
|
192
|
+
stats = luma.std.PlaneStats()
|
|
193
|
+
luma_norm = core.akarin.Expr([luma, stats], "x y.PlaneStatsMin - y.PlaneStatsMax y.PlaneStatsMin - 0.000001 max /")
|
|
194
|
+
|
|
195
|
+
sigmas_sorted = sorted(sigma)
|
|
196
|
+
sigmas_to_blur = sigmas_sorted[:-1] if fast else sigmas_sorted
|
|
197
|
+
|
|
198
|
+
blurs = []
|
|
199
|
+
w, h = luma_norm.width, luma_norm.height
|
|
200
|
+
|
|
201
|
+
for s in sigmas_to_blur:
|
|
202
|
+
if fast and s > 6:
|
|
203
|
+
ds_ratio = max(1, s / 3)
|
|
204
|
+
ds_w, ds_h = max(1, int(w / ds_ratio)), max(1, int(h / ds_ratio))
|
|
205
|
+
|
|
206
|
+
if ds_ratio > 2:
|
|
207
|
+
down = luma_norm.resize.Bicubic(ds_w, ds_h)
|
|
208
|
+
|
|
209
|
+
s_down = s / ds_ratio
|
|
210
|
+
blurred_down = gauss_blur(down, sigma=s_down)
|
|
211
|
+
|
|
212
|
+
blurred = blurred_down.resize.Bicubic(w, h)
|
|
213
|
+
else:
|
|
214
|
+
blurred = gauss_blur(luma_norm, sigma=s)
|
|
215
|
+
else:
|
|
216
|
+
blurred = gauss_blur(luma_norm, sigma=s)
|
|
217
|
+
blurs.append(blurred)
|
|
218
|
+
|
|
219
|
+
inputs = [luma_norm] + blurs
|
|
220
|
+
|
|
221
|
+
def get_char(i):
|
|
222
|
+
if i == 0: return 'x'
|
|
223
|
+
if i == 1: return 'y'
|
|
224
|
+
if i == 2: return 'z'
|
|
225
|
+
if i == 3: return 'a'
|
|
226
|
+
return chr(ord('a') + (i - 3))
|
|
227
|
+
|
|
228
|
+
terms = []
|
|
229
|
+
for i in range(1, len(inputs)):
|
|
230
|
+
c = get_char(i)
|
|
231
|
+
terms.append(f"{c} 0 <= 1 x {c} / 1 + ?")
|
|
232
|
+
|
|
233
|
+
if fast:
|
|
234
|
+
terms.append("x 1 +")
|
|
235
|
+
|
|
236
|
+
expr_code = " ".join(terms)
|
|
237
|
+
if len(terms) > 1:
|
|
238
|
+
expr_code += " " + " ".join(["+"] * (len(terms) - 1))
|
|
239
|
+
|
|
240
|
+
slen = len(sigma)
|
|
241
|
+
expr_code += f" log {slen} /"
|
|
242
|
+
|
|
243
|
+
msr = core.akarin.Expr(inputs, expr_code)
|
|
244
|
+
|
|
245
|
+
if hasattr(core, 'vszip') and (lower_thr > 0 or upper_thr > 0):
|
|
246
|
+
msr_stats = core.vszip.PlaneMinMax(msr, lower_thr, upper_thr)
|
|
247
|
+
min_key, max_key = 'psmMin', 'psmMax'
|
|
248
|
+
else:
|
|
249
|
+
msr_stats = msr.std.PlaneStats()
|
|
250
|
+
min_key, max_key = 'PlaneStatsMin', 'PlaneStatsMax'
|
|
251
|
+
|
|
252
|
+
balanced = core.akarin.Expr([msr, msr_stats], f"x y.{min_key} - y.{max_key} y.{min_key} - 0.000001 max /")
|
|
253
|
+
|
|
254
|
+
return balanced
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def advanced_edgemask(
|
|
258
|
+
clip: vs.VideoNode,
|
|
259
|
+
ref: Optional[vs.VideoNode] = None,
|
|
260
|
+
sigma1: float = 3,
|
|
261
|
+
retinex_sigma: list[float] = [50, 200, 350],
|
|
262
|
+
sigma2: float = 1,
|
|
263
|
+
sharpness: float = 0.8,
|
|
264
|
+
kirsch_weight: float = 0.5,
|
|
265
|
+
kirsch_thr: float = 0.35,
|
|
266
|
+
edge_thr: float = 0.02,
|
|
267
|
+
**kwargs
|
|
268
|
+
) -> vs.VideoNode:
|
|
269
|
+
"""
|
|
270
|
+
Advanced edge mask combining Retinex preprocessing with multiple edge detectors.
|
|
271
|
+
|
|
272
|
+
This mask uses BM3D denoising + Multi-Scale Retinex to enhance edges before detection,
|
|
273
|
+
then combines Sobel, Prewitt, TCanny and Kirsch edge detectors for robust edge detection.
|
|
274
|
+
|
|
275
|
+
:param clip: Clip to process (YUV or Gray).
|
|
276
|
+
:param ref: Optional reference clip for denoising.
|
|
277
|
+
:param sigma1: BM3D sigma for initial denoising. Default: 3.
|
|
278
|
+
:param retinex_sigma: Sigma values for Multi-Scale Retinex. Default: [50, 200, 350].
|
|
279
|
+
:param sigma2: Nlmeans strength for post-Retinex denoising. Default: 1.
|
|
280
|
+
:param sharpness: CAS sharpening amount (0-1). Default: 0.8.
|
|
281
|
+
:param kirsch_weight: Weight for Kirsch edges in final blend (0-1). Default: 0.7.
|
|
282
|
+
:param kirsch_thr: Kirsch threshold. Default: 0.25.
|
|
283
|
+
:param edge_thr: Threshold for edge combination logic (0-1). Default: 0.02.
|
|
284
|
+
:param kwargs: Additional arguments for Retinex.
|
|
285
|
+
:return: Edge mask (Gray clip).
|
|
286
|
+
"""
|
|
287
|
+
from vstools import get_y, depth
|
|
288
|
+
from vsdenoise import nl_means
|
|
289
|
+
from vsmasktools import Morpho, Kirsch
|
|
290
|
+
from .adfunc import mini_BM3D
|
|
291
|
+
from .adutils import scale_binary_value
|
|
292
|
+
|
|
293
|
+
core = vs.core
|
|
294
|
+
|
|
295
|
+
if clip.format.color_family == vs.RGB:
|
|
296
|
+
raise ValueError("advanced_edgemask: RGB clips are not supported.")
|
|
297
|
+
|
|
298
|
+
if clip.format.color_family == vs.GRAY:
|
|
299
|
+
luma = clip
|
|
300
|
+
else:
|
|
301
|
+
luma = get_y(clip)
|
|
302
|
+
|
|
303
|
+
luma = depth(luma, 16)
|
|
304
|
+
|
|
305
|
+
if ref is not None:
|
|
306
|
+
if ref.format.color_family == vs.RGB:
|
|
307
|
+
raise ValueError("advanced_edgemask: RGB reference clips are not supported.")
|
|
308
|
+
|
|
309
|
+
if ref.format.color_family == vs.GRAY:
|
|
310
|
+
ref_y = ref
|
|
311
|
+
else:
|
|
312
|
+
ref_y = get_y(ref)
|
|
313
|
+
|
|
314
|
+
if ref_y.format.bits_per_sample != 16:
|
|
315
|
+
ref_y = depth(ref_y, 16)
|
|
316
|
+
clipd = mini_BM3D(luma, sigma=sigma1, ref=ref_y, radius=1, profile="HIGH", planes=0)
|
|
317
|
+
else:
|
|
318
|
+
clipd = mini_BM3D(luma, sigma=sigma1, radius=1, profile="HIGH", planes=0)
|
|
319
|
+
|
|
320
|
+
msrcpa = depth(unbloat_retinex(
|
|
321
|
+
depth(clipd, 32),
|
|
322
|
+
sigma=retinex_sigma,
|
|
323
|
+
fast=True,
|
|
324
|
+
**kwargs
|
|
325
|
+
), 16, dither_type="none")
|
|
326
|
+
|
|
327
|
+
msrcp = nl_means(msrcpa, h=sigma2, a=2)
|
|
328
|
+
|
|
329
|
+
if sharpness > 0:
|
|
330
|
+
msrcp = core.cas.CAS(msrcp, sharpness=sharpness, opt=0, planes=0)
|
|
331
|
+
clipd = core.cas.CAS(clipd, sharpness=sharpness, opt=0, planes=0)
|
|
332
|
+
|
|
333
|
+
preSobel = core.akarin.Expr([
|
|
334
|
+
get_y(msrcp).std.Sobel(),
|
|
335
|
+
get_y(clipd).std.Sobel(),
|
|
336
|
+
], "x y max")
|
|
337
|
+
|
|
338
|
+
prePrewitt = core.akarin.Expr([
|
|
339
|
+
get_y(msrcp).std.Prewitt(),
|
|
340
|
+
get_y(clipd).std.Prewitt(),
|
|
341
|
+
], "x y max")
|
|
342
|
+
|
|
343
|
+
edges = core.akarin.Expr([preSobel, prePrewitt], "x y +")
|
|
344
|
+
|
|
345
|
+
tcanny = core.akarin.Expr([
|
|
346
|
+
core.tcanny.TCanny(get_y(msrcp), mode=1, sigma=0),
|
|
347
|
+
core.tcanny.TCanny(get_y(clipd), mode=1, sigma=0)
|
|
348
|
+
], "x y max")
|
|
349
|
+
|
|
350
|
+
kirco = core.akarin.Expr([
|
|
351
|
+
Kirsch.edgemask(get_y(msrcp), clamp=False, lthr=kirsch_thr),
|
|
352
|
+
Kirsch.edgemask(get_y(clipd), clamp=False, lthr=kirsch_thr)
|
|
353
|
+
], "x y max")
|
|
354
|
+
|
|
355
|
+
edge_thr_scaled = scale_binary_value(edges, edge_thr, return_int=True)
|
|
356
|
+
mask = core.akarin.Expr(
|
|
357
|
+
[edges, tcanny, kirco],
|
|
358
|
+
f"x y + {edge_thr_scaled} < x y + z {kirsch_weight} * + x y + ?"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return mask
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def hd_flatmask(
|
|
365
|
+
clip: vs.VideoNode,
|
|
366
|
+
ref: Optional[vs.VideoNode] = None,
|
|
367
|
+
sigma1: float = 3,
|
|
368
|
+
retinex_sigma: list[float] = [50, 200, 350],
|
|
369
|
+
sigma2: float = 1,
|
|
370
|
+
sharpness: float = 0.8,
|
|
371
|
+
edge_thr: float = 0.55,
|
|
372
|
+
texture_strength: float = 2,
|
|
373
|
+
edges_strength: float = 0.02,
|
|
374
|
+
blur: float = 2,
|
|
375
|
+
expand: int = 3,
|
|
376
|
+
**kwargs
|
|
377
|
+
) -> vs.VideoNode:
|
|
378
|
+
"""
|
|
379
|
+
Advanced edge mask combining Retinex preprocessing with multiple edge detectors.
|
|
380
|
+
|
|
381
|
+
This mask uses BM3D denoising + Multi-Scale Retinex to enhance edges before detection,
|
|
382
|
+
then combines Sobel, Prewitt, TCanny and Kirsch edge detectors for robust edge detection.
|
|
383
|
+
|
|
384
|
+
:param clip: Clip to process (YUV or Gray).
|
|
385
|
+
:param ref: Optional reference clip for denoising.
|
|
386
|
+
:param sigma1: BM3D sigma for initial denoising. Default: 3.
|
|
387
|
+
:param retinex_sigma: Sigma values for Multi-Scale Retinex. Default: [50, 200, 350].
|
|
388
|
+
:param sigma2: Nlmeans strength for post-Retinex denoising. Default: 1.
|
|
389
|
+
:param sharpness: CAS sharpening amount (0-1). Default: 0.8.
|
|
390
|
+
:param edge_thr: Threshold for edge combination logic (0-1). This allows to separate edges from texture. Default: 0.55.
|
|
391
|
+
:param texture_strength: Texture strength for mask (0-inf). Values above 1 decrese the strength of the texture in the mask, lower values increase it. The max value is theoretical infinite, but there is no gain after some point. Default: 0.8.
|
|
392
|
+
:param edges_strength: Edges strength for mask (0-1). Basic multiplier for edges strength. Default: 0.03.
|
|
393
|
+
:param blur: Blur amount for mask (0-1). Default: 2.
|
|
394
|
+
:param expand: Expand amount for mask (0-1). Higher value increases the size of the texture in the mask. Default: 3.
|
|
395
|
+
:param kwargs: Additional arguments for Retinex.
|
|
396
|
+
:return: Edge mask (Gray clip) where dark values are texture and edges, bright values are flat areas.
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
from vstools import depth
|
|
400
|
+
from vsdenoise import nl_means
|
|
401
|
+
from vsmasktools import Morpho, Kirsch, XxpandMode
|
|
402
|
+
from .adfunc import mini_BM3D
|
|
403
|
+
from .adutils import scale_binary_value, plane
|
|
404
|
+
from vsrgtools import gauss_blur
|
|
405
|
+
|
|
406
|
+
core = vs.core
|
|
407
|
+
|
|
408
|
+
if clip.format.color_family == vs.RGB:
|
|
409
|
+
raise ValueError("advanced_edgemask: RGB clips are not supported.")
|
|
410
|
+
|
|
411
|
+
if clip.format.color_family == vs.GRAY:
|
|
412
|
+
luma = clip
|
|
413
|
+
else:
|
|
414
|
+
luma = plane(clip, 0)
|
|
415
|
+
|
|
416
|
+
luma = depth(luma, 16)
|
|
417
|
+
|
|
418
|
+
if ref is not None:
|
|
419
|
+
if ref.format.color_family == vs.RGB:
|
|
420
|
+
raise ValueError("advanced_edgemask: RGB reference clips are not supported.")
|
|
421
|
+
|
|
422
|
+
if ref.format.color_family == vs.GRAY:
|
|
423
|
+
ref_y = ref
|
|
424
|
+
else:
|
|
425
|
+
ref_y = plane(ref, 0)
|
|
426
|
+
|
|
427
|
+
if ref_y.format.bits_per_sample != 16:
|
|
428
|
+
ref_y = depth(ref_y, 16)
|
|
429
|
+
clipd = mini_BM3D(luma, sigma=sigma1, ref=ref_y, radius=1, profile="HIGH", planes=0)
|
|
430
|
+
else:
|
|
431
|
+
clipd = mini_BM3D(luma, sigma=sigma1, radius=1, profile="HIGH", planes=0)
|
|
432
|
+
|
|
433
|
+
msrcpa = depth(unbloat_retinex(
|
|
434
|
+
depth(clipd, 32),
|
|
435
|
+
sigma=retinex_sigma,
|
|
436
|
+
fast=True,
|
|
437
|
+
**kwargs
|
|
438
|
+
), 16, dither_type="none")
|
|
439
|
+
|
|
440
|
+
msrcp = nl_means(msrcpa, h=sigma2, a=2)
|
|
441
|
+
|
|
442
|
+
if sharpness > 0:
|
|
443
|
+
msrcp = core.cas.CAS(msrcp, sharpness=sharpness, opt=0, planes=0)
|
|
444
|
+
clipd = core.cas.CAS(clipd, sharpness=sharpness, opt=0, planes=0)
|
|
445
|
+
|
|
446
|
+
edges = core.akarin.Expr([
|
|
447
|
+
msrcp.std.Sobel(),
|
|
448
|
+
clipd.std.Sobel(),
|
|
449
|
+
msrcp.std.Prewitt(),
|
|
450
|
+
clipd.std.Prewitt()
|
|
451
|
+
], "x y max z a max +")
|
|
452
|
+
|
|
453
|
+
if edge_thr > 0:
|
|
454
|
+
edges = _soft_threshold(edges, edge_thr, 10)
|
|
455
|
+
|
|
456
|
+
tcanny = core.akarin.Expr([
|
|
457
|
+
core.tcanny.TCanny(msrcp, mode=1, sigma=0),
|
|
458
|
+
core.tcanny.TCanny(clipd, mode=1, sigma=0)
|
|
459
|
+
], "x y max")
|
|
460
|
+
tcanny = core.std.Minimum(tcanny)
|
|
461
|
+
|
|
462
|
+
edgescombo = Morpho.inflate(core.akarin.Expr([edges, tcanny], "x y +"), iterations=2)
|
|
463
|
+
|
|
464
|
+
kirco = core.akarin.Expr([
|
|
465
|
+
Kirsch.edgemask(msrcp),
|
|
466
|
+
Kirsch.edgemask(clipd)
|
|
467
|
+
], "x y +")
|
|
468
|
+
|
|
469
|
+
edges_expanded = Morpho.expand(edgescombo, mode=XxpandMode.ELLIPSE, sw=1, sh=1)
|
|
470
|
+
kirco_diff = core.akarin.Expr([kirco, edges_expanded], "x y -")
|
|
471
|
+
kirco_expanded = Morpho.expand(kirco_diff, mode=XxpandMode.ELLIPSE, sw=expand, sh=expand)
|
|
472
|
+
|
|
473
|
+
edgescombo = core.akarin.Expr(edgescombo.std.Invert(), f"x {scale_binary_value(luma, edges_strength, return_int=True)} +")
|
|
474
|
+
kirco_expanded = luma_mask_man(kirco_expanded, t=0.001, s=texture_strength, a=0.5)
|
|
475
|
+
|
|
476
|
+
mask = core.akarin.Expr([edgescombo.std.Invert(), kirco_expanded.std.Invert()], "x y +")
|
|
477
|
+
|
|
478
|
+
mask = gauss_blur(mask, blur)
|
|
479
|
+
|
|
480
|
+
return mask
|
vsdirty/adutils.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import vapoursynth as vs
|
|
2
|
+
from typing import SupportsIndex, Optional, List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
def plane(
|
|
5
|
+
clip: vs.VideoNode,
|
|
6
|
+
index: SupportsIndex
|
|
7
|
+
) -> vs.VideoNode:
|
|
8
|
+
"""
|
|
9
|
+
Returns a plane from the given clip at the specified index.
|
|
10
|
+
|
|
11
|
+
:param clip: The input video clip.
|
|
12
|
+
:param index: The index of the plane to return.
|
|
13
|
+
:return: A new video clip containing only the specified plane.
|
|
14
|
+
"""
|
|
15
|
+
return vs.core.std.ShufflePlanes(clip, index.__index__(), vs.GRAY)
|
|
16
|
+
|
|
17
|
+
def scale_binary_value(
|
|
18
|
+
clip: vs.VideoNode,
|
|
19
|
+
value: float,
|
|
20
|
+
return_int: bool = True,
|
|
21
|
+
bit: Optional[int] = None,
|
|
22
|
+
)-> float:
|
|
23
|
+
"""
|
|
24
|
+
Scales a value based on the bit depth of the clip.
|
|
25
|
+
|
|
26
|
+
:param clip: Clip to process.
|
|
27
|
+
:param value: Value to scale (0.0 - 1.0).
|
|
28
|
+
:param return_int: Whether to return an integer value. Default is True (will be ignore if the input clip is Float).
|
|
29
|
+
:param bit: Bit depth of the clip. If None, the bit depth of the clip will be used. Default is None.
|
|
30
|
+
:return: Scaled value.
|
|
31
|
+
"""
|
|
32
|
+
if bit is None and clip is not None:
|
|
33
|
+
if clip.format is None:
|
|
34
|
+
raise ValueError("scale_binary_value: Clip must have a defined format.")
|
|
35
|
+
|
|
36
|
+
if clip.format.bits_per_sample is None:
|
|
37
|
+
raise ValueError("scale_binary_value: Clip must have a defined bit depth.")
|
|
38
|
+
|
|
39
|
+
bit = clip.format.bits_per_sample
|
|
40
|
+
|
|
41
|
+
if not (0.0 <= value <= 1.0):
|
|
42
|
+
raise ValueError("scale_binary_value: Value must be between 0.0 and 1.0.")
|
|
43
|
+
|
|
44
|
+
if clip.format.sample_type == vs.FLOAT or bit==32:
|
|
45
|
+
# For float clips, return the value as is
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
max_val = (1 << bit) - 1
|
|
49
|
+
|
|
50
|
+
if return_int:
|
|
51
|
+
return int(value * max_val)
|
|
52
|
+
else:
|
|
53
|
+
return value * max_val
|
|
54
|
+
|
|
55
|
+
def _detect_ranges(
|
|
56
|
+
diff: vs.VideoNode,
|
|
57
|
+
thr: float
|
|
58
|
+
) -> List[Tuple[int, int]]:
|
|
59
|
+
core = vs.core
|
|
60
|
+
stats = core.std.PlaneStats(diff)
|
|
61
|
+
|
|
62
|
+
indices = []
|
|
63
|
+
warned_missing = False
|
|
64
|
+
for n in range(diff.num_frames):
|
|
65
|
+
f = stats.get_frame(n)
|
|
66
|
+
pmax = f.props.get('PlaneStatsMax')
|
|
67
|
+
|
|
68
|
+
if pmax is None:
|
|
69
|
+
if not warned_missing:
|
|
70
|
+
print(f"Warning: PlaneStatsMax missing at frame {n}")
|
|
71
|
+
warned_missing = True
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if pmax > thr:
|
|
75
|
+
indices.append(n)
|
|
76
|
+
|
|
77
|
+
detected_ranges = []
|
|
78
|
+
if indices:
|
|
79
|
+
start = prev = indices[0]
|
|
80
|
+
for i in indices[1:]:
|
|
81
|
+
if i == prev + 1:
|
|
82
|
+
prev = i
|
|
83
|
+
else:
|
|
84
|
+
detected_ranges.append((start, prev))
|
|
85
|
+
start = prev = i
|
|
86
|
+
detected_ranges.append((start, prev))
|
|
87
|
+
|
|
88
|
+
return detected_ranges
|
|
89
|
+
|
|
90
|
+
def diff_and_swap(
|
|
91
|
+
clipa: vs.VideoNode,
|
|
92
|
+
clipb: vs.VideoNode,
|
|
93
|
+
thr: float = 35000.0,
|
|
94
|
+
discard_first: int = 0,
|
|
95
|
+
discard_last: int = 0,
|
|
96
|
+
ranges: Optional[Union[List[int], List[Tuple[int, int]]]] = None,
|
|
97
|
+
precise_swapping: bool = False
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
Compares two clips (clipa and clipb) frame by frame based on Luma.
|
|
101
|
+
Identifies ranges of frames where the maximum absolute difference exceeds a certain threshold.
|
|
102
|
+
|
|
103
|
+
This function is useful for "patching" a source (clipb) with frames from
|
|
104
|
+
another source (clipa) where differences are significant (e.g. artifacts,
|
|
105
|
+
corruption, or different versions).
|
|
106
|
+
|
|
107
|
+
:param clipa: The "correction" clip (e.g. WEB).
|
|
108
|
+
:param clipb: The "base" clip (e.g. BD).
|
|
109
|
+
:param thr: 16-bit threshold applied to PlaneStatsMax. Default 35000.
|
|
110
|
+
:param discard_first: Number of detected ranges to discard from the beginning (ignored if explicit ranges are used).
|
|
111
|
+
:param discard_last: Number of detected ranges to discard from the end (ignored if explicit ranges are used).
|
|
112
|
+
:param ranges: Manual range control.
|
|
113
|
+
- None: Detects ranges, prints the map and uses ALL of them.
|
|
114
|
+
- [-1]: Discards ALL ranges (returns original clipb, no calculation).
|
|
115
|
+
- [int, ...]: Detects ranges, prints the map and uses only those at the specified indices.
|
|
116
|
+
- [(start, end), ...]: Uses ONLY these explicit ranges, SKIPS DETECTION.
|
|
117
|
+
:param precise_swapping: If True, replaces only the differing pixels (diff > threshold -> expand -> blur -> mask).
|
|
118
|
+
If False (default), replaces the entire frame in the detected ranges.
|
|
119
|
+
|
|
120
|
+
:return: A tuple `(merged, selected)` where `merged` is clipb with replacements from clipa, and
|
|
121
|
+
`selected` is a clip containing only the frames from clipa used for replacement
|
|
122
|
+
(or None if no frames were swapped).
|
|
123
|
+
"""
|
|
124
|
+
from vstools import depth, replace_ranges
|
|
125
|
+
core = vs.core
|
|
126
|
+
|
|
127
|
+
if not isinstance(clipa, vs.VideoNode) or not isinstance(clipb, vs.VideoNode):
|
|
128
|
+
raise vs.Error("diff_and_swap: both inputs must be VideoNode")
|
|
129
|
+
|
|
130
|
+
if not isinstance(discard_first, int) or not isinstance(discard_last, int) or discard_first < 0 or discard_last < 0:
|
|
131
|
+
raise vs.Error('diff_and_swap: discard_first/discard_last must be non-negative integers')
|
|
132
|
+
|
|
133
|
+
if clipa.format.id != clipb.format.id:
|
|
134
|
+
raise vs.Error("diff_and_swap: both clips must have the same format")
|
|
135
|
+
|
|
136
|
+
if clipa.format.color_family != vs.YUV or clipb.format.color_family != vs.YUV:
|
|
137
|
+
raise vs.Error("diff_and_swap: only YUV clips are supported")
|
|
138
|
+
|
|
139
|
+
clipa = depth(clipa, 16)
|
|
140
|
+
clipb = depth(clipb, 16)
|
|
141
|
+
|
|
142
|
+
min_frames = min(clipa.num_frames, clipb.num_frames)
|
|
143
|
+
clipa_t = clipa.std.Trim(0, min_frames - 1) if clipa.num_frames != min_frames else clipa
|
|
144
|
+
clipb_t = clipb.std.Trim(0, min_frames - 1) if clipb.num_frames != min_frames else clipb
|
|
145
|
+
|
|
146
|
+
final_ranges = []
|
|
147
|
+
explicit_ranges = False
|
|
148
|
+
|
|
149
|
+
if ranges is not None and len(ranges) == 1 and ranges[0] == -1:
|
|
150
|
+
return clipb, None
|
|
151
|
+
|
|
152
|
+
if ranges is not None and len(ranges) > 0 and isinstance(ranges[0], tuple):
|
|
153
|
+
explicit_ranges = True
|
|
154
|
+
final_ranges = ranges
|
|
155
|
+
print(f"diff_and_swap: Using explicit ranges: {final_ranges}")
|
|
156
|
+
|
|
157
|
+
if precise_swapping:
|
|
158
|
+
diff_clip = core.std.Expr([clipa_t, clipb_t], "x y - abs")
|
|
159
|
+
|
|
160
|
+
binary_thr = scale_binary_value(diff_clip, 4/255, bit=diff_clip.format.bits_per_sample)
|
|
161
|
+
mask = diff_clip.std.Binarize(binary_thr)
|
|
162
|
+
mask = mask.std.Maximum().std.BoxBlur(hradius=1, vradius=1)
|
|
163
|
+
|
|
164
|
+
replacement = core.std.MaskedMerge(clipb_t, clipa_t, mask)
|
|
165
|
+
else:
|
|
166
|
+
diff_clip = core.std.Expr([plane(clipa_t, 0), plane(clipb_t, 0)], "x y - abs")
|
|
167
|
+
replacement = clipa_t
|
|
168
|
+
|
|
169
|
+
if not explicit_ranges:
|
|
170
|
+
detected_ranges = _detect_ranges(diff_clip, thr)
|
|
171
|
+
|
|
172
|
+
if detected_ranges:
|
|
173
|
+
if discard_first:
|
|
174
|
+
detected_ranges = detected_ranges[discard_first:]
|
|
175
|
+
if discard_last:
|
|
176
|
+
if discard_last >= len(detected_ranges):
|
|
177
|
+
detected_ranges = []
|
|
178
|
+
else:
|
|
179
|
+
detected_ranges = detected_ranges[:len(detected_ranges) - discard_last]
|
|
180
|
+
|
|
181
|
+
if detected_ranges:
|
|
182
|
+
print("diff_and_swap Detected Range Map:")
|
|
183
|
+
for i, (s, e) in enumerate(detected_ranges):
|
|
184
|
+
print(f"Range {i}: {s} - {e} (Frames: {e-s+1})")
|
|
185
|
+
else:
|
|
186
|
+
print("diff_and_swap: No ranges detected.")
|
|
187
|
+
|
|
188
|
+
if ranges is None:
|
|
189
|
+
final_ranges = detected_ranges
|
|
190
|
+
else:
|
|
191
|
+
for idx in ranges:
|
|
192
|
+
if not isinstance(idx, int):
|
|
193
|
+
print(f"diff_and_swap Warning: Invalid index '{idx}' ignored.")
|
|
194
|
+
continue
|
|
195
|
+
if 0 <= idx < len(detected_ranges):
|
|
196
|
+
final_ranges.append(detected_ranges[idx])
|
|
197
|
+
else:
|
|
198
|
+
print(f"diff_and_swap Warning: Index {idx} out of bounds of detected ranges.")
|
|
199
|
+
|
|
200
|
+
if final_ranges:
|
|
201
|
+
merged = replace_ranges(clipb_t, replacement, final_ranges)
|
|
202
|
+
segments = [replacement.std.Trim(a, b) for a, b in final_ranges]
|
|
203
|
+
selected = core.std.Splice(segments)
|
|
204
|
+
else:
|
|
205
|
+
merged = clipb_t
|
|
206
|
+
selected = None
|
|
207
|
+
|
|
208
|
+
return merged, selected
|