1、图像的加载、修改与保存
涉及API:
cv::imread(); //读取 cv::imshow(); //显示 cv::cvtColor(); //修改 cv::imwrite(); //保存
扩展图像窗口创建API:cv::namedWindow();
cv::namedWindow需要两个参数,第一个参数是窗口名称,第二个参数是关于窗口操作的关键字(包含:WINDOW_AUTOSIZE会根据图像大小自动设置窗口大小并且生成的窗口大小不能修改;WINDOW_NORMAL此关键字一般使用在跟QT集成以后的程序中,表示允许修改窗口大小)
cv::imread:
参数两个,第一个参数是图像存储的绝对路径,第二个参数读取图像类型(包含:IMREAD_UNCANGED表示加载原图;IMREAD_GRAYSCALE表示将图像作为灰度图像加载进来;IMREAD_COLOR表示原图作为RGB图像加载进来)
cv::imshow:
两个参数,第一个参数是图像窗口名称(可以自动创建),第二个参数是Mat对象名
cv::cvtColor:
三个参数,第一个是需更改的Mat对象名,第二个是用于保存更改后的Mat对象名,第三个参数是修改使用的源和目标色彩空间(如:COLOR_BGR2GRAY表示修改成灰度图像)
cv::imwrite:
使用时包含两个参数,第一个参数是保存图像的绝对路径,第二个参数是需要保存的Mat对象名
2、矩阵的掩膜操作
获取图像像素指针:
CV_Assert(image.depth() == CV_8U)
Mat.ptr(int i = 0)获取像素矩阵的指针,其中索引 i 表示第几行,从0开始计行数
获取当前行指针语句:
const uchar* current = image.ptr<uchar>(row);
获取当前像素点P(row,col)的像素值语句:
p(row,col) = current[col];
像素范围处理saturate_cast<uchar>
(重要函数)
saturate_cast<uchar>(-100) //返回0 saturate_cast<uchar>(299) //返回255 saturate_cast<uchar>(100) //返回100
说明:此函数的功能是确保RGB值的范围在0~255之间
什么是图像掩膜操作,掩膜操作实现的是图像对比度调整
opencv提供的掩膜操作(对比度提高)API:filter2D
定义掩膜矩阵:
Mat kernel = (Mat_<char>(3,3)<< 0,-1,0,-1,5,-1,0,-1,0);
API调用举例:filter2D(img, dst, img.depth(), kernel);
说明:第一个参数是操作对象名,第二个参数保存操作后对象名,第三个参数图像深度(使用depth()函数获取了原图像深度),第三个参数是掩膜方法(对应掩膜矩阵)
如何初始化一个零时Mat对象用于存储原图像?
代码:
Mat dst = Mat::zeros(img.size(), img.type()); //zeros方法代表创建RGB为0的纯黑图像,大小和类型与原图像相同
拓展:执行时间的显示
代码:
double t = getTickCount(); /* 代码部分 */ double time = (getTickCount() - t)/getTickFrequency(); // cout << "执行时间: " << time << endl;
3、Mat对象(一种比Iplimage更加安全的存储对象,Mat对象的内存空间被自动分配)
Mat对象构造函数:
Mat() Mat(int rows, int cols, int type) Mat(Size size, int type) Mat(int rows, int cols, int type, const Scaler &s) // 说明:前两个参数分别表示行和列,第三个参数是类型参数(比如CV_8UC3中8表示每个通道占8位,U表示无符号,C表示Char类型,3表示三个通道数),第四个参数是向量表示初始化每个像素值为多少,向量长度对应通道数目一致。 Mat(Size size, int type, const Scaler &s) //Scaler()用来给像素赋值 Mat(int ndims, const int *sizes, int type) Mat(int ndims, const int *sizes, int type, const Scaler &s)
说明:拷贝构造函数只会赋值对象头部,使用API–>demo = mat.clone() or mat.copyTo(demo)
才能进行完全复制(包括数据部分)
常用方法:
void copyTo(Mat mat); void convertTo(Mat dst, int type); Mat clone(); int channels(); int depth(); bool empty(); uchar* ptr(i = 0); //备注:查资料详学(读取像素值) cv::Mat::create(size, type) //create方法创建对象(可指定对象尺寸大小)
两种用法:
M.create(img.size(), img.type()); M.create(4,3,CV_8UC2); M = Scaler(123,123);
定义小数组:(掩膜运用)
Mat kernel = (Mat_<char>(3,3)<< 0,-1,0,-1,5,-1,0,-1,0);
初始化全0图像有多种方法,其中比较特殊的是Mat::zero(size, type);
用法:
Mat m = Mat::zero(img.size(), img.type()); Mat m = Mat::zero(2, 2, CV_8UC1);
拓展:Mat::eye(……)
方法,初始化对角线为一的图像矩阵。
4、图像操作
读取像素:
· 读取一个gray像素点的像素值(CV_8UC1)
Scalar intensity = img.at<uchar>(row, col); or Scalar intensity = img.at<uchar>(Point(row, col));
· 读取一个RGB像素点的像素值
Vec3f intensity = img.at<Vec3f>(row, col); float blue = intensity.val[0]; float green = intensity.val[1]; float red = intensity.val[2]; for(int row = o; row < img.rows; row++) { for(int col = 0; col < img.cols; col++) { int b = img.at<Vec3b>(row, col)[0]; int g = img.at<Vec3b>(row, col)[1]; int r = img.at<Vec3b>(row, col)[2]; } }
说明:Vec3b是一种数据结构,放置BGR像素点,3b表示3bit读取,也可以用Vec3f,3f表示以float类型读取,如第一种读取方法。
修改像素:
· 灰度图像
img.at<uchar>(row, col) = 123;
· RGB图像
img.at<Vec3b>(row, col)[0] = 123; //修改参数B img.at<Vec3b>(row, col)[1] = 123; //修改参数G img.at<Vec3b>(row, col)[2] = 123; //修改参数R
· 空白图
img = Scalar(100); //将每个像素点赋值为100
· ROI选择
Rect r(10, 10, 100, 100); Mat smallimg = img(r);
Vec3b与Vec3f
· Vec3b对应三通道顺序blue、green、red的uchar类型数据
· Vec3f对应三通道float类型数据
· 把CV_8UC1转换到CV32F1实现如下:
img.convertTo(dst, CV_32F1); //使用API-->convertTo(dst, type);
5、图像混合
· 理论-线性混合操作
g(x) = (1-a)f0(x)+af1(x)
说明:f0(x)表示一个图像,f1(x)表示另一个图像,其中a的取值范围为0~1之间,g(x)表示混合后得到的图像(注意:对图像每个像素的操作)
相关API
cv::addWeighted(inputArray src1, //参数1:输入图像Mat - src1 double alpha, //参数2:输入图像src1的alpha值(alpha表示表达式中的a) inputArray src2, //参数3:输入图像Mat - src2 double beta, //参数4:输入图像src2的alpha值 double gamma, //参数5:gamma值(校验值,使其得到正常图像) OutputArray dst, //参数6:输出混合图像 int dtpye = -1 //dtpye默认不用带入 )
注意:两张图像大小和类型必须一致才可以使用此API混合
dst(I) = saturate(src1(I) * alpha +src2(I) * beta + gamma);
拓展API:
add(img, dst, dst1); //直接叠加两个图像像素 multiply(img, dst, dst1, 1.0); //两个图像像素相乘
6、调整图像亮度和对比度
图像变换可以看作像素变换(点操作)和领域操作(区域操作),调整图像亮度和对比度属于像素变换
g(i,j) = af(i,j) + β (其中a>0,β是增益变量)
再次回顾重要API:
//像素范围处理函数
saturate_cast<uchar>(-100),返回0
saturate_cast<uchar>(299),返回255
saturate_cast<uchar>(100),返回100
//图像数据类型转换
img.convertTo(dst, CV_32F);
说明:如果图像默认bit类型,我们把数据转换成浮点型,提高图像精度可以使处理效果提高。
7、绘制形状和文字
· 使用cv::Point
与cv::Scalar
Point表示2D平面上一个点,坐标(x,y)
如下:
Point p; p.x = 10; p.y = 8; Point p1 = Point(x, y );
Scalar表示四个元素的向量
Scalar(a, b, c); //a = Blue, b = green, c = Red表示BGR三个通道
· 绘制线、矩形、圆、椭圆等基本几何形状
API使用:
线–> cv::line(LINE_4\LINE_8\LINE_AA) //参数表示绘制线的类型,LINE_AA表示反锯齿
椭圆–> cv::ellipse
椭圆API说明:使用语句示例–>
ellipse(img, Point(img.rows/2, img.cols/2), Size(img.rows/5, img.cols/6), 90, 0, 360, color, 2, 8); /* Point表示椭圆中心坐标,size表示椭圆尺寸,其中两个参数表示长短轴,angle = 90表示顺时针方向旋转角,startAngle = 0表示绘制的起始角度,endAngle = 360表示绘制的终止角度。*/
矩形 –> cv::rectangle // 五个参数,第一个参数是Mat对象,第二个参数矩形类型,第三个参数颜色,第四个参数线宽(默认1),第五个参数线的类型(默认LINE_8)
圆 –> cv::circle
填充 –> cv::fillPoly
fillPoly各参数说明:用法 –>
Point pts[1][5] = { Point(100,100), Point(100,200), Point(200,200), Point(200,100), Point(100,100) }; const Point* ppts[] = { pts[0] }; int npt[] = { 5 }; fillPoly(img, ppts, npt, 1, Scalar(255,0,255), 8); //ppts表示多边形各顶点集合(静态点对象指针数组),npt表示多边形顶点个数, //ncontours = 1表示填充个数
对象:
Rect rect = Rect(x, y, w_len, h_len); // 后两个参数分别是宽高
· 随机生成与绘制文本
绘制文本API:cv::putText(Mat&, string, Point, int_fontFace, double_fontScale, Scalar, thickness, lineType, bottomLeftOrigin = false);
代码示例:
putText(img, "Hello OpenCV", Point(100,100), CV_FONT_HERSHEY_COMPLEX, 1.0, Scalar(0,0,255), 1, 8);
说明:int_fontFace表示字体类型,double_fontScale表示字体缩放比例,bottomLeftOrigin默认为FALSE不用管。
随机生成图像(以画线为例)
代码如下:
void RandomLineDemo(Mat& img) { RNG rng(12345); Point pt1, pt2; Mat dst = Mat::zeros(img.size(), img.type()); namedWindow("test7", CV_WINDOW_AUTOSIZE); for (int i = 0; i < 10000; i++) { pt1.x = rng.uniform(0, img.cols); pt2.x = rng.uniform(0, img.cols); pt1.y = rng.uniform(0, img.rows); pt2.y = rng.uniform(0, img.rows); Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); if (waitKey(50) > 0) break; imshow("test7", dst); line(dst, pt1, pt2, color); } }
说明:RNG是opencv中的随机数类,构造函数指明随机数范围或种子个数,使用uniform(正态分布随机数)方法指定随机数范围,同样的也可以使用gaussian(double sigma)方法生成高斯随机数
8、模糊图像(一)
· 图像的模糊原理
- Smooth/Blur是图像处理中最简单和常用的操作之一
- 使用该操作的原因之一就是为了给图像预处理时候降低噪声
- 使用Smooth/Blur操作其背后是数学的卷积计算:g(i, j) = Σ f( i+k, j+l)h(k, l)
- 通常这些卷积算子计算都是线性操作,所以又叫线性滤波
- 归一化盒子滤波(均值滤波)
- 高斯滤波
· 相关API
-
均值模糊:
blur(Mat src, Mat dst, Size(xradius, yradius), Point(-1, -1)); //Point表示中心像素在哪里,(-1, -1)表示默认中心像素
-
高斯滤波:
GaussianBlur(Mat src, Mat dst, Size(11, 11), sigmax, sigmay); //sigmax, sigmay是用于调节正态分布图像的参数
注意:其Size(x, y)中x和y必须是正数而且是奇数,size表示窗口大小
9、模糊图像(二)
· 中值滤波
- 统计排序滤波器
- 中值对椒盐噪声有很好的抑制作用
比如有一个5×5图像:
123 125 126 130 140
122 124 126 127 135
118 120 150 125 134
119 115 119 123 133
111 116 110 120 130
3×3领域像素(坐标为22到44)排序如下:115,119,120,123,124,125,126,127,150
中值等于:124
均值等于:125.33
API:medianBlur (Mat src, Mat dst, ksize)
注意:中值模糊的ksize大小必须是大于1而且为奇数 --> ksize表示卷积核大小
· 双边滤波
- 均值滤波无法克服边缘像素信息丢失缺陷,原因是均值滤波是基于平均权重
- 高斯模糊部分克服该缺陷,但无法完全避免,原因是没有考虑像素值的不同
- 高斯双边模糊是边缘保留的滤波方法,避免了边缘信息的丢失,保留了图像轮廓不变
API:bilateralFilter (src, dst, d=15, 150, 3)
说明:d=15为计算半径,半径之内的像素都会被纳入计算,如果该参数提供-1则会根据sigma space参数取计算半径
150表示sigma color,决定多少差值之内像素会被计算
3表示sigma space如果d值大于0则声明无效
10、膨胀与腐蚀
· 膨胀
- 图像形态学操作:基于形状的一系列图像操作集合,主要是基于集合论基础上的形态学数学
- 形态学有四个基本操作:腐蚀、膨胀、开、闭
- 腐蚀和膨胀是图像处理中最基本的形态学操作
说明:膨胀操作跟卷积操作类似,假设有图像A和机构元素B,结构元素B在A上移动,其中B定义其中心为锚点,计算B覆盖下A的最大像素值用来替换锚点像素其中B作为结构体可以是任意形状。而腐蚀跟膨胀操作类似,唯一不同的是以最小值替换锚点重叠下图像的像素值。
相关API:
· getStructuringElement(int shape, Size ksize, Point anchor) //获取结构形状
说明:三个参数分别代表形状(MORPH_RECT \ MORPH_CROSS \ MORPH_ELLIPSE)、大小(要求奇数)、锚点(默认是Point(-1,-1)意思就是中心像素)
· dilate(src, dst, kernel) //膨胀
· erode(src, dst, kernel) //腐蚀
拓展:
动态调整结构元素大小 (GUI函数)
TrackBar -> createTrackbar(constString & trackbarname, const String winName, int* value, int count, Trackbarcallback func, void* userdata = 0) //其中最重要的是callback函数功能,如果设置为NULL就是说只有值update,但是不会调用callback的函数。
11、形态学操作(多用于二值图像处理)
· 开操作 - open
说明:图像先腐蚀后膨胀的操作即称为图像的开操作,图像开操作可以去掉小的对象。
· 闭操作 - close
说明: 先膨胀后腐蚀称之为闭操作,可以讲大面积图像中小的缺口给填充。
· 形态学梯度 - Morphological Gradient
说明:图像膨胀后减去原图的腐蚀图像,此种方法又称基本梯度,得到的图像具有梯度效果。
· 顶帽 - top hat
说明:顶帽是原图像与开操作之间的差值图像
· 黑帽 - black hat
说明:黑帽是闭操作图像与源图像的差值图像
API:morphologyEx(src, dst, CV_MOP_BLACKHAT, kernel);
参数说明:
- int OP --> CV_MOP_OPEN/ CV_MOP_CLOSE/ CV_MOP_GRADIENT/ CV_MOP_TOPHAT/ CV_MOP_BLACKHAT (表示形态学操作类型)
- Mat kernel --> 结构元素(选取大小取决于去掉的对象大小)
- int Iteration=1 --> 迭代次数,默认为1
使用实例(开操作):
Mat kernal = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1)); morphologyEx(src, dst, CV_MOP_OPEN, kernal); imshow("dst", dst); waitKey(0);
12、提取水平与垂直线(形态学应用)
· 原理方法
图像形态学操作的时候,可以通过自定义的结构元素实现结构元素对输入图像一些对象敏感、另外一些对象不敏感,这样就会让敏感的对象改变而不敏感的对象保留输出。
通过使用两个最基本的形态学操作:膨胀与腐蚀。使用不同的结构元素实现对输入图像的操作并得到想要的结果。
知识回顾:
- 膨胀,输出的像素值是结构元素覆盖下输入图像的最大像素值
- 腐蚀,输出的像素值是结构元素覆盖下输入图像的最小像素值
· 结构元素
膨胀与腐蚀过程是可以使用任意的结构元素,常见的形状:矩形、圆、直线、磁盘形状、砖石形状等各种自定义形状。
· 提取步骤
1 - 输入图像
2 - 灰度变换
3 - 二值化 --> adaptiveThreshold
4 - 定义结构元素
5 - 开操作提取水平与垂直线
相关API - adaptiveThreshold(src, dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C);
参数说明:
double maxValue - 输入图像最大灰度值
int adaptiveMethod - 自适应阈值方法(ADAPTIVE_THRESH_MEAN_C / ADAPTIVE_THRESH_GAUSSIAN_C)
int thresholdType - 阈值类型(常用 THRESH_BINARY)
int blockSize - 子块大小
double C - 常量,可为正数、负数、0
结构元素定义实例代码:
Mat hline = getStructuringElement(MORPH_RECT, Size(src.cols / 16, 1), Point(-1, -1)); Mat wline = getStructruingElement(MORPH_RECT, Size(1, src.row . 16), Point(-1, -1));
拓展API:bitwist_not(src, src);
--> 图像二进制数据“非”操作
13、图像上采样和降采样
· 图像金字塔概念说明
我们在图像处理中常常会调整图像大下,最常见的就是放大和缩小,尽管几何变换也可以实现图像的放大和缩小,但是这里所说的
大小调整是指图像金字塔从下到上分辨率的缩小。一个图像金字塔是一系列图像组成,最底下一张是图像尺寸最大,最上方图像尺
寸最小,从空间上由上向下看就像古埃及金字塔。
图像金字塔分为高斯金字塔和拉普拉斯金字塔,高斯金字塔用来对图像进行降采样,拉普拉斯金字塔用来重建一张图片根据它的上
层降采样图片
高斯金字塔:
-
高斯金字塔是从底向上,逐层采样得到的
-
降采样之后图像大小是原图像 M × N 的 M/2 × N/2,就是对原图像删除偶数行与列,即得到采样后的上层图像
-
高斯金字塔的生成过程分为两步:
1 - 对当前层进行高斯模糊
2 - 删除当前层的偶数行与列
即可得到上一层图像,这样上一层和下一层相比,都只有它大小的1/4{1, 4, 6,4,1; 4,16,24,16,4; 1/16 × 6,24,36,24,6; 4,16,24,16,4; 1, 4, 6, 4,1;}
高斯不同(DOG):
-
定义:把同一张图像在不同的参数下做高斯模糊之后的结果相减,得到的输出图像称之为高斯不同。
-
高斯不同是图像的内在特征,在灰度图像增强、角点检测中经常使用
代码示例:cvtColor(src, gray_src, CV_BGR2GRAY); GaussianBlur(gray_src, g1_dst, Size(3, 3), 0, 0); GaussianBlur(g1_dst, g2_dst, Size(3, 3), 0, 0); subtract(g1_dst, g2_dst, DOGimage, Mat()); normalize(DOGimage, DOGimage, 255, 0, NORM_MINMAX); // 还原灰度范围 imshow("DOG image", DOGimage); waitKey(0);
相关API:
· 上采用 --> cv::pyrUp(Mat src, Mat dst, Size(src.cols*2, src.rows*2))
效果:生成的图像是原图在宽和高各放大两倍
· 降采样 --> cv::pyrDown(Mat src, Mat dst, Size(src.col/2, src.rows/2))
效果:生成的图像是原图在宽与高各缩小1/2
14、阈值操作
阈值操作:二值化、反二值化、截断、阈值取零、阈值反取零
API–>cv::threshold(img, dst, thresh, max_value, type);
说明:type参数表示阈值操作类型,可填写cv::THRESH_BINARY, cv::THRESH_BINARY_INV, cv::THRESH_TRUNC, cv::THRESH_TOZERO, cv::THRESH_TOZERO_TNV等。
15、边缘填充
·OpenCV中常用的边缘填充函数为copyMakeBorder();
函数原型:void copyMakeBorder(const Mat &src, Mat& dst, int top, int bottom, int left, int right, int borderType, const Scalar &value=Scalar());
功能:扩充src的边缘,将图像变大,然后以各种外插方式自动填充图像边界,这个函数实际上调用了函数cv::borderInterpolate,这个函数最重要的功能就是为了处理边界,比如均值滤波或者中值滤波中,使用copyMakeBorder将原图稍微放大,然后我们就可以处理边界的情况。
参数说明:
src,dst:原图与目标图像
top,bottom,left,right分别表示在原图四周扩充边缘的大小
borderType:扩充边缘的类型,就是外插的类型,OpenCV中给出以下几种方式
- BORDER_REPLICATE
- BORDER_REFLECT
- BORDER_REFLECT_101
- BORDER_WRAP
- BORDER_CONSTANT
BORDER_REPLICATE:边缘像素复制法
BORDER_REFLECT_101:对称法,以最边缘像素为轴,对称
BORDER_CONSTANT:常量法
16、sobel算子锐化和Laplace算子锐化
API:
cv::Sobel(img, dst, depth, dx, dy); // dx和dy分别表示x方向和y方向上的差分阶数
cv::Laplacian(img, dst, depth);
17、Canny边缘检测算法
· Canny算法五步走:
1、高斯模糊 - GaussianBlur
2、灰度转换 - cvtColor
3、计算梯度 - Sobel / Scharr
4、非最大信号抑制
5、高低阈值连接输出二值图像
什么是非最大信号抑制:
图像梯度幅值矩阵中的元素值越大,说明图像中该点的梯度值越大,但这不不能说明该点就是边缘(这仅仅是属于图像增强的过程)。在Canny算法中,非极大值抑制是进行边缘检测的重要步骤,通俗意义上是指寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点。
根据图可知,要进行非极大值抑制,就首先要确定像素点C的灰度值在其8值邻域内是否为最大。图中蓝色的线条方向为C点的梯度方向,这样就可以确定其局部的最大值肯定分布在这条线上,也即出了C点外,梯度方向的交点dTmp1和dTmp2这两个点的值也可能会是局部最大值。因此,判断C点灰度与这两个点灰度大小即可判断C点是否为其邻域内的局部最大灰度点。如果经过判断,C点灰度值小于这两个点中的任一个,那就说明C点不是局部极大值,那么则可以排除C点为边缘。这就是非极大值抑制的工作原理。
注意以下两点:
1)中非最大抑制是回答这样一个问题:“当前的梯度值在梯度方向上是一个局部最大值吗?” 所以,要把当前位置的梯度值与梯度方向上两侧的梯度值进行比较;
2)梯度方向垂直于边缘方向。
但实际上,我们只能得到C点邻域的8个点的值,而dTmp1和dTmp2并不在其中,要得到这两个值就需要对该两个点两端的已知灰度进行线性插值,也即根据图中的g1和g2对dTmp1进行插值,根据g3和g4对dTmp2进行插值,这要用到其梯度方向,这是Canny算法中要求解梯度方向矩阵Thita的原因。
完成非极大值抑制后,会得到一个二值图像,非边缘的点灰度值均为0,可能为边缘的局部灰度极大值点可设置其灰度为128。根据下文的具体测试图像可以看出,这样一个检测结果还是包含了很多由噪声及其他原因造成的假边缘。因此还需要进一步的处理。
高低阈值连接:
· API介绍
cv::Canny(InputArray src, // 8bit输入图像 OutputArray edges, // 输出边缘图像,一般都是二值图像,背景为黑色 double threshold1, // 低阈值,常取高阈值的1/2或者1/3 double threshold2, // 高阈值 int aptertureSize, // Sobel算子size,通常为3×3,取值3 bool L2gradient // 选择true表示是L2归一化方法,否则使用L1方法 )
· 相关代码
#include <opencv2/opencv.hpp> #include <iostream> #include <math.h> using namespace cv; using std::cout; using std::endl; const int t1_value = 50, max_value = 255; Mat src, dst, gray_src; void Canny_Demo(int, void*) { Mat edge_output; blur(gray_src, gray_src, Size(3, 3), Point(-1, -1), BORDER_DEFAULT); Canny(gray_src, edge_output, t1_value, t1_value*2, 3, false); dst.create(src.size(), src.type()); src.copyTo(dst, edge_output); // imshow("output", dst); imshow("out", edge_output); } int main(int argc, char** argv) { src = imread("Path"); if(!src.data) { cout << "could not load image" << endl; return -1; } imshow("input", src); cvtColor(src, gray_src, CV_BGR2GRAY); createTrackbar("Threshold Value", "Result", &t1_value, max_value, Canny_Demo); Canny_Demo(0, 0); waitKey(0); return 0; }
-
霍夫变换算法简介:霍夫变换运用两个坐标空间之间的变换,将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化为统计峰值问题,在第二空间出现的峰值点通过反算方法再映射回原有空间中就得到需要检测图像上的像素点。
-
算法实现前提:边缘检测已经完成
-
霍夫直线检测使用的第二空间是极坐标空间,映射图示如下:
1、图像坐标系到极坐标系参数空间转化过程
说明:从上面可以看到,参数空间的每个点(ρ,θ)都对应了图像空间的一条直线,或者说图像空间的一个点在参 数空间中就对应为一条曲线。参数空间采用极坐标系,这样就可以在参数空间表示原始空间中的所有直线了。 此时图像空间(直角坐标系x-y)上的一个点对应到参数空间(极坐标系ρ-θ)上是一条曲线,确切的说是一条 正弦曲线。
2、图像空间到极坐标空间的转换过程:
这样就把在图像空间中检测直线的问题转化为在极坐标参数空间中找通过点(r,θ)的最多正弦曲线数的问题。霍 夫空间中,曲线的交点次数越多,所代表的参数越确定,画出的图形越饱满。
-
相关API介绍
· 标准的霍夫变换
cv::HoughLines
从平面坐标转换到霍夫空间,最终输出是(θ, r)表示极坐标空间· 霍夫变换直线概率
cv::HoughLinesP
最终输出是直线的两个点(x0, y0, x1, y1)cv::HoughLines(InputArray src, // 输入图像,必须是8-bit灰度图 OutputArray lines, // 输出的极坐标来表示直线 double rho, // 生成极坐标时候的像素扫描步长 double theta, // 生成极坐标时候角度步长,一般取值CV_PI/180 int threshold, // 阈值,只有获得足够交点的极坐标点才被看成是直线 double srn=0, // 是否应用多尺度霍夫变换,默认值为0表示经典霍夫变换 double stn=0, // 同上 double min_theta=0, // 表示多角度扫描范围0~180之间,默认即可 double max_theta=CV_PI ) // 一般情况是有经验的开发者使用,需要自己反变换到平面空间
cv::HoughLinesP(InputArray src, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, // 最小直线长度 double maxLineGap=0 // 最大间隔 ) // 常用API
-
代码演示
#include <opencv2/opencv.hpp> #include <iostream> #include <math.h> using namespace cv; using std::cout; using std::endl; Mat src, src_gray, dst; int main(int argc, char** argv) { src = imread("Path"); if(!src.data) { cout << "could not load image" << endl; return -1; } imshow("input", src); // 边缘检测及灰度转换 Canny(src, src_gray, 150, 200); cvtColor(src_gray, dst, CV_GRAY2BGR); imshow("edge", src_gray); // 检测并标注直线 vector<Vec4f> pLines; HoughLinesP(src_gray, pLines, 1, CV_PI/180.0, 10, 0, 10); Scalar color = Scalar(0, 0, 255); for(size_t i = 0; i < pLines.size(); i++) { Vec4f hline = pLines[i]; line(dst, Point(hline[0], hline[1]), Point(hline[2], hline[3]), color, 3, LINE_AA); } imshow("output", dst); waitKey(0); return 0; }
19、霍夫圆检测
· 霍夫圆检测原理
首先霍夫圆检测的基本思路是认为每个非零像素点都有可能是圆上的点,与霍夫直线检测一样,霍夫圆检测算法也是通过统计峰值生成积累坐标平面,设置一个累积权重来定位圆。我们知道平面坐标系中圆的方程为 (x - a)^2 + (y - b)^2 = r^2,如图:
其中(a, b)是圆心,r是半径,其函数也可以表述为:x = a + rcosθ,y = b + rsinθ,即 a = x - rcosθ,b = y - rsinθ。在xy坐标系中经过某点的圆映射到abr坐标系中,就是一条三维曲线(机器学习中的支持向量机使用过类似的思想),经过xy坐标系中所有的非零像素点的所有圆就构成了abr坐标系中很多条三维的曲线。在xy坐标系中同一个圆上的所有点的圆方程是一样的,它们映射到abr坐标系中的是同一个点,所以在abr坐标系中该点就应该有圆的总像素N0个曲线相交。通过判断abr中每一点的相交(累积)数量,大于一定阈值的点就认为是圆。如图:
· 实现霍夫圆检测的现实考量
1、因为霍夫圆检测对比噪声比较敏感,所以首先要对图像做中值滤波。
2、基于效率考虑,OpenCV中霍夫圆检测是基于图像梯度实现的,分为两步:第一步检测边缘,发现可能的圆心;第二步在第一步的基础上从候选圆心开始计算最佳半径大小。
注:霍夫梯度法的检测思路是去遍历累加所有非零点对应的圆心,对圆心进行考量。定位圆心的思想是“圆心一定是在圆上的每个点的模向量上,即在垂直于该点并且经过该点的切线的垂直线上,这些圆上的模向量的交点就是圆心”,霍夫梯度法就是要去查找这些圆心,根据该“圆心”上模向量相交数量的多少,根据阈值进行最终的判断。
· 相关API介绍:cv::HoughCircles
cv::HoughCircles(InputArray image, // 输入图像必须是8位单通道灰度图 OutputArray circles, int method, // 方法 - HOUGH_GRADIENT double dp, // dp = 1 表示在原图中寻找 double mindist, // 最短距离,可以分辨是两个圆,否则认为是同心圆 double param1, // canny edge detection low threshold double param2, // 中心点累加器阈值 - 候选圆心 int minradius, // 最小半径 int maxradius // 最大半径 )
· 代码演示
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using std::cout; using std::endl; int main(int argc, char** argv) { Mat src, dst; Mat src_gray; src = imread("path"); if(!src.data) { cout << "couldn\'t load image ... " << endl; return -1; } imshow("input_image", src); medianBlur(src, src_gray, 3); cvtColor(src_gray, src_gray, CV_BGR2GRAY); // 霍夫圆检测 vector<Vec3f> pCircle; HoughCircle(src_gray, pCircle, CV_HOUGH_GRADIENT, 1, 10, 100, 30, 5, 50); src.copyTo(dst); for(size_t i = 0; i < pCircle.size(), i++) { Vec3f cc = pCircle[i]; circle(dst, Point(cc[0], cc[1]), cc[2], Scalar(0, 0, 255), 2, LINE_AA); circle(dst, Point(cc[0], cc[1]), 2, Scalar(0, 255, 0), 2, LINE_AA); } imshow("output_image", dst); waitKey(0); return 0; }
20、像素重映射
· 什么是像素重映射:简单的说就是把输入图像中各个像素按照一定规律映射到另一张图像的对应位置上去,像素重映射可以用 g(x, y) = f(h(x, y)) 表示,其中 h 表示关系函数。
· API介绍 cv::remap
cv::remap(InputArray src, OutputArray dst, InputArray map1, // x映射表 CV_32FC1 / CV_32FC2 InputArray map2, // y映射表 int interpolation, // 选择插值算法,常使用线性插值,也可选择立方插值等 int borderMode, // BORDER_CONSTANT const Scalar borderValue // color )
· 代码演示
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using std::cout; using std::endl; int index = 0; void update_map() { for(int row = 0; row < src.rows; row++) { for(int col = 0; col < src.cols; col++) { switch(index) { case 0: if(col > src.cols*0.25 && col <= src.cols*0.75 && row > src.rows*0.25 && row <= src.rows*0.75) { map_x.at<float>(row, col) = (col - src.cols*0.25) * 2; map_y.at<float>(row, col) = (row - src.rows*0.25) * 2; } else { map_x.at<float>(row, col) = 0; map_y.at<float>(row, col) = 0; } break; case 1: map_x.at<float>(row, col) = src.cols - col - 1; map_y.at<float>(row, col) = row; break; case 2: map_x.at<float>(row, col) = col; map_y.at<float>(row, col) = src.rows - row - 1; break; case 3: map_x.at<float>(row, col) = src.cols - col - 1; map_y.at<float>(row, col) = src.rows - row - 1; break; } } } } int main(int argc, char** argv) { Mat src, dst, map_x, map_y; src = imread("path"); if(!src.data) { cout << "couldn\'t load image .. " << endl; return -1; } imshow("input_img", src); // 建立映射表 map_x.create(src.size(), CV_32FC1); map_y.create(src.size(), CV_32FC1); int c = 0; while(true) { c = watKey(500); if(char(c) == 27) break; index = c % 4; update_map(); // 像素映射 remap(src, dst, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 255, 255)); imshow("output_img", dst); } return 0; }
21、直方图均衡化
· 什么是图像直方图与直方图归一化:是指对整个图像在灰度范围内的像素值统计出现次数,据此统计次数生成的直方图称之为图像直方图(这个概念想必学习过图像处理课程的朋友们应该是了如指掌)。至于直方图归一化,也是非常好理解的一个概念,即将单个灰度值出现的次数除以像素总数将直方图纵坐标数值转换到 0 - 1 之间,以此来减小数据量。由上面两个概念我们应该注意到,我们只会对灰度图像计算其直方图。
· 直方图均衡化:是一种提高图像对比度的方法,拉伸图像灰度值范围,即将随机分布的图像直方图修改成均匀分布的直方图。基本思想是对原始图像的像素灰度做某种映射变换, 使变换后图像灰度的概率密度呈均匀分布。这就意味着图像灰度的动态范围得到了增加, 提高了图像的对比度。
· API说明 cv::equalizeHist
cv::equalizeHist(InputArray, src, // 输入图像必须是8bit灰度图像 OutputArray dst )
· 代码演示
#include <iostream> #include <opencv2/opencv.hpp> using namespace cv; using std::endl; using std::cout; int main() { Mat src, src_gray, dst; src = imread("path"); if(!src.data) { cout << "couldn\'t load image." << endl; return -1 } imshow("input_img", src); cvtColor(src, src_gray, CV_BGR2GRAY); equalizeHist(src_gray, dst); imshow("output_img", dst); waitKey(0); return 0; }
22、直方图绘制
· API说明:cv::calcHist
void cv::calcHist(const Mat *images, // 任意数量通道图像组 int nimages, // 图像数量 const int *channels, // 用于计算直方图的dims通道列表 InputArray mask, // 可选掩码,如果矩阵不为空,它必须是images[]相同大小的8为数组 OutputArray hist, // 输出直方图 int dims, // 直方图维度,必须为正数且不能大于CV_MAX_DIMS const int *histSize, // 每个维度中的直方图大小数组 const float **ranges, // 每个维度中直方图边界的dims数组的数组 bool uniform=true, // 表示直方图是否均匀的标志 bool accumulate=false // 累积标志,若设置为true,分配的直方图不会被清除,用于及时更新直方图 )
· 代码示例
单通道图像直方图绘制:
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; int main() { Mat srcImage = imread("dog.bmp", 0); // 直接将图像读取成灰度图像 imshow("原图",srcImage); if(!srcImage.data) { cout << "fail to load image" << endl; return 0; } //定义变量 Mat dstHist; int dims = 1; float hranges[] = {0, 256}; const float *ranges[] = { hranges }; // 这里需要为const类型 int size = 256; int channels = 0; //计算图像的直方图 calcHist(&srcImage, 1, &channels, Mat(), dstHist, dims, &size, ranges); Mat dstImage(size, size, CV_8U, Scalar(0)); //获取最大值和最小值 double minValue = 0; double maxValue = 0; minMaxLoc(dstHist,&minValue, &maxValue, 0, 0); // 在cv中用的是cvGetMinMaxHistValue //绘制出直方图 int hpt = saturate_cast<int>(0.9 * size); for(int i = 0; i < 256; i++) { float binValue = dstHist.at<float>(i); // 注意hist中是float类型 //拉伸到0-max int realValue = saturate_cast<int>(binValue * hpt/maxValue); line(dstImage, Point(i, size - 1), Point(i, size - realValue), Scalar(255)); } imshow("单通道直方图", dstImage); waitKey(0); return 0; }
三通道图像直方图绘制:
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; int main( ) { Mat srcImage; srcImage = imread("dog.bmp"); imshow( "原图", srcImage ); int bins = 256; int hist_size[] = {bins}; float range[] = { 0, 256 }; const float* ranges[] = { range}; MatND redHist,grayHist,blueHist; int channels_r[] = {0}; //进行直方图的计算(红色分量部分) calcHist( &srcImage, 1, channels_r, Mat(), //不使用掩膜 redHist, 1, hist_size, ranges, true, false ); //进行直方图的计算(绿色分量部分) int channels_g[] = {1}; calcHist( &srcImage, 1, channels_g, Mat(), // do not use mask grayHist, 1, hist_size, ranges, true, // the histogram is uniform false ); //进行直方图的计算(蓝色分量部分) int channels_b[] = {2}; calcHist( &srcImage, 1, channels_b, Mat(), // do not use mask blueHist, 1, hist_size, ranges, true, // the histogram is uniform false ); //参数准备 double maxValue_red,maxValue_green,maxValue_blue; minMaxLoc(redHist, 0, &maxValue_red, 0, 0); minMaxLoc(grayHist, 0, &maxValue_green, 0, 0); minMaxLoc(blueHist, 0, &maxValue_blue, 0, 0); int scale = 1; int histHeight=256; Mat histImage = Mat::zeros(histHeight, bins*3, CV_8UC3); //正式开始绘制 for(int i=0; i < bins; i++) { //参数准备 float binValue_red = redHist.at<float>(i); float binValue_green = grayHist.at<float>(i); float binValue_blue = blueHist.at<float>(i); int intensity_red = cvRound(binValue_red*histHeight/maxValue_red); //要绘制的高度 int intensity_green = cvRound(binValue_green*histHeight/maxValue_green); //要绘制的高度 int intensity_blue = cvRound(binValue_blue*histHeight/maxValue_blue); //要绘制的高度 //绘制红色分量的直方图 line(histImage,Point(i,histHeight-1),Point(i, histHeight - intensity_red),CV_RGB(255,0,0)); //绘制绿色分量的直方图 line(histImage,Point(i+bins,histHeight-1),Point(i+bins, histHeight - intensity_green),CV_RGB(0,255,0)); //绘制蓝色分量的直方图 line(histImage,Point(i+bins*2,histHeight-1),Point(i+bins*2, histHeight - intensity_blue),CV_RGB(0,0,255)); } imshow( "图像的BGR直方图", histImage ); waitKey(0);
return 0; }
23、直方图比较
· 概述
对输入的两张图像计算得到直方图H1和H2,归一化到相同的尺度空间然后可以通过计算H1与H2的之间的距离得到两个直方图的相似程度进而比较图像本身的相似程度,opencv提供的比较方法有四种:相关性比较(Correlation)、卡方比较(Chi-Square)、十字交叉性(Intersection)、巴氏距离(Bhattacharyya distance)。
· 相关性计算(CV_COMP_CORREL)
其中
其中N试直方图的BIN个数。最终计算的到的数值越接近1表示两个图像直方图相关性强,相关系数的值若为正值,称为正相关;相关系数的值若为负值,称为负相关。如图:
· 卡方计算(CV_COMP_CHISQR)
卡方比较和相关性比较恰恰相反,相关性比较的值为0,相似度最低,越趋近于1,相似度越低;卡方比较则是,值为0时说明H1= H2,这个时候相似度最高。
· 十字交叉计算(CV_COMP_INTERSECT)
· 巴氏距离计算(CV_COMP_BHATTACHARYYA)
巴氏距离的计算结果,其值完全匹配为1,完全不匹配则为0。一般来说,在直方图相似度计算时,巴氏距离获得的效果最好,但计算是最为复杂的。
· 直方图比较流程
1、首先把图像从BGR色彩空间转换到HSV色彩空间:cv::cvtColor
2、计算图像直方图并归一化:cv::calcHist
、cv::normalize
3、选择使用上述四种方法进行比较:cv::compareHist
double cv::compareHist(InputArray h1, // 直方图数据 InputArray h2, int method // 比较方法 )
· 代码演示
#include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; string convertToString(double d) { ostringstream os; if (os << d) return os.str(); return "invalid conversion"; } int main(int argc, char** argv) { Mat base, test1, test2; Mat hsvbase, hsvtest1, hsvtest2; base = imread(image_path_1); if (!base.data) { printf("could not load image...\n"); return -1; } test1 = imread(image_path_2); test2 = imread(iamge_path_3); //从RGB空间转换到HSV空间 cvtColor(base, hsvbase, CV_BGR2HSV); cvtColor(test1, hsvtest1, CV_BGR2HSV); cvtColor(test2, hsvtest2, CV_BGR2HSV); //计算直方图并归一化 int h_bins = 50; int s_bins = 60; int histSize[] = { h_bins, s_bins }; // hue varies from 0 to 179, saturation from 0 to 255 float h_ranges[] = { 0, 180 }; float s_ranges[] = { 0, 256 }; const float* ranges[] = { h_ranges, s_ranges }; // Use the o-th and 1-st channels int channels[] = { 0, 1 }; MatND hist_base; MatND hist_test1; MatND hist_test2; calcHist(&hsvbase, 1, channels, Mat(), hist_base, 2, histSize, ranges, true, false); normalize(hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat()); calcHist(&hsvtest1, 1, channels, Mat(), hist_test1, 2, histSize, ranges, true, false); normalize(hist_test1, hist_test1, 0, 1, NORM_MINMAX, -1, Mat()); calcHist(&hsvtest2, 1, channels, Mat(), hist_test2, 2, histSize, ranges, true, false); normalize(hist_test2, hist_test2, 0, 1, NORM_MINMAX, -1, Mat()); //比较直方图,并返回值 double basebase = compareHist(hist_base, hist_base, CV_COMP_INTERSECT); double basetest1 = compareHist(hist_base, hist_test1, CV_COMP_INTERSECT); double basetest2 = compareHist(hist_base, hist_test2, CV_COMP_INTERSECT); double tes1test2 = compareHist(hist_test1, hist_test2, CV_COMP_INTERSECT); Mat test12; test2.copyTo(test12); putText(base, convertToString(basebase), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA); putText(test1, convertToString(basetest1), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA); putText(test2, convertToString(basetest2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA); putText(test12, convertToString(tes1test2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA); namedWindow("base", CV_WINDOW_AUTOSIZE); namedWindow("test1", CV_WINDOW_AUTOSIZE); namedWindow("test2", CV_WINDOW_AUTOSIZE); imshow("base", base); imshow("test1", test1); imshow("test2", test2); imshow("test12", test12); waitKey(0); return 0; }
24、直方图反向投影
· 概念介绍
反向投影是反应直方图模型在目标图像中的分布情况,简单点来说就是用直方图模型去目标图像中寻找是否有相似的对象,通常使用HSV色彩空间的HS两个通道直方图模型。一般情况下,我们可以通过反向投影来实现图像分割、背景与对象分离、对已知对象位置进行定位。反向投影在模式识别、对象识别、视频跟踪中均有应用。
· 反向投影实现步骤
1、建立直方图模型
2、计算待检测图像直方图并映射到模型中
3、从模型反向计算生成图像
· 相关API说明
1、数据结构补充说明:cv::MatND
,该数据结构与cv::Mat
的差异在于cv::MatND
表示三维或多维数据,而在下面将要展示的代码中,cv::MatND
都可以用cv::Mat
代替
2、计算反向投影图像:cv::calcBackProject
// cv::calcBackProject()为重载方法,共有三种形式,根据传入参数不同会选择不同的调用 void cv::calcBackProject(const Mat *images, // 输入图像,图像深度必须为CV_8U、CV_16U或CV_32F中的一种,通道数不限 int nimages, // 输入图像数量 const int *channels, // 用于计算反向投影的通道列表,通道数必须与直方图维度相匹配 InputArray hist, // 输入直方图,直方图的bin可以是密集或者稀疏 OutputArray backProject, // 目标反向投影输出图像,单通道图像,尺寸和深度与原图像相同 const float **ranges, // 直方图中每个维度bin的取值范围 double scale=1, // 可选输出反向投影的比例因子 bool uniform=true // 直方图是否均匀分布的标识符 )
· 代码展示
#include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; Mat src, hsv, hue; int bins = 12; void hist_and_backProjecttion(int, void*) { int range[] = { 0, 180 }; const float *histRanges = { range }; Mat h_hist; calcHist(&hue, 1, 0, Mat(), h_hist, 1, &bins, &histRanges, true, false); normalize(h_hist, h_hist, 0, 255, NORM_MINMAX, -1, Mat()); Mat backPrjImage; calcBackProject(&hue, 1, 0, h_hist, backPrjImage, &histRanges); int hist_h = 400; int hist_w = 400; int bin_w = hist_w / bins; Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0)); for(int i = 1; i < bins; i++) { rectangle(histImage, Point((i - 1) * bin_w, hist_h - cvRound(h_hist.at<float>(i) * (400 / 255))), Point(i * bin_w, hist_h), Scalar(0, 0, 255), -1); } imshow("BackProjection", backPrjImage); imshow("Histogram", histImage); } int main() { src = imread("path"); if(src.empty()) { cout << "done" << endl; return -1; } cvtColor(src, hsv, CV_BGR2HSV); hue.create(hsv.size(), hsv.depth()); int nchannels[] = { 0, 0 }; mixChannels(&hsv, 1, &hue, 1, nchannels, 1); createTrackbar("Histogram bins:", "原图", &bins, 180, hist_and_backProjection); hist_and_backProjection(0, 0); imshow("原图", src); waitKey(0); return 0; }
25、模板匹配
· 模板匹配概念
简单来说就是使用一个模板对一幅图像做“伪卷积”,这里我所使用的“伪卷积”概念是在使用模板对图像进行遍历的过程中,不进行卷积运算,而只是去匹配(计算)模板与重叠的子图像是否满足一种相似关系。模板匹配是在一幅图像中找寻一个特定目标的方法之一,在匹配过程中,当图像某块子图像与模板相似度高,则我们认为找到了目标。
· 几种常见的相似度计算方法
1、差值平方和匹配(CV_TM_SQDIFF)
这类方法利用图像与模板各个像素差值的平方和来进行匹配,最好匹配的计算结果为0,匹配越差,计算结果越大。
2、相关匹配_1(CV_TM_CCORR)
这类方法采用模板和图像的相关计算作为相似度的度量方法,所以较大的数表示匹配程度较高,0表示最坏匹配效果。
3、标准化差值平方和匹配(CV_TM_SQDIFF_NORMED)
该方法类似于CV_TM_SQDIFF,只不过对其结果进行了标准化操作,这种标准化操作可以保证当模板和图像各个像素的亮度都乘上了同一个系数时,相关度不发生变化,也就是说当 I(x,y)和T(x,y) 变为kI(x,y)和kT(x,y)时,R(x,y)不发生变化。
4、标准相关匹配_1(CV_TM_CCORR_NORMED)
与上述类似,增加标准化计算都是去除亮度线性变化对相似度计算的影响,保证图像和模板同时变亮或者变暗相同倍数时结果不变。
5、相关匹配_2(CV_TM_CCOEFF)
这种方法也叫做相关匹配,但是和上面的 CV_TM_CCORR 匹配方法还是有不同的。简单的说,这里是把图像和模板都减去了各自的平均值,使得这两幅图像都没有直流分量。
6、标准相关匹配_2(CV_TM_CCOEFF_NORMED)
这是 OpenCV 支持的最复杂的一种相似度算法。这里的相关运算就是数理统计学科的相关系数计算方法。具体的说,就是在减去了各自的平均值之外,还要各自除以各自的方差。经过减去平均值和除以方差这么两步操作之后,无论是我们的待检图像还是模板都被标准化了,这样可以保证图像和模板分别改变光照亮不影响计算结果。计算出的相关系数被限制在了 -1 到 1 之间,1 表示完全相同,-1 表示两幅图像的亮度正好相反,0 表示两幅图像之间没有线性关系。
· API介绍
void cv::matchTemplate(InputArray image, // 输入图像,必须是8位或者32位浮点类型 InputArray templ, // 模板,不可大于image,并具有相同类型 OutputArray result, // 比较结果的映射,必须是单通道32位浮点 int method, // 相似度计算方法 InputArray mask=noArray() // 搜索模板的掩码,必须与templ有相同的数据类型和尺寸,默认情况下不设置 )
· 代码演示
#include<iostream> #include<opencv2/opencv.hpp> using namespace std; using namespace cv; Mat src, templ, result; char* src_window = "原图"; char* result_window = "结果"; int match_method; int max_Trackbar = 5; void MatchingMethod(int, void*) { // 将被显示的原图像 Mat src_display; src.copyTo(src_display); // 创建输出结果的矩阵 int result_cols = src.cols - templ.cols + 1; int result_rows = src.rows - templ.rows + 1; result.create(result_cols, result_rows, CV_32FC1); // 进行匹配和标准化 matchTemplate(src, templ, result, match_method); normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat()); // 通过函数 minMaxLoc 定位最匹配的位置 double minVal; double maxVal; Point minLoc; Point maxLoc; Point matchLoc; minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat()); // 对于方法 SQDIFF 和 SQDIFF_NORMED, 越小的数值代表更高的匹配结果. 而对于其他方法, 数值越大匹配越好 if (match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED) matchLoc = minLoc; else matchLoc = maxLoc; rectangle(src_display, matchLoc, Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), Scalar(0,0,255), 2, 8, 0); rectangle(result, matchLoc, Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), Scalar(0, 0, 255), 2, 8, 0); imshow(src_window, src_display); imshow(result_window, result); } int main() { src = imread("path"); templ = imread("path"); if (!src.data || !templ.data) { cout << "could not load image !"; return -1; } namedWindow(src_window, CV_WINDOW_AUTOSIZE); namedWindow(result_window, CV_WINDOW_AUTOSIZE); imshow(src_window, src); // 创建滑动条 char* trackbar_label = "Method"; createTrackbar(trackbar_label, src_window, &match_method, max_Trackbar, MatchingMethod); MatchingMethod(0, 0); waitKey(0); return 0; }
· 概念介绍
轮廓发现是基于图像边缘提取的基础寻找对象轮廓的方法,所以边缘提取的阈值选定会影响最终轮廓发现结果。其中我们所说的轮廓是一系列相连的点组成的曲线,代表了物体的基本外形,相对于边缘,轮廓是连续的,边缘并不全部连续。如图:
· API介绍
1、发现轮廓:cv::findContours
void cv::findContours(InputOutputArray binImg, // 输入图像,非0像素被看成1,0像素保持不变,8-bit OutputArrayOfArrays contours, // 所发现的全部轮廓对象 OutputArray hierachy, // 该图的拓扑结构,可选,该轮廓发现算法正是基于图像拓扑结构实现 int mode, // 轮廓返回的模式 int method, // 发现方法 Point offset=Point() // 轮廓像素的位移默认(0,0) ) /* mode参数:轮廓检索模式,可以通过cv::RetrievalModes()查看详细信息 1、RETR_EXTERNAL:表示只检测最外层轮廓,对所有轮廓设置hierarchy[i][2]=hierarchy[i][3]=-1 2、RETR_LIST:提取所有轮廓,并放置在list中,检测的轮廓不建立等级关系 3、RETR_CCOMP:提取所有轮廓,并将轮廓组织成双层结构(two-level hierarchy),顶层为连通域的外围边界,次层位内层边界 4、RETR_TREE:提取所有轮廓并重新建立网状轮廓结构 5、RETR_FLOODFILL:官网没有介绍,应该是洪水填充法 method参数:轮廓近似方法可以通过cv::ContourApproximationModes()查看详细信息 1、CHAIN_APPROX_NONE:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1 2、CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息 3、CHAIN_APPROX_TC89_L1和CHAIN_APPROX_TC89_KCOS使用Teh-Chinl链逼近算法中的一种 */
2、轮廓绘制:cv::drawContours
// 绘制轮廓,一次只能根据轮廓索引号绘制一次,需要循环所有发现的轮廓 void cv::drawContours(InputOutputArray binImg, // 输出图像 OutputArrayOfArrays contours, // 全部发现的轮廓对象,包含points的vectors的vector int contourIdx, // 轮廓索引号 const Scalar &color, // 绘制时候颜色 int thickness, // 绘制线宽,如果传-1表示填充轮廓 int lineType, // 线的类型LINE_8 InputArray hierarchy, // 拓扑结构图 int maxlevel, // 最大层数,0只绘制当前的,1表示绘制绘制当前及其内嵌的轮廓 Point offset=Point() // 轮廓位移,可选 )
· 实现步骤
1、输入图像转化为灰度图像
2、使用Canny进行边缘提取,得到二值图像
3、使用findContours寻找轮廓
4、使用drawContours绘制轮廓
· 代码展示
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; Mat src, src_gray; int thresh = 100; int max_thresh = 255; RNG rng(12345); void thresh_callback(int, void*) { Mat canny_output; vector<vector<Point>> contours; // 每个轮廓由一系列点组成 vector<Vec4i> hierarchy; // 用Canny算子检测边缘 Canny(src_gray, canny_output, thresh, thresh*2, 3); // 寻找轮廓 findContours(canny_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0)); // 发现轮廓 // 因为要用不同颜色区分轮廓,所以这里用的CV_8UC3 3通道彩色图,如果是CV_8UC1 绘制的就是灰度图,感官上不好区分 Mat drawing = Mat::zeros(canny_output.size(), CV_8UC3); for(int i = 0; i< contours.size(); i++) { Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255)); // 随机颜色 drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point()); // 颜色一致的就属同一发现的轮廓 } // 在窗体中显示结果 namedWindow("Contours", CV_WINDOW_AUTOSIZE); imshow("Contours", drawing); } int main(int argc, char** argv) { src = imread("path"); // 转成灰度并模糊化降噪 cvtColor(src, src_gray, CV_BGR2GRAY); blur(src_gray, src_gray, Size(3,3)); // 创建窗体 char* source_window = "Source"; namedWindow(source_window, CV_WINDOW_AUTOSIZE); imshow(source_window, src); createTrackbar("Canny thresh:", "Source", &thresh, max_thresh, thresh_callback); thresh_callback(0, 0); waitKey(0); return 0; }
27、凸包
· 概念介绍
凸包(Convex Hull)是一个计算几何(图形学)中的概念,它的严格的数学定义为:在一个向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。简单来讲包含点集合S中所有点的最小凸多边形称为凸包。如图:
· Graham扫描算法
Graham扫描法通过不断在凸壳中加入新的点和去除影响凸性的点,最后形成凸包。算法的主体由两部分组成,先是排序,然后扫描。
1、首先选择Y方向最低点作为起始点p0
2、从p0开始极坐标扫描,依次添加p1...pn(排列顺序是根据极坐标的角度大小,逆时针方向排列)
3、对每个点pi来说,如果添加pi点到凸包中导致一个左转向(逆时针方法)则添加该点到凸包集合中,反之如果导致右转向(顺时针方向)则从凸包中删除该点
如图:
· API介绍
void cv::convexHull(InputArray points, // 输入的二维点集,Mat类型数据即可 OutputArray hull, // 输出参数,用于输出函数调用后找到的凸包 bool clockwise=false, // 操作方向,当标识符为真时,输出凸包为顺时针方向,否则为逆时针方向 bool returnPoints=true // 操作标识符,默认为true,此时返回各凸包的点,否则返回凸包各点的指数,当输出数组为std::vector时,此标识被忽略 )
· 实现步骤
1、把图像从RGB图像转到灰度图,再由灰度图转换到二值图
2、通过轮廓发现方法找到候选点集合
3、使用cv::convexHull
寻找凸包
4、绘制显示
· 代码演示
#include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; Mat srcImage, grayImage; int thresh = 100; const int threshMaxValue = 255; RNG rng(12345); //定义回调函数 void thresh_callback(int, void*) { Mat src_copy = srcImage.clone(); Mat threshold_output; vector<vector<Point>>contours; vector<Vec4i>hierarchy; //使用Threshold检测图像边缘 threshold(grayImage, threshold_output, thresh, 255, THRESH_BINARY); //寻找图像轮廓 findContours(threshold_output, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //寻找图像凸包 vector<vector<Point>>hull(contours.size()); for (int i = 0; i < contours.size(); i++) convexHull(Mat(contours[i]), hull[i], false); //绘制轮廓和凸包 Mat drawing = Mat::zeros(threshold_output.size(), CV_8UC3); for (int i = 0; i < contours.size(); i++) { Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); drawContours(drawing, contours, i, color, 1, 8, vector<Vec4i>(), 0, Point()); drawContours(drawing, hull, i, color, 1, 8, vector<Vec4i>(), 0, Point()); } namedWindow("凸包", WINDOW_AUTOSIZE); imshow("凸包", drawing); } int main() { srcImage = imread("path"); if (srcImage.empty()) { cout << "图像加载失败" << endl; return -1; } //图像灰度图转化并平滑滤波 cvtColor(srcImage, grayImage, COLOR_BGR2GRAY); blur(grayImage, grayImage, Size(3, 3)); namedWindow("原图像", WINDOW_AUTOSIZE); imshow("原图像", grayImage); //创建轨迹条 createTrackbar("Threshold:", "原图像", &thresh, threshMaxValue, thresh_callback); thresh_callback(thresh, 0); waitKey(0); return 0; }