OpenCV 图像遍历与颜色缩减

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容