ComfyUI Fix
Browse files
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
|
| 8 |
except Exception as e:
|
| 9 |
-
|
| 10 |
IMPORT_ERROR = str(e)
|
| 11 |
else:
|
| 12 |
IMPORT_ERROR = None
|
| 13 |
|
|
|
|
| 14 |
class NovaNodes:
|
| 15 |
"""
|
| 16 |
-
ComfyUI node: Full post-processing chain using
|
| 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
|
| 115 |
-
raise ImportError(f"Could not import
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
"""Insert random but realistic camera EXIF metadata."""
|
| 192 |
import random
|
| 193 |
import io
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|