【OpenCV 4开发详解】直方图应用
直方图不仅能够表示图像像素的统计特性,应用统计的直方图结果也可以增强图像的对比度,在图像中寻找相似区域等。本节中将重点介绍如果通过调整直方图分布提高图像的对比度、利用直方图反向投影寻找相同区域以及将图像的对比度调整为指定的形式。
直方图均衡化
如果一个图像的直方图都集中在一个区域,则整体图像的对比度比较小,不便于图像中纹理的识别。例如相邻的两个像素灰度值如果分别是120和121,仅凭肉眼是如法区别出来的。同时,如果图像中所有的像素灰度值都集中在100到150之间,则整个图像想会给人一种模糊的感觉,看不清图中的内容。如果通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,就可以提高图像的对比度,进而将图像中的纹理突出显现出来,这个过程称为图像直方图均衡化。
在OpenCV 4中提供了equalizeHist()函数用于将图像的直方图均衡化,该函数的函数原型在代码清单4-7中给出。
代码清单4-7 equalizeHist()函数原型1.void cv::equalizeHist(InputArray src,2. OutputArray dst 3. )
src:需要直方图均衡化的CV_8UC1图像。
dst:直方图均衡化后的输出图像,与src具有相同尺寸和数据类型。
该函数形式比较简单,但是需要注意该函数只能对单通道的灰度图进行直方图均衡化。对图像的均衡化示例程序在代码清单4-8中给出,程序中我们将一张图像灰度值偏暗的图像进行均衡化,通过结果可以发现经过均衡化后的图像对比度明显增加,可以看清楚原来看不清的纹理。通过绘制原图和均衡化后的图像的直方图可以发现,经过均衡化后的图像直方图分布更加均匀。
代码清单4-8 myEqualizeHist.cpp直方图均衡化实现4.#include <opencv2\opencv.hpp>5.#include <iostream>6.7.using namespace cv;8.using namespace std;9.10.void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数11.{12.int hist_w = 512;13.int hist_h = 400;14.int width = 2;15.Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);16.normalize(hist, hist, 1, 0, type, -1, Mat());17.for (int i = 1; i <= hist.rows; i++)18.{22.}23.imshow(name, histImage);24.}25.//主函数26.int main()27.{28.Mat img = imread("gearwheel.jpg");29.if (img.empty())30.{31.cout << "请确认图像文件名称是否正确" << endl;32.return -1;33.}34.Mat gray, hist, hist2;35.cvtColor(img, gray, COLOR_BGR2GRAY);36.Mat equalImg;37.equalizeHist(gray, equalImg); //将图像直方图均衡化38.const int channels[1] = { 0 };39.float inRanges[2] = { 0,255 };40.const float* ranges[1] = { inRanges };41.const int bins[1] = { 256 };42.calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges);43.calcHist(&equalImg, 1, channels, Mat(), hist2, 1, bins, ranges);44.drawHist(hist, NORM_INF, "hist");45.drawHist(hist2, NORM_INF, "hist2");46.imshow("原图", gray);47.imshow("均衡化后的图像", equalImg);48.waitKey(0);49.return 0;50.}
图4-6 myEqualizeHist.cpp程序运行结果
直方图匹配
直方图均衡化函数可以自动的改变图像直方图的分布形式,这种方式极大的简化了直方图均衡化过程中需要的操作步骤,但是该函数不能指定均衡化后的直方图分布形式。在某些特定的条件下需要将直方图映射成指定的分布形式,这种将直方图映射成指定分布形式的算法称为直方图匹配或者直方图规定化。直方图匹配与直方图均衡化相似,都是对图像的直方图分布形式进行改变,只是直方图均衡化后的图像直方图是均匀分布的,而直方图匹配后的直方图可以随意指定,即在执行直方图匹配操作时,首先要知道变换后的灰度直方图分布形式,进而确定变换函数。直方图匹配操作能够有目的的增强某个灰度区间,相比于直方图均衡化操作,该算法虽然多了一个输入,但是其变换后的结果也更灵活。
由于不同图像间像素数目可能不同,为了使两个图像直方图能够匹配,需要使用概率形式去表示每个灰度值在图像像素中所占的比例。理想状态下,经过图像直方图匹配操作后图像直方图分布形式应与目标分布一致,因此两者之间的累积概率分布也一致。累积概率为小于等于某一灰度值的像素数目占所有像素中的比例。我们用 V s {V_s} Vs表示原图像直方图的各个灰度级的累积概率,用 V z {V_z} Vz表示匹配后直方图的各个灰度级累积概率。那么确定由原图像中灰度值n映射成r的条件如式(6.8)所示。
n , r = arg min n , r ∣ V s ( n ) − V z ( r ) ∣ (6.8) n,r = \arg \mathop {\min }\limits_{n,r} \left| {{V_s}(n) - {V_z}(r)} \right| \tag{6.8} n,r=argn,rmin∣Vs(n)−Vz(r)∣(6.8)
为了更清楚的说明直方图匹配过程,在图4-7中给出了一个直方图匹配示例。示例中目标直方图灰度值2以下的概率都为0,灰度值3的累积概率为0.16,灰度值4的累积概率为0.35,原图像直方图灰度值为0时累积概率为0.19。0.19距离0.16的距离小于距离0.35的距离,因此需要将原图像中灰度值0匹配成灰度值3。同样,原图像灰度值1的累积概率为0.43,其距离目标直方图灰度值4的累积概率0.35的距离为0.08,而距离目标直方图灰度值5的累积概率0.64的距离为0.21,因此需要将原图像中灰度值1匹配成灰度值4。
图4-7 直方图匹配示例
这个寻找灰度值匹配的过程是直方图匹配算法的关键,在代码实现中我们可以通过构建原直方图累积概率与目标直方图累积概率之间的差值表,寻找原直方图中灰度值n的累积概率与目标直方图中所有灰度值累积概率差值的最小值,这个最小值对应的灰度值r就是n匹配后的灰度值。
在OpenCV 4中并没有提供直方图匹配的函数,需要自己根据算法实现图像直方图匹配。在代码清单4-9中给出了实现直方图匹配的示例程序。程序中待匹配的原图是一个图像整体偏暗的图像,目标直方图分配形式来自于一张较为明亮的图像,经过图像直方图匹配操作之后,提高了图像的整体亮度,图像直方图分布也更加均匀,程序中所有的结果在图4-8、图4-9给出。
代码清单4-9 myHistMatch.cpp图像直方图匹配1.#include <opencv2\opencv.hpp>2.#include <iostream>3.4.using namespace cv;5.using namespace std;6.7.void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数8.{9.int hist_w = 512;10.int hist_h = 400;11.int width = 2;12.Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);13.normalize(hist, hist, 1, 0, type, -1, Mat());14.for (int i = 1; i <= hist.rows; i++)15.{16.rectangle(histImage, Point(width*(i - 1), hist_h - 1),17.Point(width*i - 1,hist_h - cvRound(20 * hist_h*hist.at<float>(i-1)) - 1),18.Scalar(255, 255, 255), -1);19.}20.imshow(name, histImage);21.}22.//主函数23.int main()24.{25.Mat img1 = imread("histMatch.png");26.Mat img2 = imread("equalLena.png");27.if (img1.empty()||img2.empty())28.{29.cout << "请确认图像文件名称是否正确" << endl;30.return -1;31.}32.Mat hist1, hist2;33.//计算两张图像直方图34.const int channels[1] = { 0 };35.float inRanges[2] = { 0,255 };36.const float* ranges[1] = { inRanges };37.const int bins[1] = { 256 };38.calcHist(&img1, 1, channels, Mat(), hist1, 1, bins, ranges);39.calcHist(&img2, 1, channels, Mat(), hist2, 1, bins, ranges);40.//归一化两张图像的直方图41.drawHist(hist1, NORM_L1, "hist1");42.drawHist(hist2, NORM_L1, "hist2");43.//计算两张图像直方图的累积概率44.float hist1_cdf[256] = { hist1.at<float>(0) };45.float hist2_cdf[256] = { hist2.at<float>(0) };46.for (int i = 1; i < 256; i++)47.{48.hist1_cdf[i] = hist1_cdf[i - 1] + hist1.at<float>(i);49.hist2_cdf[i] = hist2_cdf[i - 1] + hist2.at<float>(i);50.51.}52.//构建累积概率误差矩阵53.float diff_cdf[256][256];54.for (int i = 0; i < 256; i++)55.{56.for (int j = 0; j < 256; j++)57.{58.diff_cdf[i][j] = fabs(hist1_cdf[i] - hist2_cdf[j]);59.}60.}61.62.//生成LUT映射表63.Mat lut(1, 256, CV_8U);64.for (int i = 0; i < 256; i++)65.{66.// 查找源灰度级为i的映射灰度67.// 和i的累积概率差值最小的规定化灰度68.float min = diff_cdf[i][0];69.int index = 0;70.//寻找累积概率误差矩阵中每一行中的最小值71.for (int j = 1; j < 256; j++)72.{73.if (min > diff_cdf[i][j])74.{75.min = diff_cdf[i][j];76.index = j;77.}78.}79.lut.at<uchar>(i) = (uchar)index;80.}81.Mat result, hist3;82.LUT(img1, lut, result);83.imshow("待匹配图像", img1);84.imshow("匹配的模板图像", img2);85.imshow("直方图匹配结果", result);86.calcHist(&result, 1, channels, Mat(), hist3, 1, bins, ranges);87.drawHist(hist3, NORM_L1, "hist3"); //绘制匹配后的图像直方图88.waitKey(0);89.return 0;90.}
图4-8 myHistMatch.cpp程序中匹配图像原图、模板以及匹配后图像
图4-9 myHistMatch.cpp程序中给图像的直方图
直方图反向投影
如果一张图像的某个区域中显示的是一种结构纹理或者一个独特的形状,那么这个区域的直方图就可以看作是这个结构或者形状的概率函数,在图像中寻找这种概率分布就是在图像中寻找该结构纹理或者独特形状。反向投影(back projection)就是一种记录给定图像中的像素点如何适应直方图模型像素分布方式的一种方法。简单的讲,所谓反向投影就是首先计算某一特征的直方图模型,然后使用模型去寻找图像中是否存在该特征的方法。
OpenCV 4提供了calcBackProject()函数用于对图像直方图反向投影,该函数的函数原型在代码清单4-10中给出。
代码清单4-10 calcBackProject()函数原型1.void cv::calcBackProject(const Mat * images,2. int nimages,3. const int * channels,4. InputArray hist,5. OutputArray backProject,6. const float ** ranges,7. double scale = 1,8. bool uniform = true 9. )
images:待统计直方图的图像数组,数组中所有的图像应具有相同的尺寸和数据类型,并且数据类型只能是CV_8U、CV_16U和CV_32F三种中的一种,但是不同图像的通道数可以不同。
nimages:输入图像数量
channels:需要统计的通道索引数组,第一个图像的通道索引从0到images[0].channels()-1,第二个图像通道索引从images[0].channels()到images[0].channels()+ images[1].channels()-1,以此类推。
hist:输入直方图
backProject:目标反投影图像,与images[0]具有相同尺寸和数据类型的单通道图像。
ranges:每个图像通道中灰度值的取值范围
scale:输出反投影矩阵的比例因子。
uniform:直方图是否均匀的标志符,默认状态下为均匀(true)。
该函数用于在输入图像中寻找与特定图像最匹配的点或者区域,即对图像进行反向投影。该函数输入参数与计算图像直方图函数calcHist()大致相似,都需要输入图像和需要进行反向投影的通道索引数目。区别之处在于该函数需要输入模板图像的直方图统计结果,并返回的是一张图像,而不是直方图统计结果。根据该函数所需要的参数可知,该函数在使用时主要分为四个步骤:
Step1:加载模板图像和待反向投影图像;
Step2:转换图像颜色空间,常用的颜色空间为灰度图像和HSV空间;
Step3:计算模板图像的直方图,灰度图像为一维直方图,HSV图像为H-S通道的二维直方图;
tep4:将待反向投影的图像和模板图像的直方图赋值给反向投影函数calcBackProject(),最终得到反向投影结果。
为了更加熟悉该函数的使用方式,了解图像反向投影的作用,在代码清单4-11中给出了对图像进行反向投影的示例程序。程序中首先加载待反向投影图像和模板图像,模板图像从待反向投影的图像中截取,之后将两张图像由RGB颜色空间转成HSV空间中,统计H-S通道的直方图,将直方图归一化后绘制H-S通道的二维直方图。最后将待反向投影和模板图像的直方图输入给函数calcBackProject(),得到图像反向投影结果。
代码清单4-11 myCalcBackProject.cpp图像直方图反向投影1.#include <opencv2\opencv.hpp>2.#include <iostream>3.4.using namespace cv;5.using namespace std;6.7.void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数8.{9.int hist_w = 512;10.int hist_h = 400;11.int width = 2;12.Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);13.normalize(hist, hist, 255, 0, type, -1, Mat());14.namedWindow(name, WINDOW_NORMAL);15.imshow(name, hist);16.}17.//主函数18.int main()19.{20.Mat img = imread("apple.jpg");21.Mat sub_img = imread("sub_apple.jpg");22.Mat img_HSV, sub_HSV, hist, hist2;23.if (img.empty() || sub_img.empty())24.{25.cout << "请确认图像文件名称是否正确" << endl;26.return -1;27.}28.29.imshow("img", img);30.imshow("sub_img", sub_img);31.//转成HSV空间,提取S、V两个通道32.cvtColor(img, img_HSV, COLOR_BGR2HSV);33.cvtColor(sub_img, sub_HSV, COLOR_BGR2HSV);34.int h_bins = 32; int s_bins = 32;35.int histSize[] = { h_bins, s_bins };36.//H通道值的范围由0到17937.float h_ranges[] = { 0, 180 };38.//S通道值的范围由0到25539.float s_ranges[] = { 0, 256 };40.const float* ranges[] = { h_ranges, s_ranges }; //每个通道的范围41.int channels[] = { 0, 1 }; //统计的通道索引42.//绘制H-S二维直方图43.calcHist(&sub_HSV, 1, channels, Mat(), hist, 2, histSize, ranges, true, false);44.drawHist(hist, NORM_INF, "hist"); //直方图归一化并绘制直方图45.Mat backproj;//直方图反向投影47.imshow("反向投影后结果", backproj);48.waitKey(0);49.return 0;50.}
图4-10 myCalcBackProject.cpp程序运行结果