二值图像处理与分析在机器视觉与机器人视觉中非常重要,涉及到非常多的图像处理相关的知识,常见的二值图像分析包括轮廓分析、对象测量、轮廓匹配与识别、形态学处理与分割、各种形状检测与拟合、投影与逻辑操作、轮廓特征提取与编码等。二值化方法
是一种应用广泛的图像分割方法,恰当的二值化结果对于 文档图像分析、OCR以及医学图像中对 DNA 点阵图像分析等起着至关重要的作用。
通常可以分为全局二值化和局部二值化方法两类,前者将一个固定的阈值应用于整幅图像,简单易行,但在光照不均匀的条件难以应用,如 Otsu 法;后者则针对图像的不同部分采用不同的阈值来解决光照问题,其阈值实际是一个随像素变化的曲面。本文将着重介绍如何从一幅图像中获取有目的性的二值图像。
二值图像就是只有黑白两种颜色表示的图像,在数字上用0 表示黑色(0),1表示白色(255) 。在实际场景中,二值图像的获得一般需要经过如下过程。
从灰度图像到二值图像,本质上是对数据的二分类分割,所以很多数据处理的方法都可以使用,但是图像是特殊类型的数据,它有很多限制条件,决定了只有一些合适的方法才会取得比较好的效果。这些算法的最主要的一个任务就是寻找合理的分割阈值T。
二值化分割可分为手动阈值分割和自动阈值分割,前者是根据整幅图像的特征进行分析进而确定,目前常用的方法包括手动阈值法及自动阈值法,
该阈值T需要人为给定,取值范围为0-255,常规的定义为灰度图像上某点像素值 P(x, y) > T ? 255 : 0,除此之外,还有几种变式如下:
图解表示为:
在OPENCV中有相应的API可以使用,可以根据任务需求,选择不同的type值。
函数原型:
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
参数含义:
InputArray src -输入原图像,需为灰度图像
OutputArray dst -输出图像
double thresh -阈值大小 (即阈值T)
double maxval -最大值 (一般指定为255,也可指定为其他数值)
int type -阈值模式(很重要,它决定这个函数的变式,0为常规型的,1-4均为变式)
其中,type可以取0-4中的任一个值,对应的含义如下:
0: THRESH_BINARY -当前点值大于阈值时,取Maxval,否则设置为0 ;
1: THRESH_BINARY_INV -当前点值大于阈值时,设置为0,否则设置为Maxval;
2: THRESH_TRUNC -当前点值大于阈值时,设置为阈值,否则不改变;
3: THRESH_TOZERO -当前点值大于阈值时,不改变,否则设置为0 ;
4: THRESH_TOZERO_INV -当前点值大于阈值时,设置为0,否则不改变。
设阈值T = 120 ,type分别取0-4,可以得到以下结果:
1) type = 0
2)type = 1
3)type = 2
4) type = 3
5) type = 4
总结:
要使用手动阈值分割法获取理想的二值图,很关键的两个点是T与type,需要多次尝试,以经验加枚举的方式挨个测试,最终确定一个合适的阈值。但此阈值仅适用于这一特定的场景,光照等因素的改变可能导致阈值不再适用。所以,此方法适用与场景单一、固定的场合,如工业车间、流水线等机器人视觉上。
可以看出手阈值法需要多次尝试且应用场景单一,局限性较大。自动阈值分割方法可以根据环境进行阈值调整,适应性也更广。自动阈值分割法包括基于灰度均值的自动分割、基于直方图的自动分割和自适应阈值分割。下面分别对这几种方法进行分析:
此方法较为简单,即将灰度图像的像素均值作为阈值T,得到的图像如下图所示,关键代码为:
Mat src = imread("E:/images/aaa.jpg");
Mat gray, gray_mean;
cvtColor(src, gray, COLOR_BGR2GRAY);
meanStdDev(gray, gray_mean, mat_stddev);
double m;
m = mat_mean.at<double>(0, 0);
binary = Mat::zeros(src.size(), CV_8UC1);
int height = gray.rows;
int width = gray.cols;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int pv = gray.at<uchar>(row, col);
if (pv > m) {
binary.at<uchar>(row, col) = 255;
}
else {
binary.at<uchar>(row, col) = 0;
}
}
}
imshow("binary", binary);
waitKey(0);
return 0;
}
此方法是根据直方图的特征进行阈值的选择和求取,在此之前,我们先获取下灰度图像的直方图分布图如图所示,关键代码为如下,这个过程在使用直方图均值法是不需要的,为了说明问题,单独显示下直方图:
// 本段代码,在二值化中不需要,opencv中API中已包括,只是为了说明问题
Mat src = imread("E:/images/aaa.jpg");
Mat gray, gray_mean;
cvtColor(src, gray, COLOR_BGR2GRAY);
//定义变量
Mat dstHist;
int dims = 1;
float hranges[] = {0, 256};
const float *ranges[] = {hranges}; // 这里需要为const类型
int size = 256;
int channels = 0; //计算图像的直方图
calcHist(&gray, 1, &channels, Mat(), dstHist, dims, &size, ranges);
Mat dstImage(size, size, CV_8U, Scalar(0)); //获取最大值和最小值
double minValue = 0;
double maxValue = 0;
minMaxLoc(dstHist,&minValue, &maxValue, 0, 0); // 在cv中用的是cvGetMinMaxHistValue //绘制出直方图 //saturate_cast函数的作用即是:当运算完之后,结果为负,则转为0,结果超出255,则为255。
int hpt = saturate_cast<int>(0.9 * size);
for(int i = 0; i < 256; i++)
{
float binValue = dstHist.at<float>(i); // 注意hist中是float类型
int realValue = saturate_cast<int>(binValue * hpt/maxValue);
line(dstImage,Point(i, size - 1),Point(i, size - realValue),Scalar(255));
}
imshow("一维直方图", dstImage);
}
可以从直方图分布图中看出,灰度多集中在靠近白色的一端,即只有一个明显的峰值,整体图像偏白。在opencv中基于直方图获取阈值并分割的功能,实现方式仍然是使用threshold函数,只是将type类型声明为THRESH_OTSU或THRESH_RRIANGLE即可。
函数原型:
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
参数含义:
InputArray src -输入原图像,需为灰度图像
OutputArray dst -输出图像
double thresh -阈值大小,取 0
double maxval -最大值,取 255
int type -阈值模式
其中,type可以取0-4中的任一个值,对应的含义如下:
THRESH_BINARY | THRESH_OTSU 使用最大类间差分法获取阈值,然后再用该阈值进行二分 ;
THRESH_BINARY | THRESH_TRIANGLE 使用三角法获取阈值,然后再用该阈值进行二分 ;
OTSU算法对直方图有两个峰,中间有明显波谷的直方图对应图像二值化效果比较好,而对于只有一个单峰的直方图对应的图像分割效果没有双峰的好。
OTSU的是通过计算类间最大方差来确定分割阈值的阈值选择算法,它的原理是不断地求前景和背景的类件方差:
如图所示,在灰度直方图中,先将0,1,2三个灰度作为背景,求取背景类内方差,将3,4,5作为前景,求取前景类内方差。然后根据两个类内方差,求取两个类间的类间方差。如此,依次求 0 与1,2,3,4,5之间,0,1,与2,3,4,5之间,…直到遍历完整个灰度组数。然后找类间方差最大的,则灰度分界也就找到了,此分界即为所要的阈值。在opencv中实现的方法如下:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat src = imread("E:/images/aaa.jpg");
Mat gray, binary;
cvtColor(src, gray, COLOR_BGR2GRAY);
double T = threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
cout<<"threshold : %.2f\n"<< T <<endl;
imshow("binary", binary);
waitKey(0);
return 0;
}
此时返回的阈值T = 157,即采用阈值157进行常规的二值分割,其分割的结果如下图所示:
在但是有时候图像的直方图只有一个波峰,这个时候使用TRIANGLE方法寻找阈值是比较好的一个选择。
OpenCV中TRIANGLE算法使用只需要在 threshold函数的type类型声明THRESH_TRIANGLE即可。程序如下:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat src = imread("E:/images/aaa.jpg");
Mat gray, binary;
cvtColor(src, gray, COLOR_BGR2GRAY);
double T = threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_TRIANGLE);
cout<<"threshold : %.2f\n"<< T <<endl;
imshow("binary", binary);
waitKey(0);
return 0;
}
此时返回的阈值T = 215,即采用阈值215进行常规的二值分割,其分割的结果如下图所示:
OpenCV中的自适应阈值算法主要是基于均值实现,根据计算均值的方法不同分为box-filter模糊均值与高斯模糊均值,其一般步骤为:
在opencv中有相关的API可以调用,如下:
void cv::adaptiveThreshold( InputArray src, OutputArray dst, double maxValue, int
adaptiveMethod, int thresholdType, int blockSize, double C )
其中:
参数 | 取值 |
---|---|
blockSize | 取值必须是奇数,如果输入图像较大,取127左右,对于小图像取25左右 |
C | 取值多少与效果有很大关系,不能取高,也不能取低,一般取值在10/15/25 |
adaptiveMethod | ADAPTIVE_THRESH_GAUSSIAN_C = 1 , ADAPTIVE_THRESH_MEAN_C = 0 |
thresholdType | THRESH_BINARY 二值图像 = 原图 – 均值图像 > -C ? 255 : 0 ,THRESH_BINARY_INV 二值图像 = 原图 – 均值图像 > -C ? 0 : 255 |
Mat src = imread("E:/images/aaa.jpg");
Mat gray, binary;
cvtColor(src, gray, COLOR_BGR2GRAY);
adaptiveThreshold(gray, binary, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 25, 10);
imshow("binary", binary);
waitKey(0);
return 0;
在实际二值化应用中,阈值要根据实际场景和目标物特征进行选择,如果对于光照不均匀的场合应该选择自适应阈值分割,对于直方图出现明显双峰的应该选择OTSU,对与直方图出现单峰的应该选择Triangle,双峰还是单峰的分辨,可以看偏黑或偏白的占比,也可尝试运行两种方法,观察效果。
[1]: 贾志刚,《OpenCV Android开发实战》