什么是帧
DVD 电影中的场景、从 YouTube 下载的剪辑、通过网络摄像头拍摄的内容。。。无论是视频还是动画,都是由一系列静止的图像组成。然后,这些图像会一个接一个的播放,让你的眼睛误以为物体在移动。图像的播放速度越快,动作看起来越流畅,画面也越逼真。
一般来说,想要达到自然平滑的动态效果,播放速率应该在每秒 24-30 张图像之间,每个图像被称为一帧。因此,我们通常会看到 FPS(每秒帧数)这个词,它突出显示了移动速度的细节,因此而得名。
举一个栗子 - 走路,我们看到的是这样的:
其实,真实情况是这样的:
视频文件也一样,只不过它是将所有帧存储在一起并按顺序播放。对于一个典型的电影来说,存储的总帧数甚至可以达到数十万。如果要捕获其中的某一帧图像,则非常简单,只需暂停视频并按 Print Screen 键即可。
但倘若要从一个视频剪辑中提取多个连续的帧,甚至是所有帧,那么一次捕捉一个图像是非常低效和费时的。出于这个原因,可以用 libVLC 实现一个程序,用于提取想要的视频帧,并自动保存到图像文件(例如:jpg 或 png)中。
| 版权声明:一去、二三里,未经博主允许不得转载。
核心 API
要提取视频中的每一帧,主要涉及以下核心 API。
先来看第一个 - libvlc_video_set_callbacks()
,用于设置回调和私有数据,将解码后的视频渲染到内存中的自定义区域:
/**
mp:媒体播放器
lock:回调锁定视频内存(不能为 NULL)
unlock:回调解锁视频内存(如果不需要,则为 NULL)
display:回调以显示视频(如果不需要,则为 NULL)
opaque:三个回调的私有指针(作为第一个参数)
**/
LIBVLC_API void libvlc_video_set_callbacks(libvlc_media_player_t* mp,
libvlc_video_lock_cb lock,
libvlc_video_unlock_cb unlock,
libvlc_video_display_cb display,
void* opaque
)
这个函数包含了五个参数,其中有三个是函数指针:
// 当需要解码新的视频帧时,就会调用 lock 回调。
typedef void*(* libvlc_video_lock_cb) (void *opaque, void **planes)
// 当视频帧解码完成后,将调用 unlock 回调。
typedef void(* libvlc_video_unlock_cb) (void *opaque, void *picture, void *const *planes)
// 当视频帧需要显示时,由媒体回放时钟决定,将调用 display 回调。
typedef void(* libvlc_video_display_cb) (void *opaque, void *picture)
此外,还可使用 libvlc_video_set_format()
或者 libvlc_video_set_format_callbacks()
配置解码的格式。例如,设置解码后的视频色度和尺寸:
/**
mp:媒体播放器
chroma:标识色度的四个字符的字符串(例如:RV32 或 YUV)
width:像素宽度
height:像素高度
pitch:线间距(以字节为单位)
**/
LIBVLC_API void libvlc_video_set_format (libvlc_media_player_t* mp,
const char* chroma,
unsigned width,
unsigned height,
unsigned pitch
)
提取每一帧
现在,是时候一展身手了,来提取一个 RTSP 流中的连续帧:
视频比较长,为了演示,只播放一段时间(这里是 20 秒),然后提取这段时间中的帧数据:
#include <windows.h>
#include <vlc/vlc.h>
#include <QImage>
#include <QMutex>
#include <QCoreApplication>
// 定义输出视频的分辨率
#define VIDEO_WIDTH 640
#define VIDEO_HEIGHT 480
struct context {
QMutex mutex;
uchar *pixels;
};
static void *lock(void *opaque, void **planes)
{
struct context *ctx = (context *)opaque;
ctx->mutex.lock();
// 告诉 VLC 将解码的数据放到缓冲区中
*planes = ctx->pixels;
return NULL;
}
// 获取 argb 图片并保存到文件中
static void unlock(void *opaque, void *picture, void *const *planes)
{
Q_UNUSED(picture);
struct context *ctx = (context *)opaque;
unsigned char *data = (unsigned char *)*planes;
static int frameCount = 1;
QImage image(data, VIDEO_WIDTH, VIDEO_HEIGHT, QImage::Format_ARGB32);
image.save(QString("frame_%1.png").arg(frameCount++));
ctx->mutex.unlock();
}
static void display(void *opaque, void *picture)
{
Q_UNUSED(picture);
(void)opaque;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
libvlc_instance_t *vlcInstance;
libvlc_media_player_t *mediaPlayer;
libvlc_media_t *media;
// 等待 20 秒
int waitTime = 1000 * 20;
struct context ctx;
ctx.pixels = new uchar[VIDEO_WIDTH * VIDEO_HEIGHT * 4];
memset(ctx.pixels, 0, VIDEO_WIDTH * VIDEO_HEIGHT * 4);
// 创建并初始化 libvlc 实例
vlcInstance = libvlc_new(0, NULL);
// 创建一个 media,参数是一个媒体资源位置(例如:有效的 URL)。
media = libvlc_media_new_location(vlcInstance, "rtsp://184.72.239.149/vod/mp4:BigBuckBunny_175k.mov");
libvlc_media_add_option(media, ":avcodec-hw=none");
// 创建一个 media player 播放环境
mediaPlayer = libvlc_media_player_new_from_media(media);
// 现在,不需要保留 media 了
libvlc_media_release(media);
// 设置回调,用于提取帧或者在屏幕中显示。
libvlc_video_set_callbacks(mediaPlayer, lock, unlock, display, &ctx);
libvlc_video_set_format(mediaPlayer, "RGBA", VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_WIDTH * 4);
// 播放 media player
libvlc_media_player_play(mediaPlayer);
// 让它播放一会
Sleep(waitTime);
// 停止播放
libvlc_media_player_stop(mediaPlayer);
// 释放 media player
libvlc_media_player_release(mediaPlayer);
// 释放 libvlc 实例
libvlc_release(vlcInstance);
return a.exec();
}
这里为了保存图片,我们用到了 Qt 中的一个类 - QImage。该类提供了与硬件无关的图像表示,允许直接访问像素数据,并可用作绘图设备。
注意: 后面会将 libVLC 和 Qt 相结合,分享一些 GUI 相关的小程序,如果对 Qt 不太了解,建议抽空学习一下。
Github 源码地址:ExtractFrames