1. IPIImage 使用介绍
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;
1. 1 元素访问
对我们来说比较重要的两个元素是: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;i<img->height;i++)
for(int j=0;j<img->width;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;i<img->height;i++)
for(int j=0;j<img->width;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;i<img->height;i++)
for(int j=0;j<img->width;j++){
*b=data[i*step+j*chanels+0];
*g=data[i*step+j*chanels+1];
*r=data[i*step+j*chanels+2];
}
如果要修改某像素值,则直接赋值。
1.2 使用cvGet2D()函数访问:
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;i<img->height;i++)
for(int j=0;j<img->width;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;i<img->height;i++)
for(int j=0;j<img->width;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 );
这种方法对于任何图像的访问方式是一样的,比较简单,但效率较低,不推荐使用。
2. Mat 使用介绍
2.1 Mat的介绍
Mat其实就是matrix(矩阵)的缩写,在opencv中,我们用Mat类的对象存储图像。
在opencv中,Mat类分为两个部分。
- header部分
图像有很多
属性
。如:大小,宽和高,数据类型,通道数。这些数据存储在矩阵头中
- data部分:放置像素点实际值
Mat和Matlab里的数组格式有点像,但一般是二维向量,如果是灰度图
,一般存放 <uchar>
类型;如果是RGB
彩色图,存放 <Vec3b>
类型。
单通道灰度图数据存放格式
多通道的图像中,每列并列存放通道数量的子列,如RGB三通道彩色图:
注意通道的顺序反转了:BGR
。通常情况内存足够大的话图像的每一行是连续存放的,也就是在内存上图像的所有数据存放成一行,这中情况在访问时可以提供很大方便。可以用 isContinuous()
函数来判断图像数组是否为连续的。
2.2 Mat的常见属性及类型
Mat的常见属性
属性部分存储了一系列的矩阵属性:行数、列数、通道数、数据类型、矩阵数据的大小等,以及指向了矩阵数据的指针等。
-
cols
:矩阵列数 -
rows
:矩阵行数 -
channels
:通道数 -
type
:数据类型 -
total
:矩阵总元素数 -
data
:指向矩阵数据块的指针
Mat 数据类型type
表示了矩阵中元素的类型以及矩阵的通道个数,它是一系列的预定义的变量,其命名规则为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
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
elemSize
矩阵一个元素占用的字节数,例如:type是CV_16SC3
,那么elemSize = 3 * 16 / 8 = 6 bytes
elemSize1
矩阵元素一个通道
占用的字节数,例如:type是CV_16CS3,那么elemSize1 = 16 / 8 = 2 bytes = elemSize / channels
2.3 Mat的操作
2.3.1 Mat的创建
> 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位
- 3.使用
行、列、类型、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向量数目永远是等于通道数目
。其他赋值情况也一样
- 4.使用
大小、类型
个参数的构造函数创建Mat对象。
Mat m = Mat(Size(4, 4), CV_8UC3); //创建一个4x4大小的像素块,每个像素都是三通道每个通道的位数都是8位
- 5.使用
大小、类型、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_<double>(2,2) << 0,1,1,0); // 按行填充
Mat A = (Mat_<double>(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_<char>(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);
2.3.2 像素读写
uchar
:灰度图像像素,单个值Vec3b
:彩色图像像素,3个值
单个像素的访问:pixel = image.at<uchar>(row, col);
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<uchar>(row, col);
image.at<uchar>(row, col) = 255 - pv;
}
if (dims == 3) {//彩色图像
Vec3b bgr = image.at<Vec3b>(row, col);
image.at<Vec3b>(row, col)[0] = 255 - bgr[0];
image.at<Vec3b>(row, col)[1] = 255 - bgr[1];
image.at<Vec3b>(row, col)[2] = 255 - bgr[2];
}
}
}
}
-
- 指针方式遍历(比for快一点)
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<uchar>(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
}
}
}
}
对于彩色图,对单个的像素点后移通道数量次数即可,如上++了三次。
2.3.3 图像的拷贝
再开始将拷贝之前,先给大家分享一下浅拷贝和深拷贝
浅拷贝
:拷贝对象和被拷贝对象都指向同一个内存空间
,修改任何一个对象的数据都会影响另外一个;
举个例子:小明和小红在沙漠*用一个水瓶喝水,任何一个人喝了水,另外一个人都会剩下更少的水。
深拷贝
:拷贝对象和被拷贝对象指向不同的内容空间
,修改数据时互不影响。
举个例子:小明和小红各有一个水瓶,各自喝各自的水对对方不影响。
深拷贝和浅拷贝都各有优缺点:
- 1 拷贝构造函数进行拷贝
这种拷贝方式属于浅拷贝
,下面代码中的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);
2.4 Mat 矩阵运算
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);
2.5 Mat 转换
2.5.1 Mat与二维指针相互转换
对于一个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<int>(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<uchar>(i, j) = pImgdata[i][j];
}
}
return Img;
}
2.5.2 Mat与一维数组相互转换
//---------------------数组和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<cv::Vec3b>(i, j)[k];
}
}
}
Mat image3(height, width, CV_8UC3, array1);