In this chapter, we will cover:
- Computing the image histogram(计算图像的直方图)
- Applying look-up tables to modify image appearance(使用查找表修改图像外观)
- Equalizing the image histogram(直方图均衡化)
- Backprojecting a histogram to detect specific image content(反向投影直方图去检测特定的图像内容)
- Using the mean shift algorithm to find an object(使用均值漂移算法去寻找物体)
- Retrieving similar images using histogram comparison(比较直方图来检索相似图像)
Introduction
像素值在图像中的分布情况是一幅图像的重要特征,直方图可以描述图像内容,检测图像中特定的对象或纹理。这里分享一个知乎上关于google图片搜索原理的问题,其中有个答案就说了,计算图片的直方图,看直方图的相似情况。
二.Computing the image histogram
Definition
A Histogram is a simple table that gives the number of pixels that have a given value in an image(or sometime a set of images).The histogram of a gray-level image will therefore have 256 entries (or bins).Bin 0 gives the number of pixels having value 0, bin 1 the number of pixels having value 1, and so on.
计算Histogram
Computing a histogram with OpenCV can be easily done by using the cv::calcHist function. This is a general function which can compute the histogram of multiple channel images of any pixel value type. See more in cv::calcHist1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69//单通道图像的处理
#include "stdafx.h"
#include <opencv2/opencv.hpp>
class Histogram1D
{
public:
Histogram1D(){
nimages = 1;
channels[0] = 0;
dims = 1;
histSize[0] = 256;
hranges[0] = 0.0;
hranges[1] = 255.0;
ranges[0] = hranges;
}
cv::MatND GetHistogram(const cv::Mat &image);
cv::Mat GetHistogramImage(cv::Mat &image);
private:
int nimages;
int channels[1];
int dims;
int histSize[1];
const float *ranges[1]; //必须是const float**,不能是float**
float hranges[2];
};
cv::MatND Histogram1D::GetHistogram(const cv::Mat &image)
{
cv::MatND hist;
cv::calcHist(&image, nimages, channels, cv::Mat(), hist, dims, histSize, ranges);
return hist;
}
cv::Mat Histogram1D::GetHistogramImage(cv::Mat &image)
{
cv::MatND hist = GetHistogram(image);
double minVal = 0.0;
double maxVal = 0.0;
cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
//! constucts 2D matrix and fills it with the specified value _s(cv::Scalar).
cv::Mat showImg(histSize[0], histSize[0], CV_8U, cv::Scalar(255));
int hpt = static_cast<int>(histSize[0] * 0.9);
for (int i = 0; i < histSize[0]; ++i)
{
int intensity = static_cast<int>(hist.at<float>(i) * hpt / maxVal);
cv::line(showImg,
cv::Point(i, histSize[0]), cv::Point(i, histSize[0] - intensity), //之所以 histSize[0] - intensity,是为了符合人的观看习惯
cv::Scalar::all(0)); //这里为何加all?不加效果也是一样的
}
return showImg;
}
int main()
{
cv::Mat image = cv::imread("../../resource/test1.jpg");
Histogram1D h;
cv::Mat showImg = h.GetHistogramImage(image);
cv::imshow("ds", showImg);
cv::waitKey();
return 0;
}
1 | //三通道 |
解释一下这里为何没有画出直方图,因为这是三通道的,本例是在RGB颜色空间下,那意味着有256^3种可能性,而且这是三维,那么要想表示出这个直方图,你得在四维空间下才能表示出来,所以这根本就画不出来,对于一个单通道的,也就256种可能性,只有一维,在二维空间下就能表示,所以还是可以画完的,有兴趣可以将三通道图分离出三个三通道,然后分别绘制出各个单通道的直方图。可以看看这个例子
把刚才的话说的在形象一点,在灰度图中,只有一个强度值,那么这是一维图像(不知道有没有这概念,打比方),那我统计每个强度值的像素有多少个,是不是还需要一个维度来表示,映射到二维坐标系中,好比x轴是强度值,y轴是像素个数。如果是提取(Hue-Saturation)的直方图,Hue和Saturation 各占一维,我们还需要一维来表示在Hue和Saturation确定的情况下这样的像素有多少个,所以需要三维空间来表示。比如:x表示Hue,y表示Saturation,z表示像素个数。opencv中的画法我没理解错的话,采用的是二维图像来表示,就是一张图像,x,y对应我刚才说的,用图像各个位置的强度值来表示像素个数,比如强度值255代表多少,200代表多少,这样还是可以看出来的。OpenCV里有这样的demo,这里有图这也是为啥如果画三通道的直方图画不出来的原因,没有多余的维度来表示像素个数了,但是这仅仅限制其不能画出来,我们还是可以用数学来描述的。
对比单通道代码,可以看出在cv::calcHist处channels,histSize,ranges跟以前的区别就是元素个数变成了3。
这里我写的时候,遇到一个bug,这个bug的知识范畴属于C++,就是cv::calcHist函数中ranges的类型是const float **,如果你在私有变量中声明的ranges成员的类型为float **(在本例中是float *ranges[3]),那么就会报错,无法转换。这里有解释.不要以为float*可以转换成const float*就以为这个也可以,下午搞了半天才知道这里有问题,C++基础不牢啊。
Applying look-up tables to modify image appearance
Definition
A look-up table is a simple one-to-one (or many-to-one) function that defines how pixel values are transformed into new values.1
映射函数:newIntensity= lookup[oldIntensity];
How to do it
1 | cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup) // lookup is 1*256 uchar matrix |
1 | int main() |
上面已经说了lookup的映射关系(映射函数newIntensity= lookup[oldIntensity];),所以对应到这个例子的意思就是newIntensity[i]= lookup[image[i]];,如果image某点像素值为255,那么及你经过lookup表映射后,新的图像result对应该位置像素值就是255-i.因为这里写的规则是255-i = lookup[i].但是多数情况下,图像在视觉上的缺陷并非源于使用过窄的强度范围,而是因为某些颜色值出现的频率过高。我们可以认为一幅高质量图像应该平均使用所有的像素强度。
前面说过,lookup不仅是一对一,也可以是多对一,也就是说你输入一个三通道图像进来,他会把你各个通道分别按照这个规则进行映射,得到新的三通道图像。补充一下,opencv官网上的说明:1
2
3
4dst(I) <-- lut(stc(I) + d)
where
d = 0 if src has depth CV_8U
d = 128 if src has depth CV_8S
但是之前写的Histogram1D类中,即使你输入的image是三通道的,他也不会报错,不过因为通道等参数只设定了一个,所以他只会这个彩色图像的第一个通道的直方图。(对于RGB颜色空间来说,取得第一个通道是B通道)
在书上下面还写了一大段代码,相当于将原图像像素值范围从50~200映射到新图像0~255,拉伸了直方图,使用的规则如下:1
255.0*(i-imin)/(imax-imin)+0.5); //我觉得这里的0.5并没有什么作用,谁知道求告知
Equalizing the image histogram
In the previous recipe, we showed how the contrast of an image(图像对比度) can be improved by stretching a histogram so that it occupies the full range of available intensity values.This is the idea behind the concept of histogram equalization(直方图均衡化).
How to do it
opencv 提供 直方图均衡化函数: cv::equalizeHist. See more in cv::equalizeHist1
2
3
4
5
6cv::Mat equalize(const cv::Mat &image)
{
cv::Mat result;
cv::equalizeHist(image,result);
return result; //histogram equalization后的图像
}
这里我参考计算机视觉–算法与应用这本书和这篇博客,用OpenCV实现了一下直方图均衡化,效果跟cv::equalizeHist几乎一样,但是速度肯定不如他,因为查看OpenCV源码的时候,他在实现的时候应该使用了并行技术,因为看他判断原图像个数,原图像个数超过640*480,就用了这个函数parallel_for_.代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29void Histogram1D::equalizeHist(cv::Mat src, cv::Mat &dst)
{
cv::MatND hist = GetHistogram(src);
float sum = 0.0;
for (int i = 0; i < 256; ++i)
{
sum += hist.at<float>(i);
}
cv::Mat C(1, 256, CV_32F); //cumulative distribution function using Mat c to storage it
float *cData = C.ptr<float>(0);
*cData++ = hist.at<float>(0) / sum; //compute cumulative distribution function
for (int i = 1; i < 256; ++i)
{
*cData++ = hist.at<float>(i) / sum + *(cData - 1);
}
cv::Mat lut(1, 256, CV_8U); //create lookup table
cData = C.ptr<float>(0);
uchar *lutData = lut.data;
for (int i = 0; i < 256; ++i) //map
{
*lutData++ = static_cast<int>(255.0 * (*cData++));
}
cv::LUT(src, lut, dst);
}
思路是这样的:先计算直方图,根据直方图计算累计分布函数,这样就得到了像素的强度值(intensity)与累计分布函数的对应关系,比如intensity为128对应的就是intensity小于128的所有像素值占像素个数总数的比值,我将这个关系存储在矩阵C中。
接下来就是建立lookup table了,映射后函数值为255乘以累计分布函数,比如以intensity为128来举例,记intensity小于128的像素个数所占比例为c,那像素intensity为128映射后其intensity就变成255*c。
在计算机视觉算法与应用一书3.1.4节图3.7可以看出来累计分布函数相对直方图更加平坦。还有一种局部自适应直方图均衡化在这书上也有介绍。
这里说个bug,不知道算不算bug,反正我实际测试的时候有问题,cv::Mat::total函数可以返回图像的像素个数,OpenCV在实现的时候就是调用这个函数来获取像素个数总数,但是我在调用的时候发现我累计的sum和cv::Mat::total函数得到的值不一样,后者得到的值大一点,这就导致我刚开始得到的直方图相对于OpenCV整体缩小了一个比例,直观一点就是变矮了,后来用hist一个个累计出来得到的sum没有这问题,计算出来的结果基本和OpenCV一致。不知道为啥?有人知道请告诉我
其实上面的思想跟书上提到的差不多,1
lookup.at<uchar>(i)= static_cast<uchar>(255.0*p[i]);
但是书上说:where p[i] is the number of pixels having an intensity lower than or equal to i。我把p[i]理解成所占比例,不是number,而是percentage。当然书上也说了是累计直方图。有兴趣可以看看这个例子
Backprojecting a histogram to detect specific image content
如果一幅图像的区域显示的是一种独特的纹理,那么这个区域的直方图可以看成一个概率函数,他给出的是某个像素属于该纹理或物体的概率。假设直方图最大高度为1,那假如有256个bin,每一个bin value值可能大小不一,那既然在0~1范围内,我们就可以把它看作一个概率,这样我们就知道了哪一个bin的概率最大,哪一个最小,这样映射函数就可以用255*bin value来表示,我们就可以简单的认为,越亮的就是我们越与我们设置的ROI相似(很可能就是ROI一样的区域),越暗的就是我们不感兴趣的,因为他的概率小导致他映射出来的像素值低,所以暗。1
2
3
4
5
6
7
8
9
10
11void Histogram1D::calcBackProject(cv::Mat src, cv::Mat &dst)
{
cv::Mat ROI = src(cv::Rect(110, 150, 30, 15));
// imshow("ROI", ROI);
cv::MatND hist = GetHistogram(ROI); //计算直方图
cv::normalize(hist, hist, 1.0); //归一化
cv::calcBackProject(&src, 1, channels, hist, dst, ranges, 255.0); //反向投影
}
注意我这样设计不好,应该将ROI设置的选择权给用户,而不是自己就在函数里写死了,我只是举例。
那到底什么是反向投影?我觉得OpenCV官方这个tutorial比较不错,Here
What is Back Projection?
- Back Projection is a way of recording how well the pixels of a given image fit the distribution of pixels in a histogram model.
- To make it simpler: For Back Projection, you calculate the histogram model of a feature and then use it to find this feature in an image.
- Application example: If you have a histogram of flesh color (say, a Hue-Saturation histogram ), then you can use it to find flesh color areas in an image
How does it work?
这里上面的链接里面说的很清楚了:
- (1)对于测试图像的每一个像素(即P(i,j)),收集数据看他在直方图(model histogram)哪个bin下面,即找到在直方图里的位置.
- (2)找到在直方图啥位置了,那么这个位置的值(bin value)肯定能知道了。
- (3)然后在新图像中存储对应像素位置(P(i,j))存储这个bin value,就是让这位置像素值为bin value,这里也说了如果你想看到这个反向投影后的新图像(对应到代码就是
dst),那么我们需要在计算BackProjection前,将直方图(hist)先归一化。 - (4)然后重复上述步骤,把测试图像中每一个像素都这样操作一遍(相当于前面讲的遍历像素)
可能你会说原图像的作用是干嘛的?他的作用是提供直方图,也就是(1)里说的model histogram。
在OpenCV官方提供的这个demo里,我们可以看到原图像的直方图比较纯,就是手占得比重比较大,所以直方图基本集中在手势那一块,但是test图像手占比重比较小,所以最亮的那一块内就不是手了。
那BackProjection就是:给你一个原图像,让你找出在test图像中和原图像相似的一部分出来。这是我的理解,如有错误,欢迎指正 !
那这样我之前的那段代码(Histogram1D::calcBackProject)也就不难理解了,先取ROI,看看图像中有多少和ROI相似的,给你提取出来,用的方法就是上面说的四个步骤。
mixChannels
OpenCV的demo里用了这个函数,我觉得以后用着也挺方便的,所以在这里提一下。See more in cv::mixChannels顺手把示例拷贝过来看一下1
2
3
4
5
6
7
8
9
10
11Mat rgba( 100, 100, CV_8UC4, Scalar(1,2,3,4) );
Mat bgr( rgba.rows, rgba.cols, CV_8UC3 );
Mat alpha( rgba.rows, rgba.cols, CV_8UC1 );
// forming an array of matrices is a quite efficient operation,
// because the matrix data is not copied, only the headers
Mat out[] = { bgr, alpha };
// rgba[0] -> bgr[2], rgba[1] -> bgr[1],
// rgba[2] -> bgr[0], rgba[3] -> alpha[0]
int from_to[] = { 0,2, 1,1, 2,0, 3,3 };
mixChannels( &rgba, 1, out, 2, from_to, 4 );
In the example, the code splits a 4-channel RGBA image into a 3-channel BGR (with R and B channels swapped) and a separate alpha-channel image.
Using the mean shift algorithm to find an object
反向投影直方图的结果是一个概率映射(可以看成越亮,概率越大,越暗,概率越小),体现了已知的图像内容出现在图像中特定位置的概率。假设我们现在知道物体的近似位置,概率映射可用于找到对象的确切位置(这里我的理解是进行与操作,与之后还在的就是确切位置了)。最有可能的位置是在已知窗口区域中得到最大概率的位置。因此,如果我们从最初的位置开始,并且迭代移动,便可找出精确位置。这便是均值漂移(Mean Shift)算法所要完成的任务。先直接放代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100class MeanShift
{
public:
MeanShift(int minSaturation, cv::Rect rectangle) : minSat(minSaturation), rect(rectangle){
hranges[0] = 0.0;
hranges[1] = 180.0;
ranges[0] = hranges;
channels[0] = 0;
histSize[0] = 181;
}
cv::MatND getHist(cv::Mat src);
cv::Mat meanShift(cv::MatND hist, cv::Mat src2);
private:
float hranges[2];
const float *ranges[1];
int channels[1];
cv::MatND hist;
int histSize[1];
int minSat;
cv::Rect rect;
};
cv::MatND MeanShift::getHist(cv::Mat src)
{
cv::Mat hsv;
cv::cvtColor(src, hsv, CV_BGR2HSV);
cv::Mat mask; //设置掩模,至于这里为啥设置掩模,我也不确定,总之目的是为了视提取出来的直方图更能表现出ROI
if (minSat > 0)
{
std::vector<cv::Mat> v;
cv::split(hsv, v);
cv::threshold(v[1], mask, minSat, 255, CV_THRESH_BINARY);
}
cv::calcHist(&hsv, 1, channels, mask, hist, 1, histSize, ranges);
cv::normalize(hist, hist, 1.0); //归一化
return hist;
}
cv::Mat MeanShift::meanShift(cv::MatND hist, cv::Mat src2)
{
cv::Mat hsv;
cv::Mat src2copy = src2.clone();
cv::cvtColor(src2copy, hsv, CV_BGR2HSV);
cv::Mat mask;
if (minSat > 0)
{
std::vector<cv::Mat> v;
cv::split(src2copy, v);
cv::threshold(v[1], mask, minSat, 255, CV_THRESH_BINARY);
}
cv::Mat backpoj;
cv::calcBackProject(&hsv, 1, channels, hist, backpoj, ranges, 255); //反向投影
// cv::threshold(backpoj, backpoj, 255 * 0.5, 255, CV_THRESH_BINARY);
cv::bitwise_and(backpoj, mask, backpoj); //
imshow("backpoj", backpoj);
cv::rectangle(src2copy, rect, cv::Scalar(0, 0, 255));
cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER, 10, 0.01);
int countNum = cv::meanShift(backpoj, rect, criteria);
printf("%d", countNum);
cv::rectangle(src2copy, rect, cv::Scalar(0, 255, 0));
return src2copy;
}
int main()
{
cv::Mat img1 = cv::imread("../../resource/baboon1.jpg");
cv::Mat img2 = cv::imread("../../resource/baboon3.jpg");
cv::Rect rect(110, 260, 35, 40); //定义初始位置
cv::Mat img1ROI = img1(rect); //设置ROI
int minSat = 65;
MeanShift ms(minSat, rect);
cv::MatND hist = ms.getHist(img1ROI); //提取ROI的直方图
cv::Mat result = ms.meanShift(hist, img2); //运用meanshift算法
cv::rectangle(img1, rect, cv::Scalar(0, 0, 255)); //画出第一张图像ROI的位置
cv::namedWindow("img1");
cv::namedWindow("img2");
cv::imshow("img1", img1);
cv::imshow("img2", img2);
cv::namedWindow("result");
cv::imshow("result", result);
cv::waitKey();
system("pause");
return 0;
}
代码写的比较烂,按照书上说的思路写的,得到的结果就如书上所示,这里就不贴图了。先计算直方图,根据直方图在计算在当前图像上的反向投影,根据方向投影的结果利用meanShift算法一步步去迭代找到局部最优。
引用书上的话:均值漂移算法以迭代的方式锁定概率函数的局部最大值。它的原理是寻找预定义窗口中数据点的重心,或者说是加权平均值。该算法将窗口中心移动到数据点的重心处,并重复这个过程直到窗口重心收敛到一个稳定点。OpenCV文档定义了两种终止条件:迭代的最大次数以及窗口中心的位移值(低于该值即认为算法以及收敛)。这两个条件保存在cv::TermCriteria实例中。cv::meanShift函数返回本次调用中迭代的次数。显而易见,结果的质量取决于输入的概率图及初始位置。
此外,更多meanshift算法理解可以参考文章,讲的还算比较浅显易懂。
Retrieving similar images using histogram comparison
Histogram Comparison,参考这个链接写的,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59class RetrieveImage
{
public:
RetrieveImage(cv::Mat image) : src(image)
{
src_hist = getHist(src);
}
cv::MatND getHist(const cv::Mat &image);
double compare(const cv::Mat &image, int compareMethod);
private:
cv::Mat src;
cv::MatND src_hist;
};
cv::MatND RetrieveImage::getHist(const cv::Mat &image)
{
cv::Mat hsv;
cv::Mat imageCopy = image.clone();
cv::cvtColor(imageCopy, hsv, CV_BGR2HSV);
float h_ranges[2] = { 0.0, 180.0 };
float s_ranges[2] = { 0.0, 200.0 };
int channels[2] = { 0, 1 };
int h_bins = 30, s_bins = 50;
int histSize[2] = { h_bins, s_bins};
const float *ranges[2] = { h_ranges, s_ranges };
cv::MatND hist;
cv::calcHist(&hsv, 1, channels, cv::Mat(), hist, 2, histSize, ranges);
normalize(hist, hist, 0, 1, cv::NORM_MINMAX);
return hist;
}
double RetrieveImage::compare(const cv::Mat &image, int compareMethod)
{
cv::MatND hist = getHist(image);
return cv::compareHist(src_hist, hist, compareMethod);
}
int main()
{
cv::Mat img1 = cv::imread("../../resource/church01.jpg");
cv::Mat img2 = cv::imread("../../resource/church02.jpg");
cv::Mat img3 = cv::imread("../../resource/church03.jpg");
cv::Mat img4 = cv::imread("../../resource/building.jpg");
RetrieveImage ri(img1);
for (int i = 0; i < 4; ++i)
{
double img1_img1 = ri.compare(img1, i);
double img1_img2 = ri.compare(img2, i);
double img1_img3 = ri.compare(img3, i);
double img1_img4 = ri.compare(img4, i);
printf("Method [%d] compare result: %f, %f, %f, %f \n", i, img1_img1, img1_img2, img1_img3, img1_img4);
}
system("pause");
return 0;
}
#endif
上面的图片是从本书的网站上下载的图片,前三张图片比较相似,最后一张图片是非常不相似,所以放结果如下:
| Method | img1_img1 | img1_img2 | img1_img3 | img1_img4 |
|---|---|---|---|---|
| Correlation | 1 | 0.867352 | 0.857962 | 0.0196779 |
| Chi-square | 0 | 3.06412 | 11.6898 | 1287.79 |
| Intersection | 13.9362 | 10.585 | 10.2582 | 3.03085 |
| Bhattacharyya | 0 | 0.22271 | 0.242008 | 0.759152 |
For the Correlation and Intersection methods, the higher the metric, the more accurate the match. As we can see, the match img1_img1 is the highest of all as expected.For the other two metrics(Chi-square and Bhattacharyya), the less the result, the better the match.
see more in cv::compareHist
END