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
Files changed (4) hide show
  1. app.py +37 -20
  2. handover.md +59 -0
  3. pipeline.py +104 -42
  4. 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
- with st.spinner("⚡ Running AI restoration pipeline…"):
301
- if pipeline is None:
302
- st.error("AI pipeline failed to load. Check device status in sidebar.")
303
- st.stop()
304
- try:
305
- enhanced_img = pipeline.process_image(
306
- img,
307
- w=fidelity_weight,
308
- detection_model=face_detector,
309
- upscale=upscale_factor,
310
- blend_softness=blend_softness,
311
- bg_upsampler='realesrgan' if bg_upscale_toggle else None,
312
- det_threshold=det_threshold,
313
- sharpen_amount=sharpen_amount,
314
- face_upsample=face_upscale_toggle
315
- )
316
- process_duration = time.time() - start_time
317
- except Exception as e:
318
- st.error(f"Pipeline error: {e}")
319
- st.stop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- codeformer_onnx_path = os.path.join(project_dir, "weights", "CodeFormer", "codeformer.onnx")
 
38
  self.use_onnx = HAS_ONNX and os.path.exists(codeformer_onnx_path)
39
 
40
- self.realesrgan_onnx_path = os.path.join(project_dir, "weights", "realesrgan", "realesrgan.onnx")
 
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 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):
 
 
 
 
 
 
 
 
 
 
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
- for idx, cropped_face in enumerate(face_helper.cropped_faces):
227
- if self.use_onnx:
228
- try:
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
- cropped_face_t = cropped_face_t.unsqueeze(0).to(self.device)
254
-
255
- try:
256
- with torch.no_grad():
257
- # Process with fidelity weight w
258
- output = self.net(cropped_face_t, w=w, adain=True)[0]
259
- restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
260
- del output
261
- except Exception as error:
262
- print(f"[Pipeline] Failed CodeFormer inference for face index {idx}: {error}")
263
- restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
264
-
265
- restored_face = restored_face.astype('uint8')
266
- face_helper.add_restored_face(restored_face, cropped_face)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)