OpenCV C++ 学习笔记(三):矩阵基本操作、遍历图像矩阵的方法及性能分析

article/2025/8/12 2:25:02

文章目录

  • 图像矩阵在内存中的存储
  • 矩阵基本操作
  • 高性能法——使用经典的C风格运算符[](指针)
  • 迭代器法
  • 通过指定On-the-fly地址查找
  • 核心函数LUT
  • 性能分析


常用数据类型定义:

cv::Size(cols, rows);
cv::Size(width, height);cv::Scalar(gray)
cv::Scalar(blue, green, red)typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;

图像矩阵在内存中的存储

图像矩阵的大小取决于我们所用的颜色模型,确切地说,取决于所用通道数。

灰度图像矩阵:
在这里插入图片描述

RGB颜色模型的矩阵:
在这里插入图片描述

OpenCV中子列的通道顺序是反过来的:BGR而不是RGB。

很多情况下,因为内存足够大,可实现连续存储,因此,图像中的各行就能一行一行地连接起来,形成一个长行。连续存储有助于提升图像扫描(遍历)速度,我们可以使用 isContinuous() 来去判断矩阵是否是连续存储的。

矩阵基本操作

全零矩阵

CV_NODISCARD_STD static MatExpr Mat::zeros(int rows, int cols, int type);
CV_NODISCARD_STD static MatExpr Mat::zeros(Size size, int type);CV_NODISCARD_STD static MatExpr Mat::zeros(int ndims, const int* sz, int type);
//not recommended

全一矩阵

CV_NODISCARD_STD static MatExpr Mat::ones(int rows, int cols, int type);
CV_NODISCARD_STD static MatExpr Mat::ones(Size size, int type);CV_NODISCARD_STD static MatExpr Mat::ones(int ndims, const int* sz, int type);
//not recommended

单位矩阵

CV_NODISCARD_STD static MatExpr Mat::eye(int rows, int cols, int type);
CV_NODISCARD_STD static MatExpr Mat::eye(Size size, int type);

矩阵转置

MatExpr Mat::t() const;

求逆矩阵

MatExpr Mat::inv(int method=DECOMP_LU) const;

逗号式分隔创建矩阵
常用于自定义卷积核

template<typename _Tp> inline
Mat_<_Tp>::Mat_(int _rows, int _cols): Mat(_rows, _cols, traits::Type<_Tp>::value)
{
}template<typename _Tp> inline
Mat_<_Tp>::Mat_(int _rows, int _cols, const _Tp& value): Mat(_rows, _cols, traits::Type<_Tp>::value)
{*this = value;
}template<typename _Tp> inline
Mat_<_Tp>::Mat_(Size _sz): Mat(_sz.height, _sz.width, traits::Type<_Tp>::value)
{}template<typename _Tp> inline
Mat_<_Tp>::Mat_(Size _sz, const _Tp& value): Mat(_sz.height, _sz.width, traits::Type<_Tp>::value)
{*this = value;
}
Mat a=(Mat_<int>(2,2)<<1,2,3,4);
Mat b=(Mat_<double>(Size(2,2))<<1,2,3,4);

注意括号的位置。
给出的数据类型必须是基本数据类型,如intdouble。不能是CV_32F等。


通过ptr与at函数遍历矩阵

Mat a(Size(2560, 1440), CV_8UC3);
for(int i=0; i<a.rows; i++){for(int j=0; j<a.cols; j++){a.ptr(i,j)[0]=0;a.ptr(i,j)[1]=0;a.ptr(i,j)[2]=255;}
}
for(int i=0; i<a.rows; i++){for(int j=0; j<a.cols; j++){a.ptr<Vec3b>(i,j)[0]=0;a.ptr<Vec3b>(i,j)[1]=0;a.ptr<Vec3b>(i,j)[2]=255;}
}
for(int i=0; i<a.rows; i++){for(int j=0; j<a.cols; j++){a.at<Vec3b>(i,j)[0]=0;a.at<Vec3b>(i,j)[1]=0;a.at<Vec3b>(i,j)[2]=255;}
}
  • ptr访问可以不加Vec类型
  • at访问必须加Vec类型,at访问比ptr略微慢一些

通过迭代器遍历矩阵(easy but very very slow)

Mat a(Size(2560, 1440), CV_8UC3);
for(auto iter=a.begin<Vec3b>(); iter!=a.end<Vec3b>(); iter++){iter[0]=255;iter[1]=0;iter[2]=0;
}

高性能法——使用经典的C风格运算符[](指针)

Efficient Way
说到性能,推荐的效率最高的查找表赋值方法,还是经典的C风格运算符[](指针)访问:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{// accept only char type matricesCV_Assert(I.depth() != sizeof(uchar));     int channels = I.channels();int nRows = I.rows * channels; int nCols = I.cols;if (I.isContinuous()){nCols *= nRows;nRows = 1;         }int i,j;uchar* p; for( i = 0; i < nRows; ++i){p = I.ptr<uchar>(i);for ( j = 0; j < nCols; ++j){p[j] = table[p[j]];             }}return I; 
}

我们获取了每一行开始处的指针,然后遍历至该行末尾。如果矩阵是以连续方式存储的,我们只需请求一次指针、然后一路遍历下去就行。彩色图像的情况有必要加以注意:因为三个通道的原因,我们需要遍历的元素数目也是3倍。

另外一种方法实现遍历功能,就是使用 data
data会从 Mat 中返回指向矩阵第一行第一列的指针。

注意如果该指针为NULL则表明对象里面无输入,所以这是一种简单的检查图像是否被成功读入的方法。

当矩阵是连续存储时,我们就可以通过遍历 data 来扫描整个图像。例如,一个灰度图像,其操作如下:

uchar* p = I.data;for( unsigned int i =0; i < ncol*nrows; ++i)*p++ = table[*p];

这回得出和前面相同的结果。但是这种方法编写的代码可读性方面差,并且进一步操作困难。同时,在实际应用中,该方法的性能表现上并不明显优于前一种(因为现在大多数编译器都会对这类操作做出优化)。

迭代器法

The iterator (safe) method
相比于指针遍历,迭代法则被认为是一种以更安全的方式来实现这一功能。在迭代法中,所需要做的仅仅是获得图像矩阵的beginend,然后增加迭代直至从beginend。将*操作符添加在迭代指针前,即可访问当前指向的内容。

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{// accept only char type matricesCV_Assert(I.depth() != sizeof(uchar));     const int channels = I.channels();switch(channels){case 1: {MatIterator_<uchar> it, end; for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)*it = table[*it];break;}case 3: {MatIterator_<Vec3b> it, end; for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it){(*it)[0] = table[(*it)[0]];(*it)[1] = table[(*it)[1]];(*it)[2] = table[(*it)[2]];}}}return I; 
}

对于彩色图像中的一行,每列中有3个uchar元素,这可以被认为是一个小的包含uchar元素的vector,在OpenCV中用 Vec3b 来命名。

如果要访问第n个子列,我们只需要简单的利用[]来操作就可以。需要指出的是,OpenCV的迭代在扫描过一行中所有列后会自动跳至下一行,所以说如果在彩色图像中如果只使用一个简单的 uchar 而不是 Vec3b 迭代的话就只能获得蓝色通道(B)里的值。

通过指定On-the-fly地址查找

通过双层for循环直接指定行列来遍历。
事实上这个方法并不推荐被用来进行图像扫描,它本来是被用于获取或更改图像中的随机元素。它的基本用途是要确定你试图访问的元素的所在行数与列数。在前面的扫描方法中,我们观察到知道所查询的图像数据类型是很重要的。这里同样的你得手动指定好你要查找的数据类型。

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{// accept only char type matricesCV_Assert(I.depth() != sizeof(uchar));     const int channels = I.channels();switch(channels){case 1: {for( int i = 0; i < I.rows; ++i)for( int j = 0; j < I.cols; ++j )I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];break;}case 3: {Mat_<Vec3b> _I = I;for( int i = 0; i < I.rows; ++i)for( int j = 0; j < I.cols; ++j ){_I(i,j)[0] = table[_I(i,j)[0]];_I(i,j)[1] = table[_I(i,j)[1]];_I(i,j)[2] = table[_I(i,j)[2]];}I = _I;break;}}return I;
}

debug 模式下,它会检查你的输入坐标是否有效或者超出范围。如果坐标有误,则会输出一个标准的错误信息。
release模式下,和 高性能法(the efficient way) 相比,它们之间的区别仅仅是 On-the-fly方法 对于图像矩阵的每个元素,都会获取一个新的行指针,通过该指针和[]操作来获取列元素。

核心函数LUT

The Core Function
最被推荐的用于实现批量图像元素查找和更该操作图像方法。

在图像处理中,对于一个给定的值,将其替换成其他的值是一个很常见的操作,OpenCV 提供里一个函数直接实现该操作,并不需要你自己扫描图像,就是:operationsOnArrays:LUT() <lut> ,一个包含于core module的函数。

/** @brief Performs a look-up table transform of an array.The function LUT fills the output array with values from the look-up table. Indices of the entries
are taken from the input array. That is, the function processes each element of src as follows:
\f[\texttt{dst} (I)  \leftarrow \texttt{lut(src(I) + d)}\f]
where
\f[d =  \fork{0}{if \(\texttt{src}\) has depth \(\texttt{CV_8U}\)}{128}{if \(\texttt{src}\) has depth \(\texttt{CV_8S}\)}\f]
@param src input array of 8-bit elements.
@param lut look-up table of 256 elements; in case of multi-channel input array, the table should
either have a single channel (in this case the same table is used for all channels) or the same
number of channels as in the input array.
@param dst output array of the same size and number of channels as src, and the same depth as lut.
@sa  convertScaleAbs, Mat::convertTo
*/
CV_EXPORTS_W void LUT(InputArray src, InputArray lut, OutputArray dst);

首先我们建立一个mat型用于查表:

Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data; 
for( int i = 0; i < 256; ++i)p[i] = table[i];

然后我们调用函数 (I 是输入, J 是输出):

LUT(I, lookUpTable, J);

性能分析

OpenCV提供了两个简便的可用于计时的函数 cv::getTickCount()cv::getTickFrequency() 。第一个函数返回你的CPU自某个事件(如启动电脑)以来走过的时钟周期数,第二个函数返回你的CPU一秒钟所走的时钟周期数。这样,我们就能轻松地以秒为单位对某运算计时:

double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;

用一个(2560 X 1600)的彩色图片。进行数百次测试的平均值:

遍历方法平均用时
Efficient Way79.4717 milliseconds
Iterator83.7201 milliseconds
On-The-Fly RA93.7878 milliseconds
LUT function32.5759 milliseconds

我们可以得出一些结论:

  • 尽量使用 OpenCV 内置函数。调用LUT 函数可以获得最快的速度。这是因为OpenCV库可以通过英特尔线程架构启用多线程。
  • 当然,如果你喜欢使用指针的方法来扫描图像,迭代法是一个不错的选择,不过速度上较慢。
  • 在debug模式下使用on-the-fly方法扫描全图是一个最浪费资源的方法,在release模式下它的表现和迭代法相差无几,但是从安全性角度来考虑,迭代法是更佳的选择。

http://www.hkcw.cn/article/DZLLnSMImg.shtml

相关文章

java26

1.异常 报错原因&#xff1a; 缺少 性能优化是指&#xff1a;"a""b""c"----------->"abc" 下面是异常的报错信息&#xff1a; 报错信息&#xff1a; 注意&#xff1a;报错位置从下往上看 异常作用二的体现&#xff1a; 结果&…

【Oracle】高级部分 - 从入门到精通的进阶之路

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 &#x1f680; 性能优化篇&#xff1a;让Oracle跑得飞快1. 执行计划分析 - 数据库的"透视眼"2. 索引优化策略 - 数据库的"导航系统"3. 分区表的威力 - 数据库的"分治策略" &…

【AI论文】推理语言模型的强化学习熵机制

摘要&#xff1a;本文旨在克服将强化学习扩展到使用 LLM 进行推理的主要障碍&#xff0c;即策略熵的崩溃。 这种现象在没有熵干预的RL运行中一直存在&#xff0c;其中策略熵在早期训练阶段急剧下降&#xff0c;这种探索能力的减弱总是伴随着策略性能的饱和。 在实践中&#xff…

Git深入解析功能逻辑与核心业务场景流程

一、Git核心功能逻辑架构 #mermaid-svg-9tj1iCr99u6QenJM {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-9tj1iCr99u6QenJM .error-icon{fill:#552222;}#mermaid-svg-9tj1iCr99u6QenJM .error-text{fill:#552222;st…

【HarmonyOS Next之旅】DevEco Studio使用指南(二十九) -> 开发云数据库

目录 1 -> 开发流程 2 -> 创建对象类型 3 -> 添加数据条目 3.1 -> 手动创建数据条目文件 3.2 -> 自动生成数据条目文件 4 -> 部署云数据库 1 -> 开发流程 云数据库是一款端云协同的数据库产品&#xff0c;提供端云数据的协同管理、统一的数据模型和…

[Python] Python自动化:PyAutoGUI的基本操作

初次学习&#xff0c;如有错误还请指正 目录 PyAutoGUI介绍 PyAutoGUI安装 鼠标相关操作 鼠标移动 鼠标偏移 获取屏幕分辨率 获取鼠标位置 案例&#xff1a;实时获取鼠标位置 鼠标点击 左键单击 点击次数 多次有时间间隔的点击 右键/中键点击 移动时间 总结 鼠…

【Hot 100】45. 跳跃游戏 II

目录 引言跳跃游戏 IIdp解题贪心解题 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;算法专栏&#x1f4a5; 标题&#xff1a;【Hot 100】45. 跳跃游戏 II❣️ 寄语&#xff1a;书到用时方恨少&#xff0c;事非经过不知难&#xff01; 引言 跳跃…

QT-JSON

#include <QJsonDocument>#include <QJsonObject>#include <QJsonArray>#include <QFile>#include <QDebug>void createJsonFile() {// 创建一个JSON对象 键值对QJsonObject jsonObj;jsonObj["name"] "John Doe";jsonObj[…

blender 手柄驱动开发-ubuntu

ubuntu 如何安装blender 官网blender.org下载tar.xz压缩文件 tar -xvf xxx.tar.xz如何启动blender,命令行输入&#xff1a; blender 如何在blender中安装pygame模块 需要找到blender中的python解释器路径import sys print(sys.executable)然后在终端terminal中使用以下命令 $ …

(9)-Fiddler抓包-Fiddler如何设置捕获Https会话

1.简介 由于近几年来各大网站越来越注重安全性都改成了https协议&#xff0c;不像前十几年前直接是http协议直接裸奔在互联网。接着讲解如何抓取https协议会话。 2.什么是HTTPS&#xff1f; HTTPS就是加过密的HTTP。使用HTTPS后&#xff0c;浏览器客户端和Web服务器传输的数…

差分隐私技术的有效性和局限性

差分隐私&#xff08;Differential Privacy, DP&#xff09;由计算机科学家Cynthia Dwork于 2006 年提出&#xff0c;其核心思想是&#xff1a;通过向数据中添加精心设计的随机噪声&#xff0c;确保单个个体的加入或删除不会显著改变数据分析结果的分布&#xff0c;从而从数学上…

篇章七 数据结构——栈和队列

目录 1. 栈(Stack) 1.1 概念 1.图示栈概念&#xff1a; 2.栈在现实生活中的例子&#xff1a; 1.2 栈的使用 1.3 栈的模拟实现 1.接口 2.数组实现 1.4 栈的应用场景 1. 改变元素的序列 2.单链表是否可以实现栈&#xff1f; 2.1 数组实现&#xff1a;顺序栈 2.2 链…

LM393红外避障电路Multisim仿真

电路分析&#xff1a; 开关S1模拟物体的靠近&#xff0c;当按键按下时&#xff0c;表示有物体靠近。 当没有检测到物体时&#xff08;按键没有按下&#xff09;&#xff0c;LM393D的同相端被R2拉高&#xff0c;电压为5V。 此时反相端的电压经过两个电阻分压后&#xff0c;电压…

C语言进阶--文件操作

1.为什么使用文件&#xff1f; 使用文件可以将数据直接存放在电脑的硬盘上&#xff0c;做到了数据的持久化。 2.什么是文件&#xff1f; 硬盘上的文件都是文件。但是在程序化设计中&#xff0c;我们一般谈到的文件有两种&#xff1a;程序文件、数据文件&#xff08;从文件功…

力扣刷题Day 66:分割回文串(131)

1.题目描述 2.思路 用了回溯的方法。首先写一个验证字符串是否是回文串的函数&#xff0c;然后遍历s&#xff0c;依次判断从当前字符到下一字符是否是回文串&#xff0c;是的话继续往后走&#xff0c;不是的话往回退。 3.代码&#xff08;Python3&#xff09; class Solutio…

【IC】多角多模式信号完整性优化

随着互连效应增强和时钟频率加快&#xff0c;串扰噪声、毛刺和意外信号延迟的发生概率也随之增加&#xff0c;信号完整性 (SI) 问题也日益凸显。由于 65 纳米和 45 纳米设计中横向导线电容的影响日益增大&#xff0c;与 SI 相关的时序违规显著增多。设计必须运行的操作模式和工…

2,QT-Creator工具创建新项目教程

目录 1,创建一个新项目 demo_01.pro(项目配置文件) 类似 CMakeList.txt widget.h(头文件)​ main.cpp(程序入口)​ widget.cpp(源文件)​ widget.ui(界面设计文件)​ 1,创建一个新项目 依次选择: 设置路径: 选择编译器: 如果选择CMake, 就会生成cmakel…

【RocketMQ 生产者和消费者】- 生产者发送同步、异步、单向消息源码分析(1)

文章目录 1. 前言2. send 方法发送同步消息3. sendDefaultImpl 发送消息4. sendKernelImpl 发送同步、异步、单向消息5. sendMessage 发送消息6. 同步发送 sendMessageSync6.1 invokeSyncImpl 同步调用 7. 异步发送 sendMessageAsync7.1 invokeAsyncImpl 异步调用 8. 单向发送 …

【harbor】--配置https

使用自建的 CA 证书来自签署和启用 HTTPS 通信。 &#xff08;1&#xff09;生成 CA认证 使用 OpenSSL 生成一个 2048位的私钥这是 自建 CA&#xff08;证书颁发机构&#xff09; 的私钥&#xff0c;后续会用它来签发证书。 # 1创建CA认证 cd 到harbor [rootlocalhost harbo…

SOC-ESP32S3部分:23-文件系统

飞书文档https://x509p6c8to.feishu.cn/wiki/SXf5w6seIijVVskvic5cNT2wng4 目前&#xff0c;ESP-IDF 框架支持三种文件系统。 SPIFFS&#xff08;SPI Flash File System&#xff09; 简介&#xff1a;SPIFFS 是专门为 SPI NOR Flash 设备设计的轻量级文件系统&#xff0c;适…