文章目录
在本实验报告中,将会呈现对BMP文件组成的理解,然后根据其组成呈现编写的解析BMP并转换为YUV的过程,最后展示多张BMP组合成的动态YUV的原理、实现和效果。
1 BMP文件的组成
1.1 存储格式
BMP文件分为四个部分顺序线性存储:
- 位图头文件包含 BMP 图像文件的类型、显示内容等信息;
- 位图信息头包含有 BMP 图像的宽、高、压缩方法,以及定义颜色等信息。
- 调色板 ,这个部分可选,24位真彩色和16位是无调色板的。
- 实际的位图数据,一般是按照从左到右、从下到上的顺序。
所有存储的顺序都按照Intel格式存储。Intel格式可见上次作业介绍
在程序中,通过Windows.h
头文件我们可以直接访问BITMAP各类头的结构体:
File Header | Info Header |
---|---|
1.2 BMP各位深度的常见规范
- 对于
1bit
位深度的BMP文件:
- 调⾊板有2种颜⾊,每个颜⾊4个字节,共8字节的调⾊板
- 图像数据的每1bit代表⼀个像素。 - 对于
4bits
位深度的BMP文件:
- 调⾊板有16种颜⾊,每个颜⾊4个字节,共64字节的调⾊板。
- 图像数据的每4bits代表⼀个像素。 - 对于
8bits
位深度的BMP文件:
- 调⾊板有256种颜⾊,每个颜⾊4个字节,共1024字节的调⾊板。
- 图像数据的每8bits代表⼀个像素。 - 对于
16bits
位深度的BMP文件:
- 该深度的BMP文件无调色板
- 存储的数据一般是R5 G6 B5
或者R5 G5 B5,1Reserved
- 所以在映射RGB数值时需要使用位运算、相与等操作提取出有效RGB数值。 - 对于
24bits
位深度的BMP文件:
- 24bits为真彩⾊数据,不含调⾊板,每24bits为⼀个像素。(R8G8B8)
2 解析单张BMP并转换为YUV的过程
2.1 解决思路
本次的BMP图像采用PhotoShop生成,宽度和高度为1024像素,位深度为24位真彩色,不涉及调色板。
因此,打开BMP文件后,先根据FileHeader判断图像属性,然后仅需从InfoHeader中读取出宽和高,开出相应的缓存区即可。
/*<---这是一个伪代码--->*/
BITMAPFILEHEADER fileHeader;
originBMP.read infoHeader;
int width, height;
width = infoHeader.biWidth;
height = infoHeader.biHeight;
int size = width * height;
unsigned char* ImageData = new unsigned char[size * 3];
再开辟等空间的YUV等Buffer;
调用RGB2YUV函数进行转换;
RGB2YUV函数:见上次实验1
2.2 BMP的注意点:倒序读写
注意点是,BMP文件的存储顺序是从左到右、从下到上的顺序,也就是说,一张图像的显示和存储顺序如下图对应:
因此,在RGB2YUV中有一个flip参数,为0时,在每帧数据读取时可以直接将数据倒序存储
y = y_buffer + (y_dim - j - 1) * x_dim;
u = u_buffer + (y_dim - j - 1) * x_dim;
v = v_buffer + (y_dim - j - 1) * x_dim;
3 将多张图片BMP转换为YUV的动画序列
将YUV文件多帧数据追加读写,在播放时选择合适的帧率,就可以获得播放视频的效果。YUV视频存储格式如下图:
因此,设立for
循环和多个argv
参数,实现多张文件的读写。
-
argv参数:定义第一个参数是读入的bmp图片的个数n,后面n个数据读入的是bmp的文件(相对)路径 ,最后一个参数是输出yuv的文件名
-
for循环:将读取文件、转换的过程放入for循环内,但是写的文件在循环体外,进行追加写文件:
/*<---这是一个伪代码--->*/
int inputPicNum = atoi(argv[1]);
新建写出文件的流ofstream;
for (int j = 2; j <= inputPicNum+1; j++) {
循环体内进行单张BMP的解析和转换为YUV如第2部分所示;
}
4 添加炫酷的转场动画
4.1 转场的渐变算法
最初,我设定了每张图片存储60帧,3张图片共180帧。在播放时,选择帧率为30fps,参数设定如下:
为了实现转场,重新对帧数进行划分:
45 | 30 | 45 | 30 | 45 | … |
---|---|---|---|---|---|
第一张图片 | 1->2转场 | 第二张图片 | 2->3转场 | 第三张图片 | … |
为了实现淡出渐渐转换的效果,转场效果的实现算法如下:
我们对一张图像上单个像素点进行分析,其余像素点执行完全相同的效果。
设转换的帧数为 n n n,旧像素数值为 v o l d v_{old} vold,新像素数值为 v n e w v_{new} vnew。则第 i i i帧该点的像素值应该为 ( n − i ) n × v o l d + i n × v n e w \frac{(n-i)}{n}\times v_{old}+\frac{i}{n}\times v_{new} n(n−i)×vold+ni×vnew
即对两像素赋不同权重,帧数越靠后,新像素的权重越大,逐渐完成转换。
注意:第一张图片不设计转换,故无需进行此计算,后面的帧需要进行。
表示为代码:
/*<---这是一个伪代码--->*/
if (是第一张图片) {
写第一张图片的45帧数据;
}
else{
开辟Y、U、V的转换数据Buffer Y_Trans;
for (int i = 0; i < 30; i++) {
for (int k = 0; k < size; k++) {
*(Y_Trans+k)= (*(Y_Buffer_Old + k) * (30 - i) + *(Y_Buffer + k) * i) / 30;
}
U、V同理进行运算,只不过只做Y的1/4;
将数据写入输出文件;
}
写当前图像的数据45帧;
}
}
4.2 缓存区的开辟和顺序的注意点
过去一张图片的Buffer的生命周期需要长过单张图片的读入,故需要把Buffer放在for循环外面。更新值要在数据写完,下一张图片读入的开始前。
/*<---这是一个伪代码--->*/
unsigned char *Buffer_Old = new unsigned char[size];
for{
读新图片;
写转场;
写新数据;
将当前图片的值存入Buffer_Old中;
删除开辟的内存,除了Buffer_Old;
}
这样就实现了过去帧的存储和转换计算过程。
5 效果展示
使用的图片是我的头像,和一些表情包。图片经过PhotoShop处理,统一宽度和高度为1024像素,位深度为24位真彩色。
右边是一个标准版的淡出淡出的图片,左边是做了一个炫酷闪闪的切换动画。
一些脑洞
既然可以实现这样的效果,要实现炫酷七彩变色也不是不可能。只需要计算出各颜色的YUV值,然后从原像素到这个值,再到新图像的出现就可以。
6 完整代码
RGB2YUV的代码可以参考上次的实验报告。
main.cpp
# include<iostream>
# include<fstream>
# include <Windows.h>
# include "rgb2yuv.h"
using namespace std;
// 参数定义:第一个参数是读入的bmp图片的个数n,后面n个数据读入的是bmp的文件(相对)路径 ,最后一个参数是输出yuv的文件名
int main(int agrc,char ** argv) {
int inputPicNum = atoi(argv[1]);
ofstream YUV_out(argv[inputPicNum+2], ios::binary);
if (!YUV_out) {
cout << "open file failed!" << endl;
return 0;
}
for (int j = 2; j <= inputPicNum+1; j++) {
ifstream originBMP(argv[j], ios::binary);
if (!originBMP) {
cout << "open file failed!" << endl;
return 0;
}
BITMAPFILEHEADER fileHeader;
originBMP.read((char*)&fileHeader, sizeof(fileHeader));
if (fileHeader.bfType != 0x4D42) {
cout << "这不是一张BMP图像。";
exit(0);
}
BITMAPINFOHEADER infoHeader;
originBMP.read((char*)&infoHeader, sizeof(infoHeader));
int width, height;
width = infoHeader.biWidth;
height = infoHeader.biHeight;
int size = width * height;
unsigned char* ImageData = new unsigned char[size * 3];
originBMP.read((char*)ImageData, size * 3);
unsigned char* Y_Buffer = new unsigned char[size];
unsigned char* U_Buffer = new unsigned char[size / 4];
unsigned char* V_Buffer = new unsigned char[size / 4];
unsigned char *Y_Buffer_Old = new unsigned char[size];
unsigned char* U_Buffer_Old = new unsigned char[size / 4];
unsigned char* V_Buffer_Old = new unsigned char[size / 4];
RGB2YUV(width, height, ImageData, Y_Buffer, U_Buffer, V_Buffer, 0);
//flip设置为0,使函数每行倒着存。bmp是倒序存储。
if (j == 2) {
for (int i = 0; i < 45; i++) {
YUV_out.write((char*)Y_Buffer, size);
YUV_out.write((char*)U_Buffer, size / 4);
YUV_out.write((char*)V_Buffer, size / 4);
}
}
else{
unsigned char* Y_Trans = new unsigned char[size];
unsigned char* U_Trans = new unsigned char[size / 4];
unsigned char* V_Trans = new unsigned char[size / 4];
for (int i = 0; i < 30; i++) {
for (int k = 0; k < size; k++) {
*(Y_Trans+k)= (*(Y_Buffer_Old + k) * (30 - i) + *(Y_Buffer + k) * i) / 30;
}
for (int k = 0; k < size / 4; k++) {
*(U_Trans + k) = (*(U_Buffer_Old + k) * (30 - i) + *(U_Buffer + k) * i) / 30;
*(V_Trans + k) = (*(V_Buffer_Old + k) * (30 - i) + *(V_Buffer + k) * i) / 30;
}
YUV_out.write((char*)Y_Trans, size);
YUV_out.write((char*)U_Trans, size / 4);
YUV_out.write((char*)V_Trans, size / 4);
}
for (int i = 0; i < 45; i++) {
YUV_out.write((char*)Y_Buffer, size);
YUV_out.write((char*)U_Buffer, size / 4);
YUV_out.write((char*)V_Buffer, size / 4);
}
}
Y_Buffer_Old = new unsigned char[size];
U_Buffer_Old = new unsigned char[size / 4];
V_Buffer_Old = new unsigned char[size / 4];
RGB2YUV(width, height, ImageData, Y_Buffer_Old, U_Buffer_Old, V_Buffer_Old, 0);
delete[] ImageData;
delete[] Y_Buffer;
delete[] U_Buffer;
delete[] V_Buffer;
originBMP.close();
}
YUV_out.close();
return 0;
}
7 不同像素深度的BMP文件转换的伪代码实现
7.1 1/4/8位转换为24位
这一部分,需要读取调色板,然后将每个调色板的数值映射到一个RGB数值,对后面每个值都进行这样的映射
读取 BITMAP_Palette ;
开辟一个结构体{
byte B;
byte G;
byte R;
};
计算BITMAP里的值的数量,将BGR值0-255线性划分为这么多段;
取每一段值的中点值作为下面的RGB值;
开辟一个映射结构体数组,将Palette中数据映射为一个RGB值;
读取data,映射为RGB值;
调用RGB2YUV;
7.2 24位转换1/4/8位
与上伪代码类似,只不过是反向映射。
7.3 16位和24位互转
需要先把16位的值通过下列的位运算提取出,然后进行与上类似的映射。
8 参考与交流
本次转场算法与黄湘杰
同学交流后受到启发,并进行了兴高采烈的讨论。