在完成对液晶图像的预处理之后,获得了需要的二值图像,下面则需要对液晶图像进行定位。由于所选用的液晶图像具有明显的矩形边框,因此可采用基于边缘轮廓的矩形匹配的定位方法,通过检测图像中的矩形并对矩形长宽比进行匹配,进一步找到所需要的液晶位置。具体流程如下:

3.1边缘检测
要想实现矩形匹配,则首先就要检测图像的边缘。边缘检测的最通用方法就是检测图像灰度值的不连续性。经典的边缘检测算法是通过检测一阶导数最大值或二阶导数过零点来实现的。由于实际处理的是数字图像,是离散的,因此边缘检测大多是通过模板卷积来近似计算梯度的。常见的算法有Sobel算法、Canny算法、Laplace算法、形态梯度算法。本文使用最常用的Canny算法。
所谓卷积,可做解释如下:
待处理的数字图像可视为一个大型矩阵,图像中的每个像素对应矩阵中的一个元素。若图像分辨率为1024×768,则对应矩阵的行数为1024,列数为 768。
用于滤波操作的小型矩阵(也称为卷积核,其应用不仅限于滤波)通常为方阵,即行数与列数相等。例如常用的边缘检测Sobel算子,就是典型的3×3 卷积核。
滤波过程可理解为:对图像矩阵中的每个像素,以其为中心,选取与卷积核大小相同的局部区域,将区域内像素值与卷积核对应位置元素逐点相乘后求和,并将计算结果作为该像素的新值,即完成对该像素的一次滤波。

3.1.1 Sobel检测算法
Sobel算子是一种基于一阶导数的边缘检测算法,利用Sobel边缘检测算子与图像进行卷积操作来对图像进行求导。Sobel内核必须为奇数,以内核大小为3的算子为例。
在水平方向上将图像I与水平内核卷积,求出水平方向的导数,在垂直方向上将图像I与垂直内核卷积,求出垂直方向的导数,如下图:

最后,根据上面求出的水平和垂直方向的导数,求出近似梯度。

函数原型:
void Sobel(InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize=3, double scale=1, double delta=0, int borderType=BORDER_DEFAULT )
- src:为输入图像,填Mat类型即可。
- dst:输出参数,需要和源图片有一样的尺寸和类型。
- ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合:
- 若src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F
- 若src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F
- 若src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F
- 若src.depth() = CV_64F, 取ddepth = -1/CV_64F
- dx:x 方向上的差分阶数。
- dy:y方向上的差分阶数。
- ksize,有默认值3,表示Sobel核的大小;必须取1,3,5或7。
- scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。
- delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
- borderType:边界模式,默认值为BORDER_DEFAULT。
3.1.2 Canny检测算法
Canny算法是基于一阶导数的边缘检测算法,是众多经典边缘检测算法中最为常用的一种。Canny算法的检测方法是首先使用高斯滤波器对图像进行平滑处理。然后按照Sobel计算梯度的幅值和方向,梯度方向近似到四个可能的角度之一,一般是0度、45度、90度和135度。

非极大值抑制与滞后阈值是Canny算法的重要特征。
- 所谓非极大值抑制,是指寻找像素点的局部最大值,在每个点上,领域中心x与沿着其对应的梯度方向的两个像素相比,若中心像素为最大值,则保留,否则中心值置为0,这样可以抑制非极大值,保留局部梯度最大的点,以细化边缘。
- 在施加非极大值抑制后,剩余的像素可以更准确地表示图像中的实际边缘。然而,仍然存在由于噪声和颜色变化引起的一些边缘像素,为了解决这些杂散响应,必须用弱梯度值过滤边缘像素,并保留具有高梯度值的边缘像素,如此可通过选择高低阈值来实现。若某一像素高于高阈值,则保留为边缘像素;若某一像素低于低阈值,该像素被排除;若某一像素位于两个阈值之间,则通过连通性来分类边缘或非边缘。如与确定为边缘的像素邻接,则为边缘,否则为非边缘。一般来说,高阈值为低阈值的2~3倍。
函数原型:
void Canny( InputArray image, //输入图像:8-bit
OutputArray edges, //输出边缘图像:单通道,8-bit,size与输入图像一致)
double threshold1, //阈值1
double threshold2, //阈值2
int apertureSize=3, //Sober算子大小
bool L2gradient=false) //是否采用更精确的方式计算图像梯度
执行效果:

3.1.3 Laplace检测算法
Laplace检测算法是基于二阶导数的边缘检测算法,其内部也是通过Sobel算法实现的。
对于函数f(x,y)
,它的Laplace算子可以用下式表示:
,
使用差分方程分别求x、y方向的二阶偏导,近似如下:

以[i,j]为中心,则有:

同理,对于y方向的二阶偏导为:

合并后,可近似得到Laplace算子的模板:

Laplace算子对噪声比较敏感,它对一些孤立像素的响应比较强烈,因此在存在噪声的情况下,使用Laplace算子进行检测边缘之前先要进行平滑滤波处理。
函数原型:
void cv::Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0, int borderType=BORDER_DEFAULT)
- src:输入图像(支持单通道或多通道,通常先转为灰度图)。
- ddepth :输出图像深度(如 cv2.CV_64F、cv2.CV_32F、cv2.CV_16S,不能用 cv2.CV_8U,因可能产生负值)。
- dst:可选,输出图像(默认为 None,由函数自动创建)。
- ksize :核大小(必须为正奇数),默认为 1(对应 3×3 核)。
- scale:缩放因子,默认为 1。
- delta:偏移量,默认为 0。
- borderType:边界处理方式,默认为 cv2.BORDER_DEFAULT。
3.1.4 形态学检测算法
形态学用于边缘检测具有算法简单并且较好的保留图像细节的优点。形态学梯度就是将膨胀后的图像和腐蚀后的图像做差,从而得到图像的边缘信息。用公式表示就是:

其中,src代表原图像;dst代表返回矩阵,E表示进行形态学操作所使用的内核,
代表膨胀,
代表腐蚀。

3.2轮廓匹配
3.2.1 寻找并绘制轮廓
OpenCV提供了现成的查找轮廓的函数findContours,绘制轮廓的函数drawContours,从而可以将获得到的边缘轮廓绘制出来。
cpp
{
std::vector<Vec4i> hierarchy;
std::vector<std::vector<Point> > contours;
Canny(m_MorpImg, m_MorpImg, 100, 250);
findContours(m_MorpImg, contours, hierarchy, RETR_LIST, CHAIN_APPROX_SIMPLE);
for (size_t i = 0; i < contours.size(); i++)
drawContours(m_SrcImg, contours, i, Scalar(0, 0, 255), 2, LINE_AA, hierarchy, 0);
}
执行效果如下:

findContours的函数说明:
cpp
findContours( InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset=Point());
- image,单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过 Canny、拉普拉斯等边缘检测算子处理过的二值图像;
- contours,定义为" vector<vector<Point>>",是一个双重向量,向量内每个元素保存了一组由连续的Point点构成的点的集合的向量,每一组Point点集就是一个轮廓。有多少轮廓,向量contours就有多少元素。
- hierarchy,定义为"vector<Vec4i>",先来看一下Vec4i的定义: typedef Vec<int, 4> Vec4i,定义了一个"向量内每一个元素包含了4个int型变量"的向量。所以从定义上看,hierarchy也是一个向量,向量内每个元素保存了一个包含4个int整型的数组。 向量hiararchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。hierarchy向量内每一个元素的4个int型变量------hierarchy[i][0] ~hierarchy[i][3],分别表示第 i个轮廓的下一条轮廓、上一条轮廓、当前轮廓的第一条子轮廓、当前轮廓的父轮廓的索引编号。如果当前轮廓没有对应的下一条轮廓、上一条轮廓、第一条子轮廓或父轮廓的话,则hierarchy[i][0] ~hierarchy[i][3]的相应位被设置为默认值-1。
- mode,定义轮廓的检索模式:
- CV_RETR_EXTERNAL 只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
- CV_RETR_LIST 检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,
- CV_RETR_CCOMP 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
- CV_RETR_TREE, 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
- method,定义轮廓的近似方法:
- CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
- CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留
- CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS 使用 teh-Chinl chain 近似算法
- Point 偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,并且Point还可以是负值。
drawContours的函数说明
cpp
void drawContours(
InputOutputArray image,
OutputArrayOfArrays & contours,
int contourIdx,
const cv::Scalar& color,
int thickness = 1,
int lineType = 8,
const cv::Mat& hierarchy = cv::Mat(),
int maxLevel = INT_MAX,
cv::Point offset = cv::Point());
- image: 输入输出图像,在其上绘制轮廓。
- contours: 轮廓的集合,每个轮廓是一个点的向量。轮廓通常由 cv::findContours() 函数生成。每个轮廓是一个 std::vector<cv::Point> 对象,包含了一系列按顺序排列的点。
- contourIdx: 该参数指定绘制哪个轮廓。
- 取值 -1 表示绘制所有轮廓。
- 非负值指定绘制特定索引的轮廓。
- color: 绘制轮廓的颜色,类型是 cv::Scalar。
- 对于彩色图像,cv::Scalar(0, 255, 0) 表示绿色。
- 对于灰度图像,cv::Scalar(255) 表示白色(轮廓)。
- thickness: 轮廓线的厚度。取值:正整数,表示轮廓线的宽度。当为 -1 或 cv::FILLED 表示填充轮廓内部区域。
- lineType: 线条类型,指定绘制轮廓线的连接方式。
- 8:8-connectivity(默认),连通性为8。
- 4:4-connectivity,连通性为4。
- CV_AA:抗锯齿线条(Anti-Aliasing),更平滑的线条,但计算更复杂。
- hierarchy: 轮廓的层次结构矩阵(可选)。类型是 cv::Mat,通常由 cv::findContours() 函数返回。
- maxLevel: 绘制的最大层级(可选)。
- 取值:整数,INT_MAX 表示绘制所有层级的轮廓。
- offset: 对绘制轮廓的坐标进行偏移(可选)。类型是 cv::Point,用于将所有轮廓点的坐标偏移到指定位置,通常用于处理多个图像拼接的情况。
3.2.2 四边形轮廓匹配
首先,需要剔除形状过于不规则的轮廓,因为待识别的对象是矩形,因此可以根据这一原则剔除那些形状过于不规则的轮廓,从而缩小要识别的轮廓范围。
首先需要对所有轮廓进行多边形估计,根据液晶的矩形表面特征,剔除边数不等于4或面积过小的轮廓,同时保留凸四边形,多边形估计函数为approxPolyDP。
其次剔除长宽比超过误差范围的轮廓,从而进一步缩小轮廓范围,由于拍摄角度的影响,长宽比的合格范围要放的宽一些。
最后为避免出现双轮廓的情况(内轮廓与外轮廓),在满足上述要求的情况下,选择最大轮廓。
- 多边形估计函数:approxPolyDP
函数原型:void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed)
参数详解;
InputArray curve:一般是由图像的轮廓点组成的点集
OutputArray approxCurve:表示输出的多边形点集
double epsilon:主要表示输出的精度,也即是原始曲线与近似曲线之间的最大距离,
bool closed:表示输出的多边形是否封闭
- 四边形的凹凸判断函数:isContourConvex
凸四边形:每个内角都小于180度的四边形或者说四边形都在每条边所在直线的同侧。凹四边形:至少1个内角大于180度的四边形或者说四边形在某条边所在直线两侧。

OpenCV提供isContourConvex函数用于判断四边形是否为凸四边形。
函数原型:bool cv::isContourConvex(InputArray contour)
参数详解:contour:输入轮廓
- 获取四边形的长宽比:
cpp
float getAspectRate(InputArray cPoly)
{
RotatedRect rrc = minAreaRect(cPoly);
if (rrc.size.width > rrc.size.height)
return (rrc.size.width * 1.0) / rrc.size.height;
else
return (rrc.size.height * 1.0) / rrc.size.width;
}
获取四边形的长宽比可以通过minAreaRect获取四边形的最小外接矩形,该函数返回的是一个旋转矩形(RotatedRect),它包含了矩形中心的坐标、矩形的大小(宽度和高度)以及旋转角度。
利用该矩形的宽度和高度即可获取长宽比。
3.2.3 源码以及执行效果
cpp
void MFormMatch::OnBnClickedBtnPosition()
{
std::vector<Vec4i> hierarchy;
std::vector<std::vector<Point> > ContoursAll;
std::vector<std::vector<Point> > ContoursQua;
Canny(m_MorpImg, m_MorpImg, 100, 250); //Canny 边缘检测
findContours(m_MorpImg, ContoursAll, hierarchy, RETR_LIST, CHAIN_APPROX_SIMPLE); //寻找轮廓
for (size_t i = 0; i < ContoursAll.size(); i++) //循环匹配轮廓
{
approxPolyDP(ContoursAll[i], ContoursPoly, 50, true); //多边形拟合
if (ContoursPoly.size() == 4 && contourArea(ContoursPoly) > 100)//四边形,且去除面积小于100的四边形
{
float fAspectRate = getAspectRate(ContoursPoly); //计算长宽比
if (fAspectRate < 1.5)
ContoursQua.push_back(ContoursPoly); //填入符合要求的轮廓
}
}
long lMaxArea = 0;
size_t llMaxPos = -1;
for (size_t i = 0; i < ContoursQua.size(); i++) //寻找面积最大的轮廓
{
long lArea = contourArea(ContoursQua[i], false);
if (lArea > lMaxArea)
{
lMaxArea = lArea;
llMaxPos = i;
}
}
if (llMaxPos >= 0)
{
ContoursPoly = ContoursQua[llMaxPos];
drawContours(m_SrcImg, ContoursQua, llMaxPos, Scalar(0, 0, 255), 2, LINE_AA, hierarchy, 0);
}
ImgShow(m_hSrcDC, m_cSrcRect, m_SrcImg);
}