索引地址:系列索引
介绍完基本的数据结构和函数之后,我们现在正式进入OpenCV之旅。
访问某一个像素点
第一步是像素操作。一张图片由许多个点组成,每个点就是一个像素,每个像素包含不同的值。如果是一通道,那么每个像素只有一个值,比如0或者255。如果是三通道,每个像素包含三种值,可以通过分离通道的方式获取每个值,最开始我们从一通道黑白值图片为例。
首先,创建一个空白Mat对象,宽为3,高为3,一通道,8位,值类型为unsigned char:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat a= Mat(3,3,CV_8UC1); cout<<a<<endl;
waitKey(0);
return 0; }
|
执行后输出为:
1 2 3 4
| $ ./pixel [ 0, 0, 0; 0, 0, 0; 0, 0, 0]
|
可以看出Mat默认值为0,也就是全黑。
使用at方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat a= Mat(3,3,CV_8UC1); a.at<unsigned char>(1,1)=255; cout<<a<<endl;
cout<<(int)a.at<unsigned char>(1,1)<<endl;
waitKey();
return 0; }
|
按照参考手册的要求,通过at(1,1)来指定第2行第2列的那个像素,同时指定数值类型为unsigned 强制类型转换出Point(1,1)的数值,因为源类型是uc,我们使用强制类型转换为Int。
编译运行结果为:
1 2 3 4 5
| $ ./pixel [ 0, 0, 0; 0, 255, 0; 0, 0, 0] 255
|
ptr方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat a= Mat(3,3,CV_8UC1); int *data = a.ptr<int>(1,1); *data=32; cout<<a<<endl;
cout<<*(a.ptr<int>(1,1))<<endl;
waitKey();
return 0; }
|
ptr是指针,所以需要通过指针的方法来访问像素数值。
编译运行结果:
1 2 3 4 5
| $ ./pixel [ 0, 0, 0; 0, 32, 0; 0, 0, 0] 32
|
访问全部像素
一般我们处理图片是对图片中每一位像素进行处理,有三种方法:at,ptr,迭代器。
at方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat a= Mat(3,3,CV_8UC1); int nRow=a.rows; int nCol=a.cols;
a.at<int>(1,1)=255; for(int i=0;i<nCol;i++){ for(int j=0;j<nRow;j++){ cout<<a.at<int>(i,j)<<" "; } cout<<endl; } waitKey();
return 0; }
|
和二维数组输出类似。
结果如下:
1 2 3 4
| $ ./pixel 0 -16777216 0 0 255 0 65280 0 0
|
结果和我们的值有两个像素值不同。
ptr方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat a= Mat(3,3,CV_8UC1); int nRow=a.rows; int nCol=a.cols;
a.at<uchar>(1,1)=255; for(int i=0;i<nCol;i++){ uchar *data = a.ptr<uchar>(i); for(int j=0;j<nRow;j++){ cout<<(int)data[j]<<" "; } cout<<endl; } waitKey();
return 0; }
|
结果如下:
1 2 3 4
| $ ./pixel 0 0 0 0 255 0 0 0 0
|
此时,值就是我们赋予的。由此可见,uchar和int类型方面也会对数据处理产生一定影响。
迭代器方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat a= Mat(3,3,CV_8UC1); int nRow=a.rows; int nCol=a.cols;
a.at<uchar>(1,1)=255;
Mat_<int>::iterator it = a.begin<int>(); Mat_<int>::iterator itend=a.end<int>();
for(;it!=itend;++it){ int b=(uchar)*it; cout<<b<<" "; } waitKey();
return 0; }
|
输出结果为:
1 2
| $ ./pixel 0 0 0 0 255 0 0 0 0 %
|
就是普通的迭代器的用法,如果不了解也没有关系,至今为止我没有实际使用过这种方法。
数据运算时,应该保持数据类型的一致。如果是UC,那么后面的所有运算都是UC,需要时进行类型转换。
算数操作
像素算术操作就是基本的加减乘除。
首先,新建一个Mat对象用于操作:
1 2 3 4 5 6 7 8 9 10
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat src = Mat(3,3,CV_8UC1); cout<<src<<endl; }
|
输出为:
1 2 3 4
| $ ./alt [ 0, 0, 0; 0, 0, 0; 0, 0, 0]
|
内部数据默认全部为0,也就是全黑。
接下来试一试基本的算术操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat src = Mat(3,3,CV_8UC1); cout<<src<<endl;
int nCol = src.cols; int nRow = src.rows;
for(int i=0;i<nRow;i++){ for(int j=0;j<nCol;j++){ src.at<uchar>(i,j)+=3; } }
cout<<src<<endl;
for(int i=0;i<nRow;i++){ for(int j=0;j<nCol;j++){ src.at<uchar>(i,j)-=2; } }
cout<<src<<endl;
for(int i=0;i<nRow;i++){ for(int j=0;j<nCol;j++){ src.at<uchar>(i,j)*=2; } }
cout<<src<<endl;
for(int i=0;i<nRow;i++){ for(int j=0;j<nCol;j++){ src.at<uchar>(i,j)/=3; } }
cout<<src<<endl;
return 0; }
|
输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| $ ./alt [ 0, 0, 0; 0, 0, 0; 0, 0, 0] [ 3, 3, 3; 3, 3, 3; 3, 3, 3] [ 1, 1, 1; 1, 1, 1; 1, 1, 1] [ 2, 2, 2; 2, 2, 2; 2, 2, 2] [ 0, 0, 0; 0, 0, 0; 0, 0, 0]
|
和普通的算术类似,只不过因为源Mat是8UC1类型,那么带有小数会被截断为整数。
在介绍imwrite()时,我们有直接操作像素的代码,现在我们来分析一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <vector> #include <stdio.h> #include<opencv2/opencv.hpp> using namespace cv; using namespace std;
void createAlphaMat(Mat &mat) { for(int i = 0; i < mat.rows; ++i) { for(int j = 0; j < mat.cols; ++j) { Vec4b &rgba = mat.at<Vec4b>(i, j); rgba[0]= UCHAR_MAX; rgba[1]= saturate_cast<uchar>((float (mat.cols - j)) / ((float)mat.cols) *UCHAR_MAX); rgba[2]= saturate_cast<uchar>((float (mat.rows - i)) / ((float)mat.rows) *UCHAR_MAX); rgba[3]= saturate_cast<uchar>(0.5 * (rgba[1] + rgba[2])); } } }
int main( ) { Mat mat(480, 640, CV_8UC4); createAlphaMat(mat);
vector<int>compression_params; compression_params.push_back(IMWRITE_PNG_COMPRESSION); compression_params.push_back(9);
imwrite("透明Alpha值图.png", mat, compression_params); imshow("生成的png图",mat); waitKey(0);
return 0; }
|
saturate_cast是OpenCV中使用的防止溢出的操作,类似于:
1 2 3 4
| if(data<0) data=0; else if(data>255) data=255;
|
然后对每个像素的每一个通道进行赋值,赋值的值由简单的算法计算得出。
逻辑操作
根据算术操作,OpenCV的像素操作就是简单的赋值,而赋的值就是算术操作的结果。赋值的对象就是像素对象或者像素的某一个像素的某一个通道。
逻辑运算包括:&|~!&&||
等等。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #include <iostream> #include <opencv2/opencv.hpp>
using namespace std; using namespace cv;
int main(){ Mat src = Mat(3,3,CV_8UC1,Scalar(100,100,100)); cout<<src<<endl;
int nCol = src.cols; int nRow = src.rows;
Mat temp1 = ~src; cout<<temp1<<endl;
Mat temp2 = src&0x0f; cout<<temp2<<endl;
Mat temp3=src|0x0f; cout<<temp3<<endl;
Mat temp4=src; for(int i=0;i<nCol;i++){ for(int j=0;j<nRow;j++){ temp4.at<uchar>(i,j)=temp4.at<uchar>(i,j)&&0x0f; } } cout<<temp4<<endl;
Mat temp6=src; for(int i=0;i<nCol;i++){ for(int j=0;j<nRow;j++){ temp6.at<uchar>(i,j)=temp6.at<uchar>(i,j)||0x80; } } cout<<temp6<<endl;
Mat temp8=src; for(int i=0;i<nCol;i++){ for(int j=0;j<nRow;j++){ temp8.at<uchar>(i,j)=!temp8.at<uchar>(i,j); } } cout<<temp8<<endl;
return 0; }
|
测试输出为:
1 2 3 4 5 6 7
| [100, 100, 100; 100, 100, 100; 100, 100, 100] [155, 155, 155; 155, 155, 155; 155, 155, 155] [ 4, 4, 4; 4, 4, 4; 4, 4, 4] [111, 111, 111; 111, 111, 111; 111, 111, 111] [ 1, 1, 1; 1, 1, 1; 1, 1, 1] [ 1, 1, 1; 1, 1, 1; 1, 1, 1] [ 0, 0, 0; 0, 0, 0; 0, 0, 0]
|
&|~
直接在Mat对象操作,默认是对每个像素进行操作。
&& || ~
需要我们对每个像素进行操作。
像素归一化
像素值归一化就是要把图片像素值数据经过某种算法限制在需要的一定范围内。归一化可以使没有可比性的数据变得具有可比性,同时保持相比较的数据之间的相对关系。OpenCV提供了四种图片像素归一化的方法:
- L1归一化 : NORM_L1
- L2归一化 : NORM_L2
- INF归一化 : NORM_INF
- MINMAX归一化 : NORM_MINMAX(最常用)
使用的函数是normalize(),函数说明为:
1 2 3 4 5 6 7 8
| void normalize( InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0, int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray() );
|
下面以20,80,100三个像素值为例解释OpenCV中四种归一化的计算方法,由于归一化后为小数,因此将20,80,100转换成浮点型20.0,80.0,100.0以减小精度丢失。
L1归一化:NORM_L1
L1归一化依据所有像素值之和,用原始像素值除以所有像素值之和即为原始像素值归一化后的值。
例如对于20.0,80.0,100.0三个像素值,它们的和为20.0+80.0+100.0=200.0
则
20.0/200.0=0.120.0/200.0=0.1
80.0/200.0=0.480.0/200.0=0.4
100.0/200.0=0.5
所以20.0,80.0,100.0经过L1归一化后的值分别为0.1,0.4,0.5。
L2归一化:NORM_L2
L2归一化依据于原始像素值组成的单位向量,用原始像素值除以所有原始像素值平方值之和的平方根即为原始像素值归一化后的值。
例如对于20.0,80.0,100.0三个像素值,它们的平方和为:20.0∗20.0+80.0∗80.0+100.0∗100.0=16800.0,开平方得129.61。
则
20.0/129.61=0.15420.0/129.61=0.154
80.0/129.61=0.61780.0/129.61=0.617
100.0/129.61=0.772
所以20.0,80.0,100.0经过L2归一化后的值分别为0.154,0.617,0.772。
INF归一化:NORM_INF
INF归一化依据于最大值,用原始像素值除以所有原始像素值中的最大值即为原始像素值归一化后的值。
例如对于20.0,80.0,100.0三个像素值,它们的最大值为100.0。
则
20.0/100.0=0.2
80.0/100.0=0.880.0/100.0=0.8
100.0/100.0=1.0
所以20.0,80.0,100.0经过INF归一化后的值分别为0.2,0.8,1.0。
MINMAX归一化:NORM_MINMAX
INF归一化依据于最大值与最小值的差值(记为delta),用原始像素值除以所有原始像素值中的最大值即为原始像素值归一化后的值。
例如对于20.0,80.0,100.0三个像素值,它们的最小值min为20.0,最大值maxmax为100.0,则delta=max−min=100.0−20.0=80.0
所以
(20.0−min)/delta=(20.0−20.0)/80.0=0.0
(80.0−min)/delta=(80.0−20.0)/80.0=0.75(80.0−min)/delta=(80.0−20.0)/80.0=0.75
(100.0−min)/delta=(100.0−20.0)/80.0=1.0(100.0−min)/delta=(100.0−20.0)/80.0=1.0
所以20.0,80.0,100.0经过MINMAX归一化后的值分别为0.0,0.75,1.0。
测试代码1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include<opencv2/opencv.hpp> #include<iostream> #include<Windows.h>
using namespace cv; using namespace std;
int main(int argc, char**argv) { vector<double>a = {20,80,100}; vector<double>a1(3); vector<double>a2(3); vector<double>a3(3); vector<double>a4(3); cv::normalize(a, a1, 1, 0, NORM_L1); cv::normalize(a, a2, 1, 0, NORM_L2); cv::normalize(a, a3, 1, 0, NORM_INF); cv::normalize(a, a4, 1, 0, NORM_MINMAX);
cout << "L1 normal:" << endl; for (int i = 0; i < a.size(); i++){ cout << a1[i]<<" "; } cout << endl;
cout << "L2 normal:" << endl; for (int i = 0; i < a.size(); i++){ cout << a2[i] << " "; } cout << endl;
cout << "INF normal:" << endl; for (int i = 0; i < a.size(); i++){ cout << a3[i] << " "; } cout << endl;
cout << "MINMAX normal:" << endl; for (int i = 0; i < a.size(); i++){ cout << a4[i] << " "; } cout << endl;
return 0; }
|
输出结果为:
1 2 3 4 5 6 7 8 9
| $ ./normalize L1 normal: 0.1 0.4 0.5 L2 normal: 0.154303 0.617213 0.771517 INF normal: 0.2 0.8 1 MINMAX normal: 1.38778e-17 0.75 1
|
测试2
我们处理一下图片,可以看出归一化的对象要求每个像素只能有一个值,那么最好的就是灰度图或者黑白二值图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include<opencv2/opencv.hpp> #include<iostream>
using namespace cv; using namespace std;
int main(int argc, char**argv) { Mat gray_input_image = imread("1.jpg", 0); if (gray_input_image.empty()){ cout << "read input error!" << endl; return -1; } imshow("gary_input", gray_input_image);
gray_input_image.convertTo(gray_input_image, CV_32F); Mat result = Mat::zeros(gray_input_image.size(), CV_32FC1); normalize(gray_input_image, result, 1.0, 0.0, NORM_MINMAX); result = result * 255; result.convertTo(result, CV_8UC1); imshow("result", result);
waitKey(0);
return 0; }
|
处理结果:
上面显示的就是我们使用OpenCV的正常流程,读取-处理-结果,如果需要调试,可以在中间加上显示来确保或者查看中间处理结果。
插值缩放
基本的像素操作介绍完毕之后,我们来看一下图像插值,即在图像某一点的周围通过算法获取一个新的像素点数据插入,实现扩展图片的尺寸:
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
#include <iostream> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace cv; using namespace std;
cv::Mat nNeighbourInterpolation(cv::Mat srcImage) { CV_Assert(srcImage.data != NULL); int rows = srcImage.rows; int cols = srcImage.cols; cv::Mat dstImage = cv::Mat(cv::Size(150, 150), srcImage.type(), cv::Scalar::all(0)); int dstRows = dstImage.rows; int dstCols = dstImage.cols; float cx = (float)cols / dstCols; float ry = (float)rows / dstRows; std::cout << "cx: " << cx << "ry:" << ry << std::endl; for (int i = 0; i < dstCols; i++) { int ix = floor(i * cx); for (int j = 0; j < dstRows; j++) { int jy = floor(j * ry); if (ix > cols - 1) ix = cols - 1; if (jy > rows - 1) jy = rows - 1; dstImage.at<cv::Vec3b>(j, i) = srcImage.at<cv::Vec3b>(jy, ix); } } return dstImage; }
int main() { cv::Mat srcImage = cv::imread("lena.jpg"); if (!srcImage.data) return -1; cv::Mat dstImage = nNeighbourInterpolation(srcImage); cv::imshow("srcImage", srcImage); cv::imshow("dstImage", dstImage); cv::waitKey(0); return 0; }
|
floor,指地板。floor函数,其功能是“向下取整”,或者说“向下舍入”、“向零取舍”,即取不大于x的最大整数,与“四舍五入”不同,下取整是直接取按照数轴上最接近要求值的左边值,即不大于要求值的最大的那个整数值。
测试结果:
bitwise
图像的基本运算有很多种,比如两幅图像可以相加、相减、相乘、相除、位运算、平方根、对数、绝对值等;图像也可以放大、缩小、旋转,还可以截取其中的一部分作为ROI(感兴趣区域)进行操作,各个颜色通道还可以分别提取及对各个颜色通道进行各种运算操作。总之,对于图像可以进行的基本运算非常的多,只是挑了些常用的操作详解。
1 2 3 4 5 6 7 8 9 10 11
| void add(InputArray src1, InputArray src2, OutputArray dst,InputArray mask=noArray(), int dtype=-1); void subtract(InputArray src1, InputArray src2, OutputArray dst,InputArray mask=noArray(), int dtype=-1); void multiply(InputArray src1, InputArray src2,OutputArray dst, double scale=1, int dtype=-1); void divide(InputArray src1, InputArray src2, OutputArray dst,double scale=1, int dtype=-1); void divide(double scale, InputArray src2,OutputArray dst, int dtype=-1); void scaleAdd(InputArray src1, double alpha, InputArray src2, OutputArray dst); void addWeighted(InputArray src1, double alpha, InputArray src2,double beta, double gamma, OutputArray dst, int dtype=-1); void sqrt(InputArray src, OutputArray dst); void pow(InputArray src, double power, OutputArray dst); void exp(InputArray src, OutputArray dst); void log(InputArray src, OutputArray dst);
|
上述的基本操作中都属于将基础数学运算应用于图像像素的处理中,上一篇文章介绍了部分像素的处理,下面将着重介绍bitwise_and、bitwise_or、bitwise_xor、bitwise_not这四个按位操作函数。
1 2 3 4
| void bitwise_and(InputArray src1, InputArray src2,OutputArray dst, InputArray mask=noArray()); void bitwise_or(InputArray src1, InputArray src2,OutputArray dst, InputArray mask=noArray()); void bitwise_xor(InputArray src1, InputArray src2,OutputArray dst, InputArray mask=noArray()); void bitwise_not(InputArray src, OutputArray dst,InputArray mask=noArray());
|
bitwise_and是对二进制数据进行“与”操作,即对图像(灰度图像或彩色图像均可)每个像素值进行二进制“与”操作,1&1=1,1&0=0,0&1=0,0&0=0
bitwise_or是对二进制数据进行“或”操作,即对图像(灰度图像或彩色图像均可)每个像素值进行二进制“或”操作,1|1=1,1|0=0,0|1=0,0|0=0
bitwise_xor是对二进制数据进行“异或”操作,即对图像(灰度图像或彩色图像均可)每个像素值进行二进制“异或”操作,1⊕1=0,1⊕0=1,0⊕1=1,0⊕0=0
bitwise_not是对二进制数据进行“非”操作,即对图像(灰度图像或彩色图像均可)每个像素值进行二进制“非”操作,~
1=0,~
0=1
其实就是对像素逻辑操作
进行函数封装。
掩膜/掩码/mask
上面的部分函数中有mask这个带有默认参数值的参数,即此函数支持掩膜操作,首先何为掩膜以及有什么用,如下:
用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。用于覆盖的特定图像或物体称为掩模或模板。光学图像处理中,掩模可以是胶片、滤光片等。数字图像处理中,掩模为二维矩阵数组,有时也用多值图像。
数字图像处理中,图像掩模主要用于:
- 提取感兴趣区,用预先制作的感兴趣区掩模与待处理图像相乘,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为0。
- 屏蔽作用,用掩模对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
- 结构特征提取,用相似性变量或图像匹配方法检测和提取图像中与掩模相似的结构特征。
- 特殊形状图像的制作。
掩膜是一种图像滤镜的模板,实用掩膜经常处理的是遥感图像。当提取道路或者河流,或者房屋时,通过一个n∗n的矩阵来对图像进行像素过滤,然后将我们需要的地物或者标志突出显示出来。这个矩阵就是一种掩膜。
矩阵运算
以图和掩膜的与运算为例:
原图中的每个像素和掩膜中的每个对应像素进行与运算。比如1 & 1 = 1;1 & 0 = 0;
比如一个3 * 3的图像与3 * 3的掩膜进行运算,得到的结果图像就是:
简单来说,掩膜就是两幅图像之间进行的各种位运算操作。
图像掩膜
先创建掩膜,然后原图与掩膜进行运算,得到结果。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include "opencv2/opencv.hpp"
using namespace cv;
int main() { Mat image, mask; Rect r1(100, 100, 250, 300); Mat img1, img2, img3, img4; image = imread("lena.jpg"); mask = Mat::zeros(image.size(), CV_8UC1); mask(r1).setTo(255); img1 = image(r1); image.copyTo(img2, mask);
image.copyTo(img3); img3.setTo(0, mask);
imshow("original", image); imshow("img1", img1); imshow("img2", img2); imshow("img3", img3); imshow("mask", mask);
waitKey(0); return 0; }
|
测试效果: