Bootstrap

计算机视觉基础-图像分割(阈值化)

一、前言

图像分割是指根据灰度、彩色、空间纹理、几何形状等特征把图像划分成若干个互不相交的区域,使得这些特征在同一区域内表现出一致性或相似性,而在不同区域间表现出明显的不同。简单的说就是在一副图像中,把目标从背景中分离出来。对于灰度图像来说,区域内部的像素一般具有灰度相似性,而在区域的边界上一般具有灰度不连续性

一般来说,图像分割是进行图像分析特征提取模式识别之前的必要的图像预处理过程。到目前为止,图像分割可以分为两大方向:传统分割方法和深度学习的分割方法。
传统分割算法主要有4种,分为:

  • 基于阈值的分割方法
  • 基于区域的图像分割方法
  • 基于边缘检测的分割方法
  • 结合特定工具的图像分割算法(小波变换、遗传算法等)

深度学习的分割方法主要有:

  • 基于特征编码(feature encoder based):VGGnet、ResNet以及改进模型等
  • 基于区域选择(regional proposal based):R-CNN、Fast R-CNN、Faster R-CNN、Mask R-CNN以及改进模型等
  • 基于RNN的图像分割:ReSeg模型等
  • 基于上采样/反卷积的分割方法:FCN、SetNet
  • 基于提高特征分辨率的分割方法:Dilated Convolution模式
  • 基于特征增强的分割方法
  • 基于CRF/MRF的方法

下面介绍传统数字图像分割方法中的基于阈值化的方法。

二、阈值化图像分割

2.1 原理

阈值化图像分割是通过设定不同的特征阈值(Feature Threshold),把图像象素点分为若干类,这些特征阈值通常来自原始图像的灰度或彩色特征

图像阈值化的目的是要按照灰度级,对像素集合进行一个划分,得到的每个子集形成一个与现实景物相对应的区域,各个区域内部具有一致的属性,而相邻区域不具有这种一致属性。这样的划分可以通过从灰度级出发选取一个或多个阈值来实现。单个阈值时,对于原始图像 f ( x , y ) f(x,y) f(xy),按照一定的准则找到特征值T,可将图像分割为黑(灰度值为0)白(灰度值为1)两个部分,即为我们通常所说的图像二值化,这样就可以单独处理我们感兴趣的黑色或白色区域了,公式可表示为:
在这里插入图片描述    在这里插入图片描述

可见,阈值分割算法的关键是按一定的准则确定阈值,如果能确定一个合适的阈值就可准确地将图像分割开来。阈值确定后,将阈值与像素点的灰度值逐个进行比较,从而得到阈值化图像。

阈值分割的优点计算简单运算效率较高速度快,并且特别适用于目标和背景占据不同灰度级范围的图像

根据《数字图像处理(第三版)》和网上相关博客参考,介绍一种常用的阈值化图像分割方法,包括全局阈值、自适应阈值、最佳阈值及改进方法等等。

2.2 基本全局阈值处理

一般选取阈值就是图像直方图的视觉检测。将区分度大的两个灰度级部分之间进行划分,取T为阈值来分开它们。在此基础上学习一种自动地选择阈值的算法,方法如下:

  1. 针对全局阈值选择初始估计值 T T T
  2. 用T分割图像,G1是所有灰度值大于T的像素组成,G2是所有灰度值小于等于T的像素组成;
  3. 3.分别计算G1和G2区域内的平均灰度值 m 1 m1 m1 m 2 m2 m2
  4. 4.计算出新的阈值:
    T = 1 2 ( m 1 + m 2 ) T=\frac{1}{2}(m1+m2) T=21(m1+m2)
  5. 重复步骤2.-4.,直到在连续的重复中,T的差异比预先设定的参数小为止;

2.3 OTSU(最大类间方差法)

OTSU算法也称最大类间差法,有时也称之为大津算法,由大津于1979年提出。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。

OSTU算法的特点:

  • 优点:计算简单,不受图像亮度和对比度的影响
  • 缺点:对图像噪声敏感;只能针对单一目标分割;当目标和背景大小比例悬殊、类间方差函数可能呈现双峰或者多峰,这个时候效果不好。

原理:
假设一幅大小为 M × N M×N M×N像素的数字图像有 L ( 0 , 1 , 2... , L − 1 ) L({0,1,2...,L-1}) L0,1,2...,L1个不同的灰度级, n i n_i ni表示灰度级为 i i i的像素数,则灰度级概率可表示为为: p i = n i / M N p_i=n_i/MN pi=ni/MN,且
∑ i = 1 L − 1 p i = 1 , p i ⩾ 0 \sum_{i=1}^{L-1}p_{i}=1,p_i\geqslant 0 i=1L1pi=1,pi0

现在,若可选择一个阈值 T ( k ) = k ( 0 < k < L − 1 ) T(k)=k(0<k<L-1) T(k)=k(0<k<L1),使得它把输入图像阈值化处理为两类 C 1 C_1 C1 C 2 C_2 C2,其中 C 1 C_1 C1由图像中灰度值在范围 [ 0 , k ] [0,k] [0,k]内的所有像素组成, C 2 C_2 C2由灰度值在范围 [ k + 1 , L − 1 ] [k+1,L-1] [k+1,L1]内的所有像素组成,则可得像素被分为 C 1 、 C 2 C_1、C_2 C1C2中的概率 P 1 ( k ) 、 P 2 ( k ) P_1(k)、P_2(k) P1(k)P2(k)分别为:
P 1 ( k ) = ∑ i = 0 k p i P_1(k)=\sum_{i=0}^{k}p_i P1(k)=i=0kpi P 2 ( k ) = ∑ i = k + 1 L − 1 p i = 1 − P 1 ( k ) P_2(k)=\sum_{i=k+1}^{L-1}p_i=1-P_1(k) P2(k)=i=k+1L1pi=1P1(k)
根据贝叶斯公式,可得分配到 C 1 、 C 2 C_1、C_2 C1C2的像素的平均灰度值为:
m 1 ( k ) = ∑ i = 0 k i P ( i / C 1 ) = ∑ i = 0 k i P ( C 1 / i ) P ( i ) / P ( C 1 ) = 1 P 1 ( k ) ∑ i = 0 k i p i m_1(k)=\sum_{i=0}^{k}iP(i/C_1)=\sum_{i=0}^{k}iP(C_1/i)P(i)/P(C_1)=\frac{1}{P_1(k)}\sum_{i=0}^{k}ip_i m1(k)=i=0kiP(i/C1)=i=0kiP(C1/i)P(i)/P(C1)=P1(k)1i=0kipi

m 2 ( k ) = ∑ j = 0 k j P ( j / C 2 ) = ∑ j = k + 1 L − 1 j P ( C 2 / j ) P ( j ) / P ( C 2 ) = 1 P 2 ( k ) ∑ j = k + 1 L − 1 j p j m_2(k)=\sum_{j=0}^{k}jP(j/C_2)=\sum_{j=k+1}^{L-1}jP(C_2/j)P(j)/P(C_2)=\frac{1}{P_2(k)}\sum_{j=k+1}^{L-1}jp_j m2(k)=j=0kjP(j/C2)=j=k+1L1jP(C2/j)P(j)/P(C2)=P2(k)1j=k+1L1jpj
其中, P 1 ( k ) P_1(k) P1(k) P 2 ( k ) P_2(k) P2(k)由上式给出, P ( i / C 1 ) P(i/C_1) P(i/C1)项是灰度值 i i i得概率, i i i来自 C 1 C_1 C1类; P ( j / C 2 ) P(j/C_2) P(j/C2)项是灰度值 j j j得概率, j j j来自 C 2 C_2 C2类。
设灰度级 k k k的所有灰度级累加均值 m m m,公式为:
m = ∑ i = 0 k i p i m=\sum_{i=0}^{k}ip_i m=i=0kipi
则整个图像的平均灰度(全局均值) 可由下式给出:
m G = ∑ i = 0 L − 1 i p i m_G=\sum_{i=0}^{L-1}ip_i mG=i=0L1ipi

再根据全局均值和阈值分割后的两部分均值之间的关系,有:
{ P 1 m 1 + P 1 m 1 = m G P 1 + P 2 = 1 \left\{\begin{matrix} P_1m_1+P_1m_1=m_G\\ P_1+P_2=1 \end{matrix}\right. {P1m1+P1m1=mGP1+P2=1
为了清楚说明,暂时忽略了阈值 k k k,则可以根据方差的概念,得到类间方差表达式为:
σ 2 = P 1 ( m 1 − m G ) 2 + P 2 ( m 2 − m G ) 2 \sigma^2=P_1(m_1-m_G)^2+P_2(m_2-m_G)^2 σ2=P1(m1mG)2+P2(m2mG)2

化简并消去 m 2 m_2 m2 P 2 P_2 P2项,可得:
σ 2 = ( m G P 1 − m ) 2 P 1 ( 1 − P 1 ) \sigma^2=\frac{(m_GP_1-m)^2}{P_1(1-P_1)} σ2=P1(1P1)(mGP1m)2

遍历图像灰度级0-255,可求出使得类间方差 σ 2 \sigma^2 σ2最大的阈值 k k k,以上就是通过Ostu方法求全局阈值的全部过程了。

在OpenCV中使用函数threshold() 来实现方框滤波操作,其函数模型如下:

double threshold(Mat InputArray src,  //原始图像
				 Mat OutputArray dst,  //目标图像
				 double thresh,  //阈值
				 double maxval,  //目标图像最大值
				 int type  //阈值分割类型
				 )

这个type就代表不同的分割类型,OpenCV有以下几种方式可以参考:
在这里插入图片描述
Otsu算法的一些改进方法:

  1. 由于噪声会在一定程度上影响阈值T的选择,当原始图像含有噪声时,可以在阈值处理前将图像进行平滑处理,消除噪声的影响。
  2. 对于处理那些位于或接近物体和背景间边缘的像素,可以通过边缘改进的阈值处理方法来分离灰度级,使得前景和后景灰度差更大
  3. 当背景照明高度不均匀时,需要进行阈值处理的难度就增大,为了解决这个问题,运用局部统计的可变阈值处理的算法进行解决。

2.4 自适应阈值分割法

2.5 最大熵阈值分割算法

最大熵阈值分割法和OTSU算法类似,假设将图像分为背景和前景两个部分。熵代表信息量,图像信息量越大,熵就越大,最大熵算法就是找出一个最佳阈值使得背景与前景两个部分熵之和最大。
原理:
数字图像中给定一个估算的概率密度函数 p ( g ) p(g) p(g),数字图像中的熵定义为:
在这里插入图片描述
假设分割阈值为t(0<=t<K-1)将图像分割为前景 C 0 C_0 C0和背景 C 1 C_1 C1两个图像区域,则
C 0 : P 0 P n , P 1 P n , . . . , P t P n C 1 : P t + 1 1 − P n , P t + 2 1 − P n , . . . , P K − 1 1 − P n \begin{aligned} C_0&:\frac{P_0}{P_n},\frac{P_1}{P_n},...,\frac{P_t}{P_n}\\ C_1&:\frac{P_{t+1}}{1-P_n},\frac{P_{t+2}}{1-P_n},...,\frac{P_{K-1}}{1-P_n} \end{aligned} C0C1:PnP0,PnP1,...,PnPt:1PnPt+1,1PnPt+2,...,1PnPK1
其中, P n P_n Pn表示的是 t t t阈值分割的背景和前景像素的累计概率,且前后景所有像素的累计概率为1,公式表述为:
P n = ∑ i = 0 t P i ∑ i = 0 K − 1 P i = 1 , P i ≥ 0. \begin{aligned} &P_n=\sum_{i=0}^{t}P_i\\ &\sum_{i=0}^{K-1}P_i=1,P_i\geq 0. \end{aligned} Pn=i=0tPii=0K1Pi=1,Pi0.
此时,可以根据熵的计算公式得到前景 C 0 C_0 C0和后景 C 1 C_1 C1的熵,公式如下:
H 0 ( t ) = − ∑ i = 0 t P i P n l n P i P n H 1 ( t ) = − ∑ i = t + 1 K − 1 P i 1 − P n l n P i 1 − P n \begin{aligned} H_0(t)&=-\sum_{i=0}^{t}\frac{P_i}{P_n}ln\frac{P_i}{P_n}\\ H_1(t)&=-\sum_{i=t+1}^{K-1}\frac{P_i}{1-P_n}ln\frac{P_i}{1-P_n} \end{aligned} H0(t)H1(t)=i=0tPnPilnPnPi=i=t+1K11PnPiln1PnPi
则对于最佳阈值 t t t,应该使得图像的总熵 ϕ t \phi _t ϕt最大
a r g m a x t ϕ t = a r g m a x t ( H 0 ( t ) + H 1 ( t ) ) \underset{t}{argmax}\phi _t=\underset{t}{argmax}(H_0(t)+H_1(t)) targmaxϕt=targmax(H0(t)+H1(t))

三、基于OpenCV的C++代码实现

3.1基本全局阈值处理(含噪声):

#include<opencv2/opencv.hpp>
#include<opencv2/highgui.hpp>
#include<random>
#include<iostream>
using namespace cv;

// 添加Gussia噪声
void addGaussianNoise(Mat &m, int mu, int sigma)
{
    // 产生高斯分布随机数发生器
    std::random_device rd;
    std::mt19937 gen(rd());
    std::normal_distribution<> d(mu, sigma); //高斯噪声

    auto rows = m.rows; // 行数
    auto cols = m.cols * m.channels(); // 列数

    for (int i = 0; i < rows; i++){
        auto p = m.ptr<uchar>(i); // 取得行首指针
        for (int j = 0; j < cols; j++){
            auto tmp = p[j] + d(gen);
            tmp = tmp > 255 ? 255 : tmp;
            tmp = tmp < 0 ? 0 : tmp;
            p[j] = tmp;
        }
    }
}

int mean_pixel(Mat& src){
	//计算图像平均灰度
    int c=src.cols,r=src.rows;
    int sum=0;
     for(int i=0;i<r;i++){
        for(int j=0;j<c;j++){
                sum =sum+(int)src.ptr<uchar>(i)[j];
        }
    }
    return (int)round(sum/(r*c));
}

int main()
{
    Mat img=imread("C:/Users/Administrator/Desktop/beauty.jpg",0); //灰度图读入
    Size dsize = Size(round(0.3 * img.cols), round(0.3 * img.rows));//Size型 改变尺寸
    resize(img, img, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸
    
    //addGaussianNoise(img,0,0.2); //添加噪声

    //基本全局阈值处理
    int r=img.rows,c=img.cols;
    Mat dst=img.clone();
    int count = 0;

    int g_max(0),g_min(0);
    int max_count(0),min_count(0);

    int T_next; //计算下一次平均灰度
    int T=mean_pixel(img);

    bool done = false;
    while(!done){
        count  = count+1;
        for(int i=0;i<r;i++){
            for(int j=0;j<c;j++){
                if(img.ptr<uchar>(i)[j] < T){
                    g_max += img.ptr<uchar>(i)[j];
                    max_count++;
                }
                else{
                    g_min += img.ptr<uchar>(i)[j];
                    min_count++;
                }
            }
        }

        T_next = 0.5*((int)g_max/max_count+(int)g_min/min_count);

        done = abs(T-T_next)<0.5;  //直到符合条件跳出循环
        T =T_next; //否则继续
 
    }
    std::cout<<"count="<<count<<std::endl;
    std::cout<<"T="<<T<<std::endl;

    //二值化处理
	for (int i = 0; i < r; ++i){
	    uchar* ptr = dst.ptr<uchar>(i);
		for (int j = 0; j < c; ++j){
			if (ptr[j]> T)
				ptr[j] = 255;
			else
				ptr[j] = 0;
		}
	}
    
    cv::imshow("srcImage",img);
    cv::imshow("dstImage",dst);
    cv::waitKey();
    return 0;
}

实验结果如下:
在这里插入图片描述
count=6.T=160;也就是自动选择六次才达到预定的阈值分割目标,此时阈值为160。

3.2Ostu最大类间方差法

#include <opencv2\opencv.hpp>
#include <iostream>
#include <time.h>
using namespace std;
using namespace cv;
 
int myOtsu(Mat & src)
{
	int th;
	const int GrayScale = 256;	//单通道图像总灰度256级
	int pixCount[GrayScale] = {0};//每个灰度值所占像素个数
	int pixSum = src.cols * src.rows;//图像总像素点
	float pixPro[GrayScale] = {0};//每个灰度值所占总像素比例
	float p0, p1, p0tmp, p1tmp, m0, m1, deltaTmp, deltaMax = 0; 
 
    //以下给出了两种 图像像素访问的方法(1).at方法  (2).ptr指针访问(快一些)
	for(int i = 0; i < src.cols; i++){
		for(int j = 0; j < src.rows; j++){
            //.at方法 只适合灰度值为8位的图像
			pixCount[src.at<uchar>(j,i)]++;//统计每个灰度级中像素的个数  
		}
	}
    /*for (int i = 0; i < r; ++i){
		const uchar* ptr = src.ptr<uchar>(i);
		for (int j = 0; j < c; ++j){        //统计每个灰度级中像素的个数
			graynum[ptr[j]]++;
		}
	}*/

 
	for(int i = 0; i < GrayScale; i++)
	{
		pixPro[i] = pixCount[i] * 1.0 / pixSum;//计算每个灰度级的像素数目占整幅图像的比例  
	}
 
	for(int i = 0; i < GrayScale; i++)//遍历所有从0到255灰度级的阈值分割条件,测试哪一个的类间方差最大
	{
		p0 = p1 = p0tmp = p1tmp = m0 = m1 = deltaTmp = 0;  
        //w0(p0tmp)、w1(p1tmp)表示像素被分为C1、C2类中的概率(累计和)
        //u0/u1表示像素被分为C1、C2类中的平均灰度
        //deltaTmp和deltaMax维护一个最大类间方差

		for(int j = 0; j < GrayScale; j++){
			if(j <= i)//C1类
			{
				p0 += pixPro[j];
				p0tmp += j * pixPro[j]; 
			}
			else//C2类
			{
				p1 += pixPro[j];
				p1tmp += j * pixPro[j];
			}
		}
		m0 = p0tmp / p0;
		m1 = p1tmp / p1;
		deltaTmp = (float)(p0 *p1* pow((m0 - m1), 2)); //类间方差公式 g = w1 * w2 * (u1 - u2) ^ 2
		if(deltaTmp > deltaMax) 
		{
			deltaMax = deltaTmp;
			th = i;  
		}  
	}
	return th;
}
 
int main()
{
	Mat src = imread("C:/Users/Administrator/Desktop/beauty.jpg",0);//单通道读取图像
    Size dsize = Size(round(0.2 * src.cols), round(0.2 * src.rows));//Size型 改变尺寸
    resize(src, src, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸
	/*my_dst: 自己实现的大津法 得到的处理图像
	otsu_dst:opencv自带的大津法 得到的处理图像
	sub:两个处理图像相差图
	*/
	Mat my_dst, otsu_dst, sub;

	/*my_th: 自己实现的大津法 得到的最大类件方差 即阈值
	th:opencv自带的大津法 得到的最大类件方差 即阈值
	*/
	int my_th, th;
 
	/*计算开销时间,对比两个算法效率*/
	long my_start = clock();  //开始时间
	{
		my_th = myOtsu(src);
		threshold(src,my_dst,my_th,255,CV_THRESH_BINARY);
	}
	long my_finish = clock();   //结束时间
	long my_t = my_finish-my_start;
	printf("The run time is:%9.3lf\n", my_t, "ms!\n"); //输出时间
	cout << "myOtsu threshold >> " << my_th << endl;
 
	long otsu_start = clock();  //开始时间
	{
		th = threshold(src,otsu_dst,0,255,CV_THRESH_OTSU);
	}
	long otsu_finish = clock();   //结束时间
	long t = my_finish-my_start;
	printf("The run time is:%9.3lf\n",  (double) t / CLOCKS_PER_SEC, "ms!\n"); //输出时间
	cout << "Otsu threshold >> " << th << endl;
 
	subtract(otsu_dst,my_dst,sub);//两图像相减
	imshow("src",src);
	imshow("myOtsu",my_dst);
	imshow("Otsu",otsu_dst);
	imshow("Sub",sub);
	
	waitKey(0);
	return 0;
}

在这里插入图片描述
自己实现的Ostu和OpenCV自带的Ostu函数得到的阈值都是152,从色差图中也能看出来它们实现的效果一样。

3.2最大熵阈值分割算法

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp> 

int Max_Entropy(cv::Mat& src, cv::Mat& dst, int thresh, int p){	
    const int Grayscale = 256;	
    int Graynum[Grayscale] = { 0 };	
    int r = src.rows;	
    int c = src.cols;	
    for (int i = 0; i < r; ++i){		
        const uchar* ptr = src.ptr<uchar>(i);   		
        for (int j = 0; j < c; ++j){   			
            if (ptr[j] == 0)				//排除掉黑色的像素点				
                continue;			
            Graynum[ptr[j]]++;		
        }	
    } 	
    
    float probability = 0.0; //概率	
    float max_Entropy = 0.0; //最大熵	
    int totalpix = r*c;	
    for (int i = 0; i < Grayscale; ++i){ 		
        float HO = 0.0; //前景熵		
        float HB = 0.0; //背景熵 

        //计算前景像素数		
        int frontpix = 0;		
        for (int j = 0; j < i; ++j){			
            frontpix += Graynum[j];		
        }		
        
        //计算前景熵		
        for (int j = 0; j < i; ++j){			
            if (Graynum[j] != 0){				
                probability = (float)Graynum[j] / frontpix;				
                HO = HO + probability*log(1/probability);			
            }		
        } 		
        
        //计算背景熵		
        for (int k = i; k < Grayscale; ++k){			
            if (Graynum[k] != 0){				
                probability = (float)Graynum[k] / (totalpix - frontpix);				
                HB = HB + probability*log(1/probability);			
            }		
        } 		
        
        //计算最大熵		
        if(HO + HB > max_Entropy){			
            max_Entropy = HO + HB;			
            thresh = i + p;		
        }	
    } 	
    
    //阈值处理	
    src.copyTo(dst);	
    for (int i = 0; i < r; ++i){		
        uchar* ptr = dst.ptr<uchar>(i);		
        for (int j = 0; j < c; ++j){			
            if (ptr[j]> thresh)				
            ptr[j] = 255;			
            else				
            ptr[j] = 0;		
        }	
    }	
            
    return thresh;
}  

int main(){	
    cv::Mat src = cv::imread("C:/Users/Administrator/Desktop/beauty.jpg",0); //读入灰度图	
    if (src.empty()){		
        return -1;	
    }	
    cv::Size dsize = cv::Size(round(0.3 * src.cols), round(0.3 * src.rows));//Size型 改变尺寸
    cv::resize(src, src, dsize, 0, 0, cv::INTER_LINEAR); //使用双线性插值缩放一下尺寸
        
    cv::Mat dst, dst2;	
    int thresh = 0;	
    thresh = Max_Entropy(src, dst, thresh,30); //Max_Entropy	
    std::cout << "Mythresh=" << thresh << std::endl;		
    
    double  Otsu = 0;	
    Otsu = cv::threshold(src, dst2, Otsu, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);	
    std::cout << "Otsuthresh=" << Otsu << std::endl;

    
    cv::imshow("srcImage", src);	
    cv::imshow("maxEntropy", dst);	
    cv::imshow("Otsuthresh", dst2);	
    cv::waitKey(0);}

实现效果:
在这里插入图片描述
通过调整阈值偏置,可以实现Ostu的全局阈值分割的效果,
在这里插入图片描述

参考博客:
【1】《数字图像处理(第三版)》_冈萨雷斯
【2】图像分割最全综述
【3】OTSU方法:https://blog.csdn.net/mary_0830/article/details/89597672
【4】最大熵阈值分割算法原理及实现

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;