1.实验基本原理
BMP文件格式
BMP(全称Bitmap)是Windows操作系统中的标准图像文件格式,可以分成两类:设备相关位图(DDB)和设备无关位图(DIB),使用非常广。它采用位映射存储格式,除了图像深度可选以外,在绝大多数应用中不采用其他任何压缩,因此,BMP文件所占用的空间很大。BMP文件的图像深度可选lbit、4bit、8bit、16bit及24bit。BMP文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。由于BMP文件格式是Windows环境中交换与图有关的数据的一种标准,因此在Windows环境中运行的图形图像软件都支持BMP图像格式。
典型的BMP图像文件由四部分组成:
(1)位图头文件数据结构BITMAPFILEHEADER,它包含BMP图像文件的类型、显示内容等信息;
(2)位图信息数据结构BITMAPINFOHEADER,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息;
(3)调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24位的BMP)就不需要调色板。调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,决定于biClrUsed和biBitCount字段。数组中每个元素的类型是一个RGBQUAD结构。
(4)位图数据紧跟在调色板之后。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值(逻辑色)。对于真彩色图,图像数据就是实际的R、G、B值。图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是4的整倍数,也就是DWORD对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。
结构体BITMAPFILEHEADER,BITMAPINFOHEADER,RGBQUAD定义在windows.h库中,可以直接调用。
#include<windows.h>
BITMAPFILEHEADER
typedef struct tagBITMAPFILEHEADER {
WORD bfType; /* 说明文件的类型 */
DWORD bfSize; /* 说明文件的大小,用字节为单位 */
/*注意此处的字节序问题*/
WORD bfReserved1; /* 保留,设置为0 */
WORD bfReserved2; /* 保留,设置为0 */
DWORD bfOffBits; /* 说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量 */
} BITMAPFILEHEADER;
BITMAPINFOHEADER
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;
RGBQUAD
typedef struct tagRGBQUAD {
BYTE rgbBlue; /*指定蓝色分量*/
BYTE rgbGreen; /*指定绿色分量*/
BYTE rgbRed; /*指定红色分量*/
BYTE rgbReserved; /*保留,指定为0*/
} RGBQUAD;
查看BMP文件二进制流
24bitBMP文件
格式说明:
前14字节为FILEHEADER空间,之后40字节为INFOHEADER空间,24bitBMP为真彩色图像,无调色板,因此INFOHEADER空间之后为位图数据。
第1-2字节:42 4D表示图像为BMP格式
第3-6字节:表示图像大小,为倒序存储,实际大小应为00 00 E0 01
第11-14字节:表示偏移量36H=54,即文件头+信息头一共54字节
第19-22字节:倒序存储图像宽度,(02 80H)=640像素
第23-36字节:倒序存储图像高度,(01 E0H)=480像素
8bitBMP文件
格式说明:
前14字节为FILEHEADER空间,之后40字节为INFOHEADER空间,8bitBMP不是真彩色图像,有调色板。
调色板空间字节数= sizeof(RGBQUAD)*pow(2, (float)info_h.biBitCount))
字节序
不同的计算机系统采用不同的字节序存储数据,同样一个4字节的32位整数,在内存中存储的方式不同。字节序分为小尾字节序(Little Endian)和大尾字节序(Big Endian)。Intel处理器大多数使用小尾字节序,Motorola处理器大多数使用大尾(Big Endian)字节序。
小尾就是低位字节排放在内存的低端,高位字节排放在内存的高端,即所谓的“低位在前,高位在后”。大尾就是高位字节排放在内存的低端,低位字节排放在内存的高端,即所谓的“高位在前,低位在后”。TCP/IP各层协议将字节序定义为大尾,因此TCP/IP协议中使用的字节序通常称之为网络字节序。
在实现BMP文件头信息的写入时,需要注意整数保存时的字节序。
2.实验流程分析
程序初始化(打开两个文件、定义变量和缓冲区等)
读取BMP文件,抽取或生成RGB数据写入缓冲区
调用RGB2YUV的函数实现RGB到YUV数据的转换
写YUV文件
程序收尾工作(关闭文件,释放缓冲区)
3.关键代码及分析
main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <math.h>
#include<windows.h>
#include "rgb2yuv.h"
#include "readrgb.h"
#define u_int8_t unsigned __int8
#define u_int unsigned __int32
#define u_int32_t unsigned __int32
int main(int argc, char** argv)
{
bool flip = FALSE; /* --flip */
unsigned int i;
int width = 0, height = 0;
/* internal variables */
char* bmpFileName = NULL;
char* yuvFileName = NULL;
FILE* bmpFile = NULL;
FILE* yuvFile = NULL;
u_int8_t* rgbBuf = NULL;
u_int8_t* yBuf = NULL;
u_int8_t* uBuf = NULL;
u_int8_t* vBuf = NULL;
u_int32_t videoFramesWritten = 0;
//打开5幅bmp图像并生成1个YUV文件
for (int k = 1; k < 6; k++) //argv[0]=命令名,argv[1]=参数1,argv[2]=参数2,….argv[n]=参数n
{
if ((bmpFile = fopen(argv[k], "rb")) == NULL)
{
printf("bmp file open failed!");
exit(0);
}
if ((yuvFile = fopen(argv[6], "ab+")) == NULL) //ab+:以附加方式打开可读写的二进制文件。若文件不存在,则会建立该文件,
{ //如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。
printf("yuv file failed!");
exit(0);
}
BITMAPFILEHEADER File_header;
BITMAPINFOHEADER Info_header;
if (fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1)
{
printf("read file header error!");
exit(0);
}
if (File_header.bfType != 0x4D42)
{
printf("Not bmp file!");
exit(0);
}
if (fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1)
{
printf("read info header error!");
exit(0);
}
//保证行扫描为4字节的整数倍
int width, height;
if (((Info_header.biWidth * Info_header.biBitCount / 8))%4 == 0)
width = Info_header.biWidth * Info_header.biBitCount / 8;
else
width = (Info_header.biWidth*Info_header.biBitCount + 31) / 32 * 4;
//width,height定义为整型,此式保证计算结果是距离这个数最近的4字节的整数倍
if ((Info_header.biHeight % 2) == 0)
height = Info_header.biHeight;
else
height = Info_header.biHeight + 1;
//保证为偶数列
rgbBuf = (u_int8_t*)malloc(Info_header.biWidth * Info_header.biHeight * 3);//为bmp中的图像数据开辟buffer
yBuf = (u_int8_t*)malloc(Info_header.biWidth * Info_header.biHeight);
uBuf = (u_int8_t*)malloc((Info_header.biWidth * Info_header.biHeight) / 4);
vBuf = (u_int8_t*)malloc((Info_header.biWidth * Info_header.biHeight) / 4);
memset(rgbBuf, 0, Info_header.biWidth * Info_header.biHeight * 3); //memset:对结构体或者数组快速清零的函数
if (rgbBuf == NULL || yBuf == NULL || uBuf == NULL || vBuf == NULL)
{
printf("no enought memory\n");
exit(1);
}
ReadRGB(bmpFile, File_header, Info_header, rgbBuf); //读取bmp文件中的图像数据
if (RGB2YUV(Info_header.biWidth, Info_header.biHeight, rgbBuf, yBuf, uBuf, vBuf, flip))
{
printf("rgb2yuv error");
exit(1);
}
//归一化处理
for (i = 0; i < unsigned int(Info_header.biWidth * Info_header.biHeight); i++)
{
if (yBuf[i] < 16) yBuf[i] = 16;
if (yBuf[i] > 235) yBuf[i] = 235;
}
for (i = 0; i < unsigned int(Info_header.biWidth * Info_header.biHeight / 4); i++)
{
if (uBuf[i] < 16) uBuf[i] = 16;
if (uBuf[i] > 240) uBuf[i] = 240;
if (vBuf[i] < 16) vBuf[i] = 16;
if (vBuf[i] > 240) vBuf[i] = 240;
}
//每个文件写40帧
for (int j = 0; j < 40; j++)
{
fwrite(yBuf, 1, Info_header.biWidth * Info_header.biHeight, yuvFile);
fwrite(uBuf, 1, (Info_header.biWidth * Info_header.biHeight) / 4, yuvFile);
fwrite(vBuf, 1, (Info_header.biWidth * Info_header.biHeight) / 4, yuvFile);
}
printf("\n %u,%ux%u video frames written\n",Info_header.biBitCount,Info_header.biWidth, Info_header.biHeight);
memset(yBuf, 0, Info_header.biWidth * Info_header.biHeight);
memset(uBuf, 0, Info_header.biWidth * Info_header.biHeight / 4);
memset(vBuf, 0, Info_header.biWidth * Info_header.biHeight / 4);
}
//释放缓冲区,关闭文件
free(yBuf);
free(uBuf);
free(vBuf);
free(rgbBuf);
fclose(bmpFile);
fclose(yuvFile);
getchar();
return(0);
}
readRGB.cpp
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <math.h>
#include<windows.h>
#include "readrgb.h"
//检测信息头结束位置是否与图像数据起始位置重合,若中间有(2的biBitCount次方)*(结构体RGBQUAD大小)的空间,
//则说明存在调色板,即图像不是真彩色。
bool MakePalette(FILE * pFile, BITMAPFILEHEADER &file_h, BITMAPINFOHEADER & info_h, RGBQUAD *pRGB_out)
{
if ((file_h.bfOffBits - sizeof(BITMAPFILEHEADER)-info_h.biSize) == sizeof(RGBQUAD)*pow(2, (float)info_h.biBitCount))
{
fseek(pFile, sizeof(BITMAPFILEHEADER)+info_h.biSize, 0); //移动文件指针指向调色板
fread(pRGB_out, sizeof(RGBQUAD), (unsigned int)pow(2, (float)info_h.biBitCount), pFile);//从pFile指向的文件中取 sizeof(RGBQUAD)*pow(2, (float)info_h.biBitCount)
//个字节数存入pRGB_out指向的空间
return true;
}
else
return false;
}
void ReadRGB(FILE* bFile , BITMAPFILEHEADER & File_header, BITMAPINFOHEADER & Info_header, unsigned char* rgbdata)
{
unsigned long Loop, width, height;
unsigned char mask = 0, *tempData;
if (((Info_header.biWidth * Info_header.biBitCount / 8)) % 4 == 0)
width = Info_header.biWidth * Info_header.biBitCount / 8;
else
width = (Info_header.biWidth*Info_header.biBitCount + 31) / 32 * 4;
if ((Info_header.biHeight % 2) == 0)
height = Info_header.biHeight;
else
height = Info_header.biHeight + 1;
tempData = (unsigned char *)malloc(width * height);
fseek(bFile, File_header.bfOffBits, 0);
fread(tempData, 1, width * height, bFile); //将数据读入临时buffer,倒序存储,注意在rgb2yuv时也要倒序处理,即flip=0
if (Info_header.biBitCount == 24)//若图像深度为24(无调色板),则数据直接写入rgb缓冲区
{
memcpy(rgbdata, tempData, width * height);
free(tempData);
return;
}
RGBQUAD *pRGB = (RGBQUAD *)malloc(sizeof(RGBQUAD)*(unsigned int)pow(2, (float)Info_header.biBitCount));
if (!MakePalette(bFile, File_header, Info_header, pRGB))
printf("No palette!");
if (Info_header.biBitCount == 16)
{
for (Loop = 0; Loop < width * height; Loop += 2) //unsigned long Loop:一字节长度
//图像深度为16,位与移位取像素数据转换为8bit / 彩色分量,写入rgb缓冲区
{
*rgbdata = (tempData[Loop] & 0x1F) << 3;
*(rgbdata + 1) = ((tempData[Loop] & 0xE0) >> 2) + ((tempData[Loop + 1] & 0x03) << 6);
*(rgbdata + 2) = (tempData[Loop + 1] & 0x7C) << 1;
rgbdata += 3;
}
}
for (Loop = 0; Loop < width * height; Loop++)//图像深度为8及8以下,构造调色板,位与移位取像素数据,查调色板,写入rgb缓冲区
{
switch (Info_header.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) //循环mask次:1bit每字节循环8次,2bit4次,4bit2次
{
unsigned char index = mask == 0xFF ? tempData[Loop] : ((tempData[Loop] & mask) >> (8 - shiftCnt * Info_header.biBitCount));
*rgbdata = pRGB[index].rgbBlue;
*(rgbdata + 1) = pRGB[index].rgbGreen;
*(rgbdata + 2) = pRGB[index].rgbRed;
if (Info_header.biBitCount == 8)
mask = 0;
else
mask >>= Info_header.biBitCount;
rgbdata += 3;
shiftCnt++;
}
}
free(tempData);
free(pRGB);
}
rgb2yuv代码在此不再赘述
4.实验结果及分析
图像大小均为640*480,顺序为24bit,16bit,8bit,4bit,1bit处理后的结果
(实验图片来自新浪微博@伊吹鸡腿子)