-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathimage_adjustsments_2.py
More file actions
529 lines (409 loc) · 22.6 KB
/
Copy pathimage_adjustsments_2.py
File metadata and controls
529 lines (409 loc) · 22.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# ==========================================================================
# Eses Image Adjustments V2
# ==========================================================================
#
# Description:
# The 'Eses Image Adjustments V2' node offers a suite of tools
# for image post-processing within ComfyUI. It allows fine-tuning of
# various aspects of images. It applies effects in sequential order.
# This version leverages PyTorch for all image processing (except for unsharp
# mask operation), ensuring GPU acceleration and efficient tensor operations.
#
# Key Features:
#
# - Global Tonal Adjustments:
# - Contrast: Adjusts the difference between light and dark areas.
# - Gamma: Controls mid-tone brightness.
# - Saturation: Enhances or subdues image vibrancy.
#
# - Color Adjustments:
# - Hue Rotation: Rotates the color spectrum of the image.
# - RGB Channel Offsets: Allows for individual adjustments to the Red, Green,
# and Blue color channels for precise color grading.
#
# - Creative Effects:
# - Color Gel: Applies a colored tint to the image with adjustable strength.
# The gel color can be specified using hex codes (e.g., #RRGGBB)
# or RGB comma-separated values (e.g., R,G,B).
#
# - Sharpness:
# - Sharpness: Adjusts the overall sharpness of the image.
# - Unsharp Mask: Adds sharpening using the Pillow (PIL) unsharp mask algorithm
# with controls for radius, strength (percent), and threshold.
#
# - Black & White Conversion:
# - Grayscale: Easily converts the image to black and white.
#
# - Film Grain:
# - Grain Strength: Controls the intensity of added film grain.
# - Grain Contrast: Adjusts the contrast of the grain for more pronounced or subtle effects.
# - Color Grain Mix: Blends between monochromatic and colored grain.
#
# - Masking (new in 1.1.0):
# - Use Mask: Applies adjustments only to the white areas of the mask.
# - Invert Mask Influence: Inverts the mask effect, applying adjustments to black areas.
# - Mask Influence: Controls the strength of the mask's effect on adjustments (0-100).
#
# Usage:
# Connect your image tensor to the 'image' input. Adjust parameters
# as needed. The node outputs the 'adjusted_image' tensor, maintaining
# compatibility with other ComfyUI nodes.
#
# Version: 1.2.0
# License: See LICENSE.txt
#
# ==========================================================================
import torch
import re
import torch.nn.functional as F
import numpy as np
from PIL import Image, ImageFilter
# Helper Functions for Image Adjustments ----
# Helper to clamp values to [0, 1] for image tensors
def clamp_image_tensor(tensor: torch.Tensor) -> torch.Tensor:
return torch.clamp(tensor, 0.0, 1.0)
# Image Adjustments --------
def parse_color_string_to_tensor(color_str: str, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
"""Parses a hex (#RRGGBB) or RGB comma-separated (R,G,B) string into an (R, G, B) tensor (0-1 range)."""
color_str = color_str.strip()
# Try hex format: #RRGGBB
if re.match(r'^#[0-9a-fA-F]{6}$', color_str):
try:
hex_val = color_str[1:]
r = int(hex_val[0:2], 16)
g = int(hex_val[2:4], 16)
b = int(hex_val[4:6], 16)
return torch.tensor([r, g, b], device=device, dtype=dtype) / 255.0
except ValueError:
pass # Fall through to next attempt or default
# Try RGB comma-separated format: R,G,B
parts = color_str.split(',')
if len(parts) == 3:
try:
r = float(parts[0].strip())
g = float(parts[1].strip())
b = float(parts[2].strip())
# Clamp values to 0-255, then normalize to 0-1
r = max(0.0, min(255.0, r)) / 255.0
g = max(0.0, min(255.0, g)) / 255.0
b = max(0.0, min(255.0, b)) / 255.0
return torch.tensor([r, g, b], device=device, dtype=dtype)
except ValueError:
pass # Fall through to default
# Default to white (1.0, 1.0, 1.0) if parsing fails
print(f"Warning: Could not parse color string '{color_str}'. Defaulting to white (1.0,1.0,1.0) tensor.")
return torch.tensor([1.0, 1.0, 1.0], device=device, dtype=dtype)
def adjust_contrast_tensor(image_tensor: torch.Tensor, contrast_factor: float) -> torch.Tensor:
"""Adjusts the contrast of a PyTorch Tensor (B, H, W, C)."""
if contrast_factor == 1.0:
return image_tensor
# For contrast, compute the mean (gray point) and adjust deviations from it
# Mean across H, W dimensions for each B, C
mean = image_tensor.mean(dim=(-3, -2), keepdim=True) # (B, 1, 1, C)
adjusted_tensor = mean + (image_tensor - mean) * contrast_factor
return clamp_image_tensor(adjusted_tensor)
def adjust_gamma_tensor(image_tensor: torch.Tensor, gamma_factor: float) -> torch.Tensor:
"""Applies gamma correction to a PyTorch Tensor (B, H, W, C)."""
if gamma_factor == 1.0:
return image_tensor
# Gamma correction formula: output = input^(1/gamma)
adjusted_tensor = torch.pow(image_tensor, 1.0 / gamma_factor)
return clamp_image_tensor(adjusted_tensor)
def adjust_saturation_tensor(image_tensor: torch.Tensor, saturation_factor: float) -> torch.Tensor:
"""Adjusts the saturation of a PyTorch Tensor (B, H, W, C)."""
if saturation_factor == 1.0:
return image_tensor
# Convert to grayscale for desaturation baseline
# Luminance formula (sRGB): 0.299*R + 0.587*G + 0.114*B
grayscale = (image_tensor[..., 0] * 0.299 +
image_tensor[..., 1] * 0.587 +
image_tensor[..., 2] * 0.114).unsqueeze(-1) # (B, H, W, 1)
# Blend original and grayscale based on saturation factor
# Lerp: start * (1 - weight) + end * weight
# Here, grayscale is 'start', original is 'end'
# For desaturation, blend towards grayscale (weight < 1)
# For oversaturation, blend beyond original (weight > 1)
adjusted_tensor = torch.lerp(grayscale, image_tensor, saturation_factor)
return clamp_image_tensor(adjusted_tensor)
def adjust_hue_rotation_tensor(image_tensor: torch.Tensor, hue_degrees: float) -> torch.Tensor:
"""Rotates the hue of a PyTorch Tensor (B, H, W, C)."""
if hue_degrees == 0.0:
return image_tensor
# Convert RGB to HSV
# This is a manual implementation of RGB to HSV and back.
# It might be less optimized than dedicated libraries but adheres to constraints.
max_rgb, _ = torch.max(image_tensor, dim=-1, keepdim=True)
min_rgb, _ = torch.min(image_tensor, dim=-1, keepdim=True)
delta = max_rgb - min_rgb
v = max_rgb # Value is max(R, G, B)
s = torch.where(max_rgb != 0, delta / max_rgb, torch.zeros_like(delta)) # Saturation
hue = torch.zeros_like(image_tensor[..., :1]) # Initialize hue channel (B, H, W, 1)
# Compute Hue
# For R channel (max_rgb == image_tensor[..., 0])
is_r_max = (max_rgb == image_tensor[..., 0:1]) # Use slice to keep dim
hue_r_branch = torch.where(is_r_max & (delta != 0), (image_tensor[..., 1:2] - image_tensor[..., 2:3]) / delta, torch.zeros_like(hue))
hue = torch.where(is_r_max, hue_r_branch, hue)
# For G channel (max_rgb == image_tensor[..., 1])
is_g_max = (max_rgb == image_tensor[..., 1:2])
hue_g_branch = torch.where(is_g_max & (delta != 0), (image_tensor[..., 2:3] - image_tensor[..., 0:1]) / delta + 2, torch.zeros_like(hue))
hue = torch.where(is_g_max, hue_g_branch, hue)
# For B channel (max_rgb == image_tensor[..., 2])
is_b_max = (max_rgb == image_tensor[..., 2:3])
hue_b_branch = torch.where(is_b_max & (delta != 0), (image_tensor[..., 0:1] - image_tensor[..., 1:2]) / delta + 4, torch.zeros_like(hue))
hue = torch.where(is_b_max, hue_b_branch, hue)
hue = (hue * 60.0) % 360.0 # Convert to degrees and wrap around
# Apply hue rotation
hue_rotated = (hue + hue_degrees) % 360.0
# Convert HSV back to RGB
# Ensure all components are (B, H, W) before calculations
h = (hue_rotated / 360.0).squeeze(-1) # H in [0,1]
s = s.squeeze(-1) # S in [0,1]
v = v.squeeze(-1) # V in [0,1]
c = v * s
x = c * (1 - torch.abs((h * 6) % 2 - 1))
m = v - c
# Initialize R', G', B' components as (B, H, W)
r_prime = torch.zeros_like(c)
g_prime = torch.zeros_like(c)
b_prime = torch.zeros_like(c)
# Define masks for different hue segments
mask_0_1_6 = (h >= 0) & (h < 1/6)
mask_1_6_2_6 = (h >= 1/6) & (h < 2/6)
mask_2_6_3_6 = (h >= 2/6) & (h < 3/6)
mask_3_6_4_6 = (h >= 3/6) & (h < 4/6)
mask_4_6_5_6 = (h >= 4/6) & (h < 5/6)
mask_5_6_1 = (h >= 5/6) & (h <= 1.0 + 1e-6) # Use epsilon for float comparison to include 1.0
r_prime[mask_0_1_6] = c[mask_0_1_6]
g_prime[mask_0_1_6] = x[mask_0_1_6]
r_prime[mask_1_6_2_6] = x[mask_1_6_2_6]
g_prime[mask_1_6_2_6] = c[mask_1_6_2_6]
g_prime[mask_2_6_3_6] = c[mask_2_6_3_6]
b_prime[mask_2_6_3_6] = x[mask_2_6_3_6]
g_prime[mask_3_6_4_6] = x[mask_3_6_4_6]
b_prime[mask_3_6_4_6] = c[mask_3_6_4_6]
r_prime[mask_4_6_5_6] = x[mask_4_6_5_6]
b_prime[mask_4_6_5_6] = c[mask_4_6_5_6]
r_prime[mask_5_6_1] = c[mask_5_6_1]
b_prime[mask_5_6_1] = x[mask_5_6_1]
# Unsqueeze back to (B, H, W, 1) before concatenating
r_final = (r_prime + m).unsqueeze(-1)
g_final = (g_prime + m).unsqueeze(-1)
b_final = (b_prime + m).unsqueeze(-1)
adjusted_tensor = torch.cat([r_final, g_final, b_final], dim=-1)
return clamp_image_tensor(adjusted_tensor)
def adjust_color_balance_tensor(image_tensor: torch.Tensor, r_offset: float, g_offset: float, b_offset: float) -> torch.Tensor:
"""Adjusts the color balance by offsetting R, G, B channels of a PyTorch Tensor (B, H, W, C)."""
if r_offset == 0.0 and g_offset == 0.0 and b_offset == 0.0:
return image_tensor
# Convert offsets from -100 to 100 scale to -1 to 1 scale for tensor addition
r_offset_norm = r_offset / 255.0
g_offset_norm = g_offset / 255.0
b_offset_norm = b_offset / 255.0
offsets = torch.tensor([r_offset_norm, g_offset_norm, b_offset_norm], device=image_tensor.device, dtype=image_tensor.dtype)
# Add offsets, broadcasting across H, W, and B
adjusted_tensor = image_tensor + offsets
return clamp_image_tensor(adjusted_tensor)
def apply_color_gel_tensor(image_tensor: torch.Tensor, gel_color_str: str, gel_strength: float) -> torch.Tensor:
"""
Applies a color gel effect to a PyTorch Tensor (B, H, W, C).
"""
if gel_strength == 0.0:
return image_tensor
gel_color_tensor = parse_color_string_to_tensor(gel_color_str, image_tensor.device, image_tensor.dtype) # (3,)
# Broadcast gel_color_tensor to match image_tensor shape for multiplication
gel_color_broadcast = gel_color_tensor.view(1, 1, 1, 3) # (1, 1, 1, 3)
# Multiply operation (like "multiply" blend mode)
multiplied_tensor = image_tensor * gel_color_broadcast
# Blend original and multiplied based on strength
# lerp(start, end, weight) = start * (1 - weight) + end * weight
adjusted_tensor = torch.lerp(image_tensor, multiplied_tensor, gel_strength)
return clamp_image_tensor(adjusted_tensor)
def adjust_sharpness_tensor(image_tensor: torch.Tensor, sharpness_factor: float) -> torch.Tensor:
"""Adjusts the sharpness of a PyTorch Tensor using a simple convolution with replicate padding."""
if sharpness_factor == 1.0:
return image_tensor
# Permute to (B, C, H, W) for conv2d
img_permuted = image_tensor.permute(0, 3, 1, 2) # (B, C, H, W)
# Standard sharpening kernel (sums to 1, retains brightness)
sharpen_kernel = torch.tensor([[0., -1., 0.],
[-1., 5., -1.],
[0., -1., 0.]], dtype=image_tensor.dtype, device=image_tensor.device)
sharpen_kernel = sharpen_kernel.view(1, 1, 3, 3) # (out_channels, in_channels/groups, kH, kW)
# Apply replicate padding before convolution to handle borders more naturally
# For a 3x3 kernel, padding of 1 on each side is needed to maintain size
padded_img_permuted = F.pad(img_permuted, (1, 1, 1, 1), mode='replicate') # (left, right, top, bottom)
final_sharpened_channels = []
for c in range(padded_img_permuted.shape[1]):
# Apply convolution with padding=0 because we pre-padded
sharpened_channel = F.conv2d(padded_img_permuted[:, c:c+1, :, :], sharpen_kernel, padding=0)
final_sharpened_channels.append(sharpened_channel)
final_sharpened_img_permuted = torch.cat(final_sharpened_channels, dim=1) # (B, C, H, W)
# Blend original and the "sharpened version"
# lerp(start, end, weight) = start * (1 - weight) + end * weight
# If sharpness_factor=1, weight=0 => original.
# If sharpness_factor=0, weight=-1.0 => original - (final_sharpened - original).
# If sharpness_factor=2, weight=1.0 => final_sharpened.
adjusted_tensor_permuted = torch.lerp(img_permuted, final_sharpened_img_permuted, sharpness_factor - 1.0)
# Permute back to (B, H, W, C)
adjusted_tensor = adjusted_tensor_permuted.permute(0, 2, 3, 1)
return clamp_image_tensor(adjusted_tensor)
def apply_unsharp_mask_pillow_tensor(image_tensor: torch.Tensor, unsharp_blur_radius: float, unsharp_strength: float, unsharp_threshold: int) -> torch.Tensor:
"""Applies a Pillow-based unsharp mask to a PyTorch Tensor by converting to/from PIL images."""
if unsharp_strength == 0.0 or unsharp_blur_radius == 0.0:
return image_tensor
# Convert strength (e.g., 0-5.0 float) to Pillow's percent (e.g., 0-500 integer)
unsharp_percent = int(unsharp_strength * 100)
device = image_tensor.device
processed_batches = []
# Process each image in the batch individually since Pillow is CPU-based
for i in range(image_tensor.shape[0]):
# 1. Convert tensor slice to PIL Image
# Tensor (1, H, W, C) [0,1] -> Numpy (H, W, C) [0,255] -> PIL Image
slice_tensor = image_tensor[i]
image_np = (slice_tensor.cpu().numpy() * 255).astype(np.uint8)
pil_image = Image.fromarray(image_np, 'RGB')
# 2. Apply Unsharp Mask filter
sharpened_pil = pil_image.filter(ImageFilter.UnsharpMask(
radius=unsharp_blur_radius,
percent=unsharp_percent,
threshold=unsharp_threshold
))
# 3. Convert back to tensor
# PIL Image -> Numpy (H, W, C) [0,255] -> Tensor (1, H, W, C) [0,1]
sharpened_np = np.array(sharpened_pil).astype(np.float32) / 255.0
processed_tensor_slice = torch.from_numpy(sharpened_np).unsqueeze(0)
processed_batches.append(processed_tensor_slice)
# Stack the processed slices back into a single tensor on the original device
return torch.cat(processed_batches, dim=0).to(device)
def convert_to_grayscale_tensor(image_tensor: torch.Tensor) -> torch.Tensor:
"""Converts a PyTorch Tensor (B, H, W, C) to grayscale."""
# Luminance formula: 0.299*R + 0.587*G + 0.114*B
grayscale_tensor = (image_tensor[..., 0] * 0.299 +
image_tensor[..., 1] * 0.587 +
image_tensor[..., 2] * 0.114).unsqueeze(-1)
# Replicate the single channel across RGB to make it a 3-channel grayscale image
return grayscale_tensor.repeat(1, 1, 1, 3)
def add_film_grain_tensor(image_tensor: torch.Tensor, grain_strength: float, grain_contrast: float, color_grain_mix: float) -> torch.Tensor:
"""Adds realistic film grain (Gaussian noise) to a PyTorch Tensor (B, H, W, C)."""
if grain_strength == 0.0:
return image_tensor
# Generate noise with same shape as image_tensor
noise_shape = image_tensor.shape
device = image_tensor.device
dtype = image_tensor.dtype
# Color noise (different for each channel) - scaled for 0-1 range
color_noise = torch.randn(noise_shape, device=device, dtype=dtype) * grain_strength * 0.5
# Grayscale noise (same across channels) - scaled for 0-1 range
grayscale_noise_1ch = torch.randn(noise_shape[0], noise_shape[1], noise_shape[2], 1, device=device, dtype=dtype) * grain_strength * 0.5
grayscale_noise = grayscale_noise_1ch.repeat(1, 1, 1, noise_shape[3])
# Blend color and grayscale noise
blended_noise = torch.lerp(grayscale_noise, color_noise, color_grain_mix)
blended_noise *= grain_contrast # Apply contrast to the blended noise
noisy_image_tensor = image_tensor + blended_noise
return clamp_image_tensor(noisy_image_tensor)
# ComfyUI Class ---
class EsesImageAdjustments2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",), # (B, H, W, C) tensor
# Global Tonal Adjustments
"contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
"gamma": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.01}),
"saturation": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
# Color Adjustments
"hue_rotation": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0, "step": 1.0}),
"r_offset": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0}),
"g_offset": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0}),
"b_offset": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0}),
# Creative Effects
"gel_color": ("STRING", {"default": "255,200,0", "multiline": False}),
"gel_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
# Sharpness
"sharpness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
# Unsharp Mask (Also sharpness, but more control)
"unsharp_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 5.0, "step": 0.01}),
"unsharp_blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"unsharp_threshold": ("INT", {"default": 3, "min": 0, "max": 255, "step": 1}),
# B&W
"grayscale": ("BOOLEAN", {"default": False}),
# Grain
"grain_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.1, "step": 0.001}),
"grain_contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
"color_grain_mix": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
},
"optional": {
"mask": ("MASK",),
"use_mask": ("BOOLEAN", {"default": False}),
"invert_mask_influence": ("BOOLEAN", {"default": False}),
"mask_influence": ("FLOAT", {"default": 100.0, "min": 0.0, "max": 100.0, "step": 1.0}),
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("adjusted_image", "output_mask",)
FUNCTION = "adjust_image"
CATEGORY = "Eses Nodes/Image Adjustments"
def adjust_image(self, image: torch.Tensor, mask=None,
# Tonal Adjustments
contrast=1.0, gamma=1.0, saturation=1.0,
# Color Adjustments
hue_rotation=0.0,
r_offset=0.0, g_offset=0.0, b_offset=0.0,
# Creative Effects
gel_color="255,200,0", gel_strength=0.0,
# Sharpness
sharpness=1.0,
unsharp_strength=0.0, unsharp_blur_radius=0.0, unsharp_threshold=3,
# B&W
grayscale=False,
# Grain
grain_strength=0.0, grain_contrast=1.0, color_grain_mix=1.0,
# Masking
use_mask=False, invert_mask_influence=False, mask_influence=100.0):
original_image_tensor = image # Keep a copy of the original image
current_image_tensor = image # Start with the input tensor for adjustments
# Apply all adjustments sequentially
current_image_tensor = adjust_contrast_tensor(current_image_tensor, contrast)
current_image_tensor = adjust_gamma_tensor(current_image_tensor, gamma)
current_image_tensor = adjust_saturation_tensor(current_image_tensor, saturation)
current_image_tensor = adjust_hue_rotation_tensor(current_image_tensor, hue_rotation)
current_image_tensor = adjust_color_balance_tensor(current_image_tensor, r_offset, g_offset, b_offset)
current_image_tensor = apply_color_gel_tensor(current_image_tensor, gel_color, gel_strength)
current_image_tensor = adjust_sharpness_tensor(current_image_tensor, sharpness)
current_image_tensor = apply_unsharp_mask_pillow_tensor(current_image_tensor, unsharp_blur_radius, unsharp_strength, unsharp_threshold)
if grayscale:
current_image_tensor = convert_to_grayscale_tensor(current_image_tensor)
current_image_tensor = add_film_grain_tensor(current_image_tensor, grain_strength, grain_contrast, color_grain_mix)
output_mask = None # Initialize output mask
if use_mask and mask is not None:
processed_mask = mask.unsqueeze(-1)
if invert_mask_influence:
processed_mask = 1.0 - processed_mask
# Convert mask_influence (0-100) to a blend factor (0-1)
influence_factor = mask_influence / 100.0
# Calculate the blend weight for the image.
# When influence_factor is 0 (mask_influence 0%),
# mask_blend_weight should be 1.0 (effect everywhere).
# When influence_factor is 1 (mask_influence 100%),
# mask_blend_weight should be processed_mask (effect only on mask).
image_blend_weight = torch.lerp(torch.tensor(1.0, device=processed_mask.device, dtype=processed_mask.dtype), processed_mask, influence_factor)
# Alpha blend for the image
adjusted_image_tensor = torch.lerp(original_image_tensor, current_image_tensor, image_blend_weight)
# The output mask should also reflect this fade effect
# Unsqueeze if needed to match image_blend_weight dimensions
# for consistency, then squeeze for output
output_mask = image_blend_weight.squeeze(-1) if image_blend_weight.dim() > 3 else image_blend_weight
else:
adjusted_image_tensor = current_image_tensor
# If mask is not used, the output mask should be
# a full white mask (no restriction)
# Match the batch size and dimensions of the
# input image for the output mask.
if image is not None:
output_mask = torch.ones_like(image[:, :, :, 0]) # (B, H, W)
else:
output_mask = None
return (adjusted_image_tensor, output_mask,)