引言

   图像分割是计算机视觉中最常见的任务之一,它的目标是将一幅图像划分为前景和背景,或者更细致的多个区域。无论是工业检测中的缺陷识别、医学图像中的病灶提取,还是OCR中的字符定位,阈值分割(Thresholding)都扮演着极其重要的角色。

    在所有分割方法中,阈值法因其简单、直观、计算效率高而被广泛应用。最基本的思想是:选择一个或多个阈值,将灰度图像分为若干区域。然而,如何选取一个“合适”的阈值,并针对不同场景(如光照不均、前景占比未知、多类目标存在等)进行改进,始终是研究和实践中的关键问题。

本文将系统地介绍几类典型的阈值分割方法:

  1. 二分类阈值:包括固定阈值、P-Tile 方法、迭代阈值、Otsu 大津法。

  2. 改进的全局阈值策略:结合平滑、边缘信息,直方图均衡化改善离群点等改进阈值策略。

  3. 自适应阈值:应对光照不均、复杂背景的情况。

    在介绍数学原理的同时,本文还将给出基于 C++ 与 OpenCV 的完整实现代码,以便读者快速上手实验与扩展。希望通过这篇文章,你能对阈值分割有一个全面而深入的理解,并在实际项目中灵活选择合适的策略。

二分类阈值原理

    对于任意灰度图像,给定一个固定阈值T,可将像素分为前景与背景。公式表达为:

    f(x,y)=\left\{\begin{matrix} 255, f(x,y)\geqslant T\\ 0,f(x,y)< T \end{matrix}\right.

    很显然,该方法简单粗暴。在某些特定场合下我们可以根据先验知识设定一个比较合理固定阈值,再基于该固定阈值进行图像分割。然而在大多数情形下,我们很难找到一个合理的固定阈值,因此需要使用更多的策略。

    如我们对一个固定场景进行拍照,由于受到光照变化的影响,对于该场景没有办法使用固定阈值进行分割。但我们有拍摄场景的先验知识,如我们知道前景与背景物体大致的占比关系,此时可能使用P-Tile阈值进行理想分割。方法如下:

  • 根据固定场景的先验知识估算前景所在的比例,设定为P
  • 计算图像的灰度直方图,并归一化处理(直方图所有bin之和为1)。
  • 遍历直方图(反向),直到找到累计概率分布大于预设比例P,找到该场景下合适的阈值。

    P-Tile阈值同样也仅适用一些特殊场景。对于复杂的场景,我们需要一个自动阈值寻找策略。基于迭代的自动阈值策略可以实现自动阈值的目标,其思想如下:

  1. 将初始阈值T_{0}设置为全局灰度均值。
  2. 初步将图像分为两类(\leqslant T_{0}类和> T_{0}类),计算两类所对应的均值\mu _{0}, \mu _{1}
  3. 计算新阈值T=(\mu _{0} + \mu _{1}) / 2,将T赋值给T_{0}
  4. 回到第2步继续迭代,直到新阈值T与上一次阈值T_{0}之间的差异小于某个预设误差值(如5)。
  5. 将最后的阈值T作为该图像的最优分割阈值。

    基于迭代的自动阈值策略可以在直方图近似双峰的情形获得一个较好的阈值。我们注意到当前迭代的新阈值仅依赖上一次迭代的阈值所产生的两类图像均值,这里我们忽略掉了两类像素的数量,这样可能导致最终分割区域的类像素比例及不平衡,最终阈值可能收敛于具有少量像素的类。

    基于迭代的自动阈值策略也受到了初始值的影响,不同的初始值可能会产生不同的收敛值。我们期望所选择的初始阈值位于理想阈值所产生的两个分类的均值之间,一般情况下使用全局灰度均值进行初始化是合理的,但并不一定是最优的选择。同时,我们注意到收敛条件的设置也是主观的,不同的收敛条件会产生不同的结果。

    基于以上分析,我们讨论一种更加可靠的自动阈值选择机制:Otsu’s Method(大津法),该策略可以避免我们在前文讨论的一些问题,从而获得一个更佳的全局阈值。这是一个基于直方图的全局搜索策略,有明确的最优性准则。为了能够更好的理解Otsu,下面我给出该策略的原理性分析。以下分析可以直接跳过,他并不影响我们对后续应用的理解。

    设一幅图像的灰度级为\left \{ 0, 1, 2, 3,...,255\right \},且该图像的总像素个数为N

    直方图h(i)表示灰度级i所占的像素个数,因此有:N=\sum_{i = 0}^{255}h(i)

    归一化直方图p(i)=h(i)/N=h(i)/(\sum_{0}^{255}h(i)),使得\sum_{0}^{255}p(i)=1

    选择任意阈值t,将像素分为两类:

  • 类0:灰度级0,....,t,其像素权重为:\omega _{0}(t)=\sum_{i=0}^{t}p(i)
  • 类1:灰度级t+1,....,255,其像素权重为:\omega _{1}(t)=1 - \omega _{0}(t) ;

    基于归一化直方图,我们可以求解整个图像的全局平均灰度值:\mu = \sum_{i=0}^{255}ip(i)。通俗来说,通过归一化直方图我们知道每个灰度等级在整幅图像的占比,那么i*p(i)就是该灰度等级对均值的贡献度。遍历所有灰度等级并将所有贡献度相加就得到了整个图像的均值。

    同样,基于归一化直方图,我们如何求解类0和类1的均值呢?使用\mu_{0}^{'}(t)=\sum_{i=0}^{t}ip(i)\mu _{1}^{'}(t)=\sum_{i=t+1}^{255}ip(i)是否得到类0和类1的均值?很显然不是,这里的\mu_{1}^{'}(t)显然小于\mu,也就是说图像亮区的均值小于整幅图像的均值,这是一个悖论。

    我们仔细思考问题出在什么地方:归一化直方图!!!是的,p(i)为整幅图像的归一化直方图,因此我们可以使用\sum ip(i)的策略求解整幅图像的均值。当我们求解类0和类1的均值时,p(i)并不是对应分类的归一化直方图,因此我们需要再次进行对应类的归一化处理。

    已知类0的权重为\omega _{0}(t),则相对于类0的归一化直方图为p_{0}(i)=p(i)/ \omega _{0}(t),因此我们得到:\mu_{0}(t)=\sum_{i=0}^{t}ip_{0}(i)=\sum_{i=0}^{t}\frac{ip(i)}{\omega _{0}(t)},整理得:\mu_{0}(t)=\frac{\sum_{i=0}^{t}ip(i)}{\omega _{0}(t)}

    同理得类1的均值为:\mu _{1}(t)=\frac{\sum_{i=t+1}^{255}ip(i)}{\omega _{1}(t)}

    构造类间方差:\sigma ^{2}=\omega _{0}(t)(\mu _{0}(t) - \mu)^{2} + \omega _{1}(t)(\mu _{1}(t) - \mu)^{2},遍历所有灰度等级,寻找类间方差最大值作为最优阈值。其直观含义如下:

  • 两类均值到全局均值的距离均为最大;
  • 使用两类占比权重\omega _{0}(t),\omega _{1}(t)平衡分割区间,使得分割区间趋于均衡以避免极端分割;

    以上就是Otsu的基本思想。在计算机实现上,我们还有一些优化空间。在以上公式中,我们需要分别计算\omega _{0}(t)\mu _{0}(t)\mu _{1}(t)才能获得\sigma ^{2},通过整理公式可以进一步减少计算量,以下只给出结论:

    令m_{0}(t)=\sum_{i=0}^{t}ip(i),表示类0的灰度加权和(注意这里并非类0的均值!!!),最终可化简为:\sigma^{2}(t)=\frac{(\mu\omega_{0}(t)-m_{0}(t))^{2}}{\omega_{0}(t)(1-\omega_{0}(t))}。该公式中,\mu为全局均值只需要计算一次,在最优化过程中,只需要计算\omega _{0}(t)m_{0}(t)即可,不再需要计算\mu _{0}(t)\mu _{1}(t),从而减少了计算量。

全局阈值在应用中的改进策略

    在实际工程应用中,我们还可能使用一些图像处理策略改进自动阈值的选择。

    比如,我们可能遇到一些噪声比较大的图像。对于此类图像,如果直接使用Otsu策略寻找全局阈值可能会得到一个错误的结果,在Otsu之前进行滤波处理可以完美解决问题。

    对于某些直方图相对集中的图像数据,在Otsu之前使用直方图均衡化可以扩大类间对比度,从而获得一个较好的分割。图像增强基础:直方图均衡化与自适应直方图均衡化中详细讨论了直方图均衡化问题。

    通过将注意力集中到局部区域可能得到一个更加符合预期的分割。比如某些图像仅存在部分有效数据,我们可以仅在有效数据上计算分割阈值。典型应用如我们所讨论的眼底图像中,仅考虑圆形区域内的数据会更加合适,如下图:

    另外,结合图像梯度信息,可以获得一个与梯度相关的亮度直方图。该亮度直方图所描述的区域为梯度变化最明显的边缘区域,而在区域分割中的分割边界往往也是位于该区域中。因此,与梯度相关的亮度直方图在某些应用中能够得到更好的分割结果。

自适应阈值

    自适应阈值(Adaptive Thresholding)根据每个像素周围邻域的亮度特征动态计算阈值。换句话说,不再使用一个固定的阈值 T,而是让每个像素点拥有一个属于它自己的阈值 T(x, y)。基本策略如下:

  • 取一个大小为 S×S 的邻域窗口;
  • 计算该窗口的某种统计量(如均值、加权均值或高斯平滑后的均值);
  • 将该统计量作为阈值(可能加减一个偏移量 C)。

    在实际应用中,我们根据分割目标尺寸确定邻域窗口尺寸,使得我们可以完美的分割出目标区域。根据前景区域与背景区域之间的对比度确定偏移量C,从而实现更稳定的前景分割。对于统计量的选择,一般为邻域内的均值或者高斯加权均值,在OpenCV中已经实现好了两种策略,我们只需要根据需求选择即可。

代码实现

// 构造迭代阈值与OTSU阈值算法
void Threshold(unsigned char* src, unsigned char* dst,
		short width, short height, short channel, THRESHOLD_TYPE type)
{
    // 判断数据有效性
    if (!src || width <= 0 || height <= 0 || channel != 1) return;

	short widthstep = (width + 3) / 4 * 4;

    // 迭代最优阈值
    // 这里的初始化使用了不同策略,对于具体应用我们可以根据实际情况选择!
	if (type == TT_OPTIMAL)
	{
		// brief algorithm 
		// 1 assume that background is constructed by the cornor of image, and ohters form foregound;
		// 2 set (background + foreground) / 2 as initial threshold value;
		// 3 compute new background and foreground using threshold value until threshold value keeping constant;
		// 4 the final threshold value is the optimal threshold value, using this value to threshold image.

		// initialize average means of background and object
		long background = 0, bg_cnt = 0;
		long object = 0, ob_cnt = 0;
		long threshold = 0;

		background = (*(src + 2 * widthstep + 2) + *(src + 2 * widthstep + width - 2) +
				*(src + (height - 2) * widthstep + 2) + *(src + (height - 2) * widthstep + width - 2)) / 4;

		for (short y = 3; y < height - 2; y += 4)
		{
			for (short x = 3; x < width - 2; x += 4)
			{
				object += *(src + y * widthstep + x);
				++ob_cnt;
			}
		}
		object /= ob_cnt;

		
		threshold = (background + object) / 2;

		// form histgram to improve performance
		int hist[256];
		memset(hist, 0, sizeof(int) * 256);

		for (short y = 0; y < height; ++y)
		{
			for (short x = 0; x < width; ++x)
			{
				++hist[*(src + y * widthstep + x)];
			}
		}

		// iterate to search optimal threshold
		for (;;)
		{
			background = 0, bg_cnt = 0;
			object = 0, ob_cnt = 0;

			for (short i = 0; i < threshold; ++i)
			{
				background += hist[i] * i;
				bg_cnt += hist[i];
			}

			for (short i = threshold; i < 256; ++i)
			{
				object += hist[i] * i;
				ob_cnt += hist[i];
			}

			background /= bg_cnt;
			object /= ob_cnt;

			if ((background + object) / 2 == threshold)
				break;
			else
				threshold = (background + object) / 2;
		}

		// apply threhsold 
		memset(dst, 0, widthstep * height);
		for (short y = 0; y < height; ++y)
		{
			for (short x = 0; x < width; ++x)
			{
				if (*(src + y * widthstep + x) > threshold)
					*(dst + y * widthstep + x) = 255;
			}
		}
	}

    // OTSU算法
	if (type == TT_OTSU)
	{
		// brief algorithm
		// 该算法主要思想是将图像分为前景与背景两部分,并找到类间方差最大的点作为全局阈值点。

		// obtain normalize histogram
		double hist[256];
		memset(hist, 0, sizeof(double) * 256);

		for (short y = 0; y < height; ++y)
		{
			for (short x = 0; x < width; ++x)
			{
				++hist[*(src + y * widthstep + x)];
			}
		}

		for (short i = 0; i < 256; ++i)
		{
			hist[i] /= (double)(width * height);
		}

		// obtain statistical parameter 
		double p[256];
		memset(p, 0, sizeof(double) * 256);
		p[0] = hist[0];

		for (short i = 1; i < 256; ++i)
		{
			p[i] = p[i - 1] + hist[i];
		}

		double m[256];
		memset(m, 0, sizeof(double) * 256);

		double zero = 0.9 / (double)(width * height);

		for (short i = 0; i < 256; ++i)
		{
			for (short j = 0; j <= i; ++j)
			{
				m[i] += hist[j] * j;
			}
			if (p[i] > zero) m[i] /= p[i];
		}

		double mG = 0.0;
		for (short i = 0; i < 256; ++i)
		{
			mG += hist[i] * i;
		}

		// obtain between class variance at each gray
		double max_variance = 0.0;
		short  threshold = -1;

		for (short i = 0; i < 255; ++i)
		{
			double variance = 0.0;
			if (1 - p[i] > zero)
			{
				double m2 = (mG - p[i] * m[i]) / (1 - p[i]);
				variance = p[i] * (m[i] - mG) * (m[i] - mG) + (1 - p[i]) * (m2 - mG) * (m2 - mG);
			}

			if (variance > max_variance)
			{
				max_variance = variance;
				threshold = i;
			}
		}

		// apply threhsold 
		memset(dst, 0, widthstep * height);
		for (short y = 0; y < height; ++y)
		{
			for (short x = 0; x < width; ++x)
			{
				if (*(src + y * widthstep + x) > threshold)
					*(dst + y * widthstep + x) = 255;
			}
		}
	}
}

    以上代码实现了基于迭代的最优阈值选择以及OTSU算法,实际应用可根据需求自行选择。同时,OpenCV也提供了相关函数:

double threshold( InputArray src, OutputArray dst,
                               double thresh, double maxval, int type )

    我们之前提到在ROI区域中进行二值分割,OpenCV可以通过以下代码实现:

Mat img = imread("test.jpg", IMREAD_GRAYSCALE);

// 定义一个 ROI
Rect roi(100, 100, 200, 200);
Mat img_roi = img(roi);

// 对 ROI 应用 OTSU
double otsu_thresh_val = threshold(img_roi, img_roi, 0, 255,
                                       THRESH_BINARY | THRESH_OTSU);

    对于一个非矩形的ROI(如圆形),我们可以对以上自定义函数进行适当修改即可实现。基本思路是传入一个ROI标记矩阵,然后仅在ROI对应数据上进行直方图统计与分割操作即可。

总结

    本文介绍了图二分类阈值方法(固定阈值、P-Tile、迭代阈值和Otsu大津法)及其数学原理。针对实际应用中的光照不均、噪声干扰等问题,提出了结合平滑、直方图均衡化等改进策略。文章还详细探讨了自适应阈值技术,并提供了关键的C++/OpenCV实现代码。在实际项目中,我们需要根据具体情况选择合适的策略。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐