OpenCV - 均值迭代分割

本文介绍了一种基于直方图的图像分割方法——均值迭代算法。该算法通过迭代计算图像的最佳阈值,将图像分为前景和背景两部分。文章详细解释了算法的工作原理,并给出了OpenCV下的实现代码。

【题外话】:之前在博客中写过一篇“区域生长”的博客,区域生长在平时经常用到,也比较容易理解和代码实现,所以在很多情况下大家会选择这种方法。但是区域生长有一个最致命的点就是需要选取一个生长的种子点。
为了交流学习,同时也为了后面查阅方便,准备陆续将基于直方图的几种分割算法加以总结。

1、均值迭代算法的描述

对一幅图像M,均值迭代算法就是要迭代计算得到一个灰度值T,使得这个灰度值T将图像分成的两类A,B。满足条件:A类的均值和B类的均值,再求均值正好等于T
用直方图可以很直观的描述:

这里写图片描述

图示表示某个图像的灰度直方图,均值迭代就是为找到一个T,使得T左侧的积分面积中均值T1和右侧积分面的均值T2相等。

2、算法的步骤

  1. 选择一个初始化阈值T,通常取整张图灰度值的平均值;
  2. 计算T分成的两个部分的灰度均值u1u2
  3. 更新Tu1+u2/2
  4. 重复步骤2~3,直到相邻两次计算的结果相等,或者两次结果的差值小于预先设定的值某个值;
  5. 用这个T对图像进行分割。

3、OpenCV下的实现

//Mat src: 待分割灰度图像
//int n:   初始阈值
void IsodataSeg(Mat &src, int n)
{
    int threshold = 0;                          //历史阈值
    int MeansO = 0;                             //前景灰度均值
    int nObject = 0;                            //实质像素点个数
    int MeansB = 0;                             //背景灰度均值
    int nBack = 0;                              //背景像素点个数
    int nCol = src.cols * src.channels();       //每行的像素个数
    while (abs(threshold - n) > 10)             //迭代停止条件
    {
        threshold = n;
        for (int i=0; i<src.rows; ++i)
        {
            uchar* pData = src.ptr<uchar>(i);
            for (int j=0; j<nCol; ++j)
            {
                if (pData[j] < threshold)   //背景
                {
                    ++nBack;
                    MeansB += pData[j];
                }
                else                        //物体
                {
                    ++nObject;
                    MeansO += pData[j];
                }
            }
        }
        if (nBack == 0 || nObject == 0)     //防止出现除以0的计算
            continue;
        n = (MeansB/nBack + MeansO/nObject) / 2;
    }
    cv::threshold(src, src, n, 255, 0);     //进行阈值分割
}

4、进一步探究

在实际的应用中常常会发现一幅图像中,我们真正想去分割的并不是整个的矩形。比如下面的图:


这里写图片描述

假设我们想要将整个奇怪的人的眼睛,嘴巴和他们脸分开,而并不关心整个图的背景部分。(当然有很多种方法可以直接去掉黑色的背景,现在仅仅讨论整个图)在算法的思路中我们可以看到,我们统计的是整个图的像素,试想如果我们不去统计哪些我们已经知道是背景颜色的像素点,那么整个问题不就转化成了我们已经能解决的问题了么?!
所以,我们仅仅在代码的实现中讲哪些我们已知是不要统计的颜色排除在外就可以了。
这里我们假定背景是纯黑的(像素值为0)。这样上述代码仅仅需要作很小的改动就能满足应用的要求了。

void IsodataSeg(Mat &src, int n)
{
    int threshold = 0;                          //历史阈值
    int MeansO = 0;                             //前景灰度均值
    int nObject = 0;                            //实质像素点个数
    int MeansB = 0;                             //背景灰度均值
    int nBack = 0;                              //背景像素点个数
    int nCol = src.cols * src.channels();       //每行的像素个数

    while (abs(threshold - n) > 10)             //迭代停止条件
    {
        threshold = n;
        for (int i=0; i<src.rows; ++i)
        {
            uchar* pData = src.ptr<uchar>(i);
            for (int j=0; j<nCol; ++j)
            {
                //黑色区域为多余的像素,不参与计算
                if (pData[j] == 0)
                    continue;

                if (pData[j] < threshold)   //背景
                {
                    ++nBack;
                    MeansB += pData[j];
                }
                else                        //物体
                {
                    ++nObject;
                    MeansO += pData[j];
                }
            }
        }
        if (nBack == 0 || nObject == 0)
            continue;
        n = (MeansB/nBack + MeansO/nObject) / 2;
    }
    cv::threshold(src, src, n, 255, 0);                             //进行阈值分割
}

5、均值迭代在哪好用

从算法的描述步骤中可以看到,均值迭代算法的收敛速度是很快的。也就意味了在合适的场景下,使用均值迭代算法是具有绝对优势的。但是也需要注意一点均值迭代一般比较使用直方图为典型的“双峰”的图像。这一特征也是基于直方图统计类算法的共同特征。
可以体会到均值迭代算法其实是一种简单的聚类,它每次将样本分成两类,两类的均值作为两类的聚类中心,每一次计算的 T 与两类均值的距离为聚类的半径。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空空的司马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值