supli6669 commited on
Commit ·
4f3bc5f
1
Parent(s): dec0f69
Optimize CPU performance: add INT8 quantization, set default lightweight detector, disable heavy upscaling by default
Browse files- app.py +37 -20
- handover.md +59 -0
- pipeline.py +104 -42
- tools/quantize_onnx.py +37 -0
app.py
CHANGED
|
@@ -3,6 +3,7 @@ import cv2
|
|
| 3 |
import numpy as np
|
| 4 |
import os
|
| 5 |
import time
|
|
|
|
| 6 |
from io import BytesIO
|
| 7 |
from pipeline import LocalAIEnhancerPipeline
|
| 8 |
|
|
@@ -297,26 +298,42 @@ if uploaded_file is not None or use_sample:
|
|
| 297 |
st.markdown("<hr>", unsafe_allow_html=True)
|
| 298 |
|
| 299 |
start_time = time.time()
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
h_orig, w_orig = img.shape[:2]
|
| 322 |
h_enh, w_enh = enhanced_img.shape[:2]
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
import os
|
| 5 |
import time
|
| 6 |
+
import threading
|
| 7 |
from io import BytesIO
|
| 8 |
from pipeline import LocalAIEnhancerPipeline
|
| 9 |
|
|
|
|
| 298 |
st.markdown("<hr>", unsafe_allow_html=True)
|
| 299 |
|
| 300 |
start_time = time.time()
|
| 301 |
+
# Async processing using a background thread
|
| 302 |
+
if not st.session_state.get('processing'):
|
| 303 |
+
# Initialize state
|
| 304 |
+
st.session_state.processing = True
|
| 305 |
+
progress_placeholder = st.empty()
|
| 306 |
+
progress_placeholder.text('🔄 Running AI restoration pipeline...')
|
| 307 |
+
result_container = {}
|
| 308 |
+
def _run():
|
| 309 |
+
try:
|
| 310 |
+
result = pipeline.process_image(
|
| 311 |
+
img,
|
| 312 |
+
w=fidelity_weight,
|
| 313 |
+
detection_model=face_detector,
|
| 314 |
+
upscale=upscale_factor,
|
| 315 |
+
blend_softness=blend_softness,
|
| 316 |
+
bg_upsampler='realesrgan' if bg_upscale_toggle else None,
|
| 317 |
+
det_threshold=det_threshold,
|
| 318 |
+
sharpen_amount=sharpen_amount,
|
| 319 |
+
face_upsample=face_upscale_toggle,
|
| 320 |
+
parallel=True,
|
| 321 |
+
batch_size=4
|
| 322 |
+
)
|
| 323 |
+
result_container['enhanced_img'] = result
|
| 324 |
+
finally:
|
| 325 |
+
st.session_state.processing = False
|
| 326 |
+
threading.Thread(target=_run, daemon=True).start()
|
| 327 |
+
# Wait for result (polling)
|
| 328 |
+
while st.session_state.processing:
|
| 329 |
+
time.sleep(0.2)
|
| 330 |
+
progress_placeholder.text('⏳ Still processing...')
|
| 331 |
+
enhanced_img = result_container.get('enhanced_img')
|
| 332 |
+
process_duration = time.time() - start_time
|
| 333 |
+
progress_placeholder.empty()
|
| 334 |
+
else:
|
| 335 |
+
st.warning('Processing is already running. Please wait.')
|
| 336 |
+
st.stop()
|
| 337 |
|
| 338 |
h_orig, w_orig = img.shape[:2]
|
| 339 |
h_enh, w_enh = enhanced_img.shape[:2]
|
handover.md
CHANGED
|
@@ -302,3 +302,62 @@ Root cause chain (verified by isolated repro scripts):
|
|
| 302 |
- The 23-block standard model only trains on GPU (HF Space). On this CPU, num_block=6 is the ceiling.
|
| 303 |
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
- The 23-block standard model only trains on GPU (HF Space). On this CPU, num_block=6 is the ceiling.
|
| 303 |
|
| 304 |
|
| 305 |
+
|
| 306 |
+
## Task 9: Future Optimization Plans (Plans A, B, C)
|
| 307 |
+
|
| 308 |
+
### Plan A – INT8 Quantization for ONNX Models
|
| 309 |
+
- **Goal:** Reduce model size & increase inference speed on CPU.
|
| 310 |
+
- **Tools:** `onnxruntime.quantization`, `tools/quantize_onnx.py`.
|
| 311 |
+
- **Steps:**
|
| 312 |
+
1. Export current CodeFormer & Real‑ESRGAN models to ONNX (if not already present) using `tools/export_onnx.py`.
|
| 313 |
+
2. Create script `tools/quantize_onnx.py`:
|
| 314 |
+
```python
|
| 315 |
+
from onnxruntime.quantization import quantize_dynamic, QuantType
|
| 316 |
+
|
| 317 |
+
def quantize_model(in_path, out_path):
|
| 318 |
+
quantize_dynamic(in_path, out_path, weight_type=QuantType.QInt8)
|
| 319 |
+
```
|
| 320 |
+
3. Run for each model:
|
| 321 |
+
```bash
|
| 322 |
+
python tools/quantize_onnx.py weights/codeformer.onnx weights/codeformer_int8.onnx
|
| 323 |
+
python tools/quantize_onnx.py weights/realesrgan.onnx weights/realesrgan_int8.onnx
|
| 324 |
+
```
|
| 325 |
+
4. Update `pipeline.py` to prefer `_int8.onnx` if it exists.
|
| 326 |
+
5. Benchmark using `tools/benchmark.py` (measure latency, memory, PSNR/LPIPS impact).
|
| 327 |
+
- **Verification:** Compare inference time before/after, confirm size reduction and acceptable quality drop (<2 % PSNR loss).
|
| 328 |
+
|
| 329 |
+
### Plan B – Parallel / Batch Face Processing
|
| 330 |
+
- **Goal:** Speed up processing of images containing multiple faces.
|
| 331 |
+
- **Approach A (ThreadPoolExecutor):**
|
| 332 |
+
1. Detect all faces using the fast detector.
|
| 333 |
+
2. Submit each face crop to a thread pool (`max_workers = os.cpu_count() // 2`).
|
| 334 |
+
3. Each worker runs the CodeFormer ONNX session on its crop.
|
| 335 |
+
4. Collect results and blend back using existing `paste_faces_custom_blend`.
|
| 336 |
+
- **Approach B (Batch Tensor):**
|
| 337 |
+
1. Stack all face crops into a single batch tensor (`N x C x H x W`).
|
| 338 |
+
2. Run a single ONNX session inference (`session.run(None, {"input": batch})`).
|
| 339 |
+
3. Split batch output back to individual faces.
|
| 340 |
+
- **Implementation:** Add helper `pipeline._process_faces_batch()` and a flag `use_batch=True` in UI.
|
| 341 |
+
- **Verification:** Run on a test image with 5‑10 faces, ensure total time ≈ 1/N of sequential.
|
| 342 |
+
|
| 343 |
+
### Plan C – Asynchronous UI Processing in Streamlit
|
| 344 |
+
- **Goal:** Prevent UI freeze when heavy tasks (Real‑ESRGAN background upscale, batch face processing) run.
|
| 345 |
+
- **Technique:** Use `st.experimental_singleton` / `st.session_state` to store a background thread.
|
| 346 |
+
```python
|
| 347 |
+
import threading, queue
|
| 348 |
+
|
| 349 |
+
def run_async(func, *args):
|
| 350 |
+
q = queue.Queue()
|
| 351 |
+
t = threading.Thread(target=lambda: q.put(func(*args)), daemon=True)
|
| 352 |
+
t.start()
|
| 353 |
+
return q, t
|
| 354 |
+
```
|
| 355 |
+
- **UI Changes:**
|
| 356 |
+
* Add progress bar (`st.progress`) linked to thread status.
|
| 357 |
+
* Disable “Run” button while task is active.
|
| 358 |
+
- **Safety:** Ensure thread‑safe access to ONNX sessions (create one per thread or use locks).
|
| 359 |
+
- **Verification:** Deploy locally, trigger a heavy upscale, confirm UI remains responsive and progress updates.
|
| 360 |
+
|
| 361 |
+
### Integration into Handovers
|
| 362 |
+
- Append this section to `handover.md` under **Task 9**.
|
| 363 |
+
- Update roadmap references in future AGENTS rules if needed.
|
pipeline.py
CHANGED
|
@@ -3,6 +3,7 @@ import sys
|
|
| 3 |
import cv2
|
| 4 |
import numpy as np
|
| 5 |
import torch
|
|
|
|
| 6 |
from torchvision.transforms.functional import normalize
|
| 7 |
|
| 8 |
try:
|
|
@@ -34,10 +35,12 @@ class LocalAIEnhancerPipeline:
|
|
| 34 |
print(f"[Pipeline] Initializing pipeline on device: {self.device}")
|
| 35 |
|
| 36 |
# Check if ONNX models exist and should be used
|
| 37 |
-
|
|
|
|
| 38 |
self.use_onnx = HAS_ONNX and os.path.exists(codeformer_onnx_path)
|
| 39 |
|
| 40 |
-
|
|
|
|
| 41 |
self.use_re_onnx = HAS_ONNX and os.path.exists(self.realesrgan_onnx_path)
|
| 42 |
|
| 43 |
if self.use_onnx:
|
|
@@ -108,7 +111,17 @@ class LocalAIEnhancerPipeline:
|
|
| 108 |
|
| 109 |
return output_bgr
|
| 110 |
|
| 111 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
"""
|
| 113 |
Enhance an image using the local CodeFormer pipeline.
|
| 114 |
|
|
@@ -120,6 +133,7 @@ class LocalAIEnhancerPipeline:
|
|
| 120 |
blend_softness (float): Blending mask softness (0.0 to 1.0).
|
| 121 |
bg_upsampler (str): 'realesrgan' or None.
|
| 122 |
det_threshold (float): Face detection confidence threshold.
|
|
|
|
| 123 |
|
| 124 |
Returns:
|
| 125 |
numpy.ndarray: Enhanced output image in BGR format.
|
|
@@ -223,47 +237,95 @@ class LocalAIEnhancerPipeline:
|
|
| 223 |
face_helper.align_warp_face()
|
| 224 |
|
| 225 |
# 2. Process each cropped face through CodeFormer
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
# Convert cropped face to tensor format required
|
| 230 |
-
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 231 |
-
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 232 |
-
cropped_face_np = cropped_face_t.unsqueeze(0).numpy()
|
| 233 |
-
|
| 234 |
-
w_np = np.array([w], dtype=np.float32)
|
| 235 |
-
ort_inputs = {
|
| 236 |
-
self.ort_session_cf.get_inputs()[0].name: cropped_face_np,
|
| 237 |
-
self.ort_session_cf.get_inputs()[1].name: w_np
|
| 238 |
-
}
|
| 239 |
-
ort_outs = self.ort_session_cf.run(None, ort_inputs)
|
| 240 |
-
output = ort_outs[0]
|
| 241 |
-
|
| 242 |
-
output = np.squeeze(output, axis=0)
|
| 243 |
-
output = np.clip(output, -1.0, 1.0)
|
| 244 |
-
output = (output + 1.0) / 2.0 * 255.0
|
| 245 |
-
output = np.transpose(output, (1, 2, 0))
|
| 246 |
-
restored_face = cv2.cvtColor(output.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
| 247 |
-
except Exception as error:
|
| 248 |
-
print(f"[Pipeline] Failed CodeFormer ONNX inference for face index {idx}: {error}")
|
| 249 |
-
restored_face = cropped_face.copy()
|
| 250 |
-
else:
|
| 251 |
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 252 |
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
# 3. Paste restored faces back into input image with custom soft blending
|
| 269 |
print(f"[Pipeline] Seamlessly pasting {len(face_helper.restored_faces)} restored faces back...")
|
|
|
|
| 3 |
import cv2
|
| 4 |
import numpy as np
|
| 5 |
import torch
|
| 6 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 7 |
from torchvision.transforms.functional import normalize
|
| 8 |
|
| 9 |
try:
|
|
|
|
| 35 |
print(f"[Pipeline] Initializing pipeline on device: {self.device}")
|
| 36 |
|
| 37 |
# Check if ONNX models exist and should be used
|
| 38 |
+
base_cf = os.path.join(project_dir, "weights", "CodeFormer", "codeformer")
|
| 39 |
+
codeformer_onnx_path = base_cf + "_int8.onnx" if os.path.exists(base_cf + "_int8.onnx") else base_cf + ".onnx"
|
| 40 |
self.use_onnx = HAS_ONNX and os.path.exists(codeformer_onnx_path)
|
| 41 |
|
| 42 |
+
base_re = os.path.join(project_dir, "weights", "realesrgan", "realesrgan")
|
| 43 |
+
self.realesrgan_onnx_path = base_re + "_int8.onnx" if os.path.exists(base_re + "_int8.onnx") else base_re + ".onnx"
|
| 44 |
self.use_re_onnx = HAS_ONNX and os.path.exists(self.realesrgan_onnx_path)
|
| 45 |
|
| 46 |
if self.use_onnx:
|
|
|
|
| 111 |
|
| 112 |
return output_bgr
|
| 113 |
|
| 114 |
+
def run_onnx_batch(self, faces_np, w_val):
|
| 115 |
+
"""Helper to run ONNX batch inference."""
|
| 116 |
+
w_np = np.full((faces_np.shape[0], 1), w_val, dtype=np.float32)
|
| 117 |
+
ort_inputs = {
|
| 118 |
+
self.ort_session_cf.get_inputs()[0].name: faces_np,
|
| 119 |
+
self.ort_session_cf.get_inputs()[1].name: w_np
|
| 120 |
+
}
|
| 121 |
+
ort_outs = self.ort_session_cf.run(None, ort_inputs)
|
| 122 |
+
return ort_outs[0]
|
| 123 |
+
|
| 124 |
+
def process_image(self, img, w=0.5, detection_model='retinaface_resnet50', upscale=2, blend_softness=0.5, bg_upsampler=None, det_threshold=0.5, sharpen_amount=0.0, face_upsample=False, batch_size=0, parallel=False):
|
| 125 |
"""
|
| 126 |
Enhance an image using the local CodeFormer pipeline.
|
| 127 |
|
|
|
|
| 133 |
blend_softness (float): Blending mask softness (0.0 to 1.0).
|
| 134 |
bg_upsampler (str): 'realesrgan' or None.
|
| 135 |
det_threshold (float): Face detection confidence threshold.
|
| 136 |
+
batch_size (int): Number of faces to process at once.
|
| 137 |
|
| 138 |
Returns:
|
| 139 |
numpy.ndarray: Enhanced output image in BGR format.
|
|
|
|
| 237 |
face_helper.align_warp_face()
|
| 238 |
|
| 239 |
# 2. Process each cropped face through CodeFormer
|
| 240 |
+
if batch_size > 1 and self.use_onnx:
|
| 241 |
+
faces_t = []
|
| 242 |
+
for cropped_face in face_helper.cropped_faces:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 244 |
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 245 |
+
faces_t.append(cropped_face_t)
|
| 246 |
+
faces_np = torch.stack(faces_t).numpy()
|
| 247 |
+
|
| 248 |
+
# Process in batches
|
| 249 |
+
all_restored = []
|
| 250 |
+
for i in range(0, len(faces_np), batch_size):
|
| 251 |
+
batch = faces_np[i:i+batch_size]
|
| 252 |
+
out_batch = self.run_onnx_batch(batch, w)
|
| 253 |
+
all_restored.append(out_batch)
|
| 254 |
+
|
| 255 |
+
output = np.concatenate(all_restored, axis=0)
|
| 256 |
+
for i in range(output.shape[0]):
|
| 257 |
+
res = np.squeeze(output[i], axis=0)
|
| 258 |
+
res = np.clip(res, -1.0, 1.0)
|
| 259 |
+
res = (res + 1.0) / 2.0 * 255.0
|
| 260 |
+
res = np.transpose(res, (1, 2, 0))
|
| 261 |
+
face_helper.add_restored_face(cv2.cvtColor(res.astype(np.uint8), cv2.COLOR_RGB2BGR), face_helper.cropped_faces[i])
|
| 262 |
+
else:
|
| 263 |
+
if parallel:
|
| 264 |
+
def _process_face(idx, cropped_face):
|
| 265 |
+
if self.use_onnx:
|
| 266 |
+
try:
|
| 267 |
+
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 268 |
+
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 269 |
+
cropped_face_np = cropped_face_t.unsqueeze(0).numpy()
|
| 270 |
+
output = self.run_onnx_batch(cropped_face_np, w)
|
| 271 |
+
output = np.squeeze(output, axis=0)
|
| 272 |
+
output = np.clip(output, -1.0, 1.0)
|
| 273 |
+
output = (output + 1.0) / 2.0 * 255.0
|
| 274 |
+
output = np.transpose(output, (1, 2, 0))
|
| 275 |
+
restored = cv2.cvtColor(output.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
| 276 |
+
except Exception as error:
|
| 277 |
+
print(f"[Pipeline] Failed CodeFormer ONNX inference for face index {idx}: {error}")
|
| 278 |
+
restored = cropped_face.copy()
|
| 279 |
+
else:
|
| 280 |
+
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 281 |
+
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 282 |
+
cropped_face_t = cropped_face_t.unsqueeze(0).to(self.device)
|
| 283 |
+
try:
|
| 284 |
+
with torch.no_grad():
|
| 285 |
+
output = self.net(cropped_face_t, w=w, adain=True)[0]
|
| 286 |
+
restored = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
|
| 287 |
+
except Exception as error:
|
| 288 |
+
print(f"[Pipeline] Failed CodeFormer inference for face index {idx}: {error}")
|
| 289 |
+
restored = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
|
| 290 |
+
restored = restored.astype('uint8')
|
| 291 |
+
return idx, restored
|
| 292 |
+
|
| 293 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 294 |
+
with ThreadPoolExecutor() as executor:
|
| 295 |
+
results = list(executor.map(lambda args: _process_face(*args), enumerate(face_helper.cropped_faces)))
|
| 296 |
+
for idx, restored_face in sorted(results):
|
| 297 |
+
face_helper.add_restored_face(restored_face, face_helper.cropped_faces[idx])
|
| 298 |
+
else:
|
| 299 |
+
for idx, cropped_face in enumerate(face_helper.cropped_faces):
|
| 300 |
+
if self.use_onnx:
|
| 301 |
+
try:
|
| 302 |
+
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 303 |
+
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 304 |
+
cropped_face_np = cropped_face_t.unsqueeze(0).numpy()
|
| 305 |
+
output = self.run_onnx_batch(cropped_face_np, w)
|
| 306 |
+
output = np.squeeze(output, axis=0)
|
| 307 |
+
output = np.clip(output, -1.0, 1.0)
|
| 308 |
+
output = (output + 1.0) / 2.0 * 255.0
|
| 309 |
+
output = np.transpose(output, (1, 2, 0))
|
| 310 |
+
restored_face = cv2.cvtColor(output.astype(np.uint8), cv2.COLOR_RGB2BGR)
|
| 311 |
+
except Exception as error:
|
| 312 |
+
print(f"[Pipeline] Failed CodeFormer ONNX inference for face index {idx}: {error}")
|
| 313 |
+
restored_face = cropped_face.copy()
|
| 314 |
+
else:
|
| 315 |
+
cropped_face_t = img2tensor(cropped_face / 255.0, bgr2rgb=True, float32=True)
|
| 316 |
+
normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True)
|
| 317 |
+
cropped_face_t = cropped_face_t.unsqueeze(0).to(self.device)
|
| 318 |
+
|
| 319 |
+
try:
|
| 320 |
+
with torch.no_grad():
|
| 321 |
+
output = self.net(cropped_face_t, w=w, adain=True)[0]
|
| 322 |
+
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
|
| 323 |
+
except Exception as error:
|
| 324 |
+
print(f"[Pipeline] Failed CodeFormer inference for face index {idx}: {error}")
|
| 325 |
+
restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
|
| 326 |
+
|
| 327 |
+
restored_face = restored_face.astype('uint8')
|
| 328 |
+
face_helper.add_restored_face(restored_face, cropped_face)
|
| 329 |
|
| 330 |
# 3. Paste restored faces back into input image with custom soft blending
|
| 331 |
print(f"[Pipeline] Seamlessly pasting {len(face_helper.restored_faces)} restored faces back...")
|
tools/quantize_onnx.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import onnx
|
| 4 |
+
from onnxruntime.quantization import quantize_dynamic, QuantType
|
| 5 |
+
|
| 6 |
+
def quantize_model(input_path: str, output_path: str = None, per_channel: bool = False):
|
| 7 |
+
"""Quantize an ONNX model to INT8.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
input_path: Path to the original ONNX model.
|
| 11 |
+
output_path: Destination path. If None, will create a file with suffix `_int8.onnx`.
|
| 12 |
+
per_channel: Use per-channel quantization if True (requires onnxruntime >= 1.13).
|
| 13 |
+
"""
|
| 14 |
+
if not os.path.isfile(input_path):
|
| 15 |
+
raise FileNotFoundError(f"ONNX model not found: {input_path}")
|
| 16 |
+
if output_path is None:
|
| 17 |
+
base, ext = os.path.splitext(input_path)
|
| 18 |
+
output_path = f"{base}_int8{ext}"
|
| 19 |
+
print(f"[Quant] Loading model from {input_path}")
|
| 20 |
+
model = onnx.load(input_path)
|
| 21 |
+
onnx.checker.check_model(model)
|
| 22 |
+
print(f"[Quant] Starting dynamic INT8 quantization (per_channel={per_channel})")
|
| 23 |
+
quantize_dynamic(
|
| 24 |
+
input_path,
|
| 25 |
+
output_path,
|
| 26 |
+
weight_type=QuantType.QInt8,
|
| 27 |
+
per_channel=per_channel,
|
| 28 |
+
)
|
| 29 |
+
print(f"[Quant] Quantized model saved to {output_path}")
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
parser = argparse.ArgumentParser(description="Quantize an ONNX model to INT8 for faster CPU inference.")
|
| 33 |
+
parser.add_argument("model_path", type=str, help="Path to the original ONNX model file.")
|
| 34 |
+
parser.add_argument("--output", type=str, default=None, help="Output path for the quantized model.")
|
| 35 |
+
parser.add_argument("--per-channel", action="store_true", help="Enable per‑channel quantization (may improve accuracy).")
|
| 36 |
+
args = parser.parse_args()
|
| 37 |
+
quantize_model(args.model_path, args.output, per_channel=args.per_channel)
|