本文章是以学习笔记形式展开记录。根据以下文章进行总结和学习:
大概看了一下网上的音视频文章,目前来看文章大部分知识都是雷神
那些CSND来的,难怪我leader
说国内玩ffmpeg
的搞来搞去就是那么点人,和我在学校期间学习Pwn
和文章也是一样的现象内容都有雷同之处,~嗯。怎么说呢。还是希望大家学成归来的时候多反馈国内的音视频社区吧
通过显卡的视频加速功能对高清视频进行解码。因此硬解码能够将CPU从繁重的视频解码运算中释放出来,使播放设备具备流畅播放高清视频的能力。
显卡的GPU/VPU要比CPU更适合这类大数据量的、低难度的重复工作。视频解码工作从处理器那里分离出来,交给显卡去做
以前纯粹依靠CPU来解码的方式则是“软解码”。
软解码是在显卡本身不支持或者部分不支持硬件解码的前提下,将解压高清编码的任务交给CPU,这是基于硬件配置本身达不到硬解压要求的前提下,属于一个折中的无奈之举
对于一个超级电视而言,观看高清电影无疑是用户最大的诉求,而硬解码的优势就在于可以流畅的支持1080p甚至4K清晰度的电影播放,而不需要占用CPU,CPU就可以如释重负,轻松上阵,承担更多的其他任务。
如果通过软解码的方式播放高清电影,CPU的负担较重,往往会出现卡顿、不流畅的现象。
通常我们所说的硬件加速或硬件解码,就是指视频解码的这几个步骤中,用显卡专用的解码引擎替代CPU的软件计算,降低CPU的计算负荷。
但是还有一些不遵守这个命名规范的,不以 OMX. 开头的情况,它们也会被认为是软编解码器。
/system/etc/media_codecs.xml
可以确定当前设备支持哪些硬解码。/system/etc/media_profiles.xml
可以知道设备支持的具体profile和level等详细信息。所有的颜色可以通过三原色产生,这 三原色 就是 Red (红),Green(绿),Blue(蓝)
缺点是占用空间大,应用用RGB
封装格式的的多,下文中的BMP
就是使用的RGB
做封装的
以下是基于小端的模式架构体系中
RGB24 : 每个像素占3个字节,R 占 8位,G 占 8位,B 占 8 位。即格式可以混合生成 25 6 3 256^3 2563 种颜色以下的格式也是依次类推。顺序为R-G-B
RGB16
B-G-R
B-G-R
RGB32 :每个像素占4个字节,R 占 8位,G 占 8位,B 占 8 位,最后8位保留。顺序为B-G-R-0
ARGB32 :本质就是带alpha通道
的RGB32
,保留的8个bit
用来表示alpha
的值。顺序为B-G-R-A
RGB1、RGB4、RGB8
opencv默认通道是BGR
的。这是opencv的入门大坑之一,BGR
是个历史遗留问题,为了兼容早年的某些硬件
与YUV420P三个分量分开存储不同,RGB24格式的每个像素的三个分量是连续存储的,对比如下:
Planar
方式)y | y | y | y | u | u | u | u | v | v | v | v |
---|---|---|---|---|---|---|---|---|---|---|---|
y | y | y | y | u | u | u | u | v | v | v | v |
Packed
方式)R | G | B | R | G | B | R | G | B | R | G | B |
---|---|---|---|---|---|---|---|---|---|---|---|
R | G | B | R | G | B | R | G | B | R | G | B |
因为RGB24格式的每个像素的三个分量是连续存储的,所以只需要遍历全部文件数据,然后一次取3个字节,将3个字节分为R、G、B分量保存就可以了
unsigned char *pic=(unsigned char *)malloc(w*h*3);
fread(pic,1,w*h*3,fp);
for(int j=0;j<w*h*3;j=j+3){
fwrite(pic+j,1,1,fp1);
fwrite(pic+j+1,1,1,fp2);
fwrite(pic+j+2,1,1,fp3);
}
#原始文件大小 byte单位 十六进制
$ hexdump cie1931_500x500.rgb | tail -1 [~/D/y音视频]
00b71b0
$ python3.9 -c "print(int('0x0b71b0',16))" [~/D/y音视频]
750000
$ expr 750000 / 3 [~/D/y音视频]
250000
$ expr 500 \* 500 \* 3 [~/D/y音视频]
750000
$ python3.9 -c "print(hex(750000))" [~/D/y音视频]
0xb71b0
这里有个奇怪的现象,暂时放置,#####################
$ du -k cie1931_500x500.rgb [~/D/y音视频]
736K cie1931_500x500.rgb
$ expr 736 \* 1024 [~/D/y音视频]
753664
#这里的结果应该是750000才对
ffplay
查看图片
ffplay -s 500x500 -pix_fmt rgb24 cie1931_500x500.rgb
#需要指定格式 不然会按照yuv420p格式打开
ffplay -s 500x500 -pix_fmt rgb24 -vf extractplanes='r' cie1931_500x500.rgb
#分量
ffplay -s 500x500 -pix_fmt rgb24 -vf extractplanes='g' cie1931_500x500.rgb
ffplay -s 500x500 -pix_fmt rgb24 -vf extractplanes='b' cie1931_500x500.rgb
BMP 全称 Bitmap-File,是微软出的图像文件格式,目前只有 BMP 文件是用RGB 模式
BMP文件头(14 bytes) ,存放一些文件相关的信息。
0x36
)位图信息头,通常是 40 bytes 大小,也可以理解成 图像信息头,存放一些图像相关的信息,例如宽高之类的数据
调色板,大小由 颜色索引决定,本文的 juren.bmp 是 RGB24 模式,真彩色,没有用到 调色板,所以这个区域是0字节。
位图数据,对于本文的 juren.bmp 来说,里面就是 RGB 数据,一个像素占 3 个字节。
默认从左下角到右上角,排列顺序是由 位图信息头 里面那个 高度是正数还是负数决定的
这里我跟的时候发现代码运行后生成的bmp
打不开,后来发现是结构体定义格式错了,按照上面bmp
的头结构重新定义结构体后,不用进行R、B
分量互换,主流电脑、笔记本中用C
在写内存的时候会默认为小端模式,除非你在单片机上面写那就是大端模式
修订后的代码如下,因为这部分涉及到了结构体所有全部贴出来
#include
#include
struct Head {
/* char magic[2]; //一般是有4个字节长度的 Magic Number */
int32_t file_size;
int32_t extension;
int32_t offset;
} head;
struct Info {
int32_t info_size;
int32_t width; /* unit pixel */
int32_t height;
unsigned short num_color_planes;
unsigned short bit;
int32_t compress_type;
int32_t kernel_data_size;
int32_t horizontal_resolution;
short vertical_resolution; /* 2byte 下面需要再补充一个,如果置为0的话也可以不用写,编译器会通过字节对齐的方式跳过2个字节(下面是int_32) */
char _vertical_resolution; /* 补充1byte */
int32_t tab_index;
int32_t important_idx;
} info;
int main()
{
printf( "head byte -> %ld\n", sizeof(head) );
printf( "info byte -> %ld\n", sizeof(info) );
int w = 256, h = 256;
char bfType[2] = { 'B', 'M' };
FILE * fp_rgb24 = fopen( "lena_256x256_rgb24.rgb", "rb" );
FILE * fp_bmp = fopen( "out.bmp", "wb+" );
if ( fp_rgb24 == NULL || fp_bmp == NULL ){
printf( "NULL!" );
return(-1);
}
unsigned char* rgb24_buf = (unsigned char *) malloc( w * h * 3 );
unsigned char* re_rgb24_buf = (unsigned char *) malloc( w * h * 3 );
fread( re_rgb24_buf, 1, w * h * 3, fp_rgb24 );
//倒置字节
for (int i = 0; i < w * h * 3; ++i) {
rgb24_buf[i] = re_rgb24_buf[((w*h*3)-1)-i];
}
//不需要调整大小端
// char tmp;
// for ( int j = 0; j < w * h * 3; j = j + 3 ){
// tmp = rgb24_buf[j];
// rgb24_buf[j] = rgb24_buf[j + 2];
// rgb24_buf[j + 2] = tmp;
// }
int head_size = sizeof(bfType) + sizeof(head) + sizeof(info);
printf( "%d\n", head_size );
head.file_size = head_size + w * h * 3;
head.offset = head_size;
info.info_size = sizeof(info);
info.width = w;
info.height = h;
info.num_color_planes = 1;
info.bit= 3 * 8; /* rgb24 */
info.compress_type = 0;
info.kernel_data_size = w * h * 3;
info.horizontal_resolution = 0;
info.vertical_resolution = 0;
info.tab_index = 0;
info.important_idx = 0;
fwrite( bfType, 1, sizeof(bfType), fp_bmp );
fwrite( &head, 1, sizeof(head), fp_bmp );
fwrite( &info, 1, sizeof(info), fp_bmp );
fwrite( rgb24_buf, 1, w * h * 3, fp_bmp );
free( re_rgb24_buf );
re_rgb24_buf=NULL; //uaf
free( rgb24_buf );
rgb24_buf=NULL;
fclose( fp_rgb24 );
fclose( fp_bmp );
return 0;
}
这里需要注意的问题 : (不知道是不是我的编译器问题 -> Apple clang version 13.1.6
)
定义结构体的时候的字节对齐问题,我们知道BMP文件头是14 bytes,但是如果按照上面所述的结构来定义结构体的话,magic number
会被扩展字节按int_32
进行字节对齐,所以magic number
需要单独进行写入
写完的bmp
图片打开时,发生了图片颠倒的问题,所以需要倒置字节的操作,这里插个庄,后续再来处理
YUV 是 指 YCbCr ,U 就是 Cb,V 就是 Cr
各分量表示含义 : Y- 亮度,U-色调,V-色饱和度(彩度)
在位深度为8bit
的YUV 分量的值都是 0 ~ 255
(8位 = 2 8 2^8 28,即 [ 0 , ( 2 8 − 1 ) ] [0, (2^8 - 1)] [0,(28−1)]
这里的位深度就对应了PSNR
的计算,挺好玩的~
简单的说就是:
Y亮度一张图片有了Y分量那么通过Y分量在不同局部初亮度潜深就可以描绘出这张图片的大致轮廓
U、V共同的决定了这张轮廓图的色调,U、V组成了一个(0~255)二维坐标,每个坐标点就是一个颜色值
位深度就是只Y、U、V分量的取值范围,10bit就是0~1023的范围值,Y值的轮廓越来越明显,U、V色彩过滤就越来越缓和
在存储和编码之前,RGB 图像要转换为 YUV 图像,而 YUV 图像在 显示之前通常有必要转换回 RGB. 显示的时候 YUV 转成 RGB 通常是硬件或者软件内部做了,我们写代码开发的时候 这个 YUV 转 RGB 显示到屏幕这个过程通常是透明的。
经过大量研究实验表明,视觉系统 对 色度 的敏感度 是远小于 亮度的。所以可以对 色度 采用更小的采样率来压缩数据,对亮度采用正常的采样率即可,这样压缩数据不会对视觉体验产生太大的影响,从而就由yuv444p
产生了yuv422p
、yuv420p
格式,它们的共同点都是都缩减了u、v
分量的空间
yuvj444p
的p
就是planner
)这里的连续存储,不是一行像素里面连续存储,是整张图片的连续存储,例如xx444.yuv
图片是 6075kb 大小
Y U V 444 = S I Z E = 1920 ∗ 1080 ∗ 3 YUV444=SIZE=1920*1080*3 YUV444=SIZE=1920∗1080∗3
Y U V 422 = S I Z E = 1920 ∗ 1080 ∗ 2 YUV422=SIZE=1920*1080*2 YUV422=SIZE=1920∗1080∗2
Y U V 420 = S I Z E = 1920 ∗ 1080 ∗ 3 / 2 YUV420=SIZE=1920*1080*3/2 YUV420=SIZE=1920∗1080∗3/2
比如文件lena_256x256_yuv420p.yuv
因为是420p那么文件大小就是就是w*h*3/2
$ du -k lena_256x256_yuv420p.yuv [~/demo]
96 lena_256x256_yuv420p.yuv
$ expr $(expr 256 \* 256 \* 3 / 2) / 1024 [~/demo]
96
为了验证结果用vim
打开文件,然后执行%!xxd
转换成16进制,这里截取末尾
.....
00017fb0: b5b6 b4b4 b5b3 b4b5 b5b6 b4b6 b7b6 b6b7 ................
00017fc0: b6b5 b3b4 b3b2 b1b2 b1b1 afaf aeaf adad ................
00017fd0: aaab a8a5 a5a2 a09b 9895 9493 9292 9199 ................
00017fe0: 9f9e a09f a0a5 acb0 b1b0 afb1 b0aa a7a2 ................
00017ff0: 9e9f a0a0 a2a2 9e9b 9c9d 9c9e 9e9f a6ae ................
00018000: 0a
可以看到文件所有的数据长度为0x18000
byte => 十进制98304
byte => 十进制96
kb
yuv 图片文件除了 原始的像素数据,要正确打开一个YUV图片需要指定它的宽、高、采样格式
yuv 图片的像素存储顺序 是从 左上角 到 右下角的
存储顺序为 planner 文件中各分量所处位置,存在以下文件
$ du lena_256x256_yuv4* [~/demo]
192 lena_256x256_yuv420p.yuv
256 lena_256x256_yuv422p.yuv
384 lena_256x256_yuv444p.yuv
y | y | y | y | u | u | u | u | v | v | v | v |
---|---|---|---|---|---|---|---|---|---|---|---|
y | y | y | y | u | u | u | u | v | v | v | v |
unsigned char *pic=(unsigned char *)malloc(w*h*3);
fread(pic,1,w*h*3,fp);
//Y
fwrite(pic,1,w*h,fp1);
//U
fwrite(pic+w*h,1,w*h,fp2);
//V
fwrite(pic+w*h*2,1,w*h,fp3);
y | y | y | y | u | u | v | v | - | - | - | - |
---|---|---|---|---|---|---|---|---|---|---|---|
y | y | y | y | u | u | v | v | - | - | - | - |
第一个像素跟第二个像素 共享一个UV。
422 格式里面的 U 值是新值,是由第一个像素跟第二个像素的 U 值加起来除以 2 得到的,所以 422 格式的 UV 值 其实是平均值。
所以,422 格式,是第一第二个像素共享 一组UV,第三第四像素共享 一组 UV
unsigned char *pic=(unsigned char *)malloc(w*h*2);
fread(pic,1,w*h*2,fp);
//Y
fwrite(pic,1,w*h,fp1);
//U
fwrite(pic+w*h,1,w*h*1/2,fp2);
//V
fwrite(pic+w*h*3/2,1,w*h*1/2,fp3);
y | y | y | y | u | v | - | - | - | - | - | - |
---|---|---|---|---|---|---|---|---|---|---|---|
y | y | y | y | u | v | - | - | - | - | - | - |
第一行的第1,第2 像素,第二行的 第1,第2 像素共用一组 UV
不是第一行的 第 1~ 4 个像素共享 一组 UV,因为这样会造成空间距离太远,容易造成体验不太好
这些 UV 值都是新值,是以前的 4 个像素的 U 值加起来 除以4 的平均值
unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
fread(pic,1,w*h*3/2,fp);
//Y 这里Y分量正常取前面总文件大小的1/3
fwrite(pic,1,w*h,fp1);
//U 因为一个U是4个Y共享
fwrite(pic+w*h,1,w*h/4,fp2);
//V 同理一个V由4个Y共享
fwrite(pic+w*h*5/4,1,w*h/4,fp3);
分离后
使用ffmpeg
分离分量
ffmpeg -s 256x256 -pix_fmt yuv420p -i lena_256x256_yuv420p.yuv ff_420.y
#默认是yuv420p 这里也可不指定格式
ffmpeg -s 256x256 -pix_fmt yuv444p -i lena_256x256_yuv444p.yuv ff_444.y
#444p
ffmpeg -s 256x256 -pix_fmt yuv422p -i lena_256x256_yuv422p.yuv ff_422.y
#422p
测试结果是否准确
$ cmp output_420_y.y ff_420.y [~/demo]
$ cmp output_420_u.y ff_420.U [~/demo]
$ cmp output_420_v.y ff_420.V [~/demo]
$ cmp output_444_y.y ff_444.y [~/demo]
$ cmp output_444_u.y ff_444.U [~/demo]
$ cmp output_444_v.y ff_444.V [~/demo]
$ cmp output_422_y.y ff_422.y [~/demo]
$ cmp output_422_u.y ff_422.u [~/demo]
$ cmp output_422_v.y ff_422.v [~/demo]
#没有输出不一样的地方,说明提取正确
ffplay -s 256x256 -vf extractplanes='y' lena_256x256_yuv420p.yuv
#播放y分离,u,v同理
各分量 Y- 亮度,U-色调,V-色饱和度(彩度),U、V是图像中的经过偏置处理的色度分量。
因为要去掉图像的颜色所以需要将U、V分量置为无色,也就是Cr、Cb
构成的二维坐标中的原点值即(128,128)
unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
fread(pic,1,w*h*3/2,fp);
//U V分量取值为128
memset(pic+w*h,128,w*h/2);
fwrite(pic,1,w*h*3/2,fp1);
这个简单,将前面w*h
的Y
分量读出来然后取半值再进行写入(bb 一句 c语言可以直接操作内存和无缝嵌入汇编的特性不亏是写算法的瑞士军刀,现在其他语言限制太多了,这一点又想起了Darwin、linux、Windows不得不说 Linux + C + GNU-Coreutils的组合用起来那真是一个随心所欲
)
unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
int y = w*h;
int u = w*h/4;
int v = w*h/4;
fread(pic,1,w*h*3/2,fp);
unsigned char tmp = {0};
for(int i=0;i<y;i++){
tmp = pic[i]/2;
pic[i] = tmp;
}
fwrite(pic,1,w*h*3/2,fp1);
这个也没什么好说的就是计算Y分量的起始值范围
unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
int y = w*h;
int u = w*h/4;
int v = w*h/4;
fread(pic,1,w*h*3/2,fp);
unsigned char tmp = {0};
int border = 5;
for(int i=0;i<y;i++){
if( i <= border*h||
i >= (h-border)*h ||
i%w <= border ||
i%w >= (w-border)){
pic[i] = tmp;
}
}
fwrite(pic,1,w*h*3/2,fp1);
float lum_inc;
int y = width*height;
int u = width*height/4;
int v = width*height/4;
int barwidth=width/barnum;
lum_inc=((float)(ymax))/((float)(barnum-1));
unsigned char lum_temp;
unsigned char* data_y=(unsigned char *)malloc(y);
unsigned char* data_u=(unsigned char *)malloc(u);
unsigned char* data_v=(unsigned char *)malloc(v);
fp=fopen(url_out,"wb+");
for(int j=0;j<height;j++){
for(int i=0;i<width;i++){
int index = i / barwidth;
lum_temp = index*lum_inc;
data_y[j*width+i]=(char)lum_temp;
}
}
memset(data_u,128,u);
memset(data_v,128,v);
fwrite(data_y,y,1,fp);
fwrite(data_u,u,1,fp);
fwrite(data_v,v,1,fp);
这里需要求一个MSE值
和PSNR值
。这个算法打算在后面和SSIM、VMAF
一起深入研究
M S E = 1 w ⋅ h ∑ i = 0 w − 1 ∑ j = 0 h − 1 [ i m g 1 ( i , j ) − i m g 2 ( i , j ) ] 2 MSE = \frac{1}{w \cdot h } \sum_{i=0}^{w-1} \sum_{j=0}^{h-1} \left[img_1(i,j) - img_2(i,j)\right]^2 MSE=w⋅h1i=0∑w−1j=0∑h−1[img1(i,j)−img2(i,j)]2
P S N R = 10 ⋅ ln [ ( 2 n − 1 ) 2 M S E ] PSNR = 10 \cdot \ln{\left[\frac{(2^n - 1)^2}{MSE}\right]} PSNR=10⋅ln[MSE(2n−1)2]
其中 n 就是 b i t 位值,一般情况下是 8 b i t , 也就是 n = 8 \textcolor{red}{其中n就是bit位值,一般情况下是8bit,也就是n=8} 其中n就是bit位值,一般情况下是8bit,也就是n=8
程序引入数学库就很简单了
unsigned char *pic1=(unsigned char *)malloc(w*h);
unsigned char *pic2=(unsigned char *)malloc(w*h);
fread(pic1,1,w*h,fp1);
fread(pic2,1,w*h,fp2);
double mse_sum=0,mse=0,psnr=0;
for(int j=0;j<w*h;j++){
//得到两个对比文件y值的-间隔值平方之和
mse_sum+=pow((double)(pic1[j]-pic2[j]),2);
}
//得到这个-间隔值平方之和-在y分量中的比重
mse=mse_sum/(w*h);
unsigned int bit = pow(2,8);
psnr=10*log10(pow((bit - 1),2)/mse);
printf("%5.3f\n",psnr);
因为RGB中的每个分量都表示颜色值,所有不同的颜色值亮度也不同使RGB三个分量都与亮度相关,根据分量占有不同比重的亮度提取存在如下公式:
Y = K r ⋅ R + K g ⋅ G + K b ⋅ B Y = Kr \cdot R + Kg \cdot G + Kb \cdot B Y=Kr⋅R+Kg⋅G+Kb⋅B
K K K :权重因子
K r Kr Kr :Red
通道的权重
K g Kg Kg :Green
通道的权重
K b Kb Kb :Bule
通道的权重
既然是权重则有 K r + K g + K b = 1 Kr + Kg + Kb = 1 Kr+Kg+Kb=1
K权重因子会影响压缩率,所以K的具体取值是根据不同的标准而定的,现有如下标准 :
BT | Kr | Kg | Kb |
---|---|---|---|
601(SDTV标清) | 0.299 | 0.587 | 0.114 |
709(HDTV高清) | 0.2126 | 0.7152 | 0.0722 |
2020(UHDTV超高清) | 0.2627 | 0.6780 | 0.0593 |
BT.601
因为市面上的文章都是针对BT.601
进行讲解,为了方便资料查阅,这里也使用该标准进行实验
按照权重比可知.在 R 通道里面有 29.9% 的亮度信息,在 G通道 有 58.7% 的亮度信息,在 B通道 有 11.4% 的亮度信息,那么可得公式如下:
Y = 0.299 R + 0.587 G + 0.114 B Y = 0.299R + 0.587G + 0.114B Y=0.299R+0.587G+0.114B
为了对标专业理论知识,这里用Cr
来表示U
。上面YUV
讲解也有讲到
Cr
的定义是 R - Y,求解就很简单了:
∵ C r = R − Y ∴ C r = R − ( 0.299 R + 0.587 G + 0.114 B ) = R − 0.299 R − 0.587 G − 0.114 B = 0.701 R − 0.587 G − 0.114 B 当 R 值最大 G 、 B 值最小时 , 有最大值 , 在不同的格式中 R 、 G 、 B 的值不同,这里采取函数表达 ∵ R 、 G 、 B 的取值范围都是一样所以设 X = m a x , 最小值都为 0 ∴ C r m a x = 0.701 X − 0 − 0 = 0.701 X ∴ C r m i n = 0 − 0.587 X − 0.114 X = − 0.701 X \because Cr = R - Y \\ \therefore Cr = R - (0.299R + 0.587G + 0.114B) \\ = R - 0.299R - 0.587G - 0.114B \\ = 0.701R - 0.587G - 0.114B\\ 当R值最大G、B值最小时,有最大值,在不同的格式中R、G、B的值不同,这里采取函数表达\\ \because R、G、B的取值范围都是一样所以设X = max ,最小值都为0 \\ \therefore Cr_{max} = 0.701X - 0 - 0 = 0.701X \\ \therefore Cr_{min} = 0 - 0.587X - 0.114X = -0.701X ∵Cr=R−Y∴Cr=R−(0.299R+0.587G+0.114B)=R−0.299R−0.587G−0.114B=0.701R−0.587G−0.114B当R值最大G、B值最小时,有最大值,在不同的格式中R、G、B的值不同,这里采取函数表达∵R、G、B的取值范围都是一样所以设X=max,最小值都为0∴Crmax=0.701X−0−0=0.701X∴Crmin=0−0.587X−0.114X=−0.701X
所以上述可知Cr的值域为 [ − 0.701 X , 0.701 X ] [-0.701X,0.701X] [−0.701X,0.701X],那么它的增量可以为
2 ∗ 0.701 X ∗ 10 = 1.402 X 2*0.701X*10 = 1.402X 2∗0.701X∗10=1.402X
这里挖个坑,具体求值后面再来填,有点奇怪这里,需要参考一些其他资料
雷神这里的公式可以转换成功,那就先用着
Y = 0.299 R + 0.587 G + 0.114 B U = − 0.147 R + 0.289 G + 0.463 B V = 0.615 R − 0.515 G − 0.100 B Y = 0.299R+0.587G+0.114B \\ U = -0.147R +0.289G+ 0.463B\\ V = 0.615R-0.515G-0.100B Y=0.299R+0.587G+0.114BU=−0.147R+0.289G+0.463BV=0.615R−0.515G−0.100B
//R\G\B : 1byte : unsigned : 0xff
unsigned int64 tmp = 66*r + 129*g + 25*b
//转换一下
//max = 66*0xff + 129*0xff + 26*0xff = 0xdc23
//min = 66*0 + 129*0 + 26*0 = 0
//可以看到由R、G、B转过来的Y值范围是[0,0xdc23],可能是1-2字节。最大也就是2个字节
unsigned int64 tmp2 = tmp + 128
//max = 0xdc23 + 0x80 = 0xdca3
//carry = 0xdb83 + 0x80 = 0xdc03
//min = 0 + 0x80 = 0x80
//解释一下为什么是+128,因为128是二进制的第8位,加了128就可以判断低位字节是否>=128,当>=128时则会产生进位,反之不影响高位字节
unsigned int64 tmp3 = tmp2 >> 8
//这里就是去掉了低字节
//Y = 0xdc03 >> 8 = 0xdc
//再回到公式
//Y = 0.299R + 0.587G + 0.114B ,C=1
//Y = 66R + 129G + 25B, C=220
//它们的系数相加是1,
Intra-coded picture(帧内编码图像帧)
关键帧,可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成
I帧“关键帧”压缩法 : 基于离散余弦变换DCT(Discrete Cosine Transform)的压缩技术,这种算法与JPEG压缩算法类似。采用I帧压缩可达到1/6的压缩比而无明显的压缩痕迹。
当不是直播的时候一般B帧为连续2帧,从而降低码率
每次收到I帧就可以清除上一个I帧
在视频画面播放过程中,若I帧丢失了,则后面的P帧也就随着解不出来,就会出现视频画面黑屏的现象;若P帧丢失了,则视频画面会出现花屏、马赛克等现象。
Predictive-coded Picture(前向预测编码图像帧)
P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别(基于预测值和运动矢量),解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面
Bidirectionally predicted picture(双向预测编码图像帧)
B帧不管在哪个位置都需要等后面一帧解完才能解码B帧,所以会带来编码延迟,因为B帧要等P帧解码完成
I | B | P | |
---|---|---|---|
解码DTS | 1 | 3 | 2 |
显示PTS | 1 | 2 | 3 |
实现这个功能是通过下面的PTS、DTS的
B帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确。所以比较节省内存
直播:
压缩和解码B帧时,由于要双向参考,所以它需要缓冲更多的数据,且使用的CPU也会更高,实时互动直播系统中,很少使用B帧。
编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures )
解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束
GOP可以理解为I帧间隔,一般直播GOP为1-2秒
GOP越长视频压缩效率越高,视频质量越差
直播时如果帧率为25,则一般GOP为25(1秒)、50(2秒)帧率的倍数
一帧图像可以分为多个slice
通过系统线程并发编码从而加快编码速度
量化值和码率有关,量化越大图片损失越重,反之
网络好使用低码率编码进行传输,反之。所以可以使用动态修改码率,其实就是修改量化值
这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
在没有B帧的情况下,存放帧的顺序和显示帧的顺序就是一样的,PTS和DTS的值也是一样的。
H.264
H.120
、H.261
、H.263
等标准AVC(Advanced Video Coding)
MPEG-1
、MPEG-2
、MPEG-4 Part2
等标准H264/AVC
H265/HEVC
以下简称h264
是一款收费编码器,设备生产商的设备
H.264和x264的区别:
H.264是一种编码标准,x264是符合H.264标准的一个开源项目是免费的。
可以理解为一个社区版,但是不支持某些H.264的高级特性
H.264是商业版
X264不支持硬件加速
H264中没有音频、时间戳、只有图片信息,所以也是大家口中所说的码流、或者裸流,H264中分为两种不同的存储格式:1.AnnexB
2.avcC
注意:解码的环境下
- ios中ViedeoToolbox只支持avcC格式
- android中MediaCodec只支持AnnexB格式
它由一个个的NALU(Network Abstraction Layer Unit)
组成,结构如下:
图中每个NALU
之间通过startcode
(起始码)进行分隔,起始码分为:
当NALU
对应的Slice
为一帧数使用4byte
的起始码,否则使用3byte
的起始码分离出NALU
。
因为startcode可能会在码流中找到相同的字节,那么就会导致一个NALU被再次分割为消NALU,所以就出现了防竞争字节序
防竞争字节序就是在给NALU添加起始码之前,先对码流中的相同startcode进行字节替换
比如:码流中的000\001\002\003
等先给它替换成为其他字节,最后再还原
00 ∣ 00 ∣ 00 → 00 ∣ 00 ∣ 03 ∣ 00 00 ∣ 00 ∣ 01 → 00 ∣ 00 ∣ 03 ∣ 01 00 ∣ 00 ∣ 02 → 00 ∣ 00 ∣ 03 ∣ 02 00 ∣ 00 ∣ 03 → 00 ∣ 00 ∣ 03 ∣ 03 00|00|00 \rightarrow 00|00|03|00\\ 00|00|01 \rightarrow 00|00|03|01\\ 00|00|02 \rightarrow 00|00|03|02\\ 00|00|03 \rightarrow 00|00|03|03\\ 00∣00∣00→00∣00∣03∣0000∣00∣01→00∣00∣03∣0100∣00∣02→00∣00∣03∣0200∣00∣03→00∣00∣03∣03
avcC主要用于mp4容器中。它采用的分割格式是在NALU前面的字节表示单个NALU的长度(大端模式)
在avcC格式中存在两个特殊的NALU即SPS
、PPS
它们存放了在解码H264码流时必要的参数信息,在解码前必须获取到SPS
、PPS
bits | name | description |
---|---|---|
8 | version | 一般为1 |
8 | avc profile | 首个SPS的第1个字节 |
8 | avc compatibility | 首个SPS的第2个字节 |
8 | avc level | 首个SPS的第1个字节 |
6 | reserved | |
2 | NALULengthSizeMinusOne | NULU长度-1(范围0~3) |
3 | reserved | |
5 | number of SPS NALUs | 有几个SPS,一般为1 |
16 | SPS size | SPS长度 |
… | SPS NALU data | SPS NALU的数据 |
… | SPS_2 size | |
… | SPS_2 NALU data | |
… | SPS_n size | |
… | SPS_n NALU data | |
8 | number of PPS NALUs | 有几个PPS,一般为1 |
16 | PPS size | PPS长度 |
… | PPS NALU data | PPS NALU的数据 |
NALULengthSizeMinusOne为3的时候,NALU的长度为4个字节,就是说NALU的前4个字节存放着它本身的数据长度值(大端模式、且该4个字节表示的值不包括该4个字节)
NALU中除去startCode
的部分被称为EBSP
数据比特串
NALU中除去startCode
、防竞争字节的部分叫RBSP
数据比特串
RBSP中除去补位bit的部分叫SODB
数据比特串:
视频在编码是以bit
为单位写入码流的(用户在输入时候是以字节为单位的),当写入的bit不满足8的倍数即剩余的bit不满一个byte。那么会在后续操作中不便于读取,所以在构建RBSP
时会将最后不满一个byte的情况进行补齐,规则为:
先写入 1 bit 数据,数据内容是 1,然后开始补齐 0,直到补齐到一整个字节。
而当最后满了一个字节也会对它进行添加1一个字节操作00000001
最后的SODB
就是裸码流数据
结构图如下:
nal_unit(NumButeslnNaLunit) | Descriptor | |
---|---|---|
forbidden_zero_bit | 1 bit | 静止位。当NALU丢失数据时为1,一般为0 |
nal_ref_idc | 2 bit | NALU重要性 |
nal_unit_type | 5 bit | NALU类型 |
nal_ref_idc存在以下值:
当nal_ref_idc为0时则表示该NALU可有可无,即使丢失也不重要
nal_unit_type存在以下值:
nal_unit_type | NALU 类型 |
---|---|
0 | 未定义 |
1 | 非 IDR SLICE |
2 | 非 IDR SLICE,采用 A 类数据划分片段 |
3 | 非 IDR SLICE,采用 B 类数据划分片段 |
4 | 非 IDR SLICE,采用 C 类数据划分片段 |
5 | IDR SLICE |
6 | 补充增强信息 SEI |
7 | 序列参数集 SPS |
8 | 图像参数集 PPS |
9 | 分隔符 |
10 | 序列结束符 |
11 | 码流结束符 |
12 | 填充数据 |
13 | 序列参数扩展集 |
14~18 | 保留 |
19 | 未分割的辅助编码图像的编码条带 |
20~23 | 保留 |
24~31 | 未指定 |
seq_parameter_set_data() | C | Descriptor |
---|---|---|
profile_idc | 0 | u(8) |
constraint_set0_flag | 0 | u(1) |
constraint_set1_flag | 0 | u(1) |
constraint_set2_flag | 0 | u(1) |
constraint_set3_flag | 0 | u(1) |
constraint_set4_flag | 0 | u(1) |
constraint_set5_flag | 0 | u(1) |
reserved_zero_2bits | 0 | u(2) |
level_idc | 0 | u(8) |
seq_parameter_set_id | 0 | ue(8) |
H.264 中,不会逐个像素去处理图像,而是按照固定的长宽划分成小块称之为宏块
当像素为176x144时,规定一个宏块为16x16像素,那么就会划分为11x9的宏块
这里的格式是 宽x高,不是矩阵中的 行x列 ,刚好相反
解码器DAV1D
https://www.google.com/search?q=AV1+%E5%92%8C+DAV1D%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB&newwindow=1&client=safari&rls=en&sxsrf=ALiCzsanU402XgaKe__9IG7De5Vr87Mmvw%3A1672714061083&ei=TZezY8HjBOq84-EP1YKC0AM&ved=0ahUKEwiBvq-7sar8AhVq3jgGHVWBADoQ4dUDCA4&uact=5&oq=AV1+%E5%92%8C+DAV1D%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB&gs_lcp=Cgxnd3Mtd2l6LXNlcnAQAzIFCCEQoAEyBQghEKABMgUIIRCgAToKCAAQRxDWBBCwAzoECCMQJ0oECEEYAEoECEYYAFDaBVjvJWCDJ2gDcAF4BIAB7wOIAbstkgEENC0xM5gBAKABAcgBBMABAQ&sclient=gws-wiz-serp
super-resolution
compression
master
quality
码率 : kb/s 视频文件在单位时间内使用的数据流量
帧率 : fps一秒钟显示的帧数,帧率越高画面越流畅
分辨率 : 影响视频图像的大小
#修改帧率
ffmpeg -i test_video.mp4 -r 15 output.mp4 #导致画面卡顿,ppd
#修改码率
ffmpeg -i test_video.mp4 -b:v 0.5M output2.mp4 #会导致画面模糊
#修改分辨率
ffmpeg -i test_video.mp4 -s 640x480 output.mp4 #导致窗口变小
数字电影应用中 : 2K通常指2048x1080
,4K通常指4096x2160
序号 | 图像分辨率 | 图像容器 |
---|---|---|
1 | HD | 1920x1080 |
2 | 2K | 2048x1080 |
3 | 4K | 4096x2160 |
HD略比2k小,现在可以归类的2K档次
消费者普遍认知中 : 2K通常指 2560 × 1440
序号 | 图像分辨率 | 图像容器 |
---|---|---|
720P | 1280x720 | HD(高清) |
1080P | 1920x1080 | FHD(全高清) |
2K | 2560x1440 | QHD(四倍高清) |
4K | 3840x2160 | UHD(超高清) |
慢速档位 : 每秒1帧
快速档位 : 每秒30帧
常见画质对比数值 :
以下为提供硬件编码的框架:
Intel
Nvidia
AMD
厂商
因为解码只能靠CPU,而且一般硬编码压缩不如软编码好,码率相同情况下质量明显下降,比如VideoToobox实测如下:
ffmpeg -hwaccels
#显示所有可用的硬件加速器
测试视频信息:
#input.mp4:
#width=2880
#height=1800
#codec_name=h264
#pix_fmt=yuv420p
#nb_frames=4000
#bit_rate=5682608
#intput.mp4大小113M
测试命令:
ffmpeg -i intput.mp4 -pix_fmt yuv420p10le demo_no_gpu.h265 #软编h265
ffmpeg -i intput.mp4 -pix_fmt yuv420p10le -c:v hevc_videotoolbox demo.h265 #硬编h265
ffmpeg -i intput.mp4 -pix_fmt yuv420p10le demo_no_gpu.h264 #软编h264
ffmpeg -i intput.mp4 -pix_fmt yuv420p10le -c:v h264_videotoolbox demo.h264 #硬编h264
#至于为什么使用yuv420p10le 是因为h265编码时会报错,264不会
#看这里https://stackoverflow.com/questions/69777429/hevc-videotoolbox-encoding-failed
测试结果:
编码器 | 耗时 | 编码后大小 |
---|---|---|
x265 | 276.05 s | 8.3M |
hevc_videotoolbox | 75.97 s | 31M |
x264 | 114.90 | 17M |
h264_videotoolbox | 28.08 s | 22M |
ffmpeg -i demo.avi -c:v libx264 -crf 22 out.mp4
#crf表示控制压缩图像质量,取之范围0-51,范围越高质量越差,即0表示无损压缩,常用范围是19-28
ffmpeg -i demo.avi -c:v libx264 -vf "scale=1024:-1,transpose=1" out.mp4
#vf(video filter)指定过滤器
#scale-修改视频尺寸(-1表示自动推算)
#transpose-旋转视频
#crop-裁剪
#对于24帧每秒的视频 128帧是第5秒的第四帧
ffmpeg -i a.mp4 -ss 00:00:05.167 -f image2 -r 1 -t 1 -s 256*256 /home/pic.jpeg
#附注 没有-r会截取25张图片,此时-ss指定时间不指定毫秒时,会截取该秒内所有帧,否则会截取同一毫秒的25张图片
ffmpeg -f image2 -i image%d.jpg video.mpg
#当前目录下的图片(名字如:image1.jpg, image2.jpg, 等...)合并成video.mpg
ffmpeg -i source_video.avi -vn -ar 44100 -ac 2 -ab 192 -f mp3 sound.mp3
#视频抽出声音
ffmpeg -ss 00:00:30 -i 666051400.mp4 -vframes 1 0.jpeg
#抽取时间段的帧
ffmpeg -loglevel quiet -i d1.mp4 -c copy -frames:v 10000 input.mp4 < /dev/null
#提取前10000帧
ffmpeg -i input.mp4 -an -vf select='eq(pict_type\,I)' -vsync 2 -f image2 image-%001d.jpeg
#提取I帧
ffmpeg -i out.mp4 -an -vf select='eq(pict_type\,I)' -vsync 2 -f image2 -qscale:v 2 image%004d.jpeg
#质量更好
ffmpeg -ss 00:12:07 -to 0:17:33 -i input.mp4 -c copy out.mp4
#截取时间段视频
ffmpeg -ss 00:12:07 -t 60 -i input.mp4 -c copy out.mp4
#截取持续时间 单位s
ffmpeg -hwaccels
#显示所有可用的硬件加速器
一般一样格式的视频不需要进行重新编码
#ffplay 默认打开文件是I420格式也就是yuv420p格式,任何文件也是这个格式,打开rbg也是默认I420
ffplay -s 1920x1080 -pixel_format rgb24 demo.rgb
ffmpeg
解析中包括了mp4
的头部分装信息,而ffprobe
不会解析头部信息ffmpeg -i src2.mp4
#Duration: 00:00:03.73, start: 0.000000, bitrate: 400 kb/s
ffprobe
ffprobe -loglevel quiet -select_streams v -show_streams src2.mp4 | grep ^bit_rate
#bit_rate=380916
从 libavcodec/avcodec.c 中的 avcodec_string()
所定义的码率信息打印操作的代码可以知道,在 ffmpeg
中,1kb 的确为 1000 bit。
bitrate = get_bit_rate(enc);
if (bitrate != 0) {
av_bprintf(&bprint, ", %"PRId64" kb/s", bitrate / 1000);
} else if (enc->rc_max_rate > 0) {
av_bprintf(&bprint, ", max. %"PRId64" kb/s", enc->rc_max_rate / 1000);
}
具体解释 :https://wangwei1237.github.io/2022/06/19/Does-1Kbit-equal-1024bit/
测试文件src.mp4
[STREAM]
index=0
codec_name=h264
codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
profile=High
codec_type=video
codec_time_base=1/50
codec_tag_string=avc1
codec_tag=0x31637661
width=1920
height=1080
coded_width=1920
coded_height=1088
has_b_frames=2
sample_aspect_ratio=N/A
display_aspect_ratio=N/A
pix_fmt=yuv420p
level=40
chroma_location=left
field_order=unknown
timecode=N/A
refs=1
is_avc=true
nal_length_size=4
id=N/A
r_frame_rate=25/1
avg_frame_rate=25/1
time_base=1/25
start_pts=0
start_time=0.000000
duration_ts=468
duration=18.720000
bit_rate=4999281
max_bit_rate=N/A
bits_per_raw_sample=8
nb_frames=468
TAG:creation_time=2020-09-18T00:36:51.000000Z
TAG:language=und
TAG:handler_name=L-SMASH Video Handler
TAG:encoder=AVC Coding
[/STREAM]
首先将src.mp4
转成tmp.yuv
、tmp.h264
、tmp.h265
以及无损的tmp.mp4
$ du src.mp4 (master*) [~/h/ios_test]
12M src.mp4
$ ffmpeg -i src.mp4 tmp2.yuv (master*) [~/h/ios_test]
$ ffmpeg -s 1920x1080 -i tmp.yuv -crf 0 tmp.h264
$ ffmpeg -s 1920x1080 -i tmp.yuv -crf 0 tmp.h265
$ du tmp.* src.mp4 (master*) [~/h/ios_test]
159M tmp.mp4
159M tmp.h264
109M tmp.h265
1.4G tmp.yuv
12M src.mp4
可以看到h265
编码占有空间最小
//https://www.cnblogs.com/tinywan/p/6402007.html
先科普一下profile&level。(这里讨论最常用的H264)
H.264有四种画质级别,分别是baseline, extended, main, high:
1、Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;
2、Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;(用的少)
3、Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),
也支持CAVLC 和CABAC 的支持;
4、High profile:高级画质。在main Profile 的基础上增加了8x8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;
H.264 Baseline profile、Extended profile和Main profile都是针对8位样本数据、4:2:0格式(YUV)的视频序列。在相同配置情况下,High profile(HP)可以比Main profile(MP)降低10%的码率。
根据应用领域的不同,Baseline profile多应用于实时通信领域,Main profile多应用于流媒体领域,High profile则多应用于广电和存储领域。
下图清楚的给出不同的profile&level的性能区别。
库里还可以包含对 H.264/MPEG-4
AVC 视频编码的 X264 库,是最常用的有损视频编码器,支持 CBR、VBR
模式,可以在编码的过程中直接改变码率的设置,在直播
的场景中非常适用!可以做码率自适应的功能