MogensR commited on
Commit
90fbb5e
·
1 Parent(s): 2e1d581

Create processing/effects.py

Browse files
Files changed (1) hide show
  1. processing/effects.py +630 -0
processing/effects.py ADDED
@@ -0,0 +1,630 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visual effects and enhancements for BackgroundFX Pro.
3
+ Implements professional-grade effects for background replacement.
4
+ """
5
+
6
+ import cv2
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from typing import Dict, List, Optional, Tuple, Union
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ import logging
14
+ from scipy.ndimage import gaussian_filter, map_coordinates
15
+
16
+ from ..utils.logger import setup_logger
17
+ from ..utils.device import DeviceManager
18
+ from ..core.quality import QualityAnalyzer
19
+
20
+ logger = setup_logger(__name__)
21
+
22
+
23
+ class EffectType(Enum):
24
+ """Available effect types."""
25
+ BLUR = "blur"
26
+ BOKEH = "bokeh"
27
+ COLOR_SHIFT = "color_shift"
28
+ LIGHT_WRAP = "light_wrap"
29
+ SHADOW = "shadow"
30
+ REFLECTION = "reflection"
31
+ GLOW = "glow"
32
+ CHROMATIC_ABERRATION = "chromatic_aberration"
33
+ VIGNETTE = "vignette"
34
+ FILM_GRAIN = "film_grain"
35
+ MOTION_BLUR = "motion_blur"
36
+ DEPTH_OF_FIELD = "depth_of_field"
37
+
38
+
39
+ @dataclass
40
+ class EffectConfig:
41
+ """Configuration for visual effects."""
42
+ blur_strength: float = 15.0
43
+ bokeh_size: int = 21
44
+ bokeh_brightness: float = 1.5
45
+ light_wrap_intensity: float = 0.3
46
+ light_wrap_width: int = 10
47
+ shadow_opacity: float = 0.5
48
+ shadow_blur: float = 10.0
49
+ shadow_offset: Tuple[int, int] = (5, 5)
50
+ glow_intensity: float = 0.5
51
+ glow_radius: int = 20
52
+ chromatic_shift: float = 2.0
53
+ vignette_strength: float = 0.3
54
+ grain_intensity: float = 0.1
55
+ motion_blur_angle: float = 0.0
56
+ motion_blur_size: int = 15
57
+
58
+
59
+ class BackgroundEffects:
60
+ """Apply effects to background images."""
61
+
62
+ def __init__(self, config: Optional[EffectConfig] = None):
63
+ self.config = config or EffectConfig()
64
+ self.device_manager = DeviceManager()
65
+
66
+ def apply_blur(self, image: np.ndarray,
67
+ strength: Optional[float] = None,
68
+ mask: Optional[np.ndarray] = None) -> np.ndarray:
69
+ """
70
+ Apply Gaussian blur to image.
71
+
72
+ Args:
73
+ image: Input image
74
+ strength: Blur strength
75
+ mask: Optional mask for selective blur
76
+
77
+ Returns:
78
+ Blurred image
79
+ """
80
+ strength = strength or self.config.blur_strength
81
+
82
+ if strength <= 0:
83
+ return image
84
+
85
+ # Calculate kernel size (must be odd)
86
+ kernel_size = int(strength * 2) + 1
87
+
88
+ # Apply blur
89
+ blurred = cv2.GaussianBlur(image, (kernel_size, kernel_size), strength)
90
+
91
+ # Apply mask if provided
92
+ if mask is not None:
93
+ mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2)
94
+ if mask_3ch.max() > 1:
95
+ mask_3ch = mask_3ch / 255.0
96
+
97
+ blurred = image * (1 - mask_3ch) + blurred * mask_3ch
98
+ blurred = blurred.astype(np.uint8)
99
+
100
+ return blurred
101
+
102
+ def apply_bokeh(self, image: np.ndarray,
103
+ depth_map: Optional[np.ndarray] = None) -> np.ndarray:
104
+ """
105
+ Apply bokeh effect to simulate depth of field.
106
+
107
+ Args:
108
+ image: Input image
109
+ depth_map: Optional depth map for varying blur
110
+
111
+ Returns:
112
+ Image with bokeh effect
113
+ """
114
+ h, w = image.shape[:2]
115
+
116
+ # Create depth map if not provided
117
+ if depth_map is None:
118
+ # Simple radial depth map
119
+ center_x, center_y = w // 2, h // 2
120
+ Y, X = np.ogrid[:h, :w]
121
+ dist = np.sqrt((X - center_x)**2 + (Y - center_y)**2)
122
+ depth_map = dist / dist.max()
123
+
124
+ # Normalize depth map
125
+ if depth_map.max() > 1:
126
+ depth_map = depth_map / 255.0
127
+
128
+ # Create bokeh kernel
129
+ kernel_size = self.config.bokeh_size
130
+ kernel = self._create_bokeh_kernel(kernel_size)
131
+
132
+ # Apply varying blur based on depth
133
+ result = np.zeros_like(image, dtype=np.float32)
134
+
135
+ # Create multiple blur levels
136
+ blur_levels = 5
137
+ for i in range(blur_levels):
138
+ blur_strength = (i + 1) * (kernel_size // blur_levels)
139
+
140
+ if blur_strength > 0:
141
+ blurred = cv2.filter2D(image, -1, kernel[:blur_strength, :blur_strength])
142
+ else:
143
+ blurred = image
144
+
145
+ # Create mask for this depth level
146
+ depth_min = i / blur_levels
147
+ depth_max = (i + 1) / blur_levels
148
+ mask = ((depth_map >= depth_min) & (depth_map < depth_max)).astype(np.float32)
149
+
150
+ # Expand mask to 3 channels
151
+ mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2)
152
+
153
+ # Accumulate result
154
+ result += blurred * mask_3ch
155
+
156
+ # Add bokeh highlights
157
+ result = self._add_bokeh_highlights(result, depth_map)
158
+
159
+ return np.clip(result, 0, 255).astype(np.uint8)
160
+
161
+ def _create_bokeh_kernel(self, size: int) -> np.ndarray:
162
+ """Create hexagonal bokeh kernel."""
163
+ kernel = np.zeros((size, size), dtype=np.float32)
164
+ center = size // 2
165
+ radius = center - 1
166
+
167
+ # Create hexagonal shape
168
+ for i in range(size):
169
+ for j in range(size):
170
+ x, y = i - center, j - center
171
+ # Hexagon equation
172
+ if abs(x) <= radius and abs(y) <= radius * np.sqrt(3) / 2:
173
+ if abs(y) <= (radius * np.sqrt(3) / 2 - abs(x) * np.sqrt(3) / 2):
174
+ kernel[i, j] = 1.0
175
+
176
+ # Normalize
177
+ kernel /= kernel.sum()
178
+
179
+ return kernel
180
+
181
+ def _add_bokeh_highlights(self, image: np.ndarray,
182
+ depth_map: np.ndarray) -> np.ndarray:
183
+ """Add bright bokeh spots to out-of-focus areas."""
184
+ # Extract bright spots
185
+ gray = cv2.cvtColor(image.astype(np.uint8), cv2.COLOR_BGR2GRAY)
186
+ _, bright_mask = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY)
187
+
188
+ # Dilate bright spots in blurred areas
189
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
190
+ bright_mask = cv2.dilate(bright_mask, kernel, iterations=2)
191
+
192
+ # Apply only to out-of-focus areas
193
+ bright_mask = (bright_mask * depth_map).astype(np.uint8)
194
+
195
+ # Create glow effect
196
+ glow = cv2.GaussianBlur(bright_mask, (21, 21), 10)
197
+ glow = cv2.cvtColor(glow, cv2.COLOR_GRAY2BGR) / 255.0
198
+
199
+ # Add glow to image
200
+ result = image + glow * self.config.bokeh_brightness * 50
201
+
202
+ return result
203
+
204
+ def apply_light_wrap(self, foreground: np.ndarray,
205
+ background: np.ndarray,
206
+ mask: np.ndarray) -> np.ndarray:
207
+ """
208
+ Apply light wrap effect for better compositing.
209
+
210
+ Args:
211
+ foreground: Foreground image
212
+ background: Background image
213
+ mask: Foreground mask
214
+
215
+ Returns:
216
+ Foreground with light wrap
217
+ """
218
+ # Ensure mask is single channel
219
+ if len(mask.shape) == 3:
220
+ mask = mask[:, :, 0]
221
+
222
+ # Normalize mask
223
+ if mask.max() > 1:
224
+ mask = mask / 255.0
225
+
226
+ # Create edge mask
227
+ kernel = np.ones((self.config.light_wrap_width, self.config.light_wrap_width), np.uint8)
228
+ dilated_mask = cv2.dilate(mask, kernel, iterations=1)
229
+ edge_mask = dilated_mask - mask
230
+
231
+ # Blur the background
232
+ blurred_bg = cv2.GaussianBlur(background, (21, 21), 10)
233
+
234
+ # Extract light from background
235
+ bg_light = blurred_bg * edge_mask[:, :, np.newaxis]
236
+
237
+ # Add light wrap to foreground edges
238
+ mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2)
239
+ wrapped = foreground + bg_light * self.config.light_wrap_intensity
240
+
241
+ return np.clip(wrapped, 0, 255).astype(np.uint8)
242
+
243
+ def add_shadow(self, image: np.ndarray,
244
+ mask: np.ndarray,
245
+ ground_plane: Optional[float] = None) -> np.ndarray:
246
+ """
247
+ Add realistic shadow to composited image.
248
+
249
+ Args:
250
+ image: Background image
251
+ mask: Object mask
252
+ ground_plane: Y-coordinate of ground plane
253
+
254
+ Returns:
255
+ Image with shadow
256
+ """
257
+ h, w = image.shape[:2]
258
+
259
+ if ground_plane is None:
260
+ ground_plane = h * 0.9 # Default near bottom
261
+
262
+ # Create shadow mask
263
+ shadow_mask = mask.copy()
264
+ if len(shadow_mask.shape) == 3:
265
+ shadow_mask = shadow_mask[:, :, 0]
266
+
267
+ # Transform shadow (simple perspective)
268
+ offset_x, offset_y = self.config.shadow_offset
269
+
270
+ # Create transformation matrix
271
+ src_points = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
272
+ dst_points = np.float32([
273
+ [offset_x, offset_y],
274
+ [w + offset_x, offset_y],
275
+ [-offset_x * 2, h],
276
+ [w + offset_x * 2, h]
277
+ ])
278
+
279
+ matrix = cv2.getPerspectiveTransform(src_points, dst_points)
280
+ shadow_mask = cv2.warpPerspective(shadow_mask, matrix, (w, h))
281
+
282
+ # Blur shadow
283
+ blur_size = int(self.config.shadow_blur) * 2 + 1
284
+ shadow_mask = cv2.GaussianBlur(shadow_mask, (blur_size, blur_size),
285
+ self.config.shadow_blur)
286
+
287
+ # Clip shadow to ground plane
288
+ shadow_mask[:int(ground_plane), :] = 0
289
+
290
+ # Normalize and apply opacity
291
+ if shadow_mask.max() > 0:
292
+ shadow_mask = shadow_mask / shadow_mask.max()
293
+ shadow_mask *= self.config.shadow_opacity
294
+
295
+ # Darken image where shadow falls
296
+ shadow_color = np.array([0, 0, 0], dtype=np.float32)
297
+ shadow_mask_3ch = np.repeat(shadow_mask[:, :, np.newaxis], 3, axis=2)
298
+
299
+ result = image * (1 - shadow_mask_3ch) + shadow_color * shadow_mask_3ch
300
+
301
+ return np.clip(result, 0, 255).astype(np.uint8)
302
+
303
+ def add_reflection(self, image: np.ndarray,
304
+ mask: np.ndarray,
305
+ reflection_strength: float = 0.3) -> np.ndarray:
306
+ """
307
+ Add reflection effect for glossy surfaces.
308
+
309
+ Args:
310
+ image: Input image
311
+ mask: Object mask
312
+ reflection_strength: Reflection opacity
313
+
314
+ Returns:
315
+ Image with reflection
316
+ """
317
+ h, w = image.shape[:2]
318
+
319
+ # Extract object using mask
320
+ if len(mask.shape) == 2:
321
+ mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2)
322
+ else:
323
+ mask_3ch = mask
324
+
325
+ if mask_3ch.max() > 1:
326
+ mask_3ch = mask_3ch / 255.0
327
+
328
+ object_only = image * mask_3ch
329
+
330
+ # Flip vertically for reflection
331
+ reflection = cv2.flip(object_only, 0)
332
+
333
+ # Create gradient for fade-out
334
+ gradient = np.linspace(reflection_strength, 0, h)
335
+ gradient = np.repeat(gradient[:, np.newaxis], w, axis=1)
336
+ gradient = np.repeat(gradient[:, :, np.newaxis], 3, axis=2)
337
+
338
+ # Apply gradient to reflection
339
+ reflection = reflection * gradient
340
+
341
+ # Add slight blur for realism
342
+ reflection = cv2.GaussianBlur(reflection, (5, 5), 2)
343
+
344
+ # Composite reflection below object
345
+ result = image.copy()
346
+ result = result + reflection
347
+
348
+ return np.clip(result, 0, 255).astype(np.uint8)
349
+
350
+ def add_glow(self, image: np.ndarray,
351
+ mask: Optional[np.ndarray] = None,
352
+ color: Optional[Tuple[int, int, int]] = None) -> np.ndarray:
353
+ """
354
+ Add glow effect to image or masked region.
355
+
356
+ Args:
357
+ image: Input image
358
+ mask: Optional mask for selective glow
359
+ color: Glow color (BGR)
360
+
361
+ Returns:
362
+ Image with glow effect
363
+ """
364
+ if color is None:
365
+ color = (255, 255, 255) # White glow
366
+
367
+ # Create glow source
368
+ if mask is not None:
369
+ if len(mask.shape) == 2:
370
+ glow_source = np.zeros_like(image)
371
+ for i in range(3):
372
+ glow_source[:, :, i] = mask * (color[i] / 255.0)
373
+ else:
374
+ glow_source = mask * np.array(color).reshape(1, 1, 3) / 255.0
375
+ else:
376
+ # Use bright parts of image
377
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
378
+ _, bright_mask = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY)
379
+ glow_source = cv2.cvtColor(bright_mask, cv2.COLOR_GRAY2BGR)
380
+
381
+ # Create multiple blur levels for glow
382
+ glow = np.zeros_like(image, dtype=np.float32)
383
+
384
+ for i in range(1, 4):
385
+ blur_size = self.config.glow_radius * i
386
+ kernel_size = blur_size * 2 + 1
387
+
388
+ blurred = cv2.GaussianBlur(glow_source, (kernel_size, kernel_size), blur_size)
389
+ glow += blurred / (i * 2)
390
+
391
+ # Normalize and apply intensity
392
+ if glow.max() > 0:
393
+ glow = glow / glow.max()
394
+ glow *= self.config.glow_intensity * 255
395
+
396
+ # Add glow to original image
397
+ result = image.astype(np.float32) + glow
398
+
399
+ return np.clip(result, 0, 255).astype(np.uint8)
400
+
401
+ def chromatic_aberration(self, image: np.ndarray,
402
+ shift: Optional[float] = None) -> np.ndarray:
403
+ """
404
+ Apply chromatic aberration effect.
405
+
406
+ Args:
407
+ image: Input image
408
+ shift: Pixel shift amount
409
+
410
+ Returns:
411
+ Image with chromatic aberration
412
+ """
413
+ shift = shift or self.config.chromatic_shift
414
+ h, w = image.shape[:2]
415
+
416
+ # Split channels
417
+ b, g, r = cv2.split(image)
418
+
419
+ # Create radial shift
420
+ center_x, center_y = w // 2, h // 2
421
+
422
+ # Shift red channel outward
423
+ M_r = np.float32([[1 + shift/w, 0, -shift], [0, 1 + shift/h, -shift]])
424
+ r_shifted = cv2.warpAffine(r, M_r, (w, h))
425
+
426
+ # Shift blue channel inward
427
+ M_b = np.float32([[1 - shift/w, 0, shift], [0, 1 - shift/h, shift]])
428
+ b_shifted = cv2.warpAffine(b, M_b, (w, h))
429
+
430
+ # Merge channels
431
+ result = cv2.merge([b_shifted, g, r_shifted])
432
+
433
+ return result
434
+
435
+ def add_vignette(self, image: np.ndarray,
436
+ strength: Optional[float] = None) -> np.ndarray:
437
+ """
438
+ Add vignette effect to image.
439
+
440
+ Args:
441
+ image: Input image
442
+ strength: Vignette strength (0-1)
443
+
444
+ Returns:
445
+ Image with vignette
446
+ """
447
+ strength = strength or self.config.vignette_strength
448
+ h, w = image.shape[:2]
449
+
450
+ # Create radial gradient
451
+ center_x, center_y = w // 2, h // 2
452
+ Y, X = np.ogrid[:h, :w]
453
+
454
+ # Calculate distance from center
455
+ dist = np.sqrt((X - center_x)**2 + (Y - center_y)**2)
456
+ max_dist = np.sqrt(center_x**2 + center_y**2)
457
+
458
+ # Normalize and create vignette mask
459
+ vignette = 1 - (dist / max_dist) * strength
460
+ vignette = np.clip(vignette, 0, 1)
461
+
462
+ # Apply vignette
463
+ vignette_3ch = np.repeat(vignette[:, :, np.newaxis], 3, axis=2)
464
+ result = image * vignette_3ch
465
+
466
+ return np.clip(result, 0, 255).astype(np.uint8)
467
+
468
+ def add_film_grain(self, image: np.ndarray,
469
+ intensity: Optional[float] = None) -> np.ndarray:
470
+ """
471
+ Add film grain effect to image.
472
+
473
+ Args:
474
+ image: Input image
475
+ intensity: Grain intensity
476
+
477
+ Returns:
478
+ Image with film grain
479
+ """
480
+ intensity = intensity or self.config.grain_intensity
481
+
482
+ # Generate grain
483
+ h, w = image.shape[:2]
484
+ grain = np.random.randn(h, w, 3) * intensity * 255
485
+
486
+ # Add grain to image
487
+ result = image.astype(np.float32) + grain
488
+
489
+ return np.clip(result, 0, 255).astype(np.uint8)
490
+
491
+ def motion_blur(self, image: np.ndarray,
492
+ angle: Optional[float] = None,
493
+ size: Optional[int] = None) -> np.ndarray:
494
+ """
495
+ Apply directional motion blur.
496
+
497
+ Args:
498
+ image: Input image
499
+ angle: Blur angle in degrees
500
+ size: Blur kernel size
501
+
502
+ Returns:
503
+ Motion blurred image
504
+ """
505
+ angle = angle or self.config.motion_blur_angle
506
+ size = size or self.config.motion_blur_size
507
+
508
+ # Create motion blur kernel
509
+ kernel = np.zeros((size, size))
510
+ kernel[int((size-1)/2), :] = np.ones(size)
511
+ kernel = kernel / size
512
+
513
+ # Rotate kernel
514
+ M = cv2.getRotationMatrix2D((size/2, size/2), angle, 1)
515
+ kernel = cv2.warpAffine(kernel, M, (size, size))
516
+
517
+ # Apply kernel
518
+ result = cv2.filter2D(image, -1, kernel)
519
+
520
+ return result
521
+
522
+
523
+ class CompositeEffects:
524
+ """Advanced compositing effects."""
525
+
526
+ def __init__(self):
527
+ self.logger = setup_logger(f"{__name__}.CompositeEffects")
528
+ self.bg_effects = BackgroundEffects()
529
+
530
+ def smart_composite(self, foreground: np.ndarray,
531
+ background: np.ndarray,
532
+ mask: np.ndarray,
533
+ effects: List[EffectType]) -> np.ndarray:
534
+ """
535
+ Apply smart compositing with multiple effects.
536
+
537
+ Args:
538
+ foreground: Foreground image
539
+ background: Background image
540
+ mask: Alpha mask
541
+ effects: List of effects to apply
542
+
543
+ Returns:
544
+ Composited image with effects
545
+ """
546
+ result = background.copy()
547
+
548
+ # Ensure mask is proper format
549
+ if len(mask.shape) == 2:
550
+ mask_3ch = np.repeat(mask[:, :, np.newaxis], 3, axis=2)
551
+ else:
552
+ mask_3ch = mask
553
+
554
+ if mask_3ch.max() > 1:
555
+ mask_3ch = mask_3ch / 255.0
556
+
557
+ # Apply background effects
558
+ for effect in effects:
559
+ if effect == EffectType.BLUR:
560
+ result = self.bg_effects.apply_blur(result, mask=1-mask_3ch[:,:,0])
561
+ elif effect == EffectType.BOKEH:
562
+ result = self.bg_effects.apply_bokeh(result)
563
+ elif effect == EffectType.VIGNETTE:
564
+ result = self.bg_effects.add_vignette(result)
565
+
566
+ # Apply light wrap before compositing
567
+ if EffectType.LIGHT_WRAP in effects:
568
+ foreground = self.bg_effects.apply_light_wrap(
569
+ foreground, result, mask_3ch[:,:,0]
570
+ )
571
+
572
+ # Composite foreground
573
+ result = result * (1 - mask_3ch) + foreground * mask_3ch
574
+ result = result.astype(np.uint8)
575
+
576
+ # Apply post-composite effects
577
+ if EffectType.SHADOW in effects:
578
+ result = self.bg_effects.add_shadow(result, mask_3ch[:,:,0])
579
+
580
+ if EffectType.REFLECTION in effects:
581
+ result = self.bg_effects.add_reflection(result, mask_3ch[:,:,0])
582
+
583
+ if EffectType.GLOW in effects:
584
+ result = self.bg_effects.add_glow(result, mask_3ch[:,:,0])
585
+
586
+ # Apply final touches
587
+ if EffectType.CHROMATIC_ABERRATION in effects:
588
+ result = self.bg_effects.chromatic_aberration(result)
589
+
590
+ if EffectType.FILM_GRAIN in effects:
591
+ result = self.bg_effects.add_film_grain(result)
592
+
593
+ return result
594
+
595
+ def color_harmonization(self, foreground: np.ndarray,
596
+ background: np.ndarray,
597
+ mask: np.ndarray,
598
+ strength: float = 0.3) -> np.ndarray:
599
+ """
600
+ Harmonize colors between foreground and background.
601
+
602
+ Args:
603
+ foreground: Foreground image
604
+ background: Background image
605
+ mask: Foreground mask
606
+ strength: Harmonization strength
607
+
608
+ Returns:
609
+ Color-harmonized foreground
610
+ """
611
+ # Calculate background color statistics
612
+ bg_mean = np.mean(background, axis=(0, 1))
613
+ bg_std = np.std(background, axis=(0, 1))
614
+
615
+ # Calculate foreground color statistics
616
+ fg_mean = np.mean(foreground, axis=(0, 1))
617
+ fg_std = np.std(foreground, axis=(0, 1))
618
+
619
+ # Adjust foreground colors
620
+ result = foreground.astype(np.float32)
621
+
622
+ for i in range(3): # For each color channel
623
+ # Normalize foreground
624
+ result[:, :, i] = (result[:, :, i] - fg_mean[i]) / (fg_std[i] + 1e-6)
625
+
626
+ # Apply background statistics
627
+ result[:, :, i] = result[:, :, i] * (bg_std[i] * strength + fg_std[i] * (1 - strength))
628
+ result[:, :, i] += bg_mean[i] * strength + fg_mean[i] * (1 - strength)
629
+
630
+ return np.clip(result, 0, 255).astype(np.uint8)