opencv +数字识别

2023-11-11

现在很多场景需要使用的数字识别,比如银行卡识别,以及车牌识别等,在AI领域有很多图像识别算法,大多是居于opencv 或者谷歌开源的tesseract 识别.

由于公司业务需要,需要开发一个客户端程序,同时需要在xp这种老古董的机子上运行,故研究了如下几个数字识别方案:

ocr 识别的不同选择方案

  • tesseract
    • 放弃:谷歌的开源tesseract ocr识别目前最新版本不支持xp系统
  • 云端ocr 识别接口(不适用)
    • 费用比较贵:
    • 场景不同,我们的需求是可能毫秒级别就需要调用一次ocr 识别
  • opencv
  • 概念:OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。

以上几种ocr 识别比较,最后选择了opencv 的方式进行ocr 数字识别,下面讲解通过ocr识别的基本流程和算法.

opencv 数字识别流程及算法解析

要通过opencv 进行数字识别离不开训练库的支持,需要对目标图片进行大量的训练,才能做到精准的识别出目标数字;下面我会分别讲解图片训练的过程及识别的过程.

opencv 识别算法原理
  1. 比如下面一张图片,需要从中识别出正确的数字,需要对图片进行灰度、二值化、腐蚀、膨胀、寻找数字轮廓、切割等一系列操作.

原图

image

灰度化图

image

二值化图

image

寻找轮廓

image

识别后的结果图

image

以上就是简单的图片进行灰度化、二值化、寻找数字轮廓得到的识别结果(这是基于我之前训练过的数字模型下得到的识别结果)
有些图片比较赋值,比如存在背景斜杠等的图片则需要一定的腐蚀或者膨胀等处理,才能寻找到正确的数字轮廓.

上面的说到我这里使用的是opencv 图像处理库进行的ocr 识别,那我这里简单介绍下C# 怎么使用opencv 图像处理看;

为了在xp上能够运行 我这里通过nuget 包引用了 OpenCvSharp-AnyCPU 第三方库,它使用的是opencv 2410 版本,你们如果不考虑xp系统的情况下开源使用最新的版本,最新版本支持了更多的识别算法.

右击你的个人项目,选择“管理Nuget程序包”。在包管理器页面中,点击“浏览”选项,然后在搜索框中键入“OpenCvSharp-AnyCPU”。选择最顶端的正确项目,并在右侧详情页中点击“安装”,等待安装完成即可。

以上的核心代码如下:

      private void runSimpleOCR(string pathName)
       {
            //构造opcvOcr 库,这里的是我单独对opencv 库进行的一次封装,加载训练库模板
            var opencvOcr = new OpencvOcr($"{path}Template\\Traindata.xml", opencvOcrConfig: new OCR.Model.OpencvOcrConfig()
            {
                ErodeLevel = 2.5,
                ThresholdType = OpenCvSharp.ThresholdType.Binary,
                ZoomLevel = 2,
            });

            var img = new Bitmap(this.txbFilaName.Text);

            var mat = img.ToMat();
            
            //核心识别方法
            var str = opencvOcr.GetText(mat, isDebug: true);
            this.labContent.Content = str;
        }

opencvOcr 的核心代码如下


        #region Constructor

        const double Thresh = 80;
        const double ThresholdMaxVal = 255;
        const int _minHeight = 35;
        bool _isDebug = false;
        CvKNearest _cvKNearest = null;
        OpencvOcrConfig _config = new OpencvOcrConfig() { ZoomLevel = 2, ErodeLevel = 3 };
        #endregion

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="path">训练库完整路径</param>
        /// <param name="opencvOcrConfig">OCR相关配置信息</param>
        public OpencvOcr(string path, OpencvOcrConfig opencvOcrConfig = null)
        {
            if (string.IsNullOrEmpty(path))
                throw new ArgumentNullException("path is not null");

            if (opencvOcrConfig != null)
                _config = opencvOcrConfig;

            this.LoadKnearest(path);
        }
        
        /// <summary>
        /// 加载Knn 训练库模型
        /// </summary>
        /// <param name="dataPathFile"></param>
        /// <returns></returns>
        private CvKNearest LoadKnearest(string dataPathFile)
        {
            if (_cvKNearest == null)
            {

                using (var fs = new FileStorage(dataPathFile, FileStorageMode.Read))
                {
                    var samples = fs["samples"].ReadMat();
                    var responses = fs["responses"].ReadMat();
                    this._cvKNearest = new CvKNearest();
                    this._cvKNearest.Train(samples, responses);
                }
            }
            return _cvKNearest;
        }

        /// <summary>
        /// OCR 识别,仅仅只能识别单行数字 
        /// </summary>
        /// <param name="kNearest">训练库</param>
        /// <param name="path">要识别的图片路径</param>
        public override string GetText(Mat src, bool isDebug = false)
        {
            this._isDebug = isDebug;

            #region 图片处理
            var respMat = MatProcessing(src, isDebug);
            if (respMat == null)
                return "";
            #endregion

            #region 查找轮廓
            var sortRect = FindContours(respMat.FindContoursMat);
            #endregion

            return GetText(sortRect, respMat.ResourcMat, respMat.RoiResultMat);
        }
        
         /// <summary>
        /// 查找轮廓
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private List<Rect> FindContours(Mat src)
        {
            try
            {
                #region 查找轮廓
                Point[][] contours;
                HierarchyIndex[] hierarchyIndexes;
                Cv2.FindContours(
                    src,
                    out contours,
                    out hierarchyIndexes,
                    mode: OpenCvSharp.ContourRetrieval.External,
                    method: OpenCvSharp.ContourChain.ApproxSimple);

                if (contours.Length == 0)
                    throw new NotSupportedException("Couldn't find any object in the image.");
                #endregion

                #region 单行排序(目前仅仅支持单行文字,多行文字顺序可能不对,按照x坐标进行排序)
                var sortRect = GetSortRect(contours, hierarchyIndexes);
                sortRect = sortRect.OrderBy(item => item.X).ToList();
                #endregion

                return sortRect;
            }
            catch { }

            return null;
        }
        
        /// <summary>
        /// 获得切割后的数量列表
        /// </summary>
        /// <param name="contours"></param>
        /// <param name="hierarchyIndex"></param>
        /// <returns></returns>
        private List<Rect> GetSortRect(Point[][] contours, HierarchyIndex[] hierarchyIndex)
        {
            var sortRect = new List<Rect>();

            var _contourIndex = 0;
            while ((_contourIndex >= 0))
            {
                var contour = contours[_contourIndex];
                var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour

                sortRect.Add(boundingRect);
                _contourIndex = hierarchyIndex[_contourIndex].Next;
            }
            return sortRect;
        }


        /// <summary>
        /// 是否放大
        /// </summary>
        /// <param name="src"></param>
        /// <returns></returns>
        private bool IsZoom(Mat src)
        {
            if (src.Height <= _minHeight)
                return true;

            return false;
        }
        

        private List<EnumMatAlgorithmType> GetAlgoritmList(Mat src)
        {
            var result = new List<EnumMatAlgorithmType>();
            var algorithm = this._config.Algorithm;

            #region 自定义的算法
            try
            {
                if (algorithm.Contains("|"))
                {
                    result = algorithm.Split('|').ToList()
                        .Select(item => (EnumMatAlgorithmType)Convert.ToInt32(item))
                        .ToList();

                    if (!IsZoom(src))
                        result.Remove(EnumMatAlgorithmType.Zoom);

                    return result;
                }
            }
            catch { }

            #endregion

            #region 默认算法
            if (IsZoom(src))
            {
                result.Add(EnumMatAlgorithmType.Zoom);
            }
            if (this._config.ThresholdType == ThresholdType.Binary)
            {
                //result.Add(EnumMatAlgorithmType.Blur);

                result.Add(EnumMatAlgorithmType.Gray);
                result.Add(EnumMatAlgorithmType.Thresh);
                if (this._config.DilateLevel > 0)
                    result.Add(EnumMatAlgorithmType.Dilate);

                result.Add(EnumMatAlgorithmType.Erode);
                return result;
            }
            //result.Add(EnumMatAlgorithmType.Blur);

            result.Add(EnumMatAlgorithmType.Gray);
            result.Add(EnumMatAlgorithmType.Thresh);
            if (this._config.DilateLevel > 0)
                result.Add(EnumMatAlgorithmType.Dilate);

            result.Add(EnumMatAlgorithmType.Erode);
            return result;
            #endregion
        }


        /// <summary>
        /// 对查找的轮廓数据进行训练模型匹配,这里使用的是KNN 匹配算法
        /// </summary>
        private string GetText(List<Rect> sortRect, Mat source, Mat roiSource)
        {
            var response = "";
            try
            {
                if ((sortRect?.Count ?? 0) <= 0)
                    return response;

                var contourIndex = 0;
                using (var dst = new Mat(source.Rows, source.Cols, MatType.CV_8UC3, Scalar.All(0)))
                {
                    sortRect.ForEach(boundingRect =>
                    {
                        try
                        {
                            #region 绘制矩形
                            if (this._isDebug)
                            {
                                Cv2.Rectangle(source, new Point(boundingRect.X, boundingRect.Y),
                                new Point(boundingRect.X + boundingRect.Width, boundingRect.Y + boundingRect.Height),
                                new Scalar(0, 0, 255), 1);

                                Cv2.Rectangle(roiSource, new Point(boundingRect.X, boundingRect.Y),
                                   new Point(boundingRect.X + boundingRect.Width, boundingRect.Y + boundingRect.Height),
                                   new Scalar(0, 0, 255), 1);
                            }
                            #endregion

                            #region 单个ROI
                            var roi = roiSource.GetROI(boundingRect); //Crop the image
                            roi = roi.Compress();
                            var result = roi.ConvertFloat();
                            #endregion

                            #region KNN 匹配
                            var results = new Mat();
                            var neighborResponses = new Mat();
                            var dists = new Mat();
                            var detectedClass = (int)this._cvKNearest.FindNearest(result, 1, results, neighborResponses, dists);
                            var resultText = detectedClass.ToString(CultureInfo.InvariantCulture);
                            #endregion

                            #region 匹配
                            var isDraw = false;
                            if (detectedClass >= 0)
                            {
                                response += detectedClass.ToString();
                                isDraw = true;
                            }
                            if (detectedClass == -1 && !response.Contains("."))
                            {
                                response += ".";
                                resultText = ".";
                                isDraw = true;
                            }
                            #endregion

                            #region 绘制及输出切割信息库
                            try
                            {
                                //if (this._isDebug)
                                //{
                                Write(contourIndex, detectedClass, roi);
                                //}
                            }
                            catch { }

                            if (this._isDebug && isDraw)
                            {
                                Cv2.PutText(dst, resultText, new Point(boundingRect.X, boundingRect.Y + boundingRect.Height), 0, 1, new Scalar(0, 255, 0), 2);
                            }
                            #endregion

                            result?.Dispose();
                            results?.Dispose();
                            neighborResponses?.Dispose();
                            dists?.Dispose();
                            contourIndex++;
                        }
                        catch (Exception ex)
                        {
                            TextHelper.Error("GetText ex", ex);
                        }
                    });

                    #region 调试模式显示过程
                    source.IsDebugShow("Segmented Source", this._isDebug);
                    dst.IsDebugShow("Detected", this._isDebug);
                    dst.IsDebugWaitKey(this._isDebug);
                    dst.IsDebugImWrite("dest.jpg", this._isDebug);
                    #endregion
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                source?.Dispose();
                roiSource?.Dispose();
            }
            return response;
        }
        
        /// <summary>
        /// 图片处理算法
        /// </summary>
        /// <param name="src"></param>
        /// <param name="isDebug"></param>
        /// <returns></returns>
        public ImageProcessModel MatProcessing(Mat src, bool isDebug = false)
        {
            src.IsDebugShow("原图", isDebug);

            var list = GetAlgoritmList(src);
            var resultMat = new Mat();
            src.CopyTo(resultMat);
            var isZoom = IsZoom(src);
            list?.ForEach(item =>
            {
                switch (item)
                {
                    case EnumMatAlgorithmType.Dilate:
                        resultMat = resultMat.ToDilate(Convert.ToInt32(this._config.DilateLevel));
                        resultMat.IsDebugShow(EnumMatAlgorithmType.Dilate.GetDescription(), isDebug);
                        break;
                    case EnumMatAlgorithmType.Erode:
                        var eroderLevel = isZoom ? this._config.ErodeLevel * this._config.ZoomLevel : this._config.ErodeLevel;
                        resultMat = resultMat.ToErode(eroderLevel);
                        resultMat.IsDebugShow(EnumMatAlgorithmType.Erode.GetDescription(), isDebug);
                        break;
                    case EnumMatAlgorithmType.Gray:
                        resultMat = resultMat.ToGrey();
                        resultMat.IsDebugShow(EnumMatAlgorithmType.Gray.GetDescription(), isDebug);
                        break;
                    case EnumMatAlgorithmType.Thresh:
                        var thresholdValue = this._config.ThresholdValue <= 0 ? resultMat.GetMeanThreshold() : this._config.ThresholdValue;
                        resultMat = resultMat.ToThreshold(thresholdValue, thresholdType: this._config.ThresholdType);
                        resultMat.IsDebugShow(EnumMatAlgorithmType.Thresh.GetDescription(), isDebug);
                        break;
                    case EnumMatAlgorithmType.Zoom:
                        resultMat = resultMat.ToZoom(this._config.ZoomLevel);
                        src = resultMat;
                        resultMat.IsDebugShow(EnumMatAlgorithmType.Zoom.GetDescription(), isDebug);
                        break;
                    case EnumMatAlgorithmType.Blur:
                        resultMat = resultMat.ToBlur();
                        src = resultMat;
                        resultMat.IsDebugShow(EnumMatAlgorithmType.Blur.GetDescription(), isDebug);
                        break;
                }
            });

            var oldThreshImage = new Mat();
            resultMat.CopyTo(oldThreshImage);

            return new ImageProcessModel()
            {
                ResourcMat = src,
                FindContoursMat = oldThreshImage,
                RoiResultMat = resultMat
            };
        }

opencv 图片处理开放出去的配置对象实体如下:

 public class OpencvOcrConfig
    {
        /// <summary>
        /// 放大程度级别 默认2
        /// </summary>
        public double ZoomLevel { set; get; }

        /// <summary>
        /// 腐蚀级别 默认2.5
        /// </summary>
        public double ErodeLevel { set; get; }

        /// <summary>
        /// 膨胀
        /// </summary>
        public double DilateLevel { set; get; }

        /// <summary>
        /// 阀值
        /// </summary>
        public double ThresholdValue { set; get; }

        /// <summary>
        /// 图片处理算法,用逗号隔开
        /// </summary>
        public string Algorithm { set; get; }

        /// <summary>
        /// 二值化方式
        /// </summary>
        public ThresholdType ThresholdType { set; get; } = ThresholdType.BinaryInv;

        /// <summary>
        /// 通道模式
        /// </summary>
        public OcrChannelTypeEnums ChannelType { set; get; } = OcrChannelTypeEnums.BlackBox;

    }

opencv 图片处理算法扩展方法如下:

 public static partial class OpenCvExtensions
    {
        private const int Thresh = 200;
        private const int ThresholdMaxVal = 255;

        /// <summary>
        /// Bitmap Convert Mat
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        public static Mat ToMat(this System.Drawing.Bitmap bitmap)
        {
            return OpenCvSharp.Extensions.BitmapConverter.ToMat(bitmap);
        }

        /// <summary>
        /// Bitmap Convert Mat
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        public static System.Drawing.Bitmap ToBitmap(this Mat mat)
        {
            return OpenCvSharp.Extensions.BitmapConverter.ToBitmap(mat);
        }


        public static bool MatIsEqual(this Mat mat1, Mat mat2)
        {
            try
            {
                if (mat1.Empty() && mat2.Empty())
                {
                    return true;
                }
                if (mat1.Cols != mat2.Cols || mat1.Rows != mat2.Rows || mat1.Dims() != mat2.Dims() ||
                    mat1.Channels() != mat2.Channels())
                {
                    return false;
                }
                if (mat1.Size() != mat2.Size() || mat1.Type() != mat2.Type())
                {
                    return false;
                }
                var nrOfElements1 = mat1.Total() * mat1.ElemSize();
                if (nrOfElements1 != mat2.Total() * mat2.ElemSize())
                    return false;

                return MatPixelEqual(mat1, mat2);
            }
            catch (Exception ex)
            {
                TextHelper.Error("MatIsEqual 异常", ex);
                return true;
            }
        }

        /// <summary>
        /// 灰度
        /// </summary>
        /// <param name="mat"></param>
        /// <returns></returns>
        public static Mat ToGrey(this Mat mat)
        {
            try
            {
                Mat grey = new Mat();
                Cv2.CvtColor(mat, grey, OpenCvSharp.ColorConversion.BgraToGray);
                return grey;
            }
            catch
            {
                return mat;
            }
        }

        /// <summary>
        /// 二值化
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public static Mat ToThreshold(this Mat data, double threshValue = 0, ThresholdType thresholdType = ThresholdType.BinaryInv)
        {
            Mat threshold = new Mat();

            if (threshValue == 0)
                threshValue = Thresh;
            Cv2.Threshold(data, threshold, threshValue, ThresholdMaxVal, thresholdType);
            if (threshold.IsBinaryInv())
            {
                Cv2.Threshold(threshold, threshold, threshValue, ThresholdMaxVal, ThresholdType.BinaryInv);
            }


            //Mat threshold = new Mat();

            //if (threshValue == 0)
            //    threshValue = Thresh;
            //Cv2.AdaptiveThreshold(data, threshold, ThresholdMaxVal,AdaptiveThresholdType.MeanC, thresholdType,3,0);
            //if (threshold.IsBinaryInv())
            //{
            //    Cv2.AdaptiveThreshold(threshold, threshold, ThresholdMaxVal, AdaptiveThresholdType.MeanC, ThresholdType.BinaryInv,3, 0);
            //}
            //Cv2.AdaptiveThreshold()
            // Threshold to find contour
            //var threshold = data.Threshold(80, 255, ThresholdType.BinaryInv);
            //Cv2.Threshold(data, threshold, Thresh, ThresholdMaxVal, ThresholdType.BinaryInv); // Threshold to find contour

            //Cv2.AdaptiveThreshold(data, threshold, 255, AdaptiveThresholdType.MeanC, ThresholdType.BinaryInv, 11, 2);

            //Cv2.Threshold(data, data, Thresh, ThresholdMaxVal, OpenCvSharp.ThresholdType.BinaryInv); // Threshold to find contour
            //Cv2.AdaptiveThreshold(data, threshold, ThresholdMaxVal, AdaptiveThresholdType.GaussianC, OpenCvSharp.ThresholdType.Binary, 3, 0); // Threshold to find contour
            //Cv2.AdaptiveThreshold(data, threshold, 255, AdaptiveThresholdType.MeanC, ThresholdType.Binary, 3, 0);
            //CvInvoke.AdaptiveThreshold(data, data, 255, Emgu.CV.CvEnum.AdaptiveThresholdType.GaussianC, Emgu.CV.CvEnum.ThresholdType.Binary, 3, 0);
            return threshold;
            //var mat = data.Threshold(100, 255,ThresholdType.Binary);
            //return mat;
        }

        /// <summary>
        /// 是否调试显示
        /// </summary>
        /// <param name="src"></param>
        /// <param name="name"></param>
        /// <param name="isDebug"></param>
        public static void IsDebugShow(this Mat src, string name, bool isDebug = false)
        {
            if (!isDebug)
                return;

            Cv2.ImShow(name, src);
        }

        public static void IsDebugWaitKey(this Mat src, bool isDebug = false)
        {
            if (!isDebug)
                return;

            Cv2.WaitKey();
        }

        public static void IsDebugImWrite(this Mat src, string path, bool isDebug = false)
        {
            if (!isDebug)
                return;

            try
            {
                Cv2.ImWrite(path, src);
            }
            catch { }
        }

        /// <summary>
        /// Mat 转成另外一种存储矩阵方式
        /// </summary>
        /// <param name="roi"></param>
        /// <returns></returns>
        public static Mat ConvertFloat(this Mat roi)
        {
            var resizedImage = new Mat();
            var resizedImageFloat = new Mat();
            Cv2.Resize(roi, resizedImage, new Size(10, 10)); //resize to 10X10
            resizedImage.ConvertTo(resizedImageFloat, MatType.CV_32FC1); //convert to float
            var result = resizedImageFloat.Reshape(1, 1);
            return result;
        }

        /// <summary>
        /// 腐蚀
        /// </summary>
        /// <param name="mat"></param>
        /// <returns></returns>
        public static Mat ToErode(this Mat mat, double level)
        {

            #region level 2.5时默认的,自动会判断是否需要腐蚀
            if (level < 1)
            {
                return mat;
            }
            if (level == 2.5)
            {
                if (!mat.IsErode())
                    return mat;
            }
            #endregion

            var erode = new Mat();

            var copyMat = new Mat();
            mat.CopyTo(copyMat);

            Cv2.Erode(mat, erode, Cv2.GetStructuringElement(StructuringElementShape.Ellipse, new Size(level, level)));
            return erode;
        }

        /// <summary>
        /// 膨胀
        /// </summary>
        /// <param name="mat"></param>
        /// <returns></returns>
        public static Mat ToDilate(this Mat mat, int level)
        {
            if (level <= 0)
                return mat;
            var dilate = new Mat();
            Cv2.Dilate(mat, dilate, Cv2.GetStructuringElement(StructuringElementShape.Ellipse, new Size(level, level)));
            return dilate;
            //return mat;
        }

        /// <summary>
        /// mat 转Roi
        /// </summary>
        /// <param name="image"></param>
        /// <param name="boundingRect"></param>
        /// <returns></returns>
        public static Mat GetROI(this Mat image, Rect boundingRect)
        {
            try
            {
                return new Mat(image, boundingRect); //Crop the image
            }
            catch
            {

            }
            return null;
        }

        /// <summary>
        /// 获取平均阀值
        /// </summary>
        /// <param name="mat"></param>
        /// <returns></returns>
        public static int GetMeanThreshold(this Mat mat)
        {
            var width = mat.Width;
            var height = mat.Height;

            var m = mat.Reshape(1, width * height);
            return (int)m.Sum() / (width * height);
        }

        /// <summary>
        /// 获得二值化阀值
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        public static int GetMeanThreshold(this System.Drawing.Bitmap bitmap)
        {
            using (var mat = bitmap.ToMat())
            using (var grap = mat.ToGrey())
            {
                return grap.GetMeanThreshold();
            }
        }

        public static bool IsErode(this System.Drawing.Bitmap bitmap)
        {
            using (var mat = bitmap.ToMat())
            using (var grap = mat.ToGrey())
            {

                var thresholdValue = grap.GetMeanThreshold();
                using (var threshold = grap.ToThreshold(thresholdValue, ThresholdType.BinaryInv))
                {
                    return threshold.IsErode();
                }
            }
        }

        /// <summary>
        /// 放大
        /// </summary>
        /// <param name="img"></param>
        /// <param name="times"></param>
        /// <returns></returns>
        public static Mat ToZoom(this Mat img, double times)
        {
            if (times <= 0)
                return img;
            var width = img.Width * times;
            var height = img.Height * times;

            img = img.Resize(new Size(width, height), 0, 0, Interpolation.NearestNeighbor);
            return img;
        }

        /// <summary>
        /// 均值滤波
        /// </summary>
        /// <param name="img"></param>
        /// <returns></returns>
        public static Mat ToBlur(this Mat img)
        {
            return img.Blur(new Size(3, 3));
        }

        public static Mat Compress(this Mat img)
        {
            var width = 28.0 * img.Width / img.Height;

            var fWidth = width / img.Width;
            var fHeight = 28.0 / img.Height;

            img = img.Resize(new Size(width, 28), fWidth, fHeight, Interpolation.NearestNeighbor);
            return img;
        }

        public static bool MatPixelEqual(this Mat src, Mat are)
        {
            var width = src.Width;
            var height = src.Height;
            var sum = width * height;

            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < width; col++)
                {
                    byte p = src.At<byte>(row, col); //获对应矩阵坐标的取像素
                    byte pAre = are.At<byte>(row, col);
                    if (p != pAre)
                        return false;
                }
            }
            return true;
        }

        public static int GetSumPixelCount(this Mat threshold)
        {
            var width = threshold.Width;
            var height = threshold.Height;
            var sum = width * height;

            var value = 0;
            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < width; col++)
                {
                    byte p = threshold.At<byte>(row, col); //获对应矩阵坐标的取像素
                    value++;
                }
            }
            return value;
        }

        public static int GetPixelCount(this Mat threshold, System.Drawing.Color color)
        {
            var width = threshold.Width;
            var height = threshold.Height;
            var sum = width * height;

            var value = 0;
            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < width; col++)
                {
                    byte p = threshold.At<byte>(row, col); //获对应矩阵坐标的取像素
                    if (Convert.ToInt32(p) == color.R)
                    {
                        value++;
                    }
                }
            }
            return value;
        }

        /// <summary>
        /// 是否需要二值化反转
        /// </summary>
        /// <param name="threshold"></param>
        /// <returns></returns>
        public static bool IsBinaryInv(this Mat threshold)
        {
            var width = threshold.Width;
            var height = threshold.Height;
            var sum = Convert.ToDouble(width * height);

            var black = GetPixelCount(threshold, System.Drawing.Color.Black);

            return (Convert.ToDouble(black) / sum) < 0.5;
        }

        /// <summary>
        /// 是否需要腐蚀
        /// </summary>
        /// <param name="mat"></param>
        /// <returns></returns>
        public static bool IsErode(this Mat mat)
        {
            var percent = mat.GetPercent();
            return percent >= 0.20;
        }

        /// <summary>
        /// 获得白色像素占比
        /// </summary>
        /// <param name="threshold"></param>
        /// <returns></returns>
        public static double GetPercent(this Mat threshold)
        {
            var width = threshold.Width;
            var height = threshold.Height;
            var sum = Convert.ToDouble(width * height);

            var white = GetPixelCount(threshold, System.Drawing.Color.White);
            return (Convert.ToDouble(white) / sum);
        }

        /// <summary>
        /// 根据模板查找目标图片的在原图标中的开始位置坐标
        /// </summary>
        /// <param name="source"></param>
        /// <param name="template"></param>
        /// <param name="matchTemplateMethod"></param>
        /// <returns></returns>
        public static Point FindTemplate(this Mat source, Mat template, MatchTemplateMethod matchTemplateMethod = MatchTemplateMethod.SqDiffNormed)
        {
            if (source == null)
                return new OpenCvSharp.CPlusPlus.Point();

            var result = new Mat();
            Cv2.MatchTemplate(source, template, result, matchTemplateMethod);

            Cv2.MinMaxLoc(result, out OpenCvSharp.CPlusPlus.Point minVal, out OpenCvSharp.CPlusPlus.Point maxVal);

            var topLeft = new OpenCvSharp.CPlusPlus.Point();
            if (matchTemplateMethod == MatchTemplateMethod.SqDiff || matchTemplateMethod == MatchTemplateMethod.SqDiffNormed)
            {
                topLeft = minVal;
            }
            else
            {
                topLeft = maxVal;
            }
            return topLeft;
        }
    }

以上代码中开源对图片进行轮廓切割,同时会生成切割后的图片代码如下

#region 绘制及输出切割信息库
    try
    {

        Write(contourIndex, detectedClass, roi);

    }
    catch { }
#endregion

private void Write(int contourIndex, int detectedClass, Mat roi)
{
    Task.Factory.StartNew(() =>
    {
        try
        {
            var templatePath = $"{AppDomain.CurrentDomain.BaseDirectory}template";
            FileHelper.CreateDirectory(templatePath);
            var templatePathFile = $"{templatePath}/{contourIndex}_{detectedClass.ToString()}.png";
            Cv2.ImWrite(templatePathFile, roi);
            if (!roi.IsDisposed)
            {
                roi.Dispose();
            }
        }
        catch {}
   });
}

切割后的图片如下:
image

这里我已经对数字进行切割好了,接下来就是需要对0-9 这些数字进行分类(建立文件夹进行数字归类),如下:
image

图中的每一个分类都是我事先切割好的数字图片,图中有-1 和-2 这两个特殊分类,-1 里面我是放的是“.”好的分类,用于训练“.”的图片,这样就可以识别出小数点的数字支持.
-2 这个分类主要是其他一些无关紧要的图片,也就是不是数字和点的都归为这一类中.

现在训练库分类已经建立好了,接下来我们需要对这些分类数字进行归一化处理,生成训练模型. 代码如下:

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            var opencvOcr = new OpencvOcr($"{path}Template\\Traindata.xml", opencvOcrConfig: null);
            opencvOcr.Save($"{path}Template\\NumberWrite", outputPath: $"{path}Template\\Traindata.xml");
            MessageBox.Show("生成训练库成功");
            //var img = new Bitmap(this.txbFilaName.Text);

            //var str = opencvOcr.GetText(img.ToMat(), isDebug: true);
            //this.labContent.Content = str;
        }
        
        /// <summary>
        /// 保存训练模型
        /// </summary>
        /// <param name="dataPath"></param>
        /// <param name="trainExt"></param>
        /// <param name="dataPathFile"></param>
        public void Save(string dataPath, string trainExt = "*.png", string outputPath = "")
        {
            if (string.IsNullOrEmpty(outputPath))
                throw new ArgumentNullException("save dataPath is not null");

            var trainingImages = this.ReadTrainingImages(dataPath, trainExt);
            var samples = GetSamples(trainingImages);
            var response = GetResponse(trainingImages);

            //写入到训练库中
            using (var fs = new FileStorage(outputPath, FileStorageMode.WriteText))
            {
                fs.Write("samples", samples);
                fs.Write("responses", response);
            }
        }

        /// <summary>
        /// 根据目录加载文件
        /// </summary>
        /// <param name="path"></param>
        /// <param name="ext"></param>
        /// <returns></returns>
        private IList<ImageInfo> ReadTrainingImages(string path, string ext)
        {
            var images = new List<ImageInfo>();
            var imageId = 1;
            foreach (var dir in new DirectoryInfo(path).GetDirectories())
            {
                var groupId = int.Parse(dir.Name);
                foreach (var imageFile in dir.GetFiles(ext))
                {
                    var srcMat = new Mat(imageFile.FullName, OpenCvSharp.LoadMode.GrayScale);
                    var image = srcMat.ConvertFloat();
                    if (image == null)
                    {
                        continue;
                    }

                    images.Add(new ImageInfo
                    {
                        Image = image,
                        ImageId = imageId++,
                        ImageGroupId = groupId
                    });
                }
            }
            return images;
        }
        
        /// <summary>
        /// Mat 转成另外一种存储矩阵方式
        /// </summary>
        /// <param name="roi"></param>
        /// <returns></returns>
        public static Mat ConvertFloat(this Mat roi)
        {
            var resizedImage = new Mat();
            var resizedImageFloat = new Mat();
            Cv2.Resize(roi, resizedImage, new Size(10, 10)); //resize to 10X10
            resizedImage.ConvertTo(resizedImageFloat, MatType.CV_32FC1); //convert to float
            var result = resizedImageFloat.Reshape(1, 1);
            return result;
        }
        
        /// <summary>
        /// 获取Samples
        /// </summary>
        /// <param name="trainingImages"></param>
        /// <returns></returns>
        private Mat GetSamples(IList<ImageInfo> trainingImages)
        {
            var samples = new Mat();
            foreach (var trainingImage in trainingImages)
            {
                samples.PushBack(trainingImage.Image);
            }
            return samples;
        }
        
        private Mat GetResponse(IList<ImageInfo> trainingImages)
        {
            var labels = trainingImages.Select(x => x.ImageGroupId).ToArray();
            var responses = new Mat(labels.Length, 1, MatType.CV_32SC1, labels);
            var tmp = responses.Reshape(1, 1); //make continuous
            var responseFloat = new Mat();
            tmp.ConvertTo(responseFloat, MatType.CV_32FC1); // Convert  to float

            return responses;
        }

到这里ocr 训练模型以及建立好了,会在目录中生成一个Traindata.xml 的训练模型库,我们来打开这个训练模型库文件探索它的神秘的容颜.
image
image

到这里opencv + 数字识别分享已经完成,它的神秘面纱也就到此结束了
到这里opencv + 数字识别分享已经完成,它的神秘面纱也就到此结束了
欢迎各位大佬关注公众号
dotNET 博士

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

opencv +数字识别 的相关文章

  • 为什么使用abs()或fabs()而不是条件否定?

    在 C C 中 为什么要使用abs or fabs 不使用以下代码即可查找变量的绝对值 int absoluteValue value lt 0 value value 这与较低级别的指令较少有关吗 您提出的 有条件的abs 并不等于std
  • 赋值运算符和复制构造函数有什么区别?

    我不明白C 中赋值构造函数和复制构造函数之间的区别 是这样的 class A public A cout lt lt A A lt lt endl The copy constructor A a b The assignment cons
  • 添加对共享类的多个 WCF 服务的服务引用

    我正在尝试将我的 WCF Web 服务拆分为几个服务 而不是一个巨大的服务 但是 Visual Studio Silverlight 客户端 复制了两个服务共享的公共类 这是一个简单的例子来说明我的问题 在此示例中 有两个服务 两者都返回类
  • ASP .NET MVC,创建类似路由配置的永久链接

    我需要帮助在 MVC 网站中创建类似 URL 路由的永久链接 Slug 已设置为 www xyz com profile slug 代码为 routes MapRoute name Profile url profile slug defa
  • try-catch 中未处理的异常

    try list from XElement e in d Descendants wix File where e Attribute Name Value Contains temp Name e Parent Parent Attri
  • 调试内存不足异常

    在修复我制作的小型 ASP NET C Web 应用程序的错误时 我遇到了 OutOfMemoryException 没有关于在哪里查看的提示 因为这是一个编译时错误 如何诊断此异常 我假设这正是内存分析发挥作用的地方 有小费吗 Thank
  • ZLIB 解压缩

    我编写了一个小型应用程序 该应用程序应该解压缩以 gzip deflate 格式编码的数据 为了实现这一点 我使用 ZLIB 库 使用解压缩功能 问题是这个功能不起作用 换句话说 数据不是未压缩的 我在这里发布代码 int decompre
  • C++派生模板类继承自模板基类,无法调用基类构造函数[重复]

    这个问题在这里已经有答案了 我试图从基类 模板 继承 派生类也是模板 它们具有相同的类型 T 我收到编译错误 非法成员初始化 Base 不是基类或成员 为什么 如何调用基类构造函数 include
  • C# using 语句、SQL 和 SqlConnection

    使用 using 语句 C SQL 可以吗 private static void CreateCommand string queryString string connectionString using SqlConnection c
  • 如何排列表格中的项目 - MVC3 视图 (Index.cshtml)

    我想使用 ASP NET MVC3 显示特定类型食品样本中存在的不同类型维生素的含量 如何在我的视图 Index cshtml 中显示它 an example 这些是我的代码 table tr th th foreach var m in
  • UWP 无法在两个应用程序之间创建本地主机连接

    我正在尝试在两个 UWP 应用程序之间设置 TCP 连接 当服务器和客户端在同一个应用程序中运行时 它可以正常工作 但是 当我将服务器部分移动到一个应用程序并将客户端部分移动到另一个应用程序时 ConnectAsync 会引发异常 服务器未
  • 是否有一个 C++ 库可以从 PDF 文件中提取文本,例如 PDFBox for Java? [关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 去年 我使用 PDFBox 在 Java 中创建了一个应用程序来获取某些 PDF 文件中的原始文本 现在
  • gdb查找行号的内存地址

    假设我已将 gdb 附加到一个进程 并且在其内存布局中有一个文件和行号 我想要其内存地址 如何获取文件x中第n行的内存地址 这是在 Linux x86 上 gdb info line test c 56 Line 56 of test c
  • 内核开发和 C++ [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 从我know https stackoverflow com questions 580292 what languages are windo
  • 运行代码首先迁移更新数据库时出错

    我在迁移到数据库时遇到问题 并且似乎找不到我遇到的错误的答案 System MissingMethodException Method not found System Data Entity Migrations Builders Tab
  • 我应该在应用程序退出之前运行 Dispose 吗?

    我应该在应用程序退出之前运行 Dispose 吗 例如 我创建了许多对象 其中一些对象具有事件订阅 var myObject new MyClass myObject OnEvent OnEventHandle 例如 在我的工作中 我应该使
  • 过度使用委托对性能来说是一个坏主意吗? [复制]

    这个问题在这里已经有答案了 考虑以下代码 if IsDebuggingEnabled instance Log GetDetailedDebugInfo GetDetailedDebugInfo 可能是一个昂贵的方法 因此我们只想在调试模式
  • 以编程方式使用自定义元素创建网格

    我正在尝试以编程方式创建一个网格 并将自定义控件作为子项附加到网格中 作为 2x2 矩阵中的第 0 行第 0 列 为了让事情变得更棘手 我使用了 MVVM 设计模式 下面是一些代码可以帮助大家理解这个想法 应用程序 xaml cs base
  • 热重载时调用方法

    我正在使用 Visual Studio 2022 和 C 制作游戏 我想知道当您热重新加载应用程序 当它正在运行时 时是否可以触发一些代码 我基本上有 2 个名为 UnloadLevel 和 LoadLevel 的方法 我想在热重载时执行它
  • WPF/数据集:如何通过 XAML 将相关表中的数据绑定到数据网格列中?

    我正在使用 WPF DataSet 连接到 SQL Server Express XAML 和 C Visual Studio 2013 Express 我从名为 BankNoteBook 的现有 SQL Server Express 数据

随机推荐

  • Vue路由守卫

    路由守卫的作用 对路由进行权限控制 分类 全局守卫 独享守卫 组件内守卫 1 全局守卫 全局前置路由守卫 初始化和每次路由切换时调用 router beforeEach to from next gt console log 前置路由守卫
  • vue在父组件如何获取子组件的ref

    步骤 1 给父组件定义一个ref 2 子组件也是一样的ref 父组件 div div
  • 【规划】RRT Planer 以及 python实现

    本文转载自头条号作者半杯茶的小酒杯 百度的研发工程师 文章多是自动驾驶相关 推介给大家 RRT Planer 快速搜索随机树 RRT Rapidly ExploringRandom Trees 是一种常见的用于机器人路径规划的方法 他的原始
  • Qt中的JSON操作

    JSON JavaScript Object Notation js对象标记 是一种轻量级的数据交换格式 它基于ECMAScript的一个子集 使用完全独立于编程语言的文本格式来存储和表示数据 简洁和清晰的的层次结构使得JSON成为理想的数
  • ScaleTable matlab,【转】用matlab跑一下下面的程序,带上耳机,你会有发现一些惊喜噢...

    Cripple Pachebel s Canon on Matlab Have fun fs 44100 sample rate dt 1 fs T16 0 125 t16 0 dt T16 temp k size t16 t4 linsp
  • mysql导出binLog日志

    安装 https blog csdn net weixin 43423484 article details 124408565 设置 mysqld 设置3306端口 port 3306 设置mysql的安装目录 这里输入你安装的文件路径
  • 缺少项目经验 & 找工作处处碰壁?这 240 个实战项目请打包带走!

    全部面试找工作时 最常被问到的问题就是 有什么项目经验 对于毕业生来说 我们刚走出校园 还没有工作和项目经验 只能拿学校里练习做的 小游戏 或者 命令行程序 出来 但是 这些项目并不符合现代编程的需要 现代编程需要 前后端分工合作 网络接入
  • 云直播SDK核心功能对比|腾讯云、阿里云、声网、即构等SDK厂商对比

    直播业务概述 大家所熟知的直播平台虎牙 斗鱼 快手 抖音 B站 直播功能看似普遍 但从零到一开发却不简单 直播中运用到的技术难点非常之多 音频视频处理 编解码 前后处理 直播分发 即时通讯等技术 学好任何一项都需要比较高的成本 将它们融合到
  • QT从入门到实战x篇_14_消息对话框(QMessageBox、静态成员函数的访问、修改按键、利用函数返回值为枚举判断按键类型,实现指定动作)

    接上篇 QT从入门到实战x篇 xx 模态和非模态对话框创建 对话框建立在栈上运行完即释放 堆上不删除不可释放 对象在栈上及堆上的生命周期问题需弄懂 在帮助文档中 搜索QMessageBox 会看到其比较重要的几个函数如下 对应的为静态成员函
  • npm start 作用

    在配置phonecat项目时需要运行npm start在本地配置一个服务器环境 npm start首先会安装一系列的必要程序 这些程序依赖package json中的内容 package json中的内容详解如下 依赖包介绍 在克隆项目之后
  • 【Android开发】toast提示

    什么是Toast 在屏幕下方浮现出一个窗口 显示一段时间后又消失 这个可视化组件叫做 Toast 它主要用于提示用户某种事件发生了 如何在添加Toast 最关键的是在事件处理逻辑中加两条语句 定义一个Toast 用makeText 设置要浮
  • 饥荒暴食模式服务器无响应,饥荒暴食模式无银盘图文攻略介绍

    饥荒暴食模式无银盘图文攻略介绍 2018 06 26 17 03 50来源 游戏下载编辑 苦力趴评论 0 饥荒 近日推出了新的游戏模式暴食模式 玩家可以选择单人通关亦或是组队一同任务 下面就为大家带来饥荒暴食模式无银盘图文攻略介绍 基本思路
  • 数据库之数据库设计和E-R模型

    本篇文章介绍数据库设计和E R模型 内容基本是笔者在学习 数据库系统概念 时摘抄总结而来 仅作笔记 实体 联系模型 实体 联系 entity relationship E R 数据模型的提出旨在方便数据库的设计 它是通过允许定义代表数据库全
  • related work

    Traditional approaches e g genetic algorithm GA 2 and ant colony optimization ACO 3 can obtain optimal mapping results b
  • mysql查询练习(三)

    31 查询成绩比该课程平均成绩低的同学的成绩表 mysql gt select from score a where degree lt select avg degree from score b where a cno b cno sn
  • 查看linux系统版本命令

    一 查看内核版本命令 1 root SOR SYS cat proc version Linux version 2 6 18 238 el5 mockbuild x86 012 build bos redhat com gcc versi
  • Robot Framework完整流程学习--分层思想

    一 环境搭建 网上有很多的教程 这里就不多讲了 二 RIDE的界面认识 这里只介绍几个重要常用的功能 其他相信自己都能理解 1 Search Keywords F5 搜索关键字 2 Content Assistance 内容助手 3 Vie
  • java 多线程 总结一

    首先讲一下进程和线程的区别 进程 每个进程都有独立的代码和数据空间 进程上下文 进程间的切换会有较大的开销 一个进程包含1 n个线程 线程 同一类线程共享代码和数据空间 每个线程有独立的运行栈和程序计数器 PC 线程切换开销小 线程和进程一
  • nmap常规使用和参数超细详解 -- 小黑liux武器库详解<宝藏文>

    namp非常强大的主机发现和端口扫描工具 这是web渗透常用的工具 包含四项基本功能 主机发现 端口扫描 版本侦测 操作系统侦测 目录 常规使用 单体拳法 组合拳 参数详解 主机发现 扫描技术 服务版本探测 操作系统检测 防火墙 IDS规避
  • opencv +数字识别

    现在很多场景需要使用的数字识别 比如银行卡识别 以及车牌识别等 在AI领域有很多图像识别算法 大多是居于opencv 或者谷歌开源的tesseract 识别 由于公司业务需要 需要开发一个客户端程序 同时需要在xp这种老古董的机子上运行 故