首先说在前面,接下俩要介绍的直方图应该是机器视觉里面经常见到的概念了。写很长的东西不求使你成为专家,只是让你对一个概念快速入门知识体系稍微系统一点而已。抛砖引玉,大家共同学习。
一.基本概念
说起直方图这个名词你就知道这是一个统计学概念啦.而图像的直方图是用来表现图像中亮度分布的直方图,给出的是图像中某个亮度或者某个范围亮度下共有几个像素.还不明白?就是统计一幅图某个亮度像素数量.比如对于灰度值12,一幅图里面有2000 个像素其灰度值为12,那么就能够统计12这个亮度的像素为2000个,其他类推.
然后再说几个基本的名词:
***bins:***bins一般翻译为箱子,看上图,一共有16个bins,其实就和我们平时见得简单函数差不多.在图像直方图中,你可以把一个灰度值设置为一个bins,0~255强度的灰度值一共就需要256个bins.是不是很简单
**Range:***range就是范围啦,规定一个bins能够达到的最大和最小的范围.比如一张图片10*10,那么就有100个像素.然后前面已经说过,直方图是按照亮度统计像素数量,那么范围就是0~100啦.这里有一个地方要说一下,刚刚0~100还是对于比较小的图像,那么对于比较大的图像1000*1000,那么范围太大了.我们统计像素数量的时候肯定没有问题,但是要画直方图的时候,难道有一个包含100000个像素,岂不是要化的很长?所以,一般在画直方图的时候,会有一个比例缩放的过程,比如我提前定好我直方图最大的高度只能够是256,那么你就可以用(最大的高度/最大的像素量)统计到的像素量来进行缩放.这样就简单多了.我这里提到的缩放方式只是一种,你可以随便定义喜欢的缩放方式.
基本的概念其实很简答,想的时候要不要想复杂了,那么基本概念就到这里了.
二.直方图计算和绘制
Ⅰ.基本函数
官方文档:http://docs.opencv.org/3.1.0/d6/dc7/group__imgproc__hist.html#ga4b2b5fd75503ff9e6844cc4dcdaed35d
直方图计算:calcHist()函数.
原型:
void cv::calcHist (const Mat * images,
int nimages,
const int * channels,
InputArray mask,
OutputArray hist,
int dims,
const int * histSize,
const float ** ranges,
bool uniform = true,
bool accumulate = false
)
参数:
images: 源图像,注意这里的格式是const Mat*,也就是说,你要传入一个地址,输入的数组(图片)或者数组集(一堆图片)需要为相同的深度和相同的尺寸。每个都能够用任意数量的通道。
Nimages:输入图像个数
Channels:List of the dims channels used to compute the histogram. The first array channels are numerated from 0 to images[0].channels()-1 , the second array channels are counted from images[0].channels() to images[0].channels() + images[1].channels()-1, and so on.
mask:可选的掩码,如果不为空的话,那么它必须是8位且和image[i]尺寸相同。
hist:输出的直方图,二维数组。
dims:需要统计的直方图维度(特征数目),必须是正数,且不大于CV_MAX_DIMS(这个版本opencv3.1里面是32)
histSize:存放每个维度的直方图尺寸的数组。
ranges:每一维数值的取值范围。Array of the dims arrays of the histogram bin boundaries in each dimension. When the histogram is uniform ( uniform =true), then for each dimension i it is enough to specify the lower (inclusive) boundary L0 of the 0-th histogram bin and the upper (exclusive) boundary UhistSize[i]−1 for the last histogram bin histSize[i]-1 . That is, in case of a uniform histogram each of ranges[i] is an array of 2 elements. When the histogram is not uniform ( uniform=false ), then each of ranges[i] contains histSize[i]+1 elements: L0,U0=L1,U1=L2,...,UhistSize[i]−2=LhistSize[i]−1,UhistSize[i]−1 . The array elements, that are not between L0 and UhistSize[i]−1 , are not counted in the histogram.
uniform:表示直方图是否均匀
accumulate:累计标识符,若为ture,直方图再配置阶段不会被清零。
寻找最值:minMaxLoc()函数
http://docs.opencv.org/3.1.0/d2/de8/group__core__array.html#gab473bf2eb6d14ff97e89b355dac20707
函数的作用就是再数组里面找到全局最大值和最小值。且这个函数对多通道的数组无效,因此多通道的要转化为单通道再来使用。
原型:
void cv::minMaxLoc (InputArray src,
double * minVal,
double * maxVal = 0,
Point * minLoc = 0,
Point * maxLoc = 0,
InputArray mask = noArray()
)
参数:
src:输入单通道数组(图像)
minVal:指向最大值的指针
maxVal:指向最小值的指针
minLoc:最小值位置的指针(二维下)
maxLoc:最大值位置的指针(二维下)
Mask:掩膜
直方图归一化:normalize()函数
原型:
void cv::normalize (InputArray src,
InputOutputArray dst,
double alpha = 1,
double beta = 0,
int norm_type = NORM_L2,
int dtype = -1,
InputArray mask = noArray()
)
参数:
src :输入数组
Dst:输出数组(支持原地运算)
Alpha:range normalization模式的最小值
Beta:range normalization模式的最大值,不用于norm normalization(范数归一化)模式。
normType:归一化的类型,可以有以下的取值:
NORM_MINMAX:数组的数值被平移或缩放到一个指定的范围,线性归一化,一般较常用。
NORM_INF: 此类型的定义没有查到,根据OpenCV 1的对应项,可能是归一化数组的C-范数(绝对值的最大值)
NORM_L1 : 归一化数组的L1-范数(绝对值的和)
NORM_L2: 归一化数组的(欧几里德)L2-范数
Dtype:dtype为负数时,输出数组的type与输入数组的type相同;否则,输出数组与输入数组只是通道数相同,而tpye=CV_MAT_DEPTH(dtype).
Mask:操作掩膜,用于指示函数是否仅仅对指定的元素进行操作。
Ⅱ.一维直方图
首先要知道的是,维度这个东西并不仅仅只代表一个颜色的通道什么什么。我么这里的一维直方图就以对灰度图作为例子,来画出灰度图的直方图。
下面的图片是我们的源图片(载入的时候会弄成灰度图)
那么废话就不多说了,直接上例子.后面会有这个例子的详细解释.
代码:
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
int main()
{
//首先肯定是读取图片,转换为灰度图并且在一个窗口上面显示
cv::Mat sourcePic = cv::imread("3.jpg", cv::IMREAD_GRAYSCALE);
cv::imshow("Source Picture", sourcePic);
//定义函数需要的一些变量
//图片数量nimages
int nimages = 1;
//通道数量,我们总是习惯用数组来表示,后面会讲原因
int channels[1] = { 0 };
//输出直方图
cv::Mat outputHist;
//维数
int dims = 1;
//存放每个维度直方图尺寸(bin数量)的数组histSize
int histSize[1] = { 256 };
//每一维数值的取值范围ranges
float hranges[2] = { 0, 255 };
//值范围的指针
const float *ranges[1] = { hranges };
//是否均匀
bool uni = true;
//是否累积
bool accum = false;
//计算图像的直方图
cv::calcHist(&sourcePic, nimages, channels, cv::Mat(), outputHist, dims, histSize, ranges, uni, accum);
//遍历每个箱子(bin)检验,这里的是在控制台输出的。
for (int i = 0; i < 256; i++)
std::cout << "bin/value:" << i << "=" << outputHist.at<float>(i) << std::endl;
//画出直方图
int scale = 1;
//直方图的图片
cv::Mat histPic(histSize[0] * scale, histSize[0], CV_8U, cv::Scalar(255));\
//找到最大值和最小值
double maxValue = 0;
double minValue = 0;
cv::minMaxLoc(outputHist, &minValue, &maxValue, NULL, NULL);
//测试
std::cout << minValue << std::endl;
std::cout << maxValue << std::endl;
//纵坐标缩放比例
double rate = (histSize[0] / maxValue)*0.9;
for (int i = 0; i < histSize[0]; i++)
{
//得到每个i和箱子的值
float value = outputHist.at<float>(i);
//画直线
cv::line(histPic, cv::Point(i*scale, histSize[0]), cv::Point(i*scale, histSize[0] - value*rate), cv::Scalar(0));
}
cv::imshow("histgram", histPic);
cv::waitKey(0);
}
结果:
Ⅲ.RGB直方图
RBG直方图主要是是针对RBG颜色空间的彩色图片,自然有了三个特征,那么维度便变为了3,依然是以上面那副图片作为例子,计算和画出三维的GBG直方图.
代码:
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
int main()
{
//首先肯定是彩色形式读取图片
cv::Mat sourcePic = cv::imread("3.jpg");
cv::imshow("Source Picture", sourcePic);
//定义函数需要的一些变量
//图片数量nimages
int nimages = 1;
//通道数量,我们总是习惯用数组来表示,后面会讲原因
int channels[3] = { 0,1,2 };
//输出直方图
cv::Mat outputHist_red, outputHist_green, outputHist_blue;
//维数
int dims = 1;
//存放每个维度直方图尺寸(bin数量)的数组histSize
int histSize[3] = { 256,256,256 };
//每一维数值的取值范围ranges
float hranges[2] = { 0, 255 };
//值范围的指针
const float *ranges[3] = { hranges,hranges,hranges};
//是否均匀
bool uni = true;
//是否累积
bool accum = false;
//计算图像的直方图(红色通道部分)
cv::calcHist(&sourcePic, nimages, &channels[0], cv::Mat(), outputHist_red, dims, &histSize[0], &ranges[0], uni, accum);
//计算图像的直方图(绿色通道部分)
cv::calcHist(&sourcePic, nimages, &channels[1], cv::Mat(), outputHist_green, dims, &histSize[1], &ranges[1], uni, accum);
//计算图像的直方图(蓝色通道部分)
cv::calcHist(&sourcePic, nimages, &channels[2], cv::Mat(), outputHist_blue, dims, &histSize[2], &ranges[2], uni, accum);
//遍历每个箱子(bin)检验,这里的是在控制台输出的。
//for (int i = 0; i < 256; i++)
//std::cout << "bin/value:" << i << "=" << outputHist_red.at<float>(i) << std::endl;
//画出直方图
int scale = 1;
//直方图的图片,因为尺寸是一样大的,所以就以histSize[0]来表示全部了.
cv::Mat histPic(histSize[0], histSize[0] * scale * 3, CV_8UC3, cv::Scalar(0, 0, 0));
//找到最大值和最小值,索引从0到2分别是红,绿,蓝
double maxValue[3] = { 0, 0, 0 };
double minValue[3] = { 0, 0, 0 };
cv::minMaxLoc(outputHist_red, &minValue[0], &maxValue[0], NULL, NULL);
cv::minMaxLoc(outputHist_green, &minValue[1], &maxValue[1], NULL, NULL);
cv::minMaxLoc(outputHist_blue, &minValue[2], &maxValue[2], NULL, NULL);
//测试
std::cout << minValue[0] << " " << minValue[1] << " " << minValue[2]<< std::endl;
std::cout << maxValue[0] << " " << maxValue[1] << " " << maxValue[2] << std::endl;
//纵坐标缩放比例
double rate_red = (histSize[0] / maxValue[0])*0.9;
double rate_green = (histSize[0] / maxValue[1])*0.9;
double rate_blue = (histSize[0] / maxValue[2])*0.9;
for (int i = 0; i < histSize[0]; i++)
{
float value_red= outputHist_red.at<float>(i);
float value_green = outputHist_green.at<float>(i);
float value_blue = outputHist_blue.at<float>(i);
//分别画出直线
cv::line(histPic, cv::Point(i*scale, histSize[0]), cv::Point(i*scale, histSize[0] - value_red*rate_red), cv::Scalar(0,0,255));
cv::line(histPic, cv::Point((i + 256)*scale, histSize[0]), cv::Point((i + 256)*scale, histSize[0] - value_green*rate_green), cv::Scalar(0, 255, 0));
cv::line(histPic, cv::Point((i + 512)*scale, histSize[0]), cv::Point((i + 512)*scale, histSize[0] - value_blue*rate_blue), cv::Scalar(255, 0, 0));
}
cv::imshow("histgram", histPic);
cv::waitKey(0);
}
结果:
三.反向投影原理与应用
Ⅰ.基本概念和函数
一般的原理是这么说的,如果一副图像的某个区域中显示的是一种结构纹理或者一个独特的物体,那么这个区域的直方图可以看做一个概率函数,表现形式是某个像素属于该纹理或者该物体的概率.
简单一点来说,反响投影一般用来在输入图像(一般来说比较大)中查找与特定图像/模板图象(一般比较小)最匹配的点或者区域。也就是说,给你一个模板图像,在输入图像中找到和模板图像最相近的图像。
这段话看上去非常非常的抽象.但是事实上,它的原理其实很简单,接下来通过opencv内置的函数和几个例子来理解上面那个原理的意思.
首先,来看看opencv中给出的的计算反向投影的函数(这个函数同样也是在imgproc模块里面):
void cv::calcBackProject (const Mat * images,
int nimages,
const int * channels,
InputArray hist,
OutputArray backProject,
const float ** ranges,
double scale = 1,
bool uniform = true
)
参数:
images: 源图像,注意这里的格式是const Mat*,也就是说,你要传入一个地址,输入的数组(图片)或者数组集(一堆图片)需要为相同的深度和相同的尺寸。每个都能够用任意数量的通道。
Nimages:输入图像个数
Channels:List of the dims channels used to compute the histogram. The first array channels are numerated from 0 to images[0].channels()-1 , the second array channels are counted from images[0].channels() to images[0].channels() + images[1].channels()-1, and so on.
hist:输入的直方图,二维数组。
backProject:输出的目标反向投影数组(阵列),必须为单通道,并且和image[0]有相同的大小和深度.
Ranges:每一个维度数组的每一维的边界阵列,可以理解为每一维数值的取值范围.
scale:反向投影的缩放因子
Uniform:表示直方图是否均匀
该函数的执行过程就是从归一化的直方图中读取概率值,并且把输入图像中的每个像素替换为与之对应的概率值.
函数的结果就是包含以每个输入图像像素点为起点的直方图对比结果,可以看作是一个单通道的浮点型图像。每个元素都是这个元素的概率.
Ⅱ.灰度图的反向投影检测特定内容
之所以用灰度图作为第一个例子,是因为灰度图只有一个通道,方便我们处理和了解一些基本的概念。示例图片还是之前的那幅图片。
待检测的内容是在最外面那条牛的上面,有一个框框(坐标为230,250,宽高都是30),如下图,我的设想是,以这个框框中划分的牛皮肤的颜色为模板,看能不能够通过反向投影找到其他颜色相近的牛的皮肤。
那么首先就把方框中的模板选择位感兴趣区域,并且计算他的直方图并且归一化,然后在反向投影,得到”概率分布”图像.
直接看例子
代码:
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
int main()
{
//灰度图读取
cv::Mat sourcePic = cv::imread("3.jpg", cv::IMREAD_GRAYSCALE);
//感兴趣区域
cv::Mat ROI = sourcePic(cv::Rect(230, 250, 30, 30));
//定义变量计算直方图
int channels[1] = { 0 };
cv::Mat outHist;
int histSize[1] = { 256 };
float hranges[2] = { 0, 255 };
const float *ranges[1] = { hranges };
cv::calcHist(&ROI, 1, channels, cv::Mat(), outHist, 1, histSize, ranges);
//画出直方图
int scale = 1;
//直方图的图片
cv::Mat histPic(histSize[0], histSize[0] * scale, CV_8U, cv::Scalar(255));
//找到最大值和最小值
double maxValue = 0;
double minValue = 0;
cv::minMaxLoc(outHist, &minValue, &maxValue, NULL, NULL);
//测试
std::cout << minValue << std::endl;
std::cout << maxValue << std::endl;
//纵坐标缩放比例
double rate = (histSize[0] / maxValue)*0.9;
for (int i = 0; i < histSize[0]; i++)
{
float value = outHist.at<float>(i);
cv::line(histPic, cv::Point(i*scale, histSize[0]), cv::Point(i*scale, histSize[0] - value*rate), cv::Scalar(0));
}
cv::imshow("histgram", histPic);
cv::rectangle(sourcePic, cv::Rect(230, 250, 30, 30), cv::Scalar(255));
cv::imshow("source picture", sourcePic);
cv::imshow("ROI", ROI);
//反向投影
//归一化直方图,
cv::normalize(outHist, outHist, 1.0);
cv::Mat result;
//反向投影函数
cv::calcBackProject(&sourcePic, 1, channels, outHist, result, ranges, 255.0);
cv::imshow("BackProjection", result);
cv::waitKey(0);
}
结果:
得到了原图对于一小块皮肤的”概率分布图”,其中颜色很浅的就是概率越大的地方,颜色深的就是概率很浅的地方.我们可以看到颜色浅的地方有类似于马的轮廓,那确实就是我们开始取皮肤的那匹马.但是同样也有很多其他的地方是浅色,难道这些地方也是马的皮肤?当然不是!这就说明,要是对于某些需要颜色来作为特征的图像,仅仅是灰度图来做反向投影是不够的,还需要有颜色的特征,这就引出了我们下面的部分.带有颜色特征的反向投影.
未完待续…..