OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE

简单阈值(全局阈值)
全局阈值意味着整幅图像以一个固定的值进行分割。
阈值化,即图像的二值化处理就是讲图像上的点的灰度置为0或255,也就是讲整个图像呈现出明显的黑白效果。即将256个亮度等级的灰度图像通过适当的阀值选取而获得仍然可以反映图像整体和局部特征的二值化图像。
在数字图像处理中,二值图像占有非常重要的地位,特别是在实用的图像处理中,以二值图像处理实现而构成的系统是很多的,要进行二值图像的处理与分析,首先要把灰度图像二值化,得到二值化图像,这样子有利于再对图像做进一步处理时,图像的集合性质只与像素值为0或255的点的位置有关,不再涉及像素的多级值,使处理变得简单,而且数据的处理和压缩量小。为了得到理想的二值图像,一般采用封闭、连通的边界定义不交叠的区域。所有灰度大于或等于阀值的像素被判定为属于特定物体,其灰度值为255表示,否则这些像素点被排除在物体区域以外,灰度值为0,表示背景或者例外的物体区域。如果某特定物体在内部有均匀一致的灰度值,并且其处在一个具有其他等级灰度值的均匀背景下,使用阀值法就可以得到比较的分割效果。
如果物体同背景的差别表现不在灰度值上(比如纹理不同),可以将这个差别特征转换为灰度的差别,然后利用阀值选取技术来分割该图像。动态调节阀值实现图像的二值化可动态观察其分割图像的具体结果。

Opencv里面的API介绍:(全局阈值化)

C++ void threshold(Mat dst,Mat dst1,int threshold_value,int threshold_max,type);
//src:源图像,可以为8位的灰度图,也可以为32位的彩色图像。(两者有区别)
//dst:输出图像 
//thresh:阈值
//maxval:dst图像中最大值 
//type:阈值类型:1 THRESH_BINARY 2THRESH_BINARY_INV 
//3	THRESH_TRUNC 4	THRESH_TOZERO 5	THRESH_TOZERO_INV 

第五个参数Type:可以是下面五种格式,其中还有三种比较常用的像大津法(THRESH_OTSU),THRESH_MASK 和 THRESH_TRIANGLE
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第1张图片

下面着重说明一下THRESH_OTSU与THRESH_TRIANGLE。

THRESH_OTSU与THRESH_TRIANGLE适用场合
参考https://zhuanlan.zhihu.com/p/81680297。
OTSU算法对直方图有两个峰,中间有明显波谷的直方图对应图像二值化效果比较好,而对于只有一个单峰的直方图对应的图像分割效果没有双峰的好。
所以OTSU更适用于有明显区分的前景色和后景色的图像。
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第2张图片
THRESH_TRIANGLE

并不是所有的照片都是由明显的前景色和后景色的对比的,比如这个。
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第3张图片
那么,有没有一种更好的办法来处理这种单峰图像呢?答案是肯定的。THRESH_TRIANGLE应运而生。

THRESH_OTSU
大津法(OTSU)是一种确定图像二值化分割阈值的算法,由日本学者大津于1979年提出。从大津法的原理上来讲,该方法又称作最大类间方差法,因为按照大津法求得的阈值进行图像二值化分割后,前景与背景图像的类间方差最大。

它被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响,因此在数字图像处理上得到了广泛的应用。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。

应用:是求图像全局阈值的最佳方法,应用不言而喻,适用于大部分需要求图像全局阈值的场合。

优点:计算简单快速,不受图像亮度和对比度的影响。

Opencv 官方文档:https://docs.opencv.org/3.0-last-rst/modules/imgproc/doc/miscellaneous_transformations.html?highlight=threshold#threshold

Github: https://github.com/2209520576/Image-Processing-Algorithm

OTSU原理:
原理非常简单,涉及的知识点就是均值、方差等概念和一些公式推导。为了便于理解,我们从目的入手,反推一下这著名的OTSU算法。

求类间方差:

OTSU算法的假设是存在阈值TH将图像所有像素分为两类C1(小于TH)和C2(大于TH),则这两类像素各自的均值就为m1、m2,图像全局均值为mG。同时像素被分为C1和C2类的概率分别为p1、p2。因此就有:
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第4张图片
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第5张图片

OTSU代码实现:

#include 
#include 
#include 
#include 
 
int Otsu(cv::Mat& src, cv::Mat& dst, int thresh){
     
	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){
             //直方图统计
			graynum[ptr[j]]++;
		}
	}
 
    double P[Grayscale] = {
      0 };   
	double PK[Grayscale] = {
      0 };
	double MK[Grayscale] = {
      0 };
	double srcpixnum = r*c, sumtmpPK = 0, sumtmpMK = 0;
	for (int i = 0; i < Grayscale; ++i){
     
		P[i] = graynum[i] / srcpixnum;   //每个灰度级出现的概率
		PK[i] = sumtmpPK + P[i];         //概率累计和 
		sumtmpPK = PK[i];
		MK[i] = sumtmpMK + i*P[i];       //灰度级的累加均值                                                                                                                                                                                                                                                                                                                                                                                                        
		sumtmpMK = MK[i];
	}
	
	//计算类间方差
	double Var=0;
	for (int k = 0; k < Grayscale; ++k){
     
		if ((MK[Grayscale-1] * PK[k] - MK[k])*(MK[Grayscale-1] * PK[k] - MK[k]) / (PK[k] * (1 - PK[k])) > Var){
     
			Var = (MK[Grayscale-1] * PK[k] - MK[k])*(MK[Grayscale-1] * PK[k] - MK[k]) / (PK[k] * (1 - PK[k]));
			thresh = k;
		}
	}
 
	//阈值处理
	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("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\Fig1039(a)(polymersomes).tif");
	if (src.empty()){
     
		return -1;
	}
	if (src.channels() > 1)
		cv::cvtColor(src, src, CV_RGB2GRAY);
 
	cv::Mat dst,dst2;
	int thresh=0;
	double t2 = (double)cv::getTickCount();
	thresh=Otsu(src , dst, thresh); //Otsu
	std::cout << "Mythresh=" << thresh << std::endl;
	t2 = (double)cv::getTickCount() - t2;
	double time2 = (t2 *1000.) / ((double)cv::getTickFrequency());
	std::cout << "my_process=" << time2 << " ms. " << std::endl << std::endl;
    double  Otsu = 0;
 
	Otsu=cv::threshold(src, dst2, Otsu, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
	std::cout << "OpenCVthresh=" << Otsu << std::endl;
 
	cv::namedWindow("src", CV_WINDOW_NORMAL);
	cv::imshow("src", src);
	cv::namedWindow("dst", CV_WINDOW_NORMAL);
	cv::imshow("dst", dst);
	cv::namedWindow("dst2", CV_WINDOW_NORMAL);
	cv::imshow("dst2", dst2);
	//cv::imwrite("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Image Filtering\\MeanFilter\\TXT.jpg",dst);
	cv::waitKey(0);
}

THRESH_TRIANGLE
三角法求阈值最早见于Zack的论文《Automatic measurement of sister chromatid exchange frequency》主要是用于染色体的研究,该方法是使用直方图数据,基于纯几何方法来寻找最佳阈值,它的成立条件是假设直方图最大波峰在靠近最亮的一侧,然后通过三角形求得最大直线距离,根据最大直线距离对应的直方图灰度等级即为分割阈值,图示如下:
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第6张图片
对上图的详细解释
在直方图上从最高峰处bmx到最暗对应直方图bmin(p=0)%构造一条直线,从bmin处开始计算每个对应的直方图b到直线的垂直距离,知道bmax为止,其中最大距离对应的直方图位置即为图像二值化对应的阈值T。

扩展情况:
有时候最大波峰对应位置不在直方图最亮一侧,而在暗的一侧,这样就需要翻转直方图,翻转之后求得值,用255减去即得到为阈值T。扩展情况的直方图表示如下:

OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第7张图片
THRESH_TRIANGLE算法步骤

  1. 图像转灰度
  2. 计算图像灰度直方图
  3. 寻找直方图中两侧边界
  4. 寻找直方图最大值
  5. 检测是否最大波峰在亮的一侧,否则翻转
  6. 计算阈值得到阈值T,如果翻转则255-T
    代码实现:
package com.gloomyfish.filter.study;
import java.awt.image.BufferedImage;

public class TriangleBinaryFilter extends AbstractBufferedImageOp{
     

    public TriangleBinaryFilter() {
     
        System.out.println("triangle binary filter");
    }
    @Override
    public BufferedImage filter(BufferedImage src, BufferedImage dest) {
     
        int width = src.getWidth();
        int height = src.getHeight();

        if ( dest == null )
            dest = createCompatibleDestImage( src, null );
        // 图像灰度化
        int[] inPixels = new int[width*height];
        int[] outPixels = new int[width*height];
        getRGB( src, 0, 0, width, height, inPixels );
        int index = 0;
        for(int row=0; row<height; row++) {
     
            int ta = 0, tr = 0, tg = 0, tb = 0;
            for(int col=0; col<width; col++) {
     
                index = row * width + col;
                ta = (inPixels[index] >> 24) & 0xff;
                tr = (inPixels[index] >> 16) & 0xff;
                tg = (inPixels[index] >> 8) & 0xff;
                tb = inPixels[index] & 0xff;
                int gray= (int)(0.299 *tr + 0.587*tg + 0.114*tb);
                inPixels[index]  = (ta << 24) | (gray << 16) | (gray << 8) | gray;
            }
        }
        // 获取直方图
        int[] histogram = new int[256];
        for(int row=0; row<height; row++) {
     
            int tr = 0;
            for(int col=0; col<width; col++) {
     
                index = row * width + col;
                tr = (inPixels[index] >> 16) & 0xff;
                histogram[tr]++;
            }
        }
        int left_bound = 0, right_bound = 0, max_ind = 0, max = 0;
        int temp;
        boolean isflipped = false;
        int i=0, j=0;
        int N = 256;

        // 找到最左边零的位置
        for( i = 0; i < N; i++ )
        {
     
            if( histogram[i] > 0 )
            {
     
                left_bound = i;
                break;
            }
        }
     // 位置再移动一个步长,即为最左侧零位置 
        if( left_bound > 0 )
            left_bound--;
        // 找到最右边零点位置
        for( i = N-1; i > 0; i-- )
        {
     
            if( histogram[i] > 0 )
            {
     
                right_bound = i;
                break;
            }
        }
        // 位置再移动一个步长,即为最右侧零位置 
        if( right_bound < N-1 )
            right_bound++;

        // 在直方图上寻找最亮的点Hmax
        for( i = 0; i < N; i++ )
        {
     
            if( histogram[i] > max)
            {
     
                max = histogram[i];
                max_ind = i;
            }
        }
        // 如果最大值落在靠左侧这样就无法满足三角法求阈值,所以要检测是否最大值是否靠近左侧
        // 如果靠近左侧则通过翻转到右侧位置。
        if( max_ind-left_bound < right_bound-max_ind)
        {
     
            isflipped = true;
            i = 0;
            j = N-1;
            while( i < j )
            {
     
                // 左右交换
                temp = histogram[i]; histogram[i] = histogram[j]; histogram[j] = temp;
                i++; j--;
            }
            left_bound = N-1-right_bound;
            max_ind = N-1-max_ind;
        }
        // 计算求得阈值
        double thresh = left_bound;
        double a, b, dist = 0, tempdist;
        a = max; b = left_bound-max_ind;
        for( i = left_bound+1; i <= max_ind; i++ )
        {
     
            // 计算距离 - 不需要真正计算
            tempdist = a*i + b*histogram[i];
            if( tempdist > dist)
            {
     
                dist = tempdist;
                thresh = i;
            }
        }
        thresh--;

        // 对已经得到的阈值T,如果前面已经翻转了,则阈值要用255-T
        if( isflipped )
            thresh = N-1-thresh;
        // 二值化
        System.out.println("final threshold value : " + thresh);
        for(int row=0; row<height; row++) {
     
            for(int col=0; col<width; col++) {
     
                index = row * width + col;
                int gray = (inPixels[index] >> 8) & 0xff;
                if(gray > thresh)
                {
     
                    gray = 255;
                    outPixels[index]  = (0xff << 24) | (gray << 16) | (gray << 8) | gray;
                }
                else
                {
     
                    gray = 0;
                    outPixels[index]  = (0xff << 24) | (gray << 16) | (gray << 8) | gray;
                }
            }
        }
        // 返回二值图像
        setRGB(dest, 0, 0, width, height, outPixels );
        return dest;
    }

}

OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第8张图片

自适应阈值

在图像阈值化操作中,更关注的是从二值化图像中,分离目标区域和背景区域,但是仅仅通过设定固定阈值很难达到理想的分割效果。而自适应阈值,则是根据像素的邻域块的像素值分布来确定该像素位置上的二值化阈值。这样做的好处:

  1. 每个像素位置处的二值化阈值不是固定不变的,而是由其周围邻域像素的分布来决定的。

  2. 亮度较高的图像区域的二值化阈值通常会较高,而亮度低的图像区域的二值化阈值则会相适应的变小。

  3. 不同亮度、对比度、纹理的局部图像区域将会拥有相对应的局部二值化阈值。

函数原型:

1.    void adaptiveThreshold(InputArray src, OutputArray dst,  
2.                           double maxValue, int adaptiveMethod,  
3.                           int thresholdType, int bolckSize, double C) 
4. 参数说明
参数1:InputArray类型的src,输入图像,填单通道,单8位浮点类型Mat即可。
参数2:函数运算后的结果存放在这。即为输出图像(与输入图像同样的尺寸和类型)。
参数3:预设满足条件的最大值。
参数4:指定自适应阈值算法。可选择ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C两种。(具体见下面的解释)。
参数5:指定阈值类型。可选择THRESH_BINARY或者THRESH_BINARY_INV两种。(即二进制阈值或反二进制阈值)。
参数6:表示邻域块大小,用来计算区域阈值,一般选择为357......等。
参数7:参数C表示与算法有关的参数,它是一个从均值或加权均值提取的常数,可以是负数。(具体见下面的解释)。
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

对参数4与参数7内容的解释:
自适应阈值化计算大概过程是为每一个象素点单独计算的阈值,即每个像素点的阈值都是不同的,就是将该像素点周围B*B区域内的像素加权平均,然后减去一个常数C,从而得到该点的阈值。B由参数6指定,常数C由参数7指定。
ADAPTIVE_THRESH_MEAN_C,为局部邻域块的平均值,该算法是先求出块中的均值,再减去常数C。

ADAPTIVE_THRESH_GAUSSIAN_C,为局部邻域块的高斯加权和。该算法是在区域中(x, y)周围的像素根据高斯函数按照他们离中心点的距离进行加权计算,再减去常数C。

举个例子:如果使用平均值方法,平均值mean为190,差值delta(即常数C)为30。那么灰度小于160的像素为0,大于等于160的像素为255。如下图:
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第9张图片
如果是反向二值化,如下图:
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第10张图片
delta(常数C)选择负值也是可以的。

自适应阈值示例代码:

/*
    自适应阈值:adaptiveThreshold()函数
*/
#include               
#include               
#include              
#include             
using namespace std;
using namespace cv;

int main()
{
     
    //------------【1】读取源图像并检查图像是否读取成功------------    
    Mat srcImage = imread("D:\\OutPutResult\\ImageTest\\build.jpg");
    if (!srcImage.data)
    {
     
        cout << "读取图片错误,请重新输入正确路径!\n";
        system("pause");
        return -1;
    }
    imshow("【源图像】", srcImage);
    //------------【2】灰度转换------------    
    Mat srcGray;
    cvtColor(srcImage, srcGray, CV_RGB2GRAY);
    imshow("【灰度图】", srcGray);
    //------------【3】初始化相关变量---------------  
    Mat dstImage;        //初始化自适应阈值参数
    const int maxVal = 255;
    int blockSize = 3;    //取值3、5、7....等
    int constValue = 10;
    int adaptiveMethod = 0;
    int thresholdType = 1;
    /*
        自适应阈值算法
        0:ADAPTIVE_THRESH_MEAN_C
        1:ADAPTIVE_THRESH_GAUSSIAN_C
        --------------------------------------
        阈值类型
        0:THRESH_BINARY
        1:THRESH_BINARY_INV
    */
    //---------------【4】图像自适应阈值操作-------------------------
    adaptiveThreshold(srcGray, dstImage, maxVal, adaptiveMethod, thresholdType, blockSize, constValue);
 
    imshow("【自适应阈值】", dstImage);
    waitKey(0);
    return 0;
}

结果:
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第11张图片

滤波处理
另外,做不做滤波处理等对图像分割影响也比较大。

  1. adaptiveThreshold分割
Mat img=imread("D:/ImageTest/sudoku.png",CV_LOAD_IMAGE_COLOR);
    Mat dst1;
    Mat dst2;
    Mat dst3;
    cv::cvtColor(img,img,COLOR_RGB2GRAY);//进行,灰度处理
    medianBlur(img,img,5);//中值滤波
    threshold(img,dst1, 127, 255, THRESH_BINARY);//阈值分割
    adaptiveThreshold(img,dst2,255,ADAPTIVE_THRESH_MEAN_C,THRESH_BINARY,11,2);//自动阈值分割,邻域均值
    adaptiveThreshold(img,dst3,255,ADAPTIVE_THRESH_GAUSSIAN_C,THRESH_BINARY,11,2);//自动阈值分割,高斯邻域
    //ADAPTIVE_THRESH_MEAN_C : threshold value is the mean of neighbourhood area
    //ADAPTIVE_THRESH_GAUSSIAN_C : threshold value is the weighted sum of neighbourhood values where weights are a gaussian window. 
    imshow("dst1", dst1);
    imshow("dst2", dst2);
    imshow("dst3", dst3);
    imshow("img", img);
    waitKey(0);

效果对比,很明显加入邻域权重后处理更理想:
OpenCV图像阈值:简单阈值、自适应阈值、OTSU、TRIANGLE_第12张图片
2. 加入滤波处理的最大类间方差分割

Mat img=imread("D:/ImageTest/pic2.png",CV_LOAD_IMAGE_COLOR);
    Mat dst1;
    Mat dst2;
    Mat dst3;
    cv::cvtColor(img,img,COLOR_RGB2GRAY);//进行,灰度处理
    //    medianBlur(img,img,5);
    threshold(img,dst1, 127, 255, THRESH_BINARY);
    threshold(img,dst2,0, 255, THRESH_OTSU);//最大类间方差法分割 Otsu algorithm to choose the optimal threshold value
    Mat img2=img.clone();
    GaussianBlur(img2,img2,Size(5,5),0);//高斯滤波去除小噪点
    threshold(img2,dst3, 0, 255, THRESH_OTSU);
    imshow("BINARY dst1", dst1);
    imshow("OTSU dst2", dst2);
    imshow("GaussianBlur OTSU dst3", dst3);
    imshow("original img", img);
    waitKey(0);

效果如下,显然不滤波和滤波差别明显:

你可能感兴趣的:(OpenCV)