Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# SharpnessPro/Clarity.py
|
||||
import torch
|
||||
from .utils import ensure_tensor_bchw, to_bhwc, clamp01, gaussian_blur, rgb_to_luma, luma_to_rgb, midtone_mask
|
||||
|
||||
class ClarityNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"radius": ("FLOAT", {"default": 24.0, "min": 1.0, "max": 200.0, "step": 1.0}),
|
||||
"strength": ("FLOAT", {"default": 0.35, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||
"midtone_softness": ("FLOAT", {"default": 0.35, "min": 0.05, "max": 0.5, "step": 0.01}),
|
||||
"luma_only": (["Yes","No"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply"
|
||||
CATEGORY = "SharpnessPro/Tone"
|
||||
|
||||
def apply(self, image, radius, strength, midtone_softness, luma_only):
|
||||
x = ensure_tensor_bchw(image).to(torch.float32)
|
||||
# Local contrast via unsharp on a big radius, constrained to midtones
|
||||
low = gaussian_blur(x, radius)
|
||||
detail = x - low
|
||||
|
||||
mask = midtone_mask(x, softness=midtone_softness) # B,1,H,W
|
||||
if luma_only == "Yes":
|
||||
L = rgb_to_luma(x)
|
||||
L_low = gaussian_blur(x, radius=radius)
|
||||
L_low = rgb_to_luma(L_low)
|
||||
dL = (L - L_low) * mask
|
||||
L_out = (L + strength * dL).clamp(0,1)
|
||||
# Recombine
|
||||
eps = 1e-6
|
||||
baseL = L + eps
|
||||
scale = (L_out + eps) / baseL
|
||||
out = clamp01(x * scale)
|
||||
else:
|
||||
out = clamp01(x + strength * (detail * mask))
|
||||
|
||||
return (to_bhwc(out),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"Clarity": ClarityNode}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"Clarity": "SharpnessPro · Clarté"}
|
||||
@@ -0,0 +1,57 @@
|
||||
# SharpnessPro/HighPassSharpen.py
|
||||
import torch
|
||||
from .utils import ensure_tensor_bchw, to_bhwc, gaussian_blur, clamp01, rgb_to_luma, luma_to_rgb, blend_overlay, blend_soft_light, apply_opacity
|
||||
|
||||
class HighPassSharpenNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"radius": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
||||
"amount": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.05}),
|
||||
"blend_mode": (["Overlay","SoftLight"],),
|
||||
"opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"work_on_luma": (["Yes","No"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply"
|
||||
CATEGORY = "SharpnessPro/Sharpen"
|
||||
|
||||
def apply(self, image, radius, amount, blend_mode, opacity, work_on_luma):
|
||||
x = ensure_tensor_bchw(image).to(torch.float32)
|
||||
base = x
|
||||
|
||||
if work_on_luma == "Yes":
|
||||
# high-pass on luminance (more PS-like)
|
||||
L = rgb_to_luma(base)
|
||||
low = gaussian_blur(base, radius=radius)
|
||||
L_low = rgb_to_luma(low)
|
||||
detail = (L - L_low) # [-?, ?]
|
||||
hp_layer = (0.5 + amount * detail).clamp(0.0, 1.0).repeat(1,3,1,1)
|
||||
# Blend on RGB but create from luma details
|
||||
if blend_mode == "Overlay":
|
||||
blended = blend_overlay(base, hp_layer)
|
||||
else:
|
||||
blended = blend_soft_light(base, hp_layer)
|
||||
out = apply_opacity(base, blended, opacity)
|
||||
else:
|
||||
low = gaussian_blur(base, radius=radius)
|
||||
detail = (base - low)
|
||||
hp_layer = (0.5 + amount * detail).clamp(0.0, 1.0)
|
||||
if blend_mode == "Overlay":
|
||||
blended = blend_overlay(base, hp_layer)
|
||||
else:
|
||||
blended = blend_soft_light(base, hp_layer)
|
||||
out = apply_opacity(base, blended, opacity)
|
||||
|
||||
return (to_bhwc(clamp01(out)),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HighPassSharpen": HighPassSharpenNode
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HighPassSharpen": "SharpnessPro · Passe-haut"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Philippe Joye (Orion4D)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,111 @@
|
||||
# SharpnessPro pour ComfyUI
|
||||
|
||||
Une collection de nœuds de haute qualité pour [ComfyUI](https://github.com/comfyanonymous/ComfyUI), dédiée à l'amélioration de la netteté, de la clarté et de la texture de vos images. Inspirés par les outils professionnels de logiciels comme Photoshop ou Lightroom, ces nœuds offrent un contrôle précis pour des résultats nets et sans artefacts.
|
||||
|
||||
Le pack est divisé en deux catégories :
|
||||
* **SharpnessPro/Sharpen (Netteté)** : Pour l'accentuation des détails fins.
|
||||
* **SharpnessPro/Tone (Tonalité)** : Pour l'ajustement du contraste local (Clarté, Texture).
|
||||
|
||||
|
||||
*(Remplacez cette image par une capture d'écran de votre workflow)*
|
||||
|
||||
## ✨ Installation
|
||||
|
||||
### Méthode 1 : Via le ComfyUI-Manager (Recommandé)
|
||||
|
||||
1. Installez le [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) si ce n'est pas déjà fait.
|
||||
2. Dans ComfyUI, cliquez sur le bouton "Manager".
|
||||
3. Cliquez sur "Install Custom Nodes".
|
||||
4. Recherchez "SharpnessPro" et cliquez sur "Install".
|
||||
5. Redémarrez ComfyUI.
|
||||
|
||||
### Méthode 2 : Manuellement (Git Clone)
|
||||
|
||||
1. Ouvrez un terminal ou une invite de commande.
|
||||
2. Naviguez jusqu'au répertoire d'installation de ComfyUI.
|
||||
3. Clonez le dépôt dans le dossier `custom_nodes` :
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/
|
||||
git clone https://github.com/VOTRE_NOM_UTILISATEUR/SharpnessPro.git
|
||||
```
|
||||
*(N'oubliez pas de remplacer `VOTRE_NOM_UTILISATEUR/SharpnessPro.git` par l'URL réelle de votre dépôt)*
|
||||
4. Redémarrez ComfyUI.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Nœuds Incluídos
|
||||
|
||||
### Catégorie `SharpnessPro/Sharpen`
|
||||
|
||||
#### 🧠 SmartSharpen (Netteté optimisée)
|
||||
|
||||
Le nœud le plus avancé du pack. Il applique une netteté intelligente en se basant sur un filtre "edge-aware" (qui préserve les contours) plutôt qu'un simple flou gaussien. Idéal pour un résultat de haute qualité avec un minimum d'artefacts.
|
||||
|
||||
- **`image`** : L'image d'entrée.
|
||||
- **`radius`** : Définit la taille des détails à accentuer. Un faible rayon cible les détails très fins.
|
||||
- **`amount`** : L'intensité de la netteté.
|
||||
- **`reduce_noise`** : Réduit l'amplification du bruit et des micro-détails indésirables. Très utile pour les images bruitées.
|
||||
- **`fade_shadows`** : Protège les zones sombres de l'image, évitant le "clipping" (perte de détails) et l'apparition de bruit dans les ombres.
|
||||
- **`fade_highlights`** : Protège les zones claires de l'image, évitant les halos et la perte de détails dans les hautes lumières.
|
||||
|
||||
**Conseil** : C'est le nœud de netteté à utiliser par défaut pour la plupart des images.
|
||||
|
||||
#### 🎭 UnsharpMaskSharpen (Masque flou - USM)
|
||||
|
||||
Implémentation du grand classique "Unsharp Mask" (Masque Flou). C'est un algorithme puissant et rapide, qui offre un excellent contrôle.
|
||||
|
||||
- **`radius`** : Rayon du flou utilisé pour détecter les détails.
|
||||
- **`amount`** : Intensité de l'accentuation.
|
||||
- **`threshold`** : Seuil de contraste. Les détails dont le contraste est inférieur à ce seuil ne seront pas accentués. Augmentez cette valeur pour éviter de rendre le bruit ou le grain de l'image plus visible.
|
||||
- **`luma_only`** : Si "Yes", la netteté n'est appliquée qu'à la luminance, ce qui empêche l'apparition de franges colorées sur les contours.
|
||||
|
||||
**Conseil** : Utilisez ce nœud lorsque vous avez besoin d'un contrôle précis via le seuil (`threshold`).
|
||||
|
||||
#### LAYER HighPassSharpen (Passe-haut)
|
||||
|
||||
Une autre technique classique, très populaire dans le monde de la retouche photo. Elle isole les détails de l'image sur une couche grise (passe-haut) et la fusionne avec l'image originale.
|
||||
|
||||
- **`radius`** : Définit la finesse des détails à isoler.
|
||||
- **`amount`** : Contrôle le contraste de la couche de détails avant la fusion.
|
||||
- **`blend_mode`** :
|
||||
- `Overlay` : Mode de fusion standard, plus contrasté et puissant.
|
||||
- `SoftLight` : Mode de fusion plus doux et subtil.
|
||||
- **`opacity`** : Opacité globale de l'effet.
|
||||
- **`work_on_luma`** : Si "Yes", la couche de détails est calculée à partir de la luminance uniquement, pour un résultat plus propre.
|
||||
|
||||
**Conseil** : Excellent pour un look "photographique" et pour contrôler l'effet via les modes de fusion.
|
||||
|
||||
### Catégorie `SharpnessPro/Tone`
|
||||
|
||||
#### 💪 Clarity (Clarté)
|
||||
|
||||
Ce nœud augmente le **contraste local** dans les tons moyens de l'image, donnant plus de "punch" et de présence à l'image sans affecter les ombres et les hautes lumières. C'est l'équivalent du curseur "Clarté" dans Lightroom.
|
||||
|
||||
- **`radius`** : Utilise un rayon **large** pour cibler les variations de tons locales plutôt que les détails fins.
|
||||
- **`strength`** : Intensité de l'effet. Peut être négatif pour adoucir l'image.
|
||||
- **`midtone_softness`** : Contrôle la largeur de la plage des tons moyens ciblée. Une valeur plus faible resserre l'effet sur les tons moyens stricts.
|
||||
- **`luma_only`** : Applique l'effet sur la luminance pour préserver les couleurs.
|
||||
|
||||
**Conseil** : Ne pas confondre avec la netteté. La clarté donne une impression de relief et de profondeur.
|
||||
|
||||
#### 🕸️ Texture
|
||||
|
||||
Ce nœud accentue ou atténue les **détails très fins** et les textures de surface. C'est l'équivalent du curseur "Texture" dans Lightroom.
|
||||
|
||||
- **`radius`** : Utilise un rayon **faible** pour cibler le micro-contraste.
|
||||
- **`strength`** : Intensité de l'effet. Une valeur positive accentue la texture, une valeur négative l'adoucit (effet de lissage de peau léger).
|
||||
- **`luma_only`** : Applique l'effet sur la luminance.
|
||||
|
||||
**Conseil** : Utilisez une valeur positive pour faire ressortir les détails du tissu, du bois ou de la roche. Utilisez une valeur négative pour adoucir subtilement la peau sur les portraits.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Philosophie et Conseils d'Utilisation
|
||||
|
||||
- **Différence entre Netteté et Tonalité** : Les nœuds de *Netteté* (`Sharpen`) sont conçus pour l'étape finale et affectent les contours et les détails les plus fins. Les nœuds de *Tonalité* (`Tone`) comme `Clarity` et `Texture` modifient le contraste local et peuvent être utilisés plus tôt dans le workflow pour sculpter la lumière et la matière.
|
||||
- **Ordre d'application** : Il est généralement recommandé d'appliquer la netteté (`SmartSharpen`, `USM`, etc.) en toute fin de workflow, juste avant la sauvegarde, pour éviter que les effets ultérieurs n'altèrent le résultat.
|
||||
- **Commencez doucement** : Les valeurs par défaut sont un bon point de départ. Il est facile d'en faire trop avec la netteté. Zoomez à 100% pour bien juger de l'effet.
|
||||
|
||||
## Licence
|
||||
|
||||
Ce projet est distribué sous la licence MIT. Voir le fichier `LICENSE` pour plus de détails.
|
||||
@@ -0,0 +1,58 @@
|
||||
# SharpnessPro/SmartSharpen.py
|
||||
import torch
|
||||
from .utils import (ensure_tensor_bchw, to_bhwc, clamp01, gaussian_blur,
|
||||
guided_smooth, rgb_to_luma)
|
||||
|
||||
class SmartSharpenNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"radius": ("FLOAT", {"default": 1.8, "min": 0.0, "max": 50.0, "step": 0.1}),
|
||||
"amount": ("FLOAT", {"default": 1.2, "min": 0.0, "max": 5.0, "step": 0.05}),
|
||||
"reduce_noise": ("FLOAT", {"default": 0.15, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"fade_shadows": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"fade_highlights": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply"
|
||||
CATEGORY = "SharpnessPro/Sharpen"
|
||||
|
||||
def apply(self, image, radius, amount, reduce_noise, fade_shadows, fade_highlights):
|
||||
x = ensure_tensor_bchw(image).to(torch.float32)
|
||||
|
||||
# Edge-aware "blur" estimation on luma:
|
||||
L = rgb_to_luma(x)
|
||||
smooth_L = guided_smooth(x, radius=max(1.0, radius*2), eps=1e-3)
|
||||
# Detail mostly on luma to avoid color halos:
|
||||
dL = L - smooth_L
|
||||
|
||||
# Optional noise reduction: shrink very small details
|
||||
if reduce_noise > 0:
|
||||
shrink = (dL.abs() / (dL.abs() + 0.03)).pow(1.0 - reduce_noise) # compress micro noise
|
||||
dL = dL * shrink
|
||||
|
||||
# Shadow/Highlight fade masks:
|
||||
shadows = (1.0 - L).clamp(0,1)
|
||||
highlights = L.clamp(0,1)
|
||||
dL = dL * (1.0 - fade_shadows*shadows) * (1.0 - fade_highlights*highlights)
|
||||
|
||||
L_out = (L + amount * dL).clamp(0.0, 1.0)
|
||||
|
||||
# Recombine to RGB preserving chroma
|
||||
eps = 1e-6
|
||||
baseL = L + eps
|
||||
scale = (L_out + eps) / baseL
|
||||
out = clamp01(x * scale)
|
||||
|
||||
return (to_bhwc(out),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SmartSharpen": SmartSharpenNode
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SmartSharpen": "SharpnessPro · Netteté optimisée"
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# SharpnessPro/Texture.py
|
||||
import torch
|
||||
from .utils import ensure_tensor_bchw, to_bhwc, clamp01, gaussian_blur, rgb_to_luma
|
||||
|
||||
class TextureNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"radius": ("FLOAT", {"default": 2.0, "min": 0.3, "max": 10.0, "step": 0.1}),
|
||||
"strength": ("FLOAT", {"default": 0.4, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||
"luma_only": (["Yes","No"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply"
|
||||
CATEGORY = "SharpnessPro/Tone"
|
||||
|
||||
def apply(self, image, radius, strength, luma_only):
|
||||
x = ensure_tensor_bchw(image).to(torch.float32)
|
||||
# Micro-contrast = high frequency boost with small radius, symmetric, gentle
|
||||
low = gaussian_blur(x, radius=radius)
|
||||
hf = x - low # high frequencies
|
||||
|
||||
if luma_only == "Yes":
|
||||
L = rgb_to_luma(x)
|
||||
L_low = rgb_to_luma(low)
|
||||
dL = L - L_low
|
||||
L_out = (L + strength * dL).clamp(0,1)
|
||||
eps = 1e-6
|
||||
scale = (L_out + eps) / (L + eps)
|
||||
out = clamp01(x * scale)
|
||||
else:
|
||||
out = clamp01(x + strength * hf)
|
||||
|
||||
return (to_bhwc(out),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"Texture": TextureNode}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"Texture": "SharpnessPro · Texture"}
|
||||
@@ -0,0 +1,52 @@
|
||||
# SharpnessPro/UnsharpMaskSharpen.py
|
||||
import torch
|
||||
from .utils import ensure_tensor_bchw, to_bhwc, gaussian_blur, clamp01, rgb_to_luma, luma_to_rgb
|
||||
|
||||
class UnsharpMaskNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"radius": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
||||
"amount": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.05}),
|
||||
"threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.2, "step": 0.005}),
|
||||
"luma_only": (["Yes","No"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "apply"
|
||||
CATEGORY = "SharpnessPro/Sharpen"
|
||||
|
||||
def apply(self, image, radius, amount, threshold, luma_only):
|
||||
x = ensure_tensor_bchw(image).to(torch.float32)
|
||||
low = gaussian_blur(x, radius)
|
||||
detail = x - low # high freq
|
||||
|
||||
if threshold > 0:
|
||||
# Suppress tiny differences
|
||||
mask = (detail.abs() > threshold).float()
|
||||
detail = detail * mask
|
||||
|
||||
if luma_only == "Yes":
|
||||
L = rgb_to_luma(x)
|
||||
L_low = rgb_to_luma(low)
|
||||
dL = L - L_low
|
||||
if threshold > 0:
|
||||
m = (dL.abs() > threshold).float()
|
||||
dL = dL * m
|
||||
L_out = (L + amount * dL).clamp(0.0, 1.0)
|
||||
out = x * 0 # placeholder shape
|
||||
out = luma_to_rgb(L_out, x)
|
||||
else:
|
||||
out = (x + amount * detail).clamp(0.0, 1.0)
|
||||
|
||||
return (to_bhwc(clamp01(out)),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"UnsharpMaskSharpen": UnsharpMaskNode
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"UnsharpMaskSharpen": "SharpnessPro · Masque flou (USM)"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# SharpnessPro/__init__.py
|
||||
from .HighPassSharpen import NODE_CLASS_MAPPINGS as HP_MAP, NODE_DISPLAY_NAME_MAPPINGS as HP_NAME
|
||||
from .UnsharpMaskSharpen import NODE_CLASS_MAPPINGS as USM_MAP, NODE_DISPLAY_NAME_MAPPINGS as USM_NAME
|
||||
from .SmartSharpen import NODE_CLASS_MAPPINGS as SS_MAP, NODE_DISPLAY_NAME_MAPPINGS as SS_NAME
|
||||
from .Clarity import NODE_CLASS_MAPPINGS as CL_MAP, NODE_DISPLAY_NAME_MAPPINGS as CL_NAME
|
||||
from .Texture import NODE_CLASS_MAPPINGS as TX_MAP, NODE_DISPLAY_NAME_MAPPINGS as TX_NAME
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_CLASS_MAPPINGS.update(HP_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(USM_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(SS_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(CL_MAP)
|
||||
NODE_CLASS_MAPPINGS.update(TX_MAP)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(HP_NAME)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(USM_NAME)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(SS_NAME)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(CL_NAME)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(TX_NAME)
|
||||
@@ -0,0 +1,152 @@
|
||||
# SharpnessPro/utils.py
|
||||
# Helpers communs pour les filtres de netteté.
|
||||
import math
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
EPS = 1e-6
|
||||
|
||||
# ---------- Conversions / clamps ----------
|
||||
def ensure_tensor_bchw(img: torch.Tensor) -> torch.Tensor:
|
||||
# ComfyUI: images en [B,H,W,C] float32 [0..1]
|
||||
if not isinstance(img, torch.Tensor):
|
||||
raise TypeError("Expected torch.Tensor image")
|
||||
if img.ndim != 4:
|
||||
raise ValueError("Expected image with shape [B,H,W,C]")
|
||||
if img.shape[-1] == 3: # B,H,W,C -> B,C,H,W
|
||||
img = img.permute(0, 3, 1, 2).contiguous()
|
||||
return img
|
||||
|
||||
def to_bhwc(img_bchw: torch.Tensor) -> torch.Tensor:
|
||||
# B,C,H,W -> B,H,W,C
|
||||
return img_bchw.permute(0, 2, 3, 1).contiguous()
|
||||
|
||||
def clamp01(x: torch.Tensor) -> torch.Tensor:
|
||||
return x.clamp(0.0, 1.0)
|
||||
|
||||
# ---------- Flou Gaussien (propre & rapide) ----------
|
||||
def get_sigma_from_radius(radius: float) -> float:
|
||||
# Heuristique "douce" proche Photoshop
|
||||
return max(0.15, radius * 0.5 + 0.25)
|
||||
|
||||
def gaussian_kernel1d(sigma: float, truncate: float = 3.0,
|
||||
device=None, dtype=None) -> torch.Tensor:
|
||||
if sigma <= 0:
|
||||
k = torch.tensor([1.0], device=device, dtype=dtype or torch.float32)
|
||||
return k / k.sum()
|
||||
radius = int(truncate * sigma + 0.5)
|
||||
x = torch.arange(-radius, radius + 1, device=device, dtype=dtype or torch.float32)
|
||||
kernel = torch.exp(-(x ** 2) / (2 * sigma * sigma))
|
||||
kernel /= kernel.sum()
|
||||
return kernel
|
||||
|
||||
def gaussian_blur(img_bchw: torch.Tensor, radius: float) -> torch.Tensor:
|
||||
if radius <= 0:
|
||||
return img_bchw
|
||||
sigma = get_sigma_from_radius(radius)
|
||||
device, dtype = img_bchw.device, img_bchw.dtype
|
||||
k1d = gaussian_kernel1d(sigma, device=device, dtype=dtype)
|
||||
|
||||
kH = k1d.view(1, 1, -1, 1)
|
||||
kW = k1d.view(1, 1, 1, -1)
|
||||
C = img_bchw.shape[1]
|
||||
|
||||
pad_h = (kH.shape[2] - 1) // 2
|
||||
pad_w = (kW.shape[3] - 1) // 2
|
||||
|
||||
x = F.pad(img_bchw, (0, 0, pad_h, pad_h), mode='reflect')
|
||||
x = F.conv2d(x, kH.expand(C, 1, -1, 1), groups=C)
|
||||
x = F.pad(x, (pad_w, pad_w, 0, 0), mode='reflect')
|
||||
x = F.conv2d(x, kW.expand(C, 1, 1, -1), groups=C)
|
||||
return x
|
||||
|
||||
# ---------- Luma / recomposition ----------
|
||||
def rgb_to_luma(rgb_bchw: torch.Tensor) -> torch.Tensor:
|
||||
# BT.709
|
||||
r = rgb_bchw[:, 0:1]
|
||||
g = rgb_bchw[:, 1:2]
|
||||
b = rgb_bchw[:, 2:3]
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
def luma_to_rgb(luma_bchw: torch.Tensor, rgb_base_bchw: torch.Tensor) -> torch.Tensor:
|
||||
# Remplacement de luma en conservant la chroma via un scale simple
|
||||
baseL = rgb_to_luma(rgb_base_bchw)
|
||||
scale = (luma_bchw + EPS) / (baseL + EPS)
|
||||
return clamp01(rgb_base_bchw * scale)
|
||||
|
||||
# ---------- Blend modes simplifiés ----------
|
||||
def blend_overlay(base: torch.Tensor, blend: torch.Tensor) -> torch.Tensor:
|
||||
res = torch.where(base <= 0.5, 2 * base * blend, 1 - 2 * (1 - base) * (1 - blend))
|
||||
return clamp01(res)
|
||||
|
||||
def blend_soft_light(base: torch.Tensor, blend: torch.Tensor) -> torch.Tensor:
|
||||
# Approx W3C
|
||||
res = (1 - 2 * blend) * base * base + 2 * blend * base
|
||||
return clamp01(res)
|
||||
|
||||
def apply_opacity(base: torch.Tensor, effect: torch.Tensor, opacity: float) -> torch.Tensor:
|
||||
return clamp01(base * (1 - opacity) + effect * opacity)
|
||||
|
||||
# ---------- Détection de contours ----------
|
||||
def sobel_edges(img_bchw: torch.Tensor) -> torch.Tensor:
|
||||
# Magnitude Sobel sur luma, normalisée [0..1]
|
||||
gray = rgb_to_luma(img_bchw)
|
||||
kx = torch.tensor(
|
||||
[[-1, 0, 1],
|
||||
[-2, 0, 2],
|
||||
[-1, 0, 1]], dtype=img_bchw.dtype, device=img_bchw.device
|
||||
).view(1, 1, 3, 3)
|
||||
ky = torch.tensor(
|
||||
[[-1, -2, -1],
|
||||
[ 0, 0, 0],
|
||||
[ 1, 2, 1]], dtype=img_bchw.dtype, device=img_bchw.device
|
||||
).view(1, 1, 3, 3)
|
||||
gx = F.conv2d(F.pad(gray, (1, 1, 1, 1), mode='reflect'), kx)
|
||||
gy = F.conv2d(F.pad(gray, (1, 1, 1, 1), mode='reflect'), ky)
|
||||
mag = torch.sqrt(gx * gx + gy * gy)
|
||||
mag = mag / (mag.amax(dim=[2, 3], keepdim=True) + EPS)
|
||||
return mag # B,1,H,W
|
||||
|
||||
# ---------- Filtre guidé (approx) edge-aware ----------
|
||||
def guided_smooth(img_bchw: torch.Tensor, radius: float, eps: float = 1e-4) -> torch.Tensor:
|
||||
"""
|
||||
Smoothing edge-aware très léger (approx guided filter) appliqué sur la luma.
|
||||
Patch important : imposer un noyau IMPAIR pour conserver exactement H, W.
|
||||
"""
|
||||
if radius <= 0:
|
||||
return rgb_to_luma(img_bchw)
|
||||
|
||||
k = int(max(1, round(radius)))
|
||||
if k % 2 == 0:
|
||||
k += 1 # noyau impair indispensable
|
||||
|
||||
device, dtype = img_bchw.device, img_bchw.dtype
|
||||
kernel = torch.ones(1, 1, k, k, device=device, dtype=dtype) / (k * k)
|
||||
pad = (k // 2, k // 2, k // 2, k // 2) # L,R,T,B
|
||||
|
||||
L = rgb_to_luma(img_bchw)
|
||||
|
||||
mean_L = F.conv2d(F.pad(L, pad, mode="reflect"), kernel)
|
||||
mean_L2 = F.conv2d(F.pad(L*L, pad, mode="reflect"), kernel)
|
||||
var_L = mean_L2 - mean_L * mean_L
|
||||
|
||||
a = var_L / (var_L + eps)
|
||||
b = (1 - a) * mean_L
|
||||
|
||||
mean_a = F.conv2d(F.pad(a, pad, mode="reflect"), kernel)
|
||||
mean_b = F.conv2d(F.pad(b, pad, mode="reflect"), kernel)
|
||||
|
||||
return mean_a * L + mean_b # B,1,H,W
|
||||
|
||||
# ---------- Masque de tons moyens (Clarity) ----------
|
||||
def midtone_mask(img_bchw: torch.Tensor, softness: float = 0.35) -> torch.Tensor:
|
||||
"""
|
||||
Masque ~1 autour des midtones (luma≈0.5), 0 vers ombres/hautes lumières.
|
||||
softness ∈ (0, 0.5] contrôle la largeur du plateau.
|
||||
"""
|
||||
L = rgb_to_luma(img_bchw) # B,1,H,W
|
||||
s = max(1e-6, min(0.5, float(softness)))
|
||||
m = torch.cos((L - 0.5) * math.pi / (2 * s)).clamp(-1, 1)
|
||||
m = (m + 1) / 2.0
|
||||
m = torch.where((L > 0.5 + s) | (L < 0.5 - s), torch.zeros_like(m), m)
|
||||
return m # B,1,H,W
|
||||
Reference in New Issue
Block a user