25.图像直方图
1、图像直方图绘制
图像直方图是图像处理中非常重要的像素统计结果,图像直方图不再表征任何的图像纹理信息,而是对图像像素的统计。由于同一物体无论是旋转还是平移在图像中都具有相同的灰度值,因此直方图具有平移不变性、放缩不变性等优点,因此可以用来查看图像整体的变化形式,例如图像是否过暗、图像像素灰度值主要集中在哪些范围等,在特定的条件下也可以利用图像直方图进行图像的识别,例如对数字的识别。
图像直方图简单来说就是统计图像中每个灰度值的个数,之后将图像灰度值作为横轴,以灰度值个数或者灰度值所占比率作为纵轴绘制的统计图。通过直方图可以看出图像中哪些灰度值数目较多,哪些较少,可以通过一定的方法将灰度值较为集中的区域映射到较为稀疏的区域,从而使得图像在像素灰度值上分布更加符合期望状态。通常情况下,像素灰度值代表亮暗程度,因此通过图像直方图可以分析图像亮暗对比度,并调整图像的亮暗程度。
代码清单4-1 calcHist()函数原型 1. void cv::calcHist(const Mat * images, 2. int nimages, 3. const int * channels, 4. InputArray mask, 5. OutputArray hist, 6. int dims, 7. const int * histSize, 8. const float ** ranges, 9. bool uniform = true, 10. bool accumulate = false 11. )
-
images:待统计直方图的图像数组,数组中所有的图像应具有相同的尺寸和数据类型,并且数据类型只能是CV_8U、CV_16U和CV_32F三种中的一种,但是不同图像的通道数可以不同。
-
nimages:输入的图像数量
-
channels:需要统计的通道索引数组,第一个图像的通道索引从0到images[0].channels()-1,第二个图像通道索引从images[0].channels()到images[0].channels()+ images[1].channels()-1,以此类推。
-
mask:可选的操作掩码,如果是空矩阵则表示图像中所有位置的像素都计入直方图中,如果矩阵不为空,则必须与输入图像尺寸相同且数据类型为CV_8U。
-
hist:输出的统计直方图结果,是一个dims维度的数组。
-
dims:需要计算直方图的维度,必须是整数,并且不能大于CV_MAX_DIMS,在OpenCV 4.0和OpenCV 4.1版本中为32。
-
histSize:存放每个维度直方图的数组的尺寸。
-
ranges:每个图像通道中灰度值的取值范围。
-
uniform:直方图是否均匀的标志符,默认状态下为均匀(true)。
-
accumulate:是否累积统计直方图的标志,如果累积(true),则统计新图像的直方图时之前图像的统计结果不会被清除,该同能主要用于统计多个图像整体的直方图。
代码清单4-2 myCalHist.cpp绘制图像直方图 1. #include <opencv2\opencv.hpp> 2. #include <iostream> 3. 4. using namespace cv; 5. using namespace std; 6. 7. int main() 8. { 9. Mat img = imread("apple.jpg"); 10. if (img.empty()) 11. { 12. cout << "请确认图像文件名称是否正确" << endl; 13. return -1; 14. } 15. Mat gray; 16. cvtColor(img, gray, COLOR_BGR2GRAY); 17. //设置提取直方图的相关变量 18. Mat hist; //用于存放直方图计算结果 19. const int channels[1] = { 0 }; //通道索引 20. float inRanges[2] = { 0,255 }; 21. const float* ranges[1] = { inRanges }; //像素灰度值范围 22. const int bins[1] = { 256 }; //直方图的维度,其实就是像素灰度值的最大值 23. calcHist(&img, 1, channels, Mat(), hist, 1, bins, ranges); //计算图像直方图 24. //准备绘制直方图 25. int hist_w = 512; 26. int hist_h = 400; 27. int width = 2; 28. Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3); 29. for (int i = 1; i <= hist.rows; i++) 30. { 31. rectangle(histImage, Point(width*(i - 1), hist_h - 1), 32. Point(width*i - 1, hist_h - cvRound(hist.at<float>(i - 1) / 20)), 33. Scalar(255, 255, 255), -1); 34. } 35. namedWindow("histImage", WINDOW_AUTOSIZE); 36. imshow("histImage", histImage); 37. imshow("gray", gray); 38. waitKey(0); 39. return 0; 40. }
2、直方图归一化
图像的像素灰度值统计结果主要目的之一就是查看某个灰度值在所有像素中所占的比例,因此可以用每个灰度值像素的数目占一幅图像中所有像素数目的比例来表示某个灰度值数目的多少,即将统计结果再除以图像中像素个数。这种方式可以保证每个灰度值的统计结果都是0到100%之间的数据,实现统计结果的归一化,但是这种方式也存在一个弊端,就是再CV_8U类型的图像中,灰度值有256个等级,平均每个像素的灰度值所占比例为0.39%,这个比例非常低,因此为了更直观的绘制图像直方图,常需要将比例扩大一定的倍数后再绘制图像。另一种常用的归一化方式是寻找统计结果中最大数值,把所有结果除以这个最大的数值,以实现将所有数据都缩放到0到1之间。
代码清单4-3 normalize()函数原型 1. void cv::normalize(InputArray src, 2. InputOutputArray dst, 3. double alpha = 1, 4. double beta = 0, 5. int norm_type = NORM_L2, 6. int dtype = -1, 7. InputArray mask = noArray() 8. )
-
src:输入数组矩阵。
-
dst:输入与src相同大小的数组矩阵。
-
alpha:在范围归一化的情况下,归一化到下限边界的标准值
-
beta:范围归一化时的上限范围,它不用于标准规范化。
-
norm_type:归一化过程中数据范数种类标志,常用可选择参数在表4-1中给出
-
dtype:输出数据类型选择标志,如果为负数,则输出数据与src拥有相同的类型,否则与src具有相同的通道数和数据类型。
-
mask:掩码矩阵。
该函数输入一个存放数据的矩阵,通过参数alpha设置将数据缩放到的最大范围,然后通过norm_type参数选择计算范数的种类,之后将输入矩阵中的每个数据分别除以求取的范数数值,最后得到缩放的结果。输出结果是一个CV_32F类型的矩阵,可以将输入矩阵作为输出矩阵,或者重新定义一个新的矩阵用于存放输出结果。该函数的第五个参数用于选择计算数据范数的种类,常用的可选择参数以及计算范数的公式都在表4-1中给出。计算不同的范数,最后的结果也不相同,例如选择NORM_L1标志,输出结果为每个灰度值所占的比例;选择NORM_INF参数,输出结果为除以数据中最大值,将所有的数据归一化到0到1之间。
表4-1 normalize()函数归一化常用标志参数
标志参数 |
简记 |
作用 |
原理 |
NORM_INF |
1 |
无穷范数,向量最大值 |
|
NORM_L1 |
2 |
L1范数,绝对值之和 |
|
NORM_L2 |
4 |
L2范数,平方和之根 |
|
NORM_L2SQR |
5 |
L2范数平方 |
|
代码清单4-4 myNormalize.cpp直方图归一化操作 1. #include <opencv2\opencv.hpp> 2. #include <iostream> 3. 4. using namespace cv; 5. using namespace std; 6. 7. int main() 8. { 9. system("color F0"); //更改输出界面颜色 10. vector<double> positiveData = { 2.0, 8.0, 10.0 }; 11. vector<double> normalized_L1, normalized_L2, normalized_Inf, normalized_L2SQR; 12. //测试不同归一化方法 13. normalize(positiveData, normalized_L1, 1.0, 0.0, NORM_L1); //绝对值求和归一化 14. cout <<"normalized_L1=["<< normalized_L1[0]<<", " 15. << normalized_L1[1]<<", "<< normalized_L1[2] <<"]"<< endl; 16. normalize(positiveData, normalized_L2, 1.0, 0.0, NORM_L2); //模长归一化 17. cout << "normalized_L2=[" << normalized_L2[0] << ", " 18. << normalized_L2[1] << ", " << normalized_L2[2] << "]" << endl; 19. normalize(positiveData, normalized_Inf, 1.0, 0.0, NORM_INF); //最大值归一化 20. cout << "normalized_Inf=[" << normalized_Inf[0] << ", " 21. << normalized_Inf[1] << ", " << normalized_Inf[2] << "]" << endl; 22. normalize(positiveData, normalized_L2SQR, 1.0, 0.0, NORM_MINMAX); //偏移归一化 23. cout << "normalized_MINMAX=[" << normalized_L2SQR[0] << ", " 24. << normalized_L2SQR[1] << ", " << normalized_L2SQR[2] << "]" << endl; 25. //将图像直方图归一化 26. Mat img = imread("apple.jpg"); 27. if (img.empty()) 28. { 29. cout << "请确认图像文件名称是否正确" << endl; 30. return -1; 31. } 32. Mat gray,hist; 33. cvtColor(img, gray, COLOR_BGR2GRAY); 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(&gray, 1, channels, Mat(), hist, 1, bins, ranges); 39. int hist_w = 512; 40. int hist_h = 400; 41. int width = 2; 42. Mat histImage_L1 = Mat::zeros(hist_h, hist_w, CV_8UC3); 43. Mat histImage_Inf = Mat::zeros(hist_h, hist_w, CV_8UC3); 44. Mat hist_L1, hist_Inf; 45. normalize(hist, hist_L1, 1, 0, NORM_L1,-1, Mat()); 46. for (int i = 1; i <= hist_L1.rows; i++) 47. { 48. rectangle(histImage_L1, Point(width*(i - 1), hist_h - 1), 49. Point(width*i - 1, hist_h - cvRound(30*hist_h*hist_L1.at<float>(i-1))-1), 50. Scalar(255, 255, 255), -1); 51. } 52. normalize(hist, hist_Inf, 1, 0, NORM_INF, -1, Mat()); 53. for (int i = 1; i <= hist_Inf.rows; i++) 54. { 55. rectangle(histImage_Inf, Point(width*(i - 1), hist_h - 1), 56. Point(width*i - 1, hist_h - cvRound(hist_h*hist_Inf.at<float>(i-1)) - 1), 57. Scalar(255, 255, 255), -1); 58. } 59. imshow("histImage_L1", histImage_L1); 60. imshow("histImage_Inf", histImage_Inf); 61. waitKey(0); 62. return 0; 63. }
3、直方图比较
图像的直方图表示图像像素灰度值的统计特性,因此可以通过比较两张图像的直方图特性比较两张图像的相似程度。从一定程度上来讲,虽然两张图像的直方图分布相似不代表两张图像相似,但是两张图像相似则两张图像的直方图分布一定相似。例如通过插值对图像进行放缩后图像的直方图虽然不会与之前完全一致,但是两者一定具有很高的相似性,因而可以通过比较两张图像的直方图分布相似性对图像进行初步的筛选与识别。
代码清单4-5 compareHist()函数原型 1. double cv::compareHist(InputArray H1, 2. InputArray H2, 3. int method 4. )
-
H1:第一张图像直方图。
-
H2:第二张图像直方图,与H1具有相同的尺寸
-
method:比较方法标志,可选择参数及含义在表4-2中给出。
该函数前两个参数为需要比较相似性的图像直方图,由于不同尺寸的图像中像素数目可能不相同,为了能够得到两个直方图图像正确的相识性,需要输入同一种方式归一化后的图像直方图,并且要求两个图像直方图具有相同的尺寸。函数第三个参数为比较相似性的方法,选择不同的方法,会得到不同的相识性系数,函数将计算得到的相似性系数以double类型返回。由于不同计算方法的规则不一,因此相似性系数代表的含义也不相同,函数可以选择的计算方式标志在表4-2中给出,接下来介绍每种方法比较相似性的原理。
表4-2 comparaHist()函数比较直方图方法的选择标志参数
标志参数 |
简记 |
作用 |
HISTCMP_CORREL |
0 |
相关法 |
HISTCMP_CHISQR |
1 |
卡方法 |
HISTCMP_INTERSECT |
2 |
直方图相交法 |
HISTCMP_BHATTACHARYYA |
3 |
巴塔恰里雅距离(巴氏距离)法 |
HISTCMP_HELLINGER |
3 |
与HISTCMP_BHATTACHARYYA方法相同 |
HISTCMP_CHISQR_ALT |
4 |
替代卡方法 |
HISTCMP_KL_DIV |
5 |
相对熵法(Kullback-Leibler散度) |
代码清单4-6 myCompareHist.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(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. system("color F0"); //更改输出界面颜色 26. Mat img = imread("apple.jpg"); 27. if (img.empty()) 28. { 29. cout << "请确认图像文件名称是否正确" << endl; 30. return -1; 31. } 32. Mat gray, hist, gray2, hist2, gray3, hist3; 33. cvtColor(img, gray, COLOR_BGR2GRAY); 34. resize(gray, gray2, Size(), 0.5, 0.5); 35. gray3 = imread("lena.png", IMREAD_GRAYSCALE); 36. const int channels[1] = { 0 }; 37. float inRanges[2] = { 0,255 }; 38. const float* ranges[1] = { inRanges }; 39. const int bins[1] = { 256 }; 40. calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges); 41. calcHist(&gray2, 1, channels, Mat(), hist2, 1, bins, ranges); 42. calcHist(&gray3, 1, channels, Mat(), hist3, 1, bins, ranges); 43. drawHist(hist, NORM_INF, "hist"); 44. drawHist(hist2, NORM_INF, "hist2"); 45. drawHist(hist3, NORM_INF, "hist3"); 46. //原图直方图与原图直方图的相关系数 47. double hist_hist = compareHist(hist, hist, HISTCMP_CORREL); 48. cout << "apple_apple=" << hist_hist << endl; 49. //原图直方图与缩小原图直方图的相关系数 50. double hist_hist2 = compareHist(hist, hist2, HISTCMP_CORREL); 51. cout << "apple_apple256=" << hist_hist2 << endl; 52. //两张不同图像直方图相关系数 53. double hist_hist3 = compareHist(hist, hist3, HISTCMP_CORREL); 54. cout << "apple_lena=" << hist_hist3 << endl; 55. waitKey(0); 56. return 0; 57. }
4、直方图均衡化
如果一个图像的直方图都集中在一个区域,则整体图像的对比度比较小,不便于图像中纹理的识别。例如相邻的两个像素灰度值如果分别是120和121,仅凭肉眼是如法区别出来的。同时,如果图像中所有的像素灰度值都集中在100到150之间,则整个图像想会给人一种模糊的感觉,看不清图中的内容。如果通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,就可以提高图像的对比度,进而将图像中的纹理突出显现出来,这个过程称为图像直方图均衡化。
代码清单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. { 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. }
5、直方图匹配
直方图均衡化函数可以自动的改变图像直方图的分布形式,这种方式极大的简化了直方图均衡化过程中需要的操作步骤,但是该函数不能指定均衡化后的直方图分布形式。在某些特定的条件下需要将直方图映射成指定的分布形式,这种将直方图映射成指定分布形式的算法称为直方图匹配或者直方图规定化。直方图匹配与直方图均衡化相似,都是对图像的直方图分布形式进行改变,只是直方图均衡化后的图像直方图是均匀分布的,而直方图匹配后的直方图可以随意指定,即在执行直方图匹配操作时,首先要知道变换后的灰度直方图分布形式,进而确定变换函数。直方图匹配操作能够有目的的增强某个灰度区间,相比于直方图均衡化操作,该算法虽然多了一个输入,但是其变换后的结果也更灵活。
由于不同图像间像素数目可能不同,为了使两个图像直方图能够匹配,需要使用概率形式去表示每个灰度值在图像像素中所占的比例。理想状态下,经过图像直方图匹配操作后图像直方图分布形式应与目标分布一致,因此两者之间的累积概率分布也一致。累积概率为小于等于某一灰度值的像素数目占所有像素中的比例。我们用Vs表示原图像直方图的各个灰度级的累积概率,用Vz表示匹配后直方图的各个灰度级累积概率。那么确定由原图像中灰度值n映射成r的条件如式所示。
为了更清楚的说明直方图匹配过程,在图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匹配后的灰度值。
代码清单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. }