计算机系统应用教程网站

网站首页 > 技术文章 正文

基于opencv图像识别的AI五子棋系列2—查找棋盘轮廓

btikc 2024-09-30 13:13:15 技术文章 13 ℃ 0 评论

该篇文章是基于opencv图像识别的AI五子棋系列的第二篇文章。通过该系列文章,你可以进阶opencv图像识别,熟悉图像识别与处理的大致流程,熟悉AI智能......


上一篇文章对五子棋棋盘进行了预处理操作,该篇文章,则在上一篇文章的基础上进行棋盘轮廓的查找,查找到轮廓后方便我们进行后续处理。

首先看一下处理的效果图

效果示意图

程序具体实现过程

我们的目标就是寻找到棋盘的最外边轮廓,那么如何寻找呢,我们可以利用opencv的一个函数:

findContours(srcImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);//找轮廓 CV_RETR_EXTERNAL,表示最外层轮廓

这个函数的详细解释如下:

函数原型

findContours(InputOutputArray image, OutputArrayOfArrays contours, 

             OutputArray hierarchy, int mode, int method, Point offset = Point());
  • 参数1单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像;
  • 参数2contours定义为“vector<vector<Point>> contours”,是一个双重向量(向量内每个元素保存了一组由连续的Point构成的点的集合的向量),每一组点集就是一个轮廓,有多少轮廓,contours就有多少元素;
  • 参数3hierarchy定义为“vector<Vec4i> hierarchy”,Vec4i的定义:typedef Vec<int, 4> Vec4i;(向量内每个元素都包含了4个int型变量),所以从定义上看,hierarchy是一个向量,向量内每个元素都是一个包含4个int型的数组。向量hierarchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。hierarchy内每个元素的4个int型变量是hierarchy[i][0] ~ hierarchy[i][3],分别表示当前轮廓 i 的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的编号索引。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓,则相应的hierarchy[i][*]被置为-1。
  • 参数4定义轮廓的检索模式,取值如下:

CV_RETR_EXTERNAL:只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略;

CV_RETR_LIST:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1,具体下文会讲到;

CV_RETR_CCOMP: 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层;

CV_RETR_TREE: 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。

  • 参数5定义轮廓的近似方法,取值如下:

CV_CHAIN_APPROX_NONE:保存物体边界上所有连续的轮廓点到contours向量内;

CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留;

CV_CHAIN_APPROX_TC89_L1:使用teh-Chinl chain 近似算法;

CV_CHAIN_APPROX_TC89_KCOS:使用teh-Chinl chain 近似算法。

  • 参数6Point偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,并且Point还可以是负值!

注意:本文中参数3和参数6被省略。

寻找轮廓的函数看似简单,但想用好,还需要不断地进行优化和过滤,因为这个函数最开始找到的轮廓可能有十几个。因此我们需要根据经验,将其他轮廓过滤掉,这里我们利用了轮廓的长度进行了过滤,在摄像头的位置和图像分辨率确定后,我们图像的轮廓长度应该是在某个特定范围内的。

下面是通过长度过滤的代码片段:

for (int i = 0; i < contours.size(); i++)
{//历遍所有的轮廓

  length[i] = arcLength(contours[i], true);

  if (length[i] >2000 && length[i] <12000)
  {//通过长度匹配滤除小轮廓
    convexHull(Mat(contours[i]), hull[i], false);//把凸包找出来
    Area_contours[i] = contourArea(contours[i]);
    Area_hull[i] = contourArea(hull[i]);
    Rectangularity[i] = Area_contours[i] / Area_hull[i];
    printf("过滤后轮廓长度为 length=%5f\n", length[i]);
    RectContours.push_back(hull[i]);//把提取出来的方框导入到新的轮廓组
    drawContours(drawing, hull, i, Scalar(0, 0, 0), 1);//得到方框
  }
}

通过上述代码的处理,我们初步找到了轮廓,如下图:


我们可以看到上图总已经大体找到了棋盘的轮廓,但是美中不足的是,图像轮廓有两个,这是什么原因呢?原来通过观察原图,我们发现图像中间有条黑色的线,使得图像有些分离,那么我们该如何找到一个完美的轮廓呢?最简单的办法是将两个轮廓连通。

这里我利用了一个小技巧:

1、找到两个轮廓中心。

2、然后在两个轮廓中心画线,连通两个轮廓。

3、再进行查找最外边轮廓找到最终的轮廓。

下面是代码的具体实现过程:

vector<Point2f> zhongxin_zuobiao;
zhongxin_zuobiao = find_lunkuo_zhongxin(RectContours);
if (zhongxin_zuobiao.size() >= 2)
{
  for (int i = 0; i < zhongxin_zuobiao.size() - 1; i++)
  {
    for (int j = i + 1; j < zhongxin_zuobiao.size(); j++)
    {
      float dis_juxin = getDistance_1(zhongxin_zuobiao[i], zhongxin_zuobiao[j]);
      //if (dis_juxin <65)
      {
        line(drawing, zhongxin_zuobiao[i], zhongxin_zuobiao[j], Scalar(0), 40, LINE_AA);
      }

    }
  }

}
vector<vector<Point>> contours1;
findContours(drawing, contours1, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);//找轮廓 CV_RETR_EXTERNAL,表示最外层轮廓
Mat drawing1(drawing.size(), CV_8UC3, cv::Scalar(255, 255, 255));
vector<vector<Point>> hull1(contours1.size());//用于存放凸包
for (int i = 0; i < contours1.size(); i++)
{//历遍所有的轮廓
  //drawContours(drawing1, contours1, i, Scalar(0, 255, 0), 1);//得到方框
  convexHull(Mat(contours1[i]), hull1[i], false);//把凸包找出来
  RectContours_fainal.push_back(hull1[i]);//把提取出来的方框导入到新的轮廓组
  drawContours(drawing1, hull1, i, Scalar(0, 0, 0), 4);//得到方框
}

查找轮廓中点的部分,我封装成了一个函数。

/*******************************************************************************************
*函数功能 : 找每个轮廓的中心坐标
*输入参数 :轮廓或者凸包
*返 回 值 : 点向量
*编写时间 : 2018.8.9
*作    者 : diyun
********************************************************************************************/
vector< Point2f> find_lunkuo_zhongxin(vector<vector<Point>> dangban_RectContours)
{

	vector<Point2f> zhongxin_zuobiao;
	/// 计算矩
	vector<Moments> mu(dangban_RectContours.size());
	for (int i = 0; i < dangban_RectContours.size(); i++)
	{
		mu[i] = moments(dangban_RectContours[i], false);
	}
	///  计算中心矩:
	vector<Point2f> mc(dangban_RectContours.size());
	for (int i = 0; i < dangban_RectContours.size(); i++)
	{
		mc[i] = Point2f(mu[i].m10 / mu[i].m00, mu[i].m01 / mu[i].m00);
		zhongxin_zuobiao.push_back(mc[i]);
	}

	return	zhongxin_zuobiao;
}

通过上面的处理我们最终完成了棋盘轮廓的查找:

完整代码


/*****************************************************************************************
五子棋棋盘棋子识别检测
1、灰度化,二值化
2、查找棋盘最外边轮廓

*****************************************************************************************/
#include<opencv2/opencv.hpp>
#include <iostream> 
#include <fstream>  
#include <stdlib.h> //srand()和rand()函数 
#include <time.h> //time()函数 
#include <opencv2/core/core.hpp> 
#include <opencv2/highgui/highgui.hpp> 
#include <opencv2/imgproc/imgproc.hpp>  
#include <opencv2/objdetect/objdetect.hpp> 
#include <opencv2/ml/ml.hpp>  
#include<opencv2\opencv.hpp>
#include <opencv2\imgproc\types_c.h>
#include<windows.h>


using namespace std;
using namespace cv;
using namespace ml;


/*******************************************************************************************
*函数功能 : 找每个轮廓的中心坐标
*输入参数 :轮廓或者凸包
*返 回 值 : 点向量
*编写时间 : 2018.8.9
*作    者 : diyun
********************************************************************************************/
vector< Point2f> find_lunkuo_zhongxin(vector<vector<Point>> dangban_RectContours)
{

	vector<Point2f> zhongxin_zuobiao;
	/// 计算矩
	vector<Moments> mu(dangban_RectContours.size());
	for (int i = 0; i < dangban_RectContours.size(); i++)
	{
		mu[i] = moments(dangban_RectContours[i], false);
	}
	///  计算中心矩:
	vector<Point2f> mc(dangban_RectContours.size());
	for (int i = 0; i < dangban_RectContours.size(); i++)
	{
		mc[i] = Point2f(mu[i].m10 / mu[i].m00, mu[i].m01 / mu[i].m00);
		zhongxin_zuobiao.push_back(mc[i]);
	}

	return	zhongxin_zuobiao;

}

//两点之间距离
float getDistance_1(Point pointO, Point pointA)
{
	float distance;
	distance = powf((pointO.x - pointA.x), 2) + powf((pointO.y - pointA.y), 2);
	distance = sqrtf(distance);
	return distance;
}

int main()
{
	float ret = 0;

	Mat srcImage0 = imread("1.jpg");//读取图片 
									//Mat srcImage0 = imread("app/1.jpg");//读取图片 
	if (srcImage0.empty())
	{
		cout << " 待预测图像不存在: " << endl;
		printf("[ALG ERROR][函数:%s][行号:%d],图片未正常读取,请检查输入路径十分正确 \n", __FUNCTION__, __LINE__, 1);
		cout << " 待预测图像不存在: " << endl;
	}
	Mat srcImage, srcImage1;
	resize(srcImage0, srcImage0, Size(1920, 1080));
	cvtColor(srcImage0, srcImage1, CV_BGR2GRAY);

	namedWindow("灰度化", 0);
	imshow("灰度化", srcImage1);
	waitKey(2);
	srcImage = srcImage1 > 150; // 二值化
	namedWindow("二值化", 0);
	imshow("二值化", srcImage);
	waitKey(2);

	/*****1、输入二值化图像返回有用的轮廓*****/
	/*一般通过长度滤除剩下两个,选择内部那个*/

		vector<vector<Point>> contours, RectContours, RectContours_fainal;
		int height = srcImage.rows;
		findContours(srcImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);//找轮廓 CV_RETR_EXTERNAL,表示最外层轮廓
		vector<vector<Point>> hull(contours.size());//用于存放凸包
		vector<float> length(contours.size());
		vector<float> Area_contours(contours.size()), Area_hull(contours.size()), Rectangularity(contours.size()), circularity(contours.size());
		Mat drawing(srcImage.size(), CV_8UC3, cv::Scalar(255, 255, 255));
		for (int i = 0; i < contours.size(); i++)
		{//历遍所有的轮廓

			length[i] = arcLength(contours[i], true);

			if (length[i] >2000 && length[i] <12000)
			{//通过长度匹配滤除小轮廓
				convexHull(Mat(contours[i]), hull[i], false);//把凸包找出来
				Area_contours[i] = contourArea(contours[i]);
				Area_hull[i] = contourArea(hull[i]);
				Rectangularity[i] = Area_contours[i] / Area_hull[i];
				printf("过滤后轮廓长度为 length=%5f\n", length[i]);
				circularity[i] = (4 * 3.1415*Area_contours[i]) / (length[i] * length[i]);
				//drawContours(drawing, contours, i, (255, 0, 0), 2);//得到方框
				//if (Rectangularity[i]>0.9&&circularity[i]<0.8&&Area_hull[i]>8000 && Area_hull[i]<50000)
				{//通过凸包面积滤除不对的凸包,找到最终的四边形
					RectContours.push_back(hull[i]);//把提取出来的方框导入到新的轮廓组
					drawContours(drawing, hull, i, Scalar(0, 0, 0), 1);//得到方框
				}
			}
		}
		if (0 == RectContours.size())
		{
			printf("[ALG ERROR][函数:%s][行号:%d],RectContours.size=0,当前帧未找到轮廓,检查仪表二值化是否正常\n", __FUNCTION__, __LINE__);
		}
		/*识别结果显示,调试用*/
		//namedWindow("找轮廓结果", 0);
		//imshow("找轮廓结果", drawing);
		//waitKey(2);

		/*将多个轮廓,弄成一个*/
		cvtColor(drawing, drawing, CV_BGR2GRAY);
		drawing = drawing<150; // 二值化
							   //imshow("二值化", drawing);
							   //将线连接起来
		vector<Point2f> zhongxin_zuobiao;
		zhongxin_zuobiao = find_lunkuo_zhongxin(RectContours);
		if (zhongxin_zuobiao.size() >= 2)
		{
			for (int i = 0; i < zhongxin_zuobiao.size() - 1; i++)
			{
				for (int j = i + 1; j < zhongxin_zuobiao.size(); j++)
				{
					float dis_juxin = getDistance_1(zhongxin_zuobiao[i], zhongxin_zuobiao[j]);
					//if (dis_juxin <65)
					{
						line(drawing, zhongxin_zuobiao[i], zhongxin_zuobiao[j], Scalar(0), 40, LINE_AA);
					}

				}
			}

		}
		vector<vector<Point>> contours1;
		findContours(drawing, contours1, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);//找轮廓 CV_RETR_EXTERNAL,表示最外层轮廓
		Mat drawing1(drawing.size(), CV_8UC3, cv::Scalar(255, 255, 255));
		vector<vector<Point>> hull1(contours1.size());//用于存放凸包
		for (int i = 0; i < contours1.size(); i++)
		{//历遍所有的轮廓
		 //drawContours(drawing1, contours1, i, Scalar(0, 255, 0), 1);//得到方框
			convexHull(Mat(contours1[i]), hull1[i], false);//把凸包找出来
			RectContours_fainal.push_back(hull1[i]);//把提取出来的方框导入到新的轮廓组
			drawContours(drawing1, hull1, i, Scalar(0, 0, 0), 4);//得到方框
		}

		namedWindow("最终轮廓", 0);
		imshow("最终轮廓", drawing1);
		waitKey(0);

	return 0;
}

下篇文章讲述根据最外边轮廓寻找棋盘的四个顶点。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表