OpenCV图像遍历的三种方式

一、三种图像遍历方式与理解

  在上一篇Blog中,介绍了使用基础图像容器Mat创建图像矩阵的六种方式
,当我们使用创建的Mat矩阵进行图像处理时,需要对Mat矩阵中像素点进行遍历操作,图像像素点的遍历是任何图像处理算法必不可少的执行步骤。在OpenCV中提供了3种图像遍历的方法:

  • .at()函数访问
  • .ptr函数获取行指针逐行访问
  • iterator迭代器访问

这三种方式在访问速度上有所差异,其中.at方法遍历速度最慢,但是代码可读性好;ptr指针遍历更快,但是指针操作没有类型检查和访问越界检查,即使程序在编译过程中没问题在执行过程中可能报错;iterator迭代器遍历方式最快而且安全不会出现访问越界情况,但是源码编辑稍显复杂。在编程过程中应该根据实践需要采取不同的遍历方式,以达到最高效的访问、读写和编辑。

1、.at方法遍历
Mat类提供了一个at的方法用于取得图像上的点,它是一个模板函数,通过image.at<Typename>(i,j)可以取到任何类型的图像上的点,其中typename标识像素类型,i和j分别表示图像image的行和列,读取Mat矩阵第i行j列像素的表示方法为:

uchar pix_value = srcImage.at<uchar>(i,j); //读取srcImage第i行j列元素

at方法遍历图像所有像素的一般格式为(srcImage表示原图):

//单通道灰度图
for(int i=0;i<srcImage.rows;i++){ 
	for(int j=0;j<srcImage.cols;j++){
		srcImage.at<uchar>(i,j)=...
		...
	}
}

//3通道RGB彩色图
for(int i=0;i<srcImage.rows;i++){ 
	for(int j=0;j<srcImage.cols;j++){
		srcImage.at<Vec3b>(i,j)[2]=...  //R通道
		srcImage.at<Vec3b>(i,j)[1]=...  //G通道
		srcImage.at<Vec3b>(i,j)[0]=...  //B通道
		...
	}
}

【注】at方法只适合访问灰度值为8位的图像,即单通道图像,如灰度图、RGB分离单通道图等。

2、指针访问
指针访问图像像素的方式是利用C语言中的 “[]” 操作符,此时图像从上到下、从左到右可以看成是一个“一维数组”(不一定连续存储),通过Mat类成员变量cols和成员函数channels()获取图像宽和通道数,那么Mat矩阵第i行第j列元素可表示为: i ∗ s r c I m a g e . c o l s ∗ s r c I m a g e . c h a n n e l s ( ) + j i*srcImage.cols*srcImage.channels()+j isrcImage.colssrcImage.channels()+j为了简化指针运算,Mat类提供了ptr函数可以得到图像任意行首地址,它返回第i行的首地址:

uchar* data = dstImage.ptr<uchar>(i);   //data为第i行像素首地址

那么第i行第j列像素的标识方式简化为:data[i]+j

ptr函数配合指针遍历图像所有像素的一般格式为(srcImage表示原图):

//单通道灰度图
for(int i=0;i<srcImage.rows;i++){
	uchar* p=srcImage.ptr<uchar>(i);
	for(int j=0;j<srcImage.cols;j++){
		p[j]=...
		...
	}
}

//3通道RGB彩色图
for(int i=0;i<srcImage.rows;i++){
	Vec3b* p=srcImage.ptr<Vec3b>(i);
	for(int j=0;j<srcImage.cols;j++){
		p[j][2]=...  //R通道
		p[j][1]=...  //G通道
		p[j][0]=...  //B通道
		...
	}
}

3、iterator迭代器遍历
这种方式类似STL,在访问图像矩阵时,仅仅需要获得图像矩阵的beginend元素,从begin遍历到end过程中,通过   ~  *   ~  操作符获取指针所指数据,即可访问Mat图像内容。
iterator迭代器遍历图像所有像素的一般格式为(srcImage表示原图):

//单通道灰度图
MatIterator_<uchar> it_beg=srcImage.begin<uchar>();
MatIterator_<uchar> it_end=srcImage.end<uchar>();
for( , it_beg != it_end; it_beg++){
	*it_beg=...
	...
}

//3通道RGB彩色图
MatIterator_<Vec3b> it_beg=srcImage.begin<uchar>();
MatIterator_<Vec3b> it_end=srcImage.end<uchar>();
for( , it_beg != it_end; it_beg++){
	(*it_beg)[2]=...  //R通道
	(*it_beg)[1]=...  //G通道
	(*it_beg)[0]=...  //B通道
	...
}

关于uchar与Vec3b类型一些问题:
涉及到OpenCV中对图像像素的存储方式,一般情况下,单通道图像像素类型为uchar(unsigned char表示0-255区间的无符号整形),Vec是OpenCV一个模板类,表示一个向量,可表示多通道的图像像素类型,OpenCV预定义的一些类型有:

typedef Vec<uchar, n> Vecnb;

typedef Vec<short, n> Vecns;

typedef Vec<int, n> Vecni;

typedef Vec<float, n> Vecnf;

typedef Vec<double, n> Vecnd;

【注】n表示维度,可取2、3、4,分别表示2维向量、三维向量、四维向量,Vec3b就表示3维向量,每一个维度类型都是uchar型。

二、C++实现三种方式对比

下面通过一个demo程序,也是最通用的像素遍历示例程序来演示以上三种方法在遍历图像矩阵中的开销和优缺点
Test: 颜色空间缩减

加载一幅RGB彩色图像,使其颜色种类从256中变成64种。在颜色空间缩减方法中讲过这种方式,即每个像素值除以4向下取整然后再乘以4即可将其颜色种类缩减到64种。

首先,为直观对比三种方式时间开销的差异,需要用到OpenCV提供的计时函数

  • getTickCount():返回CPU自某时间开始的时钟周期数
  • getTickFrequency():获取CPU时钟频率,即CPU每秒走的时钟周期数

这两个函数配合使用即可计算出程序耗时,类似于C++中的clock()函数,具体用法如下:

double time0 = static_cast<double>(getTickCount());  //记录起始时间
//...
//一系列操作
//...
time0 = ((double)getTickCount()-time0)/getTickFrequency();
std::cout<<"..操作 运行时间为:"<<time0<<"s"<<std::endl; //输出运行时间

Code:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;

void colorReduceAt(Mat& srcImage, Mat& dstImageAt, int div);
void colorReduceIterator(Mat& srcImage, Mat& dstImageIterator, int div);
void colorReducePtr(Mat& srcImage, Mat& dstImagePtr, int div);

int main()
{
    //加载图像
    Mat srcImage = imread("");

    if(srcImage.empty())
    {
        cout << "Load failture...." << endl << endl;
        exit(-1);
    }

    //声明处理后图像变量
    Mat dstImageAt, dstImageIterator, dstImagePtr;
    dstImageAt = srcImage.clone();
    dstImageIterator = srcImage.clone();
    dstImagePtr = srcImage.clone();

    int div = 4;

    //声明时间变量
    double timeAt, timeIterator, timePtr;

    timeAt = static_cast<double>(getTickCount());
    colorReduceAt(srcImage, dstImageAt, div);
    timeAt = ((double)getTickCount() - timeAt) / getTickFrequency();
    imshow("dstImageAt",dstImageAt);
    cout << "使用at()动态地址计算耗时:" << timeAt << endl << endl;

    timeIterator = static_cast<double>(getTickCount());
    colorReduceIterator(srcImage, dstImageIterator, div);
    timeIterator = ((double)getTickCount() - timeIterator) / getTickFrequency();
    imshow("dstImageIterator",dstImageIterator);
    cout << "使用iterator迭代器耗时:" << timeIterator << endl << endl;

    timePtr = static_cast<double>(getTickCount());
    colorReducePtr(srcImage, dstImagePtr, div);
    timePtr = ((double)getTickCount() - timePtr) / getTickFrequency();
    imshow("dstImagePtr",dstImagePtr);
    cout << "使用ptr指针耗时:" << timePtr << endl;


    waitKey(0);

    return 0;
}

//使用at动态地址计算方式
void colorReduceAt(Mat& srcImage, Mat& dstImageAt, int div)
{
    int rowNumber = dstImageAt.rows;      //获取图像行数
    int colNumber = dstImageAt.cols;      //获取图像列数

    //对每个像素进行处理
    for(int i = 0; i < rowNumber; i++)
    {
        for(int j = 0; j < colNumber; j++)
        {
            dstImageAt.at<Vec3b>(i,j)[0] = dstImageAt.at<Vec3b>(i,j)[0]/div*div;    //Blue
            dstImageAt.at<Vec3b>(i,j)[1] = dstImageAt.at<Vec3b>(i,j)[1]/div*div;    //Green
            dstImageAt.at<Vec3b>(i,j)[2] = dstImageAt.at<Vec3b>(i,j)[2]/div*div;    //Red
        }
    }

}

//使用iterator迭代器方式
void colorReduceIterator(Mat& srcImage, Mat& dstImageIterator, int div)
{
    MatIterator_<Vec3b> imageIt = dstImageIterator.begin<Vec3b>();      //获取迭代器初始位置
    MatIterator_<Vec3b> imageEnd = dstImageIterator.end<Vec3b>();       //获取迭代器结束位置

    //对每个像素进行处理
    for(;imageIt != imageEnd; imageIt++)
    {
        (*imageIt)[0] = (*imageIt)[0]/div*div;      //Blue
        (*imageIt)[1] = (*imageIt)[1]/div*div;      //Green
        (*imageIt)[2] = (*imageIt)[2]/div*div;      //Red
    }
}

//使用ptr指针
void colorReducePtr(Mat& srcImage, Mat& dstImagePtr, int div)
{
    int rowNumber = dstImagePtr.rows;                           //获取图像矩阵行数
    int colNumber = dstImagePtr.cols*dstImagePtr.channels();    //三通道图像每行元素个数为列数x通道数

    for(int i = 0; i < rowNumber; i++)
    {
        uchar* pixelPtr = dstImagePtr.ptr<uchar>(i);            //获取矩阵每行首地址指针
        for(int j = 0; j < colNumber; j++)
            pixelPtr[j] = pixelPtr[j] / div * div;
    }
}

续更…
参考博客:OpenCV成长之路(2):图像的遍历

  • 6
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
OpenCV 中,Mat 是一个基本的图像容器,用于存储和处理图像。下面是一些常用的 Mat 操作: 1. 创建 Mat 对象:可以通过构造函数或者 create() 函数创建 Mat 对象,例如: ```cpp // 创建一个 3 ,2 列的 CV_8UC1 类型的图像 cv::Mat image1(3, 2, CV_8UC1); // 创建一个 3 ,2 列的 CV_8UC3 类型的图像 cv::Mat image2(3, 2, CV_8UC3); // 创建一个空的图像 cv::Mat emptyImage; // 创建一个和另一个 Mat 对象相同大小和类型的图像 cv::Mat image3 = image1.clone(); ``` 2. 访问像素值:可以使用 at() 函数或者指针访问像素值,例如: ```cpp cv::Mat image = cv::imread("image.jpg"); // 使用 at() 函数访问像素值 cv::Vec3b pixel1 = image.at<cv::Vec3b>(0, 0); // 使用指针访问像素值 cv::Vec3b pixel2 = image.ptr<cv::Vec3b>(0)[0]; ``` 3. 图像拷贝:可以使用 copyTo() 函数将一个 Mat 对象拷贝到另一个 Mat 对象,例如: ```cpp cv::Mat image1 = cv::imread("image1.jpg"); cv::Mat image2; // 将 image1 拷贝到 image2 image1.copyTo(image2); ``` 4. 图像缩放:可以使用 resize() 函数对图像缩放,例如: ```cpp cv::Mat image = cv::imread("image.jpg"); cv::Mat resizedImage; // 将图像缩小一半 cv::resize(image, resizedImage, cv::Size(), 0.5, 0.5); ``` 5. 图像裁剪:可以使用 ROI(Region of Interest)对图像裁剪,例如: ```cpp cv::Mat image = cv::imread("image.jpg"); // 创建一个矩形,表示要裁剪的区域 cv::Rect roi(100, 100, 200, 200); // 裁剪图像 cv::Mat croppedImage = image(roi); ``` 6. 图像通道分离和合并:可以使用 split() 函数将图像通道分离,使用 merge() 函数将图像通道合并,例如: ```cpp cv::Mat image = cv::imread("image.jpg"); // 分离图像通道 std::vector<cv::Mat> channels; cv::split(image, channels); // 合并图像通道 cv::Mat mergedImage; cv::merge(channels, mergedImage); ``` 7. 图像类型转换:可以使用 cvtColor() 函数将图像类型进转换,例如: ```cpp cv::Mat image = cv::imread("image.jpg"); // 将图像转换为灰度图像 cv::Mat grayImage; cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY); ``` 以上是 Mat 操作中的一些常用方法,还有很多其他的方法可以参考 OpenCV 官方文档。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值