C#使用OpenCV进行答题卡识别

前言

安装

需要安装两个依赖:

  • OpenCvSharp4
  • OpenCvSharp4.runtime.win

添加引用

1
2
3
using OpenCvSharp;
using Point = OpenCvSharp.Point;
using Rect = OpenCvSharp.Rect;

依赖扩展

  • OpenCvSharp4.Extensions

image-20220811181059854

其中

OpenCvSharp4.Extensions 主要是一些辅助的工具 比如Mat和Bitmap的互转。

操作步骤

image-20220819155853340

常用操作

Mat和Bitmap互转

1
2
3
4
//Bitmap转Mat
Mat mat = OpenCvSharp.Extensions.BitmapConverter.ToMat(image);
//Mat转Bitmap
Bitmap bitmap = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(img8);

读取图片

1
2
3
4
5
6
private void readImg()
{
Mat img1 = new Mat("D:\\Pic\\0.jpg", ImreadModes.Color);
Cv2.ImShow("win1", img1);
Cv2.WaitKey(0);
}

保存

1
2
Mat img1 = new Mat("D:\\Pic\\0.jpg", ImreadModes.Color);
img1.ImWrite("D:\\Pic\\2.jpg");

查看效果

方式1

本地保存图片

1
Cv2.ImWrite("D:\\Pic\\3.jpg", img3);

方式2

窗口打开图片

1
2
3
4
5
private void showImg(Mat img)
{
Cv2.ImShow("win1", img);
Cv2.WaitKey(0);
}

图片模式转换

1
2
Mat img10 = new Mat();
Cv2.CvtColor(img7, img10, ColorConversionCodes.GRAY2RGB);

复制

1
2
Mat img2 = new Mat();
img1.CopyTo(img2);

图片拼接

type表示了矩阵中元素的类型以及矩阵的通道个数,它是一系列的预定义的常量,其命名规则为CV_(位数)+(数据类型)+(通道数),由type()返回,但是返回值是int型,不是OpenCV预定义的宏(CV_8UC1, CV_64FC1…),也就是说你用type函数得到的只是一个int型的数值,比如CV_8UC1返回的值是0,而不是CV_8UC1。

img

数据类型

  • U(unsigned integer)表示的是无符号整数,

  • S(signed integer)是有符号整数,

  • F(float)是浮点数

方式1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// Mat拼接
/// </summary>
/// <param name="matList"></param>
/// <returns></returns>
public static Mat jointMat(List<Mat> matList)
{
if (matList.Count == 0)
{
return new Mat();
}
int rows = 0;
int cols = 0;

for (int j = 0; j < matList.Count; j++)
{
Mat img_temp = matList[j];
rows += img_temp.Rows;
cols = Math.Max(cols, img_temp.Cols);
}
Mat result = new Mat(rows, cols, matList[0].Type(), new Scalar(255, 255, 255));

int tempRows = 0;
foreach (Mat itemMat in matList)
{
Mat roi = result[new Rect(0, tempRows, itemMat.Cols, itemMat.Rows)];
itemMat.CopyTo(roi);
tempRows += itemMat.Rows;
}
return result;
}

调用

1
2
3
4
5
List<Mat> mats = new List<Mat>();
mats.Add(new Mat("D:\\Pic\\0.jpg", ImreadModes.Color));
mats.Add(new Mat("D:\\Pic\\1.jpg", ImreadModes.Color));
var result = CvCommonUtils.jointMat(mats);
result.ImWrite("D:\\Pic\\2.jpg");

注意

不同色彩模式的图片不能正常合并,和目标图片的色彩模式也要保持一致,这里使用matList[0].Type()设置目标图的模式。

默认背景是纯黑色,这里new Scalar(255, 255, 255)使图片默认为纯白色。

方式2(不推荐)

使用VConcat()HConcat()拼接则要求待拼接图像有相同的宽度或高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// Mat拼接
/// </summary>
/// <param name="matList"></param>
/// <returns></returns>
public static Mat jointMat2(List<Mat> matList)
{
int rows = 0;
int cols = 0;

foreach (Mat itemMat in matList)
{
cols = Math.Max(cols, itemMat.Cols);
}
List<Mat> matListNew = new List<Mat>();
foreach (Mat itemMat in matList)
{
if (itemMat.Cols == cols)
{
matListNew.Add(itemMat);
rows += itemMat.Rows;
}
else
{
int rowsNew = cols * itemMat.Rows / itemMat.Cols;
Mat resultMat = new Mat();
Cv2.Resize(itemMat, resultMat, new Size(cols, rowsNew));
matListNew.Add(resultMat);
rows += resultMat.Rows;
}
}
Mat result = new Mat(rows, cols, MatType.CV_8UC3, new Scalar(255, 255, 255));

Cv2.VConcat(matListNew, result);
return result;
}

调用方式

1
2
3
4
5
List<Mat> mats = new List<Mat>();
mats.Add(new Mat("D:\\Pic\\0.jpg", ImreadModes.Color));
mats.Add(new Mat("D:\\Pic\\1.jpg", ImreadModes.Color));
var result = CvCommonUtils.jointMat2(mats);
result.ImWrite("D:\\Pic\\2.jpg");

灰度

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 灰度
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat gray(Mat source)
{
Mat resultMat = new Mat();
Cv2.CvtColor(source, resultMat, ColorConversionCodes.BGR2GRAY);
return resultMat;
}

二值化

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 二值化
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat binary(Mat source)
{
Mat resultMat = new Mat();
Cv2.Threshold(source, resultMat, 200, 255, ThresholdTypes.Binary);
return resultMat;
}

腐蚀与膨胀

腐蚀与膨胀都是针对白色区域的

  • 腐蚀 白色变少 黑色变多
  • 膨胀 白色变多 黑色减少

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// <summary>
/// 膨胀
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat dilation(Mat source)
{
Mat resultMat = new Mat(source.Rows, source.Cols, source.Type());
Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
Cv2.Dilate(source, resultMat, element);
return resultMat;
}

/// <summary>
/// 腐蚀
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat eroding(Mat source)
{
Mat resultMat = new Mat(source.Rows, source.Cols, source.Type());
Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
Cv2.Erode(source, resultMat, element);
return resultMat;
}

高斯模糊

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 高斯模糊
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat gaussianBlur(Mat source)
{
Mat resultMat = new Mat();
Cv2.GaussianBlur(source, resultMat, new OpenCvSharp.Size(11, 11), 4, 4);
return resultMat;
}

缩放

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// 图片缩放
/// </summary>
/// <param name="source"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static Mat resize(Mat source, int width, int height)
{
Mat resultMat = new Mat();
Cv2.Resize(source, resultMat, new Size(width, height));
return resultMat;
}

旋转

其中方式1和方式2都一样,都只能旋转90的倍数。

方式3可以旋转任意角度,但是如果是长方形就会部分无法显示。

所以

  • 旋转90的倍数推荐方式1
  • 旋转其他角度推荐方式3

方式1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Mat rotate90Counter(Mat source)
{
Mat resultMat = new Mat();
Cv2.Rotate(source, resultMat, RotateFlags.Rotate90Counterclockwise);
return resultMat;
}

public static Mat rotate90(Mat source)
{
Mat resultMat = new Mat();
Cv2.Rotate(source, resultMat, RotateFlags.Rotate90Clockwise);
return resultMat;
}

public static Mat rotate180(Mat source)
{
Mat resultMat = new Mat();
Cv2.Rotate(source, resultMat, RotateFlags.Rotate180);
return resultMat;
}

其中方向

1
2
3
4
5
6
public enum RotateFlags
{
Rotate90Clockwise,//顺时针90
Rotate180,//180
Rotate90Counterclockwise//逆时针90
}

方式2

逆时针90

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// 旋转
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat rotate90Counter(Mat source)
{
Mat resultMat = new Mat();
Mat tempMat = new Mat();
Cv2.Transpose(source, tempMat);
Cv2.Flip(tempMat, resultMat, FlipMode.X);
return resultMat;
}

顺时针90

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// 旋转
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat rotate90(Mat source)
{
Mat resultMat = new Mat();
Mat tempMat = new Mat();
Cv2.Transpose(source, tempMat);
Cv2.Flip(tempMat, resultMat, FlipMode.Y);
return resultMat;
}

旋转180

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 旋转
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat rotate180(Mat source)
{
Mat resultMat = new Mat();
Cv2.Flip(source, resultMat, FlipMode.XY);
return resultMat;
}

总结一下:

  • 需逆时针90°旋转时Transpose(src,tmp) + Flip(tmp,dst,0)
  • 需顺时针90°旋转时Transpose(src,tmp) + Flip(tmp,dst,1)

  • 需180°旋转时Flip(src,dst,-1)

Transpose()简单来说,就相当于数学中的转置,在矩阵中,转置就是把行与列相互调换位置;

相当于将图像逆时针旋转90度,然后再关于x轴对称

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum FlipMode
{
//
// 摘要:
// means flipping around x-axis
X = 0,
//
// 摘要:
// means flipping around y-axis
Y = 1,
//
// 摘要:
// means flipping around both axises
XY = -1
}

方式3

旋转任意角度

这种方式如果是长方向旋转90度会导致黑边和遮挡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 旋转
/// </summary>
/// <param name="source"></param>
/// <param name="angle">角度</param>
/// <returns></returns>
public static Mat rotate(Mat source, double angle = 90)
{
Mat resultMat = new Mat();
Point center = new Point(source.Cols / 2, source.Rows / 2); //旋转中心
double scale = 1.0; //缩放系数
Mat rotMat = Cv2.GetRotationMatrix2D(center, angle, scale);
Cv2.WarpAffine(source, resultMat, rotMat, source.Size());

return resultMat;
}

透视变形

获取黑块顶点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using OpenCvSharp;

using System;

namespace card_scanner.util
{
public class CvContoursUtils
{
/// <summary>
/// 获取四个顶点
/// </summary>
/// <param name="img"></param>
/// <returns></returns>
public static Point[] getAllPoints(Mat img)
{
Point[] potArr = new Point[4];
for (int i = 0; i < 4; i++)
{
potArr[i] = new Point(-1, -1);
}
// 距离四个角的距离
int[] spaceArr = new int[] { -1, -1, -1, -1 };
int cols = img.Cols;
int rows = img.Rows;
int x1 = cols / 3;
int x2 = cols * 2 / 3;
int y1 = rows / 3;
int y2 = rows * 2 / 3;
for (int x = 0; x < cols; x++)
{
for (int y = 0; y < rows; y++)
{
if (x > x1 && x < x2 && y > y1 && y < y2)
{
continue;
}

Vec3b color = img.Get<Vec3b>(y, x);

if (color != null && color.Item0 == 0)
{
if (spaceArr[0] == -1)
{
potArr[0].X = x;
potArr[0].Y = y;
potArr[1].X = x;
potArr[1].Y = y;
potArr[2].X = x;
potArr[2].Y = y;
potArr[3].X = x;
potArr[3].Y = y;
spaceArr[0] = getSpace(0, 0, x, y);
spaceArr[1] = getSpace(cols, 0, x, y);
spaceArr[2] = getSpace(cols, rows, x, y);
spaceArr[3] = getSpace(0, rows, x, y);
}
else
{
int s0 = getSpace(0, 0, x, y);
int s1 = getSpace(cols, 0, x, y);
int s2 = getSpace(cols, rows, x, y);
int s3 = getSpace(0, rows, x, y);
if (s0 < spaceArr[0])
{
spaceArr[0] = s0;
potArr[0].X = x;
potArr[0].Y = y;
}
if (s1 < spaceArr[1])
{
spaceArr[1] = s1;
potArr[1].X = x;
potArr[1].Y = y;
}
if (s2 < spaceArr[2])
{
spaceArr[2] = s2;
potArr[2].X = x;
potArr[2].Y = y;
}
if (s3 < spaceArr[3])
{
spaceArr[3] = s3;
potArr[3].X = x;
potArr[3].Y = y;
}
}
}
}
}
return potArr;
}

/// <summary>
/// 计算两点之间的距离
/// </summary>
/// <param name="x1"></param>
/// <param name="y1"></param>
/// <param name="x2"></param>
/// <param name="y2"></param>
/// <returns></returns>
private static int getSpace(int x1, int y1, int x2, int y2)
{
int xspace = Math.Abs(x1 - x2);
int yspace = Math.Abs(y1 - y2);
return (int)Math.Sqrt(Math.Pow(xspace, 2) + Math.Pow(yspace, 2));
}
}
}

透视变形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using OpenCvSharp;

using System.Collections.Generic;

namespace card_scanner.util
{
public class CvPerspectiveUtils
{
/// <summary>
/// 透视变换/顶点变换
/// </summary>
/// <param name="src"></param>
/// <param name="points"></param>
/// <returns></returns>
public static Mat warpPerspective(Mat src, Point[] points)
{
//设置原图变换顶点
List<Point2f> AffinePoints0 = new List<Point2f>() {
points[0],
points[1],
points[2],
points[3]
};
//设置目标图像变换顶点
List<Point2f> AffinePoints1 = new List<Point2f>() {
new Point(0, 0),
new Point(src.Width, 0),
new Point(src.Width, src.Height),
new Point(0, src.Height)
};
//计算变换矩阵
Mat Trans = Cv2.GetAffineTransform(AffinePoints0, AffinePoints1);
//矩阵仿射变换
Mat dst = new Mat();
Cv2.WarpAffine(src, dst, Trans, new OpenCvSharp.Size() { Height = src.Rows, Width = src.Cols });
return dst;
}
}
}

调用

1
2
3
4
//透视变形
var points = CvContoursUtils.getAllPoints(img5);
Mat img6 = CvPerspectiveUtils.warpPerspective(img5, points);
Cv2.ImWrite("D:\\Pic\\6_透视变形.jpg", img6);

剪裁

1
2
3
4
// 截取左上角四分之一区域
OpenCvSharp.Rect rect = new OpenCvSharp.Rect(0, 0, img2.Cols / 2, img2.Rows / 2);
Mat img4 = new Mat(img3, rect);
Cv2.ImWrite("D:\\Pic\\4.png", img4);

文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ZPathUtil
{
private static int temp = 100;

public static string GetPathJpeg(string basePath)
{
temp += 1;
if (temp > 1000)
{
temp = 101;
}

string filename = DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss_ffff") + "_" + temp + ".jpg";
string pathAll = Path.Combine(basePath, filename);
return pathAll;
}
}

是否涂卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
///区域是否涂卡
/// </summary>
/// <param name="source"></param>
/// <param name="rect"></param>
/// <returns></returns>
public static bool isSmearCard(Mat source, Rect rect)
{
Mat matTemp = new Mat(source, rect);
int count = Cv2.CountNonZero(matTemp);
int total = rect.Width * rect.Height;
double rate = 1.0f * (total - count) / total;
return rate > 0.3;
}

注意传入的原图一定要二值化。

绘制边框

1
2
3
4
5
Mat img10 = new Mat();
Cv2.CvtColor(img7, img10, ColorConversionCodes.GRAY2RGB);
Cv2.Rectangle(img10, posModel.cantronqrcode, new Scalar(0, 0, 255), 1);
Cv2.Rectangle(img10, posModel.pageRects[0], new Scalar(0, 0, 255), 1);
Cv2.ImWrite("D:\\Pic\\10_边框.png", img10);

注意

黑白图片转为彩色

查找轮廓

实现框选用户选择的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 轮廓识别,使用最外轮廓发抽取轮廓RETR_EXTERNAL,轮廓识别方法为CHAIN_APPROX_SIMPLE
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Point[][] findContours(Mat source)
{
Point[][] contours;
HierarchyIndex[] hierarchy;

Cv2.FindContours(
source,
out contours,
out hierarchy,
RetrievalModes.List,
ContourApproximationModes.ApproxSimple
);
return contours;
}

调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int rows = mat4.Rows;
int cols = mat4.Cols;
int space = mat4.Rows * 5 / 100;
//获取定位块
OpenCvSharp.Point[][] pointAll = CvContoursUtils.findContours(mat4);
List<OpenCvSharp.Point[]> rectVec2 = new List<OpenCvSharp.Point[]>();
for (int i = 0; i < pointAll.Length; i++)
{
OpenCvSharp.Point[] pointArr = pointAll[i];
Rect rect2 = Cv2.BoundingRect(pointArr);

if (rect2.Width > 10 && rect2.Height > 10 && rect2.Width < 60 && rect2.Height < 60 && (rect2.Left < space || rect2.Top < space || (cols - rect2.Right) < space || (rows - rect2.Bottom) < space))
{
rectVec2.Add(pointArr);
}
}

Mat img7 = new Mat(mat4.Rows, mat4.Cols, MatType.CV_8UC3, new Scalar(255, 255, 255));
Cv2.DrawContours(img7, rectVec2, -1, new Scalar(0, 0, 255), 1);
Cv2.ImWrite("D:\\Pic\\5_边框.jpg", img7);

获取面积

1
2
3
4
5
6
7
8
9
//获取涂写区域
Point[][] pointAll = CvContoursUtils.findContours(img6);
List<Point[]> rectVec2 = new List<Point[]>();
for (int i = 0; i < pointAll.Length; i++)
{
Point[] pointArr = pointAll[i];
double image_area = Cv2.ContourArea(pointArr);
Console.WriteLine(image_area);
}

其中Cv2.CountNonZero(matTemp)是获取非0的像素点个素数,所以在二值化的图片中,用户涂的区域都是0,我们只需要获取涂的百分比就能判断用户是否涂卡。

获取答题卡涂的选项

其中每个选项的坐标区域是在制作答题卡的时候,后台要保存的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int[][][] ques_select = new int[][][] {
new int[][]{
new int[] { 67,6,109,30},
new int[] { 134,6,169,30},
new int[] { 199,6,237,30},
},
new int[][]{
new int[] { 67,50,109,72},
new int[] { 134,50,169,72},
new int[] { 199,50,237,72},
},
new int[][]{
new int[] { 67,92,109,114},
new int[] { 134,92,169,114},
new int[] { 199,92,237,114},
},
new int[][]{
new int[] { 67,132,109,154},
new int[] { 134,132, 169,154},
new int[] { 199,132, 237,154},
},
new int[][]{
new int[] { 67,176,109,198},
new int[] { 134,176, 169,198},
new int[] { 199,176, 237,198},
},
};
string[] opts = new string[] { "A", "B", "C" };
for (int i = 0; i < ques_select.Length; i++)
{
int[][] ques = ques_select[i];
for (int j = 0; j < ques.Length; j++)
{
int[] opt = ques[j];
int width = opt[2] - opt[0];
int height = opt[3] - opt[1];
Mat matTemp = new Mat(img6, new Rect(opt[0], opt[1], width, height));
int count = Cv2.CountNonZero(matTemp);
int total = width * height;
double rate = 1.0f * (total - count) / total;
if (rate > 0.6)
{
Console.WriteLine("题号:" + (i + 1));
Console.WriteLine("选项:" + opts[j]);
Console.WriteLine("rate:" + rate);
}
}
}

页码识别

页面我们可以转换为二进制然后进行黑块渲染,识别的时候后在转成数字即可。这里是页码从1开始,所以要减1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// 获取页码数据
/// </summary>
/// <param name="pageMat"></param>
/// <returns></returns>
private int getPageNum(Mat pageMat)
{
string pagestr = "";
var cantronpage = posModel.cantronpage;
foreach (var page in cantronpage)
{
if (CvCommonUtils.isSmearCard(pageMat, page))
{
pagestr += "1";
}
else
{
pagestr += "0";
}
}

return Convert.ToInt32(pagestr, 2)-1;
}

工具类

基本操作

CvCommonUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
using OpenCvSharp;

using System;
using System.Collections.Generic;

namespace Z.OpenCV
{
public class CvCommonUtils
{
/// <summary>
/// Mat拼接
/// </summary>
/// <param name="matList"></param>
/// <returns></returns>
public static Mat jointMat(List<Mat> matList)
{
if (matList.Count == 0)
{
return new Mat();
}
int rows = 0;
int cols = 0;

for (int j = 0; j < matList.Count; j++)
{
Mat img_temp = matList[j];
rows += img_temp.Rows;
cols = Math.Max(cols, img_temp.Cols);
}
Mat result = new Mat(rows, cols, matList[0].Type(), new Scalar(255, 255, 255));

int tempRows = 0;
foreach (Mat itemMat in matList)
{
Mat roi = result[new Rect(0, tempRows, itemMat.Cols, itemMat.Rows)];
itemMat.CopyTo(roi);
tempRows += itemMat.Rows;
}
return result;
}

/// <summary>
/// Mat拼接
/// </summary>
/// <param name="matList"></param>
/// <returns></returns>
public static Mat jointMat2(List<Mat> matList)
{
int rows = 0;
int cols = 0;

foreach (Mat itemMat in matList)
{
cols = Math.Max(cols, itemMat.Cols);
}
List<Mat> matListNew = new List<Mat>();
foreach (Mat itemMat in matList)
{
if (itemMat.Cols == cols)
{
matListNew.Add(itemMat);
rows += itemMat.Rows;
}
else
{
int rowsNew = cols * itemMat.Rows / itemMat.Cols;
Mat resultMat = new Mat();
Cv2.Resize(itemMat, resultMat, new Size(cols, rowsNew));
matListNew.Add(resultMat);
rows += resultMat.Rows;
}
}
Mat result = new Mat(rows, cols, MatType.CV_8UC3, new Scalar(255, 255, 255));

Cv2.VConcat(matListNew, result);
return result;
}

/// <summary>
/// 图片缩放
/// </summary>
/// <param name="source"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static Mat resize(Mat source, int width, int height)
{
Mat resultMat = new Mat();
Cv2.Resize(source, resultMat, new Size(width, height));
return resultMat;
}

/// <summary>
/// 灰度
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat gray(Mat source)
{
Mat resultMat = new Mat();
Cv2.CvtColor(source, resultMat, ColorConversionCodes.BGR2GRAY);
return resultMat;
}

/// <summary>
/// 二值化
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat binary(Mat source)
{
Mat resultMat = new Mat();
Cv2.Threshold(source, resultMat, 200, 255, ThresholdTypes.Binary);
return resultMat;
}

/// <summary>
/// 膨胀
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat dilation(Mat source)
{
Mat resultMat = new Mat(source.Rows, source.Cols, source.Type());
Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
Cv2.Dilate(source, resultMat, element);
return resultMat;
}

/// <summary>
/// 腐蚀
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat eroding(Mat source)
{
Mat resultMat = new Mat(source.Rows, source.Cols, source.Type());
Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
Cv2.Erode(source, resultMat, element);
return resultMat;
}

/// <summary>
/// 高斯模糊
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat gaussianBlur(Mat source)
{
Mat resultMat = new Mat();
Cv2.GaussianBlur(source, resultMat, new OpenCvSharp.Size(11, 11), 4, 4);
return resultMat;
}

/// <summary>
/// 反转
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat bitwiseNot(Mat source)
{
Mat resultMat = new Mat();
Cv2.BitwiseNot(source, resultMat, new Mat());
return resultMat;
}

/// <summary>
/// 美颜磨皮 双边滤波
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat bilateralFilter(Mat source)
{
Mat resultMat = new Mat();
Cv2.BilateralFilter(source, resultMat, 15, 35d, 35d);
return resultMat;
}

/// <summary>
/// 逆时针旋转90
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat rotate90Counter(Mat source)
{
Mat resultMat = new Mat();
Cv2.Rotate(source, resultMat, RotateFlags.Rotate90Counterclockwise);
return resultMat;
}

/// <summary>
/// 顺时针旋转90
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Mat rotate90(Mat source)
{
Mat resultMat = new Mat();
Cv2.Rotate(source, resultMat, RotateFlags.Rotate90Clockwise);
return resultMat;
}

/// <summary>
///区域是否涂卡
/// </summary>
/// <param name="source"></param>
/// <param name="rect"></param>
/// <returns></returns>
public static bool isSmearCard(Mat source, Rect rect)
{
Mat matTemp = new Mat(source, rect);
int count = Cv2.CountNonZero(matTemp);
int total = rect.Width * rect.Height;
double rate = 1.0f * (total - count) / total;
return rate > 0.3;
}
}
}

获取边界

CvContoursUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
using OpenCvSharp;

using System;
using System.Collections.Generic;

namespace Z.OpenCV
{
public class CvContoursUtils
{
/// <summary>
/// 获取四个顶点
/// </summary>
/// <param name="img"></param>
/// <returns></returns>
public static Point[] getAllPoints(Mat img, int space = 10)
{
// 忽略周围的像素

Point[] potArr = new Point[4];
for (int i = 0; i < 4; i++)
{
potArr[i] = new Point(-1, -1);
}
// 距离四个角的距离
int[] spaceArr = new int[] { -1, -1, -1, -1 };
int cols = img.Cols;
int rows = img.Rows;
int x1 = cols / 3;
int x2 = cols * 2 / 3;
int y1 = rows / 3;
int y2 = rows * 2 / 3;
for (int x = 0; x < cols; x++)
{
for (int y = 0; y < rows; y++)
{
if (x > x1 && x < x2 && y > y1 && y < y2)
{
continue;
}

if (x < space || y < space || x > cols - space || y > rows - space)
{
continue;
}

Vec3b color = img.Get<Vec3b>(y, x);

if (color != null && color.Item0 == 0)
{
if (spaceArr[0] == -1)
{
potArr[0].X = x;
potArr[0].Y = y;
potArr[1].X = x;
potArr[1].Y = y;
potArr[2].X = x;
potArr[2].Y = y;
potArr[3].X = x;
potArr[3].Y = y;
spaceArr[0] = getSpace(0, 0, x, y);
spaceArr[1] = getSpace(cols, 0, x, y);
spaceArr[2] = getSpace(cols, rows, x, y);
spaceArr[3] = getSpace(0, rows, x, y);
}
else
{
int s0 = getSpace(0, 0, x, y);
int s1 = getSpace(cols, 0, x, y);
int s2 = getSpace(cols, rows, x, y);
int s3 = getSpace(0, rows, x, y);
if (s0 < spaceArr[0])
{
spaceArr[0] = s0;
potArr[0].X = x;
potArr[0].Y = y;
}
if (s1 < spaceArr[1])
{
spaceArr[1] = s1;
potArr[1].X = x;
potArr[1].Y = y;
}
if (s2 < spaceArr[2])
{
spaceArr[2] = s2;
potArr[2].X = x;
potArr[2].Y = y;
}
if (s3 < spaceArr[3])
{
spaceArr[3] = s3;
potArr[3].X = x;
potArr[3].Y = y;
}
}
}
}
}
return potArr;
}

/// <summary>
/// 获取四个顶点(优化版本)
/// </summary>
/// <param name="mat"></param>
/// <returns></returns>
public static Point[] getAllPoints2(Mat mat)
{
// 忽略周围的像素

Point[] potArr = new Point[4];
for (int i = 0; i < 4; i++)
{
potArr[i] = new Point(-1, -1);
}
// 距离四个角的距离
int[] spaceArr = new int[] { -1, -1, -1, -1 };

int rows = mat.Rows;
int cols = mat.Cols;
//获取定位块
Point[][] pointAll = findContours(mat);
List<Point[]> rectVec2 = new List<Point[]>();
for (int i = 0; i < pointAll.Length; i++)
{
Point[] pointArr = pointAll[i];
Rect rect2 = Cv2.BoundingRect(pointArr);

if (rect2.Width > 4 && rect2.Height > 4 && rect2.Width < 100 && rect2.Height < 100)
{
rectVec2.Add(pointArr);
}
}

foreach (Point[] points in rectVec2)
{
foreach (Point point in points)
{
int x = point.X;
int y = point.Y;
if (spaceArr[0] == -1)
{
potArr[0].X = x;
potArr[0].Y = y;
potArr[1].X = x;
potArr[1].Y = y;
potArr[2].X = x;
potArr[2].Y = y;
potArr[3].X = x;
potArr[3].Y = y;
spaceArr[0] = getSpace(0, 0, x, y);
spaceArr[1] = getSpace(cols, 0, x, y);
spaceArr[2] = getSpace(cols, rows, x, y);
spaceArr[3] = getSpace(0, rows, x, y);
}
else
{
int s0 = getSpace(0, 0, x, y);
int s1 = getSpace(cols, 0, x, y);
int s2 = getSpace(cols, rows, x, y);
int s3 = getSpace(0, rows, x, y);
if (s0 < spaceArr[0])
{
spaceArr[0] = s0;
potArr[0].X = x;
potArr[0].Y = y;
}
if (s1 < spaceArr[1])
{
spaceArr[1] = s1;
potArr[1].X = x;
potArr[1].Y = y;
}
if (s2 < spaceArr[2])
{
spaceArr[2] = s2;
potArr[2].X = x;
potArr[2].Y = y;
}
if (s3 < spaceArr[3])
{
spaceArr[3] = s3;
potArr[3].X = x;
potArr[3].Y = y;
}
}
}
}

return potArr;
}

/// <summary>
/// 计算两点之间的距离
/// </summary>
/// <param name="x1"></param>
/// <param name="y1"></param>
/// <param name="x2"></param>
/// <param name="y2"></param>
/// <returns></returns>
private static int getSpace(int x1, int y1, int x2, int y2)
{
int xspace = Math.Abs(x1 - x2);
int yspace = Math.Abs(y1 - y2);
return (int)Math.Sqrt(Math.Pow(xspace, 2) + Math.Pow(yspace, 2));
}

/// <summary>
/// 轮廓识别,使用最外轮廓发抽取轮廓RETR_EXTERNAL,轮廓识别方法为CHAIN_APPROX_SIMPLE
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static Point[][] findContours(Mat source)
{
Point[][] contours;
HierarchyIndex[] hierarchy;

Cv2.FindContours(
source,
out contours,
out hierarchy,
RetrievalModes.List,
ContourApproximationModes.ApproxSimple
);
return contours;
}
}
}

透视变形

CvPerspectiveUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using OpenCvSharp;

using System.Collections.Generic;

namespace card_scanner.util
{
public class CvPerspectiveUtils
{
/// <summary>
/// 透视变换/顶点变换
/// </summary>
/// <param name="src"></param>
/// <param name="points"></param>
/// <returns></returns>
public static Mat warpPerspective(Mat src, Point[] points)
{
//设置原图变换顶点
List<Point2f> AffinePoints0 = new List<Point2f>() {
points[0],
points[1],
points[2],
points[3]
};
//设置目标图像变换顶点
List<Point2f> AffinePoints1 = new List<Point2f>() {
new Point(0, 0),
new Point(src.Width, 0),
new Point(src.Width, src.Height),
new Point(0, src.Height)
};
//计算变换矩阵
Mat Trans = Cv2.GetAffineTransform(AffinePoints0, AffinePoints1);
//矩阵仿射变换
Mat dst = new Mat();
Cv2.WarpAffine(src, dst, Trans, new OpenCvSharp.Size() { Height = src.Rows, Width = src.Cols });
return dst;
}
}
}

查看代码执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Diagnostics;

//定义一个计时对象
System.Diagnostics.Stopwatch oTime = new System.Diagnostics.Stopwatch();
//开始计时
oTime.Start();

//测试的代码
// ...

//结束计时
oTime.Stop();

//输出运行时间。
Console.WriteLine("程序的运行时间:{0} 秒", oTime.Elapsed.TotalSeconds);
Console.WriteLine("程序的运行时间:{0} 毫秒", oTime.Elapsed.TotalMilliseconds);

计时实例可以使用多次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
System.Diagnostics.Stopwatch oTime = new System.Diagnostics.Stopwatch();
oTime.Start();
var result = CvContoursUtils.getAllPoints(mat4);
foreach (var point in result)
{
Console.WriteLine(point);
}
oTime.Stop();
Console.WriteLine("程序的运行时间:{0} 毫秒", oTime.Elapsed.TotalMilliseconds);

oTime.Start();
var result2 = CvContoursUtils.getAllPoints2(mat4);
foreach (var point in result2)
{
Console.WriteLine(point);
}
oTime.Stop();
Console.WriteLine("程序的运行时间:{0} 毫秒", oTime.Elapsed.TotalMilliseconds);