本次要讲的范例是反向投影,反向投影如果是按照字面上的理解,还有书本上的理解可能会比较困难,但是如果是举一些具体的简单的例子,那可能就比较容易接受了,应用的话,可以检测出肤色区域,例如,你有一个肤色直方图 ( Hue-Saturation 直方图 ),你可以用它来寻找图像中的肤色区域,现在我们来看看反向变换吧。
1、原理
图像的反向投影图是用输入图像的某一位置上像素值(多维或灰度)对应在直方图的一个bin上的值来代替该像素值,所以得到的反向投影图是单通的。
举个小例
(1)例如灰度图像如下
Image=
0 1 2 3
4 5 6 7
8 9 10 11
8 9 14 15
(2)该灰度图的直方图为(bin指定的区间为[0,3),[4,7),[8,11),[12,16))
Histogram=
4 4 6 2
(3)反向投影图
Back_Projection=
4 4 4 4
4 4 4 4
6 6 6 6
6 6 2 2
例如位置(0,0)上的像素值为0,对应的bin为[0,3),所以反向直方图在该位置上的值这个bin的值4。
这个操作与前面介绍的LUT()方法非常类似,只不过是将LUT()参数中的查找表改成直方图而已。
2、代码实现
①、代码运用了floodfull()函数,点击图片的位置,得到填充的联通图,赋值给mask,计算mask所对应图片部位的直方图,再对图片进行反向投影。
②、对图片提取hue通道值,再进行反向投影,显示结果。
#include "stdafx.h" /** * @file BackProject_Demo2.cpp * @brief Sample code for backproject function usage ( a bit more elaborated ) * @author OpenCV team */ #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std; /// Global Variables Mat src; Mat hsv;Mat hand;Mat hue;Mat hand1; Mat mask; int lo = 20; int up = 20;int bins=25; const char* window_image = "Source image"; /// Function Headers void Hist_and_Backproj( ); void pickPoint (int event, int x, int y, int, void* ); void hue_and_Backproj(int, void* ); /** * @function main */ int main( int, char** argv ) { /// Read the image src = imread( "hand_sample2.jpg", 1 ); /// Transform it to HSV cvtColor( src, hsv, COLOR_BGR2HSV ); /// 分离 Hue 通道 hue.create( hsv.size(), hsv.depth() ); int ch[] = { 0, 0 }; mixChannels( &hsv, 1, &hue, 1, ch, 1 ); hand.create(src.size(),src.type()); hand1.create(src.size(),src.type()); /// Show the image namedWindow( window_image, WINDOW_AUTOSIZE ); imshow( window_image, src ); /// Set Trackbars for floodfill thresholds createTrackbar("* Hue bins: ", window_image, &bins, 180, hue_and_Backproj ); createTrackbar( "Low thresh", window_image, &lo, 255, 0 ); createTrackbar( "High thresh", window_image, &up, 255, 0 ); /// Set a Mouse Callback hue_and_Backproj(0,0); setMouseCallback( window_image, pickPoint, 0 ); waitKey(0); return 0; } /** * @function pickPoint */ void pickPoint (int event, int x, int y, int, void* ) { if( event != CV_EVENT_LBUTTONDOWN ) { return; } Point seed = Point( x, y ); int newMaskVal = 255; Scalar newVal = Scalar( 120, 120, 120 ); int connectivity = 8; int flags = connectivity + (newMaskVal << 8 ) + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY; Mat mask2 = Mat::zeros( src.rows + 2, src.cols + 2, CV_8UC1 ); floodFill( src, mask2, seed, newVal, 0, Scalar( lo, lo, lo ), Scalar( up, up, up), flags ); mask = mask2( Range( 1, mask2.rows - 1 ), Range( 1, mask2.cols - 1 ) ); imshow( "Mask", mask ); Hist_and_Backproj( ); } /** * @function Hist_and_Backproj */ void Hist_and_Backproj( ) { MatND hist; hand = Scalar::all(0); int h_bins = 30; int s_bins = 32; int histSize[] = { h_bins, s_bins }; float h_range[] = { 0, 179 }; float s_range[] = { 0, 255 }; const float* ranges[] = { h_range, s_range }; int channels[] = { 0, 1 }; /// Get the Histogram and normalize it calcHist( &hsv, 1, channels, mask, hist, 2, histSize, ranges, true, false ); normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() ); /// Get Backprojection Mat backproj; calcBackProject( &hsv, 1, channels, hist, backproj, ranges, 1, true ); /// Draw the backproj imshow( "BackProj", backproj ); src.copyTo(hand,backproj); imshow("hand",hand); } void hue_and_Backproj(int, void* ) { MatND hist; hand1 = Scalar::all(0); int histSize = MAX( bins, 2 ); 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 ); threshold(backproj,backproj,254,255,CV_THRESH_TOZERO); /// 显示反向投影 imshow( "BackProj1", backproj ); src.copyTo(hand1,backproj); imshow("hand1",hand1); }
3、运行结果
图1、源图像 图2、mask值
图3、h_s通道反向投影 图4、h通道反向投影
4、用到的类和函数
mixChannels
功能:从输入中拷贝某通道到输出中特定的通道。
结构:
void mixChannels(const Mat*src, size_t nsrcs, Mat* dst, size_t ndsts, const int* fromTo, size_t npairs)
src:一系列输入图像的数组, 被拷贝的通道的来源一系列输入图像的数组, 被拷贝的通道的来源
nsrcs:输入图像的个数
dst:一系列目的图像的数组, 储存拷贝的通道,所有的数组必须事先分配空间(如用create),大小和深度须与输入数组等同。
ndsts:目的数组中图像的数目
fromTo:通道索引对的数组,指示如何将输入图像的某一通道拷贝到目的图像的某一通道。偶数下标的用来标识输入矩阵,奇数下标的用来标识输出矩阵。如果偶数下标为负数,那么相应的输出矩阵为零矩阵。
npairs:fromTo中的序号对数(两个算1对)。
floodFill
功能:用指定颜色填充一个连接域
结构:
int floodFill(InputOutputArray image, InputOutputArray mask, Point seed, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4 )
image:输入的 1- 或 3-通道, 8-比特或浮点数图像。输入的图像将被函数的操作所改变,除非你选择 CV_FLOODFILL_MASK_ONLY 选项
mask:运算掩码, 应该是单通道、8-比特图像, 长和宽上都比输入图像 image 大两个象素点。若非空,则函数使用且更新掩码, 所以使用者需对 mask 内容的初始化负责。填充不会经过 mask 的非零象素, 例如,一个边缘检测子的输出可以用来作为 mask 来阻止填充边缘。或者有可能在多次的函数调用中使用同一个 mask ,以保证填充的区域不会重叠。注意: 因为 mask 比欲填充图像大,所以 mask 中与输入图像(x,y)像素点相对应的点具有(x+1,y+1)坐标。mask可以被用为输入值(此时他控制可以被填充的区域),也可以作为输出值(此时他只已经被填充的区域)。
seed:开始填充的点
newVal:新的重新绘制的象素值
rect:可选输出参数,返回重绘区域最小边界矩形
loDiff:当前观察象素值与其部件领域象素或者待加入该部件的种子象素之负差(Lower difference)的最大值。(具体看后面公式表达)
upDiff:当前观察象素值与其部件领域象素或者待加入该部件的种子象素之正差(upper difference)的最大值。(具体看后面公式表达)
flag:操作选项. 低8位(第0-7位)比特包含连通值, 4 (缺省) 或 8, 如果为4,填充算法只考虑当前像素水平方向和垂直方向的相邻点;如果为8,除上述相邻点外,还会包括对角方向的相邻点。在函数执行连通过程中确定使用哪种邻域方式。高8位(第16-23位)比特可以是 0 或下面的开关选项的组合:
CV_FLOODFILL_FIXED_RANGE - 如果设置,则考虑当前象素与种子象素之间的差,否则考虑当前象素与其相邻象素的差。(范围是浮点数).
CV_FLOODFILL_MASK_ONLY - 如果设置,函数不填充原始图像 (忽略 new_val), 但填充掩模图像 (这种情况下 MASK 必须是非空的).
中间比特位(第8-15位)指定填充掩码的像素值。如果中间比特值为0,则掩码将用1填充,此时掩码将会显示为全黑的图像,所有的flags可以通过OR操作连接起来。例如,如果想用8领域填充,并填充固定像素值范围,是填充掩码二不是填充源图像,以及填充值为47,那么输入的参数应该是:
flags=8|CV_FLOODFILL_MASK_ONLY|CV_FLOODFILL_FIXED_RANGE|(47<<8);
函数 cvFloodFill 用指定颜色,从种子点开始填充一个连通域。连通性由象素值的接近程度来衡量。在点 (x, y) 的象素被认为是属于重新绘制的区域,如果:
src(x',y')-lo_diff<=src(x,y)<=src(x',y')+up_diff, 灰度图像,浮动范围
src(seed.x,seed.y)-lo<=src(x,y)<=src(seed.x,seed.y)+up_diff, 灰度图像,固定范围
src(x',y')r-lo_diffr<=src(x,y)r<=src(x',y')r+up_diffr 和
src(x',y')g-lo_diffg<=src(x,y)g<=src(x',y')g+up_diffg 和
src(x',y')b-lo_diffb<=src(x,y)b<=src(x',y')b+up_diffb, 彩色图像,浮动范围
src(seed.x,seed.y)r-lo_diffr<=src(x,y)r<=src(seed.x,seed.y)r+up_diffr 和
src(seed.x,seed.y)g-lo_diffg<=src(x,y)g<=src(seed.x,seed.y)g+up_diffg 和
src(seed.x,seed.y)b-lo_diffb<=src(x,y)b<=src(seed.x,seed.y)b+up_diffb, 彩色图像,固定范围
其中 src(x',y') 是象素邻域点的值。
大家如果想更加了解floodfill参数到底是什么,如何变化的,可以通过 http://blog.csdn.net/xiaowei_cqu/article/details/8987387 这篇博客的代码自己运行以下,理解以下。
同时也可以查阅学习OpenCV这本书。
calcBackProject
功能:计算反向投影
结构:
void calcBackProject(const Mat* arrays, int narrays, const int* channels, InputArray hist, OutputArray backProject, const float** ranges, double scale=1, bool uniform=true )
arrays:源图像,他们有着同样的depth( CV_8U 、 CV_32F)同样的size,可以是任意的通道数。
narrays:源图像的数目
channels:用于计算反投影的通道列表。
hist:输入的直方图
backProject:输出单通道反投影图像,和arrays[0]有同样的size和depth
ranges:和calchist中的ranges一样
如果想看看更多的肤色检测算法可以点击下面这篇博客
http://blog.csdn.net/onezeros/article/details/6342567