停更了好久好久的博客,国庆第一天,起床除了祝祖国妈妈生日快乐之外居然有点不知道要干嘛好,刚好看书时发现了OpenCV里没有的一个小功能,那就来写写博客吧。。。
还记得在很久之前的《OpenCV学习笔记(9)》中,曾经记录过图像直方图的计算和绘制等内容,但是并没有进行封装,显得比较凌乱,于是便借着写直方图规定化这部分内容的时候顺带进行整理封装。
今天的重要内容还是关于直方图的规定化,在看书的时候发现这一个知识点,本来这应该是一个经常能用上的功能,但是不知道为什么OpenCV中却没有提供相应的API,所以只能靠我们自己进行实现。
在《OpenCV学习笔记(10)》中曾经整理过直方图均衡化的内容,所谓直方图均衡化,就是将原本图像的分布不均匀的灰度直方图进行拉伸,将灰度级分布尽量的变得更加均衡,这就有助于扩大图像的对比度,使得原本过暗或过亮的图像变得更加清晰。
而图像直方图规定化同样是要改变图像直方图的灰度级分布,但它的目的是将图像直方图改变为我们所希望的样子,所以进行直方图规定化需要一个模板直方图来进行匹配操作。
直方图规定化和直方图均衡化的区别在于:
(1)直方图均衡化不需要模板直方图,只将所有灰度级进行拉伸即可;而直方图规定化需要一个模板直方图来进行匹配,以将图像原直方图向着模板直方图的方向变换。
(2)直方图均衡化是相对固定的,就只是将灰度级进行均衡分布;而直方图规定化是相当灵活的,可以根据不同需求使用不同的模板直方图,可以得到不一样的规定化结果,以突出图像的不同灰度级特征。
以上是直方图规定化的作用,那么如何进行图像直方图的规定化呢?其主要步骤如下:
(1)准备一幅待规定化图像和一幅模板图像;
(2)分别将两幅图像转换为灰度图(或单通道图);
(3)分别计算两幅图像的直方图;
(4)根据直方图,分别计算两幅图像的累积概率直方图;
(5)根据累积概率直方图,计算出两幅图像直方图之间累计概率误差矩阵;
(6)根据累积概率误差矩阵,寻找待规定化图像转换为模板图像之间的映射关系;
(7)构建进行映射的LUT;
(8)应用LUT进行直方图规定化。
下面就通过代码进行上述步骤的实现:
读取图像并转换为灰度图
Mat img1 = imread("D:/opencv_c++/opencv_tutorial/data/images/lena.jpg");
Mat img2 = imread("D:/opencv_c++/opencv_tutorial/data/images/cross.png");
cvtColor(img1, img1, COLOR_BGR2GRAY);
cvtColor(img2, img2, COLOR_BGR2GRAY);
imshow("img1", img1);
imshow("img2", img2);
计算待直方图规定化图像和模板图像的直方图。这里使用了一个drawNormHist
函数,是我自己封装的对直方图进行归一化绘制的函数,在最后给出。
Mat hist1, hist2;
const int channels[] = { 0 };
const int histSize[] = { 256 };
float bin[] = { 0,255 };
const float *ranges[] = { bin };
calcHist(&img1, 1, channels, Mat(), hist1, 1, histSize, ranges);
calcHist(&img2, 1, channels, Mat(), hist2, 1, histSize, ranges);
drawNormHist(hist1.clone(), "hist1", Scalar(255,255,255), 0);
drawNormHist(hist2.clone(), "hist2", Scalar(255, 255, 255), 1);
计算累计概率直方图,计算每个灰度级像素数量占总像素数量的比例,并对每个灰度级进行累加
int pixelNum1 = img1.rows * img1.cols;
int pixelNum2 = img2.rows * img2.cols;
float hist1_cdf[256] = { hist1.at<float>(0) };
float hist2_cdf[256] = { hist2.at<float>(0) };
for (int i = 1; i < 256; i++)
{
hist1_cdf[i] = hist1_cdf[i - 1] + hist1.at<float>(i)/pixelNum1;
hist2_cdf[i] = hist2_cdf[i - 1] + hist2.at<float>(i)/pixelNum2;
}
构建累计概率误差矩阵,计算图像1中每一灰度级到图像2中每一灰度级之间的累积概率误差,并构成256x256的累积概率误差矩阵
float diff_cdf[256][256];
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < 256; j++)
{
diff_cdf[i][j] = fabs(hist1_cdf[i] - hist2_cdf[j]);
}
}
计算图像1中每一灰度级对应到图像2中所有灰度级的最小的累积概率误差,该最小误差所对应的行索引就是图像1中的原灰度级,该最小误差所对应的列索引就是图像1进行直方图规定化后原灰度级映射到的新灰度级
Mat lut(1, 256, CV_8U);
for (int i = 0; i < 256; i++)
{
float min = diff_cdf[i][0];
int index = 0;
for (int j = 1; j < 256; j++)
{
if (min > diff_cdf[i][j])
{
min = diff_cdf[i][j];
index = j;
}
}
lut.at<uchar>(i) = (uchar)index;
}
应用LUT进行映射,将图像1映射为直方图规定化后的结果图像, 计算结果图像的直方图并归一化显示
Mat result, hist_result;
LUT(img1, lut, result);
imshow("result", result);
calcHist(&result, 1, channels, Mat(), hist_result, 1, histSize, ranges);
drawNormHist(hist_result.clone(), "hist_result", Scalar(255, 255, 255), 1);
到这里就实现了对图像直方图的规定化,下面给出封装好的直方图绘制函数:
void drawNormHist(Mat hist, string winName, Scalar color, int histImgType=0)
{
if (hist.channels() != 1)
{
cout << "只允许绘制单通道图像直方图" << endl;
return;
}
int hist_w = 512;
int hist_h = 400;
int bin_width = 2;
Mat histImage = Mat::zeros(Size(hist_w, hist_h), CV_8UC3);
//对直方图进行归一化,即将灰度值范围拉伸至画布显示范围内
normalize(hist, hist, 0, hist_h, NORM_MINMAX, -1, Mat());
if (0 == histImgType)
{
//绘制柱状直方图
for (int i = 1; i < hist.rows; i++)
{
int x_b = (i - 1) * bin_width;
int y_b = hist_h - cvRound(hist.at<float>(i - 1, 0));
int width_b = i * bin_width - x_b;
int height_b = cvRound(hist.at<float>(i - 1, 0));
Rect rect(x_b, y_b, width_b, height_b);
rectangle(histImage, rect, color, 1, LINE_AA, 0);
}
}
else if (1 == histImgType)
{
//绘制折线直方图
for (int i = 1; i < hist.rows; i++)
{
line(histImage, Point((i - 1) * bin_width, hist_h - cvRound(hist.at<float>(i - 1, 0))),
Point((i)*bin_width, hist_h - cvRound(hist.at<float>(i, 0))), color, 1, LINE_AA);
}
}
else
{
cout << "绘制直方图类型参数错误" << endl;
return;
}
imshow(winName, histImage);
}
其中参数含义如下:
参数hist:输入的要绘制的直方图(单通道);
参数winName:显示直方图的窗口名称;
参数color:绘制直方图的颜色;
参数histImgType:绘制直方图的样式,默认绘制柱状直方图(histImgType=0),或者可以选择绘制折线直方图(histImgType=1)。
这里有一个需要注意的点,就是封装的这个函数中对直方图进行了归一化,所以会修改直方图中的值,也就修改了传入Mat对象的值。虽然函数参数中是以值传递的形参传入,但依然会修改函数体外Mat对象的值。
Mat类型对象作为参数时,不管是传入给函数的形参、还是作为引用传入函数,在函数体内部对该参数进行修改时都会改变函数体外的Mat对象。因为Mat类型对象可以看成一种特殊的指针,无论值传递或者引用传递后的Mat类型的形参,都和原Mat对象指向同一块内存。
所以在向函数传入Mat对象作为参数时,如果希望在函数体内修改Mat对象的值而不修改原Mat对象的值,并且又不想多创建一个局部变量出来,就可以使用Mat::clone()直接创建一个Mat类型的匿名对象作为参数传入。
该函数调用形式如下:
drawNormHist(hist.clone(), "hist1", Scalar(255, 255, 255), 1);
那么到此,就完成了直方图的规定化并进行了直方图的绘制,下面来看一下效果如何。
首先是输入的待匹配图像及其直方图:
然后是模板图像及其直方图:
从上面可以看到,待匹配图像直方图的灰度级主要分布在中间,而模板图像直方图的灰度级主要分布在右侧,从模板图像中亮度很大这一点也可以看得出来。所以可以推断,直方图规定化后的结果图像,肯定也是偏亮的,因为会把待匹配图像直方图的灰度级分布给偏移到右侧,下面给出结果图像。
可见,程序输出结果和直方图规定化的推论一致,实现了应有的效果。
那么本次笔记就到此为止啦,谢谢阅读~
PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!