一、 算法目的及原理
(1) 目的:
阈值分割可以把图像中的前景目标和背景分割开,它特别适用于目标和背景占据不同灰度级范围的图像。它不仅可以极大的压缩图像的数据信息,而且也大大简化了分析和处理步骤,因此在很多情况下,是进行图像分析、特征提取与模式识别之前的必要的图像预处理过程,常用于机器视觉产品的检测。
(2) 原理:
通过统计灰度直方图,在峰与峰的灰度级之间设定阈值,把图像分割成n类。基于OTSU的阈值分割是根据计算双峰直方图的最大类间方差,从而确定把前景目标和背景区分的最佳阈值,通过阈值把原图像分割成一张二值图像,就能明显区分出前景和背景。而图像分块则是为了解决图像的光线亮度不均匀的问题,把图像分割成只含明显双峰的小块ROI图像,再做阈值分割,就能得到比较好的分割效果。
二、算法设计
(1) OTSU的最佳阈值分割:
输入:一张图像
1.统计图片的归一化灰度直方图h[256];
2. x_max=0 ; index = 0 ; 遍历k=0-254的h[k] { //0-k为A类,255-k为B类
计算概率和均值:
p1=[k]Σ[i=0] h[i];
m1=1/p1*[k]Σ[i=0] i*pi;
p2=1-p1;
m2=[255]Σ[i=k+1] i*pi;
计算类间方差:
x=p1*p2*(m1-m2)^2;//到这里就算完了一次类间方差
//记录最大的类间方差的阈值k;
if(x>x_max) {
x_max=x;
index = k;
}
}
3.return index; //返回最佳分割阈值
(2) 图像分块中应用OTSU进行阈值分割
输入:Mat src ,int x ,int y, bool key=0;
参数解释:
原图src;
把原图分割, 横向有x块roi,纵向有y块roi;
当src.cols不能整除x时,会有余边没有纳入roi,称为边界,key可以选择是否处理边界,
默认key=0不处理边界,不处理边界时舍弃边界,边界显示为全黑
函数体:
1、把src转换成灰度图:cvtColor();
2、计算:
int cut_cols =src.cols/x; //roi的列数;
int cut_rows = src.rows/y; //roi的行数;
remainder_cols=src.cols-cut_cols*x; //右边界的列数
remainder_rows=src.rows-cut_rows*y; //下边界的行数
Mat result = Mat::zeros(src.size(), src.type());//定义结果图像
3、分割src图像成x*y块roi图像:
循环:先从src割出[0,0]位置的一块,然后循环分割到[x,y]这一块
循环体内 对某一块roi进行处理:{
调用ostu算子,求出roi的最佳阈值k;
用最佳阈值k,对roi,做阈值分割(使用阈值分割函数:threshold),成二值图像Mat temp;
把temp保存到结果图像result对应的[x,y]位置;
}
4、补充处理边界:(key=1时)
4.1 在第3步的循环体内,当处理完某一行的roi时,就可以对“右边边界”进行处理了;
判断if(remainder_cols>0){
取这行中最后一小块roi,大小为(remainder_cols,cut_rows)
然后对roi进行第3步中的循环体的操作
}
4.2 当处理完所有的[x,y]的roi时,就可以对“下边边界”进行处理了;
判断if(remainder_rows>0){
循环取这一行的roi,大小为(cut_cols, remainder_rows)
然后对roi进行第3步中的循环体的操作
4.3 循环完这一行的roi之后,就可以对“下边界的右边界”进行处理了;
取最后一块roi,大小为(remainder_cols, remainder_rows)
然后对roi进行第3步中的循环体的操作
}
三、 C++代码实现:
//使用OTSU大津法求解最佳阈值
int ostu(Mat src) {
long H[256] = {
0 }; //统计直方图
double h[256] = {
0.0 }; //归一化直方图
if (src.channels() > 1) {
cvtColor(src, src, COLOR_BGR2GRAY);
//imshow("gray", gray);
}
for (int i = 0; i < src.rows; i++) {
uchar* ptr_gray = src.ptr<uchar>(i);
for (int j = 0; j < src.cols; j++) {
H[ptr_gray[j]]++;
}
}
double size = src.rows*src.cols*1.0f;
for (int i = 0; i < 256; i++) {
h[i] = H[i] / size;
}
double x_max = 0;
int index = 0;
//求最佳阈值k
for (int k = 0; k < 255; k++) {
double p1 = 0.0;
double temp1 = 0.0;
for (int i = 0; i <= k; i++) {
p1 += h[i];
temp1 += i * h[i];//i=0?
}
double m1 = 1 / p1 * temp1;
double p2 = 1.0 - p1;
double temp2 = 0.0;
for (int i = k + 1; i < 256; i++) {
temp2 += i * h[i];
}
double m2 = 1 / p2 * temp2;
double x = p1 * p2*(m1 - m2)*(m1 - m2);
if (x > x_max) {
x_max = x;
index = k;
}
}
return index;
}
//把图像分割成x列,y行的小ROI,并用调用上面的ostu进行阈值分割,返回一张分块阈值分割完成之后的完整图像
Mat cutImage(Mat src, int x, int y,bool key) {
if (src.channels() > 1) {
cvtColor(src, src, COLOR_BGR2GRAY);
}
int cutCols = (int)(src.cols / x);//分块后,每一小块的长、列数、坐标的x
int cutRows = (int)(src.rows / y);//分块后,每一小块的宽、行数、坐标的y
int remainder_cols = src.cols - cutCols * x;
int remainder_rows = src.rows - cutRows * y;
Mat newImage = Mat::zeros(src.size(), src.type());//装阈值分割后的图像
int k = 0;//最佳阈值
Mat temp;//图像小块
Mat ostuImage;//小块阈值分割图像
int i, j;
for (i = 0; i < y; i++) {
for (j = 0; j < x; j++) {
Point2f p(j*cutCols, i*cutRows);
//cout << p << endl;
temp = src(Rect(p, Size(cutCols, cutRows)));
k = ostu(temp);
//cout<<"k: "<<k<<endl;
threshold(temp, ostuImage, k, 255, THRESH_BINARY);
//图像保存
for (int a = i * cutRows, n = 0; n < cutRows; a++, n++) {
//a < (i + 1) * cutRows
uchar* ptr_newImage = newImage.ptr<uchar>(a);
uchar* ptr_ostuImage = ostuImage.ptr<uchar>(n);
for (int b = j * cutCols, m = 0; m < cutCols; b++, m++) {
// b < (j + 1)*cutCols
ptr_newImage[b] = ptr_ostuImage[m];
}
}
}
//右余边处理
if (remainder_cols > 0 && key == 1) {
Point2f p(j*cutCols, i*cutRows);
temp = src(Rect(p, Size(remainder_cols, cutRows)));
k = ostu(temp);
threshold(temp, ostuImage, k, 255, THRESH_BINARY);
for (int a = i * cutRows, n = 0; n < cutRows; a++, n++) {
uchar* ptr_newImage = newImage.ptr<uchar>(a);
uchar* ptr_ostuImage = ostuImage.ptr<uchar>(n);
for (int b = cutCols * x, m = 0; m < remainder_cols; b++, m++) {
ptr_newImage[b] = ptr_ostuImage[m];
}
}
}
}
//下余边处理
if (remainder_rows > 0 && key == 1) {
//分块的最后的一行
for (j = 0; j < x; j++) {
//分块的列
Point2f p(j*cutCols, cutRows*y);
temp = src(Rect(p, Size(cutCols, remainder_rows)));
k = ostu(temp);
threshold(temp, ostuImage, k, 255, THRESH_BINARY);
//图像保存
for (int a = cutRows * y, n = 0; n < remainder_rows; a++, n++) {
//a < (i + 1) * cutRows
uchar* ptr_newImage = newImage.ptr<uchar>(a);
uchar* ptr_ostuImage = ostuImage.ptr<uchar>(n);
for (int b = j * cutCols, m = 0; m < cutCols; b++, m++) {
// b < (j + 1)*cutCols
ptr_newImage[b] = ptr_ostuImage[m];
}
}
}
//下余边的右余边处理
if (remainder_cols > 0) {
Point2f p(j*cutCols, y*cutRows);
temp = src(Rect(p, Size(remainder_cols, remainder_rows)));
k = ostu(temp);
threshold(temp, ostuImage, k, 255, THRESH_BINARY);
for (int a = y * cutRows, n = 0; n < remainder_rows; a++, n++) {
uchar* ptr_newImage = newImage.ptr<uchar>(a);
uchar* ptr_ostuImage = ostuImage.ptr<uchar>(n);
for (int b = cutCols * x, m = 0; m < remainder_cols; b++, m++) {
ptr_newImage[b] = ptr_ostuImage[m];
}
}
}
}
return newImage;
}
处理的函数main函数:
#include <opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
#include <ctime>
using namespace cv;
using namespace std;
int ostu(Mat src);
Mat cutImage(Mat src, int x, int y);
int main()
{
Mat src = imread("C:/Users/***/Desktop/blobs.tif");//读取本地图像
if (src.empty()) {
cout << "error!" << endl;
return 0;
}
imshow("src", src);
if (src.channels() > 1) {
cvtColor(src, src, COLOR_RGB2GRAY);
}
int k;//阈值
k = ostu(src);
Mat ostuImage;//分割图像
threshold(src, ostuImage, k, 255, THRESH_BINARY);
imshow("ostuImage", ostuImage); //背景为白色
Mat cut;
cut = cutImage(src, 4, 2);//最大余边15, 14,最好效果4,2
imshow("cut", cut);
waitKey(0);
return 0;
}
四、处理效果
原图:
阈值分割后:
五、 思考记录:
这种算法的关键点在于分块怎么分,经过测试发现,最好就是让分块后每一块ROI里都让前景和背景都占一定的比例,让分割后的ROI图像的直方图形成双峰分布。