Features: - 40+ image processing algorithms - Traditional denoising: Wavelet, NLM, BM3D (GPU-accelerated) - AI models: DnCNN, SwinIR, SCUNet, NAFNet, Noise-DA - DeepInv diffusion denoising integration - Advanced film grain detection and processing - Frequency domain processing - Richardson-Lucy deconvolution - Wiener filter - External DeepInv service with FastAPI - Comprehensive documentation and guides
20 KiB
PyTorch-BM3D GPU Implementation Plan
Date: 2025-10-13
Goal: Add GPU-accelerated BM3D node using pytorch-bm3d library
Status: 🟢 FEASIBLE - Ready for implementation
Overview
Create a new, separate node called "BM3D GPU Denoise (Eric)" that uses the PyTorch-BM3D library for GPU-accelerated denoising, while keeping the existing CPU-based BM3D node unchanged.
Performance Target: 0.3s on GPU vs 5-10s on CPU = 15-30x speedup
Library Analysis
pytorch-bm3d Repository
- URL: https://github.com/lizhihao6/pytorch-bm3d
- Stars: 7 (small but functional)
- Status: Active, last updated 1 year ago
- Base: Fork of academic bm3d-gpu (Honzátko & Kruliš, 2017)
- License: Check LICENSE.md (likely academic/non-commercial)
Technical Specifications
Language Breakdown:
- C++: 51.6%
- CUDA: 44.5%
- Python: 3.7%
- Shell: 0.2%
Dependencies:
# Build requirements
export CUDA_HOME=/usr/local/cuda
torch>=1.10.1
python>=3.8
CUDA>=11.1
libraries: cufft, cudart, png
API:
from pytorch_bm3d import BM3D
# Initialize
bm3d = BM3D(two_step=True) # two_step = hard_threshold + wiener
# Forward pass
output = bm3d(input, variance=variance)
# Input requirements:
# - Shape: [1, channels, H, W] (batch must be 1)
# - Type: torch.int (NOT float!)
# - Range: 0-255 scale (multiply by 255)
# - Device: .cuda() for GPU processing
Implementation Strategy
1. New Node Structure
File: nodes/bm3d_gpu_denoise_node.py
Script: scripts/bm3d_gpu_denoise.py
Display Name: "BM3D GPU Denoise (Eric)"
Category: "Eric's Image Processing/GPU Accelerated Denoisers"
2. Key Design Decisions
✅ Keep Separate from CPU Node
- Don't modify existing
bm3d_denoise.pyorbm3d_denoise_node.py - Create completely new files for GPU version
- Users can choose CPU or GPU based on needs
✅ Handle Library Availability Gracefully
try:
from pytorch_bm3d import BM3D
PYTORCH_BM3D_AVAILABLE = True
except ImportError:
PYTORCH_BM3D_AVAILABLE = False
BM3D = None
✅ Tensor Format Conversion
# ComfyUI format: [N, H, W, C] float32 [0-1]
comfyui_tensor = ...
# Convert to PyTorch-BM3D format: [1, C, H, W] int [0-255]
pytorch_tensor = comfyui_tensor.permute(0, 3, 1, 2) # [N, C, H, W]
pytorch_tensor = (pytorch_tensor * 255).to(torch.int32)
pytorch_tensor = pytorch_tensor.cuda() # Move to GPU
# Process
output = bm3d(pytorch_tensor, variance=sigma_squared_255)
# Convert back to ComfyUI format
output = output.float() / 255.0
output = output.permute(0, 2, 3, 1) # [N, H, W, C]
✅ Handle Batch Size Limitation
PyTorch-BM3D only supports batch_size=1:
if batch_size > 1:
# Process frame by frame
results = []
for i in range(batch_size):
frame = batch[i:i+1]
result = process_single_frame(frame)
results.append(result)
return torch.cat(results, dim=0)
✅ Variance vs Sigma Parameter
# User provides: sigma (0-1 scale, e.g., 0.05 = 5% noise)
# PyTorch-BM3D expects: variance on 255 scale
sigma_255 = sigma * 255 # e.g., 0.05 * 255 = 12.75
variance_255 = sigma_255 ** 2 # e.g., 12.75^2 = 162.56
Detailed Implementation
Script: scripts/bm3d_gpu_denoise.py
"""
GPU-Accelerated BM3D Denoising using PyTorch-BM3D
Provides 15-30x speedup over CPU implementation
"""
import torch
import numpy as np
from typing import Optional, Tuple, Dict, Any, Union, List
import warnings
try:
from pytorch_bm3d import BM3D
PYTORCH_BM3D_AVAILABLE = True
except ImportError:
PYTORCH_BM3D_AVAILABLE = False
BM3D = None
warnings.warn(
"pytorch-bm3d not available. Install with: "
"git clone https://github.com/lizhihao6/pytorch-bm3d && "
"cd pytorch-bm3d && sh install.sh"
)
class BM3DGPUProcessor:
"""
GPU-accelerated BM3D processor using PyTorch-BM3D library
Provides significant speedup over CPU implementation:
- CPU BM3D: 5-10 seconds for 1080p
- GPU BM3D: 0.3 seconds for 1080p (15-30x faster)
Limitations:
- Requires CUDA-capable GPU
- Only supports batch_size=1 per inference
- Requires pytorch-bm3d installation
"""
def __init__(self):
"""Initialize GPU BM3D processor"""
if not PYTORCH_BM3D_AVAILABLE:
raise ImportError(
"pytorch-bm3d library not available. "
"Install from: https://github.com/lizhihao6/pytorch-bm3d"
)
if not torch.cuda.is_available():
raise RuntimeError("CUDA not available. GPU BM3D requires CUDA-capable GPU.")
# Initialize BM3D with two-step processing (highest quality)
self.bm3d = BM3D(two_step=True)
self.device = torch.device('cuda')
def denoise(
self,
image: torch.Tensor,
sigma: Optional[float] = None,
two_step: bool = True
) -> Tuple[torch.Tensor, Dict[str, Any]]:
"""
Denoise image using GPU-accelerated BM3D
Args:
image: Input tensor [N, H, W, C] in range [0, 1] (ComfyUI format)
sigma: Noise standard deviation (0-1 scale, e.g., 0.05 = 5%)
If None, will be estimated
two_step: Use two-step BM3D (hard threshold + Wiener)
True = highest quality, False = faster
Returns:
Tuple of (denoised_tensor, info_dict)
"""
try:
batch_size, height, width, channels = image.shape
# Estimate noise if not provided
if sigma is None:
sigma = self._estimate_noise(image)
# Process each batch item separately (pytorch-bm3d limitation)
results = []
processing_times = []
for i in range(batch_size):
frame = image[i:i+1] # Keep batch dimension
# Time processing
import time
start_time = time.time()
denoised_frame = self._denoise_single_frame(frame, sigma, two_step)
elapsed = time.time() - start_time
processing_times.append(elapsed)
results.append(denoised_frame)
# Concatenate results
denoised = torch.cat(results, dim=0)
# Calculate metrics
info = {
'sigma_used': sigma,
'two_step': two_step,
'device': 'cuda',
'batch_size': batch_size,
'processing_time_per_frame': np.mean(processing_times),
'total_processing_time': sum(processing_times),
'input_shape': list(image.shape),
'output_shape': list(denoised.shape)
}
# Calculate PSNR if possible
try:
from skimage.metrics import peak_signal_noise_ratio
psnr = peak_signal_noise_ratio(
image.cpu().numpy(),
denoised.cpu().numpy(),
data_range=1.0
)
info['psnr'] = float(psnr)
except:
pass
return denoised, info
except Exception as e:
raise RuntimeError(f"GPU BM3D processing failed: {e}")
def _denoise_single_frame(
self,
frame: torch.Tensor,
sigma: float,
two_step: bool
) -> torch.Tensor:
"""
Denoise a single frame (batch_size=1)
Args:
frame: [1, H, W, C] tensor in [0, 1]
sigma: Noise std in [0, 1] scale
two_step: Use two-step processing
Returns:
Denoised frame [1, H, W, C] in [0, 1]
"""
# Convert ComfyUI format to PyTorch-BM3D format
# ComfyUI: [1, H, W, C] float [0-1]
# PyTorch-BM3D: [1, C, H, W] int [0-255]
# Permute to [1, C, H, W]
pytorch_tensor = frame.permute(0, 3, 1, 2)
# Scale to [0-255] and convert to int
pytorch_tensor = (pytorch_tensor * 255.0).clamp(0, 255).to(torch.int32)
# Move to GPU
pytorch_tensor = pytorch_tensor.to(self.device)
# Calculate variance in 255 scale
sigma_255 = sigma * 255.0
variance_255 = sigma_255 ** 2
# Apply BM3D
# Note: PyTorch-BM3D processes each channel separately
with torch.no_grad():
output = self.bm3d(pytorch_tensor, variance=variance_255)
# Convert back to ComfyUI format
# [1, C, H, W] int [0-255] -> [1, H, W, C] float [0-1]
output = output.float() / 255.0
output = output.permute(0, 2, 3, 1)
output = output.clamp(0, 1)
return output
def _estimate_noise(self, image: torch.Tensor) -> float:
"""
Estimate noise standard deviation from image
Args:
image: [N, H, W, C] tensor
Returns:
Estimated sigma in [0, 1] scale
"""
try:
from skimage.restoration import estimate_sigma
# Convert first frame to numpy
img_np = image[0].cpu().numpy()
# Estimate for each channel and average
sigmas = []
for c in range(img_np.shape[2]):
sigma_c = estimate_sigma(img_np[:, :, c], multichannel=False)
sigmas.append(sigma_c)
return float(np.mean(sigmas))
except:
# Fallback to conservative estimate
return 0.02 # 2% noise
def cleanup(self):
"""Clean up GPU resources"""
if hasattr(self, 'bm3d'):
del self.bm3d
torch.cuda.empty_cache()
def get_processor_info() -> Dict[str, Any]:
"""Get information about GPU BM3D availability"""
return {
'available': PYTORCH_BM3D_AVAILABLE,
'cuda_available': torch.cuda.is_available() if PYTORCH_BM3D_AVAILABLE else False,
'cuda_device_count': torch.cuda.device_count() if torch.cuda.is_available() else 0,
'cuda_device_name': torch.cuda.get_device_name(0) if torch.cuda.is_available() else None
}
Node: nodes/bm3d_gpu_denoise_node.py
"""
GPU-Accelerated BM3D Denoising Node for ComfyUI
"""
import torch
from ..base_node import BaseImageProcessingNode
try:
from ..scripts.bm3d_gpu_denoise import BM3DGPUProcessor, PYTORCH_BM3D_AVAILABLE
except ImportError:
PYTORCH_BM3D_AVAILABLE = False
BM3DGPUProcessor = None
class BM3DGPUDenoiseNode(BaseImageProcessingNode):
"""
GPU-accelerated BM3D denoising node
Provides 15-30x speedup over CPU BM3D implementation
Requires CUDA-capable GPU and pytorch-bm3d installation
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"sigma": ("FLOAT", {
"default": 0.05,
"min": 0.001,
"max": 0.5,
"step": 0.001,
"display": "slider"
}),
"two_step": ("BOOLEAN", {
"default": True,
"label_on": "High Quality (2-step)",
"label_off": "Fast (1-step)"
}),
"auto_sigma": ("BOOLEAN", {
"default": False,
"label_on": "Auto-estimate noise",
"label_off": "Use manual sigma"
})
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "denoise"
CATEGORY = "Eric's Image Processing/GPU Accelerated Denoisers"
def denoise(self, image: torch.Tensor, sigma: float, two_step: bool, auto_sigma: bool):
"""
Denoise image using GPU-accelerated BM3D
"""
if not PYTORCH_BM3D_AVAILABLE:
raise RuntimeError(
"pytorch-bm3d not available. Install from: "
"https://github.com/lizhihao6/pytorch-bm3d\\n"
"Run: git clone https://github.com/lizhihao6/pytorch-bm3d && "
"cd pytorch-bm3d && sh install.sh"
)
try:
# Initialize processor
processor = BM3DGPUProcessor()
# Use auto sigma if requested
sigma_to_use = None if auto_sigma else sigma
# Process
denoised, info = processor.denoise(image, sigma=sigma_to_use, two_step=two_step)
# Print info
print(f"GPU BM3D Denoising completed:")
print(f" Sigma: {info['sigma_used']:.4f}")
print(f" Mode: {'Two-step (high quality)' if two_step else 'One-step (fast)'}")
print(f" Processing time: {info['processing_time_per_frame']:.3f}s per frame")
if 'psnr' in info:
print(f" PSNR: {info['psnr']:.2f} dB")
# Cleanup
processor.cleanup()
return (denoised,)
except Exception as e:
raise RuntimeError(f"GPU BM3D failed: {e}")
Installation Instructions
For Users
Prerequisites:
# Must have CUDA installed
export CUDA_HOME=/usr/local/cuda # or your CUDA path
# Requires PyTorch with CUDA support (should already be installed in ComfyUI)
python -c "import torch; print(torch.cuda.is_available())" # Should print True
Install pytorch-bm3d:
cd ComfyUI/custom_nodes/Eric_Image_Processing_Nodes
# Clone the library
git clone https://github.com/lizhihao6/pytorch-bm3d
# Install
cd pytorch-bm3d
bash install.sh
# Verify installation
python -c "from pytorch_bm3d import BM3D; print('Success!')"
For Development
Test the library directly:
import torch
from pytorch_bm3d import BM3D
import cv2
# Load image
img = cv2.imread('portrait.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Convert to torch format
img_t = torch.from_numpy(img).unsqueeze(0).unsqueeze(0) # [1, 1, H, W]
img_t = img_t.repeat(1, 3, 1, 1) # [1, 3, H, W] for RGB
img_t = img_t.int() * (255 // 255) # Already in 0-255
img_t = img_t.cuda()
# Denoise
bm3d = BM3D(two_step=True)
variance = (20.0) ** 2 # sigma=20 in 255 scale
output = bm3d(img_t, variance=variance)
print(f"Input shape: {img_t.shape}")
print(f"Output shape: {output.shape}")
print("Success!")
Integration into __init__.py
# In __init__.py, add new section:
# ============================================================================
# GPU-Accelerated BM3D Denoising (requires pytorch-bm3d)
# ============================================================================
try:
from .nodes.bm3d_gpu_denoise_node import BM3DGPUDenoiseNode
BM3D_GPU_AVAILABLE = True
except ImportError as e:
print(f"Warning: GPU BM3D node not available: {e}")
BM3DGPUDenoiseNode = None
BM3D_GPU_AVAILABLE = False
# Build mappings
BM3D_GPU_MAPPINGS = {}
BM3D_GPU_DISPLAY = {}
if BM3D_GPU_AVAILABLE:
BM3D_GPU_MAPPINGS = {
"BM3DGPUDenoiseNode": BM3DGPUDenoiseNode
}
BM3D_GPU_DISPLAY = {
"BM3DGPUDenoiseNode": "BM3D GPU Denoise (Eric)"
}
# Merge into global mappings
NODE_CLASS_MAPPINGS.update(BM3D_GPU_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(BM3D_GPU_DISPLAY)
Testing Strategy
Test 1: Basic Functionality
# test_bm3d_gpu_basic.py
import torch
from scripts.bm3d_gpu_denoise import BM3DGPUProcessor
# Create test image
img = torch.rand(1, 512, 512, 3)
# Add noise
noisy = img + torch.randn_like(img) * 0.05
# Denoise
processor = BM3DGPUProcessor()
denoised, info = processor.denoise(noisy, sigma=0.05)
print(f"✅ Basic test passed")
print(f" Processing time: {info['processing_time_per_frame']:.3f}s")
print(f" PSNR: {info.get('psnr', 'N/A')}")
Test 2: Speed Comparison
# test_bm3d_gpu_vs_cpu.py
import time
import torch
from scripts.bm3d_denoise import BM3DProcessor # CPU version
from scripts.bm3d_gpu_denoise import BM3DGPUProcessor # GPU version
img = torch.rand(1, 1080, 1920, 3) # 1080p image
noisy = img + torch.randn_like(img) * 0.05
# CPU version
cpu_proc = BM3DProcessor()
start = time.time()
cpu_result, _ = cpu_proc.denoise_color(noisy[0].numpy(), sigma=0.05)
cpu_time = time.time() - start
# GPU version
gpu_proc = BM3DGPUProcessor()
start = time.time()
gpu_result, _ = gpu_proc.denoise(noisy, sigma=0.05)
gpu_time = time.time() - start
speedup = cpu_time / gpu_time
print(f"CPU time: {cpu_time:.2f}s")
print(f"GPU time: {gpu_time:.2f}s")
print(f"Speedup: {speedup:.1f}x")
Test 3: Quality Comparison
Compare denoising quality between CPU and GPU versions to ensure parity.
Expected Performance
Speed Estimates (1080p image)
| Method | Time | Speedup |
|---|---|---|
| CPU BM3D (current) | 5-10s | 1x |
| GPU BM3D (pytorch) | 0.3s | 15-30x |
| DnCNN GPU (current) | 0.1s | 50x |
| SwinIR GPU (current) | 0.5s | 10-20x |
Quality Comparison
GPU BM3D should match CPU BM3D quality (both implement same algorithm), but might have minor numerical differences due to:
- Float precision (GPU uses float32 operations)
- Different DCT/wavelet implementations
- Random block matching order
Potential Issues & Solutions
Issue 1: Batch Size Limitation
Problem: pytorch-bm3d only supports batch_size=1
Solution: Process frames sequentially in the node
Impact: Still much faster than CPU for single images
Issue 2: Integer Tensor Requirement
Problem: pytorch-bm3d expects torch.int, not torch.float
Solution: Convert with .to(torch.int32) after scaling to 0-255
Impact: None, just a conversion step
Issue 3: CUDA Compilation
Problem: Requires CUDA compiler during installation
Solution: User must have CUDA toolkit installed
Impact: Installation step, documented clearly
Issue 4: Library Maintenance
Problem: pytorch-bm3d has low GitHub activity
Solution: Fork the repo if needed, or vendor the code
Impact: Low risk, code is stable
Advantages vs Current BM3D
| Aspect | CPU BM3D (current) | GPU BM3D (new) |
|---|---|---|
| Speed | 5-10s | 0.3s (15-30x faster) |
| Quality | Excellent | Same (identical algorithm) |
| Memory | System RAM | GPU VRAM |
| Batch Processing | Yes | Sequential (but fast) |
| Installation | pip install bm3d | Compile from source |
| Dependencies | Pure Python/C++ | Requires CUDA |
| NumPy 2.x Issues | Has 'refilter' bug | No issues (different library) |
Recommendation
✅ YES - Implement as New Node
Reasoning:
- Massive speedup (15-30x) makes GPU version very attractive
- No risk to existing code - completely separate node
- Optional feature - users without CUDA can still use CPU version
- Quality parity - same algorithm, same results
- Complements existing AI denoisers - gives users choice of traditional vs AI
Implementation Priority
Priority: HIGH 🔥
This would be a flagship feature:
- Addresses your CPU performance concern
- Provides best-of-both-worlds (traditional algorithm + GPU speed)
- Differentiates our node pack from others
- Relatively simple to implement (~200 lines of code)
Next Steps
- ✅ Test pytorch-bm3d installation in your environment
- ✅ Verify CUDA compatibility with your setup
- ✅ Create minimal proof-of-concept script
- ✅ Implement full node if POC works
- ✅ Document installation process
- ✅ Add to node pack with "experimental/GPU required" label
Alternative: If pytorch-bm3d Doesn't Work
If installation or compatibility issues arise, alternatives:
- Stick with CPU BM3D but add progress callbacks
- Use existing DnCNN/SwinIR for GPU denoising (already works)
- Implement simpler GPU denoiser (e.g., bilateral filter with CUDA)
- Fork pytorch-bm3d and fix any issues ourselves
Summary
Bottom Line: pytorch-bm3d is a perfect fit for our needs:
- ✅ GPU-accelerated BM3D (15-30x speedup)
- ✅ PyTorch native (easy integration)
- ✅ Academic quality (based on published research)
- ✅ Non-invasive (separate node, no risk to existing code)
- ✅ Addresses your CPU performance concern
- ✅ Relatively easy to implement
Let's do it! Would you like me to start with a proof-of-concept test script?