OpenCV 笔记(7):基于阈值的图像分割

1. 阈值分割

图像分割是图像进行视觉分析和模式识别的基本前提,而阈值分割是最简单的图像分割方法。阈值分割是基于灰度值或灰度值的特性来将图像直接划分为区域,实现简单而且计算速度快。

1.1 threshold() 函数的5种处理类型

前面的文章提过,OpenCV 提供了基于灰度值的阈值分割函数 threshold(),在使用 threshold() 时先要将图像灰度化。

这个 threshold() 函数提供了 5 种阈值化类型。

  • THRESH_BINARY

将小于阈值的像素点灰度值置为0;大于阈值的像素点灰度值置为最大值(255)。

<math xmlns="http://www.w3.org/1998/Math/MathML"> d s t ( x , y ) = { m a x v a l , i f s r c ( x , y ) > t h r e s h 0 , o t h e r w i s e dst(x,y)= \left\{\begin{matrix} maxval, if src(x,y) > thresh\\ 0, otherwise \end{matrix}\right. </math>dst(x,y)={maxval,if src(x,y) > thresh0,otherwise

  • THRESH_BINARY_INV

将大于阈值的像素点灰度值置为0;小于阈值的像素点灰度值置为最大值(255)。

<math xmlns="http://www.w3.org/1998/Math/MathML"> d s t ( x , y ) = { 0 , i f s r c ( x , y ) > t h r e s h m a x v a l , o t h e r w i s e dst(x,y)= \left\{\begin{matrix} 0, if src(x,y) > thresh\\ maxval, otherwise \end{matrix}\right. </math>dst(x,y)={0,if src(x,y)>threshmaxval,otherwise

  • THRESH_TRUNC

小于阈值的像素点灰度值不变;大于阈值的像素点灰度值置为该阈值。

<math xmlns="http://www.w3.org/1998/Math/MathML"> d s t ( x , y ) = { t h r e s h o l d , i f s r c ( x , y ) > t h r e s h s r c ( x , y ) , o t h e r w i s e dst(x,y)= \left\{\begin{matrix} threshold, if src(x,y) > thresh\\ src(x,y) , otherwise \end{matrix}\right. </math>dst(x,y)={threshold,if src(x,y)>threshsrc(x,y),otherwise

  • THRESH_TOZERO

大于阈值的像素点灰度值不变;小于阈值的像素点灰度值置为0

<math xmlns="http://www.w3.org/1998/Math/MathML"> d s t ( x , y ) = { s r c ( x , y ) , i f s r c ( x , y ) > t h r e s h 0 , o t h e r w i s e dst(x,y)= \left\{\begin{matrix} src(x,y), if src(x,y) > thresh\\ 0, otherwise \end{matrix}\right. </math>dst(x,y)={src(x,y),if src(x,y)>thresh0,otherwise

  • THRESH_TOZERO_INV

小于阈值的像素点灰度值不变;大于阈值的像素点置为0

<math xmlns="http://www.w3.org/1998/Math/MathML"> d s t ( x , y ) = { 0 , i f s r c ( x , y ) > t h r e s h s r c ( x , y ) , o t h e r w i s e dst(x,y)= \left\{\begin{matrix} 0, if src(x,y) > thresh\\ src(x,y), otherwise \end{matrix}\right. </math>dst(x,y)={0,if src(x,y)>threshsrc(x,y),otherwise

下面的例子,通过获取图像的均值作为阈值,来分别展示这五种阈值分割的使用:

cpp 复制代码
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc,char *argv[])
{
    Mat src = imread(".../landscape.jpg");
    imshow("src",src);

    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);
    Scalar m = mean(gray);
    int thresh = m[0];

    Mat dst;
    threshold(gray, dst,thresh,255, THRESH_BINARY);
    imshow("thresh_binary",dst);

    threshold(gray, dst,thresh,255, THRESH_BINARY_INV);
    imshow("thresh_binary_inv",dst);

    threshold(gray, dst,thresh,255, THRESH_TRUNC);
    imshow("thresh_trunc",dst);

    threshold(gray, dst,thresh,255, THRESH_TOZERO);
    imshow("thresh_tozero",dst);

    threshold(gray, dst,thresh,255, THRESH_TOZERO_INV);
    imshow("thresh_tozero_inv",dst);

    waitKey(0);
    return 0;
}

THRESH_BINARY 和 THRESH_BINARY_INV 可以通过阈值分割将灰度图像转变成二值图像。而通过其他的阈值方式进行分割,仍然得到灰度图像。

2. 全局阈值分割

对图像进行灰度化之后,若图像中的目标和背景具有不同的灰度集合,且这两个灰度集合可用一个灰度级阈值 T 进行分割。

分割后的图像 g(x,y) 满足:

<math xmlns="http://www.w3.org/1998/Math/MathML"> g ( x , y ) = { 1 , i f s r c ( x , y ) > T 0 , o t h e r w i s e g(x,y)= \left\{\begin{matrix} 1, if src(x,y) > T\\ 0, otherwise \end{matrix}\right. </math>g(x,y)={1,if src(x,y)>T0,otherwise

当 T 是一个适用于整个图像的常数时,称为全局阈值分割。

在大多数情况下,图像之间会有较大变化,即使全局阈值分割是一种合适的方法,也需要有能对每幅图像自动估计阈值的算法。

2.1 OTSU 算法

OTSU 算法,是1979年由日本学者大津提出的,也被称为大津算法、最大类间差法。OTSU 算法在类间方差 最大的情况下是最佳的,完全以在一幅图像的直方图上执行计算为基础。

直方图是一种常用的数据统计图。对某一物理或特征量不同取值,找出它的最大值和最小值,然后确定一个区间,使其包含全部测量数据,将区间分成若干小区间,统计测量结果出现在各小区间的频数或占比,以测量数据为横坐标,以频数或占比为纵坐标,划出各小区间及其对应的频数或占比高度,则可得到一个矩形图。

OTSU 算法的基本思想是根据选取的阈值将图像分为目标和背景两个部分,计算该灰度值下的类间方差值。当类间方差最大时,对应的灰度值作为最佳阈值。

算法的推导过程如下:

  1. 假设 M*N 尺寸的图像的灰度值区间为[0,m],t 为阈值,它将图像分为目标和背景两个部分。即为灰度值为 [0,t] 的背景以及灰度值为 [t+1,m] 的目标两部分。其中,背景的像素点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N 0 N_0 </math>N0,目标的像素点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N 1 N_1 </math>N1。

  2. 每一部分的平均灰度值 <math xmlns="http://www.w3.org/1998/Math/MathML"> μ 0 、 μ 1 \mu_0、\mu_1 </math>μ0、μ1,计算每一部分的所占比例 <math xmlns="http://www.w3.org/1998/Math/MathML"> w 0 、 w 1 w_0、w_1 </math>w0、w1,以及总的平均灰度值 <math xmlns="http://www.w3.org/1998/Math/MathML"> μ \mu </math>μ。

<math xmlns="http://www.w3.org/1998/Math/MathML"> w 0 = N 0 / M ∗ N w_0 = N_0/ M*N </math>w0 = N0/ M∗N

<math xmlns="http://www.w3.org/1998/Math/MathML"> w 1 = N 1 / M ∗ N w_1 = N_1/M*N </math>w1 = N1/M∗N

并且 <math xmlns="http://www.w3.org/1998/Math/MathML"> N 0 + N 1 = M ∗ N N_0 + N_1 = M*N </math>N0 + N1 = M∗N

则, <math xmlns="http://www.w3.org/1998/Math/MathML"> w 0 + w 1 = 1 w_0 + w_1 = 1 </math>w0 + w1 = 1

<math xmlns="http://www.w3.org/1998/Math/MathML"> μ = μ 0 ∗ N 0 + μ 1 ∗ N 1 M ∗ N = μ 0 ∗ w 0 + μ 1 ∗ w 1 \mu = \frac{\mu_0*N_0 + \mu_1*N_1}{M*N} = \mu_0*w_0 + \mu_1*w_1 </math>μ = M∗Nμ0∗N0 + μ1∗N1 = μ0∗w0 + μ1∗w1

  1. 计算他们的类间方差

<math xmlns="http://www.w3.org/1998/Math/MathML"> δ 2 = w 0 ∗ ( μ 0 − μ ) 2 + w 1 ∗ ( μ 1 − μ ) 2 = w 0 ∗ w 1 ∗ ( μ 0 − μ 1 ) 2 \delta^2 = w_0*(\mu_0-\mu)^2+w_1*(\mu_1-\mu)^2=w_0*w_1*(\mu_0-\mu_1)^2 </math>δ2 = w0∗(μ0−μ)2+w1∗(μ1−μ)2=w0∗w1∗(μ0−μ1)2

  1. 通过遍历,获得类间方差最大时对应的阈值t作为我们最终所取的阈值。

背景和目标之间的类间方差越大,则构成图像的这两部分的差别越大,因此可以将这个阈值来进行全局的阈值分割。

下面的例子,使用 OTSU 算法来计算阈值,并且将阈值分割后的二值图像展示出来。

cpp 复制代码
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc,char *argv[])
{
    Mat src = imread(".../landscape.jpg");
    imshow("src",src);

    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);

    Mat dst;
    int thresh = threshold(gray, dst,0,255, THRESH_BINARY | THRESH_OTSU);
    imshow("thresh_ostu",dst);

    cout << "thresh = " << thresh << endl;

    waitKey(0);
    return 0;
}

输出结果:

ini 复制代码
thresh = 118

在使用 threshold() 函数时,如果使用 THRESH_BINARY类型来进行阈值分割,通常需要黑色的背景。如果是白色的背景,则需要使用 THRESH_BINARY_INV类型。例如下面的图像是白色的背景:

cpp 复制代码
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc,char *argv[])
{
    Mat src = imread(".../phone.jpg");
    imshow("src",src);

    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);

    Mat dst;
    threshold(gray, dst,0,255, THRESH_BINARY_INV | THRESH_OTSU);
    imshow("dst",dst);

    waitKey(0);
    return 0;
}

2.2 Triangle 算法

OTSU 算法是针对直方图中有两个波峰的情况,效果会比较好。但是针对直方图中只有一个波峰的情况,则 Triangle 算法会比较好。

它的成立条件是假设直方图最大波峰在靠近最亮的一侧,然后通过三角形求得最大直线距离,根据最大直线距离对应的直方图灰度值即为阈值。

下面的例子,使用 Triangle 算法来计算阈值,并且将阈值分割后的二值图像展示出来。

cpp 复制代码
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc,char *argv[])
{
    Mat src = imread(".../landscape.jpg");
    imshow("src",src);

    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);

    Mat dst;
    int thresh = threshold(gray, dst,0,255, THRESH_BINARY | THRESH_TRIANGLE);
    imshow("thresh_triangle",dst);

    cout << "thresh = " << thresh << endl;

    waitKey(0);
    return 0;
}

输出结果:

ini 复制代码
thresh = 80

3. 局部阈值分割

我们在使用灰度阈值分割图像的时候,会受到噪声、光照、反射的影响。在这种情况下,整幅图像用一个固定的阈值来分割,可能得不到好的分割效果。我们可以对图像降噪,以及使用可变阈值近似处理照明和反射引起的不均匀性。

后续的文章会单独介绍如何对图像降噪,在这里我们介绍一种自适应阈值分割 的方法。它的阈值是根据图像上的每一个小区域计算与其对应的阈值,在同一幅图像上的不同区域采用的是不同的阈值,根据像素的邻域块的像素值分布来确定该像素位置上的二值化阈值。

它的优点:

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

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

  3. 适合处理光照不均的图像。

下面的例子分别使用 OTSU 算法和自适应阈值分割来实现二值化,其中 OpenCV 提供了 adaptiveThreshold() 函数实现自适应阈值分割。

cpp 复制代码
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc,char *argv[])
{
    Mat src = imread(".../viaduct.jpg");
    imshow("src",src);

    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);

    Mat dst;
    threshold(gray, dst,0,255, THRESH_BINARY | THRESH_OTSU);
    imshow("thresh_ostu",dst);

    adaptiveThreshold(gray, dst, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 11, 2);
    imshow("adaptiveThreshold",dst);

    waitKey(0);
    return 0;
}

稍微对 adaptiveThreshold() 函数的参数做一下解释:

第四个参数 adaptiveMethod:指定自适应阈值算法。

  • ADAPTIVE_THRESH_MEAN_C:局部邻域块的平均值。该算法是先求出块中的均值,再减去常数 c。

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

第六个参数 blockSize:邻域块大小。

第七个参数 c:与算法有关的参数,阈值就等于计算出的平均值或者加权平均值减去这个常数,c 可以是负数。

4. 总结

本文介绍了传统图像分割的方法,主要是介绍了基于灰度图像的阈值分割。

阈值分割并不等同于图像的二值化, threshold() 函数有五种阈值类型,它适合全局的阈值分割。对于光照不均的图像可以采用 adaptiveThreshold() 函数进行自适应阈值分割。

相关推荐
FL16238631293 小时前
[C++]使用纯opencv部署yolov11旋转框目标检测
opencv·yolo·目标检测
方世恩6 小时前
【进阶OpenCV】 (5)--指纹验证
人工智能·opencv·目标检测·计算机视觉
FL16238631296 小时前
[C++]使用纯opencv部署yolov11-pose姿态估计onnx模型
c++·opencv·yolo
A_lvvx6 小时前
OpenCV透视变换
人工智能·opencv·计算机视觉
鲸~屿7 小时前
计算机视觉 第十章OpenCV
人工智能·opencv·计算机视觉
菜就多练_082811 小时前
《深度学习》OpenCV 摄像头OCR 过程及案例解析
人工智能·深度学习·opencv·ocr
红米煮粥1 天前
OpenCV-图像拼接
人工智能·opencv·计算机视觉
jndingxin1 天前
OpenCV视频I/O(8)视频采集类VideoCapture之从视频源中读取一帧图像函数read()的使用
人工智能·opencv·音视频
Bill661 天前
OpenCV 形态学相关函数详解及用法示例
opencv·opencv形态学函数·腐蚀与膨胀·开运算与闭运算·顶帽与黑帽
翁乐安1 天前
opencv-如何获取图像区域特定像素区域大小
人工智能·python·opencv