cvThreshold是OpenCV(Version2.4.9)中针对图像二值化的一个API,本文首先贴出小编的一个简单的源程序,之后对其源码实现进行分析。
cvThreshold函数的一个简单例子:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
//#pragma comment(lib, "opencv_core249d.lib")
//#pragma comment(lib, "opencv_highgui249d.lib")
//#pragma comment(lib, "opencv_imgproc249d.lib")
using namespace std;
using namespace cv;
int main()
{
// 定义窗口名称
char *pSrcWnd = "SrcWindow";
char *pDstWnd = "DstWindow";
Mat Src; // 读取代二值化的图像
Mat Dst; // 不需要分配内存,threshold函数内部会分配
// 以灰度图方式读取图片
Src = imread("flower.jpg", CV_LOAD_IMAGE_GRAYSCALE);
double dbThresh = 160, dbMaxVal = 255;
// 传入BGR三通道图也可以,为了直观看到效果,此处会传入单通道灰度图
threshold(Src, Dst, dbThresh, dbMaxVal, CV_THRESH_BINARY);
imshow(pSrcWnd, Src);
imshow(pDstWnd, Dst);
waitKey(0);
return 0;
}
下面是我使用VS2015社区版运行的结果(Sorry, baidu下载的图片,有水印,哈哈哈):
采用阈值160,基本上把叶子分割出来了,由于花的颜色与背景(白色)比较接近,无能为力!
接下来进入源码分析部分:
程序内部先进行一些二值化类型的判断,在这里就不一一阐述了,有兴趣的可以参考《学习OpenCV》一书中理解。
下面是几种类型的简单说明,摘录自OpenCV的type_c.h头文件:
/* Threshold types */
enum
{
CV_THRESH_BINARY =0, /* value = value > threshold ? max_value : 0 */
CV_THRESH_BINARY_INV =1, /* value = value > threshold ? 0 : max_value */
CV_THRESH_TRUNC =2, /* value = value > threshold ? threshold : value */
CV_THRESH_TOZERO =3, /* value = value > threshold ? value : 0 */
CV_THRESH_TOZERO_INV =4, /* value = value > threshold ? 0 : value */
CV_THRESH_MASK =7,
CV_THRESH_OTSU =8 /* use Otsu algorithm to choose the optimal threshold value;
combine the flag with one of the above CV_THRESH_* values */
};
之后进入到一个并发(注意与并行的区别)编程启动接口,这是一个模板函数,如下:
// 函数原型:CV_EXPORTS void parallel_for_(const Range& range, const ParallelLoopBody& //body, double nstripes=-1.);
parallel_for_(Range(0, dst.rows),
ThresholdRunner(src, dst, thresh, maxval, type),
dst.total()/(double)(1<<16));
传入三个参数,其中最重要的是第二个参数,这是一个类,继承自ParallelLoopBodyle,这是一个并行循环体抽象类,在OpenCV许多函数中都有它的派生类,下面是该类的定义部分:
// a base body class
class CV_EXPORTS ParallelLoopBody
{
public:
virtual ~ParallelLoopBody();
virtual void operator() (const Range& range) const = 0;
};
一般在派生类的构造函数中对成员变量初始化,在operator()函数中实现其OpenCV函数的主要功能,如下:
void operator () ( const Range& range ) const
{
int row0 = range.start;
int row1 = range.end;
Mat srcStripe = src.rowRange(row0, row1);
Mat dstStripe = dst.rowRange(row0, row1);
if (srcStripe.depth() == CV_8U)
{
thresh_8u( srcStripe, dstStripe, (uchar)thresh, (uchar)maxval, thresholdType );
}
else if( srcStripe.depth() == CV_16S )
{
thresh_16s( srcStripe, dstStripe, (short)thresh, (short)maxval, thresholdType );
}
else if( srcStripe.depth() == CV_32F )
{
thresh_32f( srcStripe, dstStripe, (float)thresh, (float)maxval, thresholdType );
}
}
由于我们的图像类型是CV_8U,所以进入到以下函数中,为了减少篇幅,此处我们只保留了CV_THRESH_BINARY二值化类型的程序。
static void
thresh_8u( const Mat& _src, Mat& _dst, uchar thresh, uchar maxval, int type )
{
int i, j, j_scalar = 0;
uchar tab[256];
Size roi = _src.size();
roi.width *= _src.channels();
if( _src.isContinuous() && _dst.isContinuous() )
{
roi.width *= roi.height;
roi.height = 1;
}
#ifdef HAVE_TEGRA_OPTIMIZATION
if (tegra::thresh_8u(_src, _dst, roi.width, roi.height, thresh, maxval, type))
return;
#endif
switch( type )
{
case THRESH_BINARY:
for( i = 0; i <= thresh; i++ )
tab[i] = 0;
for( ; i < 256; i++ )
tab[i] = maxval;
break;
default:
CV_Error( CV_StsBadArg, "Unknown threshold type" );
}
#if CV_SSE2
if( checkHardwareSupport(CV_CPU_SSE2) )
{
__m128i _x80 = _mm_set1_epi8('\x80');
__m128i thresh_u = _mm_set1_epi8(thresh);
__m128i thresh_s = _mm_set1_epi8(thresh ^ 0x80);
__m128i maxval_ = _mm_set1_epi8(maxval);
j_scalar = roi.width & -8;
for( i = 0; i < roi.height; i++ )
{
const uchar* src = (const uchar*)(_src.data + _src.step*i);
uchar* dst = (uchar*)(_dst.data + _dst.step*i);
switch( type )
{
case THRESH_BINARY:
for( j = 0; j <= roi.width - 32; j += 32 )
{
__m128i v0, v1;
v0 = _mm_loadu_si128( (const __m128i*)(src + j) );
v1 = _mm_loadu_si128( (const __m128i*)(src + j + 16) );
v0 = _mm_cmpgt_epi8( _mm_xor_si128(v0, _x80), thresh_s );
v1 = _mm_cmpgt_epi8( _mm_xor_si128(v1, _x80), thresh_s );
v0 = _mm_and_si128( v0, maxval_ );
v1 = _mm_and_si128( v1, maxval_ );
_mm_storeu_si128( (__m128i*)(dst + j), v0 );
_mm_storeu_si128( (__m128i*)(dst + j + 16), v1 );
}
for( ; j <= roi.width - 8; j += 8 )
{
__m128i v0 = _mm_loadl_epi64( (const __m128i*)(src + j) );
v0 = _mm_cmpgt_epi8( _mm_xor_si128(v0, _x80), thresh_s );
v0 = _mm_and_si128( v0, maxval_ );
_mm_storel_epi64( (__m128i*)(dst + j), v0 );
}
break;
#endif
if( j_scalar < roi.width )
{
for( i = 0; i < roi.height; i++ )
{
const uchar* src = (const uchar*)(_src.data + _src.step*i);
uchar* dst = (uchar*)(_dst.data + _dst.step*i);
j = j_scalar;
#if CV_ENABLE_UNROLLED
for( ; j <= roi.width - 4; j += 4 )
{
uchar t0 = tab[src[j]];
uchar t1 = tab[src[j+1]];
dst[j] = t0;
dst[j+1] = t1;
t0 = tab[src[j+2]];
t1 = tab[src[j+3]];
dst[j+2] = t0;
dst[j+3] = t1;
}
#endif
for( ; j < roi.width; j++ )
dst[j] = tab[src[j]];
}
}
}
在上面的这个函数中,OpenCV做了几个优化:
1, 判断图像内存是否连续,如果是,则把图像的Col = Col*Row*通道数(灰度图通道是1), Row = 1,目的是在遍历图像数据是,减少for循环的跳转时间,
2,接下里判断有无定义宏标志:HAVE_TEGRA_OPTIMIZATION,如果有,加入到另一个优化函数中(我对这个宏定义的优化原理不是太懂,希望知道的大神能指点一下。)
3, 判断CPU是否支持SSE2指令集(单指令多数据功能),一般intel自家的CPU都支持,在OPenCV中还有(MMX, SSE,AVX)这几种指令集,不得不说AVX指令集,想对比于SSE一次加载128Bits,AVX能加载256Bits,也就是32个字节数据,能大大加强处理图像数据的效率,最后我会给一个提升效率数据的简单比较,不过这次OpenCV官方没有采用AVX指令集,而是SSE2。
4, 第三点的指令集一次处理的数据量是16字节,大家可能有疑问了,万一我的图像数据不是16的倍数,岂不完了,放心吧!OpenCV官方不会让这样的BUG出现的,最后的一小段代码就扮演着这样的捡漏功能,不过这一段代码也做了一定的优化,一次处理的数据是四个字节,最后低于四个字节的数据采用一次处理一个字节的方式来处理完整个图像,在每次处理四个字节的循环中,并没有判断与阈值的大小,二是采用查表法(tab[]数组),第一次看到这样巧妙的表达,哦,不对,是第二次,第一次是在学习UCOSII嵌入式操作系统的看到的,记得是为了确定每次查找任务的优先级时间固定,(偏题了,哈哈)。
最后给出采用for循环遍历图像(有无使用查表法)与OpenCV指令集优化的时间对比图(两种方式各循环100次):
从表中可以看出,SEE2指令集优化效果挺好的,for循环查表法相对于for+if判断还是提升了零点几个毫秒。
PS:图像大小 = 600x400