opencv mat初始化_【2】OpenCV核心模块(2)遍历像素方法及其性能

50c1dffb0b6f2bb910446b4787cba130.png

在开发图像处理项目时,会遇到访问图像的每个像素的情况。本节主要内容是OpenCV如何访问像素,怎样提高效率,如何评价算法的性能。

目标

  • 遍历图像的每个像素
  • 内存中矩阵数据的存储
  • 测量算法性能
  • lookup table是什么
  • 原文网址How to scan images, lookup tables and time measurement with OpenCV
  • 本地目录D:opencvsourcesdoctutorialscorehow_to_scan_images
  • 代码目录D:opencvsourcessamplescpptutorial_codecorehow_to_scan_images
  • GitHub 有相应文档和OpenCV源代码
  • 版本OpenCV4.1.2(版本兼容性见英文原文,部分文档适用于OpenCV2.0和3.0)
  • 环境Windows、C++、VS2019 Community

测试案例 Our test case

一个颜色量化的例子。对于RGB图像的一个像素,有三个通道,每个通道的数据是unsigned char,那么组合起来就是1600万中颜色。在某些应用场景中这么多颜色会严重影响算法性能,并且去掉大多数颜色不会对最终结果造成影响。这种方法称为颜色空间退化,也就是把某些颜色用同一种颜色代替。比如数值0~9用0代替,10~19用10代替。对于unsigned char(0~255),除以int型,结果类型不变,会向下圆整,用下面公式可实现。

对于大型图像,每个像素进行一次除法和乘法,执行起来效率很低。如果对于给定的像素值,不再计算,而是查找对应的量化值,那么效率就会提升。这里就用到了lookup table。下面是创建lookup table的方法。

    int divideWith = 10; //量化分辨率
    uchar table[256];
    for (int i = 0; i < 256; ++i)
       table[i] = (uchar)(divideWith * (i/divideWith));

那么如何测量执行时间,OpenCV提供了下面的方法。

double t = (double)getTickCount();
// 待测试的代码
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "运行时间(秒): " << t << endl;

图像矩阵在内存中如何存储 How is the image matrix stored in memory?

对于灰度图像:

4831815b179f3d720a46bdbbde313309.png

对于RGB图像,每个子列与通道数(3个)一致,OpenCV的顺序是BGR:

2b2372132bb601cd96d91217a04a3b6b.png

通常内存足够大,可以保证一行接一行的连续存储。这样有助于加速访问每个像素。可以用isContinuous函数来判断矩阵是否是存储在连续的一块内存区域。下面是一个例子。

高效的方法 The efficient way

说到访问的访问内存的效率,C指针是第一。所有推荐下面的方法。

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
 // 只接受uchar类型的数据 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())//若图像是连续的,整个矩阵看做一行数据,行数为1,列长度为原行数乘以原列数
    {
        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]];//用lookup table赋值
        }
    }
 return I;
}

另外一种方法,可实现同样的结果。但是代码阅读性不强。

uchar* p = I.data;//如果图像导入Mat,那么I.data就是图像数据的首地址,指针指向改地址
for( unsigned int i =0; i < ncol*nrows; ++i)
    *p++ = table[*p];

迭代器(安全)方法The iterator (safe) method

为了防止访问越界或无效,采用迭代器可以安全访问像素,只需要指定begin和end。

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:
        {
            //对于三通道图像,每个像素点可以视为一个三个元素的向量,OpenCV利用Vec3b表示
            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;
}

引用返回的动态地址计算(翻译的不准确)On-the-fly address calculation with reference returning

这种方法不推荐用于遍历图像。是一种比较直观灵活的方式,可以通过设置行列坐标方式任意访问像素。

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_与Mat类似,但是用来Mat_可以简化代码,一般用不到,用Mat需要用到Mat::at函数
         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;
}

核心函数 The Core Function

这是核心模块提供的用lookup table修改图像的一种方式。因为在图像处理中,经常会用给定值来修改图像的像素值。所以OpenCV提供了实现的函数。用核心模块的cv::LUT() 函数。

首先创建一个Mat类型的lookup table:

 Mat lookUpTable(1, 256, CV_8U);//创建并且初始化lookup table
 uchar* p = lookUpTable.ptr();
 for( int i = 0; i < 256; ++i)
        p[i] = table[i];

然后调用函数 (I、J是输入、输出图像):

LUT(I, lookUpTable, J);

性能差异 Performance Difference

为了测试以上方法的性能差异,用了大小为 (2560 X 1600) 的图像。这里是彩色图像,为了比较结果,测试了上百次求平均值。

5f3041a1d56be6087ede800373c4b9ac.png


可以得出结论,尽可能使用OpenCV提供的函数,而不是自己编写。最快最简洁的是LUT函数,是因为OpenCV库使用了Intel TBB实现多线程。然而如果确实需要自己实现,推荐使用指针(还是离不开C)。迭代器安全但是速度慢,debug模式下的on-the-fly reference access是最慢的。在release模式下差不多,但是安全性不如迭代器。(个人看来,如果不用LUT函数,性能没有太大区别,哪个顺手用哪个,除非开销紧张)

YouTube 有官方的演示效果video posted 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值