扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
参考:https://blog.csdn.net/qiao_lili/article/details/83176480
成都创新互联公司是一家专业提供临沧企业网站建设,专注与做网站、网站设计、成都h5网站建设、小程序制作等业务。10年已为临沧众多企业、政府机构等服务。创新互联专业网站制作公司优惠进行中。网上关于C#结合EmguCv开发答题卡识别的资料很少,因为有做答题卡识别方面的需求,个人也只熟悉C#语言,找了半天也只找到一些通过C++、Python或matlab结合OpenCV来实现识别的例子,只好对着别人的例子尝试着翻译成EmguCv,还算成功。自己又针对答题卡识别封装了几个函数,记录在这留给需要的人。(得吐槽下,EmguCv在错误管理上还欠缺很多,经常出现莫名其妙的错误)
1.设置两个图片显示容器ib_original.SizeMode = PictureBoxSizeMode.Zoom;
ib_original.FunctionalMode = Emgu.CV.UI.ImageBox.FunctionalModeOption.Minimum;
ib_result.SizeMode = PictureBoxSizeMode.Zoom;
ib_result.FunctionalMode = Emgu.CV.UI.ImageBox.FunctionalModeOption.Minimum;
2.载入要处理的图片OpenFileDialog op = new OpenFileDialog();
if (op.ShowDialog() == DialogResult.OK)
{
Mat src = new Mat(op.FileName, Emgu.CV.CvEnum.LoadImageType.AnyColor);
ib_original.Image = src;
}
实例图片
3.获取当前图像的大矩形边界//获取当前图像的大矩形边界
VectorOfVectorOfPoint result_contour = GetBoundaryOfPic(src);
public VectorOfVectorOfPoint GetBoundaryOfPic(Mat src)
{
Mat dst = new Mat();
Mat src_gray = new Mat();
CvInvoke.CvtColor(src, src_gray, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray);
//边缘检测
CvInvoke.Canny(src_gray, dst, 120, 180);
//寻找答题卡矩形边界(大的矩形)
VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint();//创建VectorOfVectorOfPoint数据类型用于存储轮廓
CvInvoke.FindContours(dst, contours, null, Emgu.CV.CvEnum.RetrType.External,
Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxSimple);//提取轮廓
VectorOfVectorOfPoint result_contour = new VectorOfVectorOfPoint();//用于存储筛选过后的轮廓
int ksize = contours.Size; //获取连通区域个数
if (ksize == 1)
{
result_contour = contours;
}
else
{
double maxLength = -1;//用于保存轮廓周长的大值
int index = -1;//轮廓周长的大值的序号
for (int i = 0; i< ksize; i++)
{
VectorOfPoint contour = contours[i];//获取独立的连通轮廓
double length = CvInvoke.ArcLength(contour, true);//计算连通轮廓的周长
if (length >maxLength)
{
maxLength = length;
index = i;
}
}
result_contour.Push(contours[index]);//筛选后的连通轮廓
}
return result_contour;
}
4.对图像进行矫正//对图像进行矫正
Mat mat_Perspective = MyWarpPerspective(src, result_contour);
public Mat MyWarpPerspective(Mat src, VectorOfVectorOfPoint result_contour)
{
//拟合答题卡的几何轮廓,保存点集pts并顺时针排序
VectorOfPoint pts = new VectorOfPoint();//用于存放逼近的结果
VectorOfPoint tempContour = result_contour[0];//临时用
double result_length = CvInvoke.ArcLength(tempContour, true);
CvInvoke.ApproxPolyDP(tempContour, pts, result_length * 0.02, true); //几何逼近,获取矩形4个顶点坐标
//Point[]转换为PointF[]类型
PointF[] pts_src = Array.ConvertAll(pts.ToArray(), new Converter(PointToPointF));
//确定透视变换的宽度、高度
Size sizeOfRect = CalSizeOfRect(pts_src);
int width= sizeOfRect.Width;
int height=sizeOfRect.Height;
//计算透视变换矩阵
PointF[] pts_target = new PointF[] { new PointF(0, 0), new PointF(width - 1, 0) ,
new PointF(width - 1, height - 1) ,new PointF(0, height - 1)};
//计算透视矩阵
Mat data = CvInvoke.GetPerspectiveTransform(pts_src, pts_target);
//进行透视操作
Mat mat_Perspective = new Mat();
Mat src_gray = new Mat();
CvInvoke.CvtColor(src, src_gray, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray);
CvInvoke.WarpPerspective(src_gray, mat_Perspective, data, new Size(width, height));
return mat_Perspective;
}
////// 计算给定四个坐标点四边形的宽、高
/// ////// public Size CalSizeOfRect(PointF[] pts_src)
{
if (pts_src.Length != 4) return new Size(0,0);//确保为四边形
if (pts_src[1].X< pts_src[3].X)
{
//说明当前为逆时针存储,改为顺时针存储(交换第2、4点)
PointF p = new PointF();
p = pts_src[1];
pts_src[1] = pts_src[3];
pts_src[3] = p;
}
//确定透视变换的宽度、高度
int width;
int height;
double width1 = Math.Pow(pts_src[0].X - pts_src[1].X, 2) + Math.Pow(pts_src[0].Y - pts_src[1].Y, 2);
double width2 = Math.Pow(pts_src[2].X - pts_src[3].X, 2) + Math.Pow(pts_src[2].Y - pts_src[3].Y, 2);
width = width1 >width2 ? (int)Math.Sqrt(width1) : (int)Math.Sqrt(width2);//根号下a方+b方,且取宽度大的
double height1 = Math.Pow(pts_src[0].X - pts_src[3].X, 2) + Math.Pow(pts_src[0].Y - pts_src[3].Y, 2);
double height2 = Math.Pow(pts_src[1].X - pts_src[2].X, 2) + Math.Pow(pts_src[1].Y - pts_src[2].Y, 2);
height = height1 >height2 ? (int)Math.Sqrt(height1) : (int)Math.Sqrt(height2);
return new Size(width, height);
}
////// Point转换为PointF类型
/// ////// public static PointF PointToPointF(Point p)
{
return new PointF(p.X, p.Y);
}
5.阈值分割//阈值分割
Mat mat_threshold = new Mat();
CvInvoke.Threshold(mat_Perspective, mat_threshold, 160, 255, Emgu.CV.CvEnum.ThresholdType.BinaryInv);
6.获取符合标准的圆形轮廓//获取符合标准的圆形轮廓
VectorOfVectorOfPoint selected_contours =GetContoursAboveGivenSize(mat_threshold, 20, 20);
////// 提取图中大于给定宽、高的轮廓
/// ///要提取轮廓的图片///轮廓外接矩形的宽///轮廓外接矩形的高public VectorOfVectorOfPoint GetContoursAboveGivenSize(Mat mat_threshold, int width, int height)
{
//轮廓筛选
//1.膨胀,改善轮廓
Mat struct_element = CvInvoke.GetStructuringElement(Emgu.CV.CvEnum.ElementShape.Cross,
new Size(3, 3), new Point(-1, -1));//结构元素
Mat mat_dilate = new Mat();
CvInvoke.MorphologyEx(mat_threshold, mat_dilate, Emgu.CV.CvEnum.MorphOp.Dilate, struct_element, new Point(-1, -1), 1,
Emgu.CV.CvEnum.BorderType.Default, new MCvScalar(0, 0, 0));//形态学膨胀
//2.筛选轮廓。筛选条件:宽度和高度同时大于20
VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint();//所有的轮廓
VectorOfVectorOfPoint selected_contours = new VectorOfVectorOfPoint();//用于存储筛选过后的圆形轮廓
Mat mat_dilate_clone = mat_dilate.Clone();//克隆
CvInvoke.FindContours(mat_dilate_clone, contours, null, Emgu.CV.CvEnum.RetrType.External,
Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxSimple);//提取轮廓,操作过程中会对输入图像进行修改
//选取外接矩形宽、高要同时大于给定标准的轮廓
for (int i = 0; i< contours.Size; i++)
{
Rectangle rect = CvInvoke.BoundingRectangle(contours[i]);//外接矩形
if (rect.Width >width && rect.Height >height)
{
selected_contours.Push(contours[i]);
}
}
return selected_contours;
}
7.对轮廓进行分类排序,获取分类排序后的二维数组//对轮廓进行分类排序,获取分类排序后的二维数组
VectorOfVectorOfPoint[,] classed_contours = ClassedOfContours(selected_contours, 5, 5);
////// 对给定的一些轮廓进行分类排序,返回分类排序后的二维数组
/// ///要进行排序的轮廓///一行中轮廓的个数///一列中轮廓的个数/// public VectorOfVectorOfPoint[,] ClassedOfContours(VectorOfVectorOfPoint selected_contours, int countOfRow, int countOfColumn)
{
//依据圆心的位置来确认答题卡轮廓的位置
//1.计算所有外接圆基本数据
float[] radius = new float[selected_contours.Size];
PointF[] center = new PointF[selected_contours.Size];
for (int i = 0; i< selected_contours.Size; i++)
{
CircleF circleF = CvInvoke.MinEnclosingCircle(selected_contours[i]);//最小外接圆
center[i] = circleF.Center;
radius[i] = circleF.Radius;
}
//2.计算x轴、y轴分割间隔
float x_min = 999, y_min = 999;
float x_max = -1, y_max = -1;
float x_interval = 0, y_interval = 0;//相邻圆心的间距
foreach (PointF pf in center)
{
//获取所有圆心中的坐标最值
if (pf.X< x_min) x_min = pf.X;
if (pf.X >x_max) x_max = pf.X;
if (pf.Y< y_min) y_min = pf.Y;
if (pf.Y >y_max) y_max = pf.Y;
}
x_interval = (x_max - x_min) / (countOfRow - 1);//答题卡每行5个圆,即4个间隔
y_interval = (y_max - y_min) / (countOfColumn - 1);//答题卡每列5个圆,即4个间隔
//4.分类
VectorOfVectorOfPoint[,] classed_contours = new VectorOfVectorOfPoint[countOfRow, countOfColumn];
//初始化VectorOfVectorOfPoint二维数组
for (int i = 0; i< 5; i++)
{
for (int j = 0; j< 5; j++)
{
classed_contours[i, j] = new VectorOfVectorOfPoint();
}
}
for (int i = 0; i< selected_contours.Size; i++)
{
PointF pf = center[i];
int index_x = (int)Math.Round((pf.X - x_min) / x_interval);
int index_y = (int)Math.Round((pf.Y - y_min) / y_interval);
VectorOfPoint temp = selected_contours[i];
classed_contours[index_x, index_y].Push(temp);
}
return classed_contours;
}
8.检测答题者的选项//检测答题者的选项
int[,] result_count = GetResultArray(mat_threshold, classed_contours, 5, 5);
////// 检测答题者的选项,获取涂选的结果数组
/// ///经阈值处理后的图像///经排序分类后的轮廓数组///一行中轮廓的个数///一列中轮廓的个数/// public int[,] GetResultArray(Mat mat_threshold,VectorOfVectorOfPoint[,] classed_contours, int countOfRow, int countOfColumn)
{
int[,] result_count = new int[countOfRow, countOfColumn];//结果数组
//统计所有答题圆圈外接矩形内非零像素个数
Rectangle[,] re_rect = new Rectangle[countOfRow, countOfColumn];//外接矩形数组
int[,] count_roi = new int[countOfRow, countOfColumn];//外接矩形内非零像素个数
int min_count = 999;//非零像素个数大值,作为已涂选的参照
int max_count = -1;//非零像素个数最小值,作为未涂选的参照
for (int i = 0; i< countOfRow; i++)
{
for (int j = 0; j< countOfColumn; j++)
{
VectorOfPoint countour = classed_contours[i, j][0];
re_rect[i, j] = CvInvoke.BoundingRectangle(countour);
Mat temp = new Mat(mat_threshold, re_rect[i, j]);//提取ROI矩形区域
int count = CvInvoke.CountNonZero(temp);//计算图像内非零像素个数
count_roi[i, j] = count;
if (count >max_count)max_count = count;
if (count< min_count)min_count = count;
}
}
//比对涂选的答案,以涂满圆圈一半以上为标准
for (int i = 0; i< countOfRow; i++)
{
for (int j = 0; j< countOfColumn; j++)
{
if (count_roi[i, j] >max_count / 2)
{
result_count[i, j]=1;
}
}
}
return result_count;
}
9. 标示出答题者的选项//标示出答题者的选项
Mat temp_mat = new Mat();
CvInvoke.CvtColor(mat_Perspective, temp_mat, Emgu.CV.CvEnum.ColorConversion.Gray2Bgr);
for (int i = 0; i< 5; i++)
{
for (int j = 0; j< 5; j++)
{
if (result_count[i,j]==1)
{
CvInvoke.DrawContours(temp_mat, classed_contours[i,j], -1, new MCvScalar(255, 0, 0), 2);
}
}
}
ib_result.Image = temp_mat;
结果图如下:
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流