1 图像在内存之中的存储方式
在之前的章节中,我们已经了解到图像矩阵的大小取决于所用的颜色模型,确切地说,取决于所用通道数。如果是灰度图像,矩阵就会如下图所示。
而对多通道图像来说,矩阵中的列会包含多个子列,其子列个数与通道数相等。例如,如下图所示RGB颜色模型的矩阵。
可以看到,OpenCV中子列的通道顺序是反过来的—BGR而不是RGB。很多情况下,因为内存足够大,可实现连续存储,因此,图像中的各行就能一行行地连接起来,形成一个长行。连续存储有助于提升图像扫描速度,我们可以使用
isContinuous()
来判断矩阵是否是连续存储的。相关示例会在接下来的内容中提供。
2 颜色空间缩减
我们知道,若矩阵元素存储的是单通道像素,使用C或C++的无符号字符类型,那么像素可有256个不同值。但若是三通道图像,这种存储格式的颜色数就太多了(确切地说,有一千六百多万种)。用如此之多的颜色来进行处理,可能会对我们的算法性能造成严重影响。
其实,仅用这些颜色中具有代表性的很小的部分,就足以达到同样的效果。
如此,颜色空间缩减(color space reduction)便可以派上用场了,它在很多应用中可以大大降低运算复杂度。颜色空间缩减的做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。也就是“做减法”,比如颜色值0到9可取为新值0,10到19可取为10,以此类推。
如uchar类型的三通道图像,每个通道取值可以是0~255,于是就有256x256个不同的值。我们可以定义:
0~9范围的像素值为0;
10~19范围的像素值为10;
20~29范围的像素值为20。
这样的操作将颜色取值降低为26x26x26种情况。这个操作可以用一个简单的公式来实现。因为C++中 int 类型除法操作会自动截余。例如:
Iold=14;
old/10)*10=(14/10)*10=1*10=10;
uchar(无符号字符,即0到255之间取值的数)类型的值除以int值,结果仍是char。因为结果是char类型的,所以求出来小数也要向下取整。利用这一点,刚才提到在uchar 定义域中进行的颜色缩减运算就可以表达为下面的形式:
因为C++中int类型除法操作会自动截余。在处理图像像素时,每个像素需要进行一遍上述计算,也需要一定的时间花销。但我们注意到其实只有0~255种像素,即只有256种情况。进一步可以把256种计算好的结果提前存在表中 table 中,这样每种情况不需计算,直接从table中取结果即可。
int dividewith=10;
uchar table[256];
for (int i = 0; i < 256; ++1)
table[i] =divideWith * (i / divideWith);
于是 table[i]存放的是值为i的像素减小颜色空间的结果,这样也就可以理解上述方法中的操作:
p[j] = table[p[j]];
这样,简单的颜色空间缩减算法就可由下面两步组成:
(1)遍历图像矩阵的每一个像素;
(2)对像素应用上述公式。
值得注意的是,我们这里用到了除法和乘法运算,而这两种运算又特别费时,所以,应尽可能用代价较低的加、减、赋值等运算来替换它们。此外,还应注意到,上述运算的输入仅能在某个有限范围内取值,如uchar类型可取256个值。
由此可知,对于较大的图像,有效的方法是预先计算所有可能的值,然后需要这些值的时候,利用查找表直接赋值即可。查找表是一维或多维数组,存储了不同输入值所对应的输出值,其优势在于只需读取、无须计算。
3 访问图像中像素的三类方法
任何图像处理算法,都是从操作每个像素开始的。即使我们不会使用OpenCV提供的各种图像处理函数,只要了解了图像处理算法的基本原理,也可以写出具有相同功能的程序。在OpenCV中,提供了三种访问每个像素的方法。
- 方法一 指针访问:C操作符[];
- 方法二迭代器 iterator:
- 方法三 动态地址计算。
这三种方法在访问速度上略有差异。debug 模式下,这种差异非常明显,不过在release模式下,这种差异就不太明显了。我们通过一组程序来说明这几种方法。程序的目的是减少图像中颜色的数量,比如原来的图像是是256种颜色,我们希望将它变成64种颜色,那只需要将原来的颜色除以4(整除)以后再乘以4就可以了。
3.1 用指针访问像素
用指针访问像素的这种方法利用的是C语言中的操作符[]。这种方法最快,但是略有点抽象。实验条件下单次运行时间为0.00665378。范例代码如下。
void colorReduce(Mat& inputImage, Mat& outputImage, int div)
{
//参数准备
outputImage = inputImage.clone(); //拷贝实参到临时变量
int rowNumber = outputImage.rows; //行数
int colNumber = outputImage.cols*outputImage.channels(); //列数 x 通道数=每一行元素的个数
//双重循环,遍历所有的像素值
for(int i = 0;i < rowNumber;i++) //行循环
{
uchar* data = outputImage.ptr<uchar>(i); //获取第i行的首地址
for(int j = 0;j < colNumber;j++) //列循环
{
// ---------【开始处理每个像素】-------------
data[j] = data[j]/div*div + div/2;
// ----------【处理结束】---------------------
} //行处理结束
}
}
Mat类有若干成员函数可以获取图像的属性。公有成员变量cols和rows给出了图像的宽和高,而成员函数channels0用于返回图像的通道数。灰度图的通道数为1,彩色图的通道数为3。
每行的像素值由以下语句得到:
int colNumber=outputImage.cols*outputImage.channels;//列数x通道数=每一行元素的个数
为了简化指针运算,Mat类提供了ptr函数可以得到图像任意行的首地址。ptr是一个模板函数,它返回第i行的首地址:
uchar* data = outputImage.ptr(i);//获取第i行的首地址
而双层循环内部的那句处理像素的代码,我们可以等效地使用指针运算从一列移动到下一列。所以,也可以这样来写:
data++=*data/div*div+div/2;
3.2 用迭代器操作像素
第二种方法为用迭代器操作像素,这种方法与STL库的用法类似。
在迭代法中,我们所需要做的仅仅是获得图像矩阵的 begin和end,然后增加迭代直至从begin到end。将*操作符添加在迭代指针前,即可访问当前指向的内容。
相比用指针直接访问可能出现越界问题,迭代器绝对是非常安全的方法。
void colorReduce(Mat& inputImage, Mat& outputImage, int div)
{
//参数准备
outputImage = inputImage.clone(); //拷贝实参到临时变量
//获取迭代器
Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>(); //初始位置的迭代器
Mat_<Vec3b>::iterator itend = outputImage.end<Vec3b>(); //终止位置的迭代器
//存取彩色图像像素
for(;it != itend;++it)
{
// ------------------------【开始处理每个像素】--------------------
(*it)[0] = (*it)[0]/div*div + div/2;
(*it)[1] = (*it)[1]/div*div + div/2;
(*it)[2] = (*it)[2]/div*div + div/2;
// ------------------------【处理结束】----------------------------
}
}
实验条件下单次运行时间为0.242588。
不熟悉面向对象编程中迭代器的概念的读者,可以阅读与STL中迭代器相关的入门书籍和文字。用关键字“STL 迭代器”进行搜索可以找到各种相关的博文和资料。
3.3 动态地址计算
第三种方法为用动态地址计算来操作像素。下面是使用动态地址运算配合at方法的 colorReduce函数的代码。这种方法简洁明了,符合大家对像素的直观认识。实验条件下单次运行时间约为0.334131。
void colorReduce(Mat& inputImage, Mat& outputImage, int div)
{
//参数准备
outputImage = inputImage.clone(); //拷贝实参到临时变量
int rowNumber = outputImage.rows; //行数
int colNumber = outputImage.cols; //列数
//存取彩色图像像素
for(int i = 0;i < rowNumber;i++)
{
for(int j = 0;j < colNumber;j++)
{
// ------------------------【开始处理每个像素】--------------------
outputImage.at<Vec3b>(i,j)[0] = outputImage.at<Vec3b>(i,j)[0]/div*div + div/2; //蓝色通道
outputImage.at<Vec3b>(i,j)[1] = outputImage.at<Vec3b>(i,j)[1]/div*div + div/2; //绿色通道
outputImage.at<Vec3b>(i,j)[2] = outputImage.at<Vec3b>(i,j)[2]/div*div + div/2; //红是通道
// -------------------------【处理结束】----------------------------
} // 行处理结束
}
}
让我们讲解一下上述的代码。
Mat类中的cols和rows给出了图像的宽和高。而成员函数at(int y, int x)可以用来存取图像元素,但是必须在编译期知道图像的数据类型。需要注意的是,我们一定要确保指定的数据类型要和矩阵中的数据类型相符合,因为at方法本身不会对任何数据类型进行转换。
对于彩色图像,每个像素由三个部分构成:蓝色通道、绿色通道和红色通道(BGR)。因此,对于一个包含彩色图像的Mat,会返回一个由三个8位数组成的向量。OpenCV将此类型的向量定义为Vec3b,即由三个 unsigned char 组成的向 量。这也解释了为什么存取彩色图像像素的代码可以写出如下形式:
image.at<Vec3b>(j,i)[channel]=value;
其中,索引值channel标明了颜色通道号。
另外需要再次提醒大家的是,OpenCV中的彩色图像不是以RGB的顺序存放的,而是BGR,所以程序中的outputlmage e.at(ij)[0]代表的是该点的B分 量。同理还有(*it)[0]。
标签:Mat,int,像素,图像处理,图像,div,outputImage From: https://blog.51cto.com/u_11745691/6100450