Python玩转视频剪辑 - Opencv、Moviepy(附完整案例)

1. 准备工作

1.1 安装Opencv-python、Moviepy

pip install opencv-python
pip install moviepy

1.2 视频剪辑目标

如图,作者从b站下载了两个视频(仅做代码测试用,不作转载等任何商业用途),一个是刘初寻的疏远(以下简称视频一)、一个是有名的敢杀我的马(以下简称视频二),两个视频都有明显的水印,本文主要工作是去除整个视频的水印,并把两个视频拼接起来成为一个完整的视频。

2. 去除水印

2.1 截取视频的一帧图片

我们知道视频都是由若干张图片组合而成,帧率代表1s有几张图片,比如120帧的8s视频则有960张图片,我们利用cv2.VideoCapture函数来获取视频的一帧图片(注:为了获取水印模版,建议获取水印比较明显、周围像素变化不大的图片,以便水印的提取):

def Get_Video_Image(filedir, savedir = None, second = None):
    """
        截取视频一帧图片函数:  
        filedir: 视频原文件路径   
        savedir: 剪辑视频文件保存路径, 若不保存则返回图片  
        second: 截取第几秒的图片  
    """
    cap = cv2.VideoCapture(filedir)  # 打开视频文件
    frames = cap.get(cv2.CAP_PROP_FRAME_COUNT)  # 获得视频文件的帧数
    fps = cap.get(cv2.CAP_PROP_FPS)  # 获得视频文件的帧率
    width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)  # 获得视频文件的帧宽
    height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)  # 获得视频文件的帧高
    second = 0 if second is None else second
    for pos in tqdm(range(int(second*fps))):
        ret, frame = cap.read()  # 捕获一帧图像
    cap.release()

    if savedir is not None:
        cv2.imwrite(savedir, frame)
    return frame
INPUT_DIR1 = "C:/Users/user/Desktop/bilibili_video/疏远.mp4"
OUTPUT_DIR1 = "C:/Users/user/Desktop/bilibili_video/capture1.jpg"

INPUT_DIR2 = "C:/Users/user/Desktop/bilibili_video/敢杀我的马.mp4"
OUTPUT_DIR2 = "C:/Users/user/Desktop/bilibili_video/capture2.jpg"

Get_Video_Image(INPUT_DIR1, OUTPUT_DIR1, 4) # 截取第4s的图片
Get_Video_Image(INPUT_DIR2, OUTPUT_DIR2, 8) # 截取第8s的图片

如代码,我们截取了视频一第4s的图片和视频二第8s的图片,效果如下:

2.2 获取水印的位置范围

为了方便水印的提取,我们需要获取水印的位置。以下程序运行后用鼠标点击我们想要获取坐标的区域,即可获得其像素点坐标。结束方式:敲击键盘“q”,回车。代码改编自:https://blog.csdn.net/People1007/article/details/122420735

def ON_EVENT_LBUTTONDOWN(event, x, y, flags, param):
    img = param["image"] # 传进图片参数
    if event == cv2.EVENT_LBUTTONDOWN:
        xy = "%d,%d" % (x, y)
        print(x, y)
        cv2.circle(img, (x, y), 2, (0, 0, 255))
        cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,1.0, (0,0,255)) # 把坐标画在图片上
        cv2.imshow("image", img)

def Get_Position(filedir):
    """
        获取图片位置函数:  
        filedir: 视频原文件路径 
    """
    img = cv2.imread(filedir)
    cv2.namedWindow("image", cv2.WINDOW_NORMAL)
    cv2.setMouseCallback("image", ON_EVENT_LBUTTONDOWN, {"image": img})
    while(1):
        cv2.imshow("image", img)
        key = cv2.waitKey(2) & 0xFF
        if key == ord('q'): # 按q则退出图片展示
            break
    cv2.destroyAllWindows()
Get_Position(OUTPUT_DIR1) # 得到水印的位置(行列)
Get_Position(OUTPUT_DIR2) # 得到水印的位置(行列)

运行程序效果图如下:

如图,视频一水印行像素点从23到70,列像素点从974到1259(范围需要完全覆盖住水印)

视频二有两个水印:

水印一:行像素点从115到178,列像素点从135到369

水印二:行像素点从25到91,像素点从1676到1902

2.3 获取水印模版

本文获取水印模板的方法是在水印范围内,对比水印与周围像素点的差异,进而把水印的像素点提取出来(水印位置多扩大点没事,尽量覆盖,要让字体尽量粗,不然会有水印边缘留存)。比如图片一通过观察发现水印的R像素大于60,图片二发现水印的RGB像素大于195:

image = cv2.imread(OUTPUT_DIR1)
image_new = image.copy() # 复制一张相同规格的图片
image_new.fill(255) # 空白图片
for row in range(23, 70): # 水印行从23到70
    for col in range(974, 1259): # 水印列从974到1259
        if image[row][col][0] > 60: # 通过观察发现水印的R像素大于60, 水印位置多扩大点没事, 尽量覆盖
            image_new[row][col] = np.array([0, 0, 0])
cv2.imwrite(MASK_DIR1, image_new)

image = cv2.imread(OUTPUT_DIR2)
image_new = image.copy() # 复制一张相同规格的图片
image_new.fill(255) # 空白图片
for row in range(115, 178): # 水印行从115到178
    for col in range(135, 369): # 水印列从135到369
        if image[row][col][0] > 195 and image[row][col][1] > 195 and image[row][col][2] > 195: # 通过观察发现水印的RGB像素大于195, 水印位置多扩大点没事, 尽量覆盖
            image_new[row][col] = np.array([0, 0, 0])
for row in range(25, 91): # 水印行从25到91
    for col in range(1676, 1902): # 水印列从1676到1902
        if image[row][col][0] > 195 and image[row][col][1] > 195 and image[row][col][2] > 195: # 通过观察发现水印的RGB像素大于195, 水印位置多扩大点没事, 尽量覆盖
            image_new[row][col] = np.array([0, 0, 0])
cv2.imwrite(MASK_DIR2, image_new)

提取的水印模版如下:

2.4 去除水印

2.4.1 水印用相邻像素点填充

如果直接把含有水印的区域用同种颜色覆盖,那么会显得非常突兀,一种自适应的办法就是用相邻的像素点来填充水印所在的像素点(之前确定水印位置的好处还在于可以加速,填充水印时只扫描该部分区域,不然全图扫描太慢了):

def ImageWaterCancel(mask: np.array, image: np.array, mask_ranges = None) -> np.array:
    """
        除水印函数:  
        mask: 水印图片对象  
        image: 原图片对象  
        mask_ranges: 三维数组, 多个水印对应范围, 加速用, 若不传入则全图扫描  
    """
    new_image = image # 创建一张一样的图像用于保存
    cur_ele = np.array([255, 255, 255]) # 初始默认用空白元素填充
    if mask_ranges is None:
        mask_range = [[[0, image.shape[0]], [0, image.shape[1]]]]
    for mask_range in mask_ranges:
        cur_ele = np.array([255, 255, 255])
        for row in range(mask_range[0][0], mask_range[0][1]):
            for col in range(mask_range[1][0], mask_range[1][1]):
                if not (mask[row, col] == np.array([255, 255, 255])).all():
                    new_image[row, col] = cur_ele # 用最近非水印的元素填充
                else:
                    new_image[row, col] = image[row, col]
                    cur_ele = image[row, col]
    return new_image
PATH = "C:/Users/user/Desktop/bilibili_video"
MASK_DIR1, MASK_DIR2 = os.path.join(PATH, "mask1.jpg"), os.path.join(PATH, "mask2.jpg")  
TEST_DIR1, TEST_DIR2 = os.path.join(PATH, "test1.jpg"), os.path.join(PATH, "test2.jpg")
mask1, mask2 = cv2.imread(MASK_DIR1), cv2.imread(MASK_DIR2)
image1, image2 = cv2.imread(TEST_DIR1), cv2.imread(TEST_DIR2)

image1 = ImageWaterCancel(mask1, image1, [[[24, 65], [975, 1264]]]) # 对应水印位置
image2 = ImageWaterCancel(mask2, image2, [[[115, 178], [135, 369]], [[25, 91], [1676, 1902]]])

cv2.imwrite("test1_cancel.jpg", image1)
cv2.imwrite("test2_cancel.jpg", image2)

让我们来看看去除水印的效果:

图片一填充水印前:

填充水印后:

图片二填充水印前:

填充水印后:

可以看出,水印确实不怎么认得出来了,但还是有一点小痕迹,像打了马赛克一样,为了进一步增强去除水印的效果,下面介绍一种图像平滑的方法。

2.4.2 图像平滑

图像平滑是一种区域增强的算法,通过减少图像像素点和周围像素点的差,来使得图像平滑。常见的平滑算法有邻域平均法、中值滤波、边界保持类滤波等,详细可见:https://blog.csdn.net/zaishuiyifangxym/article/details/89788020

def ImageBlur(image: np.array, mask_ranges = None, blur = "median", ksize = 5, sigmax = 0):
    """
        滤波函数: 支持均值滤波、中值滤波、高斯滤波  
        image: 图片, np.array  
        mask_ranges: 三维数组, 水印对应范围, 加速用, 若不传入则全图滤波  
        ksize: 卷积核的大小, 默认为5  
        sigmax: 只在高斯滤波用, 表示X方向方差  
    """  
    if mask_ranges is None:
        mask_ranges = [[[0, image.shape[0]], [0, image.shape[1]]]]
    for mask_range in mask_ranges:
        row_s, row_e, col_s, col_e = mask_range[0][0], mask_range[0][1], mask_range[1][0], mask_range[1][1]
        if blur == "median":
            image[row_s: row_e, col_s: col_e, :] = cv2.medianBlur(image[row_s: row_e, col_s: col_e, :], ksize) # 中值滤波
        elif blur == "gauss":
            image[row_s: row_e, col_s: col_e, :] = cv2.GaussianBlur(image[row_s: row_e, col_s: col_e, :], (ksize, ksize), sigmax) # 高斯滤波
        else:
            image[row_s: row_e, col_s: col_e, :] = cv2.medianBlur(image[row_s: row_e, col_s: col_e, :], (ksize, ksize)) # 均值滤波
    return image
image1 = ImageBlur(image1, [[[24, 65], [975, 1264]]], "median", 11)
image2 = ImageBlur(image2, [[[115, 178], [135, 369]], [[25, 91], [1676, 1902]]], "median", 11)
cv2.imwrite("test1_cancel_blur.jpg", image1)
cv2.imwrite("test2_cancel_blur.jpg", image2)

图像平滑效果图如下:

看图可见,是不是水印只剩下一点点痕迹了(完美去除很难),这才是我们想要的去除水印的效果。

2.4.3 全视频除水印

我们知道视频都是由若干张图片组合而成,视频除水印等价于对每张图片进行除水印:

def CaptureVideo(filedir, savedir, cut_time = None, resolution = None, maskdir = None, mask_ranges = None, blur = None, ksize = 5):
    """ 
        剪辑视频函数:  
        filedir: 视频原文件路径  
        savedir: 剪辑视频文件保存路径   
        cut_time: 剪辑视频起始、结束时间    
        resolution: 自定义分辨率[width, height]  
        maskdir: 水印图片位置   
        mask_ranges: 三维数组, 水印对应范围, 加速用, 若不传入则全图扫描  
        blur: 滤波函数, 若不传入则不进行图片平滑处理  
        ksize: 卷积核的大小, 默认为5   
        注: 该函数生成的视频无音频, 需要再拼接音频, 此处cut_time剪切视频只是为了测试用, 建议不要在这里剪视频片段然后用moviepy合并音频(因为有一些视频帧率不为整数, 
        在剪切视频和合并音频可能会对不上), 直接用moviepy剪切视频然后合并音频最合适
    """
    cap = cv2.VideoCapture(filedir) # 读取视频文件
    frames = cap.get(cv2.CAP_PROP_FRAME_COUNT)  # 获得视频文件的帧数
    fps = cap.get(cv2.CAP_PROP_FPS)  # 获得视频文件的帧率
    if resolution is None: # 自定义分辨率
        width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)  # 获得视频文件的帧宽
        height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)  # 获得视频文件的帧高
    else:
        width = resolution[0]
        height = resolution[1]
    if maskdir is not None: # 去除水印
        mask = cv2.imread(maskdir)

    # 创建保存视频文件类对象
    fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 定义视频文件类型
    out = cv2.VideoWriter(savedir, fourcc, fps, (int(width), int(height))) # 剪辑视频对象
    if cut_time is None:
        start = 0
        end = int(frames)
    else:
        start = int(cut_time[0] * fps)
        end = int(cut_time[1] * fps)
    cap.set(cv2.CAP_PROP_POS_FRAMES, start * fps)
    for pos in tqdm(range(start, end)):
        ret, frame = cap.read()  # 捕获一帧图像
        if maskdir is not None: # 除水印
            frame = ImageWaterCancel(mask, frame, mask_ranges) 
            if blur is not None:
                frame = ImageBlur(frame, mask_ranges, blur, ksize)
        if resolution is not None:
            frame = cv2.resize(frame, resolution) # 改变分辨率
        out.write(frame)  # 保存帧

    cap.release() # 释放视频对象
    out.release()
INPUT_DIR1, OUTPUT_DIR1 = os.path.join(PATH, "疏远.mp4"), os.path.join(PATH, "疏远_剪辑.mp4")
MASK_DIR1 = os.path.join(PATH, "mask1.jpg") # 只含有水印的图片
INPUT_DIR2, OUTPUT_DIR2 = os.path.join(PATH, "敢杀我的马.mp4"), os.path.join(PATH, "敢杀我的马_剪辑.mp4")
MASK_DIR2 = os.path.join(PATH, "mask2.jpg") # 只含有水印的图片
# 视频剪辑、除水印、拼接
CaptureVideo(INPUT_DIR1, OUTPUT_DIR1, resolution = [1280, 720], maskdir = MASK_DIR1, 
                           mask_ranges = [[[24, 65], [975, 1264]]], blur = "median", ksize = 11)
CaptureVideo(INPUT_DIR2, OUTPUT_DIR2, resolution = [1280, 720], maskdir = MASK_DIR2, 
                           mask_ranges = [[[115, 178], [135, 369]], [[25, 91], [1676, 1902]]], blur = "median", ksize = 11)

运行上述代码,可以得到疏远_剪辑.mp4、敢杀我的马_剪辑两个视频,此时视频已经完全除去水印,但是视频是没有声音的(因为cv2.VideoCapture只能读取图片)。

3. 视频拼接音频

3.1 视频拼接

def MergeVideos(filedirs: list, cut_time = None):
    """
        视频拼接函数:  
        filedirs: 视频原文件路径, list  
        mask_ranges: 三维数组, 水印对应范围, 加速用, 若不传入则全图扫描  
    """    
    all_vedios = []
    for i in range(len(filedirs)):
        filedir = filedirs[i]
        if cut_time is None or cut_time[i] is None: # 如果不传入此参数或者改视频不剪切, 则直接加入
            all_vedios.append(VideoFileClip(filedir))
        else:
            all_vedios.append(VideoFileClip(filedir).subclip(cut_time[i][0], cut_time[i][1]))
    return concatenate_videoclips(all_vedios)
# 视频拼接
final_video = video_process.MergeVideos([OUTPUT_DIR1, OUTPUT_DIR2]) # 有声音的视频拼接完还是有声音, 无声音的视频拼完得拼接音频

作者运用Moviepy库,编写了一个视频拼接函数,有声音的视频拼接完有声音,无声音的视频拼完得拼接音频,同时支持剪辑视频,通过cut_time参数传入剪辑视频起始点。(注:视频必须分辨率相同才可以拼接,不然拼接的视频会出现画面雪花的迹象)

3.2 音频拼接

def MergeAudios(filedirs: list, cut_time = None):
    """
        音频拼接函数:  
        filedirs: 音频原文件路径, list  
        cut_time: 二维数组, 剪辑音频起始、结束时间
    """    
    all_audios = []
    for i in range(len(filedirs)):
        filedir = filedirs[i]
        if cut_time is None or cut_time[i] is None: # 如果不传入此参数或者该视频不剪切, 则直接加入
            all_audios.append(AudioFileClip(filedir))
        else:
            all_audios.append(AudioFileClip(filedir).subclip(cut_time[i][0], cut_time[i][1]))
    return concatenate_audioclips(all_audios)
# 音频拼接
final_audio = MergeAudios([INPUT_DIR1, INPUT_DIR2])

作者运用Moviepy库,编写了一个音频拼接函数,同时支持剪辑音频,通过cut_time参数传入剪辑视频起始点。

3.3 合并视频和音频

# 将音频剪辑与视频同步
synced_audio = final_audio.set_duration(final_video.duration)
# 合并视频和音频
final_clip = concatenate_videoclips([final_video.set_audio(synced_audio)])
# 输出合并后的视频
final_clip.write_videofile(os.path.join(PATH, "combined_video.mp4"))
# final_clip.write_videofile(os.path.join(PATH, "combined_video.wmv"), codec = "mpeg4") # 其它格式需要指定codec

将视频与音频合并后,就可以得到有声音的、去除水印的拼接视频啦!文章涉及的数据集和源代码可从Github下载:https://github.com/CNLCNL/VideoCapture,希望这些方法和技巧能对大家有所帮助,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值