图像像素访问
- 1、测试用例
- 1.1、颜色缩减算法
- 1.2、颜色缩减示例
- 2、图像矩阵的存储与访问
- 2.1、图像的存储方式
- 2.2、图像的访问方式
- 2.2.1、C 指针:高效的访问方式
- 2.2.2、迭代器:安全的访问方式
- 2.2.3、行列索引:动态计算地址
- 2.2.4、LUT 查询函数
- 2.3、访问性能对比
- 3、参考资源
1、测试用例
1.1、颜色缩减算法
让我们考虑一个简单的颜色缩减方法(color reduction method)。通过使用无符号字符 C 和 C + + 类型来存储矩阵元素,一个像素通道最多可以有256个不同的值。对于一个三通道图像,可以组合成1600万多种颜色。使用如此多的色调可能会给我们的算法性能带来沉重的负担。然而,有时候只要少用一点点就能得到相同的最终结果。
在这种情况下,我们通常做一个颜色空间缩减。这意味着我们将颜色空间当前值除以一个新的输入值,以得到更少的颜色。例如,新的值【0】代替(0~ 9)之间的每个值,新的值【10】都接受(10~19)之间的每个值接受10,以此类推。
当你用一个 int 值除一个 uchar (无符号 char,值在0~255之间)值时,结果也是 char。这些值可能只是字符值。因此,任何分数都将被四舍五入。利用这个事实,uchar 域中的操作可以表示为
I
n
e
w
=
(
I
o
l
d
10
)
∗
10
I_{new}=(\frac{I_{old}}{10})*10
Inew=(10Iold)∗10
一个简单的颜色空间缩减算法包括:访问图像矩阵的每个像素和应用这个公式。值得注意的是,我们执行了除法和乘法运算。对于一个系统来说,这些操作是非常昂贵的。如果可能的话,值得通过使用更高效的操作来避免它们,比如减法、加法,或者在最好的情况下使用简单的赋值。此外,请注意,对于上面的操作,我们只有有限数量的输入值。在 【uchar】系统中,这是【256】。
因此,对于较大的图像,明智的做法是事先计算所有可能的值,并在赋值期间使用查找表(lookup table)进行赋值。查找表是简单的数组(具有一个或多个维度) ,对于给定的输入值变量,它保存最终的输出值。它的优点是,我们不需要进行计算,我们只需要获取结果。
1.2、颜色缩减示例
我们的测试用例程序(以及下面的代码示例)将执行以下操作:(1)读取作为命令行参数传递的图像(它可以是彩色或灰度) ;(2)获取给命令行参数中的整数值;(3)应用颜色缩减算法。在 OpenCV 中,目前有三种主要的方式来逐个像素地访问图像。为了让事情变得更有趣,我们将使用这些方法中的每一种来扫描图像,并打印出所花费的时间。
./how_to_scan_images <imageNameToUse> <divideWith> [G]
最后一个命令行参数(G)是可选的,如果没有该参数默认处理彩色图,否则将图像转为灰度图处理。首先,我们需要计算查询表,代码片段如下:
int divideWith = 0; // convert our input string to number - C++ style
stringstream s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
2、图像矩阵的存储与访问
2.1、图像的存储方式
正如你已经在我的 Mat-The Basic Image Container 教程中读到的,矩阵的大小取决于所使用的颜色系统。更准确地说,它取决于所使用的通道的数量。对于灰度图像,我们有类似于下图所示,
对于多通道图像,每一列包含的子列与通道数目一样多。例如,在 BGR 颜色系统的情况下,像素分布如下图所示,比如【0,0】位置包含三个子列,对应BGR通道,
注意,通道的顺序是相反的:BGR 而不是 RGB。因为在许多情况下,内存足够大,可以以连续的方式存储行,从而创建一个单独的长行(long row)。因此,所有的像素都一个接一个存于同一片内存区域,这有助于加快像素的访问速度。我们可以使用 cv::Mat::isContinure()
函数来询问像素在内存中是否连续存储。
2.2、图像的访问方式
2.2.1、C 指针:高效的访问方式
在性能方面,最快的仍是经典的 C 样式 operator[] (pointer)
访问。因此,我们推荐的最高效的方法如下:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// 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())
{
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]];
}
}
return I;
}
这里我们基本上只是获取一个指向每行开始的指针,然后遍历它直到结束。在矩阵以连续方式存储的特殊情况下,我们只需要请求指针一次,就可以一直到结束,否则我们需要获取多次行首指针。对于彩色图像:我们有三个通道,所以我们需要在每一行移动指针三倍的次数。
2.2.2、迭代器:安全的访问方式
为了提高效率,确保通过恰当数量的 uchar 字段并跳过行之间可能出现的空白(意味着内存不连续)。迭代器方法被认为是一种更安全的方法,因为它从用户那里接管了这些任务。所有您需要做的就是获取图像矩阵的开始和结束位置,然后只需累加开始迭代器直到结束。要获取迭代器指示位置的值,可以使用 * 操作符(在它之前添加它),具体代码如下:
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:
{
// 迭代器访问图像矩阵:取出矩阵开始和结束的指针即可
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;
}
对于彩色图像,每列有三个 uchar 元素,这可能被认为是 uchar 元素的一个简短3元素长度的向量,它已经在 OpenCV 中以 Vec3b 名称命名(赋予具体的数据类型)。要访问第 n 个子列,我们使用简单的 operator[] 访问。重要的是,OpenCV 迭代器遍历列并自动跳到下一行。因此,在彩色图像的情况下,如果使用一个简单的 uchar 迭代器,能够只访问蓝色通道值。
涉及知识点:
- MatIterator_:CV的迭代器;
- 获取矩阵开始和结束的指针:
I.begin()<Vec3b>
,I.end()<Vec3b>
;- 获取具体的值:指针位置前添加星号,( ∗ i t *it ∗it)[];
- Vec3b的数据定义,如下图,它可以存储3个uchar 值,
2.2.3、行列索引:动态计算地址
最后一种方法不推荐用于访问图像。它被用来获取或修改图像中的随机元素。它的基本用法是指定要访问元素的行号和列号。在我们早期的像素访问的方法中,很重要的一点是要明确要访问图像的数据类型。这里没有什么不同,因为您需要在自动查找中手动指定要使用的类型。对于以下源代码(cv::Mat::at ()
函数的用法)的灰度图像,可以观察到这一点:
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_<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.at<Vec3b>(i,j)[0],需要指定数据类型
_I(i,j)[1] = table[_I(i,j)[1]]; // 或者 _I.at<Vec3b>(i,j)[1]
_I(i,j)[2] = table[_I(i,j)[2]]; // 或者 _I.at<Vec3b>(i,j)[2]
}
I = _I;
break;
}
}
return I;
}
补充知识点:
- Mat_ 继承 Mat,包含了一些特殊的处理方法,具体描述如下图
- 类 Mat _ < _ Tp > 是 Mat 类之上的一个简化版模板包装器。它没有任何额外的数据字段。这个类和 Mat 都没有任何虚方法。因此,对这两个类的引用或指针可以自由地相互转换,但也需注意一下具体写法。参考下面的代码片段,
// 创建 100x100 8-bit 矩阵
Mat M(100,100,CV_8U);
// this will be compiled fine. no any data conversion will be done.
Mat_<float>& M1 = (Mat_<float>&)M;
- 多通道图像或矩阵使用Mat_,传递 Vec 作为 Mat_ 参数,举例如下
// 创建 320x240 彩色图像,填充为绿色的值
Mat_<Vec3b> img(240, 320, Vec3b(0,255,0));
// 对角的像素值改为白色
for(int i = 0; i < 100; i++)
img(i,i)=Vec3b(255,255,255);
2.2.4、LUT 查询函数
这是在图像中实现查找表修改的一种额外方法。在图像处理中,通常需要将所有给定的图像值修改为其他值。OpenCV 提供了一个修改图像值的函数,无需编写图像的访问逻辑。我们使用核心模块的cv::LUT()
函数。首先,我们构建一个 Mat
类型的查找表
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
最后调用函数(I 是输入图像,J 是输出图像) :
LUT(I, lookUpTable, J);
2.3、访问性能对比
图像大小为 512x512,循环运行1000次,平均每次运行时间如下图:
3、参考资源
- How to scan images, lookup tables and time measurement with OpenCV
- 官方示例代码文件