在使用OpenCV时,您最常处理图像。但是,您可能会发现从多个图像创建动画很有用。快速连续显示图像可能会给您不同的见解,或者通过引入时间轴可以更容易地可视化您的工作。
在这篇文章中,您将了解如何在OpenCV中创建视频片段。作为一个示例,您还将学习一些基本的图像处理技术来创建图像。特别是,您将学习:
- 如何将图像作为Numpy数组进行操作
- 如何使用OpenCV函数操作图像
- 如何在OpenCV中创建视频文件
通过我的书《OpenCV 机器学习》启动您的项目。它提供了带有可用代码的自学教程。
让我们开始吧。

如何使用 OpenCV 变换图像和创建视频
照片由 KAL VISUALS 提供。保留部分权利。
概述
这篇文章分为两部分:
- 肯·伯恩斯效应 (Ken Burns Effect)
- 写入视频
肯·伯恩斯效应 (Ken Burns Effect)
您将通过阅读其他帖子创建大量图像。这可能是为了可视化您的机器学习项目的一些进展,或者展示计算机视觉技术如何操作您的图像。为了简化操作,您将对输入图像进行最简单的操作:裁剪。
这篇文章的任务是创建**肯·伯恩斯效应 (Ken Burns effect)**。这是一种以电影制片人肯·伯恩斯命名的平移和缩放技术。
肯·伯恩斯效应不是在屏幕上显示一张大型静态照片,而是裁剪细节,然后在图像上平移。
— 维基百科,“肯·伯恩斯效应”
让我们看看如何使用OpenCV在Python代码中创建肯·伯恩斯效应。我们从一张图像开始,例如下面这张您可以从维基百科下载的鸟类图片:

一张《兜帽山唐纳雀 (Buthraupis montana cucullata)》的图片。摄影师:Charles J. Sharp。(CC-BY-SA)
这张图片的分辨率为4563×3042像素。使用OpenCV打开这张图片很简单:
1 2 3 4 5 6 7 |
import cv2 imgfile = "Hooded_mountain_tanager_(Buthraupis_montana_cucullata)_Caldas.jpg" img = cv2.imread(imgfile, cv2.IMREAD_COLOR) cv2.imshow("bird", img) cv2.waitKey(0) |
OpenCV读取的图像`img`确实是一个形状为(3042, 4563, 3)的Numpy数组,数据类型为`uint8`(8位无符号整数),因为它是彩色图像,每个像素由0到255之间的BGR值表示。
肯·伯恩斯效应是缩放和平移。视频中的每一帧都是原始图像的裁剪(然后放大以填充屏幕)。给定Numpy数组,裁剪图像很简单,因为Numpy已经提供了切片语法:
1 |
cropped = img[y0:y1, x0:x1] |
图像是一个三维的Numpy数组。前两个维度分别代表高度和宽度(与设置矩阵坐标的方式相同)。因此,您可以使用Numpy切片语法在垂直方向上获取 $y_0$ 到 $y_1$ 像素,在水平方向上获取 $x_0$ 到 $x_1$ 像素(请记住,在矩阵中,坐标从上到下,从左到右编号)。
裁剪图片意味着将尺寸为 $W \times H$ 的图片变为尺寸较小的 $W' \times H'$。为了制作视频,您需要创建固定尺寸的帧。裁剪后的尺寸 $W' \times H'$ 将需要调整大小。此外,为了避免失真,裁剪后的图像还需要保持预定义的纵横比。
要调整图像大小,您可以定义一个新的Numpy数组,然后逐个计算并填充像素值。有许多方法可以计算像素值,例如使用线性插值或直接复制最近的像素。如果您尝试实现调整大小操作,您会发现它并不难,但仍然相当繁琐。因此,更简单的方法是使用OpenCV的本机函数,例如以下内容:
1 |
resized = cv2.resize(cropped, dsize=target_dim, interpolation=cv2.INTER_LINEAR) |
函数`cv2.resize()`接受图像和目标尺寸(作为(宽度, 高度)的元组)并返回一个新的Numpy数组。您可以指定调整大小的算法。上面使用的是线性插值,在大多数情况下看起来不错。
这些基本上是您在OpenCV中操作图像的所有方式,即:
- 直接操作Numpy数组。这对于需要处理像素级别的简单任务非常有效。
- 使用OpenCV函数。这更适用于复杂任务,您需要考虑整个图像或者单独操作每个像素效率太低。
有了这些,你就可以制作你的肯·伯恩斯动画了。流程如下:
- 给定一张图像(最好是高分辨率的),您需要通过指定起始和结束焦点坐标来定义平移。您还需要定义起始和结束缩放比例。
- 您有一个预定义的视频时长和FPS(每秒帧数)。视频的总帧数是时长乘以FPS。
- 对于每一帧,计算裁剪坐标。然后将裁剪后的图像调整为视频的目标分辨率。
- 所有帧准备好后,您将其写入视频文件。
让我们从常数开始:假设我们将创建一个两秒钟的720p视频(分辨率1280×720),帧率为25 FPS(这有点低,但在视觉上可以接受)。平移将从图像左侧40%、顶部60%的中心开始,并在图像左侧50%、顶部50%的中心结束。缩放将从原始图像的70%开始,然后缩小到100%。
1 2 3 4 5 6 7 8 |
imgfile = "Hooded_mountain_tanager_(Buthraupis_montana_cucullata)_Caldas.jpg" video_dim = (1280, 720) fps = 25 duration = 2.0 start_center = (0.4, 0.6) end_center = (0.5, 0.5) start_scale = 0.7 end_scale = 1.0 |
您将多次裁剪图像以创建帧(精确地说,有2×25=50帧)。因此,创建一个裁剪函数是有益的:
1 2 3 4 |
def crop(img, x, y, w, h): x0, y0 = max(0, x-w//2), max(0, y-h//2) x1, y1 = x0+w, y0+h return img[y0:y1, x0:x1] |
这个裁剪函数接受一个图像、像素坐标中的暂定中心位置以及像素数量的宽度和高度。裁剪将确保它不会超出图像边界,因此使用了两个`max()`函数。裁剪是使用Numpy切片语法完成的。
如果您认为当前时间点处于整个持续时间的 $\alpha$% 时,您可以使用仿射变换来计算精确的缩放级别和平移位置。就平移中心(相对于原始宽度和高度的百分比)的相对位置而言,仿射变换给出:
1 2 |
rx = end_center[0]*alpha + start_center[0]*(1-alpha) ry = end_center[1]*alpha + start_center[1]*(1-alpha) |
其中`alpha`在0到1之间。类似地,缩放级别为:
1 |
scale = end_scale*alpha + start_scale*(1-alpha) |
给定原始图像大小和比例,您可以通过乘法计算裁剪图像的大小。但是,由于图像的纵横比可能与视频不同,因此您应该调整裁剪尺寸以适应视频的纵横比。假设图像Numpy数组是`img`,并且缩放级别为上面计算的`scale`,则裁剪大小可以计算如下:
1 2 3 4 5 6 7 8 |
orig_shape = img.shape[:2] if orig_shape[1]/orig_shape[0] > video_dim[0]/video_dim[1]: h = int(orig_shape[0]*scale) w = int(h * video_dim[0] / video_dim[1]) else: w = int(orig_shape[1]*scale) h = int(w * video_dim[1] / video_dim[0]) |
以上代码通过比较图像和视频的纵横比(宽度除以高度),并使用缩放级别来确定较小边缘的尺寸,然后根据目标纵横比计算另一个边缘。
一旦您知道需要多少帧,就可以使用for循环以不同的仿射参数`alpha`创建每一帧,该参数可以使用numpy函数`linspace()`获得。完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import cv2 import numpy as np imgfile = "Hooded_mountain_tanager_(Buthraupis_montana_cucullata)_Caldas.jpg" video_dim = (1280, 720) fps = 25 duration = 2.0 start_center = (0.4, 0.6) end_center = (0.5, 0.5) start_scale = 0.7 end_scale = 1.0 img = cv2.imread(imgfile, cv2.IMREAD_COLOR) orig_shape = img.shape[:2] def crop(img, x, y, w, h): x0, y0 = max(0, x-w//2), max(0, y-h//2) x1, y1 = x0+w, y0+h return img[y0:y1, x0:x1] num_frames = int(fps * duration) frames = [] for alpha in np.linspace(0, 1, num_frames): rx = end_center[0]*alpha + start_center[0]*(1-alpha) ry = end_center[1]*alpha + start_center[1]*(1-alpha) x = int(orig_shape[1]*rx) y = int(orig_shape[0]*ry) scale = end_scale*alpha + start_scale*(1-alpha) # 根据宽/高的纵横比确定如何裁剪 if orig_shape[1]/orig_shape[0] > video_dim[0]/video_dim[1]: h = int(orig_shape[0]*scale) w = int(h * video_dim[0] / video_dim[1]) else: w = int(orig_shape[1]*scale) h = int(w * video_dim[1] / video_dim[0]) # 裁剪,缩放到视频大小,并保存帧 cropped = crop(img, x, y, w, h) scaled = cv2.resize(cropped, dsize=video_dim, interpolation=cv2.INTER_LINEAR) frames.append(scaled) # 写入MP4文件 vidwriter = cv2.VideoWriter("output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, video_dim) for frame in frames: vidwriter.write(frame) vidwriter.release() |
最后几行是您如何使用OpenCV写入视频。您创建一个`VideoWriter`对象,并指定FPS和分辨率。然后,您逐帧写入,并释放对象以关闭写入的文件。
创建的视频就像 这个。预览如下:
创建视频的预览。观看此视频需要支持的浏览器。
写入视频
从上一节的示例中,您看到了我们如何创建一个`VideoWriter`对象:
1 |
vidwriter = cv2.VideoWriter("output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, video_dim) |
与您可能写入图像文件(如JPEG或PNG)的方式不同,OpenCV创建的视频格式不是从文件名推断出来的。它是指定视频格式的第二个参数,即**FourCC**,它是一个由四个字符组成的代码。您可以在以下URL的列表中找到FourCC代码和相应的视频格式:
然而,并非所有的FourCC代码都可以使用。这是因为OpenCV使用FFmpeg工具创建视频。您可以使用以下命令查找支持的视频格式列表:
1 |
ffmpeg -codecs |
请确保`ffmpeg`命令与OpenCV使用的相同。另请注意,上述命令的输出只告诉您ffmpeg支持哪些格式,而不是相应的FourCC代码。您需要从其他地方查找代码,例如前面提到的URL。
要检查您是否可以使用特定的FourCC代码,您必须尝试并查看OpenCV是否引发异常:
1 2 3 4 5 6 7 |
try: fourcc = cv2.VideoWriter_fourcc(*"mp4v") writer = cv2.VideoWriter('temp.mkv', fourcc, 30, (640, 480)) assert writer.isOpened() print("Supported") except: print("Not supported") |
想开始学习 OpenCV 机器学习吗?
立即参加我的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
总结
在这篇文章中,您学习了如何在OpenCV中创建视频。创建的视频是由一系列帧(即无音频)构建的。每帧都是固定大小的图像。作为一个示例,您学习了如何将肯·伯恩斯效应应用于图片,其中您特别应用了:
- 使用Numpy切片语法裁剪图像的技术
- 使用OpenCV函数调整图像大小的技术
- 使用仿射变换计算缩放和平移参数,并创建视频帧
最后,您使用OpenCV中的`VideoWriter`对象将帧写入视频文件。
非常棒的教程!一个问题:为了对称性,使用以下方式是否更合适?
y = int(orig_shape[0]*ry)
而不是
y = int(orig_shape[0]*rx)?
嗨,Nuno……感谢您的反馈!您可能是对的!请继续您的建议,并告诉我们您的发现。
非常感谢!
一篇精彩的帖子。