楔子
西施沉魚自苎蘿 當為複越首功臣
昭君落雁出邊塞 一平烽火即五旬
貂蟬月下邀對酌 離間巧計除奸惡
玉環香消馬嵬坡 舍身為國安大唐----《王者四大美女–紅昭願》
目的
将b站很火的4小戲骨的紅昭願視訊中相信很多人都看過,
裡面的角色背景是我國古代四大美女。本人又是一個王者榮耀er,
所有就想到了将王者榮耀中四大美女的形象人臉替換到B站視訊上面的創意。
項目位址
aistudio位址
github位址
bilibli視訊位址
使用到的模型:
模型1pyramidbox_lite_mobile
模型2resnet_v2_18_imagenet
實作思路
1.先将視訊按幀提取儲存為圖檔的集合
2.然後使用paddlehub提供的pyramidbox_lite_mobile模型進行人臉位置檢測,使用自己finetune的resnet_v2_18_imagenet模型對每一幀圖檔進行人臉識别。可能出現圖檔中沒有人、圖檔中隻有一個人、圖檔中有超過一個人三種情況。對于圖中沒有人,則不做任何操作;如果圖中隻有一個人,那麼可以使用模型2精确分類該人物是誰,然後換上對應的貼圖;如果圖中使用模型1識别處理超過1人,則采用随機算法,随機給人臉換上貼圖。
3.将經過人臉分類+識别+貼圖的圖檔集合使用cv2生成視訊
4.步驟3中生成的視訊是沒有bgm的,采用VideoFileClip給視訊加上bgm
項目前期準備
首先需要有王者榮耀中四大美女的頭像。部落客于是打開了王者榮耀,截取了四個英雄的海報照片,然後使用ps将四個英雄的頭像扣取了出來。本來是準備使用paddlehub提供的摳圖模型的,但是貌似這個模型對于遊戲人物的識别能力不是很強,所有還是回歸了最初的ps。最終四大美女法師的頭像如下。
下面是項目的一個檔案目錄結構:
face中放的就是上面的四張頭像,底部是透明的
video中放的就是源視訊
下面進入代碼部分
工具函數部分:
def checkdir(dir_path):
'''
檢查是否存在該檔案,如果存在就删除了建立,不存在就建立
:param dir_path:需要檢測的路徑
:return:
'''
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
os.makedirs(dir_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
def getvideoinfo(video_path):
'''
擷取視訊的幀率和撒小
:param src_video:視訊的位址
:return: 幀率,視訊大小
fps: 25.0
size: (720.0, 576.0)
'''
video_capture = cv2.VideoCapture(video_path)
fps = video_capture.get(cv2.CAP_PROP_FPS)
size = (int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)), int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print("視訊的fps: {}\n視訊的size: {}".format(fps, size))
return fps,size
finetune resnet_v2_18_imagenet模型對四個人進行識别:
class DemoDataset(BaseCVDataset):
def __init__(self):
# 資料集存放位置
self.dataset_dir = "./train-data"
super(DemoDataset, self).__init__(
base_path=self.dataset_dir,
train_list_file="train_list.txt",
validate_list_file="validate_list.txt",
test_list_file="test_list.txt",
label_list_file="label_list.txt",
)
def data_config_and_run():
module = hub.Module(name="resnet_v2_18_imagenet")
dataset = DemoDataset()
print(dataset)
data_reader = hub.reader.ImageClassificationReader(
image_width=module.get_expected_image_width(),
image_height=module.get_expected_image_height(),
images_mean=module.get_pretrained_images_mean(),
images_std=module.get_pretrained_images_std(),
dataset=dataset)
config = hub.RunConfig(
use_cuda=False, # 是否使用GPU訓練,預設為False;
num_epoch=4, # Fine-tune的輪數;
checkpoint_dir="cv_finetune", # 模型checkpoint儲存路徑, 若使用者沒有指定,程式會自動生成;
batch_size=10, # 訓練的批大小,如果使用GPU,請根據實際情況調整batch_size;
eval_interval=10, # 模型評估的間隔,預設每100個step評估一次驗證集;
strategy=hub.finetune.strategy.DefaultFinetuneStrategy()) # Fine-tune優化政策;
# 組建FinetuneTask
input_dict, output_dict, program = module.context(trainable=True)
img = input_dict["image"]
feature_map = output_dict["feature_map"]
feed_list = [img.name]
task = hub.ImageClassifierTask(
data_reader=data_reader,
feed_list=feed_list,
feature=feature_map,
num_classes=dataset.num_labels,
config=config)
# run the finetune task啟動finetune任務
run_states = task.finetune_and_eval()
#如果第一次使用,請打開下面的注釋,将人臉的分類模型訓練一些
data_config_and_run()
使用前面一步訓練好的模型對傳入的圖檔進行人臉識别分類
def human_classfication(data):
'''
使用前面訓練好的圖檔進行人臉識别分類
:param data: 要檢測的圖檔的位址
:return: 人臉的标簽(是誰)
'''
module = hub.Module(name="resnet_v2_18_imagenet")
dataset = DemoDataset()
# 模型建構
data_reader = hub.reader.ImageClassificationReader(
image_width=module.get_expected_image_width(),
image_height=module.get_expected_image_height(),
images_mean=module.get_pretrained_images_mean(),
images_std=module.get_pretrained_images_std(),
dataset=dataset)
config = hub.RunConfig(
use_cuda=False, # 是否使用GPU訓練,預設為False;
num_epoch=4, # Fine-tune的輪數;
checkpoint_dir="cv_finetune", # 模型checkpoint儲存路徑, 若使用者沒有指定,程式會自動生成;
batch_size=10, # 訓練的批大小,如果使用GPU,請根據實際情況調整batch_size;
eval_interval=10, # 模型評估的間隔,預設每100個step評估一次驗證集;
strategy=hub.finetune.strategy.DefaultFinetuneStrategy()) # Fine-tune優化政策;
# 組建FinetuneTask
input_dict, output_dict, program = module.context(trainable=True)
img = input_dict["image"]
feature_map = output_dict["feature_map"]
feed_list = [img.name]
task = hub.ImageClassifierTask(
data_reader=data_reader,
feed_list=feed_list,
feature=feature_map,
num_classes=dataset.num_labels,
config=config)
task.load_checkpoint()
# ##--------------開始預測
label_map = dataset.label_dict()
index = 0
run_states = task.predict(data=data)
results = [run_state.run_results for run_state in run_states]
for batch_result in results:
batch_result = np.argmax(batch_result, axis=2)[0]
for result in batch_result:
return result
前面的一些工具函數都已經準備好了,下面主要就是前面思路中的四個步驟,每一個一個函數實作
分成4步(4個函數)完成本任務
- 從視訊中提取圖檔 function1
- 對圖檔進行王者榮耀貼圖 function2
- 将進過face貼圖處理的圖檔從新合成為視訊 function3
- 視訊添加聲音
function1
# step1 從視訊中提取圖檔
def extract_images(src_video, dst_dir):
'''
src_video:為目标的視訊檔案位址
dst_dir:為視訊圖檔的儲存路徑
'''
video = cv2.VideoCapture(src_video)
count = 0
while True:
flag, frame = video.read()
if not flag:
break
cv2.imwrite(os.path.join(dst_dir, str(count) + '.png'), frame)
count = count + 1
print('extracted {} frames in total.'.format(count))
function2
# step2 對圖檔進行王者榮耀貼圖
def face_chartlet(img_path,save_path):
module = hub.Module(name="pyramidbox_lite_mobile")
offset_length = 30 #人臉識别的框可能會比較小,給貼圖加一個偏移值
img = cv2.imread(img_path)
# prep mask
masks = [
cv2.imread('./work-hong/face/dc.png', -1),
cv2.imread('./work-hong/face/wzj.png', -1),
cv2.imread('./work-hong/face/xs.png', -1),
cv2.imread('./work-hong/face/yyh.png', -1)
]
# set input dict 因為這個模型value為待檢測的圖檔,numpy.array類型,shape為[H, W, C],BGR格式。所有使用cv2讀取
input_dict = {"data": [img]}
results = module.face_detection(data=input_dict)
img2 = img.copy()
# 因為計劃一次隻傳入一張圖檔,所有隻需要取results的第一個元素就行了
result_list = results[0]['data']
if len(result_list) < 1:
print('圖中沒有人臉,不做處理')
elif len(result_list) == 1:
print('圖中隻有一張人臉,先進行人臉識别,識别出對應的人臉後再進行貼圖')
index_ = human_classfication([img_path])
for pos_result in result_list:
# 擷取人臉的位置
left_pos, right_pos, top_pos, bottom_pos, _ = pos_result.values()
# print(left_pos, right_pos, top_pos, bottom_pos)
left_pos = int(left_pos)#将坐标值轉換為int類型,防止後面計算出錯
right_pos = int(right_pos)
top_pos = int(top_pos)
bottom_pos = int(bottom_pos)
# print(left_pos,right_pos,top_pos,bottom_pos)
mask = cv2.resize(masks[index_],(int(right_pos - left_pos + offset_length), int(bottom_pos - top_pos + offset_length)))
# 取出mask非0的值
index = mask[:, :, 3] != 0
index = np.repeat(index[:, :, np.newaxis], axis=2, repeats=3)
try:
img2[int(top_pos - offset_length / 2):int(bottom_pos + offset_length / 2), int(left_pos - offset_length / 2):int(right_pos + offset_length / 2), :][index] = mask[:, :, :3][index]
except Exception as e:
print(e)
elif len(result_list) > 1:
print('檢測出了多張人臉,采用随機貼圖的方式進行貼圖')
count = 0
#對人臉識别中識别到的所有人臉進行随機貼圖
for pos_result in result_list:
# 擷取人臉的位置
left_pos, right_pos, top_pos, bottom_pos, _ = pos_result.values()
# print(left_pos, right_pos, top_pos, bottom_pos)
left_pos = int(left_pos)
right_pos = int(right_pos)
top_pos = int(top_pos)
bottom_pos = int(bottom_pos)
mask = cv2.resize(masks[count],
(int(right_pos - left_pos + offset_length), int(bottom_pos - top_pos + offset_length)))
# mask = cv2.resize(masks[random.randint(0,3)], (int(right_pos - left_pos), int(bottom_pos - top_pos)))
# 取出mask非0的值
index = mask[:, :, 3] != 0
index = np.repeat(index[:, :, np.newaxis], axis=2, repeats=3)
try:
img2[int(top_pos - offset_length / 2):int(bottom_pos + offset_length / 2), int(left_pos - offset_length / 2):int(right_pos + offset_length / 2), :][index] = mask[:, :, :3][index]
except Exception as e:
print(e)
count += 1
# 儲存圖檔
cv2.imwrite(os.path.join(save_path, os.path.basename(img_path)), img2)
function3
# step3image2video
#将進過face貼圖處理的圖檔從新合成為視訊
def img2video(dst_video_path,pic_path,size,frame):
'''
dst_video_path:合成視訊的儲存路徑(包含檔案名)
pic_path:合成的所有圖檔的路徑
size:圖檔的大小,即是視訊的大小
frame:幀率
VideoWriter_fourcc為視訊編解碼器
fourcc意為四字元代碼(Four-Character Codes),顧名思義,該編碼由四個字元組成,下面是VideoWriter_fourcc對象一些常用的參數,注意:字元順序不能弄混
cv2.VideoWriter_fourcc('I', '4', '2', '0'),該參數是YUV編碼類型,檔案名字尾為.avi
cv2.VideoWriter_fourcc('P', 'I', 'M', 'I'),該參數是MPEG-1編碼類型,檔案名字尾為.avi
cv2.VideoWriter_fourcc('X', 'V', 'I', 'D'),該參數是MPEG-4編碼類型,檔案名字尾為.avi
cv2.VideoWriter_fourcc('T', 'H', 'E', 'O'),該參數是Ogg Vorbis,檔案名字尾為.ogv
cv2.VideoWriter_fourcc('F', 'L', 'V', '1'),該參數是Flash視訊,檔案名字尾為.flv
cv2.VideoWriter_fourcc('m', 'p', '4', 'v') 檔案名字尾為.mp4
'''
dst_video = cv2.VideoWriter(dst_video_path, cv2.VideoWriter_fourcc(*'mp4v'), frame, size, True)
for index in range(len(os.listdir(pic_path))):
frame = cv2.imread(os.path.join(pic_path,'{}.png'.format(index)))
dst_video.write(frame)
dst_video.release()
function4
# step4 給視訊添加聲音
def add_audio(s_video_path,d_video_path):
'''
給視訊加聲音
:param s_video_path: 原視訊位址-含有聲音的
:param d_video_path: 目的視訊位址-需要加聲音的
:return:
'''
video_s = VideoFileClip(s_video_path)
video_d = VideoFileClip(d_video_path)
audio_o = video_s.audio
video_dd = video_d.set_audio(audio_o)
video_dd.write_videofile(d_video_path[0:d_video_path.rfind('/')+1]+'hong_audio.mp4')
主函數 依次執行上面四步即可:
if __name__ == '__main__':
src_video = './work-hong/video/hong4-sing.mov'
dst_img_dir = './work-hong/video-imgs' #每一幀圖檔的儲存路徑
# # step1 extract images of the videos
# checkdir(dst_img_dir)
# extract_images(src_video,dst_img_dir)
# print('step1提取圖檔完成')
# step2 give the face chartlet of each pic
save_video_imgs = './work-hong/video-imgs-face' #貼圖圖檔的儲存路徑
checkdir(save_video_imgs)
# 對目前路徑下的所有圖檔進行貼圖處理 後期可以改進為将所有圖檔都放入到data_list中,隻需要進行一次模型的加載即可完成任務
for i in tqdm(os.listdir(dst_img_dir)):
face_chartlet(os.path.join(dst_img_dir,i),save_video_imgs)
print('step2所有圖檔貼圖完成')
# step3 image2video
dst_video_path = './work-hong/video-result' #最終合成視訊的儲存路徑
checkdir(dst_video_path)
video_name = 'hong-sing.mp4'
dst_video_name = os.path.join(dst_video_path,video_name)
fps,size = getvideoinfo(src_video)
size = (int(size[0]),int(size[1]))
img2video(dst_video_name, save_video_imgs, size, int(fps))
print('step3将所有圖檔合成為視訊完成')
# step4 add the audio
add_audio(src_video, dst_video_name)
print('step4給視訊加聲音完成')
最終效果
參見bilibili視訊王者榮耀版紅昭願
總結
- 貼圖已經增加了30個pixel的偏移了,還是不能遮住臉。應該是模型中的人臉定位的框太小了,後期可以稍微在調大一點。
- 對于一種圖中有多人的情況采用的是随機貼圖的方式,本來應該将圖中的人臉部分切圖圖來然後進行分類再貼圖的,但是一個原因是這樣切圖出來的進行人臉分類的效果不好,另一個原因是因為在一幅圖中有很多人的情況下,這次使用的人臉定位模型并不能将一些小的臉給精準定位。
- 後期還可以做一個将人的身子扣出來然後用王者榮耀中的人物模型替換的創意