【数据压缩】第四次作业——BMP2YUV实验

目录

一、 实验项目名称

二、 实验目的

三、实验内容

1. BMP文件的组成结构

2. 字节序

四、 实验思路

五、 实验步骤

1. 在图像处理软件中生成5个不同场景画面的BMP文件

2.  编写将第一步所生成的多个BMP文件转化为YUV文件,最后形成的YUV文件包含200帧

(1) 新建工程,填写命令行参数并指定工作目录

(2) 程序部分

a. 主函数:main.cpp

b. 读取RGB函数:readrgb.cpp

c. RGB转YUV函数:rgb2yuv.cpp

d. 写入YUV输出文件函数:writeyuv.cpp

六、实验结果

一、 实验项目名称

图像文件的读写和转换

二、 实验目的

  1. 理解图像文件的基本组成。
  2. 掌握结构体作为复杂数据对象的用法。进一步熟悉由问题到程序的解决方案,并掌握编程细节:如内存分配、倒序读写、字节序、文件读写过程等

三、实验内容

1. BMP文件的组成结构

        BMP(全称Bitmap)是Windows操作系统中的标准图像文件格式,可以分成两类:设备相关位图(DDB)和设备无关位图(DIB),使用广泛。它采用位映射存储格式,除了图像深度可选以外,在绝大多数应用中不采用其他任何压缩,因此,BMP文件所占用的空间很大。BMP文件的图像深度可选lbit、4bit、8bit、16bit及24bit。BMP文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。由于BMP文件格式是Windows环境中交换与图有关的数据的一种标准,因此在Windows环境中运行的图形图像软件都支持BMP图像格式。

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

位图头文件数据结构包含BMP图像文件的类型、显示内容等信息
位图信息数据结构包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息
调色板可选部分,有些位图需要调色板,有些位图比如真彩色图(24位的BMP)就不需要调色
位图数据数据内容根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜色索引值

相应的数据结构表示如下:

(1) 位图文件头主要有:

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

(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

(4) 位图数据

        紧跟在调色板之后的是图像数据字节阵列。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值(逻辑色)。对于真彩色图,图像数据就是实际的R、G、B值。图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是4的整倍数,也就是DWORD对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。

2. 字节序

        不同的计算机系统采用不同的字节序存储数据,同样一个4字节的32位整数,在内存中存储的方式不同。

        字节序分为小尾字节序(Little Endian)和大尾字节序(Big Endian)。Intel处理器大多数使用小尾字节序,Motorola处理器大多数使用大尾(Big Endian)字节序。小尾就是低位字节排放在内存的低端,高位字节排放在内存的高端,即所谓的“低位在前,高位在后”。大尾就是高位字节排放在内存的低端,低位字节排放在内存的高端,即所谓的“高位在前,低位在后”。

        BMP文件中的数据是小尾字节序,在实现BMP文件头信息的写入和读出时,需要注意整数保存时的字节序。

四、 实验思路

BMP文件需要先转为RGB,进而将RGB转为YUV写入YUV文件

五、 实验步骤

1. 在图像处理软件中生成5个不同场景画面的BMP文件

 如图借助ffmpeg截取了5张图片并保存为BMP文件,右上角加上水印

通过二进制编辑器打开No1,bmp进行查看

424D0x4D42='BM',表示这是BMP文件
36D20F000x000FD236,共1036854个字节
360000000x00000036,54,从文件开头到具体图像数据的字节偏移量,文件头(14)+位图信息头(40)=54字节(没有调色板)
D00200000x000002D0,720,位图宽度为720个像素
E00100000x000001E0,480,位图高度为480个像素
00180x0018,24,表示颜色深度为24(不需要调色板)

接着来看一下8bit位深的BMP图片

360400000x00000436,1078,从文件开头到具体图像数据的字节偏移量,文件头(14)+位图信息头(40)+调色板(256*4)=1078字节
00080x0008,8,表示颜色深度为8
000000000,表示不压缩

2.  编写将第一步所生成的多个BMP文件转化为YUV文件,最后形成的YUV文件包含200帧

(1) 新建工程,填写命令行参数并指定工作目录

int main(int argc,char *argv[])
/*argc=n+1=8; 
argv[0]=命令名 指向该代码生成的exe文件
argv[1]=No1.bmp
argv[2]=No2.bmp  
….
argv[5]=No5.bmp 
argv[6]=每幅图像出现的帧数
argv[7]=test.yuv*/

将实验BMP图片,每幅图片出现的帧数,结果输出YUV文件写进命令参数,方便程序调用

(2) 程序部分

程序部分分为四大块,分别是主函数“main.cpp”、“readrgb.cpp”、“rgb2yuv.cpp”、“writeyuv.cpp”,看名字就可以大致理解每一部分实现的是什么功能了。

a. 主函数:main.cpp

首先定义了文件指针,缓存区等一些变量,接着使用for循环遍历每个图片,打开bmp图片文件,读取bmp文件文件头和信息头,处理宽度和高度,使其满足一行字节数是4的倍数和行数是偶数的条件,得到处理后的宽度和高度以及处理后的一行的字节数和行数,开辟缓存区,读取BMP文件的RGB,RGB转为YUV,YUV写入输出文件,最后释放缓存。

注意本次结果YUV文件的格式是4:2:0,因此缓存区UV的大小是Y的四分之一

int main(int argc, char* argv[])
{
	//定义文件指针,缓存区等一些变量
	BITMAPFILEHEADER File_header; //位图文件头
	BITMAPINFOHEADER Info_header; //位图信息头
	FILE* bmpFile = NULL;         //指向输入bmp文件指针
	FILE* yuvFile = NULL;         //指向输出yuv文件指针

	unsigned char* rgbBuf = NULL; //rgb缓存区
	unsigned char* yBuf = NULL;   //y缓存区
	unsigned char* uBuf = NULL;   //u缓存区
	unsigned char* vBuf = NULL;   //v缓存区

	unsigned long width, height, w, h;  //宽和高的处理变量

	yuvFile = fopen(argv[7], "wb");      //二进制方式打开test.yuv文件,只写

	//for循环遍历每个bmp文件进行处理
	for (int i = 1; i < argc - 2; i++)
	{
		bmpFile = fopen(argv[i], "rb");       //二进制方式打开第一个No1.bmp文件,只读
		

		//判断文件是否存在
		if (bmpFile == NULL)                  //找不到bmp文件
		{
			printf("cannot find bmp file\n");
			exit(1);
		}
		if (yuvFile == NULL)                  //找不到yuv文件
		{
			printf("cannot find yuv file\n");
			exit(1);
		}

		//读取bmp文件文件头和信息头并判断读取是否正确
		if (fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1) //读取的文件头数据项数量不为1
		{
			printf("read file header error!");
			exit(0);
		}
		if (File_header.bfType != 0x4D42) //类型不是bmp
		{
			printf("Not bmp file!");
			exit(0);
		}
		else
		{
			printf("this is a bmp file!");
		}
		if (fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1) //读取的信息头数据项数量不为1
		{
			printf("read info header error!");
			exit(0);
		}

		//处理宽度和高度
		//每行字节数必须是4的整数倍,若不足则要补齐
		if ((Info_header.biWidth % 4) == 0) {
			w = Info_header.biWidth;
		}
		else {
			w = (Info_header.biWidth * Info_header.biBitCount + 31) / 32 * 4;
			//处理后的宽度
		}
		//高度需要为偶数不足则补为偶数
		if ((Info_header.biHeight % 2) == 0) {
			h = Info_header.biHeight;
		}
		else {
			h = Info_header.biHeight + 1;
			//处理后的高度
		}
		//得到处理后的一行的字节数width和行数height
		width = w * Info_header.biBitCount / 8;
		height = h;

		//开辟缓存区
		rgbBuf = new unsigned char[h * w * 3];
		yBuf = new unsigned char[h * w];
		uBuf = new unsigned char[h * w / 4];
		vBuf = new unsigned char[h * w / 4];

		//读取BMP文件的RGB
		ReadRGB(bmpFile, File_header, Info_header, rgbBuf, width, height);
		//RGB转为YUV
		RGB2YUV(w, h, rgbBuf, yBuf, uBuf, vBuf);
		//YUV写入输出文件,每幅图像写20次(传入的命令参数)
		WriteYUV(yBuf, uBuf, vBuf, w * h, yuvFile, atoi(argv[6]));
		
		free(rgbBuf);
		free(yBuf);
		free(uBuf);
		free(vBuf);

		fclose(bmpFile);
	}
	fclose(yuvFile);
	return 0;
}

b. 读取RGB函数:readrgb.cpp

这一部分实现从BMP文件中读取得到RGB。

首先注意扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素,因此为了还原图片,数据部分先进行倒序存储。

接着看位图深度:

如果是24bit深度则位图数据部分就是真实的RGB直接输出即可;

如果位图深度是16bit,这里仅考虑了555格式,即biCompression的值为BI_RGB,它没有调色板,16位中最低的5位表示B,中间的5位表示G,高的5位表示R,最高的1位保留为0,这里采取相与移位的方式来从2字节中得到RGB

如果位图深度小于等于8bit,则要用到调色板,原位图数据部分存放的是调色板的索引值,借助掩膜读取到真实RGB值

同时注意RGB排列顺序为BGRBGRBGR……

bool MakePalette(FILE* bmpFile, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, RGBQUAD* pRGB_out);

void ReadRGB(FILE* bmpFile, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, unsigned char* rgbDataOut, unsigned long width, unsigned long height)
{
    unsigned long Loop, iLoop, jLoop;
	unsigned char mask=0, * Index_Data, * Data; //Index_Data为倒序前数据,Data为倒序后数据
 
    Index_Data = (unsigned char*)malloc(height * width);
    Data = (unsigned char*)malloc(height * width);

    fseek(bmpFile, File_header.bfOffBits, 0); //bmpFile文件指针从0偏移偏移量个字节到数据部分
 
    //读取BMP有效数据并判断读取是否正确
    if(fread(Index_Data, height*width, 1, bmpFile) != 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位,Data数据即为RGB数据,直接输出
    if (Info_header.biBitCount== 24)
    {
        memcpy(rgbDataOut,Data, height*width);
        free(Index_Data);
        free(Data);
        return;
    }

    //颜色深度16位,通过移位从2字节中取出RGB信息存到3字节中
    else if (Info_header.biBitCount== 16)
    {
        if (Info_header.biCompression == BI_RGB)
        {   //未压缩
            for (Loop = 0; Loop < height * width; Loop += 2)
            {
                *rgbDataOut = (Data[Loop] & 0x1F) << 3;
                //取出B,与00011111相与取出低字节的右5位,通过右移3位放到目标字节的高5位
                *(rgbDataOut + 1) = ((Data[Loop] & 0xE0) >> 2) + ((Data[Loop + 1] & 0x03) << 6);
                //取出G:与11100000相与取出低字节的左3位,与00000011相与取出高字节的右2位,合并后放到目标字节的高5位
                *(rgbDataOut + 2) = (Data[Loop + 1] & 0x7C) << 1; 
                //取出R:与01111100相与取出高字节的中间5位,再放到目标字节的高5位

                rgbDataOut += 3;
            }
        }
    }

    //颜色深度1,2,4,8位。通过移位取出RGB信息存到3字节中
    else if (Info_header.biBitCount <= 8) {
        //生成调色板数组,通过下标对应查找得到该数据对应的颜色
        RGBQUAD* pRGB = (RGBQUAD*)malloc(sizeof(RGBQUAD) * (unsigned int)pow((float)2, Info_header.biBitCount));

        //读取位图调色板数据并判断是否有调色板
        if (!MakePalette(bmpFile, File_header, Info_header, pRGB))
            printf("Nopalette!");

        
        for (Loop = 0; Loop < width * height; Loop++)
        {
            //根据位深设置掩膜
            switch (Info_header.biBitCount)
            {
            case 1:                     //1000 0000,1位
                mask = 0x80;
                break;
            case 2:                     //1100 0000,2位
                mask = 0xC0;
                break;
            case 4:                     //1111 0000,4位
                mask = 0xF0;
                break;
            case 8:                     //1111 1111,8位
                mask = 0xFF;
            }

            int shiftCnt = 1;          //控制mask的移位

            while (mask)
            {
                unsigned char index = mask == 0xFF ? Data[Loop] : ((Data[Loop] & mask) >> (8 - shiftCnt * Info_header.biBitCount));
                //index为bmp中原始有效数据,是对应调色板数组下标

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

                if (Info_header.biBitCount == 8)
                    mask = 0;                           //8位一次性可取完
                else
                    mask >>= Info_header.biBitCount;    //1位取8次;2位取4次;4位取2次

                if (Loop == width * height - 1)
                {
                    rgbDataOut = rgbDataOut + 3 - width * height * 3;
                    break;
                }
                rgbDataOut += 3;
                shiftCnt++;
            }
        }
        free(pRGB);
    }
    free(Index_Data);
    free(Data);
}

bool MakePalette(FILE* bmpFile, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, RGBQUAD* pRGB_out) {

    //判断是否存在调色板
    if ((File_header.bfOffBits - sizeof(BITMAPFILEHEADER) - Info_header.biSize) == sizeof(RGBQUAD) * pow((float)2, Info_header.biBitCount))
    {
        fseek(bmpFile, sizeof(BITMAPFILEHEADER) + Info_header.biSize, 0);
        //获取调色板数据放入pRGB_out并判断是否读取正常
        if (fread(pRGB_out, sizeof(RGBQUAD), (unsigned int)pow((float)2, Info_header.biBitCount), bmpFile) != (unsigned int)pow((float)2, Info_header.biBitCount))
        {
            printf("Fail to read RGBQUAD!\n");
        }
        return true;
    }
    else
        return false;

}

c. RGB转YUV函数:rgb2yuv.cpp

首先RGB转YUV,这一步用到公式:

Y=0.2990R+0.5870G+0.1140B

U=−0.1684R−0.3316G+0.5000B

V=0.5000R−0.4187G−0.0813B

这是对两个色差信号进行归一化后的,且UV还需要零电平调整要+128。

接着对UV进行采样得到4:2:0格式

最后对YUV进行限量化电平处理。

float RGBYUV02990[256], RGBYUV05870[256], RGBYUV01140[256];
float RGBYUV01684[256], RGBYUV03316[256];
float RGBYUV04187[256], RGBYUV00813[256];

void initLookupTable();
void RGB2YUV(unsigned long w, unsigned long h, unsigned char* rgbData, 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, 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] = (unsigned char)(-RGBYUV01684[nr] - RGBYUV03316[ng] + nb / 2 + 128);
        vtemp[i] = (unsigned char)(nr / 2 - RGBYUV04187[ng] - RGBYUV00813[nb] + 128);
        i++;
    }
    //对u信号及v信号进行采样
    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);
}

//采用部分查找表法提高运行效率
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;
    }
}

d. 写入YUV输出文件函数:writeyuv.cpp

直接循环写入即可

void WriteYUV(unsigned char* Y, unsigned char* U, unsigned char* V, unsigned long size, FILE* outFile,int number)
{
    //将YUV写入文件outFile
    for (int j = 0; j < number; j++){
        fwrite(Y, 1, size, outFile);
        fwrite(U, 1, size / 4, outFile);
        fwrite(V, 1, size / 4, outFile);
    }
}

六、实验结果

24bitBMP2YUV

对于其他位深图片,可以通过电脑画图软件生成,程序中只需要修改命令行参数即可 

8bitBMP2YUV

4bitBMP2YUV

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值