对图像应用阈值创建二值图像,是提取有意义元素的好方法。但有的时候单一阈值达不到目标提取的效果。
下面比较一下几种方法的效果。例如,给定一幅图如下,将“富贵白头”四个字提取出来。
通过OpenCV阈值化函数cv::threshold(),纯手工选择参数,进行固定阈值二值化,发现当阈值设为80时效果最好。
//采用固定阈值
cv::threshold(image, binaryFixed, 80, 255, cv::THRESH_BINARY);
固定阈值(取为80)的效果,其实还行了,只不过阈值还是得手工微调:
采用最大类间方差法怎么样?自动选择最优阈值(其实还是单一阈值)。
//大津法
std::cout<<cv::threshold(image,binaryOTSU,70,255,cv::THRESH_OTSU);//经测试cout输出为143
可见大津法算出的阈值为143。但是阈值化效果是很差的:
下面采用OpenCV的自适应阈值化方法:
cv::adaptiveThreshold(image,binaryAdaptive,255,cv::ADAPTIVE_THRESH_MEAN_C,cv::THRESH_BINARY,7,-4);
效果如下,还不错,同时花树的轮廓也描绘出了:
自适应阈值是一种局部方法。它的原理是根据每个像素的邻域计算阈值,包括上面语句采用的将每个像素的值与邻域的平均值进行比较。如果某像素的值与它的局部平均值差别很大,就会被当做异常值在阈值化过程中被分离。
代码实现:
1、使用积分图像实现自适应阈值化
//自己实现自定义阈值
cv::Mat iimage;
cv::integral(image,iimage,CV_32S);
cv::Mat binary;
image.copyTo(binary);
int blockSize = 7;
int threshold = -4;//像素将与(mean-threshold)比较
int halfSize = blockSize / 2;
for (int j = halfSize; j < image.rows-halfSize-1;j++)
{
//j行首地址
uchar* data = binary.ptr<uchar>(j);
int *data1 = iimage.ptr<int>(j-halfSize);
int *data2 = iimage.ptr<int>(j+halfSize+1);
for (int i = halfSize; i < image.cols - halfSize - 1;i++)
{
int sum = (data2[i + halfSize + 1] -
data2[i-halfSize] -
data1[i + halfSize + 1] +
data1[i-halfSize])/
(blockSize*blockSize);
if (data[i] > (sum - threshold))
data[i] = 255;
else
data[i] = 0;
}
}
cv::imshow("thresholdAdaptive",binary);
为了简化计算,计算像素周围方形邻域像素的和,采用了积分图像。假设邻域块的大小为blockSize*blockSize,不采用积分图像,每次需要计算blockSize*blockSize次加法运算;而采用积分图像,运算复杂度并不随邻域大小而改变,每次只需计算2次加法和2次减法。
2、使用盒子滤波实现自适应阈值化
cv::Mat filtered;
cv::Mat binaryFiltered;
cv::boxFilter(image,filtered,CV_8U,cv::Size(7,7));//这个盒子滤波相当于平均的效果
filtered = filtered + 4;
binaryFiltered = image >= filtered;
imshow("thresholdBox",binaryFiltered);