OpenCV 实战:裁剪、缩放与仿射变换实现 Ken Burns 视频动画

部署运行你感兴趣的模型镜像

【精选优质专栏推荐】


每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

在这里插入图片描述

前言

当你使用 OpenCV 时,你最常处理的是图像。然而,你可能会发现将多张图像制作成动画很有用。快速连续显示图像可能会带来不同的洞察,或者通过引入时间轴来可视化你的工作会更容易。

在这篇文章中,你将看到如何在 OpenCV 中创建视频剪辑。作为示例,你还将学习一些基本的图像操作技巧来创建图像。具体而言,你将学习:

  • 如何将图像作为 numpy 数组进行操作

  • 如何使用 OpenCV 函数操作图像

  • 如何在 OpenCV 中创建视频文件

这篇文章分为两个部分:

  • Ken Burns 效果

  • 写入视频

Ken Burns 效果

你将通过参考其他文章来创建许多图像。也许是为了可视化你机器学习项目的进展,或者展示某个计算机视觉技术如何处理你的图像。为了简化操作,你将对输入图像做最简单的处理:裁剪。

本文的任务是创建 Ken Burns 效果。这是一种以电影制片人 Ken Burns 命名的平移和缩放技巧:

不是在屏幕上展示一张大且静态的照片,而是使用 Ken Burns 效果裁剪某个细节,然后在图像上平移。
—— 维基百科,“Ken Burns effect”

让我们看看如何在 Python 中使用 OpenCV 创建 Ken Burns 效果。我们从一张图像开始,例如下面的鸟类照片,你可以从维基百科下载:

https://upload.wikimedia.org/wikipedia/commons/b/b7/Hooded_mountain_tanager_%28Buthraupis_montana_cucullata%29_Caldas.jpg

在这里插入图片描述

这张图片的分辨率是 4563×3042。用 OpenCV 打开这张图片非常简单:

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 值表示。

Ken Burns 效果是缩放和平移。视频中的每一帧都是原图像的裁剪(然后再缩放以填满屏幕)。给定一个 numpy 数组来裁剪图像很容易,因为 numpy 已经提供了切片语法:

cropped = img[y0:y1, x0:x1]

图像是一个三维 numpy 数组。前两个维度分别表示高度和宽度(与矩阵坐标设置方式相同)。因此你可以使用 numpy 切片语法来获取垂直方向从 𝑦0 到 𝑦1、以及水平方向从 𝑥0 到 𝑥1 的像素(记住,在矩阵中坐标从上到下、从左到右编号)。

裁剪图片意味着把一张大小为 𝑊 × 𝐻 的图片变为更小的 𝑊′ × 𝐻′。为了制作视频,你希望创建固定尺寸的帧。因此裁剪得到的 𝑊′ × 𝐻′ 的图像需要被缩放。此外,为避免失真,裁剪后的图像也需要保持预定义的宽高比。

如果你自己实现缩放,你可以定义一个新的 numpy 数组,然后逐像素计算并填充值。像素值的计算方式很多,例如线性插值或直接复制最近像素。如果尝试自己实现缩放,你会发现虽然不难,但相当繁琐。因此更简单的方法是使用 OpenCV 的原生函数,例如:

resized = cv2.resize(cropped, dsize=target_dim, interpolation=cv2.INTER_LINEAR)

cv2.resize() 函数接收图像和目标尺寸(一个像素宽度和高度的元组),并返回新的 numpy 数组。你可以指定缩放算法,上述是使用线性插值,通常看起来效果不错。

这些基本上就是你能在 OpenCV 中操作图像的方式,即:

  • 直接操作 numpy 数组。这适用于需要逐像素处理的简单任务
  • 使用 OpenCV 函数。这适合需要整体操作图像或逐像素操作效率太低的复杂任务

有了这些,你可以构建你的 Ken Burns 动画。流程如下:

  1. 给定一张图片(最好是高分辨率的),你需要通过指定起始和结束的焦点坐标来定义平移路径。同时你也需要定义起始和结束的缩放比例。

  2. 你有预定义的视频时长和 FPS(帧率)。总帧数等于时长乘以 FPS。

  3. 对于每一帧,计算裁剪坐标,然后将裁剪后的图像缩放到视频的目标分辨率。

  4. 所有帧准备好之后,将其写入视频文件。

让我们从常量开始:假设我们要创建 2 秒钟的 720p 视频(分辨率 1280×720),帧率为 25 FPS(较低但视觉上可接受)。平移将从图像左侧 40%、顶部 60% 的中心位置开始,结束于图像左侧 50%、顶部 50% 的中心位置。缩放将从原图的 70% 开始,逐渐缩放到 100%。

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 帧)。因此创建一个裁剪函数是有益的:

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 的切片语法实现。

如果认为当前时间点是整个时长的 𝛼%,即可使用仿射变换计算缩放级别和平移位置。以平移中心的相对位置(宽高百分比)来表示,仿射变换为:

rx = end_center[0]*alpha + start_center[0]*(1-alpha)
ry = end_center[1]*alpha + start_center[1]*(1-alpha)

其中 alpha 在 0 到 1 之间。相同地,缩放级别为:

scale = end_scale*alpha + start_scale*(1-alpha)

给定原始图像尺寸和 scale,你可以通过乘法计算裁剪尺寸。但由于原图的宽高比可能与视频不一致,你应根据视频宽高比调整裁剪尺寸。假设图像数组为 img,缩放比例为 scale,裁剪尺寸可计算为:

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 循环,通过 numpy 的 linspace() 获取不同的 alpha 值来创建每一帧。完整代码如下:

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)
    # determined how to crop based on the aspect ratio of width/height
    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])
    # crop, scale to video size, and save the frame
    cropped = crop(img, x, y, w, h)
    scaled = cv2.resize(cropped, dsize=video_dim, interpolation=cv2.INTER_LINEAR)
    frames.append(scaled)
 
# write to MP4 file
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 对象:

vidwriter = cv2.VideoWriter("output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, video_dim)

与写入图像文件(如 JPEG 或 PNG)不同,OpenCV 创建的视频格式并不是从文件名推断出来的。它由第二个参数指定,即 FourCC,它是由四个字符组成的编码。你可以在以下网址找到 FourCC 代码及其对应的视频格式列表:

https://fourcc.org/codecs.php

然而,并非所有的 FourCC 都能使用。这是因为 OpenCV 使用 FFmpeg 工具来创建视频。你可以使用以下命令查看 FFmpeg 支持的视频格式列表:

ffmpeg -codecs

确保 ffmpeg 命令与 OpenCV 所使用的版本一致。还要注意,上述命令的输出只会告诉你 ffmpeg 支持哪些格式,而不是对应的 FourCC 代码。你需要从其他地方查找代码,例如前述的网址。

要检查你是否能使用某个特定的 FourCC 代码,你必须实际尝试并查看 OpenCV 是否抛出异常:

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 中创建视频。创建的视频是由一系列帧组成的(即没有音频)。每一帧都是固定大小的图像。作为示例,你学习了如何对图片应用 Ken Burns 效果,具体来说,你应用了:

  • 使用 numpy 切片语法裁剪图像的技巧

  • 使用 OpenCV 函数调整图像大小的技巧

  • 使用仿射变换计算缩放和平移参数并为视频创建帧

最后,你使用 OpenCV 中的 VideoWriter 对象将这些帧写入视频文件。

您可能感兴趣的与本文相关的镜像

PyTorch 2.9

PyTorch 2.9

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋说

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值