前言
图片增强主要是处理目标图片,从而达到处理结果的图片比原图片更适合特定的应用。
而在各种各样的图片处理当中,就有一些是专门处理灰度图的。
我们可以利用图片的像素亮度(灰度级别)看成是一个随机变量,其分布的情况就反应了图片的特征。我们可以使用Probability Density Function(PDF)来刻画与描述,表现为灰度直方图
直方图统计
灰度直方图是灰度级函数,他表现为图像当中某种灰度的像素个数,反应了图像中每种灰度的出现频率
S
r
=
P
(
r
)
S_r = P(r)
Sr=P(r)
其中S为第r级灰度的灰度频率
算法
假定图像有L级灰度,大小为P = M x N,各像素的灰度为f(x,y),pBuffer[k]为各像素的灰度的量
step1(初始化) : pBuffer[k] = 0 (k = 0,1,2....,M×N)
step2(统计) : pBuffer[f(x,y)]++
step3(归一化) : pBuffer[k] /= MxN(k = 0,1,2,....,MxN)
第三步归一化操作是可选的。
实现
实现这个直方图统计并不复杂,操作也一样是读取BITMAP文件(注意跳字节),提取出自己有用的部分,然后再进行统计
在这里我先定义了两个数据结构以便于之后的操作
typedef struct ImageData
{
BITMAPFILEHEADER fileHeader;
BITMAPINFOHEADER infoHeader;
RGBQUAD rgbquad[256];
BYTE * img;
int length;
int width, height;
}IMGDATA;
typedef struct GrayHistogram
{
float gray[256] = {0};
int pixelCount = 0;
void normalize();
void draw();
private :
bool isNormalize = false;
}GRAYHISTOGRAM;
然后就是对BITMAP文件的读取
IMGDATA loadImage(const std::string& path)
{
std::ifstream ifstream;
ifstream.open(path, std::ios::binary);
if (!ifstream.is_open())
return {};
BITMAPFILEHEADER fileHeader;
BITMAPINFOHEADER infoHeader;
RGBQUAD rgbquad[256];
ifstream.read(reinterpret_cast<char*>(&fileHeader), sizeof(BITMAPFILEHEADER));
ifstream.read(reinterpret_cast<char *>(&infoHeader), sizeof(BITMAPINFOHEADER));
ifstream.read(reinterpret_cast<char *>(&rgbquad), sizeof(RGBQUAD) * infoHeader.biClrUsed);
BYTE *img = new BYTE[infoHeader.biSizeImage];
ifstream.read(reinterpret_cast<char*>(img), infoHeader.biSizeImage);
IMGDATA imgdate;
imgdate.infoHeader = infoHeader;
imgdate.fileHeader = fileHeader;
for(int i = 0;i < 256;i++)
{
imgdate.rgbquad[i] = rgbquad[i];
}
imgdate.pImg = img;
imgdate.length = infoHeader.biSizeImage;
imgdate.width = infoHeader.biWidth;
imgdate.height = infoHeader.biHeight;
ifstream.close();
return imgdate;
}
解析读取到的信息,并进行分析,最后保存到GrayHistogram当中
GRAYHISTOGRAM getHistogram(const IMGDATA data)
{
GRAYHISTOGRAM grayhistogram;
int point = 0;
for (int i = 0; i < data.height; i++)
{
for(int j = 0;j < data.width;j++)
{
grayhistogram.gray[data.img[point++]]++;
}
while (point % 4 != 0)
point++;
}
grayhistogram.pixelCount = data.width * data.width;
return grayhistogram;
}
最后一步就是归一化以及画出来了,由于归一化是可选的,因此这个也可以不进行归一化,当然,无论有没有归一化都是可以表现图片的特征的
//归一化
void GrayHistogram::normalize()
{
if(isNormalize)
return;
for (float& i : gray)
{
i = i / pixelCount;
}
isNormalize = true;
}
输出直方图
之后,我们就需要输出直方图了。由于直方图是一个在图像处理当中比较重要的环节,因此,我们需要输出一个比较好看的直方图,因此,我选择将直方图输出到图片当中。
这个图片的大小为256 x 256,因此图片的u轴就是相当于横坐标,v轴相当于纵坐标,然后,由白色的像素组成直方图,其余部分用黑色像素显示,从而达到了输出一张好看的直方图的目的。
因此,我们需要利用数据源的图片构建一个直方图信息以及一张新的图片。
构建一张新的图片并不难,只要将信息按照BITMAP的格式输入就可以了,而直方图信息的建立刚刚也说了,因此,我们现在就可以建立一个输出直方图的图片了。
//由于可以使用原图本身的信息,因此可以省略一些填写
void outputHistogram(const IMGDATA data,const std::string& path)
{
IMGDATA newData = data;
GRAYHISTOGRAM histogram = getHistogram(data);
// newData.fileHeader.bfType = 0x4d42;
// newData.fileHeader.bfReserved1 = 0;
// newData.fileHeader.bfReserved2 = 0;
newData.fileHeader.bfSize = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 2 + 256 * 256;
newData.fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 2;
//
// newData.infoHeader.biSize = sizeof(BITMAPINFOHEADER);
// newData.infoHeader.biPlanes = 1;
newData.infoHeader.biBitCount = 8;
newData.infoHeader.biClrUsed = 2;
//newData.infoHeader.biCompression = BI_RGB;
newData.infoHeader.biSizeImage = 256 * 256;
newData.infoHeader.biHeight = 256;
newData.infoHeader.biWidth = 256;
// newData.infoHeader.biClrImportant = data.infoHeader.biClrImportant;
// newData.infoHeader.biXPelsPerMeter = data.infoHeader.biXPelsPerMeter;
// newData.infoHeader.biYPelsPerMeter = data.infoHeader.biYPelsPerMeter;
newData.pImg = new BYTE[256 * 256]{ 0 };
histogram.normalize();
RGBQUAD white;
white.rgbReserved = 0;
white.rgbRed = 255;
white.rgbBlue = 255;
white.rgbGreen = 255;
RGBQUAD black;
black.rgbReserved = 0;
black.rgbRed = 0;
black.rgbBlue = 0;
black.rgbGreen = 0;
newData.rgbquad[0] = black;
newData.rgbquad[1] = white;
for(int i = 0 ;i < 256;i++)
{
int length = histogram.gray[i] * 255 * 25;
if (length > 255)
length = 255;
for(int j = 0;j < length;j++)
{
newData.pImg[j * 256 + i] = 1;
}
}
newData.length = 256 * 256;
newData.width = 256;
newData.height = 256;
output(newData, 2, path);
}
void output(IMGDATA data,int clrUsed,const std::string& path)
{
std::ofstream out;
out.open(path, std::ios::out | std::ios::trunc | std::ios::binary);
if (!out.is_open())
return;
std::cout << "output " << path << "...." << std::endl;
out.write(reinterpret_cast<char *>(&data.fileHeader), sizeof(BITMAPFILEHEADER));
out.write(reinterpret_cast<char *>(&data.infoHeader), sizeof(BITMAPINFOHEADER));
out.write(reinterpret_cast<char *>(&data.rgbquad), clrUsed * sizeof(RGBQUAD));
out.write(reinterpret_cast<char *>(data.pImg), data.length);
out.close();
}
此时,直方图的信息统计就完成了。就可以进行下一步的操作了。
直方图均衡化
直方图均衡化,指的是,对图片的直方图进行均衡化处理,也就是对图片的灰度级数进行均衡,直观的从图片上来说,就是图片的对比度更加的明显。因为,在这种情况下,原图片的灰度级数摊到了其他的级数上去了。
在直方图均衡化当中,均衡化函数满足以下特点
s
=
T
(
r
)
0
≤
r
≤
1
,
0
≤
T
(
r
)
≤
1
s = T(r)\qquad 0\leq r\leq1,0\leq T(r)\leq 1
s=T(r)0≤r≤1,0≤T(r)≤1
且T( r )在区间内为单值且单调递增
累积函数分布方法(DCF)
累积函数分布方法是一种比较经典的直方图均衡化方法,他的公式表现为
s
=
T
(
r
)
=
∫
0
r
p
r
(
w
)
d
w
s = T(r) = \int_0^rp_r(w)dw
s=T(r)=∫0rpr(w)dw
也就是对于每一处的灰度分布概率都进行积分,由于概率总和为1,因此,T( r )自然也是在0与1之间了,而且也必然是单调递增且单值的。
利用这个函数,可以将直方图当中的灰度信息进行均衡,灰度集中的地方自然会更加显眼,而本身没什么刻画的地方也会更加的白,因此对比度自然提上来了。
当然,由于我们的图片的灰度级别是离散的,因此,这个函数应该也是离散的,所以他的离散形式为
S
k
=
T
(
r
k
)
=
∑
j
=
0
k
p
r
(
r
j
)
=
∑
j
=
0
k
n
j
n
S_k = T(r_k)=\sum_{j=0}^kp_r(r_j)=\sum_{j=0}^k\frac{n_j}{n}
Sk=T(rk)=j=0∑kpr(rj)=j=0∑knnj
均衡化之后,每一个点的灰度级别都是自己的灰度级数的概率的积分。
算法实现
算法的实现比较简单,按照之前写出的公式即可
//均衡化
IMGDATA balance(const GRAYHISTOGRAM histogram,const IMGDATA data)
{
const IMGDATA newData = data;
for(int i = 0;i < data.length;i++)
{
newData.pImg[i] = calculate(data.pImg[i], histogram) * 255;
}
return newData;
}
//对灰度级别积分
float calculate(int index, const GRAYHISTOGRAM histogram)
{
float result = 0;
for(int i = 0;i < index + 1;i++)
{
result += histogram.gray[i];
}
if (result > 1)
result = 1;
return result;
}
此时,我们的直方图处理也总算是完成了。