在上一篇博客《OpenCV4学习笔记(54)》中,整理了关于KNN最近邻算法的一些相关内容和一个手写体数字识别的例子。但是上次所实现的手写体数字识别,每次只能固定地输入测试图像进行预测,而且所使用的也是从OpenCV自带测试图中裁剪下来的数字。那么这一次笔记,我们就把上一次笔记的内容和OpenCV中的鼠标操作进行结合,实现一个可以利用鼠标书写数字并实时进行手写体数字识别的小程序。
关于KNN算法实现手写数字识别的部分内容,可以参阅我的上一篇博客,这里就不多赘述了。这里主要看一下OpenCV中关于鼠标操作的内容。
首先,我们需要声明一个鼠标的回调函数:
static void on_Mouse(int event, int x, int y, int flag, void*);
该函数的参数含义如下:
(1)参数event:鼠标事件
(2)参数x:鼠标当前点的x坐标
(3)参数y:鼠标当前的的y坐标
(4)参数flag:鼠标所处状态
然后,在程序中需要使用鼠标的时候,就加入一条定义鼠标操作的函数,当鼠标位于该指定窗口上时,执行回调函数:
setMouseCallback("digit", on_Mouse, 0);
该函数参数含义如下:
(1)参数windowName:指定的窗口名,当鼠标位于该窗口上时就执行回调函数。
(2)参数MouseCallback:需要执行的鼠标回调函数。
(3)参数userdata:用户传入数据,不需要可以直接置零。
接着就需要定义我们的鼠标回调函数来进行所需的操作了,在窗口中按下鼠标左键进行书写数字,写完后按空格键进行识别,按鼠标右键是清空窗口。代码演示如下:
static void on_Mouse(int event, int x, int y, int flag, void*)
{
if (event == EVENT_LBUTTONDOWN) //当鼠标左键按下,获取该点坐标
{
prePoint = Point(x, y);
}
//当鼠标左键保持按下标志,且鼠标移动
else if (event == EVENT_MOUSEMOVE && (flag & EVENT_FLAG_LBUTTON))
{
Point nowPoint(x, y);
line(digit, nowPoint, prePoint, Scalar(255), 2, 8, 0);
prePoint = nowPoint; //将鼠标的当前点作为上个点位,以开始下一个线段区域的绘制
imshow("digit", digit); //在窗口更新目标图像
}
//当鼠标右键按下时
else if (event == EVENT_RBUTTONDOWN)
{
imshow("digit", canvas);
digit = canvas.clone();
}
}
到这里,就实现了在一个窗口中书写数字的功能了,接下来只需要把上次笔记中的训练、识别部分加进来就可以了,下面是完整代码演示:
#include<opencv2/opencv.hpp>
#include<stdlib.h>
#include<iostream>
#include<string>
#include<Windows.h>
using namespace std;
using namespace cv;
Point prePoint; //定义鼠标指向的上一个点
Mat canvas = Mat::zeros(Size(28, 28), CV_8UC1); //黑底画布
Mat digit = canvas.clone(); //鼠标写下的手写体数字
static void on_Mouse(int event, int x, int y, int flag, void*); //定义鼠标操作的回调函数
void training(); //训练函数
int main()
{
//未得到模型前需要调用训练函数
//training();
//加载训练好的knn模型
Ptr<ml::KNearest> knn_test = ml::KNearest::load("knn_digits_model.yml");
namedWindow("digit", WINDOW_NORMAL);
resizeWindow("digit", Size(100, 100));
imshow("digit", canvas);
setMouseCallback("digit", on_Mouse, 0); //定义鼠标操作,当鼠标位于该指定窗口上时,执行回调函数
int i = 0;
while (true)
{
char ch = waitKey();
if (ch == ' ')
{
//resize(digit, digit, Size(28, 28));
digit = digit.reshape(1, 1);
digit.convertTo(digit, CV_32F);
Mat results = Mat::zeros(Size(1, 1), CV_32F);
knn_test->findNearest(digit, 11, results);
cout << "预测手写数字结果: " << results.at<float>(0, 0) << endl;
/* resize(digit, digit, Size(28, 28));
string path1 = "D:/opencv_c++/Learning-OpenCV/KNN手写数字识别/KNN手写数字识别/train_data/ " + to_string(i) + "_" + to_string(260) + ".jpg";
imwrite(path1, digit);
i++;*/
}
else if (ch == 27)
{
break;
}
}
waitKey(0);
return 0;
}
static void on_Mouse(int event, int x, int y, int flag, void*)
{
if (event == EVENT_LBUTTONDOWN) //当鼠标左键按下,获取该点坐标
{
prePoint = Point(x, y);
}
//当鼠标左键保持按下标志,且鼠标移动
else if (event == EVENT_MOUSEMOVE && (flag & EVENT_FLAG_LBUTTON))
{
Point nowPoint(x, y);
line(digit, nowPoint, prePoint, Scalar(255), 2, 8, 0);
prePoint = nowPoint; //将鼠标的当前点作为上个点位,以开始下一个线段区域的绘制
imshow("digit", digit); //在窗口更新目标图像
}
//当鼠标右键按下时
else if (event == EVENT_RBUTTONDOWN)
{
imshow("digit", canvas);
digit = canvas.clone();
}
}
void training()
{
vector<Mat> train_images;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 760; j++)
{
string path = "D:/opencv_c++/Learning-OpenCV/KNN手写数字识别/KNN手写数字识别/train_data/" + to_string(i) + "_" + to_string(j) + ".jpg";
Mat train_image = imread(path);
cvtColor(train_image, train_image, COLOR_BGR2GRAY);
train_images.push_back(train_image);
}
}
for (int n = 0; n < train_images.size(); n++)
{
train_images[n] = train_images[n].reshape(1, 1);
}
int height = 7600;
int width = 784;
Mat train_data = Mat::zeros(Size(width, height), CV_8UC1);
for (int r = 0; r < height; r++)
{
for (int c = 0; c < width; c++)
{
train_data.at<uchar>(r, c) = train_images[r].at<uchar>(0, c);
}
}
Mat labels = Mat::zeros(Size(1, height), CV_8UC1);
for (int k = 0; k < height; k++)
{
int label = k / 760;
labels.at<uchar>(k, 0) = label;
}
train_data.convertTo(train_data, CV_32F);
labels.convertTo(labels, CV_32F);
auto knn = ml::KNearest::create();
int K = 11;
knn->setDefaultK(K);
knn->setIsClassifier(true);
auto data = ml::TrainData::create(train_data, ml::ROW_SAMPLE, labels);
knn->train(data);
knn->save("knn_digits_model.yml");
//使用训练集进行测试,计算正确率
Mat results = Mat::zeros(labels.size(), CV_32F);
knn->findNearest(train_data, K, results);
float acc = 0;
for (int n = 0; n < labels.rows; n++)
{
int result = results.at<float>(n, 0);
int label = labels.at<float>(n, 0);
if (result == label)
{
acc++;
}
}
acc = acc / 7600 * 100;
cout << "正确率: " << acc << "%" << endl;
}
注意,第一次运行的时候需要先进行训练,当得到手写体数字识别模型后就可以把train()
函数给注释掉,直接进行识别。
下面就来看一下实现的效果是怎样的:
在演示中,我从0到5依次书写数字,除了最后的5被识别成了3,其他均识别正确,至于后面的数字就不再一一演示,有兴趣的朋友可以自己尝试一下。
好了,本次笔记就到此结束啦,谢谢阅读~
PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!