cvAdaptiveThreshold的探讨

cvAdaptiveThreshold 的陷阱 — 转帖 2008-06-23 21:52

本文引自 http://www.opencv.org.cn/forum/viewtopic.php?t=3355 感谢作者

关于二值化函数cvAdaptiveThreshold和cvThreshold的一些发现,望大家共同探讨

《cvAdaptiveThreshold的探讨》lyhblank 于 2008-05-27 1:01

今天根据参考手册的指导使用函数cvAdaptiveThreshold时,发现所得的结果很奇怪,它只获取了物体的边缘,而非二值化。于是我怀着好奇的心情,看了它的源码,果不其然,它实在是个边缘提取函数。
以下便是本人对其算法的一些描述:
函数cvAdaptiveThreshold的代码很少,除了一些类型检查的语句,主要的处理部分是由一原型为: “static void icvAdaptiveThreshold_MeanC( const CvMat* src, CvMat* dst, int method, int maxValue, int type, int size, double delta )” 的函数完成的,其输入参数基本照搬cvAdaptiveThreshold的,只是名字不同罢了,src(src)、dst(dst)、method(adaptive_method)、maxValue(对max_value取整)、type(threshold_type)、size(block_size)、delta(param1)。
为了方便大家查看,特将函数icvAdaptiveThreshold_MeanC代码贴出:

代码:
全选
static void
icvAdaptiveThreshold_MeanC( const CvMat* src, CvMat* dst, int method,
int maxValue, int type, int size, double delta )
{
/*1*/ CvMat* mean = 0;
/*2*/ CV_FUNCNAME( "icvAdaptiveThreshold_MeanC" );
/*3*/
/*4*/ __BEGIN__;
/*5*/
/*6*/ int i, j, rows, cols;
/*7*/ int idelta = type == CV_THRESH_BINARY ? cvCeil(delta) : cvFloor(delta);
/*8*/ uchar tab[768];
/*9*/
/*10*/ if( size <= 1 || (size&1) == 0 )
/*11*/ CV_ERROR( CV_StsOutOfRange, "Neighborhood size must be >=3 and odd (3, 5, 7, ...)" );
/*12*/
/*13*/ if( maxValue < 0 )
/*14*/ {
/*15*/ CV_CALL( cvSetZero( dst ));
/*16*/ EXIT;
/*17*/ }
/*18*/
/*19*/ rows = src->rows;
/*20*/ cols = src->cols;
/*21*/
/*22*/ if( src->data.ptr != dst->data.ptr )
/*23*/ mean = dst;
/*24*/ else
/*25*/ CV_CALL( mean = cvCreateMat( rows, cols, CV_8UC1 ));
/*26*/
/*27*/ CV_CALL( cvSmooth( src, mean, method == CV_ADAPTIVE_THRESH_MEAN_C ?
/*28*/ CV_BLUR : CV_GAUSSIAN, size, size ));
/*29*/ if( maxValue > 255 )
/*30*/ maxValue = 255;
/*31*/
/*32*/ if( type == CV_THRESH_BINARY )
/*33*/ for( i = 0; i < 768; i++ )
/*34*/ tab[i] = (uchar)(i - 255 > -idelta ? maxValue : 0);
/*35*/ else
/*36*/ for( i = 0; i < 768; i++ )
/*37*/ tab[i] = (uchar)(i - 255 <= -idelta ? maxValue : 0);
/*38*/
/*39*/ for( i = 0; i < rows; i++ )
/*40*/ {
/*41*/ const uchar* s = src->data.ptr + i*src->step;
/*42*/ const uchar* m = mean->data.ptr + i*mean->step;
/*43*/ uchar* d = dst->data.ptr + i*dst->step;
/*44*/
/*45*/ for( j = 0; j < cols; j++ )
/*46*/ d[j] = tab[s[j] - m[j] + 255];
/*47*/ }
/*48*/
/*49*/ __END__;
/*50*/
/*51*/ if( mean != dst )
/*52*/ cvReleaseMat( &mean );
}

重点在下面3处地方:
第27-28行,调用函数cvSmooth函数对src图像平滑,并将结果存储在mean矩阵中;
第32-37行,根据type和delta值为“映射数组”tab赋值;
第39-47行,将src与mean矩阵对应元素相减,所得差异值再加上偏移值255作为映射表tab的索引获取输出dst矩阵对应元素的值。

在做进一步讨论前先来看两张图片:

 

《cvAdaptiveThreshold的探讨》 图像mask1 mask1.JPG (3.89 KB) 被浏览 546 次
首先来看mask1,这是利用一个3×3的掩模从源图像中取出的子矩阵,设白色部分的值为255,黑色部分为0,此时中心点(绿圈标明)像素值为255,则对此掩模使用CV_BLUR或是CV_GAUSSIAN平滑方法求得的新中心点像素值,必定比其原来像素值低( < 255 )。

《cvAdaptiveThreshold的探讨》 图像mask2 mask2.JPG (3.64 KB) 被浏览 544 次
再来看mask2,这是中心点像素值为0,通过平滑方法求得的新中心点像素值,必定比原来像素值高( > 0 )。
此算法也正是利用这样的差异值(原像素值-平滑后像素值,即最后部分代码中的“src-mean”),来定位边缘的位置。

最后就是tab数组和参数idelta(param1)的意义:
tab数组实际上是一个映射表,它指明了当差异值小于<对应于参数CV_THRESH_BINARY>或大于<对应于参数CV_THRESH_BINARY>-idelta时输出像素值为0,大于(小于)-idelta时输出像素值为255。由于差异值可以是负值,而数组的下标却不行,所以作者将tab数组的下标偏移了+255,即当差异值为-10时,对应的tab数组下标为-10+255=245;差异为+10时,下标为265。
至于参数idelta(param1),如果取得比较小,则所提取边缘就粗,取得大,边缘就细。另外如果取的是正值,那么提取的将是物体的内边缘,即所有边缘上的点都处于物体内部,并且平滑区域的像素值为255,边缘区域为0;如果取的是负值,提取的就是外边缘,此边缘位于物体外部,平滑区域的像素值为0,边缘区域为255。另外阈值类型CV_THRESH_BINARY / CV_THRESH_BINARY_INV选取的不同也会影响平滑区域和边缘区域的像素值,前面的陈述假定的都是CV_THRESH_BINARY类型。

有趣的是,我也留意了一下资料介绍的固定阈值化函数cvThreshold,其实它也有一种自适应阈值方法,那就是OSTU(大津法)!!只需要设定参数thresh_type的值为CV_THRESH_OTSU即可。

我使用的是1.0版的OpenCV,不知其他版本的情况如何,以上描述若有任何错误或遗漏,希望大家指正,同时也欢迎大家讨论!谢谢!

以下是我对cvAdaptiveThreshold测试的图片:
测试所用部分参数值:
adaptive_method=CV_ADATIVE_THRESH_MEAN_C,threshold_type=CV_THRESH_BINARY,maxValue=255,
block_size=7,param1=±7

《cvAdaptiveThreshold的探讨》 此图片由三个图片并排连成(由于帖子最多只允许3个附件- -!,只有出此下策了)。其中最左边的为原图,中间的使用正的param1值,最右边的使用负的param1值。

点赞