基于EmguCv的圆形答题卡识别

参考:https://blog.csdn.net/qiao_lili/article/details/83176480

网上关于C#结合EmguCv开发答题卡识别的资料很少,因为有做答题卡识别方面的需求,个人也只熟悉C#语言,找了半天也只找到一些通过C++、Python或matlab结合OpenCV来实现识别的例子,只好对着别人的例子尝试着翻译成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<Point, PointF>(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;
        }

/// <summary>
        /// 计算给定四个坐标点四边形的宽、高
        /// </summary>
        /// <param name="pts_src"></param>
        /// <returns></returns>
        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);
        }

/// <summary>
        /// Point转换为PointF类型
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        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);

/// <summary>
        /// 提取图中大于给定宽、高的轮廓
        /// </summary>
        /// <param name="mat_threshold">要提取轮廓的图片</param>
        /// <param name="width">轮廓外接矩形的宽</param>
        /// <param name="height">轮廓外接矩形的高</param>
        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);

/// <summary>
        /// 对给定的一些轮廓进行分类排序,返回分类排序后的二维数组
        /// </summary>
        /// <param name="selected_contours">要进行排序的轮廓</param>
        /// <param name="countOfRow">一行中轮廓的个数</param>
        /// <param name="countOfColumn">一列中轮廓的个数</param>
        /// <returns></returns>
        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);

/// <summary>
        /// 检测答题者的选项,获取涂选的结果数组
        /// </summary>
        /// <param name="mat_threshold">经阈值处理后的图像</param>
        /// <param name="classed_contours">经排序分类后的轮廓数组</param>
        /// <param name="countOfRow">一行中轮廓的个数</param>
        /// <param name="countOfColumn">一列中轮廓的个数</param>
        /// <returns></returns>
        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;

结果图如下: 

猜你喜欢

转载自blog.csdn.net/xjjatdna/article/details/85908405