【拜小白opencv】16-用四种方法访问图像中的每个像素

来源:互联网 发布:激战2人类女捏脸数据库 编辑:程序博客网 时间:2024/06/16 04:41

常言道“温故而知新”,写此文章就是对自己目前学习内容的小小的总结与记录。

本文力求用最简洁的语言,详细的代码将此部分内容讲解清楚,但由于博主同样是刚刚接触OpenCV,或许表达上有些瑕疵,还望读者能够指教探讨,大家共同进步。

博主机器配置为:VS2013+opencv2.4.13+Win-64bit。

若本文能给读者带来一点点启示与帮助,我就很开心了。

===========================分割线========================


颜色压缩(Color Reduction)最简单的理解就是减少表示图像的颜色数目,我们都知道,8位的3通道RGB真彩图像包括了1600多万(256×256×256=16777216)的颜色数目,其实在某些应用中用不到这么多数量,往往并不是将图像作为一个整体进行操作,而是对图像中的所有点或特殊点进行运算,所以遍历图像就显得很重要,如何高效的遍历图像是一个很值得探讨的问题。本节将介绍四种方法访问图像中的每个像素。

  1. 动态地址计算——配合.at<Vec3b>(i, j)[k]访问像素。
  2. 用指针来遍历图像——C操作符[]。
  3. 更高效的方法——isContinuous()函数。
  4. 迭代器——比较安全的方式。

=================================分割线===========================

图像矩阵如何存储在内存中

首先大致说明下图像数据如何在内存中存储。Mat是OpenCV2.x版本以上基本的图像类型,Mat可以视为一个矩阵,矩阵的大小依赖于该Mat是什么颜色空间(Color Space),比如最基本的灰度(Gray scale)或者RGB,CMYK,YCbCr等,因为这决定了该Mat具有多少个通道,一般来讲,灰度图像只有一个通道,而RGB图像具有三个通道。

对于灰度图像来讲,图像数据在内存中的存储如图所示:


对于多通道图像来讲,有几个通道,每一列就包含多少个子列。对于经常使用的基于RGB颜色空间,其图像数据存储如下:

需要注意的是通道的顺序是BGR而非RGB。

一般而言,图像数据的每一行在内存中都是连续存储的,因为这样对于遍历图像数据更高效。Mat提供了isContinuous()函数来获取是否是连续存储的数据。

下面就看看如何使用四种方法访问图像中的每个像素!

========================分割线=======================

方法一:动态地址计算——配合.at<Vec3b>(i, j)[k]访问像素

  1. Mat类提供了一个.at的方法用于取得图像上的点,它是一个模板函数,可以取到任何类型的图像上的点。
  2. 种方式不推荐用来遍历图像,一般用在要随机访问很少量的图像数据的时候,更适合随机访问的方式。
  3. 基本用法就是指定行列号,返回该位置的像素值。不过需要你事先知道返回的数据类型是uchar还是Vec3b或者其他的。

下面看看它是如何实现,此函数是自己定义的函数,使用时需要你在主函数中声明并调用使用。
//---------------【方法一】colorReduce()函数--------------------------//描述:使用【动态地址运算配合at】方法版本的颜色空间缩减函数//--------------------------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制创建与原图像相同的图像int rowNumber = outputImage.rows;//行数int colNumber = outputImage.cols;//列数//获取彩色图像像素for (int i = 0; i < rowNumber; i++){for (int j = 0; j < colNumber; j++){//---------------【开始处理每个像素】------------------outputImage.at<Vec3b>(i, j)[0] = outputImage.at<Vec3b>(i, j)[0] / div*div + div / 2;//蓝色通道outputImage.at<Vec3b>(i, j)[1] = outputImage.at<Vec3b>(i, j)[1] / div*div + div / 2;//绿色通道outputImage.at<Vec3b>(i, j)[2] = outputImage.at<Vec3b>(i, j)[2] / div*div + div / 2;//红色通道//---------------【处理结束】--------------------------}//行处理结束}}


通过上面的代码我们可以看出,at方法取图像中的点的2个用法:

  • outputImage.at<uchar>(i,j):取出灰度图像中i行j列的点。
  • outputImage.at<Vec3b>(i,j)[k]:取出彩色图像中i行j列第k通道的颜色点。

其中uchar,Vec3b都是图像像素值的类型,不要对Vec3b这种类型感觉害怕,其实在core里它是通过typedef Vec<T,N>来定义的,N代表元素的个数,T代表类型。
==============================分割线============================


方法二:用指针来遍历图像——C操作符[]

用指针访问像素的这种方式利用的是C语言中的操作符[],这种方式高效一点,但有点抽象,但这种方式也是比较推荐的遍历图像的方式。

下面看看它是如何实现,此函数是自己定义的函数,使用时需要你在主函数中声明并调用使用。
//-------------【方法二】colorReduce()函数--------------//高效一点:用指针来遍历图像//描述:使用【指针访问:C操作符[]】方法版的颜色空间缩减函数//-----------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制创建与原图像相同的图像int rowNumber = outputImage.rows;//行数int colNumbwer = outputImage.cols*outputImage.channels();//列数*通道数=每一行元素的个数。可以理解将3通道转换为1通道//双重循环,遍历所有的像素值for (int i = 0; i < rowNumber; i++)//行循环{uchar* data = outputImage.ptr<uchar>(i);//获取第i行的首地址for (int j = 0; j < colNumbwer; j++)//列循环{//------------【开始处理每个元素】--------data[j] = data[j] / div*div + div / 2;//第一种写法//*data++ = *data / div*div + div /2;//第二种写法//------------【处理结束】----------------}}}


从上面的代码看出,取出图像第i行的数据指针:outputImage.ptr<uchar>(i);
值得说明的是:程序中将三通道的数据转换为1通道,在建立在每一行数据元素之间在内存里是连续存储的,每个像素三通道像素按顺序存储。也就是一幅图像数据最开始的三个值,是最左上角的那像素的三个通道的值。

但是这种用法不能用在行与行之间,因为图像在opencv里的存储机制问题,行与行之间可能有空白单元。这些空白单元对图像来说是没有意思的,只是为了在某些架构上能够更有效率,比如intel MMX可以更有效的处理那种个数是4或8倍数的行。但是我们可以申明一个连续的空间来存储图像,这个话题引入下面最为高效的遍历图像的机制。

===================================分割线===========================

方法三:更高效的方法——isContinuous()函数

上面已经提到过了,一般来说图像行与行之间往往存储是不连续的,但是有些图像可以是连续的,Mat提供了一个检测图像是否连续的函数isContinuous()。当图像连通时,我们就可以把图像完全展开,看成是一行。

下面看看它是如何实现,此函数是自己定义的函数,使用时需要你在主函数中声明并调用使用。
//---------------【方法三】colorReduce()函数--------------------------//描述:使用【更高效的方法isContinuous()函数】方法版本的颜色空间缩减函数//--------------------------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制实参到临时变量int rowNumber = outputImage.rows;//行数int colNumber = outputImage.cols;//列数if (inputImage.isContinuous() && outputImage.isContinuous()){rowNumber = 1;colNumber = colNumber*inputImage.rows*inputImage.channels();}for (int i = 0; i < rowNumber; i++){const uchar* inData = inputImage.ptr<uchar>(i);uchar* outData = outputImage.ptr<uchar>(i);for (int j = 0; j < colNumber; j++){*outData++ = *inData++ / div*div + div / 2;}}}

这里获取一个指向每一行的指针,然后遍历这一行所有的数据。当图像数据是连续存储的时候,只需要取一次指针,然后就可以遍历整个图像数据。

===============================分割线=========================

方法四:迭代器——比较安全的方式

下面的方法可以让我们来为图像中的像素声明一个迭代器:
MatIterator_<Vec3b> it;
Mat_<Vec3b>::iterator it;

如果迭代器指向一个const图像,则可以用下面的声明:
MatConstIterator<Vec3b> it; 或者
Mat_<Vec3b>::const_iterator it;

下面看看它是如何实现,此函数是自己定义的函数,使用时需要你在主函数中声明并调用使用。
//-----------------------【方法四】colorReduce()函数-----------------------//描述:使用【迭代器】方法版本的颜色空间缩减函数//-------------------------------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制创建与原图像相同的图像//获取迭代器Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>();//初始位置的迭代器Mat_<Vec3b>::iterator itend = outputImage.end<Vec3b>();//终止位置的迭代器//存取彩色图像像素for (; it != itend; ++it){//------------【开始处理每个像素】----------------------------(*it)[0] = (*it)[0] / div*div + div / 2;(*it)[1] = (*it)[1] / div*div + div / 2;(*it)[2] = (*it)[2] / div*div + div / 2;//------------【处理结束】----------------------------}}

相较于高效的方式需要自己来计算需要遍历的数据量,以及当图像的行与行之间数据不连续的时候需要跳过一些间隙。迭代器(iterator)方式提供了一个更安全的访问图像像素的方式。你只需要做的就是声明两个MatIterator_变量,一个指向图像开始,一个指向图像结束,然后迭代。

================================分割线=============================

完整代码演示

/*功能:用四种方法访问图像中的每个像素*/#include <opencv2/core/core.hpp>      #include <opencv2/highgui/highgui.hpp>      #include <opencv2/imgproc/imgproc.hpp>     #include <iostream>    using namespace std;using namespace cv;//-----------------【全局函数声明部分】--------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div);int main(){//【1】创建原始图并显示Mat srcImage = imread("D:\\OutPutResult\\ImageTest\\scenery.jpg");if (srcImage.empty()){cout << "读取图像有误,请重新输入正确路径!\n";system("pause");return -1;}imshow("源图像", srcImage);//【2】按原始图的参数规格来创建输出图Mat dstImage;dstImage.create(srcImage.rows, srcImage.cols, srcImage.type());//【3】调用颜色空间缩减函数colorReduce()colorReduce(srcImage, dstImage, 128);//【4】显示效果图imshow("输出图", dstImage);waitKey(0);return 0;}//---------------【方法一】colorReduce()函数--------------------------//描述:使用【动态地址运算配合at】方法版本的颜色空间缩减函数//--------------------------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制创建与原图像相同的图像int rowNumber = outputImage.rows;//行数int colNumber = outputImage.cols;//列数//获取彩色图像像素for (int i = 0; i < rowNumber; i++){for (int j = 0; j < colNumber; j++){//---------------【开始处理每个像素】  单通道是uchar,没有[0][1][2]----------------outputImage.at<Vec3b>(i, j)[0] = outputImage.at<Vec3b>(i, j)[0] / div*div + div / 2;//蓝色通道outputImage.at<Vec3b>(i, j)[1] = outputImage.at<Vec3b>(i, j)[1] / div*div + div / 2;//绿色通道outputImage.at<Vec3b>(i, j)[2] = outputImage.at<Vec3b>(i, j)[2] / div*div + div / 2;//红色通道//---------------【处理结束】--------------------------}//行处理结束}}/*//-------------【方法二】colorReduce()函数--------------//高效一点:用指针来遍历图像//描述:使用【指针访问:C操作符[]】方法版的颜色空间缩减函数//-----------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制创建与原图像相同的图像int rowNumber = outputImage.rows;//行数int colNumbwer = outputImage.cols*outputImage.channels();//列数*通道数=每一行元素的个数。可以理解将3通道转换为1通道//双重循环,遍历所有的像素值for (int i = 0; i < rowNumber; i++)//行循环{uchar* data = outputImage.ptr<uchar>(i);//获取第i行的首地址for (int j = 0; j < colNumbwer; j++)//列循环{//------------【开始处理每个元素】--------data[j] = data[j] / div*div + div / 2;//第一种写法//*data++ = *data / div*div + div /2;//第二种写法//------------【处理结束】----------------}}}//---------------【方法三】colorReduce()函数--------------------------//描述:使用【更高效的方法isContinuous()函数】方法版本的颜色空间缩减函数//--------------------------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制实参到临时变量int rowNumber = outputImage.rows;//行数int colNumber = outputImage.cols;//列数if (inputImage.isContinuous() && outputImage.isContinuous()){rowNumber = 1;colNumber = colNumber*inputImage.rows*inputImage.channels();}for (int i = 0; i < rowNumber; i++){const uchar* inData = inputImage.ptr<uchar>(i);uchar* outData = outputImage.ptr<uchar>(i);for (int j = 0; j < colNumber; j++){*outData++ = *inData++ / div*div + div / 2;}}}//-----------------------【方法四】colorReduce()函数-----------------------//描述:使用【迭代器】方法版本的颜色空间缩减函数//-------------------------------------------------------------------------void colorReduce(Mat& inputImage, Mat& outputImage, int div){//参数准备outputImage = inputImage.clone();//复制创建与原图像相同的图像//获取迭代器Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>();//初始位置的迭代器Mat_<Vec3b>::iterator itend = outputImage.end<Vec3b>();//终止位置的迭代器//存取彩色图像像素for (; it != itend; ++it){//------------【开始处理每个像素】----------------------------(*it)[0] = (*it)[0] / div*div + div / 2;(*it)[1] = (*it)[1] / div*div + div / 2;(*it)[2] = (*it)[2] / div*div + div / 2;//------------【处理结束】----------------------------}}*/

============================分割线=========================

显示结果



============================分割线=====================

程序说明

通过四种方式访问图像中的像素,我们可以通过  【拜小白opencv】4-测量程序运行时间;getTickCount()与getTickFrequency() 中的方法,在主函数中添加测量程序时间方法,这样就可以比较四种访问方式的时间快慢。

=========================END===================