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

前言

安装

需要安装两个依赖:

  • OpenCvSharp4
  • OpenCvSharp4.runtime.win

添加引用

using OpenCvSharp;
using Point = OpenCvSharp.Point;
using Rect = OpenCvSharp.Rect;

依赖扩展

  • OpenCvSharp4.Extensions

其中

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

操作步骤

常用操作

Mat和Bitmap互转

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

读取图片

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

保存

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

查看效果

方式1

本地保存图片

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

方式2

窗口打开图片

private void showImg(Mat img)
{
  Cv2.ImShow("win1", img);
  Cv2.WaitKey(0);
}

图片模式转换

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

复制

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

图片拼接

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

数据类型

  • U(unsigned integer)表示的是无符号整数,
  • S(signed integer)是有符号整数,
  • F(float)是浮点数

方式1

/// <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;
}

调用

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()拼接则要求待拼接图像有相同的宽度或高度

/// <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;
}

调用方式

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");

灰度

/// <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>
/// <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

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;
}

其中方向

public enum RotateFlags
{
  Rotate90Clockwise,//顺时针90
  Rotate180,//180
  Rotate90Counterclockwise//逆时针90
}

方式2

逆时针90

/// <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

/// <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

/// <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轴对称

枚举

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度会导致黑边和遮挡。

/// <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;
}

透视变形

获取黑块顶点

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));
    }
  }
}

透视变形

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;
    }
  }
}

调用

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

剪裁

// 截取左上角四分之一区域
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);

文件名

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;
  }
}

是否涂卡

/// <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;
}

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

绘制边框

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);

注意

黑白图片转为彩色

查找轮廓

实现框选用户选择的选项

/// <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;
}

调用方式

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);

获取面积

//获取涂写区域
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,我们只需要获取涂的百分比就能判断用户是否涂卡。

获取答题卡涂的选项

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

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。

/// <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

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

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 = 20)
    {
      // 忽略周围的像素

      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;
      int space = mat.Rows * 5 / 100;
      //获取定位块
      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 > 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);
        }
      }

      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

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;
        }
    }
}

查看代码执行时间

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);

计时实例可以使用多次

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);

版权声明:
作者:剑行者
链接:https://jkboy.com/archives/11402.html
来源:随风的博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
C#使用OpenCV进行答题卡识别
前言 安装 需要安装两个依赖: OpenCvSharp4 OpenCvSharp4.runtime.win 添加引用 using OpenCvSharp; using Point = OpenCvSharp.Point; using Rect = Open……
<<上一篇
下一篇>>