OpenCV作为一个开源的图像处理库,它的任务主要是对图像进行操作. 那么对于我们学习者来说,首先要弄明白的是OpenCV的基础数据结构.
稍微有点图像处理基础的同学都知道,图像的基础结构就是像素. 那么对于OpenCV2.1以上的版本来说,不需要再使用IplImage这个数据结构来作为图像的基础数据结构了,现在我们只需要了解Mat.
今天的例程是一个对图像进行反色处理的样例.
老规矩,先上代码,再进行分析.
代码1.数组方法访问像素.
int test1()
{
std::string filename = "../images/lena.jpg";
cv::Mat srcImg = cv::imread(filename, CV_8UC2); // 读灰度图时改为CV_8UC1
cv::Mat dstImg = cv::Mat::zeros(srcImg.size(), srcImg.type());
//uchar grayVal = 0;
//uchar Blue = 0, Green = 0, Red = 0;
for (int i = 0; i < srcImg.rows; ++i)
{
uchar* curr_row = srcImg.ptr<uchar>(i);
uchar* dst_row = dstImg.ptr<uchar>(i);
for (int j = 0; j < srcImg.cols; ++j)
{
if (3 == srcImg.channels())
{
//Blue = 255 - srcImg.at(i, j)[0];
//Green = 255 - srcImg.at(i, j)[1];
//Red = 255 - srcImg.at(i, j)[2];
//dstImg.at(i, j) = cv::Vec3b(Blue, Green, Red);
dstImg.at<cv::Vec3b>(i, j)[0] = 255 - srcImg.at<cv::Vec3b>(i, j)[0];
dstImg.at<cv::Vec3b>(i, j)[1] = 255 - srcImg.at<cv::Vec3b>(i, j)[1];
dstImg.at<cv::Vec3b>(i, j)[2] = 255 - srcImg.at<cv::Vec3b>(i, j)[2];
}
else if (1 == srcImg.channels())
{
//grayVal = 255 - srcImg.at(i, j);
//dstImg.at(i, j) = grayVal;
dstImg.at<uchar>(i, j) = 255 - srcImg.at<uchar>(i, j);
}
}
}
cv::namedWindow("result", CV_WINDOW_AUTOSIZE);
cv::imshow("result", dstImg);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}
代码2.利用指针访问像素.
int test2()
{
std::string filename = "../images/lena.jpg";
cv::Mat srcImg = cv::imread(filename, CV_8UC2);
cv::Mat dstImg = cv::Mat::zeros(srcImg.size(), srcImg.type());
//uchar grayVal = 0;
//uchar Blue = 0, Green = 0, Red = 0;
for (int i = 0; i < srcImg.rows; ++i)
{
uchar* curr_row = srcImg.ptr<uchar>(i);
uchar* dst_row = dstImg.ptr<uchar>(i);
for (int j = 0; j < srcImg.cols; ++j)
{
if (3 == srcImg.channels())
{
//Blue = 255 - *curr_row++;
//Green = 255 - *curr_row++;
//Red = 255 - *curr_row++;
//*dst_row++ = Blue;
//*dst_row++ = Green;
//*dst_row++ = Red;
*dst_row++ = 255 - *curr_row++;
*dst_row++ = 255 - *curr_row++;
*dst_row++ = 255 - *curr_row++;
}
else if (1 == srcImg.channels())
{
//grayVal = 255 - *curr_row++;
//*dst_row++ = grayVal;
*dst_row++ = 255 - *curr_row++;
}
}
}
cv::namedWindow("result", CV_WINDOW_AUTOSIZE);
cv::imshow("result", dstImg);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}
注:当以灰度图像读取时,则imread参数2选择CV_8UC1,若以RGB三通道图像读取时,则imread参数2选择CV_8UC2(注意此处并不是CV_8UC3,CV_8UC2表示RGB3通道图像,CV_8UC3表示的是RGB+透明色即4通道图像)
Mat的数据结构可以理解为Head部分+Data部分.
这里的Head部分用来存储Mat的属性参数,例如维度(dim)、尺寸大小(size)等.
Data部分用来存储Mat的图像矩阵,实际上这里只是保存了图像矩阵像素数据的指针.
对于图像像素矩阵来说, rows表示行数(图像的高度),cols表示列数(图像的宽度),如同代码中的srcImg.rows
和srcImg.cols
.
以类型为CV_8UC1为例,像素中存储的是uchar格式的数据,范围为0~255.
以类型为CV_8UC2为例 ,像素中存储的是Vec3b格式的数据,Vec3b由三个uchar数据组成,依次存储B、G、R三个通道的像素值.
若以内存的角度来看Mat的像素矩阵存储,对于CV_8UC2来说,则如上图所示,存储的顺序为
pix[0,0].B, pix[0,0].G, pix[0,0].R, pix[0,1].B, pix[0,1].G, pix[0,1].R ... pix[0, cols-1].B, pix[0, cols-1].G, pix[0, cols-1].R
pix[1,0].B, pix[1,0].G, pix[1,0].R, pix[1,1].B, pix[1,1].G, pix[1,1].R ... pix[1, cols-1].B, pix[1, cols-1].G, pix[1, cols-1].R
...
pix[rows-1,0].B, pix[rows-1,0].G, pix[rows-1,0].R, pix[rows-1,1].B, pix[rows-1,1].G, pix[rows-1,1].R ... pix[rows-1, cols-1].B, pix[rows-1, cols-1].G, pix[rows-1, cols-1].R
这个顺序非常重要,在后面如何访问像素时,我们将会再次提到它.
来看一下读取到的lena图像在Mat中的存储状态:
std::string filename = "../images/lena.jpg";
cv::Mat srcImg = cv::imread(filename, CV_8UC2);
cv::Mat dstImg = cv::Mat::zeros(srcImg.size(), srcImg.type());
cv::imread
此函数通过带路径的图像文件名,和图像的存储类型,来读取图像.
如前所述,若类型选为CV_8UC1,则读取的是灰度图像;
若类型选为CV_8UC2,则读取的是三通道图像(注意,源图像是RGB或者是BGR没有关系,读取进来以后是以BGR通道形式.
访问Mat中的像素,有两种方式,第一种方式是以数组的方式访问.
for (int i = 0; i < srcImg.rows; ++i)
{
uchar* curr_row = srcImg.ptr<uchar>(i);
uchar* dst_row = dstImg.ptr<uchar>(i);
for (int j = 0; j < srcImg.cols; ++j)
{
if (3 == srcImg.channels())
{
//Blue = 255 - srcImg.at(i, j)[0];
//Green = 255 - srcImg.at(i, j)[1];
//Red = 255 - srcImg.at(i, j)[2];
//dstImg.at(i, j) = cv::Vec3b(Blue, Green, Red);
dstImg.at<cv::Vec3b>(i, j)[0] = 255 - srcImg.at<cv::Vec3b>(i, j)[0];
dstImg.at<cv::Vec3b>(i, j)[1] = 255 - srcImg.at<cv::Vec3b>(i, j)[1];
dstImg.at<cv::Vec3b>(i, j)[2] = 255 - srcImg.at<cv::Vec3b>(i, j)[2];
}
else if (1 == srcImg.channels())
{
//grayVal = 255 - srcImg.at(i, j);
//dstImg.at(i, j) = grayVal;
dstImg.at<uchar>(i, j) = 255 - srcImg.at<uchar>(i, j);
}
}
}
若是灰度图像(1==srcImg.channels()),则像素为uchar类型,则srcImg.at
读取了第i行(i从0到rows-1),第j列(j从0到cols-1)的像素.
若是彩色图像(3==srcImg.channels()),则像素为Vec3b类型,则srcImg.at
读取了第i行(i从0到rows-1),第j列(j从0到cols-1)的像素. 在这个像素里,srcImg.at
为Blue通道的灰度值,srcImg.at
为Green通道的灰度值,srcImg.at
为Red通道的灰度值.
反色的处理即用255减去每个通道的灰度值,将结果对应赋值给dstImg的对应像素.
看一下程序运行过程中的dstImg像素值变化情况:
对第一行操作完成后,才开始操作第二行,依此类推:
第二种方式是以指针方式访问:
for (int i = 0; i < srcImg.rows; ++i)
{
uchar* curr_row = srcImg.ptr<uchar>(i);
uchar* dst_row = dstImg.ptr<uchar>(i);
for (int j = 0; j < srcImg.cols; ++j)
{
if (3 == srcImg.channels())
{
//Blue = 255 - *curr_row++;
//Green = 255 - *curr_row++;
//Red = 255 - *curr_row++;
//*dst_row++ = Blue;
//*dst_row++ = Green;
//*dst_row++ = Red;
*dst_row++ = 255 - *curr_row++;
*dst_row++ = 255 - *curr_row++;
*dst_row++ = 255 - *curr_row++;
}
else if (1 == srcImg.channels())
{
//grayVal = 255 - *curr_row++;
//*dst_row++ = grayVal;
*dst_row++ = 255 - *curr_row++;
}
}
}
srcImg.ptr
提供了访问像素中数据的指针方法,回忆起像素的存储顺序,则可知:
若为灰色图像,则srcImg.ptr
指向每一行的像素首地址,向下移动,直至移动到第j列,即到行末.
若为彩色图像,则srcImg.ptr
也同样指向每一行的像素首地址,顺序为BGRBGRBGR…,向下移动3个位置则到下一个像素,直至移动到第j列,即到行末.