MogensR commited on
Commit
e9f947a
·
1 Parent(s): 5d491c9

Create core/edge.py

Browse files
Files changed (1) hide show
  1. core/edge.py +555 -0
core/edge.py ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Edge processing and symmetry correction for BackgroundFX Pro.
3
+ Fixes hair segmentation asymmetry and improves edge quality.
4
+ """
5
+
6
+ import numpy as np
7
+ import cv2
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from typing import Dict, List, Optional, Tuple, Any
11
+ from dataclasses import dataclass
12
+ from scipy import ndimage, signal
13
+ from scipy.spatial import distance
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class EdgeConfig:
21
+ """Configuration for edge processing."""
22
+ edge_thickness: int = 3
23
+ smoothing_iterations: int = 2
24
+ symmetry_threshold: float = 0.3
25
+ hair_detection_sensitivity: float = 0.7
26
+ refinement_radius: int = 5
27
+ use_guided_filter: bool = True
28
+ bilateral_d: int = 9
29
+ bilateral_sigma_color: float = 75
30
+ bilateral_sigma_space: float = 75
31
+ morphology_kernel_size: int = 5
32
+ edge_preservation_weight: float = 0.8
33
+
34
+
35
+ class EdgeProcessor:
36
+ """Main edge processing and refinement system."""
37
+
38
+ def __init__(self, config: Optional[EdgeConfig] = None):
39
+ self.config = config or EdgeConfig()
40
+ self.hair_segmentation = HairSegmentation(config)
41
+ self.edge_refinement = EdgeRefinement(config)
42
+ self.symmetry_corrector = SymmetryCorrector(config)
43
+
44
+ def process(self, image: np.ndarray, mask: np.ndarray,
45
+ detect_hair: bool = True) -> np.ndarray:
46
+ """Process edges with full pipeline."""
47
+ # 1. Initial edge detection
48
+ edges = self._detect_edges(mask)
49
+
50
+ # 2. Hair-specific processing
51
+ if detect_hair:
52
+ hair_mask = self.hair_segmentation.segment(image, mask)
53
+ mask = self._blend_hair_mask(mask, hair_mask)
54
+
55
+ # 3. Symmetry correction
56
+ mask = self.symmetry_corrector.correct(mask, image)
57
+
58
+ # 4. Edge refinement
59
+ mask = self.edge_refinement.refine(image, mask, edges)
60
+
61
+ # 5. Final smoothing
62
+ mask = self._final_smoothing(mask)
63
+
64
+ return mask
65
+
66
+ def _detect_edges(self, mask: np.ndarray) -> np.ndarray:
67
+ """Detect edges in mask."""
68
+ # Convert to uint8
69
+ mask_uint8 = (mask * 255).astype(np.uint8)
70
+
71
+ # Multi-scale edge detection
72
+ edges1 = cv2.Canny(mask_uint8, 50, 150)
73
+ edges2 = cv2.Canny(mask_uint8, 30, 100)
74
+ edges3 = cv2.Canny(mask_uint8, 70, 200)
75
+
76
+ # Combine edges
77
+ edges = np.maximum(edges1, np.maximum(edges2, edges3))
78
+
79
+ return edges / 255.0
80
+
81
+ def _blend_hair_mask(self, original_mask: np.ndarray,
82
+ hair_mask: np.ndarray) -> np.ndarray:
83
+ """Blend hair mask with original mask."""
84
+ # Find hair regions
85
+ hair_regions = hair_mask > 0.5
86
+
87
+ # Smooth blending
88
+ alpha = 0.7 # Hair mask weight
89
+ blended = original_mask.copy()
90
+ blended[hair_regions] = (
91
+ alpha * hair_mask[hair_regions] +
92
+ (1 - alpha) * original_mask[hair_regions]
93
+ )
94
+
95
+ return blended
96
+
97
+ def _final_smoothing(self, mask: np.ndarray) -> np.ndarray:
98
+ """Apply final smoothing pass."""
99
+ # Guided filter for edge-preserving smoothing
100
+ if self.config.use_guided_filter:
101
+ mask = self._guided_filter(mask, mask)
102
+
103
+ # Morphological smoothing
104
+ kernel = cv2.getStructuringElement(
105
+ cv2.MORPH_ELLIPSE,
106
+ (self.config.morphology_kernel_size, self.config.morphology_kernel_size)
107
+ )
108
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
109
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
110
+
111
+ return mask
112
+
113
+ def _guided_filter(self, input_img: np.ndarray,
114
+ guidance: np.ndarray,
115
+ radius: int = 4,
116
+ epsilon: float = 0.2**2) -> np.ndarray:
117
+ """Apply guided filter for edge-preserving smoothing."""
118
+ # Implementation of guided filter
119
+ mean_I = cv2.boxFilter(guidance, cv2.CV_64F, (radius, radius))
120
+ mean_p = cv2.boxFilter(input_img, cv2.CV_64F, (radius, radius))
121
+ mean_Ip = cv2.boxFilter(guidance * input_img, cv2.CV_64F, (radius, radius))
122
+ cov_Ip = mean_Ip - mean_I * mean_p
123
+
124
+ mean_II = cv2.boxFilter(guidance * guidance, cv2.CV_64F, (radius, radius))
125
+ var_I = mean_II - mean_I * mean_I
126
+
127
+ a = cov_Ip / (var_I + epsilon)
128
+ b = mean_p - a * mean_I
129
+
130
+ mean_a = cv2.boxFilter(a, cv2.CV_64F, (radius, radius))
131
+ mean_b = cv2.boxFilter(b, cv2.CV_64F, (radius, radius))
132
+
133
+ q = mean_a * guidance + mean_b
134
+
135
+ return q
136
+
137
+
138
+ class HairSegmentation:
139
+ """Specialized hair segmentation module."""
140
+
141
+ def __init__(self, config: EdgeConfig):
142
+ self.config = config
143
+ self.hair_detector = HairDetector()
144
+
145
+ def segment(self, image: np.ndarray, initial_mask: np.ndarray) -> np.ndarray:
146
+ """Segment hair regions with improved accuracy."""
147
+ # 1. Detect hair regions
148
+ hair_probability = self.hair_detector.detect(image)
149
+
150
+ # 2. Refine with initial mask
151
+ hair_mask = self._refine_with_mask(hair_probability, initial_mask)
152
+
153
+ # 3. Fix asymmetry specific to hair
154
+ hair_mask = self._fix_hair_asymmetry(hair_mask, image)
155
+
156
+ # 4. Enhance hair strands
157
+ hair_mask = self._enhance_hair_strands(hair_mask, image)
158
+
159
+ return hair_mask
160
+
161
+ def _refine_with_mask(self, hair_prob: np.ndarray,
162
+ initial_mask: np.ndarray) -> np.ndarray:
163
+ """Refine hair probability with initial mask."""
164
+ # Only keep hair within or near initial mask
165
+ kernel = np.ones((15, 15), np.uint8)
166
+ dilated_mask = cv2.dilate(initial_mask, kernel, iterations=2)
167
+
168
+ # Combine probabilities
169
+ refined = hair_prob * dilated_mask
170
+
171
+ # Threshold
172
+ threshold = self.config.hair_detection_sensitivity
173
+ hair_mask = (refined > threshold).astype(np.float32)
174
+
175
+ # Smooth
176
+ hair_mask = cv2.GaussianBlur(hair_mask, (5, 5), 1.0)
177
+
178
+ return hair_mask
179
+
180
+ def _fix_hair_asymmetry(self, mask: np.ndarray,
181
+ image: np.ndarray) -> np.ndarray:
182
+ """Fix asymmetry in hair segmentation."""
183
+ h, w = mask.shape[:2]
184
+ center_x = w // 2
185
+
186
+ # Split mask into left and right
187
+ left_mask = mask[:, :center_x]
188
+ right_mask = mask[:, center_x:]
189
+
190
+ # Flip right for comparison
191
+ right_flipped = np.fliplr(right_mask)
192
+
193
+ # Compute difference
194
+ if left_mask.shape[1] == right_flipped.shape[1]:
195
+ diff = np.abs(left_mask - right_flipped)
196
+ asymmetry_score = np.mean(diff)
197
+
198
+ if asymmetry_score > self.config.symmetry_threshold:
199
+ logger.info(f"Detected hair asymmetry: {asymmetry_score:.3f}")
200
+
201
+ # Balance the masks
202
+ balanced_left = 0.5 * left_mask + 0.5 * right_flipped
203
+ balanced_right = np.fliplr(0.5 * right_mask + 0.5 * np.fliplr(left_mask))
204
+
205
+ # Reconstruct
206
+ mask[:, :center_x] = balanced_left
207
+ mask[:, center_x:center_x + balanced_right.shape[1]] = balanced_right
208
+
209
+ return mask
210
+
211
+ def _enhance_hair_strands(self, mask: np.ndarray,
212
+ image: np.ndarray) -> np.ndarray:
213
+ """Enhance fine hair strands."""
214
+ # Convert image to grayscale
215
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
216
+
217
+ # Detect fine structures using Gabor filters
218
+ enhanced_mask = mask.copy()
219
+
220
+ # Multiple orientations for Gabor filters
221
+ orientations = [0, 45, 90, 135]
222
+ gabor_responses = []
223
+
224
+ for angle in orientations:
225
+ theta = np.deg2rad(angle)
226
+ kernel = cv2.getGaborKernel(
227
+ (21, 21), 4.0, theta, 10.0, 0.5, 0, ktype=cv2.CV_32F
228
+ )
229
+ filtered = cv2.filter2D(gray, cv2.CV_32F, kernel)
230
+ gabor_responses.append(np.abs(filtered))
231
+
232
+ # Combine Gabor responses
233
+ gabor_max = np.max(gabor_responses, axis=0)
234
+ gabor_normalized = gabor_max / (np.max(gabor_max) + 1e-6)
235
+
236
+ # Enhance mask in high-response areas
237
+ hair_enhancement = gabor_normalized * (1 - mask)
238
+ enhanced_mask = np.clip(mask + 0.3 * hair_enhancement, 0, 1)
239
+
240
+ return enhanced_mask
241
+
242
+
243
+ class HairDetector:
244
+ """Detects hair regions in images."""
245
+
246
+ def detect(self, image: np.ndarray) -> np.ndarray:
247
+ """Detect hair probability map."""
248
+ # Convert to appropriate color spaces
249
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
250
+ lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
251
+
252
+ # Hair color detection in HSV
253
+ hair_colors = [
254
+ # Black hair
255
+ ((0, 0, 0), (180, 255, 30)),
256
+ # Brown hair
257
+ ((10, 20, 20), (20, 255, 100)),
258
+ # Blonde hair
259
+ ((15, 30, 50), (25, 255, 200)),
260
+ # Red hair
261
+ ((0, 50, 50), (10, 255, 150)),
262
+ ]
263
+
264
+ hair_masks = []
265
+ for (lower, upper) in hair_colors:
266
+ mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
267
+ hair_masks.append(mask)
268
+
269
+ # Combine color masks
270
+ color_mask = np.max(hair_masks, axis=0) / 255.0
271
+
272
+ # Texture analysis for hair-like patterns
273
+ texture_mask = self._detect_hair_texture(image)
274
+
275
+ # Combine color and texture
276
+ hair_probability = 0.6 * color_mask + 0.4 * texture_mask
277
+
278
+ # Smooth the probability map
279
+ hair_probability = cv2.GaussianBlur(hair_probability, (7, 7), 2.0)
280
+
281
+ return hair_probability
282
+
283
+ def _detect_hair_texture(self, image: np.ndarray) -> np.ndarray:
284
+ """Detect hair-like texture patterns."""
285
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
286
+
287
+ # Compute texture features using LBP-like approach
288
+ texture_score = np.zeros_like(gray, dtype=np.float32)
289
+
290
+ # Directional derivatives
291
+ dx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
292
+ dy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
293
+
294
+ # Gradient magnitude and orientation
295
+ magnitude = np.sqrt(dx**2 + dy**2)
296
+ orientation = np.arctan2(dy, dx)
297
+
298
+ # Hair tends to have consistent local orientation
299
+ # Compute local orientation consistency
300
+ window_size = 9
301
+ kernel = np.ones((window_size, window_size)) / (window_size**2)
302
+
303
+ # Local orientation variance (low variance = consistent = hair-like)
304
+ orient_mean = cv2.filter2D(orientation, -1, kernel)
305
+ orient_sq_mean = cv2.filter2D(orientation**2, -1, kernel)
306
+ orient_var = orient_sq_mean - orient_mean**2
307
+
308
+ # Low variance and high magnitude indicates hair
309
+ texture_score = magnitude * np.exp(-orient_var)
310
+
311
+ # Normalize
312
+ texture_score = texture_score / (np.max(texture_score) + 1e-6)
313
+
314
+ return texture_score
315
+
316
+
317
+ class EdgeRefinement:
318
+ """Refines edges for better quality."""
319
+
320
+ def __init__(self, config: EdgeConfig):
321
+ self.config = config
322
+
323
+ def refine(self, image: np.ndarray, mask: np.ndarray,
324
+ edges: np.ndarray) -> np.ndarray:
325
+ """Refine mask edges."""
326
+ # 1. Bilateral filtering for edge-aware smoothing
327
+ refined = self._bilateral_smooth(mask, image)
328
+
329
+ # 2. Snap to image edges
330
+ refined = self._snap_to_edges(refined, image, edges)
331
+
332
+ # 3. Subpixel refinement
333
+ refined = self._subpixel_refinement(refined, image)
334
+
335
+ # 4. Feathering
336
+ refined = self._apply_feathering(refined)
337
+
338
+ return refined
339
+
340
+ def _bilateral_smooth(self, mask: np.ndarray,
341
+ image: np.ndarray) -> np.ndarray:
342
+ """Apply bilateral filtering for edge-aware smoothing."""
343
+ # Convert mask to uint8 for bilateral filter
344
+ mask_uint8 = (mask * 255).astype(np.uint8)
345
+
346
+ # Apply bilateral filter
347
+ smoothed = cv2.bilateralFilter(
348
+ mask_uint8,
349
+ self.config.bilateral_d,
350
+ self.config.bilateral_sigma_color,
351
+ self.config.bilateral_sigma_space
352
+ )
353
+
354
+ return smoothed / 255.0
355
+
356
+ def _snap_to_edges(self, mask: np.ndarray, image: np.ndarray,
357
+ detected_edges: np.ndarray) -> np.ndarray:
358
+ """Snap mask boundaries to image edges."""
359
+ # Detect strong edges in image
360
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
361
+ image_edges = cv2.Canny(gray, 50, 150) / 255.0
362
+
363
+ # Find mask edges
364
+ mask_edges = cv2.Canny((mask * 255).astype(np.uint8), 50, 150) / 255.0
365
+
366
+ # Distance transform from image edges
367
+ dist_transform = cv2.distanceTransform(
368
+ (1 - image_edges).astype(np.uint8),
369
+ cv2.DIST_L2, 5
370
+ )
371
+
372
+ # Snap mask edges to nearby image edges
373
+ snap_radius = self.config.refinement_radius
374
+ refined = mask.copy()
375
+
376
+ # For pixels near mask edges
377
+ edge_region = cv2.dilate(mask_edges, np.ones((5, 5))) > 0
378
+
379
+ # If close to image edge, strengthen the mask edge
380
+ close_to_image_edge = (dist_transform < snap_radius) & edge_region
381
+ refined[close_to_image_edge] = np.where(
382
+ mask[close_to_image_edge] > 0.5, 1.0, 0.0
383
+ )
384
+
385
+ return refined
386
+
387
+ def _subpixel_refinement(self, mask: np.ndarray,
388
+ image: np.ndarray) -> np.ndarray:
389
+ """Apply subpixel refinement to edges."""
390
+ # Use image gradient for subpixel accuracy
391
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
392
+
393
+ # Compute gradients
394
+ grad_x = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
395
+ grad_y = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
396
+ grad_mag = np.sqrt(grad_x**2 + grad_y**2)
397
+
398
+ # Normalize gradient
399
+ grad_mag = grad_mag / (np.max(grad_mag) + 1e-6)
400
+
401
+ # Refine mask edges based on gradient
402
+ # Strong gradients push toward binary values
403
+ refined = mask.copy()
404
+ strong_gradient = grad_mag > 0.3
405
+
406
+ refined[strong_gradient] = np.where(
407
+ mask[strong_gradient] > 0.5,
408
+ np.minimum(mask[strong_gradient] + 0.1, 1.0),
409
+ np.maximum(mask[strong_gradient] - 0.1, 0.0)
410
+ )
411
+
412
+ return refined
413
+
414
+ def _apply_feathering(self, mask: np.ndarray,
415
+ radius: int = 3) -> np.ndarray:
416
+ """Apply feathering to edges."""
417
+ # Distance transform from edges
418
+ mask_binary = (mask > 0.5).astype(np.uint8)
419
+
420
+ # Distance from outside
421
+ dist_outside = cv2.distanceTransform(
422
+ mask_binary, cv2.DIST_L2, 5
423
+ )
424
+
425
+ # Distance from inside
426
+ dist_inside = cv2.distanceTransform(
427
+ 1 - mask_binary, cv2.DIST_L2, 5
428
+ )
429
+
430
+ # Create feathering
431
+ feather_region = (dist_outside <= radius) | (dist_inside <= radius)
432
+
433
+ if np.any(feather_region):
434
+ # Smooth transition in feather region
435
+ alpha = np.zeros_like(mask)
436
+ alpha[dist_outside > radius] = 1.0
437
+ alpha[feather_region] = dist_outside[feather_region] / radius
438
+
439
+ # Blend
440
+ mask = mask * (1 - feather_region) + alpha * feather_region
441
+
442
+ return mask
443
+
444
+
445
+ class SymmetryCorrector:
446
+ """Corrects asymmetry in masks."""
447
+
448
+ def __init__(self, config: EdgeConfig):
449
+ self.config = config
450
+
451
+ def correct(self, mask: np.ndarray, image: np.ndarray) -> np.ndarray:
452
+ """Correct asymmetry in mask."""
453
+ # Detect face/object center
454
+ center = self._find_center(mask)
455
+
456
+ # Check asymmetry
457
+ asymmetry_score = self._compute_asymmetry(mask, center)
458
+
459
+ if asymmetry_score > self.config.symmetry_threshold:
460
+ logger.info(f"Correcting asymmetry: {asymmetry_score:.3f}")
461
+ mask = self._balance_mask(mask, center)
462
+
463
+ return mask
464
+
465
+ def _find_center(self, mask: np.ndarray) -> int:
466
+ """Find vertical center of object."""
467
+ # Use center of mass
468
+ mask_binary = (mask > 0.5).astype(np.uint8)
469
+
470
+ moments = cv2.moments(mask_binary)
471
+ if moments['m00'] > 0:
472
+ cx = int(moments['m10'] / moments['m00'])
473
+ return cx
474
+ else:
475
+ return mask.shape[1] // 2
476
+
477
+ def _compute_asymmetry(self, mask: np.ndarray, center: int) -> float:
478
+ """Compute asymmetry score."""
479
+ h, w = mask.shape[:2]
480
+
481
+ # Split at center
482
+ left_width = center
483
+ right_width = w - center
484
+ min_width = min(left_width, right_width)
485
+
486
+ if min_width <= 0:
487
+ return 0.0
488
+
489
+ # Compare left and right
490
+ left = mask[:, center-min_width:center]
491
+ right = mask[:, center:center+min_width]
492
+
493
+ # Flip right for comparison
494
+ right_flipped = np.fliplr(right)
495
+
496
+ # Compute difference
497
+ diff = np.abs(left - right_flipped)
498
+ asymmetry = np.mean(diff)
499
+
500
+ return asymmetry
501
+
502
+ def _balance_mask(self, mask: np.ndarray, center: int) -> np.ndarray:
503
+ """Balance mask to reduce asymmetry."""
504
+ h, w = mask.shape[:2]
505
+ balanced = mask.copy()
506
+
507
+ # Split at center
508
+ left_width = center
509
+ right_width = w - center
510
+ min_width = min(left_width, right_width)
511
+
512
+ if min_width <= 0:
513
+ return mask
514
+
515
+ # Get regions
516
+ left = mask[:, center-min_width:center]
517
+ right = mask[:, center:center+min_width]
518
+
519
+ # Weight based on confidence (higher values = more confident)
520
+ left_confidence = np.mean(np.abs(left - 0.5))
521
+ right_confidence = np.mean(np.abs(right - 0.5))
522
+
523
+ # Weighted average favoring more confident side
524
+ total_conf = left_confidence + right_confidence + 1e-6
525
+ left_weight = left_confidence / total_conf
526
+ right_weight = right_confidence / total_conf
527
+
528
+ # Balance
529
+ balanced_left = left_weight * left + right_weight * np.fliplr(right)
530
+ balanced_right = right_weight * right + left_weight * np.fliplr(left)
531
+
532
+ # Apply balanced versions
533
+ balanced[:, center-min_width:center] = balanced_left
534
+ balanced[:, center:center+min_width] = balanced_right
535
+
536
+ # Smooth the center seam
537
+ seam_width = 5
538
+ seam_start = max(0, center - seam_width)
539
+ seam_end = min(w, center + seam_width)
540
+ balanced[:, seam_start:seam_end] = cv2.GaussianBlur(
541
+ balanced[:, seam_start:seam_end], (5, 1), 1.0
542
+ )
543
+
544
+ return balanced
545
+
546
+
547
+ # Export classes
548
+ __all__ = [
549
+ 'EdgeProcessor',
550
+ 'EdgeConfig',
551
+ 'HairSegmentation',
552
+ 'EdgeRefinement',
553
+ 'SymmetryCorrector',
554
+ 'HairDetector'
555
+ ]