直方图是图像像素灰度级的统计数目的直观表示。本文将从像素级操作完成直方图显示、直方图均衡、直方图规定(匹配)、直方图局部均衡以及直方图统计测试实验。
对于直方图的计算,我们需要统计各个灰度级上像素的数目,然后根据此关系勾画出直方图。直方图能够直观的显示图像像素在各个灰度级的分布,是其余实验的基础。
#include
#include
#include
#include
#include
using namespace cv;
using namespace std;
void test1();
double* computeHist(Mat src);
void showHist(double* rank, string name);
double findNumber(double* num, int start, int end, int n);
void test2();
Mat myBalanceHist(Mat &src);
double* standHist(double* rank);
double* balanceHist(double* rank);
Mat mapping(Mat &src, double* rank);
void test3();
void standardImageSingleArray(Mat &src, double* num, int rank);
void gama_image(Mat &img, double gama, double rate);
Mat myMatchHist(Mat &src, double* dstHist);
void test4();
Mat myLocalBalance(Mat &src, int size);
void test5();
Mat statisticHist(Mat &src, double rate, double muk0, double sigmak1, double sigmak2, bool dark, int size);
Mat countMV(double* num, int nums);
int main()
{
test1();
test2();
test3();
test4();
test5();
return 0;
}
// 测试一
void test1()
{
// 读取灰度图像
Mat img = imread("D:/lena.jpg", 0);
// 显示直方图
double* rank = computeHist(img);
showHist(rank, "原图直方图");
imshow("原图", img);
waitKey(0);
destroyAllWindows();
}
// 计算直方图
double* computeHist(Mat src)
{
double* rank = new double[256];
// 初始化
for (int n = 0; n < 256; n++)
rank[n] = 0;
// 计算灰度分布
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
rank[src.at<uchar>(i, j)]++;
}
return rank;
}
// 显示直方图
void showHist(double* rank, string name)
{
Mat hist = Mat::zeros(255, 255, CV_8UC1);
// 深拷贝
double* rankCopy = new double[256];
for (int n = 0; n < 256; n++)
rankCopy[n] = rank[n];
double min = findNumber(rankCopy, 0, 256, 0);
double max = findNumber(rankCopy, 0, 256, 255);
for (int i = 0; i < 256; i++)
{
line(hist, Point(i, 255), Point(i, saturate_cast<int>(255 - (rank[i] - min) * 255 / (max - min))), Scalar(255));
}
imshow(name, hist);
}
// 寻找序列值
double findNumber(double* num, int start, int end, int n)
{
int p = start;
double stand = num[start];
for (int i = start + 1; i < end; i++)
{
if (num[i] < stand)
{
num[p] = num[i];
p++;
num[i] = num[p];
}
}
if (p == n)
return stand;
else if (p > n)
return findNumber(num, start, p, n);
else
return findNumber(num, p + 1, end, n);
}
直方图能够让观察者对图的灰度分布有一个直观的认识,方便后续图像增强工作的处理。
直方图均衡是根据变换函数 s = ( L − 1 ) ∫ 0 r w ( r ) d w s = (L-1)\int_{0}^{r}w(r)\text{d}w s=(L−1)∫0rw(r)dw 来进行映射的。原理大概可解释为在任意间隔内的灰度密度等于灰度间隔除以总灰度级,那么这个灰度分布就是均衡的。
// 测试二
void test2()
{
// 读取灰度图像
Mat img = imread("D:/lena.jpg", 0);
double* rank = computeHist(img);
Mat dst = myBalanceHist(img);
// 显示均衡后的直方图
double* rankBalance = computeHist(dst);
showHist(rank, "原图像直方图");
showHist(rankBalance, "均衡后的直方图");
imshow("原图", img);
imshow("直方图均衡", dst);
waitKey(0);
destroyAllWindows();
}
// 直方图均衡化
Mat myBalanceHist(Mat &src)
{
double* rank;
rank = computeHist(src);
// 归一化直方图
rank = standHist(rank);
// 直方图均衡
balanceHist(rank);
// 图像映射
Mat dst = mapping(src, rank);
return dst;
}
// 归一化直方图
double* standHist(double* rank)
{
// 深拷贝
double* rankCopy = new double[256];
for (int n = 0; n < 256; n++)
rankCopy[n] = rank[n];
double sum = 0;
for (int n = 0; n < 256; n++)
sum += rankCopy[n];
for (int n = 0; n < 256; n++)
rankCopy[n] /= sum;
return rankCopy;
}
// 直方图均衡
double* balanceHist(double* rank)
{
// 深拷贝
double* rankCopy = new double[256];
for (int n = 0; n < 256; n++)
rankCopy[n] = rank[n];
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < i; j++)
rank[i] += rankCopy[j];
rank[i] *= 255;
}
return rank;
}
// 映射
Mat mapping(Mat &src, double* rank)
{
Mat dst = src.clone();
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
dst.at<uchar>(i, j) = saturate_cast<int>(rank[src.at<uchar>(i, j)]);
}
return dst;
}
对比原图与均衡后的图像,可发现变换图像的层次效果更加明显,直方图的分布基本占据整个灰> > 度级且各灰度级密度基本相当,而存在空缺的灰度级是因为图像灰度级是离散的,变换中存在多> > 个灰度级映射到同一个灰度级。
直方图均衡是自适应的,不需要对不同图像进行特定的调整,因此使用比较方便,应用场景较宽泛,多用于图像的预处理,增强图像的对比度。
直方图规定,或者称直方图匹配,算是直方图均衡的变种。直方图均衡化后图像的直方图是均匀分布的,而直方图规定化后图像的直方图分布是可控制的,因此需要提供一个各个灰度级的概率密度数据来进行匹配。
该算法的实现原理:
先对原图像进行直方均衡化,可得到一种映射关系 f ( x ) f(x) f(x);再对输入的灰度密度数据进行直方图均衡化,又可以得到一种映射关系 g ( x ) g(x) g(x)。之后求 g ( x ) g(x) g(x)的反函数 g − 1 ( x ) g^{-1}(x) g−1(x),可得到映射关系 f ( g − 1 ( x ) ) f(g^{-1}(x)) f(g−1(x)),即原图像到目标概率密度的映射。但实际上灰度级是离散的,多个灰度级映射到一个灰度级上, g ( x ) g(x) g(x)可能是递增的但不是严格单调递增的,反函数可能不存在,这种情况需要进行一些特殊的处理, g − 1 ( x ) g^{-1}(x) g−1(x)如果输出对应多个则映射到最小值,如果输出不存在,则对应到差值最小的存在的灰度级上。
// 测试三
void test3()
{
// 读取灰度图像
Mat img = imread("D:/lena.jpg", 0);
Mat dst = img.clone();
gama_image(dst, 2.5, 1);
// 绘制直方图
double* rankImg = computeHist(img);
double* rankDst = computeHist(dst);
// 直方图规定
Mat matchDst = myMatchHist(img, rankDst);
// 绘制规定的直方图
double* rankMatch = computeHist(matchDst);
showHist(rankImg, "原图直方图");
showHist(rankDst, "伽马直方图");
showHist(rankMatch, "规定直方图");
imshow("原图", img);
imshow("伽马变换", dst);
imshow("直方图规定", matchDst);
waitKey(0);
destroyAllWindows();
}
// 单通道图像标定——数组存储
void standardImageSingleArray(Mat &src, double* num, int rank = 255)
{
// 查找数组内的最大值与最小值
double min = 1000000000, max = -1000000000;
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
if (num[i*src.cols + j] < min)
min = num[i*src.cols + j];
if (num[i*src.cols + j] > max)
max = num[i*src.cols + j];
// cout << num[i*src.cols + j] <
}
// 对图像像素值进行标定
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
src.at<uchar>(i, j) = saturate_cast<int>(rank * ((num[i*src.cols + j] - min) / (max - min)));
}
}
void gama_image(Mat &img, double gama, double rate = 1)
{
double* num = new double[img.rows*img.cols];
int n = 0;
// 初始化图像迭代器
MatIterator_<uchar> srcIterStart = img.begin<uchar>();
MatIterator_<uchar> srcIterEnd = img.end<uchar>();
while (srcIterStart != srcIterEnd)
{
if (*srcIterStart == 0)
num[n] = 1.0;
else
num[n] = rate * pow(*srcIterStart, gama);
n++;
srcIterStart++;
}
standardImageSingleArray(img, num);
}
// 直方图规定化
Mat myMatchHist(Mat &src, double* dstHist)
{
// 均衡映射数组
double* srcRank = computeHist(src);
srcRank = standHist(srcRank);
srcRank = balanceHist(srcRank);
// 均衡规定映射数组
double* dstRank = standHist(dstHist);
dstRank = balanceHist(dstRank);
double* rank = new double[256];
for (int i = 0; i < 256; i++)
for (int j = 0; j < 256; j++)
{
if (srcRank[i] >= dstRank[j])
if (abs(srcRank[i] - dstRank[j]) <= abs(srcRank[i] - dstRank[j + 1]) || j == 255)
rank[i] = j;
}
Mat dst = src.clone();
dst = mapping(dst, rank);
return dst;
}
本测试, 使用伽马变换后的图像灰度分布当做规定模板,对比伽马变换图与规定直方图可知,两者区别不大,本次实验成功。直方图规定化可应用在指定灰度分布的作业上。
全局直方图均衡是在整个图像中做一次均衡化, 而局部直方图均衡是在给定的模板大小的像素邻域内进行均衡化,有多少像素就得进行多少次均衡化,计算量较大。
// 测试四
void test4()
{
// 读取灰度图像
Mat img = imread("D:/lena.jpg", 0);
Mat globalDst = myBalanceHist(img);
Mat localDst = myLocalBalance(img, 3);
imshow("原图", img);
imshow("直方图均衡", globalDst);
imshow("局部直方图均衡", localDst);
waitKey(0);
destroyAllWindows();
}
// 局部直方图均衡 ——待优化
Mat myLocalBalance(Mat &src, int size = 3)
{
Mat dst = src.clone();
for (int i = size / 2; i < (src.rows - size / 2); i++)
for (int j = size / 2; j < (src.cols - size / 2); j++)
{
// 初始化
double* num = new double[256];
for (int p = 0; p < 256; p++)
num[p] = 0;
for (int m = -size / 2; m <= size / 2; m++)
for (int n = -size / 2; n <= size / 2; n++)
{
num[dst.at<uchar>(i + m, j + n)]++;
}
num = standHist(num);
num = balanceHist(num);
dst.at<uchar>(i, j) = saturate_cast<int>(num[dst.at<uchar>(i, j)]);
delete[]num;
}
return dst;
}
局部直方图均衡可应用于显示局部对比度不强,而全局对比度又足够的图像,来获取局部的细节。
直方图统计也是根据模板大小内的像素邻域的均值、方差与全局的均值、方差的比较,来决定像素的操作。操作暗亮区域的图像只需要设置局部均值和全局均值的比值,而局部方差一般设置成小于全局的方差。
// 测试五
void test5()
{
// 读取灰度图像
Mat img = imread("D:/lena.jpg", 0);
Mat darkDst = statisticHist(img, 10, 0.5, 0.02, 1, true, 3);
Mat lightDst = statisticHist(img, 0.1, 0.5, 0.02, 1, false, 3);
Mat globalDst = myBalanceHist(img);
imshow("原图", img);
imshow("直方图统计暗区域", darkDst);
imshow("直方图统计亮区域", lightDst);
imshow("直方图均衡", globalDst);
waitKey(0);
destroyAllWindows();
}
// 直方图统计
Mat statisticHist(Mat &src, double rate, double muk0, double sigmak1, double sigmak2, bool dark = true, int size = 3)
{
Mat dst = src.clone();
double mean, variance, lmean, lvariance;
double* num = new double[src.rows*src.cols];
double* lnum = new double[size*size];
int point;
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
num[i*src.rows + j] = src.at<uchar>(i, j);
Mat xy = countMV(num, src.rows*src.cols);
mean = xy.at<double>(0, 0);
variance = xy.at<double>(1, 0);
delete []num;
for (int i = size / 2; i < (src.rows - size / 2); i++)
for (int j = size / 2; j < (src.cols - size / 2); j++)
{
point = 0;
for (int m = -size / 2; m <= size / 2; m++)
for (int n = -size / 2; n <= size / 2; n++)
{
lnum[point] = dst.at<uchar>(i + m, j + n);
point++;
}
xy = countMV(lnum, size*size);
lmean = xy.at<double>(0, 0);
lvariance = xy.at<double>(1, 0);
if (dark)
{
if (lmean < muk0*mean && lvariance > sigmak1*variance && lvariance < sigmak2*variance)
dst.at<uchar>(i, j) = saturate_cast<int>(rate * dst.at<uchar>(i, j));
}
else
{
if (lmean > muk0*mean && lvariance > sigmak1*variance && lvariance < sigmak2*variance)
dst.at<uchar>(i, j) = saturate_cast<int>(rate * dst.at<uchar>(i, j));
}
}
return dst;
}
// 计算均值和方差
Mat countMV(double* num, int nums)
{
double sum = 0, mu, sigma;
for (int i = 0; i < nums; i++)
{
sum += num[i];
}
mu = sum / nums;
sum = 0;
for (int i = 0; i < nums; i++)
{
sum += pow(num[i] - mu, 2);
}
sigma = sum / nums;
Mat xy = (Mat_<double>(2, 1) << mu, sigma);
return xy;
}
直方图统计可指定灰度且对比差的区域进行相应的提升。