十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 的所有值取新值 010~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。它的功能是读取命令行传入的图像彩色或灰度均可按命令行传入的整数执行色彩压缩分别用 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] 0、table[10..19] 10……table[250..255] 250恰好实现了向下取整到 10 的倍数的压缩语义。时间测量getTickCount 与 getTickFrequency如何测量算法耗时OpenCV 提供了两个简单函数声明见 utility.hppcv::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.ptruchar(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.ptruchar(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.beginuchar(), end I.enduchar(); it ! end; it) *it table[*it]; break; } case 3: { MatIterator_Vec3b it, end; for( it I.beginVec3b(), end I.endVec3b(); 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段。两个关键细节彩色图像每个“列”含 3 个uchar可看作一个短的uchar向量OpenCV 将其命名为Vec3b访问第 n 个子通道用简单的operator[]。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.atuchar(i,j) table[I.atuchar(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为 0CV_8S为 128CV_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 毫秒即时地址计算at93.7878 毫秒LUT 函数32.5759 毫秒可以得出几点结论优先使用 OpenCV 已有的函数不要重新发明轮子。本例中最快的恰恰是cv::LUT()——这是因为 OpenCV 库通过并行后端如 TBB开启了多线程执行从源码结构看cv::parallel_for_等并行设施是这类向量化操作获得加速的基础而手写三重循环只能跑满单核。如果必须手写简单的图像扫描优先选指针方式它是三种手写方案中最快的。迭代器是更安全的选择自动处理行间隙与通道推进但速度稍慢。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),仅供参考
返回列表