Initial release: Advanced Image Denoiser
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# 🧹 Advanced Image Denoiser — ComfyUI Custom Node
|
||||
|
||||
A premium ComfyUI custom node for image denoising with **6 algorithms**, a **dark neon-themed UI**, and **separate luminance/chrominance control**.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy this folder into `ComfyUI/custom_nodes/comfyui-advanced-denoiser/`
|
||||
2. Install dependencies: `pip install -r requirements.txt`
|
||||
3. Restart ComfyUI — find the node under **image → denoising**
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Best For | Rating |
|
||||
|--------|----------|--------|
|
||||
| ⚡ **Auto-Blend** | Overall best results (NLM + Bilateral combo) | ★★★★★ |
|
||||
| 🔍 **NLM** | Photos with grain/noise | ★★★★☆ |
|
||||
| 🎯 **Bilateral** | Portraits, sharp edges | ★★★★☆ |
|
||||
| 〰️ **Wavelet** | Subtle, fine-grained noise | ★★★★☆ |
|
||||
| ☁️ **Gaussian** | Quick, light smoothing | ★★★☆☆ |
|
||||
| ⚡ **Median** | Salt-and-pepper artifacts | ★★★☆☆ |
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Start with **Auto-Blend** at `strength = 0.3`, `preserve_detail = 0.7`
|
||||
2. For photos with grain, try **NLM** at `strength = 0.4–0.6`
|
||||
3. For portraits, use **Bilateral** with higher `preserve_detail`
|
||||
4. Use `blend_original = 0.1–0.3` for the most natural results
|
||||
|
||||
## Requirements
|
||||
|
||||
- ComfyUI (any recent version)
|
||||
- `opencv-python >= 4.8.0`
|
||||
- `scikit-image >= 0.21.0` (optional — wavelet falls back to Gaussian)
|
||||
- `numpy >= 1.24.0`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
ComfyUI Advanced Image Denoiser
|
||||
================================
|
||||
A custom node providing multiple advanced denoising algorithms with
|
||||
fine-grained control over strength, detail preservation, and luminance/
|
||||
chrominance channels.
|
||||
|
||||
Install:
|
||||
Copy this folder into ComfyUI/custom_nodes/
|
||||
pip install -r requirements.txt
|
||||
"""
|
||||
|
||||
from .nodes import AdvancedImageDenoiser
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AdvancedImageDenoiser": AdvancedImageDenoiser,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AdvancedImageDenoiser": "🧹 Advanced Image Denoiser",
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,542 @@
|
||||
/**
|
||||
* 🧹 Advanced Image Denoiser — Premium Custom UI
|
||||
* ================================================
|
||||
* Dark-themed, neon-styled DOM widget with method tabs,
|
||||
* descriptions, styled sliders, and dynamic visibility.
|
||||
*/
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
/* ── Constants ───────────────────────────────────────────────────────── */
|
||||
const NODE_MIN_WIDTH = 420;
|
||||
const NODE_TITLE_HEIGHT = 70; // approximate title + input/output row height
|
||||
|
||||
const METHODS = {
|
||||
auto_blend: {
|
||||
icon: "⚡", label: "Auto-Blend", rating: 5, badge: "★ RECOMMENDED",
|
||||
desc: "Intelligently combines Non-Local Means + Bilateral for the best of both worlds. Balances noise removal with edge preservation automatically.",
|
||||
tip: "Start here! Adjust Preserve Detail to control the NLM ↔ Bilateral mix."
|
||||
},
|
||||
non_local_means: {
|
||||
icon: "🔍", label: "NLM", rating: 4, badge: "BEST FOR PHOTOS",
|
||||
desc: "Searches for similar patches across the image and averages them. Excellent at preserving fine textures while removing photographic grain.",
|
||||
tip: "Increase Search Window for better quality (slower). Increase Patch Size for smoother results."
|
||||
},
|
||||
bilateral: {
|
||||
icon: "🎯", label: "Bilateral", rating: 4, badge: "EDGE-PRESERVING",
|
||||
desc: "Smooths flat areas while keeping sharp edges crisp. Perfect for portraits, architecture, and any image where edge definition matters.",
|
||||
tip: "Sigma Color controls how much color variation is smoothed. Sigma Spatial controls the smoothing radius."
|
||||
},
|
||||
wavelet: {
|
||||
icon: "〰️", label: "Wavelet", rating: 4, badge: "SUBTLE & PRECISE",
|
||||
desc: "Separates the image into frequency layers and removes noise from each band independently. Very gentle and precise — ideal for fine grain.",
|
||||
tip: "Increase Wavelet Level for coarser denoising. Requires scikit-image (falls back to Gaussian if missing)."
|
||||
},
|
||||
gaussian: {
|
||||
icon: "☁️", label: "Gaussian", rating: 3, badge: "QUICK & SIMPLE",
|
||||
desc: "Classic Gaussian blur that softens everything evenly. Fast and effective for very light noise or as a gentle smoothing pass.",
|
||||
tip: "Keep Strength low (0.1–0.3) for subtle smoothing. Use Preserve Detail to recover sharpness."
|
||||
},
|
||||
median: {
|
||||
icon: "⚡", label: "Median", rating: 3, badge: "ARTIFACT REMOVAL",
|
||||
desc: "Replaces each pixel with the median of its neighborhood. Specialized for removing salt-and-pepper noise and digital compression artifacts.",
|
||||
tip: "Most effective for impulse noise (random bright/dark pixels). Less effective for Gaussian noise."
|
||||
},
|
||||
};
|
||||
|
||||
const METHOD_WIDGETS = {
|
||||
auto_blend: ["color_strength", "patch_size", "search_window", "sigma_spatial", "sigma_color", "blend_original"],
|
||||
non_local_means: ["luminance_strength", "color_strength", "patch_size", "search_window", "blend_original"],
|
||||
bilateral: ["luminance_strength", "color_strength", "sigma_spatial", "sigma_color", "blend_original"],
|
||||
wavelet: ["wavelet_level", "blend_original"],
|
||||
gaussian: ["luminance_strength", "color_strength", "blend_original"],
|
||||
median: ["luminance_strength", "color_strength", "blend_original"],
|
||||
};
|
||||
|
||||
const SLIDER_META = {
|
||||
strength: { label: "Strength", min: 0, max: 1, step: 0.01, def: 0.50, unit: "", desc: "Overall denoising power. Higher = more smoothing." },
|
||||
preserve_detail: { label: "Preserve Detail", min: 0, max: 1, step: 0.01, def: 0.70, unit: "", desc: "Blends sharp detail from original back in. Higher = more texture kept." },
|
||||
luminance_strength: { label: "Luminance", min: 0, max: 1, step: 0.01, def: 0.50, unit: "", desc: "Brightness channel denoising. Controls grain removal." },
|
||||
color_strength: { label: "Chrominance", min: 0, max: 1, step: 0.01, def: 0.50, unit: "", desc: "Color channel denoising. Removes color noise / splotches." },
|
||||
patch_size: { label: "Patch Size", min: 3, max: 15, step: 2, def: 7, unit: "px", desc: "Comparison patch size. Larger = smoother but slower." },
|
||||
search_window: { label: "Search Window", min: 7, max: 35, step: 2, def: 21, unit: "px", desc: "Area searched for similar patches. Larger = better quality." },
|
||||
sigma_spatial: { label: "Sigma Spatial", min: 1, max: 300, step: 1, def: 75, unit: "", desc: "Spatial smoothing radius. Higher = smoother over larger areas." },
|
||||
sigma_color: { label: "Sigma Color", min: 1, max: 300, step: 1, def: 75, unit: "", desc: "Color similarity threshold. Higher = more colors blended." },
|
||||
wavelet_level: { label: "Wavelet Levels", min: 1, max: 5, step: 1, def: 2, unit: "", desc: "Decomposition depth. More levels = coarser denoising." },
|
||||
blend_original: { label: "Blend Original", min: 0, max: 1, step: 0.01, def: 0.00, unit: "", desc: "Mix denoised with original for subtle, natural results." },
|
||||
sharpen_amount: { label: "Amount", min: 0, max: 1, step: 0.01, def: 0.00, unit: "", desc: "Sharpening intensity. Higher = stronger edge enhancement." },
|
||||
sharpen_radius: { label: "Radius", min: 0.05, max: 1, step: 0.01, def: 0.30, unit: "", desc: "Detail scale to sharpen. Low = fine micro-detail, High = broader edges." },
|
||||
};
|
||||
|
||||
const SHARPEN_MODES = {
|
||||
off: { icon: "⊘", label: "Off" },
|
||||
unsharp_mask: { icon: "🔪", label: "Unsharp" },
|
||||
high_pass: { icon: "🔬", label: "High-Pass" },
|
||||
luminance_only: { icon: "💡", label: "Luma Only" },
|
||||
};
|
||||
|
||||
const ALL_OPTIONAL = Object.keys(SLIDER_META).filter(k => k !== "strength" && k !== "preserve_detail" && k !== "sharpen_amount" && k !== "sharpen_radius");
|
||||
|
||||
/* ── CSS injection ───────────────────────────────────────────────────── */
|
||||
function injectCSS() {
|
||||
if (document.getElementById("den-styles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "den-styles";
|
||||
style.textContent = `
|
||||
.den-root {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: #0c0c1a;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
color: #d0d0e0;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.den-header {
|
||||
text-align: center;
|
||||
padding: 4px 0 8px;
|
||||
border-bottom: 1px solid rgba(180, 77, 255, 0.2);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.den-title {
|
||||
font-size: 15px; font-weight: 700; letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
background: linear-gradient(135deg, #b44dff, #00ff88);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
background-clip: text; margin: 0;
|
||||
}
|
||||
.den-subtitle {
|
||||
font-size: 9px; color: #666; letter-spacing: 2px;
|
||||
text-transform: uppercase; margin-top: 2px;
|
||||
}
|
||||
.den-methods {
|
||||
display: flex; flex-wrap: wrap; gap: 4px;
|
||||
justify-content: center; margin-bottom: 10px;
|
||||
}
|
||||
.den-method-btn {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid rgba(180, 77, 255, 0.25);
|
||||
border-radius: 16px;
|
||||
background: rgba(180, 77, 255, 0.06);
|
||||
color: #999; cursor: pointer;
|
||||
font-size: 10px; font-weight: 500;
|
||||
transition: all 0.25s ease; outline: none; white-space: nowrap;
|
||||
}
|
||||
.den-method-btn:hover {
|
||||
background: rgba(180, 77, 255, 0.15);
|
||||
color: #ccc; border-color: rgba(180, 77, 255, 0.5);
|
||||
}
|
||||
.den-method-btn.active {
|
||||
background: rgba(180, 77, 255, 0.25);
|
||||
border-color: #b44dff; color: #fff;
|
||||
box-shadow: 0 0 12px rgba(180, 77, 255, 0.3), inset 0 0 8px rgba(180, 77, 255, 0.1);
|
||||
}
|
||||
.den-method-btn.recommended { border-color: rgba(0, 255, 136, 0.5); }
|
||||
.den-method-btn.recommended.active {
|
||||
border-color: #00ff88;
|
||||
box-shadow: 0 0 14px rgba(0, 255, 136, 0.3), inset 0 0 8px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
.den-info {
|
||||
background: rgba(180, 77, 255, 0.06);
|
||||
border: 1px solid rgba(180, 77, 255, 0.15);
|
||||
border-radius: 8px; padding: 8px 10px;
|
||||
margin-bottom: 10px; font-size: 10px; line-height: 1.4;
|
||||
}
|
||||
.den-info-badge {
|
||||
display: inline-block; font-size: 8px; font-weight: 700;
|
||||
letter-spacing: 1px; padding: 2px 6px; border-radius: 4px;
|
||||
background: rgba(180, 77, 255, 0.25); color: #b44dff; margin-bottom: 4px;
|
||||
}
|
||||
.den-info-badge.recommended { background: rgba(0, 255, 136, 0.2); color: #00ff88; }
|
||||
.den-info-desc { color: #aaa; margin: 4px 0; }
|
||||
.den-info-tip { color: #666; font-size: 9px; font-style: italic; margin-top: 4px; }
|
||||
.den-info-stars { color: #b44dff; font-size: 11px; letter-spacing: 1px; }
|
||||
.den-section {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
margin: 10px 0 6px; font-size: 9px; font-weight: 600;
|
||||
letter-spacing: 1.5px; text-transform: uppercase; color: #555;
|
||||
}
|
||||
.den-section::before, .den-section::after {
|
||||
content: ""; flex: 1; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(180,77,255,0.2), transparent);
|
||||
}
|
||||
.den-slider-row {
|
||||
display: flex; align-items: center; gap: 8px; margin: 5px 0;
|
||||
transition: opacity 0.3s, max-height 0.3s; overflow: hidden;
|
||||
max-height: 30px;
|
||||
}
|
||||
.den-slider-row.hidden { opacity: 0; max-height: 0; margin: 0; pointer-events: none; }
|
||||
.den-slider-label {
|
||||
min-width: 80px; font-size: 10px; color: #888;
|
||||
text-align: right; cursor: help;
|
||||
}
|
||||
.den-slider-label:hover { color: #b44dff; }
|
||||
.den-slider {
|
||||
-webkit-appearance: none; appearance: none;
|
||||
flex: 1; height: 5px; border-radius: 3px;
|
||||
background: linear-gradient(90deg, #1a1030, #3d1d6e);
|
||||
outline: none; cursor: pointer;
|
||||
}
|
||||
.den-slider:hover { background: linear-gradient(90deg, #1a1030, #5a2ea0); }
|
||||
.den-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; width: 14px; height: 14px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, #fff 30%, #b44dff 100%);
|
||||
border: none; cursor: grab;
|
||||
box-shadow: 0 0 8px rgba(180, 77, 255, 0.5);
|
||||
}
|
||||
.den-slider::-webkit-slider-thumb:hover {
|
||||
box-shadow: 0 0 14px rgba(180, 77, 255, 0.8);
|
||||
}
|
||||
.den-slider::-moz-range-thumb {
|
||||
width: 14px; height: 14px; border-radius: 50%;
|
||||
background: radial-gradient(circle, #fff 30%, #b44dff 100%);
|
||||
border: none; cursor: grab;
|
||||
box-shadow: 0 0 8px rgba(180, 77, 255, 0.5);
|
||||
}
|
||||
.den-slider::-moz-range-track {
|
||||
height: 5px; border-radius: 3px;
|
||||
background: linear-gradient(90deg, #1a1030, #3d1d6e);
|
||||
}
|
||||
.den-slider-val {
|
||||
min-width: 42px; text-align: right; font-size: 10px;
|
||||
font-family: 'Consolas','Monaco',monospace;
|
||||
color: #b44dff; font-weight: 600;
|
||||
}
|
||||
.den-tooltip { position: relative; }
|
||||
.den-tooltip::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute; bottom: 120%; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1a1a2e; color: #aaa;
|
||||
border: 1px solid rgba(180,77,255,0.3);
|
||||
padding: 4px 8px; border-radius: 6px;
|
||||
font-size: 9px; max-width: 220px;
|
||||
white-space: normal; pointer-events: none;
|
||||
opacity: 0; transition: opacity 0.2s; z-index: 999; line-height: 1.3;
|
||||
}
|
||||
.den-tooltip:hover::after { opacity: 1; }
|
||||
.den-slider.green { background: linear-gradient(90deg, #0a1a10, #0d4a2a); }
|
||||
.den-slider.green:hover { background: linear-gradient(90deg, #0a1a10, #1a6040); }
|
||||
.den-slider.green::-webkit-slider-thumb {
|
||||
background: radial-gradient(circle, #fff 30%, #00ff88 100%);
|
||||
box-shadow: 0 0 8px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
.den-slider-row.green .den-slider-val { color: #00ff88; }
|
||||
.den-slider.orange { background: linear-gradient(90deg, #1a1008, #6e3d1d); }
|
||||
.den-slider.orange:hover { background: linear-gradient(90deg, #1a1008, #a05a2e); }
|
||||
.den-slider.orange::-webkit-slider-thumb {
|
||||
background: radial-gradient(circle, #fff 30%, #ff9f43 100%);
|
||||
box-shadow: 0 0 8px rgba(255, 159, 67, 0.5);
|
||||
}
|
||||
.den-slider-row.orange .den-slider-val { color: #ff9f43; }
|
||||
.den-sharpen-modes {
|
||||
display: flex; gap: 4px; justify-content: center; margin-bottom: 6px;
|
||||
}
|
||||
.den-sharpen-btn {
|
||||
padding: 4px 9px; border: 1px solid rgba(255, 159, 67, 0.25);
|
||||
border-radius: 14px; background: rgba(255, 159, 67, 0.06);
|
||||
color: #999; cursor: pointer; font-size: 10px; font-weight: 500;
|
||||
transition: all 0.25s ease; outline: none; white-space: nowrap;
|
||||
}
|
||||
.den-sharpen-btn:hover {
|
||||
background: rgba(255, 159, 67, 0.15); color: #ccc;
|
||||
border-color: rgba(255, 159, 67, 0.5);
|
||||
}
|
||||
.den-sharpen-btn.active {
|
||||
background: rgba(255, 159, 67, 0.25); border-color: #ff9f43;
|
||||
color: #fff; box-shadow: 0 0 10px rgba(255, 159, 67, 0.3);
|
||||
}
|
||||
.den-sharpen-btn.off-mode.active {
|
||||
background: rgba(100,100,100,0.15); border-color: #666;
|
||||
color: #888; box-shadow: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/* ── Build DOM ───────────────────────────────────────────────────────── */
|
||||
function buildUI() {
|
||||
const el = document.createElement("div");
|
||||
el.className = "den-root";
|
||||
el.innerHTML = `
|
||||
<div class="den-header">
|
||||
<div class="den-title">Advanced Image Denoiser</div>
|
||||
<div class="den-subtitle">6 Algorithms • LAB Control • Sharpening</div>
|
||||
</div>
|
||||
<div class="den-methods" id="den-methods"></div>
|
||||
<div class="den-info" id="den-info"></div>
|
||||
<div class="den-section">Core Settings</div>
|
||||
<div id="den-core"></div>
|
||||
<div class="den-section" id="den-ch-section">Channel Control</div>
|
||||
<div id="den-channels"></div>
|
||||
<div class="den-section" id="den-method-section">Method Settings</div>
|
||||
<div id="den-method-params"></div>
|
||||
<div class="den-section">Blending</div>
|
||||
<div id="den-blend"></div>
|
||||
<div class="den-section">✨ Sharpening</div>
|
||||
<div class="den-sharpen-modes" id="den-sharpen-modes"></div>
|
||||
<div id="den-sharpen-params"></div>
|
||||
`;
|
||||
|
||||
const mc = el.querySelector("#den-methods");
|
||||
for (const [key, m] of Object.entries(METHODS)) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "den-method-btn" + (key === "auto_blend" ? " recommended" : "");
|
||||
btn.dataset.method = key;
|
||||
btn.textContent = `${m.icon} ${m.label}`;
|
||||
mc.appendChild(btn);
|
||||
}
|
||||
|
||||
el.querySelector("#den-core").appendChild(createSliderRow("strength"));
|
||||
el.querySelector("#den-core").appendChild(createSliderRow("preserve_detail"));
|
||||
|
||||
const ch = el.querySelector("#den-channels");
|
||||
ch.appendChild(createSliderRow("luminance_strength"));
|
||||
ch.appendChild(createSliderRow("color_strength"));
|
||||
|
||||
const mp = el.querySelector("#den-method-params");
|
||||
for (const n of ["patch_size", "search_window", "sigma_spatial", "sigma_color", "wavelet_level"])
|
||||
mp.appendChild(createSliderRow(n));
|
||||
|
||||
el.querySelector("#den-blend").appendChild(createSliderRow("blend_original", true));
|
||||
|
||||
/* Sharpen mode buttons */
|
||||
const smc = el.querySelector("#den-sharpen-modes");
|
||||
for (const [key, m] of Object.entries(SHARPEN_MODES)) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "den-sharpen-btn" + (key === "off" ? " off-mode active" : "");
|
||||
btn.dataset.smode = key;
|
||||
btn.textContent = `${m.icon} ${m.label}`;
|
||||
smc.appendChild(btn);
|
||||
}
|
||||
|
||||
/* Sharpen sliders */
|
||||
const sp = el.querySelector("#den-sharpen-params");
|
||||
sp.appendChild(createSliderRow("sharpen_amount", false, true));
|
||||
sp.appendChild(createSliderRow("sharpen_radius", false, true));
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function createSliderRow(name, green = false, orange = false) {
|
||||
const m = SLIDER_META[name];
|
||||
const row = document.createElement("div");
|
||||
const colorClass = orange ? " orange" : green ? " green" : "";
|
||||
row.className = "den-slider-row" + colorClass;
|
||||
row.id = "den-row-" + name;
|
||||
const isInt = Number.isInteger(m.step) && Number.isInteger(m.min);
|
||||
const valStr = isInt ? String(m.def) : m.def.toFixed(2);
|
||||
const sliderColor = orange ? ' orange' : green ? ' green' : '';
|
||||
row.innerHTML = `
|
||||
<span class="den-slider-label den-tooltip" data-tip="${m.desc}">${m.label}</span>
|
||||
<input type="range" class="den-slider${sliderColor}"
|
||||
id="den-s-${name}" min="${m.min}" max="${m.max}" step="${m.step}" value="${m.def}">
|
||||
<span class="den-slider-val" id="den-v-${name}">${valStr}${m.unit ? ' ' + m.unit : ''}</span>
|
||||
`;
|
||||
return row;
|
||||
}
|
||||
|
||||
function formatVal(name, val) {
|
||||
const m = SLIDER_META[name];
|
||||
const isInt = Number.isInteger(m.step) && Number.isInteger(m.min);
|
||||
const s = isInt ? String(Math.round(val)) : parseFloat(val).toFixed(2);
|
||||
return m.unit ? s + " " + m.unit : s;
|
||||
}
|
||||
|
||||
function updateInfoBox(el, method) {
|
||||
const m = METHODS[method];
|
||||
const stars = "★".repeat(m.rating) + "☆".repeat(5 - m.rating);
|
||||
const bc = method === "auto_blend" ? " recommended" : "";
|
||||
el.querySelector("#den-info").innerHTML = `
|
||||
<span class="den-info-badge${bc}">${m.badge}</span>
|
||||
<span class="den-info-stars">${stars}</span>
|
||||
<div class="den-info-desc">${m.desc}</div>
|
||||
<div class="den-info-tip">💡 ${m.tip}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateVisibility(el, method) {
|
||||
const visible = new Set(METHOD_WIDGETS[method] || []);
|
||||
for (const name of ALL_OPTIONAL) {
|
||||
const row = el.querySelector("#den-row-" + name);
|
||||
if (row) row.classList.toggle("hidden", !visible.has(name));
|
||||
}
|
||||
const chSec = el.querySelector("#den-ch-section");
|
||||
if (chSec) chSec.style.display = (visible.has("luminance_strength") || visible.has("color_strength")) ? "" : "none";
|
||||
const mSec = el.querySelector("#den-method-section");
|
||||
if (mSec) mSec.style.display = ["patch_size", "search_window", "sigma_spatial", "sigma_color", "wavelet_level"].some(n => visible.has(n)) ? "" : "none";
|
||||
}
|
||||
|
||||
function getWidget(node, name) {
|
||||
return node.widgets?.find(w => w.name === name);
|
||||
}
|
||||
|
||||
/* ── Aggressively hide a default ComfyUI widget ─────────────────────── */
|
||||
function hideDefaultWidget(w) {
|
||||
if (!w) return;
|
||||
w.hidden = true;
|
||||
w.type = "hidden";
|
||||
w.computeSize = () => [0, -4];
|
||||
// Prevent any drawing
|
||||
w.draw = function () { };
|
||||
w.mouse = function () { return false; };
|
||||
}
|
||||
|
||||
/* ── Calculate proper node size based on visible DOM content ─────────── */
|
||||
function calcNodeSize(el, node) {
|
||||
// Count visible slider rows
|
||||
const visibleRows = el.querySelectorAll(".den-slider-row:not(.hidden)").length;
|
||||
const visibleSections = el.querySelectorAll(".den-section:not([style*='display: none'])").length;
|
||||
|
||||
// Approximate heights
|
||||
const headerH = 55; // header + subtitle
|
||||
const methodBtnsH = 35; // method tab buttons
|
||||
const infoBoxH = 80; // info panel
|
||||
const sectionH = 24; // section divider
|
||||
const sliderH = 26; // each slider row
|
||||
const paddingH = 30; // root padding + gaps
|
||||
|
||||
const domH = headerH + methodBtnsH + infoBoxH
|
||||
+ (visibleSections * sectionH)
|
||||
+ (visibleRows * sliderH)
|
||||
+ paddingH;
|
||||
|
||||
const totalH = NODE_TITLE_HEIGHT + domH + 10;
|
||||
const currentW = Math.max(node.size?.[0] || 0, NODE_MIN_WIDTH);
|
||||
|
||||
return [currentW, totalH];
|
||||
}
|
||||
|
||||
/* ── Extension ───────────────────────────────────────────────────────── */
|
||||
app.registerExtension({
|
||||
name: "AdvancedImageDenoiser.PremiumUI",
|
||||
|
||||
nodeCreated(node) {
|
||||
if (node.comfyClass !== "AdvancedImageDenoiser") return;
|
||||
injectCSS();
|
||||
|
||||
const paramNames = ["method", "strength", "preserve_detail",
|
||||
"luminance_strength", "color_strength", "patch_size",
|
||||
"search_window", "sigma_spatial", "sigma_color",
|
||||
"wavelet_level", "blend_original",
|
||||
"sharpen_mode", "sharpen_amount", "sharpen_radius"];
|
||||
|
||||
/* Aggressively hide all default parameter widgets */
|
||||
for (const name of paramNames) {
|
||||
hideDefaultWidget(getWidget(node, name));
|
||||
}
|
||||
|
||||
/* Build custom UI */
|
||||
const el = buildUI();
|
||||
|
||||
/* Add DOM widget — serialize:false so it doesn't conflict with hidden widgets */
|
||||
node.addDOMWidget("denoiser_ui", "customwidget", el, {
|
||||
serialize: false,
|
||||
});
|
||||
|
||||
/* Force minimum width */
|
||||
if (!node.size || node.size[0] < NODE_MIN_WIDTH) {
|
||||
node.size = [NODE_MIN_WIDTH, 600];
|
||||
}
|
||||
|
||||
/* ── Sync: sliders → hidden widgets ─────────────────────── */
|
||||
for (const name of Object.keys(SLIDER_META)) {
|
||||
const slider = el.querySelector("#den-s-" + name);
|
||||
if (!slider) continue;
|
||||
slider.addEventListener("input", () => {
|
||||
const w = getWidget(node, name);
|
||||
if (w) { w.value = parseFloat(slider.value); if (w.callback) w.callback(w.value); }
|
||||
const ve = el.querySelector("#den-v-" + name);
|
||||
if (ve) ve.textContent = formatVal(name, slider.value);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Method buttons → hidden widget + UI update ─────────── */
|
||||
const methodBtns = el.querySelectorAll(".den-method-btn");
|
||||
methodBtns.forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const method = btn.dataset.method;
|
||||
const w = getWidget(node, "method");
|
||||
if (w) { w.value = method; if (w.callback) w.callback(method); }
|
||||
methodBtns.forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
updateInfoBox(el, method);
|
||||
updateVisibility(el, method);
|
||||
|
||||
/* Resize: preserve current width, only adjust height */
|
||||
requestAnimationFrame(() => {
|
||||
const newSize = calcNodeSize(el, node);
|
||||
node.setSize(newSize);
|
||||
node.setDirtyCanvas(true, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Sharpen mode buttons → hidden widget ─────────────── */
|
||||
const sharpenBtns = el.querySelectorAll(".den-sharpen-btn");
|
||||
function updateSharpenVisibility(mode) {
|
||||
const paramsEl = el.querySelector("#den-sharpen-params");
|
||||
if (paramsEl) paramsEl.style.display = (mode === "off") ? "none" : "";
|
||||
}
|
||||
sharpenBtns.forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const mode = btn.dataset.smode;
|
||||
const w = getWidget(node, "sharpen_mode");
|
||||
if (w) { w.value = mode; if (w.callback) w.callback(mode); }
|
||||
sharpenBtns.forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
updateSharpenVisibility(mode);
|
||||
requestAnimationFrame(() => {
|
||||
const newSize = calcNodeSize(el, node);
|
||||
node.setSize(newSize);
|
||||
node.setDirtyCanvas(true, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Sync: hidden widgets → DOM (workflow load) ─────────── */
|
||||
function syncFromWidgets() {
|
||||
for (const name of Object.keys(SLIDER_META)) {
|
||||
const w = getWidget(node, name);
|
||||
const slider = el.querySelector("#den-s-" + name);
|
||||
const ve = el.querySelector("#den-v-" + name);
|
||||
if (w && slider) {
|
||||
slider.value = w.value;
|
||||
if (ve) ve.textContent = formatVal(name, w.value);
|
||||
}
|
||||
}
|
||||
const mw = getWidget(node, "method");
|
||||
const cur = mw?.value || "auto_blend";
|
||||
methodBtns.forEach(b => b.classList.toggle("active", b.dataset.method === cur));
|
||||
updateInfoBox(el, cur);
|
||||
updateVisibility(el, cur);
|
||||
/* Sync sharpen mode */
|
||||
const sw = getWidget(node, "sharpen_mode");
|
||||
const curS = sw?.value || "off";
|
||||
sharpenBtns.forEach(b => b.classList.toggle("active", b.dataset.smode === curS));
|
||||
updateSharpenVisibility(curS);
|
||||
}
|
||||
|
||||
/* Hook onConfigure (workflow load) */
|
||||
const origConfigure = node.onConfigure;
|
||||
node.onConfigure = function (info) {
|
||||
origConfigure?.apply(this, arguments);
|
||||
setTimeout(() => {
|
||||
syncFromWidgets();
|
||||
const sz = calcNodeSize(el, node);
|
||||
node.setSize(sz);
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}, 80);
|
||||
};
|
||||
|
||||
/* Initial sync + size */
|
||||
setTimeout(() => {
|
||||
syncFromWidgets();
|
||||
const sz = calcNodeSize(el, node);
|
||||
node.setSize(sz);
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}, 150);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Advanced Image Denoiser — ComfyUI Custom Node
|
||||
==============================================
|
||||
6 denoising algorithms with LAB-space luminance/chrominance control
|
||||
and high-frequency detail preservation.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import cv2
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def tensor_to_numpy(tensor):
|
||||
"""ComfyUI IMAGE [B,H,W,C] float32 0–1 → numpy [H,W,C] uint8 RGB."""
|
||||
return np.clip(tensor[0].cpu().numpy() * 255.0, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def numpy_to_tensor(img):
|
||||
"""numpy [H,W,C] uint8 → ComfyUI IMAGE [1,H,W,C] float32 0–1."""
|
||||
return torch.from_numpy(img.astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
|
||||
def ensure_odd(n, minimum=1):
|
||||
"""Clamp to >= minimum and ensure odd."""
|
||||
n = max(minimum, int(n))
|
||||
return n if n % 2 == 1 else n + 1
|
||||
|
||||
|
||||
def restore_detail(original, denoised, amount):
|
||||
"""Blend high-frequency detail from original back into denoised result."""
|
||||
if amount <= 0.0:
|
||||
return denoised
|
||||
orig_f = original.astype(np.float32)
|
||||
den_f = denoised.astype(np.float32)
|
||||
blur = cv2.GaussianBlur(orig_f, (0, 0), sigmaX=3.0)
|
||||
detail = orig_f - blur
|
||||
return np.clip(den_f + detail * amount, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
# ── Per-Channel Denoising (for LAB split) ─────────────────────────────────
|
||||
|
||||
def denoise_channel(ch, method, strength, **p):
|
||||
"""Denoise a single grayscale channel."""
|
||||
if strength <= 0.01:
|
||||
return ch.copy()
|
||||
|
||||
if method == "nlm":
|
||||
h = max(1.0, strength * 30.0)
|
||||
tw = ensure_odd(p.get("patch_size", 7), 3)
|
||||
sw = ensure_odd(p.get("search_window", 21), 7)
|
||||
return cv2.fastNlMeansDenoising(
|
||||
ch, None, h=h, templateWindowSize=tw, searchWindowSize=sw
|
||||
)
|
||||
|
||||
elif method == "bilateral":
|
||||
d = max(1, int(5 + strength * 20))
|
||||
sc = p.get("sigma_color", 75.0)
|
||||
ss = p.get("sigma_spatial", 75.0)
|
||||
return cv2.bilateralFilter(ch, d, sc, ss)
|
||||
|
||||
elif method == "gaussian":
|
||||
ks = ensure_odd(int(strength * 12), 1)
|
||||
sigma = max(0.1, strength * 5.0)
|
||||
return cv2.GaussianBlur(ch, (ks, ks), sigmaX=sigma)
|
||||
|
||||
elif method == "median":
|
||||
ks = ensure_odd(int(strength * 10), 3)
|
||||
ks = min(ks, 31)
|
||||
return cv2.medianBlur(ch, ks)
|
||||
|
||||
return ch.copy()
|
||||
|
||||
|
||||
def denoise_lab_split(img_bgr, method, lum_str, chroma_str, **params):
|
||||
"""Denoise in LAB space with separate luminance/chrominance strength."""
|
||||
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
|
||||
L, A, B = cv2.split(lab)
|
||||
|
||||
L = denoise_channel(L, method, lum_str, **params)
|
||||
A = denoise_channel(A, method, chroma_str, **params)
|
||||
B = denoise_channel(B, method, chroma_str, **params)
|
||||
|
||||
return cv2.cvtColor(cv2.merge([L, A, B]), cv2.COLOR_LAB2BGR)
|
||||
|
||||
|
||||
# ── Full-Image Denoise Methods ───────────────────────────────────────────
|
||||
|
||||
def denoise_nlm_color(img_bgr, lum_str, chroma_str, patch_size, search_window):
|
||||
"""Non-Local Means with native luminance/chrominance control.
|
||||
Uses positional args for OpenCV 4.13+ compatibility."""
|
||||
h = max(1.0, lum_str * 30.0)
|
||||
h_c = max(1.0, chroma_str * 30.0)
|
||||
tw = ensure_odd(patch_size, 3)
|
||||
sw = ensure_odd(search_window, 7)
|
||||
# Positional args: src, dst, h, hForColorComponents, templateWindowSize, searchWindowSize
|
||||
return cv2.fastNlMeansDenoisingColored(img_bgr, None, h, h_c, tw, sw)
|
||||
|
||||
|
||||
def denoise_bilateral_full(img_bgr, strength, sigma_spatial, sigma_color):
|
||||
"""Bilateral filter on full BGR image."""
|
||||
d = max(1, int(5 + strength * 20))
|
||||
return cv2.bilateralFilter(img_bgr, d, sigma_color, sigma_spatial)
|
||||
|
||||
|
||||
def denoise_wavelet_full(img_bgr, strength, wavelet_level):
|
||||
"""Wavelet denoising (requires scikit-image; falls back to Gaussian)."""
|
||||
try:
|
||||
from skimage.restoration import denoise_wavelet as _sk_wav
|
||||
from skimage import img_as_float, img_as_ubyte
|
||||
|
||||
rgb_f = img_as_float(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
|
||||
sigma = max(0.001, strength * 0.3)
|
||||
|
||||
out = _sk_wav(
|
||||
rgb_f, method="BayesShrink", mode="soft",
|
||||
wavelet_levels=wavelet_level, sigma=sigma,
|
||||
channel_axis=-1, rescale_sigma=True
|
||||
)
|
||||
return cv2.cvtColor(img_as_ubyte(np.clip(out, 0, 1)), cv2.COLOR_RGB2BGR)
|
||||
|
||||
except ImportError:
|
||||
print("[AdvancedDenoiser] scikit-image not available — Gaussian fallback")
|
||||
ks = ensure_odd(int(strength * 12), 1)
|
||||
return cv2.GaussianBlur(img_bgr, (ks, ks), sigmaX=max(0.1, strength * 5.0))
|
||||
|
||||
|
||||
def denoise_gaussian_full(img_bgr, strength):
|
||||
"""Simple Gaussian blur."""
|
||||
if strength <= 0.01:
|
||||
return img_bgr.copy()
|
||||
ks = ensure_odd(int(strength * 12), 1)
|
||||
return cv2.GaussianBlur(img_bgr, (ks, ks), sigmaX=max(0.1, strength * 5.0))
|
||||
|
||||
|
||||
def denoise_median_full(img_bgr, strength):
|
||||
"""Median filter for impulse noise."""
|
||||
if strength <= 0.01:
|
||||
return img_bgr.copy()
|
||||
ks = ensure_odd(int(strength * 10), 3)
|
||||
ks = min(ks, 31)
|
||||
return cv2.medianBlur(img_bgr, ks)
|
||||
|
||||
|
||||
def denoise_auto_blend(img_bgr, strength, lum_str, chroma_str,
|
||||
preserve_detail, patch_size, search_window,
|
||||
sigma_spatial, sigma_color):
|
||||
"""Smart NLM + Bilateral blend weighted by preserve_detail."""
|
||||
nlm = denoise_nlm_color(img_bgr, lum_str, chroma_str, patch_size, search_window)
|
||||
bilateral = denoise_lab_split(
|
||||
img_bgr, "bilateral", lum_str, chroma_str,
|
||||
sigma_spatial=sigma_spatial, sigma_color=sigma_color
|
||||
)
|
||||
a = preserve_detail # 0 → full NLM, 1 → full bilateral
|
||||
return cv2.addWeighted(nlm, 1.0 - a, bilateral, a, 0)
|
||||
|
||||
|
||||
# ── Sharpening ───────────────────────────────────────────────────────────
|
||||
|
||||
def sharpen_unsharp_mask(img_bgr, amount, radius):
|
||||
"""Classic unsharp mask sharpening.
|
||||
amount: strength 0-1, radius: blur kernel sigma."""
|
||||
if amount <= 0.01:
|
||||
return img_bgr
|
||||
sigma = max(0.5, radius * 3.0)
|
||||
blurred = cv2.GaussianBlur(img_bgr.astype(np.float32), (0, 0), sigmaX=sigma)
|
||||
sharpened = img_bgr.astype(np.float32) + amount * 2.0 * (img_bgr.astype(np.float32) - blurred)
|
||||
return np.clip(sharpened, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def sharpen_high_pass(img_bgr, amount, radius):
|
||||
"""High-pass filter sharpening — great for fine micro-detail.
|
||||
Extracts high frequencies and adds them back."""
|
||||
if amount <= 0.01:
|
||||
return img_bgr
|
||||
sigma = max(0.5, radius * 5.0)
|
||||
img_f = img_bgr.astype(np.float32)
|
||||
low_pass = cv2.GaussianBlur(img_f, (0, 0), sigmaX=sigma)
|
||||
high_pass = img_f - low_pass
|
||||
result = img_f + high_pass * amount * 3.0
|
||||
return np.clip(result, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def sharpen_luminance_only(img_bgr, amount, radius):
|
||||
"""Sharpen only the luminance channel (LAB L) to avoid color fringing."""
|
||||
if amount <= 0.01:
|
||||
return img_bgr
|
||||
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
|
||||
L, A, B = cv2.split(lab)
|
||||
sigma = max(0.5, radius * 3.0)
|
||||
L_f = L.astype(np.float32)
|
||||
blurred = cv2.GaussianBlur(L_f, (0, 0), sigmaX=sigma)
|
||||
L_sharp = L_f + amount * 2.0 * (L_f - blurred)
|
||||
L_out = np.clip(L_sharp, 0, 255).astype(np.uint8)
|
||||
return cv2.cvtColor(cv2.merge([L_out, A, B]), cv2.COLOR_LAB2BGR)
|
||||
|
||||
|
||||
def apply_sharpening(img_bgr, sharpen_mode, sharpen_amount, sharpen_radius):
|
||||
"""Dispatch to the correct sharpening method."""
|
||||
if sharpen_mode == "off" or sharpen_amount <= 0.01:
|
||||
return img_bgr
|
||||
if sharpen_mode == "unsharp_mask":
|
||||
return sharpen_unsharp_mask(img_bgr, sharpen_amount, sharpen_radius)
|
||||
elif sharpen_mode == "high_pass":
|
||||
return sharpen_high_pass(img_bgr, sharpen_amount, sharpen_radius)
|
||||
elif sharpen_mode == "luminance_only":
|
||||
return sharpen_luminance_only(img_bgr, sharpen_amount, sharpen_radius)
|
||||
return img_bgr
|
||||
|
||||
|
||||
# ── ComfyUI Node ─────────────────────────────────────────────────────────
|
||||
|
||||
class AdvancedImageDenoiser:
|
||||
"""🧹 Advanced Image Denoiser"""
|
||||
|
||||
METHODS = [
|
||||
"auto_blend",
|
||||
"non_local_means",
|
||||
"bilateral",
|
||||
"wavelet",
|
||||
"gaussian",
|
||||
"median",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"method": (cls.METHODS, {"default": "auto_blend"}),
|
||||
"strength": ("FLOAT", {
|
||||
"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
"preserve_detail": ("FLOAT", {
|
||||
"default": 0.70, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"luminance_strength": ("FLOAT", {
|
||||
"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
"color_strength": ("FLOAT", {
|
||||
"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
"patch_size": ("INT", {
|
||||
"default": 7, "min": 3, "max": 15, "step": 2,
|
||||
}),
|
||||
"search_window": ("INT", {
|
||||
"default": 21, "min": 7, "max": 35, "step": 2,
|
||||
}),
|
||||
"sigma_spatial": ("FLOAT", {
|
||||
"default": 75.0, "min": 1.0, "max": 300.0, "step": 1.0,
|
||||
}),
|
||||
"sigma_color": ("FLOAT", {
|
||||
"default": 75.0, "min": 1.0, "max": 300.0, "step": 1.0,
|
||||
}),
|
||||
"wavelet_level": ("INT", {
|
||||
"default": 2, "min": 1, "max": 5, "step": 1,
|
||||
}),
|
||||
"blend_original": ("FLOAT", {
|
||||
"default": 0.00, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
"sharpen_mode": (["off", "unsharp_mask", "high_pass", "luminance_only"], {
|
||||
"default": "off",
|
||||
}),
|
||||
"sharpen_amount": ("FLOAT", {
|
||||
"default": 0.00, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
"sharpen_radius": ("FLOAT", {
|
||||
"default": 0.30, "min": 0.05, "max": 1.0, "step": 0.01,
|
||||
"display": "slider",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "denoise"
|
||||
CATEGORY = "image/denoising"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Advanced image denoising with 6 algorithms, separate luminance/"
|
||||
"chrominance control, detail preservation, and fine-detail sharpening."
|
||||
)
|
||||
|
||||
def denoise(self, image, method, strength, preserve_detail,
|
||||
luminance_strength=0.5, color_strength=0.5,
|
||||
patch_size=7, search_window=21,
|
||||
sigma_spatial=75.0, sigma_color=75.0,
|
||||
wavelet_level=2, blend_original=0.0,
|
||||
sharpen_mode="off", sharpen_amount=0.0,
|
||||
sharpen_radius=0.3):
|
||||
"""Process each image in the batch."""
|
||||
|
||||
results = []
|
||||
for i in range(image.shape[0]):
|
||||
img_np = tensor_to_numpy(image[i:i + 1]) # [H,W,C] RGB u8
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
||||
original = img_bgr.copy()
|
||||
|
||||
# ── dispatch ──
|
||||
if method == "non_local_means":
|
||||
# NLM natively supports separate lum/chroma via h params
|
||||
out = denoise_nlm_color(
|
||||
img_bgr, luminance_strength, color_strength,
|
||||
patch_size, search_window
|
||||
)
|
||||
|
||||
elif method == "bilateral":
|
||||
# Per-channel LAB split for separate lum/chroma control
|
||||
out = denoise_lab_split(
|
||||
img_bgr, "bilateral", luminance_strength, color_strength,
|
||||
sigma_spatial=sigma_spatial, sigma_color=sigma_color
|
||||
)
|
||||
|
||||
elif method == "wavelet":
|
||||
out = denoise_wavelet_full(img_bgr, strength, wavelet_level)
|
||||
|
||||
elif method == "gaussian":
|
||||
out = denoise_lab_split(
|
||||
img_bgr, "gaussian", luminance_strength, color_strength
|
||||
)
|
||||
|
||||
elif method == "median":
|
||||
out = denoise_lab_split(
|
||||
img_bgr, "median", luminance_strength, color_strength
|
||||
)
|
||||
|
||||
elif method == "auto_blend":
|
||||
out = denoise_auto_blend(
|
||||
img_bgr, strength, luminance_strength, color_strength,
|
||||
preserve_detail, patch_size, search_window,
|
||||
sigma_spatial, sigma_color
|
||||
)
|
||||
else:
|
||||
out = img_bgr
|
||||
|
||||
# ── detail preservation ──
|
||||
if preserve_detail > 0.0 and method != "auto_blend":
|
||||
out = restore_detail(original, out, preserve_detail)
|
||||
|
||||
# ── blend with original ──
|
||||
if blend_original > 0.0:
|
||||
out = cv2.addWeighted(
|
||||
out, 1.0 - blend_original, original, blend_original, 0
|
||||
)
|
||||
|
||||
# ── sharpening (post-denoise) ──
|
||||
if sharpen_mode != "off" and sharpen_amount > 0.01:
|
||||
out = apply_sharpening(out, sharpen_mode, sharpen_amount, sharpen_radius)
|
||||
|
||||
# ── back to RGB tensor ──
|
||||
out_rgb = cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
|
||||
results.append(numpy_to_tensor(out_rgb))
|
||||
|
||||
return (torch.cat(results, dim=0),)
|
||||
@@ -0,0 +1,3 @@
|
||||
opencv-python>=4.8.0
|
||||
scikit-image>=0.21.0
|
||||
numpy>=1.24.0
|
||||
Reference in New Issue
Block a user