目录
实验要求
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、文件组成:位图文件头、位图信息头、调色板、实际数据
组成部分 | 代表含义 | 字节数 |
位图文件头(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、调用函数实现RGB到YUV数据的转换
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调试~