opencv6.4-imgproc图像处理模块之直方图与模板

opencv6.3-imgproc图像处理模块之边缘检测

九、直方图的相关操作

直方图是图像中像素强度分布的图形表达方式;它统计了每一个强度值所具有的像素个数

opencv6.4-imgproc图像处理模块之直方图与模板

上图是一个灰色图像,通过对图像的每个不同值进行统计个数,得到了右边的直方图,这是图像操作中算是最简单的了,因为最简单,泛化很好,但是效果也只能呵呵了。不过简单的如果两幅图的对比强烈,那么采用直方图对比分类也算是最简单的了。

1、均衡化

直方图均衡化是通过拉伸像素强度分布范围来增强图像对比度的一种方法。说得更清楚一些, 以上面的直方图为例, 你可以看到像素主要集中在中间的一些强度值上.
直方图均衡化要做的就是 拉伸 这个范围. 见下面左图: 绿圈圈出了 少有像素分布其上的 强度值.
对其应用均衡化后, 得到了中间图所示的直方图. 均衡化的图像见下面右图:

opencv6.4-imgproc图像处理模块之直方图与模板

均衡化指的是把一个分布 (给定的直方图) 映射 到另一个分布
(一个更宽更统一的强度值分布), 所以强度值分布会在整个范围内展开.

要想实现均衡化的效果, 映射函数应该是一个 累积分布函数 (cdf) (更多细节, 参考*学习OpenCV*).
对于直方图H(i), 它的 累积分布H'(i)是:

opencv6.4-imgproc图像处理模块之直方图与模板

要使用其作为映射函数, 我们必须对最大值为255 (或者用图像的最大强度值) 的累积分布 H'(i)进行归一化.
同上例, 累积分布函数为:

opencv6.4-imgproc图像处理模块之直方图与模板

最后, 我们使用一个简单的映射过程来获得均衡化后像素的强度值:

opencv6.4-imgproc图像处理模块之直方图与模板

 /// 转为灰度图
cvtColor( src, src, CV_BGR2GRAY ); /// 应用直方图均衡化
equalizeHist( src, dst );

先转换成灰度图然后在进行直方图均衡化

函数原型:void equalizeHist(InputArray src, OutputArray
dst);

参数列表:输入图像、输出图像

第一个参数:单通道8-bit图像

第二个参数:与原图像有着一样的尺寸和类型。

内部操作过程:1、计算原图像的直方图;2、归一化这个直方图,使得直方图条状图的和为255;3、计算积分,也就是上面说的累积分布函数;4、使用H'来作为一个查找表来转换这个图像,即opencv6.4-imgproc图像处理模块之直方图与模板

note:该函数是归一化图像的亮度和增加对比度。

2、计算

这里介绍如何计算上面的直方图:

直方图是对数据的集合 统计 ,并将统计结果分布于一系列预定义的 bins 中。

这里的 数据 不仅仅指的是灰度值
(如上面说的), 统计数据可能是任何能有效描述图像的特征。

先看一个例子吧。 假设有一个矩阵包含一张图像的信息 (灰度值0-255):

opencv6.4-imgproc图像处理模块之直方图与模板

如果我们按照某种方式去 统计 这些数字,会发生什么情况呢?
既然已知数字的 范围 包含 256 个值, 我们可以将这个范围分割成子区域(称作bins),
如:

opencv6.4-imgproc图像处理模块之直方图与模板

然后再统计掉入每一个 bin 的像素数目。采用这一方法来统计上面的数字矩阵,我们可以得到下图(
x轴表示 第几个bin, y轴表示各个bin中的像素个数)

opencv6.4-imgproc图像处理模块之直方图与模板

以上只是一个说明直方图如何工作以及它的用处的简单示例。直方图可以统计的不仅仅是颜色灰度, 它可以统计任何图像特征 (如 梯度, 方向等等)。

让我们再来搞清楚直方图的一些具体细节:

     dims: 需要统计的特征的数目, 在上例中, dims
= 1
 因为我们仅仅统计了灰度值(灰度图像)。

     bins: 每个特征空间 子区段 的数目,在上例中, bins
= 16

     range: 每个特征空间的取值范围,在上例中, range
= [0,255]

怎样去统计两个特征呢? 在这种情况下, 直方图就是3维的了,x轴和y轴分别代表一个特征, z轴是掉入opencv6.4-imgproc图像处理模块之直方图与模板 组合中的样本数目。
同样的方法适用于更高维的情形 (当然会变得很复杂)。OpenCV提供了一个简单的计算数组集(通常是图像或分割后的通道)的直方图函数 calcHist 。
支持高达 32 维的直方图。

/// 分割成3个单通道图像 ( R, G 和 B )
vector<Mat> rgb_planes;
split( src, rgb_planes ); /// 设定bin数目
int histSize = 255;//这里是一个bin占了一个像素 /// 设定取值范围 ( R,G,B) )
float range[] = { 0, 255 } ;//这是RGB模型,不是HSV等模型。
const float* histRange = { range };//范围数组 bool uniform = true; bool accumulate = false;//把bin范围设定成同样大小(均一);以及开始统计前先清除直方图中的痕迹 Mat r_hist, g_hist, b_hist;//创建储存R、G、B三个通道的直方图矩阵 /// 计算直方图:
calcHist( &rgb_planes[0], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &rgb_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &rgb_planes[2], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate ); // 创建直方图画布
int hist_w = 400; int hist_h = 400;
int bin_w = cvRound( (double) hist_w/histSize ); Mat histImage( hist_w, hist_h, CV_8UC3, Scalar( 0,0,0) );//显示不同直方图的画布 /// 将直方图归一化到范围 [ 0, histImage.rows ],用 normalize 归一化直方图,这样直方图bin中的值就被缩放到指定范围
//归一化的原因在于之前的calcHist是归一化每个bin的范围,而这里是为了让两幅直方图图能够在同一个画布上显示
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); /// 在直方图画布上画出直方图
用了以下表达式:
r_hist.at<float>(i):`i` 指示维度,假如我们要访问2维直方图,我们就要用到这样的表达式: r_hist.at<float>( i, j )
 for( int i = 1; i < histSize; i++ )   {
line( histImage, Point( bin_w*(i-1), hist_h - cvRound(r_hist.at<float>(i-1)) ) ,//上一个末端作为当前的起始端
Point( bin_w*(i), hist_h - cvRound(r_hist.at<float>(i)) ),//当前所需要的末端,作为下一个的起始端
Scalar( 0, 0, 255), 2, 8, 0 );//颜色及线条的粗度等信息
line( histImage, Point( bin_w*(i-1), hist_h - cvRound(g_hist.at<float>(i-1)) ) ,
Point( bin_w*(i), hist_h - cvRound(g_hist.at<float>(i)) ),
Scalar( 0, 255, 0), 2, 8, 0 );
line( histImage, Point( bin_w*(i-1), hist_h - cvRound(b_hist.at<float>(i-1)) ) ,
Point( bin_w*(i), hist_h - cvRound(b_hist.at<float>(i)) ),
Scalar( 255, 0, 0), 2, 8, 0 ); } /// 显示直方图
namedWindow("calcHist Demo", CV_WINDOW_AUTOSIZE );
imshow("calcHist Demo", histImage );

上面的代码是实现进行一张RGB的图像的直方图计算,所以1、首先是使用split()分成不同的通道;2、使用calcHist()函数来计算直方图;3、设定好显示直方图画布的宽和高;4、归一化每个直方图;5、使用line()函数来画线。

直方图计算的函数原型:void 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 );

参数列表:输入图像、图像的数量、通道、可选的掩码、输出直方图、维数、直方图尺寸、范围、统一标识、累加标识;

第一个参数:输入的图像数组,所以才是指针类型,其中每个元素都具有相同的深度CV_8U或者CV_32F,同时它们也是相同的尺寸,不过每       个元素可以有任意的通道数;例如Mat array[] = {mat1,mat2,mat3};等等

第二个参数:输入图像的个数;(个人:即第一个数组中几个mat,即几张图像)

第三个参数:用于计算直方图的维数通道列表,也是指针类型。第一个数组通道是从0 到images[0].channels()-1,第二个数组通道是从         images[0].channels()到images[0].channels()+images[1].channels()-1以此类推。(个人:可以看出与第一个参数对比,这里说的是每个图像的通道数的排序,如果输入是2副图像,第一副图像有0,1,2共三个channel,第二幅图像只有0一个channel,那么输入就一共有4个channes,如果int
channels[3] = {3, 2, 0},那么就表示是使用第二副图像的第一个通道和第一副图像的第2和第0个通道来计算直方图)

第四个参数:可选的掩码,如果矩阵不是空的,那么必须是一个与images[i]有着同样的尺寸,而且是8-bit的数组。非0掩码元素在直方图中会与数组元素一起计算(一般是1,所以如果显示这个矩阵,会发现和纯黑一样,可以通过阈值的方式放大不同的部分,而且这里是InputArray类型,虽不能用它来声明一个变量,但是通过对他传递Mat,std::vector<>,Matx<>,Vec<>,或者Scalar都是可以的,它是“代理”类,所以这里可以是Vector<Mat>类型传递给它)。

第五个参数:输出直方图,可以是密集或者是稀疏 dims-维度的数组。需要注意这里可以使用Mat类型,然后如果第六个参数是大于2的,那么这里就能发现这个矩阵也是dim维度的。

第六个参数:直方图的维度必须是正的而且不大于CV_MAX_DIMS(在3.0和之前的版本中,这个值都是32).(这里指输出的直方图上bin的维数,下面的附带代码中维度是2,说明同时考虑h和s,即统计的时候是按照(bin_h1,bin_s1)这个块中多少个像素点落在这个上面这时候Mat的行和列表是bin的不同维度轴上的标识,里面的元素表是落在这个上面的个数,这样的以此类推)。

第七个参数:在每个维度上直方图尺寸的数组;即每个维度的bin个数。

第八个参数:在每个维度上直方图bin边界的dims数组的数组(每个维度的取值范围)。当直方图归一化了(uniform = true),那么对于第 i 个维度指明第0 个直方图bin的下限(包括)L_0和最后一个直方图bin_histSIze[i]-1的上限(不包含)U_histSize[i]-1。也就是说在,ranges[i]的每个归一化直方图中都是一个2元素的数组(如下面附带代码的hranges和sranges中的2个元素,归一化会直接让每个bin的大小相同,也就是均分)。当直方图没有归一化(uniform=
false)ranges[i]的每一个直方图都包含histSize[i]+1个元素: opencv6.4-imgproc图像处理模块之直方图与模板。(例如第2维上设置了5个bin,那么需要设定6个值,假设自己设定为{1,3,5,7,9,11},那么bin的大小就为【1,3】,【3,5】,【5,7】,【7,9】,【9,11】完全自己指定)其中不在L_0和U_histSize[i]-1之间的元素都不会计算在直方图中。

第九个参数:标识,用来指示直方图是否需要归一化;即bin大小相同。是归一化,那么就第八个参数每个维度上只需要设定上限和下限就行,会自动通过第七个参数每个维度上bin的个数来均分的。

第十个参数:累加标识。如果为true的话,直方图不清除在开始时候的分配。这个功能能够让你从几个数组集合中计算一个单一的直方图,或者及时的更新直方图。

这个函数calcHist计算一个或多个数组的直方图。一个元组的元素用来增加在同一个位置上对应于输入数组得到的直方图bin。

为了加深理解,下面是另一个对该函数的使用代码:

cvtColor(src, hsv, COLOR_BGR2HSV);

int hbins = 30, sbins = 32;// Quantize the hue to 30 levels and the saturation to 32 levels
int histSize[] = {hbins, sbins};
float hranges[] = { 0, 180 };// hue varies from 0 to 179, see cvtColor
float sranges[] = { 0, 256 };
const float* ranges[] = { hranges, sranges };
MatND hist;//typedef Mat MatND;同一个东西罢了
int channels[] = {0, 1};
// we compute the histogram from the 0-th and 1-st channels
calcHist( &hsv, 1, channels, Mat(), // do not use mask
                 hist, 2, histSize, ranges,
                 true, // the histogram is uniform
                 false );
double maxVal=0;
minMaxLoc(hist, 0, &maxVal, 0, 0);//留待以后介绍????
int scale = 10;
Mat histImg = Mat::zeros(sbins*scale, hbins*10, CV_8UC3);
for( int h = 0; h < hbins; h++ )
    for( int s = 0; s < sbins; s++ )
{
            float binVal = hist.at<float>(h, s);
                int intensity = cvRound(binVal*255/maxVal);
        rectangle( histImg, Point(h*scale, s*scale),
                            Point( (h+1)*scale - 1, (s+1)*scale - 1),
                            Scalar::all(intensity),
                            CV_FILLED );
}
namedWindow( "Source", 1 );
imshow( "Source", src );
namedWindow( "H-S Histogram", 1 );
imshow( "H-S Histogram", histImg );

归一化的函数原型:void normalize(InputArray src, InputOutputArray dst, double alpha=1, double beta=0, int norm_type  = NORM_L2, int dtype=-1, InputArray mask=noArray() );

参数列表:输入数组、输出数组、alpha值、beta值、归一化类型、目标图像深度的类型、可选操作掩码;

第一个参数:输入数组;

第二个参数:与输入数组有同样尺寸的输出数组;

第三个参数:用于归一化的范数或者是归一化范围内的下限值;

第四个参数:归一化范围内的上限值;不用来作为范数归一化;

第五个参数:当为负的时候,输出数组有着与原数组一样的size;不然它有着与输入数组一样的通道数而且depth = CV_MAT_DEPTH(dtype);

第六个参数:可选的操作掩码。

这个函数归一化 缩放并且平移输入数组的元素,使得当normType = NORM_INF,L1,NORM_L2,的时候, p = Inf,1,或者2:

opencv6.4-imgproc图像处理模块之直方图与模板

;或者当normTpye=NORM_MINMXA(只对密集数组有用)

opencv6.4-imgproc图像处理模块之直方图与模板

。可选的掩码指定一个子数组进行归一化。也就是说norm或者min-n-max可以基于这个子数组计算得到,然后这个子数组来被归一化。如果只想计算这个norm或者min-max 但是却想修改整个数组,你可以使用norm()和MAT::convertTo()。

在稀疏矩阵中,只有非0值被分析和转换。而且稀疏矩阵可以平移0 level,所以稀疏矩阵的范围转换不被允许。

3、对比

要比较两个直方图( H1 和 H2 ),
首先必须要选择一个衡量直方图相似度的 对比标准 (opencv6.4-imgproc图像处理模块之直方图与模板)

OpenCV 函数 compareHist 执行了具体的直方图对比的任务。该函数提供了好几种对比标准来计算相似度,不同版本的个数不同,在3.0beta中提供了6种,在2.49版本中提供了5种:

a、Correlation ( CV_COMP_CORREL ):

opencv6.4-imgproc图像处理模块之直方图与模板

其中:

opencv6.4-imgproc图像处理模块之直方图与模板

N是直方图中bin的数目。

b、Chi-Square ( CV_COMP_CHISQR )

opencv6.4-imgproc图像处理模块之直方图与模板

c、Alternative Chi-Square (method=CV_COMP_CHISQR_ALT)

opencv6.4-imgproc图像处理模块之直方图与模板

这个 alternative 公式 通常用来作为纹理对比

c、Intersection ( CV_COMP_INTERSECT )

opencv6.4-imgproc图像处理模块之直方图与模板

d、Bhattacharyya 距离( CV_COMP_BHATTACHARYYA或者CV_COMP_HELLINGER
),
在opencvn中计算的hellinger距离是与             bhattacharyya系数相关的

opencv6.4-imgproc图像处理模块之直方图与模板

e、Kullback-Leibler divergence (method=CV_COMP_KL_DIV).

opencv6.4-imgproc图像处理模块之直方图与模板

Mat src_base, hsv_base;//声明原图像和转换到hsv的图像
Mat hsv_half_down;//提取转换后hsv下半部分的图像
cvtColor( src_base, hsv_base, CV_BGR2HSV );//rgb转换到hsv空间
hsv_half_down = hsv_base( Range( hsv_base.rows/2, hsv_base.rows - 1 ), Range( 0, hsv_base.cols - 1 ) );//提取下半部分
/// 对hue通道使用30个bin,对saturatoin通道使用32个bin
int h_bins = 50; int s_bins = 60;
int histSize[] = { h_bins, s_bins }; // hue的取值范围从0到256, saturation取值范围从0到180
float h_ranges[] = { 0, 256 };
float s_ranges[] = { 0, 180 }; const float* ranges[] = { h_ranges, s_ranges }; // 使用第0和第1通道
int channels[] = { 0, 1 };
/// 直方图 其实MatND就等于Mat
MatND hist_base;
MatND hist_half_down;
/// 计算HSV图像的直方图
calcHist( &hsv_base, 1, channels, Mat(), hist_base, 2, histSize, ranges, true, false );
normalize( hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat() ); calcHist( &hsv_half_down, 1, channels, Mat(), hist_half_down, 2, histSize, ranges, true, false );
normalize( hist_half_down, hist_half_down, 0, 1, NORM_MINMAX, -1, Mat() ); ///应用不同的直方图对比方法
for( int i = 0; i < 4; i++ )
{ int compare_method = i;
double base_base = compareHist( hist_base, hist_base, compare_method );
double base_half = compareHist( hist_base, hist_half_down, compare_method ); }

上面的代码就是个计算直方图相关性的例子,其中会预料到当将基准图像直方图及其自身进行对比时会产生完美的匹配,与来源于同一样的背景环境的半身图对比时应该会有比较高的相似度, 当与来自不同亮度光照条件的其它图像(这里未列出)对比时匹配度应该不是很好:

opencv6.4-imgproc图像处理模块之直方图与模板(最后两列不再代码里面,这个是为了说明四种标准的评判趋势的,)
对于 Correlation 和 Intersection 标准,
值越大相似度越大。*基准 - 基准* 的对比结果值是最大的, 而 基准 - 半身 的匹配则是第二好(跟我们预测的一致);而另外两种对比标准,则是结果越小相似度越大。

range函数原型

class CV_EXPORTS Range
{
public:
Range();
Range(int _start, int _end);
Range(const CvSlice& slice);
int size() const;
bool empty() const;
static Range all();
operator CvSlice() const;
int start, end;
};

这个类是用来指定在矩阵(Mat)行或者列的范围的,Range(a,b)就是在Matlab中的a:b或者是Python中的 a..b。在Python中这是类似【a,b)这样的范围的。

其中的静态方法Range::all()返回一个特殊的值,用来表示“整个序列”或者“整个范围”,就像在matlab中的“:”或者Python中的“...”。在opencv中所有的方法和函数使用Range的话也支持Range::all()。不过在用户自定义的处理方法中,需要显式的检查和处理

void my_function(..., const Range& r, ....)
{
if(r == Range::all()) {
// process all the data
}
else {
// process [r.start, r.end)
}
}

直方图对比的函数原型:double compareHist(InputArray H1, InputArray H2, int method)

                      double compareHist(const SparseMat& H1, const SparseMat& H2, int method)

参数列表:第一个要对比的直方图、第二个要对比的直方图、方法;

第一个参数:略

第二个参数:与第一个参数需要有相同的size

第三个参数:方法:

– CV_COMP_CORREL Correlation

      – CV_COMP_CHISQR Chi-Square

      – CV_COMP_CHISQR_ALT Alternative Chi-Square (来自3.0beta)

      – CV_COMP_INTERSECT Intersection

      – CV_COMP_BHATTACHARYYA Bhattacharyya distance

      – CV_COMP_HELLINGER Synonym for CV_COMP_BHATTACHARYYA

      – CV_COMP_KL_DIV Kullback-Leibler divergence; (来自3.0beta)

该函数包含两个原型,对应密集矩阵和稀疏矩阵。

当与1,2,3维度的密度直方图的时候,这个函数的效果还是很不错的,但是不适合在维度特别的大的时候。因为这样的直方图的aliasing和sampling的问题(也就是别名和采样的问题,别名可参考搜索“__restrict__”,通过指定唯一访问数据的方法就是该指针,从而让编译器能够毫无顾及的将所有该数据出现的地方的公共子表达部分进行优化,减少指令的数量)非零直方图bins的坐标系有可能会有轻微的平移。为了对比这样的直方图或者更通用的带有权值点的稀疏结构,可以考虑使用EMD()函数。

4、反向投影

反向投影是一种记录给定图像中的像素点如何适应直方图模型像素分布的方式。简单的讲, 所谓反向投影就是首先计算某一特征的直方图模型,然后使用模型去寻找图像中存在的该特征。例如,
你有一个肤色直方图 ( Hue-Saturation 直方图 ),你可以用它来寻找图像中的肤色区域:

这里通过使用肤色直方图为例来解释反向投影的工作原理:

a、假设你已经通过下图得到一个肤色直方图(Hue-Saturation), 旁边的直方图就是 模型直方图 (
代表手掌的皮肤色调).你可以通过掩码操作来抓取手掌所在区域的直方图:

opencv6.4-imgproc图像处理模块之直方图与模板

b、下图是另一张手掌图(测试图像) 以及对应的整张图像的直方图:

opencv6.4-imgproc图像处理模块之直方图与模板

c、我们要做的就是使用 模型直方图 (代表手掌的皮肤色调)
来检测测试图像中的皮肤区域。以下是检测的步骤:

i)对测试图像中的每个像素 p(i,j),获取色调数据并找到该色调(h_ij,s_ij)在直方图中的bin的位置。

ii)查询 模型直方图 中对应的bin
-(h_ij,s_ij) - 并读取该bin的数值。

iii)将此数值储存在新的图像中(BackProjection)。
你也可以先归一化 模型直方图 ,这样测试图像的输出就可以在屏幕显示了。

iiii)通过对测试图像中的每个像素采用以上步骤, 我们得到了下面的 BackProjection 结果图:

opencv6.4-imgproc图像处理模块之直方图与模板

iiiii)使用统计学的语言, BackProjection 中储存的数值代表了测试图像中该像素属于皮肤区域的 概率 。比如以上图为例,
亮起的区域是皮肤区域的概率更大(事实确实如此),而更暗的区域则表示更低的概率(注意手掌内部和边缘的阴影影响了检测的精度)。

。这里的一个保证是之前提取的模型是对的,那么假设其颜色区域的直方图范围为200-230之间的直方图的bin,然后读取的当前像素位置的值在去查找模型的值,模型中那个对应的bin是20,也就是有20个像素点落在这里,然后在测试图像刚刚那个像素点位置上填上20,所以结果就是不再模型中的直方图。例如灰度或者颜色的部分就被忽略了。乍一看是很好,不过觉得这个鲁棒性肯定很差。理解这个的突破点在于最后得到的那个backprojection是个统计矩阵。相比较背景来说,颜色不同亮度不同所以可以被忽略。

下面的例子是简单的示例,如果想更复杂的有使用 H-S 直方图和 floodFill 来定义皮肤区域的掩码);或者查看camshiftdemo例子。

/// 全局变量
Mat src; Mat hsv; Mat hue;
int bins = 25;
 /// 转换到 HSV 空间
cvtColor( src, hsv, CV_BGR2HSV );
/// 分离 Hue 通道
hue.create( hsv.size(), hsv.depth() );//创建个空值矩阵
int ch[] = { 0, 0 };
mixChannels( &hsv, 1, &hue, 1, ch, 1 );//抽取 HSV图像的0通道(Hue)
  MatND hist;
int histSize = MAX( bins, 2 );//#  define MAX(a,b)  ((a) < (b) ? (b) : (a))
float hue_range[] = { 0, 180 };
const float* ranges = { hue_range }; /// 计算直方图并归一化
calcHist( &hue, 1, 0, Mat(), hist, 1, &histSize, &ranges, true, false );
normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() ); /// 计算反向投影
MatND backproj;
calcBackProject( &hue, 1, 0, hist, backproj, &ranges, 1, true ); /// 显示反向投影
imshow( "BackProj", backproj ); /// 显示直方图
int w = 400; int h = 400;
int bin_w = cvRound( (double) w / histSize );
Mat histImg = Mat::zeros( w, h, CV_8UC3 ); for( int i = 0; i < bins; i ++ )
{ rectangle( histImg, 
                 Point( i*bin_w, h ), 
                 Point( (i+1)*bin_w, h - cvRound( hist.at<float>(i)*h/255.0 ) ), 
                 Scalar( 0, 0, 255 ), -1 ); }

  imshow( "Histogram", histImg );
}

抽取通道函数的原型:void mixChannels(const Mat* src, size_t nsrcs, Mat* dst, size_t ndsts, const int* fromTo, size_t npairs);

                                        void mixChannels(InputArrayOfArrays src, InputOutputArrayOfArrays dst, const int* fromTo, size_t npairs);

                                        void mixChannels(InputArrayOfArrays src, InputOutputArrayOfArrays dst, const std::vector<int>&fromTo);

参数列表:src:输入的数组或者是矩阵向量;其中所有的矩阵必须有着相同的size和depth;

nsrcs:在src中的矩阵的个数;

dst:输出的数组或者是矩阵向量;所有的矩阵必须已经被预分配了空间;他们的size和depth必须和src【0】中的一致;

ndst:在dst中的矩阵的个数;

fromTo:索引对的数组,用来指定哪个通道被复制和复制到哪里;fromTo[k*2]在src中是一个基于0(以0作为开始)的输入通道的索引;fromTo[k*2+1]在dst中是作为输出通道的索引;可以使用连续的通道数字;第一个输入图像的通道由0到src【0】.channels()-1,第二个输入图像的通道由src【0】.channels()到src【0】.channels()+src【1】.channels()-1,以此类推。同样的方案可以用来指定输出图像的通道;作为一个特别的例子,当fromTo[k*2]是负的时候,对应的输出通道可以用0来填充。

npairs:在fromTo中索引对的个数。

该函数提供了一个打乱图像通道的高级机制,而split()和merge()以及cvtColot()的部分格式是对于mixChannels()的特殊情况。下面的代码是将一个4通道的RGBA划分成一个3通道的BGR(这里R和B通道替换了)和一个独立的alpha通道图像:

Mat 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 );;

反向投影的函数原型:void calcBackProject(const Mat* images, int nimages, const int* channels, InputArray hist, OutputArray backProject, const float** ranges, double scale=1, bool uniform= true );

                                        void calcBackProject(const Mat* images, int nimages, const int* channels, const SparseMat& hist, OutputArray backProject, const float** ranges, double scale=1, bool uniform= true );

参数列表:images:原数组,它们都具有相同的depth,CV_8U或者是CV_32F,并且具有相同的size,每张图像可以有任意的通道数;

nimages:原图像的个数;

channels:通道列表,用来计算反向投影的。通道数必须匹配直方图的维度数。第一个数组通道由0到images[0].channels()-1,第二个数组通道         由images[0].channels()到images[0].channels()+images[1].channels()-1,以此类推;

hist:输入的直方图,可以是密集或者稀疏的;

backProject:目标反向投影数组,必须是和images[0]一样size和depth的单通道数组;

ranges:每个维度上直方图bin边界数组的数组,可见calcHist();

scale:可选的为了输出反向投影的缩放因子;

uniform:标识,用来指示直方图是否需要归一化。

函数calcBackProject是计算直方图的反向投影。与calcHist相似的,在每个位置上(x,y),该函数计算来自输入图像被选取通道的值并查找对应的直方图的bin。不过不是对bin的值进行增加,而是读取bin的值,并通过scale进行缩放,和存储在backProject(x,y)。统计学观点来说,这个函数是通过对表示的直方图的经验性概率分布来计算每个元素值的概率。例如,你可以在一个场景中追踪一个具有亮颜色的对象:

a、在追踪之前,将对象进行拍照并且使得对象几乎能够占满整个图像帧。计算hue直方图。这个直方图会有很强的最大值,对应着对象中的领域颜色;

b、在追踪的时候,使用预计算过的直方图来计算每个输入视频帧的hue平面的反向投影。使用阈值反向索引来压缩弱颜色。对于没有足够的颜色饱和度的压缩像素和太黑或者太亮的像素来说都是很有意义的。

c、查找相连的成分生成图像,并选定最大的成分。

这是CamShift()颜色对象追踪的一个相似的算法。

十、模板匹配

模板匹配是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术.;

我们需要2幅图像:

           原图像 (I): 在这幅图像里,我们希望找到一块和模板匹配的区域

           模板 (T): 将和原图像比照的图像块

我们的目标是检测最匹配的区域:

opencv6.4-imgproc图像处理模块之直方图与模板

为了确定匹配区域, 我们不得不滑动模板图像和原图像进行 比较 :

opencv6.4-imgproc图像处理模块之直方图与模板

通过 滑动, 我们的意思是图像块一次移动一个像素 (从左往右,从上往下). 在每一个位置, 都进行一次度量计算来表明它是 “好” 或 “坏” 地与那个位置匹配 (或者说块图像和原图像的特定区域有多么相似).

对于 T 覆盖在 I 上的每个位置,你把度量值 保存 到 结果图像矩阵 (R) 中.
在 R 中的每个位置(x,y)都包含匹配度量值:

opencv6.4-imgproc图像处理模块之直方图与模板

上图就是 TM_CCORR_NORMED 方法处理后的结果图像 R .
最白的位置代表最高的匹配. 正如您所见, 红色椭圆框住的位置很可能是结果图像矩阵中的最大数值, 所以这个区域 (以这个点为顶点,长宽和模板图像一样大小的矩阵) 被认为是匹配的.

实际上, 我们使用函数 minMaxLoc 来定位在矩阵 R 中的最大值点
(或者最小值, 根据函数输入的匹配参数) .

OpenCV通过函数 matchTemplate 实现了模板匹配算法.
可用的方法有6个:

下面的符号:I表示image,T表示template,R表示result。得到的结果为x' = 0,...,w-1;y' = 0,...,h-1。

a、平方差匹配 method=CV_TM_SQDIFF:这类方法利用平方差来进行匹配,最好匹配为0.匹配越差,匹配值越大.

opencv6.4-imgproc图像处理模块之直方图与模板
b、标准平方差匹配 method=CV_TM_SQDIFF_NORMED
opencv6.4-imgproc图像处理模块之直方图与模板

c、相关匹配 method=CV_TM_CCORR:这类方法采用模板和图像间的乘法操作,所以较大的数表示匹配程度较高,0标识最坏的匹配效果.

opencv6.4-imgproc图像处理模块之直方图与模板

d、标准相关匹配 method=CV_TM_CCORR_NORMED

opencv6.4-imgproc图像处理模块之直方图与模板

e、相关匹配 method=CV_TM_CCOEFF:这类方法将模版对其均值的相对值与图像对其均值的相关值进行匹配,1表示完美匹配,-1表示糟糕的匹配,0表示没有任何相关性(随机序列).

opencv6.4-imgproc图像处理模块之直方图与模板

这里:

opencv6.4-imgproc图像处理模块之直方图与模板

f、标准相关匹配 method=CV_TM_CCOEFF_NORMED

opencv6.4-imgproc图像处理模块之直方图与模板

通常,随着从简单的测量(平方差)到更复杂的测量(相关系数),我们可获得越来越准确的匹配(同时也意味着越来越大的计算代价). 最好的办法是对所有这些设置多做一些测试实验,以便为自己的应用选择同时兼顾速度和精度的最佳方案.

在该函数完成对比之后,最好的匹配可以使用函数minMaxLoc()查找全局最小值(CV_TM_SQDIFF)或者最大值(CV_TM_CCORR、CV_TM_CCOEFF)。在一个颜色图像中,基于所有的通道计算分子中的模板总和和分母中每个和,不过对于每个通道使用的是分离的均值。也就是说,这个函数可以采用一个颜色模板和一个彩色图像。这个结果仍然是一个单通道图像,这也更容易理解。

/// 载入原图像和模板块
img = imread( );
templ = imread( );
/// 将被显示的原图像
Mat img_display;
img.copyTo( img_display ); /// 创建输出结果的矩阵,用来输出匹配结果,这里很像cnn中的卷积层中的特征子图
int result_cols = img.cols - templ.cols + 1;
int result_rows = img.rows - templ.rows + 1; result.create( result_cols, result_rows, CV_32FC1 ); /// 进行匹配和标准化
matchTemplate( img, templ, result, match_method );
normalize( result, result, 0, 1, NORM_MINMAX, -1, Mat() ); /// 通过函数 minMaxLoc 定位最匹配的位置
double minVal; double maxVal; Point minLoc; Point maxLoc;
Point matchLoc; minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() ); /// 对于方法 SQDIFF 和 SQDIFF_NORMED, 越小的数值代表更高的匹配结果. 而对于其他方法, 数值越大匹配越好
if( match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED )
{ matchLoc = minLoc; }
else
{ matchLoc = maxLoc; } /// 让我看看您的最终结果
rectangle( img_display, matchLoc, Point( matchLoc.x + templ.cols , matchLoc.y + templ.rows ), Scalar::all(0), 2, 8, 0 );
rectangle( result, matchLoc, Point( matchLoc.x + templ.cols , matchLoc.y + templ.rows ), Scalar::all(0), 2, 8, 0 ); imshow( image_window, img_display );
imshow( result_window, result );

上图就是使用图像匹配的例子代码,其中在<tpyes_c.h>中method的定义如下:enum

{

    CV_TM_SQDIFF        =0,

    CV_TM_SQDIFF_NORMED =1,

    CV_TM_CCORR         =2,

    CV_TM_CCORR_NORMED  =3,

    CV_TM_CCOEFF        =4,

    CV_TM_CCOEFF_NORMED =5

};

匹配模板的函数原型:void matchTemplate(InputArray image, InputArray templ, OutputArray result, int method);

参数列表;输入图像、输入模板、输出结果、方法选项

第一个参数:所需要搜寻的图像,必须是8-bit或者32-bit的浮点(floating-point);

第二个参数:搜寻的模板。必须是不大于原图像,而且需要有同样的数据类型;

第三个参数:对比的结果映射图。必须是单通道32-bit的浮点类。如果image是W×H,而templ是w×h,那么result就是(W-w+1)×(H-h+1);

第四个参数:指定具体的方法。

寻找最大值的函数原型:void minMaxLoc(InputArray src, double* minVal, double* maxVal=0, Point* minLoc=0, Point* maxLoc=0, InputArray mask= noArray() );

                                           void minMaxLoc(const SparseMat& a, double* minVal, double* maxVal, int* minIdx=0, int* maxIdx=0 );

参数列表:src:输入的单通道数组;

minVal:指向返回的最小值的指针;当不需要的时候赋值NULL即可;

maxVal:指向返回的最大值的指针;当不需要的时候赋值NULL即可;

minLoc:指向返回的最小值的位置(在2D情况中);当不需要的时候赋值NULL即可;

maxLoc:指向返回的最大值的位置(在2D情况中);当不需要的时候赋值NULL即可;

mask:可选的用来选择一个子数组。

该函数用来查找最小和最大的元素值,并给出它们的位置。极端的情况就是如果mask是一个空矩阵,或者非空,但是在特定的数组区域中,那么就需要搜索整个数组。该函数不能与多通道数组一起工作。如果想要在所有的通道中找最小和最大值,那么使用Mat::reshape()先将涉及到的数组转换成单通道;或者你使用extractImageCOI(),或者mixChannels(),或者split()来先提取特定的通道。

上一篇:Linux学习之CentOS(十九)------linux 下压缩与解压之 tar、gzip、bzip2、zip、rar


下一篇:AngularJS 中的 Promise 和 设计模式(转)