前言

相信大家在登陆某个网站时或多或少都会经历过需要验证码才能登陆。常见的验证码方式有字符输入验证码、滑动条拼图验证码以及字符点选验证码。本案例要实现的是中文字符点选验证。本案例逻辑算法仅为本人为实现此功能所设计,供大家参考交流。

一、图像预处理

OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP
如图所示,本案例要实现的功能是:只有依次点击”灭“、”芽“、”切“三个字符才能验证通过,除此之外的任何验证都不能通过。因此,后面就需要进行字符识别。

1.1 字符切割

首先,我们需要将”灭“、”芽“、”切“三个字符从原图中切割出来。进行轮廓提取,切割出最外矩形就行了,后面的识别也是根据轮廓进行匹配的。关于模板字符切割部分没在源码中展示,请大家自行编写代码进行字符切割。
OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP
如图为切割出来的字符,保存在本地文件夹,且按”灭“、”芽“、”切“顺序进行读取。

1.2 字符提取

首先需要进行的是图像二值化。但是考虑到每个字符的颜色都不同,简单的灰度阈值行不通,所以得先把原图转成HSV类型,之后再用滑动条确定高低阈值。下面注释掉的源码仅是为了确定高低阈值。


	//int hmin, hmax, smin, smax, vmin, vmax;
	//void myfunc(int, void*)
	//{
	//	Scalar lower(hmin, smin, vmin);
	//	Scalar upper(hmax, smax, vmax);
	//
	//	Mat mask;
	//	inRange(gaussian, lower, upper, mask);
	//	imshow("test", mask);
	//}


	//这段注释掉的代码使用滑动条确定阈值范围,即下面的lowerb,upperb
	//namedWindow("demo", WINDOW_NORMAL);
	//createTrackbar("hmin", "demo", &hmin, 180, myfunc);
	//createTrackbar("hmax", "demo", &hmax, 180, myfunc);
	//createTrackbar("smin", "demo", &smin, 255, myfunc);
	//createTrackbar("smax", "demo", &smax, 255, myfunc);
	//createTrackbar("vmin", "demo", &vmin, 255, myfunc);
	//createTrackbar("vmax", "demo", &vmax, 255, myfunc);
	//得到掩模图像
	Mat mask;
	Scalar lowerb(0, 90, 90);
	Scalar upperb(180, 255, 255);
	inRange(gaussian, lowerb, upperb, mask);

OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP
如上图所示为二值化后的结果,虽然以及将这些字符分割出来了,但是一个字符它们的轮廓并不是连着的,对后面单字符轮廓提取不方便。所以,还需要进行一些形态学操作。我这里使用的是膨胀操作,将二值图像进行膨胀处理,得到的字符轮廓就融为一体了。

	//对得到的二值图像进行膨胀处理,将整个字符轮廓融为一体
	Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11));
	dilate(mask, mask, kernel);

OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP
二值化处理之后的结果如图所示,下面就方便进行单字符提取了。使用findContours进行轮廓提取,并将字符图像以及字符所在的矩形区域进行保存。

	vector<vector<Point>>contours;
	findContours(mask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);//仅提取最外围轮廓
	for (int i = 0; i < contours.size(); i++)
	{
		double area = contourArea(contours[i]);
		if (area > 1000)
		{
			Rect rect = boundingRect(contours[i]);

			Mat roi = src(rect);

			roi_mat.push_back(ROI{ roi,rect });//将roi图像及其矩形框放置容器,以便后续识别
		}
	}

二、鼠标点击事件

既然是点击验证,当然需要进行鼠标点击事件。使用setMouseCallback进行鼠标点击。
首先我们需要将每个字符所在的矩形区域进行保存,因为后续需要判断:鼠标点击区域是否落在字符区域,只有有效的字符点击才会进行下一步识别匹配。

	//提取所有矩形区域四角点,用于后续判断鼠标点击坐标是否落在矩形框内
	for (int i = 0; i < roi_mat.size(); i++)
	{
		Point top_left = Point(roi_mat[i].rect.x, roi_mat[i].rect.y);
		Point bottom_left = Point(roi_mat[i].rect.x, roi_mat[i].rect.y + roi_mat[i].rect.height);
		Point bottom_right = Point(roi_mat[i].rect.x + roi_mat[i].rect.width, roi_mat[i].rect.y + roi_mat[i].rect.height);
		Point top_right = Point(roi_mat[i].rect.x + roi_mat[i].rect.width, roi_mat[i].rect.y);

		PointRect.push_back({ top_left ,bottom_left,bottom_right,top_right });
	}

通过使用pointPolygonTest 用于判断“点”是否在某闭合区域内。

			//pointPolygonTest 用于判断“点”是否在某闭合区域内
			if (pointPolygonTest(PointRect[i], point, false) >= 0)

通过判断点击区域与之前保存的矩形区域,将点击选中的roi图像放入容器中。

						if (roi_mat[j].rect.x == PointRect[i][0].x)
						{
							clicked_mat.push_back(roi_mat[j]);//将点击选中的roi图像放入容器中
						}

还有一个需要注意的地方就是,同一个字符不能点击两次,即同一个字符,不管你点击多少次,有效保存记录的只有一次。

					vector<vector<Point>>::iterator it = PointRect.begin() + i;
					PointRect.erase(it);  //将以点击过的可选范围剔除,即同一个区域不可点击两次

具体实现请大家自行阅读源码,关键地方注释也比较清楚。

2.1 功能源码

Point point(-1, -1);//初始化鼠标点击坐标
int clicked_Count = 0;//鼠标有效点击次数
void onMouse(int event, int x, int y,  int flags, void *userdata)
{
	Mat image = *((Mat*)userdata);

	if (event == EVENT_LBUTTONDOWN)
	{
		point.x = x;
		point.y = y;
	}
	if (event == EVENT_LBUTTONUP)
	{
		for (int i = 0; i < PointRect.size(); i++)
		{
			//pointPolygonTest 用于判断“点”是否在某闭合区域内
			if (pointPolygonTest(PointRect[i], point, false) >= 0)
			{
				clicked_Count++; //计数,用于统计有效点击次数(只有在字符区域点击才有效)

				if (clicked_Count <= 3)
				{
					for (int j = 0; j < roi_mat.size(); j++)
					{
						if (roi_mat[j].rect.x == PointRect[i][0].x)
						{
							clicked_mat.push_back(roi_mat[j]);//将点击选中的roi图像放入容器中
						}
					}

					//绘制点击效果
					circle(image, Point(PointRect[i][0].x+20, PointRect[i][0].y-20), 30, Scalar(0, 0, 255), 2);
					putText(image, to_string(clicked_Count), PointRect[i][0], FONT_HERSHEY_DUPLEX, 2, Scalar(0, 0, 255), 3);
					imshow("demo", image);

					vector<vector<Point>>::iterator it = PointRect.begin() + i;
					PointRect.erase(it);  //将以点击过的可选范围剔除,即同一个区域不可点击两次

				}
			}
		}
	}
}

OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP

三、字符匹配

上一步,已经将点击的字符都提取保存下来了(按有效点击顺序保存)。下一步需要的是,将点击到的字符与之前准备好的模板字符进行匹配。由于模板字符与点击字符是一摸一样的,所以这里我使用最简单的字符匹配方法,即两个字符的像素差,像素差小于某阈值的字符即为匹配成功。关于字符匹配问题大家如果有更好、更鲁棒的算法可以一起交流学习!!!

3.1 功能源码

	/*
		字符识别
		由于我使用的模板图像是直接从原图上截取下来的,
		故这里使用最简单的字符匹配算法,取其两图片像素差以判断两图片相似程度
	*/

	int correct_Count = 0;//匹配正确次数
	for (int i = 0; i < file_name.size(); i++)
	{
		Mat src_roi = imread(file_name[i]);
		resize(src_roi, src_roi, Size(88, 88), 1.0, 1.0, INTER_LINEAR);//这里需注意:需将两张图片resize成相同大小才能进行像素差计算
		cvtColor(src_roi, src_roi, COLOR_BGR2HSV);
		inRange(src_roi, Scalar(0, 70, 0), Scalar(180, 255, 255), src_roi);
		//imshow("src_roi", src_roi);

		Mat clicked_roi = clicked_mat[i].roi;
		resize(clicked_roi, clicked_roi, Size(88, 88), 1.0, 1.0, INTER_LINEAR);
		cvtColor(clicked_roi, clicked_roi, COLOR_BGR2HSV);
		inRange(clicked_roi, Scalar(0, 70, 0), Scalar(180, 255, 255), clicked_roi);
		//imshow("clicked_roi", clicked_roi);

		Mat result;
		absdiff(src_roi, clicked_roi, result);//计算两张图片像素差
		//imshow("result", result);

		kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
		erode(result, result, kernel);

		int pix_sum = countNonZero(result);

		if (pix_sum < 10)
		{
			correct_Count++;
		}
	}

四、结果展示

最终的结果如下图所示,可以看到,当鼠标点击到的字符不是”灭“、”芽“、”切“时,验证失败;当鼠标点击顺序不是”灭“、”芽“、”切“时,验证失败;只有当鼠标点击字符以及点击顺序是”灭“、”芽“、”切“时,才会验证通过。
OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP
OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP

OpenCV C++案例实战三十《中文点选验证码识别》-LMLPHP

五、源码

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;


//int hmin, hmax, smin, smax, vmin, vmax;
//void myfunc(int, void*)
//{
//	Scalar lower(hmin, smin, vmin);
//	Scalar upper(hmax, smax, vmax);
//
//	Mat mask;
//	inRange(gaussian, lower, upper, mask);
//	imshow("test", mask);
//}


//自定义结构体,用于保存roi图像及其矩形框
struct ROI
{
	cv::Mat roi;
	cv::Rect rect;
};


//定义全局变量
vector<ROI>roi_mat;//所有的中文字符roi区域(图片、矩形)
vector<vector<Point>>PointRect;//所有的中文字符roi矩形区域
vector<ROI>clicked_mat;//所有鼠标点击过的中文字符roi


Point point(-1, -1);//初始化鼠标点击坐标
int clicked_Count = 0;//鼠标有效点击次数
void onMouse(int event, int x, int y,  int flags, void *userdata)
{
	Mat image = *((Mat*)userdata);

	if (event == EVENT_LBUTTONDOWN)
	{
		point.x = x;
		point.y = y;
	}
	if (event == EVENT_LBUTTONUP)
	{
		for (int i = 0; i < PointRect.size(); i++)
		{
			//pointPolygonTest 用于判断“点”是否在某闭合区域内
			if (pointPolygonTest(PointRect[i], point, false) >= 0)
			{
				clicked_Count++; //计数,用于统计有效点击次数(只有在字符区域点击才有效)

				if (clicked_Count <= 3)
				{
					for (int j = 0; j < roi_mat.size(); j++)
					{
						if (roi_mat[j].rect.x == PointRect[i][0].x)
						{
							clicked_mat.push_back(roi_mat[j]);//将点击选中的roi图像放入容器中
						}
					}

					//绘制点击效果
					circle(image, Point(PointRect[i][0].x+20, PointRect[i][0].y-20), 30, Scalar(0, 0, 255), 2);
					putText(image, to_string(clicked_Count), PointRect[i][0], FONT_HERSHEY_DUPLEX, 2, Scalar(0, 0, 255), 3);
					imshow("demo", image);

					vector<vector<Point>>::iterator it = PointRect.begin() + i;
					PointRect.erase(it);  //将以点击过的可选范围剔除,即同一个区域不可点击两次

				}
			}
		}
	}
}

int main()
{
	Mat src = imread("src.jpg");
	if (src.empty())
	{
		cout << "can not read the image..." << endl;
		system("pause");
		return -1;
	}

	//读取模板图像,此处的模板图像是按正确点击顺序排列
	string path_name = "template";
	vector<string>file_name;
	glob(path_name, file_name);
	if (file_name.empty())
	{
		cout << "can not read the file..." << endl;
		system("pause");
		return -1;
	}

	//将图像转成hsv通道,进行阈值分割
	Mat hsv;
	cvtColor(src, hsv, COLOR_BGR2HSV);

	Mat gaussian;
	GaussianBlur(hsv, gaussian, Size(3, 3), 0);

	//这段注释掉的代码使用滑动条确定阈值范围,即下面的lowerb,upperb
	//namedWindow("demo", WINDOW_NORMAL);
	//createTrackbar("hmin", "demo", &hmin, 180, myfunc);
	//createTrackbar("hmax", "demo", &hmax, 180, myfunc);
	//createTrackbar("smin", "demo", &smin, 255, myfunc);
	//createTrackbar("smax", "demo", &smax, 255, myfunc);
	//createTrackbar("vmin", "demo", &vmin, 255, myfunc);
	//createTrackbar("vmax", "demo", &vmax, 255, myfunc);

	//得到掩模图像
	Mat mask;
	Scalar lowerb(0, 90, 90);
	Scalar upperb(180, 255, 255);
	inRange(gaussian, lowerb, upperb, mask);

	//对得到的二值图像进行膨胀处理,将整个字符轮廓融为一体
	Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11));
	dilate(mask, mask, kernel);
	imwrite("temp.jpg", mask);

	vector<vector<Point>>contours;
	findContours(mask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);//仅提取最外围轮廓
	for (int i = 0; i < contours.size(); i++)
	{
		double area = contourArea(contours[i]);
		if (area > 1000)
		{
			Rect rect = boundingRect(contours[i]);

			Mat roi = src(rect);

			roi_mat.push_back(ROI{ roi,rect });//将roi图像及其矩形框放置容器,以便后续识别
		}
	}

	//提取所有矩形区域四角点,用于后续判断鼠标点击坐标是否落在矩形框内
	for (int i = 0; i < roi_mat.size(); i++)
	{
		Point top_left = Point(roi_mat[i].rect.x, roi_mat[i].rect.y);
		Point bottom_left = Point(roi_mat[i].rect.x, roi_mat[i].rect.y + roi_mat[i].rect.height);
		Point bottom_right = Point(roi_mat[i].rect.x + roi_mat[i].rect.width, roi_mat[i].rect.y + roi_mat[i].rect.height);
		Point top_right = Point(roi_mat[i].rect.x + roi_mat[i].rect.width, roi_mat[i].rect.y);

		PointRect.push_back({ top_left ,bottom_left,bottom_right,top_right });
	}

	Mat src_canvas = src.clone();//将原图拷贝一份,用于效果绘制

	//鼠标点击事件
	while (true)
	{
		char key = waitKey(10);
		namedWindow("demo", WINDOW_NORMAL);
		imshow("demo", src_canvas);
		setMouseCallback("demo", onMouse, &src_canvas);
		if (clicked_Count == 3)break; //当有效点击次数为3时,退出鼠标点击操作,执行下一步识别工作
	}

	if (file_name.size() != clicked_mat.size())return -1;

	/*
		字符识别
		由于我使用的模板图像是直接从原图上截取下来的,
		故这里使用最简单的字符匹配算法,取其两图片像素差以判断两图片相似程度
	*/

	int correct_Count = 0;//匹配正确次数
	for (int i = 0; i < file_name.size(); i++)
	{
		Mat src_roi = imread(file_name[i]);
		resize(src_roi, src_roi, Size(88, 88), 1.0, 1.0, INTER_LINEAR);//这里需注意:需将两张图片resize成相同大小才能进行像素差计算
		cvtColor(src_roi, src_roi, COLOR_BGR2HSV);
		inRange(src_roi, Scalar(0, 70, 0), Scalar(180, 255, 255), src_roi);
		//imshow("src_roi", src_roi);

		Mat clicked_roi = clicked_mat[i].roi;
		resize(clicked_roi, clicked_roi, Size(88, 88), 1.0, 1.0, INTER_LINEAR);
		cvtColor(clicked_roi, clicked_roi, COLOR_BGR2HSV);
		inRange(clicked_roi, Scalar(0, 70, 0), Scalar(180, 255, 255), clicked_roi);
		//imshow("clicked_roi", clicked_roi);

		Mat result;
		absdiff(src_roi, clicked_roi, result);//计算两张图片像素差
		//imshow("result", result);

		kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
		erode(result, result, kernel);

		int pix_sum = countNonZero(result);

		if (pix_sum < 10)
		{
			correct_Count++;
		}
	}

	//结果显示--仅有每次都顺序点击才能验证成功
	if (correct_Count == 3)
	{
		putText(src_canvas, "Validation succeeded !", Point(src_canvas.cols / 2 - 300, src_canvas.rows - 20), FONT_HERSHEY_DUPLEX, 2, Scalar(0, 255, 0), 3);
	}
	else
	{
		putText(src_canvas, "Validation failed !", Point(src_canvas.cols / 2 - 300, src_canvas.rows - 20), FONT_HERSHEY_DUPLEX, 2, Scalar(0, 0, 255), 3);
	}

	namedWindow("demo", WINDOW_NORMAL);
	imshow("demo", src_canvas);
	waitKey(0);
	destroyAllWindows();
	system("pause");
	return 0;
}

总结

本文使用OpenCV C++ 进行中文点选验证码识别,主要操作有以下几点。
1、图像预处理,提取出字符轮廓并进行切割
2、使用鼠标响应事件进行字符点选,即三次有效字符点选
3、字符匹配,即按点选顺序与模板顺序进行字符比较
4、只有鼠标点击字符与点击顺序都符合要求时,才会验证通过。

10-05 09:31