使用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,希望这些方法和技巧能对大家有所帮助,谢谢!
作者:藏在云中的猫