博文末尾支持二维码赞赏哦 _
形态学操作就是基于形状的一系列图像处理操作。
通过将 结构元素 作用于输入图像来产生输出图像。
最基本的形态学操作有二:
腐蚀与膨胀(Erosion 与 Dilation)。
他们的运用广泛:
消除噪声
分割(isolate)独立的图像元素,以及连接(join)相邻的元素。
寻找图像中的明显的极大值区域或极小值区域。 连通域
【1】膨胀Dilation
选择核内部的最大值(值越大越亮 约白)
此操作将图像 A 与任意形状的内核 (B),通常为正方形或圆形,进行卷积。
内核 B 有一个可定义的 锚点, 通常定义为内核中心点。
进行膨胀操作时,将内核 B 划过图像,将内核 B 覆盖区域的最大相素值提取,
并代替锚点位置的相素。显然,这一最大化操作将会导致图像中的亮区开始”扩展”
(因此有了术语膨胀 dilation )。
背景(白色)膨胀,而黑色字母缩小了。
【2】腐蚀 Erosion
选择核内部的最小值(值越小越暗 约黑)
腐蚀在形态学操作家族里是膨胀操作的孪生姐妹。它提取的是内核覆盖下的相素最小值。
进行腐蚀操作时,将内核 B 划过图像,将内核 B 覆盖区域的最小相素值提取,并代替锚点位置的相素。
以与膨胀相同的图像作为样本,我们使用腐蚀操作。
从下面的结果图我们看到亮区(背景)变细,而黑色区域(字母)则变大了。
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
#include
using namespace std;
using namespace cv;
// 全局变量
Mat src, erosion_dst, dilation_dst;
int erosion_elem = 0;
int erosion_size = 0;
int dilation_elem = 0;
int dilation_size = 0;
int const max_elem = 2;
int const max_kernel_size = 21;
//窗口名字
string Erosion_w("Erosion 腐蚀 Demo");
string Dilation_w("Dilation 膨胀 Demo");
//函数声明
void Erosion( int, void* );
void Dilation( int, void* );
int main( int argc, char** argv )
{
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
namedWindow( Erosion_w, WINDOW_AUTOSIZE );
namedWindow( Dilation_w, WINDOW_AUTOSIZE );
moveWindow( Dilation_w, src.cols, 0 );//新建一个
// 创建腐蚀 Trackbar
createTrackbar( "Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", Erosion_w,
&erosion_elem, max_elem,// 滑动条 动态改变参数 erosion_elem 核窗口形状
Erosion );//回调函数 Erosion
createTrackbar( "Kernel size:\n 2n +1", Erosion_w,
&erosion_size, max_kernel_size,// 滑动条 动态改变参数 erosion_size 窗口大小
Erosion );//回调函数 Erosion
// 创建膨胀 Trackbar
createTrackbar( "Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", Dilation_w,
&dilation_elem, max_elem,// 滑动条 动态改变参数 dilation_elem 核窗口形状
Dilation );//回调函数 Dilation
createTrackbar( "Kernel size:\n 2n +1", Dilation_w,
&dilation_size, max_kernel_size,// 滑动条 动态改变参数 dilation_size 窗口大小
Dilation );//回调函数 Dilation
// 默认 开始参数 长方形核 1核子大小
Erosion( 0, 0 );
Dilation( 0, 0 );
waitKey(0);//等待按键
return 0;
}
// 腐蚀操作
void Erosion( int, void* )
{
int erosion_type = 0;
if( erosion_elem == 0 ){ erosion_type = MORPH_RECT; }// 矩形
else if( erosion_elem == 1 ){ erosion_type = MORPH_CROSS; }// 交叉形
else if( erosion_elem == 2) { erosion_type = MORPH_ELLIPSE; }// 椭圆形
Mat element = getStructuringElement( erosion_type,//核形状
Size( 2*erosion_size + 1, 2*erosion_size+1 ),//核大小
Point( erosion_size, erosion_size ) );//锚点 默认锚点在内核中心位置
erode( src, erosion_dst, element );
imshow( Erosion_w, erosion_dst );
}
// 膨胀操作
void Dilation( int, void* )
{
int dilation_type = 0;
if( dilation_elem == 0 ){ dilation_type = MORPH_RECT; }// 矩形
else if( dilation_elem == 1 ){ dilation_type = MORPH_CROSS; }// 交叉形
else if( dilation_elem == 2) { dilation_type = MORPH_ELLIPSE; }// 椭圆形
Mat element = getStructuringElement( dilation_type,//核形状
Size( 2*dilation_size + 1, 2*dilation_size+1 ),//核大小
Point( dilation_size, dilation_size ) );//锚点 默认锚点在内核中心位置
dilate( src, dilation_dst, element );
imshow( Dilation_w, dilation_dst );
}
/*
利用形态学操作提取水平线和垂直线
腐蚀膨胀来提取线特征
*/
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
string imageName("../../common/data/notes.png"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
Mat src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
// 显示图像
imshow("src", src);
// 得到灰度图
Mat gray;
if (src.channels() == 3)//如果原图是彩色图
{
cvtColor(src, gray, CV_BGR2GRAY);//转换到灰度图
}
else
{
gray = src;
}
// 显示灰度图像
imshow("gray", gray);
// 灰度图二值化 ~ symbol
Mat bw;
// 原图取反 输出 最大 自适应方法阈值 阈值类型 块大小
adaptiveThreshold(~gray, bw, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
// THRESH_BINARY 大于阈值的都变为 255最大值 其余变为 0
// THRESH_BINARY_INV 小于阈值的都变为 255最大值 其余变为 0
// 显示二值图
imshow("binary", bw);
// 创建水平线和垂直线图像
Mat horizontal = bw.clone();
Mat vertical = bw.clone();
//========水平线提取 参考列数=====================
int horizontalsize = horizontal.cols / 30;
// 水平线 提取框 核子 窗口大小
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontalsize,1));
// 腐蚀+膨胀 = 开运算 (Opening) 去除 小型 白洞 保留水平白线
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));
// 显示水平线
imshow("horizontal", horizontal);
//========垂直线提取==========================
int verticalsize = vertical.rows / 30;//
// 核子 窗口大小
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size( 1,verticalsize));
// 腐蚀+膨胀 = 开运算 (Opening)
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));
// 显示垂直线
imshow("vertical", vertical);
// 垂直线图 反向二值化
bitwise_not(vertical, vertical);
imshow("vertical_bit", vertical);
// Extract edges and smooth image according to the logic
// 1. extract edges
// 2. dilate(edges)
// 3. src.copyTo(smooth)
// 4. blur smooth img
// 5. smooth.copyTo(src, edges)
//Step 1 提取边缘
Mat edges;
adaptiveThreshold(vertical, edges, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
imshow("edges", edges);
// Step 2 膨胀操作
Mat kernel = Mat::ones(2, 2, CV_8UC1);//核大小
dilate(edges, edges, kernel);
imshow("dilate", edges);
// Step 3 得到平滑图像
Mat smooth;
vertical.copyTo(smooth);
// Step 4 平滑图像
blur(smooth, smooth, Size(2, 2));
// Step 5
smooth.copyTo(vertical, edges);
// Show final result
imshow("smooth", vertical);
waitKey(0);
return 0;
}
图像阈值操作
最简单的图像分割的方法。
应用举例:从一副图像中利用阈值分割出我们需要的物体部分
(当然这里的物体可以是一部分或者整体)。
这样的图像分割方法是基于图像中物体与背景之间的灰度差异,而且此分割属于像素级的分割。
为了从一副图像中提取出我们需要的部分,应该用图像中的每一个
像素点的灰度值与选取的阈值进行比较,并作出相应的判断。
(注意:阈值的选取依赖于具体的问题。即:物体在不同的图像中有可能会有不同的灰度值。
一旦找到了需要分割的物体的像素点,我们可以对这些像素点设定一些特定的值来表示。
(例如:可以将该物体的像素点的灰度值设定为:‘0’(黑色),
其他的像素点的灰度值为:‘255’(白色);当然像素点的灰度值可以任意,
但最好设定的两种颜色对比度较强,方便观察结果)。
【1】阈值类型1:二值阈值化
大于阈值的 设置为最大值 255 其余为0
先要选定一个特定的阈值量,比如:125,
这样,新的阈值产生规则可以解释为大于125的像素点的灰度值设定为最大值(如8位灰度值最大为255),
灰度值小于125的像素点的灰度值设定为0。
【2】阈值类型2:反二进制阈值化
小于阈值的 设置为最大值 255 其余为0
【3】阈值类型3:截断阈值化
大于阈值的 设置为阈值 其余保持原来的值
【4】阈值类型4:阈值化为0
大于阈值的 保持原来的值 其余设置为0
【5】阈值类型5:反阈值化为0
大于阈值的 设置为0 其余保持原来的值
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
using namespace cv;
using namespace std;
// 全局变量定义及赋值
int threshold_value = 0;
int threshold_type = 3;
int const max_value = 255;
int const max_type = 4;
int const max_BINARY_value = 255;
Mat src, src_gray, dst;
const char* window_name = "Threshold Demo";//窗口名
// 滑动条显示
const char* trackbar_type = "Type: \n 0: Binary \n 1: Binary Inverted \n 2: Truncate \n 3: To Zero \n 4: To Zero Inverted";
const char* trackbar_value = "Value";
//阈值
void Threshold_Demo( int, void* );
int main( int argc, char** argv )
{
string imageName("../../common/data/notes.png"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
//转成灰度图
cvtColor( src, src_gray, COLOR_RGB2GRAY );
//显示
namedWindow( window_name, WINDOW_AUTOSIZE );
// 阈值类型
createTrackbar( trackbar_type,
window_name, &threshold_type,
max_type, Threshold_Demo );
// 阈值大小
createTrackbar( trackbar_value,
window_name, &threshold_value,
max_value, Threshold_Demo );
// 初始化为
Threshold_Demo( 0, 0 );
//检测按键
for(;;)
{
int c;
c = waitKey( 20 );
if( (char)c == 27 )//esc键退出
{ break; }
}
}
void Threshold_Demo( int, void* )
{
/* 0: Binary 二值
1: Binary Inverted 二值反
2: Threshold Truncated 截断
3: Threshold to Zero 阈值化为0 大于阈值的 保持原来的值 其余设置为0
4: Threshold to Zero Inverted 大于阈值的 设置为0 其余保持原来的值
*/
threshold( src_gray, dst, threshold_value, max_BINARY_value,threshold_type );
imshow( window_name, dst );
}
【1】卷积
高度概括地说,卷积是在每一个图像块与某个算子(核)之间进行的运算。
【2】核是什么?
核说白了就是一个固定大小的数值数组。该数组带有一个 锚点 ,一般位于数组中央。
自定义滤波器核
用OpenCV函数 filter2D 创建自己的线性滤
kernel = Mat::ones( kernel_size, kernel_size, CV_32F )/ (float)(kernel_size*kernel_size);
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
using namespace cv;
using namespace std;
//主函数
int main ( int argc, char** argv )
{
/// 声明变量
Mat src, dst;
Mat kernel;
Point anchor;
double delta;
int ddepth;
int kernel_size;
char* window_name = "filter2D Demo";
int c;
string imageName("../../common/data/notes.png"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 创建窗口
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
/// 初始化滤波器参数
anchor = Point( -1, -1 );//锚点
delta = 0;// 偏置 delta: 在卷积过程中,该值会加到每个像素上。默认情况下,这个值为 0
ddepth = -1;
// ddepth: dst 的深度。若为负值(如 -1 ),则表示其深度与源图像相等。
/// 循环 - 每隔0.5秒,用一个不同的核来对图像进行滤波
int ind = 0;
while( true )
{
c = waitKey(500);
/// 按'ESC'可退出程序
if( (char)c == 27 )
{ break; }
/// 更新归一化块滤波器的核大小
kernel_size = 3 + 2*( ind%5 );//ind%5 0,1,2,3,4
// 核的大小 设置为 [3,11] 范围内的奇数
// 第二行代码把1填充进矩阵,并执行归一化——除以矩阵元素数——以构造出所用的核。
kernel = Mat::ones( kernel_size, kernel_size, CV_32F )/ (float)(kernel_size*kernel_size);
/// 使用滤波器
// ddepth: dst 的深度。若为负值(如 -1 ),则表示其深度与源图像相等。
// delta: 在卷积过程中,该值会加到每个像素上。默认情况下,这个值为 0
filter2D(src, dst, ddepth , kernel, anchor, delta, BORDER_DEFAULT );
imshow( window_name, dst );
ind++;//核子尺寸参数
}
return 0;
}
一个最重要的卷积运算就是导数的计算(或者近似计算).
为什么对图像进行求导是重要的呢? 假设我们需要检测图像中的 边缘 球图像梯度大的地方
【1】Sobel算子
Sobel 算子是一个离散微分算子 (discrete differentiation operator)。
它用来计算图像灰度函数的近似梯度。
Sobel 算子结合了高斯平滑和微分求导。
假设被作用图像为 I:
在两个方向求导:
水平变化: 将 I 与一个奇数大小的内核 G_{x} 进行卷积。比如,当内核大小为3时, G_{x} 的计算结果为:
G_{x} = [-1 0 +1
-2 0 +2
-1 0 +1]
垂直变化: 将:m I 与一个奇数大小的内核 G_{y} 进行卷积。
比如,当内核大小为3时, G_{y} 的计算结果为:
G_{y} = [-1 -2 -1
0 0 0
+1 +2 +1]
在图像的每一点,结合以上两个结果求出近似 梯度:
G = sqrt(GX^2 + GY^2)
Sobel内核
当内核大小为 3 时, 以上Sobel内核可能产生比较明显的误差(毕竟,Sobel算子只是求取了导数的近似值)。
为解决这一问题,OpenCV提供了 Scharr 函数,但该函数仅作用于大小为3的内核。
该函数的运算与Sobel函数一样快,但结果却更加精确,其内核为:
G_{x} = [-3 0 +3
-10 0 +10
-3 0 +3]
G_{y} = [-3 -10 -3
0 0 0
+3 +10 +3]
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
using namespace std;
using namespace cv;
/** @function main */
int main( int argc, char** argv )
{
Mat src, src_gray;
Mat grad;
char* window_name = "Sobel Demo - Simple Edge Detector";
int scale = 1;// 计算导数 放大因子 scale ?
int delta = 0;// 偏置 delta: 在卷积过程中,该值会加到每个像素上。默认情况下,这个值为 0
int ddepth = CV_16S;// ddepth: 输出图像的深度,设定为 CV_16S 避免外溢。
int c;
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
// 高斯平滑 降噪 ( 内核大小 = 3 )
GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );
// 将降噪后的图像转换为灰度图:
cvtColor( src, src_gray, CV_RGB2GRAY );
/// 创建显示窗口
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
/// 创建 水平和垂直梯度图像 grad_x 和 grad_y
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
/// 求 X方向梯度
//Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_x, abs_grad_x );
// ddepth: 输出图像的深度,设定为 CV_16S 避免外溢。
// 偏置 delta: 在卷积过程中,该值会加到每个像素上。默认情况下,这个值为 0
// 计算导数 放大因子 scale ?
/// 求Y方向梯度
//Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_y, abs_grad_y );
/// 合并梯度(近似)
addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad );
imshow( window_name, grad );
waitKey(0);
return 0;
}
图像金字塔
使用OpenCV函数 pyrUp 和 pyrDown 对图像进行向上和向下采样。
然后高斯平滑
当我们需要将图像转换到另一个尺寸的时候, 有两种可能:
放大 图像 或者
缩小 图像。
我们首先学习一下使用 图像金字塔 来做图像缩放, 图像金字塔是视觉运用中广泛采用的一项技术。
图像金字塔:
一个图像金字塔是一系列图像的集合 -
所有图像来源于同一张原始图像 - 通过梯次向下采样获得,直到达到某个终止条件才停止采样。
有两种类型的图像金字塔常常出现在文献和应用中:
【1】 高斯金字塔(Gaussian pyramid): 用来向下采样
【2】 拉普拉斯金字塔(Laplacian pyramid): 用来从金字塔低层图像重建上层未采样图像
在这篇文档中我们将使用 高斯金字塔 。
高斯金字塔:想想金字塔为一层一层的图像,层级越高,图像越小。
高斯内核:
1/16 [1 4 6 4 1
4 16 24 16 4
6 24 36 24 6
4 16 24 16 4
1 4 6 4 1]
下采样:
将 图像 与高斯内核做卷积
将所有偶数行和列去除。
显而易见,结果图像只有原图的四分之一。
如果将图像变大呢?:
首先,将图像在每个方向扩大为原来的两倍,新增的行和列以0填充(0)
使用先前同样的内核(乘以4)与放大后的图像卷积,获得 “新增像素” 的近似值。
这两个步骤(向下和向上采样) 分别通过OpenCV函数 pyrUp 和 pyrDown 实现,
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
using namespace cv;
/// 全局变量
Mat src, dst, tmp;
char* window_name = "Pyramids Demo";
// 主函数
int main( int argc, char** argv )
{
/// 指示说明
printf( "\n Zoom In-Out demo \n " );
printf( "------------------ \n" );
printf( " * [u] -> Zoom in \n" );
printf( " * [d] -> Zoom out \n" );
printf( " * [ESC] -> Close program \n \n" );
/// 测试图像 - 尺寸必须能被 2^{n} 整除
src = imread("../../common/data/chicky_512.png");
if( !src.data )
{ printf(" No data! -- Exiting the program \n");
return -1; }
tmp = src;
dst = tmp;
/// 创建显示窗口
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
imshow( window_name, dst );
/// 循环 检测 按键响应
while( true )
{
int c;
c = waitKey(10);//获得按键
if( (char)c == 27 )//esc
{ break; }
if( (char)c == 'u' )//上采样
{ pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 ) );
printf( "** Zoom In: Image x 2 \n" );
}
else if( (char)c == 'd' )//下采样
{ pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 ) );
printf( "** Zoom Out: Image / 2 \n" );
}
imshow( window_name, dst );
tmp = dst;
}
return 0;
}
Laplacian 算子 的离散模拟。 图像二阶倒数 梯度的梯度 0值的话 边缘概率较大
Sobel 算子 ,其基础来自于一个事实,即在边缘部分,像素值出现”跳跃“或者较大的变化。
如果在此边缘部分求取一阶导数,你会看到极值的出现。
你会发现在一阶导数的极值位置,二阶导数为0。
所以我们也可以用这个特点来作为检测图像边缘的方法。
但是, 二阶导数的0值不仅仅出现在边缘(它们也可能出现在无意义的位置),
但是我们可以过滤掉这些点。
Laplacian 算子
从以上分析中,我们推论二阶导数可以用来 检测边缘 。
因为图像是 “2维”, 我们需要在两个方向求导。使用Laplacian算子将会使求导过程变得简单。
Laplacian 算子 的定义:
Laplace(f) = df^2/ dx^2 + df^2 / dy^2
OpenCV函数 Laplacian 实现了Laplacian算子。
实际上,由于 Laplacian使用了图像梯度,它内部调用了 Sobel 算子。
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
using namespace std;
using namespace cv;
/** @函数 main */
int main( int argc, char** argv )
{
Mat src, src_gray, dst;
int kernel_size = 3;
int scale = 1;
int delta = 0;
int ddepth = CV_16S;
char* window_name = "Laplace Demo";
int c;
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 使用高斯滤波消除噪声
GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );
/// 转换为灰度图
cvtColor( src, src_gray, CV_RGB2GRAY );
/// 创建显示窗口
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
/// 使用Laplace函数
Mat abs_dst;
Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT );
convertScaleAbs( dst, abs_dst );
// ddepth: 输出图像的深度,设定为 CV_16S 避免外溢。
// kernel_size 卷积核大小
// 偏置 delta: 在卷积过程中,该值会加到每个像素上。默认情况下,这个值为 0
// 计算导数 放大因子 scale ?
// 边界填充 BORDER_DEFAULT 默认使用 复制填充
/// 显示结果
imshow( window_name, abs_dst );
waitKey(0);
return 0;
}
Canny 边缘检测 边缘检测最优算法
综合使用 高斯平滑 soble梯度检测 非极大值抑制 滞后阈值 等操作来检测物体边缘
Canny 边缘检测算法 是 John F. Canny 于 1986年开发出来的一个多级边缘检测算法,
也被很多人认为是边缘检测的 最优算法, 最优边缘检测的三个主要评价标准是:
低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。
最小响应: 图像中的边缘只能标识一次。
步骤::
【1】消除噪声。 使用高斯平滑滤波器卷积降噪。 下面显示了一个 size = 5 的高斯内核示例:
K = 1/159 [2 4 5 4 2
4 9 12 9 4
5 12 15 12 5
4 9 12 9 4
2 4 5 4 2]
【2】计算梯度幅值和方向。
此处,按照Sobel滤波器的步骤:
在两个方向求导:
水平变化: 将 I 与一个奇数大小的内核 G_{x} 进行卷积。比如,当内核大小为3*3时, G_{x} 的计算结果为:
G_{x} = [-1 0 +1
-2 0 +2
-1 0 +1]
垂直变化: 将: I 与一个奇数大小的内核 G_{y} 进行卷积。
比如,当内核大小为3×3时, G_{y} 的计算结果为:
G_{y} = [-1 -2 -1
0 0 0
+1 +2 +1]
在图像的每一点,结合以上两个结果求出近似 梯度:
G = sqrt(GX^2 + GY^2)
梯度角度方向 = arctan(GY/GX)
梯度方向近似到四个可能角度之一(一般 0, 45, 90, 135)
【3】梯度大小非极大值 抑制。 这一步排除非边缘像素, 仅仅保留了一些细线条(候选边缘)。
【4】滞后阈值: 最后一步,Canny 使用了滞后阈值,滞后阈值需要两个阈值(高阈值 和 低阈值):
如果某一像素位置的幅值超过 高 阈值, 该像素被保留为边缘像素。
如果某一像素位置的幅值小于 低 阈值, 该像素被排除。
如果某一像素位置的幅值在两个阈值之间,该像素在连接到一个高于 高阈值的像素时被保留。
Canny 推荐的 高:低 阈值比在 2:1 到3:1之间。
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
#include
using namespace std;
using namespace cv;
/// 全局变量
Mat src, src_gray;
Mat dst, detected_edges;
int edgeThresh = 1;
int lowThreshold;
int const max_lowThreshold = 100;//最大 低阈值
int ratio = 3;// 高低阈值 比值
int kernel_size = 3;//candy 核尺寸
char* window_name = "Edge Map";
// 回调函数 CannyThreshold
//@简介: trackbar 交互回调 - Canny阈值输入比例1:3
void CannyThreshold(int, void*)
{
/// 使用 3x3内核 均值滤波 降噪
blur( src_gray, detected_edges, Size(3,3) );
/// 运行Canny算子
Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
/// 使用 Canny算子输出边缘作为掩码显示原图像
dst = Scalar::all(0);
src.copyTo( dst, detected_edges);
imshow( window_name, dst );
}
/** @函数 main */
int main( int argc, char** argv )
{
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 创建与src同类型和大小的矩阵(dst)
dst.create( src.size(), src.type() );
/// 原图像转换为灰度图像
cvtColor( src, src_gray, CV_BGR2GRAY );
/// 创建显示窗口
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
/// 创建trackbar 滑动条 调节 低阈值参数 回调函数 CannyThreshold
createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
/// 显示图像
CannyThreshold(0, 0);
/// 等待用户反应
waitKey(0);//按键后结束
return 0;
}
霍夫线变换 检测图像中的直线 先candy边缘检测 在找直线
使用OpenCV的以下函数 HoughLines 和 HoughLinesP 来检测图像中的直线.
霍夫线变换
霍夫线变换是一种用来寻找直线的方法.
是用霍夫线变换之前, 首先要对图像进行边缘检测的处理,
也即霍夫线变换的直接输入只能是边缘二值图像.
众所周知, 一条直线在图像二维空间可由两个变量表示. 例如:
在 笛卡尔坐标系: 可由参数: (m,b) 斜率和截距表示. y = m*x + b
在 极坐标系: 可由参数: (r,\theta) 极径和极角表示 r = x * cos(theta) + y*sin(theta)
一般来说, 一条直线能够通过在平面 theta - r 寻找交于一点的曲线数量来 检测.
越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成.
一般来说我们可以通过设置直线上点的 阈值 来定义多少条曲线交于一点我们才认为 检测 到了一条直线.
这就是霍夫线变换要做的. 它追踪图像中每个点对应曲线间的交点.
如果交于一点的曲线的数量超过了 阈值,
那么可以认为这个交点所代表的参数对 (theta, r_{theta}) 在原图像中为一条直线.
标准霍夫线变换和统计概率霍夫线变换
OpenCV实现了以下两种霍夫线变换:
【1】标准霍夫线变换
它能给我们提供一组参数对 (\theta, r_{\theta}) 的集合来表示检测到的直线
在OpenCV 中通过函数 HoughLines 来实现
【2】统计概率霍夫线变换
这是执行起来效率更高的霍夫线变换.
它输出检测到的直线的端点 (x_{0}, y_{0}, x_{1}, y_{1})
在OpenCV 中它通过函数 HoughLinesP 来实现
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
using namespace cv;
using namespace std;
void help()
{
cout << "\nThis program demonstrates line finding with the Hough transform.\n"
"Usage:\n"
"./houghlines , Default is pic1.jpg\n" << endl;
}
int main(int argc, char** argv)
{
const char* filename = argc >= 2 ? argv[1] : "../../common/data/77.jpeg";
Mat src = imread(filename, 0);
if(src.empty())
{
help();
cout << "can not open " << filename << endl;
return -1;
}
Mat dst, cdst;
// 检测边缘
Canny(src, dst, 50, 200, 3);//低阈值 高阈值 核尺寸
cvtColor(dst, cdst, CV_GRAY2BGR);//灰度图
//【1】标准霍夫线变换
#if 0
vector lines;//得到 直线的参数 r theta
HoughLines(dst, lines, 1, CV_PI/180, 100, 0, 0 );
for( size_t i = 0; i < lines.size(); i++ )
{
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line( cdst, pt1, pt2, Scalar(0,0,255), 3, CV_AA);
}
#else
// 【2】统计概率霍夫线变换
vector lines;//直线首尾点
HoughLinesP(dst, lines, 1, CV_PI/180, 100, 50, 10 );
// 以像素值为单位的分辨率. 我们使用 1 像素.
// theta: 参数极角 theta 以弧度为单位的分辨率. 我们使用 1度 (即CV_PI/180)
// threshold: 要”检测” 一条直线所需最少的的曲线交点 50
// minLineLength = 0, 最小线长
// maxLineGap = 0 , 最大线间隔 maxLineGap: 能被认为在一条直线上的亮点的最大距离.
for( size_t i = 0; i < lines.size(); i++ )
{
Vec4i l = lines[i];
line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA);
}
#endif
imshow("source", src);
imshow("detected lines", cdst);
waitKey();
return 0;
}
/*
霍夫 圆变换 在图像中检测圆.
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
using namespace cv;
using namespace std;
void help()
{
cout << "\nThis program demonstrates line finding with the Hough transform.\n"
"Usage:\n"
"./houghlines , Default is pic1.jpg\n" << endl;
}
int main(int argc, char** argv)
{
string filename = argc >= 2 ? argv[1] : "../../common/data/apple.jpeg";
Mat src = imread(filename, IMREAD_COLOR); // 按圆图片颜色 读取
if(src.empty())
{
help();
cout << "can not open " << filename << endl;
return -1;
}
// 得到灰度图
Mat src_gray = src.clone();
if (src.channels() == 3)//如果原图是彩色图
{
cvtColor(src, src_gray, CV_BGR2GRAY);//转换到灰度图
}
//else
//{
// src_gray = src.clone();
//}
// 高斯平滑降噪
GaussianBlur( src_gray, src_gray, Size(9, 9), 2, 2 );
// 执行霍夫圆变换
vector circles;//中性点(x,y)半价 r三个参数
HoughCircles( src_gray, circles, CV_HOUGH_GRADIENT, 1, src_gray.rows/10, 80, 50, 0, 0 );
// CV_HOUGH_GRADIENT: 指定检测方法. 现在OpenCV中只有霍夫梯度法
// dp = 1: 累加器图像的反比分辨率
// min_dist = src_gray.rows/8: 检测到圆心之间的最小距离
// param_1 = 200: Canny边缘函数的高阈值
// param_2 = 100: 圆心检测阈值.
// min_radius = 0: 能检测到的最小圆半径, 默认为0.
// max_radius = 0: 能检测到的最大圆半径, 默认为0
for( size_t i = 0; i < circles.size(); i++ )
{
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));//圆中心点
int radius = cvRound(circles[i][2]);//半径 像素值单位
// 画圆中心点 circle center
circle( src, center, 3, Scalar(0,255,255), -1, 8);// green 绿色 粗细 线形
// 画圆外圈 circle outline
circle( src, center, radius, Scalar(0,0,255), 3, 8);// red 红色 bgr
}
namedWindow( "Hough Circle Transform Demo", CV_WINDOW_AUTOSIZE );
imshow("Hough Circle Transform Demo", src);
//imshow("detected circles", cdst);
waitKey(0);// 等待用户按键结束程序
return 0;
}
重映射是什么意思?
把一个图像中一个位置的像素放置到另一个图片指定位置的过程.
为了完成映射过程, 有必要获得一些插值为非整数像素坐标,
因为源图像与目标图像的像素坐标不是一一对应的.
我们通过重映射来表达每个像素的位置 (x,y) :
goal(x,y) = f(s(s,y))
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
using namespace cv;
using namespace std;
/// 全局变量
Mat src, dst;
Mat map_x, map_y;
char* remap_window = "Remap demo";
int ind = 0;
/// 函数声明 更新映射关系
void update_map( void );
/// 主函数
int main( int argc, char** argv )
{
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 创建几张映射矩阵
// map_x: x方向的映射参数. 它相当于方法 h(i,j) 的第一个参数
// map_y: y方向的映射参数. 注意 map_y 和 map_x 与 src 的大小一致。
dst.create( src.size(), src.type() );
map_x.create( src.size(), CV_32FC1 );
map_y.create( src.size(), CV_32FC1 );
/// 创建显示窗口
namedWindow( remap_window, CV_WINDOW_AUTOSIZE );
/// 循环
while( true )
{
int c = waitKey( 1000 );//1s检测一次按键 按Esc键退出
if( (char)c == 27 )
{ break; }
/// 更新重映射图
update_map();
// map_x: x方向的映射参数. 它相当于方法 h(i,j) 的第一个参数
// map_y: y方向的映射参数. 注意 map_y 和 map_x 与 src 的大小一致。
remap( src, dst, map_x, map_y, CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0, 0) );
/// 显示结果
imshow( remap_window, dst );
}
return 0;
}
// 更新函数
void update_map( void )
{
ind = ind%4;// 0 1 2 3
for( int j = 0; j < src.rows; j++ )//每行
{ for( int i = 0; i < src.cols; i++ )//每列
{
switch( ind )
{
case 0:// 图像宽高缩小一半,并显示在中间:
if( i > src.cols*0.25 && i < src.cols*0.75 && j > src.rows*0.25 && j < src.rows*0.75 )
{
map_x.at(j,i) = 2*( i - src.cols*0.25 ) + 0.5 ;//记录的是坐标
map_y.at(j,i) = 2*( j - src.rows*0.25 ) + 0.5 ;
}
else
{ map_x.at(j,i) = 0 ;
map_y.at(j,i) = 0 ;
}
break;
case 1:// 图像上下颠倒
map_x.at(j,i) = i ;//列不变
map_y.at(j,i) = src.rows - j ;//行交换
break;
case 2:// 图像左右颠倒
map_x.at(j,i) = src.cols - i ;//列交换
map_y.at(j,i) = j ;//行不变
break;
case 3:// 上下颠倒 + 左右颠倒
map_x.at(j,i) = src.cols - i ;
map_y.at(j,i) = src.rows - j ;
break;
} // end of switch
}
}
ind++;
}
使用OpenCV函数 warpAffine 来实现一些简单的重映射.
使用OpenCV函数 getRotationMatrix2D 来获得一个 3 * 3 旋转矩阵
什么是仿射变换?
一个任意的仿射变换都能表示为 乘以一个矩阵 (线性变换) 接着再 加上一个向量 (平移).
综上所述, 我们能够用仿射变换来表示:
旋转 (线性变换)
平移 (向量加)
缩放操作 (线性变换)
你现在可以知道, 事实上, 仿射变换代表的是两幅图之间的 关系 . [R T]
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
using namespace cv;
using namespace std;
/// 全局变量
char* source_window = "Source image";
char* warp_window = "Warp";
char* warp_rotate_window = "Warp + Rotate";
// 主函数
int main( int argc, char** argv )
{
Point2f srcTri[3];//原来 3个点
Point2f dstTri[3];//目标三点
Mat rot_mat( 2, 3, CV_32FC1 );//
Mat warp_mat( 2, 3, CV_32FC1 );
Mat src, warp_dst, warp_rotate_dst;// 储存中间和目标图像的Mat
/// 加载源图像
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 设置目标图像的大小和类型与源图像一致
warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
/// 设置源图像和目标图像上的三组点以计算仿射变换
srcTri[0] = Point2f( 0,0 );
srcTri[1] = Point2f( src.cols - 1, 0 );
srcTri[2] = Point2f( 0, src.rows - 1 );
dstTri[0] = Point2f( src.cols*0.0, src.rows*0.33 );
dstTri[1] = Point2f( src.cols*0.85, src.rows*0.25 );
dstTri[2] = Point2f( src.cols*0.15, src.rows*0.7 );
/// 求得仿射变换
// 通过这两组点, 我们能够使用OpenCV函数 getAffineTransform 来求出仿射变换:
warp_mat = getAffineTransform( srcTri, dstTri );
/// 对源图像应用上面求得的仿射变换
warpAffine( src, warp_dst, warp_mat, warp_dst.size() );
/** 对图像扭曲后再旋转 */
/// 计算绕图像中点顺时针旋转50度缩放因子为0.6的旋转矩阵
Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
double angle = -50.0;//逆时针为正 旋转
double scale = 0.6; // 缩放
/// 通过上面的旋转细节信息求得旋转矩阵
rot_mat = getRotationMatrix2D( center, angle, scale );//旋转矩阵
/// 旋转已扭曲图像 将刚刚求得的仿射变换应用到源图像
warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() );
/// 显示结果
namedWindow( source_window, CV_WINDOW_AUTOSIZE );
imshow( source_window, src );
namedWindow( warp_window, CV_WINDOW_AUTOSIZE );
imshow( warp_window, warp_dst );
namedWindow( warp_rotate_window, CV_WINDOW_AUTOSIZE );
imshow( warp_rotate_window, warp_rotate_dst );
/// 等待用户按任意按键退出程序
waitKey(0);
return 0;
}
九、直方图计算 直方图均衡化 直方图反向投影 查找 搜索匹配
图像的直方图计算
单个通道内的像素值进行统计
什么是直方图?
直方图是对数据的集合 统计 ,并将统计结果分布于一系列预定义的 bins 中。
这里的 数据 不仅仅指的是灰度值 (如上一篇您所看到的),
统计数据可能是任何能有效描述图像的特征。
先看一个例子吧。 假设有一个矩阵包含一张图像的信息 (灰度值 0-255):
OpenCV提供了一个简单的计算数组集(通常是图像或分割后的通道)
的直方图函数 calcHist 。 支持高达 32 维的直方图。
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
using namespace std;
using namespace cv;
/** @函数 main */
int main( int argc, char** argv )
{
Mat src, dst;
/// 加载源图像
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// split分割成3个单通道图像 ( R, G 和 B )
vector rgb_planes;
split( src, rgb_planes );
/// 设定bin数目
int histSize = 255;
/// 设定取值范围 ( R,G,B) )
float range[] = { 0, 255 } ;
const float* histRange = { range };
bool uniform = true; bool accumulate = false;
Mat r_hist, g_hist, b_hist;
/// 计算直方图:
calcHist( &rgb_planes[0], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &rgb_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate );
calcHist( &rgb_planes[2], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate );
// 1: 输入数组的个数 (这里我们使用了一个单通道图像,我们也可以输入数组集 )
// 0: 需要统计的通道 (dim)索引 ,这里我们只是统计了灰度 (且每个数组都是单通道)所以只要写 0 就行了。
// Mat(): 掩码( 0 表示忽略该像素), 如果未定义,则不使用
// r_hist: 储存直方图的矩阵
// 1: 直方图维数
// histSize: 每个维度的bin数目
// histRange: 每个维度的取值范围
// uniform 和 accumulate: bin大小相同,清除直方图痕迹
// 创建直方图画布
int hist_w = 400; int hist_h = 400;
int bin_w = cvRound( (double) hist_w/histSize );
Mat histImage( hist_w, hist_h, CV_8UC3, Scalar( 0,0,0) );
/// 将直方图归一化到范围 [ 0, histImage.rows ]
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
// 0 及 histImage.rows: 这里,它们是归一化 r_hist 之后的取值极限
/// 在直方图画布上画出直方图
for( int i = 1; i < histSize; i++ )
{
line( histImage, Point( bin_w*(i-1), hist_h - cvRound(r_hist.at(i-1)) ) ,
Point( bin_w*(i), hist_h - cvRound(r_hist.at(i)) ),
Scalar( 0, 0, 255), 2, 8, 0 );
line( histImage, Point( bin_w*(i-1), hist_h - cvRound(g_hist.at(i-1)) ) ,
Point( bin_w*(i), hist_h - cvRound(g_hist.at(i)) ),
Scalar( 0, 255, 0), 2, 8, 0 );
line( histImage, Point( bin_w*(i-1), hist_h - cvRound(b_hist.at(i-1)) ) ,
Point( bin_w*(i), hist_h - cvRound(b_hist.at(i)) ),
Scalar( 255, 0, 0), 2, 8, 0 );
}
/// 显示直方图
namedWindow("calcHist Demo", CV_WINDOW_AUTOSIZE );
imshow("calcHist Demo", histImage );
waitKey(0);
return 0;
}
求得对直方图均衡化的映射矩阵 在对原图像进行映射
图像的直方图是什么?
直方图是图像中像素强度分布的图形表达方式.
它统计了每一个强度值(灰度 0~255 256个值)所具有的像素点个数.
直方图均衡化是什么?
直方图均衡化是通过拉伸像素强度分布范围来增强图像对比度的一种方法.
说得更清楚一些, 以上面的直方图为例, 你可以看到像素主要集中在中间的一些强度值上.
直方图均衡化要做的就是 拉伸 这个范围. 见下面左图: 绿圈圈出了 少有像素分布其上的 强度值.
对其应用均衡化后, 得到了中间图所示的直方图. 均衡化的图像见下面右图.
直方图均衡化是怎样做到的?
均衡化指的是把一个分布 (给定的直方图) 映射
到另一个分布 (一个更宽更统一的强度值分布), 所以强度值分布会在整个范围内展开.
要想实现均衡化的效果, 映射函数应该是一个 累积分布函数 (cdf)
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
using namespace cv;
using namespace std;
// 主函数
int main( int argc, char** argv )
{
Mat src, dst;
char* source_window = "Source image";
char* equalized_window = "Equalized Image";
/// 加载源图像
string imageName("../../common/data/77.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 转为灰度图
cvtColor( src, src, CV_BGR2GRAY );
/// 应用直方图均衡化
equalizeHist( src, dst );
/// 显示结果
namedWindow( source_window, CV_WINDOW_AUTOSIZE );
namedWindow( equalized_window, CV_WINDOW_AUTOSIZE );
imshow( source_window, src );
imshow( equalized_window, dst );
/// 等待用户按键退出程序
waitKey(0);
return 0;
}
直方图对比
如何使用OpenCV函数 compareHist 产生一个表达两个直方图的相似度的数值。
如何使用不同的对比标准来对直方图进行比较。
要比较两个直方图( H_1 and H_2 ),
首先必须要选择一个衡量直方图相似度的 对比标准 (d(H_{1}, H_{2})) 。
OpenCV 函数 compareHist 执行了具体的直方图对比的任务。
该函数提供了4种对比标准来计算相似度:
【1】相关关系 Correlation ( CV_COMP_CORREL ) 与均值的偏差 积
【2】平方差 Chi-Square ( CV_COMP_CHISQR )
【3】交集?Intersection ( CV_COMP_INTERSECT ) 对应最小值集合
【4】Bhattacharyya 距离( CV_COMP_BHATTACHARYYA ) 巴氏距离(巴塔恰里雅距离 / Bhattacharyya distance)
本程序做什么?
装载一张 基准图像 和 两张 测试图像 进行对比。
产生一张取自 基准图像 下半部的图像。
将图像转换到HSV格式。
计算所有图像的H-S直方图,并归一化以便对比。
将 基准图像 直方图与 两张测试图像直方图,基准图像半身像直方图,以及基准图像本身的直方图分别作对比。
显示计算所得的直方图相似度数值。
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
using namespace std;
using namespace cv;
/** @函数 main */
int main( int argc, char** argv )
{
Mat src_base, hsv_base;
Mat src_test1, hsv_test1;
Mat src_test2, hsv_test2;
Mat hsv_half_down;
/// 装载三张背景环境不同的图像
if( argc < 4 )
{ printf("** Error. Usage: ./compareHist_Demo \n");
return -1;
}
src_base = imread( argv[1], 1 );
src_test1 = imread( argv[2], 1 );
src_test2 = imread( argv[3], 1 );
/// 转换到 HSV
cvtColor( src_base, hsv_base, CV_BGR2HSV );
cvtColor( src_test1, hsv_test1, CV_BGR2HSV );
cvtColor( src_test2, hsv_test2, CV_BGR2HSV );
hsv_half_down = hsv_base( Range( hsv_base.rows/2, hsv_base.rows - 1 ), Range( 0, hsv_base.cols - 1 ) );
/// 对hue通道使用30个bin,对saturatoin通道使用32个bin
int h_bins = 50; int s_bins = 60;
int histSize[] = { h_bins, s_bins };
// hue的取值范围从0到256, saturation取值范围从0到180
float h_ranges[] = { 0, 256 };
float s_ranges[] = { 0, 180 };
const float* ranges[] = { h_ranges, s_ranges };
// 使用第0和第1通道
int channels[] = { 0, 1 };
/// 直方图
MatND hist_base;
MatND hist_half_down;
MatND hist_test1;
MatND hist_test2;
/// 计算HSV图像的直方图
calcHist( &hsv_base, 1, channels, Mat(), hist_base, 2, histSize, ranges, true, false );
normalize( hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat() );
calcHist( &hsv_half_down, 1, channels, Mat(), hist_half_down, 2, histSize, ranges, true, false );
normalize( hist_half_down, hist_half_down, 0, 1, NORM_MINMAX, -1, Mat() );
calcHist( &hsv_test1, 1, channels, Mat(), hist_test1, 2, histSize, ranges, true, false );
normalize( hist_test1, hist_test1, 0, 1, NORM_MINMAX, -1, Mat() );
calcHist( &hsv_test2, 1, channels, Mat(), hist_test2, 2, histSize, ranges, true, false );
normalize( hist_test2, hist_test2, 0, 1, NORM_MINMAX, -1, Mat() );
///应用不同的直方图对比方法
for( int i = 0; i < 4; i++ )
{ int compare_method = i;
double base_base = compareHist( hist_base, hist_base, compare_method );
double base_half = compareHist( hist_base, hist_half_down, compare_method );
double base_test1 = compareHist( hist_base, hist_test1, compare_method );
double base_test2 = compareHist( hist_base, hist_test2, compare_method );
printf( " Method [%d] Perfect, Base-Half, Base-Test(1), Base-Test(2) : %f, %f, %f, %f \n", i, base_base, base_half , base_test1, base_test2 );
}
printf( "Done \n" );
return 0;
}
反向投影 利用直方图模型 搜索 对应的 图像区域
反向投影是一种记录给定图像中的像素点如何适应直方图模型像素分布的方式。
简单的讲, 所谓反向投影就是首先计算某一特征的直方图模型,然后使用模型去寻找图像中存在的该特征。
例如, 你有一个肤色直方图 ( Hue-Saturation 直方图 ),你可以用它来寻找图像中的肤色区域:
http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/histograms/back_projection/back_projection.html
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
using namespace cv;
using namespace std;
/// 全局变量
Mat src; Mat hsv; Mat hue;
int bins = 25;
/// 函数申明
void Hist_and_Backproj(int, void* );
/** @函数 main */
int main( int argc, char** argv )
{
/// 读取图像
src = imread( argv[1], 1 );
/// 转换到 HSV 空间
cvtColor( src, hsv, CV_BGR2HSV );
/// 分离 Hue 通道
hue.create( hsv.size(), hsv.depth() );
int ch[] = { 0, 0 };
mixChannels( &hsv, 1, &hue, 1, ch, 1 );
/// 创建 Trackbar 来输入bin的数目
char* window_image = "Source image";
namedWindow( window_image, CV_WINDOW_AUTOSIZE );
createTrackbar("* Hue bins: ", window_image, &bins, 180, Hist_and_Backproj );
Hist_and_Backproj(0, 0);
/// 现实图像
imshow( window_image, src );
/// 等待用户反应
waitKey(0);
return 0;
}
/**
* @函数 Hist_and_Backproj
* @简介:Trackbar事件的回调函数
*/
void Hist_and_Backproj(int, void* )
{
MatND hist;
int histSize = MAX( bins, 2 );
float hue_range[] = { 0, 180 };
const float* ranges = { hue_range };
/// 计算直方图并归一化
calcHist( &hue, 1, 0, Mat(), hist, 1, &histSize, &ranges, true, false );
normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() );
/// 计算反向投影
MatND backproj;
calcBackProject( &hue, 1, 0, hist, backproj, &ranges, 1, true );
/// 显示反向投影
imshow( "BackProj", backproj );
/// 显示直方图
int w = 400; int h = 400;
int bin_w = cvRound( (double) w / histSize );
Mat histImg = Mat::zeros( w, h, CV_8UC3 );
for( int i = 0; i < bins; i ++ )
{ rectangle( histImg, Point( i*bin_w, h ), Point( (i+1)*bin_w, h - cvRound( hist.at(i)*h/255.0 ) ), Scalar( 0, 0, 255 ), -1 ); }
imshow( "Histogram", histImg );
}
/* 计算物体的凸包 边缘包围圈
对图像进行二值化 candy边缘检测也得到 二值图
寻找轮廓
对每个轮廓计算其凸包
绘出轮廓及其凸包
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
#include
using namespace cv;
using namespace std;
Mat src; Mat src_gray;
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);
/// Function header
void thresh_callback(int, void* );
/** @function main */
int main( int argc, char** argv )
{
/// 加载源图像
string imageName("../../common/data/apple.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 转成灰度图并进行模糊降噪
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( " Threshold:", "Source", &thresh, max_thresh, thresh_callback );
thresh_callback( 0, 0 );
waitKey(0);
return(0);
}
// 回调函数
void thresh_callback(int, void* )
{
Mat src_copy = src.clone();
Mat threshold_output;
vector > contours;
vector hierarchy;
/// 对图像进行二值化 这里 candy边缘检测也得到 二值图
threshold( src_gray, threshold_output, thresh, 255, THRESH_BINARY );
/// 寻找轮廓
findContours( threshold_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );
/// 对每个轮廓计算其凸包
vector >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(), 0, Point() );
drawContours( drawing, hull, i, color, 1, 8, vector(), 0, Point() );
}
/// 把结果显示在窗体
namedWindow( "Hull demo", CV_WINDOW_AUTOSIZE );
imshow( "Hull demo", drawing );
}
/*
创建包围轮廓的矩形和圆形边界框
使用Threshold检测边缘 二值化 阈值 thresh
找到轮廓 findContours
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
#include
using namespace cv;
using namespace std;
Mat src; Mat src_gray;
int thresh = 100;// 二值化阈值
int max_thresh = 255;
RNG rng(12345);
/// 函数声明 回调函数
void thresh_callback(int, void* );
// @主函数
int main( int argc, char** argv )
{
/// 加载源图像
string imageName("../../common/data/apple.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 转化成灰度图像并进行平滑
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( " Threshold:", "Source", &thresh, max_thresh, thresh_callback );
thresh_callback( 0, 0 );
waitKey(0);
return(0);
}
/** @thresh_callback 函数 */
void thresh_callback(int, void* )
{
Mat threshold_output;//阈值检测边缘
vector > contours;
vector hierarchy;
/// 使用Threshold检测边缘 二值化 阈值 thresh
threshold( src_gray, threshold_output, thresh, 255, THRESH_BINARY );
/// 找到轮廓 轮廓 contours
findContours( threshold_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );
/// 多边形逼近轮廓 + 获取矩形和圆形边界框
vector > contours_poly( contours.size() );// 多边形逼近轮廓
vector boundRect( contours.size() );//矩形边界框
vectorcenter( contours.size() );//圆形边界框 中心
vectorradius( contours.size() );//圆形边界框 半径
for( int i = 0; i < contours.size(); i++ )//对于每一个轮廓
{ approxPolyDP( Mat(contours[i]), contours_poly[i], 3, true );// 多边形逼近轮廓
boundRect[i] = boundingRect( Mat(contours_poly[i]) );//矩形边界框
minEnclosingCircle( contours_poly[i], center[i], radius[i] );//圆形边界框
}
/// 画多边形轮廓 + 包围的矩形框 + 圆形框
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_poly, i, color, 1, 8, vector(), 0, Point() );//多边形轮廓
rectangle( drawing, boundRect[i].tl(), boundRect[i].br(), color, 2, 8, 0 );//矩形框
circle( drawing, center[i], (int)radius[i], color, 2, 8, 0 );//圆形框
}
/// 显示在一个窗口
namedWindow( "Contours", CV_WINDOW_AUTOSIZE );
imshow( "Contours", drawing );
}
/*
为轮廓创建可倾斜的边界框和椭圆
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
#include
using namespace cv;
using namespace std;
Mat src; Mat src_gray;
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);
/// 回调函数Function header
void thresh_callback(int, void* );
// @function main
int main( int argc, char** argv )
{
// 加载源图像
string imageName("../../common/data/apple.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 转为灰度图并模糊化
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 );
// 滑动条 动态改变参数 二值化 阈值边界检测 thresh
createTrackbar( " Threshold:", "Source", &thresh, max_thresh, thresh_callback );
thresh_callback( 0, 0 );
waitKey(0);
return(0);
}
// @function thresh_callback
void thresh_callback(int, void* )
{
Mat threshold_output;
vector > contours;
vector hierarchy;
/// 阈值化检测边界 二值化 阈值边界检测
threshold( src_gray, threshold_output, thresh, 255, THRESH_BINARY );
/// 寻找轮廓
findContours( threshold_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );
/// 对每个找到的轮廓创建可倾斜的边界框和椭圆
vector minRect( contours.size() );// 可倾斜的边界框
vector minEllipse( contours.size() );// 椭圆
for( int i = 0; i < contours.size(); i++ )
{ minRect[i] = minAreaRect( Mat(contours[i]) );// 可倾斜的边界框
if( contours[i].size() > 5 )
{ minEllipse[i] = fitEllipse( Mat(contours[i]) ); }// 椭圆
}
/// 绘出轮廓及其可倾斜的边界框和边界椭圆
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) );
// contour 轮廓
drawContours( drawing, contours, i, color, 1, 8, vector(), 0, Point() );
// ellipse 椭圆
ellipse( drawing, minEllipse[i], color, 2, 8 );
// rotated rectangle 可倾斜的边界框
Point2f rect_points[4]; minRect[i].points( rect_points );
for( int j = 0; j < 4; j++ )
line( drawing, rect_points[j], rect_points[(j+1)%4], color, 1, 8 );
}
/// 结果在窗体中显示
namedWindow( "Contours", CV_WINDOW_AUTOSIZE );
imshow( "Contours", drawing );
}
/*
轮廓矩
因为我们常常会将随机变量(先假定有任意阶矩)作一个线性变换,
把一阶矩(期望)归零,
二阶矩(方差)归一,以便统一研究一些问题。
三阶矩,就是我们所称的「偏度」。
典型的正偏度投资,就是彩票和保险:
一般来说,你花的那一点小钱就打水漂了,但是这一点钱完全是在承受范围内的;
而这点钱则部分转化为小概率情况下的巨大收益。
而负偏度变量则正好相反,「一般为正,极端值为负」,
可以参照一些所谓的「灰色产业」:
一般情况下是可以赚到一点钱的,但是有较小的概率「东窗事发」,赔得血本无归。
四阶矩,又称峰度,简单来说相当于「方差的方差」,
和偏度类似,都可以衡量极端值的情况。峰度较大通常意味着极端值较常出现,
峰度较小通常意味着极端值即使出现了也不会「太极端」。
峰度是大还是小通常与3(即正态分布的峰度)相比较。
至于为什么五阶以上的矩没有专门的称呼,主要是因为我们习惯的线性变换,
只有两个自由度,故最多只能将前两阶矩给「标准化」。
这样,标准化以后,第三、第四阶的矩就比较重要了,前者衡量正负,
后者衡量偏离程度,与均值、方差的关系类似。换句话说,
假如我们能把前四阶矩都给「标准化」了,那么五阶、六阶的矩就会比较重要了吧。
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
#include
using namespace cv;
using namespace std;
Mat src; Mat src_gray;
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);
/// 回调函数声明
void thresh_callback(int, void* );
// @主函数
int main( int argc, char** argv )
{
// 加载源图像
string imageName("../../common/data/apple.jpeg"); // 图片文件名路径(默认值)
if( argc > 1)
{
imageName = argv[1];//如果传递了文件 就更新
}
src = imread( imageName );
if( src.empty() )
{
cout << "can't load image " << endl;
return -1;
}
/// 把原图像转化成灰度图像并进行平滑
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 );
// 滑动条 动态改变参数 二值化 阈值边界检测 thresh
createTrackbar( " Canny thresh:", "Source", &thresh, max_thresh, thresh_callback );
thresh_callback( 0, 0 );
waitKey(0);
return(0);
}
// @thresh_callback 函数
void thresh_callback(int, void* )
{
Mat canny_output;
vector > contours;
vector hierarchy;
/// 使用Canndy检测边缘 低阈值 thresh 高阈值 thresh*2 核大小
Canny( src_gray, canny_output, thresh, thresh*2, 3 );
/// 找到轮廓
findContours( canny_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );
/// 计算矩
vector mu(contours.size() );
for( int i = 0; i < contours.size(); i++ )
{ mu[i] = moments( contours[i], false ); }
/// 计算中心矩:
vector mc( contours.size() );
for( int i = 0; i < contours.size(); i++ )
{ mc[i] = Point2f( mu[i].m10/mu[i].m00 , mu[i].m01/mu[i].m00 ); }
/// 绘制轮廓
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() );
circle( drawing, mc[i], 4, color, -1, 8, 0 );
}
/// 显示到窗口中
namedWindow( "Contours", CV_WINDOW_AUTOSIZE );
imshow( "Contours", drawing );
/// 通过m00计算轮廓面积并且和OpenCV函数比较
printf("\t Info: Area and Contour Length \n");
for( int i = 0; i< contours.size(); i++ )
{
printf(" * Contour[%d] - Area (M_00) = %.2f - Area OpenCV: %.2f - Length: %.2f \n", i, mu[i].m00, contourArea(contours[i]), arcLength( contours[i], true ) );
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() );
circle( drawing, mc[i], 4, color, -1, 8, 0 );
}
}
图像 模板匹配 是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术.
使用OpenCV函数 matchTemplate 在模板块和输入图像之间寻找匹配, 获得匹配结果图像
使用OpenCV函数 minMaxLoc 在给定的矩阵(上述得到的匹配结果矩阵)中寻找最大和最小值(包括它们的位置).
什么是模板匹配?
模板匹配是一项在一幅图像中寻找与另一幅模板图像最匹配(相似)部分的技术.
我们需要2幅图像:
原图像 (I): 在这幅图像里,我们希望找到一块和模板匹配的区域
模板 (T): 将和原图像比照的图像块
我们的目标是检测最匹配的区域:
为了确定匹配区域, 我们不得不滑动模板图像和原图像进行 比较 :
通过 滑动, 我们的意思是图像块一次移动一个像素 (从左往右,从上往下).
在每一个位置, 都进行一次度量计算来表明它是 “好” 或 “坏” 地与那个位置匹配 (或者说块图像和原图像的特定区域有多么相似).
对于 T 覆盖在 I 上的每个位置,你把度量值 保存 到 结果图像矩阵 (R) 中. 在 R 中的每个位置 (x,y) 都包含匹配度量值:
上图就是 TM_CCORR_NORMED 方法处理后的结果图像 R . 最白的位置代表最高的匹配. 正如您所见, 红色椭圆框住的位置很可能是结果图像矩阵中的最大数值, 所以这个区域 (以这个点为顶点,长宽和模板图像一样大小的矩阵) 被认为是匹配的.
实际上, 我们使用函数 minMaxLoc 来定位在矩阵 R 中的最大值点 (或者最小值, 根据函数输入的匹配参数) .
OpenCV中支持哪些匹配算法
【1】 平方差匹配 method=CV_TM_SQDIFF square dirrerence(error)
这类方法利用平方差来进行匹配,最好匹配为0.匹配越差,匹配值越大.
【2】标准平方差匹配 method=CV_TM_SQDIFF_NORMED standard square dirrerence(error)
【3】 相关匹配 method=CV_TM_CCORR
这类方法采用模板和图像间的乘法操作,所以较大的数表示匹配程度较高,0标识最坏的匹配效果.
【4】 标准相关匹配 method=CV_TM_CCORR_NORMED
【5】 相关匹配 method=CV_TM_CCOEFF
这类方法将模版对其均值的相对值与图像对其均值的相关值进行匹配,1表示完美匹配,
-1表示糟糕的匹配,0表示没有任何相关性(随机序列).
【6】标准相关匹配 method=CV_TM_CCOEFF_NORMED
通常,随着从简单的测量(平方差)到更复杂的测量(相关系数),
我们可获得越来越准确的匹配(同时也意味着越来越大的计算代价).
最好的办法是对所有这些设置多做一些测试实验,
以便为自己的应用选择同时兼顾速度和精度的最佳方案.
在这程序实现了什么?
载入一幅输入图像和一幅模板图像块 (template)
通过使用函数 matchTemplate 实现之前所述的6种匹配方法的任一个. 用户可以通过滑动条选取任何一种方法.
归一化匹配后的输出结果
定位最匹配的区域
用矩形标注最匹配的区域
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
#include
using namespace std;
using namespace cv;
/// 全局变量
Mat img; Mat templ; Mat result;
char* image_window = "Source Image";
char* result_window = "Result window";
int match_method;
int max_Trackbar = 5;
/// 函数声明
void MatchingMethod( int, void* );
// @主函数
int main( int argc, char** argv )
{
/// 载入原图像和模板块
img = imread( argv[1], 1 );
templ = imread( argv[2], 1 );
/// 创建窗口
namedWindow( image_window, CV_WINDOW_AUTOSIZE );
namedWindow( result_window, CV_WINDOW_AUTOSIZE );
/// 创建滑动条
char* trackbar_label = "Method: \n 0: SQDIFF \n 1: SQDIFF NORMED \n 2: TM CCORR \n 3: TM CCORR NORMED \n 4: TM COEFF \n 5: TM COEFF NORMED";
createTrackbar( trackbar_label, image_window, &match_method, max_Trackbar, MatchingMethod );
MatchingMethod( 0, 0 );
waitKey(0);
return 0;
}
/**
* @函数 MatchingMethod
* @简单的滑动条回调函数
*/
void MatchingMethod( int, void* )
{
/// 将被显示的原图像
Mat img_display;
img.copyTo( img_display );
/// 创建输出结果的矩阵
// 创建了一幅用来存放匹配结果的输出图像矩阵. 仔细看看输出矩阵的大小(它包含了所有可能的匹配位置)
int result_cols = img.cols - templ.cols + 1;
int result_rows = img.rows - templ.rows + 1;
result.create( result_cols, result_rows, CV_32FC1 );
/// 进行匹配和标准化
// 很自然地,参数是输入图像 I, 模板图像 T, 结果图像 R 还有匹配方法 (通过滑动条给出)
matchTemplate( img, 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( img_display, matchLoc, Point( matchLoc.x + templ.cols , matchLoc.y + templ.rows ), Scalar::all(0), 2, 8, 0 );
// 匹配结果图上显示
rectangle( result, matchLoc, Point( matchLoc.x + templ.cols , matchLoc.y + templ.rows ), Scalar::all(0), 2, 8, 0 );
imshow( image_window, img_display );
imshow( result_window, result );
return;
}