IplImage是OpenCV中CxCore部分基础的数据结构,用来表示图像,其中Ipl是Intel Image Processing Library的简写。以下是IplImage的结构分析。参见:OpenCV中文网站
typedef struct _IplImage{int nSize; /* IplImage大小 */int ID; /* 版本 (=0)*/int nChannels; /* 大多数OPENCV函数支持1,2,3 或 4 个通道 */int alphaChannel; /* 被OpenCV忽略 */int depth; /* 像素的位深度: IPL_DEPTH_8U, IPL_DEPTH_8S, IPL_DEPTH_16U,IPL_DEPTH_16S, IPL_DEPTH_32S, IPL_DEPTH_32F and IPL_DEPTH_64F 可支持 */char colorModel[4]; /* 被OpenCV忽略 */char channelSeq[4]; /* 同上 */int dataOrder; /* 0 - 交叉存取颜色通道, 1 - 分开的颜色通道.cvCreateImage只能创建交叉存取图像 */int origin; /* 0 - 顶—左结构,1 - 底—左结构 (Windows bitmaps 风格) */int align; /* 图像行排列 (4 or 8). OpenCV 忽略它,使用 widthStep 代替 */int width; /* 图像宽像素数 */int height; /* 图像高像素数*/struct _IplROI *roi;/* 图像感兴趣区域. 当该值非空只对该区域进行处理 */struct _IplImage *maskROI; /* 在 OpenCV中必须置NULL */void *imageId; /* 同上*/struct _IplTileInfo *tileInfo; /*同上*/int imageSize; /* 图像数据大小(在交叉存取格式下imageSize=image->height*image->widthStep),单位字节*/char *imageData; /* 指向排列的图像数据 */int widthStep; /* 排列的图像行大小,以字节为单位 */int BorderMode[4]; /* 边际结束模式, 被OpenCV忽略 */int BorderConst[4]; /* 同上 */char *imageDataOrigin; /* 指针指向一个不同的图像数据结构(不是必须排列的),是为了纠正图像内存分配准备的 */}IplImage;
对我们来说比较重要的两个元素是:char *imageData
以及widthStep
。imageData存放图像像素数据,而widStep类似CvMat中的step,表示以字节为单位的行数据长度
。
一个m*n的单通道
字节型图像,其imageData排列如下:
如果我们要遍历图像中的元素,只需:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
uchar* tmp;
for(int i=0;iheight;i++)for(int j=0;jwidth;j++)*tmp=((uchar *)(img->imageData + i*img->widthStep))[j];
这种直接访问的方法速度快,但容易出错,我们可以通过定义指针来访问。即:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
ucha* data=(uchar *)img->imageData;
int step = img->widthStep/sizeof(uchar);
uchar* tmp;
for(int i=0;iheight;i++)for(int j=0;jwidth;j++)*tmp=data[i*step+j];
而多通道(三通道)字节图像中,imageData排列如下:
//IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,3);
uchar* data=(uchar *)img->imageData;
int step = img->widthStep/sizeof(uchar);
int channels = img->nChannels;
uchar *b,*g,*r;
for(int i=0;iheight;i++)for(int j=0;jwidth;j++){*b=data[i*step+j*chanels+0];*g=data[i*step+j*chanels+1];*r=data[i*step+j*chanels+2];}
如果要修改某像素值,则直接赋值。
cvGet*D系列函数可以用来返回特定位置的数组元素(一般使用cvGet2D),原型如下:
CvScalar cvGet1D( const CvArr* arr, int idx0 );
CvScalar cvGet2D( const CvArr* arr, int idx0, int idx1 );
CvScalar cvGet3D( const CvArr* arr, int idx0, int idx1, int idx2 );
CvScalar cvGetND( const CvArr* arr, int* idx );
idx0,idx1,idx2分别用来指示元素数组下标,即cvGet2D返回(idx0,idx1)处元素的值。因此,单通道图像像素访问方式如下:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
double tmp;
for(int i=0;iheight;i++)for(int j=0;jwidth;j++)tmp=cvGet2D(img,i,j).val[0];
多通道字节型/浮点型图像:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_32F,3);
double tmpb,tmpg,bmpr;
for(int i=0;iheight;i++)for(int j=0;jwidth;j++){tmpb=cvGet2D(img,i,j).val[0];tmpg=cvGet2D(img,i,j).val[1];tmpr=cvGet2D(img,i,j).val[2];}
如果是修改元素的值,可用cvSet*D(一般是cvSet2D)函数:
void cvSet1D( CvArr* arr, int idx0, CvScalar value );
void cvSet2D( CvArr* arr, int idx0, int idx1, CvScalar value );
void cvSet3D( CvArr* arr, int idx0, int idx1, int idx2, CvScalar value );
void cvSetND( CvArr* arr, int* idx, CvScalar value );
这种方法对于任何图像的访问方式是一样的,比较简单,但效率较低,不推荐使用。
Mat其实就是matrix(矩阵)的缩写,在opencv中,我们用Mat类的对象存储图像。
在opencv中,Mat类分为两个部分。
图像有很多
属性
。如:大小,宽和高,数据类型,通道数。这些数据存储在矩阵头中
Mat和Matlab里的数组格式有点像,但一般是二维向量,如果是灰度图
,一般存放
类型;如果是RGB
彩色图,存放
类型。
单通道灰度图数据存放格式
多通道的图像中,每列并列存放通道数量的子列,如RGB三通道彩色图:
注意通道的顺序反转了:BGR
。通常情况内存足够大的话图像的每一行是连续存放的,也就是在内存上图像的所有数据存放成一行,这中情况在访问时可以提供很大方便。可以用 isContinuous()
函数来判断图像数组是否为连续的。
属性部分存储了一系列的矩阵属性:行数、列数、通道数、数据类型、矩阵数据的大小等,以及指向了矩阵数据的指针等。
cols
:矩阵列数rows
:矩阵行数channels
:通道数type
:数据类型total
:矩阵总元素数data
:指向矩阵数据块的指针表示了矩阵中元素的类型以及矩阵的通道个数,它是一系列的预定义的变量,其命名规则为CV_(位数)+(数据类型)+(通道数)。具体的有以下值:
这里U
(unsigned integer)表示的是无符号整数,S
(signed integer),F(float)是浮点数。
例如:CV_16UC2: 表示的是元素类型是一个16位的无符号整数,通道为2. C1,C2,C3,C4则表示通道是1,2,3,4
type一般是在创建Mat对象时设定,如果要取得Mat的元素类型,则无需使用type,使用下面的depth
矩阵中元素的一个通道的数据类型,这个值和type是相关的。例如 type为 CV_16SC2,一个2通道的16位的有符号整数。那么,depth则是CV_16S。depth也是一系列的预定义值:
将type的预定义值去掉通道信息就是depth值:CV_8U CV_8S CV_16U CV_16S CV_32S CV_32F CV_64F
矩阵一个元素占用的字节数,例如:type是CV_16SC3
,那么elemSize = 3 * 16 / 8 = 6 bytes
矩阵元素一个通道
占用的字节数,例如:type是CV_16CS3,那么elemSize1 = 16 / 8 = 2 bytes = elemSize / channels
> 1. Mat ()
> 2. Mat (int rows, int cols, int type)
> 3. Mat (Size size, int type)
> 4. Mat (int rows, int cols, int type, const Scalar &s)
> 5. Mat (Size size, int type, const Scalar &s)
> 6. Mat (int ndims, const int *sizes, int type)
> 7. Mat (int ndims, const int *sizes, int type, const Scalar &s)
无参数
构造函数,创建Mat对象Mat image = Mat();
image.create(4, 4, CV_8UC3);//创建一个4x4大小的像素块,每个像素都是三通道每个通道的位数都是8位
行、列、类型
这个三个参数的构造函数创建Mat对象Mat m = Mat(4, 4, CV_8UC3); //创建一个4x4大小的像素块,每个像素都是三通道每个通道的位数都是8位
行、列、类型、Scalar向量
四个参数的构造函数创建Mat对象Mat m = Mat(4, 4, CV_8UC3, Scalar(0, 255, 255));
//创建一个4x4大小的像素块,每个像素都是三通道每个通道的位数都是8位,指定三通道颜色值向量Scalar(0, 255, 255)
同样表示创建一个4x4的像素块,唯一的区别是颜色不是默认值,而是我们指定的三通道颜色值向量Scalar(0, 255, 255)。其中Scalar向量数目永远是等于通道数目
。其他赋值情况也一样
大小、类型
个参数的构造函数创建Mat对象。Mat m = Mat(Size(4, 4), CV_8UC3); //创建一个4x4大小的像素块,每个像素都是三通道每个通道的位数都是8位
大小、类型、Scalar向量
三个参数的构造函数创建Mat对象Mat m = Mat(Size(4, 4), CV_8UC3, Scalar(255, 0, 0)); //创建一个4x4大小的像素块,每个像素都是三通道每个通道的位数都是8位
zeros(),eye(), ones()
创建对象Mat img = zeros(100,100,CV_8UC3); // 全0矩阵
Mat img1 = eye(100,100,CV_8UC3); // 对角为1的对角矩阵
Mat img2 = ones(100,100,CV_8UC3); // 全1矩阵
逗号数组
创建对象Mat img = (Mat_(2,2) << 0,1,1,0); // 按行填充
Mat A = (Mat_(4, 4) <<0.5, 0.4, 0.6, 1,0.2, 0.3, 0.1, 2,0.7, 0.8, 0.9, 3,0, 0, 0, 1);
Mat对象创建,常用的是创建空白图像。如下演示了三种
Mat src = imread("……");
Mat m4 = Mat::zeros(src.size(),src.type())Mat m5 = Mat::zeros(Size(512,512),CV_8UC3);Mat m6 = Mat::ones(Size(512,512),CV_8UC3);Mat kernel = (Mat_(3,3)<<0,-1,0,-1,5,-1,0,-1,0); 位数,只有灰度
当对图像直接赋值,如果为多通道时:
Mat m3 = Mat::zeros(Size(8, 8), CV_8UC3);
m3 = 145;
结果如下:
只有第一个通道的值被赋值了
正确的写法如下:
Mat m3 = Mat::zeros(Size(8, 8), CV_8UC3);
m3 = Scalar(129,39,0);
uchar
:灰度图像像素,单个值
Vec3b
:彩色图像像素,3个值
单个像素的访问:pixel = image.at
c++中像素遍历和读写:
void pixel_visit_Demo(Mat& image) {int h = image.rows;int w = image.cols;int dims = image.channels();for (int row = 0; row < h; row++) {for (int col = 0; col < w; col++) {if (dims == 1) {//灰度图像int pv = image.at(row, col);image.at(row, col) = 255 - pv;}if (dims == 3) {//彩色图像Vec3b bgr = image.at(row, col);image.at(row, col)[0] = 255 - bgr[0];image.at(row, col)[1] = 255 - bgr[1];image.at(row, col)[2] = 255 - bgr[2];}}}
}
void pixel_visit_Demo(Mat& image) {int h = image.rows;int w = image.cols;int dims = image.channels();for (int row = 0; row < h; row++) {uchar* curren_row = image.ptr(row);for (int col = 0; col < w; col++) {if (dims == 1) {//灰度图像int pv = *curren_row;*curren_row++ = 255 - pv;}if (dims == 3) {//彩色图像*curren_row++ = 255 - *curren_row; // b*curren_row++ = 255 - *curren_row; // g*curren_row++ = 255 - *curren_row; / r}}}
}
对于彩色图,对单个的像素点后移通道数量次数即可,如上++了三次。
再开始将拷贝之前,先给大家分享一下浅拷贝和深拷贝
浅拷贝
:拷贝对象和被拷贝对象都指向同一个内存空间
,修改任何一个对象的数据都会影响另外一个;
举个例子:小明和小红在沙漠中共用一个水瓶喝水,任何一个人喝了水,另外一个人都会剩下更少的水。
深拷贝
:拷贝对象和被拷贝对象指向不同的内容空间
,修改数据时互不影响。
举个例子:小明和小红各有一个水瓶,各自喝各自的水对对方不影响。
深拷贝和浅拷贝都各有优缺点:
这种拷贝方式属于浅拷贝
,下面代码中的img和img2都指向相同的内存空间,修改img或者img2,另外一个中的变量也会跟着变化。
Mat img = imread("test.jpg", CV_LOAD_IMAGE_COLOR);
Mat img2(img); // 拷贝构造函数
2 赋值运算符进行拷贝
这种拷贝方式属于浅拷贝
,下面代码中的img和img2都指向相同的内存空间,修改img或者img2,另外一个中的变量也会跟着变化。
Mat img = imread("test.jpg", CV_LOAD_IMAGE_COLOR);
Mat img2 = img; // 赋值运算符
3 使用Rect截取拷贝
这种拷贝方式属于浅拷贝
,下面代码中的img2指向的内存空间为img的子内存空间。
Mat img = imread("test.jpg", CV_LOAD_IMAGE_COLOR);
Mat img2(img, Rect(200,200,300,300));
4 使用clone()函数拷贝
这种拷贝方式属于深拷贝
,img和img2分别指向不同的内存空间,修改img或img2 的数据,不影响另一个变量。
Mat img = imread("test.jpg", CV_LOAD_IMAGE_COLOR);
Mat img2 = img.clone();
5 使用copyTo()函数拷贝
这种拷贝方式属于深拷贝
,img和img2分别指向不同的内存空间,修改img或img2 的数据,不影响另一个变量。
Mat img = imread("test.jpg", CV_LOAD_IMAGE_COLOR);
Mat img2;
img.copyTo(img2);
Mat imageadd = image1 + image2;//imshow("加法", imageadd);Mat imageadd1;Mat imageadd2;add(image1, image2, imageadd1);add(image1, 2, imageadd2);//函数重载//imshow("加法", imageadd);Mat imagesub = image1 - image2;//运算符重载//imshow("减法", imagesub);Mat imageAbsdiff;absdiff(image1, image2, imageAbsdiff);//imshow("减法绝对值", imageAbsdiff);Mat imagesub1;subtract(image1, image2, imagesub1);//imshow("减法", imagesub);Mat imageweighted;addWeighted(image1, 0.5, image2, 0.2, 50, imageweighted);//imshow("加权", imageweighted);Mat imagemultiply;multiply(image1, image2, imagemultiply, 1.0, CV_32FC1);//imshow("点乘", imagemultiply);Mat imagedivide;divide(image1, image2, imagedivide, 1.0, -1);//imshow("点除", imagedivide);//非Mat image2not;Mat mask = Mat::zeros(image2.size(), CV_8UC1);mask(Rect(200, 100, 200, 200)) = 255;bitwise_not(image2, image2not, mask);//imshow("非", image2not);//或Mat image2or;bitwise_or(image2, imagesub, image2or, mask);//imshow("或", image2or);//异或Mat image2xor;bitwise_xor(image2, imageadd2, image2xor, mask);imshow("异或", image2xor);//与Mat image2and;bitwise_and(image2, imagesub, image2and, mask);//imshow("与", image2and);
对于一个Mat,有时需要将其转为二维指针传递
int** mat2ptrarray(Mat& pic)
{int** data;data = new int* [pic.rows];for (int i = 0; i < pic.rows; i++){data[i] = new int[pic.cols];for (int j = 0; j < pic.cols; j++){data[i][j] = pic.at(i, j);}}return data;
}
释放指针指向的内存空间
for (int i = 0; i < pic.rows; i++){delete[] data[i];}delete[] data;
将二维指针的数据再拼回到Mat中(这个是按单通道的例子)
Mat ImgData(int** pImgdata, int width, int height)
{Mat Img;Img.create(height, width, CV_8U);//这里按照uchar类型for (int i = 0; i < height; i++) //行数--高度{for (int j = 0; j < width; j++) //列数 -- 宽度{Img.at(i, j) = pImgdata[i][j];}}return Img;
}
//---------------------数组和Mat------------------------int height = image.rows;int width = image.cols;//转16位一维数组uint8_t* array1 = new uint8_t[height * width * 3];for (int i = 0; i < height; i++){for (int j = 0; j < width; j++){for (int k = 0; k < 3; k++){array1[i * width * 3 + j * 3 + k] = image.at(i, j)[k];} }}Mat image3(height, width, CV_8UC3, array1);