vsdirty 0.2.2__tar.gz → 0.4.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.
vsdirty-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: vsdirty
3
+ Version: 0.4.0
4
+ Summary: VapourSynth wrappers for denoising, masking and edge fixing
5
+ Author: PingWer, Mhanz3500
6
+ Project-URL: Homepage, https://github.com/PingWer/vs-dirty
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: vsjetpack
14
+ Requires-Dist: vapoursynth==73
15
+ Dynamic: license-file
16
+
17
+ # vs-dirty
18
+
19
+ A collection of VapourSynth wrappers and utility functions focused on advanced denoising, masking, and edge fixing.
20
+
21
+ Follow the docstring for more information about the parameters.
22
+
23
+ ## Installation
24
+
25
+ You can install `vsdirty` via pip:
26
+
27
+ ```bash
28
+ pip install vsdirty
29
+ ```
30
+
31
+ Or build from source:
32
+
33
+ ```bash
34
+ git clone https://github.com/PingWer/vs-dirty
35
+ cd vs-dirty
36
+ pip install .
37
+ ```
38
+
39
+ ## Dependencies
40
+
41
+ This package relies on several external VapourSynth plugins. Ensure these are installed and available in your VapourSynth plugins folder.
42
+
43
+ | Plugin | URL |
44
+ | :--- | :--- |
45
+ | **fmtc** | [GitLab](https://gitlab.com/EleonoreMizo/fmtconv/) |
46
+ | **akarin** | [GitHub](https://github.com/AkarinVS/vapoursynth-plugin) |
47
+ | **cas** | [GitHub](https://github.com/HomeOfVapourSynthEvolution/VapourSynth-CAS) |
48
+ | **bore** | [GitHub](https://github.com/OpusGang/bore) |
49
+ | **mvtools** | [GitHub](https://github.com/dubhater/vapoursynth-mvtools) |
50
+ | **BM3DCuda** | [GitHub](https://github.com/WolframRhodium/VapourSynth-BM3DCUDA) |
51
+ | **nlm-cuda** | [GitHub](https://github.com/AmusementClub/vs-nlm-cuda) |
52
+ | **vsmlrt** | [GitHub](https://github.com/AmusementClub/vs-mlrt) |
53
+
54
+ ## License
55
+
56
+ MIT License
@@ -0,0 +1,40 @@
1
+ # vs-dirty
2
+
3
+ A collection of VapourSynth wrappers and utility functions focused on advanced denoising, masking, and edge fixing.
4
+
5
+ Follow the docstring for more information about the parameters.
6
+
7
+ ## Installation
8
+
9
+ You can install `vsdirty` via pip:
10
+
11
+ ```bash
12
+ pip install vsdirty
13
+ ```
14
+
15
+ Or build from source:
16
+
17
+ ```bash
18
+ git clone https://github.com/PingWer/vs-dirty
19
+ cd vs-dirty
20
+ pip install .
21
+ ```
22
+
23
+ ## Dependencies
24
+
25
+ This package relies on several external VapourSynth plugins. Ensure these are installed and available in your VapourSynth plugins folder.
26
+
27
+ | Plugin | URL |
28
+ | :--- | :--- |
29
+ | **fmtc** | [GitLab](https://gitlab.com/EleonoreMizo/fmtconv/) |
30
+ | **akarin** | [GitHub](https://github.com/AkarinVS/vapoursynth-plugin) |
31
+ | **cas** | [GitHub](https://github.com/HomeOfVapourSynthEvolution/VapourSynth-CAS) |
32
+ | **bore** | [GitHub](https://github.com/OpusGang/bore) |
33
+ | **mvtools** | [GitHub](https://github.com/dubhater/vapoursynth-mvtools) |
34
+ | **BM3DCuda** | [GitHub](https://github.com/WolframRhodium/VapourSynth-BM3DCUDA) |
35
+ | **nlm-cuda** | [GitHub](https://github.com/AmusementClub/vs-nlm-cuda) |
36
+ | **vsmlrt** | [GitHub](https://github.com/AmusementClub/vs-mlrt) |
37
+
38
+ ## License
39
+
40
+ MIT License
@@ -4,15 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vsdirty"
7
- version = "0.2.2"
7
+ version = "0.4.0"
8
8
  description = "VapourSynth wrappers for denoising, masking and edge fixing"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "PingWer" }, { name = "Mhanz3500" }]
11
11
  requires-python = ">=3.12"
12
12
  dependencies = [
13
- "vapoursynth>=70",
14
- "vstools",
15
- "vsjetpack==1.1.0",
13
+ "vsjetpack",
14
+ "vapoursynth==73", # For 0.4.0 we need to force r73, we will support APIv4 in the next update
16
15
  ]
17
16
  classifiers = [
18
17
  "Programming Language :: Python :: 3",
@@ -21,7 +20,7 @@ classifiers = [
21
20
  ]
22
21
 
23
22
  [project.urls]
24
- Homepage = "https://github.com/r74mi/vs-dirty"
23
+ Homepage = "https://github.com/PingWer/vs-dirty"
25
24
 
26
25
  [tool.setuptools.packages.find]
27
26
  include = ["vsdirty*"]
@@ -1,7 +1,8 @@
1
1
  import vapoursynth as vs
2
2
 
3
- from typing import Optional
3
+ from typing import Optional, Any
4
4
  from vstools import PlanesT
5
+ from vsmlrt import BackendV2
5
6
 
6
7
  core = vs.core
7
8
 
@@ -149,20 +150,7 @@ def mini_BM3D(
149
150
  refS,
150
151
  fulls=True,
151
152
  fulld=True,
152
- coef=[
153
- 1 / 3,
154
- 1 / 3,
155
- 1 / 3,
156
- 0,
157
- 1 / 2,
158
- 0,
159
- -1 / 2,
160
- 0,
161
- 1 / 4,
162
- -1 / 2,
163
- 1 / 4,
164
- 0,
165
- ],
153
+ coef=[1 / 3, 1 / 3, 1 / 3, 0, 1 / 2, 0, -1 / 2, 0, 1 / 4, -1 / 2, 1 / 4, 0],
166
154
  col_fam=vs.YUV,
167
155
  )
168
156
  else:
@@ -255,9 +243,9 @@ class adenoise:
255
243
  :param thsad: Thsad for mc_degrain (luma denoise strength and chroma ref).
256
244
  Recommended values: 300-800
257
245
  :param tr: Temporal radius for temporal consistency across al the filter involved.
258
- Recommended values: 2-3 (1 means no temporal denoise).
246
+ Recommended values: 2-3.
259
247
  :param sigma: Sigma for BM3D (luma denoise strength).
260
- Recommended values: 1-5.
248
+ Recommended values: 3-6.
261
249
  :param luma_mask_weaken: Controls how much dark spots should be denoised. Lower values mean stronger overall denoise.
262
250
  Recommended values: 0.6-0.9
263
251
  :param luma_mask_thr: Threshold that determines what is considered bright and what is dark in the luma mask.
@@ -297,6 +285,7 @@ class adenoise:
297
285
  luma_over_texture: float = 0.4,
298
286
  kwargs_flatmask: Optional[dict] = {},
299
287
  show_mask: int = 0,
288
+ backend: Optional[BackendV2] = None,
300
289
  **kwargs,
301
290
  ) -> tuple[vs.VideoNode, vs.VideoNode]:
302
291
 
@@ -331,7 +320,7 @@ class adenoise:
331
320
  selected_mask = darken_luma_mask
332
321
 
333
322
  # Degrain
334
- if "is_digital" not in kwargs:
323
+ if "is_bm3d_only" not in kwargs:
335
324
  mvtools = MVTools(clip)
336
325
  vectors = mvtools.analyze(
337
326
  blksize=16,
@@ -370,21 +359,17 @@ class adenoise:
370
359
  if show_mask == 2:
371
360
  selected_mask = flatmask
372
361
 
373
- if luma_over_texture > 1.0:
374
- raise ValueError("luma_over_texture must be less than 1")
375
- elif luma_over_texture < 0.0:
376
- raise ValueError("luma_over_texture must be greater than 0")
377
- final_mask = core.akarin.Expr(
378
- [darken_luma_mask, flatmask],
379
- f"x {luma_over_texture} * y {abs(luma_over_texture - 1)} * +",
380
- )
362
+ final_mask = core.std.Merge(flatmask, darken_luma_mask, weight=luma_over_texture)
381
363
  else:
382
364
  final_mask = darken_luma_mask
383
365
 
384
366
  if show_mask == 3:
385
367
  selected_mask = final_mask
386
368
 
387
- denoised = mini_BM3D(plane(degrain, 0), sigma=sigma, radius=tr, profile="HIGH")
369
+ if "is_digital" in kwargs:
370
+ denoised = mini_BM3D(plane(clip, 0), ref=plane(degrain, 0), sigma=sigma, radius=tr, profile="HIGH")
371
+ else:
372
+ denoised = mini_BM3D(plane(degrain, 0), sigma=sigma, radius=tr, profile="HIGH")
388
373
  y_denoised = core.std.MaskedMerge(
389
374
  denoised, plane(clip, 0), final_mask
390
375
  ) # denoise applied to darker areas
@@ -414,7 +399,7 @@ class adenoise:
414
399
  elif chroma_denoise[1] == "artcnn":
415
400
  from vsscale import ArtCNN
416
401
 
417
- chroma_denoised = ArtCNN.R8F64_JPEG420().scale(clip)
402
+ chroma_denoised = ArtCNN.R8F64_JPEG420(backend=backend).scale(clip)
418
403
  weights = [
419
404
  0,
420
405
  chroma_denoise[0]
@@ -478,12 +463,13 @@ class adenoise:
478
463
  fast: bool = True,
479
464
  luma_mask_weaken: float = 0.9,
480
465
  luma_mask_thr: float = 0.196,
481
- chroma_denoise: float | tuple[float, str] = [0.5, "nlm"],
466
+ chroma_denoise: float | str | tuple[float, str] = [0.5, "nlm"],
482
467
  precision: bool = True,
483
468
  chroma_masking: bool = False,
484
469
  show_mask: int = 0,
485
470
  luma_over_texture: float = 0.4,
486
471
  kwargs_flatmask: dict = {},
472
+ backend: Optional[BackendV2] = None,
487
473
  ) -> vs.VideoNode:
488
474
  """changes: thsad=200, sigma=2, luma_mask_weaken=0.9, chroma_strength=0.5"""
489
475
  denoised = adenoise._adaptive_denoiser(
@@ -500,6 +486,7 @@ class adenoise:
500
486
  luma_over_texture,
501
487
  kwargs_flatmask,
502
488
  show_mask,
489
+ backend=backend,
503
490
  )
504
491
  if show_mask in [1, 2, 3, 4, 5]:
505
492
  return denoised
@@ -514,12 +501,13 @@ class adenoise:
514
501
  fast: bool = True,
515
502
  luma_mask_weaken: float = 0.8,
516
503
  luma_mask_thr: float = 0.196,
517
- chroma_denoise: float | tuple[float, str] = [0.7, "nlm"],
504
+ chroma_denoise: float | str | tuple[float, str] = [0.7, "nlm"],
518
505
  precision: bool = True,
519
506
  chroma_masking: bool = False,
520
507
  show_mask: int = 0,
521
508
  luma_over_texture: float = 0.4,
522
509
  kwargs_flatmask: dict = {},
510
+ backend: Optional[BackendV2] = None,
523
511
  ) -> vs.VideoNode:
524
512
  """changes: thsad=400, sigma=4, luma_mask_weaken=0.8, chroma_strength=0.7"""
525
513
  denoised = adenoise._adaptive_denoiser(
@@ -536,6 +524,7 @@ class adenoise:
536
524
  luma_over_texture,
537
525
  kwargs_flatmask,
538
526
  show_mask,
527
+ backend=backend,
539
528
  )
540
529
  if show_mask in [1, 2, 3, 4, 5]:
541
530
  return denoised
@@ -550,12 +539,13 @@ class adenoise:
550
539
  fast: bool = True,
551
540
  luma_mask_weaken: float = 0.75,
552
541
  luma_mask_thr: float = 0.196,
553
- chroma_denoise: float | tuple[float, str] = [1.0, "nlm"],
542
+ chroma_denoise: float | str | tuple[float, str] = [1.0, "nlm"],
554
543
  precision: bool = True,
555
544
  chroma_masking: bool = False,
556
545
  show_mask: int = 0,
557
546
  luma_over_texture: float = 0.4,
558
547
  kwargs_flatmask: dict = {},
548
+ backend: Optional[BackendV2] = None,
559
549
  ) -> vs.VideoNode:
560
550
  denoised = adenoise._adaptive_denoiser(
561
551
  clip,
@@ -571,6 +561,7 @@ class adenoise:
571
561
  luma_over_texture,
572
562
  kwargs_flatmask,
573
563
  show_mask,
564
+ backend=backend,
574
565
  )
575
566
  if show_mask in [1, 2, 3, 4, 5]:
576
567
  return denoised
@@ -585,12 +576,13 @@ class adenoise:
585
576
  fast: bool = False,
586
577
  luma_mask_weaken: float = 0.75,
587
578
  luma_mask_thr: float = 0.196,
588
- chroma_denoise: float | tuple[float, str] = [1.5, "nlm"],
579
+ chroma_denoise: float | str | tuple[float, str] = [1.5, "nlm"],
589
580
  precision: bool = True,
590
581
  chroma_masking: bool = False,
591
582
  show_mask: int = 0,
592
583
  luma_over_texture: float = 0.4,
593
584
  kwargs_flatmask: dict = {},
585
+ backend: Optional[BackendV2] = None,
594
586
  ) -> vs.VideoNode:
595
587
  """changes: thsad=800, sigma=12, luma_over_texture=0.4, fast=False"""
596
588
  denoised = adenoise._adaptive_denoiser(
@@ -607,11 +599,12 @@ class adenoise:
607
599
  luma_over_texture,
608
600
  kwargs_flatmask,
609
601
  show_mask,
602
+ backend=backend,
610
603
  )
611
604
  if show_mask in [1, 2, 3, 4, 5]:
612
605
  return denoised
613
606
  return denoised[0]
614
-
607
+
615
608
  @staticmethod
616
609
  def digital(
617
610
  clip: vs.VideoNode,
@@ -621,12 +614,13 @@ class adenoise:
621
614
  fast: bool = True,
622
615
  luma_mask_weaken: float = 0.75,
623
616
  luma_mask_thr: float = 0.196,
624
- chroma_denoise: float | tuple[float, str] = [1.0, "nlm"],
617
+ chroma_denoise: float | str | tuple[float, str] = [1.0, "nlm"],
625
618
  precision: bool = True,
626
619
  chroma_masking: bool = False,
627
620
  show_mask: int = 0,
628
621
  luma_over_texture: float = 0.2,
629
622
  kwargs_flatmask: dict = {},
623
+ backend: Optional[BackendV2] = None,
630
624
  ) -> vs.VideoNode:
631
625
  """changes: thsad=300, sigma=3, luma_over_texture=0.2"""
632
626
  denoised = adenoise._adaptive_denoiser(
@@ -644,6 +638,46 @@ class adenoise:
644
638
  kwargs_flatmask,
645
639
  show_mask,
646
640
  is_digital=True,
641
+ backend=backend,
642
+ )
643
+ if show_mask in [1, 2, 3, 4, 5]:
644
+ return denoised
645
+ return denoised[0]
646
+
647
+ @staticmethod
648
+ def bm3d(
649
+ clip: vs.VideoNode,
650
+ thsad: int = 500,
651
+ tr: int = 2,
652
+ sigma: float = 3,
653
+ fast: bool = True,
654
+ luma_mask_weaken: float = 0.75,
655
+ luma_mask_thr: float = 0.196,
656
+ chroma_denoise: float | str | tuple[float, str] = [1.0, "nlm"],
657
+ precision: bool = True,
658
+ chroma_masking: bool = False,
659
+ show_mask: int = 0,
660
+ luma_over_texture: float = 0.2,
661
+ kwargs_flatmask: dict = {},
662
+ backend: Optional[BackendV2] = None,
663
+ ) -> vs.VideoNode:
664
+ """changes: sigma=3, luma_over_texture=0.2"""
665
+ denoised = adenoise._adaptive_denoiser(
666
+ clip,
667
+ thsad,
668
+ tr,
669
+ sigma,
670
+ luma_mask_weaken,
671
+ luma_mask_thr,
672
+ chroma_denoise,
673
+ precision,
674
+ fast,
675
+ chroma_masking,
676
+ luma_over_texture,
677
+ kwargs_flatmask,
678
+ show_mask,
679
+ is_bm3d_only=True,
680
+ backend=backend,
647
681
  )
648
682
  if show_mask in [1, 2, 3, 4, 5]:
649
683
  return denoised
@@ -658,12 +692,13 @@ class adenoise:
658
692
  fast: bool = True,
659
693
  luma_mask_weaken: float = 0.75,
660
694
  luma_mask_thr: float = 0.196,
661
- chroma_denoise: float | tuple[float, str] = [1.0, "nlm"],
695
+ chroma_denoise: float | str | tuple[float, str] = [1.0, "nlm"],
662
696
  precision: bool = True,
663
697
  chroma_masking: bool = False,
664
698
  show_mask: int = 0,
665
699
  luma_over_texture: float = 0.4,
666
700
  kwargs_flatmask: dict = {},
701
+ backend: Optional[BackendV2] = None,
667
702
  ) -> vs.VideoNode:
668
703
  """default profile"""
669
704
  denoised = adenoise._adaptive_denoiser(
@@ -680,6 +715,7 @@ class adenoise:
680
715
  luma_over_texture,
681
716
  kwargs_flatmask,
682
717
  show_mask,
718
+ backend=backend,
683
719
  )
684
720
  if show_mask in [1, 2, 3, 4, 5]:
685
721
  return denoised
@@ -738,21 +774,26 @@ def auto_deblock(
738
774
  def msaa2x(
739
775
  clip: vs.VideoNode,
740
776
  ref: Optional[vs.VideoNode] = None,
741
- mask: bool = False,
777
+ show_mask: bool = False,
778
+ edgemask: Optional[vs.VideoNode] = None,
742
779
  sigma: float = 3,
743
780
  thr: float = None,
781
+ strength: float = None,
744
782
  planes: PlanesT = 0,
783
+ backend: Optional[BackendV2] = None,
745
784
  **kwargs,
746
- ) -> vs.VideoNode:
785
+ ) -> vs.VideoNode | tuple[vs.VideoNode, vs.VideoNode]:
747
786
  """
748
787
  Upscales only the edges with AI (ArtCNN DN) and downscales them.
749
788
 
750
789
  :param clip: Clip to process (YUV or Grayscale).
751
790
  :param planes: Which planes to process. Defaults to Y.
752
791
  :param ref: Reference clip used to create the edgemask (should be a denoised clip). If None, clip will be used and will be denoised with adenoise.digital to prevent edge detail loss, but remove grain and noise.
753
- :param mask: If True will return the mask used.
792
+ :param show_mask: If True, returns a tuple containing the processed clip and the mask used.
793
+ :param edgemask: Pre-computed edgemask. If None, it will be computed internally.
754
794
  :param sigma: Sigma used for edge fixing during antialiasing (remove dirty spots and blocking) only if ref is None.
755
795
  :param thr: Threshold used for Binarize the clip, only 0-1 value area allowed. If None, no Binarize will be applied.
796
+ :param strength: Strength of the final merge between the original clip and the upscaled clip. 0-1 values accepted.
756
797
  :param kwargs: Accepts advanced_edgemask arguments.
757
798
  """
758
799
  from vsscale import ArtCNN
@@ -770,35 +811,35 @@ def msaa2x(
770
811
 
771
812
  clip = depth(clip, 16, dither_type="none")
772
813
 
773
- if ref is None:
774
- ref = adenoise.digital(
775
- clip,
776
- sigma=sigma,
777
- precision=False,
778
- chroma_denoise=[(0 if (1 in planes or 2 in planes) else 2), "cbm3d"],
779
- )
780
-
781
- if len(planes) == 1:
782
- edgemask = advanced_edgemask(plane(ref, 0), **kwargs)
783
- else:
784
- masks = [
785
- advanced_edgemask(plane(ref, p), **kwargs)
786
- if p in planes
787
- else plane(ref, p).std.BlankClip()
788
- for p in range(3)
789
- ]
790
- edgemask = core.std.ShufflePlanes(
791
- masks, planes=[0, 0, 0], colorfamily=ref.format.color_family
792
- )
814
+ if edgemask is None:
815
+ if ref is None:
816
+ ref = adenoise.digital(
817
+ clip,
818
+ sigma=sigma,
819
+ precision=False,
820
+ chroma_denoise=[(0 if (1 in planes or 2 in planes) else 2), "cbm3d"],
821
+ backend=backend,
822
+ )
823
+
824
+ if len(planes) == 1:
825
+ edgemask = advanced_edgemask(plane(ref, 0), **kwargs)
826
+ else:
827
+ masks = [
828
+ advanced_edgemask(plane(ref, p), **kwargs)
829
+ if p in planes
830
+ else plane(ref, p).std.BlankClip()
831
+ for p in range(3)
832
+ ]
833
+ edgemask = core.std.ShufflePlanes(
834
+ masks, planes=[0, 0, 0], colorfamily=ref.format.color_family
835
+ )
793
836
 
794
837
  if thr is not None and thr != 0:
795
838
  edgemask = edgemask.std.Binarize(
796
839
  threshold=scale_binary_value(edgemask, thr, return_int=True)
797
840
  )
798
- if mask:
799
- return edgemask
800
841
 
801
- upscaled = ArtCNN.C4F32_DN().scale(clip, clip.width * 2, clip.height * 2)
842
+ upscaled = ArtCNN.C4F32_DN(backend=backend).supersample(clip, 2)
802
843
  downscaled = core.resize.Bicubic(upscaled, clip.width, clip.height)
803
844
  aa = core.std.MaskedMerge(clip, downscaled, edgemask, planes=0)
804
845
 
@@ -807,7 +848,7 @@ def msaa2x(
807
848
  aa = core.std.ShufflePlanes(
808
849
  [aa, lefted, lefted], planes=[0, 1, 2], colorfamily=clip.format.color_family
809
850
  )
810
- aa = ArtCNN.R8F64_Chroma().scale(aa)
851
+ aa = ArtCNN.R8F64_Chroma(backend=backend).scale(aa)
811
852
  chroma_downscaled = core.resize.Bicubic(aa, clip.width / 2, clip.height / 2)
812
853
  u = plane(chroma_downscaled, 1)
813
854
  v = plane(chroma_downscaled, 2)
@@ -818,4 +859,10 @@ def msaa2x(
818
859
  )
819
860
  aa = core.std.MaskedMerge(clip, all_downscaled, edgemask, planes=planes)
820
861
 
862
+ if strength is not None or strength != 0:
863
+ aa = core.std.Merge(aa, clip, weight=strength)
864
+
865
+ if show_mask:
866
+ return aa, edgemask
867
+
821
868
  return aa
@@ -11,6 +11,16 @@ if not (
11
11
  )
12
12
 
13
13
 
14
+ def _morpho_radius(clip: vs.VideoNode, radius: int, **kwargs) -> vs.VideoNode:
15
+ """Helper to dynamically switch between expand and inpand based on sign."""
16
+ from vsmasktools import Morpho, XxpandMode
17
+
18
+ if radius == 0:
19
+ return clip
20
+ op = Morpho.expand if radius > 0 else Morpho.inpand
21
+ return op(clip, sw=abs(radius), sh=abs(radius), mode=XxpandMode.ELLIPSE, **kwargs)
22
+
23
+
14
24
  def _soft_threshold(
15
25
  clip: vs.VideoNode, thr: float, steepness: float = 20.0
16
26
  ) -> vs.VideoNode:
@@ -293,7 +303,7 @@ def advanced_edgemask(
293
303
  :param kirsch_weight: Weight for Kirsch edges in final blend (0-1). Default: 0.7.
294
304
  :param kirsch_thr: Kirsch threshold. Default: 0.25.
295
305
  :param edge_thr: Threshold for edge combination logic (0-1). Default: 0.02.
296
- :param expand: Expand the mask by a given number of pixels. Default: 0.
306
+ :param expand: Expand or inpand the mask by a given number of pixels. Default: 0.
297
307
  :param kwargs: Additional arguments for Retinex.
298
308
  :return: Edge mask (Gray clip).
299
309
  """
@@ -386,11 +396,7 @@ def advanced_edgemask(
386
396
  f"x y + {edge_thr_scaled} < x y + z {kirsch_weight} * + x y + ?",
387
397
  )
388
398
 
389
- return (
390
- mask
391
- if expand == 0
392
- else Morpho.expand(mask, mode=XxpandMode.ELLIPSE, sw=expand, sh=expand)
393
- )
399
+ return _morpho_radius(mask, expand)
394
400
 
395
401
 
396
402
  def hd_flatmask(
@@ -398,7 +404,7 @@ def hd_flatmask(
398
404
  ref: Optional[vs.VideoNode] = None,
399
405
  sigma1: float = 3,
400
406
  retinex_sigma: list[float] = [50, 200, 350],
401
- sigma2: float = 1,
407
+ sigma2: float = 1.5,
402
408
  sharpness: float = 0.8,
403
409
  edge_thr: float = 0.55,
404
410
  texture_strength: float = 2,
@@ -418,16 +424,16 @@ def hd_flatmask(
418
424
  :param ref: Optional reference clip for denoising.
419
425
  :param sigma1: BM3D sigma for initial denoising. Default: 3.
420
426
  :param retinex_sigma: Sigma values for Multi-Scale Retinex. Default: [50, 200, 350].
421
- :param sigma2: Nlmeans strength for post-Retinex denoising. Default: 1.
427
+ :param sigma2: Nlmeans strength for post-Retinex denoising. Default: 1.5.
422
428
  :param sharpness: CAS sharpening amount (0-1). Default: 0.8.
423
429
  :param edge_thr: Threshold for edge combination logic (0-1). This allows to separate edges from texture. Default: 0.55.
424
430
  :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.
425
431
  :param edges_strength: Edges strength for mask (0-1). Basic multiplier for edges strength. Default: 0.03.
426
- :param blur: Blur amount for mask (0-1). Default: 2.
427
- :param expand: Expand amount for mask (0-1). Higher value increases the size of the texture in the mask. Default: 3.
432
+ :param blur: Gauss blur sigma for mask. Default: 2.
433
+ :param expand: Expand or inpand (idk why you'd want to do this) amount for mask. Higher value expands the size of the texture in the mask. Default: 3.
428
434
  :param kwargs: Additional arguments for Retinex.
429
435
 
430
- :return: Edge mask (Gray clip) where dark values are texture and edges, bright values are flat areas.
436
+ :return: Edge mask (Gray clip) where bright values are texture and edges, dark values are flat areas.
431
437
  """
432
438
 
433
439
  from vstools import depth
@@ -485,7 +491,7 @@ def hd_flatmask(
485
491
  dither_type="none",
486
492
  )
487
493
 
488
- msrcp = nl_means(msrcpa, h=sigma2, a=2)
494
+ msrcp = nl_means(msrcpa, h=sigma2, a=3)
489
495
 
490
496
  if sharpness > 0:
491
497
  msrcp = core.cas.CAS(msrcp, sharpness=sharpness, opt=0, planes=0)
@@ -521,9 +527,7 @@ def hd_flatmask(
521
527
 
522
528
  edges_expanded = Morpho.expand(edgescombo, mode=XxpandMode.ELLIPSE, sw=1, sh=1)
523
529
  kirco_diff = core.akarin.Expr([kirco, edges_expanded], "x y -")
524
- kirco_expanded = Morpho.expand(
525
- kirco_diff, mode=XxpandMode.ELLIPSE, sw=expand, sh=expand
526
- )
530
+ kirco_expanded = _morpho_radius(kirco_diff, expand)
527
531
 
528
532
  edgescombo = core.akarin.Expr(
529
533
  edgescombo.std.Invert(),
@@ -1,5 +1,6 @@
1
1
  import vapoursynth as vs
2
2
  from typing import SupportsIndex, Optional, List, Tuple, Union
3
+ from vstools import PlanesT
3
4
 
4
5
 
5
6
  def plane(clip: vs.VideoNode, index: SupportsIndex) -> vs.VideoNode:
@@ -224,3 +225,70 @@ def diff_and_swap(
224
225
  selected = None
225
226
 
226
227
  return merged, selected
228
+
229
+
230
+ def smart_CAS(
231
+ clip: vs.VideoNode,
232
+ sharpness: float = 0.5,
233
+ strength: float = 0.5,
234
+ texture: bool = False,
235
+ mask: Union[vs.VideoNode, int, None] = None,
236
+ # character_masking: bool = False,
237
+ planes: PlanesT = 0,
238
+ show_mask: bool = False,
239
+ **kwargs,
240
+ ) -> vs.VideoNode:
241
+ """
242
+ Applies Contrast Adaptive Sharpening (CAS) with optional masking and strength control.
243
+
244
+ :param clip: Input video clip.
245
+ :param sharpness: Sharpening intensity (0.0 - 1.0). Default is 0.5.
246
+ :param strength: Mixing factor to dilute the sharpening effect (0.0 - 1.0).
247
+ 0.0 means full CAS force, while 1.0 returns the original clip.
248
+ Default is 0.5.
249
+ :param texture: If True, uses hd_flatmask for masking. If False, uses advanced_edgemask.
250
+ Ignored if a custom mask is provided. Default is False.
251
+ :param mask: Custom mask for sharpening. If None, a mask is generated.
252
+ The sharpened clip is applied where the mask is white (light).
253
+ If set to 0 (int), returns the sharpened clip without any masking.
254
+ :param planes: List of planes to process. Default is plane 0 (Luma).
255
+ :param show_mask: If True, returns the mask instead of the sharpened clip.
256
+ :param kwargs: Additional arguments passed to the mask generation functions.
257
+
258
+ :return: Sharpened video clip with masking applied to detail areas (unless mask=0).
259
+ """
260
+ core = vs.core
261
+ from vsrgtools import gauss_blur
262
+
263
+ cassed = core.cas.CAS(clip, sharpness=sharpness, planes=planes, opt=0)
264
+
265
+ if strength != 0.0:
266
+ cassed = core.std.Merge(cassed, clip, weight=strength)
267
+
268
+ if isinstance(mask, vs.VideoNode):
269
+ if show_mask:
270
+ return mask
271
+ return core.std.MaskedMerge(clip, cassed, mask, planes=planes)
272
+ elif mask is None:
273
+ mask_expand = kwargs.pop("expand", -1)
274
+
275
+ if texture:
276
+ from .admask import hd_flatmask
277
+
278
+ mask = hd_flatmask(clip, expand=mask_expand, **kwargs)
279
+ mask = gauss_blur(mask, sigma=1, planes=planes)
280
+ if show_mask:
281
+ return mask
282
+ return core.std.MaskedMerge(clip, cassed, mask, planes=planes)
283
+ else:
284
+ from .admask import advanced_edgemask
285
+
286
+ mask = advanced_edgemask(clip, expand=mask_expand, **kwargs)
287
+ mask = gauss_blur(mask, sigma=1, planes=planes)
288
+ if show_mask:
289
+ return mask
290
+ return core.std.MaskedMerge(clip, cassed, mask, planes=planes)
291
+ elif isinstance(mask, int) and mask == 0:
292
+ return cassed
293
+ else:
294
+ raise vs.Error("smart_CAS: mask must be a VideoNode or None")