supli6669 commited on
Commit
c2bec22
·
1 Parent(s): eeaf8b6

feat: implement custom enhancement pipeline with adjustable soft blending

Browse files
Files changed (3) hide show
  1. handover.md +21 -1
  2. pipeline.py +228 -0
  3. test_pipeline.py +63 -0
handover.md CHANGED
@@ -37,6 +37,26 @@
37
  - [NEW] [verify_imports.py](file:///C:/Users/admin/.gemini/antigravity-ide/scratch/custom-ai-enhancer/verify_imports.py) (Path and import verification test)
38
 
39
  ### Git Commit & Push Status
40
- - **Files Staged:** CodeFormer setup and model weights downloader files.
41
  - **Commit Message:** "feat: clone CodeFormer, download pretrained weights, and verify imports"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  - **Remote Push:** Scheduled for execution.
 
37
  - [NEW] [verify_imports.py](file:///C:/Users/admin/.gemini/antigravity-ide/scratch/custom-ai-enhancer/verify_imports.py) (Path and import verification test)
38
 
39
  ### Git Commit & Push Status
 
40
  - **Commit Message:** "feat: clone CodeFormer, download pretrained weights, and verify imports"
41
+ - **Remote Push:** Completed.
42
+
43
+ ---
44
+
45
+ ## Task 3: Build Custom Hybrid Pipeline
46
+
47
+ ### Completed Operations
48
+ - Created `pipeline.py` implementing the `LocalAIEnhancerPipeline` class.
49
+ - Configured OpenCV image reading, loading `FaceRestoreHelper` for face landmarks detection and warping/cropping.
50
+ - Passed warped face crops through the local CodeFormer model with customizable fidelity parameter ($w$) using PyTorch.
51
+ - Designed a custom face pasting function (`paste_faces_custom_blend`) that exposes a `blend_softness` (0.0 to 1.0) parameter. This dynamically modifies the erosion radius and Gaussian blur size applied to the face boundary mask for seamless blending back into the upscaled background image.
52
+ - Combined the soft edge boundary mask with CodeFormer's PyTorch face features parsing segmentation mask to prevent blending artifacts.
53
+ - Created `test_pipeline.py` which runs the entire pipeline on a local sample image, verifies the upscaled dimensions, and saves the output to `test_output.png`. Tested successfully.
54
+
55
+ ### Code Changes
56
+ - [NEW] [pipeline.py](file:///C:/Users/admin/.gemini/antigravity-ide/scratch/custom-ai-enhancer/pipeline.py) (Main processing pipeline with customizable fidelity and soft blending mask)
57
+ - [NEW] [test_pipeline.py](file:///C:/Users/admin/.gemini/antigravity-ide/scratch/custom-ai-enhancer/test_pipeline.py) (Verification test for the custom pipeline)
58
+
59
+ ### Git Commit & Push Status
60
+ - **Files Staged:** `pipeline.py`, `test_pipeline.py`, `handover.md`
61
+ - **Commit Message:** "feat: implement custom enhancement pipeline with adjustable soft blending"
62
  - **Remote Push:** Scheduled for execution.
pipeline.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import cv2
4
+ import numpy as np
5
+ import torch
6
+ from torchvision.transforms.functional import normalize
7
+
8
+ # Ensure CodeFormer directory is on sys.path
9
+ project_dir = os.path.dirname(os.path.abspath(__file__))
10
+ codeformer_dir = os.path.join(project_dir, "models", "CodeFormer")
11
+ if codeformer_dir not in sys.path:
12
+ sys.path.insert(0, codeformer_dir)
13
+
14
+ from basicsr.utils import img2tensor, tensor2img
15
+ from basicsr.utils.registry import ARCH_REGISTRY
16
+ from facelib.utils.face_restoration_helper import FaceRestoreHelper
17
+
18
+ class LocalAIEnhancerPipeline:
19
+ def __init__(self, device=None):
20
+ """Initialize the CodeFormer model and helper pipeline."""
21
+ if device is None:
22
+ self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
23
+ else:
24
+ self.device = torch.device(device)
25
+
26
+ print(f"[Pipeline] Initializing pipeline on device: {self.device}")
27
+
28
+ # Load CodeFormer network architecture
29
+ self.net = ARCH_REGISTRY.get('CodeFormer')(
30
+ dim_embd=512,
31
+ codebook_size=1024,
32
+ n_head=8,
33
+ n_layers=9,
34
+ connect_list=['32', '64', '128', '256']
35
+ ).to(self.device)
36
+
37
+ # Load weights
38
+ weights_path = os.path.join(project_dir, "weights", "CodeFormer", "codeformer.pth")
39
+ if not os.path.exists(weights_path):
40
+ raise FileNotFoundError(f"CodeFormer weights not found at {weights_path}. Please run download_weights.py first.")
41
+
42
+ print(f"[Pipeline] Loading weights from {weights_path}...")
43
+ checkpoint = torch.load(weights_path, map_location=self.device)
44
+ if 'params_ema' in checkpoint:
45
+ self.net.load_state_dict(checkpoint['params_ema'])
46
+ else:
47
+ self.net.load_state_dict(checkpoint['params'])
48
+ self.net.eval()
49
+ print("[Pipeline] CodeFormer model loaded successfully.")
50
+
51
+ def process_image(self, img, w=0.5, detection_model='retinaface_resnet50', upscale=2, blend_softness=0.5):
52
+ """
53
+ Enhance an image using the local CodeFormer pipeline.
54
+
55
+ Args:
56
+ img (numpy.ndarray): Input image in BGR format (OpenCV default).
57
+ w (float): Fidelity weight (0.0 to 1.0). 0.0 for max quality, 1.0 for max fidelity.
58
+ detection_model (str): Face detector model ('retinaface_resnet50', 'YOLOv5l', etc.).
59
+ upscale (int): Upscale factor for output image.
60
+ blend_softness (float): Blending mask softness (0.0 to 1.0).
61
+
62
+ Returns:
63
+ numpy.ndarray: Enhanced output image in BGR format.
64
+ """
65
+ # Set up FaceRestoreHelper
66
+ # We tell it where the facelib weights are (under project weights/facelib)
67
+ os.environ['FACE_DETECTOR_PATH'] = os.path.join(project_dir, "weights", "facelib")
68
+
69
+ face_helper = FaceRestoreHelper(
70
+ upscale,
71
+ face_size=512,
72
+ crop_ratio=(1, 1),
73
+ det_model=detection_model,
74
+ save_ext='png',
75
+ use_parse=True,
76
+ device=self.device
77
+ )
78
+
79
+ face_helper.clean_all()
80
+ face_helper.read_image(img)
81
+
82
+ # 1. Detect face landmarks and align/crop faces
83
+ print(f"[Pipeline] Running face detection model: {detection_model}...")
84
+ num_det_faces = face_helper.get_face_landmarks_5(
85
+ only_center_face=False,
86
+ resize=640,
87
+ eye_dist_threshold=5
88
+ )
89
+ print(f"[Pipeline] Detected {num_det_faces} faces.")
90
+
91
+ if num_det_faces == 0:
92
+ # Return resized background if no faces are detected
93
+ h, w_img, _ = img.shape
94
+ return cv2.resize(img, (w_img * upscale, h * upscale), interpolation=cv2.INTER_LINEAR)
95
+
96
+ face_helper.align_warp_face()
97
+
98
+ # 2. Process each cropped face through CodeFormer
99
+ for idx, cropped_face in enumerate(face_helper.cropped_faces):
100
+ cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
101
+ normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
102
+ cropped_face_t = cropped_face_t.unsqueeze(0).to(self.device)
103
+
104
+ try:
105
+ with torch.no_grad():
106
+ # Process with fidelity weight w
107
+ output = self.net(cropped_face_t, w=w, adain=True)[0]
108
+ restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
109
+ del output
110
+ except Exception as error:
111
+ print(f"[Pipeline] Failed CodeFormer inference for face index {idx}: {error}")
112
+ restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
113
+
114
+ restored_face = restored_face.astype('uint8')
115
+ face_helper.add_restored_face(restored_face, cropped_face)
116
+
117
+ # 3. Paste restored faces back into input image with custom soft blending
118
+ print(f"[Pipeline] Seamlessly pasting {len(face_helper.restored_faces)} restored faces back...")
119
+ face_helper.get_inverse_affine(None)
120
+
121
+ enhanced_img = self.paste_faces_custom_blend(
122
+ face_helper,
123
+ upscale=upscale,
124
+ blend_softness=blend_softness
125
+ )
126
+ return enhanced_img
127
+
128
+ def paste_faces_custom_blend(self, face_helper, upscale, blend_softness):
129
+ """Custom implementation of face pasting with adjustable soft blending mask."""
130
+ h, w, _ = face_helper.input_img.shape
131
+ h_up, w_up = int(h * upscale), int(w * upscale)
132
+
133
+ # Initialize background image (upsampled background)
134
+ upsample_img = cv2.resize(face_helper.input_img, (w_up, h_up), interpolation=cv2.INTER_LINEAR)
135
+
136
+ for restored_face, inverse_affine in zip(face_helper.restored_faces, face_helper.inverse_affine_matrices):
137
+ # Alignment offset
138
+ if upscale > 1:
139
+ extra_offset = 0.5 * upscale
140
+ else:
141
+ extra_offset = 0
142
+
143
+ # Create a local copy to avoid modifying original matrices
144
+ inv_aff = inverse_affine.copy()
145
+ inv_aff[:, 2] += extra_offset
146
+
147
+ face_size = face_helper.face_size
148
+ inv_restored = cv2.warpAffine(restored_face, inv_aff, (w_up, h_up))
149
+
150
+ # Create boundary mask
151
+ mask = np.ones(face_size, dtype=np.float32)
152
+ inv_mask = cv2.warpAffine(mask, inv_aff, (w_up, h_up))
153
+
154
+ # Erode slightly to remove absolute boundary black edges
155
+ erosion_size = max(1, int(2 * upscale))
156
+ inv_mask_erosion = cv2.erode(
157
+ inv_mask,
158
+ np.ones((erosion_size, erosion_size), np.uint8)
159
+ )
160
+
161
+ pasted_face = inv_mask_erosion[:, :, None] * inv_restored
162
+ total_face_area = np.sum(inv_mask_erosion)
163
+
164
+ # --- CUSTOM ADJUSTABLE SOFT MASK BLENDING ---
165
+ # Default CodeFormer edge is total_face_area**0.5 / 20. We scale it with blend_softness.
166
+ base_edge = int(total_face_area ** 0.5) // 20
167
+
168
+ # Map blend_softness (0.0 - 1.0) to actual feather radius
169
+ # 0.0 -> very small feathering (harder edge, raw paste)
170
+ # 0.5 -> standard CodeFormer feathering
171
+ # 1.0 -> double size feathering (extra soft blend)
172
+ feather_radius = max(1, int(base_edge * 2 * blend_softness))
173
+
174
+ # Additional erosion to pull the mask inside the face region
175
+ inv_mask_center = cv2.erode(
176
+ inv_mask_erosion,
177
+ np.ones((feather_radius, feather_radius), np.uint8)
178
+ )
179
+
180
+ # Blur the core mask to create the soft gradient
181
+ blur_size = feather_radius * 2
182
+ if blur_size % 2 == 0:
183
+ blur_size += 1
184
+
185
+ inv_soft_mask = cv2.GaussianBlur(inv_mask_center, (blur_size, blur_size), 0)
186
+ inv_soft_mask = inv_soft_mask[:, :, None]
187
+
188
+ # Apply parsing mask (if segmenter is available and loaded)
189
+ if face_helper.use_parse and hasattr(face_helper, 'face_parse'):
190
+ face_input = cv2.resize(restored_face, (512, 512), interpolation=cv2.INTER_LINEAR)
191
+ face_input = img2tensor(face_input.astype('float32') / 255.0, bgr2rgb=True, float32=True)
192
+ normalize(face_input, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
193
+ face_input = torch.unsqueeze(face_input, 0).to(face_helper.device)
194
+
195
+ with torch.no_grad():
196
+ out = face_helper.face_parse(face_input)[0]
197
+ out = out.argmax(dim=1).squeeze().cpu().numpy()
198
+
199
+ parse_mask = np.zeros(out.shape)
200
+ MASK_COLORMAP = [0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 255, 0, 0, 0]
201
+ for p_idx, color in enumerate(MASK_COLORMAP):
202
+ parse_mask[out == p_idx] = color
203
+
204
+ # Double Gaussian blur on parse mask
205
+ parse_mask = cv2.GaussianBlur(parse_mask, (101, 101), 11)
206
+ parse_mask = cv2.GaussianBlur(parse_mask, (101, 101), 11)
207
+
208
+ # Remove black border artifacts
209
+ thres = 10
210
+ parse_mask[:thres, :] = 0
211
+ parse_mask[-thres:, :] = 0
212
+ parse_mask[:, :thres] = 0
213
+ parse_mask[:, -thres:] = 0
214
+ parse_mask = parse_mask / 255.0
215
+
216
+ parse_mask = cv2.resize(parse_mask, face_size)
217
+ parse_mask = cv2.warpAffine(parse_mask, inv_aff, (w_up, h_up), flags=3)
218
+ inv_soft_parse_mask = parse_mask[:, :, None]
219
+
220
+ # Intersect soft boundary mask with face feature parsing mask
221
+ fuse_mask = (inv_soft_parse_mask < inv_soft_mask).astype('int')
222
+ inv_soft_mask = inv_soft_parse_mask * fuse_mask + inv_soft_mask * (1 - fuse_mask)
223
+
224
+ # Merge restored face onto the background
225
+ upsample_img = inv_soft_mask * pasted_face + (1 - inv_soft_mask) * upsample_img
226
+
227
+ upsample_img = np.clip(upsample_img, 0, 255).astype(np.uint8)
228
+ return upsample_img
test_pipeline.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import sys
4
+ from pipeline import LocalAIEnhancerPipeline
5
+
6
+ def main():
7
+ project_dir = os.path.dirname(os.path.abspath(__file__))
8
+ input_path = os.path.join(project_dir, "models", "CodeFormer", "inputs", "whole_imgs", "00.jpg")
9
+ output_path = os.path.join(project_dir, "test_output.png")
10
+
11
+ print(f"Loading input image: {input_path}")
12
+ if not os.path.exists(input_path):
13
+ print(f"Error: Test image not found at {input_path}")
14
+ sys.exit(1)
15
+
16
+ img = cv2.imread(input_path)
17
+ if img is None:
18
+ print("Error: Could not read image.")
19
+ sys.exit(1)
20
+
21
+ print(f"Input image shape: {img.shape}")
22
+
23
+ try:
24
+ # Initialize pipeline
25
+ pipeline = LocalAIEnhancerPipeline(device='cpu') # Use CPU to run stably on test env
26
+
27
+ # Test enhancement with w = 0.5 and blend_softness = 0.5
28
+ print("Processing image with w=0.5 and blend_softness=0.5...")
29
+ upscale = 2
30
+ enhanced_img = pipeline.process_image(
31
+ img,
32
+ w=0.5,
33
+ detection_model='retinaface_resnet50',
34
+ upscale=upscale,
35
+ blend_softness=0.5
36
+ )
37
+
38
+ print(f"Enhanced image shape: {enhanced_img.shape}")
39
+
40
+ # Save output image
41
+ cv2.imwrite(output_path, enhanced_img)
42
+ print(f"Saved enhanced image to: {output_path}")
43
+
44
+ # Calculate expected output dimensions (using round to match OpenCV's resizing)
45
+ min_dim = min(img.shape[:2])
46
+ scale_factor = 512.0 / min_dim if min_dim < 512 else 1.0
47
+ expected_h = int(round(img.shape[0] * scale_factor)) * upscale
48
+ expected_w = int(round(img.shape[1] * scale_factor)) * upscale
49
+
50
+ print(f"Expected output shape: ({expected_h}, {expected_w}, 3)")
51
+ assert enhanced_img.shape[0] == expected_h, f"Output height mismatch: got {enhanced_img.shape[0]}, expected {expected_h}"
52
+ assert enhanced_img.shape[1] == expected_w, f"Output width mismatch: got {enhanced_img.shape[1]}, expected {expected_w}"
53
+
54
+ print("\nSUCCESS: Custom hybrid pipeline tested successfully.")
55
+
56
+ except Exception as e:
57
+ print(f"\nFAILURE: Error during pipeline test: {e}")
58
+ import traceback
59
+ traceback.print_exc()
60
+ sys.exit(1)
61
+
62
+ if __name__ == "__main__":
63
+ main()