《OpenCV:灰度图像阈值化分割常见方法总结及VC代码》

支持原创,拿来收藏!转载地址:http://blog.csdn.net/likezhaobin/article/details/6915755?userName=u014395105&userInfo=aWOfy4XjkeuESVqMgVdrnPewKx6gaD2TZ6xUFF%2FXs%2FeZjmZKRHLyhzVPli3izF4JpSQuVNfcdFRe6pvuXl6VvRJ%2FSmjVpClq8XgXbwl56GUA19Luch91NWA57umNAidF94p6X1kqBpQ9l4%2FEkqjEnw%3D%3D

在图像处理领域,二值图像运算量小,并且能够体现图像的关键特征,因此被广泛使用。将灰度图像变为二值图像的常用方法是选定阈值,然后将待处理图像的每个像素点进行单点处理,即将其灰度值与所设置的门限进行比对,从而得到二值化的黑白图。这样一种方式因为其直观性以及易于实现,已经在图像分割领域处于中心地位。本文主要对最近一段时间作者所学习的阈值化图像分割算法进行总结,全文描述了作者对每种算法的理解,并基于OpenCV和VC6.0对这些算法进行了实现。最终将源代码公开,希望大家一起进步。(本文的代码暂时没有考虑执行效率问题)

首先给出待分割的图像如下:

1、Otsu法(最大类间方差法)

该算法是日本人Otsu提出的一种动态阈值分割算法。它的主要思想是按照灰度特性将图像划分为背景和目标2部分,划分依据为选取门限值,使得背景和目标之间的方差最大。(背景和目标之间的类间方差越大,说明这两部分的差别越大,当部分目标被错划分为背景或部分背景错划分为目标都会导致这两部分差别变小。因此,使用类间方差最大的分割意味着错分概率最小。)这是该方法的主要思路。其主要的实现原理为如下:

1)建立图像灰度直方图(共有L个灰度级,每个出现概率为p)

2)计算背景和目标的出现概率,计算方法如下:

上式中假设t为所选定的阈值,A代表背景(灰度级为0~N),根据直方图中的元素可知,Pa为背景出现的概率,同理B为目标,Pb为目标出现的概率。

3)计算A和B两个区域的类间方差如下:

第一个表达式分别计算A和B区域的平均灰度值;

第二个表达式计算灰度图像全局的灰度平均值;

第三个表达式计算A、B两个区域的类间方差。

4)以上几个步骤计算出了单个灰度值上的类间方差,因此最佳分割门限值应该是图像中能够使得A与B的类间灰度方差最大的灰度值。在程序中需要对每个出现的灰度值据此进行寻优。

本人的VC实现代码如下。

/*****************************************************************************
*
* \函数名称:
*   OneDimentionOtsu()
*
* \输入参数:
*   pGrayMat:      二值图像数据
*   width:         图形尺寸宽度
*   height:        图形尺寸高度
*   nTlreshold:    经过算法处理得到的二值化分割阈值
* \返回值:
*   无
* \函数说明:实现灰度图的二值化分割——最大类间方差法(Otsu算法,俗称大津算法)
*
****************************************************************************/  

void CBinarizationDlg::OneDimentionOtsu(CvMat *pGrayMat, int width, int height, BYTE &nThreshold)
{
    double nHistogram[256];         //灰度直方图
    double dVariance[256];          //类间方差
    int N = height*width;           //总像素数
    for(int i=0; i<256; i++)
    {
        nHistogram[i] = 0.0;
        dVariance[i] = 0.0;
    }
    for(i=0; i<height; i++)
    {
        for(int j=0; j<width; j++)
        {
            unsigned char nData = (unsigned char)cvmGet(pGrayMat, i, j);
            nHistogram[nData]++;     //建立直方图
        }
    }
    double Pa=0.0;      //背景出现概率
    double Pb=0.0;      //目标出现概率
    double Wa=0.0;      //背景平均灰度值
    double Wb=0.0;      //目标平均灰度值
    double W0=0.0;      //全局平均灰度值
    double dData1=0.0,  dData2=0.0;
    for(i=0; i<256; i++)     //计算全局平均灰度
    {
        nHistogram[i] /= N;
        W0 += i*nHistogram[i];
    }
    for(i=0; i<256; i++)     //对每个灰度值计算类间方差
    {
        Pa += nHistogram[i];
        Pb = 1-Pa;
        dData1 += i*nHistogram[i];
        dData2 = W0-dData1;
        Wa = dData1/Pa;
        Wb = dData2/Pb;
        dVariance[i] = (Pa*Pb* pow((Wb-Wa), 2));
    }
    //遍历每个方差,求取类间最大方差所对应的灰度值
    double temp=0.0;
    for(i=0; i<256; i++)
    {
        if(dVariance[i]>temp)
        {
            temp = dVariance[i];
            nThreshold = i;
        }
    }
}  

阈值分割结果如下图,求解所得的阈值为116.

2、一维交叉熵值法

这种方法与类间最大方差很相似,是由Li和Lee应用了信息论中熵理论发展而来。首先简要介绍交叉熵的概念。

对于两个分布P和Q,定义其信息交叉熵D如下:

这代表的物理意义是两个分布之间信息理论距离,另外一种理解是,将分布P变为Q后所带来的信息变化。那么对于图像分割来说,如果要用分割图像来替换原来的图像,最优的分割依据应该就是使得两幅图像之间的交叉熵最小。以下对最小交叉熵法的过程进行简要总结。

可以假设上文的P为源图像的灰度分布,Q为所得到的分割图像的灰度分布,其中:

上式中H为统计直方图;

N为图像总的像素点数;

L为源图像总的灰度级数;

P代表源图像,其每个元素代表每个灰度级上的灰度分布(平均灰度值);

Q为分割后的二值图像,两个u分别代表两个分割后的区域的平均灰度值,其中t为分割图像所采用的阈值。

根据以上定义,以每个灰度级上的灰度和为计算量,可以很容易根据交叉熵的公式,推导出P和Q之间的交叉熵定量表达式:

根据上文所述思路,使得D最小的t即为最小交叉熵意义下的最优阈值。

作者VC实现代码如下。

/*****************************************************************************
*
* \函数名称:
*   MiniCross()
*
* \输入参数:
*   pGrayMat:      二值图像数据
*   width:         图形尺寸宽度
*   height:        图形尺寸高度
*   nTlreshold:    经过算法处理得到的二值化分割阈值
* \返回值:
*   无
* \函数说明:实现灰度图的二值化分割——最小交叉熵算法
*
****************************************************************************/  

void CBinarizationDlg::MiniCross(CvMat *pGrayMat, int width, int height, BYTE &nThreshold)
{
    double dHistogram[256];         //灰度直方图
    double dEntropy[256];           //每个像素的交叉熵
    int N = height*width;           //总像素数
    for(int i=0; i<256; i++)
    {
        dHistogram[i] = 0.0;
        dEntropy[i] = 0.0;
    }
    for(i=0; i<height; i++)
    {
        for(int j=0; j<width; j++)
        {
            unsigned char nData = (unsigned char)cvmGet(pGrayMat, i, j);
            dHistogram[nData]++;     //建立直方图
        }
    }
    double Pa=0.0;      //区域1平均灰度值
    double Pb=0.0;      //区域2平均灰度值
    double P0=0.0;      //全局平均灰度值
    double Wa=0.0;      //第一部分熵
    double Wb=0.0;      //第二部分的熵
    double dData1=0.0, dData2=0.0;  //中间值
    double dData3=0.0, dData4=0.0;  //中间值  

    for(i=0; i<256; i++)     //计算全局平均灰度
    {
        dHistogram[i] /= N;
        P0 += i*dHistogram[i];
    }  

    for(i=0; i<256; i++)
    {
        Wa=Wb=dData1=dData2=dData3=dData4=Pa=Pb=0.0;
        for(int j=0; j<256; j++)
        {
            if(j<=i)
            {
                dData1 += dHistogram[j];
                dData2 += j*dHistogram[j];
            }
            else
            {
                dData3 += dHistogram[j];
                dData4 += j*dHistogram[j];
            }
        }
        Pa = dData2/dData1;
        Pb = dData4/dData3;
        for(j=0; j<256; j++)
        {
            if(j<=i)
            {
                if((Pa!=0)&&(dHistogram[j]!=0))
                {
                    double d1 = log(dHistogram[j]/Pa);
                    Wa += j*dHistogram[j]*d1/log(2);
                }
            }
            else
            {
                if((Pb!=0)&&(dHistogram[j]!=0))
                {
                    double d2 = log(dHistogram[j]/Pb);
                    Wb += j*dHistogram[j]*d2/log(2);
                }
            }
        }
        dEntropy[i] = Wa+Wb;
    }
    //遍历熵值,求取最小交叉熵所对应的灰度值
    double temp=dEntropy[0];
    for(i=1; i<256; i++)
    {
        if(dEntropy[i]<temp)
        {
            temp = dEntropy[i];
            nThreshold = i;
        }
    }
}  

阈值分割结果如下图,求解所得的阈值为106.

3、二维OTSU法

这种方法是对类间最大方差法的扩展,将其从求两个一维分布最大类间方差扩充为求解类间离散度矩阵的迹的最大值,考虑像素点灰度级的基础上增加了对像素点邻域平均像素值的考虑。

以下按照本人的理解对该方法的思路以及推倒过程进行分析:

1)首先需要建立二维的灰度统计直方图P(f, g);

图像的灰度级为L级,那么其每个像素点的8邻域灰度平均值的灰度级也为L级,据此来构建直方图P。二维统计直方图的横轴为每个像素点的灰度值f(i, j),纵坐标为同一个点对应的邻域平均值g(i, j) 其中(0≤i<height, 0≤j<width, 0≤f(i, j)<L),而所对应的P(f,g)整幅图像中灰度值为f,邻域灰度均值为g的点的统计值占总像素点的比例(即为灰度值出现的联合概率密度)。其中P的每个元素满足如下公式:

n为整幅图像中灰度值为f,邻域灰度均值为g的点的统计值;

N为图像总的像素点个数;

2)对于下图所示的二维统计直方图,t代表横坐标(灰度值),s代表纵坐标(像素点邻域的灰度均值)

对已图像中的阈值点(t,s)来说,其灰度值t和其邻域内的灰度均值s不应该相差太多,如果t比s大很多(点位于上图中的II区域),说明像素的灰度值远远大于其临域的灰度均值,故而该点很可能是噪声点,反之如果t比s小很多(点位于途中的IV区域),即该点的像素值比其临域均值小很多,则说明是一个边缘点。据此我们在进行背景前景分割的时候忽略这些干扰因素,认为这两个区域内Pi,j=0。剩下的I区域和III区域则分别代表了前景和背景。以下据此来推导对于选定的阈值(t, s),进行离散度判据的最优推导。

3)推导阈值(t, s)点处的离散度矩阵判据

根据上文分析可知,由阈值(t, s)所分割的前景和背景出现的概率如下:

定义两个中间变量,方便下面推导:

据此,这两部分的灰度均值向量可以推导如下(两个分量分别根据灰度值以及每个点的灰度均值计算):

整幅图像的灰度均值向量为:

与一维的大津法一样的思路,推导类间方差,这里是二维因此要用矩阵形式。参考一维法,可同样定义类间“方差”矩阵如下:

为了在实现的时候,容易判断出这样一个矩阵的“最大值”,因此数学中采用矩阵的迹(对角线之和)来衡量矩阵的“大小”。因此以该矩阵的迹作为离散度测度,推导如下:

这样就可以通过求解使得这个参数最大时的(t,s)即为所求得的最佳阈值组合。

以下为具体算法实现过程:

1)建立二维直方图

2)对直方图进行遍历,计算每个(t,s)组合所得到的矩阵离散度,也就是一维大津法中所谓的最大类间方差。

3)求得使“类间方差”最大的(t,s),由于t代表灰度值,s代表改点在其邻域内的灰度均值,因此本人认为在选择阈值时可以选择s为最佳,当然用t也可以,因为从求解结果可以看出,这这个数值往往很接近。

具体的实现代码如下:

/*****************************************************************************
*
* \函数名称:
*   TwoDimentionOtsu()
*
* \输入参数:
*   pGrayMat:      二值图像数据
*   width:         图形尺寸宽度
*   height:        图形尺寸高度
*   nTlreshold:    经过算法处理得到的二值化分割阈值
* \返回值:
*   无
* \函数说明:实现灰度图的二值化分割——最大类间方差法(二维Otsu算法)
* \备注:在构建二维直方图的时候,采用灰度点的3*3邻域均值
******************************************************************************/
void CBinarizationDlg::TwoDimentionOtsu(CvMat *pGrayMat, int width, int height, BYTE &nThreshold)
{
    double dHistogram[256][256];        //建立二维灰度直方图
    double dTrMatrix = 0.0;             //离散矩阵的迹
    int N = height*width;               //总像素数
    for(int i=0; i<256; i++)
    {
        for(int j=0; j<256; j++)
            dHistogram[i][j] = 0.0;      //初始化变量
    }
    for(i=0; i<height; i++)
    {
        for(int j=0; j<width; j++)
        {
            unsigned char nData1 = (unsigned char)cvmGet(pGrayMat, i, j);  //当前的灰度值
            unsigned char nData2 = 0;
            int nData3 = 0;         //注意9个值相加可能超过一个字节
            for(int m=i-1; m<=i+1; m++)
            {
                for(int n=j-1; n<=j+1; n++)
                {
                    if((m>=0)&&(m<height)&&(n>=0)&&(n<width))
                        nData3 += (unsigned char)cvmGet(pGrayMat, m, n); //当前的灰度值
                }
            }
            nData2 = (unsigned char)(nData3/9);    //对于越界的索引值进行补零,邻域均值
            dHistogram[nData1][nData2]++;
        }
    }
    for(i=0; i<256; i++)
        for(int j=0; j<256; j++)
            dHistogram[i][j] /= N;  //得到归一化的概率分布  

    double Pai = 0.0;      //目标区均值矢量i分量
    double Paj = 0.0;      //目标区均值矢量j分量
    double Pbi = 0.0;      //背景区均值矢量i分量
    double Pbj = 0.0;      //背景区均值矢量j分量
    double Pti = 0.0;      //全局均值矢量i分量
    double Ptj = 0.0;      //全局均值矢量j分量
    double W0 = 0.0;       //目标区的联合概率密度
    double W1 = 0.0;       //背景区的联合概率密度
    double dData1 = 0.0;
    double dData2 = 0.0;
    double dData3 = 0.0;
    double dData4 = 0.0;   //中间变量
    int nThreshold_s = 0;
    int nThreshold_t = 0;
    double temp = 0.0;     //寻求最大值
    for(i=0; i<256; i++)
    {
        for(int j=0; j<256; j++)
        {
            Pti += i*dHistogram[i][j];
            Ptj += j*dHistogram[i][j];
        }
    }
    for(i=0; i<256; i++)
    {
        for(int j=0; j<256; j++)
        {
            W0 += dHistogram[i][j];
            dData1 += i*dHistogram[i][j];
            dData2 += j*dHistogram[i][j];  

            W1 = 1-W0;
            dData3 = Pti-dData1;
            dData4 = Ptj-dData2;
/*          W1=dData3=dData4=0.0;   //对内循环的数据进行初始化
            for(int s=i+1; s<256; s++)
            {
                for(int t=j+1; t<256; t++)
                {
                    W1 += dHistogram[s][t];
                    dData3 += s*dHistogram[s][t];  //方法2
                    dData4 += t*dHistogram[s][t];  //也可以增加循环进行计算
                }
            }*/  

            Pai = dData1/W0;
            Paj = dData2/W0;
            Pbi = dData3/W1;
            Pbj = dData4/W1;   // 得到两个均值向量,用4个分量表示
            dTrMatrix = ((W0*Pti-dData1)*(W0*Pti-dData1)+(W0*Ptj-dData1)*(W0*Ptj-dData2))/(W0*W1);
            if(dTrMatrix > temp)
            {
                temp = dTrMatrix;
                nThreshold_s = i;
                nThreshold_t = j;
            }
        }
    }
    nThreshold = nThreshold_t;   //返回结果中的灰度值
    //nThreshold = 100;
}  

阈值分割结果如下图,求解所得的阈值为114.(s=114,t=117)

参考文献<>

[1] Nobuyuki Otsu. A Threshold SelectionMethod from Gray-Level Histograms.

[2] C. H. Li and C. K. Lee. Minimum CrossEntropy Thresholding

[3] Jian Gong, LiYuan Liand WeiNan Chen. Fast Recursive Algorithms For Two-Dimensional Thresholding

时间: 2024-12-22 22:23:43

《OpenCV:灰度图像阈值化分割常见方法总结及VC代码》的相关文章

CI框架源码阅读笔记3 全局函数Common.php

从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作). 打开Common.php中,第一行代码就非常诡异: if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 上一篇(CI框架源码阅读笔记2 一切的入口 index

IOS测试框架之:athrun的InstrumentDriver源码阅读笔记

athrun的InstrumentDriver源码阅读笔记 作者:唯一 athrun是淘宝的开源测试项目,InstrumentDriver是ios端的实现,之前在公司项目中用过这个框架,没有深入了解,现在回来记录下. 官方介绍:http://code.taobao.org/p/athrun/wiki/instrumentDriver/ 优点:这个框架是对UIAutomation的java实现,在代码提示.用例维护方面比UIAutomation强多了,借junit4的光,我们可以通过junit4的

Yii源码阅读笔记 - 日志组件

?使用 Yii框架为开发者提供两个静态方法进行日志记录: Yii::log($message, $level, $category);Yii::trace($message, $category); 两者的区别在于后者依赖于应用开启调试模式,即定义常量YII_DEBUG: defined('YII_DEBUG') or define('YII_DEBUG', true); Yii::log方法的调用需要指定message的level和category.category是格式为“xxx.yyy.z

源码阅读笔记 - 1 MSVC2015中的std::sort

大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格式化,去掉或者展开用于条件编译或者debug检查的宏,依重要程度重新排序函数,但是不会改变命名方式(虽然MSVC的STL命名实在是我不能接受的那种),对于代码块的解释会在代码块前(上面)用注释标明. template<class _RanIt, class _Diff, class _Pr> in

CI框架源码阅读笔记5 基准测试 BenchMark.php

上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功能,各模块之间可以相互调用,共同构成了CI的核心骨架. 从本篇开始,将进一步去分析各组件的实现细节,深入CI核心的黑盒内部(研究之后,其实就应该是白盒了,仅仅对于应用来说,它应该算是黑盒),从而更好的去认识.把握这个框架. 按照惯例,在开始之前,我们贴上CI中不完全的核心组件图: 由于BenchMa

CI框架源码阅读笔记2 一切的入口 index.php

上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里这次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中,我们并不会逐行进行解释,而只解释核心的功能和实现. 1.       设置应用程序环境 define('ENVIRONMENT', 'development'); 这里的development可以是任何你喜欢的环境名称(比如dev,再如test),相对应的,你要在下面的switch case代码块中

Apache Storm源码阅读笔记

欢迎转载,转载请注明出处. 楔子 自从建了Spark交流的QQ群之后,热情加入的同学不少,大家不仅对Spark很热衷对于Storm也是充满好奇.大家都提到一个问题就是有关storm内部实现机理的资料比较少,理解起来非常费劲. 尽管自己也陆续对storm的源码走读发表了一些博文,当时写的时候比较匆忙,有时候衔接的不是太好,此番做了一些整理,主要是针对TridentTopology部分,修改过的内容采用pdf格式发布,方便打印. 文章中有些内容的理解得益于徐明明和fxjwind两位的指点,非常感谢.

CI框架源码阅读笔记4 引导文件CodeIgniter.php

到了这里,终于进入CI框架的核心了.既然是"引导"文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.com/usr/reg 经过引导文件,实际上会交给Application中的UsrController控制器的reg方法去处理. 这之中,CodeIgniter.php做了哪些工作?我们一步步来看. 1.    导入预定义常量.框架环境初始化 之前的一篇博客(CI框架源码阅读笔记2 一切的入

jdk源码阅读笔记之java集合框架(二)(ArrayList)

关于ArrayList的分析,会从且仅从其添加(add)与删除(remove)方法入手. ArrayList类定义: p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Monaco } span.s1 { color: #931a68 } public class ArrayList<E> extends AbstractList<E> implements List<E> ArrayList基本属性: /** *

dubbo源码阅读笔记--服务调用时序

上接dubbo源码阅读笔记--暴露服务时序,继续梳理服务调用时序,下图右面红线流程. 整理了调用时序图 分为3步,connect,decode,invoke. 连接 AllChannelHandler.connected(Channel) line: 38 HeartbeatHandler.connected(Channel) line: 47 MultiMessageHandler(AbstractChannelHandlerDelegate).connected(Channel) line: