基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换

news2024/11/17 16:50:30

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
  3. 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码

文章目录

  • 系列文章目录
  • 前言
  • 像素格式
  • AVFrame 的像素格式转换
    • PPM 文件
    • Linesize 是什么?
    • 保存 PPM 文件的代码解释
    • 使用 sws_scale 进行像素格式转换
  • 总结
  • 参考


前言

经过前面三章的学习,我们快要完成我们的目标任务了:使用 ffmpeg 解码视频,并将解码后的视频帧保存在本地(就像对视频截图一样)。

现在就差临门一脚,如何将解码后的视频帧保存到本地呢?这是今天要讨论的内容。

本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 01: Making Screencaps。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。

本文的代码在 ffmpeg_video_player_tutorial-tutorial01。

像素格式

在使用 FFmpeg 进行视频处理时,AVPacket 表示被算法压缩后的视频数据,对 AVPacket 进行解码后,我们可以得到 AVFrame,它是用来表示视频画面的一个数据结构,包含了该画面的宽度、高度、像素格式等信息。

像素格式(Pixel format)是一种用于表示和存储数字图像中每个像素数据的方式。它描述了每个像素中哪些颜色通道是可用的,以及这些通道的顺序、比特深度和其他属性。像素格式决定了图像数据在内存中的存储布局。常见的像素格式包括:

  • RGB:每个像素包含红、绿、蓝三种颜色通道。
  • RGBA:每个像素包含红、绿、蓝和透明度四种通道。
  • YUV:每个像素表示为亮度(Y),色差(U)和饱和度(V)三个通道。
  • Grayscale:图像只有一个颜色通道,通常表示为灰度值。

这些格式可能有不同的比特深度,比如8位、16位等,表示每个颜色通道的数据精度。不同的像素格式和比特深度将影响颜色的展示和图像的质量,同时也决定了文件大小和处理速度。在计算机图形、视频编码和图像处理中,选择合适的像素格式尤为重要。

在 FFmpeg 的 AVPixelFormat 枚举中有大几十种像素格式,你可能会发问:为什么要有这么多的像素格式?都使用 RGB 不行吗?

将所有图像都使用RGB像素格式的确是可能的,但实际上,不同的像素格式存在的原因在于它们各自适用于不同的场景和需求。这些场景可能需要考虑存储空间、颜色表现力和处理效率等方面的差异。以下是一些不同像素格式存在的原因:

  • 存储和带宽优化:一些像素格式,如YUV,可能需要比RGB更少的存储空间。因为YUV格式考虑到人眼对亮度敏感度高于色度,常常在色度分量上采样更低的分辨率,因此可以在保持相对较高的图像质量的同时减小文件大小。这在视频压缩和传输等场景下特别重要。

  • 颜色空间:RGB是一种基于颜色的光学叠加模型,对某些颜色计算并不直观。其他颜色空间,如HSB(色相、饱和度、亮度)或YUV,可能在特定的颜色操作或计算上更具优势。

  • 兼容性和专业领域需求:例如,在视频行业和电视广播中,YUV格式有更好的性能和兼容性。而在一些图形应用程序中,可能需要包含透明度通道的像素格式,例如RGBA,从而支持透明度相关的效果和操作。

  • 灰度图像:对于只需要单个颜色通道的应用,如文本扫描、医学成像或图像分析等,使用灰度格式可以大幅减少存储和处理资源的需求。

总之,不同的像素格式适用于不同的场景。RGB虽然是一种常用的颜色表示方法,但在某些情况下,使用其他像素格式可能会更加高效或更适合任务需求。

关于 YUV 格式,笔者之前写过一篇 YUV 文件读取、显示、缩放、裁剪等操作教程,有兴趣的读者可以参考参考。

AVFrame 的像素格式转换

经过前面三章内容,我们现在已经能够将 AVPacket 解码为 AVFrame,为了保存 AVFrame 中的图像数据到本地,需要对 AVFrame 做一次像素格式的转换,这样方便我们直接看到视频帧的内容。

为啥要做像素转换呢?这是因为多数解码后的 AVFrame 使用 YUV 作为像素格式(比 RGB 更少的存储空间),如果你想看 YUV 格式的图片,你需要一个类似 YUV Viewer 的软件来打开 YUV 图片。当然你可以选择用我开发的 simple_yuv_viewer。

但如果我们将 AVFrame 转为 RGB,然后将 RGB 数据保存在 PPM 格式文件中,那么你基本可以使用任意图片预览软件就能打开。方便不少。

PPM 文件

首先需要介绍写 PPM 文件,PPM(Portable Pixmap)文件是一种简单的图像文件格式,用于存储彩色图像。PPM文件格式通常存储未压缩的RGB图像数据,因此文件大小相对较大。由于其简易的文件结构和易于解析的特点,该格式常用于学习和测试图像处理算法。

PPM文件包含以下部分:

  1. 文件头,包括:
    • 魔数(Magic number):文件头的开始,表明文件类型。对于二进制PPM文件,常用"P6";对于ASCII PPM文件,常用"P3"。
    • 图像宽度和高度:以像素为单位的图像尺寸。
    • 最大颜色值:通常为255,代表每个颜色通道的最大值。
  2. 图像数据:字节序列表示的图像像素数据。二进制格式的PPM文件存储连续的RGB值(范围为0到最大颜色值),每个像素由3个字节表示,分别为红、绿、蓝颜色通道的值。

尽管PPM文件易于处理,但它不适合于大型或复杂图像,因为它占用较大的存储空间且不支持数据压缩。在实际应用中,我们通常会使用其他压缩格式,如JPEG、PNG等。

实际代码中,将一个 RGB 格式的 AVFrame 保存到本地非常简单:

void saveFrame(AVFrame *avFrame, int width, int height, const char* output_name)
{
  FILE *pFile;

  // Open file
  pFile = fopen(output_name, "wb");
  if (pFile == NULL) {
    return;
  }

  // Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);

  // Write pixel data
  const int kBytesPerPixel = 3; // R(8) G(8) B(8)
  for (int y = 0; y < height; y++) {
    uint8_t *img_row = avFrame->data[0] + y * avFrame->linesize[0];
    fwrite(img_row, 1, width * kBytesPerPixel, pFile);
  }

  // Close file
  fclose(pFile);
}

Linesize 是什么?

在解释上面代码逻辑之前,需要对 linesize 的概念进行说明。linesize,有时候它也叫 stride,或者 pitch,名字不同但含义相同。

linesize是指图像一行数据所占用的字节数。在处理图像时,图像的像素数据是以一行一行的方式存储的。由于一行像素数据不一定和图像宽度相等,因此需要用linesize来表示每行像素数据所占用的字节数。通常情况下,一个像素占用的字节数是已知的,因此可以通过图像宽度和像素占用的字节数计算出每行数据的字节数(linesize)。

当图像数据按行存储时,linesize可能大于实际像素宽度乘以每个像素所需的字节数。例如,如果一幅图像有宽度W、高度H、RGB格式的像素, 并且使用32位对齐存储,则linesize大于3*W。两行相邻像素之间的多余空间可能用于存储其他信息,或者是因为对齐的原因而保留。

了解linesize是有必要的,因为在执行图像处理任务时,你可能需要使用它来遍历图像中每行的所有像素。逐行遍历图像时,需要利用行跨距来确定每一行数据在内存中的起始位置,从而能够正确地访问和处理像素数据。

假设你正在处理一张100×100像素的彩色RGB图像,每个像素通常需要3个字节(24位)来存储红、绿和蓝颜色通道的数据。要进行8位字节对齐,我们首先找出紧密排列时每一行所需的字节数:

  1. 紧密排列的字节数 = 图像宽度 × 每个像素字节数 = 100 × 3 = 300
  2. 然后根据8字节对齐要求,计算出距离下一个最近的8字节对齐位置,即:
  3. linesize = (紧密排列的字节数 + 7) // 8 * 8 = (300 + 7) // 8 * 8 = 307 // 8 * 8 = 38 * 8 = 304

所以,在采用8字节对齐存储的情况下,linesize为 304 字节。这意味着在每行末尾有4个字节的填充空间以符合8字节对齐规则。

因此,存放一张 100 * 100 的 RGB 图片,实际使用的内存大小是:100 * 304 = 30400,而不是 100 * 300 = 30000。

保存 PPM 文件的代码解释

回到之前的代码中来,对保存 AVFrrame 到 PPM 文件的逻辑进行一些说明:

  1. avFrame->data[0] 指针指向一片内存,该内存存放着整张图片的 RGB 数据。注意:这片内存包含着用于填充 linesize 的无用数据。
  2. avFrame->data[0] 按行存放 RGB 数据,大致如下图,其中灰色表示无用数据。
    在这里插入图片描述
  3. fprintf(pFile, "P6\n%d %d\n255\n", width, height); 这是往 PPM 文件中写入头部信息,其中 “255” 表示 RGB 分量最大值为 255
  4. PPM 文件按行存储连续的 RGB 值,因此我们需要按行将 RGB 像素写入文件。 for 中循环写入每一行数据,其中 avFrame->data[0] + y * avFrame->linesize[0]; 定位到每一行的开始的位置;fwrite(img_row, 1, width * kBytesPerPixel, pFile); 写入每一行中的所有 RGB 数据。

使用 sws_scale 进行像素格式转换

正如前面所说,视频中一般使用 YUV 作为像素格式,为了保存为 PPM 文件,需要将 YUV 转换 RGB。在 FFmpeg 中,已经提供了方便的工具来做像素格式转换的工作,它就是 sws_scale 函数。

sws_scale 函数是 FFmpeg 中的一个关键功能,用于实现视频图像缩放和格式转换。它是由 libswscale 库提供的,这是一个专门用于处理各种像素格式以及実现高效缩放和颜色空间/格式转换的库。

sws_scale 函数的作用具体包括以下几点:

  1. 缩放:根据源图像和目标图像的尺寸,调整图像大小。例如,把 1920x1080 的图像缩放到 1280x720。
  2. 格式转换:在不同的像素格式之间转换图像数据。例如,将 YUV420 转换为 RGB24,或者将 NV12 转为 YUV420P。
  3. 颜色空间/范围转换:将图像的颜色空间与范围调整为不同的类型,例如从 BT.709 转为 BT.601,或者将全范围 YUV 转为有限范围 YUV。
  4. 效率:利用 CPU 的 SIMD 指令集(如 MMX、SSE 和 AVX)进行优化,以提高缩放和格式转换的速度。

那么如何使用 sws_scale 呢?步骤如下:

  1. 创建 SwsContext 结构体
  2. 创建一个新的 AVFrame,以便存放转换后的数据
  3. 使用 sws_scale 函数进行转换即可

让我们来看每个步骤具体的实现。这部分代码参考 ffmpeg_image_converter.h 中的实现

首先,使用 sws_getContext 创建 SwsContext。你需要填很多信息,包括源视频的宽高、像素格式,以及目标宽高、目标像素格式等

sws_ctx = sws_getContext(srcW, srcH, srcFormat, dstW, dstH, dstFormat,
                             flags, srcFilter, dstFilter, param);

创建一个 AVFrame 用于存放转换后的数据,其中 av_frame_get_buffer, 函数是用于为 AVFrame 结构体分配内存空间的函数。它会根据 AVFrame 的格式和大小信息,为 AVFrame 的数据指针分配内存并设置行大小,以准备存储原始数据。

frame = av_frame_alloc();
frame->width = dstW;
frame->height = dstH;
frame->format = dstFormat;
frame->format = (int)dstFormat;
frame->width = dstW;
frame->height = dstH;
frame->channels = 0;
frame->channel_layout = 0;
frame->nb_samples = 0;
av_frame_get_buffer(frame, 16);

最后,使用 sws_scale 进行转换

frame->pict_type = in_frame->pict_type;
frame->pts = in_frame->pts;
frame->pkt_dts = in_frame->pkt_dts;
frame->key_frame = in_frame->key_frame;
frame->coded_picture_number = in_frame->coded_picture_number;
frame->display_picture_number = in_frame->display_picture_number;
int output_height = sws_scale(
    sws_ctx, (uint8_t const *const *)in_frame->data, in_frame->linesize, 0,
    in_frame->height, frame->data, frame->linesize);

总结

本文讲述了如何将一帧视频保存到本地 PPM 文件,以便浏览。介绍了关于像素格式、PPM、Linesize、FFmpeg 中 sws_scale 等知识点。
结合前面三章内容,我们终于完成了第一个任务,对应 An ffmpeg and SDL Tutorial - Tutorial 01: Making Screencaps。接下来我们将继续 ffmpeg 学习路程。

参考

  • 图像处理、显示中的行宽(linesize)、步长(stride)、间距(pitch)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/700338.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5G NR:RACH随机接入过程

1. 简述 无论是3G,4G还是现在的5G都需要随机接入过程&#xff0c;随机接入过程主要是为了让基站和UE之间做好上行同步以及初始接入。此文章仅仅帮助大家了解此过程&#xff0c;更加具体的用途及其场景需要参考具体的3GPP协议&#xff08;38.211,38.212,38.213).主要帮助理解如下…

Redis设计与实现笔记之链表

以下为Redis链表中一个节点的结构 typedef struct listNode {// 前置节点struct listNode *prev;// 后置节点struct listNode *next;// 节点的值void *value;} listNode; 可以看见此结构与我们常见的双向链表结构类似。由前去后继节点的地址以及当前节点的值组成。 redis中链…

使用percona xtraBackup8.x进行MySql8.0备份与还原(完全备份,增量备份,差异备份)

MySQL备份与还原 备份目标&#xff1a;数据的一致性&#xff0c;服务的可用性备份技术&#xff1a;物理备份(冷备份)【直接复制数据库文件&#xff0c;适用大型数据库&#xff0c;缺点是操作时服务需要停止】和 逻辑备份(热备份)【备份的是建表建库插入的SQL语句】备份方式&am…

CSDN周赛60期简要题解

一转眼&#xff0c;周赛都举办了60期了&#xff1f;还以为可以“寿终正寝”了&#xff0c;结果61期又安排上了。打开一看&#xff0c;还是《计算之魂》主题的周赛&#xff0c;还是这种 4 &#xff08;非编程&#xff09; 2 &#xff08;编程&#xff09; 的题型。可能目前就指…

HttpRunner 使用小结

目录 https 请求证书验证 2.0.3 (2019-02-24) 代理调试 $ 符引用 json 响应中数组的提取和断言 text/html 响应的提取和断言 testcase 之间传递参数 2.2.2 (2019-06-26) 复用 cookies 和 token 1. 每个 testcase 登录一次 2. 将 cookies 或 token 写入文件&#xff…

如何设计一个高并发系统?

其实所谓的高并发&#xff0c;如果你要理解这个问题呢&#xff0c;其实就得从高并发的根源出发&#xff0c;为啥会有高并 发&#xff1f;为啥高并发就很牛逼&#xff1f; 浅显一点&#xff0c;很简单&#xff0c;就是因为刚开始系统都是连接数据库的&#xff0c;但是要知道数据…

关于英语翻译中的归化与异化,你可以了解一下

据了解&#xff0c;归化和异化是翻译中使用的两种不同手段&#xff0c;它们都能在目的语文化中完成各自的使命&#xff0c;都有其存在的价值。那么&#xff0c;究竟什么是归化&#xff0c;什么是异化&#xff1f;英语翻译中的归化与异化有什么特征&#xff1f; 归化是让作者靠近…

Rust 基础入门 —— 字符、布尔、单元 类型

字符、布尔、单元 类型 字符类型&#xff08;char&#xff09; 对于字符类型我们有更大的自由性&#xff0c;概括一下&#xff1a; 更大的编码范围&#xff0c;让rust 可以展示更多的内容。统一的字节空间&#xff0c;字符也是四个字节的内存大小。严格区分的 "" …

reduceByKey 和 groupByKey 的分析与区别

reduceByKey 源码 def reduceByKey(partitioner: Partitioner, func: (V, V) > V): RDD[(K, V)] self.withScope {combineByKeyWithClassTag[V]((v: V) > v, func, func, partitioner)}/*** Merge the values for each key using an associative and commutative reduce…

保姆级教程:带你体验华为云测试计划CodeArts TestPlan

华为云测试计划&#xff08;CodeArts TestPlan&#xff09;是面向软件开发者提供的一站式云端测试平台&#xff0c;覆盖测试管理、接口测试&#xff0c;融入DevOps敏捷测试理念&#xff0c;帮助您高效管理测试活动&#xff0c;保障产品高质量交付。 登录华为云账号&#xff1a…

使用JMeter安装RabbitMQ测试插件的步骤

整体流程如下&#xff1a;先下载AMQP插件源码&#xff0c;可以通过antivy在本地编译成jar包&#xff0c;再将jar包导入JMeter目录下&#xff0c;重启JMeter生效。 Apache Ant 是一个基于 Java 的构建工具。Ant 可用于自动化构建和部署 Java 应用程序&#xff0c;使开发人员更轻…

【2023年江西省研究生数学建模竞赛】题目一 蒸汽发生器倒U型管内液体流动 建模方案及参考文献

代码与结果如下&#xff1a;完整文档见文末 完整思路”请点击这里“到原文章获取 题目&#xff1a; PACTEL压水堆整体测试设备在2009年建造&#xff0c;用于带有垂直倒U型管蒸汽发生器的压水堆热液压相关的安全性研究,参见图1。 PACTEL压水堆设施包括一个反应堆压力容器模型…

Redis如何统计一个亿的keys?

前言 不知你大规模的用过Redis吗&#xff1f;还是仅仅作为缓存的工具了&#xff1f;在Redis中使用最多的就是集合了&#xff0c;举个例子&#xff0c;如下场景&#xff1a; 签到系统中&#xff0c;一天对应一系列的用户签到记录。 电商系统中&#xff0c;一个商品对应一系列的…

ARM-SWI 和未定义指令异常中断处理程序的返回(七)

文章目录 处理流程示例代码实现SWI未定义指令 附录源码 处理流程 SWI 和未定义指令异常中断是由当前执行的指令自身产生的&#xff0c;当 SWI 和未定义指令异常中断产生时&#xff0c;程序计数器的 PC 的值还未更新&#xff0c;它指向当前指令后面第 2 条指令&#xff08;对于…

Git的常见操作

Git版本控制 开发难题 在实际开发中我们会遇到一些问题&#xff0c;电脑蓝屏&#xff0c;代码丢了&#xff0c;懊悔不&#xff1f; 时间长了&#xff0c;文件找不到了。懊悔不&#xff1f;手欠&#xff0c;之前代码运行好好的&#xff0c;非要去优化下。结果还 不如以前&am…

京东天猫数据查询与分析:2023年厨电细分市场数据分析

随着消费者对生活品质的追求持续提高&#xff0c;我国厨房电器产品的需求也日趋多样化&#xff0c;市场中厨房电器的品类越来越多&#xff0c;我国厨房电器的市场规模也不断扩大。 根据鲸参谋电商数据显示&#xff0c;2023年1月至4月&#xff0c;天猫平台上厨房电器的销量为670…

搭建个人hMailServer 邮件服务实现远程发送邮件

文章目录 1. 安装hMailServer2. 设置hMailServer3. 客户端安装添加账号4. 测试发送邮件5. 安装cpolar6. 创建公网地址7. 测试远程发送邮件8. 固定连接公网地址9. 测试固定远程地址发送邮件 转载自cpolar极点云文章&#xff1a;搭建个人hMailServer 邮件服务实现远程发送邮件 hM…

ChatGPT微调系列一:总述 微调 的基本流程

文章目录 前言一、啥叫微调二、为啥要微调三、不是所有模型都可以微调的四、总述微调的基本流程&#xff0c;以及涉及的主要函数&#xff0c;参数1. 安装2. 准备训练数据3. openai.api_key os.getenv() 进行一个说明4. 通过API 调用模型 常用函数5. 微调模型 常用函数6. OpenA…

Maven 使用详细教程

目录 Maven 介绍 Maven 安装 1、安装JDK 2、下载Maven安装文件 3、配置环境变量 4、检测安装成功 Maven 标准工程结构 Maven 版本要素 Maven仓库 1、本地仓库&#xff1a; 2、中央仓库 3、其他远程仓库 创建Maven工程 使用命令方式创建Maven工程 Eclipse中创建…

智能大棚自动控制系统 实现传统农业精细化管理

新型农业经营主体管理系统是指为了适应农村经济发展需求&#xff0c;提高农业生产组织化、规模化、现代化程度&#xff0c;促进农业产业结构调整和农村产业转型升级&#xff0c;推动农村经济社会持续健康发展而建立的一套管理体系。 该系统主要包括农产品生产、种植、养…