import av
|
import torch
|
import threading, queue
|
from threading import current_thread
|
import numpy as np
|
import subprocess
|
import json
|
import cv2
|
import time
|
import insightface
|
from insightface.app.common import Face
|
from insightface.app import FaceAnalysis
|
from gfpgan.utils import GFPGANer
|
from .typing import Frame
|
from auto_media_publisher.config.conf_base import FACE_ENHANCER_MODEL_PATH,FACE_SWAPPER_MODEL_PATH
|
from auto_media_publisher.utils.logger import get_logger
|
import os
|
from fractions import Fraction
|
|
SIMILAR_FACE_DISTANCE: float = 0.45 # < 0.4 同一人 0.4 -0.6 大概是同一人 >0.6 不是同一个
|
|
class FaceSwapperEnhancer:
|
|
def __init__(self):
|
self.logger = get_logger("FaceSwapperEnhancer","admin")
|
self.frame_q = None #queue.Queue(maxsize=10)
|
self.processed_q = None #queue.Queue(maxsize=10)
|
self.device =self._select_device()
|
self.with_swapper =False
|
self.with_enhancer =False
|
self.face_analyser = FaceAnalysis(name='buffalo_l', root='./', providers=['CUDAExecutionProvider'])
|
self.face_analyser.prepare(ctx_id=0, det_size=(640, 640))
|
self.face_swapper = insightface.model_zoo.get_model(FACE_SWAPPER_MODEL_PATH, providers=['CUDAExecutionProvider'])
|
self.face_enhancer = GFPGANer(model_path=FACE_ENHANCER_MODEL_PATH, upscale=1, device=self.device)
|
|
|
def _select_device(self):
|
if torch.cuda.is_available():
|
return 'cuda:0'
|
elif torch.backends.mps.is_available():
|
return 'mps'
|
else:
|
return 'cpu'
|
|
|
|
def enhance_face(self,target_face, temp_frame:np.ndarray) -> np.ndarray:
|
start_x, start_y, end_x, end_y = map(int, target_face['bbox'])
|
padding_x = int((end_x - start_x) * 0.5)
|
padding_y = int((end_y - start_y) * 0.5)
|
start_x = max(0, start_x - padding_x)
|
start_y = max(0, start_y - padding_y)
|
end_x = max(0, end_x + padding_x)
|
end_y = max(0, end_y + padding_y)
|
temp_face = temp_frame[start_y:end_y, start_x:end_x]
|
|
if temp_face.size:
|
_, _, temp_face = self.face_enhancer.enhance(
|
temp_face,
|
paste_back=True
|
)
|
temp_frame[start_y:end_y, start_x:end_x] = temp_face
|
return temp_frame
|
|
|
def decode_video(self,video_path):
|
container = av.open(video_path)
|
for idx, frame in enumerate(container.decode(video=0)):
|
img = frame.to_ndarray(format='rgb24')
|
self.frame_q.put((idx, img))
|
self.frame_q.put((None, None))
|
|
def process_frame(self,new_face=None):
|
start_time = time.time()
|
while True:
|
idx, frame = self.frame_q.get()
|
if idx is None:
|
self.processed_q.put((None, None))
|
break
|
try:
|
many_faces = self.face_analyser.get(frame)
|
if many_faces:
|
if self.with_swapper:
|
old_face = many_faces[0]
|
frame = self.face_swapper.get(frame, old_face, new_face, paste_back=True)
|
|
if self.with_enhancer:
|
for sinlge_face in many_faces:
|
frame = self.enhance_face(sinlge_face, frame)
|
|
self.processed_q.put((idx, frame))
|
print(f"{current_thread().name} has proceesed frame :{idx}")
|
|
except Exception as e:
|
print(f"[ERROR] Frame {idx} failed: {e}")
|
|
print(f"totol time is : {time.time()- start_time}")
|
|
def probe_video(self,old_video_path):
|
cmd = [
|
"ffprobe",
|
"-v", "error",
|
"-select_streams", "v:0",
|
"-show_entries", "stream=width,height,r_frame_rate",
|
"-of", "json",
|
old_video_path,
|
]
|
proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
info = json.loads(proc.stdout)
|
stream = info["streams"][0]
|
fps = eval(stream["r_frame_rate"])
|
fps_fraction = Fraction(fps).limit_denominator()
|
return fps_fraction,int(stream["width"]),int(stream["height"])
|
|
def encode_video(self,output_path, fps, width, height,audio_source_path):
|
|
audio_in_container = av.open(audio_source_path)
|
audio_stream_in = None
|
if audio_in_container.streams.audio:
|
audio_stream_in = audio_in_container.streams.audio[0]
|
|
out_container = av.open(output_path, mode='w')
|
|
v_stream = out_container.add_stream('libx264', rate=fps)
|
v_stream.width = width
|
v_stream.height = height
|
v_stream.pix_fmt = 'yuv420p'
|
v_stream.options = {'crf': '18', 'preset': 'veryfast'}
|
|
# 添加音频流,基于输入音频流模板
|
audio_out_stream = None
|
if audio_stream_in:
|
audio_out_stream = out_container.add_stream(audio_stream_in.codec.name)
|
'''
|
audio_codec_name = audio_stream_in.codec.name
|
audio_out_stream = out_container.add_stream(
|
audio_codec_name,
|
rate=audio_stream_in.rate or audio_stream_in.sample_rate
|
)
|
# 设置一些可写的 metadata(layout)
|
try:
|
audio_out_stream.layout = audio_stream_in.layout
|
except Exception:
|
pass
|
'''
|
else:
|
audio_out_stream = None
|
|
next_index = 0
|
buffer = {}
|
|
while True:
|
idx, frame = self.processed_q.get()
|
if idx is None:
|
break
|
buffer[idx] = frame
|
|
# 只要有当前要写入的帧,就输出
|
while next_index in buffer:
|
frame = buffer.pop(next_index)
|
video_frame = av.VideoFrame.from_ndarray(frame, format='rgb24')
|
for packet in v_stream.encode(video_frame):
|
out_container.mux(packet)
|
next_index += 1
|
|
# 清空编码缓存
|
for packet in v_stream.encode():
|
out_container.mux(packet)
|
|
# 复制原音频流(直接拷贝,不解码)
|
# 拷贝音频包,修改流指向
|
if audio_stream_in and audio_out_stream:
|
for packet in audio_in_container.demux(audio_stream_in):
|
if packet.dts is None:
|
continue
|
packet.stream = audio_out_stream # 直接改,不用 copy()
|
out_container.mux(packet)
|
|
out_container.close()
|
audio_in_container.close()
|
|
def enhance(self,old_video_path,output_video_path):
|
try:
|
if not os.path.exists(old_video_path):
|
self.logger.error(f"[face swap] input video file is not exist. {old_video_path}")
|
return False
|
|
self.frame_q = queue.Queue(maxsize=10)
|
self.processed_q = queue.Queue(maxsize=10)
|
self.with_swapper = False
|
self.with_enhancer = True
|
|
self.start_threads(old_video_path,output_video_path,None)
|
|
return True
|
except Exception as e:
|
self.logger.error(f"[ error ]. {e}")
|
return False
|
|
def start_threads(self,old_video_path,output_video_path,new_face=None):
|
threads = []
|
# 解码线程
|
t_decode = threading.Thread(target=self.decode_video,args=(old_video_path,))
|
threads.append(t_decode)
|
t_decode.start()
|
|
# 处理线程 多处理线程 anaylse enhancer 可能会出问题,暂时还是单线程
|
t_process = threading.Thread(target=self.process_frame, args=(new_face,))
|
threads.append(t_process)
|
t_process.start()
|
|
fps,width,height =self.probe_video(old_video_path)
|
|
# 编码线程
|
t_encode = threading.Thread(target=self.encode_video,args=(output_video_path, fps,width,height,old_video_path))
|
threads.append(t_encode)
|
t_encode.start()
|
|
for t in threads:
|
t.join()
|
|
def swap(self,old_video_path,output_video_path,new_face_image_path,with_enhancer=True):
|
try:
|
if not os.path.exists(old_video_path):
|
self.logger.error(f"[face swap] input video file is not exist. {old_video_path}")
|
return False
|
if not os.path.exists(new_face_image_path):
|
self.logger.error(f"[face swap] input new face image file is not exist. {new_face_image_path}")
|
return False
|
faces = self.face_analyser.get(cv2.imread(new_face_image_path))
|
if not faces:
|
self.logger.error(f"[face swap] input new face image file DOES NOT HAVE FACE. {new_face_image_path}")
|
return False
|
new_face = sorted(faces, key=lambda x: x.bbox[0])[0]
|
self.frame_q = queue.Queue(maxsize=10)
|
self.processed_q = queue.Queue(maxsize=10)
|
self.with_swapper = True
|
self.with_enhancer = with_enhancer
|
|
self.start_threads(old_video_path,output_video_path,new_face)
|
|
return True
|
except Exception as e:
|
self.logger.error(f"[ error ]. {e}")
|
return False
|
|