BMP文件转YUV文件_C语言实现

时间:2021-06-15 03:40:15

一、最终实现的效果

BMP图像序列(本次实验共195张BMP图片)如下:

BMP文件转YUV文件_C语言实现BMP文件转YUV文件_C语言实现

上述BMP图片经过程序转换后,生成YUV图像如下:

BMP文件转YUV文件_C语言实现

上述YUV文件经过YUVPlayer播放后,效果如下:

 BMP文件转YUV文件_C语言实现

对于不同位数的图像测试如下:

24位BMP生成的YUV:

BMP文件转YUV文件_C语言实现BMP文件转YUV文件_C语言实现

8位BMP(因为位深是通过“画图”软件对上一幅图像的基础上直接设置的,所以会8位BMP会出现出现明显的失真。ps:“画图”保存好像无法保存成16位,我也没有装ps,请大家自行测试)

BMP文件转YUV文件_C语言实现BMP文件转YUV文件_C语言实现

4位BMP生成的YUV:

BMP文件转YUV文件_C语言实现BMP文件转YUV文件_C语言实现

 

二、BMP2YUV实现思路分析

思路方面理解起来很简单,理清BMP文件数据中的四层结构,分别是位图文件头、位图信息头、调色板、实际的位图数据。从位图信息头中提取图像宽高、实际的位图数据所在数据,然后从BMP中读取实际的位图数据,进而索引调色板数组得到每一数据的实际物理意义,即查找得到各位图数据代表了什么颜色(顺序是BGR),进而采用下列转化公式,将RGB转化为YUV。

 

Y=0.30R+0.59G+0.11B U=0.493(BY) V=0.877(RY)

 

理解程序的关键是理清BMP文件的自包含结构,学会通过位运算提取相应数据,进行文件的读写操作。

但实际上,程序中有很多细节需要注意,本文最后会给出总结。


三、位图图像BMP介绍

位图图像(全称:bitmap), 亦称为点阵图像,区别于矢量图放大不失真的特征,位图图像是由单个像素点组成,放大后会失真。它是 Windows操作系统中的标准系统中的标准图像,是Windows环境中交换与图有关的数据的一种标准。BMP文件属于自包含文件,包含表格中四个部分。BMP 文件的图像深度可选 lbit、4bit、8bit、16bit及24bit 。

典型的BMP图像由四部分组成:

位图文件头BITMAPFILEHEADER

位图信息头BITMAPINFOHEADER

调色板Palette

实际的位图数据ImageData

(1)位图头文件数据结构,它包含 BMP图像文件的类型、显示内容等信息;

(2)位图信息数据结构,它包含有BMP图像的宽、高压缩方法,以及定义颜色等信息;

(3)调色板,这个部分是可选的,有些位图需要调色板,有些位图需要调色板,比如真彩(24位的BMP)就不需要调色板;

(4)位图数据,这部分的内容根据位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜索引值。

四、数据结构

(1)位图文件头

typedef  struct  tagBITMAPFILEHEADER {
WORD bfType; /* 说明文件的类型 */
DWORD bfSize; /* 说明文件的大小,用字节为单位*/
/*注意此处的字节序问题*/
WORD bfReserved1; /*保留,设置为0 */
WORD bfReserved2; /*保留,设置为0 */
DWORD bfOffBits; /* 说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量 */

} BITMAPFILEHEADER;

(2)位图信息头

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;

(3)调色板

调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,决定于biClrUsed和biBitCount字段。数组中每个元素的类型是一个RGBQUAD结构。真彩色无调色板部分。

typedef  struct  tagRGBQUAD {
BYTE rgbBlue; /*指定蓝色分量*/
BYTE rgbGreen; /*指定绿色分量*/
BYTE rgbRed; /*指定红色分量*/
BYTE rgbReserved; /*保留,指定为0*/
} RGBQUAD;

五、关键代码

1.前序工作

这里以生成多帧YUV播放视频为例,首先设置命令行参数

argv[1]=potter  argv[2]=195,第一个参数代表文件名的共同部分,第二个代表图片张数(也就是视频的帧数),目的是方便读取图一所示的文件

 

至于YUV文件,文件名直接使用argv[1]:potter.yuv

2.主函数分析

主函数的思路就是整个实验的思路:

Ø  读取BMP文件

Ø  抽取BMP文件头和信息头信息,用结构体File_header盛放文件头数据,用结构体Info_header盛放信息头数据

Ø 调用ReadRGB 函数,从bmp文件中读取RGB信息

Ø 最后调用RGB2YUV函数,将rgb数据转为yuv

Ø 写YUV文件

 

int main(int argc,char**argv)
{
BITMAPFILEHEADERFile_header;
BITMAPINFOHEADERInfo_header;
FILE*bmpFile;
FILE*yuvFile;

unsignedchar*rgbBuffer, *yBuffer, *uBuffer, *vBuffer;
charname__[20];
//连接成字符串:potter.yuv,方便文件写入磁盘
strcpy_s(name__, argv[1]);
strcat_s(name__, 20, ".yuv");
fopen_s(&yuvFile,name__ , "wb");
if(!yuvFile)
{
printf("yuvfile open error!\n");
exit(0);
}
for (inti = 1; i <= atoi(argv[2]); ++i){
charstr[20];
charname_[20];
//将“potter”“序号”“.bmp”连接成文件名,再用fopen函数打开文件
strcpy_s(name_, argv[1]);
_itoa_s(i, str, 20, 10);
strcat_s(name_, 20, str);
strcat_s(name_, 20,".bmp");
fopen_s(&bmpFile,name_ , "rb");
if(!bmpFile)
{
printf("bmpfile open error!\n");
exit(0);
}

//读取位图文件头
if(fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1)
{
printf("readfile header error!\n");
exit(0);
}

//判断文件类型
if(File_header.bfType != 0x4D42)
{
printf("Notbmp file!\n");
exit(0);
}
else
{
printf("thisis a bmp file\n");
}

//读取位图信息头
if(fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1)
{
printf("readinfo header error!\n");
exit(0);
}

unsignedlongwidth, height;
if(Info_header.biWidth % 4 == 0)
width = Info_header.biWidth;
else
//为了保证图像大小是4的倍数,每一行的像素数*每像素的位数=一行总位数
//一行总位数必须是32位的整数倍。
//为什么呢?这样只有才能保证一行数据量是4字节的整数倍。
//怎么做呢?(x+31)除以32,再下取整,得到结果以后,再乘以32,就得到比原位数大且是32的倍数的数字,再除以8便得到字节数,肯定是4字节的整数倍。
width =(Info_header.biWidth*Info_header.biBitCount + 31) / 32 * (32 / 8);

//保证列数是偶数
if((Info_header.biHeight % 2) == 0)
height = Info_header.biHeight;
else
height = Info_header.biHeight + 1;

//开辟缓冲区
rgbBuffer = newunsignedchar[height*width* 3];
yBuffer = newunsignedchar[height*width];
uBuffer = newunsignedchar[height*width/ 4];
vBuffer = newunsignedchar[height*width/ 4];

//从bmp文件中读取rgb信息
ReadRGB(bmpFile, File_header,Info_header, rgbBuffer);

//核心步骤,将rgb数据转为yuv
RGB2YUV(width, height, rgbBuffer,yBuffer, uBuffer, vBuffer);

//将转好的yuv写到文件中
if(WriteYUV(yBuffer, uBuffer, vBuffer, width*height, yuvFile))
printf("writeYUV file successful!\n");
else
printf("writeYUV file failed!\n");

//free时一定要小心,指针不要指向不是堆的区域
if(rgbBuffer)
free(rgbBuffer);
if(yBuffer)
free(yBuffer);
if(uBuffer)
free(uBuffer);
if(vBuffer)
free(vBuffer);
fclose(bmpFile);
}
fclose(yuvFile);//注意多帧播放时,yuvFile一定只关一次,并且在最后关,不要重复关闭,否则会出现文件覆盖
return 0;
}

 

2.读取BMP文件,得到RGB数据

Ø  首先要保证BMP图像的长必须是4的倍数,宽必须是2的倍数

Ø  然后通过信息头得到有效位图数据在BMP图像中的位置,进而对有效数据进行读取

Ø  需要注意的是,BMP规定:位图数据自左向右、自下向上依次存放。因此注意。Index_Data是直接从BMP文件中读取的数据倒序存放的buffer,需要变为正序的,即Data

Ø  得到的Data并不一定是RGB数据,因为BMP有位深的类别。24位BMP,位图有效数据就是RGB,而16位、8位、4位、2位、1位的BMP则需要位操作,从DATA数据中“解析”出RGB数据,具体方法见下图与注释


voidReadRGB(FILE *pFile,constBITMAPFILEHEADER &file_h, constBITMAPINFOHEADER &info_h, unsignedchar*rgbDataOut)
{
unsignedlongLoop, iLoop, jLoop, width, height, w, h;
unsignedcharmask, *Index_Data, *Data;

//保证是图像大小是4字节的整数倍,具体理由见下面的注释
if ((info_h.biWidth% 4) == 0)
w = info_h.biWidth;
else
w = (info_h.biWidth*info_h.biBitCount+ 31) / 32 * 4;
if ((info_h.biHeight% 2) == 0)
h = info_h.biHeight;
else
h = info_h.biHeight+ 1;

//若是24位,则bmp中有效数据大小是长*宽*3字节
//若是16位,则bmp中有效数据大小是长*宽*2字节
//若是8位,则bmp中有效数据大小是长*宽字节
//若是4位,则bmp中有效数据大小是长*宽/2字节
//若是2位,则bmp中有效数据大小是长*宽/4字节
//若是1位,则bmp中有效数据大小是长*宽/8字节(这大概是为什么bmp图像的长必须是4的倍数,宽必须是2的倍数的原因吧。。。)
width = w / 8 * info_h.biBitCount;
height = h;

//倒序前数据缓存区
Index_Data = (unsignedchar*)malloc(height*width);//buffer大小应该与bmp中有效数据大小相同
//倒序后数据缓存区,用于存放bmp中的有效数
Data = (unsignedchar*)malloc(height*width);//buffer大小应该与bmp中有效数据大小相同

//文件指针定位到有效数据起始处,读取有效数据
fseek(pFile,file_h.bfOffBits,0);

if(fread(Index_Data, height*width, 1, pFile) != 1)
{
printf("readfile error!");
exit(0);
}

//倒序存放
for (iLoop= 0; iLoop < height; iLoop++)
for (jLoop= 0; jLoop < width; jLoop++)
{
Data[iLoop*width + jLoop] =Index_Data[(height - iLoop - 1)*width

+ jLoop];
}

//24位:直接把倒序后的缓存区数据复制给输出缓存区
if (info_h.biBitCount== 24)
{
memcpy(rgbDataOut,Data, height*width);
free(Index_Data);
free(Data);
return;
}

//非24位:解码生成rgb,需要调色板信息

//生成调色板数组,数组的下标,对应bmp文件中有效数据,通过下标对应查找,便可得到该数据对应的颜色
//debug by LiuDong:(unsignedlong long)pow(),pow前的强制类型转换不能转换成unsignedint、unsignedchar等
//因为pow((float)2, info_h.biBitCount)最大值是2^24,unsigned char最大能表示255,unsigned int最大能表示2^32
RGBQUAD*pRGB = (RGBQUAD *)malloc(sizeof(RGBQUAD)*(unsigned int)pow((float)2,info_h.biBitCount));
/*一个单元代表一种颜色
调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,决定于biClrUsed和biBitCount字段。
数组中每个元素的类型是一个RGBQUAD结构。真彩色无调色板部分。
biBitCount;位数/像素,1、2、4、8、24.假如是16位,R占5位,G占5位,B占5位,空出一位,排列组合一共有2^15种颜色,其他位数同理。*/

//读取位图调色板数据
if(!MakePalette(pFile, file_h,info_h,pRGB))
printf("Nopalette!");

//16位:移位操作,从2字节中取出RGB信息,存到3字节中
if (info_h.biBitCount== 16)
{
for(Loop = 0; Loop < height * width; Loop += 2)
{
*rgbDataOut= (Data[Loop] & 0x1F) << 3;//B:用0001 1111取出低字节的右五位,再放到目标字节的高5位(通过右移3位),得到五位的B
*(rgbDataOut+ 1) = ((Data[Loop] & 0xE0) >> 2) + ((Data[Loop + 1] & 0x03)<< 6);//G:11100000取出低字节的左三位,00000011取出高字节的右两位,合并后,放到再放到目标字节的高5位,得到五位的G
*(rgbDataOut+ 2) = (Data[Loop + 1] & 0x7C) << 1; //R:0111 1100取出高字节的中间五位,再放到目标字节的高5位,得到5位的R

rgbDataOut+= 3;
}//RGB都各自位于字节的高5位
}

//1~8位:移位操作,从有限固定位中取出RGB信息,存到3字节中
//循环次数:有效数据字节数
for (Loop =0; Loop<width*height; Loop++)
{
//根据位深设置掩膜
switch(info_h.biBitCount)
{
case1://1000 0000,1位,黑白双色图
mask = 0x80;
break;
case2://1100 0000,2位,4色图
mask = 0xC0;
break;
case4://1111 0000,4位,16色图
mask = 0xF0;
break;
case8://1000 0000,8位,256色图
mask = 0xFF;
}

intshiftCnt = 1;//控制mask的移位,决定取字节中哪些数据

while(mask)//循环一次就是一个字节的解析过程
{
unsignedcharindex = mask == 0xFF ? Data[Loop] : ((Data[Loop] & mask) >> (8 -shiftCnt * info_h.biBitCount));
//Loop代表第几个带转换的原始有效数据
//pRGB代表调色板
//8位:mask=1111 1111,index=data[Loop],即index为bmp中原始有效数据,直接对应调色板数组下标,得到相应颜色

//查找调色板,取出对应BGR存入目标buffer:rgbDataOut
*rgbDataOut= pRGB[index].rgbBlue;//B
*(rgbDataOut+ 1) = pRGB[index].rgbGreen;//G
*(rgbDataOut+ 2) = pRGB[index].rgbRed;//R

if(info_h.biBitCount== 8)
mask = 0;//如果是8位bmp,一次性取完一个字节的颜色数据,直接跳出循环即可
else
mask >>= info_h.biBitCount;//若是1位bmp,则一字节取8次数据;若是2位bmp,则一字节取4次数据;若是4位bmp,则一字节取2次数据
//debugby LiuDong
if(Loop == width*height - 1)
{
rgbDataOut= rgbDataOut + 3 - width*height*3;
break;
}
rgbDataOut+= 3;
shiftCnt++;
}
}
if(Index_Data)
free(Index_Data);
if (Data)
free(Data);
if (pRGB)
free(pRGB);
}
3.RGB数据转化为YUV数据

Ø 计算公式:Y=0.30R+0.59G+0.11B , U=0.493(B-Y) , V=0.877(R-Y)
Ø 需要注意采用4:2:0格式,u的数据是y的数据的1/4,v的数据是y的数据的1/4,有一个下采样的过程
voidRGB2YUV(unsigned longw,unsignedlongh,unsignedchar* rgbData, unsignedchar*y,unsignedchar*u,unsignedchar*v)
{
initLookupTable();//初始化查找表
unsignedchar*ytemp = NULL;
unsignedchar*utemp = NULL;
unsignedchar*vtemp = NULL;
utemp = (unsignedchar*)malloc(w*h);
vtemp = (unsignedchar*)malloc(w*h);

unsignedlongi, nr, ng, nb, nSize;
//对每个像素进行 rgb -> yuv的转换
for (i = 0,nSize = 0; nSize<w*h* 3; nSize += 3)
{
nb = rgbData[nSize];
ng = rgbData[nSize+ 1];
nr = rgbData[nSize+ 2];
y[i]= (unsigned char)(RGBYUV02990[nr]+ RGBYUV05870[ng] + RGBYUV01140[nb]);
utemp[i] = (unsignedchar)(-RGBYUV01684[nr]- RGBYUV03316[ng] + nb / 2 + 128);
vtemp[i] = (unsignedchar)(nr/ 2 - RGBYUV04187[ng] - RGBYUV00813[nb] + 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 (unsignedlongj = 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);
}

 

其中用到了读取调色板信息的函数:MakePalette()

//参数: bmp文件,该文件的位图文件头,该文件的位图信息头,输出buffer(bmp文件开头的调色板数据)
boolMakePalette(FILE* pFile,constBITMAPFILEHEADER &file_h, constBITMAPINFOHEADER &info_h, RGBQUAD *pRGB_out)
{
//先判断是否存在调色板:有效数据开始处离文件开头的距离-位图文件头大小-位图信息头大小=调色数据大小
if ((file_h.bfOffBits- sizeof(BITMAPFILEHEADER)-info_h.biSize)== sizeof(RGBQUAD)*pow((float)2,info_h.biBitCount))
{
fseek(pFile,sizeof(BITMAPFILEHEADER)+info_h.biSize,0);
//把调色数据放到pRGB_out中,完成make palette工作
if(fread(pRGB_out, sizeof(RGBQUAD), (unsigned int)pow((float)2,info_h.biBitCount),pFile)!= (unsigned int)pow((float)2,info_h.biBitCount))
{
printf("Failto read RGBQUAD!\n");
}
returntrue;
}
else
returnfalse;
}

4.将YUV文件写入磁盘中

boolWriteYUV(unsigned char*Y,unsignedchar*U,unsignedchar*V,unsignedlongsize,FILE *outFile)
{
if(fwrite(Y, 1, size,outFile)!= size)
returnfalse;
if(fwrite(U, 1, size/ 4, outFile) != size/ 4)
returnfalse;
if(fwrite(V, 1, size/ 4, outFile) != size/ 4)
returnfalse;
return true;
}

六、实验总结

在调试程序的过程中,遇到了一些问题,在这里整理一下。

1. 提示“触发了一个断点”,中断时停在free(buffer)处

这种问题通常是因为指针越界,这是本次实验中的一个大坑。

出错点:  

RGBQUAD *pRGB = (RGBQUAD*)malloc(sizeof(RGBQUAD)*(unsigned int)pow((float)2,info_h.biBitCount));

(unsigned long long)pow(),pow前的强制类型转换不能转换成unsigned int、unsigned char等,因为pow((float)2, info_h.biBitCount)最大值是2^24,unsigned char最大能表示255,unsigned int最大能表示2^32。

这样pRGB分配空间过少,调用fread()将调色板数据读入pRGB中时,读入数据溢出到程序员分配的内存以外的区域。

2.进行多帧操作,结果只有最一帧的图像显示

这是我室友遇到的问题,是因为YUV文件重复打开、关闭,导致前一帧的YUV数据不断被后一帧的YUV数据覆盖,所以最后只显示最后一帧的YUV图像。


代码与实验结果下载:http://download.csdn.net/detail/vacu_um/9799565