作业要求:

1、输入:有一张写着数字的A4纸的图片(如下)

 

2、A4纸矫正

3、数字字符切割

4、用Adaboost或SVM训练一个手写数字分类器

5、识别并输出:连串数字,如“13924579693”与“02087836761”等

实现环境:

Windows10 + VS2015 + cimg库 (+ opencv (用于svm预测时特征提取) + libsvm (用于训练/测试模型与预测))

由于此次作业量比较大,所以会分为上下两部分讲述。一部分为把字符切割出来,另一部分为SVM的训练和识别。此博文主要讲述到数字字符切割的一系列操作。

完整代码可以到我的github上查看:https://github.com/MarkMoHR/HandwritingNumberClassification

阶段结果:

实现步骤(仅到数字字符切割阶段):

1、A4纸边缘顶点提取

2、A4纸矫正

3、数字按顺序分割

3.1) 图像二值化

3.2) 基于垂直方向直方图,把原图进行行分割为多张行子图,每张行子图包含一行数字(可能有多列)

3.3) 基于水平方向直方图,把行子图进行列分割为多张真子图,每张真子图包含单行单列的数字

3.4) 对每张子图,进行扩张(dilation),并进行断裂字符修复

3.5) 对每张子图,用连通区域标记方法(connected-component_labeling algorithm)从左到右分割数字

3.6) 对每张子图,存储单个数字以及一个图像名列表文本

具体实现:

1、A4纸边缘顶点提取:

A4纸边缘与顶点提取在前面的博文也有写过,在这里就不再说了。

[传送门]:[计算机视觉] A4纸边缘检测

阶段结果:

2、A4纸矫正:

A4纸矫正也是之前的作业,记得貌似没写成博文。但是就一个公式的转换,没其他了,所以在这里也不打算细说。详细可到我的github上参考代码~

阶段结果:

3、数字按顺序分割

3.1、图像二值化

1) 图像二值化之前,需要先将矫正后的图像转化为灰度图。

2) 图像二值化,就是将得到的灰度图,转化为只有灰度为0和255的图像。图像二值化有多种方法,比如全局阈值分割、局部阈值分割等。我这里使用了全局阈值分割,简单、速度快、效果不坏哈哈:) 。顾名思义,就是将小于某阈值的像素点像素设为0,其余的设为255。因此找到适合的阈值很关键,我根据几张图片的效果,选择阈值为135。可以发现上面的矫正后的图,会有边缘上的黑边影响,于是可以把靠近边缘的一些像素点都设为白色像素点。

阶段结果:

3.2、基于垂直方向直方图,把原图进行行分割为多张子图,每张子图包含一行数字

1) 与上一个版本相比,现在在做扩张(dilation)之前做直方图行分割,原因是先做扩张可能使两行数字在原来相隔的地方连起来,接下来就不好做行分割了。

2) 为什么需要做行分割,而不是直接用连通区域标记方法做分割就好了?之前我在做的时候,也是直接做下面第3.4、3.5步,分割数字字符。但是由于连通区域标记算法必须从上到下或从左到右扫描,直接做的话结果是,得到分割出来的数字的顺序是乱的!从上到下扫描的话,顺序是越高的数字排越前;从左到右扫描的话,顺序是越左的数字排越前。但是我们需要的结果是一行一行的从左到右按顺序数字,不管是什么样的扫描顺序,对整张图一次性做的话,都得不到我们想要的结果。

3) 鉴于上述原因,我们需要先把数字一行行的先分割出来。基于垂直方向直方图的方法就是,可以想象到,如果我们做竖直方向的灰度直方图,就会出现波和谷。我们只需要在谷做一条分割线即可。找分割线的方法是,我们得先找到由黑转白和由白转黑的拐点,两拐点中间就是分割点。

4) 这样做分割之后,会出现下面这样一种情况,即白色之中有一个黑点都视为峰,这明显是一些断裂的点造成的,而这些点也显然可以忽略(不影响大的数字),也就是其所在的子图无意义,可抛弃。显然一种处理的方法是:统计子图的黑色像素个数,只有超过整张子图大小一定比例才可视为该子图存在完整数字,同时更新割线;否则抛弃。

(原图)  (放大图)

void ImageSegmentation::findDividingLine() {HistogramImage = CImg<int>(BinaryImg._width, BinaryImg._height, 1, 3, 0);DividingImg = CImg<int>(BinaryImg._width, BinaryImg._height, 1, 3, 0);int lineColor[3]{ 255, 0, 0 };cimg_forY(HistogramImage, y) {int blackPixel = 0;cimg_forX(BinaryImg, x) {HistogramImage(x, y, 0) = 255;HistogramImage(x, y, 1) = 255;HistogramImage(x, y, 2) = 255;DividingImg(x, y, 0) = BinaryImg(x, y, 0);DividingImg(x, y, 1) = BinaryImg(x, y, 0);DividingImg(x, y, 2) = BinaryImg(x, y, 0);if (BinaryImg(x, y, 0) == 0)blackPixel++;}cimg_forX(HistogramImage, x) {if (x < blackPixel) {HistogramImage(x, y, 0) = 0;HistogramImage(x, y, 1) = 0;HistogramImage(x, y, 2) = 0;}}//判断是否为拐点if (y > 0) {if (blackPixel <= HistogramValleyMaxPixelNumber && HistogramImage(HistogramValleyMaxPixelNumber, y - 1, 0) == 0) {    //下白上黑:取下inflectionPointSet.push_back(y);//HistogramImage.draw_line(0, y, HistogramImage._width - 1, y, lineColor);}else if (blackPixel > HistogramValleyMaxPixelNumber&& HistogramImage(HistogramValleyMaxPixelNumber, y - 1, 0) != 0) {    //下黑上白:取上inflectionPointSet.push_back(y - 1);//HistogramImage.draw_line(0, y - 1, HistogramImage._width - 1, y - 1, lineColor);}}}divideLinePointSet.push_back(-1);//两拐点中间做分割if (inflectionPointSet.size() > 2) {for (int i = 1; i < inflectionPointSet.size() - 1; i = i + 2) {int divideLinePoint = (inflectionPointSet[i] + inflectionPointSet[i + 1]) / 2;divideLinePointSet.push_back(divideLinePoint);}}divideLinePointSet.push_back(BinaryImg._height - 1);
}void ImageSegmentation::divideIntoBarItemImg() {vector<int> newDivideLinePointSet;int lineColor[3]{ 255, 0, 0 };for (int i = 1; i < divideLinePointSet.size(); i++) {int barHright = divideLinePointSet[i] - divideLinePointSet[i - 1];int blackPixel = 0;CImg<int> barItemImg = CImg<int>(BinaryImg._width, barHright, 1, 1, 0);cimg_forXY(barItemImg, x, y) {barItemImg(x, y, 0) = BinaryImg(x, divideLinePointSet[i - 1] + 1 + y, 0);if (barItemImg(x, y, 0) == 0)blackPixel++;}double blackPercent = (double)blackPixel / (double)(BinaryImg._width * barHright);cout << "blackPercent " << blackPercent << endl;if (blackPercent > SubImgBlackPixelPercentage) {subImageSet.push_back(barItemImg);newDivideLinePointSet.push_back(divideLinePointSet[i - 1]);//barItemImg.display("barItemImg");if (i > 1) {HistogramImage.draw_line(0, divideLinePointSet[i - 1], HistogramImage._width - 1, divideLinePointSet[i - 1], lineColor);DividingImg.draw_line(0, divideLinePointSet[i - 1], HistogramImage._width - 1, divideLinePointSet[i - 1], lineColor);}}}divideLinePointSet.clear();for (int i = 0; i < newDivideLinePointSet.size(); i++)divideLinePointSet.push_back(newDivideLinePointSet[i]);
}

阶段结果:

    

3.3、基于水平方向直方图,把行子图进行列分割为多张真子图,每张真子图包含单行单列的数字:

1) 从上面行分割得到的图像,有可能是多列的数字(如下图)。

2) 显然,我们会想到,利用上面使用过的直方图的方法,只不过改为水平的,不就好了吗。然而做了直方图后,发现与垂直方向的直方图还是有挺大差别的(如下图)。我们能看到,水平方向直方图会出现很多峰,而这是跟我们阿拉伯数字的书写方式有关,即两个数字之间有间隔

3) 那怎么使靠近的各组数字分隔开来呢?我一开始想了用固定间距阈值,即两个峰之间间隔大于一定阈值的时候,视为中间需要隔开。但是固定阈值不适用于所有图片,毕竟可能存在两组数字之间间隔不大,但也能明显区分为两组数字的情况。于是我想出了一种动态间距阈值的方法:计算所有峰之间的间距的均值,只有当间距大于均值的一定倍数时,才视为要隔开。这种方法的好处是把同组数字之间的间隔也考虑进去了,因为我们可以明显看到,两组数字之间的间距大小,与同组数字间的间隔有关。

//根据X方向直方图判断真实的拐点
vector<int> getInflectionPosXs(const CImg<int>& XHistogramImage) {vector<int> resultInflectionPosXs;vector<int> tempInflectionPosXs;int totalDist = 0, avgDist;int distNum = 0;//查找拐点cimg_forX(XHistogramImage, x) {if (x >= 1) {//白转黑if (XHistogramImage(x, 0, 0) == 0 && XHistogramImage(x - 1, 0, 0) == 255) {tempInflectionPosXs.push_back(x - 1);}//黑转白else if (XHistogramImage(x, 0, 0) == 255 && XHistogramImage(x - 1, 0, 0) == 0) {tempInflectionPosXs.push_back(x);}}}for (int i = 2; i < tempInflectionPosXs.size() - 1; i = i + 2) {int dist = tempInflectionPosXs[i] - tempInflectionPosXs[i - 1];if (dist <= 0)distNum--;totalDist += dist;}//计算间距平均距离distNum += (tempInflectionPosXs.size() - 2) / 2;avgDist = totalDist / distNum;//cout << "avgDist " << avgDist << endl;resultInflectionPosXs.push_back(tempInflectionPosXs[0]);    //头//当某个间距大于平均距离的一定倍数时,视为分割点所在间距for (int i = 2; i < tempInflectionPosXs.size() - 1; i = i + 2) {int dist = tempInflectionPosXs[i] - tempInflectionPosXs[i - 1];//cout << "dist " << dist << endl;if (dist > avgDist * XHistogramValleyMaxPixelNumber) {resultInflectionPosXs.push_back(tempInflectionPosXs[i - 1]);resultInflectionPosXs.push_back(tempInflectionPosXs[i]);}}resultInflectionPosXs.push_back(tempInflectionPosXs[tempInflectionPosXs.size() - 1]);  //尾return resultInflectionPosXs;
}//获取一行行的子图的水平分割线
vector<int> getDivideLineXofSubImage(const CImg<int>& subImg) {vector<int> InflectionPosXs;//先绘制X方向灰度直方图CImg<int> XHistogramImage = CImg<int>(subImg._width, subImg._height, 1, 3, 0);cimg_forX(subImg, x) {int blackPixel = 0;cimg_forY(subImg, y) {XHistogramImage(x, y, 0) = 255;XHistogramImage(x, y, 1) = 255;XHistogramImage(x, y, 2) = 255;if (subImg(x, y, 0) == 0)blackPixel++;}//对于每一列x,只有黑色像素多于一定值,才绘制在直方图上if (blackPixel >= XHistogramValleyMaxPixelNumber) {cimg_forY(subImg, y) {if (y < blackPixel) {XHistogramImage(x, y, 0) = 0;XHistogramImage(x, y, 1) = 0;XHistogramImage(x, y, 2) = 0;}}}}InflectionPosXs = getInflectionPosXs(XHistogramImage);    //获取拐点cout << "InflectionPosXs.size() " << InflectionPosXs.size() << endl;for (int i = 0; i < InflectionPosXs.size(); i++)XHistogramImage.draw_line(InflectionPosXs[i], 0, InflectionPosXs[i], XHistogramImage._height - 1, lineColor);//XHistogramImage.display("XHistogramImage");//两拐点中间做分割vector<int> dividePosXs;dividePosXs.push_back(-1);if (InflectionPosXs.size() > 2) {for (int i = 1; i < InflectionPosXs.size() - 1; i = i + 2) {int divideLinePointX = (InflectionPosXs[i] + InflectionPosXs[i + 1]) / 2;dividePosXs.push_back(divideLinePointX);}}dividePosXs.push_back(XHistogramImage._width - 1);return dividePosXs;
}//分割行子图,得到列子图
//@_dividePosXset 以-1起,以lineImg._width结束
vector<CImg<int>> getRowItemImgSet(const CImg<int>& lineImg, vector<int> _dividePosXset) {vector<CImg<int>> result;for (int i = 1; i < _dividePosXset.size(); i++) {int rowItemWidth = _dividePosXset[i] - _dividePosXset[i - 1];CImg<int> rowItemImg = CImg<int>(rowItemWidth, lineImg._height, 1, 1, 0);cimg_forXY(rowItemImg, x, y) {rowItemImg(x, y, 0) = lineImg(x + _dividePosXset[i - 1] + 1, y, 0);}result.push_back(rowItemImg);}return result;
}

阶段结果:

3.4、对每张子图,进行扩张(dilation),并进行断裂字符修复

1) 利用扩张进行字符修复:做二值化的时候,阈值取太小,有些数字的像素点被视为白点,容易造成字符断裂;阈值取太大,很多由于阴影产生的噪声点又会混进来。所以取恰当的阈值很重要。但是不管取什么阈值,都有可能出现字符断裂的情况(如下图)。扩张(Dilation)就是解决字符断裂的一种方法,这是数字图像处理上学到的一种方法,解释起来也比较复杂,可以参考:https://en.wikipedia.org/wiki/Dilation_(morphology)  简单的说就是当前点是0还是255还要根据周围的像素点来判断。

2) 我用了以下两个滤波器来进行扩张与断裂字符修复:先用filterA做2次滤波,再用filterB做1次滤波(注意使用的次数以及顺序!)

3) filterB作用是:当前位置为白色像素时,检测上下左右的像素,若为黑色,则把自身设为黑色。

4) filterA作用是:当前位置为白色像素时,检测上/下1个单位像素,与左/右2个单位像素,统计黑色像素的总统计数。1为黑色像素个数加1,-1为黑色像素个数减1,只有当最后黑色像素的总统计数大于0,才把自身设为黑色。

5) 明显,filterB使数字往4个方向变厚,但这很可能导致的结果是,像0、6、8、9这几个数字中间的洞被填充成黑色。所以我提出filterA来解决这个问题,可以看到使用filterA,像素的灰度(黑or白)与当前位置的水平邻居关系很大,即遇到类似洞的位置能够尽可能防止被填充成黑色。

void ImageSegmentation::doDilationForEachBarItemImg(int barItemIndex) {//扩张Dilation -X-X-X-XYY方向CImg<int> answerXXY = CImg<int>(subImageSet[barItemIndex]._width, subImageSet[barItemIndex]._height, 1, 1, 0);cimg_forXY(subImageSet[barItemIndex], x, y) {int intensity = getDilationIntensityXXY(subImageSet[barItemIndex], x, y);answerXXY(x, y, 0) = intensity;}//扩张Dilation -X-X-X-XYY方向CImg<int> answerXXY2 = CImg<int>(answerXXY._width, answerXXY._height, 1, 1, 0);cimg_forXY(answerXXY, x, y) {int intensity = getDilationIntensityXXY(answerXXY, x, y);answerXXY2(x, y, 0) = intensity;}//扩张Dilation XY方向CImg<int> answerXY = CImg<int>(answerXXY2._width, answerXXY2._height, 1, 1, 0);cimg_forXY(answerXXY2, x, y) {int intensity = getDilationIntensityXY(answerXXY2, x, y);answerXY(x, y, 0) = intensity;}cimg_forXY(subImageSet[barItemIndex], x, y) {subImageSet[barItemIndex](x, y, 0) = answerXY(x, y, 0);}
}

断裂字符修复对比图(左为修复前,右为修复后):

3.5、对每张子图,用连通区域标记方法(connected-component_labeling algorithm)从左到右分割数字:

1) 连通区域标记,从字面上很好理解,毕竟一个数字本身就是一个连通区域。而这种方法的实现也有很多种算法,比如二次扫描法、双向反复扫描法、区域增长法。

2) 我这里使用了速度相对较快的二次扫描法。下面利用一些图结合帮助理解算法的实现:

① 扫描图像第一列和第一行,每个黑色点作为一类,做上标记。每一类各个点的坐标用一个链表存储起来,每个链表的首地址存储在一个容器下,容器的下标刚好对应类的标记:

一列一列进行遍历,遇到黑色点,即当前红点所在位置,检测其正上、左上、左前、左下4个位置,如下

③ 找到上述4个邻点的最小类标记(当前即是0),对其余标记的黑色点(即1和2),在链表容器里面找到对应的链表,然后接到刚找到的最小标记的链表上,接着其标记全改为刚才的最小标记。最后红点的坐标也加到最小标记的链表,红点也标记为最小标记。即变成如下:

  

④ 如果当前黑点的4个邻点都没有黑色点,则自身作为新类,加上新的标记,在链表容器里面加入新的链表,存储自己的坐标位置

⑤ 最后根据链表容器,同一个链表的即为同一类,即代表为同一个数字,然后可以根据链表的坐标提取单个数字。

void ImageSegmentation::connectedRegionsTaggingOfBarItemImg(int barItemIndex) {TagImage = CImg<int>(subImageSet[barItemIndex]._width, subImageSet[barItemIndex]._height, 1, 1, 0);tagAccumulate = -1;cimg_forX(subImageSet[barItemIndex], x)cimg_forY(subImageSet[barItemIndex], y) {//第一行和第一列if (x == 0 || y == 0) {int intensity = subImageSet[barItemIndex](x, y, 0);if (intensity == 0) {addNewClass(x, y, barItemIndex);}}//其余的行和列else {int intensity = subImageSet[barItemIndex](x, y, 0);if (intensity == 0) {//检查正上、左上、左中、左下这四个邻点int minTag = Infinite;        //最小的tagPointPos minTagPointPos(-1, -1);//先找最小的标记findMinTag(x, y, minTag, minTagPointPos, barItemIndex);//当正上、左上、左中、左下这四个邻点有黑色点时,合并;if (minTagPointPos.x != -1 && minTagPointPos.y != -1) {mergeTagImageAndList(x, y - 1, minTag, minTagPointPos, barItemIndex);for (int i = -1; i <= 1; i++) {if (y + i < subImageSet[barItemIndex]._height)mergeTagImageAndList(x - 1, y + i, minTag, minTagPointPos, barItemIndex);}//当前位置TagImage(x, y, 0) = minTag;PointPos cPoint(x, y + divideLinePointSet[barItemIndex] + 1);pointPosListSet[minTag].push_back(cPoint);}//否则,作为新类else {addNewClass(x, y, barItemIndex);}}}}
}void ImageSegmentation::addNewClass(int x, int y, int barItemIndex) {tagAccumulate++;//cout << "tagAccumulate " << tagAccumulate << endl;TagImage(x, y, 0) = tagAccumulate;classTagSet.push_back(tagAccumulate);list<PointPos> pList;PointPos cPoint(x, y + divideLinePointSet[barItemIndex] + 1);pList.push_back(cPoint);pointPosListSet.push_back(pList);
}void ImageSegmentation::findMinTag(int x, int y, int &minTag, PointPos &minTagPointPos, int barItemIndex) {if (subImageSet[barItemIndex](x, y - 1, 0) == 0) {     //正上if (TagImage(x, y - 1, 0) < minTag) {minTag = TagImage(x, y - 1, 0);minTagPointPos.x = x;minTagPointPos.y = y - 1;}}for (int i = -1; i <= 1; i++) {        //左上、左中、左下if (y + i < subImageSet[barItemIndex]._height) {if (subImageSet[barItemIndex](x - 1, y + i, 0) == 0 && TagImage(x - 1, y + i, 0) < minTag) {minTag = TagImage(x - 1, y + i, 0);minTagPointPos.x = x - 1;minTagPointPos.y = y + i;}}}
}void ImageSegmentation::mergeTagImageAndList(int x, int y, const int minTag, const PointPos minTagPointPos, int barItemIndex) {//赋予最小标记,归并列表if (subImageSet[barItemIndex](x, y, 0) == 0) {int tagBefore = TagImage(x, y, 0);if (tagBefore != minTag) {    //不是最小的tag//把所有同一类的tag替换为最小tag、把list接到最小tag的listlist<PointPos>::iterator it = pointPosListSet[tagBefore].begin();for (; it != pointPosListSet[tagBefore].end(); it++) {TagImage((*it).x, (*it).y - divideLinePointSet[barItemIndex] - 1, 0) = minTag;}pointPosListSet[minTag].splice(pointPosListSet[minTag].end(), pointPosListSet[tagBefore]);}}
}

3) 算法的实现主要参考两个链接:

https://segmentfault.com/a/1190000006120473

https://en.wikipedia.org/wiki/Connected-component_labeling

但是可以发现我这里对链接说的算法做一些改进。上面链接提到的是对图像一行行像素进行扫描,而这导致的结果是,上面也提到了:输出数字的顺序乱了,越高的数字排在越前输出。因此我根据这个算法的原理做出以下改进:

① 从一行行扫描改为一列列扫描

② 从取左前、左上、正上、右上4个点检测连通域,改为取正上、左上、左前、左下4个点检测连通域。

阶段结果:

 

3.6、对每张子图,存储单个数字以及一个图像名列表文本

由于后面SVM预测的读入格式要求,需要将需要预测的图像的名字制造成一张列表文本:

void ImageSegmentation::saveSingleNumberImageAndImglist(int barItemIndex) {for (int i = 0; i < pointPosListSet.size(); i++) {if (pointPosListSet[i].size() != 0) {//先找到数字的包围盒int xMin, xMax, yMin, yMax;getBoundingOfSingleNum(i, xMin, xMax, yMin, yMax);int width = xMax - xMin;int height = yMax - yMin;//将单个数字填充到新图像:扩充到正方形//int imgSize = (width > height ? width : height) + SingleNumberImgBoundary * 2;//CImg<int> singleNum = CImg<int>(imgSize, imgSize, 1, 1, 0);//list<PointPos>::iterator it = pointPosListSet[i].begin();//for (; it != pointPosListSet[i].end(); it++) {//    int x = (*it).x;// int y = (*it).y;// int singleNumImgPosX, singleNumImgPosY;//   if (height > width) {//      singleNumImgPosX = (x - xMin) + (imgSize - width) / 2;//      singleNumImgPosY = (y - yMin) + SingleNumberImgBoundary;//    }// else {//        singleNumImgPosX = (x - xMin) + SingleNumberImgBoundary;//        singleNumImgPosY = (y - yMin) + (imgSize - height) / 2;// }// singleNum(singleNumImgPosX, singleNumImgPosY, 0) = 255;//}//将单个数字填充到新图像:原长宽比int imgSizeH = height + SingleNumberImgBoundary * 2;int imgSizeW = width + SingleNumberImgBoundary * 2;CImg<int> singleNum = CImg<int>(imgSizeW, imgSizeH, 1, 1, 0);list<PointPos>::iterator it = pointPosListSet[i].begin();for (; it != pointPosListSet[i].end(); it++) {int x = (*it).x;int y = (*it).y;int singleNumImgPosX, singleNumImgPosY;singleNumImgPosX = (x - xMin) + SingleNumberImgBoundary;singleNumImgPosY = (y - yMin) + SingleNumberImgBoundary;singleNum(singleNumImgPosX, singleNumImgPosY, 0) = 255;}//singleNum.display("single Number");string postfix = ".bmp";char shortImgName[200];sprintf(shortImgName, "%d_%d%s\n", barItemIndex, classTagSet[i], postfix.c_str());imglisttxt += string(shortImgName);char addr[200];sprintf(addr, "%s%d_%d%s", basePath.c_str(), barItemIndex, classTagSet[i], postfix.c_str());singleNum.save(addr);}}imglisttxt += "*\n";//把tag集、每一类链表数据集清空classTagSet.clear();for (int i = 0; i < pointPosListSet.size(); i++) {pointPosListSetForDisplay.push_back(pointPosListSet[i]);pointPosListSet[i].clear();}pointPosListSet.clear();
}

阶段结果:

(用*号将原图上一行行数字分隔)

好了!大功告成,后面的SVM预测只需要读入bmp以及txt就可以了~

剩余问题:

1、单独数字连接了起来,需要分开:

像下面的2 6 2连在一起了,需要再研究如何分开。

[计算机视觉] 手写数字识别(上)——数字字符分割相关推荐

  1. 神经网络初探(BP 算法、手写数字识别)

    神经网络的结构就不说了,网上一大堆-- 这次手写数字识别采用的是 sigmoid 激活函数和 MSE 损失函数. 虽然网上说这种方式比不上 softmax 激活函数和交叉熵损失函数,后者更适合用于分类 ...

  2. 基于Paddle的计算机视觉入门教程——第7讲 实战:手写数字识别

    B站教程地址 https://www.bilibili.com/video/BV18b4y1J7a6/ 任务介绍 手写数字识别是计算机视觉的一个经典项目,因为手写数字的随机性,使用传统的计算机视觉技术 ...

  3. 在MNIST数据集上训练一个手写数字识别模型

    使用Pytorch在MNIST数据集上训练一个手写数字识别模型, 代码和参数文件 可下载 1.1 数据下载 import torchvision as tvtraining_sets = tv.dat ...

  4. opencl 加速 c语言程序_在AlveoU200加速卡上实现简单手写数字识别

    最近实验室租了块xilinx家的AlveoU200加速卡,过去几天被这块板吸引了注意力.刚开始了解,做点什么来试试水呢?一想,可以把曾经学 @蔡宇杰 大佬在pynq-z2上做的那个手写数字识别工程在这 ...

  5. 基于LeNet5的手写数字识别,在ModelArts和GPU上复现

    基于LeNet5的手写数字识别 实验介绍 LeNet5 + MNIST被誉为深度学习领域的"Hello world".本实验主要介绍使用MindSpore在MNIST手写数字数据集 ...

  6. 手写数字识别画板前后端实现 | Flask+深度神经网络

    1. 系统概要 手写数字识别画板系统,按照MVC原则开发,主要由两部分组成:交互界面(视图View)部分是传统的HTML +CSS+JS网页(这同样也是一种遵循MVC开发方式):手写数字识别部分(模型 ...

  7. 【毕业设计_课程设计】手写数字识别系统的设计实现(源码+论文)

    文章目录 0 项目说明 1 系统概述 1.1 系统实现环境 2 研究方法 2.1 图像预处理阶段 2.2 特征提取阶段 2.3 数字识别阶段 3 研究结论 4 论文概览 5 项目工程 0 项目说明 手 ...

  8. 深度学习21天——卷积神经网络(CNN):实现mnist手写数字识别(第1天)

    目录 一.前期准备 1.1 环境配置 1.2 CPU和GPU 1.2.1 CPU 1.2.2 GPU 1.2.3 CPU和GPU的区别 第一步:设置GPU 1.3 MNIST 手写数字数据集 第二步: ...

  9. 毕业设计 手写数字识别算法研究与实现(源码+论文)

    文章目录 0 项目说明 1 系统概述 1.1 系统实现环境 2 研究方法 2.1 图像预处理阶段 2.2 特征提取阶段 2.3 数字识别阶段 3 研究结论 4 论文概览 5 最后 0 项目说明 手写数 ...

最新文章

  1. 【VB】学生信息管理系统5——数据库代码
  2. url参数解析 url解析 ?解析成对象
  3. Java设计模式之结构型:组合模式
  4. 织梦其他模型使用联动类型地区联动
  5. Python字符串处理小案例
  6. 【Android】16.3 带Intent过滤器的Services
  7. 浏览器导入和导出cookie
  8. OwinStartupAttribute
  9. LeetCode Factorial Trailing Zeroes (阶乘后缀零)
  10. Aitit 认证体系之道 attilax著艾龙著 1. 认证体系分类 2 1.1. 按照语言来分 java net php 2 1.2. 按照平台来分 web cs 桌面 2 1.3. 综合性认证
  11. 10分钟搭建一个H5商城,支持微信支付和各平台小程序
  12. 6个方法帮交互设计师与上下游顺畅合作
  13. python 爬取数据(CBA所有球队数据) -爬虫
  14. GPU中实现反距离加权插值(IDW)
  15. php语句连接,php – 内连接选择语句
  16. 一个普通程序员和他的猫
  17. 关于华为昆仑关键业务服务器
  18. YUV/YCbCr/YPbPr
  19. 演讲:星座决定命运(密码为作者姓名拼音)
  20. Mysql高可用集群搭建(一)一主两从服务搭建

热门文章

  1. 常用操作系统扫描工具介绍
  2. c++后台开发项目_在京东,我是怎么做项目管理的
  3. Android笔记(翻译):card.io SDK for Android银行卡扫描
  4. HP滤波图文介绍与python代码实现
  5. Git 分支合并分支代码
  6. 鹏华基金核心系统完成国产化升级,腾讯云大数据TBDS再拓应用新场景
  7. Windows Server 2012 R2 系统安装详细步骤
  8. Docker学习1-CentOS 7安装Docker
  9. MATLAB可视化手动抠图
  10. 关于地推统计,APP地推统计哪家强?