猿创征文|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. 图片处理过程,通过灰度变换、各种滤镜、图片透视等操作,此处不是本文重点,但是非常重要!!!需要根据不同的图片进行个性化操作,如果各种操作看不懂,还得再自学一下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;
                }
//透视函数参考
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<OpenCvSharp.Point>[] contoursList = new Mat<OpenCvSharp.Point>[0];
            //List<Mat> contoursList = new List<Mat>();

            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)那么条码的位置取轮廓中x坐标最小的点(可能会有误差,但是正常情况下肯定可以大幅度提高准确性)
3)轮廓中计算y相等的所有x值的差,得到最大值为宽度K
4)得到左上角坐标(x,0),再知道宽度即可画出一条宽为K的线(其实是一个矩形)
5)最终得到下图中最下边的标准条码。
条码处理过程
这样的条形码,再用ZXing去进行识别,识别率非常高,基本可以达到99.9%。
但是如果原图太不规则,神仙也没办法解析,不在考虑范围之内。

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

/// <summary>
        /// 计算条码x坐标和宽度
        /// </summary>
        /// <param name="src">原始条码图片(截取后的)</param>
        /// <returns></returns>
        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<int, int> list_topleft_width = new Dictionary<int, int>();
            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<int, int> result = new Dictionary<int, int>();

            //根据轮廓计算条码宽度和坐标
            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;
        }

        /// <summary>
        /// 获取条码宽度
        /// 注意:可优化
        /// </summary>
        /// <param name="maxWidth"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        private int GetStandardWith(int maxWidth)
        {

            if (maxWidth < 10)
            {
                maxWidth = 7;
            }
            else
            {
                maxWidth = 17;
            }

            return maxWidth;
        }

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

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

转载请注明出处。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简码笔记

您的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值