数字图像处理(二)——BMP图像的统计

  上一篇文章我们介绍了《数字图像处理(一)——BMP图像的介绍和读取》,介绍了BMP图像文件格式和用C语言读取单波段的BMP图像。这一篇文章将继续用C语言编程提取BMP图像像素的最小值最大值,计算像素的均值标准差熵值,并计算和绘制图像的灰度直方图

1. 说几个概念

  我们首先了解一下这篇文章中进行计算的的几个概念。

1.1. 图像像素的最小值、最大值、均值和标准差

  顾名思义,对于单波段的图像来说,图像像素的最小值就是图像中所有像素的最小值;图像像素的最大值就是图像中所有像素的最大值;图像的均值1就是整个图像中所有像素的平均值;图像像素的标准差2就是图像中所有像素的标准差。

1.2. 图像信息量H(熵)

  熵的概念在多门学科上都有定义,这里我们所说的熵是指信息论上的熵3。下面我们介绍一下图像的信息量(熵):
  假设一幅数字图像的灰度范围为[ 0, L-1 ],各灰度像素出现的概率为P0,P1,P2,···,PL-1。根据信息论可知,各灰度像素具有的信息量分别为:-log2P0,-log2P1,-log2P2,···,-log2PL-1。则该幅图像的平均信息量(熵)为
H = − ∑ i = 0 L − 1 P i l o g 2 P i H = -\sum_{i=0}^{L-1} P_{i} log_{2} P_{i} H=i=0L1Pilog2Pi
  熵 H 反映了图像信息丰富程度,在图像编码和图像质量评价中有重要意义。

1.3. 图像灰度直方图

  灰度直方图反映的是一幅图像中各灰度级像素出现的频率之间的关系。以灰度级为横坐标,纵坐标为灰度级的频率,绘制频率同灰度级的关系图就是灰度直方图。它是图像的重要特征之一,反映了图像灰度分布的情况。频率的计算式为
v i = n i n v_{i} = {n_{i} \over n} vi=nni
  式中: n i n_{i} ni 是图像中灰度为 i i i 的像素数, n n n为图像的总像素数。
  灰度直方图有如下的性质:
  (1)灰度直方图只能反映图像的灰度分布状况,而不能反映图像像素的位置,即丢失了像素的位置信息;
  (2)一幅图像对应唯一的灰度直方图,反之不成立;
  (3)一幅图像分成多个区域,多个区域的直方图之和即为原图像的直方图。
  直方图有如下的应用:
  (1)用于判断图像量化是否恰当;
  (2)用于确定图像二值化的阙值;
  (3)计算图像中物体的面积;
  (4)计算图像信息量H(熵)。

2. 分析一下怎么实现
2.1. 提取图像像素的最小值、最大值

  提取图像像素的最小值、最大值就要遍历一下所有像素,然后挑出像素中的最小值和最大值。
  为了提取结果在以后使用方便,我们接着在上一节定义的结构体中接着定义像素的最小值、最大值、均值、标准差、熵值、直方图的数组。

typedef struct {
	// 定义文件头信息
	BITMAPFILEHEADER header;
	// 定义位图信息头
	BITMAPINFOHEADER info;
	// 定义彩色表
	RGBQUAD rgb[256];
	// 定义位图数据指针
	unsigned char *data;
	// 定义图像像素的最小值
	int min;
	// 定义图像像素的最大值
	int max;
	// 定义图像像素的均值
	float mean;
	// 定义图像像素的标准差
	double std;
	// 定义直方图的熵值
	double entropy;
	// 定义图像像素直方图的数组
	float histArray[256] = { 0.0 };
} BMP;

  补充完上面的结构体,我们接下来进行遍历一下像素,提取像素的最小值和最大值。

// 定义图像像素的最大值,赋一个值便于比较
int max = -9999;
// 定义图像像素的最小值,赋一个值便于比较
int min = 9999;
// 循环读取像素值
for (int i = 0; i < bmp.info.biHeight; i++)
{
	for (int j = 0; j < bmp.info.biWidth; j++)
	{
		// 获取循环到像元的像素值
		unsigned char pixel = bmp.data[j + i*bmp.info.biWidth];
		// 开始比较像素值
		if (pixel > max)
			max = pixel;
		if (pixel < min)
			min = pixel;
	}
}
// 将图像像素的最大值赋值到结构体
bmp.max = max;
// 将图像像素的最小值赋值到结构体
bmp.min = min;
2.2. 计算图像像素的均值、标准差

  计算图像像素的均值、标准差和提取图像像素的最小值、最大值一样,都需要需要对所有的像素进行遍历。

// 定义图像像素的均值
float mean = 0.0;
// 定义图像像素的标准差
double std = 0.0;
// 循环读取像素值
for (int i = 0; i < bmp.info.biHeight; i++)
{
	for (int j = 0; j < bmp.info.biWidth; j++)
	{
		// 获取循环到像元的像素值
		unsigned char pixel = bmp.data[j + i*bmp.info.biWidth];
		// 计算所有图像像素的和
		mean += pixel;
		// 计算所有图像像素的平方和
		std += pixel*pixel;
	}
}
// 计算图像像素的均值
mean /= bmp.info.biHeight*bmp.info.biWidth;
// 计算图像像素的标准差
std = sqrt(std / (bmp.info.biHeight*bmp.info.biWidth) - mean*mean);

  注意: C语言中使用 s q r t ( ) sqrt() sqrt() 函数时候要引用 math.h 头文件

2.3. 计算和绘制图像灰度直方图和熵值

  计算图像灰度直方图也像上面提取最大值、最小值,计算均值、标准差一样对所有的像元进行遍历。而计算图像的熵要用到图像的灰度直方图。

// 定义图像的熵
double entropy = 0.0;
// 定义图像灰度直方图的数组
float histArray[256] = { 0.0 };
// 循环读取像素值
for (int i = 0; i < bmp.info.biHeight; i++)
{
	for (int j = 0; j < bmp.info.biWidth; j++)
	{
		// 获取循环到像元的像素值
		unsigned char pixel = bmp.data[j + i*bmp.info.biWidth];
		// 统计灰度值为pixel像元的个数
		histArray[pixel]++;
	}
}
for (int i = 0; i < 256; i++)
{
	// 计算每个像素值的频率
	histArray[i] /= (bmp.info.biHeight*bmp.info.biWidth);
	// 利用灰度直方图计算图像的熵
	if (histArray[i] != 0.0)
	{
		entropy -= histArray[i] * (log(histArray[i]) / log(2));
	}
	// 将每个像素值的频率赋值到结构体
	bmp.histArray[i] = histArray[i];
}
// 将图像的熵赋值到结构体
bmp.entropy = entropy;
3. 编码实现
3.1. 新建项目

  上面我们已经分析了该如何实现提取图像像素的最小值和最大值,计算像素的均值、标准差和熵值,并计算和绘制图像的灰度直方图。虽然这篇文章的代码是接着上一篇文章写的,但是为了每篇文章的独立性,我们接着在解决方案中新建一个项目。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  注意: 上面最后一张图是在新建的项目中新建一个C语言文件,并将新建的项目设置成启动项目。如果不设置成启动项目,编译运行时候还会运行上一篇文章中的项目。

3.2.编码实现

  在编译器中写上下面的代码(下面的代码是灰度图的读取,关于后面多波段的读取,我们以后在再介绍)。

#include <stdio.h> // C语言代码中必须要引用的头文件
#include <windows.h> // 图像读取的头文件
#include <math.h> // 常用数学函数的头文件

// 使代码在新版本的VS中正常运行不报错
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)

// 定义一个结构体来读取BMP的信息
typedef struct {
	// 定义文件头信息
	BITMAPFILEHEADER header;
	// 定义位图信息头
	BITMAPINFOHEADER info;
	// 定义彩色表
	RGBQUAD rgb[256];
	// 定义位图数据指针
	unsigned char *data;
	// 定义图像像素的最小值
	int min;
	// 定义图像像素的最大值
	int max;
	// 定义图像像素的均值
	float mean;
	// 定义图像像素的标准差
	double std;
	// 定义直方图的熵值
	double entropy;
	// 定义图像像素直方图的数组
	float histArray[256] = { 0.0 };
} BMP;

// 读图像函数
void bmpRead(const char *bmpPath, BMP &bmp) {
	// 以二进制的方式打开图片
	FILE *file = fopen(bmpPath, "rb");
	// 读取文件信息头
	fread(&bmp.header, sizeof(BITMAPFILEHEADER), 1, file);
	// 读取位图信息头
	fread(&bmp.info, sizeof(BITMAPINFOHEADER), 1, file);
	// 读取彩色表
	fread(&bmp.rgb, sizeof(RGBQUAD), 256, file);
	// 定义位图数据内存大小
	bmp.data = new unsigned char[bmp.info.biWidth * bmp.info.biHeight];
	// 读取元素的位图数据
	fread(bmp.data, sizeof(unsigned char), bmp.info.biWidth*bmp.info.biHeight, file);
	// 关闭图片
	fclose(file);
}

// 图像统计函数
void bmpStatistics(BMP &bmp) {
	// 定义图像像素的最大值,赋一个值便于比较
	int max = -9999;
	// 定义图像像素的最小值,赋一个值便于比较
	int min = 9999;
	// 定义图像像素的均值
	float mean = 0.0;
	// 定义图像像素的标准差
	double std = 0.0;
	// 定义图像的熵
	double entropy = 0.0;
	// 定义图像灰度直方图的数组
	float histArray[256] = { 0.0 };
	// 循环读取像素值
	for (int i = 0; i < bmp.info.biHeight; i++)
	{
		for (int j = 0; j < bmp.info.biWidth; j++)
		{
			// 获取循环到像元的像素值
			unsigned char pixel = bmp.data[j + i*bmp.info.biWidth];
			// 开始比较像素值
			if (pixel > max)
				max = pixel;
			if (pixel < min)
				min = pixel;
			// 计算所有图像像素的和
			mean += pixel;
			// 计算所有图像像素的平方和
			std += pixel*pixel;
			// 统计灰度值为pixel像元的个数
			histArray[pixel]++;
		}
	}
	for (int i = 0; i < 256; i++)
	{
		// 计算每个像素值的频率
		histArray[i] /= (bmp.info.biHeight*bmp.info.biWidth);
		// 利用灰度直方图计算图像的熵
		if (histArray[i] != 0.0)
		{
			entropy -= histArray[i] * (log(histArray[i]) / log(2));
		}
		// 将每个像素值的频率赋值到结构体
		bmp.histArray[i] = histArray[i];
	}
	// 计算图像像素的均值
	mean /= bmp.info.biHeight*bmp.info.biWidth;
	// 计算图像像素的标准差
	std = sqrt(std / (bmp.info.biHeight*bmp.info.biWidth) - mean*mean);
	// 将图像像素的最大值赋值到结构体
	bmp.max = max;
	// 将图像像素的最小值赋值到结构体
	bmp.min = min;
	// 将图像像素的均值赋值到结构体
	bmp.mean = mean;
	// 将图像像素的标准差赋值到结构体
	bmp.std = std;
	// 将图像的熵赋值到结构体
	bmp.entropy = entropy;
}

// 定义一个函数存储统计结果
void saveStatisticsResult(const char *statisticsResultPath, BMP &bmp) {
	// 新建一个txt文件用于存储统计结果
	FILE *file = fopen(statisticsResultPath, "w");
	// 存储图像像素的最小值
	fprintf(file, "图像像素的最小值为:%d\n", bmp.min);
	// 存储图像像素的最大值
	fprintf(file, "图像像素的最大值为:%d\n", bmp.max);
	// 存储图像像素的均值
	fprintf(file, "图像像素的均值为:%f\n", bmp.mean);
	// 存储图像像素的标准差
	fprintf(file, "图像像素的标准差为:%lf\n", bmp.std);
	// 存储图像的熵
	fprintf(file, "图像的熵为:%lf\n", bmp.entropy);
	// 循环存储每个像素的频率
	for (int i = 0; i < 256; i++)
	{
		fprintf(file, "像素值为%d的像元频率为:%lf\n", i, bmp.histArray[i]);
	}
	// 关闭文件
	fclose(file);
}

int main() {
	// 定义图像结构体
	BMP mbmp;
	// 读取图像(如果不是按照本文的新建项目方法,建议使用绝对路径,否则可能会因为文件路径错误而报错。)
	bmpRead("../lena.bmp", mbmp);
	// 调用统计函数
	bmpStatistics(mbmp);
	// 将统计结果存储到一个txt文本中
	saveStatisticsResult("../statisticsResult.txt", mbmp);
	// 删除指针,释放内存
	delete[] mbmp.data;
	// 使程序暂停,便于查看
	scanf("……");

	return 0;
}
4. 验证一下结果
4.1. 结果图

  接下来让我们看一下运行的结果。
在这里插入图片描述

4.2. 验证正确性

  我们再用ERDAS IMAGE验证一下结果是否正确。
在这里插入图片描述
  下面这张图是代码测试过程中输出每个像素值像元出现的个数。
在这里插入图片描述
  通过上面的初步对比,我们可以看出我们编码实现的结果是正确的。

5. 附:生成一个直方图图片

  上面我们是将每个像素值出现的频率存储到一个 txt 文件中,下面我绘制一个直方图的图片。这里使用的是graphics.h头文件,由于计算机中没有这个头文件,所以我们首先要进行安装。点击
链接, 提取码: 758y。直接安装电脑中VS对应的版本即可。

5.1. 代码
// 首行添加绘制直方图需要的头文件
#include <graphics.h>

// 绘制直方图函数
void histDraw(BMP &bmp) {
	float max = -9999.0;
	// 循环找出频率中最大值,以便绘制直方图
	for (int i = 0; i < 256; i++)
	{
		if (max < bmp.histArray[i])
		{
			max = bmp.histArray[i];
		}
	}
	// 新建一个256x256的窗口
	initgraph(256, 256);
	// 设置绘图的颜色
	setcolor(WHITE);
	// 绘制直方图
	for (int i = 0; i < 256; i++)
	{
		//绘制直线,上一篇文章我们讲了像素坐标,所以绘制直方图时候从下到上要进行转换,line函数中使用的是整型,所以我们进行类型转换
		line(i, 256, i, (int)(256 - 256 * (bmp.histArray[i] / max)));
	}
}
5.2. 结果

在这里插入图片描述
  这节的介绍我们到这里就结束了,有什么错误欢迎大家指出。下一节我继续介绍《数字图像处理(三)——BMP图像的拉伸》,介绍一下图像的线性拉伸、分段拉伸、标准差拉伸。


  1. 均值:数学上的均值我们可以到百度百科的均值的介绍中找到↩︎

  2. 标准差:标准差的数学介绍我们可以在百度百科中找到。 ↩︎

  3. 信息学上的熵的介绍我们可以在百度百科中的介绍中找到。 ↩︎

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值