45th commited on
Commit
0e7beee
·
verified ·
1 Parent(s): aad186e

Upload 91 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +7 -0
  2. custom_nodes/ComfyUI-DyPE/src/patch_utils.py +272 -0
  3. custom_nodes/ComfyUI-EsesImageEffectLevels/js/image_effect_levels.js +967 -0
  4. custom_nodes/NeuralISP/__init__.py +10 -0
  5. custom_nodes/NeuralISP/__pycache__/__init__.cpython-313.pyc +0 -0
  6. custom_nodes/NeuralISP/__pycache__/mirnetv2_model.cpython-313.pyc +0 -0
  7. custom_nodes/NeuralISP/__pycache__/mirnetv2_node.cpython-313.pyc +0 -0
  8. custom_nodes/NeuralISP/__pycache__/neural_isp_node.cpython-313.pyc +0 -0
  9. custom_nodes/NeuralISP/mirnetv2_model.py +301 -0
  10. custom_nodes/NeuralISP/neural_isp_node.py +407 -0
  11. custom_nodes/comfyui-faded-branding/__init__.py +6 -0
  12. custom_nodes/comfyui-faded-branding/__pycache__/__init__.cpython-313.pyc +0 -0
  13. custom_nodes/comfyui-faded-branding/__pycache__/faded_logo_node.cpython-313.pyc +0 -0
  14. custom_nodes/comfyui-faded-branding/__pycache__/ocean_logo_node.cpython-313.pyc +0 -0
  15. custom_nodes/comfyui-faded-branding/faded_logo_node.py +30 -0
  16. custom_nodes/comfyui-faded-branding/js/faded_logo.js +117 -0
  17. custom_nodes/comfyui-faded-branding/js/logo/info03.png +0 -0
  18. custom_nodes/comfyui-faded-branding/js/logo/logo01.png +3 -0
  19. custom_nodes/comfyui-faded-branding/js/logo/logo40.png +3 -0
  20. custom_nodes/comfyui-faded-branding/js/logo/logo44.png +3 -0
  21. custom_nodes/comfyui-faded-branding/js/logo/text01.png +0 -0
  22. custom_nodes/comfyui_essentials/luts/realisticLUT.cube +0 -0
  23. custom_nodes/image_routing.py +38 -0
  24. custom_nodes/latent_routing.py +87 -0
  25. custom_nodes/latent_trick.py +109 -0
  26. custom_nodes/save_image_jpeg.py +124 -0
  27. custom_nodes/srgb_noise/__init__.py +6 -0
  28. custom_nodes/srgb_noise/__pycache__/__init__.cpython-313.pyc +0 -0
  29. custom_nodes/srgb_noise/__pycache__/srgb_noise_node.cpython-313.pyc +0 -0
  30. custom_nodes/srgb_noise/repo/Crop_SIDD.py +14 -0
  31. custom_nodes/srgb_noise/repo/README.md +94 -0
  32. custom_nodes/srgb_noise/repo/data/__pycache__/dataset_base.cpython-39.pyc +0 -0
  33. custom_nodes/srgb_noise/repo/data/__pycache__/dataset_noise_synthesis.cpython-39.pyc +0 -0
  34. custom_nodes/srgb_noise/repo/data/dataset_base.py +62 -0
  35. custom_nodes/srgb_noise/repo/data/dataset_noise_synthesis.py +54 -0
  36. custom_nodes/srgb_noise/repo/data_preparation/G4_test.txt +4 -0
  37. custom_nodes/srgb_noise/repo/data_preparation/G4_train.txt +12 -0
  38. custom_nodes/srgb_noise/repo/data_preparation/GP_test.txt +5 -0
  39. custom_nodes/srgb_noise/repo/data_preparation/GP_train.txt +28 -0
  40. custom_nodes/srgb_noise/repo/data_preparation/IP_test.txt +10 -0
  41. custom_nodes/srgb_noise/repo/data_preparation/IP_train.txt +44 -0
  42. custom_nodes/srgb_noise/repo/data_preparation/N6_test.txt +4 -0
  43. custom_nodes/srgb_noise/repo/data_preparation/N6_train.txt +18 -0
  44. custom_nodes/srgb_noise/repo/data_preparation/S6_test.txt +7 -0
  45. custom_nodes/srgb_noise/repo/data_preparation/S6_train.txt +28 -0
  46. custom_nodes/srgb_noise/repo/doc/baseline.png +3 -0
  47. custom_nodes/srgb_noise/repo/doc/quantitative_results.png +0 -0
  48. custom_nodes/srgb_noise/repo/doc/visual_results.png +3 -0
  49. custom_nodes/srgb_noise/repo/main_test.py +78 -0
  50. 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

  • SHA256: 6510fc9bc58f2f1f4737ee490df4ca2be4f6491ab523377810840c7a78657658
  • Pointer size: 131 Bytes
  • Size of remote file: 933 kB
custom_nodes/comfyui-faded-branding/js/logo/logo40.png ADDED

Git LFS Details

  • SHA256: bab8d83ddd7e975874099d2e5f0e290cbb2fdd7f26d77dfa0fc807ac2d8b5717
  • Pointer size: 131 Bytes
  • Size of remote file: 935 kB
custom_nodes/comfyui-faded-branding/js/logo/logo44.png ADDED

Git LFS Details

  • SHA256: 43c43f1ed20e55644ae641c43b5b8880ecc309053a29ab50eda7b6320e08d6cc
  • Pointer size: 131 Bytes
  • Size of remote file: 947 kB
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
+ ![baseline](./doc/baseline.png)
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

  • SHA256: 79bff6e17a2400b788426bcfedf7026943cd161979591f72ce5a05e2a90077f4
  • Pointer size: 132 Bytes
  • Size of remote file: 3.24 MB
custom_nodes/srgb_noise/repo/doc/quantitative_results.png ADDED
custom_nodes/srgb_noise/repo/doc/visual_results.png ADDED

Git LFS Details

  • SHA256: 62314fa584e3898020214f88c435159589833b063e86b7c1c4b8caafa01a92c6
  • Pointer size: 133 Bytes
  • Size of remote file: 11.3 MB
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
+