目录
前言
一、KNN原理
二、数字识别系统效果演示
三、数字识别系统
1.图像采集
2.阈值分割--(定位数字区域)
3.轮廓特征分析--(定位数字区域)
4.数字轮廓提取
5.扩充边界并归一化其尺寸
6.训练数据集并预测测试样本
总结
前言
本章使用一个基于 MINIST 的小型数据集通过OpenCV中的 KNN 方法进行从0到9数字识别,由于数据集小,不能完全识别正确,经测试其识别正确率能达到 90%以上。这数字识别系统基本完成,但其适应性还有所欠缺,比如任意角度书写数字情况也能够高正确率识别。由于笔者能力局限,再加上时间仓促,特此提供本系统的所有工程代码,详细说明步骤,让大家继续深入开发,共同进步。
一、KNN原理
KNN(K-NearestNeighbor)的任务就是在训练本集中,依据距离测度找到与测试样本最相似的K个训练样本,对于分类问题,大多采用“多数表决”的方式确定测试样本的最终分类,即在这K个训练样本中,属于那个类别的样本数最多,测试样本就属于那个类。
我们常说:“物以类聚,人以群分”。相似的东西我们就归为一类,人的一生有三个阶段,幼年、成年、老年。通过年龄大小的进行分类,比如我们现在要判断一个人处于说明阶段,就要通过其年龄大小进行分类。KNN原理可以这样通俗理解。
二、数字识别系统效果演示
本系统任务是在一个普通的环境下找出数字区域并识别出数字。本系统是在学习桌面上进行演示系统的效果,其效果图如图所示。
三、数字识别系统
1.图像采集
本系统使用海康的MV-EB435i悉灵相机,其图像采集效果通过GIF图演示。其性能强大,且轻盈便携。

2.阈值分割--(定位数字区域)
由于我们在一个普通环境下,并非是复杂环境下进行定位,为此定位方法也简单,笔者采用阈值分割进行图像分析,将前景与背景区分开。在阈值分割方法中,其最常用的方法是大津法分割,其效果是较理想的,当然,其方法也不是万能的,你也知道,没有一种方法是万能的,只有较好的。
大津法(OTSU)是一种自动选择阈值(无参数且无监督)的的图像分割方法,日本学者 Nobuyuki Otsu 1979年提出。该方法又称作最大类间方差法,因为按照大津法求得的阈值进行图像二值化分割后,前景与背景图像的类间方差最大。
算法原理:①.首先假设阈值为 K(0-255),然后根据K值将灰度图分为两部分,前景与背景。
②.计算前景与背景占整幅图的比例。P前景 与 P背景。其之和为1。
③.算出属于前景/背景/全景中均值(灰度值),M前景、M背景与M全景。
④.根据Otsu的算法原理,求类间方差。
⑤.从0-255遍历K,计算每个K对应的间类方差,最大方差对应的K值即为阈值。
通过使用大津法对采集的灰度图像进行阈值分割,其效果图如下。

3.轮廓特征分析--(定位数字区域)
前面将采集的图像进行二值化,我们要通过OpenCV中findContours函数进行轮廓分析,通过轮廓特征(面积,长宽比等特征)提取最大轮廓(我们假定最大轮廓包含数字)并在原图中做出标记与提取。笔者对最大轮廓进行最小外接正/斜矩形。
其效果如下图所示:

4.数字轮廓提取
对最大轮廓的外接斜矩形(原图中的蓝色矩形)进行灰度值翻转操作即可。代码实现思路是遍历最小外接正矩形内区域所有点(原图中的红色区域)判断点是否为外接斜矩形区域,如果属于且该点的灰度值为 0 即为数字轮廓的点。将其灰度值翻转为255即可。
其效果如图所示:

5.扩充边界并归一化其尺寸
由于我们的训练数据集中采用归一化尺寸,对测试样本也要进行归一化。其尺寸归一化为20*20,其归一化后效果如图所示。

6.训练数据集并预测测试样本
本文使用的小型数据集是从MINST中随机挑选出数字0-9的图片各500张,每张图片按比例缩小为20像素*20像素,然后将这5000张图片拼成一张整图,MINIST数据集如图所示。

对这些样本作为训练集,创建并初始化KNN模型,训练分割好的训练集,然后将前面我们已经提取处理的数字区域作为测试样本进行测试,获取其预测结果,将其标注在原图中。其效果如图所示。

总结
以上就是本系统采用的方案,本系统充其量为小儿科水平,其局限性较大,比如我们要解决在任意角度下也能够高正确识别出数字。因为一个企业级项目考虑条件是非常复杂的,我深知自己的能力不足,欠缺考虑复杂条件,所以让我们共同学习,共同进步。
代码:
#include "../common/common.hpp"
#include "../common/RenderImage.hpp"
#include
using namespace cv;
void* handle = NULL;
RIFrameInfo depth = { 0 };
RIFrameInfo rgb = { 0 };
vector DigitImgs;
//int类型转string类型函数
//参数:int类型的预测数字结果
//作用:是将预测结果标记在视频上,故需要转换类型
string intToString(int number)
{
ostringstream ss;
ss << number;
return ss.str();
}
//初始化+开启设备
void HIK_initialization()
{
MV3D_RGBD_VERSION_INFO stVersion;
ASSERT_OK(MV3D_RGBD_GetSDKVersion(&stVersion));
LOGD("dll version: %d.%d.%d", stVersion.nMajor, stVersion.nMinor, stVersion.nRevision);
ASSERT_OK(MV3D_RGBD_Initialize());
unsigned int nDevNum = 0;
ASSERT_OK(MV3D_RGBD_GetDeviceNumber(DeviceType_Ethernet | DeviceType_USB, &nDevNum));
LOGD("MV3D_RGBD_GetDeviceNumber success! nDevNum:%d.", nDevNum);
ASSERT(nDevNum);
// 查找设备
std::vector devs(nDevNum);
ASSERT_OK(MV3D_RGBD_GetDeviceList(DeviceType_Ethernet | DeviceType_USB, &devs[0], nDevNum, &nDevNum));
for (unsigned int i = 0; i < nDevNum; i++)
{
LOG("Index[%d]. SerialNum[%s] IP[%s] name[%s].\r\n", i, devs[i].chSerialNumber, devs[i].SpecialInfo.stNetInfo.chCurrentIp, devs[i].chModelName);
}
//打开设备
unsigned int nIndex = 0;
ASSERT_OK(MV3D_RGBD_OpenDevice(&handle, &devs[nIndex]));
LOGD("OpenDevice success.");
//改变分辨率参数,0x00010001为 1280×720, 0x00020002为 640×360
//MV3D_RGBD_PARAM stparam;
//stparam.enParamType = ParamType_Enum;
//stparam.ParamInfo.stEnumParam.nCurValue = 0x00010001;
//ASSERT_OK(MV3D_RGBD_SetParam(handle, MV3D_RGBD_ENUM_RESOLUTION, &stparam));
//改变曝光参数
//MV3D_RGBD_PARAM stparam;
//stparam.enParamType = ParamType_Float;;
//stparam.ParamInfo.stFloatParam.fCurValue = 100.0000;
//ASSERT_OK(MV3D_RGBD_SetParam(handle, MV3D_RGBD_FLOAT_EXPOSURETIME, &stparam));
//LOGD("EXPOSURETIME: (%f)", stparam.ParamInfo.stFloatParam.fCurValue);
// 开始工作流程
ASSERT_OK(MV3D_RGBD_Start(handle));
LOGD("Start work success.");
}
//关闭释放设备
void HIK_stop()
{
ASSERT_OK(MV3D_RGBD_Stop(handle));
ASSERT_OK(MV3D_RGBD_CloseDevice(&handle));
ASSERT_OK(MV3D_RGBD_Release());
LOGD("Main done!");
}
//作用:判断点是否在矩形框内
//参数:参数一表示包含平面上的旋转矩形,参数二表示预测点
//返回值为true表示点在矩形框内
bool DoesRectangleContainPoint(RotatedRect rectangle, Point2f point)
{
//转化为轮廓
Point2f corners[4];
rectangle.points(corners);
Point2f *lastItemPointer = (corners + sizeof corners / sizeof corners[0]);
vector contour(corners, lastItemPointer);
//判断
double indicator = pointPolygonTest(contour, point, false); //pointPolygonTest 检测点是否在轮廓内
//pointPolygonTest:
//C++: double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)
//用于测试一个点是否在多边形中
//当measureDist设置为true时,返回实际距离值。若返回值为正,表示点在多边形内部,返回值为负,表示在多边形外部,返回值为0,表示在多边形上。
//当measureDist设置为false时,返回 - 1、0、1三个固定值。若返回值为 + 1,表示点在多边形内部,返回值为 - 1,表示在多边形外部,返回值为0,表示在多边形上。
if (indicator >= 0)
return true;
else
return false;
}
//作用:提取数字轮廓
//参数:参数一为采集BGR格式图,参数二为采集的灰度图
Mat getNumberControus(Mat &bgrImg,Mat &grayImg)
{
/*******************************************************/
//注意:这是看自己摄像头摆放位置是否需要图片翻转!!!
//void cv::flip(InputArray src,OutputArray dst,int flipCode);
//flipCode Anno
// 1 水平翻转
// 0 垂直翻转
// -1 水平垂直翻转
//图像翻转
flip(bgrImg, bgrImg, -1);
flip(grayImg, grayImg, -1);
/*******************************************************/
/*******************************************************/
//Step1:阈值化(采用大津法-最大间类方差)
Mat binary;
threshold(grayImg,binary,0,255,THRESH_BINARY|THRESH_OTSU);
namedWindow("binary", 0);
resizeWindow("binary", 640, 360);
imshow("binary",binary);
/*******************************************************/
/*******************************************************/
//Step2:轮廓分析
//这里我们假设最大轮廓是白底黑字
int max_area_contour_idx = 0;
double max_area = -1;
vector> contours;
findContours(binary, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
Rect roiRect; //最小外接正矩形
RotatedRect roiRoRect;//最小外接斜矩形
//如果轮廓数量为0则退出函数返回空
if (contours.size() == 0)
return Mat();
//遍历每个轮廓寻找含数字的轮廓
for (uint i = 0; i < contours.size(); i++) {
//计算轮廓面积
double temp_area = contourArea(contours[i]);
//比较轮廓面积,找出最大轮廓
if (max_area < temp_area) {
max_area_contour_idx = i;
max_area = temp_area;
}
}
//最小外接正矩形
roiRect = boundingRect(contours[max_area_contour_idx]);
//最小外接斜矩形
roiRoRect = minAreaRect(contours[max_area_contour_idx]);
Mat roiMat = Mat::zeros(roiRect.size(),CV_8UC1);
//如果面积过小/大则判断为伪轮廓
if (roiRect.area() <= 100 || roiRect.area() > bgrImg.size().area()*0.75)
return Mat();
else//绘制标记区域
{
//绘制最小外接正矩形
rectangle(bgrImg, roiRect, Scalar(0, 0, 255), 3, 8);
Point2f P[4];
roiRoRect.points(P);
//绘制最小外接斜矩形
for (int j = 0; j <= 3; j++)
{
line(bgrImg, P[j], P[(j + 1) % 4], Scalar(255), 2);
}
}
namedWindow("putSignImg", 0);
resizeWindow("putSignImg", 640, 360);
imshow("putSignImg", bgrImg);
/*******************************************************/
/*******************************************************/
//由于前面我们提取轮廓是白底黑字的轮廓,并没有提取出数字轮廓,为此我们只需要在白底黑字轮廓进行处理,将其翻转即可获得数字轮廓
//Step3 提取数字区域
for (int h = roiRect.y + 5; h < roiRect.y + roiRect.height - 5; h++)
{
for (int w = roiRect.x + 5; w < roiRect.x + roiRect.width - 5; w++)
{
Point pt(w,h);
//如果点在最小外接矩形内且该点灰度图为黑则属于数字区域,将该点的灰度图置为255即可
if (DoesRectangleContainPoint(roiRoRect, pt) && !binary.at(h,w))
{
roiMat.at(h - roiRect.y, w - roiRect.x) = 255;
}
}
}
//imshow("roiImg", roiMat);
/*******************************************************/
/*******************************************************/
//Step4分析数字轮廓
//这里我们假定数字轮廓同样为最大轮廓
findContours(roiMat, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
if (contours.size() == 0)
return Mat();
max_area_contour_idx = 0;
max_area = -1;
for (uint i = 0; i < contours.size(); i++) {
double temp_area = contourArea(contours[i]);
if (max_area < temp_area) {
max_area_contour_idx = i;
max_area = temp_area;
}
}
//求数字轮廓外接正矩形
Rect rroiRect = boundingRect(contours[max_area_contour_idx]);
//求数字轮廓外接斜矩形
RotatedRect rroiRoRect = minAreaRect(contours[max_area_contour_idx]);
//如果轮廓区域面积过小则返回
if (max_area_contour_idx == 0 || max_area < 500)
return Mat();
roiMat = roiMat(rroiRect).clone();
//Step5 扩充数字轮廓,扩充为正方形
Mat NumMat;
int top, bottom, left, right;
top = rroiRect.height / 5;
bottom = top;
left = (rroiRect.height * 7 / 5 - rroiRect.width)/2 < 0 ? 0 : (rroiRect.height * 7 / 5 - rroiRect.width) / 2;
right = left;
copyMakeBorder(roiMat, NumMat,top,bottom,left,right,BORDER_CONSTANT,Scalar(0));
namedWindow("NumberImg", 0);
resizeWindow("NumberImg", 640, 360);
imshow("NumberImg", NumMat);
return NumMat;
}
void generateDataSet(Mat &img, Mat &trainData, Mat &testData, Mat &trainLabel, Mat &testLabel, int train_rows = 5)
{
int width_slice = 20;
int height_slice = 20;
int row_sample = 100;
int col_sample = 50;
Mat trainMat(train_rows * 20 * 10, img.cols, CV_8UC1, Scalar::all(0));
// printf("开始生成训练,测试数据...\n");
Rect roi;
for (int i = 1; i <= col_sample; i++)//50
{
//printf("第%d行\n", i);
for (int j = 1; j <= row_sample; j++)
{
//第j行为训练集
Mat temp_single_num;
roi = Rect((j - 1) * width_slice, (i - 1)*height_slice, width_slice, height_slice);
temp_single_num = img(roi).clone();
{
trainData.push_back(temp_single_num.reshape(0, 1));
}
}
}
trainData.convertTo(trainData, CV_32FC1);
testData.convertTo(testData, CV_32FC1);
printf("训练、测试数据已完成\n\n");
//生成标签
printf("开始生成标签...\n");
for (int i = 1; i <= 10; i++)
{
Mat temp_label_train = Mat::ones(train_rows * row_sample, 1, CV_32FC1);
temp_label_train = temp_label_train * (i - 1);
Mat temp = trainLabel.rowRange((i - 1)*train_rows * row_sample, i * train_rows * row_sample);
temp_label_train.copyTo(temp);
}
printf("标签数据已经生成\n\n");
}
int main(int argc,char** argv)
{
HIK_initialization();
MV3D_RGBD_FRAME_DATA stFrameData = { 0 };
//knn
//第一步利用QT上的打开图片的功能,返回图片地址放入ImgAddress变量中
Mat img = imread("digits.png", 1);
cvtColor(img, img, COLOR_BGR2GRAY);
/*cout << img.size() << endl;*/
int train_sample_count = 5000;
int test_sample_count = 1000;
int train_rows = 5;
Mat trainData, testData;
Mat trainLabel(train_sample_count, 1, CV_32FC1);
Mat testLabel(test_sample_count, 1, CV_32FC1);
generateDataSet(img, trainData, testData, trainLabel, testLabel, train_rows);
string knnPath = "digits_knn.xml";
cv::Ptr knn = cv::ml::KNearest::create();
int K = 7;
knn->setDefaultK(K);
knn->setIsClassifier(true);
knn->setAlgorithmType(cv::ml::KNearest::BRUTE_FORCE);
printf("开始训练.....\n");
knn->train(trainData, cv::ml::ROW_SAMPLE, trainLabel);
printf("训练完成\n\n");
//knn->save(knnPath);
//printf("已保存模型\n\n");
while (1)
{
// 获取图像数据
int nRet = MV3D_RGBD_FetchFrame(handle, &stFrameData, 5000);
if (MV3D_RGBD_OK == nRet)
{
LOGD("MV3D_RGBD_FetchFrame success.");
//分析获取每帧数据
parseFrame(&stFrameData, &depth, &rgb);
Mat rgb_frame(rgb.nHeight, rgb.nWidth, CV_8UC3, rgb.pData);
//LOGD("rgb: FrameNum(%d), height(%d), width(%d)。", rgb.nFrameNum, rgb.nHeight, rgb.nWidth);
//滤波,中值滤波方法进行图像滤波
medianBlur(rgb_frame, rgb_frame,3);
//B、R通道交换,显示正常彩色图像
Mat frame, gray_frame;
cvtColor(rgb_frame, frame, COLOR_BGR2RGB);
cvtColor(rgb_frame, gray_frame, COLOR_BGR2GRAY);
/****************************************************/
Mat roiImg = getNumberControus(frame, gray_frame);
if (roiImg.empty())
{
continue;
}
resize(roiImg, roiImg, Size(20, 20), 0.0, 0.0);//缩小尺寸
imshow("roiImg",roiImg);
/****************************************************/
/****************************************************/
Mat roiImgData;
roiImgData.push_back(roiImg.reshape(0, 1));
roiImgData.convertTo(roiImgData, CV_32F);
cout << roiImg.size() << endl;
/****************************************************/
/****************************************************/
//printf("开始测试....\n");
Mat result;
//knn->findNearest(testData,K,result);
knn->findNearest(roiImgData, K, result);
int predict = int(result.at(0));
printf(" predict: %d\n", predict);
/****************************************************/
namedWindow("testImg", 0);
resizeWindow("testImg", 640, 360);
string text = " predict:" + intToString(predict);
putText(frame, text, Point(5, 30), FONT_HERSHEY_COMPLEX, 1, Scalar(255,0,255), 2, 8);
imshow("testImg", frame);
namedWindow("HIK_face", 0);
resizeWindow("HIK_face", 640, 360);
imshow("HIK_face", frame);
waitKey(1);
}
}
HIK_stop();
return 0;
}