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 算法的基本思想是根据选取的阈值将图像分为目标和背景两个部分,计算该灰度值下的类间方差值。当类间方差最大时,对应的灰度值作为最佳阈值。
算法的推导过程如下:
-
假设 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。
-
每一部分的平均灰度值 <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
- 计算他们的类间方差
<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
- 通过遍历,获得类间方差最大时对应的阈值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. 局部阈值分割
我们在使用灰度阈值分割图像的时候,会受到噪声、光照、反射的影响。在这种情况下,整幅图像用一个固定的阈值来分割,可能得不到好的分割效果。我们可以对图像降噪,以及使用可变阈值近似处理照明和反射引起的不均匀性。
后续的文章会单独介绍如何对图像降噪,在这里我们介绍一种自适应阈值分割 的方法。它的阈值是根据图像上的每一个小区域计算与其对应的阈值,在同一幅图像上的不同区域采用的是不同的阈值,根据像素的邻域块的像素值分布来确定该像素位置上的二值化阈值。
它的优点:
-
每个像素位置处的二值化阈值是由其周围邻域像素的分布来决定的。
-
不同亮度、对比度、纹理的局部图像区域将会拥有相对应的局部二值化阈值。
-
适合处理光照不均的图像。
下面的例子分别使用 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() 函数进行自适应阈值分割。