【数据压缩4】Lab2图像文件的读写和转换
一、实验名称
图像文件的读写和转换(设计性实验)
二、实验目的
1.理解图像文件的基本组成。
2.掌握结构体作为复杂数据对象的用法。进一步熟悉由问题到程序的解决方案,并掌握编程细节:如内存分配、倒序读写、字节序、文件读写过程等。
三、实验环境
安装 Visual Studio 软件或其他的个人计算机
四、实验步骤
基于C++实现BMP序列转YUV文件
- 在图像处理软件中自行生成多个BMP文件,至少含5个不同的场景画面,要求带logo。基本要求位深度为24bit的BMP,进阶要求为支持小于24bit的BMP。
- 编写将第一步所生成的多个BMP文件转化为YUV文件,要求可在命令行中设置每个画面出现的帧数。最后形成的YUV文件应至少包含200帧。重点掌握缓冲区分配、倒序读写、结构体的操作。
- 对整个程序进行调试,并将生成的YUV文件用播放软件观看,验证是否正确。
五、实验内容
1. BMP文件结构分析
(1)典型的 BMP 图像文件由四部分组成
组成 | 说明 |
---|---|
位图头文件数据结构BITMAPFILEHEADER | 包含 BMP 图像文件的类型、显示内容等信息 |
位图信息数据结构BITMAPINFOHEADER | 包含有 BMP 图像的宽、高、压缩方法,以及定义颜色等信息 |
调色板Palette | 这个部分是可选的,有些位图需要调色板,有些位图就不需要调色板,比如真彩色图(24位的 BMP) |
实际的位图数据ImageData | 这部分的内容根据 BMP 位图使用的位数不同而不同,在 24 位图中直接使用 RGB,而其他的小于 24 位的使用调色板中颜色索引值 |
- 位图头文件结构体
typedef struct tagBITMAPFILEHEADER {
WORD bfType; //说明文件的类型
DWORD bfSize; // 说明文件的大小,用字节为单位
WORD bfReserved1; //保留,设置为0
WORD bfReserved2; // 保留,设置为0
DWORD bfOffBits; //说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量
} BITMAPFILEHEADER;
- 位图信息数据结构
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; //说明结构体所需字节数
LONG biWidth; //以像素为单位说明图像的宽度
LONG biHeight; //以像素为单位说明图像的高度
WORD biPlanes; //说明位面数,必须为1
WORD biBitCount; //说明位数像素,1、2、4、8、24
DWORD biCompression; //说明图像是否压缩及压缩类型BI_RGB、BI_RLE8、BI_RLE4、BI_BITFIELDS
DWORD biSizeImage; //以字节为单位说明图像大小,必须是4的整数倍
LONG biXPelsPerMeter; //目标设备的水平分辨率,像素/米
LONG biYPelsPerMeter; //目标设备的垂直分辨率,像素/米
DWORD biClrUsed; //说明图像实际用到的颜色数,如果为0则颜色数为2的 biBitCount次方
DWORD biClrImportant; //说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要
} BITMAPINFOHEADER;
- 调色板
typedef struct tagRGBQUAD {
BYTE rgbBlue; //指定蓝色分量
BYTE rgbGreen; //指定绿色分量
BYTE rgbRed; //指定红色分量
BYTE rgbReserved;//指定保留,指定为0
} RGBQUAD;
(2) 解决的问题
如何组织复杂的数据?用结构体组织
BITMAPFILEHEADER File_header;
RGBQUAD* pRGB;
如何用函数处理结构中的数据?结构成员的值个别地传给函数处理
frameWidth = Info_header.biWidth;
2. 程序设计
下面展示一次大循环对每张图片的处理过程,其余图片同理。
(1)程序初始化
定义变量和缓冲区
//定义变量
BITMAPFILEHEADER File_header;//位图头
BITMAPINFOHEADER Info_header;//位图信息
FILE* bmpFile = NULL;//输入bmp文件
FILE* yuvFile = NULL;//输出yuv文件
int frameWidth;//宽
int frameHeight;//高
const int imageNum = 5;//图片数量
//输入输出文件名
const char* bmpFileName[imageNum] = { "img001.bmp","img002.bmp","img003.bmp","img004.bmp","img005.bmp" };
const char* yuvFileName = "video.yuv";
//定义缓冲区
unsigned char* rgbBuf;
unsigned char* yBuf;
unsigned char* uBuf;
unsigned char* vBuf;
bool flip = TRUE;
指针数组作为main函数的形参
void main(int argc, char *argv[])
argv[0]=BMP2YUV.exe;
argc=n+1;
argv[1]=1.bmp;
argv[2]=2.bmp;
argv[n]=video.yuv;
//打开输出文件
yuvFile = fopen(argv[6], "wb");
//打开第1个输入文件
bmpFile = fopen(argv[1], "rb");
(2)读取BMP中的RGB数据
读取BMP文件,抽取或生成RGB数据写入缓冲区
(a)读bmp文件的位图头文件和位图信息,并进行异常判断
//1. 判断是否可读出位图头
if (fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1){
cout << "read file header error!" << endl;
exit(0);
}
//2. 判断位图头是否是BMP文件
if (File_header.bfType != 0x4D42){
cout << "Not bmp file!" << endl;
exit(0);
}
else{
cout << "this is a bmp file!" << endl;
}
//3. 判断位图信息是否可以读出
if (fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1){
cout << "read info header error!" << endl;
exit(0);
}
(b)读取像素的实际点阵数,分配缓存空间
注意字节序:
在实现 BMP 文件头信息的写入和读出时,需要注意整数保存时的字节序。在编程前先用二进制打开方式观察 BMP 文件各个部分的数据存储格式。如图所示,文件宽高就是小端方式存储,D0 02 00 00
实际数值是 00 00 02 D0
,十进制720。
//读取宽高像素数
frameWidth = Info_header.biWidth;
frameHeight = Info_header.biHeight;
//分配空间
rgbBuf = (unsigned char*)malloc(frameWidth * frameHeight * 3);
yBuf = (unsigned char*)malloc(frameWidth * frameHeight);
uBuf = (unsigned char*)malloc(frameWidth * frameHeight / 4);
vBuf = (unsigned char*)malloc(frameWidth * frameHeight / 4);
(c)读RGB数据,倒序转正序写入缓存区
注意位数:根据每像素位数的不同,执行不同的操作,如下代码实现了24位和小于8位bmp文件的操作。
位数 | 操作 |
---|---|
24/32bit | 直接取像素数据写RGB缓冲区 |
16bit | 位与移位取像素数据转换为8bit/彩色分量 |
8bit以下 | 构造调色板,位与移位取像素数据 |
例如下图,18 00
小端存储实际是00 18
即24bit位图,无调色板直接读取选中区域
注意倒序:BMP文件的存储顺序是从左到右、从下到上的顺序
//读取BMP文件中的RGB数据
ReadRGB(bmpFile, File_header, Info_header, rgbBuf);
int ReadRGB(FILE* pFile, BITMAPFILEHEADER& file_h, BITMAPINFOHEADER& info_h, unsigned char* rgb)
{
int width = info_h.biWidth * biBitCount / 8;
int height = info_h.biHeight;
unsigned char* bmp = (unsigned char*)malloc(sizeof(unsigned char) * width * height);
unsigned char* yx_rgb = (unsigned char*)malloc(sizeof(unsigned char) * width * height);
unsigned char mask;
unsigned long Loop;
fread(bmp, width * height, 1, pFile);
//倒序转正序
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
yx_rgb[i * width + j] = bmp[(height - i - 1) * width + j];
}
}
if (biBitCount == 24)
{
memcpy(rgb, yx_rgb, height * width);
return 0;
}
//小于等于8位
else if (biBitCount <= 8) {
//调色板
RGBQUAD* pRGB = (RGBQUAD*)malloc(sizeof(RGBQUAD) * (unsigned int)pow((float)2, info_h.biBitCount));
MakePalette(pFile, info_h, file_h, pRGB);
for (Loop = 0; Loop < width * height; Loop++)
{
//根据位深设置掩膜
switch (biBitCount){
case 1:
mask = 0x80;
break;
case 2:
mask = 0xC0;
break;
case 4:
mask = 0xF0;
break;
case 8:
mask = 0xFF;
}
int shiftCnt = 1;
while (mask)
{
unsigned char index = mask == 0xFF ? yx_rgb[Loop] : ((yx_rgb[Loop] & mask) >> (8 - shiftCnt * biBitCount));
//查找调色板,取出对应BGR存入rgb
*rgb = pRGB[index].rgbBlue;//B
*(rgb + 1) = pRGB[index].rgbGreen;//G
*(rgb + 2) = pRGB[index].rgbRed;//R
mask = 0;//8位一次循环就取完一个字节的颜色数据
rgb += 3;
shiftCnt++;
}
}
}
return 0;
}
(3)RGB到YUV数据的转换
格式转换原理
Y
=
0.2990
R
+
0.5870
G
+
0.1140
B
U
=
128
−
0.1684
R
−
0.3316
G
+
0.5
B
V
=
128
+
0.5
R
−
0.4187
G
−
0.0813
B
Y=0.2990R+0.5870G+0.1140B\\ U=128−0.1684R−0.3316G+0.5B\\ V=128+0.5R−0.4187G−0.0813B
Y=0.2990R+0.5870G+0.1140BU=128−0.1684R−0.3316G+0.5BV=128+0.5R−0.4187G−0.0813B
调用RGB2YUV的函数实现RGB到YUV数据的转换
//RGB转YUV 4:2:0
RGB2YUV(frameWidth, frameHeight, rgbBuf, yBuf, uBuf, vBuf)
采用部分查找表法,提高运行效率
void initLookupTable(){
for (int i=0;i<256;i++){
RGBYUV02990[i] = (float)0.2990 * i;
RGBYUV05870[i] = (float)0.5870 * i;
RGBYUV01140[i] = (float)0.1140 * i;
RGBYUV01684[i] = (float)0.1684 * i;
RGBYUV03316[i] = (float)0.3316 * i;
RGBYUV04187[i] = (float)0.4187 * i;
RGBYUV00813[i] = (float)0.0813 * i;
}
}
RGB2YUV函数
void RGB2YUV(unsigned long w, unsigned long h, unsigned char * rgbBuf, unsigned char * y, unsigned char * u, unsigned char * v)
{
InitLookupTable();//初始化查找表
unsigned char* ytemp = NULL;
unsigned char* utemp = NULL;
unsigned char* vtemp = NULL;
utemp = (unsigned char*)malloc(w * h);
vtemp = (unsigned char*)malloc(w * h);
unsigned long i, r, g, b, nSize;
//对每个像素进行 rgb -> yuv的转换
for (i = 0, nSize = 0; nSize < w * h * 3; nSize += 3)
{
b = rgbBuf[nSize];
g = rgbBuf[nSize + 1];
r = rgbBuf[nSize + 2];
y[i] = (unsigned char)(RGBYUV02990[r] + RGBYUV05870[g] + RGBYUV01140[b]);
utemp[i] = (unsigned char)(-RGBYUV01684[r] - RGBYUV03316[g] + b / 2 + 128);
vtemp[i] = (unsigned char)(r / 2 - RGBYUV04187[g] - RGBYUV00813[b] + 128);
i++;
}
//对u信号及v信号进行采样,因为是4:2:0格式,所以u的数据是y的数据的1/4,v的数据是y的数据的1/4
int k = 0;
for (i = 0; i < h; i += 2)
for (unsigned long j = 0; j < w; j += 2)
{
u[k] = (utemp[i * w + j] + utemp[(i + 1) * w + j] + utemp[i * w + j + 1] + utemp[(i + 1)* w + j + 1]) / 4;
v[k] = (vtemp[i * w + j] + vtemp[(i + 1) * w + j] + vtemp[i * w + j + 1] + vtemp[(i + 1)* w + j + 1]) / 4;
k++;
}
//对y、u、v 信号进行限电平处理
for (i = 0; i < w * h; i++)
{
if (y[i] < 16)
y[i] = 16;
if (y[i] > 235)
y[i] = 235;
}
for (i = 0; i < h * w / 4; i++)
{
if (u[i] < 16)
u[i] = 16;
if (v[i] < 16)
v[i] = 16;
if (u[i] > 240)
u[i] = 240;
if (v[i] > 240)
v[i] = 240;
}
if (utemp)
free(utemp);
if (vtemp)
free(vtemp);
}
(4)循环写YUV文件
//写入yuv文件,每张图片重复20帧,4:2:0
for (int i = 0; i < 20; i++){
fwrite(yBuf, 1, frameWidth * frameHeight, yuvFile);
fwrite(uBuf, 1, (frameWidth * frameHeight) / 4, yuvFile);
fwrite(vBuf, 1, (frameWidth * frameHeight) / 4, yuvFile);
}
(5)关闭文件,释放缓冲区
//释放缓存空间
free(rgbBuf);
free(yBuf);
free(uBuf);
free(vBuf);
//关闭文件
fclose(bmpFile);
fclose(yuvFile);
六、实验结果
输入bmp文件
来自流媒体实验的test.mp4,使用ffmpeg截取bmp格式的文件,并加水印
输出yuv文件
24bit的10张bmp合成的yuv视频:
24bitBMP2YUV
8bit的10张bmp合成的yuv视频:
8bitBMP2YUV