开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天
1 概述
如果一幅图像的区域中显示的是一种结构纹理或者一个独特的物体,那么这个区域的直方图可以看作一个概率函数,其表现形式是某个像素属于该纹理或物体的概率。
而反向投影(back projection)就是一种记录给定图像中的像素点如何适应直方图模型像素分布方式的一种方法。
简单的讲,所谓反向投影就是首先计算某一特征的直方图模型,然后使用模型去寻找图像中存在的该特征的方法。
2 反向投影的工作原理
下面,我们将使用 H—S 肤色直方图为例来解释反向投影的工作原理。
首先通过之前讲过的求 H—S 直方图的示例程序,得到如下图所示的H—S 肤色直方图。
素材图1
素材图1的H-S直方图
素材图2
素材图3的H-S直方图
而我们要做的,就是使用模型直方图(代表手掌的皮肤色调)来检测测试图像中的皮肤区域。以下是检测步骤。
(1)对测试图像中的每个像素(p(i.j)),获取色调数据并找到该色调(hijgsj在直方图中的 bin 的位置。
(2)查询模型直方图中对应 bin 的数值。
(3)将此数值储存在新的反射投影图像中。也可以先归一化直方图数值到0—255 范围,这样可以直接显示反射投影图像(单通道图像)。
(4)通过对测试图像中的每个像素采用以上步骤,可以得到最终的反射投影图像。如图下图所示。
(5)使用统计学的语言进行分析。反向投影中储存的数值代表了测试图像中该像素属于皮肤区域的概率。比如以上图为例,亮起的区域是皮肤区域的概率更大,而更暗的区域则表示是皮肤的概率更低。另外,可以注意到,手掌内部和边缘的阴影影响了检测的精度。
3 反向投影的作用
反向投影用于在输入图像(通常较大)中查找与特定图像(通常较小或者仅1个像素,以下将其称为模板图像)最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。
4 反向投影的结果
反向投影的结果包含了以每个输入图像像素点为起点的直方图对比结果。可以把它看成是一个二维的浮点型数组、二维矩阵,或者单通道的浮点型图像。
5 计算反向投影: calcBackProject()函数
calcBackProject()函数用于计算直方图的反向投影。
C++:
void calcBackProject(const Mat* images, int nimages, const int* channels, InputArray hist, OutputArray backProject, const float** ranges, double scale=1, bool uniform=true )
-
第一个参数, const Mat* 类型的 images,输入的数组(或数组集),它们须为相同的深度(CV_8U 或CV_32F)和相同的尺寸,而通道数则可以任意。
-
第二个参数,int 类型的 nimages,输入数组的个数,也就是第一个参数中存放了多少张“图像”,有几个原数组。
-
第三个参数, const int* 类型的 channels,需要统计的通道(dim)索引。第一个数组通道从0到images[0].channels() - 1,而第二个数组通道从images[0].channels()计算到images[0].channels() + images[1].channels()- 1。
-
第四个参数, InputArray类型的hist,输入的直方图。
-
第五个参数, OutputArray类型的backProject,目标反向投影阵列,其须为单通道,并且和image[0]有相同的大小和深度。
-
第六个参数, const float** 类型的ranges,表示每一个维度数组(第六个参数dims)的每一维的边界阵列,可以理解为每一维数值的取值范围。
-
第七个参数, double scale,有默认值1,输出的方向投影可选的缩放因子,默认值为 1。
-
第八个参数, bool类型的uniform,指示直方图是否均匀的标识符,有默认值 true。
6 通道复制: mixChannels()函数
此函数由输入参数复制某通道到输出参数特定的通道中。有两个版本的 C++原型,采用函数注释方式分别介绍如下。
C++: void mixChannels(
const Mat* src,//输入的数组,所有的矩阵必须有相同的尺寸和深度
size_t nsrcs,//第一个参数 src 输入的矩阵数
Mat* dst,//输出的数组,所有矩阵必须被初始化,且大小和深度必须与 src[0] 相同
size_t ndsts,//第三个参数 dst 输入的矩阵数
const int* fromTo,//对指定的通道进行复制的数组索引
size_t npairs //第五个参数 fromTo 的索引数
)
C++:
void mixChannels( const vector& src,//输入的矩阵向量,所有的矩阵必须有相同的尺寸和深度
vector&dst,//输出的矩阵向量,所有矩阵须被初始化,且大小和深度须与 src[0]相同
const int* fromTo, //对指定的通道进行复制的数组索引
size_t npairs//第三个参数 fromTo 的索引数 )
此函数为重排图像通道提供了比较先进的机制。其实,之前我们接触到的split()和merge(),以及cvtColor()的某些形式,都只是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 );
//组成矩阵数组来进行操作
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 );
7 综合示例:反射投影
源码
//---------------------------------【头文件、命名空间包含部分】----------------------------
// 描述:包含程序所使用的头文件和命名空间
//---------------------------------------------------------------------------------------
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
//-----------------------------------【宏定义部分】--------------------------------------------
// 描述:定义一些辅助宏
//--------------------------------------------------------------------------------------------
#define WINDOW_NAME1 "【原始图】" //为窗口标题定义的宏
//-----------------------------------【全局变量声明部分】--------------------------------------
// 描述:全局变量声明
//-------------------------------------------------------------------------------------------
Mat g_srcImage; Mat g_hsvImage; Mat g_hueImage;
int g_bins = 30;//直方图组距
//-----------------------------------【全局函数声明部分】--------------------------------------
// 描述:全局函数声明
//-------------------------------------------------------------------------------------------
void on_BinChange(int, void* );
//--------------------------------------【main( )函数】-----------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//---------------------------------------------------------------------------------------------
int main( )
{
//【0】改变console字体颜色
system("color 6F");
//【1】读取源图像,并转换到 HSV 空间
g_srcImage = imread( "1.jpg", 1 );
if(!g_srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; }
cvtColor( g_srcImage, g_hsvImage, CV_BGR2HSV );
//【2】分离 Hue 色调通道
g_hueImage.create( g_hsvImage.size(), g_hsvImage.depth() );
int ch[ ] = { 0, 0 };
mixChannels( &g_hsvImage, 1, &g_hueImage, 1, ch, 1 );
//【3】创建 Trackbar 来输入bin的数目
namedWindow( WINDOW_NAME1 , CV_WINDOW_AUTOSIZE );
createTrackbar("色调组距 ", WINDOW_NAME1 , &g_bins, 180, on_BinChange );
on_BinChange(0, 0);//进行一次初始化
//【4】显示效果图
imshow( WINDOW_NAME1 , g_srcImage );
// 等待用户按键
waitKey(0);
return 0;
}
//-----------------------------------【on_HoughLines( )函数】--------------------------------
// 描述:响应滑动条移动消息的回调函数
//------------------------------------------------------------------------------------------
void on_BinChange(int, void* )
{
//【1】参数准备
MatND hist;
int histSize = MAX( g_bins, 2 );
float hue_range[] = { 0, 180 };
const float* ranges = { hue_range };
//【2】计算直方图并归一化
calcHist( &g_hueImage, 1, 0, Mat(), hist, 1, &histSize, &ranges, true, false );
normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() );
//【3】计算反向投影
MatND backproj;
calcBackProject( &g_hueImage, 1, 0, hist, backproj, &ranges, 1, true );
//【4】显示反向投影
imshow( "反向投影图", backproj );
//【5】绘制直方图的参数准备
int w = 400; int h = 400;
int bin_w = cvRound( (double) w / histSize );
Mat histImg = Mat::zeros( w, h, CV_8UC3 );
//【6】绘制直方图
for( int i = 0; i < g_bins; i ++ )
{ rectangle( histImg, Point( i*bin_w, h ), Point( (i+1)*bin_w, h - cvRound( hist.at<float>(i)*h/255.0 ) ), Scalar( 100, 123, 255 ), -1 ); }
//【7】显示直方图窗口
imshow( "直方图", histImg );
}
原图
反射投影图
直方图
编译并运行此程序,可以通过滑动条的调节改变直方图组距(bin)的值,得到不同的反向投影效果图。