为提升识别准确率,采用改进神经网络,通过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.cpp、lenet_num_mode.h、common_util.hpp和image_process.hpp,其中,CMakeLists.txt是配置文件。
lenet文件是网络参数,lenet.prototxt、lenet_iter_20000.caffemodel、mean.binaryproto、synset_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上交流。
本项目为开源项目,不以盈利为目的,开源社区需要大家一起努力,欢迎大家一起来开发!