猿创征文|OpenCV 如何提高条形码识别率
今天介绍一个使用OpenCV提高条形码识别率的算法
最近,在研究机器视觉的课题,除了百度等一些收费的项目,免费的算法库经过我的筛选,感觉OpenCV还不错,条码识别用到的是ZXing插件,但在识别条码(最近只涉及到了条形码)的时候识别率比较低。无论怎么优化都收效甚微。查阅了一些知识文档,不是实现难度太大就是效果不好,在某天深夜看着条码发呆的时候突然一个简单的算法浮现在脑海里,测试后果然效果非常明显,下面分享给各位猿友。欢迎批评指正~~
平台及OpenCV库简介
在界面方面我还是喜欢.Net 平台,网上讲解OpenCV的例子多为Python,无妨,其实方法都是类似的,关于winform线程的问题不在本文讨论之列。
-
C# winform程序
-
OpenCV库,直接NuGget引入OpenCVSharp库即可
-
ZXing库引入
-
其他小插件看各位看官心情添加
平台搭建还是比较简单的。
强烈建议:先学习一下OpenCV的课程
B站是个好地方,免费资源很多,讲的也很好,视觉识别是一个和一般开发不太一样的赛道,直接看代码还是很吃力的。然后下载OpenCV源码及示例进行辅助理解。工欲善其事必先利其器,磨刀不误砍柴工。这给后面的工作无疑是扫清了大部分的障碍。
步入正题:从图片读取到条码截取部分(非重点,但很重要)
- 获取含有条形码的图片
我不想赘述如何进行图片读取,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);
}
原图如下:敏感部位马赛克处理了
-
图片处理过程,通过灰度变换、各种滤镜、图片透视等操作,此处不是本文重点,但是非常重要!!!需要根据不同的图片进行个性化操作,如果各种操作看不懂,还得再自学一下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;
}
====================================
简码笔记,让你的代码更加简约精炼。
转载请注明出处。