大二的时候写的一个CV小玩意,最终决定还是把它放出来,也许会帮助到很多人,代码写的很丑,大家多多包涵。附加实验报告主要部分。大家会给这个课设打多少分呢?
课题背景及意义:
本项目主要目标是设计一套能自动分析我校现行的试卷封面并获取学生题目得分信息以及学号信息的原型系统。
本项目的实现有助于提升我校成绩管理的自动化程度以及试卷分析的量化程度,分担一部分期末教师阅卷的工作。
课题相关研究情况概述:
本项目进行至今已经完成了单个数字的识别,并且准确率高达98.74%。完成了试卷卷面的基本分析工作,可以准确定位评分栏并读取评分栏中的内容。不足之处在于没有实现一个成熟的分字功能,无法正确识别连续多个数字,这也就无法识别学号、大于9的分数信息。此外,还完成了界面的设计以及数据处理和用户界面的通信模块,并且设计并建立了相应的数据库。
主要研究内容:
准备工作:
1、读一些书本资料+论文,涉及图像处理、机器学习、opencv手册、大量相关论文等。
2、Opencv环境配置,尝试使用caffe+cuda未果,最终,机器学习部分使用的是Opencv的machine learning库。
3、大致确定思路,选择可能会使用的训练模型,收集训练集,获取试卷封面样本。
正式工作:
模式识别部分:
1、学习图像处理相关的算法,实现一部分接下来的工作中需要用到的函数、模块:大津法求二值化阈值、图像二值化、获取灰度分布直方图、图片放缩、伽马校对等。
图像二值化:使用大津法对一张图片求出其阈值,之后遍历所有像素点,判断是否超出阈值,做相应的重新赋值操作。
大津法的基本思路和公式推导:
对于图像I(x,y),前景(即目标)和背景的分割阈值记作T,属于前景的像素点数占整幅图像的比例记为ω0,其平均灰度μ0;背景像素点数占整幅图像的比例为ω1,其平均灰度为μ1。图像的总平均灰度记为μ,类间方差记为g。
假设图像的背景较暗,并且图像的大小为M*N,图像中像素的灰度值小于阈值T的像素个数记作N0,像素灰度大于阈值T的像素个数记作N1,则有:
得到等价公式:,这就是类间方差。
采用遍历的方法得到使类间方差g最大的阈值T,即为所求。
2、处理训练集。由于选用的是mnist在官网提供的美国中学生手写体,被打包在了一个文件里,编码释放该文件内容,共计六万幅28*28训练测试用手写数字图像。
3、查询opencv手册,查找opencv的ml库内有什么现成的模型,最终选定k-邻近算法(knn)作为本次工作的分类算法。
4、(特征提取)选择提取合适的特征向量来描述每一图数字图片,选择提取hog特征(方向梯度直方图)。
1)、将梯度方向平均划分为9个区间;细胞单元大小7*7;扫描窗口步长为7;块大小14*14,即四个细胞单元组成一个块,串联每个细胞中的特征获得块特征。
2)、计算梯度并构建梯度方向直方图:
定义Gx(x,y),Gy(x,y),H(x,y)分别表示输入图像中像素点(x,y)处的水平方向梯度、垂直方向梯度和该点像素值。则有如下公式:
Gx(x,y)=H(x+1,y)-H(x-1,y)
Gy(x,y)=H(x,y+1)-H(x,y-1)
定义G(x,y)为像素点(x,y)处的梯度幅值、梯度方向为α(x,y),则有:
4)、组合成块,获取整个图片的特征向量。
hog特征向量的维度计算:对于一张28*28的图片,每7*7的像素组成一个细胞单元,每2*2的细胞单元组成一个块,每个细胞单元有9个特征,所以每个块有2*2*9=36个特征。扫描窗口步长为7,那么水平方向就有28/7-1=3个窗口,竖直方向也有28/7-1=3个窗口。所以对于整张图片而言,有36*3*3=324个特征。
5、(模式识别)编码实现knn分类器对0~9十个数字进行分类,使用六万幅图片中的五万幅作为训练样本,剩余一万幅作为测试样本,准确超过98.5%。关于kNN:如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 kNN方法在类别决策时,只与极少量的相邻样本有关。
图像处理部分:
1、预处理,二值化、平滑处理、去噪。
2、定位表格的思路:hough变换提取线段,提取出的线段要通过判断长度来确定是不是符合要求,即是不是组成表格的线段。hough变换的思路很简单,就是把下的以横纵坐标为轴的坐标系下的直线方程投影到一个直角坐标系下,这时候直线变成了一个点。公式推导:
设直线
则直线:
化简:
竖:,
横:,
随后求横竖线的笛卡尔积集获取交点。
3、通过几何学运算获取横竖线段两两交点,将交点保存。公式推导:
104 // (p1-p0)X(p2-p0)=0
105 // (p3-p0)X(p2-p0)=0
106
107 // (y1-y2)x0+(x2-x1)y0+x1y2-x2y1=0
108 // (y3-y4)x0+(x4-x3)y0+x3y4-x4y3=0
109
110 // a1x0+b1y0+c1=0
111 // a2x0+b2y0+c2=0
112
113 // x0=(c1*b2-c2*b1)/(a2*b1-a1*b2)
114 // y0=(a2*c1-a1*c2)/(a1*b2-a2*b1)
4、由于两个点可以确定一个矩形,所以提取出包含分数信息的矩形图像并保存。
5、 进一步处理刚刚提取的矩形图像,进行简单的预处理后裁切,使得数字占图片的比例尽可能地大,之后缩小成28*28的图片。
6、利用识别模块进行识别,将结果保存在Message结构体内,Message结构体定义:
typedef struct Message {
string studentID;
size_t score[];
size_t sum;
Message() {
studentID.clear();
memset(score, -, sizeof(score));
sum = ;
}
}Message;
附图:
测试分类结果
测试单独的样例
线段识别
卷面的分析和分数的识别
识别过程中的中间文件
连通块分解
总结与展望:
1、关于界面:我比较注重实际的功能,在功能不完全的时候添加界面不但没有使工作变得快捷,反而会使程序不是那么容易调试。不过,未来还是会添加一个界面。
2、关于分类器的选择和性能的一点解释:knn不是真正需要训练,但是开工前简单分析了一下,觉得使用最好实现的knn分类效果会很好,所以没有使用卷积神经网络(cnn) IO上过于频繁,计算复杂度过高,hog特征提取出的特征向量维度过高,使得运算开销过大,所以需要优化。由于多线程对于IO频繁的程序性能上有较好的表现,所以在程序上做多进程优化;在对knn分类判断方法做改进,达到优化目的。具体是将单纯的欧几里德距离改变成中心向量投影的方式,即在分类时,先将待分类样本投影到样本中心所在的直线上,根据待分类样本的投影点和训练样本的投影点之间的距离关系确定样本类别(参见文献1001-7119(2013)12-0127-03)。可以将问题转换成多个二分类问题,从而达到优化的目的;另一种是换用其他的分类器,现在做手写字符识别技术基本都是采用了cnn,老师拟采用cnn也是考虑了这个原因,所以cnn是很不错的选择。另外,由于项目代码中涉及了不少的图像和矩阵运算,所以接下来可能考虑使用GPU做加速,我的笔记本的显卡是NVIDIA,所以厂商开发的CUDA运算平台是一个很好的选择。
3、关于表格中多个数字以及学号的提取:时间关系没有做数字的切分,但是我实现了一个根据连通块的简单的切分功能。具体一些就是任取一个非空白起点开始做dfs,拟定8个方向可以扩展,每次扩展一个像素。如果走到的一个像素非空白,那认为这个像素和上一个像素是属于同一个连通块的,那我就更新这个像素的连通块从属关系,维护像素属于哪个连通块的时候可以用并查集。由于受限太大,命中率低,我没有加入到demo中,而且这个切分是递归写的,如果不调整程序运行时栈的大小一定会爆栈,所以要么写成迭代形式的,要么手工开栈。这样就使得程序变得不可控了。所以未来的工作里一定会有这一项,可能会采取其他的切分方式,比如求最小包围矩阵、投影法或者滴水算法。
4、关于题目数少于表格数:会有这样的普遍情况,那就是一共给了十个题的格子,总题数却只有不到十个。这种问题的解决方法有很多。现在的程序中是事先默认一个题数,在识别的时候只去识别前几个题,最后保存信息只保存对应题数的分数等信息。还有一种方法是在分类器上做文章,就是规定一个阈值,相邻前k个中最多的几个样本距离的平均值如果大于某一个数值的话,那我就认为这些挑出来的被认为是“接近”的样本实际上不是真正的接近,他们只是相对于其他样本而言接近而已,所以可以认为这个待测样本是无法识别的。由于完全空白的表格实际上无法被分类器正确分类,所以我可以认为这种情况是上述情况的特例,这样遇到第一个无法识别的情况我就可以认为是没有这个题了。
5、关于表格的定位和提取:因为生产环境中的样本——试卷长得都一样,所以我认为没有必要采用其他类似腐蚀运算、膨胀运算等提取方法。因为试卷的样式实在是太唯一了,唯一可以影响算法的因素就是阅卷时老师照相的手抖得实在不行。只是在定位表格以后,如何能够优雅地把数字提取出来就是一个大问题了。目前我还没有一个好的方法,期待将来的学习可以让我有所突破。
6、关于图片的预处理:因为对计算机图形学了解得不是那么地深刻,所以对于要处理的图片需要做什么预处理还是会有很多疑问。我认为在将来的工作中应该加入更多的预处理,使图片的一些问题提前在预处理阶段解决,这样有助于后面工作的展开和准确率的提高。
7、关于整个项目代码的架构:我很少去写一个项目。加上有opencv这样庞大的第三方库,使得我很难掌控整个代码的架构,这一切都归咎于我没有想清楚一切问题就开始写代码了。我想一个完整的项目一定有清晰的逻辑结构,也一定有清晰的代码结构。所以我接下来会把代码分成多个文件,便于管理。
我的收获:
1、读了很多的资料文章,学到了很多有关计算机图形学和机器学习方面的知识,并且在这个项目上有部分体现。
2、了解了做一个项目的基本流程。这个项目虽然简单,但是代码写起来会很繁琐。因为小的细节问题实在是太多了,所以我采用了计算机网络课上学到的,像协议一样分层实现,每一层有每一层的工作,他们之间的工作不能越级,在这样一个规则下进行项目使得一切都变得清晰明了,于是就可以有更多的时间投入到具体问题的分析当中去了。
3、认识到自己有些浮躁,对一个问题的认识还没有达到深刻的前提下就开始盲目地做,导致自己走了很多很多的弯路。以后在分析和解决一个问题之前一定要先对问题本身多做一些讨论,确保自己理解问题的情况下再去着手寻找问题的解决方案。
4、思维不够活跃,解决问题的时候容易被固有知识限制,导致思考不出问题的解。应该多读一些论文和文章,多去了解一下别人的解决方法,开阔视野;再多做一些思维上的训练,锻炼思维能力。
5、上述的整个项目都是我一个人完成的,这会使项目受限于我自己的思维和能力。应当找一个人或者多个人讨论,一起做,这样就会使项目进展得更快,解决问题的方法也一定不止一个,相互学习会获得更大的进步。
附上全部代码:
#pragma warning(disable : 4018) #include <algorithm>
#include <iostream>
#include <iomanip>
#include <cstring>
#include <climits>
#include <complex>
#include <fstream>
#include <cassert>
#include <cstdio>
#include <bitset>
#include <vector>
#include <deque>
#include <queue>
#include <stack>
#include <ctime>
#include <set>
#include <map>
#include <cmath> #include <opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp> using namespace std;
using namespace cv; #define TEST Mat test_mat; \
test_mat = imread("./test/0/0_05001.jpg"); \
imshow("", test_mat); \
waitKey() #define dbg(x) \
cout << "Kirai Debug> " << #x << " = " << x << endl #define cvQueryHistValue_1D( hist, idx0 ) \
((float)cvGetReal1D( (hist)->bins, (idx0))) //#define MAIN_CPP_START //识别模块 //#define SVMTRAIN //训练svm
//#define ONLYONECLASS //只获取一类样例
//#define GETFILEONETIME //某类样例集合中获取一个样例
#define SAVETRAINED //训练模式
//#define SAVEASTXT //保存knn训练结果到txt格式的文件中 const int bin = ; //HOG特征提取时方向数
const int ImgWidth = ; //图片宽度
const int ImgHeight = ; //图片高度
const int nImgNum = ; //训练图片数量(每个数字)
const int testNum = ; //测试样例图片数量(每个数字)
const char* sample = "./data/linetest.jpg";
const char* bined = "./data/conv.jpg";
const string rootDir("./sample/");
const string jpg(".jpg");
const string dirNames[] = {
rootDir + "0/", rootDir + "1/", rootDir + "2/",
rootDir + "3/", rootDir + "4/", rootDir + "5/",
rootDir + "6/", rootDir + "7/", rootDir + "8/",
rootDir + "9/"
}; vector<Mat> samples;
vector<int> labels; Mat dat_mat, res_mat;
KNearest knn;
//CvSVM svm;
vector<float> descriptors; //卷面信息
typedef struct Message {
string studentID;
size_t score[];
size_t sum;
Message() {
studentID.clear();
memset(score, -, sizeof(score));
sum = ;
}
}Message; //重载Point类中的运算符
Point operator +(Point a, Point b) {return Point(a.x + b.x, a.y + b.y);}
Point operator -(Point a, Point b) {return Point(a.x - b.x, a.y - b.y);}
int operator ^(Point a, Point b) {return a.x * b.y - a.y * b.x;}
int operator *(Point a, Point b) {return a.x * b.x + a.y * b.y;} //用两个无穷远的点来作为直线的标记
typedef struct Line {
Point a;
Point b;
Line() = default;
Line(Point aa, Point bb) : a(aa), b(bb) {}
}Line;
vector<Line> para;//平行线
vector<Line> vert;//垂直线 //求p1p2和p3p4的交点p0(x0,y0)
Point getCross(Line p, Line q) {
//------------ 公式推导 ------------//
// (p1-p0)X(p2-p0)=0
// (p3-p0)X(p2-p0)=0 // (y1-y2)x0+(x2-x1)y0+x1y2-x2y1=0
// (y3-y4)x0+(x4-x3)y0+x3y4-x4y3=0 // a1x0+b1y0+c1=0
// a2x0+b2y0+c2=0 // x0=(c1*b2-c2*b1)/(a2*b1-a1*b2)
// y0=(a2*c1-a1*c2)/(a1*b2-a2*b1)
//--------------------------------//
long double x1 = p.a.x, x2 = p.b.x;
long double y1 = p.a.y, y2 = p.b.y;
long double x3 = q.a.x, x4 = q.b.x;
long double y3 = q.a.y, y4 = q.b.y;
long double a1 = y1 - y2, b1 = x2 - x1, c1 = x1 * y2 - x2 * y1;
long double a2 = y3 - y4, b2 = x4 - x3, c2 = x3 * y4 - x4 * y3;
Point ans;
ans.x = int((c1 * b2 - c2 * b1) / (a2 * b1 - a1 * b2));
ans.y = int((a2 * c1 - a1 * c2) / (a1 * b2 - a2 * b1));
return ans;
} //大津法求二值化阈值
int otsu(const IplImage* src_image) {
double w0 = 0.0;
double w1 = 0.0;
double u0_temp = 0.0;
double u1_temp = 0.0;
double u0 = 0.0;
double u1 = 0.0;
double delta_temp = 0.0;
double delta_max = 0.0; int pixel_count[] = { };
float pixel_pro[] = { };
int threshold = ;
uchar* data = (uchar*)src_image->imageData;
for (int i = ; i < src_image->height; i++) {
for (int j = ; j < src_image->width; j++) {
pixel_count[(int)data[i * src_image->width + j]]++;
}
}
for (int i = ; i < ; i++) {
pixel_pro[i] = (float)pixel_count[i] / (src_image->height * src_image->width);
}
for (int i = ; i < ; i++) {
w0 = w1 = u0_temp = u1_temp = u0 = u1 = delta_temp = ;
for (int j = ; j < ; j++) {
if (j <= i) {
w0 += pixel_pro[j];
u0_temp += j * pixel_pro[j];
}
else {
w1 += pixel_pro[j];
u1_temp += j * pixel_pro[j];
}
}
u0 = u0_temp / w0;
u1 = u1_temp / w1;
delta_temp = (float)(w0 *w1* pow((u0 - u1), ));
if (delta_temp > delta_max) {
delta_max = delta_temp;
threshold = i;
}
}
return threshold;
}
int otsu(Mat src_image) {
double w0 = 0.0;
double w1 = 0.0;
double u0_temp = 0.0;
double u1_temp = 0.0;
double u0 = 0.0;
double u1 = 0.0;
double delta_temp = 0.0;
double delta_max = 0.0; int pixel_count[] = { };
float pixel_pro[] = { };
int threshold = ; for (int i = ; i < src_image.size().height; i++) {
uchar* data = src_image.ptr<uchar>(i);
for (int j = ; j < src_image.size().width; j++) {
pixel_count[int(data[j])]++;
}
}
for (int i = ; i < ; i++) {
pixel_pro[i] = (float)pixel_count[i] / (src_image.size().height * src_image.size().width);
}
for (int i = ; i < ; i++) {
w0 = w1 = u0_temp = u1_temp = u0 = u1 = delta_temp = ;
for (int j = ; j < ; j++) {
if (j <= i) {
w0 += pixel_pro[j];
u0_temp += j * pixel_pro[j];
}
else {
w1 += pixel_pro[j];
u1_temp += j * pixel_pro[j];
}
}
u0 = u0_temp / w0;
u1 = u1_temp / w1;
delta_temp = (float)(w0 * w1 * pow((u0 - u1), )); if (delta_temp > delta_max) {
delta_max = delta_temp;
threshold = i;
}
}
return threshold;
} //图像二值化
int imageBinarization(IplImage* src_image) {
IplImage* binImg = cvCreateImage(cvGetSize(src_image), src_image->depth, src_image->nChannels);
CvScalar s;
int ave = ;
int binThreshold = otsu(src_image);
for (int i = ; i < src_image->height; i++) {
for (int j = ; j < src_image->width; j++) {
s = cvGet2D(src_image, i, j);
ave = int((s.val[] + s.val[] + s.val[]));
if (ave < * binThreshold) { //取反 ave < binThreshold
s.val[] = 0xff;
s.val[] = 0xff;
s.val[] = 0xff;
cvSet2D(src_image, i, j, s);
}
else {
s.val[] = 0x00;
s.val[] = 0x00;
s.val[] = 0x00;
cvSet2D(src_image, i, j, s);
}
}
}
cvCopy(src_image, binImg);
cvSaveImage(bined, binImg);
//cvShowImage("binarization", binImg);
//waitKey(0);
return binThreshold;
}
int imageBinarization(Mat src_image) {
Mat binImg = Mat::zeros(src_image.size().height, src_image.size().width, CV_8UC3);
int ave = ;
int binThreshold = otsu(src_image);
for (int i = ; i < src_image.size().width; i++) {
for (int j = ; j < src_image.size().height; j++) {
ave = src_image.at<Vec3b>(j, i)[] + src_image.at<Vec3b>(j, i)[] + src_image.at<Vec3b>(j, i)[];
if (ave < * binThreshold) {
binImg.at<Vec3b>(j, i)[] = 0xff;
binImg.at<Vec3b>(j, i)[] = 0xff;
binImg.at<Vec3b>(j, i)[] = 0xff;
}
else {
binImg.at<Vec3b>(j, i)[] = ;
binImg.at<Vec3b>(j, i)[] = ;
binImg.at<Vec3b>(j, i)[] = ;
}
}
}
imwrite("./data/otsu.jpg", binImg);
//imshow("", binImg);
//waitKey();
return binThreshold;
} //获取灰度分布直方图
CvHistogram* getHistogram(const char* fileName) {
IplImage* src = cvLoadImage(fileName);
IplImage* gray_plane = cvCreateImage(cvGetSize(src), , );
cvCvtColor(src, gray_plane, CV_BGR2GRAY); int hist_size = ;
int hist_height = ;
float range[] = { , };
float* ranges[] = { range };
CvHistogram* gray_hist = cvCreateHist(, &hist_size, CV_HIST_ARRAY, ranges, );
cvCalcHist(&gray_plane, gray_hist, , );
cvNormalizeHist(gray_hist, 1.0); int scale = ;
IplImage* hist_image = cvCreateImage(cvSize(hist_size*scale, hist_height), , );
cvZero(hist_image);
float max_value = ;
cvGetMinMaxHistValue(gray_hist, , &max_value, , ); for (int i = ; i<hist_size; i++) {
float bin_val = cvQueryHistValue_1D(gray_hist, i);
int intensity = cvRound(bin_val*hist_height / max_value);
cvRectangle(hist_image,
cvPoint(i*scale, hist_height - ),
cvPoint((i + )*scale - , hist_height - intensity),
CV_RGB(, , ));
} //cvNamedWindow("GraySource", 1);
//cvShowImage("GraySource", gray_plane);
//cvNamedWindow("H-S Histogram", 1);
//cvShowImage("H-S Histogram", hist_image); cvWaitKey(); return gray_hist;
}
CvHistogram* getHistogram(IplImage* src) {
IplImage* gray_plane = cvCreateImage(cvGetSize(src), , );
cvCvtColor(src, gray_plane, CV_BGR2GRAY);
int hist_size = ;
int hist_height = ;
float range[] = { , };
float* ranges[] = { range };
CvHistogram* gray_hist = cvCreateHist(, &hist_size, CV_HIST_ARRAY, ranges, );
cvCalcHist(&gray_plane, gray_hist, , );
cvNormalizeHist(gray_hist, 1.0); int scale = ;
IplImage* hist_image = cvCreateImage(cvSize(hist_size*scale, hist_height), , );
cvZero(hist_image);
float max_value = ;
cvGetMinMaxHistValue(gray_hist, , &max_value, , ); for (int i = ; i<hist_size; i++) {
float bin_val = cvQueryHistValue_1D(gray_hist, i);
int intensity = cvRound(bin_val*hist_height / max_value);
cvRectangle(hist_image,
cvPoint(i*scale, hist_height - ),
cvPoint((i + )*scale - , hist_height - intensity),
CV_RGB(, , ));
}
return gray_hist;
} //数字转换成特定位数的字符串
inline void i2s(string& str, int i, int len = ) {
stringstream ss;
ss << setw(len) << setfill('') << i;
str = ss.str();
} //图像切割
void jpgSplit(string fileName) {
Mat curSample = imread(fileName);
const int spt = ;
vector<Mat> part;
int s = ;
for (int i = ; i < ; i += spt) {
for (int j = ; j < ; j += spt) {
part.push_back(curSample(Rect(i, j, spt, spt)));
imshow("A", part[part.size() - ]);
waitKey();
stringstream ss;
string tmp;
ss << s++; ss >> tmp;
tmp = tmp + ".jpg";
imwrite(tmp, part[part.size() - ]);
}
}
} //伽马校对
void gammaCorrection(IplImage& cData) {
for (int i = ; i < cData.height; i++) {
for (int j = ; j < cData.width; j++) {
CvScalar s = cvGet2D(&cData, i, j);
s.val[] = sqrt(s.val[]);
s.val[] = sqrt(s.val[]);
s.val[] = sqrt(s.val[]);
}
}
}
void gammaCorrection(Mat& cData) {
for (int i = ; i < cData.size().height; i++) {
for (int j = ; j < cData.size().width; j++) {
for (int k = ; k < ; k++) {
cData.at<Vec3b>(i, j)[k] = uchar(sqrt(cData.at<Vec3b>(i, j)[k]));
}
}
}
} //图片尺寸修改
void imgResize(const string fileName, Mat& dst, int height, int width) {
IplImage* src = cvLoadImage(fileName.c_str());
if (src->height != height || src->width != width) {
IplImage* reSizedMat = NULL;
CvSize ImgSize;
ImgSize.width = width;
ImgSize.height = height;
reSizedMat = cvCreateImage(ImgSize, src->depth, src->nChannels);
cvResize(src, reSizedMat, CV_INTER_AREA);
dst = Mat(reSizedMat, );
}
else dst = Mat(src, );
}
void imgResize(IplImage* src, Mat& dst, int height, int width) {
if (src->height != height || src->width != width) {
IplImage* reSizedMat = NULL;
CvSize ImgSize;
ImgSize.width = width;
ImgSize.height = height;
reSizedMat = cvCreateImage(ImgSize, src->depth, src->nChannels);
cvResize(src, reSizedMat, CV_INTER_AREA);
dst = Mat(reSizedMat, );
}
else dst = Mat(src, );
}
void imgResize(Mat& msrc, Mat& dst, int height, int width) {
IplImage _src(msrc);
IplImage* src = &_src;
if (src->height != height || src->width != width) {
IplImage* reSizedMat = NULL;
CvSize ImgSize;
ImgSize.width = width;
ImgSize.height = height;
reSizedMat = cvCreateImage(ImgSize, src->depth, src->nChannels);
cvResize(src, reSizedMat, CV_INTER_AREA);
dst = Mat(reSizedMat, );
}
else dst = Mat(src, );
} //定位并切割数字的精确位置
void getROI(Mat& src, Mat& dst, int binThreshold) {
size_t aimRGB = 0x00; //黑色0x00 白色0xff
int sx = , sy = , ex = src.rows, ey = src.cols;
for (int i = ; i != src.rows; i++) {
int avg = ;
for (int j = ; j != src.cols; j++) {
avg += src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[];
}
avg = avg / src.cols / ;
if (avg > aimRGB) {
sx = i;
break;
}
}
for (int i = ; i != src.cols; i++) {
int avg = ;
for (int j = ; j != src.rows; j++) {
avg += src.at<Vec3b>(j, i)[] + src.at<Vec3b>(j, i)[] + src.at<Vec3b>(j, i)[];
}
avg = avg / src.rows / ;
if (avg > aimRGB) {
sy = i;
break;
}
}
for (int i = src.rows - ; i != ; i--) {
int avg = ;
for (int j = ; j != src.cols; j++) {
avg += src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[];
}
avg = avg / src.cols / ;
if (avg > aimRGB) {
ex = i;
break;
}
}
for (int i = src.cols - ; i != ; i--) {
int avg = ;
for (int j = ; j != src.rows; j++) {
avg += src.at<Vec3b>(j, i)[] + src.at<Vec3b>(j, i)[] + src.at<Vec3b>(j, i)[];
}
avg = avg / src.rows / ;
if (avg > aimRGB) {
ey = i;
break;
}
}
dst = src(Range(sx, ex), Range(sy, ey));
} //获取文件
void getFile(string dirName, int num) {
static int sum = ;
string tmp;
string fileName;
string snum;
stringstream ss;
int cur = ;
tmp.clear();
ss << num; ss >> snum;
ifstream fileRead;
for (; cur <= nImgNum; cur++) {
i2s(tmp, cur);
fileName = dirNames[num] + snum + "_" + tmp + jpg;
fileRead.open(fileName);
if (!fileRead) {
break;
}
fileRead.close();
Mat sample = imread(fileName);
samples.push_back(sample);
#ifdef GETFILEONETIME
break;
#endif
}
cout << "number : " << num << ". Get samples. Total : " << samples.size() << endl << "Now training..." << endl;
} //求得特征向量组
void preTrain() {
int num = ;
for (int i = ; i != samples.size(); i++) {
if (i != && i % nImgNum == ) {
num++;
}
Mat trainImg = Mat::zeros(ImgWidth, ImgHeight, CV_8UC3);
resize(samples[i], trainImg, cv::Size(ImgWidth, ImgHeight), , , INTER_CUBIC);
HOGDescriptor *hog = new HOGDescriptor(
cvSize(ImgWidth, ImgHeight), cvSize(, ), cvSize(, ), cvSize(, ), bin);
descriptors.clear();
hog->compute(trainImg, descriptors, Size(, ), Size(, ));
int n = ;
for (vector<float>::iterator iter = descriptors.begin(); iter != descriptors.end(); iter++) {
dat_mat.at<float>(i, n++) = *iter;
}
res_mat.at<float>(i, ) = float(num);
}
} //读取mnist文件
int ReverseInt(int i) {
unsigned char ch1, ch2, ch3, ch4;
ch1 = i & ;
ch2 = (i >> ) & ;
ch3 = (i >> ) & ;
ch4 = (i >> ) & ;
return((int)ch1 << ) + ((int)ch2 << ) + ((int)ch3 << ) + ch4;
}
void read_Mnist(string filename, vector<cv::Mat> &vec) {
ifstream file(filename, ios::binary);
if (file.is_open()) {
int magic_number = ;
int number_of_images = ;
int n_rows = ;
int n_cols = ;
file.read((char*)&magic_number, sizeof(magic_number));
magic_number = ReverseInt(magic_number);
file.read((char*)&number_of_images, sizeof(number_of_images));
number_of_images = ReverseInt(number_of_images);
file.read((char*)&n_rows, sizeof(n_rows));
n_rows = ReverseInt(n_rows);
file.read((char*)&n_cols, sizeof(n_cols));
n_cols = ReverseInt(n_cols); for (int i = ; i < number_of_images; ++i) {
cv::Mat tp = cv::Mat::zeros(n_rows, n_cols, CV_8UC1);
for (int r = ; r < n_rows; ++r) {
for (int c = ; c < n_cols; ++c) {
unsigned char temp = ;
file.read((char*)&temp, sizeof(temp));
tp.at<uchar>(r, c) = (int)temp;
}
}
vec.push_back(tp);
}
}
}
void read_Mnist_Label(string filename, vector<int> &vec) {
ifstream file(filename, ios::binary);
if (file.is_open()) {
int magic_number = ;
int number_of_images = ;
int n_rows = ;
int n_cols = ;
file.read((char*)&magic_number, sizeof(magic_number));
magic_number = ReverseInt(magic_number);
file.read((char*)&number_of_images, sizeof(number_of_images));
number_of_images = ReverseInt(number_of_images); for (int i = ; i < number_of_images; ++i) {
unsigned char temp = ;
file.read((char*)&temp, sizeof(temp));
vec[i] = (int)temp;
}
}
}
void readTrained() {
samples.clear();
labels.clear();
dat_mat = Mat::zeros( * nImgNum, , CV_32FC1);
res_mat = Mat::zeros( * nImgNum, , CV_32FC1);
labels = vector<int>(nImgNum); string filename_train_images = "./mnist/train-images.idx3-ubyte";
string filename_train_labels = "./mnist/train-labels.idx1-ubyte"; read_Mnist(filename_train_images, samples);
read_Mnist_Label(filename_train_labels, labels);
if (samples.size() != labels.size()) {
cout << "parse MNIST train file error" << endl;
cout << samples.size() << " != " << labels.size() << endl;
exit(EXIT_FAILURE);
}
for (int i = ; i != nImgNum; i++) {
Mat trainImg = Mat::zeros(ImgWidth, ImgHeight, CV_8UC3);
resize(samples[i], trainImg, cv::Size(ImgWidth, ImgHeight), , , INTER_CUBIC);
HOGDescriptor *hog = new HOGDescriptor(
cvSize(ImgWidth, ImgHeight), cvSize(, ), cvSize(, ), cvSize(, ), bin);
descriptors.clear();
hog->compute(trainImg, descriptors, Size(, ), Size(, ));
int n = ;
for (vector<float>::iterator iter = descriptors.begin(); iter != descriptors.end(); iter++) {
dat_mat.at<float>(i, n++) = *iter;
}
res_mat.at<float>(i, ) = float(labels[i]);
}
cout << dat_mat.size() << endl;
knn.train(dat_mat, res_mat, Mat(), false, );
} //knn训练模块
void knnTrain() {
#ifdef SAVETRAINED
//knn training;
samples.clear();
dat_mat = Mat::zeros( * nImgNum, , CV_32FC1);
res_mat = Mat::zeros( * nImgNum, , CV_32FC1);
for (int i = ; i != ; i++) {
getFile(dirNames[i], i);
}
preTrain();
cout << "------ Training finished. -----" << endl << endl;
knn.train(dat_mat, res_mat, Mat(), false, ); #ifdef SAVEASTXT
cout << "Here are " << dat_mat.size().height << " eigenvectors. " << endl;
ofstream fileWrite("./trained/knnTrained.dat");
for (int i = ; i < dat_mat.size().height; i++) {
for (int j = ; j < dat_mat.size().width; j++) {
fileWrite << dat_mat.at<float>(i, j) << " ";
}
}
fileWrite.close(); fileWrite.open("./trained/result.dat");
for (int i = ; i < res_mat.size().height; i++) {
fileWrite << res_mat.at<float>(i, ) << " ";
}
#endif #else
readTrained();
#endif
} //用作准确度统计的knn测试
void selfknnTest() {
//knn test
cout << endl << "--- KNN test mode : ---" << endl;
int tCnt = ;
int tAc = ; const string testRootDir("./test/");
const string testDir[] = {
testRootDir + "0/", testRootDir + "1/", testRootDir + "2/",
testRootDir + "3/", testRootDir + "4/", testRootDir + "5/",
testRootDir + "6/", testRootDir + "7/", testRootDir + "8/",
testRootDir + "9/"
};
for (int i = ; i != ; i++) {
cout << "Now test : " << i << endl;
string tmp;
string fileName;
string snum;
stringstream ss;
int cur = ;
tmp.clear();
ss << i; ss >> snum;
ifstream fileRead;
for (; cur <= testNum; cur++) {
i2s(tmp, cur + );
fileName = testDir[i] + snum + "_" + tmp + jpg;
fileRead.open(fileName);
if (!fileRead) {
break;
}
fileRead.close(); Mat test_mat;
test_mat = imread(fileName); Mat testVec;
descriptors.clear();
testVec = Mat::zeros(, , CV_32FC1);
HOGDescriptor *hog = new HOGDescriptor(
cvSize(ImgWidth, ImgHeight), cvSize(, ), cvSize(, ), cvSize(, ), bin);
hog->compute(test_mat, descriptors, Size(, ), Size(, ));
int n = ;
for (vector<float>::iterator iter = descriptors.begin(); iter != descriptors.end(); iter++) {
testVec.at<float>(, n++) = float(*iter);
}
float result = knn.find_nearest(testVec, );
if (result == float(i)) {
tAc++;
}
//cout << result << endl;
//imshow("", test_mat);
//waitKey(); test_mat.~Mat();
}
} cout << endl << endl << "Total number of test samples : " << tCnt << endl; cout << "Accuracy : " << float(float(tAc) / float(tCnt)) * << "%" << endl;
} //识别模块
int recongnition(IplImage* src) {
Mat dst;
imgResize(src, dst, ImgHeight, ImgWidth);
Mat testVec;
descriptors.clear();
testVec = Mat::zeros(, , CV_32FC1);
HOGDescriptor *hog = new HOGDescriptor(
cvSize(ImgWidth, ImgHeight), cvSize(, ), cvSize(, ), cvSize(, ), bin);
hog->compute(dst, descriptors, Size(, ), Size(, ));
int n = ;
for (vector<float>::iterator iter = descriptors.begin(); iter != descriptors.end(); iter++) {
testVec.at<float>(, n++) = float(*iter);
}
float result = knn.find_nearest(testVec, );
return int(result);
//imshow("", dst);
//waitKey();
}
int recongnition(const string fileName) {
Mat dst;
imgResize(fileName, dst, ImgHeight, ImgWidth);
Mat testVec;
descriptors.clear();
testVec = Mat::zeros(, , CV_32FC1);
HOGDescriptor *hog = new HOGDescriptor(
cvSize(ImgWidth, ImgHeight), cvSize(, ), cvSize(, ), cvSize(, ), bin);
hog->compute(dst, descriptors, Size(, ), Size(, ));
int n = ;
for (vector<float>::iterator iter = descriptors.begin(); iter != descriptors.end(); iter++) {
testVec.at<float>(, n++) = float(*iter);
}
float result = knn.find_nearest(testVec, );
return int(result);
//imshow("", dst);
//waitKey();
}
int recongnition(Mat src) {
Mat dst;
imgResize(src, dst, ImgHeight, ImgWidth);
Mat testVec;
descriptors.clear();
testVec = Mat::zeros(, , CV_32FC1);
HOGDescriptor *hog = new HOGDescriptor(
cvSize(ImgWidth, ImgHeight), cvSize(, ), cvSize(, ), cvSize(, ), bin);
hog->compute(dst, descriptors, Size(, ), Size(, ));
int n = ;
for (vector<float>::iterator iter = descriptors.begin(); iter != descriptors.end(); iter++) {
testVec.at<float>(, n++) = float(*iter);
}
float result = knn.find_nearest(testVec, );
return int(result);
//imshow("", dst);
//waitKey();
} //去噪
typedef class ImgDenoising {
public:
ImgDenoising() = default;
ImgDenoising(Mat& ss, int nn, int mm, int bb) :
src(ss), n(nn), m(mm), binThreshold(bb) {
int dt = max(n, m);
G = new int*[dt];
for (int i = ; i < dt; i++) {
G[i] = new int[dt];
}
}
Mat src;
int** G;
int n, m, cnt, binThreshold;
bool ok(int i, int j) {
return G[i][j] == - && i > && j > && i < n && j < m;
}
void _dfs(int r, int c) {
static int dx[] = { , , , - };
static int dy[] = { , -, , };
if (G[r][c] == -) G[r][c] = cnt;
else return;
for (int i = ; i < ; i++) {
int x = r + dx[i];
int y = c + dy[i];
if (ok(x, y)) _dfs(x, y);
}
}
void imgDenoising() {
memset(G, -, sizeof(G));
cnt = ;
n = src.rows;
m = src.cols;
for (int i = ; i < n; i++) {
for (int j = ; j < m; j++) {
int rgb = (src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[]) / ;
if (rgb < binThreshold) G[i][j] = -;
}
}
for (int i = ; i < src.rows; i++) {
for (int j = ; j < src.cols; j++) {
if (G[i][j] == -) {
_dfs(i, j);
cnt++;
}
}
}
cout << cnt << endl;
}
}ImgDenoising; //连通域切分
const int inf = 0x7f7f;
bool ok(int** G, int n, int m, int i, int j) {
return G[i][j] == && i >= && j >= && i < n && j < m;
}
void _dfs(int** G, int n, int m, int r, int c, int cnt) {
static int dx[] = { , , , - };
static int dy[] = { , -, , };
G[r][c] = cnt;
for (int i = ; i < ; i++) {
int x = r + dx[i];
int y = c + dy[i];
if (ok(G, n, m, x, y)) _dfs(G, n, m, x, y, cnt);
}
}
int devideNum(Mat& src, int binThreshold, int** G) {
int n = src.rows;
int m = src.cols;
int dt = max(n, m);
int cnt = ;
G = new int*[dt];
for (int i = ; i < dt; i++) {
G[i] = new int[dt];
for (int j = ; j < m; j++) {
G[i][j] = inf;
}
}
for (int i = ; i < n; i++) {
for (int j = ; j < m; j++) {//试卷白底RGB=(0xff,0xff,0xff)
int rgb = (src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[] + src.at<Vec3b>(i, j)[]) / ;
if (rgb > binThreshold) G[i][j] = ;
}
}
for (int i = ; i < src.rows; i++) {
for (int j = ; j < src.cols; j++) {
if (G[i][j] == ) {
_dfs(G, n, m, i, j, cnt);
cnt++;
}
}
}
return cnt - ;
}
void saveNums(Mat& src, vector<Mat>& nums) { } //获取直线[rho,theta]
vector<Vec2f> getLines(const char* fileName) {
Mat src = cvLoadImage(fileName);
Mat img;
cvtColor(src, img, CV_RGB2GRAY);
GaussianBlur(img, img, Size(, ), , );
Canny(img, img, , , );
vector<Vec2f> lines, dre;
lines.clear(); dre.clear();
HoughLines(img, lines, 1.0, CV_PI / , , , );
sort(lines.begin(), lines.end(), [](Vec2f i, Vec2f j){
if (i.val[] == j.val[]) return i.val[] < j.val[];
return i.val[] < j.val[];
});
//去重复,排序后记下第一个,然后设定阈值。记录的值和遍历到的作差,小于阈值则不放入容器中。
double rho = lines[].val[];
double threshold = ;
dre.push_back(lines[]);
for (int i = ; i < lines.size(); i++) {
if (lines[i].val[] - rho <= threshold) continue;
dre.push_back(lines[i]); rho = lines[i].val[];
}
para.clear(); vert.clear();
//通过计算几何学的运算求出交点。
//分别丢入横线para和竖线vert容器中。
for (int i = ; i < dre.size(); i++) {
float rho = dre[i][], theta = dre[i][];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
pt1.x = cvRound(x0 + * (-b));
pt1.y = cvRound(y0 + * (a));
pt2.x = cvRound(x0 - * (-b));
pt2.y = cvRound(y0 - * (a));
//横线para: 1<=theta<=2 if (dre[i].val[] >= && dre[i].val[] <= ) para.push_back(Line(pt1, pt2));
else vert.push_back(Line(pt1, pt2));
}
sort(vert.begin(), vert.end(), [](Line p, Line q){ //调整竖线顺序
if (p.a.x == q.a.x) return p.a.y < q.a.y;
return p.a.x < q.a.x;
});
sort(para.begin(), para.end(), [](Line p, Line q){ //调整横线顺序
if (p.a.y == q.a.y) return p.a.x > q.a.x;
return p.a.x > q.a.y;
});
//imwrite("./data/houghlines.jpg", img);
//imshow("Y", img);
//waitKey();
return dre;
} //测试霍夫变换的函数
void houghTest(vector<Vec2f> lines, string picName) {
ofstream fileWrite("./data/lines.txt");
Mat dst = imread(picName);
//霍夫变换后,y=kx+b -> rho=x*cos(theta)+y*sin(theta)
for (size_t i = ; i < lines.size(); i++) {
float rho = lines[i][], theta = lines[i][];
fileWrite << rho << " " << theta << endl;
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
pt1.x = cvRound(x0 + * (-b));
pt1.y = cvRound(y0 + * (a));
pt2.x = cvRound(x0 - * (-b));
pt2.y = cvRound(y0 - * (a));
line(dst, pt1, pt2, Scalar(, , ), , CV_AA); }
imwrite("./data/withline.jpg", dst);
//imshow("A", dst);
//waitKey();
fileWrite.close();
} //获取学号,分数栏内多项信息。
void getMessage(Message& paperMessage, string picName, size_t binThreshold) {
vector<vector<Point>> cross; cross.clear(); //[para][vert]
// 霍夫线变换公式推导:
// 设直线 y = k*x+b
// k = cosθ / sinθ, b = ρ / sinθ
// 则直线: y = (cosθ / sinθ) * x + ρ / cosθ
// 化简: ρ = x * cosθ + y * sinθ
// 竖: ρ = y * sinθ, y = ρ / cosθ {θ=0}
// 横: ρ = x * cosθ, x = ρ / sinθ {θ=π/2} // 求横线竖线的笛卡尔积集,cross[i][j],横线i和竖线j交点
Mat dst;
dst = imread(picName);
for (int i = ; i < para.size(); i++) {
cross.push_back(vector<Point>());
for (int j = ; j < vert.size(); j++) {
//求交点
Point cp = getCross(para[i], vert[j]);
cross[i].push_back(cp);
}
}
//获取到十个分数外加总分的表格边界,后面需要处理线之间距离
//这里简单处理,假设图片完整,那么收集到的横竖线个数是一定的。排序列后找对应交点提取表格。
vector<Line> bound; bound.clear();
vector<Mat> sheet;
for (int i = ; i < para.size() - ; i++) {
int beginvert = ;
bound.push_back(Line(cross[i][beginvert], cross[i][beginvert + ]));
//line(dst, cross[i][beginvert], cross[i][beginvert+1], Scalar(0, 0, 255), 1, CV_AA);
//line(dst, cross[i][beginvert], cross[i+1][beginvert], Scalar(0, 0, 255), 1, CV_AA);
//line(dst, cross[i][beginvert+1], cross[i+1][beginvert+1], Scalar(0, 0, 255), 1, CV_AA);
}
for (int i = ; i < bound.size() - ; i++) {
Mat tmp = dst(Rect(
bound[i].a.x+,
bound[i].a.y+,
bound[i + ].b.x - bound[i].a.x-,
bound[i + ].b.y - bound[i].a.y-));
sheet.push_back(tmp);
}
imwrite("./data/pointed.jpg", dst); char t = '';
for (int i = ; i < sheet.size(); i++) {
//getROI(sheet[i], sheet[i], binThreshold);
imgResize(sheet[i], sheet[i], ImgHeight, ImgWidth);
int score = recongnition(sheet[i]);
paperMessage.score[i] = score;
paperMessage.sum += score;
imwrite(string(string("./data/tmp/sheet")+t+string(".jpg")), sheet[i]);
t++;
}
Mat tmp = dst(Rect(
bound[].a.x + ,
bound[].a.y + ,
bound[].b.x - bound[].a.x - ,
bound[].b.y - bound[].a.y - ));
cout << "Result :" << endl;
for (int i = ; i < ; i++) {
cout << "Problem ID: " << i + << ". " << "Score : " << paperMessage.score[i] << endl;
}
imgResize(tmp, tmp, , );
cout << "Sum : " << paperMessage.sum << endl;
imshow("", tmp);
waitKey();
} #ifdef MAIN_CPP_START
int main() {
knnTrain();
ifstream fileOpenTest;
while () {
string fileName;
cout << "Please input the image file name : ";
cin >> fileName;
fileOpenTest.open(fileName);
if (!fileOpenTest) {
cout << "This file doesn't exist. Please try again." << endl;
continue;
}
fileOpenTest.close();
IplImage* img = cvLoadImage(fileName.c_str());
int binThreshold = imageBinarization(img);
Mat src(img, ), dst;
blur(src, src, Size(, ));
//ImgDenoising idi(src, src.rows, src.cols, binThreshold);//在这里统计连通块数并且处理掉噪点
//idi.imgDenoising(); //图像连通域切分,结果保存在G中
int** G = NULL;
int comDom = devideNum(src, binThreshold, G);
getROI(src, dst, binThreshold);
imgResize(dst, dst, ImgHeight, ImgWidth);
cout << "It's number : " << recongnition(dst) << endl;
imshow("", dst);
waitKey();
} //knnTrain();
//selfknnTest();
return ;
} #else
int main() {
knnTrain();
//selfknnTest();
Mat src;
src = cvLoadImage("./data/qq.jpg");
size_t binThreshold = imageBinarization(src); string picName("./data/otsu.jpg");
houghTest(getLines(picName.c_str()), picName);
Message paperMessage;
getMessage(paperMessage, picName, binThreshold);
return ;
} #endif
现行的试卷封面并获取学生题目得分信息以及学号信息的原型系统
测试分类结果