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

作业要求:

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

[计算机视觉] 手写数字识别(上)——数字字符分割_第1张图片 [计算机视觉] 手写数字识别(上)——数字字符分割_第2张图片

        2、A4纸矫正

        3、数字字符切割

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

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


实现环境:

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


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

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


阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第3张图片


[计算机视觉] 手写数字识别(上)——数字字符分割_第4张图片

[计算机视觉] 手写数字识别(上)——数字字符分割_第5张图片



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

   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纸边缘检测


阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第6张图片


2、A4纸矫正:

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

阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第7张图片


3、数字按顺序分割

  3.1、图像二值化

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

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


阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第8张图片



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

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

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

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

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

        [计算机视觉] 手写数字识别(上)——数字字符分割_第9张图片(原图)  [计算机视觉] 手写数字识别(上)——数字字符分割_第10张图片(放大图)

void ImageSegmentation::findDividingLine() {
	HistogramImage = CImg(BinaryImg._width, BinaryImg._height, 1, 3, 0);
	DividingImg = CImg(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 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 barItemImg = CImg(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]);
}


阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第11张图片[计算机视觉] 手写数字识别(上)——数字字符分割_第12张图片 

[计算机视觉] 手写数字识别(上)——数字字符分割_第13张图片    [计算机视觉] 手写数字识别(上)——数字字符分割_第14张图片


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

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


[计算机视觉] 手写数字识别(上)——数字字符分割_第15张图片

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

[计算机视觉] 手写数字识别(上)——数字字符分割_第16张图片[计算机视觉] 手写数字识别(上)——数字字符分割_第17张图片


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

//根据X方向直方图判断真实的拐点
vector getInflectionPosXs(const CImg& XHistogramImage) {
	vector resultInflectionPosXs;
	vector 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 getDivideLineXofSubImage(const CImg& subImg) {
	vector InflectionPosXs;

	//先绘制X方向灰度直方图
	CImg XHistogramImage = CImg(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 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> getRowItemImgSet(const CImg& lineImg, vector _dividePosXset) {
	vector> result;
	for (int i = 1; i < _dividePosXset.size(); i++) {
		int rowItemWidth = _dividePosXset[i] - _dividePosXset[i - 1];
		CImg rowItemImg = CImg(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;
}

阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第18张图片

[计算机视觉] 手写数字识别(上)——数字字符分割_第19张图片


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

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

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

        [计算机视觉] 手写数字识别(上)——数字字符分割_第20张图片

    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 answerXXY = CImg(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 answerXXY2 = CImg(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 answerXY = CImg(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);
	}
}

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

[计算机视觉] 手写数字识别(上)——数字字符分割_第21张图片


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

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

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

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

            [计算机视觉] 手写数字识别(上)——数字字符分割_第22张图片

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

                [计算机视觉] 手写数字识别(上)——数字字符分割_第23张图片

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

               [计算机视觉] 手写数字识别(上)——数字字符分割_第24张图片  [计算机视觉] 手写数字识别(上)——数字字符分割_第25张图片

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

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


void ImageSegmentation::connectedRegionsTaggingOfBarItemImg(int barItemIndex) {
	TagImage = CImg(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;        //最小的tag
				PointPos 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 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的list
			list::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个点检测连通域。


阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第26张图片 [计算机视觉] 手写数字识别(上)——数字字符分割_第27张图片



  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 singleNum = CImg(imgSize, imgSize, 1, 1, 0);

			//list::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 singleNum = CImg(imgSizeW, imgSizeH, 1, 1, 0);

			list::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();
}

阶段结果:

[计算机视觉] 手写数字识别(上)——数字字符分割_第28张图片

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



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


剩余问题:

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

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

        [计算机视觉] 手写数字识别(上)——数字字符分割_第29张图片







你可能感兴趣的:(计算机视觉)