经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门书籍《OpenCV 4开发详解》。为了更让小伙伴更早的了解最新版的OpenCV 4,小白与出版社沟通,提前在公众号上连载部分内容,请持续关注小白。 |
直方图不仅能够表示图像像素的统计特性,应用统计的直方图结果也可以增强图像的对比度,在图像中寻找相似区域等。本节中将重点介绍如果通过调整直方图分布提高图像的对比度、利用直方图反向投影寻找相同区域以及将图像的对比度调整为指定的形式。
如果一个图像的直方图都集中在一个区域,则整体图像的对比度比较小,不便于图像中纹理的识别。例如相邻的两个像素灰度值如果分别是120和121,仅凭肉眼是如法区别出来的。同时,如果图像中所有的像素灰度值都集中在100到150之间,则整个图像想会给人一种模糊的感觉,看不清图中的内容。如果通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,就可以提高图像的对比度,进而将图像中的纹理突出显现出来,这个过程称为图像直方图均衡化。
在OpenCV 4中提供了equalizeHist()函数用于将图像的直方图均衡化,该函数的函数原型在代码清单4-7中给出。
代码清单4-7 equalizeHist()函数原型
1. void cv::equalizeHist(InputArray src,
2. OutputArray dst
3. )
该函数形式比较简单,但是需要注意该函数只能对单通道的灰度图进行直方图均衡化。对图像的均衡化示例程序在代码清单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. {
19. rectangle(histImage, Point(width*(i - 1), hist_h - 1),
20. Point(width*i - 1, hist_h - cvRound(hist_h*hist.at<float>(i - 1)) - 1),
21. Scalar(255, 255, 255), -1);
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. }
直方图均衡化函数可以自动的改变图像直方图的分布形式,这种方式极大的简化了直方图均衡化过程中需要的操作步骤,但是该函数不能指定均衡化后的直方图分布形式。在某些特定的条件下需要将直方图映射成指定的分布形式,这种将直方图映射成指定分布形式的算法称为直方图匹配或者直方图规定化。直方图匹配与直方图均衡化相似,都是对图像的直方图分布形式进行改变,只是直方图均衡化后的图像直方图是均匀分布的,而直方图匹配后的直方图可以随意指定,即在执行直方图匹配操作时,首先要知道变换后的灰度直方图分布形式,进而确定变换函数。直方图匹配操作能够有目的的增强某个灰度区间,相比于直方图均衡化操作,该算法虽然多了一个输入,但是其变换后的结果也更灵活。
由于不同图像间像素数目可能不同,为了使两个图像直方图能够匹配,需要使用概率形式去表示每个灰度值在图像像素中所占的比例。理想状态下,经过图像直方图匹配操作后图像直方图分布形式应与目标分布一致,因此两者之间的累积概率分布也一致。累积概率为小于等于某一灰度值的像素数目占所有像素中的比例。我们用 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。
这个寻找灰度值匹配的过程是直方图匹配算法的关键,在代码实现中我们可以通过构建原直方图累积概率与目标直方图累积概率之间的差值表,寻找原直方图中灰度值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. }
如果一张图像的某个区域中显示的是一种结构纹理或者一个独特的形状,那么这个区域的直方图就可以看作是这个结构或者形状的概率函数,在图像中寻找这种概率分布就是在图像中寻找该结构纹理或者独特形状。反向投影(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. )
该函数用于在输入图像中寻找与特定图像最匹配的点或者区域,即对图像进行反向投影。该函数输入参数与计算图像直方图函数calcHist()大致相似,都需要输入图像和需要进行反向投影的通道索引数目。区别之处在于该函数需要输入模板图像的直方图统计结果,并返回的是一张图像,而不是直方图统计结果。根据该函数所需要的参数可知,该函数在使用时主要分为四个步骤:
为了更加熟悉该函数的使用方式,了解图像反向投影的作用,在代码清单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到179
37. float h_ranges[] = { 0, 180 };
38. //S通道值的范围由0到255
39. 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;
46. calcBackProject(&img_HSV, 1, channels, hist, backproj,ranges,1.0); //直方图反向投影
47. imshow("反向投影后结果", backproj);
48. waitKey(0);
49. return 0;
50. }
OpenCV 4开发详解 |
往期推荐 |
---|
【OpenCV 4开发详解】Image Watch插件的使用 |
【OpenCV 4开发详解】安装过程中问题解决方案 |
【OpenCV 4开发详解】了解OpenCV的模块架构 |
【OpenCV 4开发详解】Mat类介绍 |
【OpenCV 4开发详解】Mat类构造与赋值 |
【OpenCV 4开发详解】4种读取Mat类元素的的方法 |
【OpenCV 4开发详解】图像的读取与显示 |
【OpenCV 4开发详解】视频加载与摄像头调用 |
【OpenCV 4开发详解】图像与视频的保存 |
【OpenCV 4开发详解】保存和读取XML和YMAL文件 |
【OpenCV 4开发详解】颜色模型与转换 |
【OpenCV 4开发详解】多通道分离与合并 |
【OpenCV 4开发详解】图像像素统计 |
【OpenCV 4开发详解】两图像间的像素操作 |
【OpenCV 4开发详解】图像二值化 |
【OpenCV 4开发详解】图像LUT查找表 |
【OpenCV 4开发详解】图像连接 |
【OpenCV 4开发详解】图像仿射变换 |
【OpenCV 4开发详解】图像透视变换 |
【OpenCV 4开发详解】图像极坐标变换 |
【OpenCV 4开发详解】图像上绘制几何图形 |
【OpenCV 4开发详解】图像金字塔 |
【OpenCV 4开发详解】窗口交互操作 |
【OpenCV 4开发详解】图像直方图绘制 |
【OpenCV 4开发详解】直方图操作 |
经过几个月的努力,市面上第一本OpenCV 4入门书籍《OpenCV 4开发详解》将春节后由人民邮电出版社发行。如果小伙伴觉得内容有帮助,希望到时候多多支持! |
关注小白的小伙伴可以提前看到书中的内容,我们创建了学习交流群,欢迎各位小伙伴添加小白微信加入交流群,添加小白时请备注“学习OpenCV 4”。 |