实验2 | 多张BMP转换为YUV动画,加上炫酷的转场动画

本文详细介绍了BMP文件的组成,包括存储格式和位深度规范,并展示了如何解析BMP文件并转换为YUV格式。通过逐帧处理,实现了多张BMP图片间的炫酷转场动画,包括淡入淡出效果。同时,讨论了不同像素深度的BMP文件转换的伪代码实现。最后,提供了完整的C++代码示例。
摘要由CSDN通过智能技术生成

在本实验报告中,将会呈现对BMP文件组成的理解,然后根据其组成呈现编写的解析BMP并转换为YUV的过程,最后展示多张BMP组合成的动态YUV的原理、实现和效果。

1 BMP文件的组成

1.1 存储格式

BMP文件分为四个部分顺序线性存储:
在这里插入图片描述

  1. 位图头文件包含 BMP 图像文件的类型、显示内容等信息;
  2. 位图信息头包含有 BMP 图像的宽、高、压缩方法,以及定义颜色等信息。
  3. 调色板 ,这个部分可选,24位真彩色和16位是无调色板的。
  4. 实际的位图数据,一般是按照从左到右、从下到上的顺序。

所有存储的顺序都按照Intel格式存储。Intel格式可见上次作业介绍

在程序中,通过Windows.h头文件我们可以直接访问BITMAP各类头的结构体:

File HeaderInfo 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,参数设定如下:
在这里插入图片描述
为了实现转场,重新对帧数进行划分:

4530453045
第一张图片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(ni)×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 参考与交流

本次转场算法与黄湘杰同学交流后受到启发,并进行了兴高采烈的讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值