Python-OpenCV开发总结:替换视频绿幕并重新调整大小和位置

Python-OpenCV开发总结:替换视频绿幕并重新调整大小和位置

在Python中,使用OpenCV(cv2)替换视频的绿幕背景为新的图片,同时还需要调整透明的视频的大小和位置,首先需要解决抠图的问题,因为是替换视频绿幕,所以视频帧抠图后,还需要确保合成的视频的清晰度,同时需要将原始视频的声音合并到新视频中。

.

1 主要的处理步骤

-

为了实现以上功能,处理过程的主要步骤为:

(1)通过cv2.VideoCapture逐帧读取源视频为图片;

(2)先增加alpha通道,再将图片中的绿幕抠出来,;

(3)使用cv2提供的方法将抠出绿幕的透明图片粘贴到新的背景图片上;

(4)将合成后的图片写入视频流,形成新的视频文件。

(5)通过moviepy读取原视频的声音,添加到新的视频文件中。

.

2 优化图片抠像的效果

-

以上步骤中,第2步是抠图效果的关键,容易踩的坑有以下两个,以及正确的避坑方法如下:

2.1 增加alpha通道

如果没有给图片增加alpha通道,直接做绿幕抠图,结果是绿幕没有了,但是被黑色填充,可谓“才脱虎口,又入狼窝”。为图片增加alpha通道的代码如下:

def add_alpha_channel(self, frame):

    #print('增加alpha通道前大小:', frame.shape)

    b_channel, g_channel, r_channel = cv2.split(frame)

    alpha_channel = np.ones(b_channel.shape, dtype=b_channel.dtype) * 255

    # 最小值为0,代表图像透明(不可见)
    alpha_channel[:, :int(b_channel.shape[0] / 2)] = 255

    frame = cv2.merge((b_channel, g_channel, r_channel, alpha_channel))

    #print('增加alpha通道后大小:', frame.shape)

    return frame

2.2 矩阵计算拼图的坑

抠出绿幕后,为了获得剩下的透明图片,使用numpy的矩阵计算,通过像素点的值为0的条件取值(numpy.where函数),会造成抠出的图形出现穿透等异常情况。


#frame002为视频中的帧,测试的时候也可以通过cv2.imread读取一张图片

def green_screen_matting(self, frame002):
    #将帧的颜色空间由RGB转HSV
    hsv_img = cv2.cvtColor(frame002, cv2.COLOR_BGR2HSV)
    
    #标准的HSV中的绿色的区间
    lower_g = (35, 43, 46)
    upper_g = (77, 255, 255)
    
    #转换后的HSV空间的图片hsv_img,选择绿幕的部分,非绿幕的部分就被剔除了
    mask_of_green = cv2.inRange(hsv_img, lower_g, upper_g)
    #cv2.imshow('mask', mask_of_green)
    #cv2.waitKey()
    
    #通过 bitwise_not 反转mask_of_green这个mask,这样绿幕的那部分 就没有了
    frame_without_green = cv2.bitwise_not(mask_of_green)
    
    #将HSV的mask作为掩码,通过bitwise_and合成到原图片上,则只保留mask有内容的那部分,即抠图完成
    final = cv2.bitwise_and(frame002, frame002, mask=frame_without_green)

    return final

final = green_screen_matting(frame002)
    
#这里是将抠掉绿幕的图片合成到背景图片上,bg_img_cv是一张背景图片,通过imread读取进来
#此方法是最后一步,也是问题所在,不能这么处理   
final = np.where(final == 0, bg_img_cv, final)

Python-OpenCV开发总结:替换视频绿幕并重新调整大小和位置

2.3 优化后的矩阵计算贴图

正确的处理过程的代码如下:

def overlay_transparent(self, background_img, img_to_overlay_t, x, y, overlay_size=None):
    """
    @brief      Overlays a transparant PNG onto another image using CV2

    @param      background_img    The background image
    @param      img_to_overlay_t  The transparent image to overlay (has alpha channel)
    @param      x                 x location to place the top-left corner of our overlay
    @param      y                 y location to place the top-left corner of our overlay
    @param      overlay_size      The size to scale our overlay to (tuple), no scaling if None

    @return     Background image with overlay on top
    """

    bg_img = background_img.copy()

    if overlay_size is not None:
        img_to_overlay_t = cv2.resize(img_to_overlay_t.copy(), overlay_size)

    # Extract the alpha mask of the RGBA image, convert to RGB
    b, g, r, a = cv2.split(img_to_overlay_t)
    overlay_color = cv2.merge((b, g, r))

    # Apply some simple filtering to remove edge noise
    mask = cv2.medianBlur(a, 5)

    h, w, _ = overlay_color.shape
    roi = bg_img[y:y + h, x:x + w]

    # Black-out the area behind the logo in our original ROI
    img1_bg = cv2.bitwise_and(roi.copy(), roi.copy(), mask=cv2.bitwise_not(mask))

    # Mask out the logo from the logo image.
    img2_fg = cv2.bitwise_and(overlay_color, overlay_color, mask=mask)

    # Update the original image with our new ROI
    bg_img[y:y + h, x:x + w] = cv2.add(img1_bg, img2_fg)

    return bg_img


#指定final的位置和大小,贴图
frame002 = overlay_transparent(bg_img_cv, final, x_offset, y_offset,
                                          (final_width, final_height))

.

3 优化视频比特率

-

3.1 读取源视频的过程

input_video_cv = cv2.VideoCapture(input_video_path)



#视频相关的参数说明: https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html
input_video_width = int(input_video_cv.get(cv2.CAP_PROP_FRAME_WIDTH))
input_video_height = int(input_video_cv.get(cv2.CAP_PROP_FRAME_HEIGHT))

input_video_fps = 30  # 设置想要写成的视频的帧率
input_video_fps = int(input_video_cv.get(cv2.CAP_PROP_FPS))
input_video_frame_count = int(input_video_cv.get(cv2.CAP_PROP_FRAME_COUNT))
input_video_bit_rate = int(input_video_cv.get(cv2.CAP_PROP_BITRATE))

print('视频帧率:%d 比特率:%d 合计帧数:%d' % (input_video_fps, input_video_bit_rate, input_video_frame_count))

3.2 循环读取视频帧

接下来循环读取视频帧,并在处理好后写入到新的视频文件中,过程如下:

#fourcc = cv2.VideoWriter_fourcc(*"mp4v")  # 设置编码参数

output_video_cv = cv2.VideoWriter(output_video_file002,
                           cv2.VideoWriter_fourcc(*"mp4v"),
                           input_video_fps, (output_video_width, output_video_height))

#设置视频质量 0 ~ 100
output_video_cv.set(cv2.VIDEOWRITER_PROP_QUALITY, 100)
#用于并行编码的条纹数
output_video_cv.set(cv2.VIDEOWRITER_PROP_NSTRIPES, -1)


while True:
    ret, frame = input_video_cv.read()

    #logger.info(counter)

    if ret:

        if frame.any() == None:
            break

        counter += 1

        #具体的抠图过程,见上文,不赘述。
        #...
        #得到frame002

        output_video_cv.write(frame002)

    else:
        input_video_cv.set(cv2.CAP_PROP_POS_FRAMES, 0)
        break

    if cv2.waitKey(5) == ord('q'):
        break

input_video_cv.release()
output_video_cv.release()  #写入了  output_video_file002

logger.info('视频更换背景成功,合计帧数:' + str(counter))

cv2.destroyAllWindows()

3.3 新视频文件的比特率

视频文件的比特率,决定了视频的清晰度,比特率越大,视频越清晰,同时,对应的视频文件也就越大。以上写视频文件的方法的主要问题是没有办法主动控制目标视频的比特率,因此需要引入包“vidgear”,通过其提供的方法,控制ffmpeg的参数。优化后的代码如下:

from vidgear.gears import WriteGear


output_params = {"-vcodec": "libx264", "-crf": 0, \
                 "-preset": "fast"}  # define (Codec,CRF,preset) FFmpeg tweak parameters for writer

output_video_cv = WriteGear(output=output_video_file002, compression_mode=True, logging=True,
                   **output_params)  # Define writer with output filename 'Output.mp4'


while True:
    ret, frame = input_video_cv.read()

    #logger.info(counter)

    if ret:

        if frame.any() == None:
            break

        counter += 1

        #具体的抠图过程,见上文,不赘述。
        #...
        #得到frame002

        output_video_cv.write(frame002)

    else:
        input_video_cv.set(cv2.CAP_PROP_POS_FRAMES, 0)
        break

    if cv2.waitKey(5) == ord('q'):
        break

input_video_cv.release()
output_video_cv.close()  #写入了  output_video_file002

logger.info('视频更换背景成功,合计帧数:' + str(counter))

cv2.destroyAllWindows()

同时,也有另一种不使用包“vidgear”,直接通过管道调用ffmpeg的方法,参考的代码如下,同上面代码的原理,控制帧率的参数为“-vcodec”。

# import packages
from PIL import Image
from subprocess import Popen, PIPE
from imutils.video import VideoStream
from imutils.object_detection import non_max_suppression
from imutils import paths
import cv2
import numpy as np
import imutils

# ffmpeg setup
p = Popen(['ffmpeg', '-y', '-f', 'image2pipe', '-vcodec', 'mjpeg', '-r', '24', '-i', '-', '-vcodec', 'h264', '-qscale', '5', '-r', '24', 'video.mp4'], stdin=PIPE)

video = cv2.VideoCapture('videos.mp4')

while True:
    ret, frame = video.read()
    if ret:
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        im = Image.fromarray(frame)
        im.save(p.stdin, 'JPEG')
    else:
        break

p.stdin.close()
p.wait()
video.release()
cv2.destroyAllWindows()

Python-OpenCV开发总结:替换视频绿幕并重新调整大小和位置

4 moviepy合成声音

以上处理过程中,因为是处理视频帧的,重新合成的视频文件没有声音,我们需要将源视频文件的声音合成到新的视频文件中,这个过程我们使用moviepy来完成。

from moviepy import editor
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.editor import VideoFileClip, CompositeVideoClip
from moviepy.audio.io.AudioFileClip import AudioFileClip


logger.info('开始合成声音')

audio = AudioFileClip(input_video_path)    # 分离声轨
clip002 = VideoFileClip(output_video_file002)
videoclip = clip002.set_audio(audio)  # 写入声轨

videoclip.write_videofile(output_video_file, audio=True,
                          fps=input_video_fps,
                          bitrate=str(input_video_bit_rate) + 'k' )

logger.info('运行结束')