import cv2
|
import os
|
import torch
|
import queue
|
import threading
|
import subprocess
|
import json
|
import numpy as np
|
import time
|
from typing import Union
|
import insightface
|
from insightface.app.common import Face
|
from .face_nsfw_predictor import NSFWPredictor
|
from .face_enhancer import FaceEnhancer
|
from .face_analyser import FaceAnalyser
|
from auto_media_publisher.utils.logger import get_logger
|
from auto_media_publisher.config.conf_base import FACE_SWAPPER_MODEL_PATH
|
|
|
class FaceSwapWorker:
|
def __init__(self, enable_enhancer:bool = False,enable_nsfw_predictor:bool=False):
|
self.logger = get_logger("FaceSwapWorker", "admin")
|
self.device = self._select_device()
|
self.face_swaper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider' if self.device == 'cuda' else 'CPUExecutionProvider'])
|
self.main_analyser = FaceAnalyser()
|
self.enable_enhancer = enable_enhancer
|
self.enable_nsfw_predictor = enable_nsfw_predictor
|
if self.enable_nsfw_predictor:
|
self.nsfw_predictor = NSFWPredictor()
|
self._init()
|
|
def _init(self):
|
self.frame_queue = queue.Queue()
|
self.processed_queue = queue.Queue()
|
self.buffered_frames = {}
|
self.expected_index = 0
|
self.fps = 0
|
self.width = 0
|
self.height = 0
|
self.total_frames = 0
|
self.process_threads = []
|
self.num_processors = 4
|
|
def _select_device(self):
|
if torch.cuda.is_available():
|
return 'cuda'
|
elif torch.backends.mps.is_available():
|
return 'mps'
|
else:
|
return 'cpu'
|
|
def _decode_video(self):
|
# === Step 1: 用 ffprobe 获取视频信息 ===
|
probe_cmd = [
|
"ffprobe",
|
"-v", "error",
|
"-select_streams", "v:0",
|
"-show_entries", "stream=width,height,r_frame_rate",
|
"-of", "json",
|
self.input_video_path
|
]
|
probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
|
probe_data = json.loads(probe_result.stdout)
|
video_stream = probe_data['streams'][0]
|
self.width = int(video_stream['width'])
|
self.height = int(video_stream['height'])
|
self.fps = eval(video_stream['r_frame_rate']) # e.g. "30/1"
|
|
print(f"Video info: {self.width}x{self.height} @ {self.fps}fps")
|
|
# === Step 2: 用 ffmpeg 解码输出 rawvideo 到 stdout pipe ===
|
decode_cmd = [
|
"ffmpeg",
|
"-loglevel", "info",
|
"-i", self.input_video_path,
|
"-f", "rawvideo",
|
"-pix_fmt", "rgb24",
|
"pipe:1"
|
]
|
process = subprocess.Popen(decode_cmd, stdout=subprocess.PIPE, bufsize=10**8)
|
|
frame_size = self.width * self.height * 3
|
idx = 0
|
while True:
|
in_bytes = process.stdout.read(frame_size)
|
if not in_bytes:
|
break
|
frame = np.frombuffer(in_bytes, dtype=np.uint8).reshape((self.height, self.width, 3))
|
self.frame_queue.put((idx, frame)) # 改为 tuple 形式
|
idx += 1
|
|
self.total_frames = idx
|
print(f"_decode_video Total frames: {self.total_frames}")
|
|
process.stdout.close()
|
process.wait()
|
|
# 通知处理线程结束
|
for _ in range(self.num_processors):
|
self.frame_queue.put(None)
|
|
def _process_frame(self):
|
thread_face_analyser = FaceAnalyser()
|
if self.enable_enhancer:
|
thread_face_enhancer = FaceEnhancer()
|
while True:
|
item = self.frame_queue.get()
|
if item is None:
|
self.processed_queue.put(None) # 把结束信号继续传递
|
break
|
|
idx, frame = item
|
|
print(f"[Thread ID: {threading.get_ident()}] is processing frame : {idx} of video file :{self.input_video_path}")
|
|
old_face = thread_face_analyser.get_one_face(frame)
|
if old_face:
|
frame = self.face_swaper.get(frame,old_face,self.new_face)
|
else:
|
print("face not detected")
|
|
print(self.enable_enhancer)
|
|
if self.enable_enhancer:
|
many_faces= thread_face_analyser.get_many_faces(frame)
|
if many_faces:
|
frame = thread_face_enhancer.process_frame(frame,many_faces)
|
|
self.processed_queue.put((idx, frame))
|
|
def _encode_video(self):
|
while not (self.width and self.height and self.fps):
|
time.sleep(0.01)
|
encode_cmd = [
|
"ffmpeg",
|
"-loglevel", "info",
|
"-y", # 覆盖输出文件
|
"-f", "rawvideo",
|
"-pixel_format", "rgb24",
|
"-video_size", f"{self.width}x{self.height}",
|
"-framerate", str(self.fps),
|
"-i", "pipe:0", # 从 stdin 读取原始视频帧
|
#"-i", self.input_video_path if self.new_audio_path is None else self.new_audio_path, # 提供音频
|
"-c:v", "libx264", # 你可以替换成 h264_nvenc、hevc_nvenc、h264_amf、h264_qsv 等硬件编码器,质量可能稍微逊色
|
"-crf", "18",
|
"-preset", "veryfast",
|
"-pix_fmt", "yuv420p",
|
#"-c:a", "copy",
|
#"-map", "0:v:0",
|
#"-map", "1:a:0",
|
#"-shortest",
|
self.output_video_path
|
]
|
|
process = subprocess.Popen(encode_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,bufsize=10**8)
|
|
end_signals_received = 0
|
finished = 0
|
|
while True:
|
item = self.processed_queue.get()
|
if item is None:
|
end_signals_received += 1
|
if end_signals_received == self.num_processors:
|
break
|
else:
|
continue
|
idx,frame = item
|
self.buffered_frames[idx] = frame
|
|
while self.expected_index in self.buffered_frames:
|
next_frame = self.buffered_frames.pop(self.expected_index)
|
print(f"_encode frame next_frame: {self.expected_index}")
|
try:
|
process.stdin.write(next_frame.astype(np.uint8).tobytes())
|
except BrokenPipeError:
|
print("BrokenPipeError: ffmpeg process closed stdin.")
|
break
|
self.expected_index += 1
|
finished += 1
|
print(f"_encode frame finished: {finished}")
|
|
|
print(f"Total frames encoded: {finished}")
|
|
process.stdin.close()
|
return_code = process.wait()
|
print("FFmpeg exited with code:", return_code)
|
|
def swap_face(self,new_face_image_path:str,input_video_path: str, output_video_path: str,num_processors=4):
|
"""
|
执行换脸操作,参数如下:
|
1、 新的脸照片,路径要正确,还要包含一张人脸;
|
2、 老的视频,路径要正确
|
3、 新视频 最终输出的视频路径
|
4、 并发处理进程数量,默认是4,依据硬件性能设置
|
5、 新视频 的音频,如果不为空,就使用; 否则 使用老视频的音频
|
"""
|
self._init()
|
self.num_processors = num_processors
|
|
if not os.path.exists(new_face_image_path):
|
self.logger.error(f"face swap image_path: {new_face_image_path} does not exists.")
|
return
|
self.new_face = self.main_analyser.get_one_face(cv2.imread(new_face_image_path))
|
if not self.new_face :
|
self.logger.error(f"target image DO NOT has face : {new_face_image_path} does not exists.")
|
return
|
|
if not os.path.exists(input_video_path):
|
self.logger.error(f"face swap video_path: {input_video_path} does not exists.")
|
return
|
|
self.input_video_path = input_video_path
|
self.output_video_path = output_video_path
|
#self.new_audio_path =new_audio_path
|
|
if self.enable_nsfw_predictor and self.nsfw_predictor:
|
if self.nsfw_predictor.predict_video(input_video_path):
|
self.logger.error(f" 视频违规: {self.input_video_path}")
|
return
|
|
# 1. 启动 encode 线程(需要等待 width/height/fps 设置,但可以放在前面)
|
encode_thread = threading.Thread(target=self._encode_video)
|
encode_thread.start()
|
|
# 2. 启动 decode 线程(负责从视频中读取帧)
|
decode_thread = threading.Thread(target=self._decode_video)
|
decode_thread.start()
|
|
# 3. 启动处理线程
|
for i in range(self.num_processors):
|
t = threading.Thread(target=self._process_frame)
|
t.start()
|
self.process_threads.append(t)
|
|
# 4. 等待 decode 完成
|
decode_thread.join()
|
|
# 5. 等待所有处理线程完成
|
for t in self.process_threads:
|
t.join()
|
|
# 6. 等待 encode 完成
|
encode_thread.join()
|