直方图与匹配
一、直方图简介
图像直方图(Histogram)是一种频率分布图,它描述了不同强度值在图像中出现的频率。图像直方图可以统计任何图像特征,如灰度、饱和度、梯度等。
彩色图像的亮度直方图就是其灰度图的直方图。亮度直方图考虑了所有颜色通道,但有时也需要对单种颜色通道进行观察分析。计算单种颜色通道直方图时,每种颜色通道都作为一个独立的灰度图像,分别计算其直方图。各种颜色通道的直方图有时近似的,有时则相差甚远,特别是当图像偏于某一色系时。
在讨论直方图时经常涉及以下3个概念:
- Dims(维数):需要统计的特征的维数,一般情况下,图像直方图统计的特征只有一种,即灰度,此时的维数等于1。
- Bins(组距):每个特征空间子区段的数目
- Range(范围):需要统计的特征的取值范围。通常情况下,图像直方图的灰度范围为[0, 255]
下面用一个示例说明直方图的画法。假设有一副8*8的图像,其灰度数据如下,为了简化起见,将灰度区段数(Bins)设为16,编号为b0~b15,具体如下:
java
[0, 255] = [0, 15] U [16, 31] U...U [240, 255];
其中b0==[0,15],b1=[16,31],...,b15=[240,255]
//灰度值的取值范围为[0, 255]
灰度值数据
38 | 130 | 167 | 191 | 215 | 180 | 33 | 18 |
---|---|---|---|---|---|---|---|
154 | 165 | 39 | 10 | 66 | 2 | 185 | 24 |
243 | 252 | 62 | 213 | 94 | 54 | 68 | 3 |
139 | 2 | 204 | 111 | 1 | 189 | 204 | 83 |
119 | 188 | 60 | 241 | 154 | 196 | 244 | 169 |
44 | 100 | 17 | 204 | 28 | 185 | 166 | 196 |
163 | 100 | 17 | 204 | 28 | 185 | 166 | 196 |
226 | 4 | 220 | 55 | 87 | 28 | 166 | 146 |
第一步,将灰度值转换为Bin的编号,方法是将灰度值除以16,然后舍弃小数部分,如38除以16等于2,130除以16等于8等,转换后的数据如下:
2 | 8 | 10 | 11 | 13 | 11 | 2 | 1 |
---|---|---|---|---|---|---|---|
9 | 10 | 2 | 0 | 4 | 0 | 11 | 1 |
15 | 15 | 3 | 13 | 5 | 3 | 4 | 0 |
8 | 0 | 12 | 6 | 0 | 11 | 12 | 5 |
7 | 11 | 3 | 15 | 9 | 12 | 15 | 10 |
2 | 1 | 14 | 6 | 2 | 12 | 11 | 12 |
10 | 6 | 1 | 12 | 1 | 11 | 10 | 12 |
14 | 0 | 13 | 3 | 5 | 1 | 10 | 9 |
第二步,统计16个灰度范围(Bin的编号)的个数,如下表
Bin | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 合计 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
个数 | 6 | 6 | 5 | 4 | 2 | 3 | 3 | 1 | 2 | 3 | 6 | 7 | 7 | 3 | 2 | 4 | 64 |
第三步,根据上表,画出直方图如下,x轴代表一个灰度范围,y轴代表个数:
由于直方图只统计数量而不考虑像素在图像中的位置,因而具有平移性、旋转和缩放不变性。也正是因为这个原因,两幅截然不同不同的图像的直方图可能是一样的。
通过对直方图的分析,可以发现有关亮度(曝光)和对比度的问题,并可以了解一张图像是否有效利用了整个强度范围。直方图的均值和中值可用来描述图像的亮度,其中中值比均值更具稳健性:直方图的标准差(或方差)则可以用来描述图像的对比度。
由于直方图中数字越大亮度也越大,因此直方图中的柱形明显击中于中间和右侧,左侧靠近0(黑色)的位置则非常稀疏,这说明这张图像整体偏亮。直方图的峰值都集中在左侧的图像往往曝光不足,而击中在右侧的图像则往往曝光过度。对比度的高低在直方图上也是一目了然的。如果图像的大部分像素集中在直方图的某个范围,则说明其对比度较低,如果像素扩展至直方图整个范围,则对比度较高。
二、直方图统计
在OpenCV中绘制直方图需要先进行直方图统计,然后用绘图函数把直方图绘制出来。
java
//图像直方图的数据统计
void Imgproc.calcHist(List<Mat> images, MatOfint channels, Mat mask, Mat hist, MatOfInt histSize, MatOfFloat ranges)
- image:输入图像
- channels:需要统计直方图的第几通道:如输入图像是灰度图,则它的值是0,如是彩色图像,则用0、1、2代表B、G、R三个通道
- mask:掩膜,如是整幅图像的直方图,则无须定义
- hist:直方图计算结束
- histSize:直方图被分成多少个取件,即bin的个数
- ranges:像素取值范围,通常为0-255
上述函数只负责统计数据,如果想要看到直方图,则还需要用绘图函数把直方图画出来,但是这里有一个小问题。直方图统计出来的是灰度值范围的个数,有的值可能很大,有的则可能很小,要在一张图像中把它们画出来需要先统计出这些值的最大值,然后根据比例画出来。这样相当繁琐,而用OpenCV中的normalize()函数进行归一化就可以解决这个问题。
java
//对矩阵进行归一化
void Core.normalize(Mat src, Mat dst, double alpha, double beta, int norm_type)
- src:输入图像
- dst:输出矩阵,与src具有同样的尺寸
- alpha:归一化后的上限值
- beta:归一化后的上限值
- norm_type:归一化类型,常用参数如下:
- Core.NORM_INF:无穷范数,向量最大值
- Core.NORM_L1:L1范数,绝对值之和
- Core.NORM_L2:L2范数,平方和之平方根
- Core.NORM_L2SQR:L2范数,平方和
- Core.NORM_MINMAX:偏移归一化
java
public class CalcHist {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像灰度图并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/key.jpg", Imgcodecs.IMREAD_GRAYSCALE);
HighGui.imshow("src", src);
HighGui.waitKey(0);
//参数准备
List<Mat> mat = new ArrayList<>();
mat.add(src);
float[] range = {0, 256};//直方图统计值范围
Mat hist = new Mat();
MatOfFloat histRange = new MatOfFloat(range);
//直方图数据统计并归一化
Imgproc.calcHist(mat, new MatOfInt(0), new Mat(), hist, new MatOfInt(256), histRange);
//直方图尺寸
int width = 512;
int height = 400;
Core.normalize(hist, hist, 0, height, Core.NORM_MINMAX);
//将直方图数据转存到数组中以便后续使用
float[] histData = new float[(int)(hist.total() * hist.channels())];
hist.get(0, 0, histData);
//绘制直方图
Scalar black = new Scalar(0, 0, 0);
Scalar white = new Scalar(255, 255, 255);
Mat histImage = new Mat(height, width, CvType.CV_8UC3, black);
int binWid = (int)Math.round((double)width / 256);
//bin的宽度
for (int i = 0; i < 256; i++) {
Imgproc.line(histImage, new Point(i * binWid, height), new Point(i * binWid, height - Math.round(histData[i])), white, binWid);
}
//在屏幕上显示绘制的直方图
HighGui.imshow("calcHist", histImage);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
直方图:

三、直方图比较
由于直方图反映了图像的灰度值的分布特性,因而通过直方图的比较可以在一定程度上了解两幅图像的相似程度。当然,由于两幅截然不同的图像的直方图可能是完全一样的,这种比较只能作为参考。
java
//比较两幅直方图。此函数适用于一维、二维、三维密集直方图,但可能不适用于高维稀疏直方图
double Imgproc.compareHist(Mat h1, Mat h2, int method)
- h1:第一个直方图
- h2:第二个直方图
- method:比较方法,如下:

java
public class CompareHist {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像灰度图并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/pond.png", Imgcodecs.IMREAD_GRAYSCALE);
HighGui.imshow("src", src);
HighGui.waitKey(0);
//对图像进行中值滤波并显示
Mat src2 = new Mat();
Imgproc.medianBlur(src, src2, 5);
HighGui.imshow("Median Blur", src2);
HighGui.waitKey(0);
//直方图参数设置
float[] range = {0, 256};
MatOfFloat histRange = new MatOfFloat(range);
//图像1的直方图数据统计并归一化
Mat hist = new Mat();
List<Mat> matList = new LinkedList<>();
matList.add(src);
Imgproc.calcHist(matList, new MatOfInt(0), new Mat(), hist, new MatOfInt(256), histRange);
Core.normalize(hist, hist, 0, 400, Core.NORM_MINMAX);
//图像2的直方图数据统计并归一化
Mat hist2 = new Mat();
List<Mat> matList2 = new LinkedList<>();
matList2.add(src2);
Imgproc.calcHist(matList2, new MatOfInt(0), new Mat(), hist2, new MatOfInt(256), histRange);
Core.normalize(hist2, hist2, 0, 400, Core.NORM_MINMAX);
double s = Imgproc.compareHist(hist, hist2, Imgproc.HISTCMP_CORREL);
System.out.println("相似度:" + s);
System.exit(0);
}
}
java
相似度:0.9987133134938458
程序中用于比较两幅图像中一幅是未处理的原图像。另一幅是经过中值滤波后的图像。经过比较两者相似度约为0.9987。由于比较方法用的是相关性比较,完全一致时相似度为1,此结果显示两者相似度非常高。
当然,如前所述,直方图只统计数量而不考虑像素在图像中的位置,因而两幅截然不同的图像的直方图可能是一样的。直方图的比较结果完全匹配也并不能说明两幅图像是一样的,但如果两幅图像完全一样,则它们的直方图必然是完全匹配的。
四、直方图均衡化
在曝光不足或曝光过度时,直方图往往集中在一个区域,而解决问题的方法就是直方图均衡化。所谓直方图均衡化,就是尽可能地让一张图像的像素占据全部可能的灰度级并且分布均匀,从而具有较高的对比度。直方图均衡化的原理图如下:

java
//对图像进行直方图均衡化
void Imgproc.equalizeHist(Mat src, Mat dst)
- src:输入图像,必须是8位单通道图像
- dst:输出图像,和src具有相同的尺寸和数据类型
java
public class EqualizeHist {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像灰度图并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/grotto.jpg", Imgcodecs.IMREAD_GRAYSCALE);
HighGui.imshow("src", src);
HighGui.waitKey(0);
//直方图均衡化并在屏幕上显示结果
Mat dst = new Mat();
Imgproc.equalizeHist(src, dst);
HighGui.imshow("dst", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:

直方图均衡化:

五、自适应的直方图均衡化
直方图均衡对于背景和前景都太亮或太暗的图像很有效,但是在很多情况下其效果并不理想。直方图均衡化主要存在以下两个问题:
- 某些区域由于对比度增强过大而成为噪点
- 某些区域调整后变得更暗或更亮,从而丢失细节
针对上述两个问题,先后有人提出了对比度限制直方图均衡算法(CLHE算法)和自适应直方图均衡算法(AHE算法)。
CLHE算法在HE算法的基础上加入了对比度限制,算法中设置了一个直方图分布的阈值,将超过该阈值的部分均匀地分散到其他Bins中,其原理如下:
AHE算法则将图像分成很多小块,对每个小块进行直方图均衡化,然后将这些小块拼接起来,但是这样又产生了新的问题,由于对每个小块进行均衡化时的参数不同,小块之间会产生一些边界。
限制对比度自适应直方图均衡化(CLAHE算法)综合了这两个算法的优点,并通过双线性差值的方法对小块进行缝合以消除边界问题。严格的说,自适应的直方图均衡化算法是指AHE算法,而不是CLAHE算法。不过,为了简化起见,目前在提起 自适应的直方图均衡化算法 时所指的基本是CLAHE算法。
为了实现这个算法,OpenCV中专门设置了CLAHE类。CLAHE算法的实现一般需要如下两步:
1. 创建一个CLAHE类:
java
CLAHE clahe = Imgproc.createCLAHE();
2. 调用CLAHE.apply()函数进行自适应的直方图均衡化:
java
clahe.apply(src, dst);
java
public class Clahe {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像灰度图并显示
Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo2/grotto.jpg", Imgcodecs.IMREAD_GRAYSCALE);
HighGui.imshow("src", src);
HighGui.waitKey(0);
//直方图均衡化并显示结果
Mat dst = new Mat();
Imgproc.equalizeHist(src, dst);
HighGui.imshow("dst", dst);
HighGui.waitKey(0);
//自适应直方图均衡化并显示
CLAHE clahe = Imgproc.createCLAHE();
clahe.apply(src, dst);
HighGui.imshow("CLAHE", dst);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:
直方图均衡化:
自适应直方图均衡化:

总体来看,直方图均衡化后的图像黑白对比仍然强烈,佛像背后有一块黑色阴影,细节根本看不到,另外佛像下方底座部分仍然曝光过度。自适应的直方图均衡化后的图像总体对比度降低、因而细节展现更多。得益于分块均衡化的算法,佛像背后的阴影部分没有那么黑了,因而可以看到一些细节;另外佛像下方曝光过度问题也大有改善,但是左上角出现了明显的块状,这是原图中没有的,这就是CLAHE算法在分块均衡化后缝合效果不理想的表现,也可以说是这个算法的一个副作用。
六、直方图反向投影
直方图反向投影是指先计算某一特征的直方图模型,然后使用该模型去寻找图像中是否存在该特征。反向投影可用于检测输入图像在给定图像中最匹配的区域,因而常用于目标追踪的MeanShift算法配合使用。
java
//对图像直方图进行反向投影
void Imgproc.calcBackProject(List<Mat> images, MatOfInt channels, Mat hist, Mat dst, MatOfFloat ranges, double scale)
- images:输入的图像集,所有图像应具有相同的尺寸和数据类型,但通道数可以不同,图像深度应为CV_8U、CV_16U或CV_32F
- channels:需要统计的通道索引
- hist:输入的直方图
- dst:输出的反向投影图像
- ranges:直方图中bin的取值范围
- scale:输出的反向投影的缩放因子
java
public class BackProject {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像转换为HSV色彩空间并显示
Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo/leaf.png");
Mat hsv = new Mat();
Imgproc.cvtColor(src, hsv, Imgproc.COLOR_BGR2HSV);
HighGui.imshow("leaf", hsv);
HighGui.waitKey(0);
//将图像的hue(色调)通道提取至hueList中
Mat hue = new Mat(hsv.size(), hsv.depth());
List<Mat> hsvList = new LinkedList<>();
List<Mat> hueList = new LinkedList<>();
hsvList.add(hsv);
hueList.add(hue);
Core.mixChannels(hsvList, hueList, new MatOfInt(0, 0));
//直方图参数设置
int bins = 25;
int histSize = Math.max(bins, 2);
float[] hueRange = {0, 180};
//直方图数据统计并归一化
Mat hist = new Mat();
Imgproc.calcHist(hueList, new MatOfInt(0), new Mat(), hist, new MatOfInt(histSize), new MatOfFloat(hueRange));
Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX);
//计算反向投影并显示
Mat backproj = new Mat();
Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, backproj, new MatOfFloat(hueRange), 1);
HighGui.imshow("calcHist", backproj);
HighGui.waitKey(0);
System.exit(0);
}
}
原图HSV:
反向投影:
七、模板匹配
直方图反向投影可用于检测输入图像在给定图像中最匹配的区域,但是由于直方图的局限性,直方图反向投影得到的匹配结果只能作为参考,如果需要精确匹配,则还需要用到模板匹配。
模板匹配是指在一张图像中寻找与另一幅模板图像最佳匹配(相似)区域。所谓模板,就是用来对比的图像。模板匹配的具体方法是:在待匹配图像中选择与模板相同尺寸的滑动窗口,然后不断地移动滑动窗口,计算其与图像中相应区域的匹配度,最终匹配度最高的区域即为匹配结果。
java
//在图像中寻找与模板匹配的区域
void Imgproc.matchTemplate(Mat image, Mat temp1, Mat result, int method)
- image:待匹配图像,要求是8位或32位浮点图像
- temp1:模板图像,其数据类型与待匹配图像相同,并且尺寸不能大于待匹配图像
- result:输出的匹配图,必须是32位浮点图像。如果待匹配图像的尺寸为WH,模板图像尺寸为wh,则输出图像尺寸为(W-w+1)*(H-h+1)
- method:匹配方法,可选参数如下,相应计算公式如下表
- Imgproc.TM_SQDIFF:平方差匹配法。完全匹配时计算值为0,匹配度越低数值越大
- Imgproc.TM_SQDIFF_NORMED:归一化平方差匹配法。将平方差匹配法归一化到0~1
- Imgproc.TM_CCORR:相关匹配法。0为最差匹配,数值越大匹配效果越好
- Imgproc.TM_CCORR_NORMED:归一化相关匹配法。将相关匹配法归一化到0~1
- Imgproc.TM_CCOEFF:系数匹配法。数值越大匹配度越高,数值越小匹配度越低
- Imgproc.TM_CCOEFF_NORMED:归一化系数匹配法。将系数匹配法归一化到-1~1,1表示完全匹配,-1表示完全不匹配
由于matchTemplate()函数只是计算各个区域的匹配度,要得到最佳匹配还需要用minMaxLoc()函数来定位:
java
//寻找矩阵中的最大值和最小值及在矩阵中的位置
MinMaxLocResult Core.minMaxLoc(Mat src)
- src:输入矩阵,必须是单通道

java
public class MatchTemplate {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/fish.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
//读取模板图像并显示
Mat template = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/leaf.png");
HighGui.imshow("template", template);
HighGui.waitKey(0);
//进行模板匹配:结果在result中
Mat result = new Mat();
Imgproc.matchTemplate(src, template, result, Imgproc.TM_CCOEFF);
//取出最大值的位置(TM_CCOEFF模式用最大值)
Core.MinMaxLocResult mmr = Core.minMaxLoc(result);
Point pt = mmr.maxLoc;
//用矩形画出匹配位置并在屏幕上显示
Scalar red = new Scalar(0, 0, 255);
Imgproc.rectangle(src, pt, new Point(pt.x + template.cols(), pt.y + template.rows()), red, 3);
HighGui.imshow("match", src);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:

模板:
匹配结构:

上述程序只能找到最佳匹配,而有时匹配的图像不止一个,此时就要从matchTemplate()函数的结构中找出所有符合条件的图像。下面用一个完整的程序说明如何将模板匹配运用于多目标匹配:
java
public class MatchTemplate2 {
static {
OpenCV.loadLocally(); // 自动下载并加载本地库
}
public static void main(String[] args) {
//读取图像并显示
Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo2/rose.png");
HighGui.imshow("src", src);
HighGui.waitKey(0);
//读取模板图像并显示
Mat template = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo2/my.png");
HighGui.imshow("template", template);
HighGui.waitKey(0);
//进行模板匹配,匹配值范围为-1~1
Mat result = new Mat();
Imgproc.matchTemplate(src, template, result, Imgproc.TM_CCOEFF_NORMED);
System.out.println(result);
//参数准备
float[] p = new float[3];
Scalar red = new Scalar(0, 0, 255);
int temprow = template.rows();
int tempcol = template.cols();
//搜索匹配值>0.8的像素
for (int i = 0; i < result.rows(); i++) {
for (int j = 0; j < result.cols(); j++) {
//获取像素的匹配值
result.get(i, j, p);
//匹配值>0.8的像素用矩形画出
if (p[0] > 0.8) {
Imgproc.rectangle(src, new Point(j, i), new Point(j + tempcol, i + temprow), red, 3);
}
}
}
//显示匹配结果
HighGui.imshow("match", src);
HighGui.waitKey(0);
System.exit(0);
}
}
原图:

模板:

匹配结果:
