Skip to content

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

Description

@upcAutoLang

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中任一像素的地址为:
$$ addr(M_{i,j}) = M.data + M.step[0] \times i + M.step[1] \times j $$
计算得到的便是M矩阵中像素(i, j)的地址。而又有:
$ M.step[0] = M.cols \times M.channels $
$ M.step[1] = M.channels $
所以上式可以改写为:
$$ addr(M_{i,j}) = M.data + M.cols \times M.channels \times i + M.channels \times 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版本,各个方法的效率也没有太大差别。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions