AI-视频换脸

本文涉及到的代码均已放置在我的github中 -->链接

环境

Anaconda 集成 python 3.6.5
主要使用了 dlib numpy opencv (调用为cv2)os这四个库
录制+截屏:FSCapture 8.0

目的

将视频中的人脸更换为指定照片中的人脸,并且输出视频。

思路

首先使用opencv将一个视频分割为帧,将每一帧保存至origin文件夹内,然后利用transfer.py将每一帧图片转换并且保存至transfer文件夹内,然后继续使用opencv将每一帧的图片在转换为视频,最终保存新视频并且删除origin和 transfer内的缓存帧图。

步骤(以transfer为主)

在transfer.py里面主要使用了dlib库去提取人脸的68个特征点,通过特征点的重叠和转换以及色彩校正实现了换脸。(本项目改编自此位大神之手)

这个过程主要分以下四步:
1、检测脸部标记。
2、旋转、缩放、平移和第二张图片,以配合第一步。
3、调整第二张图片的色彩平衡,以适配第一张图片。
4、把第二张图像的特性混合在第一张图像中。

主要代码有:
1、检测脸部标记:(准备工作)
读取图片:

    def read_im_and_landmarks(fname):
        im = cv2.imread(fname, cv2.IMREAD_COLOR)
        im = cv2.resize(im, (im.shape[1] * SCALE_FACTOR,
                             im.shape[0] * SCALE_FACTOR))
        s = get_landmarks(im)
    return im, s

已经训练好的模型路径(人脸的68个特征点):(下载链接

PREDICTOR_PATH = "shape_predictor_68_face_landmarks.dat"

人脸检测器:

detector = dlib.get_frontal_face_detector()

预测器:

predictor = dlib.shape_predictor(PREDICTOR_PATH)

预测大致人脸:
预测器需要粗略的边界框作为算法的输入,这是由检测器提供的,该检测器返回矩形列表,每个矩形对应图像中的面部,如果大于一个人脸则报出TooManyFaces错误,代码如下:

    def get_landmarks(im):
        rects = detector(im, 1)
    
        if len(rects) > 1:
            raise TooManyFaces
        if len(rects) == 0:
            raise NoFaces
 	    return numpy.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])

2.用 Procrustes 分析调整脸部:

现在我们已经有了两个标记矩阵,每行有一组坐标对应一个特定的面部特征(如第30行的坐标对应于鼻头)。我们现在要解决如何旋转、翻译和缩放第一个向量,使它们尽可能适配第二个向量的点。一个想法是可以用相同的变换在第一个图像上覆盖第二个图像,其实最终是一个正交矩阵的解决办法,代码如下:(参考文档,维基百科

    def transformation_from_points(points1, points2):
        points1 = points1.astype(numpy.float64)
        points2 = points2.astype(numpy.float64)
  		c1 = numpy.mean(points1, axis=0)
        c2 = numpy.mean(points2, axis=0)
        points1 -= c1
        points2 -= c2
    
        s1 = numpy.std(points1)
        s2 = numpy.std(points2)
        points1 /= s1
        points2 /= s2
    
        U, S, Vt = numpy.linalg.svd(points1.T * points2)
        R = (U * Vt).T
    
        return numpy.vstack([numpy.hstack(((s2 / s1) * R,
                                           c2.T - (s2 / s1) * R * c1.T)),
                             numpy.matrix([0., 0., 1.])])

代码实现了这几步:

  • 将输入矩阵转换为浮点数。这是后续操作的基础。

  • 每一个点集减去它的矩心。一旦为点集找到了一个最佳的缩放和旋转方法,这两个矩心 c1 和 c2 就可以用来找到完整的解决方案。

  • 同样,每一个点集除以它的标准偏差。这会消除组件缩放偏差的问题。

  • 使用奇异值分解计算旋转部分。可以在维基百科上看到关于解决正交 Procrustes 问题的细节。

  • 利用仿射变换矩阵返回完整的转化。

3、色彩校正
两幅图像之间不同的肤色和光线造成了覆盖区域的边缘不连续,若无此步,则制作的图片色彩不均匀。
此函数试图改变 im2(第二张图) 的颜色来适配 im1。它通过用 im2 除以 im2 的高斯模糊值,然后乘以im1的高斯模糊值。代码如下:

   def correct_colors(im1, im2, landmarks1,landmarks2): #修改
        blur_amount = COLOUR_CORRECT_BLUR_FRAC * numpy.linalg.norm(
            numpy.mean(landmarks1[LEFT_EYE_POINTS], axis=0) -
            numpy.mean(landmarks2[RIGHT_EYE_POINTS], axis=0))
        blur_amount = int(blur_amount)
        if blur_amount % 2 == 0:
            blur_amount += 1
    
        im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0)
        im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0)
    
        # Avoid divide-by-zero errors:
        im2_blur += (128 * (im2_blur <= 1.0)).astype(im2_blur.dtype)
    
        return (im2.astype(numpy.float64) * im1_blur.astype(numpy.float64) /
                im2_blur.astype(numpy.float64))

4、第二张图特征混合在第一张图

def get_face_mask(im, landmarks):
    im = numpy.zeros(im.shape[:2], dtype=numpy.float64)

    for group in OVERLAY_POINTS:
        draw_convex_hull(im,
                         landmarks[group],
                         color=1)

    im = numpy.array([im, im, im]).transpose((1, 2, 0))

    im = (cv2.GaussianBlur(im, (FEATURE_AMOUNT, FEATURE_AMOUNT), 0) > 0) * 1.0
    im = cv2.GaussianBlur(im, (FEATURE_AMOUNT, FEATURE_AMOUNT), 0)

	return im

get_face_mask()的定义是为一张图像和一个标记矩阵生成一个遮罩,它画出了两个白色的凸多边形:一个是眼睛周围的区域,一个是鼻子和嘴部周围的区域。之后它由11个像素向遮罩的边缘外部羽化扩展,可以帮助隐藏任何不连续的区域。最终返回优化过后的图像。

其余代码

分解视频帧代码
注意此步骤文件名必须命名为 PythonCv2.py!!!不知道别的环境如何,我这里必须这样命名才能运行程序(卡了我两天哎!???)最终找到原因是在Google上搜到的stackowerflow上的办法 改名字,附上这位大神|
在这里插入图片描述
在这里插入图片描述
官方的解决办法,emmmmm
在这里插入图片描述
Make sure proper versions of ffmpeg or gstreamer is installed. Sometimes, it is a headache to work with Video Capture mostly due to wrong installation of ffmpeg/gstreamer.
然而安装了ffmpeg也并没什么卵用,还是民间高手多,也或许是我错误类型不一样呢,反正是这样解决了,如果有类似情况的小伙伴也可以尝试一下,说不定就好了呢。
除了这个名字比较坑爹,代码比较简单:

def save_img(videofilepath,originpath):
    vc = cv2.VideoCapture(videofilepath)
    c=1
    if vc.isOpened():
        rval , frame=vc.read()
    else:
        rval = False

    timeF=1  #由于视频帧数过多 用此变量作为测试使用 正常使用时值为 1

    while rval:
        rval,frame=vc.read()
        if(c%timeF == 0):
            cv2.imwrite(originpath +str(c) + '.jpg' , frame)
        c=c+1
        cv2.waitKey(1)
    vc.release()

转换视频帧

  • 即为上文叙述的transfer.py,不再赘述

合并视频帧
此处注意视频分解得到的帧必须要和准备替换的图片大小一致,否则程序无法正常运行

def size(transferpath):#获取图片像素的大小
    filelist = os.listdir(transferpath)
    
    img = Image.open(transferpath + filelist[0])  
    return  img.size
def mergevideo(transferpath):
    img_root = transferpath #这里写你的文件夹路径,比如:/home/youname/data/img/,注意最后一个文件夹要有斜杠
    fps = 24    #保存视频的FPS,可以适当调整
    filelist = os.listdir(transferpath)  #得到所有帧的文件名,在循环中使用到文件数目

    #可以用(*'DVIX')或(*'X264'),如果都不行先装ffmepg
    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
    s=size(transferpath) #获取图片像素的大小
    videoWriter = cv2.VideoWriter('saveVideo.avi',fourcc,fps,s)#最后一个是保存图片的尺寸

    for i in range(len(filelist)):
        frame = cv2.imread(img_root+str(i+1)+'.jpg')
        videoWriter.write(frame)
    videoWriter.release()

删除帧缓存
在origin和transfer会存储大量的帧缓存,设置此步骤为了清除缓存,当然在main.py运行时会提示你是否删除,你也可以选择不删除

    def del_file(path):
        ls = os.listdir(path)
        for i in ls:
            c_path = os.path.join(path, i)
            if os.path.isdir(c_path):
                del_file(c_path)
            else:
                os.remove(c_path)

main.py
最终我是使用的main.py去循环读取图片并且将其更改,最终合并视频删除缓存,大致流程和思想就是这样了

 	from transfer import *
    from newvideo import mergevideo
    from PythonCv2 import save_img
    from deletecache import del_file
    
    import os 
    
    count = 1
    
    originpath = 'origin/'
    transferpath = 'transfer/'  #原始帧的路径和转换后帧的路径 一定要加 / 
    videofilepath = 'trump.mp4'    #文件路径和名称可自己定义
    
    save_img(videofilepath,originpath)
    
    print('视频的每一帧分离完成,开始转换')
    filelist=os.listdir(originpath)
    num = len(filelist)
    
    os.remove(originpath + str(num) +'.jpg')
    #删除最后一个乱码帧  不知道为啥会出现这样的帧 ...
    
    newlist = os.listdir(originpath)
    
    print('视频一共有:'+ str(num-1) + '帧')
    
    im2, landmarks2 = read_im_and_landmarks("1.jpg")  #人物模型,将要替换到视频中的人
                                                #在同级目录下,如果更换,请定义路径
                            #写到for循环外边 减少了建立模型的时间,稍微快了那么一丢丢
    
    for i in newlist:  #获得文件数目进行逐帧转换
        
        im1, landmarks1 = read_im_and_landmarks(originpath + i )
    
        M = transformation_from_points(landmarks1[ALIGN_POINTS],
                                       landmarks2[ALIGN_POINTS])
    
        mask = get_face_mask(im2, landmarks2)
        warped_mask = warp_im(mask, M, im1.shape)
        combined_mask = numpy.max([get_face_mask(im1, landmarks1), warped_mask],axis=0)
    
        warped_im2 = warp_im(im2, M, im1.shape)
        warped_corrected_im2 = correct_colors(im1, warped_im2, landmarks1,landmarks2)
    
        output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask
    
        cv2.imwrite(transferpath + i, output_im)
    
        count = count + 1
        if (count%100 ==0):
            print('已完成'+str(count)+'帧')
    
    print('逐帧转换完成,开始合并视频:')
    
    mergevideo(transferpath)
    
    print('视频转换完成,请确认是否清除图片缓存')
    
    m = input('是否删除缓存 y/n')
    
    if (m == 'y'):
        del_file(originpath)
        del_file(transferpath)
        print('删除成功')
    else:
        print('缓存未删除')
    
    print ('Done!!!')

总结

除了这个命名比较恶心,别的还是比较容易的,看看随后再使用别的算法做不同的尝试,或者使用机器学习重新构建模型,还有很多可拓展的空间,那样或许会更好点吧

附上常用视频的编码格式

CV_FOURCC('P','I','M','1') = MPEG-1 codec
CV_FOURCC('M','J','P','G') = motion-jpeg codec
CV_FOURCC('M', 'P', '4', '2') = MPEG-4.2 codec
CV_FOURCC('D', 'I', 'V', '3') = MPEG-4.3 codec
CV_FOURCC('D', 'I', 'V', 'X') = MPEG-4 codec
CV_FOURCC('U', '2', '6', '3') = H263 codec
CV_FOURCC('I', '2', '6', '3') = H263I codec
CV_FOURCC('F', 'L', 'V', '1') = FLV1 codec
  • 22
    点赞
  • 124
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值