supli6669 commited on
Commit ·
c2bec22
1
Parent(s): eeaf8b6
feat: implement custom enhancement pipeline with adjustable soft blending
Browse files- handover.md +21 -1
- pipeline.py +228 -0
- 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()
|