参考这位大佬用c++写的,和这位大佬用EmguCv写的,一步一步翻译成c#OpenCv

具体逻辑为
1.先读取到包裹选项的框(这里是把包裹选项的框的面积设为最大,再获取面积最大的框)
2.用选项的行数和列数获取第一步获取到的框中所有选项的位置(拿列来说,程序可以获取到x最小和x最大的选项,再把最大x和最小x的差除以列数,就可以得到选项之间的间隔,就能获取到每个选项的x,y也同理,所以也有每个选项间的x间隔和y间隔必须相同)
3.选项位置获取到了后,拿每个选项的xy获取到对应点的选项,再根据提供的选项宽高获取此点所在的矩形和矩形所填涂的面积,再根据提供的判断是否填涂参数对比来判断是否选中

限制:1.选项的宽高必须固定;2.图片分辨率最好为1200*1700左右;3.包裹选项的框的粗细要小于选项的框);4.包裹选项的框面积要最大(不最大的话要修改获取框的逻辑);5.扫描的图片不能太倾斜;

直接上代码

具体demo可以去这里拿 https://github.com/cstajj/Scann

调用示例

List<Point[]> selectOption;
int[,] resultArray;
MatchAnswer(fileTextBox.Text, int.Parse(rowTextBox.Text), int.Parse(celTextBox.Text), int.Parse(yzMinTextBox.Text), int.Parse(yzMaxTextBox.Text), int.Parse(heightTextBox.Text), int.Parse(widthTextBox.Text), int.Parse(ttTextBox.Text), out resultArray, out selectOption, out int fzfgCount);

原图

结果

引用

类库
OpenCvSharp4,版本为4.7.0.20230115
OpenCvSharp4.runtime.win ,版本为4.7.0.20230115

using OpenCvSharp;
using Point = OpenCvSharp.Point;
using Size = OpenCvSharp.Size;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Numerics;
using System.Security.Cryptography;
using System.Net.Http;
using System.IO;

方法

     private static int imgShowIndex = 1;private static string showIndex = "";//显示步骤 1234private static string showSuqIndex = "";//显示检测到的矩形下标(每个答题卡有很多框) 123/// <summary>/// 匹配答案/// </summary>/// <param name="path">文件路径</param>/// <param name="row">答案行数</param>/// <param name="column">答案列数</param>/// <param name="par1">阈值Min可以默认给150</param>/// <param name="par2">阈值Max</param>/// <param name="height">填涂框的高度 px</param>/// <param name="width">填涂框的宽度 px</param>/// <param name="judgeSize">填涂覆盖值,达到这个值才为选中</param>/// <param name="resultArray">返回数据</param>/// <param name="selectOption">选中数据</param>/// <param name="fzfgCount">阈值调整次数</param>/// <returns></returns>public static Mat MatchAnswer(string path, int? row, int? column, int? par1, int? par2, int? height, int? width, int? judgeSize, out int[,] resultArray, out List<Point[]> selectOption, out int fzfgCount){JudgeHasValueAndSet(row, 4);JudgeHasValueAndSet(column, 6);JudgeHasValueAndSet(par1, 150);JudgeHasValueAndSet(par2, 255);JudgeHasValueAndSet(height, 35);JudgeHasValueAndSet(width, 50);JudgeHasValueAndSet(judgeSize, 1500);resultArray = new int[,] { };selectOption = new List<Point[]>();Mat answerSheet = Cv2.ImRead(path);Point[] result_contour = GetBoundaryOfPic(answerSheet);Mat birdMat = WarpPerspective(answerSheet, result_contour);ShowImg(birdMat, "鸟瞰");//OTSU阈值分割Mat target = new Mat();int selectOptionCount = 0;List<Point[]> selected_contour = new List<Point[]>();fzfgCount = 0;//自动调整阈值,取到填涂框才继续while (selectOptionCount <= 1){if (fzfgCount >= 100)return null;if (fzfgCount > 0)par1--;fzfgCount++;Cv2.Threshold(birdMat, target, par1.Value, par2.Value, ThresholdTypes.BinaryInv);//修改thresh或maxval可以调整轮廓取值范围(调的不好会直接取外面的大轮廓)//ShowImg(target, "阈值分割");selected_contour = SelectedContour(target, height.Value, width.Value);selectOptionCount = selected_contour.Count();}selectOption = selected_contour;//3.验证结果Mat answerSheet_con = target.Clone();Cv2.CvtColor(answerSheet_con, answerSheet_con, ColorConversionCodes.GRAY2BGR);Cv2.DrawContours(answerSheet_con, selected_contour, -1, new Scalar(0, 0, 255), 2);ShowImg(answerSheet_con, "选项");List<Point>[,] classed_contours = ClassedOfContours(selected_contour, row.Value, column.Value);//5.绘制并验证List<Scalar> color = new List<Scalar>();color.Add(new Scalar(0, 0, 255));color.Add(new Scalar(255, 0, 255));color.Add(new Scalar(0, 255, 255));color.Add(new Scalar(255, 0, 0));color.Add(new Scalar(0, 255, 0));Mat groupMap = target.Clone();Cv2.CvtColor(groupMap, groupMap, ColorConversionCodes.GRAY2BGR);for (int i = 0; i < row; i++){List<List<Point>> tempGroupPoints = new List<List<Point>>();for (int j = 0; j < column; j++){if (classed_contours[i, j].Count > 0)tempGroupPoints.Add(classed_contours[i, j]);}if (tempGroupPoints.Count > 0)Cv2.DrawContours(groupMap, tempGroupPoints, -1, color[(i >= 5 ? i % 5 : i)], 2);}ShowImg(groupMap, "分组");//检测答题者的选项resultArray = GetResultArray(target, classed_contours, row.Value, column.Value, judgeSize.Value);Mat resultMat = new Mat();Cv2.CvtColor(target, resultMat, ColorConversionCodes.GRAY2BGR);List<List<Point>> tempPoints = new List<List<Point>>();for (int i = 0; i < row; i++){for (int j = 0; j < column; j++){if (resultArray[i, j] == 1){tempPoints.Add(classed_contours[i, j]);}}}Cv2.DrawContours(resultMat, tempPoints, -1, new Scalar(255, 0, 0), 2);ShowImg(resultMat, "结果");return resultMat;}public static void JudgeHasValueAndSet(int? value,int defaultValue = 0) {if (value == null)value = defaultValue;}/// <summary>/// 寻找边界/// </summary>/// <param name="mat"></param>/// <returns></returns>public static Point[] GetBoundaryOfPic(Mat mat, string size = "0,90,3,true"){//灰度转化Mat gray = new Mat();Cv2.CvtColor(mat, gray, ColorConversionCodes.RGB2GRAY);//进行高斯滤波Mat blurred = new Mat();Cv2.GaussianBlur(gray, blurred, new Size(3, 3), 0);//进行canny边缘检测Mat canny = new Mat();//Cv2.Canny(blurred, canny, 0, 180);string[] str = size.Split(",");Cv2.Canny(blurred, canny, int.Parse(str[0]), int.Parse(str[1]), int.Parse(str[2]), bool.Parse(str[3]));ShowImg(canny, "canny");//寻找矩形边界Point[][] contours;HierarchyIndex[] hierarchly;Cv2.FindContours(canny, out contours, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxSimple);//Cv2.FindContours(canny, out contours, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxNone);//Cv2.FindContours(canny, out contours, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxTC89KCOS);//Cv2.FindContours(canny, out contours, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxTC89L1);Point[] result_contour;if (contours.Length == 1){result_contour = contours[0];}else{double max = -1;int index = -1;for (int i = 0; i < contours.Length; i++){if (contours[i].Length < 4){continue;}double tem = Cv2.ArcLength(contours[i], true);bool pass = IsGreatArc(contours[i]);if (tem > max && pass){max = tem;index = i;}if (!string.IsNullOrEmpty(showSuqIndex)){Mat birdMat = WarpPerspective(mat, contours[i]);ShowImg2(birdMat, "鸟瞰" + i + "|" + tem.ToString("f2") + (pass ? "|通过" : ""));}}//取面积最大的矩形为检测填涂区域,所以在做答题卡时需要把填涂区的框做最大,或者自己调整,但上面的判断也需要一并调整result_contour = contours[index];}return result_contour;}class TempXY{public int X { get; set; }public int Y { get; set; }}public static bool IsGreatArc(Point[] contours){TempXY minXmaxY = new TempXY() { X = -1 };TempXY minXminY = new TempXY() { X = -1 };TempXY maxXmaxY = new TempXY() { X = -1 };TempXY maxXminY = new TempXY() { X = -1 };int minX = -1, minY = -1, maxX = -1, maxY = -1;foreach (var item in contours){if (item.X < minX || minX == -1)minX = item.X;if (item.X > maxX || maxX == -1)maxX = item.X;if (item.Y < minY || minY == -1)minY = item.Y;if (item.Y > maxY || maxY == -1)maxY = item.Y;}foreach (var item in contours){if (Math.Abs(item.X - minX) + Math.Abs(item.Y - maxY) < Math.Abs(minXmaxY.X - minX) + Math.Abs(minXmaxY.Y - maxY) || minXmaxY.X == -1){minXmaxY.X = item.X;minXmaxY.Y = item.Y;}if (Math.Abs(item.X - minX) + Math.Abs(item.Y - minY) < Math.Abs(minXminY.X - minX) + Math.Abs(minXminY.Y - minY) || minXminY.X == -1){minXminY.X = item.X;minXminY.Y = item.Y;}if (Math.Abs(item.X - maxX) + Math.Abs(item.Y - maxY) < Math.Abs(maxXmaxY.X - maxX) + Math.Abs(maxXmaxY.Y - maxY) || maxXmaxY.X == -1){maxXmaxY.X = item.X;maxXmaxY.Y = item.Y;}if (Math.Abs(item.X - maxX) + Math.Abs(item.Y - minY) < Math.Abs(maxXminY.X - maxX) + Math.Abs(maxXminY.Y - minY) || maxXminY.X == -1){maxXminY.X = item.X;maxXminY.Y = item.Y;}}if (Math.Abs(minXminY.X - minXmaxY.X) < 30 && Math.Abs(maxXminY.X - maxXmaxY.X) < 30 && minXmaxY.X > 5 && minXminY.Y > 5 && maxXminY.X - minXminY.X > 50 && minXmaxY.Y - minXminY.Y > 50){if (Math.Abs(minXminY.Y - maxXminY.Y) < 30 && Math.Abs(minXmaxY.Y - maxXmaxY.Y) < 30){return true;}}return false;}/// <summary>/// 对图像进行矫正(转为鸟瞰图,删除多余边界)/// </summary>/// <param name="mat"></param>/// <param name="result_contour"></param>/// <returns></returns>public static Mat WarpPerspective(Mat mat, Point[] result_contour){//使用DP算法拟合答题卡的几何轮廓,保存点集pts并顺时针排序double result_length = Cv2.ArcLength(result_contour, true);Point[] pts = Cv2.ApproxPolyDP(result_contour, result_length * 0.02, true);int width = 0;int height = 0;if (pts.Length == 4){if (pts[1].X < pts[3].X){//说明当前为逆时针存储,改为顺时针存储(交换第2、4点)Point p = new Point();p = pts[1];pts[1] = pts[3];pts[3] = p;}if (Math.Abs(pts[0].X - pts[3].X) > 100){Point temp = pts[pts.Length - 1];for (int i = pts.Length - 1; i >= 0; i--){if (i == 0)pts[i] = temp;elsepts[i] = pts[i - 1];}}//进行透视变换//1.确定变化尺寸的宽度float width1 = (pts[0].X - pts[1].X) * (pts[0].X - pts[1].X) + (pts[0].Y - pts[1].Y) * (pts[0].Y - pts[1].Y);float width2 = (pts[2].X - pts[3].X) * (pts[2].X - pts[3].X) + (pts[2].Y - pts[3].Y) * (pts[2].Y - pts[3].Y);width = width1 > width2 ? (int)Math.Sqrt(width1) : (int)Math.Sqrt(width2);//2.确定变化尺寸的高度float height1 = (pts[0].X - pts[3].X) * (pts[0].X - pts[3].X) + (pts[0].Y - pts[3].Y) * (pts[0].Y - pts[3].Y);float height2 = (pts[2].X - pts[1].X) * (pts[2].X - pts[1].X) + (pts[2].Y - pts[1].Y) * (pts[2].Y - pts[1].Y);height = height1 > height2 ? (int)Math.Sqrt(height1) : (int)Math.Sqrt(height2);}Point2f[] pts_src = Array.ConvertAll(pts.ToArray(), new Converter<Point, Point2f>(PointToPointF));Point2f[] pts_target = new Point2f[] { new Point2f(0, 0), new Point2f(width - 1, 0), new Point2f(width - 1, height - 1), new Point2f(0, height - 1) };//4.计算透视变换矩阵//4.1类型转化Mat data = Cv2.GetPerspectiveTransform(pts_src, pts_target);//5.进行透视变换Mat birdMat = new Mat();//进行透视操作Mat mat_Perspective = new Mat();Mat src_gray = new Mat();Cv2.CvtColor(mat, src_gray, ColorConversionCodes.BGR2GRAY);Cv2.WarpPerspective(src_gray, birdMat, data, new Size(width, height));return birdMat;}/// <summary>/// 获取所有选项位置/// </summary>/// <param name="target">矫正和OTSU阈值分割后的mat</param>/// <returns></returns>public static List<Point[]> SelectedContour(Mat target, int height, int width){//轮廓筛选//1.改善轮廓Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(1, 1));Cv2.Dilate(target, target, element);//ShowImg(target);//2.筛选轮廓Point[][] target_contour;List<Point[]> selected_contour = new List<Point[]>();HierarchyIndex[] hierarchly2;Cv2.FindContours(target, out target_contour, out hierarchly2, RetrievalModes.External, ContourApproximationModes.ApproxSimple);foreach (var m in target_contour){Rect rect = Cv2.BoundingRect(m);double k = (double)rect.Height / rect.Width;if (rect.Height > height && rect.Width > width && rect.Width < 100){selected_contour.Add(m);}}return selected_contour;}/// <summary>/// 把选项分为有序的行列数据/// </summary>/// <param name="selected_contour">位置</param>/// <param name="countOfRow">选项行数</param>/// <param name="countOfColumn">列数</param>/// <returns></returns>public static List<Point>[,] ClassedOfContours(List<Point[]> selected_contour, int countOfRow, int countOfColumn){//依据圆心的位置来确认答题卡轮廓的位置//1.计算所有外接圆基本数据float[] radius = new float[selected_contour.Count];Point2f[] center = new Point2f[selected_contour.Count];for (int i = 0; i < selected_contour.Count; i++){float radiusItem;Point2f centerItem;Cv2.MinEnclosingCircle(selected_contour[i], out centerItem, out radiusItem);//最小外接圆center[i] = centerItem;radius[i] = radiusItem;}//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 (Point2f 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) / (countOfColumn - 1);//答题卡每列x个,即x-1个间隔y_interval = (y_max - y_min) / (countOfRow - 1);//答题卡每行y个圆,即y-1个间隔//4.分类List<Point>[,] classed_contours = new List<Point>[countOfRow, countOfColumn];//初始化VectorOfVectorOfPoint二维数组for (int i = 0; i < countOfRow; i++){for (int j = 0; j < countOfColumn; j++){classed_contours[i, j] = new List<Point>();}}if (x_interval == 0 || y_interval == 0)return classed_contours;for (int i = 0; i < selected_contour.Count; i++){Point2f pf = center[i];int index_r = (int)Math.Round((pf.Y - y_min) / y_interval);//行号int index_c = (int)Math.Round((pf.X - x_min) / x_interval);//列号Point[] temp = selected_contour[i];classed_contours[index_r, index_c].AddRange(temp.ToList());}return classed_contours;}/// <summary>/// 检测答题者的选项,获取涂选的结果数组/// </summary>/// <param name="mat_threshold">经阈值处理后的图像</param>/// <param name="classed_contours">经排序分类后的轮廓数组</param>/// <param name="countOfRow">一行中轮廓的个数</param>/// <param name="countOfColumn">一列中轮廓的个数</param>/// <returns></returns>public static int[,] GetResultArray(Mat mat_threshold, List<Point>[,] classed_contours, int countOfRow, int countOfColumn, int judgeSize){int[,] result_count = new int[countOfRow, countOfColumn];//结果数组//统计所有答题圆圈外接矩形内非零像素个数Rect[,] re_rect = new Rect[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++){List<Point> countour = classed_contours[i, j];re_rect[i, j] = Cv2.BoundingRect(countour);Mat temp = new Mat(mat_threshold, re_rect[i, j]);//提取ROI矩形区域int count = Cv2.CountNonZero(temp);//计算图像内非零像素个数count_roi[i, j] = count;if (count > max_count) max_count = count;if (count < min_count) min_count = count;}}if (judgeSize > 0){max_count = judgeSize * 2;}//比对涂选的答案,以涂满圆圈一半以上为标准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;}public static void ShowImg(Mat mat, string name = ""){bool show = false;foreach (var item in showIndex){int i = int.Parse(item.ToString());int i2 = (int)item;if (imgShowIndex == int.Parse(item.ToString())){show = true;}}if (!show){imgShowIndex++;return;}Cv2.ImShow(!string.IsNullOrEmpty(name) ? name : imgShowIndex.ToString(), mat);imgShowIndex++;}private static int suqShowIndex = 1;public static void ShowImg2(Mat mat, string name = ""){bool show = false;if (showSuqIndex == "0"){Cv2.ImShow(name, mat);return;}else if (!string.IsNullOrEmpty(showSuqIndex)){foreach (var item in showSuqIndex){int i = int.Parse(item.ToString());int i2 = (int)item;if (suqShowIndex == int.Parse(item.ToString())){show = true;}}if (!show){suqShowIndex++;return;}Cv2.ImShow(!string.IsNullOrEmpty(name) ? name : suqShowIndex.ToString(), mat);suqShowIndex++;}}/// <summary>/// Point转换为PointF类型/// </summary>/// <param name="p"></param>/// <returns></returns>public static Point2f PointToPointF(Point p){return new Point2f(p.X, p.Y);}

C#使用OpenCV识别答题卡填涂区域(方形圆形都可)相关推荐

  1. servlet destroy 示例_KET答题卡怎么填写?2020年KET答题卡填涂示例

    KET答题卡怎么填写?2020年KET答题卡填涂示例哪里有?2020年KET考试改革了,改革力度不小,题型变了,答题卡自然也随之而变,备考的同学们也陆续开始刷题了,没有一份"与时俱进&quo ...

  2. 计算机网络应用答题卡,2020年计算机软件水平考试答题卡填涂技巧

    2020年计算机软件水平考试备考正在进行中,为了便于大家及时有效的备考,那么,小编为您精心整理了2020年计算机软件水平考试答题卡填涂技巧,欢迎大家的查阅.如想获取更多计算机软件水平考试的备考资料,请 ...

  3. Opencv识别答题卡

    转自:http://blog.csdn.net/cp562090732/article/details/47804003#comments OpenCV处理答题卡的软件,用于统计答题卡中选择题的得分. ...

  4. 计算机软考答题卡填涂格式,全国计算机软考网络工程师填涂答题卡须知

    唯学网小编提醒:如果你到现在还没有复习网络工程师考试,那么你必须从现在开始拟订一个学习计划起并且不间断地利用课余时间坚持7个月,一般教程用清华大学出版社的<网络工程师教程(第2版)>和&l ...

  5. PMP2019模拟答题卡填涂练习

  6. 基于Android和OpenCV的答题卡识别软件

    基于Android和OpenCV的答题卡识别软件 1. 软件介绍 设计目标是可以添加不同的考试,在不同考试下可以设置模板,包括题目数量.答题卡样式.每题分值以及每题答案:扫描结果按列表显示,并讲识别出 ...

  7. 使用 OpenCV-Python 识别答题卡判卷

    任务 识别用相机拍下来的答题卡,并判断最终得分(假设正确答案是B, E, A, D, B) 主要步骤 轮廓识别--答题卡边缘识别 透视变换--提取答题卡主体 轮廓识别--识别出所有圆形选项,剔除无关轮 ...

  8. usb摄像头识别答题卡系统

    最近使用QT+Mysql+OpenCv写了一个USB摄像头识别答题卡系统,可以把数据保存到数据库中并支持导出Excel文件和输出学生答题情况的日志,话不多说,下面介绍一下大概的流程,使用一个线程作为图 ...

  9. Opencv之答题卡识别判卷

    项目要求 提供一张答题卡图像,通过图像处理识别出答题卡上每个题的选项,与正确答案对比,得出分数并写在答题卡上. 代码实现过程 1.引入需要的库 import numpy as np import cv ...

  10. 基于MATLAB的答题卡识别方法

    基于MATLAB的答题卡识别方法 摘 要 背景: 随着科技的发展,电子与计算机技术的进步,答题卡的出现大大减轻教学工作者们批改试卷的工作量.答题卡是光标阅读机输入信息的载体,是配套光标阅读机的各种信息 ...

最新文章

  1. java常见_Java 常用类
  2. python中面向对象空间时间_python基础学习Day15 面向对象、类名称空间、对象名称空间 (2)...
  3. python中的魔术方法
  4. java ee程序设计师_软件设计师:Java EE开发四大常用框架[1]
  5. 软考初级程序员考试大纲
  6. 高等数学(第七版)同济大学 习题3-2 个人解答
  7. 怎么把unity游戏写进HTML,用 HTML代码加载 Unity 内容_Unity3d中文翻译用户手册-游戏蛮牛...
  8. 15.计算几何: 坐标值的精度【eps、sgn()、dcmp()】+ 平面上的点用struct表示 + 向量的定义与加减乘除
  9. obj文件(1):obj文件用txt打开并且了解v,f,vn,vt的含义
  10. 两手空空也创业 没钱照样做老板
  11. 前台传入数据后台被转义问题解决
  12. java pdf文件转换图片,pdf文件转图片(java实现)
  13. 计算机英语领域有哪些构词法,计算机专业英语的构词方法(共2969字).doc
  14. 2020年度英国商业大奖获奖名单公布
  15. LightMap烘焙技巧
  16. 年终总结没有思路?别怕,这里有一个万能写法
  17. MIUI10刷入Magisk模块——记小白的第一次刷机
  18. 二字动词 复盘赋能_互联网公司晋升必备的高级词汇
  19. LogisticRegressionCV作图
  20. java教程设计_Java教学设计方案.doc

热门文章

  1. 【backtrader与IB(盈透证券)实盘交易教程5】TWS API 与 IBPY应该使用哪一个?
  2. congratulation的用法_高考英语口语:Congratulations的口语用法
  3. 掘金社区签到青龙面板脚本
  4. 如何还原桌面图标_在Windows 7、8或10中还原丢失的桌面图标
  5. IntelliJ IDEA里pom.xml文件变成了橘黄色或者是变成了灰色
  6. linux服务器使用jupyter notebook时提示磁盘已满“Unexpected error while saving file:ipynb database or disk is full”
  7. Hyper-V Linux VM Disk扩容
  8. 解决老浏览器不支持ES6的方法
  9. 斯坦福辍学生创建的「匿名社交平台」|Fizz
  10. excel插入行 uipath_UIPath入门系列三之操作Excel表格