• 猿创征文|OpenCV 如何提高条形码识别率


    今天介绍一个使用OpenCV提高条形码识别率的算法

    最近,在研究机器视觉的课题,除了百度等一些收费的项目,免费的算法库经过我的筛选,感觉OpenCV还不错,条码识别用到的是ZXing插件,但在识别条码(最近只涉及到了条形码)的时候识别率比较低。无论怎么优化都收效甚微。查阅了一些知识文档,不是实现难度太大就是效果不好,在某天深夜看着条码发呆的时候突然一个简单的算法浮现在脑海里,测试后果然效果非常明显,下面分享给各位猿友。欢迎批评指正~~

    平台及OpenCV库简介

    在界面方面我还是喜欢.Net 平台,网上讲解OpenCV的例子多为Python,无妨,其实方法都是类似的,关于winform线程的问题不在本文讨论之列。

    1. C# winform程序

    2. OpenCV库,直接NuGget引入OpenCVSharp库即可

    3. ZXing库引入

    4. 其他小插件看各位看官心情添加

      平台搭建还是比较简单的。

    强烈建议:先学习一下OpenCV的课程

    B站是个好地方,免费资源很多,讲的也很好,视觉识别是一个和一般开发不太一样的赛道,直接看代码还是很吃力的。然后下载OpenCV源码及示例进行辅助理解。工欲善其事必先利其器,磨刀不误砍柴工。这给后面的工作无疑是扫清了大部分的障碍。

    步入正题:从图片读取到条码截取部分(非重点,但很重要)

    1. 获取含有条形码的图片
      我不想赘述如何进行图片读取,OpenCV也有类似的接口连接摄像头,也可以直接读取图片,So easy.
      贴点儿代码吧,不然以为我偷的文章,需要的童鞋可以自己看下,否则可以略过。以下是直接连接本机摄像头,也可以连接网络摄像头,可以自己选择。
     					//FrameSource video = Cv2.CreateFrameSource_Camera(0);//.CreateFrameSource_Video.VideoCapture("rtsp://192.168.0.200:554/av0_0")
                        VideoCapture video = VideoCapture.FromCamera(0);
                        //VideoCapture video = VideoCapture.FromFile("rtsp://admin:999999@10.100.103.224/cam/realmonitor?channel=1&subtype=1");
                        //VideoCapture video = new VideoCapture("rtsp://admin:999999@10.100.103.225/cam/realmonitor?channel=1&subtype=1");
                        bool video_isOpened = false;
                        //video.Release();
                        if (video.IsOpened())
                        {
                            video_isOpened = true;
                            led_Status.BeginInvoke(new Action<string>(text => { led_Status.Text = text; led_Status.ForeColor = Color.Lime; }),"Opened");
                        }
                        else
                        {
                            led_Status.BeginInvoke(new Action<string>(text => { led_Status.Text = text; led_Status.ForeColor = Color.Red; }),"Closed");
    
                            //Console.WriteLine("摄像头打开失败!");
                        }
    
                        Mat pre_frame = null;
                        if(video_isOpened)
                        {
                            Mat frame = new Mat();
                            video.Read(frame);
                            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    原图如下:敏感部位马赛克处理了
    包含条形码的图片

    1. 图片处理过程,通过灰度变换、各种滤镜、图片透视等操作,此处不是本文重点,但是非常重要!!!需要根据不同的图片进行个性化操作,如果各种操作看不懂,还得再自学一下OpenCV的课程。也欢迎留言讨论,我有时间也会回复各位。以下为参考代码,。

      ① 截取图片中白色标签部分(此处也自己写了一个算法,关于通过四条不连续的直线,计算出四个四
      边形的顶点,这里就不讨论了,感兴趣的话可以留言)
      ② 透视变换
      ③ 获取条码部分

    			// 截取白色标签部分(略)
    			//透视变换
                Toushi();
    
                MemoryStream ms = new MemoryStream();
                picBox_Pre.Image.Save(ms, picBox_Pre.Image.RawFormat);
                Mat src = Mat.FromImageData(ms.ToArray(), ImreadModes.Unchanged);
    
                Mat dst = Mat.FromImageData(ms.ToArray(), ImreadModes.Grayscale);
               
                Mat src_gray = dst.Clone();
                //dst = dst.Clone();
                
    
                var open_kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(30, 1));
                dst = dst.MorphologyEx(MorphTypes.Open, open_kernel);
    
                //二值化
                Cv2.Threshold(dst, dst, 100, 200, ThresholdTypes.Binary);
                Mat edges = dst.Canny(80, 255);
                OutputArrayOfMatList edl= new OutputArrayOfMatList(List < Mat > list);
    
                var contours = edges.FindContoursAsMat(RetrievalModes.External, ContourApproximationModes.ApproxSimple);
                //Cv2.DrawContours(src, contours, -1, Scalar.Blue);
    
                OpenCvSharp.Rect barcodeRect=new OpenCvSharp.Rect();
                OpenCvSharp.Rect barcodeStringRect = new OpenCvSharp.Rect();
                OpenCvSharp.Rect splitLineRect = new OpenCvSharp.Rect();
                //breakFlag=2时退出
                bool barcodeFlag = false;
                bool barcodeStringFlag = false;
                bool splitLineFlag = false;
                //先找到条码
                //再找到票包最高的坐标
                for (int i = 0; i < contours.Length; i++)
                {
                    //判断条码 1. 宽度、高度 2.对比
                    OpenCvSharp.Rect rect = contours[i].BoundingRect();
    
                    Console.WriteLine($"rect width:--{rect.Width}----,height:---{rect.Height}----,w/h={rect.Width / rect.Height}");
    
                    //条码区域
                    if (rect.Width > 700 && rect.Height > 150 && rect.Width / rect.Height > 2)
                    {
                        //Cv2.DrawContours(src, contours, i, Scalar.Green, 2);
                        Console.WriteLine($"barcode width:{rect.Width},height:{rect.Height},w/h={rect.Width / rect.Height}");
    
                        //条码区域(扩大)
                        int extentX = 6;
                        int extentY = 5;
                        rect = new OpenCvSharp.Rect(new OpenCvSharp.Point(Convert.ToDouble(rect.X - extentX), Convert.ToDouble(rect.Y - extentY)),
                                         new OpenCvSharp.Size(Convert.ToDouble(rect.Width + 2 * extentX), Convert.ToDouble(rect.Height + 2 * extentY)));
                        barcodeRect = rect;
                        Cv2.Rectangle(src, rect, Scalar.DarkGreen, 4);
                        //picBox_Pre.Image = Image.FromStream(src.ToMemoryStream());
    
                        //条码区域(扩大)
                        Mat selectedROI = src_gray.SubMat(rect);
                        string showtext = GetStandardBarCodeText(selectedROI.Clone());
                        txb_BarCode.BeginInvoke(new Action<string>(m => txb_BarCode.Text = m), showtext);
    
                        barcodeFlag = true;
                    }
    
                    // 箱号数字串
                    if (rect.Width > 500 && rect.Height > 30 && rect.Width / rect.Height > 10)
                    {
                        //Cv2.DrawContours(src, contours, i, Scalar.Green, 2);
                        Console.WriteLine($"barcodeString width:{rect.Width},height:{rect.Height},w/h={rect.Width / rect.Height}");
    
                        //条码字符串区域(扩大)
                        int extentX = 6;
                        int extentY = 2;
                        rect = new OpenCvSharp.Rect(new OpenCvSharp.Point(Convert.ToDouble(rect.X - extentX), Convert.ToDouble(rect.Y - extentY)),
                                         new OpenCvSharp.Size(Convert.ToDouble(rect.Width + 2 * extentX), Convert.ToDouble(rect.Height + 2 * extentY)));
    
                        barcodeStringRect = rect;
                        Cv2.Rectangle(src, rect, Scalar.DarkGreen, 4);
                        //picBox_Pre.Image = Image.FromStream(src.ToMemoryStream());
    
                        //条码字符串区域
                        Mat barcodeStringROI = src_gray.SubMat(rect);
                        //Cv2.ImShow("zif", barcodeStringROI);
                        string showtext = ImageToText(ImageToBytes(Image.FromStream(barcodeStringROI.Clone().ToMemoryStream())),"eng");
                        txb_Result.BeginInvoke(new Action<string>(m => txb_Result.Text = $"箱号:{m}\r\n"), showtext);
    
                        barcodeStringFlag = true;
                    }
    
    • 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
    //透视函数参考
    private void Toushi()
    {
    //读取图片
                MemoryStream ms = new MemoryStream();
                picBox_Pre.Image.Save(ms, picBox_Pre.Image.RawFormat);
                Mat src = Mat.FromImageData(ms.ToArray(), ImreadModes.Unchanged);
    
                //src.Resize(,)
                Mat mat = src.Clone();
                OutputArray dst = null;
    
                //Cv2.GaussianBlur(mat, mat, new OpenCvSharp.Size(5,5), 0);
    
                //灰度变换
                mat.CvtColor(ColorConversionCodes.BGR2GRAY);
                //Cv2.CvtColor
                //高斯滤镜
                mat = mat.GaussianBlur(new OpenCvSharp.Size(9, 9),0);
                
               
    
                InputArray element = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(7, 7));
    
               
    
                //查找边缘
                Mat edges = new Mat();
                Cv2.Canny(mat, edges, 75, 200);
    
                
                //获取轮廓
                var contours = Cv2.FindContoursAsMat(edges, RetrievalModes.Tree, ContourApproximationModes.ApproxSimple);
               // contours.sort
                Console.WriteLine(contours.Length.ToString()+"-------");
    
                //Mat[] contoursList = new Mat[0];
                //List contoursList = new List();
    
                Mat show = src.Clone();
                for (int i = 0; i < contours.Length - 1; i++)
                {
                    if (contours[i].MinAreaRect().Size.Width < 100)
                        continue;
    
                    contours[i].MinAreaRect().Size.Width
                    var w_h = contours[i].MinAreaRect().Size.Width / contours[i].MinAreaRect().Size.Height;
    
                    //判断宽高比
                    if (w_h > 0.8 && w_h < 1.2)
                    {
                                           
                        var approx = contours[i].ApproxPolyDP(0.02 * Cv2.ArcLength(contours[i], true), true);
                        var points = approx.ConvexHullPoints();
                        List<OpenCvSharp.Point> pts = new List<OpenCvSharp.Point>();
                        //获取标签正规图
                        pts = GetFourIntersections(points);
                        points = pts.ToArray();
                        
                        //透视变换
                        Mat normalizedImage = GetWarpPerspectiveMat(show, points);
    
                        //Cv2.WarpPerspective(src, normalizedImage, transform, new OpenCvSharp.Size(700, 700));
    
                        picBox_Pre.Image = Image.FromStream(normalizedImage.ToMemoryStream());
                        break;
                    }
                    }
    
    • 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

    先根据轮廓形状进行分区,再将条码部分截取出来单独分析。
    分区后
    最终结果为下图所示:可见本图条码并不清楚,而且打印粗细也不规范,经过透视后略有变形。
    条码部分

    条码图片处理部分(本文重点)

    经过上面的步骤我们可以得到一个不易识别的条码图片,本人经过测试,这样的图片识别率相当低,图片稍微不清楚或者角度稍微变化就无法读取。于是我的思路是(当然是在各种尝试之后得出的办法),既然图片本身就有缺陷,即使再怎么变换,原图都很难达到要求,经过分析,可以自行画一个规范的条码出来。
    前提条件还要从条码的原理来讲,条码是一系列竖条组成,靠宽度和距离来表示信息。
    我的条码只有两种宽度(如果需要多种宽度,稍微修正一下代码即可实现),那么主要就是距离了。
    算法原理:1)首先得到条码的轮廓,将轮廓用点来表示
    2)那么条码的位置取轮廓中x坐标最小的点(可能会有误差,但是正常情况下肯定可以大幅度提高准确性)
    3)轮廓中计算y相等的所有x值的差,得到最大值为宽度K
    4)得到左上角坐标(x,0),再知道宽度即可画出一条宽为K的线(其实是一个矩形)
    5)最终得到下图中最下边的标准条码。
    条码处理过程
    这样的条形码,再用ZXing去进行识别,识别率非常高,基本可以达到99.9%。
    但是如果原图太不规则,神仙也没办法解析,不在考虑范围之内。

    以下是部分处理代码,由于代码尚未进行整理,比较乱,请见谅,又看不懂的地方欢迎留言。

    /// 
            /// 计算条码x坐标和宽度
            /// 
            /// 原始条码图片(截取后的)
            /// 
            private string GetStandardBarCodeText(Mat barCodeROI)
            {
                //预处理 :根据实际情况对条形码长宽进行标准化处理
    
                barCodeROI=barCodeROI.Resize(new OpenCvSharp.Size(860, 190), 0, 0);
                //1.二值化
                //barCodeROI.CvtColor(ColorConversionCodes.BGR2GRAY);
    
    
                //2.图片上下截取固定值(防止噪音)
                int padding = 3;
                //Mat barCodeROI_new = barCodeROI.SubMat(padding, barCodeROI.BoundingRect().Bottom - padding, 0, barCodeROI.BoundingRect().Right);
                Mat barCodeROI_new = barCodeROI;//.CopyMakeBorder(5, 5, 0, 0,BorderTypes.Default,Scalar.White);
                //barCodeROI_new = barCodeROI_new.R;
                //Cv2.ImShow("roi", barCodeROI);
                Cv2.ImShow("roi_new", barCodeROI_new);
                //算法设计:1.确保轮廓都是条码的轮廓
                //          2.轮廓中X坐标最左边的点为目标值
                //          3.计算y相等的所有值,得到最大值为宽度
    
                //高斯去噪(平滑)
                barCodeROI_new = barCodeROI_new.GaussianBlur(new OpenCvSharp.Size(1, 3), 1);
                barCodeROI_new = barCodeROI_new.Threshold(80, 255, ThresholdTypes.Binary);
                //Cv2.ImShow("gauss", barCodeROI_new);
                //开操作去除毛刺
                var open_kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(1, 3));
                barCodeROI_new = barCodeROI_new.MorphologyEx(MorphTypes.Open, open_kernel);
                //Cv2.ImShow("open", barCodeROI_new);
                var edge = barCodeROI_new.Canny(80, 255);
                var contours = edge.FindContoursAsMat(RetrievalModes.External, ContourApproximationModes.ApproxNone);
                //Dictionary list_topleft_width = new Dictionary();
                Mat result = new Mat(new OpenCvSharp.Size(barCodeROI_new.Width + 40, barCodeROI_new.Height + 25), MatType.CV_8UC3, Scalar.White);
                Mat result2 = result.Clone();
                result2.DrawContours(contours, -1, Scalar.Red, 1);
                Cv2.ImShow("contours", result2);
                //Dictionary result = new Dictionary();
    
                //根据轮廓计算条码宽度和坐标
                int maxWidth = 0;
                int minX=0;
                int lastX = 0;
                int barCounter = 0;
                List<Mat<OpenCvSharp.Point>> list = contours.ToArray().OrderBy(x => x.BoundingRect().Left).ToList();
                for (int i = 0; i < list.Count; i++)
                {
                    maxWidth = 0;
                    var rect = list[i].BoundingRect();
                    if (rect.Height / rect.Width <6)
                    {
                        //不是条码轮廓
                        Console.WriteLine($"rect.Height / rect.Width={rect.Height / rect.Width}");
                        continue;
                        
                    }
    
                    //maxWidth = from p1 in contours[i].ToArray()
                    //           from p2 in contours[i].ToArray()
                    //           where p1.Y == p2.Y
                    //           select p1 - p2;
                    minX = list[i].ToArray().Min(c => c.X);
                    if (minX - lastX < 5)
                    {
                        //一根条码分两段
                        Console.WriteLine($"minX={minX},lastX={lastX}, {minX - lastX}");
                        continue;
                    }
                    else
                    {
                        lastX = minX;
                    }
                    //循环查找
                    int minY = list[i].ToArray().Min(p => p.Y);
                    int maxY = list[i].ToArray().Max(p => p.Y);
    
                    for (int y = minY; y <= maxY; y++)
                    {
                        int x1= list[i].ToArray().Where(p=>p.Y==y).Min(c => c.X);
                        int x2 = list[i].ToArray().Where(p => p.Y == y).Max(c => c.X);
    
                        maxWidth = Math.Max(maxWidth, x2 - x1 + 1);
                        //Console.WriteLine($"y={y} \t x2-x1:\t{x2}-{x1}={x2 - x1} \t maxwidth={maxWidth}");
                    }
    
                    int paddingTop = 10;
                    int paddingBottom = 5;
                    int paddingLeft = 10;
    
                    Console.WriteLine($"width:----{maxWidth}");
                    //计算条码宽度
                    maxWidth = GetStandardWith(maxWidth);
    
                    Console.WriteLine($"Index {i + 1} x:{minX},width:{maxWidth}");
    
                    OpenCvSharp.Rect barRect = new OpenCvSharp.Rect(new OpenCvSharp.Point(minX + paddingLeft, paddingTop),
                        new OpenCvSharp.Size(maxWidth, result.Height - paddingBottom - paddingTop));
                    
                    //画矩形,thickness=-1为填充
                    result.Rectangle(barRect, Scalar.Red,-1);
                    //for (int x = minX; x <= minX + maxWidth; x++)
                    //{
                    //    result.Line(new OpenCvSharp.Point(x, minY - paddingTop), new OpenCvSharp.Point(x, result.Height - paddingBottom)
                    //}
                    barCounter++;
                }
                Console.WriteLine($"result size:{result.Size()},barCounter:{barCounter}");
                Cv2.ImShow("barNew", result);
    
                string barCode = ZXingReadBarCode(result);
                //string barCode = OpenCVReadBarCode(result); 
                return barCode;
            }
    
            /// 
            /// 获取条码宽度
            /// 注意:可优化
            /// 
            /// 
            /// 
            /// 
            private int GetStandardWith(int maxWidth)
            {
    
                if (maxWidth < 10)
                {
                    maxWidth = 7;
                }
                else
                {
                    maxWidth = 17;
                }
    
                return maxWidth;
            }
    
    • 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

    ====================================

    简码笔记,让你的代码更加简约精炼。

    转载请注明出处。

  • 相关阅读:
    每天一个知识点- 线程池中线程执行任务发生异常会怎么样
    git 相关命令
    【Axure高保真原型】3D柱状图_中继器版
    java毕业设计花苑物业综合服务平台mybatis+源码+调试部署+系统+数据库+lw
    37.轮播图
    java毕业设计畅言情感互助网站mybatis+源码+调试部署+系统+数据库+lw
    全套完整版实战型Java云HIS系统源码
    K8s 里多容器 Pod 的健康检查探针工作机制分析
    [旭日X3派] 初识篇 - 01
    数据结构第二章
  • 原文地址:https://blog.csdn.net/happyxjbf/article/details/126699846