欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

OpenCV源码阅读001-自适应阈值分割adaptiveThreshold

程序员文章站 2024-02-11 12:54:10
...

OpenCV自带的adaptiveThreshold

首先,查看一下build\doc\opencv2refman中关于adaptiveThreshold的说明:

C++: void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)

/*
 * 函数功能:图像基于局部的自适应阈值化处理
 * 输入参数:src – Source 8-bit single-channel image.	
				//源图像,8位单通道图像.			
			 maxValue – Non-zero value assigned to the pixels for which the condition is satisfied.
				//灰度最大值,满足条件时像素值被设置为此最大值.
			 adaptiveMethod – Adaptive thresholding algorithm to use, ADAPTIVE_THRESH_MEAN_C or ADAPTIVE_THRESH_GAUSSIAN_C . 
				//自适应方法,ADAPTIVE_THRESH_MEAN_C 均值法,窗口内像素权重相同;ADAPTIVE_THRESH_GAUSSIAN_C 高斯法,窗口内像素权重服从高斯分布.
			 thresholdType – Thresholding type that must be either THRESH_BINARY or THRESH_BINARY_INV
				//二值化类型,THRESH_BINARY 灰度值>阈值的像素被置为maxValue;THRESH_BINARY_INV 灰度值≤阈值的像素被置为maxValue
			 blockSize – Size of a pixel neighborhood that is used to calculate a threshold value for the pixel: 3, 5, 7, and so on
				//像素的邻域窗口大小,必须为奇数且大于1,如3,5……
			 C – Constant subtracted from the mean or weighted mean
				//常量,在源图中一个像素的邻域窗口的均值或者加权均值求取之后减去常量C才是最终目标图中的像素值
 * 输出参数:dst – Destination image of the same size and the same type as src .	
				//目标图像,其size和type和源图像一样
 * 备注:
			 where T(x,y) is a threshold calculated individually for each pixel.
				For the method ADAPTIVE_THRESH_MEAN_C , the threshold value T(x,y) is a mean of the blockSize*blockSize
			 neighborhood of (x, y) minus C . 
			 对于方法ADAPTIVE_THRESH_MEAN_C,二值化之后的像素值T(x,y)等于源图像点(x,y)在邻域窗口blockSize*blockSize
			 内的均值减去常数C.
			 For the method ADAPTIVE_THRESH_GAUSSIAN_C , the threshold value T(x, y) is a weighted sum (cross-correlation with
			 a Gaussian window) of the blockSize*blockSize neighborhood of (x, y) minus C.
			 对于方法ADAPTIVE_THRESH_GAUSSIAN_C,二值化之后的像素值T(x,y)等于源图像点(x,y)在邻域窗口blockSize*blockSize
			 内的高斯加权均值减去常数C.
*/

其次,查看sources\modules\imgproc\include\opencv2\imgproc\imgproc.hpp中函数声明:

//! applies variable (adaptive) threshold to the image
CV_EXPORTS_W void adaptiveThreshold( InputArray src, OutputArray dst,
                                     double maxValue, int adaptiveMethod,
                                     int thresholdType, int blockSize, double C );

最后,在sources\modules\imgproc\src\thresh.cpp中有函数实现:

//函数实现									 
void cv::adaptiveThreshold( InputArray _src, OutputArray _dst, double maxValue,
                            int method, int type, int blockSize, double delta )
{
	//输入有效性检查
    Mat src = _src.getMat();
    CV_Assert( src.type() == CV_8UC1 );	//输入图像的类型必须是CV_8UC1
    CV_Assert( blockSize % 2 == 1 && blockSize > 1 );//输入的窗口大小必须是奇数,并且大于1
    Size size = src.size();

    _dst.create( size, src.type() );
    Mat dst = _dst.getMat();

    if( maxValue < 0 )	//输入的最大值必须大于0
    {
        dst = Scalar(0);
        return;
    }

    Mat mean;	//均值图,计算均值或者高斯加权均值

    if( src.data != dst.data )
        mean = dst;

    if( method == ADAPTIVE_THRESH_MEAN_C )	//均值法
        boxFilter( src, mean, src.type(), Size(blockSize, blockSize),
                   Point(-1,-1), true, BORDER_REPLICATE );
    else if( method == ADAPTIVE_THRESH_GAUSSIAN_C )	//高斯加权均值法
        GaussianBlur( src, mean, Size(blockSize, blockSize), 0, 0, BORDER_REPLICATE );
    else
        CV_Error( CV_StsBadFlag, "Unknown/unsupported adaptive threshold method" );

    int i, j;
    uchar imaxval = saturate_cast<uchar>(maxValue);	//将maxValue由double类型转换为uchar型
		
    int idelta = type == THRESH_BINARY ? cvCeil(delta) : cvFloor(delta);//将idelta由double类型转换为int型	
    
	uchar tab[768];	//保存查找表,为什么是768(256 * 3)?? sdata[j] - mdata[j] + 255的最大值不应该是256*2吗?
	if( type == CV_THRESH_BINARY )
		for( i = 0; i < 768; i++ )
			tab[i] = (uchar)(i - 255 > -idelta ? imaxval : 0);	 
	else if( type == CV_THRESH_BINARY_INV )
		for( i = 0; i < 768; i++ )
			tab[i] = (uchar)(i - 255 <= -idelta ? imaxval : 0);
	else
		CV_Error( CV_StsBadFlag, "Unknown/unsupported threshold type" );

	if( src.isContinuous() && mean.isContinuous() && dst.isContinuous() )
	{
		size.width *= size.height;
		size.height = 1;
	}

	for( i = 0; i < size.height; i++ )
	{
		const uchar* sdata = src.data + src.step*i;
		const uchar* mdata = mean.data + mean.step*i;
		uchar* ddata = dst.data + dst.step*i;

		for( j = 0; j < size.width; j++ )
			ddata[j] = tab[sdata[j] - mdata[j] + 255];
	}
}

注意:在使用adaptiveThreshold的时候,最重要的参数就是blockSize的设定,这个参数最好设定根据图像高宽自适应调整,而不是设定一个固定的值。

其实,在接触自带的adaptiveThreshold之前,我自己写了一个分块阈值分割的函数,我写的函数是带图像掩码的,可以只针对图像的有效区域进行阈值分割,因为很多实际使用的时候,我们并不关心整幅图像,而是关心图中某一部分,这时候就需要加图像掩码了。我写的函数中还可以对均值进行比例系数的调整,因为有时候求出的均值并不能很好的设定阈值,乘以一定的系数之后可以设置更合适的阈值。最近,经过同事程老大的提醒,接触到adaptiveThreshold,看到了源码之后,觉得这个函数很不错,比我自己写的更加规范,而且经过测试速度更快。但是,它还不够好。于是,我自己添加了图像掩码和阈值比例系数,也就是下面即将提到的改进版的adaptiveThreshold。

改进的adaptiveThreshold

/*
 * 函数功能:自适应阈值分割,在OpenCV自带的adaptiveThreshold基础上加了图像掩码和比例系数
 * 输入参数:IN InputArray _src	输入图像
			  IN InputArray _mask	输入图像掩码(255有效,0无效)
			  IN double maxValue		输入满足阈值条件时像素取值
			  IN double invalidValue	输入图像掩码无效区域时像素取值
			  IN int method		输入均值方法
			  IN int type		输入阈值判断类型
			  IN int blockSize	输入窗口大小
			  IN double ratio	输入均值比例系数
			  IN double delta	输入偏移常量
 * 输出参数:OUT OutputArray _dst	输出二值化图像
 * 返 回 值:-1	输入图像有误
			  -2	输入图像掩码有误
			  -3	输入满足阈值条件时像素取值有误
			  -4	输入图像掩码无效区域时像素取值有误
			  -5	输入均值方法有误
			  -6	输入阈值判断类型有误
			  -7	输入窗口大小有误
			  -8	输入比例系数有误
			   1	正常输出
 * 计算公式:
	if(type == CV_THRESH_BINARY)
		Dst(i,j) = maxValue   if mask(i, j) == 255 && Src(i,j) > T
		Dst(i,j) = 0		  if mask(i, j) == 255 && Src(i,j) ≤ T
		Dst(i,j) = invalidValue if mask(i, j) == 0
	if(type == CV_THRESH_BINARY_INV)
		Dst(i,j) = maxValue   if mask(i, j) == 255 && Src(i,j) ≤ T
		Dst(i,j) = 0		  if mask(i, j) == 255 && Src(i,j) >T
		Dst(i,j) = invalidValue if mask(i, j) == 0

	阈值T = (Mean(blockSize * blockSize) * ratio - delta) 
	其中计算Mean(blockSize * blockSize)有两种方法:
	method == ADAPTIVE_THRESH_MEAN_C	平均值法
	method == ADAPTIVE_THRESH_GAUSSIAN_C高斯加权均值法
	
*/
int SingleThickPointer::adaptiveThresholdWithMask(IN const cv::Mat &src,		//输入图像
												  IN cv::Mat &mask,		//输入图像掩码(255有效,0无效)
												  IN double maxValue,		//输入满足阈值条件时像素取值
												  IN double invalidValue,	//输入图像掩码无效区域时像素取值
												  IN int method,			//输入均值方法
												  IN int type,				//输入阈值判断类型
												  IN int blockSize,		//输入窗口大小
												  IN double ratio,			//输入均值比例系数
												  IN double delta,			//输入偏移常量
												  OUT cv::Mat &dst)	//输出二值化图像
{
	//输入有效性检查
	if(src.empty() || src.type() != CV_8UC1)
		return -1;	//输入图像有误

	if(mask.empty())
		mask = cv::Mat(src.size(), src.type(), cv::Scalar(255));
	else
	{
		if(mask.size() != src.size() || mask.type() != CV_8UC1)
			return -2;	//输入图像掩码有误
	}

	if(maxValue < 0)
		return -3;	//输入满足阈值条件时像素取值有误

	if(invalidValue < 0)
		return -4;	//输入图像掩码无效区域时像素取值有误

	if(method != ADAPTIVE_THRESH_MEAN_C && method != ADAPTIVE_THRESH_GAUSSIAN_C)
		return -5;	//输入均值方法有误

	if(type != CV_THRESH_BINARY && type != CV_THRESH_BINARY_INV)
		return -6;	//输入阈值判断类型有误

	if(blockSize <= 1 || blockSize % 2 == 0)
		return -7;	//输入窗口大小有误

	if(ratio < DBL_EPSILON)
		return -8;	//输入比例系数有误

	Size size = src.size();
	Mat _dst(size, src.type());
	
	Mat mean;	//存放均值

	if( src.data != _dst.data )	
		mean = _dst;

	if( method == ADAPTIVE_THRESH_MEAN_C )	//平均值法
		boxFilter( src, mean, src.type(), Size(blockSize, blockSize), Point(-1,-1), true, BORDER_REPLICATE );
	else if( method == ADAPTIVE_THRESH_GAUSSIAN_C )	//高斯加权均值法
		GaussianBlur( src, mean, Size(blockSize, blockSize), 0, 0, BORDER_REPLICATE );
	//else	//输入的均值方法有误
	//	CV_Error( CV_StsBadFlag, "Unknown/unsupported adaptive threshold method" );

	int i, j;
	uchar imaxval = saturate_cast<uchar>(maxValue);	//将maxValue由double类型转换为uchar型
	int idelta = type == THRESH_BINARY ? cvCeil(delta) : cvFloor(delta);	//将idelta由double类型转换为int型

	if( src.isContinuous() && mean.isContinuous() && _dst.isContinuous() )
	{
		size.width *= size.height;
		size.height = 1;
	}

	for( i = 0; i < size.height; i++ )
	{
		const uchar* sdata = src.data + src.step * i;		//指向源图
		const uchar* maskdata = mask.data + mask.step * i;	//指向掩码图
		const uchar* mdata = mean.data + mean.step * i;		//指向均值图
		uchar* ddata = _dst.data + _dst.step * i;	//指向输出图

		for( j = 0; j < size.width; j++ )
		{
			double Thresh = mdata[j] * ratio - idelta;	//阈值
			if(CV_THRESH_BINARY == type)	//S>T时为imaxval
			{				
				if(255 == maskdata[j])	//掩码有效区域
					ddata[j] = sdata[j] > Thresh ? imaxval : 0;	
				else
					ddata[j] = invalidValue;
			}
			else if(CV_THRESH_BINARY_INV == type)	//S≤T时为imaxval
			{
				if(255 == maskdata[j])	//掩码有效区域
					ddata[j] = sdata[j] > Thresh ?  0 : imaxval;
				else
					ddata[j] = invalidValue;
			}
			//else
			//	CV_Error( CV_StsBadFlag, "Unknown/unsupported threshold type" );
		}
	}

	dst = _dst.clone();

	return 1;
}

改进版之后添加了三个参数:

IN InputArray _mask 输入图像掩码(255有效,0无效)

IN double invalidValue 输入图像掩码无效区域时像素取值

IN double ratio 输入均值比例系数

关于计算公式上面函数说明解释了,由于第一次写博客,不会使用公式编辑,所以只能这样了。下面展示一下效果图。

效果图展示

OpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThreshold

原图                                                自带的adaptiveThreshold效果图

OpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThreshold

图像掩码                                         改进之后的adaptiveThreshold效果图(invalidValue = 255, ratio = 1)

OpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThreshold

原图                                自带                            图像掩码                        改进版(invalidValue = 0, ratio = 1)

OpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThreshold

原图                                                                      自带

OpenCV源码阅读001-自适应阈值分割adaptiveThresholdOpenCV源码阅读001-自适应阈值分割adaptiveThreshold

图像掩码                                                                    改进的(invalidValue = 0, ratio = 1)

参考博客

关于adaptiveThreshold,其实我用到它主要是处理一幅光照不均匀的图像,光照不均匀的图像用全局阈值效果很差。下面的这些帖子也都是针对光照不均匀情况的处理。每一种处理都不能适应所有的情况,如果图像光照较为均匀,也许自适应阈值法的效果没有全局的好。大家在使用的时候结合自己的实际情况分析更好。

1 https://blog.csdn.net/XiaoHeiBlack/article/details/53106087?locationNum=5&fps=1
大米
看完这篇博客,我的想法是基本原理和OpenCV自带的adaptiveThreshold思路差不多
src(i,j) - mean(block * block) > T1 等价于 src > mean(block * block) + T1

2 https://blog.csdn.net/kk55guang2/article/details/78475414
橡皮加硬币

这篇博客关于分块阈值的思路阐述的很清晰


3 https://blog.csdn.net/u013162930/article/details/47755363

文本

完结。

今天是自己第一次写博客,如果有错误的地方,欢迎大家指出。如需转载,请附上此文地址说明是转载的。花了接近两个小时整理的,希望你不要随意窃取我的劳动成果。谢谢!

相关标签: 阈值分割