Fioceen commited on
Commit
4c76d26
·
1 Parent(s): be1cf9c

ComfyUI Fix

Browse files
Files changed (1) hide show
  1. nodes.py +163 -82
nodes.py CHANGED
@@ -1,20 +1,27 @@
 
1
  from PIL import Image
2
  import numpy as np
3
  import os
4
  import tempfile
5
  from types import SimpleNamespace
 
6
  try:
7
- from worker import Worker
8
  except Exception as e:
9
- Worker = None
10
  IMPORT_ERROR = str(e)
11
  else:
12
  IMPORT_ERROR = None
13
 
 
14
  class NovaNodes:
15
  """
16
- ComfyUI node: Full post-processing chain using Worker from GUI
17
  All augmentations with tunable parameters.
 
 
 
 
18
  """
19
 
20
  @classmethod
@@ -111,87 +118,160 @@ class NovaNodes:
111
  iso_scale=1.0,
112
  read_noise=2.0):
113
 
114
- if Worker is None:
115
- raise ImportError(f"Could not import Worker module: {IMPORT_ERROR}")
116
-
117
- # Ensure input image is a PIL Image
118
- if not isinstance(image, Image.Image):
119
- raise ValueError("Input image must be a PIL Image object")
120
-
121
- # Save input image as temporary file
122
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_input:
123
- input_path = tmp_input.name
124
- image.save(input_path)
125
-
126
- # Prepare reference image if provided
127
- ref_path = None
128
- if ref_image is not None:
129
- if not isinstance(ref_image, Image.Image):
130
- raise ValueError("Reference image must be a PIL Image object")
131
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_ref:
132
- ref_path = tmp_ref.name
133
- ref_image.save(ref_path)
134
-
135
- # Create output temporary file path
136
- with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_output:
137
- output_path = tmp_output.name
138
-
139
- # Prepare parameters for Worker
140
- args = SimpleNamespace(
141
- noise_std=noise_std_frac,
142
- hot_pixel_prob=hot_pixel_prob,
143
- perturb=perturb_mag_frac,
144
- clahe_clip=clahe_clip,
145
- tile=clahe_grid,
146
- fstrength=fourier_strength,
147
- strength=fourier_strength,
148
- randomness=fourier_randomness,
149
- phase_perturb=fourier_phase_perturb,
150
- alpha=fourier_alpha,
151
- radial_smooth=fourier_radial_smooth,
152
- fft_mode=fourier_mode,
153
- vignette_strength=vignette_strength,
154
- chroma_strength=ca_shift,
155
- banding_strength=1.0 if apply_banding_o else 0.0,
156
- motion_blur_kernel=motion_blur_ksize,
157
- jpeg_cycles=jpeg_cycles,
158
- jpeg_qmin=jpeg_quality,
159
- jpeg_qmax=jpeg_quality,
160
- sim_camera=sim_camera,
161
- no_no_bayer=enable_bayer,
162
- iso_scale=iso_scale,
163
- read_noise=read_noise,
164
- ref=ref_path,
165
- fft_ref=ref_path,
166
- seed=None, # Seed handling can be added if Worker supports it
167
- cutoff=0.25 # Default value from GUI, adjustable if needed
168
- )
169
-
170
- # Run Worker
171
- worker = Worker(input_path, output_path, args)
172
- worker.run() # Assuming Worker has a synchronous run() method
173
-
174
- # Load output image
175
- output_img = Image.open(output_path)
176
-
177
- # Handle EXIF
178
- new_exif = ""
179
- if apply_exif_o:
180
- output_img, new_exif = self._add_fake_exif(output_img)
181
-
182
- # Clean up temporary files
183
- os.unlink(input_path)
184
- if ref_path:
185
- os.unlink(ref_path)
186
- os.unlink(output_path)
187
-
188
- return (output_img, new_exif)
189
-
190
- def _add_fake_exif(self, img: Image.Image) -> tuple[Image.Image, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  """Insert random but realistic camera EXIF metadata."""
192
  import random
193
  import io
194
- import piexif
 
 
 
 
195
  exif_dict = {
196
  "0th": {
197
  piexif.ImageIFD.Make: random.choice(["Canon", "Nikon", "Sony", "Fujifilm", "Olympus", "Leica"]),
@@ -213,6 +293,7 @@ class NovaNodes:
213
  output.seek(0)
214
  return (Image.open(output), str(exif_bytes))
215
 
 
216
  # -------------
217
  # Registration
218
  # -------------
@@ -221,4 +302,4 @@ NODE_CLASS_MAPPINGS = {
221
  }
222
  NODE_DISPLAY_NAME_MAPPINGS = {
223
  "NovaNodes": "Image Postprocess (NOVA NODES)",
224
- }
 
1
+ import torch
2
  from PIL import Image
3
  import numpy as np
4
  import os
5
  import tempfile
6
  from types import SimpleNamespace
7
+ from typing import Tuple
8
  try:
9
+ from .image_postprocess import process_image
10
  except Exception as e:
11
+ process_image = None
12
  IMPORT_ERROR = str(e)
13
  else:
14
  IMPORT_ERROR = None
15
 
16
+
17
  class NovaNodes:
18
  """
19
+ ComfyUI node: Full post-processing chain using process_image from image_postprocess
20
  All augmentations with tunable parameters.
21
+
22
+ NOTE: Adjusted to match FOOLAI output:
23
+ - Returns an IMAGE as a single PyTorch tensor shaped (1, H, W, C), dtype=float32, values in [0.0, 1.0].
24
+ - Returns EXIF as a STRING (second output slot).
25
  """
26
 
27
  @classmethod
 
118
  iso_scale=1.0,
119
  read_noise=2.0):
120
 
121
+ if process_image is None:
122
+ raise ImportError(f"Could not import process_image function: {IMPORT_ERROR}")
123
+
124
+ tmp_files = []
125
+
126
+ def to_pil_from_any(inp):
127
+ """Convert a torch tensor / numpy array of many shapes into a PIL RGB Image."""
128
+ # get numpy
129
+ if isinstance(inp, torch.Tensor):
130
+ arr = inp.detach().cpu().numpy()
131
+ else:
132
+ arr = np.asarray(inp)
133
+
134
+ # remove leading batch dimension if present
135
+ if arr.ndim == 4 and arr.shape[0] == 1:
136
+ arr = arr[0]
137
+
138
+ # CHW -> HWC
139
+ if arr.ndim == 3 and arr.shape[0] in (1, 3):
140
+ arr = np.transpose(arr, (1, 2, 0))
141
+
142
+ # if still 3D and last dim is channel (H,W,C) but C==1 or 3: OK
143
+ if arr.ndim == 2:
144
+ # grayscale HxW -> make HxWx1
145
+ arr = arr[:, :, None]
146
+
147
+ # Now arr should be H x W x C
148
+ if arr.ndim != 3:
149
+ # try permutations heuristically (rare)
150
+ for perm in [(1, 2, 0), (2, 0, 1), (0, 2, 1)]:
151
+ try:
152
+ cand = np.transpose(arr, perm)
153
+ if cand.ndim == 3:
154
+ arr = cand
155
+ break
156
+ except Exception:
157
+ pass
158
+
159
+ if arr.ndim != 3:
160
+ raise TypeError(f"Cannot convert array to HWC image, final ndim={arr.ndim}, shape={arr.shape}")
161
+
162
+ # Normalize numeric range to 0..255 uint8
163
+ if np.issubdtype(arr.dtype, np.floating):
164
+ # assume floats are 0..1 if max <= 1.0
165
+ if arr.max() <= 1.0:
166
+ arr = (arr * 255.0).clip(0, 255).astype(np.uint8)
167
+ else:
168
+ arr = np.clip(arr, 0, 255).astype(np.uint8)
169
+ else:
170
+ arr = arr.astype(np.uint8)
171
+
172
+ # If single channel, replicate to 3 channels (we want RGB files)
173
+ if arr.shape[2] == 1:
174
+ arr = np.repeat(arr, 3, axis=2)
175
+
176
+ # finally create PIL
177
+ return Image.fromarray(arr)
178
+
179
+ try:
180
+ # ---- Input image -> temporary input file ----
181
+ pil_img = to_pil_from_any(image[0])
182
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_input:
183
+ input_path = tmp_input.name
184
+ pil_img.save(input_path)
185
+ tmp_files.append(input_path)
186
+
187
+ # ---- Reference image if present ----
188
+ ref_path = None
189
+ if ref_image is not None:
190
+ pil_ref = to_pil_from_any(ref_image[0])
191
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_ref:
192
+ ref_path = tmp_ref.name
193
+ pil_ref.save(ref_path)
194
+ tmp_files.append(ref_path)
195
+
196
+ # ---- Output path ----
197
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_output:
198
+ output_path = tmp_output.name
199
+ tmp_files.append(output_path)
200
+
201
+ # Prepare args for process_image (keeping your names)
202
+ args = SimpleNamespace(
203
+ input=input_path,
204
+ output=output_path,
205
+ ref=ref_path,
206
+ noise_std=noise_std_frac,
207
+ hot_pixel_prob=hot_pixel_prob,
208
+ perturb=perturb_mag_frac,
209
+ clahe_clip=clahe_clip,
210
+ tile=clahe_grid,
211
+ fstrength=fourier_strength if apply_fourier_o else 0.0,
212
+ randomness=fourier_randomness,
213
+ phase_perturb=fourier_phase_perturb,
214
+ fft_alpha=fourier_alpha,
215
+ radial_smooth=fourier_radial_smooth,
216
+ fft_mode=fourier_mode,
217
+ fft_ref=ref_path,
218
+ vignette_strength=vignette_strength if apply_vignette_o else 0.0,
219
+ chroma_strength=ca_shift if apply_chromatic_aberration_o else 0.0,
220
+ banding_strength=1.0 if apply_banding_o else 0.0,
221
+ motion_blur_kernel=motion_blur_ksize if apply_motion_blur_o else 1,
222
+ jpeg_cycles=jpeg_cycles if apply_jpeg_cycles_o else 1,
223
+ jpeg_qmin=jpeg_quality,
224
+ jpeg_qmax=jpeg_quality,
225
+ sim_camera=sim_camera,
226
+ no_no_bayer=enable_bayer,
227
+ iso_scale=iso_scale,
228
+ read_noise=read_noise,
229
+ seed=None,
230
+ cutoff=0.25
231
+ )
232
+
233
+ # ---- Run the processing function ----
234
+ process_image(input_path, output_path, args)
235
+
236
+ # ---- Load result (force RGB to avoid unexpected single-channel shapes) ----
237
+ output_img = Image.open(output_path).convert("RGB")
238
+ img_out = np.array(output_img) # H x W x 3, uint8
239
+
240
+ # ---- EXIF insertion (optional) ----
241
+ new_exif = ""
242
+ if apply_exif_o:
243
+ try:
244
+ output_img_with_exif, new_exif = self._add_fake_exif(output_img)
245
+ output_img = output_img_with_exif
246
+ img_out = np.array(output_img.convert("RGB"))
247
+ except Exception:
248
+ new_exif = ""
249
+
250
+ # ---- Convert to FOOLAI-style tensor: (1, H, W, C), float32 in [0,1] ----
251
+ img_float = img_out.astype(np.float32) / 255.0 # H x W x C
252
+ tensor_out = torch.from_numpy(img_float).to(dtype=torch.float32).unsqueeze(0) # 1 x H x W x C
253
+ tensor_out = torch.clamp(tensor_out, 0.0, 1.0)
254
+
255
+ # Return the same format FOOLAI uses: (tensor, exif_string)
256
+ return (tensor_out, new_exif)
257
+
258
+ finally:
259
+ for p in tmp_files:
260
+ try:
261
+ os.unlink(p)
262
+ except Exception:
263
+ pass
264
+
265
+
266
+ def _add_fake_exif(self, img: Image.Image) -> Tuple[Image.Image, str]:
267
  """Insert random but realistic camera EXIF metadata."""
268
  import random
269
  import io
270
+ try:
271
+ import piexif
272
+ except Exception:
273
+ raise
274
+
275
  exif_dict = {
276
  "0th": {
277
  piexif.ImageIFD.Make: random.choice(["Canon", "Nikon", "Sony", "Fujifilm", "Olympus", "Leica"]),
 
293
  output.seek(0)
294
  return (Image.open(output), str(exif_bytes))
295
 
296
+
297
  # -------------
298
  # Registration
299
  # -------------
 
302
  }
303
  NODE_DISPLAY_NAME_MAPPINGS = {
304
  "NovaNodes": "Image Postprocess (NOVA NODES)",
305
+ }