|
""" |
|
臉部處理核心模組 |
|
""" |
|
import cv2 |
|
import numpy as np |
|
from PIL import Image |
|
import insightface |
|
from insightface.app import FaceAnalysis |
|
import onnxruntime |
|
from pathlib import Path |
|
import urllib.request |
|
import os |
|
|
|
class FaceProcessor: |
|
"""臉部處理器,封裝了所有 AI 模型""" |
|
def __init__(self): |
|
self.swapper = None |
|
self.face_analyzer = None |
|
self._initialize_models() |
|
|
|
def _ensure_model_downloaded(self, root_dir: Path, model_name: str, url: str): |
|
""" |
|
確保指定的模型檔案存在於 models 資料夾中。 |
|
如果不存在,則從提供的 URL 下載。 |
|
""" |
|
model_dir = root_dir / 'models' |
|
model_path = model_dir / model_name |
|
|
|
model_dir.mkdir(parents=True, exist_ok=True) |
|
|
|
if model_path.exists(): |
|
print(f"INFO:core.face_processor:找到本地模型: {model_path},將跳過下載。") |
|
return |
|
|
|
print(f"INFO:core.face_processor:在本地找不到模型,將從以下 URL 下載:") |
|
print(f" -> URL: {url}") |
|
print(f" -> 目標路徑: {model_path}") |
|
|
|
try: |
|
response = urllib.request.urlopen(url) |
|
total_length = response.getheader('content-length') |
|
|
|
with open(model_path, 'wb') as f: |
|
if total_length is None: |
|
f.write(response.read()) |
|
print("INFO:core.face_processor:模型下載完成。") |
|
else: |
|
dl = 0 |
|
total_length = int(total_length) |
|
for data in response: |
|
dl += len(data) |
|
f.write(data) |
|
done = int(50 * dl / total_length) |
|
print(f"\r [ {'=' * done}{' ' * (50-done)} ] {dl * 100 / total_length:.2f}%", end='') |
|
print("\nINFO:core.face_processor:模型下載完成。") |
|
|
|
except Exception as e: |
|
print(f"\nERROR:core.face_processor:下載模型時發生錯誤。") |
|
if model_path.exists(): |
|
os.remove(model_path) |
|
print(f" 請檢查您的網路連線,或嘗試手動從以下 URL 下載模型:") |
|
print(f" {url}") |
|
print(f" 並將其放置在 '{model_path.parent}' 資料夾中。") |
|
raise RuntimeError(f"模型下載失敗: {url}") from e |
|
|
|
def _initialize_models(self): |
|
"""初始化所有需要的模型""" |
|
try: |
|
print("INFO:core.face_processor:正在初始化 AI 模型...") |
|
onnxruntime.set_default_logger_severity(3) |
|
|
|
root_dir = Path(__file__).parent.parent |
|
models_dir = root_dir / 'models' |
|
models_dir.mkdir(exist_ok=True) |
|
|
|
providers = onnxruntime.get_available_providers() |
|
if 'CUDAExecutionProvider' in providers: |
|
print("INFO:core.face_processor:檢測到 CUDA,將使用 GPU。") |
|
else: |
|
print("INFO:core.face_processor:未檢測到 CUDA,將使用 CPU。") |
|
if 'CoreMLExecutionProvider' in providers: |
|
providers.insert(0, 'CoreMLExecutionProvider') |
|
|
|
|
|
print(f"INFO:core.face_processor:正在檢查臉部分析模型 'buffalo_l'...") |
|
buffalo_l_path = models_dir / 'buffalo_l' |
|
print(f" -> 預期路徑: {buffalo_l_path}") |
|
|
|
if not buffalo_l_path.is_dir() or not any(buffalo_l_path.iterdir()): |
|
print("WARNING:core.face_processor:未在預期路徑找到 'buffalo_l' 模型資料夾,或該資料夾為空。") |
|
print(" -> 程式將嘗試從網路下載。若要手動放置,請將解壓縮後的 'buffalo_l' 資料夾完整放入 'models' 目錄中。") |
|
else: |
|
print("INFO:core.face_processor:成功找到本地 'buffalo_l' 模型資料夾。") |
|
|
|
|
|
self.face_analyzer = FaceAnalysis(name='buffalo_l', root=str(root_dir), providers=providers) |
|
self.face_analyzer.prepare(ctx_id=0, det_size=(640, 640)) |
|
|
|
|
|
model_name = "inswapper_128.onnx" |
|
model_url = "https://huggingface.co/xingren23/comfyflow-models/resolve/976de8449674de379b02c144d0b3cfa2b61482f2/insightface/inswapper_128.onnx?download=true" |
|
|
|
self._ensure_model_downloaded(root_dir, model_name, model_url) |
|
|
|
model_path = root_dir / 'models' / model_name |
|
print(f"INFO:core.face_processor:準備從本地路徑載入模型: {model_path}") |
|
|
|
self.swapper = insightface.model_zoo.get_model( |
|
str(model_path), |
|
download=False, |
|
download_zip=False |
|
) |
|
print("INFO:core.face_processor:AI 模型載入完成!") |
|
|
|
except Exception as e: |
|
print(f"ERROR:core.face_processor:模型初始化失敗:{e}") |
|
raise RuntimeError(f"無法初始化 AI 模型:{e}") from e |
|
|
|
def get_faces(self, image): |
|
"""從圖片中偵測所有臉部""" |
|
try: |
|
faces = self.face_analyzer.get(image) |
|
if faces: |
|
print(f"INFO:core.face_processor:偵測到 {len(faces)} 張臉部") |
|
else: |
|
print("WARNING:core.face_processor:在來源圖片中未偵測到任何臉部。") |
|
return faces |
|
except Exception as e: |
|
print(f"ERROR:core.face_processor:臉部偵測失敗:{e}") |
|
return [] |
|
|
|
def swap_face(self, source_img, target_img, face_index=0): |
|
""" |
|
在來源和目標圖片之間交換臉部。 |
|
:param source_img: 來源圖片 (包含要使用的臉部) |
|
:param target_img: 目標圖片 (要將臉部換到這張圖上) |
|
:param face_index: 要從來源圖片中使用的臉部索引 (預設為第 0 張) |
|
:return: 換臉後的圖片 |
|
""" |
|
try: |
|
source_faces = self.get_faces(source_img) |
|
if not source_faces: |
|
raise ValueError("在來源圖片中找不到任何臉部,無法進行換臉。") |
|
|
|
if face_index >= len(source_faces): |
|
raise ValueError(f"指定的臉部索引 {face_index} 超出範圍,來源圖片只有 {len(source_faces)} 張臉。") |
|
|
|
target_faces = self.get_faces(target_img) |
|
if not target_faces: |
|
print("WARNING:core.face_processor:在目標圖片中未偵測到臉部,將直接在原圖上操作。") |
|
|
|
|
|
|
|
source_face = source_faces[face_index] |
|
|
|
|
|
|
|
|
|
target_face = target_faces[0] if target_faces else source_face |
|
result_img = self.swapper.get(target_img, target_face, source_face, paste_back=True) |
|
print("INFO:core.face_processor:換臉處理完成") |
|
return result_img |
|
|
|
except ValueError as ve: |
|
print(f"ERROR:core.face_processor:{ve}") |
|
raise |
|
except Exception as e: |
|
print(f"ERROR:core.face_processor:換臉處理失敗:{e}") |
|
raise RuntimeError("換臉過程中發生未預期的錯誤") from e |
|
|