数据压缩第四次作业——BMPtoYUV

目录

实验要求

实验基本原理

(一)BMP文件格式

(二)RGB和YUV文件格式

RGB和YUV空间

 色度格式

实验流程

实验步骤

格式转换代码(附对应各步骤解释)

预备——初始化命令行

1、头文件—“rgbtoyuv.h”

2、主函数—“main.cpp"

3、转换函数部分——“RGB2YUV.cpp”

实验结果

实验总结


实验要求

1、进一步理解图像文件格式(以BMP为例);

2、掌握相关编程知识(包括开辟文件缓冲区、操作指针);

3、利用Visiual studio生成至少5个不同场景的bmp图片,要求带有班级、学号后四位和本人姓名的标志;

4、将BMP转换为YUV文件(至少包含200帧);

5、播放YUV文件,验证实验结果。

实验基本原理

(一)BMP文件格式

在实验二中,我们分析了TIFF文件格式,这里用同样的方法对BMP文件进行介绍。

1、BMP(Bitmap):位图格式,是一种Windows操作系统中的标准图像文件格式。它能够不做任何变换的存储图像像素域数据。通常可以分成设备相关位图(DDB)和设备无关位图(DIB)两类。由于它采用位映射存储格式,在绝大多数应用中不采用其他任何压缩,因此占用的空间也比其他突文件格式要大。BMP文件的图像深度可选lbit、4bit、8bit、16bit及24bit。

2、文件组成:位图文件头、位图信息头、调色板、实际数据

BMP文件组成
组成部分代表含义字节数
位图文件头(BITMAPFILEHEADER)包含BMP图像文件类型、显示内容等14
位图信息头(BITMAPINFOHEADER)

包含BMP图像的压缩方式、颜色等信息

40

                        调色板

                    (Palette)

仅对于灰度图像或索引图像而言。真彩色图像没有调色板。4

                      实际数据

                  (ImageData)

真彩色图像:位图数据就是实际的RGB值;

灰度图像或索引图象:位图数据就是像素颜色在调色板中的索引值

4*n

3、位图文件头组成:

        注意文件开头bfType,bmp文件始终为0x424D(BM);bfsize包括文件头(包括这14个字节)总大小

//定义bmp文件结构体
typedef struct BITMAPFILEHEADER {
    WORD bfType; //说明文件的类型 BMP必须为“0x424D”
    DWORD bfSize; // 说明文件的大小,包括文件头部分
    WORD bfReserved1; //保留,设置为0 
    WORD bfReserved2; // 保留,设置为0 
    DWORD bfOffBits; //说明从文件头结构开始到实际的图像数据之间的字节偏移量 
} BITMAPFILEHEADER;

3、位图信息头组成:

        其中强调biBitCount表明表示颜色时要用到的位数:1表示黑白图、 4表示16 色图、8表示256 色图、24表示真彩色图。(1bit表示1像素,黑白;4bit表示1像素,2^4种图......)

typedef struct BITMAPINFOHEADER {
    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字段。其中,每4字节表示一种颜色,分别为B、G、R、alpha通道。即首先4字节表示颜色号0的颜色,接下来表示颜色号1的颜色,依此类推。16色图和24色图均无调色板。

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

4、位图数据:存放实际的数据,规定每一扫描行的字节数必须是4的整倍数

5、数据存储方式:从左到右,自上而下。

(二)RGB和YUV文件格式

        由于bmp文件可以理解为在RGB像素数据上加上数据头的文件格式(观察第一部分的文件数据部分,全部为RGB数据),我们先对RGB文件格式进行简单介绍:

RGB和YUV空间:

1、RGB图像存储:

-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-B-R-G-

         图像像素分为红、绿、蓝三种色彩模式。通过R、G、B三个通道,相互叠加得到不同种类的颜色,他们取值范围都是0~255。标准的RGB是24bit的,即每个分量8bit。考虑到带宽限制,也有16bit的RGB,R和B用5bit表示,G用6bit表示(联想拜尔滤镜)。实际存储类型为B-R-G形式存储。

2、YUV图像存储:

         亮度分量Y是RGB分量的组合,U和V由B-Y,R-Y色差信号提供。(回顾亮度方程公式)

通常情况下由于信号动态范围太大,我们会对信号做压缩处理,减小色差信号幅度,压缩后的色度信号变化动态范围更小,更适合传输!

Y=0. 299Re+0.587Ge+0.114Be

U=0.493(B−Y)=−0.1684R−0.3316G+0.5000B

V=0.877(R−Y)=0.5000R−0.4187G−0.0813B

 色度格式:

亮度信号和两个色差信号可以有不同的取样格式,如:

 4:4:4 —— RGB取样格式,R、G、B信号取样的点数相同

 4:2:0 —— YUV取样格式,U和V在水平和垂直方向的取样点数为亮度信号的二分之一

       


实验流程

1、选取bmp文件(至少选择5张,且不同场景)标好学号后四位、姓名缩写、班级

2、程序初始化(定义变量缓冲区)

3、读取BMP文件,抽取或生成RGB数据写入缓冲区(颜色位深不同处理方法不同)

4、调用函数实现RGBYUV数据的转换

5、写YUV文件

6、关闭文件、释放缓冲区


!!BMP文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。(低位在前)

实验步骤

选择五张“特色”bmp (这里5张图片的尺寸大小固定为600*600,颜色位深也固定位24bit)

 以1.bmp为例子:

 对选取的图片进行查看(用二进制流查看)

文件类型:0x424D(BMP文件!);文件大小:0x7AF8(31480字节)

字节偏移量:0x36(实际数据据文件头54字节);文件宽度:0x0258(600像素);

文件高度:0x0258(600像素);颜色位深:0x18,24位图

格式转换代码(附对应各步骤解释)

预备——初始化命令行:

首先我们对命令行参数进行简单介绍:

在C或C++中,我们经常会提到main函数,其中最常见的形式是:int main(int argc, char** argv)。int是函数的返回类型,如果main函数最终不需要返回结果可以将int修改为void。函数中包含一个整型和一个指针数组。当程序经过编译、链接后会生成拓展名为.exe的可执行文件。main函数是系统在启动时自动传递参数的函数。

其中argc表示命令行输入参数的个数(因为可执行程序本身也算一个参数,因此argc至少是1);argv中存储所有命令行参数,argc[0]表示对应生成的可执行文件,argc[1]表示传入的参数。

 本次实验修改的命令行参数如下: 

一共定义11个参数(argv[1]~argv[5]表示5张输入图片,argv[6]表示1张输出图片,argv[7]~argv[11]表示5个循环读写帧数)

1、头文件—“rgbtoyuv.h”

定义头文件:声明查找表和读函数以及RGB与YUV转换函数

#pragma once
void LookupTable();//声明查找表
void RGB2YUV(int width, int height, unsigned char* rgb_buffer, unsigned char* y, unsigned char* u, unsigned char* v);
void READRGB(int width, int height, FILE* bmpFile, unsigned char* rgb, BITMAPINFOHEADER Infoheader);

2、主函数—“main.cpp"

(1)准备工作,声明变量和缓冲区

(可以直接用Windows.h直接调用写好的bmp文件组成)

    //直接用window.h调用结构体
	FILE* bmpf = NULL;//声明bmp文件指针(记得定义为null)
	FILE* yuvf = NULL;//声明YUV文件指针

	BITMAPFILEHEADER Fileheader;
	BITMAPINFOHEADER Infoheader;

	//定义缓冲区
	unsigned char* rgb_buffer;
	unsigned char* y_buffer;
	unsigned char* u_buffer;
	unsigned char* v_buffer;


	//定义输入输出
	int width;
	int height;

	//打开输出文件
	fopen_s(&yuvf, argv[6], "wb");

 (2)for大循环,分别对5张图片进行转换

  ①读取bmp文件,检查文件格式是否正确。

注意之前提到过,bmp格式的图片data部分必须为4的整数倍,在检查时一定要对数据进行预处理。在检查图像数据大小时,可以利用图像的长宽*字节数进行判断,例如一行的数据量=宽度*位深。每一扫描行的字节数必须是 4 的整倍数(32位的整数倍),宽的字节数必须是4的的整数倍,高(按字节)必须是2的整数倍。

        fopen_s(&bmpf,argv[i], "rb");		
        if (bmpf == NULL) {
			cout << i << ".bmp文件打开失败" << endl;
		}
		if (fread(&Fileheader, sizeof(BITMAPFILEHEADER), 1, bmpf) != 1)
		{
			cout << i << ".bmp文件头无法读取" << endl;
		}
		if (Fileheader.bfType != 0x4D42) {
			cout << i << ".bmp文件头格式有误" << endl;
		}
		if (fread(&Infoheader, sizeof(BITMAPINFOHEADER), 1, bmpf) != 1)
		{
			cout << i << ".bmp信息头无法读取" << endl;
		}
		//检查图像的宽高
		if ((Infoheader.biWidth % 4) == 0) {
			width = Infoheader.biWidth;
			cout << i << ".bmp width=" << width << endl;
		}
		else {
			width = (Infoheader.biWidth * Infoheader.biBitCount + 31) / 32 * 4;
		}
		if((Infoheader.biHeight % 2) == 0) {
			height = Infoheader.biHeight;
			cout << i << ".bmp width=" << height << endl;
		}
		else {
			height = Infoheader.biHeight + 1;
		}

 ②初始化缓冲区,用于读写bmp数据

rgb_buffer:存储r、g、b三个通道数据(由于是4:4:4采样格式)

yuv分别存储YUV格式的亮度信号和色差信号数据(4:2:0采样格式)

   //存储rgb、yuv数据
    rgb_buffer = (unsigned char*)malloc(width * height * 3);
	y_buffer = (unsigned char*)malloc(width * height);
	u_buffer = (unsigned char*)malloc(width * height / 4);
	v_buffer = (unsigned char*)malloc(width * height / 4);

③调用函数保存bmp的rgb数据。(详细见函数转换模块)

		//根据偏移量找数据部分
		fseek(bmpf, Fileheader.bfOffBits, 0);
		READRGB(width, height, bmpf, rgb_buffer,Infoheader);

④数据转换(由4:4:4转换为4:2:0)详细见函数转换模块

RGB2YUV(width, height, rgb_buffer, y_buffer, u_buffer, v_buffer);

⑤check数据,是否有计算出界,对超出电平进行修正。

		for (int i = 0; i < width * height; i++)
		{
			if (y_buffer[i] < 16) y_buffer[i] = 16;
			if (y_buffer[i] > 235) y_buffer[i] = 235;
		}
		for (int i = 0; i < width * height / 4; i++)
		{
			if (u_buffer[i] < 16) u_buffer[i] = 16;
			if (u_buffer[i] > 240) u_buffer[i] = 240;
			if (v_buffer[i] < 16) v_buffer[i] = 16;
			if (v_buffer[i] > 240) v_buffer[i] = 240;
		}

 ⑥按照预先设定的帧数写入yuv图片

		int time = atoi(argv[i + 6]);
		for (int i = 0; i < 50;i++) {
			fwrite(y_buffer, 1, width * height, yuvf);
			fwrite(u_buffer, 1, (width * height) / 4, yuvf);
			fwrite(v_buffer, 1, (width * height) / 4, yuvf);
		}

 以下是主函数完整的代码~


#include <iostream>
#include <stdio.h>
#include <Windows.h>
#include "rgbtoyuv.h"

using namespace std;


int main(int argc,char* argv[])
{
	//直接用window.h调用结构体
	FILE* bmpf = NULL;//声明bmp文件指针(记得定义为null)
	FILE* yuvf = NULL;//声明YUV文件指针

	BITMAPFILEHEADER Fileheader;
	BITMAPINFOHEADER Infoheader;

	//定义缓冲区
	unsigned char* rgb_buffer;
	unsigned char* y_buffer;
	unsigned char* u_buffer;
	unsigned char* v_buffer;

	//定义输入输出
	int width;
	int height;

	//打开输出文件
	fopen_s(&yuvf, argv[6], "wb");

	//读取文件
	for (int i = 1; i < 6; i++) {
		fopen_s(&bmpf,argv[i], "rb");
		if (bmpf == NULL) {
			cout << i << ".bmp文件打开失败" << endl;
		}
		if (fread(&Fileheader, sizeof(BITMAPFILEHEADER), 1, bmpf) != 1)
		{
			cout << i << ".bmp文件头无法读取" << endl;
		}
		if (Fileheader.bfType != 0x4D42) {
			cout << i << ".bmp文件头格式有误" << endl;
		}
		if (fread(&Infoheader, sizeof(BITMAPINFOHEADER), 1, bmpf) != 1)
		{
			cout << i << ".bmp信息头无法读取" << endl;
		}
		//检查图像的宽高
		if ((Infoheader.biWidth % 4) == 0) {
			width = Infoheader.biWidth;
			cout << i << ".bmp width=" << width << endl;
		}
		else {
			width = (Infoheader.biWidth * Infoheader.biBitCount + 31) / 32 * 4;
		}
		if((Infoheader.biHeight % 2) == 0) {
			height = Infoheader.biHeight;
			cout << i << ".bmp width=" << height << endl;
		}
		else {
			height = Infoheader.biHeight + 1;
		}

		rgb_buffer = (unsigned char*)malloc(width * height * 3);
		y_buffer = (unsigned char*)malloc(width * height);
		u_buffer = (unsigned char*)malloc(width * height / 4);
		v_buffer = (unsigned char*)malloc(width * height / 4);

		//开辟缓冲区(倒叙存放数据)
		unsigned char* data_buffer,*data;
		data= (unsigned char*)malloc(width * height * 3);

		//根据偏移量找数据部分
		fseek(bmpf, Fileheader.bfOffBits, 0);
		READRGB(width, height, bmpf, rgb_buffer,Infoheader);
		//转换
		RGB2YUV(width, height, rgb_buffer, y_buffer, u_buffer, v_buffer);

		for (int i = 0; i < width * height; i++)
		{
			if (y_buffer[i] < 16) y_buffer[i] = 16;
			if (y_buffer[i] > 235) y_buffer[i] = 235;
		}
		for (int i = 0; i < width * height / 4; i++)
		{
			if (u_buffer[i] < 16) u_buffer[i] = 16;
			if (u_buffer[i] > 240) u_buffer[i] = 240;
			if (v_buffer[i] < 16) v_buffer[i] = 16;
			if (v_buffer[i] > 240) v_buffer[i] = 240;
		}
		int num = atoi(argv[i+6]);
		for (int j = 0; j < num;j++) {
			fwrite(y_buffer, 1, width * height, yuvf);
			fwrite(u_buffer, 1, (width * height) / 4, yuvf);
			fwrite(v_buffer, 1, (width * height) / 4, yuvf);
		}
	}
	fclose(bmpf);
	fclose(yuvf);
	return 0;
}

3、转换函数部分——“RGB2YUV.cpp”

①读取bmp文件,将rgb数据存储到data_buffer中(位深我只判断了24位和16位,没琢磨太懂要怎么掉调色板)

void READRGB(int width, int height, FILE* bmpf, unsigned char* rgb_buffer, BITMAPINFOHEADER Infoheader)
{
	unsigned char* data_buffer,*data;
	data_buffer= (unsigned char*)malloc(width * height * 3);
	data = (unsigned char*)malloc(width * height * 3);

	//读取bmp文件
	fread(data_buffer, width * height * 3, 1, bmpf);

	//倒序转正序写入缓存区
	int k = 0;
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width * 3; j++)
		{
			*(data + (height - 1 - i) * width * 3 + j) = *(data_buffer+k);
			k++;
		}
	}
	//根据颜色位深判断:
	if (Infoheader.biBitCount == 24) {
		memcpy(rgb_buffer, data, height * width*3);
		free(data_buffer);
		free(data);
	}
	if (Infoheader.biBitCount == 16)
	{
		for (long i = 0; i < width * height; i++)
		{
			*rgb_buffer = data[i] & 0x1F << 3;
			*(rgb_buffer + 1) = ((data[i] & 0xE0) >> 2) + ((data[i + 1] & 0x03) << 6);
			*(rgb_buffer + 2) = (data[i + 1] & 0x7C) << 1;
			rgb_buffer += 3;
		}
	}
}

②RGB2YUV转换:

void RGB2YUV(int width, int height, unsigned char* rgb_buffer, unsigned char* y, unsigned char* u, unsigned char* v)
{
	initLookupTable();//初始化查找表
	unsigned char* u_temp = NULL;
	unsigned char* v_temp = NULL;

	int r,g,b;//原图像

	u_temp = (unsigned char*)malloc(width * height);
	v_temp = (unsigned char*)malloc(width * height);


	for (int i = 0, j = 0; i < width * height * 3; i = i + 3)
	{
		b = rgb_buffer[i];
		g = rgb_buffer[i + 1];
		r = rgb_buffer[i + 2];
		y[j] =(RGB2YUV02990[r] + RGB2YUV05870[g] + RGB2YUV01140[b]);
		u_temp[j] =(-RGB2YUV01684[r] - RGB2YUV03316[g] + b/2 + 128);
		v_temp[j] =(r/2 - RGB2YUV04187[g] - RGB2YUV00813[b] + 128);
		j++;
	}
	//防止电平溢出
	//转换为4:2:0格式
	//y保持不变,u变成1/4,v变成1/4
	int k = 0;
	for (int i = 0; i < height; i+=2) {
		for (int j = 0; j < width; j += 2) {
			u[k] = (u_temp[i * width + j] + u_temp[(i + 1) * width + j] + u_temp[i * width + j + 1] + u_temp[(i + 1) * width + j + 1]) / 4;
			v[k] = (v_temp[i * width + j] + v_temp[(i + 1) * width + j] + v_temp[i * width + j + 1] + v_temp[(i + 1) * width + j + 1]) / 4;
			k++;
		}
	}
	free(u_temp);
	free(v_temp);
}

为了方便我们还定义了查找表,这样可直接调用计算好的分量结果:

//查找表定义
void initLookupTable() {
	for (int i = 0; i < 256; i++) {
		RGB2YUV02990[i] = (double)0.2990 * i;
		RGB2YUV05870[i] = (double)0.5870 * i;
		RGB2YUV01140[i] = (double)0.1140 * i;
		RGB2YUV01684[i] = (double)0.1684 * i;
		RGB2YUV03316[i] = (double)0.3316 * i;
		RGB2YUV04187[i] = (double)0.4187 * i;
		RGB2YUV00813[i] = (double)0.0813 * i;
	}
}

实验结果

用YUV viewer作为播放器进行播放:

 

实验总结

1、本次实验因为只讲到24位深的图像转换,略微提及了16位图的转换过程,对于mask等的应用我不是特别理解,因此也没有拓展到8位一下位深的转换。下次补课后看是否能改正。

2、实验中出现过只出现半边图的状况,经过debug,发现是readrgb函数书写有问题,刚开始文件读数据有问题,缓冲区开的大小太小,只开了height*width大小的缓冲区,后来对其进行修正。

3、因为本次实验使用C++,反复调用指针操作,如果操作不当经常会出现栈溢出?的现象,可以通过打断点先判断出有问题的区域在哪一部分,在具体debug调试~


 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值