OpenCV入门教程03.02:基本阈值操作

索引地址:系列索引

阈(yu同玉)值

阈(你要是读fa/伐我也没意见)的意思是界限,故阈值又叫临界值,是指一个效应能够产生的最低值或最高值。此一名词广泛用于各方面,包括建筑学、生物学、飞行、化学、电信、电学、心理学等,如生态阈值。

全局二值化/全局阈值

根据自定义阀值对图像进行二值化处理,即灰度值大于阀值时设改像素灰度值为255,灰度值小于阈值时设该像素灰度值为0。这就是全局阈值,整幅图像采用同一个数作为阈值。但是这种方法并不适应与所有情况,尤其是当同一幅图像上的不同部分的具有不同亮度时。

局部二值化/局部阈值

在局部范围内根据特定算法算出局部的阀值,这个局部的大小可以自己决定(例8*8,算法也可以自己决定,本篇文章所用的用法是局部平局的灰度值作为阀值。得到局部阀值再进行局部二值化处理)。一般情况下,我们使用全局阈值。

简单来说,全局阈值是一个阈值对一整幅图有效,而局部阈值对一张图中的一部分有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include "opencv2/opencv.hpp"
#include <iostream>
#include <string>

using namespace cv;
using namespace std;

Mat image3;
Mat target3;

//全局阈值二值化
void globalTwoValue(Mat src, int value) {
target3 = Mat::zeros(src.size(), src.type());

for (int i = 0; i < src.rows; i++) {
for (int j = 0; j < src.cols; j++) {
break;
if (value > (int)src.at<uchar>(i, j))
target3.at<uchar>(i, j) = 0;
else
target3.at<uchar>(i, j) = 255;
}
}
}
// 8*8局部二值化,阈值=局部平均灰度值
void localTwoValue(Mat src) {
// 1.先算出一列有几个8,剩下几个像素
int countRow = src.rows / 8;
int rowLeft = src.rows % 8;

// 2.算出一列有几个8,剩下几个像素
int countCol = src.cols / 8;
int colLeft = src.cols % 8;

target3 = Mat::zeros(src.size(), src.type());

for (int k = 0; k < countRow; k++) {
for (int l = 0; l < countCol; l++) {
int value = 0;
for (int i = k * 8; i < (k + 1) * 8; i++) {
for (int j = l * 8; j < (l + 1) * 8; j++) {
value += (int)src.at<uchar>(i, j);
}
}
value = value / 64;
for (int i = k * 8; i < (k + 1) * 8; i++) {
for (int j = l * 8; j < (l + 1) * 8; j++) {
if ((int)src.at<uchar>(i, j) < value)
target3.at<uchar>(i, j) = 0;
else
target3.at<uchar>(i, j) = 255;
}
}
}
}

//底部不足8*8部分
if (rowLeft != 0) {
for (int k = countRow; k < countRow + rowLeft; k++) {
for (int l = 0; l < countCol; l++) {
int value = 0;
for (int i = countRow * 8; i < countRow * 8 + rowLeft; i++) {
for (int j = l * 8; j < (l + 1) * 8; j++) {
value += (int)src.at<uchar>(i, j);
}
}
value = value / (8 * rowLeft);
for (int i = countRow * 8; i < countRow * 8 + rowLeft; i++) {
for (int j = l * 8; j < (l + 1) * 8; j++) {
if ((int)src.at<uchar>(i, j) < value)
target3.at<uchar>(i, j) = 0;
else
target3.at<uchar>(i, j) = 255;
}
}
}
}
}
//右侧不足8*8部分
if (colLeft != 0) {
for (int k = 0; k < countRow; k++) {
for (int l = countCol; l < countCol + colLeft; l++) {
int value = 0;
for (int i = k * 8; i < (k + 1) * 8; i++) {
for (int j = countCol * 8; j < countCol * 8 + colLeft; j++) {
value += (int)src.at<uchar>(i, j);
}
}
value = value / (8 * colLeft);
for (int i = k * 8; i < (k + 1) * 8; i++) {
for (int j = countCol * 8; j < countCol * 8 + colLeft; j++) {
if ((int)src.at<uchar>(i, j) < value)
target3.at<uchar>(i, j) = 0;
else
target3.at<uchar>(i, j) = 255;
}
}
}
}
}
//右下角 rowleft * colleft 部分
if (rowLeft != 0 && colLeft != 0) {
int value = 0;
for (int i = 8 * countRow; i < src.rows; i++) {
for (int j = 8 * countCol; j < src.cols; j++) {
value += (int)src.at<uchar>(i, j);
}
}
value = value / (rowLeft * colLeft);
for (int i = 8 * countRow; i < src.rows; i++) {
for (int j = 8 * countCol; j < src.cols; j++) {
if ((int)src.at<uchar>(i, j) < value)
target3.at<uchar>(i, j) = 0;
else
target3.at<uchar>(i, j) = 255;
}
}
}
}

int main() {
//加上0表示读入灰度图
image3 = imread("1.jpg", 0);

if (image3.empty()) {
printf("could not load pic!\n");
return -1;
}

imshow("image3", image3);

localTwoValue(image3);

imshow("target3", target3);

waitKey(0);

return 0;
}

效果如下:

局部阈值

动态阈值效果

根据二值图定义,我们知道实际上是黑白图。灰度图值为0~255中的任意值。定义一个阈值,灰度值大于它就置为255,否则置为0。这样的话,灰度图就只剩下0/255两个值,所有的像素值只能是这两个,就是黑白图。

如何获取灰度图,就是前面提到的imread参数和cvtColor()。

而阈值函数为:

1
2
3
4
5
double cv::threshold (InputArray src,
OutputArray dst,
double thresh,
double maxval,
int type)

函数中四个参数分别是:

  • 原图像
  • 输出图像
  • 阈值 由开发者指定
  • 最大值 一般使用此函数是灰度图,灰度图最大值为255。
  • 阈值类型 一般分为五种:
    • THRESH_BINARY:大于阈值的部分像素值变为maxval,其他变为0
    • THRESH_BINARY_INV:大于阈值的部分变为0,其他部分变为最大值
    • THRESH_TRUNC:大于阈值的部分变为阈值,其余部分不变
    • THRESH_TOZERO:大于阈值的部分不变,其余部分变为0
    • THRESH_TOZERO_INV:大于阈值的部分变为0,其余部分不变

这个时候,滑动栏的用处就体现出来了。通过滑动栏,我们可以动态的调整阈值。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//---------------------------------【头文件、命名空间包含部分】----------------------------
// 描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------------------------------------
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;

//-----------------------------------【宏定义部分】--------------------------------------------
// 描述:定义一些辅助宏
//------------------------------------------------------------------------------------------------
#define WINDOW_NAME "threshold" //为窗口标题定义的宏

//-----------------------------------【全局变量声明部分】--------------------------------------
// 描述:全局变量的声明
//-----------------------------------------------------------------------------------------------
int g_nThresholdValue = 100;
int g_nThresholdType = 3;
Mat g_srcImage, g_grayImage, g_dstImage;

//-----------------------------------【全局函数声明部分】--------------------------------------
// 描述:全局函数的声明
//-----------------------------------------------------------------------------------------------
static void ShowHelpText(); //输出帮助文字
void on_Threshold(int, void *); //回调函数

//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main() {
//【0】显示欢迎和帮助文字
ShowHelpText();

//【1】读入源图片
g_srcImage = imread("1.jpg");
if (!g_srcImage.data) {
printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n");
return false;
}
imshow("Original", g_srcImage);

//【2】存留一份原图的灰度图
cvtColor(g_srcImage, g_grayImage, COLOR_RGB2GRAY);

//【3】创建窗口并显示原始图
namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE);

//【4】创建滑动条来控制阈值
createTrackbar("模式", WINDOW_NAME, &g_nThresholdType, 4, on_Threshold);

createTrackbar("参数值", WINDOW_NAME, &g_nThresholdValue, 255, on_Threshold);

//【5】初始化自定义的阈值回调函数
on_Threshold(0, 0);

// 【6】轮询等待用户按键,如果ESC键按下则退出程序
while (1) {
int key;
key = waitKey(20);
if ((char)key == 27) {
break;
}
}
}

//-----------------------------------【on_Threshold( )函数】------------------------------------
// 描述:自定义的阈值回调函数
//-----------------------------------------------------------------------------------------------
void on_Threshold(int, void *) {
//调用阈值函数
threshold(g_grayImage, g_dstImage, g_nThresholdValue, 255, g_nThresholdType);

//更新效果图
imshow(WINDOW_NAME, g_dstImage);
}

//-----------------------------------【ShowHelpText( )函数】----------------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText() {
//输出一些帮助信息
printf("\n\t欢迎来到【基本阈值操作】示例程序~\n\n");
printf("\n\t按键操作说明: \n\n"
"\t\t键盘按键【ESC】- 退出程序\n"
"\t\t滚动条模式0- 二进制阈值\n"
"\t\t滚动条模式1- 反二进制阈值\n"
"\t\t滚动条模式2- 截断阈值\n"
"\t\t滚动条模式3- 反阈值化为0\n"
"\t\t滚动条模式4- 阈值化为0\n");
}

测试结果:

threshold

自适应阈值

当同一幅图像上的不同部分的具有不同亮度时,这种情况下我们需要采用自适应阈值。此时的阈值是根据图像上的每一个小区域计算与其对应的阈值。因此在同一幅图像上的不同区域采用的是不同的阈值,从而使我们能在亮度不同的情况下得到更好的结果。

函数:

1
2
3
4
5
6
7
void cv::adaptiveThreshold (InputArray src,
OutputArray dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blockSize,
double C)

参数说明:

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

对参数adaptiveMethod与参数C内容的解释:

自适应阈值化计算大概过程是为每一个象素点单独计算的阈值,即每个像素点的阈值都是不同的,就是将该像素点周围BBB * 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。如下图:

1

如果是反向二值化,如下图:

2

delta(常数C)选择负值也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
自适应阈值:adaptiveThreshold()函数
*/

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
using namespace std;
using namespace cv;

int main()
{
//------------【1】读取源图像并检查图像是否读取成功------------
Mat srcImage = imread("1.jpg");
if (!srcImage.data)
{
cout << "读取图片错误,请重新输入正确路径!\n";
return -1;
}
imshow("Original", srcImage);
//------------【2】灰度转换------------
Mat srcGray;
cvtColor(srcImage, srcGray, COLOR_BGR2GRAY);
imshow("Gray", 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("Adaptive", dstImage);
waitKey(0);
return 0;
}

效果如图:

自定义阈值

OTSU二值化

如果是一副双峰图像(简单来说双峰图像是指图像直方图中存在两个峰)呢?我们岂不是应该在两个峰之间的峰谷选一个值作为阈值?这就是OTSU二值化要做的。简单来说就是对一副双峰图像自动根据其直方图计算出一个阈值。(对于非双峰图像,这种方法得到的结果可能会不理想)。

函数还是threshold(),但是需要多传入一个参数(flag): THRESH_OTSU。这时要把阈值设为 0。然后算法会找到最优阈值,这个最优阈值就是返回值。如果不使用OTSU二值化,返回的值与设定的阈值相等。

算法分类的原理是让背景和目标之间的类间方差最大,因为背景和目标之间的类间方差越大,说明构成图像的两部分的差别越大,错分的可能性越小。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(){
Mat srcImage=imread("1.jpg",IMREAD_GRAYSCALE);
if(srcImage.empty()){
cout<<"Read image error"<<endl;
return -1;
}

imshow("original",srcImage);

Mat resultImage;
threshold(srcImage,resultImage,0,255,THRESH_BINARY+THRESH_OTSU);

imshow("result",resultImage);

waitKey();

return 0;
}

测试结果:

ostu

三角法二值化

三角法求阈值最早见于Zack的论文《Automatic measurement of sister chromatid exchange frequency》主要是用于染色体的研究,该方法是使用直方图数据,基于纯几何方法来寻找最佳阈值,它的成立条件是假设直方图最大波峰在靠近最亮的一侧,然后通过三角形求得最大直线距离,根据最大直线距离对应的直方图灰度等级即为分割阈值,图示如下:

2

对上图的详细解释:

在直方图上从最高峰处bmx到最暗对应直方图bmin(p=0)%构造一条直线,从bmin处开始计算每个对应的直方图b到直线的垂直距离,知道bmax为止,其中最大距离对应的直方图位置即为图像二值化对应的阈值T。

扩展情况:

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

3

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(){
Mat srcImage=imread("1.jpg",IMREAD_GRAYSCALE);
if(srcImage.empty()){
cout<<"Read image error"<<endl;
return -1;
}

imshow("original",srcImage);

Mat resultImage;
threshold(srcImage,resultImage,0,255,THRESH_BINARY+THRESH_TRIANGLE);

imshow("result",resultImage);

waitKey();

return 0;
}

测试结果:

triangle

双阈值法

双阈值是使用两个阈值进行分别进行二值化操作,然后将操作结果合并起来.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 功能:代码 3-4 双阈值法的实现
// 作者:朱伟 zhu1988wei@163.com
// 来源:《OpenCV图像处理编程实例》
// 博客:http://blog.csdn.net/zhuwei1988
// 更新:2016-8-1
// 说明:版权所有,引用或摘录请联系作者,并按照上面格式注明出处,谢谢。//
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
int main( )
{
// 图像读取及判断
cv::Mat srcImage = cv::imread("..\\images\\hand1.jpg");
if( !srcImage.data )
return 1;
// 灰度转换
cv::Mat srcGray;
cv::cvtColor(srcImage, srcGray, cv::COLOR_RGB2GRAY);
cv::imshow("srcGray", srcGray);
// 初始化阈值参数
const int maxVal = 255;
int low_threshold = 150;
int high_threshold = 210;
cv::Mat dstTempImage1, dstTempImage2, dstImage;
// 小阈值对源灰度图像进行阈值化操作
cv::threshold( srcGray, dstTempImage1,
low_threshold, maxVal, cv::THRESH_BINARY );
// 大阈值对源灰度图像进行阈值化操作
cv::threshold( srcGray, dstTempImage2,
high_threshold, maxVal,cv::THRESH_BINARY_INV );
// 矩阵与运算得到二值化结果
cv::bitwise_and( dstTempImage1,
dstTempImage2, dstImage );
cv::imshow("dstImage", dstImage);
cv::waitKey(0);
return 0;
}

效果为:

dual threshold

半阈值法

半阈值法是先阈值处理,然后将操作结果与原图进行与操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 功能:代码 3-5 半阈值法的实现
// 作者:朱伟 zhu1988wei@163.com
// 来源:《OpenCV图像处理编程实例》
// 博客:http://blog.csdn.net/zhuwei1988
// 更新:2016-8-1
// 说明:版权所有,引用或摘录请联系作者,并按照上面格式注明出处,谢谢。//
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main( )
{
// 读取源图像及判断
cv::Mat srcImage = cv::imread("..\\images\\hand1.jpg");
if( !srcImage.data )
return 1;
// 转化为灰度图像
cv::Mat srcGray;
cv::cvtColor(srcImage, srcGray, cv::COLOR_RGB2GRAY);
cv::imshow("srcGray", srcGray);
// 初始化阈值参数
const int maxVal = 255;
int thresholdVal = 150;
cv::Mat dstTempImage, dstImage;
// 阈值对源灰度图像进行阈值化操作
cv::threshold( srcGray, dstTempImage,
thresholdVal, 255, cv::THRESH_BINARY );
// 矩阵与运算得到二值化结果
cv::bitwise_and( srcGray, dstTempImage, dstImage );
cv::imshow("dstImage", dstImage);
cv::waitKey(0);
return 0;
}

效果为:

half threshold


OpenCV入门教程03.02:基本阈值操作
https://blog.jackeylea.com/opencv/basic-operations-of-opencv-threshold/
作者
JackeyLea
发布于
2020年6月7日
许可协议