Goal
在本教程中,您将学习如何:
使用 OpenCV 函数 cv::split 将图像划分为其对应的平面。
使用 OpenCV 函数 cv::calcHist 计算图像数组的直方图
使用函数 cv::normalize 规范化数组
笔记
在上一个教程(直方图均衡)中,我们讨论了一种特殊的直方图,称为图像直方图。 现在我们将考虑它的更一般的概念。 继续阅读!
What are histograms?
直方图是收集的数据计数,组织成一组预定义的 bin
当我们说数据时,我们并没有将其限制为强度值(正如我们在前面的教程直方图均衡中看到的那样)。 收集的数据可以是您认为对描述图像有用的任何特征。
让我们看一个例子。 想象一个矩阵包含图像的信息(即 0-255 范围内的强度):
如果我们想以有组织的方式统计这些数据会发生什么? 由于我们知道这种情况下的信息值范围是 256 个值,因此我们可以将我们的范围划分为子部分subparts(称为 bin),例如:
[0,255]=[0,15]∪[16,31]∪....∪[240,255]range=bin1∪bin2∪....∪binn=15
我们可以计算落在每个 bini 范围内的像素数。 将其应用于上面的示例,我们得到下面的图像(轴 x 表示 bin,轴 y 表示每个 bin 中的像素数)。
这只是直方图如何工作以及为什么有用的一个简单示例。 直方图不仅可以计算颜色强度,还可以计算我们想要测量的任何图像特征(即梯度、方向等)。
让我们识别直方图的某些部分:
dims:要收集数据的参数数量。 在我们的示例中, dims = 1 因为我们只计算每个像素的强度值(在灰度图像中)。
bins:是每个dim中的细分数。 在我们的示例中,bins = 16
range:要测量的值的限制。 在这种情况下:range = [0,255]
如果你想计算两个特征怎么办? 在这种情况下,您生成的直方图将是一个 3D 图(其中 x 和 y 将是每个特征的 binx 和 biny,z 将是 (binx,biny) 的每个组合的计数数。这同样适用于更多特征 (当然它变得更棘手)。
What OpenCV offers you
出于简单的目的,OpenCV 实现了函数 cv::calcHist ,它计算一组数组(通常是图像或图像平面)的直方图。 它可以操作多达 32 个维度。 我们将在下面的代码中看到它!
Code
这个程序有什么作用?
加载图像
使用函数 cv::split 将图像拆分为 R、G 和 B 平面
通过调用函数 cv::calcHist 计算每个 1 通道平面的直方图
在窗口中绘制三个直方图
代码一览:
/**
* @function calcHist_Demo.cpp
* @brief 计算直方图Demo code to use the function calcHist
* @author
*/
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace std;
using namespace cv;
/**
* @function main
*/
int main(int argc, char** argv)
{
//! [加载图像]
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR );
if( src.empty() )
{
return EXIT_FAILURE;
}
//! [Load image]
//! [将图像分成 3 个平面(B、G 和 R)]
vector<Mat> bgr_planes;
split( src, bgr_planes );
//! [Separate the image in 3 places ( B, G and R )]
//! [确定 bin 数量]
int histSize = 256;
//! [Establish the number of bins]
//! [设置范围(用于 B、G、R))]
float range[] = { 0, 256 }; //上边界是排除在外的
const float* histRange = { range };
//! [Set the ranges ( for B,G,R) )]
//! [设置直方图参数]
bool uniform = true, accumulate = false;
//! [Set histogram param]
//! [计算直方图]
Mat b_hist, g_hist, r_hist;
calcHist( &bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate );
//! [Compute the histograms]
//! [绘制 B、G 和 R 的直方图]
int hist_w = 512, hist_h = 400;
int bin_w = cvRound( (double) hist_w/histSize );
Mat histImage( hist_h, hist_w, CV_8UC3, Scalar( 0,0,0) );//黑色背景
//! [Draw the histograms for B, G and R]
//! [将结果归一化为 ( 0, histImage.rows )]
normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
//! [Normalize the result to ( 0, histImage.rows )]
//! [为每个通道绘制曲线]
for( int i = 1; i < histSize; i++ )
{
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 );
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(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 );
}
//! [Draw for each channel]
//! [显示]
imshow("Source image", src );
imshow("calcHist Demo", histImage );
waitKey();
//! [Display]
return EXIT_SUCCESS;
}
Explanation
- 加载源图像
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR );
if( src.empty() )
{
return EXIT_FAILURE;
}
- 在其三个 R、G 和 B 平面中分离源图像。 为此,我们使用 OpenCV 函数 cv::split :
vector<Mat> bgr_planes;// Mat 的向量
split( src, bgr_planes );
我们的输入是要分割的图像(这种情况下是三个通道),输出是 Mat 的向量)
现在我们准备开始为每个平面配置直方图。 由于我们正在使用 B、G 和 R 平面,我们知道我们的值将在 [0,255] 区间内
确定 bin 的数量(5、10...):
int histSize = 256;
- 设置值的范围(如我们所说,介于 0 和 255 之间)
float range[] = { 0, 256 }; //the upper boundary is exclusive
const float* histRange[] = { range };
- 我们希望我们的 bin 具有相同的大小(统一)并在开始时清除直方图,因此:
bool uniform = true, accumulate = false;
- 我们继续使用 OpenCV 函数 cv::calcHist :计算直方图:
Mat b_hist, g_hist, r_hist;
calcHist( &bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, histRange, uniform, accumulate );
calcHist( &bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, histRange, uniform, accumulate );
calcHist( &bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, histRange, uniform, accumulate );
- 参数是 (C++ code):
&bgr_planes[0]: 源数组
1:源数组的数量(在这种情况下,我们使用 1。我们也可以在此处输入数组列表)
0:要测量的通道(dim)。 在这种情况下,它只是强度(每个array 都是单通道),所以我们只写 0。
Mat():要在源数组上使用的掩码(零表示要忽略的像素)。 如果未定义,则不使用
b_hist:将存储直方图的 Mat 对象
1:直方图维数。
histSize:每个使用的维度的 bin 数量
histRange:每个维度要测量的值的范围
uniform and accumulate::bin 大小相同,柱状图在开始时被清除。
- 创建图像以显示直方图:
int hist_w = 512, hist_h = 400;
int bin_w = cvRound( (double) hist_w/histSize );
Mat histImage( hist_h, hist_w, CV_8UC3, Scalar( 0,0,0) );
- 请注意,在绘制之前,我们首先对直方图进行 cv::normalize,使其值落在输入参数指示的范围内:
normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
- 此函数接收这些参数(C++ 代码):
b_hist:输入数组
b_hist:输出归一化数组(可以相同)
0 和 histImage.rows:对于这个例子,它们是标准化 r_hist 值的下限和上限
NORM_MINMAX:表示归一化类型的参数(如上所述,它调整之前设置的两个限制之间的值)
-1:表示输出的归一化数组将与输入的类型相同
Mat():可选掩码
- 观察以访问bin(在本例中为一维直方图):
for( int i = 1; i < histSize; i++ )
{ //直线
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 );
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(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 );
}
我们使用表达式(C++ 代码):
b_hist.at<float>(i)
其中 i 表示维度值。 如果它是一个二维直方图,我们会使用类似的东西:
b_hist.at<float>( i, j )
最后我们显示我们的直方图并等待用户退出:
imshow("Source image", src );
imshow("calcHist Demo", histImage );
waitKey();
Result
-
- 使用如下图所示的图像作为输入参数:
-
- 生成以下直方图: