Instructions to use 45th/comfyui-model-pack with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use 45th/comfyui-model-pack with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("45th/comfyui-model-pack", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use 45th/comfyui-model-pack with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf 45th/comfyui-model-pack # Run inference directly in the terminal: llama cli -hf 45th/comfyui-model-pack
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf 45th/comfyui-model-pack # Run inference directly in the terminal: llama cli -hf 45th/comfyui-model-pack
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf 45th/comfyui-model-pack # Run inference directly in the terminal: ./llama-cli -hf 45th/comfyui-model-pack
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf 45th/comfyui-model-pack # Run inference directly in the terminal: ./build/bin/llama-cli -hf 45th/comfyui-model-pack
Use Docker
docker model run hf.co/45th/comfyui-model-pack
- LM Studio
- Jan
- Ollama
How to use 45th/comfyui-model-pack with Ollama:
ollama run hf.co/45th/comfyui-model-pack
- Unsloth Desktop
- Pi
How to use 45th/comfyui-model-pack with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf 45th/comfyui-model-pack
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "45th/comfyui-model-pack" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use 45th/comfyui-model-pack with Docker Model Runner:
docker model run hf.co/45th/comfyui-model-pack
- Lemonade
How to use 45th/comfyui-model-pack with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull 45th/comfyui-model-pack
Run and chat with the model
lemonade run user.comfyui-model-pack-{{QUANT_TAG}}List all available models
lemonade list
- Hermes Agent
How to use 45th/comfyui-model-pack with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf 45th/comfyui-model-pack
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default 45th/comfyui-model-pack
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use 45th/comfyui-model-pack with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf 45th/comfyui-model-pack
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "45th/comfyui-model-pack" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Upload 91 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +7 -0
- custom_nodes/ComfyUI-DyPE/src/patch_utils.py +272 -0
- custom_nodes/ComfyUI-EsesImageEffectLevels/js/image_effect_levels.js +967 -0
- custom_nodes/NeuralISP/__init__.py +10 -0
- custom_nodes/NeuralISP/__pycache__/__init__.cpython-313.pyc +0 -0
- custom_nodes/NeuralISP/__pycache__/mirnetv2_model.cpython-313.pyc +0 -0
- custom_nodes/NeuralISP/__pycache__/mirnetv2_node.cpython-313.pyc +0 -0
- custom_nodes/NeuralISP/__pycache__/neural_isp_node.cpython-313.pyc +0 -0
- custom_nodes/NeuralISP/mirnetv2_model.py +301 -0
- custom_nodes/NeuralISP/neural_isp_node.py +407 -0
- custom_nodes/comfyui-faded-branding/__init__.py +6 -0
- custom_nodes/comfyui-faded-branding/__pycache__/__init__.cpython-313.pyc +0 -0
- custom_nodes/comfyui-faded-branding/__pycache__/faded_logo_node.cpython-313.pyc +0 -0
- custom_nodes/comfyui-faded-branding/__pycache__/ocean_logo_node.cpython-313.pyc +0 -0
- custom_nodes/comfyui-faded-branding/faded_logo_node.py +30 -0
- custom_nodes/comfyui-faded-branding/js/faded_logo.js +117 -0
- custom_nodes/comfyui-faded-branding/js/logo/info03.png +0 -0
- custom_nodes/comfyui-faded-branding/js/logo/logo01.png +3 -0
- custom_nodes/comfyui-faded-branding/js/logo/logo40.png +3 -0
- custom_nodes/comfyui-faded-branding/js/logo/logo44.png +3 -0
- custom_nodes/comfyui-faded-branding/js/logo/text01.png +0 -0
- custom_nodes/comfyui_essentials/luts/realisticLUT.cube +0 -0
- custom_nodes/image_routing.py +38 -0
- custom_nodes/latent_routing.py +87 -0
- custom_nodes/latent_trick.py +109 -0
- custom_nodes/save_image_jpeg.py +124 -0
- custom_nodes/srgb_noise/__init__.py +6 -0
- custom_nodes/srgb_noise/__pycache__/__init__.cpython-313.pyc +0 -0
- custom_nodes/srgb_noise/__pycache__/srgb_noise_node.cpython-313.pyc +0 -0
- custom_nodes/srgb_noise/repo/Crop_SIDD.py +14 -0
- custom_nodes/srgb_noise/repo/README.md +94 -0
- custom_nodes/srgb_noise/repo/data/__pycache__/dataset_base.cpython-39.pyc +0 -0
- custom_nodes/srgb_noise/repo/data/__pycache__/dataset_noise_synthesis.cpython-39.pyc +0 -0
- custom_nodes/srgb_noise/repo/data/dataset_base.py +62 -0
- custom_nodes/srgb_noise/repo/data/dataset_noise_synthesis.py +54 -0
- custom_nodes/srgb_noise/repo/data_preparation/G4_test.txt +4 -0
- custom_nodes/srgb_noise/repo/data_preparation/G4_train.txt +12 -0
- custom_nodes/srgb_noise/repo/data_preparation/GP_test.txt +5 -0
- custom_nodes/srgb_noise/repo/data_preparation/GP_train.txt +28 -0
- custom_nodes/srgb_noise/repo/data_preparation/IP_test.txt +10 -0
- custom_nodes/srgb_noise/repo/data_preparation/IP_train.txt +44 -0
- custom_nodes/srgb_noise/repo/data_preparation/N6_test.txt +4 -0
- custom_nodes/srgb_noise/repo/data_preparation/N6_train.txt +18 -0
- custom_nodes/srgb_noise/repo/data_preparation/S6_test.txt +7 -0
- custom_nodes/srgb_noise/repo/data_preparation/S6_train.txt +28 -0
- custom_nodes/srgb_noise/repo/doc/baseline.png +3 -0
- custom_nodes/srgb_noise/repo/doc/quantitative_results.png +0 -0
- custom_nodes/srgb_noise/repo/doc/visual_results.png +3 -0
- custom_nodes/srgb_noise/repo/main_test.py +78 -0
- custom_nodes/srgb_noise/repo/main_train_jointly.py +449 -0
.gitattributes
CHANGED
|
@@ -34,3 +34,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
models/clip/qwen-4b-zimage-hereticV2-q8.gguf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
models/clip/qwen-4b-zimage-hereticV2-q8.gguf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
custom_nodes/comfyui-faded-branding/js/logo/logo01.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
custom_nodes/comfyui-faded-branding/js/logo/logo40.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
custom_nodes/comfyui-faded-branding/js/logo/logo44.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
custom_nodes/srgb_noise/repo/doc/baseline.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
custom_nodes/srgb_noise/repo/doc/visual_results.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
custom_nodes/srgb_noise/repo/test/imgs/clean.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
custom_nodes/srgb_noise/repo/test/imgs/noisy.png filter=lfs diff=lfs merge=lfs -text
|
custom_nodes/ComfyUI-DyPE/src/patch_utils.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import types
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
import comfy
|
| 6 |
+
from comfy.model_patcher import ModelPatcher
|
| 7 |
+
from comfy import model_sampling
|
| 8 |
+
|
| 9 |
+
from .models.flux import PosEmbedFlux
|
| 10 |
+
from .models.nunchaku import PosEmbedNunchaku
|
| 11 |
+
from .models.qwen import PosEmbedQwen
|
| 12 |
+
from .models.zimage import PosEmbedZImage
|
| 13 |
+
|
| 14 |
+
def apply_dype_to_model(model: ModelPatcher, model_type: str, width: int, height: int, method: str, yarn_alt_scaling: bool, enable_dype: bool, dype_scale: float, dype_exponent: float, base_shift: float, max_shift: float, base_resolution: int = 1024, dype_start_sigma: float = 1.0) -> ModelPatcher:
|
| 15 |
+
m = model.clone()
|
| 16 |
+
|
| 17 |
+
is_nunchaku = False
|
| 18 |
+
is_qwen = False
|
| 19 |
+
is_z_image = False
|
| 20 |
+
|
| 21 |
+
if model_type == "nunchaku":
|
| 22 |
+
is_nunchaku = True
|
| 23 |
+
elif model_type == "qwen":
|
| 24 |
+
is_qwen = True
|
| 25 |
+
elif model_type == "z_image":
|
| 26 |
+
is_z_image = True
|
| 27 |
+
elif model_type == "flux":
|
| 28 |
+
pass
|
| 29 |
+
else: # auto
|
| 30 |
+
if hasattr(m.model, "diffusion_model"):
|
| 31 |
+
dm = m.model.diffusion_model
|
| 32 |
+
model_class_name = dm.__class__.__name__
|
| 33 |
+
if "QwenImage" in model_class_name:
|
| 34 |
+
is_qwen = True
|
| 35 |
+
elif hasattr(dm, "rope_embedder"):
|
| 36 |
+
is_z_image = True
|
| 37 |
+
elif hasattr(dm, "model") and hasattr(dm.model, "pos_embed"):
|
| 38 |
+
is_nunchaku = True
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError("The provided model is not a compatible model.")
|
| 41 |
+
|
| 42 |
+
new_dype_params = (width, height, base_shift, max_shift, method, yarn_alt_scaling, base_resolution, dype_start_sigma, is_nunchaku, is_qwen, is_z_image)
|
| 43 |
+
|
| 44 |
+
should_patch_schedule = True
|
| 45 |
+
if hasattr(m.model, "_dype_params"):
|
| 46 |
+
if m.model._dype_params == new_dype_params:
|
| 47 |
+
should_patch_schedule = False
|
| 48 |
+
|
| 49 |
+
base_patch_h_tokens = None
|
| 50 |
+
base_patch_w_tokens = None
|
| 51 |
+
if is_z_image:
|
| 52 |
+
axes_lens = getattr(m.model.diffusion_model, "axes_lens", None)
|
| 53 |
+
if isinstance(axes_lens, (list, tuple)) and len(axes_lens) >= 3:
|
| 54 |
+
base_patch_h_tokens = int(axes_lens[1])
|
| 55 |
+
base_patch_w_tokens = int(axes_lens[2])
|
| 56 |
+
|
| 57 |
+
patch_size = 2
|
| 58 |
+
try:
|
| 59 |
+
if is_nunchaku:
|
| 60 |
+
patch_size = m.model.diffusion_model.model.config.patch_size
|
| 61 |
+
else:
|
| 62 |
+
patch_size = m.model.diffusion_model.patch_size
|
| 63 |
+
except:
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
if base_patch_h_tokens is not None and base_patch_w_tokens is not None:
|
| 67 |
+
derived_base_patches = max(base_patch_h_tokens, base_patch_w_tokens)
|
| 68 |
+
derived_base_seq_len = base_patch_h_tokens * base_patch_w_tokens
|
| 69 |
+
else:
|
| 70 |
+
derived_base_patches = (base_resolution // 8) // 2
|
| 71 |
+
derived_base_seq_len = derived_base_patches * derived_base_patches
|
| 72 |
+
|
| 73 |
+
if enable_dype and should_patch_schedule:
|
| 74 |
+
try:
|
| 75 |
+
if isinstance(m.model.model_sampling, model_sampling.ModelSamplingFlux) or is_qwen or is_z_image:
|
| 76 |
+
latent_h, latent_w = height // 8, width // 8
|
| 77 |
+
padded_h, padded_w = math.ceil(latent_h / patch_size) * patch_size, math.ceil(latent_w / patch_size) * patch_size
|
| 78 |
+
image_seq_len = (padded_h // patch_size) * (padded_w // patch_size)
|
| 79 |
+
|
| 80 |
+
base_seq_len = derived_base_seq_len
|
| 81 |
+
max_seq_len = image_seq_len
|
| 82 |
+
|
| 83 |
+
if max_seq_len <= base_seq_len:
|
| 84 |
+
dype_shift = base_shift
|
| 85 |
+
else:
|
| 86 |
+
slope = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
| 87 |
+
intercept = base_shift - slope * base_seq_len
|
| 88 |
+
dype_shift = image_seq_len * slope + intercept
|
| 89 |
+
|
| 90 |
+
dype_shift = max(0.0, dype_shift)
|
| 91 |
+
|
| 92 |
+
class DypeModelSamplingFlux(model_sampling.ModelSamplingFlux, model_sampling.CONST):
|
| 93 |
+
pass
|
| 94 |
+
|
| 95 |
+
new_model_sampler = DypeModelSamplingFlux(m.model.model_config)
|
| 96 |
+
new_model_sampler.set_parameters(shift=dype_shift)
|
| 97 |
+
|
| 98 |
+
m.add_object_patch("model_sampling", new_model_sampler)
|
| 99 |
+
m.model._dype_params = new_dype_params
|
| 100 |
+
except:
|
| 101 |
+
pass
|
| 102 |
+
|
| 103 |
+
elif not enable_dype:
|
| 104 |
+
if hasattr(m.model, "_dype_params"):
|
| 105 |
+
class DefaultModelSamplingFlux(model_sampling.ModelSamplingFlux, model_sampling.CONST): pass
|
| 106 |
+
default_sampler = DefaultModelSamplingFlux(m.model.model_config)
|
| 107 |
+
m.add_object_patch("model_sampling", default_sampler)
|
| 108 |
+
del m.model._dype_params
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
if is_nunchaku:
|
| 112 |
+
orig_embedder = m.model.diffusion_model.model.pos_embed
|
| 113 |
+
target_patch_path = "diffusion_model.model.pos_embed"
|
| 114 |
+
elif is_z_image:
|
| 115 |
+
orig_embedder = m.model.diffusion_model.rope_embedder
|
| 116 |
+
target_patch_path = "diffusion_model.rope_embedder"
|
| 117 |
+
else:
|
| 118 |
+
orig_embedder = m.model.diffusion_model.pe_embedder
|
| 119 |
+
target_patch_path = "diffusion_model.pe_embedder"
|
| 120 |
+
|
| 121 |
+
theta, axes_dim = orig_embedder.theta, orig_embedder.axes_dim
|
| 122 |
+
except AttributeError:
|
| 123 |
+
raise ValueError("The provided model is not a compatible FLUX/Qwen model structure.")
|
| 124 |
+
|
| 125 |
+
embedder_cls = PosEmbedFlux
|
| 126 |
+
if is_nunchaku:
|
| 127 |
+
embedder_cls = PosEmbedNunchaku
|
| 128 |
+
elif is_qwen:
|
| 129 |
+
embedder_cls = PosEmbedQwen
|
| 130 |
+
elif is_z_image:
|
| 131 |
+
embedder_cls = PosEmbedZImage
|
| 132 |
+
|
| 133 |
+
embedder_base_patches = derived_base_patches if is_z_image else None
|
| 134 |
+
|
| 135 |
+
new_pe_embedder = embedder_cls(
|
| 136 |
+
theta, axes_dim, method, yarn_alt_scaling, enable_dype,
|
| 137 |
+
dype_scale, dype_exponent, base_resolution, dype_start_sigma, embedder_base_patches
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
m.add_object_patch(target_patch_path, new_pe_embedder)
|
| 141 |
+
|
| 142 |
+
if is_z_image:
|
| 143 |
+
base_hw_override = None
|
| 144 |
+
if base_patch_h_tokens is not None and base_patch_w_tokens is not None:
|
| 145 |
+
base_hw_override = (base_patch_h_tokens, base_patch_w_tokens)
|
| 146 |
+
elif derived_base_patches is not None:
|
| 147 |
+
base_hw_override = (derived_base_patches, derived_base_patches)
|
| 148 |
+
|
| 149 |
+
if base_hw_override is not None:
|
| 150 |
+
m.model.diffusion_model._dype_base_hw = base_hw_override
|
| 151 |
+
|
| 152 |
+
def dype_patchify_and_embed(self, x, cap_feats, cap_mask, t, num_tokens, transformer_options={}, **kwargs):
|
| 153 |
+
bsz = len(x)
|
| 154 |
+
pH = pW = self.patch_size
|
| 155 |
+
device = x[0].device
|
| 156 |
+
|
| 157 |
+
if self.pad_tokens_multiple is not None:
|
| 158 |
+
pad_extra = (-cap_feats.shape[1]) % self.pad_tokens_multiple
|
| 159 |
+
if pad_extra:
|
| 160 |
+
# FIX 2: Zero padding instead of cap_pad_token due to dimension mismatch in Turbo
|
| 161 |
+
cap_pad = torch.zeros(1, 1, cap_feats.shape[-1], device=cap_feats.device, dtype=cap_feats.dtype)
|
| 162 |
+
cap_feats = torch.cat((cap_feats, cap_pad.repeat(cap_feats.shape[0], pad_extra, 1)), dim=1)
|
| 163 |
+
|
| 164 |
+
cap_pos_ids = torch.zeros(bsz, cap_feats.shape[1], 3, dtype=torch.float32, device=device)
|
| 165 |
+
cap_pos_ids[:, :, 0] = torch.arange(cap_feats.shape[1], dtype=torch.float32, device=device) + 1.0
|
| 166 |
+
|
| 167 |
+
B, C, H, W = x.shape
|
| 168 |
+
x = self.x_embedder(x.view(B, C, H // pH, pH, W // pW, pW).permute(0, 2, 4, 3, 5, 1).flatten(3).flatten(1, 2))
|
| 169 |
+
|
| 170 |
+
requested_hw = transformer_options.get("dype_requested_hw", (height, width))
|
| 171 |
+
rope_base_resolution = transformer_options.get("dype_base_resolution", base_resolution)
|
| 172 |
+
|
| 173 |
+
raw_scale_y = float(rope_base_resolution) / max(1.0, float(requested_hw[0]))
|
| 174 |
+
raw_scale_x = float(rope_base_resolution) / max(1.0, float(requested_hw[1]))
|
| 175 |
+
|
| 176 |
+
iso_scale = min(raw_scale_y, raw_scale_x)
|
| 177 |
+
rope_scale_y = iso_scale
|
| 178 |
+
rope_scale_x = iso_scale
|
| 179 |
+
|
| 180 |
+
freq_scale_factor = 1.0 / iso_scale
|
| 181 |
+
new_pe_embedder.set_scale_hint(freq_scale_factor)
|
| 182 |
+
|
| 183 |
+
h_start = 0.0
|
| 184 |
+
w_start = 0.0
|
| 185 |
+
|
| 186 |
+
original_hw = transformer_options.get("dype_original_hw")
|
| 187 |
+
if original_hw is None:
|
| 188 |
+
original_hw = (H, W)
|
| 189 |
+
|
| 190 |
+
H_tokens = math.ceil(original_hw[0] / pH)
|
| 191 |
+
W_tokens = math.ceil(original_hw[1] / pW)
|
| 192 |
+
|
| 193 |
+
token_stride_y = (original_hw[0] / max(1, H_tokens)) * rope_scale_y
|
| 194 |
+
token_stride_x = (original_hw[1] / max(1, W_tokens)) * rope_scale_x
|
| 195 |
+
|
| 196 |
+
shift_y = h_start * (original_hw[0] / max(1, H_tokens))
|
| 197 |
+
shift_x = w_start * (original_hw[1] / max(1, W_tokens))
|
| 198 |
+
|
| 199 |
+
def _build_spatial_pos_ids(batch: int, total_len: int, width_tokens: int, cap_len: int, stride_y: float, stride_x: float, h_start: float, w_start: float, device: torch.device):
|
| 200 |
+
base_pos = torch.arange(total_len, device=device, dtype=torch.float32)
|
| 201 |
+
y = torch.div(base_pos, width_tokens, rounding_mode='floor') * stride_y + h_start
|
| 202 |
+
x = torch.remainder(base_pos, width_tokens) * stride_x + w_start
|
| 203 |
+
|
| 204 |
+
pos = torch.stack([
|
| 205 |
+
torch.full_like(base_pos, cap_len + 1),
|
| 206 |
+
y,
|
| 207 |
+
x
|
| 208 |
+
], dim=-1)
|
| 209 |
+
return pos.unsqueeze(0).repeat(batch, 1, 1)
|
| 210 |
+
|
| 211 |
+
base_img_tokens = H_tokens * W_tokens
|
| 212 |
+
x_pos_ids = _build_spatial_pos_ids(bsz, base_img_tokens, W_tokens, cap_feats.shape[1], token_stride_y, token_stride_x, shift_y, shift_x, device)
|
| 213 |
+
|
| 214 |
+
if self.pad_tokens_multiple is not None:
|
| 215 |
+
pad_extra = (-x.shape[1]) % self.pad_tokens_multiple
|
| 216 |
+
if pad_extra:
|
| 217 |
+
x = torch.cat((x, self.x_pad_token.to(device=x.device, dtype=x.dtype, copy=True).unsqueeze(0).repeat(x.shape[0], pad_extra, 1)), dim=1)
|
| 218 |
+
|
| 219 |
+
if x.shape[1] != x_pos_ids.shape[1]:
|
| 220 |
+
x_pos_ids = _build_spatial_pos_ids(bsz, x.shape[1], W_tokens, cap_feats.shape[1], token_stride_y, token_stride_x, shift_y, shift_x, device)
|
| 221 |
+
|
| 222 |
+
freqs_cis = self.rope_embedder(torch.cat((cap_pos_ids, x_pos_ids), dim=1)).movedim(1, 2)
|
| 223 |
+
|
| 224 |
+
# FIX 3: Missing cap_embedder projection to map text encoder dimension to model's hidden dimension
|
| 225 |
+
cap_feats = self.cap_embedder(cap_feats)
|
| 226 |
+
|
| 227 |
+
for layer in self.context_refiner:
|
| 228 |
+
cap_feats = layer(cap_feats, cap_mask, freqs_cis[:, :cap_pos_ids.shape[1]], transformer_options=transformer_options)
|
| 229 |
+
|
| 230 |
+
padded_img_mask = None
|
| 231 |
+
for layer in self.noise_refiner:
|
| 232 |
+
x = layer(x, padded_img_mask, freqs_cis[:, cap_pos_ids.shape[1]:], t, transformer_options=transformer_options)
|
| 233 |
+
|
| 234 |
+
padded_full_embed = torch.cat((cap_feats, x), dim=1)
|
| 235 |
+
mask = None
|
| 236 |
+
img_sizes = [(H, W)] * bsz
|
| 237 |
+
l_effective_cap_len = [cap_feats.shape[1]] * bsz
|
| 238 |
+
|
| 239 |
+
# FIX 4: Return tuple missing sixth value (None for non-omni path)
|
| 240 |
+
return padded_full_embed, mask, img_sizes, l_effective_cap_len, freqs_cis, None
|
| 241 |
+
|
| 242 |
+
m.add_object_patch(
|
| 243 |
+
"diffusion_model.patchify_and_embed",
|
| 244 |
+
types.MethodType(dype_patchify_and_embed, m.model.diffusion_model)
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
sigma_max = m.model.model_sampling.sigma_max.item()
|
| 248 |
+
|
| 249 |
+
def dype_wrapper_function(model_function, args_dict):
|
| 250 |
+
timestep_tensor = args_dict.get("timestep")
|
| 251 |
+
if timestep_tensor is not None and timestep_tensor.numel() > 0:
|
| 252 |
+
current_sigma = timestep_tensor.flatten()[0].item()
|
| 253 |
+
|
| 254 |
+
if sigma_max > 0:
|
| 255 |
+
normalized_timestep = min(max(current_sigma / sigma_max, 0.0), 1.0)
|
| 256 |
+
new_pe_embedder.set_timestep(normalized_timestep)
|
| 257 |
+
|
| 258 |
+
input_x, c = args_dict.get("input"), args_dict.get("c", {})
|
| 259 |
+
|
| 260 |
+
if is_z_image and isinstance(input_x, torch.Tensor) and input_x.dim() >= 4:
|
| 261 |
+
c = dict(c)
|
| 262 |
+
transformer_options = dict(c.get("transformer_options", {}))
|
| 263 |
+
transformer_options["dype_original_hw"] = (input_x.shape[-2], input_x.shape[-1])
|
| 264 |
+
transformer_options["dype_requested_hw"] = (height, width)
|
| 265 |
+
transformer_options["dype_base_resolution"] = base_resolution
|
| 266 |
+
c["transformer_options"] = transformer_options
|
| 267 |
+
|
| 268 |
+
return model_function(input_x, args_dict.get("timestep"), **c)
|
| 269 |
+
|
| 270 |
+
m.set_model_unet_function_wrapper(dype_wrapper_function)
|
| 271 |
+
|
| 272 |
+
return m
|
custom_nodes/ComfyUI-EsesImageEffectLevels/js/image_effect_levels.js
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ==========================================================================
|
| 2 |
+
// Eses Image Effect Levels
|
| 3 |
+
// ==========================================================================
|
| 4 |
+
//
|
| 5 |
+
// Description:
|
| 6 |
+
// The 'Eses Image Effect Levels' node provides a versatile, image editor style
|
| 7 |
+
// levels adjustment tool directly within the ComfyUI interface. It allows for
|
| 8 |
+
// precise, interactive control over the tonal range of images and masks,
|
| 9 |
+
// complete with a live histogram for visual feedback.
|
| 10 |
+
//
|
| 11 |
+
// Key Features:
|
| 12 |
+
//
|
| 13 |
+
// - Interactive Controls:
|
| 14 |
+
// - An interactive preview of the level adjustments in the node.
|
| 15 |
+
// - Features sliders for input Black, Mid, and White points, as well as
|
| 16 |
+
// sliders for the final Output Black and White points.
|
| 17 |
+
// - A live histogram visualizes the tonal distribution of the selected
|
| 18 |
+
// channel.
|
| 19 |
+
//
|
| 20 |
+
// - Multi-Channel Adjustments:
|
| 21 |
+
// - Apply level adjustments to the combined RGB channels for general
|
| 22 |
+
// corrections.
|
| 23 |
+
// - Isolate adjustments to the individual Red, Green, or Blue channels.
|
| 24 |
+
// - Apply a separate level adjustment directly to an input mask.
|
| 25 |
+
//
|
| 26 |
+
// - State Serialization:
|
| 27 |
+
// - All level adjustments are saved with the workflow and restored on
|
| 28 |
+
// reload.
|
| 29 |
+
// - The node's state persists after refreshing the browser page.
|
| 30 |
+
//
|
| 31 |
+
// - Live Preview:
|
| 32 |
+
// - The node displays a preview of the connected image with the level
|
| 33 |
+
// adjustments applied in real-time as you move the sliders.
|
| 34 |
+
//
|
| 35 |
+
// - Quality of Life Features:
|
| 36 |
+
// - "Set Auto Levels" button to automatically calculate optimal
|
| 37 |
+
// input levels.
|
| 38 |
+
// - A simple file-based preset system for saving and loading adjustment
|
| 39 |
+
// presets
|
| 40 |
+
// - "Reset" buttons to revert the current channel or all channels to
|
| 41 |
+
// default values.
|
| 42 |
+
// - Adjustable auto levels sensitivity for fine-tuned automatic adjustments.
|
| 43 |
+
//
|
| 44 |
+
// Version: 1.3.2
|
| 45 |
+
//
|
| 46 |
+
// License: See LICENSE.txt
|
| 47 |
+
//
|
| 48 |
+
// ==========================================================================
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
import { app } from "/scripts/app.js";
|
| 52 |
+
import { api } from "/scripts/api.js";
|
| 53 |
+
|
| 54 |
+
const HEADER_HEIGHT = 445;
|
| 55 |
+
const PADDING = 10;
|
| 56 |
+
const HISTOGRAM_AREA_HEIGHT = 40;
|
| 57 |
+
const AREA_SPACING = 8;
|
| 58 |
+
const SLIDER_BOX_HEIGHT = 15;
|
| 59 |
+
const OUTPUT_SLIDER_AREA_HEIGHT = 20;
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
// --- PRESET HANDLING START ---
|
| 63 |
+
|
| 64 |
+
// Global cache for presets to avoid
|
| 65 |
+
// fetching them for every single node
|
| 66 |
+
let allPresets = null;
|
| 67 |
+
|
| 68 |
+
async function getPresets() {
|
| 69 |
+
if (allPresets === null) {
|
| 70 |
+
try {
|
| 71 |
+
const response = await api.fetchApi("/eses_levels/get_presets", { cache: "no-store" });
|
| 72 |
+
allPresets = await response.json();
|
| 73 |
+
//console.log("[EsesImageEffectLevels] Presets loaded:", allPresets);
|
| 74 |
+
}
|
| 75 |
+
catch (e) {
|
| 76 |
+
//console.error("[EsesImageEffectLevels] Failed to load presets:", e);
|
| 77 |
+
allPresets = {};
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return allPresets;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Pre-fetch presets when the script is loaded
|
| 85 |
+
getPresets();
|
| 86 |
+
|
| 87 |
+
// --- PRESET HANDLING END ---
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
app.registerExtension({
|
| 92 |
+
name: "Eses.EsesImageEffectLevels",
|
| 93 |
+
|
| 94 |
+
// --- PRESET HANDLING START ---
|
| 95 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
| 96 |
+
|
| 97 |
+
// Runs once when the node definition is being registered.
|
| 98 |
+
// Dynamically add the preset names to the combo box definition.
|
| 99 |
+
if (nodeData.name === "EsesImageEffectLevels") {
|
| 100 |
+
const presets = await getPresets();
|
| 101 |
+
const presetNames = ["None", ...Object.keys(presets)];
|
| 102 |
+
|
| 103 |
+
// Find the 'preset' input in the node's
|
| 104 |
+
// definition and update its options
|
| 105 |
+
const presetInput = nodeData.input.required.preset;
|
| 106 |
+
if (presetInput) {
|
| 107 |
+
presetInput[0] = presetNames;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
// --- PRESET HANDLING END ---
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
nodeCreated(node) {
|
| 115 |
+
|
| 116 |
+
if (node.comfyClass === "EsesImageEffectLevels") {
|
| 117 |
+
const originalConfigure = node.configure;
|
| 118 |
+
const originalSerialize = node.serialize;
|
| 119 |
+
|
| 120 |
+
node.size = [256, 575];
|
| 121 |
+
node.imagePreview = null;
|
| 122 |
+
node.originalImage = null;
|
| 123 |
+
node.originalMask = null;
|
| 124 |
+
node.isManuallyResized = false;
|
| 125 |
+
node.dragging = null;
|
| 126 |
+
node.resetButtonState = 'idle';
|
| 127 |
+
|
| 128 |
+
// Add state for auto color button
|
| 129 |
+
node.autoColorButtonState = 'idle';
|
| 130 |
+
node.onResize = function () { this.isManuallyResized = true; };
|
| 131 |
+
|
| 132 |
+
const defaultLevels = () => ({ black_point: 0.0, white_point: 1.0, mid_point: 1.0, output_black: 0, output_white: 255 });
|
| 133 |
+
const resetAllLevels = () => ({ rgb: defaultLevels(), r: defaultLevels(), g: defaultLevels(), b: defaultLevels(), mask: defaultLevels() });
|
| 134 |
+
|
| 135 |
+
node.allLevels = resetAllLevels();
|
| 136 |
+
node.histograms = {};
|
| 137 |
+
|
| 138 |
+
let blackWidget, midWidget, whiteWidget, outputBlackWidget, outputWhiteWidget;
|
| 139 |
+
|
| 140 |
+
function updateWidgetsFromState() {
|
| 141 |
+
const levels = node.allLevels[node.activeChannel];
|
| 142 |
+
|
| 143 |
+
if (!levels)
|
| 144 |
+
return;
|
| 145 |
+
|
| 146 |
+
if (blackWidget)
|
| 147 |
+
blackWidget.value = parseFloat(levels.black_point.toFixed(2));
|
| 148 |
+
|
| 149 |
+
if (midWidget)
|
| 150 |
+
midWidget.value = parseFloat(levels.mid_point.toFixed(2));
|
| 151 |
+
|
| 152 |
+
if (whiteWidget)
|
| 153 |
+
whiteWidget.value = parseFloat(levels.white_point.toFixed(2));
|
| 154 |
+
|
| 155 |
+
if (outputBlackWidget)
|
| 156 |
+
outputBlackWidget.value = Math.round(levels.output_black);
|
| 157 |
+
|
| 158 |
+
if (outputWhiteWidget)
|
| 159 |
+
outputWhiteWidget.value = Math.round(levels.output_white);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
node.getResetButtonName = (isConfirm = false) => {
|
| 163 |
+
const channelWidget = node.widgets.find(w => w.name === "channel");
|
| 164 |
+
|
| 165 |
+
if (node.activeChannel === 'rgb')
|
| 166 |
+
return isConfirm ? "CONFIRM RESET ALL" : "Reset All Levels";
|
| 167 |
+
|
| 168 |
+
const channelName = channelWidget.value;
|
| 169 |
+
|
| 170 |
+
return isConfirm ? `CONFIRM RESET ${channelName}` : `Reset ${channelName} Level`;
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
node.resetButtonCallback = function () {
|
| 174 |
+
const resetButton = node.widgets.find(w => w.name.startsWith("Reset") || w.name.startsWith("CONFIRM"));
|
| 175 |
+
|
| 176 |
+
if (node.resetButtonState === 'armed') {
|
| 177 |
+
clearTimeout(node.resetTimeout);
|
| 178 |
+
|
| 179 |
+
if (node.activeChannel === 'rgb') {
|
| 180 |
+
node.allLevels = resetAllLevels();
|
| 181 |
+
}
|
| 182 |
+
else {
|
| 183 |
+
node.allLevels[node.activeChannel] = defaultLevels();
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
updateWidgetsFromState();
|
| 187 |
+
node.sendLevelUpdateToPython();
|
| 188 |
+
resetButton.name = node.getResetButtonName();
|
| 189 |
+
node.resetButtonState = 'idle';
|
| 190 |
+
}
|
| 191 |
+
else {
|
| 192 |
+
node.resetButtonState = 'armed';
|
| 193 |
+
resetButton.name = node.getResetButtonName(true);
|
| 194 |
+
|
| 195 |
+
node.resetTimeout = setTimeout(() => {
|
| 196 |
+
resetButton.name = node.getResetButtonName();
|
| 197 |
+
node.resetButtonState = 'idle';
|
| 198 |
+
app.graph.setDirtyCanvas(true);
|
| 199 |
+
}, 3000);
|
| 200 |
+
}
|
| 201 |
+
app.graph.setDirtyCanvas(true);
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
function setupCustomWidgets() {
|
| 205 |
+
const levelsWidget = node.widgets.find(w => w.name === "all_levels_json");
|
| 206 |
+
const channelWidget = node.widgets.find(w => w.name === "channel");
|
| 207 |
+
|
| 208 |
+
// Hide the boolean widgets that are only used for triggering
|
| 209 |
+
const autoLevelsWidget = node.widgets.find(w => w.name === "auto_levels");
|
| 210 |
+
const autoColorWidget = node.widgets.find(w => w.name === "auto_color");
|
| 211 |
+
|
| 212 |
+
if (!levelsWidget || !channelWidget)
|
| 213 |
+
return;
|
| 214 |
+
|
| 215 |
+
levelsWidget.type = "hidden";
|
| 216 |
+
if (levelsWidget.element) {
|
| 217 |
+
levelsWidget.element.style.display = "none";
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
if (autoLevelsWidget) {
|
| 221 |
+
autoLevelsWidget.type = "hidden";
|
| 222 |
+
if (autoLevelsWidget.element) {
|
| 223 |
+
autoLevelsWidget.element.style.display = "none";
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
if (autoColorWidget) {
|
| 228 |
+
autoColorWidget.type = "hidden";
|
| 229 |
+
if (autoColorWidget.element) {
|
| 230 |
+
autoColorWidget.element.style.display = "none";
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
const channelMap = { "RGB": "rgb", "Red": "r", "Green": "g", "Blue": "b", "Mask": "mask" };
|
| 234 |
+
node.activeChannel = channelMap[channelWidget.value] || "rgb";
|
| 235 |
+
|
| 236 |
+
if (!node.widgets.find(w => w.name === "Black")) {
|
| 237 |
+
blackWidget = node.addWidget("number", "Black", 0.0, (val) => {
|
| 238 |
+
|
| 239 |
+
if (node.allLevels[node.activeChannel]) {
|
| 240 |
+
node.allLevels[node.activeChannel].black_point = val;
|
| 241 |
+
node.sendLevelUpdateToPython();
|
| 242 |
+
}
|
| 243 |
+
}, { min: 0.0, max: 1.0, step: 0.01, precision: 2 });
|
| 244 |
+
|
| 245 |
+
midWidget = node.addWidget("number", "Mid", 1.0, (val) => {
|
| 246 |
+
|
| 247 |
+
if (node.allLevels[node.activeChannel]) {
|
| 248 |
+
node.allLevels[node.activeChannel].mid_point = val;
|
| 249 |
+
node.sendLevelUpdateToPython();
|
| 250 |
+
}
|
| 251 |
+
}, { min: 0.01, max: 9.99, step: 0.01, precision: 2 });
|
| 252 |
+
|
| 253 |
+
whiteWidget = node.addWidget("number", "White", 1.0, (val) => {
|
| 254 |
+
|
| 255 |
+
if (node.allLevels[node.activeChannel]) {
|
| 256 |
+
node.allLevels[node.activeChannel].white_point = val;
|
| 257 |
+
node.sendLevelUpdateToPython();
|
| 258 |
+
}
|
| 259 |
+
}, { min: 0.0, max: 1.0, step: 0.01, precision: 2 });
|
| 260 |
+
|
| 261 |
+
outputBlackWidget = node.addWidget("number", "Output Black", 0, (val) => {
|
| 262 |
+
|
| 263 |
+
if (node.allLevels[node.activeChannel]) {
|
| 264 |
+
node.allLevels[node.activeChannel].output_black = val;
|
| 265 |
+
node.sendLevelUpdateToPython();
|
| 266 |
+
}
|
| 267 |
+
}, { min: 0, max: 255, step: 1, precision: 0 });
|
| 268 |
+
|
| 269 |
+
outputWhiteWidget = node.addWidget("number", "Output White", 255, (val) => {
|
| 270 |
+
|
| 271 |
+
if (node.allLevels[node.activeChannel]) {
|
| 272 |
+
node.allLevels[node.activeChannel].output_white = val;
|
| 273 |
+
node.sendLevelUpdateToPython();
|
| 274 |
+
}
|
| 275 |
+
}, { min: 0, max: 255, step: 1, precision: 0 });
|
| 276 |
+
}
|
| 277 |
+
else {
|
| 278 |
+
blackWidget = node.widgets.find(w => w.name === "Black");
|
| 279 |
+
midWidget = node.widgets.find(w => w.name === "Mid");
|
| 280 |
+
whiteWidget = node.widgets.find(w => w.name === "White");
|
| 281 |
+
outputBlackWidget = node.widgets.find(w => w.name === "Output Black");
|
| 282 |
+
outputWhiteWidget = node.widgets.find(w => w.name === "Output White");
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// --- PRESET HANDLING START ---
|
| 286 |
+
|
| 287 |
+
// Remove old "Copy Settings" button if it exists
|
| 288 |
+
const copySettingsButton = node.widgets.find(w => w.name === "Copy Settings");
|
| 289 |
+
|
| 290 |
+
if (copySettingsButton)
|
| 291 |
+
node.widgets.splice(node.widgets.indexOf(copySettingsButton), 1);
|
| 292 |
+
|
| 293 |
+
// Add "Preset Name" text field and "Save Preset" button
|
| 294 |
+
if (!node.widgets.find(w => w.name === "Preset Name")) {
|
| 295 |
+
const presetNameWidget = node.addWidget("string", "Preset Name", "", () => { }, {});
|
| 296 |
+
|
| 297 |
+
node.addWidget("button", "Save Preset", null, async () => {
|
| 298 |
+
const name = presetNameWidget.value;
|
| 299 |
+
|
| 300 |
+
if (!name || !name.trim()) {
|
| 301 |
+
alert("Please enter a preset name.");
|
| 302 |
+
return;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
try {
|
| 306 |
+
const resp = await api.fetchApi("/eses_levels/save_preset", {
|
| 307 |
+
method: "POST",
|
| 308 |
+
headers: { "Content-Type": "application/json" },
|
| 309 |
+
body: JSON.stringify({
|
| 310 |
+
preset_name: name,
|
| 311 |
+
preset_data: node.allLevels,
|
| 312 |
+
}),
|
| 313 |
+
});
|
| 314 |
+
|
| 315 |
+
const res = await resp.json();
|
| 316 |
+
|
| 317 |
+
if (res.success) {
|
| 318 |
+
alert(`Preset '${name}' saved successfully!`);
|
| 319 |
+
|
| 320 |
+
// Refresh the presets in the dropdown
|
| 321 |
+
allPresets = res.presets;
|
| 322 |
+
const presetWidget = node.widgets.find(w => w.name === "preset");
|
| 323 |
+
|
| 324 |
+
if (presetWidget) {
|
| 325 |
+
presetWidget.options.values = ["None", ...Object.keys(allPresets)];
|
| 326 |
+
presetWidget.value = name;
|
| 327 |
+
}
|
| 328 |
+
presetNameWidget.value = "";
|
| 329 |
+
}
|
| 330 |
+
else {
|
| 331 |
+
alert(`Error saving preset: ${res.message}`);
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
catch (e) {
|
| 335 |
+
console.error("[EsesImageEffectLevels] Error saving preset:", e);
|
| 336 |
+
alert(`An error occurred: ${e}`);
|
| 337 |
+
}
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
const presetWidget = node.widgets.find(w => w.name === "preset");
|
| 342 |
+
if (presetWidget) {
|
| 343 |
+
presetWidget.callback = async (value) => {
|
| 344 |
+
|
| 345 |
+
if (value === "None")
|
| 346 |
+
return;
|
| 347 |
+
|
| 348 |
+
const presets = await getPresets();
|
| 349 |
+
const selectedPreset = presets[value];
|
| 350 |
+
|
| 351 |
+
if (selectedPreset) {
|
| 352 |
+
|
| 353 |
+
// Create a fresh default state
|
| 354 |
+
const newLevels = resetAllLevels();
|
| 355 |
+
const loadedLevels = JSON.parse(JSON.stringify(selectedPreset));
|
| 356 |
+
|
| 357 |
+
// Deep merge the loaded preset into the default state.
|
| 358 |
+
// This ensures compatibility with older presets.
|
| 359 |
+
for (const channelKey in newLevels) {
|
| 360 |
+
|
| 361 |
+
if (loadedLevels[channelKey]) {
|
| 362 |
+
Object.assign(newLevels[channelKey], loadedLevels[channelKey]);
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
node.allLevels = newLevels;
|
| 367 |
+
updateWidgetsFromState();
|
| 368 |
+
node.sendLevelUpdateToPython();
|
| 369 |
+
node.setDirtyCanvas(true, true);
|
| 370 |
+
}
|
| 371 |
+
};
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// --- PRESET HANDLING END ---
|
| 375 |
+
|
| 376 |
+
if (!node.widgets.find(w => w.name === "Set Auto Levels")) {
|
| 377 |
+
node.addWidget("button", "Set Auto Levels", null, () => {
|
| 378 |
+
const autoLevelsWidget = node.widgets.find(w => w.name === "auto_levels");
|
| 379 |
+
|
| 380 |
+
if (autoLevelsWidget) {
|
| 381 |
+
const originalValue = autoLevelsWidget.value;
|
| 382 |
+
autoLevelsWidget.value = true;
|
| 383 |
+
app.queuePrompt();
|
| 384 |
+
setTimeout(() => { autoLevelsWidget.value = originalValue; }, 100);
|
| 385 |
+
}
|
| 386 |
+
});
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// Add "Set Auto Color" button
|
| 390 |
+
if (!node.widgets.find(w => w.name === "Set Auto Color")) {
|
| 391 |
+
|
| 392 |
+
node.addWidget("button", "Set Auto Color", null, () => {
|
| 393 |
+
|
| 394 |
+
const autoColorWidget = node.widgets.find(w => w.name === "auto_color");
|
| 395 |
+
|
| 396 |
+
if (autoColorWidget) {
|
| 397 |
+
const originalValue = autoColorWidget.value;
|
| 398 |
+
autoColorWidget.value = true;
|
| 399 |
+
app.queuePrompt();
|
| 400 |
+
|
| 401 |
+
// Reset the value back to false after a short delay
|
| 402 |
+
setTimeout(() => { autoColorWidget.value = originalValue; }, 100);
|
| 403 |
+
}
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
const oldResetBtn = node.widgets.find(w => w && w.name && (w.name.startsWith("Reset") || w.name.startsWith("CONFIRM")));
|
| 409 |
+
|
| 410 |
+
if (oldResetBtn)
|
| 411 |
+
node.widgets.splice(node.widgets.indexOf(oldResetBtn), 1);
|
| 412 |
+
|
| 413 |
+
const resetButton = node.addWidget("button", node.getResetButtonName(), null, node.resetButtonCallback.bind(node));
|
| 414 |
+
|
| 415 |
+
channelWidget.callback = (value) => {
|
| 416 |
+
node.activeChannel = channelMap[value] || "rgb";
|
| 417 |
+
resetButton.name = node.getResetButtonName();
|
| 418 |
+
|
| 419 |
+
updateWidgetsFromState();
|
| 420 |
+
node.updateHistogram();
|
| 421 |
+
node.sendLevelUpdateToPython();
|
| 422 |
+
};
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
node.configure = function (data) {
|
| 426 |
+
originalConfigure.apply(this, arguments);
|
| 427 |
+
|
| 428 |
+
if (data.custom_all_levels) {
|
| 429 |
+
this.allLevels = data.custom_all_levels;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
setupCustomWidgets();
|
| 433 |
+
updateWidgetsFromState();
|
| 434 |
+
};
|
| 435 |
+
|
| 436 |
+
node.serialize = function () {
|
| 437 |
+
const data = originalSerialize.call(this);
|
| 438 |
+
data.custom_all_levels = node.allLevels;
|
| 439 |
+
return data;
|
| 440 |
+
};
|
| 441 |
+
|
| 442 |
+
setupCustomWidgets();
|
| 443 |
+
|
| 444 |
+
Object.assign(node, {
|
| 445 |
+
|
| 446 |
+
getImageArea: function () {
|
| 447 |
+
const interactiveHeight = HISTOGRAM_AREA_HEIGHT + OUTPUT_SLIDER_AREA_HEIGHT + AREA_SPACING;
|
| 448 |
+
|
| 449 |
+
// Moved image top edge down by the
|
| 450 |
+
// amount increased by HEADER_HEIGHT
|
| 451 |
+
const totalContentHeight = this.size[1] - HEADER_HEIGHT - PADDING * 2;
|
| 452 |
+
const imageAreaHeight = totalContentHeight - interactiveHeight;
|
| 453 |
+
|
| 454 |
+
if (imageAreaHeight < 10)
|
| 455 |
+
return null;
|
| 456 |
+
|
| 457 |
+
return { x: PADDING, y: HEADER_HEIGHT + PADDING, width: this.size[0] - PADDING * 2, height: imageAreaHeight };
|
| 458 |
+
},
|
| 459 |
+
|
| 460 |
+
getBarArea: function () {
|
| 461 |
+
const imageArea = this.getImageArea();
|
| 462 |
+
|
| 463 |
+
if (!imageArea)
|
| 464 |
+
return null;
|
| 465 |
+
|
| 466 |
+
const barAreaHeight = HISTOGRAM_AREA_HEIGHT - SLIDER_BOX_HEIGHT - 4;
|
| 467 |
+
return { x: PADDING, y: imageArea.y + imageArea.height + AREA_SPACING, width: this.size[0] - PADDING * 2, height: barAreaHeight };
|
| 468 |
+
},
|
| 469 |
+
|
| 470 |
+
getInputSliderArea: function () {
|
| 471 |
+
const barArea = this.getBarArea();
|
| 472 |
+
|
| 473 |
+
if (!barArea)
|
| 474 |
+
return null;
|
| 475 |
+
|
| 476 |
+
return { x: PADDING, y: barArea.y + barArea.height + 4, width: this.size[0] - PADDING * 2, height: SLIDER_BOX_HEIGHT };
|
| 477 |
+
},
|
| 478 |
+
|
| 479 |
+
getOutputSliderArea: function () {
|
| 480 |
+
const inputSliderArea = this.getInputSliderArea();
|
| 481 |
+
|
| 482 |
+
if (!inputSliderArea)
|
| 483 |
+
return null;
|
| 484 |
+
|
| 485 |
+
return { x: PADDING, y: inputSliderArea.y + inputSliderArea.height + AREA_SPACING, width: this.size[0] - PADDING * 2, height: SLIDER_BOX_HEIGHT };
|
| 486 |
+
},
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
updateHistogram: async function () {
|
| 490 |
+
const channelWidget = node.widgets.find(w => w.name === "channel");
|
| 491 |
+
let base_data_image = (this.activeChannel === 'mask') ? this.originalMask : this.originalImage;
|
| 492 |
+
|
| 493 |
+
if (!base_data_image)
|
| 494 |
+
return;
|
| 495 |
+
|
| 496 |
+
this.histograms[this.activeChannel] = null;
|
| 497 |
+
const canvas = document.createElement('canvas');
|
| 498 |
+
canvas.width = base_data_image.naturalWidth;
|
| 499 |
+
canvas.height = base_data_image.naturalHeight;
|
| 500 |
+
canvas.getContext('2d').drawImage(base_data_image, 0, 0);
|
| 501 |
+
const base_data_b64 = canvas.toDataURL('image/png').split(',')[1];
|
| 502 |
+
|
| 503 |
+
try {
|
| 504 |
+
const result = await api.fetchApi("/eses_levels/get_histogram", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ base_data_b64, channel_mode: channelWidget.value }) }).then(r => r.json());
|
| 505 |
+
|
| 506 |
+
if (result.histogram) {
|
| 507 |
+
this.histograms[this.activeChannel] = result.histogram;
|
| 508 |
+
this.setDirtyCanvas(true, true);
|
| 509 |
+
}
|
| 510 |
+
}
|
| 511 |
+
catch (err) {
|
| 512 |
+
console.error("Failed to fetch histogram:", err);
|
| 513 |
+
}
|
| 514 |
+
},
|
| 515 |
+
|
| 516 |
+
sendLevelUpdateToPython: async function () {
|
| 517 |
+
this.setDirtyCanvas(true, true);
|
| 518 |
+
|
| 519 |
+
const levelsWidget = this.widgets.find(w => w.name === "all_levels_json");
|
| 520 |
+
|
| 521 |
+
if (!levelsWidget)
|
| 522 |
+
return;
|
| 523 |
+
|
| 524 |
+
levelsWidget.value = JSON.stringify(this.allLevels);
|
| 525 |
+
let base_data_type, base_data_image;
|
| 526 |
+
|
| 527 |
+
if (this.activeChannel === 'mask') {
|
| 528 |
+
if (!this.originalMask) {
|
| 529 |
+
this.imagePreview = null; this.setDirtyCanvas(true, true); return;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
base_data_type = 'mask';
|
| 533 |
+
base_data_image = this.originalMask;
|
| 534 |
+
}
|
| 535 |
+
else {
|
| 536 |
+
if (!this.originalImage) {
|
| 537 |
+
this.imagePreview = null; this.setDirtyCanvas(true, true); return;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
base_data_type = 'image';
|
| 541 |
+
base_data_image = this.originalImage;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
const canvas = document.createElement('canvas');
|
| 545 |
+
canvas.width = base_data_image.naturalWidth;
|
| 546 |
+
canvas.height = base_data_image.naturalHeight;
|
| 547 |
+
canvas.getContext('2d').drawImage(base_data_image, 0, 0);
|
| 548 |
+
const base_data_b64 = canvas.toDataURL('image/png').split(',')[1];
|
| 549 |
+
|
| 550 |
+
const body = {
|
| 551 |
+
base_data_b64,
|
| 552 |
+
all_levels_json: levelsWidget.value,
|
| 553 |
+
base_data_type
|
| 554 |
+
};
|
| 555 |
+
|
| 556 |
+
try {
|
| 557 |
+
const result = await api.fetchApi("/eses_levels/apply", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(r => r.json());
|
| 558 |
+
|
| 559 |
+
if (result.adjusted_image_data) {
|
| 560 |
+
const img = new Image();
|
| 561 |
+
img.src = `data:image/png;base64,${result.adjusted_image_data}`;
|
| 562 |
+
|
| 563 |
+
img.onload = () => {
|
| 564 |
+
this.imagePreview = img; this.setDirtyCanvas(true, true);
|
| 565 |
+
};
|
| 566 |
+
}
|
| 567 |
+
}
|
| 568 |
+
catch (err) { }
|
| 569 |
+
},
|
| 570 |
+
|
| 571 |
+
drawHistogram: function (ctx, area, data, step = 1) {
|
| 572 |
+
if (!data || data.length === 0)
|
| 573 |
+
return;
|
| 574 |
+
|
| 575 |
+
step = Math.max(1, Math.floor(step));
|
| 576 |
+
ctx.fillStyle = "#999";
|
| 577 |
+
const baseBarWidth = area.width / 256;
|
| 578 |
+
|
| 579 |
+
for (let i = 0; i < 256; i += step) {
|
| 580 |
+
const chunk = data.slice(i, i + step);
|
| 581 |
+
|
| 582 |
+
if (chunk.length === 0)
|
| 583 |
+
continue;
|
| 584 |
+
|
| 585 |
+
const maxVal = Math.max(...chunk);
|
| 586 |
+
const barHeight = maxVal * area.height;
|
| 587 |
+
const barWidth = baseBarWidth * chunk.length;
|
| 588 |
+
const x = area.x + i * baseBarWidth;
|
| 589 |
+
ctx.fillRect(x, area.y + area.height - barHeight, barWidth, barHeight);
|
| 590 |
+
}
|
| 591 |
+
},
|
| 592 |
+
|
| 593 |
+
drawHandle: function (ctx, x, y, size, color, strokeColor) {
|
| 594 |
+
ctx.fillStyle = color;
|
| 595 |
+
ctx.strokeStyle = strokeColor;
|
| 596 |
+
ctx.lineWidth = 1;
|
| 597 |
+
ctx.beginPath();
|
| 598 |
+
ctx.moveTo(x, y);
|
| 599 |
+
ctx.lineTo(x - size, y + size * 2);
|
| 600 |
+
ctx.lineTo(x + size, y + size * 2);
|
| 601 |
+
ctx.closePath();
|
| 602 |
+
ctx.fill();
|
| 603 |
+
ctx.stroke();
|
| 604 |
+
},
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
onDrawForeground: function (ctx) {
|
| 608 |
+
|
| 609 |
+
if (this.flags?.collapsed)
|
| 610 |
+
return;
|
| 611 |
+
|
| 612 |
+
const imageArea = this.getImageArea();
|
| 613 |
+
const barArea = this.getBarArea();
|
| 614 |
+
const inputSliderArea = this.getInputSliderArea();
|
| 615 |
+
const outputSliderArea = this.getOutputSliderArea();
|
| 616 |
+
|
| 617 |
+
if (!imageArea || !barArea || !inputSliderArea || !outputSliderArea)
|
| 618 |
+
return;
|
| 619 |
+
|
| 620 |
+
if (this.imagePreview) {
|
| 621 |
+
ctx.drawImage(this.imagePreview, imageArea.x, imageArea.y, imageArea.width, imageArea.height);
|
| 622 |
+
}
|
| 623 |
+
else {
|
| 624 |
+
ctx.save();
|
| 625 |
+
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR || "#CCC";
|
| 626 |
+
ctx.textAlign = "center";
|
| 627 |
+
ctx.textBaseline = "middle";
|
| 628 |
+
ctx.font = "bold 14px Arial";
|
| 629 |
+
const message = "Connect inputs and run workflow";
|
| 630 |
+
ctx.fillText(message, imageArea.x + imageArea.width / 2, imageArea.y + imageArea.height / 2);
|
| 631 |
+
ctx.restore();
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
// Draw Input Levels Area
|
| 635 |
+
ctx.fillStyle = "#282828";
|
| 636 |
+
ctx.fillRect(barArea.x, barArea.y, barArea.width, barArea.height);
|
| 637 |
+
ctx.fillStyle = "#202020";
|
| 638 |
+
ctx.fillRect(inputSliderArea.x, inputSliderArea.y, inputSliderArea.width, inputSliderArea.height);
|
| 639 |
+
|
| 640 |
+
const histogramData = this.histograms[this.activeChannel];
|
| 641 |
+
|
| 642 |
+
if (histogramData) {
|
| 643 |
+
this.drawHistogram(ctx, barArea, histogramData, 1);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
const levels = this.allLevels[this.activeChannel];
|
| 647 |
+
if (!levels) return;
|
| 648 |
+
|
| 649 |
+
// Draw Input Handles
|
| 650 |
+
const blackX = inputSliderArea.x + levels.black_point * inputSliderArea.width;
|
| 651 |
+
const whiteX = inputSliderArea.x + levels.white_point * inputSliderArea.width;
|
| 652 |
+
const relativeRangeWidth = whiteX - blackX;
|
| 653 |
+
|
| 654 |
+
const midPointToSlider = (gamma) => {
|
| 655 |
+
if (gamma >= 1.0)
|
| 656 |
+
return 1.0 - (0.5 + ((gamma - 1.0) / 8.99) * 0.5);
|
| 657 |
+
else return 1.0 - (((gamma - 0.01) / 0.99) * 0.5);
|
| 658 |
+
};
|
| 659 |
+
|
| 660 |
+
const midHandleX = blackX + (midPointToSlider(levels.mid_point) * relativeRangeWidth);
|
| 661 |
+
const handleTipY = inputSliderArea.y + 2.5;
|
| 662 |
+
|
| 663 |
+
this.drawHandle(ctx, blackX, handleTipY, 5, "black", "white");
|
| 664 |
+
this.drawHandle(ctx, whiteX, handleTipY, 5, "white", "black");
|
| 665 |
+
this.drawHandle(ctx, midHandleX, handleTipY, 5, "#888", "white");
|
| 666 |
+
|
| 667 |
+
// Draw Output Levels Area
|
| 668 |
+
const gradient = ctx.createLinearGradient(outputSliderArea.x, 0, outputSliderArea.x + outputSliderArea.width, 0);
|
| 669 |
+
const blackOutPos = levels.output_black / 255.0;
|
| 670 |
+
const whiteOutPos = levels.output_white / 255.0;
|
| 671 |
+
|
| 672 |
+
gradient.addColorStop(0, "black");
|
| 673 |
+
gradient.addColorStop(blackOutPos, "black");
|
| 674 |
+
gradient.addColorStop(whiteOutPos, "white");
|
| 675 |
+
gradient.addColorStop(1, "white");
|
| 676 |
+
ctx.fillStyle = gradient;
|
| 677 |
+
ctx.fillRect(outputSliderArea.x, outputSliderArea.y, outputSliderArea.width, outputSliderArea.height);
|
| 678 |
+
|
| 679 |
+
// Draw Output Handles
|
| 680 |
+
const outBlackX = outputSliderArea.x + blackOutPos * outputSliderArea.width;
|
| 681 |
+
const outWhiteX = outputSliderArea.x + whiteOutPos * outputSliderArea.width;
|
| 682 |
+
const outHandleTipY = outputSliderArea.y + 2.5;
|
| 683 |
+
|
| 684 |
+
this.drawHandle(ctx, outBlackX, outHandleTipY, 5, "black", "white");
|
| 685 |
+
this.drawHandle(ctx, outWhiteX, outHandleTipY, 5, "white", "black");
|
| 686 |
+
},
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
onMouseDown: function (event) {
|
| 690 |
+
|
| 691 |
+
if (event.button !== 0)
|
| 692 |
+
return false;
|
| 693 |
+
|
| 694 |
+
const localPos = app.canvas.convertEventToCanvasOffset(event);
|
| 695 |
+
const mouseX = localPos[0] - this.pos[0];
|
| 696 |
+
const mouseY = localPos[1] - this.pos[1];
|
| 697 |
+
|
| 698 |
+
const levels = this.allLevels[this.activeChannel];
|
| 699 |
+
|
| 700 |
+
if (!levels)
|
| 701 |
+
return false;
|
| 702 |
+
|
| 703 |
+
// Check output slider first
|
| 704 |
+
const outputSliderArea = this.getOutputSliderArea();
|
| 705 |
+
if (outputSliderArea && mouseX > outputSliderArea.x && mouseX < outputSliderArea.x + outputSliderArea.width &&
|
| 706 |
+
mouseY > outputSliderArea.y && mouseY < outputSliderArea.y + outputSliderArea.height) {
|
| 707 |
+
|
| 708 |
+
const outBlackX = outputSliderArea.x + (levels.output_black / 255.0) * outputSliderArea.width;
|
| 709 |
+
|
| 710 |
+
if (Math.abs(mouseX - outBlackX) < 10) {
|
| 711 |
+
this.dragging = 'output_black'; return true;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
const outWhiteX = outputSliderArea.x + (levels.output_white / 255.0) * outputSliderArea.width;
|
| 715 |
+
|
| 716 |
+
if (Math.abs(mouseX - outWhiteX) < 10) {
|
| 717 |
+
this.dragging = 'output_white'; return true;
|
| 718 |
+
}
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
// Check input slider
|
| 723 |
+
const inputSliderArea = this.getInputSliderArea();
|
| 724 |
+
if (inputSliderArea && mouseX > inputSliderArea.x && mouseX < inputSliderArea.x + inputSliderArea.width &&
|
| 725 |
+
mouseY > inputSliderArea.y && mouseY < inputSliderArea.y + inputSliderArea.height) {
|
| 726 |
+
|
| 727 |
+
const blackX = inputSliderArea.x + levels.black_point * inputSliderArea.width;
|
| 728 |
+
const whiteX = inputSliderArea.x + levels.white_point * inputSliderArea.width;
|
| 729 |
+
const relativeRangeWidth = whiteX - blackX;
|
| 730 |
+
|
| 731 |
+
const midPointToSlider = (gamma) => {
|
| 732 |
+
if (gamma >= 1.0) {
|
| 733 |
+
return 1.0 - (0.5 + ((gamma - 1.0) / 8.99) * 0.5);
|
| 734 |
+
}
|
| 735 |
+
else {
|
| 736 |
+
return 1.0 - (((gamma - 0.01) / 0.99) * 0.5);
|
| 737 |
+
}
|
| 738 |
+
};
|
| 739 |
+
|
| 740 |
+
const midHandleX = blackX + (midPointToSlider(levels.mid_point) * relativeRangeWidth);
|
| 741 |
+
|
| 742 |
+
if (Math.abs(mouseX - midHandleX) < 10) {
|
| 743 |
+
this.dragging = 'mid';
|
| 744 |
+
}
|
| 745 |
+
else if (Math.abs(mouseX - blackX) < 10) {
|
| 746 |
+
this.dragging = 'black';
|
| 747 |
+
}
|
| 748 |
+
else if (Math.abs(mouseX - whiteX) < 10) {
|
| 749 |
+
this.dragging = 'white';
|
| 750 |
+
}
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
return this.dragging !== null;
|
| 754 |
+
},
|
| 755 |
+
|
| 756 |
+
onMouseMove: function (event) {
|
| 757 |
+
if (event.buttons !== 1) {
|
| 758 |
+
|
| 759 |
+
if (this.dragging) {
|
| 760 |
+
this.dragging = null; this.sendLevelUpdateToPython();
|
| 761 |
+
}
|
| 762 |
+
return;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
if (!this.dragging)
|
| 766 |
+
return;
|
| 767 |
+
|
| 768 |
+
const localPos = app.canvas.convertEventToCanvasOffset(event);
|
| 769 |
+
const mouseX = localPos[0] - this.pos[0];
|
| 770 |
+
const levels = this.allLevels[this.activeChannel];
|
| 771 |
+
|
| 772 |
+
if (!levels)
|
| 773 |
+
return;
|
| 774 |
+
|
| 775 |
+
let needsUpdate = false;
|
| 776 |
+
|
| 777 |
+
// Handle input slider dragging
|
| 778 |
+
if (this.dragging === 'black' || this.dragging === 'white' || this.dragging === 'mid') {
|
| 779 |
+
const inputSliderArea = this.getInputSliderArea();
|
| 780 |
+
|
| 781 |
+
if (!inputSliderArea)
|
| 782 |
+
return;
|
| 783 |
+
|
| 784 |
+
if (this.dragging === 'black' || this.dragging === 'white') {
|
| 785 |
+
let newLevel = (mouseX - inputSliderArea.x) / inputSliderArea.width;
|
| 786 |
+
newLevel = Math.max(0.0, Math.min(1.0, newLevel));
|
| 787 |
+
|
| 788 |
+
if (this.dragging === 'black')
|
| 789 |
+
levels.black_point = Math.min(newLevel, levels.white_point - 0.01);
|
| 790 |
+
else
|
| 791 |
+
levels.white_point = Math.max(newLevel, levels.black_point + 0.01);
|
| 792 |
+
}
|
| 793 |
+
else if (this.dragging === 'mid') {
|
| 794 |
+
const blackX = inputSliderArea.x + levels.black_point * inputSliderArea.width;
|
| 795 |
+
const whiteX = inputSliderArea.x + levels.white_point * inputSliderArea.width;
|
| 796 |
+
const relativeRangeWidth = whiteX - blackX;
|
| 797 |
+
|
| 798 |
+
if (relativeRangeWidth < 1)
|
| 799 |
+
return;
|
| 800 |
+
|
| 801 |
+
let relativePos = (mouseX - blackX) / relativeRangeWidth;
|
| 802 |
+
relativePos = Math.max(0.0, Math.min(1.0, relativePos));
|
| 803 |
+
let invertedPos = 1.0 - relativePos;
|
| 804 |
+
|
| 805 |
+
if (invertedPos >= 0.5) {
|
| 806 |
+
levels.mid_point = 1.0 + ((invertedPos - 0.5) / 0.5) * (9.99 - 1.0);
|
| 807 |
+
}
|
| 808 |
+
else {
|
| 809 |
+
levels.mid_point = 0.01 + (invertedPos / 0.5) * (1.0 - 0.01);
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
levels.mid_point = parseFloat(levels.mid_point.toFixed(2));
|
| 813 |
+
}
|
| 814 |
+
needsUpdate = true;
|
| 815 |
+
}
|
| 816 |
+
// Handle output slider dragging
|
| 817 |
+
else if (this.dragging === 'output_black' || this.dragging === 'output_white') {
|
| 818 |
+
const outputSliderArea = this.getOutputSliderArea();
|
| 819 |
+
|
| 820 |
+
if (!outputSliderArea)
|
| 821 |
+
return;
|
| 822 |
+
|
| 823 |
+
let newLevel = ((mouseX - outputSliderArea.x) / outputSliderArea.width) * 255;
|
| 824 |
+
newLevel = Math.max(0, Math.min(255, newLevel));
|
| 825 |
+
|
| 826 |
+
if (this.dragging === 'output_black') {
|
| 827 |
+
levels.output_black = Math.min(newLevel, levels.output_white - 1);
|
| 828 |
+
}
|
| 829 |
+
else {
|
| 830 |
+
levels.output_white = Math.max(newLevel, levels.output_black + 1);
|
| 831 |
+
}
|
| 832 |
+
needsUpdate = true;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
if (needsUpdate) {
|
| 837 |
+
updateWidgetsFromState();
|
| 838 |
+
this.setDirtyCanvas(true, true);
|
| 839 |
+
clearTimeout(this.updateTimeout);
|
| 840 |
+
this.updateTimeout = setTimeout(() => this.sendLevelUpdateToPython(), 50);
|
| 841 |
+
}
|
| 842 |
+
},
|
| 843 |
+
|
| 844 |
+
onMouseUp: function (event) {
|
| 845 |
+
if (this.dragging) {
|
| 846 |
+
this.dragging = null;
|
| 847 |
+
this.sendLevelUpdateToPython();
|
| 848 |
+
}
|
| 849 |
+
}
|
| 850 |
+
});
|
| 851 |
+
|
| 852 |
+
const methodsToBind = ['getImageArea', 'getBarArea', 'getInputSliderArea', 'getOutputSliderArea', 'updateHistogram', 'sendLevelUpdateToPython', 'onDrawForeground', 'onMouseDown', 'onMouseMove', 'onMouseUp', 'drawHistogram', 'drawHandle'];
|
| 853 |
+
methodsToBind.forEach(m => { if (node[m]) node[m] = node[m].bind(node); });
|
| 854 |
+
}
|
| 855 |
+
},
|
| 856 |
+
});
|
| 857 |
+
|
| 858 |
+
|
| 859 |
+
|
| 860 |
+
// Event listeners -----------
|
| 861 |
+
|
| 862 |
+
api.addEventListener("eses.levels_preview_base", ({ detail }) => {
|
| 863 |
+
const node = app.graph.getNodeById(detail.node_id);
|
| 864 |
+
|
| 865 |
+
if (node) {
|
| 866 |
+
node.isManuallyResized = false;
|
| 867 |
+
|
| 868 |
+
// Check for both auto_levels_updated
|
| 869 |
+
// and auto_color_updated
|
| 870 |
+
if (detail.updated_levels) {
|
| 871 |
+
|
| 872 |
+
// Runs for auto-levels or auto-color.
|
| 873 |
+
// Update all widgets.
|
| 874 |
+
node.allLevels = detail.updated_levels;
|
| 875 |
+
|
| 876 |
+
const channelWidget = node.widgets.find(w => w.name === "channel");
|
| 877 |
+
const channelMap = { "RGB": "rgb", "Red": "r", "Green": "g", "Blue": "b", "Mask": "mask" };
|
| 878 |
+
|
| 879 |
+
node.activeChannel = channelMap[channelWidget.value] || "rgb";
|
| 880 |
+
|
| 881 |
+
// Refresh the widgets for the currently
|
| 882 |
+
// selected channel to show the newly reset
|
| 883 |
+
// output levels.
|
| 884 |
+
const blackWidget = node.widgets.find(w => w.name === "Black");
|
| 885 |
+
const midWidget = node.widgets.find(w => w.name === "Mid");
|
| 886 |
+
const whiteWidget = node.widgets.find(w => w.name === "White");
|
| 887 |
+
const outputBlackWidget = node.widgets.find(w => w.name === "Output Black");
|
| 888 |
+
const outputWhiteWidget = node.widgets.find(w => w.name === "Output White");
|
| 889 |
+
|
| 890 |
+
const levels = node.allLevels[node.activeChannel];
|
| 891 |
+
|
| 892 |
+
if (levels) {
|
| 893 |
+
if (blackWidget)
|
| 894 |
+
blackWidget.value = parseFloat(levels.black_point.toFixed(2));
|
| 895 |
+
|
| 896 |
+
if (midWidget)
|
| 897 |
+
midWidget.value = parseFloat(levels.mid_point.toFixed(2));
|
| 898 |
+
|
| 899 |
+
if (whiteWidget)
|
| 900 |
+
whiteWidget.value = parseFloat(levels.white_point.toFixed(2));
|
| 901 |
+
|
| 902 |
+
if (outputBlackWidget)
|
| 903 |
+
outputBlackWidget.value = Math.round(levels.output_black);
|
| 904 |
+
|
| 905 |
+
if (outputWhiteWidget)
|
| 906 |
+
outputWhiteWidget.value = Math.round(levels.output_white);
|
| 907 |
+
}
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
const onAssetLoad = async () => {
|
| 911 |
+
if (!node.isManuallyResized) {
|
| 912 |
+
const sizingObj = node.originalImage || node.originalMask;
|
| 913 |
+
|
| 914 |
+
if (sizingObj) {
|
| 915 |
+
const aspectRatio = sizingObj.naturalWidth / sizingObj.naturalHeight;
|
| 916 |
+
const imageDrawWidth = node.size[0] - PADDING * 2;
|
| 917 |
+
const imageDrawHeight = imageDrawWidth / aspectRatio;
|
| 918 |
+
const interactiveHeight = HISTOGRAM_AREA_HEIGHT + OUTPUT_SLIDER_AREA_HEIGHT + AREA_SPACING;
|
| 919 |
+
|
| 920 |
+
node.size[1] = imageDrawHeight + interactiveHeight + HEADER_HEIGHT + PADDING * 2;
|
| 921 |
+
node.isManuallyResized = true; // Set to true after auto-resize
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
await node.updateHistogram();
|
| 925 |
+
await node.sendLevelUpdateToPython();
|
| 926 |
+
};
|
| 927 |
+
|
| 928 |
+
let assetsToLoad = 0;
|
| 929 |
+
|
| 930 |
+
if (detail.image_data)
|
| 931 |
+
assetsToLoad++;
|
| 932 |
+
|
| 933 |
+
if (detail.mask_data)
|
| 934 |
+
assetsToLoad++;
|
| 935 |
+
|
| 936 |
+
if (assetsToLoad === 0) {
|
| 937 |
+
onAssetLoad(); return;
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
let loadedCount = 0;
|
| 941 |
+
const assetLoadedCallback = async () => {
|
| 942 |
+
if (++loadedCount === assetsToLoad) {
|
| 943 |
+
await onAssetLoad();
|
| 944 |
+
}
|
| 945 |
+
};
|
| 946 |
+
|
| 947 |
+
if (detail.image_data) {
|
| 948 |
+
const img = new Image();
|
| 949 |
+
img.src = "data:image/png;base64," + detail.image_data;
|
| 950 |
+
img.onload = assetLoadedCallback;
|
| 951 |
+
node.originalImage = img;
|
| 952 |
+
}
|
| 953 |
+
else {
|
| 954 |
+
node.originalImage = null;
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
if (detail.mask_data) {
|
| 958 |
+
const mask = new Image();
|
| 959 |
+
mask.src = "data:image/png;base64," + detail.mask_data;
|
| 960 |
+
mask.onload = assetLoadedCallback;
|
| 961 |
+
node.originalMask = mask;
|
| 962 |
+
}
|
| 963 |
+
else {
|
| 964 |
+
node.originalMask = null;
|
| 965 |
+
}
|
| 966 |
+
}
|
| 967 |
+
});
|
custom_nodes/NeuralISP/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .neural_isp_node import (
|
| 2 |
+
NODE_CLASS_MAPPINGS,
|
| 3 |
+
NODE_DISPLAY_NAME_MAPPINGS
|
| 4 |
+
)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"NODE_CLASS_MAPPINGS",
|
| 9 |
+
"NODE_DISPLAY_NAME_MAPPINGS"
|
| 10 |
+
]
|
custom_nodes/NeuralISP/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (318 Bytes). View file
|
|
|
custom_nodes/NeuralISP/__pycache__/mirnetv2_model.cpython-313.pyc
ADDED
|
Binary file (15.5 kB). View file
|
|
|
custom_nodes/NeuralISP/__pycache__/mirnetv2_node.cpython-313.pyc
ADDED
|
Binary file (6.82 kB). View file
|
|
|
custom_nodes/NeuralISP/__pycache__/neural_isp_node.cpython-313.pyc
ADDED
|
Binary file (6.44 kB). View file
|
|
|
custom_nodes/NeuralISP/mirnetv2_model.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
## Learning Enriched Features for Fast Image Restoration and Enhancement
|
| 3 |
+
## Syed Waqas Zamir, Aditya Arora, Salman Khan, Munawar Hayat, Fahad Shahbaz Khan, Ming-Hsuan Yang, and Ling Shao
|
| 4 |
+
## IEEE Transactions on Pattern Analysis and Machine Intelligence (TPAMI)
|
| 5 |
+
## https://www.waqaszamir.com/publication/zamir-2022-mirnetv2/
|
| 6 |
+
|
| 7 |
+
# --- Imports --- #
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
import numpy as np
|
| 12 |
+
from pdb import set_trace as stx
|
| 13 |
+
import math
|
| 14 |
+
|
| 15 |
+
##########################################################################
|
| 16 |
+
##---------- Selective Kernel Feature Fusion (SKFF) ----------
|
| 17 |
+
class SKFF(nn.Module):
|
| 18 |
+
def __init__(self, in_channels, height=3,reduction=8,bias=False):
|
| 19 |
+
super(SKFF, self).__init__()
|
| 20 |
+
|
| 21 |
+
self.height = height
|
| 22 |
+
d = max(int(in_channels/reduction),4)
|
| 23 |
+
|
| 24 |
+
self.avg_pool = nn.AdaptiveAvgPool2d(1)
|
| 25 |
+
self.conv_du = nn.Sequential(nn.Conv2d(in_channels, d, 1, padding=0, bias=bias), nn.LeakyReLU(0.2))
|
| 26 |
+
|
| 27 |
+
self.fcs = nn.ModuleList([])
|
| 28 |
+
for i in range(self.height):
|
| 29 |
+
self.fcs.append(nn.Conv2d(d, in_channels, kernel_size=1, stride=1,bias=bias))
|
| 30 |
+
|
| 31 |
+
self.softmax = nn.Softmax(dim=1)
|
| 32 |
+
|
| 33 |
+
def forward(self, inp_feats):
|
| 34 |
+
batch_size = inp_feats[0].shape[0]
|
| 35 |
+
n_feats = inp_feats[0].shape[1]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
inp_feats = torch.cat(inp_feats, dim=1)
|
| 39 |
+
inp_feats = inp_feats.view(batch_size, self.height, n_feats, inp_feats.shape[2], inp_feats.shape[3])
|
| 40 |
+
|
| 41 |
+
feats_U = torch.sum(inp_feats, dim=1)
|
| 42 |
+
feats_S = self.avg_pool(feats_U)
|
| 43 |
+
feats_Z = self.conv_du(feats_S)
|
| 44 |
+
|
| 45 |
+
attention_vectors = [fc(feats_Z) for fc in self.fcs]
|
| 46 |
+
attention_vectors = torch.cat(attention_vectors, dim=1)
|
| 47 |
+
attention_vectors = attention_vectors.view(batch_size, self.height, n_feats, 1, 1)
|
| 48 |
+
# stx()
|
| 49 |
+
attention_vectors = self.softmax(attention_vectors)
|
| 50 |
+
|
| 51 |
+
feats_V = torch.sum(inp_feats*attention_vectors, dim=1)
|
| 52 |
+
|
| 53 |
+
return feats_V
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class ContextBlock(nn.Module):
|
| 58 |
+
|
| 59 |
+
def __init__(self, n_feat, bias=False):
|
| 60 |
+
super(ContextBlock, self).__init__()
|
| 61 |
+
|
| 62 |
+
self.conv_mask = nn.Conv2d(n_feat, 1, kernel_size=1, bias=bias)
|
| 63 |
+
self.softmax = nn.Softmax(dim=2)
|
| 64 |
+
|
| 65 |
+
self.channel_add_conv = nn.Sequential(
|
| 66 |
+
nn.Conv2d(n_feat, n_feat, kernel_size=1, bias=bias),
|
| 67 |
+
nn.LeakyReLU(0.2),
|
| 68 |
+
nn.Conv2d(n_feat, n_feat, kernel_size=1, bias=bias)
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def modeling(self, x):
|
| 72 |
+
batch, channel, height, width = x.size()
|
| 73 |
+
input_x = x
|
| 74 |
+
# [N, C, H * W]
|
| 75 |
+
input_x = input_x.view(batch, channel, height * width)
|
| 76 |
+
# [N, 1, C, H * W]
|
| 77 |
+
input_x = input_x.unsqueeze(1)
|
| 78 |
+
# [N, 1, H, W]
|
| 79 |
+
context_mask = self.conv_mask(x)
|
| 80 |
+
# [N, 1, H * W]
|
| 81 |
+
context_mask = context_mask.view(batch, 1, height * width)
|
| 82 |
+
# [N, 1, H * W]
|
| 83 |
+
context_mask = self.softmax(context_mask)
|
| 84 |
+
# [N, 1, H * W, 1]
|
| 85 |
+
context_mask = context_mask.unsqueeze(3)
|
| 86 |
+
# [N, 1, C, 1]
|
| 87 |
+
context = torch.matmul(input_x, context_mask)
|
| 88 |
+
# [N, C, 1, 1]
|
| 89 |
+
context = context.view(batch, channel, 1, 1)
|
| 90 |
+
|
| 91 |
+
return context
|
| 92 |
+
|
| 93 |
+
def forward(self, x):
|
| 94 |
+
# [N, C, 1, 1]
|
| 95 |
+
context = self.modeling(x)
|
| 96 |
+
|
| 97 |
+
# [N, C, 1, 1]
|
| 98 |
+
channel_add_term = self.channel_add_conv(context)
|
| 99 |
+
x = x + channel_add_term
|
| 100 |
+
|
| 101 |
+
return x
|
| 102 |
+
|
| 103 |
+
##########################################################################
|
| 104 |
+
### --------- Residual Context Block (RCB) ----------
|
| 105 |
+
class RCB(nn.Module):
|
| 106 |
+
def __init__(self, n_feat, kernel_size=3, reduction=8, bias=False, groups=1):
|
| 107 |
+
super(RCB, self).__init__()
|
| 108 |
+
|
| 109 |
+
act = nn.LeakyReLU(0.2)
|
| 110 |
+
|
| 111 |
+
self.body = nn.Sequential(
|
| 112 |
+
nn.Conv2d(n_feat, n_feat, kernel_size=3, stride=1, padding=1, bias=bias, groups=groups),
|
| 113 |
+
act,
|
| 114 |
+
nn.Conv2d(n_feat, n_feat, kernel_size=3, stride=1, padding=1, bias=bias, groups=groups)
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
self.act = act
|
| 118 |
+
|
| 119 |
+
self.gcnet = ContextBlock(n_feat, bias=bias)
|
| 120 |
+
|
| 121 |
+
def forward(self, x):
|
| 122 |
+
res = self.body(x)
|
| 123 |
+
res = self.act(self.gcnet(res))
|
| 124 |
+
res += x
|
| 125 |
+
return res
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
##########################################################################
|
| 129 |
+
##---------- Resizing Modules ----------
|
| 130 |
+
class Down(nn.Module):
|
| 131 |
+
def __init__(self, in_channels, chan_factor, bias=False):
|
| 132 |
+
super(Down, self).__init__()
|
| 133 |
+
|
| 134 |
+
self.bot = nn.Sequential(
|
| 135 |
+
nn.AvgPool2d(2, ceil_mode=True, count_include_pad=False),
|
| 136 |
+
nn.Conv2d(in_channels, int(in_channels*chan_factor), 1, stride=1, padding=0, bias=bias)
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def forward(self, x):
|
| 140 |
+
return self.bot(x)
|
| 141 |
+
|
| 142 |
+
class DownSample(nn.Module):
|
| 143 |
+
def __init__(self, in_channels, scale_factor, chan_factor=2, kernel_size=3):
|
| 144 |
+
super(DownSample, self).__init__()
|
| 145 |
+
self.scale_factor = int(np.log2(scale_factor))
|
| 146 |
+
|
| 147 |
+
modules_body = []
|
| 148 |
+
for i in range(self.scale_factor):
|
| 149 |
+
modules_body.append(Down(in_channels, chan_factor))
|
| 150 |
+
in_channels = int(in_channels * chan_factor)
|
| 151 |
+
|
| 152 |
+
self.body = nn.Sequential(*modules_body)
|
| 153 |
+
|
| 154 |
+
def forward(self, x):
|
| 155 |
+
x = self.body(x)
|
| 156 |
+
return x
|
| 157 |
+
|
| 158 |
+
class Up(nn.Module):
|
| 159 |
+
def __init__(self, in_channels, chan_factor, bias=False):
|
| 160 |
+
super(Up, self).__init__()
|
| 161 |
+
|
| 162 |
+
self.bot = nn.Sequential(
|
| 163 |
+
nn.Conv2d(in_channels, int(in_channels//chan_factor), 1, stride=1, padding=0, bias=bias),
|
| 164 |
+
nn.Upsample(scale_factor=2, mode='bilinear', align_corners=bias)
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
def forward(self, x):
|
| 168 |
+
return self.bot(x)
|
| 169 |
+
|
| 170 |
+
class UpSample(nn.Module):
|
| 171 |
+
def __init__(self, in_channels, scale_factor, chan_factor=2, kernel_size=3):
|
| 172 |
+
super(UpSample, self).__init__()
|
| 173 |
+
self.scale_factor = int(np.log2(scale_factor))
|
| 174 |
+
|
| 175 |
+
modules_body = []
|
| 176 |
+
for i in range(self.scale_factor):
|
| 177 |
+
modules_body.append(Up(in_channels, chan_factor))
|
| 178 |
+
in_channels = int(in_channels // chan_factor)
|
| 179 |
+
|
| 180 |
+
self.body = nn.Sequential(*modules_body)
|
| 181 |
+
|
| 182 |
+
def forward(self, x):
|
| 183 |
+
x = self.body(x)
|
| 184 |
+
return x
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
##########################################################################
|
| 188 |
+
##---------- Multi-Scale Resiudal Block (MRB) ----------
|
| 189 |
+
class MRB(nn.Module):
|
| 190 |
+
def __init__(self, n_feat, height, width, chan_factor, bias,groups):
|
| 191 |
+
super(MRB, self).__init__()
|
| 192 |
+
|
| 193 |
+
self.n_feat, self.height, self.width = n_feat, height, width
|
| 194 |
+
|
| 195 |
+
self.dau_top = RCB(int(n_feat*chan_factor**0), bias=bias, groups=groups)
|
| 196 |
+
self.dau_mid = RCB(int(n_feat*chan_factor**1), bias=bias, groups=groups)
|
| 197 |
+
self.dau_bot = RCB(int(n_feat*chan_factor**2), bias=bias, groups=groups)
|
| 198 |
+
|
| 199 |
+
self.down2 = DownSample(int((chan_factor**0)*n_feat),2,chan_factor)
|
| 200 |
+
self.down4 = nn.Sequential(
|
| 201 |
+
DownSample(int((chan_factor**0)*n_feat),2,chan_factor),
|
| 202 |
+
DownSample(int((chan_factor**1)*n_feat),2,chan_factor)
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
self.up21_1 = UpSample(int((chan_factor**1)*n_feat),2,chan_factor)
|
| 206 |
+
self.up21_2 = UpSample(int((chan_factor**1)*n_feat),2,chan_factor)
|
| 207 |
+
self.up32_1 = UpSample(int((chan_factor**2)*n_feat),2,chan_factor)
|
| 208 |
+
self.up32_2 = UpSample(int((chan_factor**2)*n_feat),2,chan_factor)
|
| 209 |
+
|
| 210 |
+
self.conv_out = nn.Conv2d(n_feat, n_feat, kernel_size=1, padding=0, bias=bias)
|
| 211 |
+
|
| 212 |
+
# only two inputs for SKFF
|
| 213 |
+
self.skff_top = SKFF(int(n_feat*chan_factor**0), 2)
|
| 214 |
+
self.skff_mid = SKFF(int(n_feat*chan_factor**1), 2)
|
| 215 |
+
|
| 216 |
+
def forward(self, x):
|
| 217 |
+
x_top = x.clone()
|
| 218 |
+
x_mid = self.down2(x_top)
|
| 219 |
+
x_bot = self.down4(x_top)
|
| 220 |
+
|
| 221 |
+
x_top = self.dau_top(x_top)
|
| 222 |
+
x_mid = self.dau_mid(x_mid)
|
| 223 |
+
x_bot = self.dau_bot(x_bot)
|
| 224 |
+
|
| 225 |
+
x_mid = self.skff_mid([x_mid, self.up32_1(x_bot)])
|
| 226 |
+
x_top = self.skff_top([x_top, self.up21_1(x_mid)])
|
| 227 |
+
|
| 228 |
+
x_top = self.dau_top(x_top)
|
| 229 |
+
x_mid = self.dau_mid(x_mid)
|
| 230 |
+
x_bot = self.dau_bot(x_bot)
|
| 231 |
+
|
| 232 |
+
x_mid = self.skff_mid([x_mid, self.up32_2(x_bot)])
|
| 233 |
+
x_top = self.skff_top([x_top, self.up21_2(x_mid)])
|
| 234 |
+
|
| 235 |
+
out = self.conv_out(x_top)
|
| 236 |
+
out = out + x
|
| 237 |
+
|
| 238 |
+
return out
|
| 239 |
+
|
| 240 |
+
##########################################################################
|
| 241 |
+
##---------- Recursive Residual Group (RRG) ----------
|
| 242 |
+
class RRG(nn.Module):
|
| 243 |
+
def __init__(self, n_feat, n_MRB, height, width, chan_factor, bias=False, groups=1):
|
| 244 |
+
super(RRG, self).__init__()
|
| 245 |
+
modules_body = [MRB(n_feat, height, width, chan_factor, bias, groups) for _ in range(n_MRB)]
|
| 246 |
+
modules_body.append(nn.Conv2d(n_feat, n_feat, kernel_size=3, stride=1, padding=1, bias=bias))
|
| 247 |
+
self.body = nn.Sequential(*modules_body)
|
| 248 |
+
|
| 249 |
+
def forward(self, x):
|
| 250 |
+
res = self.body(x)
|
| 251 |
+
res += x
|
| 252 |
+
return res
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
##########################################################################
|
| 256 |
+
##---------- MIRNet -----------------------
|
| 257 |
+
class MIRNet_v2(nn.Module):
|
| 258 |
+
def __init__(self,
|
| 259 |
+
inp_channels=3,
|
| 260 |
+
out_channels=3,
|
| 261 |
+
n_feat=80,
|
| 262 |
+
chan_factor=1.5,
|
| 263 |
+
n_RRG=4,
|
| 264 |
+
n_MRB=2,
|
| 265 |
+
height=3,
|
| 266 |
+
width=2,
|
| 267 |
+
scale=1,
|
| 268 |
+
bias=False,
|
| 269 |
+
task= None
|
| 270 |
+
):
|
| 271 |
+
super(MIRNet_v2, self).__init__()
|
| 272 |
+
|
| 273 |
+
kernel_size=3
|
| 274 |
+
self.task = task
|
| 275 |
+
|
| 276 |
+
self.conv_in = nn.Conv2d(inp_channels, n_feat, kernel_size=3, padding=1, bias=bias)
|
| 277 |
+
|
| 278 |
+
modules_body = []
|
| 279 |
+
|
| 280 |
+
modules_body.append(RRG(n_feat, n_MRB, height, width, chan_factor, bias, groups=1))
|
| 281 |
+
modules_body.append(RRG(n_feat, n_MRB, height, width, chan_factor, bias, groups=2))
|
| 282 |
+
modules_body.append(RRG(n_feat, n_MRB, height, width, chan_factor, bias, groups=4))
|
| 283 |
+
modules_body.append(RRG(n_feat, n_MRB, height, width, chan_factor, bias, groups=4))
|
| 284 |
+
|
| 285 |
+
self.body = nn.Sequential(*modules_body)
|
| 286 |
+
self.conv_out = nn.Conv2d(n_feat, out_channels, kernel_size=3, padding=1, bias=bias)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def forward(self, inp_img):
|
| 290 |
+
shallow_feats = self.conv_in(inp_img)
|
| 291 |
+
deep_feats = self.body(shallow_feats)
|
| 292 |
+
|
| 293 |
+
if self.task == 'defocus_deblurring':
|
| 294 |
+
deep_feats += shallow_feats
|
| 295 |
+
out_img = self.conv_out(deep_feats)
|
| 296 |
+
|
| 297 |
+
else:
|
| 298 |
+
out_img = self.conv_out(deep_feats)
|
| 299 |
+
out_img += inp_img
|
| 300 |
+
|
| 301 |
+
return out_img
|
custom_nodes/NeuralISP/neural_isp_node.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import folder_paths
|
| 5 |
+
|
| 6 |
+
from .mirnetv2_model import MIRNet_v2
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
MODEL_DIR = os.path.join(
|
| 10 |
+
folder_paths.models_dir,
|
| 11 |
+
"mirnetv2"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class NeuralISPNode:
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@classmethod
|
| 19 |
+
def INPUT_TYPES(cls):
|
| 20 |
+
|
| 21 |
+
models = []
|
| 22 |
+
|
| 23 |
+
if os.path.exists(MODEL_DIR):
|
| 24 |
+
|
| 25 |
+
for f in os.listdir(MODEL_DIR):
|
| 26 |
+
if f.endswith((".pth", ".pt")):
|
| 27 |
+
models.append(f)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
if not models:
|
| 31 |
+
models = [
|
| 32 |
+
"enhancement_fivek.pth",
|
| 33 |
+
"enhancement_lol.pth",
|
| 34 |
+
"real_denoising.pth"
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
return {
|
| 39 |
+
"required": {
|
| 40 |
+
|
| 41 |
+
"image":
|
| 42 |
+
("IMAGE",),
|
| 43 |
+
|
| 44 |
+
"model":
|
| 45 |
+
(models,),
|
| 46 |
+
|
| 47 |
+
"strength":
|
| 48 |
+
(
|
| 49 |
+
"FLOAT",
|
| 50 |
+
{
|
| 51 |
+
"default":0.2,
|
| 52 |
+
"min":0.0,
|
| 53 |
+
"max":1.0,
|
| 54 |
+
"step":0.05
|
| 55 |
+
}
|
| 56 |
+
),
|
| 57 |
+
|
| 58 |
+
"tile_size":
|
| 59 |
+
(
|
| 60 |
+
"INT",
|
| 61 |
+
{
|
| 62 |
+
"default":512,
|
| 63 |
+
"min":128,
|
| 64 |
+
"max":2048,
|
| 65 |
+
"step":64
|
| 66 |
+
}
|
| 67 |
+
),
|
| 68 |
+
|
| 69 |
+
"overlap":
|
| 70 |
+
(
|
| 71 |
+
"INT",
|
| 72 |
+
{
|
| 73 |
+
"default":128,
|
| 74 |
+
"min":32,
|
| 75 |
+
"max":512,
|
| 76 |
+
"step":32
|
| 77 |
+
}
|
| 78 |
+
)
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
RETURN_TYPES = ("IMAGE",)
|
| 84 |
+
|
| 85 |
+
FUNCTION = "apply"
|
| 86 |
+
|
| 87 |
+
CATEGORY = "📸 shots by Faded/Neural Post FX"
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def load_model(self, filename):
|
| 92 |
+
|
| 93 |
+
device = (
|
| 94 |
+
"cuda"
|
| 95 |
+
if torch.cuda.is_available()
|
| 96 |
+
else "cpu"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
model = MIRNet_v2()
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
path = os.path.join(
|
| 104 |
+
MODEL_DIR,
|
| 105 |
+
filename
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
checkpoint = torch.load(
|
| 110 |
+
path,
|
| 111 |
+
map_location="cpu"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if "params" in checkpoint:
|
| 116 |
+
checkpoint = checkpoint["params"]
|
| 117 |
+
|
| 118 |
+
elif "state_dict" in checkpoint:
|
| 119 |
+
checkpoint = checkpoint["state_dict"]
|
| 120 |
+
|
| 121 |
+
elif "model" in checkpoint:
|
| 122 |
+
checkpoint = checkpoint["model"]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
cleaned = {}
|
| 127 |
+
|
| 128 |
+
for k,v in checkpoint.items():
|
| 129 |
+
|
| 130 |
+
if k.startswith("module."):
|
| 131 |
+
k = k[7:]
|
| 132 |
+
|
| 133 |
+
cleaned[k] = v
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
model.load_state_dict(
|
| 138 |
+
cleaned,
|
| 139 |
+
strict=False
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
model.eval()
|
| 144 |
+
|
| 145 |
+
model.to(device)
|
| 146 |
+
|
| 147 |
+
# MIRNet лучше держать FP32
|
| 148 |
+
model.float()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
return model, device
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def create_mask(
|
| 156 |
+
self,
|
| 157 |
+
h,
|
| 158 |
+
w,
|
| 159 |
+
device
|
| 160 |
+
):
|
| 161 |
+
|
| 162 |
+
mask = torch.ones(
|
| 163 |
+
1,
|
| 164 |
+
1,
|
| 165 |
+
h,
|
| 166 |
+
w,
|
| 167 |
+
device=device
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
return mask
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def process_tile(
|
| 176 |
+
self,
|
| 177 |
+
model,
|
| 178 |
+
img,
|
| 179 |
+
tile_size,
|
| 180 |
+
overlap
|
| 181 |
+
):
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
_,_,h,w = img.shape
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
stride = tile_size - overlap
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
output = torch.zeros_like(
|
| 191 |
+
img,
|
| 192 |
+
dtype=torch.float32
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
weight = torch.zeros_like(
|
| 197 |
+
img,
|
| 198 |
+
dtype=torch.float32
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
for y in range(0,h,stride):
|
| 204 |
+
|
| 205 |
+
for x in range(0,w,stride):
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
y1 = min(
|
| 209 |
+
y+tile_size,
|
| 210 |
+
h
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
x1 = min(
|
| 214 |
+
x+tile_size,
|
| 215 |
+
w
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
y0 = max(
|
| 220 |
+
0,
|
| 221 |
+
y1-tile_size
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
x0 = max(
|
| 225 |
+
0,
|
| 226 |
+
x1-tile_size
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
patch = img[
|
| 231 |
+
:,
|
| 232 |
+
:,
|
| 233 |
+
y0:y1,
|
| 234 |
+
x0:x1
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
ph = patch.shape[-2]
|
| 239 |
+
pw = patch.shape[-1]
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
# MIRNet требует размеры кратные 32
|
| 243 |
+
|
| 244 |
+
pad_h = (
|
| 245 |
+
32 - ph % 32
|
| 246 |
+
) % 32
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
pad_w = (
|
| 250 |
+
32 - pw % 32
|
| 251 |
+
) % 32
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
patch_pad = F.pad(
|
| 256 |
+
patch,
|
| 257 |
+
(
|
| 258 |
+
0,
|
| 259 |
+
pad_w,
|
| 260 |
+
0,
|
| 261 |
+
pad_h
|
| 262 |
+
),
|
| 263 |
+
mode="reflect"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
with torch.no_grad():
|
| 269 |
+
|
| 270 |
+
result = model(
|
| 271 |
+
patch_pad.float()
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# возвращаем исходный размер
|
| 277 |
+
|
| 278 |
+
result = result[
|
| 279 |
+
:,
|
| 280 |
+
:,
|
| 281 |
+
:ph,
|
| 282 |
+
:pw
|
| 283 |
+
]
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
mask = self.create_mask(
|
| 288 |
+
ph,
|
| 289 |
+
pw,
|
| 290 |
+
img.device
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
output[
|
| 296 |
+
:,
|
| 297 |
+
:,
|
| 298 |
+
y0:y1,
|
| 299 |
+
x0:x1
|
| 300 |
+
] += (
|
| 301 |
+
result *
|
| 302 |
+
mask
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
weight[
|
| 308 |
+
:,
|
| 309 |
+
:,
|
| 310 |
+
y0:y1,
|
| 311 |
+
x0:x1
|
| 312 |
+
] += mask
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
return (
|
| 317 |
+
output /
|
| 318 |
+
weight.clamp(min=1e-6)
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def apply(
|
| 324 |
+
self,
|
| 325 |
+
image,
|
| 326 |
+
model,
|
| 327 |
+
strength,
|
| 328 |
+
tile_size,
|
| 329 |
+
overlap
|
| 330 |
+
):
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
net,device = self.load_model(
|
| 334 |
+
model
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
img = image[0]
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
img = img.permute(
|
| 342 |
+
2,
|
| 343 |
+
0,
|
| 344 |
+
1
|
| 345 |
+
).unsqueeze(0)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
img = img.to(device)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
with torch.no_grad():
|
| 353 |
+
|
| 354 |
+
result = self.process_tile(
|
| 355 |
+
net,
|
| 356 |
+
img,
|
| 357 |
+
tile_size,
|
| 358 |
+
overlap
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
result = (
|
| 364 |
+
result * strength
|
| 365 |
+
+
|
| 366 |
+
img.float() *
|
| 367 |
+
(1-strength)
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
result = result.clamp(
|
| 372 |
+
0,
|
| 373 |
+
1
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
result = result.squeeze(0)
|
| 378 |
+
|
| 379 |
+
result = result.permute(
|
| 380 |
+
1,
|
| 381 |
+
2,
|
| 382 |
+
0
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
return (
|
| 387 |
+
result.unsqueeze(0)
|
| 388 |
+
.cpu(),
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
NODE_CLASS_MAPPINGS = {
|
| 394 |
+
|
| 395 |
+
"NeuralISP":
|
| 396 |
+
NeuralISPNode
|
| 397 |
+
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 403 |
+
|
| 404 |
+
"NeuralISP":
|
| 405 |
+
"📸Neural ISP (x MIRNet)"
|
| 406 |
+
|
| 407 |
+
}
|
custom_nodes/comfyui-faded-branding/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .faded_logo_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
| 2 |
+
|
| 3 |
+
# КРИТИЧЕСКИ ВАЖНО: без этой строчки ComfyUI не загрузит ваш JavaScript!
|
| 4 |
+
WEB_DIRECTORY = "./js"
|
| 5 |
+
|
| 6 |
+
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
custom_nodes/comfyui-faded-branding/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (350 Bytes). View file
|
|
|
custom_nodes/comfyui-faded-branding/__pycache__/faded_logo_node.cpython-313.pyc
ADDED
|
Binary file (1.12 kB). View file
|
|
|
custom_nodes/comfyui-faded-branding/__pycache__/ocean_logo_node.cpython-313.pyc
ADDED
|
Binary file (1.1 kB). View file
|
|
|
custom_nodes/comfyui-faded-branding/faded_logo_node.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class OceanLogoDisplay:
|
| 2 |
+
def __init__(self):
|
| 3 |
+
pass
|
| 4 |
+
|
| 5 |
+
@classmethod
|
| 6 |
+
def INPUT_TYPES(s):
|
| 7 |
+
# Пустой словарь означает отсутствие входных пинов
|
| 8 |
+
return {
|
| 9 |
+
"required": {},
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
# Пустой кортеж гарантирует, что нода не выдает никаких данных в граф
|
| 13 |
+
RETURN_TYPES = ()
|
| 14 |
+
FUNCTION = "display_logo"
|
| 15 |
+
|
| 16 |
+
CATEGORY = "📸 shots by Faded/Utils"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def display_logo(self):
|
| 20 |
+
# Эта функция теперь никогда не будет вызвана при инференсе,
|
| 21 |
+
# так как нода не участвует в графе выполнения.
|
| 22 |
+
return {}
|
| 23 |
+
|
| 24 |
+
NODE_CLASS_MAPPINGS = {
|
| 25 |
+
"OceanLogoDisplay": OceanLogoDisplay
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 29 |
+
"OceanLogoDisplay": "🖼️ Faded Branding Banner"
|
| 30 |
+
}
|
custom_nodes/comfyui-faded-branding/js/faded_logo.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { app } from "../../../scripts/app.js";
|
| 2 |
+
|
| 3 |
+
const original_drawNode = LGraphCanvas.prototype.drawNode;
|
| 4 |
+
LGraphCanvas.prototype.drawNode = function(node, ctx) {
|
| 5 |
+
if (node && node.type === "OceanLogoDisplay") {
|
| 6 |
+
if (node.flags.collapsed) return;
|
| 7 |
+
node.title_height = 0;
|
| 8 |
+
|
| 9 |
+
ctx.save();
|
| 10 |
+
const w = node.size[0];
|
| 11 |
+
const h = node.size[1];
|
| 12 |
+
|
| 13 |
+
if (this.selected_nodes && this.selected_nodes[node.id]) {
|
| 14 |
+
ctx.lineWidth = 3;
|
| 15 |
+
ctx.strokeStyle = this.node_colors?.selected || "#ff9000";
|
| 16 |
+
ctx.beginPath();
|
| 17 |
+
const m = 3;
|
| 18 |
+
if (ctx.roundRect) ctx.roundRect(-m, -m, w + m * 2, h + m * 2, 12);
|
| 19 |
+
else ctx.rect(-m, -m, w + m * 2, h + m * 2);
|
| 20 |
+
ctx.stroke();
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
if (node.bgImage) {
|
| 24 |
+
const imgW = node.bgImage.width;
|
| 25 |
+
const imgH = node.bgImage.height;
|
| 26 |
+
const imgRatio = imgW / imgH;
|
| 27 |
+
const nodeRatio = w / h;
|
| 28 |
+
let drawW, drawH;
|
| 29 |
+
if (imgRatio > nodeRatio) {
|
| 30 |
+
drawW = w;
|
| 31 |
+
drawH = w / imgRatio;
|
| 32 |
+
} else {
|
| 33 |
+
drawH = h;
|
| 34 |
+
drawW = h * imgRatio;
|
| 35 |
+
}
|
| 36 |
+
const offsetX = (w - drawW) / 2;
|
| 37 |
+
const offsetY = (h - drawH) / 2;
|
| 38 |
+
ctx.drawImage(node.bgImage, offsetX, offsetY, drawW, drawH);
|
| 39 |
+
}
|
| 40 |
+
ctx.restore();
|
| 41 |
+
return;
|
| 42 |
+
}
|
| 43 |
+
return original_drawNode.apply(this, arguments);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
app.registerExtension({
|
| 47 |
+
name: "Faded.BrandingBanner",
|
| 48 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
| 49 |
+
if (nodeData.name === "OceanLogoDisplay") {
|
| 50 |
+
|
| 51 |
+
Object.defineProperty(nodeType.prototype, "title", {
|
| 52 |
+
get: function() { return ""; },
|
| 53 |
+
set: function(v) {},
|
| 54 |
+
enumerable: true,
|
| 55 |
+
configurable: true
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
nodeType.prototype.loadLocalImage = function(filename) {
|
| 59 |
+
if (!filename) return;
|
| 60 |
+
|
| 61 |
+
const imgUrl = `/extensions/comfyui-faded-branding/logo/${filename}?t=${Date.now()}`;
|
| 62 |
+
const img = new Image();
|
| 63 |
+
|
| 64 |
+
img.onload = () => {
|
| 65 |
+
this.bgImage = img;
|
| 66 |
+
this.setDirtyCanvas(true, true);
|
| 67 |
+
console.log(`[Faded] Загружена картинка: ${filename}`);
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
img.onerror = (e) => {
|
| 71 |
+
console.error(`[Faded] Ошибка загрузки: ${filename}`, e);
|
| 72 |
+
// Fallback на logo.png
|
| 73 |
+
if (filename !== "logo.png") {
|
| 74 |
+
console.warn(`[Faded] Пробуем загрузить logo.png`);
|
| 75 |
+
this.loadLocalImage("logo.png");
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
img.src = imgUrl;
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
| 83 |
+
nodeType.prototype.onNodeCreated = function() {
|
| 84 |
+
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
| 85 |
+
|
| 86 |
+
this.addProperty("Image to show", "logo.png", "string");
|
| 87 |
+
this.bgImage = null;
|
| 88 |
+
this.size = [320, 180];
|
| 89 |
+
this.title_height = 0;
|
| 90 |
+
|
| 91 |
+
// Принудительно загружаем картинку при создании ноды
|
| 92 |
+
setTimeout(() => {
|
| 93 |
+
this.loadLocalImage(this.properties["Image to show"] || "logo.png");
|
| 94 |
+
}, 100);
|
| 95 |
+
|
| 96 |
+
return r;
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Восстановление при загрузке воркфлоу
|
| 100 |
+
nodeType.prototype.onConfigure = function(info) {
|
| 101 |
+
if (info.properties && info.properties["Image to show"]) {
|
| 102 |
+
// Небольшая задержка для надёжности
|
| 103 |
+
setTimeout(() => {
|
| 104 |
+
this.loadLocalImage(info.properties["Image to show"]);
|
| 105 |
+
}, 200);
|
| 106 |
+
}
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
nodeType.prototype.onPropertyChanged = function(property, value) {
|
| 110 |
+
if (property === "Image to show") {
|
| 111 |
+
this.loadLocalImage(value);
|
| 112 |
+
}
|
| 113 |
+
return true;
|
| 114 |
+
};
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
});
|
custom_nodes/comfyui-faded-branding/js/logo/info03.png
ADDED
|
custom_nodes/comfyui-faded-branding/js/logo/logo01.png
ADDED
|
Git LFS Details
|
custom_nodes/comfyui-faded-branding/js/logo/logo40.png
ADDED
|
Git LFS Details
|
custom_nodes/comfyui-faded-branding/js/logo/logo44.png
ADDED
|
Git LFS Details
|
custom_nodes/comfyui-faded-branding/js/logo/text01.png
ADDED
|
custom_nodes/comfyui_essentials/luts/realisticLUT.cube
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
custom_nodes/image_routing.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ImageRouting:
|
| 2 |
+
@classmethod
|
| 3 |
+
def INPUT_TYPES(s):
|
| 4 |
+
return {
|
| 5 |
+
"required": {},
|
| 6 |
+
"optional": {
|
| 7 |
+
"image1": ("IMAGE",),
|
| 8 |
+
"image2": ("IMAGE",),
|
| 9 |
+
"image3": ("IMAGE",),
|
| 10 |
+
"image4": ("IMAGE",),
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
RETURN_TYPES = ("IMAGE",)
|
| 15 |
+
RETURN_NAMES = ("image",)
|
| 16 |
+
FUNCTION = "route"
|
| 17 |
+
CATEGORY = "📸 shots by Faded/Utils"
|
| 18 |
+
|
| 19 |
+
def route(self, image1=None, image2=None, image3=None, image4=None):
|
| 20 |
+
if image1 is not None:
|
| 21 |
+
return (image1,)
|
| 22 |
+
if image2 is not None:
|
| 23 |
+
return (image2,)
|
| 24 |
+
if image3 is not None:
|
| 25 |
+
return (image3,)
|
| 26 |
+
if image4 is not None:
|
| 27 |
+
return (image4,)
|
| 28 |
+
|
| 29 |
+
return (image1,)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
NODE_CLASS_MAPPINGS = {
|
| 33 |
+
"ImageRouting": ImageRouting
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 37 |
+
"ImageRouting": "Image Routing"
|
| 38 |
+
}
|
custom_nodes/latent_routing.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
# --- 1. АВТОМАТИЧЕСКАЯ НОДА (СТАРАЯ) ---
|
| 4 |
+
class LatentRouting:
|
| 5 |
+
@classmethod
|
| 6 |
+
def INPUT_TYPES(s):
|
| 7 |
+
return {
|
| 8 |
+
"required": {},
|
| 9 |
+
"optional": {
|
| 10 |
+
"samples1": ("LATENT",),
|
| 11 |
+
"samples2": ("LATENT",),
|
| 12 |
+
"samples3": ("LATENT",),
|
| 13 |
+
"samples4": ("LATENT",),
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
RETURN_TYPES = ("LATENT",)
|
| 18 |
+
RETURN_NAMES = ("samples",)
|
| 19 |
+
FUNCTION = "route"
|
| 20 |
+
CATEGORY = "utils"
|
| 21 |
+
|
| 22 |
+
def route(self, samples1=None, samples2=None, samples3=None, samples4=None):
|
| 23 |
+
if samples1 is not None:
|
| 24 |
+
return (samples1,)
|
| 25 |
+
if samples2 is not None:
|
| 26 |
+
return (samples2,)
|
| 27 |
+
if samples3 is not None:
|
| 28 |
+
return (samples3,)
|
| 29 |
+
if samples4 is not None:
|
| 30 |
+
return (samples4,)
|
| 31 |
+
|
| 32 |
+
return (None,)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# --- 2. НОДА С РУЧНЫМ ПЕРЕКЛЮЧАТЕЛЕМ (НОВАЯ) ---
|
| 36 |
+
class LatentSwitchManual:
|
| 37 |
+
@classmethod
|
| 38 |
+
def INPUT_TYPES(s):
|
| 39 |
+
return {
|
| 40 |
+
"required": {
|
| 41 |
+
# Выпадающий список для точного выбора входа
|
| 42 |
+
"select": (["1", "2", "3", "4"],),
|
| 43 |
+
},
|
| 44 |
+
"optional": {
|
| 45 |
+
"samples1": ("LATENT",),
|
| 46 |
+
"samples2": ("LATENT",),
|
| 47 |
+
"samples3": ("LATENT",),
|
| 48 |
+
"samples4": ("LATENT",),
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
RETURN_TYPES = ("LATENT",)
|
| 53 |
+
RETURN_NAMES = ("samples",)
|
| 54 |
+
FUNCTION = "switch"
|
| 55 |
+
CATEGORY = "📸 shots by Faded/Utils"
|
| 56 |
+
|
| 57 |
+
def switch(self, select, samples1=None, samples2=None, samples3=None, samples4=None):
|
| 58 |
+
# Строгая проверка по выбранному пункту меню
|
| 59 |
+
if select == "1" and samples1 is not None:
|
| 60 |
+
return (samples1,)
|
| 61 |
+
if select == "2" and samples2 is not None:
|
| 62 |
+
return (samples2,)
|
| 63 |
+
if select == "3" and samples3 is not None:
|
| 64 |
+
return (samples3,)
|
| 65 |
+
if select == "4" and samples4 is not None:
|
| 66 |
+
return (samples4,)
|
| 67 |
+
|
| 68 |
+
# Если выбранный вход пустой (например, отключен байпасом),
|
| 69 |
+
# берем первый рабочий, чтобы не выдавать ошибку
|
| 70 |
+
if samples1 is not None: return (samples1,)
|
| 71 |
+
if samples2 is not None: return (samples2,)
|
| 72 |
+
if samples3 is not None: return (samples3,)
|
| 73 |
+
if samples4 is not None: return (samples4,)
|
| 74 |
+
|
| 75 |
+
return (None,)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# --- 3. РЕГИСТРАЦИЯ НОД В COMFYUI ---
|
| 79 |
+
NODE_CLASS_MAPPINGS = {
|
| 80 |
+
"LatentRouting": LatentRouting,
|
| 81 |
+
"LatentSwitchManual": LatentSwitchManual
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 85 |
+
"LatentRouting": "Latent Routing",
|
| 86 |
+
"LatentSwitchManual": "Latent Routing Manual"
|
| 87 |
+
}
|
custom_nodes/latent_trick.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.fft as fft
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
# ─────────────────────────────────────────────
|
| 6 |
+
# Core (1:1 логика оригинала)
|
| 7 |
+
# ─────────────────────────────────────────────
|
| 8 |
+
|
| 9 |
+
def create_frequency_masks(height, width, device, low_cutoff, high_cutoff):
|
| 10 |
+
freq_y = torch.fft.fftfreq(height, device=device)
|
| 11 |
+
freq_x = torch.fft.fftfreq(width, device=device)
|
| 12 |
+
fy, fx = torch.meshgrid(freq_y, freq_x, indexing="ij")
|
| 13 |
+
freq_dist = torch.sqrt(fy ** 2 + fx ** 2)
|
| 14 |
+
|
| 15 |
+
max_freq = freq_dist.max()
|
| 16 |
+
if max_freq > 0:
|
| 17 |
+
freq_dist = freq_dist / max_freq
|
| 18 |
+
|
| 19 |
+
sharpness = 20.0
|
| 20 |
+
|
| 21 |
+
low_mask = torch.sigmoid(sharpness * (low_cutoff - freq_dist))
|
| 22 |
+
high_mask = torch.sigmoid(sharpness * (freq_dist - high_cutoff))
|
| 23 |
+
mid_mask = torch.clamp(1.0 - low_mask - high_mask, min=0.0)
|
| 24 |
+
|
| 25 |
+
return low_mask, mid_mask, high_mask
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _expand_mask(mask, target):
|
| 29 |
+
while mask.dim() < target.dim():
|
| 30 |
+
mask = mask.unsqueeze(0)
|
| 31 |
+
return mask.expand_as(target)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def spectral_latent_enhance(latent, detail_boost, denoise_high, low_cutoff, high_cutoff):
|
| 35 |
+
h, w = latent.shape[-2:]
|
| 36 |
+
|
| 37 |
+
if h <= 4 or w <= 4:
|
| 38 |
+
return latent
|
| 39 |
+
|
| 40 |
+
freq = fft.fft2(latent)
|
| 41 |
+
|
| 42 |
+
low_m, mid_m, high_m = create_frequency_masks(
|
| 43 |
+
h, w, latent.device, low_cutoff, high_cutoff
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
low_m = _expand_mask(low_m, freq)
|
| 47 |
+
mid_m = _expand_mask(mid_m, freq)
|
| 48 |
+
high_m = _expand_mask(high_m, freq)
|
| 49 |
+
|
| 50 |
+
gain = (
|
| 51 |
+
low_m * 1.0 +
|
| 52 |
+
mid_m * (1.0 + detail_boost) +
|
| 53 |
+
high_m * (1.0 - denoise_high)
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
return fft.ifft2(freq * gain).real
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# ─────────────────────────────────────────────
|
| 60 |
+
# Node (как в оригинале, но без UI параметров)
|
| 61 |
+
# ─────────────────────────────────────────────
|
| 62 |
+
|
| 63 |
+
class LatentTrickNode:
|
| 64 |
+
|
| 65 |
+
@classmethod
|
| 66 |
+
def INPUT_TYPES(cls):
|
| 67 |
+
return {
|
| 68 |
+
"required": {
|
| 69 |
+
"latent": ("LATENT",),
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
RETURN_TYPES = ("LATENT",)
|
| 74 |
+
FUNCTION = "apply"
|
| 75 |
+
CATEGORY = "📸 shots by Faded/Utils"
|
| 76 |
+
|
| 77 |
+
def apply(self, latent):
|
| 78 |
+
|
| 79 |
+
# 🔒 фиксированные значения (как ты просил)
|
| 80 |
+
detail_boost = 0.10
|
| 81 |
+
denoise_high = 0.00
|
| 82 |
+
low_cutoff = 0.40
|
| 83 |
+
high_cutoff = 0.73
|
| 84 |
+
|
| 85 |
+
out = latent.copy()
|
| 86 |
+
samples = out["samples"]
|
| 87 |
+
|
| 88 |
+
out["samples"] = spectral_latent_enhance(
|
| 89 |
+
samples,
|
| 90 |
+
detail_boost,
|
| 91 |
+
denoise_high,
|
| 92 |
+
low_cutoff,
|
| 93 |
+
high_cutoff,
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
return (out,)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ─────────────────────────────────────────────
|
| 100 |
+
# Registration
|
| 101 |
+
# ─────────────────────────────────────────────
|
| 102 |
+
|
| 103 |
+
NODE_CLASS_MAPPINGS = {
|
| 104 |
+
"latent_trick": LatentTrickNode,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 108 |
+
"latent_trick": "Latent Trick",
|
| 109 |
+
}
|
custom_nodes/save_image_jpeg.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import numpy as np
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from PIL import Image, ImageCms
|
| 6 |
+
import folder_paths
|
| 7 |
+
|
| 8 |
+
# Standard Apple ImageIO DQT Quantization Tables for JPEG (Luminance & Chrominance)
|
| 9 |
+
APPLE_DQT_LUMA = [
|
| 10 |
+
2, 1, 1, 2, 2, 4, 5, 6,
|
| 11 |
+
1, 1, 1, 2, 3, 6, 6, 6,
|
| 12 |
+
1, 1, 2, 2, 4, 6, 7, 6,
|
| 13 |
+
1, 2, 2, 3, 5, 9, 8, 7,
|
| 14 |
+
2, 2, 4, 6, 7, 11, 10, 8,
|
| 15 |
+
2, 3, 5, 6, 8, 10, 11, 9,
|
| 16 |
+
5, 6, 8, 9, 10, 12, 12, 10,
|
| 17 |
+
7, 9, 9, 10, 11, 10, 10, 10
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
APPLE_DQT_CHROMA = [
|
| 21 |
+
2, 2, 2, 4, 8, 8, 8, 8,
|
| 22 |
+
2, 2, 3, 5, 8, 8, 8, 8,
|
| 23 |
+
2, 3, 5, 8, 8, 8, 8, 8,
|
| 24 |
+
4, 5, 8, 8, 8, 8, 8, 8,
|
| 25 |
+
8, 8, 8, 8, 8, 8, 8, 8,
|
| 26 |
+
8, 8, 8, 8, 8, 8, 8, 8,
|
| 27 |
+
8, 8, 8, 8, 8, 8, 8, 8,
|
| 28 |
+
8, 8, 8, 8, 8, 8, 8, 8
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
def process_filename_prefix(prefix):
|
| 32 |
+
def replace_date(match):
|
| 33 |
+
fmt_str = match.group(1)
|
| 34 |
+
replacements = [
|
| 35 |
+
("yyyy", "%Y"), ("YY", "%y"), ("yy", "%y"),
|
| 36 |
+
("MM", "%m"), ("dd", "%d"),
|
| 37 |
+
("HH", "%H"), ("hh", "%H"),
|
| 38 |
+
("mm", "%M"), ("ss", "%S")
|
| 39 |
+
]
|
| 40 |
+
for old, new in replacements:
|
| 41 |
+
fmt_str = fmt_str.replace(old, new)
|
| 42 |
+
return datetime.now().strftime(fmt_str)
|
| 43 |
+
|
| 44 |
+
prefix = re.sub(r"%date:([^%]+)%", replace_date, prefix)
|
| 45 |
+
return prefix
|
| 46 |
+
|
| 47 |
+
class SaveImageJPEG:
|
| 48 |
+
def __init__(self):
|
| 49 |
+
self.output_dir = folder_paths.get_output_directory()
|
| 50 |
+
self.type = "output"
|
| 51 |
+
self.prefix_append = ""
|
| 52 |
+
|
| 53 |
+
# Generate binary sRGB IEC61966-2.1 ICC profile
|
| 54 |
+
srgb_profile = ImageCms.createProfile("sRGB")
|
| 55 |
+
self.srgb_icc_bytes = ImageCms.ImageCmsProfile(srgb_profile).tobytes()
|
| 56 |
+
|
| 57 |
+
@classmethod
|
| 58 |
+
def INPUT_TYPES(s):
|
| 59 |
+
return {
|
| 60 |
+
"required": {
|
| 61 |
+
"Images": ("IMAGE", {"tooltip": "Input images tensor to save."}),
|
| 62 |
+
"Filename Prefix": ("STRING", {"default": "iPhone_JPEG"}),
|
| 63 |
+
"Quality": ("INT", {"default": 88, "min": 1, "max": 100, "step": 1, "tooltip": "JPEG Quality factor (Apple default is 85-90%)."}),
|
| 64 |
+
"Use Apple JPEG Tables": ("BOOLEAN", {"default": True, "tooltip": "Use custom Apple DQT quantization tables."}),
|
| 65 |
+
"Subsampling": (["4:2:0", "4:2:2", "4:4:4"], {"default": "4:2:0", "tooltip": "Chroma subsampling mode (Apple default is 4:2:0)."}),
|
| 66 |
+
},
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
RETURN_TYPES = ()
|
| 70 |
+
FUNCTION = "save_images"
|
| 71 |
+
OUTPUT_NODE = True
|
| 72 |
+
CATEGORY = "📸 shots by Faded/Utils"
|
| 73 |
+
DESCRIPTION = "Saves JPEG images matching Apple's compression profile with dynamic date formatting."
|
| 74 |
+
|
| 75 |
+
def save_images(self, Images, **kwargs):
|
| 76 |
+
filename_prefix = kwargs.get("Filename Prefix", "iPhone_JPEG")
|
| 77 |
+
quality = kwargs.get("Quality", 88)
|
| 78 |
+
use_apple_jpeg_tables = kwargs.get("Use Apple JPEG Tables", True)
|
| 79 |
+
subsampling = kwargs.get("Subsampling", "4:2:0")
|
| 80 |
+
|
| 81 |
+
filename_prefix += self.prefix_append
|
| 82 |
+
filename_prefix = process_filename_prefix(filename_prefix)
|
| 83 |
+
|
| 84 |
+
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
| 85 |
+
filename_prefix, self.output_dir, Images[0].shape[2], Images[0].shape[1]
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
results = list()
|
| 89 |
+
qtables_param = [APPLE_DQT_LUMA, APPLE_DQT_CHROMA] if use_apple_jpeg_tables else None
|
| 90 |
+
|
| 91 |
+
for (batch_number, image) in enumerate(Images):
|
| 92 |
+
# Convert PyTorch tensor (float 0..1) to PIL Image (uint8 0..255 RGB)
|
| 93 |
+
i = 255. * image.cpu().numpy()
|
| 94 |
+
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
| 95 |
+
|
| 96 |
+
file = f"{filename}_{counter:05}_.jpg"
|
| 97 |
+
file_path = os.path.join(full_output_folder, file)
|
| 98 |
+
|
| 99 |
+
img.save(
|
| 100 |
+
file_path,
|
| 101 |
+
format="JPEG",
|
| 102 |
+
quality=quality,
|
| 103 |
+
subsampling=subsampling,
|
| 104 |
+
progressive=False,
|
| 105 |
+
qtables=qtables_param,
|
| 106 |
+
icc_profile=self.srgb_icc_bytes,
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
results.append({
|
| 110 |
+
"filename": file,
|
| 111 |
+
"subfolder": subfolder,
|
| 112 |
+
"type": self.type
|
| 113 |
+
})
|
| 114 |
+
counter += 1
|
| 115 |
+
|
| 116 |
+
return {"ui": {"images": results}}
|
| 117 |
+
|
| 118 |
+
NODE_CLASS_MAPPINGS = {
|
| 119 |
+
"SaveImageJPEG": SaveImageJPEG
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 123 |
+
"SaveImageJPEG": "Save Image (JPEG)"
|
| 124 |
+
}
|
custom_nodes/srgb_noise/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .srgb_noise_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"NODE_CLASS_MAPPINGS",
|
| 5 |
+
"NODE_DISPLAY_NAME_MAPPINGS"
|
| 6 |
+
]
|
custom_nodes/srgb_noise/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (309 Bytes). View file
|
|
|
custom_nodes/srgb_noise/__pycache__/srgb_noise_node.cpython-313.pyc
ADDED
|
Binary file (7.98 kB). View file
|
|
|
custom_nodes/srgb_noise/repo/Crop_SIDD.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import utils.utils_image as util
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
ori_dir = './Datasets/SIDD_Medium_Srgb_Parts/' # put original SIDD directory here
|
| 6 |
+
target_dir = './Datasets/SIDD_Medium_Crop/' # put target cropped SIDD directory here
|
| 7 |
+
|
| 8 |
+
if not os.path.exists(target_dir):
|
| 9 |
+
os.mkdir(target_dir)
|
| 10 |
+
|
| 11 |
+
util.crop_sidd_dataset(ori_dir, target_dir, p_size=512, p_overlap=128, p_max=800)
|
| 12 |
+
|
| 13 |
+
if __name__ == '__main__':
|
| 14 |
+
main()
|
custom_nodes/srgb_noise/repo/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# sRGB Real Noise Synthesizing with Neighboring Correlation-Aware Noise Model (CVPR'23)
|
| 2 |
+
|
| 3 |
+
This is the official inplementation of CVPR 2023 paper [sRGB Real Noise Synthesizing with Neighboring Correlation-Aware Noise Model](https://openaccess.thecvf.com/content/CVPR2023/papers/Fu_sRGB_Real_Noise_Synthesizing_With_Neighboring_Correlation-Aware_Noise_Model_CVPR_2023_paper.pdf).
|
| 4 |
+
|
| 5 |
+
## Abstract
|
| 6 |
+
|
| 7 |
+
Modeling and synthesizing real noise in the standard RGB (sRGB) domain is challenging due to the complicated noise distribution. While most of the deep noise generators
|
| 8 |
+
proposed to synthesize sRGB real noise using an end-to-end trained model, the lack of explicit noise modeling degrades the quality of their synthesized noise. In this work, we propose to model the real noise as not only dependent on the underlying clean image pixel intensity, but also highly correlated to its neighboring noise realization within the local region. Correspondingly, we propose a novel noise synthesizing framework by explicitly learning its neighboring correlation on top of the signal dependency. With the proposed noise model, our framework greatly bridges the distribution gap between synthetic noise and real noise. We show that our generated “real” sRGB noisy images can be used for training supervised deep denoisers, thus to improve their real denoising results with a large margin, comparing to the popular classic denoisers or the deep denoisers that are trained on other sRGB noise generators.
|
| 9 |
+
|
| 10 |
+
For more details, please refer to our original [paper](https://openaccess.thecvf.com/content/CVPR2023/papers/Fu_sRGB_Real_Noise_Synthesizing_With_Neighboring_Correlation-Aware_Noise_Model_CVPR_2023_paper.pdf).
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+

|
| 14 |
+
|
| 15 |
+
## Setup
|
| 16 |
+
|
| 17 |
+
### Requirements
|
| 18 |
+
|
| 19 |
+
- PyTorch 1.13.1
|
| 20 |
+
- CUDA 11.7
|
| 21 |
+
- Python 3.9
|
| 22 |
+
- opencv 4.7.0
|
| 23 |
+
- numpy 1.21.5
|
| 24 |
+
|
| 25 |
+
### Directory
|
| 26 |
+
|
| 27 |
+
Descriptions of code directory.
|
| 28 |
+
|
| 29 |
+
```
|
| 30 |
+
sRGB-Noise-Synthesis
|
| 31 |
+
|-- data
|
| 32 |
+
|-- data_preparation
|
| 33 |
+
|-- Datasets
|
| 34 |
+
|-- SIDD_Medium_Srgb_Parts
|
| 35 |
+
|-- SIDD_Medium_Crop
|
| 36 |
+
|-- models
|
| 37 |
+
|-- options
|
| 38 |
+
|-- pretrain
|
| 39 |
+
|-- saves
|
| 40 |
+
|-- test
|
| 41 |
+
|-- imgs
|
| 42 |
+
|-- saves
|
| 43 |
+
|-- utils
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Pretrained models
|
| 47 |
+
|
| 48 |
+
Download pretrained checkpoints here and place them into `/pretrain/` folder.
|
| 49 |
+
|
| 50 |
+
Google drive: | Camera name | [IP](https://drive.google.com/drive/folders/1jouUbKfSz9vpvChuY5J-gvWN4JLkYs3H?usp=sharing) | [G4](https://drive.google.com/drive/folders/1oqhax1aESnqIx6q4jVqp_7XuoloGlJuk?usp=sharing) | [GP](https://drive.google.com/drive/folders/1L2lzAMmzUT1flYWdkmTtGnPAWBirGvAr?usp=sharing) | [N6](https://drive.google.com/drive/folders/1I7TEWZ2HnEVjAvE24wwYGBQvElqqdEH4?usp=sharing) | [S6](https://drive.google.com/drive/folders/1HgHNluBdvky5HHB-fvRDelfBRv9gebcT?usp=sharing) |
|
| 51 |
+
|
| 52 |
+
## Quick test
|
| 53 |
+
|
| 54 |
+
Test our noise synthesis pipeline with pre-trained models
|
| 55 |
+
|
| 56 |
+
1. place noisy-clean image pairs into `/test/imgs/` folder. Name noisy image as `noisy.png`, clean image as `clean.png`.
|
| 57 |
+
2. Run the code as
|
| 58 |
+
```python main_test.py --cam_name IP --NeCA_type S```. You can also control detail test configurations (e.g. cam_name: IP/GP/G4/N6/S6, NecA_type: S/W).
|
| 59 |
+
3. Images are saved in `/test/saves/` folder.
|
| 60 |
+
|
| 61 |
+
## Train
|
| 62 |
+
|
| 63 |
+
1. Download SIDD-Medium sRGB part at [here](https://www.eecs.yorku.ca/~kamel/sidd/dataset.php).
|
| 64 |
+
2. Use `Crop_SIDD.py` to crop images into 512×512 patches.
|
| 65 |
+
3. Run the code as
|
| 66 |
+
|
| 67 |
+
```
|
| 68 |
+
python main_train_jointly.py --dir_save './saves/noise_syn/' --train_img_dir './Datasets/SIDD_Medium_Crop/' --data_prep_dir './data_preparation/' --not_aug --patch_size 96 --cam_name GP --batch_size 16 --num_workers 8 --verbose --epochs 5000 --gpu_ids 0 --joint_start_epoch 50 --seperate_train --gan_mode wgangp --test_epoch 5 --lr_decay_2 80
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Results
|
| 72 |
+
|
| 73 |
+
### Visual results
|
| 74 |
+
|
| 75 |
+
<p align=center><img width="50%" src="./doc/visual_results.png"/></p>
|
| 76 |
+
|
| 77 |
+
### Denoising performance on SIDD/DND benchmark
|
| 78 |
+
<p align=center><img width="70%" src="./doc/quantitative_results.png"/></p>
|
| 79 |
+
|
| 80 |
+
## Reference
|
| 81 |
+
|
| 82 |
+
Our implementation is based on [KAIR](https://github.com/cszn/KAIR), [CycleGAN](https://github.com/junyanz/CycleGAN). We would like to thank them.
|
| 83 |
+
|
| 84 |
+
## Citation
|
| 85 |
+
```
|
| 86 |
+
@InProceedings{Fu_2023_CVPR,
|
| 87 |
+
author = {Fu, Zixuan and Guo, Lanqing and Wen, Bihan},
|
| 88 |
+
title = {sRGB Real Noise Synthesizing With Neighboring Correlation-Aware Noise Model},
|
| 89 |
+
booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
| 90 |
+
month = {June},
|
| 91 |
+
year = {2023},
|
| 92 |
+
pages = {1683-1691}
|
| 93 |
+
}
|
| 94 |
+
```
|
custom_nodes/srgb_noise/repo/data/__pycache__/dataset_base.cpython-39.pyc
ADDED
|
Binary file (2.3 kB). View file
|
|
|
custom_nodes/srgb_noise/repo/data/__pycache__/dataset_noise_synthesis.cpython-39.pyc
ADDED
|
Binary file (1.78 kB). View file
|
|
|
custom_nodes/srgb_noise/repo/data/dataset_base.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch.utils.data import Dataset
|
| 2 |
+
import torch
|
| 3 |
+
import os
|
| 4 |
+
import numpy as np
|
| 5 |
+
from abc import ABC, abstractmethod
|
| 6 |
+
import cv2
|
| 7 |
+
import glob
|
| 8 |
+
|
| 9 |
+
"""
|
| 10 |
+
# --------------------------------------------
|
| 11 |
+
# define a base dataset
|
| 12 |
+
# --------------------------------------------
|
| 13 |
+
"""
|
| 14 |
+
class dataset_base(Dataset, ABC):
|
| 15 |
+
"""This is an abstract class for datesets"""
|
| 16 |
+
|
| 17 |
+
@abstractmethod
|
| 18 |
+
def __init__(self):
|
| 19 |
+
"""Initialize the dataset, this should be overriden by the subclass"""
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
@abstractmethod
|
| 23 |
+
def __len__(self):
|
| 24 |
+
"""Return the number of images in the dataset, this should be overriden by the subclass"""
|
| 25 |
+
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
@abstractmethod
|
| 29 |
+
def __getitem__(self, index):
|
| 30 |
+
"""Return the image at index, this should be overriden by the subclass"""
|
| 31 |
+
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
def _get_img_paths(self, root_dir, cam_name, flag='noisy'):
|
| 35 |
+
sub_dirs = os.listdir(root_dir)
|
| 36 |
+
img_lists = []
|
| 37 |
+
|
| 38 |
+
for sub_dir in sub_dirs:
|
| 39 |
+
if cam_name in sub_dir:
|
| 40 |
+
img_dir = os.path.join(root_dir, sub_dir, flag, '*.png')
|
| 41 |
+
img_lists.extend(glob.glob(img_dir))
|
| 42 |
+
|
| 43 |
+
return img_lists
|
| 44 |
+
|
| 45 |
+
def _padding(self, img, win_size=10, factor=8):
|
| 46 |
+
# pad the image if not_multiple of win_size * 8, win_size is the precision of padding stride
|
| 47 |
+
multiple_factor = win_size*factor
|
| 48 |
+
h, w, _ = img.shape
|
| 49 |
+
H, W = (h//multiple_factor+1)*multiple_factor, (w//multiple_factor+1)*multiple_factor
|
| 50 |
+
padh = H - h if h%multiple_factor != 0 else 0
|
| 51 |
+
padw = W - w if w%multiple_factor != 0 else 0
|
| 52 |
+
patch = cv2.copyMakeBorder(img, top=0, bottom=padh, left=0, right=padw, borderType=cv2.BORDER_REFLECT)
|
| 53 |
+
return patch, h, w
|
| 54 |
+
|
| 55 |
+
def _PD_scheme(self, img, stride=2):
|
| 56 |
+
"""input: img (tensor), output: img_mosaic (tensor)"""
|
| 57 |
+
num = stride*stride
|
| 58 |
+
idx = np.random.randint(0, num)
|
| 59 |
+
h_start = idx // stride
|
| 60 |
+
w_start = idx % stride
|
| 61 |
+
img_mosaic = img[:, h_start::stride, w_start::stride]
|
| 62 |
+
return img_mosaic
|
custom_nodes/srgb_noise/repo/data/dataset_noise_synthesis.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
import utils.utils_image as util
|
| 4 |
+
from data.dataset_base import dataset_base
|
| 5 |
+
|
| 6 |
+
class dataset_noise_synthesis(dataset_base):
|
| 7 |
+
def __init__(self,
|
| 8 |
+
img_lists,
|
| 9 |
+
img_channels,
|
| 10 |
+
not_train=False,
|
| 11 |
+
not_aug=False,
|
| 12 |
+
patch_size=96):
|
| 13 |
+
|
| 14 |
+
self.noisy_img_lists = img_lists
|
| 15 |
+
self.img_channels = img_channels
|
| 16 |
+
self.not_train = not_train
|
| 17 |
+
self.not_aug = not_aug
|
| 18 |
+
self.patch_size = patch_size
|
| 19 |
+
|
| 20 |
+
def __getitem__(self, index):
|
| 21 |
+
noisy_img_path = self.noisy_img_lists[index]
|
| 22 |
+
noisy_img_dir, noisy_img_name = os.path.split(noisy_img_path)
|
| 23 |
+
clean_img_dir = noisy_img_dir.replace('noisy', 'clean')
|
| 24 |
+
clean_img_name = noisy_img_name.replace('NOISY', 'GT')
|
| 25 |
+
clean_img_path = os.path.join(clean_img_dir, clean_img_name)
|
| 26 |
+
clean_img = util.imread_uint(clean_img_path, self.img_channels)
|
| 27 |
+
noisy_img = util.imread_uint(noisy_img_path, self.img_channels)
|
| 28 |
+
|
| 29 |
+
if not self.not_train:
|
| 30 |
+
h, w, _ = clean_img.shape
|
| 31 |
+
rnd_h = random.randint(0, max(0, h - self.patch_size))
|
| 32 |
+
rnd_w = random.randint(0, max(0, w - self.patch_size))
|
| 33 |
+
clean_patch = clean_img[rnd_h:rnd_h + self.patch_size, rnd_w:rnd_w + self.patch_size, :]
|
| 34 |
+
noisy_patch = noisy_img[rnd_h:rnd_h + self.patch_size, rnd_w:rnd_w + self.patch_size, :]
|
| 35 |
+
|
| 36 |
+
if not self.not_aug:
|
| 37 |
+
mode = random.randint(0, 7)
|
| 38 |
+
clean_patch = util.augment_img(clean_patch, mode)
|
| 39 |
+
noisy_patch = util.augment_img(noisy_patch, mode)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
clean_patch = util.uint2tensor3(clean_patch)
|
| 43 |
+
noisy_patch = util.uint2tensor3(noisy_patch)
|
| 44 |
+
|
| 45 |
+
else:
|
| 46 |
+
clean_patch = util.uint2tensor3(clean_img)
|
| 47 |
+
noisy_patch = util.uint2tensor3(noisy_img)
|
| 48 |
+
|
| 49 |
+
noise_map = noisy_patch - clean_patch
|
| 50 |
+
return {'clean': clean_patch, 'noisy': noisy_patch, 'noise': noise_map}
|
| 51 |
+
|
| 52 |
+
def __len__(self):
|
| 53 |
+
return len(self.noisy_img_lists)
|
| 54 |
+
|
custom_nodes/srgb_noise/repo/data_preparation/G4_test.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0122_006_G4_00100_00050_3200_N
|
| 2 |
+
0077_004_G4_00100_00025_3200_N
|
| 3 |
+
0059_003_G4_00800_01000_5500_L
|
| 4 |
+
0173_008_G4_00400_00400_4400_N
|
custom_nodes/srgb_noise/repo/data_preparation/G4_train.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0172_008_G4_00100_00100_4400_N
|
| 2 |
+
0147_007_G4_00100_00100_4400_L
|
| 3 |
+
0045_002_G4_00100_00060_3200_L
|
| 4 |
+
0025_001_G4_00100_00060_5500_L
|
| 5 |
+
0098_005_G4_00100_00050_3200_N
|
| 6 |
+
0057_003_G4_00100_00125_5500_L
|
| 7 |
+
0149_007_G4_00800_00800_4400_L
|
| 8 |
+
0027_001_G4_00800_00350_5500_L
|
| 9 |
+
0047_002_G4_00800_00640_3200_L
|
| 10 |
+
0078_004_G4_00200_00050_3200_N
|
| 11 |
+
0123_006_G4_00400_00160_3200_N
|
| 12 |
+
0099_005_G4_00400_00200_3200_N
|
custom_nodes/srgb_noise/repo/data_preparation/GP_test.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0086_004_GP_00100_00100_5500_L
|
| 2 |
+
0034_002_GP_00100_00160_3200_N
|
| 3 |
+
0199_010_GP_00800_01600_5500_N
|
| 4 |
+
0156_007_GP_00800_00800_5500_N
|
| 5 |
+
0107_005_GP_01600_01600_4400_N
|
custom_nodes/srgb_noise/repo/data_preparation/GP_train.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0132_006_GP_00100_00200_5500_L
|
| 2 |
+
0063_003_GP_00100_00100_4400_N
|
| 3 |
+
0066_003_GP_00100_00200_3200_L
|
| 4 |
+
0180_008_GP_00100_00100_5500_N
|
| 5 |
+
0129_006_GP_00100_00100_4400_N
|
| 6 |
+
0017_001_GP_00100_00060_5500_N
|
| 7 |
+
0105_005_GP_00100_00100_4400_N
|
| 8 |
+
0155_007_GP_00100_00100_5500_N
|
| 9 |
+
0198_010_GP_00100_00200_5500_N
|
| 10 |
+
0018_001_GP_00100_00160_5500_L
|
| 11 |
+
0181_008_GP_00800_00800_5500_N
|
| 12 |
+
0035_002_GP_00800_00350_3200_N
|
| 13 |
+
0020_001_GP_00800_00350_5500_N
|
| 14 |
+
0133_006_GP_00800_01600_5500_L
|
| 15 |
+
0019_001_GP_00800_00640_5500_L
|
| 16 |
+
0038_002_GP_00800_00640_3200_L
|
| 17 |
+
0087_004_GP_00800_00640_5500_L
|
| 18 |
+
0108_005_GP_06400_06400_4400_N
|
| 19 |
+
0036_002_GP_06400_03200_3200_N
|
| 20 |
+
0064_003_GP_01600_01600_4400_N
|
| 21 |
+
0157_007_GP_01600_01600_5500_N
|
| 22 |
+
0200_010_GP_01600_03200_5500_N
|
| 23 |
+
0065_003_GP_10000_08460_4400_N
|
| 24 |
+
0083_004_GP_00050_00020_4400_N
|
| 25 |
+
0084_004_GP_00200_00100_4400_N
|
| 26 |
+
0130_006_GP_00400_00400_4400_N
|
| 27 |
+
0106_005_GP_00400_00400_4400_N
|
| 28 |
+
0182_008_GP_03200_03200_5500_N
|
custom_nodes/srgb_noise/repo/data_preparation/IP_test.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0110_005_IP_00100_00100_5500_L
|
| 2 |
+
0114_005_IP_00100_00200_5500_N
|
| 3 |
+
0134_006_IP_00100_00100_5500_N
|
| 4 |
+
0193_009_IP_00800_02000_3200_N
|
| 5 |
+
0032_001_IP_00800_01000_3200_N
|
| 6 |
+
0165_007_IP_00800_00800_3200_N
|
| 7 |
+
0191_008_IP_01600_01600_3200_N
|
| 8 |
+
0090_004_IP_01600_00750_5500_N
|
| 9 |
+
0139_006_IP_00200_00200_3200_L
|
| 10 |
+
0115_005_IP_00400_00750_5500_N
|
custom_nodes/srgb_noise/repo/data_preparation/IP_train.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0163_007_IP_00100_00100_3200_N
|
| 2 |
+
0197_009_IP_00100_00200_5500_L
|
| 3 |
+
0192_009_IP_00100_00200_3200_N
|
| 4 |
+
0044_002_IP_00100_00180_5500_N
|
| 5 |
+
0138_006_IP_00100_00100_3200_L
|
| 6 |
+
0028_001_IP_00100_00160_5500_N
|
| 7 |
+
0159_007_IP_00100_00100_3200_L
|
| 8 |
+
0184_008_IP_00100_00100_3200_L
|
| 9 |
+
0033_001_IP_00100_00160_3200_N
|
| 10 |
+
0188_008_IP_00100_00100_3200_N
|
| 11 |
+
0039_002_IP_00100_00180_5500_L
|
| 12 |
+
0088_004_IP_00100_00050_5500_N
|
| 13 |
+
0029_001_IP_00800_01000_5500_N
|
| 14 |
+
0043_002_IP_00800_01520_5500_N
|
| 15 |
+
0040_002_IP_00800_02000_5500_L
|
| 16 |
+
0136_006_IP_00800_00800_5500_N
|
| 17 |
+
0116_005_IP_00800_01520_5500_N
|
| 18 |
+
0140_006_IP_00800_00800_3200_L
|
| 19 |
+
0196_009_IP_00800_02000_5500_L
|
| 20 |
+
0186_008_IP_00800_00800_3200_L
|
| 21 |
+
0161_007_IP_00800_00800_3200_L
|
| 22 |
+
0190_008_IP_00800_00800_3200_N
|
| 23 |
+
0137_006_IP_01600_01600_5500_N
|
| 24 |
+
0042_002_IP_01600_03100_5500_N
|
| 25 |
+
0194_009_IP_01600_04000_3200_N
|
| 26 |
+
0195_009_IP_01600_04000_5500_L
|
| 27 |
+
0030_001_IP_01600_02000_5500_N
|
| 28 |
+
0113_005_IP_01600_01520_5500_L
|
| 29 |
+
0166_007_IP_01600_01600_3200_N
|
| 30 |
+
0117_005_IP_01600_04160_5500_N
|
| 31 |
+
0068_003_IP_00200_00400_3200_N
|
| 32 |
+
0073_003_IP_00200_01000_5500_L
|
| 33 |
+
0069_003_IP_01000_02000_3200_N
|
| 34 |
+
0072_003_IP_01000_02000_5500_L
|
| 35 |
+
0070_003_IP_02000_04000_3200_N
|
| 36 |
+
0089_004_IP_00500_00250_5500_N
|
| 37 |
+
0091_004_IP_00320_00080_3200_L
|
| 38 |
+
0092_004_IP_00640_00125_3200_L
|
| 39 |
+
0160_007_IP_00400_00400_3200_L
|
| 40 |
+
0111_005_IP_00400_00400_5500_L
|
| 41 |
+
0135_006_IP_00400_00400_5500_N
|
| 42 |
+
0164_007_IP_00400_00400_3200_N
|
| 43 |
+
0189_008_IP_00400_00400_3200_N
|
| 44 |
+
0185_008_IP_00400_00400_3200_L
|
custom_nodes/srgb_noise/repo/data_preparation/N6_test.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0054_003_N6_00100_00160_5500_N
|
| 2 |
+
0075_004_N6_00800_00080_3200_L
|
| 3 |
+
0050_002_N6_03200_03200_5500_L
|
| 4 |
+
0096_005_N6_01600_01000_3200_L
|
custom_nodes/srgb_noise/repo/data_preparation/N6_train.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0022_001_N6_00100_00060_5500_N
|
| 2 |
+
0142_007_N6_00100_00100_4400_N
|
| 3 |
+
0048_002_N6_00100_00100_5500_L
|
| 4 |
+
0167_008_N6_00100_00050_4400_L
|
| 5 |
+
0094_005_N6_00100_00050_3200_L
|
| 6 |
+
0118_006_N6_00100_00025_3200_L
|
| 7 |
+
0055_003_N6_00800_01000_5500_N
|
| 8 |
+
0169_008_N6_00800_00400_4400_L
|
| 9 |
+
0023_001_N6_00800_00350_5500_N
|
| 10 |
+
0097_005_N6_03200_02000_3200_L
|
| 11 |
+
0145_007_N6_03200_03200_4400_N
|
| 12 |
+
0076_004_N6_03200_00320_3200_L
|
| 13 |
+
0121_006_N6_03200_01000_3200_L
|
| 14 |
+
0120_006_N6_01600_00400_3200_L
|
| 15 |
+
0144_007_N6_01600_01600_4400_N
|
| 16 |
+
0170_008_N6_01600_00800_4400_L
|
| 17 |
+
0146_007_N6_00400_00400_4400_N
|
| 18 |
+
0168_008_N6_00400_00200_4400_L
|
custom_nodes/srgb_noise/repo/data_preparation/S6_test.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0007_001_S6_00100_00100_5500_L
|
| 2 |
+
0008_001_S6_00100_00100_5500_N
|
| 3 |
+
0002_001_S6_00100_00020_3200_N
|
| 4 |
+
0081_004_S6_00800_00160_4400_L
|
| 5 |
+
0014_001_S6_03200_01250_3200_N
|
| 6 |
+
0152_007_S6_01600_01600_5500_L
|
| 7 |
+
0126_006_S6_00400_00200_4400_L
|
custom_nodes/srgb_noise/repo/data_preparation/S6_train.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0051_002_S6_00100_00060_5500_N
|
| 2 |
+
0125_006_S6_00100_00050_4400_L
|
| 3 |
+
0101_005_S6_00100_00050_4400_L
|
| 4 |
+
0001_001_S6_00100_00060_3200_L
|
| 5 |
+
0006_001_S6_00100_00060_4400_H
|
| 6 |
+
0175_008_S6_00100_00025_5500_L
|
| 7 |
+
0150_007_S6_00100_00100_5500_L
|
| 8 |
+
0060_003_S6_00100_00100_4400_L
|
| 9 |
+
0003_001_S6_00100_00060_3200_H
|
| 10 |
+
0004_001_S6_00100_00060_4400_L
|
| 11 |
+
0005_001_S6_00100_00060_4400_N
|
| 12 |
+
0177_008_S6_00800_00200_5500_L
|
| 13 |
+
0010_001_S6_00800_00350_3200_N
|
| 14 |
+
0012_001_S6_00800_00500_5500_N
|
| 15 |
+
0011_001_S6_00800_00500_5500_L
|
| 16 |
+
0151_007_S6_00800_00800_5500_L
|
| 17 |
+
0179_008_S6_03200_00800_5500_L
|
| 18 |
+
0013_001_S6_03200_01250_3200_L
|
| 19 |
+
0016_001_S6_03200_01600_5500_N
|
| 20 |
+
0062_003_S6_03200_02500_4400_L
|
| 21 |
+
0015_001_S6_03200_01600_5500_L
|
| 22 |
+
0104_005_S6_03200_01600_4400_L
|
| 23 |
+
0127_006_S6_01600_00800_4400_L
|
| 24 |
+
0178_008_S6_01600_00400_5500_L
|
| 25 |
+
0052_002_S6_01600_01000_5500_N
|
| 26 |
+
0080_004_S6_00200_00050_3200_N
|
| 27 |
+
0102_005_S6_00400_00200_4400_L
|
| 28 |
+
0154_007_S6_00400_00400_5500_L
|
custom_nodes/srgb_noise/repo/doc/baseline.png
ADDED
|
Git LFS Details
|
custom_nodes/srgb_noise/repo/doc/quantitative_results.png
ADDED
|
custom_nodes/srgb_noise/repo/doc/visual_results.png
ADDED
|
Git LFS Details
|
custom_nodes/srgb_noise/repo/main_test.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from models.network_unet import UNet
|
| 8 |
+
from models.network_plain import Noise_estimator, Noise_level_predictor, DnCNN
|
| 9 |
+
import utils.utils_image as util
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
|
| 13 |
+
opt = argparse.ArgumentParser()
|
| 14 |
+
opt.add_argument('--cam_name', type=str, default='IP', help='camera name for training data, the dataset will load the images from the specified camera')
|
| 15 |
+
opt.add_argument('--dir_test_save', type=str, default='./test/saves/', help='save directory for test')
|
| 16 |
+
opt.add_argument('--dir_test_img', type=str, default='./test/imgs/', help='image directory for test data')
|
| 17 |
+
opt.add_argument('--NeCA_type', type=str, default='W', help='set mode of NeCA')
|
| 18 |
+
opt.add_argument('--noise_level', type=int, default=25, help='set Gaussian noise level for NeCA_S')
|
| 19 |
+
opt = opt.parse_args()
|
| 20 |
+
|
| 21 |
+
net_E = Noise_estimator(in_nc=3, out_nc=1, nc=96, nb=5, act_mode='R')
|
| 22 |
+
net_G1 = Noise_level_predictor(in_nc=3, out_nc=3, nc=96)
|
| 23 |
+
net_G2 = UNet(in_nc=3, out_nc=3, nc=64, act_mode='R', num_stages=4, downsample_mode='strideconv', upsample_mode='upsampling', bias=True, padding_mode='zeros', final_act='Tanh')
|
| 24 |
+
|
| 25 |
+
checkpoint_E = torch.load(os.path.join('./pretrain/', opt.cam_name, 'checkpoint_E.pth'), map_location='cpu')
|
| 26 |
+
checkpoint_G1 = torch.load(os.path.join('./pretrain/', opt.cam_name, 'checkpoint_G1.pth'), map_location='cpu')
|
| 27 |
+
checkpoint_G2 = torch.load(os.path.join('./pretrain/', opt.cam_name, 'checkpoint_G2.pth'), map_location='cpu')
|
| 28 |
+
|
| 29 |
+
net_E.load_state_dict(checkpoint_E['model_state_dict'])
|
| 30 |
+
net_G1.load_state_dict(checkpoint_G1['model_state_dict'])
|
| 31 |
+
net_G2.load_state_dict(checkpoint_G2['model_state_dict'])
|
| 32 |
+
|
| 33 |
+
net_E.cuda()
|
| 34 |
+
net_G1.cuda()
|
| 35 |
+
net_G2.cuda()
|
| 36 |
+
|
| 37 |
+
net_E.eval()
|
| 38 |
+
net_G1.eval()
|
| 39 |
+
net_G2.eval()
|
| 40 |
+
|
| 41 |
+
clean = util.imread_uint(os.path.join(opt.dir_test_img, 'clean.png'), 3)
|
| 42 |
+
noisy = util.imread_uint(os.path.join(opt.dir_test_img, 'noisy.png'), 3)
|
| 43 |
+
h, w, _ = clean.shape
|
| 44 |
+
if h%8 != 0 or w%8 != 0:
|
| 45 |
+
clean = clean[:h//8*8, :w//8*8, :]
|
| 46 |
+
noisy = noisy[:h//8*8, :w//8*8, :]
|
| 47 |
+
|
| 48 |
+
clean = util.uint2tensor3(clean).unsqueeze(0).cuda()
|
| 49 |
+
noisy = util.uint2tensor3(noisy).unsqueeze(0).cuda()
|
| 50 |
+
|
| 51 |
+
with torch.no_grad():
|
| 52 |
+
if opt.NeCA_type == 'W':
|
| 53 |
+
z = torch.randn_like(clean).cuda()
|
| 54 |
+
gain_factor = net_E(noisy)
|
| 55 |
+
pred_noise_level_map = net_G1(clean, gain_factor)
|
| 56 |
+
sdnu_noise = pred_noise_level_map.mul(z)
|
| 57 |
+
sdnc_noise = net_G2(sdnu_noise) + sdnu_noise
|
| 58 |
+
fake_noisy = clean + sdnc_noise
|
| 59 |
+
|
| 60 |
+
util.imsave(util.tensor2uint(fake_noisy), os.path.join(opt.dir_test_save, 'fake_noisy.png'))
|
| 61 |
+
util.imsave(util.tensor2uint(sdnc_noise+0.5), os.path.join(opt.dir_test_save, 'sdnc_noise.png'))
|
| 62 |
+
util.imsave(util.tensor2uint(sdnu_noise+0.5), os.path.join(opt.dir_test_save, 'sdnu_noise.png'))
|
| 63 |
+
|
| 64 |
+
elif opt.NeCA_type == 'S':
|
| 65 |
+
z = torch.randn_like(clean).cuda()
|
| 66 |
+
z.mul_(opt.noise_level/255.0)
|
| 67 |
+
sinc_noise = net_G2(z) + z
|
| 68 |
+
fake_noisy = clean + sinc_noise
|
| 69 |
+
|
| 70 |
+
util.imsave(util.tensor2uint(fake_noisy), os.path.join(opt.dir_test_save, 'fake_noisy.png'))
|
| 71 |
+
util.imsave(util.tensor2uint(sinc_noise+0.5), os.path.join(opt.dir_test_save, 'sinc_noise.png'))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
else:
|
| 75 |
+
raise NotImplementedError('NeCA_type [%s] is not found' % opt.NeCA_type)
|
| 76 |
+
|
| 77 |
+
if __name__ == '__main__':
|
| 78 |
+
main()
|
custom_nodes/srgb_noise/repo/main_train_jointly.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from torch.utils.data import DataLoader
|
| 6 |
+
import torch.optim as optim
|
| 7 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 8 |
+
from collections import OrderedDict
|
| 9 |
+
|
| 10 |
+
from options.option_noise_synthesis import BaseOptions
|
| 11 |
+
from utils.utils_logger import logger_info
|
| 12 |
+
from data.dataset_noise_synthesis import dataset_noise_synthesis
|
| 13 |
+
from models.network_unet import UNet
|
| 14 |
+
from models.network_plain import Noise_estimator, Noise_level_predictor, DnCNN
|
| 15 |
+
from models.network_discriminator import Discriminator_96, Discriminator_32
|
| 16 |
+
import utils.utils_network as net_util
|
| 17 |
+
import utils.utils_image as util
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if __name__ == '__main__':
|
| 21 |
+
"""
|
| 22 |
+
# -----------------------------------------------
|
| 23 |
+
# step 1 -- initiate logs and args
|
| 24 |
+
# -----------------------------------------------
|
| 25 |
+
"""
|
| 26 |
+
# get training options
|
| 27 |
+
opt, opt_message = BaseOptions().parse()
|
| 28 |
+
|
| 29 |
+
# initiate data folder
|
| 30 |
+
cam_name = opt.cam_name
|
| 31 |
+
dir_save_logs = os.path.join(opt.dir_save, cam_name, 'logs/')
|
| 32 |
+
dir_save_imgs = os.path.join(opt.dir_save, cam_name, 'imgs/')
|
| 33 |
+
dir_save_models = os.path.join(opt.dir_save, cam_name, 'models/')
|
| 34 |
+
dir_save_tb = os.path.join(opt.dir_save, cam_name, 'tb/')
|
| 35 |
+
|
| 36 |
+
if not os.path.exists(dir_save_logs):
|
| 37 |
+
os.makedirs(dir_save_logs)
|
| 38 |
+
if not os.path.exists(dir_save_imgs):
|
| 39 |
+
os.makedirs(dir_save_imgs)
|
| 40 |
+
if not os.path.exists(dir_save_models):
|
| 41 |
+
os.makedirs(dir_save_models)
|
| 42 |
+
if not os.path.exists(dir_save_tb):
|
| 43 |
+
os.makedirs(dir_save_tb)
|
| 44 |
+
|
| 45 |
+
# initiate logs
|
| 46 |
+
logger_name = 'train'
|
| 47 |
+
logger_info(logger_name, log_path=dir_save_logs+logger_name+'.log')
|
| 48 |
+
logger = logging.getLogger(logger_name)
|
| 49 |
+
logger.info(opt_message)
|
| 50 |
+
|
| 51 |
+
"""
|
| 52 |
+
# -----------------------------------------------
|
| 53 |
+
# step 2 -- prepare dataloader
|
| 54 |
+
# -----------------------------------------------
|
| 55 |
+
"""
|
| 56 |
+
if opt.generate_data_lists:
|
| 57 |
+
util.train_test_split(opt.train_img_dir, opt.data_prep_dir, cam_name=cam_name, ratio=0.8)
|
| 58 |
+
|
| 59 |
+
train_img_lists, val_img_lists = util.get_img_list(opt.train_img_dir, cam_name=cam_name, mode='train', ratio=1)
|
| 60 |
+
test_img_lists = util.get_img_list(opt.train_img_dir, cam_name=cam_name, mode='test')
|
| 61 |
+
data_message = 'train: {:d}, val: {:d}, test: {:d}'.format(len(train_img_lists), len(val_img_lists), len(test_img_lists))
|
| 62 |
+
print(data_message)
|
| 63 |
+
logger.info(data_message)
|
| 64 |
+
|
| 65 |
+
train_data = dataset_noise_synthesis(img_lists=train_img_lists,
|
| 66 |
+
img_channels=opt.img_channels,
|
| 67 |
+
not_train=opt.not_train,
|
| 68 |
+
not_aug=opt.not_aug,
|
| 69 |
+
patch_size=opt.patch_size)
|
| 70 |
+
val_data = dataset_noise_synthesis(img_lists=val_img_lists,
|
| 71 |
+
img_channels=opt.img_channels,
|
| 72 |
+
not_train=True)
|
| 73 |
+
test_data = dataset_noise_synthesis(img_lists=test_img_lists,
|
| 74 |
+
img_channels=opt.img_channels,
|
| 75 |
+
not_train=True)
|
| 76 |
+
|
| 77 |
+
train_loader = DataLoader(dataset=train_data,
|
| 78 |
+
batch_size=opt.batch_size,
|
| 79 |
+
shuffle=not opt.not_train_shuffle,
|
| 80 |
+
num_workers=opt.num_workers,
|
| 81 |
+
pin_memory=True,
|
| 82 |
+
drop_last=True)
|
| 83 |
+
val_loader = DataLoader(dataset=val_data,
|
| 84 |
+
batch_size=1,
|
| 85 |
+
shuffle=False,
|
| 86 |
+
num_workers=opt.num_workers,
|
| 87 |
+
pin_memory=True,
|
| 88 |
+
drop_last=False)
|
| 89 |
+
test_loader = DataLoader(dataset=test_data,
|
| 90 |
+
batch_size=1,
|
| 91 |
+
shuffle=False,
|
| 92 |
+
num_workers=opt.num_workers,
|
| 93 |
+
pin_memory=True,
|
| 94 |
+
drop_last=False)
|
| 95 |
+
|
| 96 |
+
"""
|
| 97 |
+
# ------------------------------------------------
|
| 98 |
+
# step 3 -- prepare networks, optims and loss
|
| 99 |
+
# ------------------------------------------------
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
"""prepare networks
|
| 103 |
+
net_E -- Gain estimation network (GENet)
|
| 104 |
+
net_G1 -- Noise-level prediction network (NPNet)
|
| 105 |
+
net_G2 -- Neighboring correlation network (NCNet)
|
| 106 |
+
net_D1 -- Discriminator for neighboring uncorrelated noise
|
| 107 |
+
net_D2 -- Discriminator for neighboring correlated noise
|
| 108 |
+
net_G3 -- Denoiser for real noise, used for joint training
|
| 109 |
+
"""
|
| 110 |
+
net_E = Noise_estimator(in_nc=opt.E_in_nc,
|
| 111 |
+
out_nc=opt.E_out_nc,
|
| 112 |
+
nc=opt.E_nc,
|
| 113 |
+
nb=opt.E_nb,
|
| 114 |
+
act_mode=opt.E_act_mode)
|
| 115 |
+
|
| 116 |
+
net_G1 = Noise_level_predictor(in_nc=opt.G1_in_nc, out_nc=opt.G1_out_nc, nc=opt.G1_nc)
|
| 117 |
+
|
| 118 |
+
net_G2 = UNet(in_nc=opt.G2_in_nc,
|
| 119 |
+
out_nc=opt.G2_out_nc,
|
| 120 |
+
nc=opt.G2_nc,
|
| 121 |
+
act_mode=opt.G2_act_mode,
|
| 122 |
+
downsample_mode=opt.G2_downsample_mode,
|
| 123 |
+
upsample_mode=opt.G2_upsample_mode,
|
| 124 |
+
padding_mode=opt.G2_padding_mode,
|
| 125 |
+
final_act=opt.G2_final_act)
|
| 126 |
+
|
| 127 |
+
net_D1 = Discriminator_96(in_nc=opt.D1_in_nc, nc=opt.D1_nc, act_mode=opt.D1_act_mode)
|
| 128 |
+
|
| 129 |
+
net_D2 = Discriminator_32(in_nc=opt.D2_in_nc, nc=opt.D2_nc, act_mode=opt.D2_act_mode)
|
| 130 |
+
|
| 131 |
+
net_G3 = DnCNN(in_nc=opt.G3_in_nc,
|
| 132 |
+
out_nc=opt.G3_out_nc,
|
| 133 |
+
nc=opt.G3_nc,
|
| 134 |
+
act_mode=opt.G3_act_mode)
|
| 135 |
+
|
| 136 |
+
# print networks
|
| 137 |
+
net_message = '------------ Network params -----------\n'
|
| 138 |
+
net_message += net_util.print_networks(network=net_E, network_name='GENet', verbose=opt.verbose)
|
| 139 |
+
net_message += net_util.print_networks(network=net_G1, network_name='NPNet', verbose=opt.verbose)
|
| 140 |
+
net_message += net_util.print_networks(network=net_G2, network_name='NCNet', verbose=opt.verbose)
|
| 141 |
+
net_message += net_util.print_networks(network=net_D1, network_name='D1', verbose=opt.verbose)
|
| 142 |
+
net_message += net_util.print_networks(network=net_D2, network_name='D2', verbose=opt.verbose)
|
| 143 |
+
net_message += net_util.print_networks(network=net_G3, network_name='Denoiser', verbose=opt.verbose)
|
| 144 |
+
net_message += '-----------------------------------------------\n'
|
| 145 |
+
logger.info(net_message)
|
| 146 |
+
|
| 147 |
+
# send networks to GPU
|
| 148 |
+
net_E, gpu_message, device = net_util.model_to_device(net_E, gpu_ids=opt.gpu_ids)
|
| 149 |
+
net_G1, _, _ = net_util.model_to_device(net_G1, gpu_ids=opt.gpu_ids)
|
| 150 |
+
net_G2, _, _ = net_util.model_to_device(net_G2, gpu_ids=opt.gpu_ids)
|
| 151 |
+
net_D1, _, _ = net_util.model_to_device(net_D1, gpu_ids=opt.gpu_ids)
|
| 152 |
+
net_D2, _, _ = net_util.model_to_device(net_D2, gpu_ids=opt.gpu_ids)
|
| 153 |
+
net_G3, _, _ = net_util.model_to_device(net_G3, gpu_ids=opt.gpu_ids)
|
| 154 |
+
logger.info(gpu_message)
|
| 155 |
+
|
| 156 |
+
# load pretrained networks
|
| 157 |
+
if opt.use_pretrained:
|
| 158 |
+
checkpoint_E = torch.load(os.path.join(dir_save_models, opt.checkpoint_E_name), map_location=device)
|
| 159 |
+
checkpoint_G1 = torch.load(os.path.join(dir_save_models, opt.checkpoint_G1_name), map_location=device)
|
| 160 |
+
checkpoint_G2 = torch.load(os.path.join(dir_save_models, opt.checkpoint_G2_name), map_location=device)
|
| 161 |
+
checkpoint_D1 = torch.load(os.path.join(dir_save_models, opt.checkpoint_D1_name), map_location=device)
|
| 162 |
+
checkpoint_D2 = torch.load(os.path.join(dir_save_models, opt.checkpoint_D2_name), map_location=device)
|
| 163 |
+
# checkpoint_G3 = torch.load(os.path.join(dir_save_models, opt.checkpoint_G3_name), map_location=device)
|
| 164 |
+
net_E.load_state_dict(checkpoint_E['model_state_dict'])
|
| 165 |
+
net_G1.load_state_dict(checkpoint_G1['model_state_dict'])
|
| 166 |
+
net_G2.load_state_dict(checkpoint_G2['model_state_dict'])
|
| 167 |
+
net_D1.load_state_dict(checkpoint_D1['model_state_dict'])
|
| 168 |
+
net_D2.load_state_dict(checkpoint_D2['model_state_dict'])
|
| 169 |
+
# net_G3.load_state_dict(checkpoint_G3['model_state_dict'])
|
| 170 |
+
|
| 171 |
+
else:
|
| 172 |
+
net_util.init_weights(net_E, init_type=opt.init_type)
|
| 173 |
+
net_util.init_weights(net_G1, init_type=opt.init_type)
|
| 174 |
+
net_util.init_weights(net_G2, init_type=opt.init_type)
|
| 175 |
+
net_util.init_weights(net_D1, init_type=opt.init_type)
|
| 176 |
+
net_util.init_weights(net_D2, init_type=opt.init_type)
|
| 177 |
+
net_util.init_weights(net_G3, init_type=opt.init_type)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# set optimizers
|
| 181 |
+
optim_E = optim.Adam(net_E.parameters(), lr=opt.lr_E, betas=(opt.beta1, opt.beta2))
|
| 182 |
+
optim_G1 = optim.Adam(net_G1.parameters(), lr=opt.lr_G1, betas=(opt.beta1, opt.beta2))
|
| 183 |
+
optim_G2 = optim.Adam(net_G2.parameters(), lr=opt.lr_G2, betas=(opt.beta1, opt.beta2))
|
| 184 |
+
optim_D1 = optim.Adam(net_D1.parameters(), lr=opt.lr_D1, betas=(opt.beta1, opt.beta2))
|
| 185 |
+
optim_D2 = optim.Adam(net_D2.parameters(), lr=opt.lr_D2, betas=(opt.beta1, opt.beta2))
|
| 186 |
+
optim_G3 = optim.Adam(net_G3.parameters(), lr=opt.lr_G3, betas=(opt.beta1, opt.beta2))
|
| 187 |
+
|
| 188 |
+
# set learning rate schedulers
|
| 189 |
+
lr_scheduler_E = optim.lr_scheduler.MultiStepLR(optimizer=optim_E, milestones=opt.lr_decay_1, gamma=0.1)
|
| 190 |
+
lr_scheduler_G1 = optim.lr_scheduler.MultiStepLR(optimizer=optim_G1, milestones=opt.lr_decay_1, gamma=0.1)
|
| 191 |
+
lr_scheduler_G2 = optim.lr_scheduler.MultiStepLR(optimizer=optim_G2, milestones=opt.lr_decay_1, gamma=0.1)
|
| 192 |
+
lr_scheduler_D1 = optim.lr_scheduler.MultiStepLR(optimizer=optim_D1, milestones=opt.lr_decay_1, gamma=0.1)
|
| 193 |
+
lr_scheduler_D2 = optim.lr_scheduler.MultiStepLR(optimizer=optim_D2, milestones=opt.lr_decay_1, gamma=0.1)
|
| 194 |
+
lr_scheduler_G3 = optim.lr_scheduler.MultiStepLR(optimizer=optim_G3, milestones=opt.lr_decay_2, gamma=0.1)
|
| 195 |
+
|
| 196 |
+
# set loss functions
|
| 197 |
+
Cri_rec = nn.MSELoss()
|
| 198 |
+
Cri_gan = net_util.GANLoss(gan_mode=opt.gan_mode).to(device)
|
| 199 |
+
|
| 200 |
+
# set tensorboard writer
|
| 201 |
+
if opt.use_tb:
|
| 202 |
+
tb = SummaryWriter(log_dir=dir_save_tb)
|
| 203 |
+
|
| 204 |
+
"""
|
| 205 |
+
# ------------------------------------------------
|
| 206 |
+
# step 4 -- training
|
| 207 |
+
# ------------------------------------------------
|
| 208 |
+
"""
|
| 209 |
+
current_step = 0
|
| 210 |
+
start_epoch = 1 if not opt.use_pretrained else checkpoint_E['epoch']
|
| 211 |
+
iter_step = 5 if opt.gan_mode == 'wgangp' else 1
|
| 212 |
+
log_step = iter_step
|
| 213 |
+
for epoch in range(start_epoch, opt.epochs+1):
|
| 214 |
+
for i, data in enumerate(train_loader):
|
| 215 |
+
|
| 216 |
+
current_step += 1
|
| 217 |
+
clean = data['clean'].to(device)
|
| 218 |
+
noisy = data['noisy'].to(device)
|
| 219 |
+
noise = data['noise'].to(device)
|
| 220 |
+
training_message = OrderedDict()
|
| 221 |
+
|
| 222 |
+
# 1) feed data
|
| 223 |
+
z = torch.randn_like(clean).to(device)
|
| 224 |
+
gain_factor = net_E(noisy) # estimate gain factor from noisy image
|
| 225 |
+
pred_noise_level_map = net_G1(clean, gain_factor)
|
| 226 |
+
sdnu_noise = pred_noise_level_map.mul(z) # synthesize signal-dependent neighboring uncorrelated noise
|
| 227 |
+
sdnc_noise = net_G2(sdnu_noise) + sdnu_noise # synthesize signal-dependent neighboring correlated noise
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
global_nl = torch.std(noise, dim=(1, 2, 3), keepdim=True) # global estimated noise level
|
| 231 |
+
mean_filter = nn.AvgPool2d((7, 7), stride=1, padding=3)
|
| 232 |
+
local_nl = torch.sqrt(mean_filter(noise**2) - (mean_filter(noise))**2) # local estimated noise level, var=E(x^2)-E(x)^2
|
| 233 |
+
pixel_shuffle = util.pixel_shuffle(stride=opt.pd_stride)
|
| 234 |
+
sub_noise = pixel_shuffle._crop(noise)
|
| 235 |
+
sub_sdnu_noise = pixel_shuffle._crop(sdnu_noise)
|
| 236 |
+
|
| 237 |
+
if opt.joint_start_epoch <= epoch:
|
| 238 |
+
if opt.seperate_train:
|
| 239 |
+
fake_noisy = sdnc_noise.detach() + clean
|
| 240 |
+
else:
|
| 241 |
+
fake_noisy = sdnc_noise + clean
|
| 242 |
+
rec_clean = net_G3(fake_noisy)
|
| 243 |
+
|
| 244 |
+
# 2) optimize generator
|
| 245 |
+
optim_E.zero_grad()
|
| 246 |
+
optim_G1.zero_grad()
|
| 247 |
+
optim_G2.zero_grad()
|
| 248 |
+
optim_G3.zero_grad()
|
| 249 |
+
|
| 250 |
+
std1_loss = Cri_rec(gain_factor, global_nl)
|
| 251 |
+
std2_loss = Cri_rec(pred_noise_level_map, local_nl)
|
| 252 |
+
reg_loss = Cri_rec(pred_noise_level_map, gain_factor.expand_as(pred_noise_level_map))
|
| 253 |
+
G_losses = opt.lambda_reg * reg_loss + opt.lambda_std1 * std1_loss + opt.lambda_std2 * std2_loss
|
| 254 |
+
if opt.joint_start_epoch <= epoch:
|
| 255 |
+
rec_loss = Cri_rec(rec_clean, clean)
|
| 256 |
+
G_losses += opt.lambda_rec * rec_loss
|
| 257 |
+
|
| 258 |
+
if current_step % iter_step == 0:
|
| 259 |
+
|
| 260 |
+
fake1 = net_D1(sdnc_noise)
|
| 261 |
+
fake2 =net_D2(sub_sdnu_noise)
|
| 262 |
+
adv1_loss = Cri_gan(fake1, True)
|
| 263 |
+
adv2_loss = Cri_gan(fake2, True)
|
| 264 |
+
G_losses += opt.lambda_adv1 * adv1_loss + opt.lambda_adv2 * adv2_loss
|
| 265 |
+
|
| 266 |
+
G_losses.backward()
|
| 267 |
+
optim_E.step()
|
| 268 |
+
optim_G1.step()
|
| 269 |
+
optim_G2.step()
|
| 270 |
+
optim_G3.step()
|
| 271 |
+
|
| 272 |
+
# 3) optimize discriminator
|
| 273 |
+
optim_D1.zero_grad()
|
| 274 |
+
optim_D2.zero_grad()
|
| 275 |
+
|
| 276 |
+
real1 = net_D1(noise)
|
| 277 |
+
fake1 = net_D1(sdnc_noise.detach())
|
| 278 |
+
real2 = net_D2(sub_noise)
|
| 279 |
+
fake2 = net_D2(sub_sdnu_noise.detach())
|
| 280 |
+
D_r_loss = Cri_gan(real1, True) + Cri_gan(real2, True)
|
| 281 |
+
D_f_loss = Cri_gan(fake1, False) + Cri_gan(fake2, False)
|
| 282 |
+
if opt.gan_mode == 'wgangp':
|
| 283 |
+
gp = net_util.gradient_penalty(net_D1, noise, sdnc_noise.detach(), device) + net_util.gradient_penalty(net_D2, sub_noise, sub_sdnu_noise.detach(), device)
|
| 284 |
+
D_losses = D_r_loss + D_f_loss+ opt.lambda_gp * gp
|
| 285 |
+
else:
|
| 286 |
+
D_losses = 0.5*(D_r_loss + D_f_loss)
|
| 287 |
+
D_losses.backward()
|
| 288 |
+
optim_D1.step()
|
| 289 |
+
optim_D2.step()
|
| 290 |
+
|
| 291 |
+
# 4) logs
|
| 292 |
+
if current_step % log_step == 0:
|
| 293 |
+
training_message['reg_loss'] = reg_loss.item()
|
| 294 |
+
training_message['std1_loss'] = std1_loss.item()
|
| 295 |
+
training_message['std2_loss'] = std2_loss.item()
|
| 296 |
+
training_message['adv1_loss'] = adv1_loss.item()
|
| 297 |
+
training_message['adv2_loss'] = adv2_loss.item()
|
| 298 |
+
training_message['D_r_loss'] = D_r_loss.item()
|
| 299 |
+
training_message['D_f_loss'] = D_f_loss.item()
|
| 300 |
+
loss_message = '<epoch:{:d}, step: {:08d}> '.format(epoch, current_step)
|
| 301 |
+
|
| 302 |
+
for k, v in training_message.items(): # merge log information into message
|
| 303 |
+
loss_message += '{:s}: {:.3e} '.format(k, v)
|
| 304 |
+
print(loss_message)
|
| 305 |
+
logger.info(loss_message)
|
| 306 |
+
|
| 307 |
+
if opt.use_tb:
|
| 308 |
+
tb.add_scalar('reg_loss', training_message['reg_loss'], current_step)
|
| 309 |
+
tb.add_scalar('std1_loss', training_message['std1_loss'], current_step)
|
| 310 |
+
tb.add_scalar('std2_loss', training_message['std2_loss'], current_step)
|
| 311 |
+
tb.add_scalar('adv1_loss', training_message['adv1_loss'], current_step)
|
| 312 |
+
tb.add_scalar('adv2_loss', training_message['adv2_loss'], current_step)
|
| 313 |
+
tb.add_scalar('D_r_loss', training_message['D_r_loss'], current_step)
|
| 314 |
+
tb.add_scalar('D_f_loss', training_message['D_f_loss'], current_step)
|
| 315 |
+
|
| 316 |
+
# if current_step % opt.val_step == 0:
|
| 317 |
+
# # validation
|
| 318 |
+
# net_E.eval()
|
| 319 |
+
# net_G1.eval()
|
| 320 |
+
# net_G2.eval()
|
| 321 |
+
# net_G3.eval()
|
| 322 |
+
# val_message = OrderedDict()
|
| 323 |
+
# val_kl = 0
|
| 324 |
+
# val_psnr = 0
|
| 325 |
+
# val_num = 0
|
| 326 |
+
|
| 327 |
+
# for data in val_loader:
|
| 328 |
+
# val_num += 1
|
| 329 |
+
# clean = data['clean'].to(device)
|
| 330 |
+
# noisy = data['noisy'].to(device)
|
| 331 |
+
# noise = data['noise'].to(device)
|
| 332 |
+
# with torch.no_grad():
|
| 333 |
+
# z = torch.randn_like(clean).to(device)
|
| 334 |
+
# gain_factor = net_E(noisy)
|
| 335 |
+
# pred_noise_level_map = net_G1(clean, gain_factor)
|
| 336 |
+
# sdnu_noise = pred_noise_level_map.mul(z)
|
| 337 |
+
# sdnc_noise = sdnu_noise + net_G2(sdnu_noise)
|
| 338 |
+
# fake_noisy = clean + sdnc_noise
|
| 339 |
+
# quantized_noise = util.noise_quantization(fake_noisy, clean)
|
| 340 |
+
# if opt.joint_start_epoch <= epoch:
|
| 341 |
+
# rec_clean = net_G3(noisy)
|
| 342 |
+
|
| 343 |
+
# val_kl += util.cal_kld(util.tensor2data(noise), util.tensor2data(quantized_noise))
|
| 344 |
+
# if opt.joint_start_epoch <= epoch:
|
| 345 |
+
# val_psnr += util.calculate_psnr(util.tensor2uint(rec_clean), util.tensor2uint(clean))
|
| 346 |
+
|
| 347 |
+
# val_kl /= val_num
|
| 348 |
+
# val_psnr /= val_num
|
| 349 |
+
# val_message['val_kl'] = val_kl
|
| 350 |
+
# val_message['val_psnr'] = val_psnr
|
| 351 |
+
# message = '<epoch:{:d}, step: {:08d}> '.format(epoch, current_step)
|
| 352 |
+
|
| 353 |
+
# for k, v in val_message.items():
|
| 354 |
+
# message += '{:s}: {:.3e} '.format(k, v)
|
| 355 |
+
# print(message)
|
| 356 |
+
# logger.info(message)
|
| 357 |
+
# if opt.use_tb:
|
| 358 |
+
# tb.add_scalar('val_kl', val_kl, current_step)
|
| 359 |
+
# tb.add_scalar('val_psnr', val_psnr, current_step)
|
| 360 |
+
|
| 361 |
+
# net_E.train()
|
| 362 |
+
# net_G1.train()
|
| 363 |
+
# net_G2.train()
|
| 364 |
+
# net_G3.train()
|
| 365 |
+
|
| 366 |
+
# test
|
| 367 |
+
if epoch % opt.test_epoch == 0:
|
| 368 |
+
net_E.eval()
|
| 369 |
+
net_G1.eval()
|
| 370 |
+
net_G2.eval()
|
| 371 |
+
net_G3.eval()
|
| 372 |
+
test_message = OrderedDict()
|
| 373 |
+
test_kl = 0
|
| 374 |
+
test_psnr = 0
|
| 375 |
+
test_num = 0
|
| 376 |
+
|
| 377 |
+
for data in test_loader:
|
| 378 |
+
test_num += 1
|
| 379 |
+
clean = data['clean'].to(device)
|
| 380 |
+
noisy = data['noisy'].to(device)
|
| 381 |
+
noise = data['noise'].to(device)
|
| 382 |
+
with torch.no_grad():
|
| 383 |
+
z = torch.randn_like(clean).to(device)
|
| 384 |
+
gain_factor = net_E(noisy)
|
| 385 |
+
pred_noise_level_map = net_G1(clean, gain_factor)
|
| 386 |
+
sdnu_noise = pred_noise_level_map.mul(z)
|
| 387 |
+
sdnc_noise = sdnu_noise + net_G2(sdnu_noise)
|
| 388 |
+
fake_noisy = clean + sdnc_noise
|
| 389 |
+
quantized_noise = util.noise_quantization(fake_noisy, clean)
|
| 390 |
+
|
| 391 |
+
if opt.use_tb:
|
| 392 |
+
tb.add_image('clean', clean.squeeze(), epoch)
|
| 393 |
+
tb.add_image('noisy', noisy.squeeze(), epoch)
|
| 394 |
+
tb.add_image('noise', noise.squeeze(), epoch)
|
| 395 |
+
tb.add_image('sdnu_noise', sdnu_noise.squeeze()+0.5, epoch)
|
| 396 |
+
tb.add_image('sdnc_noise', sdnc_noise.squeeze()+0.5, epoch)
|
| 397 |
+
tb.add_image('fake_noisy', fake_noisy.squeeze(), epoch)
|
| 398 |
+
|
| 399 |
+
if opt.joint_start_epoch <= epoch:
|
| 400 |
+
rec_clean = net_G3(noisy)
|
| 401 |
+
|
| 402 |
+
test_kl += util.cal_kld(util.tensor2data(noise), util.tensor2data(quantized_noise))
|
| 403 |
+
if opt.joint_start_epoch <= epoch:
|
| 404 |
+
test_psnr += util.calculate_psnr(util.tensor2uint(rec_clean), util.tensor2uint(clean))
|
| 405 |
+
|
| 406 |
+
if opt.joint_start_epoch <= epoch and opt.use_tb:
|
| 407 |
+
tb.add_image('rec_clean', rec_clean.squeeze(), epoch)
|
| 408 |
+
|
| 409 |
+
test_kl /= test_num
|
| 410 |
+
test_psnr /= test_num
|
| 411 |
+
test_message['test_kl'] = test_kl
|
| 412 |
+
test_message['test_psnr'] = test_psnr
|
| 413 |
+
message = '<epoch:{:d}> '.format(epoch)
|
| 414 |
+
|
| 415 |
+
for k, v in test_message.items():
|
| 416 |
+
message += '{:s}: {:.3e} '.format(k, v)
|
| 417 |
+
print(message)
|
| 418 |
+
logger.info(message)
|
| 419 |
+
if opt.use_tb:
|
| 420 |
+
tb.add_scalar('test_kl', test_kl, current_step)
|
| 421 |
+
tb.add_scalar('test_psnr', test_psnr, current_step)
|
| 422 |
+
|
| 423 |
+
net_E.train()
|
| 424 |
+
net_G1.train()
|
| 425 |
+
net_G2.train()
|
| 426 |
+
net_G3.train()
|
| 427 |
+
|
| 428 |
+
# save models
|
| 429 |
+
checkpoint_E = {'epoch': epoch, 'model_state_dict': net_util.get_bare_model(net_E).state_dict()}
|
| 430 |
+
checkpoint_G1 = {'epoch': epoch, 'model_state_dict': net_util.get_bare_model(net_G1).state_dict()}
|
| 431 |
+
checkpoint_G2 = {'epoch': epoch, 'model_state_dict': net_util.get_bare_model(net_G2).state_dict()}
|
| 432 |
+
checkpoint_D1 = {'epoch': epoch, 'model_state_dict': net_util.get_bare_model(net_D1).state_dict()}
|
| 433 |
+
checkpoint_D2 = {'epoch': epoch, 'model_state_dict': net_util.get_bare_model(net_D2).state_dict()}
|
| 434 |
+
checkpoint_G3 = {'epoch': epoch, 'model_state_dict': net_util.get_bare_model(net_G3).state_dict()}
|
| 435 |
+
torch.save(checkpoint_E, os.path.join(dir_save_models, 'checkpoint_E_{:08d}.pth'.format(epoch)))
|
| 436 |
+
torch.save(checkpoint_G1, os.path.join(dir_save_models, 'checkpoint_G1_{:08d}.pth'.format(epoch)))
|
| 437 |
+
torch.save(checkpoint_G2, os.path.join(dir_save_models, 'checkpoint_G2_{:08d}.pth'.format(epoch)))
|
| 438 |
+
torch.save(checkpoint_D1, os.path.join(dir_save_models, 'checkpoint_D1_{:08d}.pth'.format(epoch)))
|
| 439 |
+
torch.save(checkpoint_D2, os.path.join(dir_save_models, 'checkpoint_D2_{:08d}.pth'.format(epoch)))
|
| 440 |
+
torch.save(checkpoint_G3, os.path.join(dir_save_models, 'checkpoint_G3_{:08d}.pth'.format(epoch)))
|
| 441 |
+
|
| 442 |
+
# update learning rate
|
| 443 |
+
lr_scheduler_E.step()
|
| 444 |
+
lr_scheduler_G1.step()
|
| 445 |
+
lr_scheduler_G2.step()
|
| 446 |
+
lr_scheduler_D1.step()
|
| 447 |
+
lr_scheduler_D2.step()
|
| 448 |
+
lr_scheduler_G3.step()
|
| 449 |
+
|