目录
1. 使用场景
使用OpenCV,避免不了的就是对图像像素进行操作,遍历操作更是家常便饭,当图像数据不多时,各类方法的速度差不太多;但面对数据计算次数非常多,尤其在进行迭代、拟合、递归等数据反复计算的工作时,此时不得不面对的就是程序性能、算法提速和优化的问题了。
在OpenCV中有三种最常用的像素操作方法,分别是动态地址操作法、迭代器操作法和指针操作法。下面简单介绍下该三种方法的优劣和使用场景:
- 动态地址操作法。最常用,比如直接A.at,操作Mat矩阵A第i行第j列的像素值,简洁明了,适合对某个或某几个值直接操作时使用,定位非常方便,也可以遍历计算使用,但是速度方面比其他两个方法都要慢;
- 迭代器操作法。运用了C++中STL的理念,通过迭代器的方式,获取矩信息的头尾(begin和end),然后依次操作,该方法适合连续计算时使用,速度和动态地址操作法差不多,比指针法慢,但是胜在安全性好;
- 指针法。我最喜欢的遍历法,一遇到遍历操作必用ptr,该方法缺点就是指针容易越界,编代码时要慎重,确保安全和稳定,但是速度没得说。
三种方法一般情况在debug下运行差异性很明显,release下没那么明显;但是一旦遍历的函数复杂性加大了,release下的差异性就会体现出来了,指针法绝对是最快的,我在做图像迭代拟合计算时,用指针法替代动态地址法,速度至少提高5-10倍,一点不夸张。。。
2. 示例代码
#include<iostream>
#include<opencv2/opencv.hpp>
#include<ctime>
using namespace std;
using namespace cv;
int main(void)
{
Mat A = Mat::zeros(10000, 10000, CV_32FC1);
// 随意创建一个A矩阵
for (int i = 0; i < A.rows; i++)
{
for (int j = 0; j < A.cols; j++)
{
A.at<float>(i, j) = rand()%100/100.f;
}
}
Mat B0,B1,B2;
B0 = A.clone();
B1 = A.clone();
B2 = A.clone();
// 动态地址操作法
double time0 = static_cast<double>(getTickCount());
for (int i = 0; i < A.rows; i++)
{
for (int j = 0; j < A.cols; j++)
{
B0.at<float>(i, j) *= 2;
}
}
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << " 动态地址法运行时间为:" << time0 << "秒" << endl << endl;
// 迭代器操作法
double time1 = static_cast<double>(getTickCount());
Mat_<float>::iterator it = B1.begin<float>();
Mat_<float>::iterator itend = B1.end<float>();
for (; it!=itend; ++it)
{
(*it)*= 2;
}
time1 = ((double)getTickCount() - time1) / getTickFrequency();
cout << " 迭代器法运行时间为:" << time1 << "秒" << endl << endl;
// 指针操作法
double time2 = static_cast<double>(getTickCount());
for (int i = 0; i < A.rows; i++)
{
float *data = B2.ptr<float>(i);
for (int j = 0; j < A.cols; j++)
{
data[j]*= 2;
}
}
time2 = ((double)getTickCount() - time2) / getTickFrequency();
cout << " 指针法运行时间为:" << time2<< "秒" << endl << endl;
system("pause");
return 0;
}
3. 示例代码解析
使用OpenCV库来处理矩阵操作,并通过三种不同的方式对矩阵元素进行操作,比较它们的执行时间。
3.1 头文件和命名空间
#include<iostream>
#include<opencv2/opencv.hpp>
#include<ctime>
using namespace std;
using namespace cv;
#include<iostream>
:引入标准输入输出流库,用于控制台输出。#include<opencv2/opencv.hpp>
:引入OpenCV库的头文件,提供矩阵操作的功能。#include<ctime>
:提供时间相关函数库,这里其实没有用到(应该是冗余的,因为计时使用了OpenCV自带的计时函数)。using namespace std;
和using namespace cv;
:为了方便使用标准库和OpenCV库中的符号。
3.2 主函数 main
3.2.1 创建并初始化矩阵A
Mat A = Mat::zeros(10000, 10000, CV_32FC1);
- 创建一个10000×10000的矩阵
A
,数据类型为单通道32位浮点数(CV_32FC1
),矩阵所有元素初始值为0。
3.2.2 随机初始化矩阵A的值
for (int i = 0; i < A.rows; i++) {
for (int j = 0; j < A.cols; j++) {
A.at<float>(i, j) = rand() % 100 / 100.f;
}
}
- 使用两重循环给矩阵
A
的每个元素赋值,值为随机生成的0到1之间的浮点数。 rand() % 100
生成0到99的整数。/ 100.f
将其转为浮点数并缩放至[0, 1)的范围。
3.2.3 克隆矩阵
Mat B0, B1, B2;
B0 = A.clone();
B1 = A.clone();
B2 = A.clone();
- 创建三个矩阵
B0
,B1
,B2
,并分别通过A.clone()
初始化为矩阵A
的副本。使用clone()
确保B0
,B1
,B2
与A
是独立的对象,不会共享内存。
3.2.4 使用动态地址操作法进行矩阵操作并计时
double time0 = static_cast<double>(getTickCount());
for (int i = 0; i < A.rows; i++) {
for (int j = 0; j < A.cols; j++) {
B0.at<float>(i, j) *= 2;
}
}
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << "动态地址法运行时间为:" << time0 << "秒" << endl << endl;
动态地址法:每次访问矩阵元素时,at()
函数内部会进行边界检查,这就是所谓的动态地址法。
- getTickCount() 用于获取系统时钟周期数。返回值是一个
int
类型。 - static_cast 是 C++ 中的一种类型转换运算符,用于在不同的数据类型之间进行安全转换。将
getTickCount()
返回的时钟周期数(int
类型)转换为double
类型。 B0.at<float>(i, j)
获取矩阵元素的值并对其每个元素的值乘以2。- 通过
getTickCount()
的差值除以getTickFrequency()
(表示每秒的时钟周期数),计算代码段的运行时间。
3.2.5 使用迭代器操作法进行矩阵操作并计时
double time1 = static_cast<double>(getTickCount());
Mat_<float>::iterator it = B1.begin<float>();
Mat_<float>::iterator itend = B1.end<float>();
for (; it != itend; ++it) {
(*it) *= 2;
}
time1 = ((double)getTickCount() - time1) / getTickFrequency();
cout << "迭代器法运行时间为:" << time1 << "秒" << endl << endl;
迭代器操作法:迭代器在遍历时不会进行边界检查(与 at()
方法不同),因此相比动态地址法,性能会高一些,但不如指针操作法(直接访问矩阵的内存)的性能高。
Mat_<float>::iterator
:- OpenCV 提供了
Mat_
模板类用于方便操作矩阵的元素,Mat_<float>
是Mat_
的一个具体实例,它表示一个浮点类型(float
)的矩阵。 iterator
是迭代器类型,用于遍历矩阵中的元素。
- OpenCV 提供了
B1.begin<float>()
和B1.end<float>()
获取矩阵B1
的起始(矩阵首元素)和结束迭代器(最后一个元素的后一个位置),迭代器类型为Mat_<float>::iterator
。*it
解引用迭代器,获取当前迭代器指向的矩阵元素。通过(*it) *= 2
,将当前元素的值乘以 2。- 计算代码段的运行时间。
3.2.6 使用指针操作法进行矩阵操作并计时
double time2 = static_cast<double>(getTickCount());
for (int i = 0; i < A.rows; i++) {
float* data = B2.ptr<float>(i);
for (int j = 0; j < A.cols; j++) {
data[j] *= 2;
}
}
time2 = ((double)getTickCount() - time2) / getTickFrequency();
cout << "指针法运行时间为:" << time2 << "秒" << endl << endl;
指针法:执行效率通常是最高的,因为它直接操作内存,没有边界检查或迭代器进行遍历时的开销。
- 第一层循环:遍历行
float* data = B2.ptr<float>(i);
:获取指向第i
行的指针,然后可以用该指针直接访问该行的所有元素。B2.ptr<float>(i)
:获取矩阵B2
的第i
行的首地址(即该行第一个元素的指针)。ptr<float>(i)
是 OpenCV 中Mat
类的一个成员函数,返回一个指向矩阵第i
行的第一个元素的指针,类型为float*
(假设矩阵为浮点型CV_32FC1
)。- 该指针
data
可以直接用于访问第i
行的所有元素。
- 第二层循环:遍历列
j
用于遍历矩阵第i
行的所有列。data[j]
:使用指针偏移量直接访问矩阵第i
行第j
列的元素(data[j]
相当于B2.at<float>(i, j)
)。- 因为
data
是指向B2
第i
行的指针,因此data[j]
就是矩阵B2
第i
行第j
列的元素。 - 该行代码将矩阵
B2
的第i
行第j
列的元素乘以2。
- 因为
- 计算代码段的运行时间。
3.2.7 系统暂停
system("pause");
- 等待用户输入以暂停程序,防止程序结束后窗口自动关闭(在Windows系统上)。
4. 总结
- 动态地址操作法:最慢的。因为每次访问矩阵元素时,
at()
函数内部会进行边界检查。 - 迭代器操作法:稍快。因为迭代器在遍历时不会进行边界检查(与
at()
方法不同),相比动态地址法,性能会高一些。 - 指针操作法:最快,因为执行效率通常是最高的,因为它直接操作内存,没有边界检查或迭代器进行遍历时的开销,但缺点就是指针容易越界。
程序的设计目的是对比三种方法的运行效率,并展示如何在OpenCV中操作大规模矩阵数据。
来自:https://zhaitianbao.blog.csdn.net/article/details/116268352
标签:遍历,Mat,迭代,操作法,矩阵,像素,OpenCV,getTickCount,B2 From: https://www.cnblogs.com/keye/p/18394161