多行手写数字识别(嵌入式设备实现)--嵌入式AI设备--火星人视觉传感器

           为提升识别准确率,采用改进神经网络,通过Mnist数据集进行训练。整体处理过程分为两步:图像预处理和改进神经网络推理。图像预处理主要根据图像的特征,将数据处理成规范的格式,而改进神经网络推理主要用于输出结果。

        整个过程分为两个步骤:图像预处理和神经网络推理

       需要提前安装Tengine框架,参考:https://blog.csdn.net/Bluesyxx/article/details/85255634

       代码地址为:https://github.com/BluesYu/MarStech_Vision_Sensor/tree/master/lenet_num_mode

一,图像预处理过程

       该算法通过滤波、灰度化、二值化等一系列操作,提取出感兴趣的区域(即ROI的特征抽取过程),然后根据该特征,勾画出具体轮廓,然后对具体轮廓进行排序,最后完成数字的切割,将其转换为二值图,具体流程如下图所示。

                                                       

       首先使用加权分量法对RGB图像进行灰度化处理,然后使用大津阈值法将图像得到二值图像,对二值图颜色反转后,进行轮廓检测。将二值图中的可连通区域用连接点表示,生成矩阵来包围对应的数字,其中有特例数字特例4、6、8和9,拥有多个连通域,需要单独处理。例如,数字8含有两个圆圈,默认检测会将其全部检测到,增加判断轮廓位置,只保留外轮廓

      然后根据轮廓的位置对相应的矩形进行排序,直接根据勾画出的轮廓的坐标位置,对生成的矩阵进行排序接着进入ROI域进行数字的切割,切割流程如下图所示,首先将排序得到的轮廓区域(即待剪切的区域)设置为ROI区域,然后新建一个与该区域大小相同的新图像,将源图像复制到该新图像中,完成图像切割,最后释放ROI区域

                                                                 

            最后,进行图像的归一化处理,按顺序切割得到的二值图像,改为28*28像素大小,然后进行灰度反转(即白色变换黑色,黑色变为白色,0和1数值倒换),完成图像的预处理操作。

二,训练神经网络模型   

           数字识别是分类问题,传统方法是通过使用尺度不变的特征变换和方向梯度直方图等特征,然后采用支持向量机(SVM)、Adaboost等分类算法进行识别分类。但处理精确度都很低,难以识别手写数字等复杂情况。因而,本文提出一种改进型的小型卷积神经网络(CNN)LeNet-5,该网络专为手写数字识别而设计,采用28*28像素大小的Mnist手写数据集,直接对输入的图像进行训练来学习,极大减少算法的设计难度,同时提升识别的准确率。

       上世纪90年代,工程师LeCun在卷积神经网络(CNN)的基础上提出了LeNet网络,主要针对手写数字识别,该算法目前已经非常成熟,稳定性很高。而LeNet-5是该网络的一个典型应用,该网络模型一共有7层(不含输出层),每层含有大量的训练参数,内部结构示意图如下图。

                                                              

 

         网络的具体参数如下表示,其中输入图像数据依次经过了C1卷积层、S2池化层、C3卷积层、S4池化层、C5卷积层、F6全连接层,最后输出结果。

 

卷积核大小

核种类

输入图片尺寸

输出图像(featureMap)

神经元

数量

训练参数

连接次数

C1层

5*5

6

32*32

28*28

28*28*6

(5*5+1)*6

(5*5+1)*6*28*28=122304

S2层

2*2

6

28*28

14*14

 

2*6

(2*2+1)*6*14*14=5880

C3层

5*5

16

14*14

10*10

14*14*6

1516

10*10*1516=151600

S4层

2*2

16

10*10

5*5

 

2*16

16*(2*2+1)*5*5=2000

C5层

5*5

120

5*5

1*1

5*5*16

48120

120*(16*5*5+1)=48120

F6层

             

 

84*(120+1)

(120+1)x84=10164

三,手写数字识别代码实现

      相应工程:lenet_num_mode.cpplenet_num_mode.hcommon_util.hppimage_process.hpp,其中,CMakeLists.txt是配置文件。

      lenet文件是网络参数,lenet.prototxtlenet_iter_20000.caffemodelmean.binaryprotosynset_words.txt

    1,图像处理:

  (1)图像切割:

获取图像句柄:

/*******************************************************************
* 名称:                get_num_Rect
* 功能:                获取图像矩阵
* 入口参数:graph_t graph:运行图句柄
            int node_idx:输出节点索引值
			int tensor_idx: 张量索引值
* 出口参数:        tensor_t:张量句柄,失败返回NULL
*******************************************************************/
int get_num_Rect(Mat &dis_binary, vector<Rect> &Rect_temp)//找到的矩阵
{
	vector<vector<Point>> contours;//有多少轮廓,向量contours就有多少元素
	vector<Vec4i> hierarchy;//每一个元素的4个int型变量——hierarchy[i][0] ~hierarchy[i][3],分别表示第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号
							//CV_RETR_EXTERNAL  只检测最外围轮廓
							//CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内   CV_CHAIN_APPROX_SIMPLE  只有拐点信息
	findContours(dis_binary, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, Point());//查找轮廓
	vector<vector<Point>>::const_iterator itc = contours.begin();//起始轮廓
																 // while(itc!=contours.end())
	for (int i = 0; itc != contours.end(); i++)
	{
		Rect ret = boundingRect(Mat(*itc));//计算右上点集的边界矩形
		int rect_height = ret.height;
		int rect_width = ret.width;

		if (itc->size() <100 || itc->size() > 1000) {// 删除当前连通域轮廓 200为最小轮廓长,3000为最大轮廓长
			itc = contours.erase(itc);
		}
		else
			if (((double)rect_height) / ((double)rect_width)>100 ||//图像的比例参数
				((double)rect_height / ((double)rect_width)<0.2))
			{
				itc = contours.erase(itc);
			}
			else
			{//满足要求,保存矩形参数。
				itc++;
				ret.x = (ret.x > 0 ? ret.x : 0);//防止越界
				ret.y = (ret.y > 0 ? ret.y : 0);
				Rect_temp.push_back(ret);//存储矩形信息
			}
	}
	return Rect_temp.size();
}
/*******************************************************************
* 名称:                get_div_Rect
* 功能:                  图像分割
* 入口参数:vector<Rect> &Rect_temp:分割得到的矩阵
* 出口参数:return= Rect_temp.size():返回参数大小        
*******************************************************************/
int get_div_Rect(vector<Rect> &Rect_temp)//图像分割
{
	int max_y = 0;
	int min_y = 480;
	int mean_y;
	int isSorted = 1;//冒泡排序的标记
	for (unsigned int i = 0; i < Rect_temp.size() - 1; i++) //求切割阈值
	{
		if (max_y < Rect_temp[i].y) max_y = Rect_temp[i].y;
		if (min_y > Rect_temp[i].y) min_y = Rect_temp[i].y;
	}
	mean_y = (max_y + min_y)/2;//切割的阈值,默认切割2排
								 //cout<< "max_y=" << max_y << " " << "min_y=" << min_y << "mean_y=" << mean_y << endl;
	if (max_y - min_y > 30)
	{
		vector<Rect> Rect_div1;//切割的矩阵
		vector<Rect> Rect_div2;//切割的矩阵

		for (unsigned int i = 0; i < Rect_temp.size(); i++) { //	切割两排
			if (Rect_temp[i].y < mean_y)//第一行
				Rect_div1.push_back(Rect_temp[i]);
			else
				Rect_div2.push_back(Rect_temp[i]);
		}

		for (unsigned int i = 0; i<Rect_div1.size() - 1; i++) { //div1排序
			isSorted = 1; //假设剩下的元素已经排序好了
			for (unsigned int j = 0; j<Rect_div1.size() - 1 - i; j++) {
				if (Rect_div1[j].x > Rect_div1[j + 1].x)//交换顺序
				{
					swap(Rect_div1[j + 1], Rect_div1[j]);
					isSorted = 0; //一旦需要交换数组元素,就说明剩下的元素没有排序好
				}
			}
			if (isSorted) break; //如果没有发生交换,说明剩下的元素已经排序好了
		}

		/*for (int i = 0; i < Rect_div1.size(); i++)
		cout << Rect_div1[i].x << " ";
		cout << endl;*/
		for (unsigned int i = 0; i<Rect_div2.size() - 1; i++) { //div2排序
			isSorted = 1; //假设剩下的元素已经排序好了
			for (unsigned int j = 0; j<Rect_div2.size() - 1 - i; j++) {
				if (Rect_div2[j].x > Rect_div2[j + 1].x)//交换顺序
				{
					swap(Rect_div2[j + 1], Rect_div2[j]);
					isSorted = 0; //一旦需要交换数组元素,就说明剩下的元素没有排序好
				}
			}
			if (isSorted) break; //如果没有发生交换,说明剩下的元素已经排序好了
		}

		/*for (int i = 0; i < Rect_div2.size(); i++)
		cout << Rect_div2[i].x << " ";
		cout << endl;*/

		for (unsigned int i = 0; i < Rect_div1.size(); i++)//合并数组
			Rect_temp[i] = Rect_div1[i];
		for (unsigned int i = 0; i < Rect_div2.size(); i++)
			Rect_temp[Rect_div1.size() + i] = Rect_div2[i];
	}
	else
	{
		for (unsigned int i = 0; i<Rect_temp.size() - 1; i++) {//y轴方向
			isSorted = 1; //假设剩下的元素已经排序好了
			for (unsigned int j = 0; j<Rect_temp.size() - 1 - i; j++) {
				if (Rect_temp[j].x > Rect_temp[j + 1].x)//交换顺序
				{
					swap(Rect_temp[j + 1], Rect_temp[j]);
					isSorted = 0; //一旦需要交换数组元素,就说明剩下的元素没有排序好
				}
			}
			if (isSorted) break; //如果没有发生交换,说明剩下的元素已经排序好了
		}
	}
	return Rect_temp.size();
	// for (int i = 0; i < Rect_temp.size(); i++)
	//cout << Rect_temp[i].x << " ";
	// cout << endl;
}

    图像反转:

/*******************************************************************
* 名称:                get_get_div_Image
* 功能:            根据矩矩形对图像进行图像分割,反转,得到最终图像
* 入口参数:Mat &grayImage: 原始图像
            vector<Rect> &Rect_temp:输入的参数矩阵数组
			vector<Mat> &Mat_temp:最终图像数组
* 出口参数:return= Mat_temp..size():返回切割图形的数目        
*******************************************************************/
int get_get_div_Image(Mat &grayImage, vector<Rect> &Rect_temp, vector<Mat> &Mat_temp)//图像分割,反转
{

	for (unsigned int i = 0; i <Rect_temp.size(); i++) {
		Mat rot_image, final_result;
		rot_image = grayImage;
		Rect ret = Rect_temp[i];
		ret.x = (ret.x > 0 ? ret.x : 0);//防止越界
		ret.y = (ret.y > 0 ? ret.y : 0);
		final_result = rot_image(ret);//提取ROI
		Mat_temp.push_back(final_result);
	}
	return Mat_temp.size();
}

  (2)图像扩张:图像切割后,适当增加边距;

/*******************************************************************
* 名称:                get_expand_Image
* 功能:            根据矩矩形对图像进行图像分割,反转,得到最终图像
* 入口参数:vector<Mat> &Mat_temp:输入图像数组
            vector<Mat> &Mat_dst:最终得到图像数组
			unsigned int add_pixel:膨胀大小
* 出口参数:return= Mat_dst.size():返回切割图形的数目    
*******************************************************************/
int get_expand_Image(vector<Mat> &Mat_temp, vector<Mat> &Mat_dst,unsigned int add_pixel)//图像扩大,膨胀
{
	if (add_pixel < 5) return -1;//参数过小
	if (Mat_temp.size()<1) return -2;//范围为空
	//Mat temp;//空白参量
	Rect roi_rect;//切割矩形
	for (unsigned int i = 0; i < Mat_temp.size(); i++)//图像膨胀
	{
		//cout <<"Mat="<<Mat_temp[i].cols << "  " << Mat_temp[i].rows << endl;
	    Mat temp = Mat::zeros(Mat_temp[i].rows + add_pixel, Mat_temp[i].cols+ add_pixel, CV_8UC1);//灰度图,(行,列,参数)格式:宽*长
		//cout << "temp=" << temp.cols <<"  "<< temp.rows << endl;//(w和h)
        roi_rect = Rect(add_pixel/2, add_pixel/2, Mat_temp[i].cols, Mat_temp[i].rows);
		//cout << "Rect=" << add_pixel / 2 <<"  "<<Mat_temp[i].cols << "  " << Mat_temp[i].rows << endl;//(w和h)
		Mat_temp[i].copyTo(temp(roi_rect));	//图像加和
		Mat_dst.push_back(temp);
	}

	for (unsigned int i = 0; i < Mat_dst.size(); i++)//对目标图像进行膨胀
	{
		Mat element1 = getStructuringElement(MORPH_RECT, Size(3,3));//原图膨胀
		dilate(Mat_dst[i], Mat_dst[i], element1);

		resize(Mat_dst[i], Mat_dst[i], Size(28, 28), CV_INTER_LINEAR);//设置大小  双线性差值
		Mat element = getStructuringElement(MORPH_RECT, Size(2, 2));//膨胀
		dilate(Mat_dst[i], Mat_dst[i], element);

		for(int j=0;j<Mat_dst[i].cols;j++)
			for (int k = 0; k< Mat_dst[i].rows; k++)
			{
				unsigned int pixy_temp=Mat_dst[i].at<uchar>(k,j);
				if (pixy_temp != 0)
					Mat_dst[i].at<uchar>(k,j) = 255;//白色点
			}
		//imwrite("C:\\Users\\yxx\\Desktop\\fsdownload\\num2\\temp3\\" + pic_name[i] + ".bmp", Mat_dst[i]);   //  将gray图像保存为bmp
	}
	return Mat_dst.size();
}

(3),导入模型,进行前向传播:

/*******************************************************************
* 名称:                  LoadLabelFile
* 功能:                   导入图像模型
* 入口参数:  vector<string> &result:     const char *fname:
* 出口参数:        空
*******************************************************************/
void LoadLabelFile(vector<string> &result, const char *fname)//导入图片标记
{
    ifstream labels(fname);
    string line;
    while (getline(labels, line))
        result.push_back(line);
}

/*******************************************************************
* 名称:                  PrintTopLabels
* 功能:                   打印标记函数(千分类)
* 入口参数:        const char *lenet_label_file:文件参数     float *data:输入的数据
* 出口参数:            返回数据(0-9),错误返回-1
*******************************************************************/
int PrintTopLabels(const char *lenet_label_file, float *data)//打印图片标记
{
    // load labels
    vector<string> labels;
    LoadLabelFile(labels, lenet_label_file);

    float *end = data + 10;
    vector<float> result(data, end);
    vector<int> top_N = Argmax(result,2);//最大的三个数值

    for (unsigned int i = 0; i < top_N.size(); i++)
    {
        unsigned int idx = top_N[i];//参数序号
// setiosflags 是包含在命名空间iomanip 中的C++ 操作符,该操作符的作用是执行由有参数指定区域内的动作;
// iso::fixed 是操作符setiosflags 的参数之一,该参数指定的动作是以带小数点的形式表示浮点数,并且在允许的精度范围内尽可能的把数字移向小数点右侧;
        if(result[idx]>0.5)
		  {
			// cout<<idx<<" ";
			 return idx;
			// cout<<labels[idx]<<endl;
			// cout << "("<<fixed << setprecision(3)<< result[idx] << ") "<<labels[idx] << " ";
		  }   
    } 
  return -1;
}

/*******************************************************************
* 名称:                  get_input_data
* 功能:                   获取输入数据
* 入口参数:Mat img:灰度图                  float *input_data:输出
            int img_h:图片高度               int img_w:图片宽度    
			const float* mean:RGB取值阈值    float scale:缩放因子
* 出口参数:        空
*******************************************************************/
void get_input_data(Mat img, float *input_data, int img_h, int img_w)//获取输入数据 float scale:缩放
{
   if (img.empty())
   {
         cerr << "failed to read image file " << "\n";
         return;
   }
	img.convertTo(img, CV_32FC3); // 类型又UINT8变为了FLOAT32位
	float *img_data = (float *)img.data;//图像数据
    for (int h = 0; h < img_h; h++)//y
       for (int w = 0; w < img_w; w++)//x
	      {
            input_data[h * img_w + w] = (*img_data)*0.00390625;//对应RGB阈值
            img_data++;
	       }
}

   (4)设置编译的规则:

  注意路径问题,#opencv路径,TENGINE_DIR 框架路径。

Skip to content
 
Search or jump to…

Pull requests
Issues
Trending
Explore
 
@BluesYu 
90 BluesYu/MarStech_Vision_Sensor
 Code  Issues 0  Pull requests 0  Projects 0  Wiki  Settings
MarStech_Vision_Sensor/lenet_num_mode/CMakeLists.txt
@BluesYu BluesYu Init
7baa3d2 on 13 May
35 lines (28 sloc)  1.19 KB
    
# ------------------------------------------FileInfo-----------------------------------------------------
# File name:              CmksList.txt
# Created by:             BluesYu  
# Last modified Date:    2019-04-23
# Last Version:               1.0
# Descriptions:       模型编译所需设置     
# ------------------------------------------------------------------------------------------------------
cmake_minimum_required (VERSION 2.8)
project(LeNet)

set( TENGINE_DIR ~/Tengine)
#set( TENGINE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../ )
set( INSTALL_DIR ${TENGINE_DIR}/install/ )
set( TENGINE_LIBS tengine protobuf)

#flag
set(CMAKE_CFLAGS "-std=gnu89 -O3 -Wall")
set(CMAKE_CXX_FLAGS "-std=c++11 -O3 -Wall")

#opencv
find_package(OpenCV REQUIRED)

#include
include_directories(${INSTALL_DIR}/include)
include_directories(./uart_io)
include_directories(~/lenet_num_mode)

#lib
link_directories(${INSTALL_DIR}/lib)
find_package(OpenCV REQUIRED)

#exe
add_executable(lenet_test lenet_test.cpp lenet_num_mode.cpp uart_io/uart_io.cpp)#将名为.cpp的源文件编译成一个名称为lenet_test的可执行文件
target_link_libraries(lenet_test ${TENGINE_LIBS} ${OpenCV_LIBS})#将目标文件与库文件进行链接

    使用注意细则:

    1,调节焦距;

    2,图像采集需要注意,尽量靠近物体;

    3, 编译命令为:cmake . make -j4

四,其他

       具体视觉传感器测试、购买可以咨询:火星人俱乐部官网(https://www.imarsclub.com/web/index),电话或邮件联系即可。传感器已经申请专利,商业使用需要授权。

        火星人视觉传感器是一个开放平台,相关电路版图、代码对外开放,可以自行下载,代码地址:https://github.com/BluesYu/MarStech_Vision_Sensor,欢迎star和fork,有问题可以再github上交流。

       本项目为开源项目,不以盈利为目的,开源社区需要大家一起努力,欢迎大家一起来开发!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值