cv::Mat 类是用于保存图像以及其他矩阵数据的数据结构。默认情况下,它们的尺寸为0,但是也可以指定初始尺寸。
cv::Mat ima(240,320,CV_8U,cv::Scalar(100));
同时指定矩阵中元素的类型,这里的CV_8U对应的是单字节的像素图像。字母U意味着无符号的(Unsigned)。也可以使用字母S声明带符号的类型。
对于彩色图像,需要指定三个通道(CV_8UC3).也可以声明16位或32位的整数图像,或者浮点数图像。
当CV::Mat对象离开作用域后,分配的内存将被自动释放,防止内存溢出。
cv::Mat还实现了引用计数以及浅拷贝,当图像之间进行赋值时,图像数据并没有发生赋值,两个对象都指向同一块的内存块。这也可用于参数传值的图像,以及返回值传值的图像。引用计数的作用是只有当所有引用内存数据的对象都被析构后,才会释放内存块。
可以使用copyTo()方法。
cv::Mat image2,image3;
image2 = result;
result.copyTo(image3);
//创建新的对象实现拷贝
cv::Mat function()
//作为类 返回图像 自动实现浅拷贝
{
cv::Mat ima(240,320,CV_8U,cv::Scalar(100));
return ima;
}
//得到灰度图
cv::Mat gray = function();
gray变量将包含函数中创建的图像内容,而不涉及额外的内存分配。
然而在类中使用时需要谨慎,避免返回类中的图像成员
class Test{
cv::Mat ima;
public:
//构造函数
Test() :ima(240,320,CV_8U,cv::Scalar(100))
//初始化
{}
cv::Mat method()
{return ima;}
如果调用了类的这个方法,返回值和属性共用一块内存,这样修改返回值时很容易引起对象属性的改变,影响侯敏的计算。
所以应该对返回的变量进行深拷贝。
第二章
操作像素
存取像素值
为了存取矩阵元素,需要在代码中指定元素所在的行和列。程序会返回相应的元素。如果图像时单通道的,返回值时单个数值,如果图像时多通道的,返回值则是一组向量。
椒盐噪点:随机的将部分像素设置为白色或者黑色。在传输过程中,如果部分像素值丢失,那么这种早点就会出现。
首先对一个图像随机修改一些白色像素点。
void salt(cv::Mat &image, int n)
{
for(int k=0; k<n ;k++)
{
rand()
//生成随机数
int i = rand()%image.cols;
int j = rand()%image.rows;
if(image.channels() == 1)
//判断是否为灰度图
{
image.at<uchar>(j,i) = 255;
}
else if(image.channels() == 3)
//判断是否为彩色图
{
image.at<cv::Vec3b>(j,i)[0] = 255;
image.at<cv::Vec3b>(j,i)[1] = 255;
image.at<cv::Vec3b>(j,i)[2] = 255;
//对于彩色图像需要将每个通道的值都设为白色,才能得到一个白色像素
}
}
}
//打开图像
cv::Mat image = cv::imread("boldt.jpg");
salt(image,3000);
cv::namedWindow("image");
cv::imshow("image",image);
类cv::Mat 有若干成员函数可以获取图像的属性,公有属性cols和rows给出了图像的宽和高。
成员函数at(inty,intx)可以用来存储图像元素。但是必须在编译期已知图像的类型。
因为cv::Mat可以存放任意数据类型的元素。
所以需要先指定数据类型
image.at<uchar>(j,i) = 255;
at方法不会进行任何数据类型的转换。
opencv将彩色图像类向量定义为cv::Vec3b,即由三个unsigned char的向量组成。
使用指针遍历图像
使用双重循环遍历所有的像素值
void colorReduce(cv::Mat &image,int div=64)
{
int nl = image.rows;
int nc = image.cols*image.channels();
for(int j=0;j<nl;j++)
{
//得到第j行的首地址
uchar* data = image.ptr<uchar>(j);
for(int i = 0;i < nc;i++)
{
data[i] = data[i]/div*div + div/2;
}
}
}
//主函数
image = cv::imread("boldt.jpg");
colorReduce(image);
cv::namedWindow("Image");
cv::imshow("Image",image);
cv::Mat中的ptr函数可以得到图像任意行的首地址。
可以等效的使用指针运算从一列移动到下一列。
*data++ = *data/div*div + div/2;
其它的颜色缩减公式
也可以通过模运算来计算最接近被除数的除数(缩减因子,div)的整数倍数。
data[i]= data[i] - data[i]%div + div/2;
但是这个计算方式会偏慢,因为它需要存取每个像素两次。
另一个选择是使用位运算。如果我们限制缩减因子为2的幂次,即div = pow(2,n), 那么,只取像素值的前n位即可得到不大于该值的关于缩减因子的最大整数倍数。运算掩模可以通过简单的移位操作得到。
uchar mask = 0xFF<<n;
//for div = 16, mask = 0xF0;
颜色缩减的计算
data[i] = (data[i]&mask) + div/2;
通常而言,位运算非常搞笑,所以在需要考虑效率的情况下位运算是一个强大的备选方式。
使用输入和输出参数
颜色变换在上文中,是直接作用于输入图像的,我们称之为inplace变换,这种方式不需要额外的图像来保存输出结果。
但是有些时候不希望改变原图像,所以调用之前应当创建原始图像的拷贝。
最简单的深拷贝就是使用clone函数。
image = cv::imread("boldt.jpg");
cv::Mat imageClone = image.clone();
colorReduce(imageClone);
cv::namedWindow("Image result");
cv::imshow("Image result", imageClone);
在具体实现过程中,可以给用户选择是否采用in-place的处理方式。函数的实现如下。
void colorReduce(const cv::Mat &image,
cv::Mat &result,int div = 64);
这里输入图像是通过常量传递的,函数不会修改原图像。
但是当选择In-place的处理方式时,用户可以将输入输出指定为同一个变量:
coloeReduce(image,image);
否则,用户必须提供另一个cv::Mat实例
cv::Mat result;
colorReduce(image,result);
注意:
这里必须检查输入和输出图像的大小和元素数据类型是否一致。
cv::Mat成员函数create内置了这个检查操作。
如果需要新的尺寸和数据类型对一个矩阵进行重新分配,那么我们就可以调用create成员函数。而且,如果新指定的尺寸和数据类型与原有的一样,create函数会直接返回。
首先,利用create来创建一个与输入图像的尺寸和类型相同的矩阵:
result.create(image.rows,image.cols,image.type());
注意:create函数创建的图像的内存都是自连续的,create函数不会对图像的行进行填补。分配的内存大小为total()*elemSize()。循环使用两个指针完成。
for(int j = 0;j<nl,j++)
{
const uchar*data_in = image.ptr<uchar>(j);
//获取第j行的首地址
uchar* data_out = result.ptr<uchar>(j);
for(int i = 0;i < nc; i++)
{
data_out[i] = data_in[i]/div*div
+ div/2;
}
}
高效遍历连续图像
考虑到效率 图像有可能会在行尾扩大若干个像素,但是当不对图像进行填补时,图像可以被视为一个一维数组,可以通过cv::Mat的isContinuous方法来判断这个图像是否进行了填补。如果isContinuous方法返回值为真的话,说明没有进行过填补。
在一些图像处理算法中,我们可以利用图像的连续性,把整个处理过程使用一个循环完成。颜色缩减函数可以重写为
void colorReduce(cv::Mat &image, int div = 64)
{
int nl = image.rows;
int nc = image.cols*image.channels();
if(image.isContinous())
//没有额外的填补像素
{
nc = nc*nl;
nl = 1;
//是一个一维数组,将图片展平
}
//对于连续图像,本循环只执行一次
for(int j = 0;j < nl;j++)
{
uchar*data = image.ptr<uchar>(j);
for(int i = 0;i < nc;i++)
{
data[i] = data[i]/div*div + div/2;
}
}
}
当通过isContinuous得知图像没有对行进行填补后,就可以将宽设置为1,高度设置为W×H,从而消除外层循环。
注意:也可以使用reshape方法来重写这段代码。
if(image.isContinous())
{
//确定没有填补
image.reshape(1,image.cols*image.rows);
}
int nl = image.rows;
int nc = image.cols*umage.channels();
reshape不需要内存拷贝或者重新分配就能改变矩阵的维度。两个参数分别为新的通道数和新的行数,矩阵的列数可以根据新的通道数和行数来自适应。
在这些实现中,内层循环依次处理图像的全部像素。这个方法在同时处理若干个小图像时会很有优势。
底层指针运算
在类cv::Mat中,图像数据以unsigned char的形式保存在一块内存中,这块内存的首地址可以通过data成员变量得到。data是一个unsigned char型的指针,所以循环可以以如下方式开始。
uchar* data = image.data;
从当前行到下一行可以通过对指针加上行宽完成。
data += image.step;
step代表图像的行宽(包括填补像素)。
通常可以通过如下方式获取第j行第i列的像素的地址
data = image.data + j*image.step + i*image.elemszie();
//或
data = &image.at(j,i);
但是这种方法容易出错,且不适用于待遇感兴趣取余的图像。
使用迭代器遍历图像
一个cv::Mat实例的迭代器可以通过创建一个cv::Mat Iterator_的实例来得到。下划线意味着它是一个模板类。
因为通过迭代器来存取图像的元素,必须在编译期知道图像元素的数据类型。
cv::Mat Iterator_<cv::Vec3b> it;
也可以使用定位在Mat_内部的迭代器类型:
cv::Mat_<cv::Vec3b>::iterator it;
本文主要介绍了OpenCV中cv::Mat类,它用于保存图像及矩阵数据,能自动释放内存,实现了引用计数和浅拷贝。还阐述了操作像素的方法,如存取像素值、使用指针或迭代器遍历图像,以及颜色缩减的计算方式,同时提到了图像连续性处理和深拷贝等内容。
868

被折叠的 条评论
为什么被折叠?



