OpenCV2.4+遍历读写像素方法总结及时间度量

以下文本及代码基本基于《OpenCV 2.4.13.0 documentation》的How to scan images, lookup tables and time measurement with OpenCV一节,英文好的同学可以直接看原文。

  • 1. 颜色压缩

颜色压缩(Color Reduction)最简单的理解就是减少表示图像的颜色数目,我们都知道,8位位深的3通道RGB真彩图像包括了1600多万(16777216)的颜色数目,其实在某些应用中用不到这么多数量(例如图像传输(transmission)、分割(segmentation)、压缩(compression))的颜色。这也是一个研究的小方向,想了解更多,可以阅读文章Adaptive Color ReductionColor reduction and estimation of the number of dominant colors by using a self-growing and self-organized neural gas 。

在这里,我们实现一个很简单的方法:

I_old 为输入的像素值,I_new为输出的像素值,divideWidth代表要减少的度,我们可以理解为当divideWidth为128的时候,对于灰度图像就做的是一个阈值为128的二值化。

上点图更直观一点,左边为灰度原始图像,右边为输出图像:

当divideWidth为128时:

当divideWidth为64时:

根据以上描述,实际上这个公式我们可以建立一个映射表来避免重复计算,对于0-255的有限的输入值,建立输出值的映射表:

// color space divide width
const int divideWidth = 128;
// converting table for reducing color space
uchar table[256];
// first, we should build the converting table
for (int i = 0; i < 256; i++)
{
table[i] = (uchar)(divideWidth * (i / divideWidth));
}

  

我们的测试程序做的就是:

1. 读入一幅灰度图像和一幅RGB彩色图像;

2. 按照下文描述的四种访问像素的方式来实现这个算法;

3. 多次分别跑算法,取平均,对四种访问像素的方式进行对比。

  • 2. 图像数据的存储

首先大致说明下图像数据如何在内存中存储。Mat是OpenCV2.x版本以上基本的图像类型,Mat可以视为一个矩阵,矩阵的大小依赖于该Mat是什么颜色空间(Color Space),比如最基本的灰度(Gray scale)或者RGB,CMYK,YCbCr等,因为这决定了该Mat具有多少个通道,一般来讲,灰度图像只有一个通道,而RGB图像具有三个通道。

对于灰度图像来讲,图像数据在内存中的存储如图所示:

对于多通道图像来讲,有几个通道,每一列就包含多少个子列。对于经常使用的基于RGB颜色空间,其图像数据存储如下:

需要注意的是通道的顺序是BGR而非RGB。

一般而言,图像数据的每一行在内存中都是连续存储的,因为这样对于遍历图像数据更高效。Mat提供了isContinuous()函数来获取是否是连续存储的数据。

  • 3. 时间的度量

OpenCV提供了两个简单的函数,getTickCount()getTickFrequency()。getTickCount返回从操作系统启动到当前所经的计时周期数,类型为int64。getTickFrequency返回每秒的计时周期数,类型为double。因此就可以用如下的代码计算以秒为单位的两个操作所耗费的时间:

double dtime = (double) getTickCount();
// do something
dtime = ((double)getTickCount() - dtime)/getTickFrequency();
  • 4. 图像像素的访问方式

4.1 ptr操作和指针-高效的方式

这种方式基于.ptr和C的[]操作,这种方式也是比较推荐的遍历图像的方式。

/** @Method 1: the efficient method
 accept grayscale image and RGB image */
int ScanImageEfficiet(Mat & image)
{
	// channels of the image
	int iChannels = image.channels();
	// rows(height) of the image
	int iRows = image.rows;
	// cols(width) of the image
	int iCols = image.cols * iChannels;

	// check if the image data is stored continuous
	if (image.isContinuous())
	{
		iCols *= iRows;
		iRows = 1;
	}

	uchar* p;
	for (int i = 0; i < iRows; i++)
	{
		// get the pointer to the ith row
		p = image.ptr<uchar>(i);
		// operates on each pixel
		for (int j = 0; j < iCols; j++)
		{
			// assigns new value
			p[j] = table[p[j]];
		}
	}

	return 0;
}

这里获取一个指向每一行的指针,然后遍历这一行所有的数据。当图像数据是连续存储的时候,只需要取一次指针,然后就可以遍历整个图像数据。

4.2 迭代器-比较安全的方式

相较于高效的方式需要自己来计算需要遍历的数据量,以及当图像的行与行之间数据不连续的时候需要跳过一些间隙。迭代器(iterator)方式提供了一个更安全的访问图像像素的方式。你只需要做的就是声明两个MatIterator_变量,一个指向图像开始,一个指向图像结束,然后迭代。

/** @Method 2: the iterator(safe) method
 accept grayscale image and RGB image */
int ScanImageIterator(Mat & image)
{
	// channels of the image
	int iChannels = image.channels();

	switch (iChannels)
	{
	case 1:
	{
		MatIterator_<uchar> it, end;
		for (it = image.begin<uchar>(), end = image.end<uchar>(); it != end; it++)
		{
			*it = table[*it];
		}
		break;
	}
	case 3:
	{
		MatIterator_<Vec3b> it, end;
		for (it = image.begin<Vec3b>(), end = image.end<Vec3b>(); it != end; it++)
		{
			(*it)[0] = table[(*it)[0]];
			(*it)[1] = table[(*it)[1]];
			(*it)[2] = table[(*it)[2]];
		}
		break;
	}
	}

	return 0;
}

彩色图像的话,由于是三个通道的向量,OpenCV提供了Vec3b的数据类型来存储。

  • 4.3 动态地址计算-更适合随机访问的方式

这种方式不推荐用来遍历图像,一般用在要随机访问很少量的图像数据的时候。基本用法就是指定行列号,返回该位置的像素值。不过需要你事先知道返回的数据类型是uchar还是Vec3b或者其他的。

/** @Method 3: random access method
 accept grayscale image and RGB image */
int ScanImageRandomAccess(Mat & image)
{
	// channels of the image
	int iChannels = image.channels();
	// rows(height) of the image
	int iRows = image.rows;
	// cols(width) of the image
	int iCols = image.cols;

	switch (iChannels)
	{
	// grayscale
	case 1:
	{
		for (int i = 0; i < iRows; i++)
		{
			for (int j = 0; j < iCols; j++)
			{
				image.at<uchar>(i, j) = table[image.at<uchar>(i, j)];
			}
		}
		break;
	}
	// RGB
	case 3:
	{
		Mat_<Vec3b> _image = image;
		for (int i = 0; i < iRows; i++)
		{
			for (int j = 0; j < iCols; j++)
			{
				_image(i, j)[0] = table[_image(i, j)[0]];
				_image(i, j)[1] = table[_image(i, j)[1]];
				_image(i, j)[2] = table[_image(i, j)[2]];
			}
		}
		image = _image;
		break;
	}
	}

	return 0;
}

4.4 查找表-一颗赛艇的方式

OpenCV大概也考虑到了有很多这种需要改变单个像素值的场合(比如基于单个像素值的亮度变换,gamma矫正等),因此在core模块提供了一个更加高效很一颗赛艇的LUT()函数来进行这种操作而且不需要遍历整个图像。

首先建个映射查找表:

// build a Mat type of the lookup table
Mat lookupTable(1, 256, CV_8U);
uchar* p = lookupTable.data;
for (int i = 0; i < 256; i++)
{
	p[i] = table[i];
}

然后调用LUT()函数:

// call the function
LUT(image, lookupTable, matout);

image是输入图像,matout是输出图像。

  • 5. 不同方式的性能度量

测试环境:OpenCV版本3.1.0,Windows 7 64位系统。

测试图像是512*512的Lena灰度图和512*512的Lena彩色图。分别跑100次不同的方法,然后得到的平均时间如下:

可以看到LUT算法稍优于高效的方法,但都很高效,而迭代器方式与随机访问方式就很慢了。

时间: 2024-12-27 12:21:39

OpenCV2.4+遍历读写像素方法总结及时间度量的相关文章

Opencv中图像的遍历与像素操作

Opencv中图像的遍历与像素操作 OpenCV中表示图像的数据结构是cv::Mat,Mat对象本质上是一个由数值组成的矩阵.矩阵的每一个元素代表一个像素,对于灰度图像,像素是由8位无符号数来表示(0代表黑,255代表白):对于彩色图像,每个像素是一个三元向量,即由三个8位无符号数来表示三个颜色通道(Opencv中顺次为蓝.绿.红). 我们先来介绍下cv::Mat类的获取像素的成员函数at(),其函数原型如下: template<typename _Tp> _Tp& at(int i0

在.net中序列化读写xml方法的总结

在.net中序列化读写xml方法的总结 阅读目录 开始 最简单的使用XML的方法 类型定义与XML结构的映射 使用 XmlElement 使用 XmlAttribute 使用 InnerText 重命名节点名称 列表和数组的序列化 列表和数组的做为数据成员的序列化 类型继承与反序列化 反序列化的实战演练 反序列化的使用总结 排除不需要序列化的成员 强制指定成员的序列化顺序 自定义序列化行为 序列化去掉XML命名空间及声明头 XML的使用建议 XML是一种很常见的数据保存方式,我经常用它来保存一些

mapcontrol 遍历所有图层方法

通过IMap中的get_layers()可以遍历MapControl中当前的图层.此方法可以通过指定UID对图层进行过滤或者分类. 1. 遍历矢量图层 public IEnumLayer GetFeatureLayers() { UID uid = new UIDClass(); uid.Value = "{40A9E885-5533-11d0-98BE-00805F7CED21}";//FeatureLayer IEnumLayer layers = frmMap.m_mapCtrl

Java读写文件方法总结

Java的读写文件方法在工作中相信有很多的用处的,本人在之前包括现在都在使用Java的读写文件方法来处理数据方面的输入输出,确实很方便.奈何我的记性实在是叫人着急,很多时候既然都会想不起来怎么写了,不过我的Java代码量也实在是少的可怜,所以应该多多练习.这里做一个总结,集中在一起方面今后查看. Java读文件 1 package 天才白痴梦; 2 3 import java.io.BufferedReader; 4 import java.io.File; 5 import java.io.F

java遍历泛型的方法

一.List遍历 Java中List遍历有三种方法来遍历泛型,主要为: 1.for循环遍历 2.iterator遍历 3.foreach遍历 package com.gmail.lsgjzhuwei; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.junit.Test; public class test { //第一种方法:for循环遍历 @Test public

Python文件遍历二种方法

分享下有关Python文件遍历的两种方法,使用的OS模块的os.walk和os.listdir实现. 关于Python的文件遍历,大概有两种方法,一种是较为便利的os.walk(),还有一种是利用os.listdir()递归遍历.方法一:利用os.walkos.walk可以自顶向下或者自底向上遍历整个文件树,然后返回一个含有3个元素的tuple,(dirpath, dirnames, filenames).注意,os.walk()会返回一个generater,所以调用的时候一定要放到for循环中

【转】更简单的非递归遍历二叉树的方法

解决二叉树的很多问题的方案都是基于对二叉树的遍历.遍历二叉树的前序,中序,后序三大方法算是计算机科班学生必写代码了.其递归遍历是人人都能信手拈来,可是在手生时写出非递归遍历恐非易事.正因为并非易事,所以网上出现无数的介绍二叉树非递归遍历方法的文章.可是大家需要的真是那些非递归遍历代码和讲述吗?代码早在学数据结构时就看懂了,理解了,可为什么我们一而再再而三地忘记非递归遍历方法,却始终记住了递归遍历方法? 三种递归遍历对遍历的描述,思路非常简洁,最重要的是三种方法完全统一,大大减轻了我们理解的负担.

文件操作-正确的遍历目录的方法

1. 看手册的例子说的 very good 在删除目录 和 遍历目录的时候要注意了,应该很少人把名字取为0吧. //  正确的遍历目录的方法 while(false !==( $file= readdir($file_path) ) ){ } //错误的遍历目录的方法 while($file=readdir($file_path)){ } 偶之前就一直使用这个错误的方法 ------------------ 全等 ===  ,不全等 :  !==    值不等,类型不等,值&类型都不等.. re

C#_在.net中序列化读写xml方法的总结

阅读目录 开始 最简单的使用XML的方法 类型定义与XML结构的映射 使用 XmlElement 使用 XmlAttribute 使用 InnerText 重命名节点名称 列表和数组的序列化 列表和数组的做为数据成员的序列化 类型继承与反序列化 反序列化的实战演练 反序列化的使用总结 排除不需要序列化的成员 强制指定成员的序列化顺序 自定义序列化行为 序列化去掉XML命名空间及声明头 XML的使用建议 XML是一种很常见的数据保存方式,我经常用它来保存一些数据,或者是一些配置参数. 使用C#,我