news 2026/9/7 7:36:48

OpenCV 图像逐像素遍历实战:指针、迭代器、at() 与 LUT 四种扫描方式解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV 图像逐像素遍历实战:指针、迭代器、at() 与 LUT 四种扫描方式解析

OpenCV 图像逐像素遍历实战:指针、迭代器、at() 与 LUT 四种扫描方式解析

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

本文围绕 OpenCV 的图像逐像素扫描技术展开,以仓库中doc/tutorials/core/how_to_scan_images/教程为骨架,讲清楚四个核心问题:如何遍历图像中的每一个像素、cv::Mat矩阵在内存中如何存储、如何用cv::getTickCount()/cv::getTickFrequency()精确测量算法耗时,以及为什么用查找表(LUT)替代逐像素计算能显著提升性能。读完后,你将能熟练使用 C 风格指针访问、迭代器访问、at()随机访问和cv::LUT()四种扫描方式,并能结合仓库示例 how_to_scan_images.cpp 自行编译运行、复现性能对比。

灰度图像在内存中的单通道存储布局(源自 how_to_scan_images.markdown)

问题背景:色彩空间压缩测试用例

教程选择了一个典型应用场景:色彩空间压缩(color space reduction)。使用unsigned char存储的图像每个通道最多有 256 个取值,三通道图像理论上可组合出 1677 万种颜色,过多的色阶会对算法性能造成沉重打击。但很多情况下,用更少的颜色就能得到几乎相同的最终效果。

所谓色彩空间压缩,就是把颜色空间的当前值除以一个新的输入值,从而减少颜色数量:例如 0~9 的所有值取新值 0,10~19 取新值 10,依此类推。

uchar(取值 0~255 的无符号字符)值除以int值时,结果仍是字符类型,小数部分会被向下取整。利用这一点,上述压缩运算在uchar域内可表达为:

I_new = (I_old / 10) * 10

一个朴素的算法只需遍历图像的每个像素并套用该公式。但注意这里包含一次除法和一次乘法——这两类运算对系统而言非常昂贵,应尽量用减法、加法甚至简单赋值等更廉价的运算替代。更重要的是,参与运算的输入值种类有限:对uchar而言正好只有 256 种。

因此,对于大图更明智的做法是:预先计算所有可能的取值,遍历时只做赋值——这正是查找表(lookup table)的价值所在。查找表是一个(或可多维的)简单数组,对给定的输入取值保存最终输出值。它的优势在于不需要现场计算,只需读取结果。

测试程序与命令行用法

仓库中的完整示例代码位于 samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp。它的功能是:

  1. 读取命令行传入的图像(彩色或灰度均可);
  2. 按命令行传入的整数执行色彩压缩;
  3. 分别用 OpenCV 中三种主要的逐像素遍历方式扫描图像,并打印各自的耗时。

基本用法:

how_to_scan_images imageName.jpg intValueToReduce [G]

最后一个参数可选:传入G时以灰度格式加载图像,否则使用 BGR 彩色空间(对应源码中imread(argv[1], IMREAD_GRAYSCALE)imread(argv[1], IMREAD_COLOR)两个分支)。

第一步是构建查找表。源码(main 函数中的 dividewith 段)先用 C++ 的stringstream类把第三个命令行参数从文本转成整数,再用简单的循环套用上式计算查找表——这一段与 OpenCV 本身无关,是纯 C++ 代码:

//! [dividewith] int divideWith = 0; // convert our input string to number - C++ style stringstream s; s << argv[2]; s >> divideWith; if (!s || !divideWith) { cout << "Invalid number entered for dividing. " << endl; return -1; } uchar table[256]; for (int i = 0; i < 256; ++i) table[i] = (uchar)(divideWith * (i/divideWith)); //! [dividewith]

divideWith = 10为例,table[0..9] = 0table[10..19] = 10……table[250..255] = 250,恰好实现了向下取整到 10 的倍数的压缩语义。

时间测量:getTickCount 与 getTickFrequency

如何测量算法耗时?OpenCV 提供了两个简单函数(声明见 utility.hpp):

  • cv::getTickCount():返回自某个事件(如系统开机)以来 CPU 经历的 tick 数;
  • cv::getTickFrequency():返回 CPU 每秒钟发出多少次 tick。

因此,测量两次操作之间的耗时非常简单:

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

示例程序进一步把这一模式封装为“循环 100 次再取平均”的做法(main 函数),以 2560×1600 的大图反复运行、摊平波动,保证性能数据可比:

const int times = 100; t = (double)getTickCount(); for (int i = 0; i < times; ++i) { cv::Mat clone_i = I.clone(); J = ScanImageAndReduceC(clone_i, table); } t = 1000*((double)getTickCount() - t)/getTickFrequency(); t /= times;

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

如 mat_the_basic_image_container 教程所述,矩阵的存储尺寸取决于使用的颜色系统,更准确地说,取决于通道数。

灰度图像类似下图——每个像素就是一个uchar

多通道图像中,每列包含与通道数相同的子列。以 BGR 彩色系统为例,每个像素占据连续的 3 个字节(B、G、R)。注意通道顺序与 RGB 相反——OpenCV 使用 BGR 而非 RGB。

由于在许多情况下内存足够大,各行可以依次连续存放,拼成一条“长行”。因为所有数据集中在一个地方、前后相连,这有助于加速扫描过程。可以用cv::Mat::isContinuous()函数来询问矩阵是否满足该条件(声明见 mat.hpp),下一节将给出实际用法。

高效方式:C 风格指针访问

从性能角度看,无人能击败经典的 C 风格operator[](指针)访问。因此,推荐的最有效赋值方式是逐行取指针:

//! [scan-c] Mat& ScanImageAndReduceC(Mat& I, const uchar* const table) { // accept only char type matrices CV_Assert(I.depth() == CV_8U); int channels = I.channels(); int nRows = I.rows; int nCols = I.cols * channels; 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; } //! [scan-c]

(上述代码取自 how_to_scan_images.cpp 的scan-c段。)

这段代码的逻辑要点:

  • nCols = I.cols * channels:彩色图像每行要遍历的元素是灰度的 3 倍,必须把通道数乘进去;
  • I.isContinuous()为真时,整个矩阵其实是一条连续内存,于是把nCols乘上nRows、令nRows = 1,只需取一次指针就能走到底;
  • I.ptr<uchar>(i)获取第i行起始指针,内层循环用operator[]依次访问。

另外还有一种等价写法:Mat对象的data数据成员返回第一行第一列的指针。如果该指针为 null,说明对象中没有有效输入——检查这一点是判断图像加载是否成功的最简单方法。若存储是连续的,可以直接遍历整个data指针。灰度图像下可写成:

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

效果相同,但这段代码日后更难读,若有更复杂的技术在里面就更是如此。而且实际观察中两者的性能结果一致——现代编译器大概率会自动完成这一小优化。

迭代器(安全)方式

高效方式中,“遍历正确数量的uchar字段”以及“跳过行之间可能出现的间隙”是你自己的责任。迭代器方式被认为更安全,因为它把这两件事从用户手中接管过去。你只需请求图像矩阵的 begin 和 end,然后把 begin 迭代器递增到 end 为止。要获取迭代器指向的值,用\*运算符(加在它前面):

//! [scan-iterator] Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table) { // accept only char type matrices CV_Assert(I.depth() == CV_8U); 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; } //! [scan-iterator]

(取自 how_to_scan_images.cpp 的scan-iterator段。)

两个关键细节:

  1. 彩色图像每个“列”含 3 个uchar,可看作一个短的uchar向量,OpenCV 将其命名为Vec3b;访问第 n 个子通道用简单的operator[]
  2. OpenCV 迭代器按列推进并自动跳到下一行。因此对彩色图像,如果误用简单的uchar迭代器,你实际只能访问到蓝色通道的值——这正是示例中对 1 通道和 3 通道分别switch处理的原因。

即时地址计算 + 引用返回:at() 随机访问

最后一种方式不推荐用于整图扫描,它的本意是获取或修改图像中某个随机位置的元素。基本用法是给出要访问元素的行号和列号。与前面的扫描方式一样,这里也取决于你“通过什么类型”去看这张图——需要手动指定at<>模板参数(scan-random段,见 how_to_scan_images.cpp):

//! [scan-random] Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table) { // accept only char type matrices CV_Assert(I.depth() == CV_8U); 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; } //! [scan-random]

at()接收你的输入类型和坐标,现场计算出目标元素的地址,然后返回一个引用:读取时为const引用,写入时为非const引用。作为安全措施,仅在 debug 模式下会对输入坐标做有效性校验,越界时会在标准错误输出流打印友好的报错信息。与高效方式相比,release 模式下它的差异在于:对图像每个元素都要重新计算一行行指针,再用 C 风格operator[]取列元素。

如果需要对同一张图像做多次at()查找,每次都手写类型和at关键字会很繁琐耗时。为此 OpenCV 提供了cv::Mat_数据类型:它要求在定义时指定通过什么类型查看数据矩阵,作为回报你可以用operator()快速访问元素,且与普通的cv::Mat之间可轻松互相转换——上面彩色分支中的Mat_<Vec3b> _I = I;就是一个实例。需要强调的是:同样的操作(以同样的运行时速度)用cv::Mat::at也能完成Mat_只是给懒于重复书写的程序员省事的技巧,二者并无本质性能差异。

核心函数:cv::LUT

这是一个“附加”方法——用查找表修改图像值,而无需自己编写任何扫描逻辑。图像处理中“把一张图的所有值改写成别的值”非常常见,OpenCV 为此提供了cv::LUT()(声明及文档见 core.hpp)。其语义是:

dst(I) <- lut(src(I) + d)

其中d的取值取决于src的深度:CV_8U/CV_16U为 0,CV_8S为 128,CV_16S为 32768。输入数组须为 8 位或 16 位整数;lut对 8 位输入须有 256 个元素、对 16 位输入须有 65536 个元素;多通道输入时查找表可以是单通道(各通道共用同一张表)或与输入相同通道数。仓库中另有 OpenCL 加速实现 lut.cl,说明该函数在多后端环境下均有覆盖。

用法分两步。先把查找表构建为Mattable-init段):

//! [table-init] Mat lookUpTable(1, 256, CV_8U); uchar* p = lookUpTable.ptr(); for( int i = 0; i < 256; ++i) p[i] = table[i]; //! [table-init]

然后调用函数(I为输入图像,J为输出图像;table-use段):

//! [table-use] LUT(I, lookUpTable, J); //! [table-use]

一行代码替代了整个双层循环——这就是“能用现成函数就别重复造轮子”的直观体现。

性能对比

想要最准确的结果,应自行编译并运行程序。教程作者为了使差异更明显,使用了一张相当大(2560×1600)的图像,且以下性能数据针对彩色图像,为每个函数调用 100 次的平均值:

方法耗时
高效方式(C 指针)79.4717 毫秒
迭代器83.7201 毫秒
即时地址计算(at)93.7878 毫秒
LUT 函数32.5759 毫秒

可以得出几点结论:

  1. 优先使用 OpenCV 已有的函数,不要重新发明轮子。本例中最快的恰恰是cv::LUT()——这是因为 OpenCV 库通过并行后端(如 TBB)开启了多线程执行(从源码结构看,cv::parallel_for_等并行设施是这类向量化操作获得加速的基础),而手写三重循环只能跑满单核。
  2. 如果必须手写简单的图像扫描,优先选指针方式:它是三种手写方案中最快的。
  3. 迭代器是更安全的选择(自动处理行间隙与通道推进),但速度稍慢。
  4. at()即时引用访问用于整图扫描在 debug 模式下代价最高(每像素都有一次坐标校验);release 模式下它可能略胜迭代器也可能不,但无论如何它牺牲了迭代器的安全性。

小结

  • 逐像素访问cv::Mat有四种途径:C 指针(最快、需自己处理行间隙与通道数)、迭代器(安全、自动跨行)、at()/Mat_(随机访问友好,不推荐整图扫描)、cv::LUT()等现成函数(向量化 + 并行,通常最快)。
  • 查找表把“每个像素一次除加运算”降为“每个像素一次查表赋值”,是处理 8 位图像点运算的经典技巧。
  • 计时用getTickCount()getTickFrequency()相除,多次运行取平均,才能得到可比的性能数据。
  • 想深入复现,直接参考 how_to_scan_images.cpp:编译后执行how_to_scan_images imageName.jpg 10(彩色)或追加G(灰度),即可在本地得到属于你的性能对比数据。

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 7:34:41

用Python合成γ波专注声场:双耳节拍与等时音调实战

当你在工作、学习或阅读时&#xff0c;是否经常发现注意力难以长时间集中&#xff1f;市面上提到“γ波”“心流”“专注声场”的音频越来越多&#xff0c;很多人直接播放现成音频&#xff0c;却不知道这些声音是怎么“合成”出来的。如果把这类音频当成黑盒&#xff0c;一旦遇…

作者头像 李华
网站建设 2026/9/7 7:33:08

VLC 3.0.11 原生支持 AVS+ 与 DRA:国产音视频标准播放不再难

简介&#xff1a;VLC 3.0.11 增强版播放器是一份面向 Windows 7 及以上系统的特殊构建&#xff0c;核心价值在于内置了对国产 AVS 与 DRA 编码格式的原生支持。AVS 是中国自主研发的高效视频标准&#xff0c;常应用于高清广播电视与 IPTV 传输&#xff1b;DRA 是国产高保真数字…

作者头像 李华
网站建设 2026/9/7 7:32:37

软考高级系统架构设计师备考全攻略:从刷题到论文的完整方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:32:12

Java接入讯飞语音转文字:WebSocket实时流式接口全解析

简介&#xff1a;这是一份面向Java开发者与后端工程师的讯飞语音转文字集成示例&#xff0c;核心解决了在Java后端中快速接入讯飞ASR音频识别能力的问题&#xff0c;适合有基础API调用经验、正为智能设备或AI交互功能寻找语音方案的程序员。资源包为zip格式&#xff0c;共八个文…

作者头像 李华
网站建设 2026/9/7 7:31:19

用manim动画讲透极大似然估计:从似然函数到参数求解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:31:01

FanControl 风扇控制完整指南:三步配好温度曲线

FanControl 风扇控制完整指南&#xff1a;三步配好温度曲线 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/fa/FanCo…

作者头像 李华