OpenCV像素点邻域遍历效率比较,以及访问像素点的几种方法

OpenCV像素点邻域遍历效率比较,以及访问像素点的几种方法

前言:

以前笔者在项目中经常使用到OpenCV的算法,而大部分OpenCV的算法都需要进行遍历操作,而且很多遍历操作都是需要对目标像素点的邻域进行二次遍历操作。笔者参考了很多博文,经过了实验,在这篇博文中总结了OpenCV的遍历操作的效率。

参考博文: 《OpenCV获取与设置像素点的值的几个方法 》 《【OpenCV】访问Mat中每个像素的值(新)》 《[OpenCV] 访问Mat矩阵中的元素》 《OpenCV学习笔记(四十二)——Mat数据操作之普通青年、文艺青年、暴力青年》 《OpenCV学习笔记(四十三)——存取像素值操作汇总core》 《图像噪声的抑制——均值滤波、中值滤波、对称均值滤波》

一. 均值滤波

由于笔者想要了解像素点及其邻域的遍历,所以本文用于测试的算法是均值滤波。 均值滤波的方法比较简单。对待需要处理的当前像素,选择一个模板,该模板为其邻域内若干像素,然后用模板内所有像素的均值来替代原像素值。公式如下:

均值滤波模板如下图所示:

图中的模板大小选择了3 × 3矩阵,图中的1–8绿色部分的像素都是邻域内像素,黄色像素是在图像(x, y)像素处的均值滤波结果。

该方法十分简单,包括了简单的邻域像素点的操作,优缺点也十分明显:

  • 优点:算法简单,计算速度快;
  • 缺点:降低噪声的同时使图像产生模糊,特别是景物的边缘和细节部分。

二. 邻域遍历方法

笔者对不同邻域遍历方法使用的算法与图片如下:

  • 测试算法:均值滤波
  • 滤波内核尺寸:3 × 3
  • 图片尺寸:580 × 410

原图如下:

笔者参考了博文《【OpenCV】访问Mat中每个像素的值(新)》,从其中学习了几种遍历像素点的方法。针对邻域遍历,笔者最后总结了三种方法如下:

1. ptr与[]

Mat最直接的访问方法,是通过.ptr<>函数得到一行的指针,并用[]操作符访问某一列的像素值。 源码如下:

// using .ptr and []
void MyBlur_1(Mat src, Mat& dst)
{
    dst.create(src.size(), src.type());
    // 强制将ksize变为奇数
//   int ksize = ksize / 2 * 2 + 1;
    int ksize = 3;
    // kernel的半径
//    kr = ksize / 2;
    int kr = 1;

    int nr = src.rows - kr;
    int nc = (src.cols - kr) * src.channels();

    for(int j = kr; j < nr; j++)
    {
        // 获取图像三行数据的地址
        const uchar* previous = src.ptr<uchar>(j - 1);
        const uchar* current = src.ptr<uchar>(j);
        const uchar* next = src.ptr<uchar>(j + 1);

        uchar* output = dst.ptr<uchar>(j);

        for(int i = 1 * src.channels(); i < nc; i++)
        {
            output[i] = cv::saturate_cast<uchar>(
                        (previous[i - src.channels()] + previous[i] + previous[i + src.channels()]
                    + current[i - src.channels()] + current[i] + current[i + src.channels()]
                    + next[i - src.channels()] + next[i] + next[i + src.channels()]) / 9);
        }
    }
}

2. at获取图像坐标

.at操作可以用于操作单个像素点。通常的操作如下:

// 对于单通道图像
img.at<uchar>(i, j) = 255;
// 对于多通道图像
img.at<cv::Vec3b>(i, j)[0] = 255;

用at实现均值滤波的代码如下:

// using at
void MyBlur_2(Mat src, Mat& dst)
{
    dst.create(src.size(), src.type());
    // 强制将ksize变为奇数
//   int ksize = ksize / 2 * 2 + 1;
    int ksize = 3;
    // kernel的半径
//    kr = ksize / 2;
    int kr = 1;

    int nr = src.rows - kr;
    int nc = src.cols - kr;

    for(int j = kr; j < nr; j++)
    {
        for(int i = src.channels(); i < nc; i++)
        {
            dst.at<Vec3b>(j, i)[0] = cv::saturate_cast<uchar>(
                        (src.at<Vec3b>(j-1, i-1)[0] + src.at<Vec3b>(j, i-1)[0] + src.at<Vec3b>(j+1, i-1)[0]
                    + src.at<Vec3b>(j-1, i)[0] + src.at<Vec3b>(j, i)[0] + src.at<Vec3b>(j+1, i)[0]
                    + src.at<Vec3b>(j-1, i+1)[0] + src.at<Vec3b>(j, i+1)[0] + src.at<Vec3b>(j+1, i+1)[0]) / 9 );
            dst.at<Vec3b>(j, i)[1] = cv::saturate_cast<uchar>(
                        (src.at<Vec3b>(j-1, i-1)[1] + src.at<Vec3b>(j, i-1)[1] + src.at<Vec3b>(j+1, i-1)[1]
                    + src.at<Vec3b>(j-1, i)[1] + src.at<Vec3b>(j, i)[1] + src.at<Vec3b>(j+1, i)[1]
                    + src.at<Vec3b>(j-1, i+1)[1] + src.at<Vec3b>(j, i+1)[1] + src.at<Vec3b>(j+1, i+1)[1]) / 9 );
            dst.at<Vec3b>(j, i)[2] = cv::saturate_cast<uchar>(
                        (src.at<Vec3b>(j-1, i-1)[2] + src.at<Vec3b>(j, i-1)[2] + src.at<Vec3b>(j+1, i-1)[2]
                    + src.at<Vec3b>(j-1, i)[2] + src.at<Vec3b>(j, i)[2] + src.at<Vec3b>(j+1, i)[2]
                    + src.at<Vec3b>(j-1, i+1)[2] + src.at<Vec3b>(j, i+1)[2] + src.at<Vec3b>(j+1, i+1)[2]) / 9 );
        }
    }
}

然而,Debug版本下,at操作要比指针的操作慢很多,所以对于不连续数据或者单个点处理,可以考虑at操作,对于连续的大量数据,尽量不要使用它。

Release版本与Debug版本的对比,以及at操作的效率,在后面会有比较。

3. data

Mat类中,对data的定义如下:

//! pointer to the data
uchar* data;

data是Mat对象中的指针,指向存放内存中存放矩阵数据的一块内存,类型为uchar*。 又由于二维矩阵Mat中任一像素的地址为:

计算得到的便是M矩阵中像素(i, j)的地址。而又有:

所以上式可以改写为:

综上,用.data实现均值滤波的代码如下:

// using .data
void MyBlur_3(Mat src, Mat& dst)
{
    dst.create(src.size(), src.type());
    // 强制将ksize变为奇数
//   int ksize = ksize / 2 * 2 + 1;
    int ksize = 3;
    // kernel的半径
//    kr = ksize / 2;
    int kr = 1;

    int nr = src.rows - kr;
    int nc = (src.cols - kr) * src.channels();
    uchar* srcdata = (uchar*)src.data;
    uchar* dstdata = (uchar*)dst.data;

    for(int j = kr; j < nr; j++)
    {
        uchar* psrc_0 = srcdata + (j-1) * src.cols * src.channels();
        uchar* psrc_1 = srcdata + j * src.cols * src.channels();
        uchar* psrc_2 = srcdata + (j+1) * src.cols * src.channels();
        uchar* pdst = dstdata + j * dst.cols * dst.channels();
        for(int i = src.channels(); i < nc; i++)
        {
            *pdst++ = (psrc_0[i - src.channels()] + psrc_0[i] + psrc_0[i + src.channels()]
                    + psrc_1[i - src.channels()] + psrc_1[i] + psrc_1[i + src.channels()]
                    + psrc_2[i - src.channels()] + psrc_2[i] + psrc_2[i + src.channels()]) / 9;
        }
    }
}

需要注意的是,前面说的at操作与ptr操作,都是带有内存检查,防止操作越界的,然而使用data指针比较危险,虽然在Debug版本下的速度确实让人眼前一亮。所以在《Mat数据操作之普通青年、文艺青年、暴力青年》中,博主将.data操作称之为暴力青年。

三. 不同方法效率比较

写这篇博文之前,我还以为不同的方法对效率的影响十分巨大。当然这种想法是没有错的,因为在Debug版本中确实不同的遍历方法有着很大的效率区别。但是看了博客《Mat数据操作之普通青年、文艺青年、暴力青年》中的总结后,笔者才意识到原来在Release版本下,上述方法的效率其实差别不大。

1. 测试源码:

#include "cv.h"
#include "highgui.h"
#include <vector>
#include <cmath>
#include <math.h>
#include <iostream>

using namespace cv;
using namespace std;

void MyBlur_1(Mat src, Mat& dst);
void MyBlur_2(Mat src, Mat& dst);
void MyBlur_3(Mat src, Mat& dst);

// 测试遍历像素点对均值滤波的效率
int main()
{
    double time;
    double start;

    Mat img;
    img = imread("/home/grq/miska.jpg");

    Mat dst1;
    start = static_cast<double>(getTickCount());
    MyBlur_1(img, dst1);
    time = ((double)getTickCount() - start) / getTickFrequency() * 1000;
    cout << "using .ptr and []"<<endl;
    cout << "Time: " << time << "ms" << endl<<endl;

    Mat dst2;
    start = static_cast<double>(getTickCount());
    MyBlur_2(img, dst2);
    time = ((double)getTickCount() - start) / getTickFrequency() * 1000;
    cout << "using at "<<endl;
    cout<< "Time: " << time << "ms" << endl <<endl;

    Mat dst4;
    start = static_cast<double>(getTickCount());
    MyBlur_3(img, dst4);
    time = ((double)getTickCount() - start) / getTickFrequency() * 1000;
    cout <<"using .data"<<endl;
    cout << "Time: " << time << "ms" << endl<<endl;

    Mat dst3;
    start = static_cast<double>(getTickCount());
    blur(img, dst3, Size(3, 3));
    time = ((double)getTickCount() - start) / getTickFrequency() * 1000;
    cout<<"using OpenCV's blur"<<endl;
    cout << "Time: " << time << "ms" <<endl << endl;

    imshow("src", img);
    imshow("using .ptr and []", dst1);
    imshow("using at ", dst2);
    imshow("using .data", dst4);
    waitKey(0);

    return 0;
}

2. Debug版本

运行结束后,结果输出如下:

如图所示,在Debug版本下,不同的遍历方法对遍历的效率影响还是很大的。值得注意的有下面几点:

(1) 检查操作对效率的影响

实际上,at操作符与ptr操作符在Debug版本下都是有内存检查、防止操作越界的操作,而data十分简单粗暴,没有任何检查,由于它的简单粗暴所以使得data操作速度很快。所以在Debug版本下,at操作符与ptr操作符相较于data,速度还是慢了不少。 另外在Debug版本下,at操作要比指针操作慢得多,所以对于不连续数据或者单个点处理,可以考虑at操作,对于连续的大量数据,尽量不要使用它。

(2) 对重复计算进行优化

在博文《OpenCV学习笔记(四十三)——存取像素值操作汇总core 》中,博主提到:“在循环中重复计算已经得到的值,是个费时的工作。”并做了对比:

    int nc = img.cols * img.channels();  
    for (int i=0; i<nc; i++)  
    {.......}  

    //**************************  

    for (int i=0; i<img.cols * img.channels(); i++)  
    {......}  

博主说经过他的测试,前者明显快于后者。 笔者发现这正好是笔者的编程风格,躺着中了一枪…… 那么如果笔者把这一点改了,是不是也会有比较好的效率提升呢?作死的笔者尝试了一下: 在没有更改for循环之前,Debug版本的效率是这样的:

之后笔者对所有for循环做了上面的优化,测试结果如下:

结果显示,确实对于这几种遍历方式都是有一定的提升效果的。

3. Release版本

笔者尝试运行了一下Release版本,结果如下:

作对比如下:

Method

Debug(ms)

Release(ms)

ptr

15.226

3.5582

at

35.370

2.8227

data

11.642

2.1686

的确,Release版本下的遍历方法基本上效率都差不多,尤其是at,本来速度应该最慢,但在Release版本下也有很快的速度。这是由于Mat::at()操作在Debug模式下加入了CV_DbgAssert()来判断一些条件,所以拖慢了函数的运行;而在Release版本下没有了CV_DbgAssert(),所以速度有了提升。

四. 其他遍历像素点的方法

笔者推荐博文《【OpenCV】访问Mat中每个像素的值(新) 》,博主在文中提出了十余种遍历像素点的方法,且在文章最后给出了各种方法的运行效率,可谓十分详细,所以笔者在此就不赘述了。 注:博主的对不同方法的比较,在评论区也被指出都是在Debug版本下的对比,如果将程序调整至Release版本,各个方法的效率也没有太大差别。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏机器之心

教程 | 如何在Tensorflow.js中处理MNIST图像数据

对任何一个机器学习问题而言,数据处理都是很重要的一步。本文将采用 Tensorflow.js(0.11.1)的 MNIST 样例(https://github....

993
来自专栏人工智能LeadAI

用TensorFlow的Linear/DNNRegrressor预测数据

今天要处理的问题对于一个只学了线性回归的机器学习初学者来说还是比较棘手——通过已知的几组数据预测一组数据。用excel看了下,关系不是很明显,平方,log都不是...

831
来自专栏瓜大三哥

多任务验证码识别

使用Alexnet网络进行训练,多任务学习:验证码是根据随机字符生成一幅图片,然后在图片中加入干扰象素,用户必须手动填入,防止有人利用机器人自动批量注册、灌水、...

4857
来自专栏ATYUN订阅号

PyTorch 4.0版本迁移指南

欢迎阅读PyTorch 0.4.0的迁移指南。在此版本中,我们引入了许多振奋人心的新功能和重要的bug修复,旨在为用户提供更好,更清晰的接口。在这个指南中,我们...

1182
来自专栏闪电gogogo的专栏

压缩感知重构算法之正则化正交匹配追踪(ROMP)

  在看代码之前,先拜读了ROMP的经典文章:Needell D,VershyninR.Signal recovery from incompleteand i...

2996
来自专栏算法修养

文本分类学习 (十)构造机器学习Libsvm 的C# wrapper(调用c/c++动态链接库)

前言: 对于SVM的了解,看前辈写的博客加上读论文对于SVM的皮毛知识总算有点了解,比如线性分类器,和求凸二次规划中用到的高等数学知识。然而SVM最核心的地方应...

532
来自专栏企鹅号快讯

C+实现神经网络之四—神经网络的预测和输入输出的解析

在上一篇的结尾提到了神经网络的预测函数predict(),说道predict调用了forward函数并进行了输出的解析,输出我们看起来比较方便的值。 神经网络的...

1786
来自专栏AI科技大本营的专栏

Variable和Tensor合并后,PyTorch的代码要怎么改?

昨日(4 月 25 日),Facebook 推出了 PyTorch 0.4.0 版本,该版本有诸多更新和改变,比如支持 Windows,Variable 和 T...

3532
来自专栏阮一峰的网络日志

哈希碰撞与生日攻击

所谓哈希(hash),就是将不同的输入映射成独一无二的、固定长度的值(又称"哈希值")。它是最常见的软件运算之一。

1312
来自专栏数据之美

相似文档查找算法之 simHash 简介及其 java 实现

传统的 hash 算法只负责将原始内容尽量均匀随机地映射为一个签名值,原理上相当于伪随机数产生算法。产生的两个签名,如果相等,说明原始内容在一定概 率 下是相...

38010

扫码关注云+社区