【实验四 使用数组】


前言

本实验主要是通过对数组的操作来操作bmp文件。

一、实验目的

掌握BMP文件的读写方法;
掌握数组的使用方法;
数组的遍历和数组元素的操作;
动态分配一维和二维数组;

二、实验内容

后面的实验需要从图片中读入数据,进行处理。我们使用格式比较简单的BMP图像文件。我们先仔细学习一下BMP文件格式的定义和容易出错的地方,下面是关于BMP文件的说明。

1.BMP文件的读写

熟悉BMP文件的文件格式,练习文件的读写操作。

BMP文件格式是Windows操作系统推荐和支持的标准图像文件格式,是一种将内存或显示器的图像数据不经过压缩而直接按位存盘的文件格式,故称位图(bitmap),其扩展名为BMP。BMP图像通常有4个部分组成:文件头、信息头、颜色表、数据。
在这里插入图片描述

在这里插入图片描述

第一部分为位图文件头BITMAPFILEHEADER。位图文件头结构长度固定为14个字节,包含文件的类型、大小、位图文件保留字、位图数据距文件头的偏移量。其中WORD为无符号16位整数(unsigned short,2字节),DWORD为无符号32位整数(unsigned long,4字节)。这两个都是为了代码的简洁做的typedef定义。具体结构体定义如下:
//位图文件头
typedef struct tagBITMAPFILEHEADER {
WORD bfType; //位图文件的类型,必须为0x4d42 即BM两个字符的ASCII码,注意这里的little endian存储方式,’B’(42)在低位,‘M’(4d)在高位,因此第一个字节是’B’,第二个字节是’M’。
DWORD bfSize; //位图文件的大小,以字节为单位 包括该14字节
WORD bfReserved1; //位图文件保留字,暂时不用,一般为0
WORD bfReserved2; //位图文件保留字,暂时不用,一般为0
DWORD bfOffBits; //位图数据距文件头的偏移量,以字节为单位,即前三部分和
} BITMAPFILEHEADER;

第二部分为位图信息头BITMAPINFOHEADER,该结构固定为40个字节,用于说明位图的尺寸、宽高、像素、分辨率、颜色表等信息。具体结构定义如下:
//位图信息头
typedef struct tagBITMAPINFOHEADER {
DWORD biSize; //本结构所占用字节数 40字节
LONG biWidth; //位图的宽度,以像素为单位
LONG biHeight; //位图的高度,以像素为单位
WORD biPlanes; //目标设备的级别,必须为1
WORD biBitCount; //每个像素所需的位数,必须是1(双色)、
//4(16色)、8(256色)或24(真彩色)之一
DWORD biCompression; //位图压缩类型,必须是 0(BI_RGB不压缩)、
//1(BI_RLE8压缩类型)
//2(BI_RLE压缩类型)之一
DWORD biSizeImage; //位图的大小,以字节为单位
LONG biXPelsPerMeter; //位图水平分辨率,每米像素数
LONG biYPelsPerMeter; //位图垂直分辨率,每米像素数
DWORD biClrUsed; //位图实际使用的颜色表中的颜色数
DWORD biClrImportant; //位图显示过程中重要的颜色数
} BITMAPINFOHEADER;

第三部分为颜色表或调色板(Palette)。有些位图需要调色板,有些位图如真彩色图(biBitCount=24)不需要调色板,它们的BITMAPINFOHEADER后面直接是位图数据。调色板实际是一个数组,共有biClrUsed个元素(如果该值为零,则有2的biBitCount次幂个元素)。数组中每个元素的类型是一个RGBQUAD结构,占4字节。定义如下:
//位图颜色表
typedef struct tagRGBQUAD
{
BYTE rgbBlue; //蓝色的亮度(值范围为0~255)
BYTE rgbGreen; //绿色的亮度(值范围为0~255)
BYTE rgbRed; //红色的亮度(值范围为0~255)
BYTE rgbReserved; //保留,必须为0
} RGBQUAD;

第四部分就是实际的图像数据。对于真彩色图(24位位图 biBitCount=24),图像数据就是实际的RGB值;对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值。下面对2色、16色、256色和真彩色位图分别介绍:
(1)2色位图:当biBitCount=1时,用1位就可以表示该像素的颜色(0表示黑,1表示白),所以8个像素占1个字节;
(2)16色位图:当biBitCount=4时,用4为可以表示一个像素的颜色,所以2个像素占1个字节;
(3)256色位图:当biBitCount=8时,用1个字节表示1个像素,1个像素占1个字节;
(4)真彩色图:当biBitCount=24时,此时用3个字节表示1个像素,其中RGB各占1字节,由于没有颜色表,位图信息头后面是位图数据。

注意以下几点:

  1. 由于Windows规定一个扫描所占的字节数必须是4的倍数(即以long为单位),不足的以0填充。因此,图像每一行的数据最后有可能出现补0。同时注意下面公式,计算只含位图数据的大小:
    biSizeImage=(((bi.biWidthbi.biBitCount)+31)/324)*bi.Height
    上述公式可以用来验证BMP文件补零的问题。最外层括号计算的是每行的字节数,包括了补的零。在读取文件时可以通过信息头进行查看验证。
  2. BMP图片格式的数据是从下到上、从左到右读。即文件中最先读到的图像是最下面一行的左边第一个元素,即从左下角开始存储(0,0)点,从左下角到右上角存储数据。另外,如果是真彩色图像,即biBitCount=24,每个像素的3通道像素值的存储顺序,不是RGB,而是BGR。
  3. 使用C++读取BMP图片,可以自定义上述结构体,包含BMP位图的位图文件头结构、位图信息头结构、位图颜色表3个结构。Windows API在其库文件wingdi.h中系统定义了BMP图像的结构BITMAPFILEHEADER、BITMAPINFOHEADER。我们自己写的标准C/C++代码自己定义这些结构体即可。
  4. 如果你自定义这些结构体,你可能会掉进BITMAPFILEHEADER的坑里。用sizeof算一下这个结构体所占的字节数,你会发现结果不是14,而是16,这是由与上面第1条类似的为了效率而默认的4字节对齐原因产生的。请自行搜索“内存对齐Memory Alignment”问题学习。解决办法有3种:1)不使用自定义的结构体,在MFC下可直接使用系统库自带的结构体;2)更改对齐规则;3)不整体读该结构体,而是把该结构体内部的各个元素分别单独读取。网上有很多资料提供解决这个问题的办法:
    https://blog.csdn.net/xiaosuanzaowb/article/details/8067941
    https://blog.csdn.net/soundzyt/article/details/1911701
    https://www.cnblogs.com/jh818012/p/4245044.html

下面这段示例代码展示了读取一幅24位彩色BMP文件的总行数一半的数据,然后保存成另外一个BMP图像文件。这里读取的图像每行像素的字节数正好是4的整数倍,所以没有处理补零的情况,仅供实验时参考。

#include <cstdio>
#include <cstdlib>

// 针对该结构体的字节对齐问题调整对齐策略
#pragma pack(push,1)
struct BMPFILEHEADER
{
    unsigned short bfType;
    unsigned int   bfSize;
    unsigned short bfReserved1;
    unsigned short bfReserved2;
    unsigned int   bfOffBits;
};
#pragma pack(pop)

struct BITMAPINFOHEADER
{
    unsigned long    biSize;            //本结构所占用字节数 40字节
    long             biWidth;           //位图的宽度,以像素为单位
    long             biHeight;          //位图的高度,以像素为单位
    unsigned short   biPlanes;          //目标设备的级别,必须为1
    unsigned short   biBitCount;        //每个像素所需的位数,必须是1(双色)、
                                        //4(16色)、8(256色)或24(真彩色)之一
    unsigned long    biCompression;     //位图压缩类型,必须是 0(BI_RGB不压缩)、
                                        //1(BI_RLE8压缩类型)
                                        //2(BI_RLE压缩类型)之一
    unsigned long    biSizeImage;       //位图的大小,以字节为单位
    long             biXPelsPerMeter;   //位图水平分辨率,每米像素数
    long             biYPelsPerMeter;   //位图垂直分辨率,每米像素数
    unsigned long    biClrUsed;         //位图实际使用的颜色表中的颜色数
    unsigned long    biClrImportant;    //位图显示过程中重要的颜色数 
};


int main()
{    
    FILE* fp = NULL;  // C标准库的文件指针
    fopen_s(&fp, "Fruits.bmp", "rb"); // 二进制读取方式打开文件

    BMPFILEHEADER bmpfileheader;  // 文件头
    BITMAPINFOHEADER bitmapinfoheader;  // 信息头
    // 读取文件头
    fread(&bmpfileheader, sizeof(bmpfileheader), 1, fp);
    // 读取信息头
    fread(&bitmapinfoheader, sizeof(bitmapinfoheader), 1, fp);

    // 计算总的像素个数
    int num_pix = bitmapinfoheader.biHeight * bitmapinfoheader.biWidth;

    // 动态分配一维数组存储像素数据,注意3通道,每个像素3个字节
    unsigned char* data = (unsigned char *)malloc(num_pix / 2 * 3);
    // 读取像素数据,只读了一半的行数
    fread(data, num_pix / 2 * 3, 1, fp);

    // 更新信息头里的行数和数据总字节数
    bitmapinfoheader.biHeight /= 2;
    bitmapinfoheader.biSizeImage /= 2;

    FILE* fp2 = NULL; // 保存文件的文件指针
    fopen_s(&fp2, "Fruits2.bmp", "wb"); // 二进制写入方式打开文件
    // 写入文件头
    fwrite(&bmpfileheader, sizeof(bmpfileheader), 1, fp2);
    // 写入信息头
    fwrite(&bitmapinfoheader, sizeof(bitmapinfoheader), 1, fp2);
    // 写入数据
    fwrite(data, num_pix / 2 * 3, 1, fp2);
    // 关闭写入的文件
    fclose(fp2);

    // 释放动态分配的内存
    free(data);
    // 关闭读取的文件
    fclose(fp);

    return 0;
}

实验提供的图像是24位真彩色图像,即biBitCount=24,每个像素有3个值(RGB三个通道)来表示,因此没有颜色表那部分数据。为简单起见,我们接下来的实验是要把图像读入二维数组后再做其他处理,因此要把这个三维的数据(行、列、通道)转换为二维数据(只有行、列)。因此,实验要求在读取BMP文件的时候,把每个像素的三个像素值转换成一个值,一个简单的做法是取RGB三个通道的平均数。
本次实验读入一幅图像,比如“Fruits.bmp”。将图像数据读入到内存中动态分配的二维数组中。图像的大小是height行,width列像素,每个像素占3个字节,类型是unsigned char的整数(即在区间[0,255]内)。在读取的时候把每个像素的3个通道的值求平均,存入二维数组对应的位置。按照下面的任务处理完后,再写入一幅新的BMP文件。注意,因为在读取的时候计算3通道的平均值,丢失了颜色信息,我们是无法再还原成彩色图像了,因此在把结果保存成BMP图像文件的时候,需要把二维数组的每一个值赋值到新图像的3个通道,结果图像虽然内部有RGB三个通道的数据,但是由于都是同一个值,因此图像没有颜色,称为灰度图。

请编程实现:

  1. 读取给定的BMP图像。通过读取文件信息头里的biSizeImage的值,验证BMP每行像素所占字节数是否补0达到4的整数倍。
  2. 动态分配二维数组a,将图像数据“降维”后读入a中。这里的“降维”指的是把3通道的彩色图像读取到单通道的二维数组中。
  3. 将数组a的元素上下翻转。即第一行变为最后一行。即实现函数FlipImageUpDown。
  4. 将变换后的数组a写入一幅新的BMP图像文件。如果以上步骤正确,新的图像是原图翻转后的图像。
  5. 将数组a的元素左右翻转。即第一列变为最后一列。即实现函数FlipImageLeftRight。
  6. 将变换后的数组a写入一幅新的BMP图像文件。如果以上步骤正确,会显示翻转后的图像。
  7. 将图像缩小为原来尺寸的一半,存入动态分配内存的二维数组b。一个简单的做法是将a中的属于奇数行和奇数列的元素读取写入到b中。操作完成后,将b写回硬盘一个新的BMP文件中。

2.代码提示

void ReadBMP(const char *filename, unsigned char **d, 文件头, 信息头)
{
// 当读入的图像是"Fruits_480x511.jpg"时,这个图像每行字节数不是4的整数倍,文件里每行都有补零
// 你的这个函数应该能够正确处理这种情况
}

void WriteBMP(const char *filename, unsigned char **d, 文件头, 信息头)
{// 同样需要注意你的函数应该能够处理需要补零的情况
}

void FlipImageUpDown(unsigned char **d, int rows, int cols) { }

void FlipImageLeftRight(unsigned char **d, int rows, int cols)  { }

void ResizeImage(/*相关的形参*/) { }

int main(int argc, char* argv[])
{
// 调用你写的读取BMP图像的函数,读取数据到二维数组
// write your code here

// 上下翻转图像
// 调用FlipImageUpDown
// 将反转后的数组的数据写入一幅新的图像文件

// 左右翻转图像
// 调用FlipImageLeftRight
// 将反转后的数组的数据写入一幅新的图像文件

// 将原图缩小为原尺寸的一半,结果存入新的动态二维数组中 

// 将缩小操作后的结果数组的数据写入一幅新的图像文件
    
    
return 0;
}


三、实验要求

完成上述代码,并能显示正确的结果图像。
注意事项

  1. 图像的基本组成单元用“像素”表示,例如如果图像的大小是480*511像素,表示图像的高度(行数)为480像素,宽度(列数)为511像素,;

  2. 本实例中给的图像都是3通道(24位)的彩色图像。读取的时候要将其处理为灰度图像。

  3. 二级指针作为函数参数时,应在函数声明时指明其行、列数,否则函数内部无法得知数组的维度;

  4. 动态分配的数组,使用完后要及时释放,防止内存的泄漏。
    实验效果图(仅供参考)
    1.读入原图像到二维数组后,再保存为另一幅图像。由于丢失了颜色信息,变成了灰度图。
    在这里插入图片描述

2.图像上下翻转
在这里插入图片描述

3.图像左右翻转
在这里插入图片描述

4.图像尺寸缩小为原来的一半
在这里插入图片描述
以上均为老师提供的实验内容,实现如下。

四、实验实现

#define _CRT_SECURE_NO_WARNINGS 
#include <iostream>
#include <Windows.h>
#include<cstdio>
#include<cstdlib>
#include<cmath>

using namespace std;

void tprint()//菜单提示
{
	cout << "菜单功能栏" << endl;
	cout << "请选择相应操作:" << endl;
	cout << "1.图片上下翻转" << endl;
	cout << "2.图片左右翻转" << endl;
	cout << "3.图片尺寸缩小为原来的一半" << endl;
	cout << "4.退出" << endl;
}
unsigned char** ReadBMP(const char* filename)
{
	BITMAPFILEHEADER fh;
	BITMAPINFOHEADER ih;
	unsigned char** d;
	FILE* fp = fopen(filename, "rb");
	if (fp == NULL)
	{
		cout << "文件打开失败!" << endl;
		exit(0);
	}
	long height, width;
	fread(&fh, sizeof(BITMAPFILEHEADER), 1, fp);
	fread(&ih, sizeof(BITMAPINFOHEADER), 1, fp);
	height = ih.biHeight;
	width = ih.biWidth;
	if (height % 4 != 0)
	{
		height = (height * (ih.biBitCount) / 8 + 3) / 4 * 4;
		height = height / 3;
	}
	if (width % 4 != 0)
	{
		width = (width * (ih.biBitCount) / 8 + 3) / 4 * 4;
		width = width / 3;
	}
	d = new unsigned char* [height];
	for (int i = 0; i < height; i++)
	{
		d[i] = new unsigned char[width];
		for (int j = 0; j < width; j++)
		{
			unsigned char r, g, b;
			fread(&r, sizeof(unsigned char), 1, fp);
			fread(&g, sizeof(unsigned char), 1, fp);
			fread(&b, sizeof(unsigned char), 1, fp);
			d[i][j] = r / 3 + g / 3 + b / 3;
		}
	}
	fclose(fp);
	cout << "文件读入完成!" << endl;
	return d;
	//释放内存
	/*for (int i = 0; i < height; i++)
	{
		delete[]d[i];
	}
	delete[]d;*/
}
//写入文件
void WriteBMP(unsigned char** d)
{
	BITMAPFILEHEADER fh;
	BITMAPINFOHEADER ih;
	FILE* fp = fopen("out.bmp", "wb");
	if (fp == NULL)
	{
		cout << "文件输入失败!" << endl;
		exit(0);
	}
	fh.bfOffBits = 54;
	fh.bfReserved1 = 0;
	fh.bfReserved2 = 0;
	fh.bfSize = 737334;
	fh.bfType = 19778;
	ih.biBitCount = 24;
	ih.biCompression = 0;
	ih.biXPelsPerMeter = 0;
	ih.biYPelsPerMeter = 0;
	ih.biClrImportant = 0;
	ih.biSize = 40;
	ih.biPlanes = 1;
	ih.biClrUsed = 0;
	ih.biWidth = 512;
	ih.biHeight =480;
	ih.biSizeImage = 737280;
	//上述信息头信息可通过在ReadBMP设置断点获得
	//写入信息头信息
	fwrite(&fh, sizeof(BITMAPFILEHEADER), 1, fp);
	fwrite(&ih, sizeof(BITMAPINFOHEADER), 1, fp);
	int width, height;
	width = ih.biWidth;
	height = ih.biHeight;
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
			for (int k = 0; k < 3; k++)
			{
				fwrite(&d[i][j], sizeof(unsigned char), 1, fp);
			}
		}
	}
	fclose(fp);
	cout << "文件写入成功!" << endl;
}
//缩小后文件的写入
void WriteBMPP(unsigned char** d)
{
	BITMAPFILEHEADER fh;
	BITMAPINFOHEADER ih;
	FILE* fp = fopen("out.bmp", "wb");
	if (fp == NULL)
	{
		cout << "文件输入失败!" << endl;
		exit(0);
	}
	fh.bfOffBits = 54;
	fh.bfReserved1 = 0;
	fh.bfReserved2 = 0;
	fh.bfSize = 737334;
	fh.bfType = 19778;
	ih.biBitCount = 24;
	ih.biCompression = 0;
	ih.biXPelsPerMeter = 0;
	ih.biYPelsPerMeter = 0;
	ih.biClrImportant = 0;
	ih.biSize = 40;
	ih.biPlanes = 1;
	ih.biClrUsed = 0;
	ih.biWidth = 512 / 2;
	ih.biHeight = 480 / 2;
	ih.biSizeImage = 737280 / 4;
	fwrite(&fh, sizeof(BITMAPFILEHEADER), 1, fp);
	fwrite(&ih, sizeof(BITMAPINFOHEADER), 1, fp);
	int width, height;
	width = ih.biWidth;
	height = ih.biHeight;
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
			for (int k = 0; k < 3; k++)
			{
				fwrite(&d[i][j], sizeof(unsigned char), 1, fp);
			}
		}
	}
	fclose(fp);
	cout << "写入成功!" << endl;
}
//上下翻转
unsigned char** FlipImageUpDown(unsigned char** d, int rows, int cols)
{
	int i, j;
	unsigned char** t=nullptr;
	t = new unsigned char* [rows];
	for (i = 0; i < rows; i++)
	{
		t[i] = new unsigned char[cols];
		for (j = 0; j < cols; j++)
		{
			t[i][j] = d[rows - 1 - i][j];
		}
	}
	return t;
}
//左右翻转
unsigned char** FlipImageLeftRight(unsigned char** d, int rows, int cols)
{
	int i, j;
	unsigned char** t=nullptr;
	t = new unsigned char* [rows];
	for (i = 0; i < rows; i++)
	{
		t[i] = new unsigned char[cols];
		for (j = 0; j < cols; j++)
		{
			t[i][j] = d[i][cols - 1 - j];
		}
	}
	return t;
}
//尺寸缩小一半
unsigned char** ResizeImage(unsigned char** d, int rows, int cols)
{
	int i, j;
	unsigned char** t=nullptr;
	t = new unsigned char* [rows / 2];
	for (i = 0; i < rows/2;i++ )
	{
		t[i] = new unsigned char[cols / 2];
		for (j = 0; j < cols/2; j++)
		{
			t[i][j] = d[i*2][j*2];
		}
	}
	return t;
}
int main()
{
	tprint();
	char filename[30] = "Fruits_480x511.bmp";
	unsigned char** r;
	int width = 512;
	int height=480;
	r = new unsigned char* [height];
	for (int i = 0; i < height; i++)
	{
		r[i] = new unsigned char[width];
	}
	r = ReadBMP(filename);
	int a;
	cin >> a;
	switch (a)
	{
	case 1:
		WriteBMP(FlipImageUpDown(r, height, width));
		break;
	case 2:
		WriteBMP(FlipImageLeftRight(r, height, width));
		break;
	case 3:
		WriteBMPP(ResizeImage(r, height, width));
		break;
	case 4:
		break;
	default:
		cout << "输入错误!";
	}
	return 0;
}

总结

总结来说,对数组的操作方法实验内容里也说的很明白不难,我主要是卡在了WriteBMP这个函数的实现上,一开始只定义了信息头和文件头,没有往里面写具体的值导致out.bmp这个文件一直打不开。大家可以尝试把结构体也写入main.cpp里,就可以避免这个错误了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值