灰度图像与二值化
问题描述:将彩色图像转换为灰度图像并进行二值化处理
代码如下(示例):
#include <opencv2/opencv.hpp>
cv::Mat BGR2GRAY(cv::Mat img)
{
int width = img.cols;
int height = img.rows;
// CV_8UC1代表单通道的8bit array数组
cv::Mat new_image = cv::Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < height; ++i){
for (int j = 0; j < width; ++j){
// 转换公式 img_gray = 0.2126 * R + 0.7152 * G + 0.0722 * B;
// 需要注意图像格式输出的是整数
new_image.at<uchar>(i, j) = 0.2126 * (float)img.at<cv::Vec3b>(i, j)[2]\
+ 0.7152 * (float)img.at<cv::Vec3b>(i, j)[1]\
+ 0.0722 * (float)img.at<cv::Vec3b>(i, j)[0];
}
}
return new_image;
}
cv::Mat Binarize(cv::Mat img, int thres)
{
int width = img.cols;
int height = img.rows;
cv::Mat new_image = cv::Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < height; ++i){
for (int j = 0; j < width; ++j){
if (img.at<uchar>(i, j) > thres)
{
new_image.at<uchar>(i, j) = 255;
}
else
{
img.at<uchar>(i, j) = 0;
}
}
}
return new_image;
}
cv::Mat Binarize_Otsu(cv::Mat img)
{
int width = img.cols;
int height = img.rows;
double count0 = 0, count1 = 0;
double sum_val0 = 0, sum_val1 = 0;
double sigma_max = 0, sigma = 0;
int thres = 0;
int val = 0;
for (int th = 0; th < 255; ++th)
{
for (int i = 0; i < height; ++i)
{
count0 = 0; // 每次循环重置参数
count1 = 0;
sum_val0 = 0;
sum_val1 = 0;
for (int j = 0; j < width; ++j)
{
val = (int)img.at<uchar>(i, j);
if (val < th)
{
count0++;
sum_val0 += val;
}
else
{
count1++;
sum_val1 += val;
}
}
}
sum_val0 /= count0;
sum_val1 /= count1;
count0 /= (height * width);
count1 /= (height * width);
sigma = count0 * count1 * pow((sum_val0 - sum_val1), 2);
if (sigma > sigma_max)
{
sigma_max = sigma;
thres = th;
}
}
std::cout << "threshold: " << thres << std::endl;
cv::Mat new_image = cv::Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < height; ++i){
for (int j = 0; j < width; ++j){
if (img.at<uchar>(i, j) > thres)
{
new_image.at<uchar>(i, j) = 255;
}
else
{
img.at<uchar>(i, j) = 0;
}
}
}
return new_image;
}
int main()
{
cv::Mat img = cv::imread("../Neopolitan.png", cv::IMREAD_COLOR);
// BGR2GRAY
cv::Mat gray_image = BGR2GRAY(img);
cv::Mat new_image = Binarize(gray_image, 128);
// cv::Mat new_image = Binarize_Otsu(gray_image); // 大津二值化
cv::imwrite("../1-10/Neopolitan2.png", gray_image);
cv::imwrite("../1-10/Neopolitan3.png", new_image);
// cv::imwrite("../1-10/Neopolitan4.png", new_image);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
输入图像 (Neopolitan.png) | 灰度图像 (Neopolitan2.png) | 二值化图像 (Neopolitan3.png) | 大津二值化图像 (Neopolitan4.png) |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
Vec3b
Vec3b
可以看作是vector<uchar, 3>
,简单而言就是一个uchar类型的,长度为3的vector向量。
由于在OpenCV中,使用imread读取到的Mat图像数据,都是用uchar类型的数据存储,对于RGB三通道的图像,每个点的数据都是一个Vec3b类型的数据。使用at定位方法如下:
img.at<Vec3b>(row, col)[0] = 255; // 这是指修改B通道数据
img.at<Vec3b>(row, col)[1] = 255; // 这是指修改G通道数据
img.at<Vec3b>(row, col)[2] = 255; // 这是指修改R通道数据
大津二值化
大津算法,也被称作最大类间方差法,是一种可以自动确定二值化中阈值的算法。即,使得类内方差和类间方差的比值最大的阈值即为大津二值化阈值。
我们可以通过以下的参数来计算大津二值化阈值:
- 我们可以通过阈值 t h th th将图像灰度值分为两类,小于阈值 t h th th的类记作 0 0 0,大于阈值 t h th th的类记作 1 1 1;
- w 0 w_0 w0和 w 1 w_1 w1分别代表 0 0 0类和 1 1 1类中的灰度值数目占总灰度值数目(也即像素个数)的比率(满足 w 0 + w 1 = 1 w_0+w_1=1 w0+w1=1);
- S 0 2 {S_0}^2 S02, S 1 2 {S_1}^2 S12分别代表 0 0 0类和 1 1 1类中灰度值的方差;
- M 0 M_0 M0, M 1 M_1 M1分别代表 0 0 0类和 1 1 1类中灰度值的平均值;
基于上述定义,我们便可以根据下述公式计算出类内方差和类间方差,即:
- 类内方差: S w 2 = w 0 S 0 2 + w 1 S 1 2 {S_w}^2=w_0\ {S_0}^2+w_1\ {S_1}^2 Sw2=w0 S02+w1 S12
- 类间方差: S b 2 = w 0 ( M 0 − M t ) 2 + w 1 ( M 1 − M t ) 2 = w 0 w 1 ( M 0 − M 1 ) 2 {S_b}^2 = w_0 \ (M_0 – M_t)^2 + w_1\ (M_1 – M_t)^2 = w_0\ w_1\ (M_0 – M_1) ^2 Sb2=w0 (M0−Mt)2+w1 (M1−Mt)2=w0 w1 (M0−M1)2
- 图像所有灰度值的方差: S t 2 = S w 2 + S b 2 = 常数 {S_t}^2 = {S_w}^2 + {S_b}^2 = \text{常数} St2=Sw2+Sb2=常数
根据上述公式,我们可以类内方差和类间方差的比值,并用分离度 X X X来表示:
X = S b 2 S w 2 = S b 2 S t 2 − S b 2 X = \frac{ {S_b}^2}{ {S_w}^2} = \frac{ {S_b}^2}{ {S_t}^2 – {S_b}^2} X=Sw2Sb2=St2−Sb2Sb2
也就是说:
arg max t X = arg max t S b 2 \arg\max\limits_{t}\ X=\arg\max\limits_{t}\ {S_b}^2 argtmax X=argtmax Sb2
换言之,如果使 S b 2 = w 0 w 1 ( M 0 − M 1 ) 2 {S_b}^2={w_0}\ {w_1}\ (M_0 – M_1)^2 Sb2=w0 w1 (M0−M1)2最大,就可以得到最好的二值化阈值 t h th th。