1. 视频基础知识
1. 图像基础概念
- 像素:像素是一个图片的基本单位,
pix
是英语单词picture
,加上英语单词“元素element
”,就得到了pixel
,简称px
。所以“像素”有“图像元素”之意。 - 分辨率:指的是图像的大小或者尺寸。比如 1920x1080 。
- 位深:指在记录数字图像的颜色时,计算机实际上是用每个像素需要的位深来表示的。比如红色分量使用 8bit 。
- 帧率:在一秒钟时间内传输图片的帧数(张数),也可以理解为图形处理器每秒钟能够刷新几次。例如:25fps 表示一秒有 25 张图片。
- 码率:视频文件在单位时间内使用的数据流量。例如 1Mbps 。
Stride
:指在内存中每行像素所占的空间。为了实现内存对齐,每行像素在内存中所占的空间并不一定是图像的宽度。
1.1. 像素
像素是一个图片的基本单位,pix
是英语单词picture
,加上英语单词“元素element
”,就得到了pixel
,简称px
。所以“像素”有“图像元素”之意。
例如 2500x2000 的照片就是指横向有 2500 个像素点,竖向有 2000 个像素点,总共是 500 万个像素,也俗称为 500 万像素照片。
将这个图片放大,可以发现是由一块一块的小方块组成的,每一个小方块就是一个像素点。
1.2. 分辨率
图像(视频)的分辨率指的是图像的大小或尺寸。我们通常用像素来表示图像的尺寸。
例如分辨率为 2500x2000 的照片就是指的 横向(宽)有 2500 个像素点,竖向(高)有 2000 个像素点。
常见的分辨率:
360P(640x360)、720P(1280x720)、1080P(1920x1080)、4K(3840x2160)、8K(7680x4320)
常说的 1080P 和 720P 其实指的是分辨率的垂直像素,一般情况下我们都是按照 16:9 (宽:高)来计算分辨率。
那么 720P 的分辨率计算流程如下:
- 720P 代表垂直(高)像素有 720 个像素点
- 由于图片的比例为 16:9 ,那么水平(宽)的像素点数量为 720 ÷ 9 × 16 = 1280
- 故 720P 的分辨率为 1280x720
像素越多视频就越清晰,1080P 的像素点总共有 1920x1080 约等于 200 万个像素,720p 的像素点总共有 1080x720 约等于 92 万个像素。1080p 的像素点数量比 720p 的像素点数量多两倍多,故 1080p 比 720p 更清晰。图像的分辨率越高,图像就越清晰。
低分辨率下只能看到一个大概的轮廓,而高分辨率能看到更多的细节,故更加清晰。
1.3. 位深
我们看到的彩色图片,都有三个通道,分别为红(R),绿(G),蓝(B)通道。(如果需要透明度则还有 alpha
分量)。
通常每个通道用 8bit 表示,8bit 能表示 256 种颜色,所以可以组成 256*256*256=16,777,216=1677万种颜色。这里的 8bit 就是我们讲的位深。
每个通道的位深越大,能够表示的颜色值也就越大。比如现在高端电视说的是 10bit 色彩,即每个通道用 10bit 表示,10bit 能表示 1024 种颜色,故可以组成 1024*1024*1024 约等于 10亿种颜色,是 8bit 的 64 倍。
常见的颜色还是 8bit 居多。
1.4. 帧率
帧率即 FPS(每秒有多少帧画面)。经常玩游戏的小伙伴应该很熟,我们玩游戏时,FPS 越高就代表游戏画面越流畅,越低则越卡顿。
由于视觉图像在视网膜的暂时停留,一般图像帧率能达到 24 帧,我们就认为图像是连续动态的。
- 电影帧率一般是 24fps
- 电视剧一般是 25fps
- 监控行业一般是 25fps
- 音视频通话一般是 25fps
帧率越高,画面越流畅,需要的设备性能也越高。
1.5. 码率
- 码率指的是视频文件在单位时间内使用的数据流量。比如 1Mbps 。
- 大多数情况下码率越高,分辨率越高,也就越清晰。但本身就模糊的视频文件码率也可以很大,分辨率小的视频文件也可能比分辨率大的视频文件清晰。【比如光线不好的时候录制的视频】
- 对于同一个原始图像源的时候,同样的编码算法,码率越高,图像的失真就会越小,视频画面就会越清晰。【可见后面的h264编码,当采用低码率,那么在编码的时候就会设置更高的QStep】
- 码率是一个统计的指标,并不是一个实际的功能。
1.6. Stride
设计到计算机底层对于内存的利用,不影响理解,只是开发时使用,这里不做介绍。
2. RGB、YUV深入讲解
- RGB:红R、绿G、蓝B三种基色。
- YUV:”Y“ 表示明亮度(Luma),也就是灰阶值,”U“ 和 ”V“ 表示的是色度(Chroma)。
2.1. RGB
- 我们前面已经讲过 RGB 色彩表示,这里我们重点讲 RGB 的排列。通常的图像像素是按 RGB 顺序进行排列的,但有些图像处理要转换为转成其他顺序,比如 OpenCV 经常要转换为 BGR 的排列方式
常见的一些排列方式:
2.2. YUV
2.2.1. 基础介绍
- 与我们熟知的 RGB 类似,YUV 也是一种颜色编码方法,它是指将亮度参量和色度参量分开进行表示的像素编码格式。
- 这样分开的好处就是不但可以避免互相干扰,没有 UV 信息一样可以显示完整的图像(因而解决了彩色电视与黑白电视的兼容问题),还可以降低色度的采样率而不会对图像质量影响太大,降低了视频信号传输时对频宽(宽度)的要求。
- YUV 是一个比较笼统的说法,针对它的具体排列方式,可以分为很多种具体的格式,但基础两个格式如下:
-
- 打包(packed)格式:将每个像素点的Y、U、V分量交叉排列并以像素点为单元连续的存放在同一个数组中,通常几个相邻的像素组成一个宏像素(macro-pixel)。
- 平面(planar)格式:使用三个数组分开连续的存放Y、U、V三个分量,即Y、U、V分别存放在各种的数组中。
2.2.2. 采样表示法
- YUV 采用 A:B:C 表示法来描述 Y,U,V 采用频率比例,下图中黑点表示采样像素 Y 分量,空心圆表示采用像素的 UV 分量。主要分为 YUV 4:4:4、YUV 4:2:2、YUV 4:2:0 这三种常用的类型
- 4:4:4 表示色度频道没有下采样,即每一个 Y 分量对应 一个 UV 分量。
- 4:2:2 表示 2:1 的水平下采样,没有垂直下采样,即每两个 Y 分量共享 一个 UV 分量。
- 4:2:0 表示 2:1 的水平下采样,2:1 的垂直下采样,即每四个 Y 分量共享 一个 UV 分量。
YUV4:4 : 4,不是说4个Y,4个V,4个U,而是说 Y 和 UV的比例关系。
同理 YUV4:2:0,也不是说4个Y,2个Y,0个U,也是说的 Y 和 UV 的比例关系。
2.2.3. YUV数据存储方式
下面以每个分量的采用数据为位深 8bit 为例描述YUV的数据存储方式
- 4:4:4 格式
- 4:2:2 格式
- 4:2:0 格式
2.2.3.1. YUV 4:4:4 格式
比如 I444(YUV444P)格式
- 对应 FFmpeg 像素表示
AV_PIX_FMT_YUV444P
- 该类型为 平面(planar)格式
- 一个宏素块 24bit,即 3 个字节
2.2.3.2. YUV 4:2:2 格式
比如 I422(YUV422P)格式
- 对应 FFmpeg 像素表示
AV_PIX_FMT_YUV422P
- 该类型为 平面(planar)格式
- 一个宏素块 16bit,即 2 个字节
2.2.3.3. YUV 4:2:0 格式【最常用】
比如 I420(YUV420P)格式
- 对应 FFmpeg 像素表示
AV_PIX_FMT_YUV420P
- 该类型为 平面(planar)格式
- 一个宏素块 12bit,即 1.5 个字节
2.2.3.4. YUV 4:2:0 格式 - NV12
比如 NV12 格式
- 对应 FFmpeg 像素表示
AV_PIX_FMT_NV12
- 该类型为 YUV420P 的变种,对 Y 分量采用平面模式,对 UV 采用打包模式
- 一个宏素块 12bit,即 1.5 个字节
2.2.3.5. YUV 4:2:0 格式 - 各种变种格式参考
2.3. RGB和YUV的转换
- 通常情况下 RGB 和 YUV 直接的互相转换都是调用接口实现,比如 FFmpeg 的 swscale 或者 libyuv 等库。
- 主要转换标准是 BT601 和 BT709。
-
- TV range 的分量范围: 16-235(Y)、16-240(UV),故叫做 Limited Range
- PC range 的分量范围: 0-255(Y、UV),故叫做 Full Range
- 而对于 RGB 来说没有分量范围之分,全是 0-255
- BT601 TV Range 转换公式
RGB 转换为 YUV:
Y = 0.299 * R + 0.587 * G + 0.114 * B;
U = -0.169 * R - 0.331 * G + 0.5 * B;
V = 0.5 * R - 0.419 * G - 0.081 * B;
YUV 转换为 RGB:
R = Y + 1.402 * (Y - 128)
G = Y +0.34414 * (U - 128) - 0.71414 * (U - 128)
B = Y + 1.772 * (V - 128)
- 从 YUV 转换到 RGB ,如果值小于 0 要取 0,值大于 255 要取 255。
2.4. RGB和YUV的转换——为什么解码出错会出现绿屏
当解码时,从 packet 中解析 frame,由于解码失败,导致得到YUV的都为0,再根据公式转换为 RGB
R = 1.402 * (-128) = -126.598
G = -0.34414 * (-128) - 0.71414 * (-128) = 135.45984
B = 1.722 * (-128) = -126.228
由于 RGB 值范围是 [0,255],所以最终的值为:
R = 0
G = 135
B = 0
此时只有 G 分量有值,故全屏是绿色。
2.5. YUV Stride对齐问题
设计到计算机底层对于内存的利用,不影响理解,只是开发时使用,这里不做介绍。
3. 视频的主要概念
3.1. 基础名词
3.2. I帧
3.3. P、B帧
3.4. 常用视频压缩算法
4. 补充知识点
4.1. 常用分辨率
标号 | 视频类型 | 格式尺寸 | 类型 | 比例 |
1 | 4K | 4096*2160 | 4K | 16:9 |
2 | 2K | 2560*1440 | 2K | 16:9 |
3 | 全高清 | 1920*1080 | 1080p | 16:9 |
4 | 高清 | 1280*720 | 720p | 16:9 |
5 | 标清 | 720*480 | 480p | 3:2 |
6 | 标清 | 640*480 | 480p | 4:3 |
7 | 流畅 | 320*240 | 240p | 4:3 |
补充说明:
多数情况下4k特指4096 * 2160分辨率。而根据使用范围的不同,4K分辨率也有各种各样的衍生分辨率,例如Full Aperture 4K的4096 * 3112、Academy 4K的3656 * 2664以及UHDTV标准的3840 * 2160等,都属于4K分辨率的范畴。
原文链接:4K、2K、1080p、720p、480p、240p常见视频清晰度-CSDN博客
4.2. 位深
如果不是8bit,而是1bit,那么就只会有八种颜色了,全红、全绿、全蓝、全黑、全白...。因为每个通道只能显示两种颜色,红、非红;绿,非绿;蓝,非蓝。
4.3. 码率
每一秒传输的帧数是固定的,比如一秒传输10帧,每帧大小1MB,那么码率就是10MBps,但是如果降低码率了,降低为5Mbps,由于每秒传输的帧数不应该发生变化,所以每帧的大小只能传输0.5MB了,所以就会造成数据丢失(压缩的结果),回显就会出现不清晰,或者马赛克了。
4.3.1. 核心概念
- 帧率(FPS)与码率的关系
-
- 帧率(FPS)是每秒显示的帧数(如20FPS=20帧/秒),由视频的时序特性决定,与码率无关。
- 码率(比特率)是每秒传输的数据量(如200Mbps),决定了每帧能分配到的平均数据量。
- 关键结论:
-
-
- 当帧率固定时(如20FPS),降低码率会迫使编码器压缩每帧的数据量(减少每帧的比特数),而非减少帧数。
- 提高码率则允许每帧保留更多数据,从而提升画质。
-
- 为什么压缩每帧会影响清晰度?
-
- 视频编码(如H.264/H.265)的本质是用尽可能少的数据表示图像。编码器会通过以下手段压缩每帧:
-
-
- 空间压缩:合并相似区域、舍弃高频细节(如纹理)。
- 时间压缩:通过运动预测(帧间编码)减少冗余。
-
-
- 码率降低时:编码器必须更激进地压缩每帧,导致:
-
-
- 量化步长增大 → 更多细节被丢弃 → 画面模糊或出现块效应。
- 运动预测精度下降 → 残差数据更粗糙 → 动态画面出现拖影。
-
4.3.2. 你的例子修正
- 假设条件:
-
- 原始视频:10秒,20FPS → 共200帧,原始每帧1MB(未压缩)。
- 编码后目标码率:
-
-
- 200Mbps(25MB/s):每秒分配25MB数据,平均每帧
25MB/20帧=1.25MB/帧
。 - 100Mbps(12.5MB/s):每秒分配12.5MB数据,平均每帧
12.5MB/20帧=0.625MB/帧
。
- 200Mbps(25MB/s):每秒分配25MB数据,平均每帧
-
- 关键区别:
-
- 高码率(1.25MB/帧)时,编码器可以保留更多细节(接近原始1MB/帧的未压缩质量)。
- 低码率(0.625MB/帧)时,编码器必须将每帧压缩到更小的体积,导致画质下降。
4.3.3. 类比说明
- 把视频想象成一本书:
-
- 帧率:每秒翻多少页(固定20页/秒)。
- 码率:每页允许写的字数。
-
-
- 高码率 → 每页字数多(细节丰富,清晰)。
- 低码率 → 每页字数少(必须缩写或删减内容,信息丢失)。
-
4.3.4. 常见误区澄清
- “码率降低是否会导致丢帧?”
-
- 不会!帧率(FPS)是编码前确定的参数(如20FPS),编码器会优先保证帧数,转而压缩每帧质量。
- 只有极端情况下(如网络传输拥塞),可能主动丢帧,但这属于实时传输问题,而非编码本身的特性。
- “为什么不能直接降低分辨率?”
-
- 降低分辨率(如1080p→720p)确实可以减少数据量,但这是另一种压缩手段。
- 固定分辨率下,码率降低是通过压缩算法(如量化、预测)实现的,而非改变分辨率。
4.3.5. 总结
- 帧率(FPS):决定视频的流畅度,编码时固定不变。
- 码率:决定每帧的画质,码率越低,每帧被压缩得越狠,清晰度越低。
- 编码器的任务:在固定帧率和目标码率下,智能分配每帧的数据量,平衡压缩率与画质。
4.4. 像素排列
常规理解来说一个像素不是一个字节,可以理解为一个对象。【这里指的位深为 8bit 】
比如不是说 char a[1280*720], a[0]是一个像素,应该理解为:
struct pixel
{char r;char g;char b;
}
pixel a[1280*720];
a[0] 是一个像素。
当然,写代码肯定是按照char来理解的,那么就是 char a[1280*720*3],a[0]、a[1]、a[2] 三个组合是一个像素;a[3]、a[4]、a[5] 三个组合起来是一个像素。其中a[0]、a[1]、a[2]谁是R,谁是G,谁是B,就看排列规则了。
4.5. 花屏
- 如果页面完全看不出来轮廓,代表分辨率选错了
- 如果能看出来轮廓,但是花屏,那么分辨率没问题,数据格式选错了
4.6. YUV444和YUV420的命名
YUV444 代表4个Y,4个U,4个V。
YUV420 代表4个Y,1个U,一个V。
但是看名字不应该是4个Y,2个U,0个V吗,按道理4个Y,1个U,一个V不应该叫做 YUV411 吗?
但其实它的命名源于历史约定和采样网格的排列方式,而非严格的数学比例。下面详细解释:
4.6.1. 1. YUV 4:2:0 的采样结构
YUV 4:2:0 的色度(UV)采样方式如下:
- 每4个Y(亮度)像素共享1个U和1个V(色度)值。
- 但色度采样在垂直方向也进行了减半,即每隔一行才保留一组UV值。
实际存储示例(以2×2像素块为例):
Y1 Y2 → 亮度(Y)全采样
Y3 Y4U1 V1 → 色度(UV)在水平和垂直方向均减半
- 4个Y对应 1个U和1个V(水平和垂直方向均共享)。
4.6.2. 2. 为什么叫4:2:0而不是4:1:1?
- 4:2:0的命名逻辑:
-
- 第一个数字(4):参考基准(通常表示亮度的横向采样数)。
- 第二个数字(2):色度(U/V)在水平方向的采样比例(相对于亮度的一半)。
- 第三个数字(0):色度在垂直方向的采样比例(0表示垂直方向再减半)。
-
-
- 即色度在水平和垂直方向均为亮度的一半,最终等效于“4个Y对应1个UV”。
-
- 对比其他格式:
-
- 4:4:4:色度无降采样(每个Y对应一个UV)。
- 4:2:2:色度水平减半,垂直全采样(每行保留UV)。
- 4:1:1:色度水平降为1/4,垂直全采样(常见于DV视频)。
- 关键区别:
4:2:0的“0”强调垂直方向也减半,而4:1:1的色度在垂直方向未减半。
4.6.3. 3. 命名背后的历史原因
- 4:2:0的“0”是约定俗成:
早期工程师用数字表示色度采样相对于亮度的比例,垂直减半被标记为“0”(类似占位符),而非数学上的严格除法。 - 与4:1:1的对比:
4:1:1是水平方向更激进的降采样(每4个Y共享1个UV,但垂直方向不降采样),而4:2:0是水平和垂直均减半。
4.6.4. 4. 直观图示
以8×8像素块为例:
- Y(亮度):全采样(64个值)。
- U/V(色度):
-
- 水平方向:每隔1个像素采样(4:2:0 → 水平减半 → 每行4个UV)。
- 垂直方向:每隔1行采样(4:2:0 → 垂直减半 → 共4行UV)。
- 最终UV数据量:
4×4=16
个U和16个V,相当于64个Y对应16个UV → 4:1的比例。
4.6.5. 5. 为什么不是4:1:1?
- 4:1:1的色度采样:
-
- 水平方向:每4个Y共享1个UV。
- 垂直方向:每行都保留UV(不降采样)。
- 示例:8×8像素块中,UV数据量为
2×8=16
个(每行2个UV,共8行)。 - 相当于64个Y对应16个UV → 4:1的比例,但垂直方向未减半。
- 4:2:0的色度采样:
-
- 水平和垂直均减半 → 最终UV数据量是4:1:1的一半(更节省带宽)。
4.6.6. 总结
- YUV 4:2:0的命名反映的是色度在水平和垂直方向的减半采样(“0”代表垂直减半),而实际存储比例是4个Y对应1个UV。
- 4:1:1则是水平方向更激进的降采样,但垂直方向未减半。
- 这种命名是历史惯例,数字更多表示“采样网格的分布方式”,而非严格的数学比例。
如果需要记忆,可以简单理解为:
- 4:2:0 = 色度在两个方向都减半(最终4Y:1UV)。
- 4:1:1 = 色度仅在水平方向减半(垂直方向全采样)。
4.7. 生成BMP图片文件
#include <iostream>
#include <fstream>
#include <vector>// BMP 文件头
#pragma pack(push, 1) // 确保结构体没有字节填充
struct BMPFileHeader {uint16_t bfType; // 文件类型,必须是 'BM'uint32_t bfSize; // 文件大小uint16_t bfReserved1; // 保留,必须为 0uint16_t bfReserved2; // 保留,必须为 0uint32_t bfOffBits; // 从文件头到像素数据的偏移
};// DIB 头(BITMAPINFOHEADER)
struct BMPInfoHeader {uint32_t biSize; // 此结构的大小(40 字节)int32_t biWidth; // 图像宽度int32_t biHeight; // 图像高度uint16_t biPlanes; // 颜色平面数,必须为 1uint16_t biBitCount; // 每像素位数(24 表示 RGB)uint32_t biCompression; // 压缩类型(0 表示不压缩)uint32_t biSizeImage; // 图像大小(可以为 0,如果不压缩)int32_t biXPelsPerMeter; // 水平分辨率int32_t biYPelsPerMeter; // 垂直分辨率uint32_t biClrUsed; // 使用的颜色数(0 表示所有)uint32_t biClrImportant; // 重要颜色数(0 表示所有)
};
#pragma pack(pop)void createRedBMP(const char* filename) {int width = 640;int height = 480;/** 这里 width * 3,是因为一个像素点有RGB3个点,故实际上一个像素点有三个点,刚好与下面的 infoHeader.biBitCount 对应。* 而且也一定要和下面的 infoHeader.biBitCount 对应,因为这里是计算的是总字节大小,这里 *3,就代表一个像素点3字节,那么每个RGB对应1个字节,那么 infoHeader.biBitCount 就该是24位,即3个字节,* 否则图形的总字节大小算下来就不对。 * +3 & (~3) 就是对齐四个字节的操作,先+3,向上填充直最近4的倍数,然后在 &(~3),把余数给去掉,(3 = 011, ~3 = 100,即最后两位一定会被去掉)*/// 计算图像数据大小int row_padded = (width * 3 + 3) & (~3); // 每行 4 字节对齐int dataSize = row_padded * height;// 创建文件头和信息头BMPFileHeader fileHeader = {};BMPInfoHeader infoHeader = {};fileHeader.bfType = 0x4D42; // 'BM'fileHeader.bfSize = sizeof(BMPFileHeader) + sizeof(BMPInfoHeader) + dataSize;fileHeader.bfOffBits = sizeof(BMPFileHeader) + sizeof(BMPInfoHeader);infoHeader.biSize = sizeof(BMPInfoHeader);infoHeader.biWidth = width;infoHeader.biHeight = height;infoHeader.biPlanes = 1;infoHeader.biBitCount = 24; // RGB。 24 / 3 = 8位 = 1个字节。0~255。为什么要除3,因为一个像素要包含RGB3个点,故一位像素点,要切分成三份来使用。与上面的row_padded对应infoHeader.biCompression = 0; // 不压缩// 打开文件std::ofstream file(filename, std::ios::binary);if (!file) {std::cerr << "无法创建文件: " << filename << std::endl;return;}// 写入文件头和信息头file.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));file.write(reinterpret_cast<const char*>(&infoHeader), sizeof(infoHeader));// 写入像素数据(红色)std::vector<uint8_t> row(row_padded, 0); // 用于存储每行的数据for (int y = 0; y < height; ++y) {for (int x = 0; x < width; ++x) {row[x * 3 + 0] = 0; // Brow[x * 3 + 1] = 0; // Grow[x * 3 + 2] = 255; // R}file.write(reinterpret_cast<const char*>(row.data()), row_padded);}file.close();
}int main() {createRedBMP("red_image.bmp");std::cout << "红色 BMP 图像已创建: red_image.bmp" << std::endl;return 0;
}
4.8. 生成YUV图片文件
#include <iostream>
#include <fstream>void generateRedYUVImage(const char* filename, int width, int height) {// 分配内存int size = width * height * 3; // YUV444: 每个像素3个字节unsigned char* yuvData = new unsigned char[size];// 填充数据(全红)for (int y = 0; y < height; ++y) {for (int x = 0; x < width; ++x) {int index = (y * width + x) * 3;yuvData[index + 0] = 255; // YyuvData[index + 1] = 255; // UyuvData[index + 2] = 255; // V}}// 保存数据到文件std::ofstream outFile(filename, std::ios::binary);if (outFile.is_open()) {outFile.write(reinterpret_cast<const char*>(yuvData), size);outFile.close();std::cout << "Image saved to " << filename << std::endl;}else {std::cerr << "Unable to open file for writing" << std::endl;}// 释放内存delete[] yuvData;
}int main() {int width = 640; // 图像宽度int height = 480; // 图像高度const char* filename = "red_image.yuv";generateRedYUVImage(filename, width, height);return 0;
}
使用ffplay播放:
ffplay -f rawvideo -pixel_format yuv444p -video_size 640x480 red_image.yuv