OpenCV 图像遍历与颜色缩减

Author Avatar
Nightn 1月 21, 2017
  • 在其它设备中阅读本文章

图像处理的基础是对图像每一个像素点的遍历,即图像扫描。在本节中,将介绍几种不同的图像遍历方式,为了对比不同方法的效率,我们不是单纯的遍历,而是对图像做更多的处理。在此,我们测试的是一种简单的颜色缩减方法。为了比较不同遍历算法的运行时间,你还将看到 OpenCV 中计时函数的用法。

1. 概述

图像处理的基础是对图像每一个像素点的遍历,即图像扫描。在本节中,将介绍几种不同的图像遍历方式,为了对比不同方法的效率,我们不是单纯的遍历,而是对图像做更多的处理。在此,我们测试的是一种简单的颜色缩减方法。为了比较不同遍历算法的运行时间,你还将看到 OpenCV 中计时函数的用法。

2. 图像存储方式

在进行下面的论述之前,先对图像矩阵在内存中的存储方式简单介绍。对于单通道灰度图像:

而对于多通道图像来说,矩阵中的列会包含多个子列,子列数与通道数相等,如 BGR 颜色模型的矩阵为:

3. 颜色缩减

何谓颜色缩减?对于元素类型为 uchar 的单通道图像矩阵,每个像素点有 256 个灰度值,但是对于三通道图像,每个像素点的颜色种类达 16777216 种(256 的三次方)。如此多的颜色可能会对算法性能造成严重影响,我们往往只需要颜色的一部分,也能满足要求,因此引入了颜色缩减。

如上图所示,左侧为颜色缩减前,右侧为颜色缩减后,数字表示灰度值。可以看出,灰度值 92 到 114 映射为 92;115 到 137 映射为 115,以此类推,左侧 164 种颜色缩减为右侧的 7 种颜色。实现方法也很简单:

//color1输入,color2输出 
color2 = (color1 / 23) * 23;

但是,如果直接对图像的每个像素进行上述除法和乘法,这样效率是很低的。一个较好的办法是事先生成一张颜色缩减的查找表,表中缩减前后的值都明确给定,这样遍历图像时,利用查找表直接对相应像素点进行赋值即可。其优势在于只需读取、无需计算。以下代码生成颜色查找表 color_table:

// 生成颜色查找表
vector<int> color_table;
int width = 20;
for (int i = 0; i < 256; i++)
{
    color_table.push_back(i / width * width);
}

4. 图像遍历

有了颜色查找表后,我们便可以对图像进行遍历并对像素点进行颜色缩减了。我们采用了几种不同的图像遍历方法,为了对比它们的效率,采用 OpenCV 提供的两个简单的计时器函数 getTickCount() 和 getTickFrequency(), 它们分别返回 CPU 走过的时钟周期数和 CPU 一秒的时钟周期数。因此,可以这样来计时(单位:秒):

double time_begin = (double)getTickCount();
// do something
double time_end = (double)getTickCount();
double time = (time_end - time_begin) / getTickFrequency();

4.1 利用指针遍历

Mat& ScanImageAndReduceC(Mat& I, vector<int> color_table)
{
     //只接收字符型矩阵
     CV_Assert(I.depth() != sizeof(uchar));
     int channels = I.channels(); //获取图像通道数
     int row = I.rows * channels;
     int col = I.cols;
     uchar* p;
     if (I.isContinuous()) //判断像素是否连续存储
     {
           col *= row;
           row = 1;
     }
     for (int i = 0; i < row; i++)
     {
           p = I.ptr<uchar>(i);
           for (int j = 0; j < col; j++)
           {
                p[j] = color_table[p[j]];
           }
     }
     return I;
}

颜色缩减结果(根据查找表的width设置缩减的程度,在此 width = 20)

算法执行时间为 0.0134007 秒。

4.2 利用迭代器遍历

Mat& ScanImageAndReduceIterator(Mat& I, vector<int> color_table)
{
     //只接收字符型矩阵
     CV_Assert(I.depth() != sizeof(uchar));
     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 = color_table[*it];
           break;
     }
     case 3:
     {
           MatIterator_<Vec3b> it, end;
           for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; it++)
           {
                (*it)[0] = color_table[(*it)[0]];
                (*it)[1] = color_table[(*it)[1]];
                (*it)[2] = color_table[(*it)[2]];
           }
           break;
     }
     }
     return I;
}

算法执行时间 0.0693951 秒。用迭代器遍历速度稍慢,但是更加安全。上述代码中,我们首先对图像的通道数进行判断,通道数为 1 时,直接对灰度值赋值;对于三通道彩色图像,每个像素可以看做一个包含三个 uchar 元素的 vector, 在 OpenCV 中用 Vec3b 命名。对于彩色图像,如果我们仅仅使用 uchar 而不是 Vec3b 迭代的话就只能获得蓝色通道的值(BGR模型中的第一个通道)。

4.3 通过相关返回值的 On-the-fly 地址遍历

Mat& ScanImageAndReduceRadomAccess(Mat& I, vector<int> color_table)
{
     //只接收字符型矩阵
     CV_Assert(I.depth() != sizeof(uchar));
     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) = color_table[I.at<uchar>(i, j)];
                }
           }
           break;
     }
     case 3:
     {
           for (int i = 0; i < I.rows; i++)
           {
                for (int j = 0; j < I.cols; j++)
                {
                      I.at<Vec3b>(i, j)[0] = color_table[I.at<Vec3b>(i, j)[0]];
                      I.at<Vec3b>(i, j)[1] = color_table[I.at<Vec3b>(i, j)[1]];
                      I.at<Vec3b>(i, j)[2] = color_table[I.at<Vec3b>(i, j)[2]];
                }
           }
           break;
     }
     }
     return I;
}

通过 at() 函数获取并更改图像中的元素。事实上,这种方法并不推荐呗用来进行图像扫描。

4.4 核心函数 LUT (The Core Function)

核心函数 LUT 是最被推荐用于实现批量图像元素查找和更改的方法,它并不需要你自己去扫描图像。我们先建立一个查找表:

Mat table(1, 256, CV_8U);
uchar* p = table.data;
for(int i = 0; i < 256; i++)
    p[i] = color_table[i];

然后调用函数:

//image是输入,image_reduce是输出
LUT(image, table, image_reduce);

4.5 结论

尽量使用 OpenCV 内置函数,调用 LUT 函数可以获得最快的速度,这是因为 OpenCV 库可以通过英特尔线程架构启用多线程,当然,迭代器也是一个不错的选择,优点是安全,缺点是速度较慢,on-the-fly方法不推荐使用。


More