参考这位大佬用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 = "";//显示步骤 1234
private static string showSuqIndex = "";//显示检测到的矩形下标(每个答题卡有很多框) 123
///
/// 匹配答案
///
/// 文件路径
/// 答案行数
/// 答案列数
/// 阈值Min可以默认给150
/// 阈值Max
/// 填涂框的高度 px
/// 填涂框的宽度 px
/// 填涂覆盖值,达到这个值才为选中
/// 返回数据
/// 选中数据
/// 阈值调整次数
///
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;
}
///
/// 寻找边界
///
///
///
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;
}
///
/// 对图像进行矫正(转为鸟瞰图,删除多余边界)
///
///
///
///
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;
else
pts[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;
}
///
/// 获取所有选项位置
///
/// 矫正和OTSU阈值分割后的mat
///
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;
}
///
/// 把选项分为有序的行列数据
///
/// 位置
/// 选项行数
/// 列数
///
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;
}
///
/// 检测答题者的选项,获取涂选的结果数组
///
/// 经阈值处理后的图像
/// 经排序分类后的轮廓数组
/// 一行中轮廓的个数
/// 一列中轮廓的个数
///
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++;
}
}
///
/// Point转换为PointF类型
///
///
///
public static Point2f PointToPointF(Point p)
{
return new Point2f(p.X, p.Y);
}