基于zxing的条形码轮廓的机器识别++原创哦~
前天傍晚车子被人撞了,昨天拿去修,估计要3-4天。终于有时间坐下来好好研究这个条形码了。通过2天的摸索(当然之前也研究过,只是没有这次这么专注),大致了解了zxing 的工作原理。由于不是很懂C++和C,同时下载了c++源码和C语言封装的源代码,还下载了不同的版本,最新的好像是3.0。
分别用2种语言去实现了对条码轮廓的机器识别,待会一一把算法讲解给大家,理解了它的工作方式,我们甚至可以用vfp语言独立去完成机器识别,都不用任何其他的库,当然估计没几个人愿意去尝试。
zxing的所有操作基于imageview,我叫它图像视图,这是原生C++类,将原始的字节数组、宽度、高度等信息“打包”成一个统一的对象,以供后续的解码函数使用。
第一步就是要传给zxing图像数据,就是原始的字节数组,被称为rawimagedata, 这里我碰到第一个坑,它跟后来要讲的rawdata很难区分,不太容易搞懂。zxing不提供如何从图片中获得rawimagedata的方法,要我们自己提供,真坑爹啊,你说你搞都搞了,还差那么一点吗,就不能一起提供吗!好在它的这个原始字节数据就是屏幕/图像上所有像素的数据,以RGBA,BGRA等格式顺序排列,可以是彩色,可以是灰度,反正它内部最终是转为为黑白数据的,研究过bmp位图文件格式的应该都知道,跟这个位图文件的数据区很像,但是位图文件的末尾通常有行填充,即每行像素数据的字节数必须是4的倍数。这又是一个让人抓狂的问题,从bmp文件中提取这个数据,还得剔除行填充内容。如果是其他格式的图片文件,比如jpg,tif,png等文件需要转换以后再提取像素数据,由此可见,烦的一逼。。。所幸我以前写了个功能强大的imageconverter类就是用来做这个事的,可以实现不同文件格式的互换,截图,剪贴簿,放大,缩小,旋转,灰度化,二值化等等,纯vfp代码实现。创建imageview的是ZXing_ImageView_new 函数,
c++ 代码:
ZXing_ImageView* iv = ZXing_ImageView_new(imagedata, width, height, ZXing_ImageFormat_Lum, 0, 0);
vfp 申明代码:
DECLARE INTEGER ZXing_ImageView_new IN zxingc.dll STRING data, INTEGER width, INTEGER height, INTEGER format, INTEGER rowStride, INTEGER pixStride
第二步,zxing从这个rawimagedata中开始识别条码,主要实现函数是ZXing_ReadBarcodes;
c++代码:
zxing_Results* results = zxing_ReadBarcodes(iv, opts);
vfp 申明代码:
DECLARE INTEGER ZXing_ReadBarcodes IN zxingc.dll INTEGER iv, INTEGER opts
第一个参数就是上面的imageview,第二个参数是一个结构体,用来设置解码选项,这里可以设定解码选项是某种或几种格式的二维码,以及某种或者几种一维码,也可以全部,类似flag,可以叠加组合。
常见选项是tryHarder(是否费力),tryRotate(是否旋转),tryInvert(是否反白),isPure(是否纯净),不同的选项识别的速度不同。这个结构体不能直接访问,只能通过一些列的api函数去创建,访问,以及设置。
第三步,分析识别出来的条码。识别出来的结果保存在一个叫做results的对象数组里,每一个result对象就是一个barcode对象(c语言里把它叫barcode对象),主要属性就是text(条码文本),barcodeFormat(条码格式),resultpoints(旧版的条码坐标数组),新版的叫Position(C语言封装的就是这个,可以直接拿来用,直接就是条码的外接最小矩形),我开始傻傻的搞不清,做了2套定位条码轮廓的识别方案。等会主要就讲根据resultpoints的机器识别方法。这个对象数组有一些列的方法,用来执行数组操作的,常用的比如size(),获得数组的大小;at()根据索引引用;empty()判断是否为空,为空就是没有识别出条码;push_back()添加一个数组元素等。
最后,我们根据得到条码坐标去做条码轮廓的机器识别。
这里的resultpoints是一个坑,不小心又要跌倒吃不少苦头,它识别一维条码后返回的是2个点,识别二维码的时候返回有的是3个点,有的是4个点,不同的格式返回不同的数值,一步步摸索,不知道拐了几个弯,碰了几次壁。。。
扫一维条码,我们可以得到2个点,这两个点的连线和条码相交,位置主打一个随心所欲,不知道源码是怎么实现的,有时在条码外侧,有时在条码外侧。二维条码得到的点是二维码中的三/四个定位的小方块的中心,这时的四边形倒是在条码的内部。
核心算法:
第一,找上下边界,确定四个点的Y坐标;
一维条码稍微麻烦点,如果条码是水平的,那比较简单,通常条码的宽高比在2.5~5:1, 假设2点间的距离为150,算它是5:1,那么高度大概是30,根据2点连线,我们往上跳跃20~30个像素(20%),如果此时的线段上所有的像素是白色,我们可以认为,已经到达条码的空白静区,那么我们在逐像素往下移;如果这条线段上有黑色像素,那么说明我们的边界还在条码内部,那么再按5个像素步长外移,如此循环,每次跳跃都要检查线段是否全白,一旦检查出整条线段全是白色,再逐像素回退,直到线段上有黑色像素,这时就到达我们的上边界,为什么要设计跳跃式检测呢?主要是为了判断效率,比逐像素往外扩展要快一点。由于实际上zxing给出的2个端点位置是偏上部的,并不是条码中心位置,估计她的扫描原理也是跳跃式的,否则没法解释这2点的位置会因为条码不同位置会不同;接下来找下边界,由于初始线段是偏上的,所以首次跳跃可以多点,30-50个像素(约30%),再5个像素步长下移,同上边界一样的原理,考虑到条码下面可能有文字,步长可以适当加大点,这样有大几率越过字符和条码之间的空隙,把文字也包括在里面。当然要做边界检查,万一条码是靠近图像的边缘,可能一次跳跃就跑到图像外面去了,一旦跃出边界就以边界为边,再慢慢回退。
第二,找左右边界,确定四个点的X坐标;
左右边界的寻找方法和上下边界类似,根据我们找出来的2条上下边界,连接成左右2条线段,zxing的初始端点基本上在条码边界附近,有的偏外,有的在条码内部。我们直接5~10个步长往外移,如果竖直的线段全白,就可以逐像素回退,直到检测到线段上有黑点,这样就确定了X坐标,所有点外移一个像素,就是这个条码的最小外接矩形,也就是轮廓了。
第三,假如条码是倾斜的,怎么办?这个方法好像要失效了。。。倾斜的条码算法稍微复杂些,首先我们得知道,斜线在像素点阵下是个什么样的?斜线其实在像素点阵下就是一组折线,肉眼看起来有锯齿状,这时找边界就不是简单的上下移动及左右移动了,同时还得左右及上下移动。这时我们要引入浮点像素,才能解决这个问题。假设zxing给出的初始端点是(30,20),和(180,40)意味着有个顺时针方向的倾斜(左斜),我们搜索边界时,端点是向右上移动的,如果是右斜,那么端点是往左上移动。按照我么前面的跳跃法,先往上跳20个像素,那么向右应该跳跃多少像素?我们只需看高度差和水平差,这2个点的差是(150,20),我们向上移动了10个像素,那么就应该往右移动1.3333个点,这时左上的点的坐标(31.3333, 10),右下的坐标是(181.3333,30),此时对线段上所有的浮点像素进行判断是否为白色,就取样20个点,就得把边界线分成20份,求出每个点的坐标,分别是(31.3333, 38.8333, 46.3333, 53.8333, 61.3333, 68.8333, 76.3333, 83.8333, 91.3333, 98.8333, 106.3333, 113.8333, 121.3333, 128.8333, 136.3333, 143.8333, 151.3333, 158.8333, 166.3333, 173.8333, 181.3333)和(10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30)对小数进行四舍五入,就可以进行判断了。(31~38,10),(39~46,11)...(174~181,30),正好是一组折线,同理可以找出其他3条边的边界线。
至于二维码,就更简单了,去掉右下角的无效点,给它计算一个端点,使四个点正好成为矩形,沿四周扩展,先上下扩展,再左右扩展,倾斜的情况和一维码相同。水平的条码其实也可以沿用倾斜的方法算,这样可以统一代码。
这个算法非常强大,速度很快的,由于我是在C++/c语言实现的,代码就不上传了。知道了算法,用vfp也可以做到,就看有没有人愿意去花时间实践一下了。。。
另外C语言版的zxing封装库,那个position是直接给出条码的边框的,但是如果包含文字,它会把文字剔除的,个人觉得那样不好。这个position也是个坑鬼,实现的函数是ZXing_Barcode_position,这个函数参数是barcode或result类,返回值是c语言的结构体,当时郁闷得不得了,vfp申明integer(指针)没有用,申明成string也不行。。。实在没有办法,只好在c语言层面动手改了,为vfp做适配,所有输出函数都加了“_vfp”后缀,主要实现函数ZXing_Barcode_position_vfp。
vfp测试代码:使用libzxing.dll 和我自己封装的zxing4vfp.dll
程序代码:
CLEAR
CLEAR DLLS
* 声明 DLL 函数
DECLARE INTEGER _ZxingDetectAndDecodeFromFile@24 IN "zxing4vfp.dll" ;
AS ZxingDetectAndDecodeFromFile ;
INTEGER, INTEGER, INTEGER, INTEGER, INTEGER, INTEGER
* 测试图片路径
cImageFile = GETFILE("bmp;png;jpg")
IF EMPTY(cImageFile)
? "未选择文件"
RETURN
ENDIF
* 分配内存
DECLARE INTEGER GlobalAlloc IN kernel32 INTEGER, INTEGER
DECLARE INTEGER GlobalFree IN kernel32 INTEGER
nPathMem = GlobalAlloc(0, LEN(cImageFile) + 1)
nTextMem = GlobalAlloc(0, 256)
nTypeMem = GlobalAlloc(0, 64)
nCornerMem = GlobalAlloc(0, 32) && 8 个 int (4 个角点 × 2 坐标)
IF nPathMem = 0 OR nTextMem = 0 OR nTypeMem = 0 OR nCornerMem = 0
? "内存分配失败"
RETURN
ENDIF
* 写入数据
= SYS(2600, nPathMem, LEN(cImageFile), cImageFile)
= SYS(2600, nPathMem + LEN(cImageFile), 1, CHR(0))
= SYS(2600, nTextMem, 256, SPACE(256))
= SYS(2600, nTypeMem, 64, SPACE(64))
= SYS(2600, nCornerMem, 32, REPLICATE(CHR(0), 32))
* 调用 DLL
nResult = ZxingDetectAndDecodeFromFile(;
nPathMem, nTextMem, 256, nTypeMem, 64, nCornerMem)
* 读取结果
cText = SYS(2600, nTextMem, 256)
cType = SYS(2600, nTypeMem, 64)
cCorners = SYS(2600, nCornerMem, 32)
* 释放内存
GlobalFree(nPathMem)
GlobalFree(nTextMem)
GlobalFree(nTypeMem)
GlobalFree(nCornerMem)
* 处理结果
IF nResult = 1
* 去掉 CHR(0) 后面的内容
cText = LEFT(cText, AT(CHR(0), cText) - 1)
cType = LEFT(cType, AT(CHR(0), cType) - 1)
? "========================================"
? "解码结果: " + cText
? "条码类型: " + cType
? ""
? "ZXing 角点坐标:"
* 解析 4 个角点 (8 个 int)
DIMENSION aCorners[4,2]
FOR i = 0 TO 3
aCorners[i+1,1] = CTOBIN(SUBSTR(cCorners, i*8 + 1, 4), "4RS")
aCorners[i+1,2] = CTOBIN(SUBSTR(cCorners, i*8 + 5, 4), "4RS")
? " 点" + STR(i+1) + ": (" + STR(aCorners[i+1,1]) + ", " + STR(aCorners[i+1,2]) + ")"
ENDFOR
* 计算外接矩形
minX = aCorners[1,1]
minY = aCorners[1,2]
maxX = aCorners[1,1]
maxY = aCorners[1,2]
FOR i = 2 TO 4
IF aCorners[i,1] < minX
minX = aCorners[i,1]
ENDIF
IF aCorners[i,2] < minY
minY = aCorners[i,2]
ENDIF
IF aCorners[i,1] > maxX
maxX = aCorners[i,1]
ENDIF
IF aCorners[i,2] > maxY
maxY = aCorners[i,2]
ENDIF
ENDFOR
? ""
? "外接矩形: (" + STR(minX) + "," + STR(minY) + ") - (" + STR(maxX) + "," + STR(maxY) + ")"
? "========================================"
* 调用画框函数(只画红色外接矩形)
DrawBoxOnImage(cImageFile, minX, minY, maxX, maxY)
ELSE
? "解码失败,返回值: " + STR(nResult)
ENDIF
RETURN
*===========================================
* 使用 GDI[color=#808080]+ 在图片上画外接矩形[/color]
* 红色,1像素宽
*===========================================
FUNCTION DrawBoxOnImage(cImageFile, minX, minY, maxX, maxY)
LOCAL cOutputFile, nToken, nImage, nGraphics, nPen
LOCAL nRed, nWidth, nResult
LOCAL cImageFileW, cOutputFileW
? "=== 绘制红色外接矩形 ==="
? "矩形范围: (" + STR(minX) + "," + STR(minY) + ") - (" + STR(maxX) + "," + STR(maxY) + ")"
? "矩形宽度: " + STR(maxX - minX) + ", 高度: " + STR(maxY - minY)
cOutputFile = FORCEEXT(cImageFile, "_boxed.bmp")
*--- 检查输出目录 ---
LOCAL cDir
cDir = JUSTPATH(cOutputFile)
IF NOT DIRECTORY(cDir)
MD (cDir)
IF NOT DIRECTORY(cDir)
? "? 无法创建输出目录: " + cDir
RETURN .F.
ENDIF
ENDIF
*--- 声明 GDI+ 函数 ---
DECLARE INTEGER GdiplusStartup IN gdiplus.dll ;
INTEGER @lpToken, STRING @lpInput, INTEGER @lpOutput
DECLARE INTEGER GdiplusShutdown IN gdiplus.dll INTEGER lpToken
DECLARE INTEGER GdipCreateBitmapFromFile IN gdiplus.dll ;
STRING lpFile, INTEGER @lpBitmap
DECLARE INTEGER GdipGetImageGraphicsContext IN gdiplus.dll ;
INTEGER lpImage, INTEGER @lpGraphics
DECLARE INTEGER GdipCreatePen1 IN gdiplus.dll ;
INTEGER nColor, SINGLE nWidth, INTEGER nUnit, INTEGER @lpPen
DECLARE INTEGER GdipDrawRectangle IN gdiplus.dll ;
INTEGER lpGraphics, INTEGER lpPen, ;
SINGLE nX, SINGLE nY, SINGLE nWidth, SINGLE nHeight
DECLARE INTEGER GdipSaveImageToFile IN gdiplus.dll ;
INTEGER lpImage, STRING lpFile, STRING lpEncoder, INTEGER lpEncoderParams
DECLARE INTEGER GdipDeletePen IN gdiplus.dll INTEGER lpPen
DECLARE INTEGER GdipDeleteGraphics IN gdiplus.dll INTEGER lpGraphics
DECLARE INTEGER GdipDisposeImage IN gdiplus.dll INTEGER lpImage
*--- 转换文件名为 Unicode ---
cImageFileW = STRCONV(cImageFile, 5) + CHR(0) + CHR(0)
cOutputFileW = STRCONV(cOutputFile, 5) + CHR(0) + CHR(0)
*--- 启动 GDI+ ---
lcInput = CHR(1) + REPLICATE(CHR(0), 15)
nToken = 0
nResult = GdiplusStartup(@nToken, @lcInput, 0)
IF nResult != 0 OR nToken = 0
? "? GDI+ 启动失败,错误码: " + STR(nResult)
RETURN .F.
ENDIF
*--- 加载图片 ---
nImage = 0
nResult = GdipCreateBitmapFromFile(cImageFileW, @nImage)
IF nResult != 0 OR nImage = 0
? "? 加载图片失败,错误码: " + STR(nResult)
= GdiplusShutdown(nToken)
RETURN .F.
ENDIF
*--- 创建 Graphics ---
nGraphics = 0
nResult = GdipGetImageGraphicsContext(nImage, @nGraphics)
IF nResult != 0 OR nGraphics = 0
= GdipDisposeImage(nImage)
= GdiplusShutdown(nToken)
RETURN .F.
ENDIF
*===========================================
* 绘制红色外接矩形(1像素宽)
*===========================================
nRed = 0xFFFF0000 && 红色 (ARGB)
nPen = 0
nWidth = 1.0
nResult = GdipCreatePen1(nRed, nWidth, 2, @nPen)
IF nResult = 0 AND nPen != 0
= GdipDrawRectangle(nGraphics, nPen, minX, minY, maxX-minX, maxY-minY)
= GdipDeletePen(nPen)
? "? 红色外接矩形已绘制"
ELSE
? "? 笔创建失败"
= GdipDeleteGraphics(nGraphics)
= GdipDisposeImage(nImage)
= GdiplusShutdown(nToken)
RETURN .F.
ENDIF
*--- 保存图片 ---
LOCAL cEncoder
cEncoder = CHR(0x06) + CHR(0xF4) + CHR(0x7C) + CHR(0x55) + ;
CHR(0x04) + CHR(0x1A) + CHR(0xD3) + CHR(0x11) + ;
CHR(0x9A) + CHR(0x73) + CHR(0x00) + CHR(0x00) + ;
CHR(0xF8) + CHR(0x1E) + CHR(0xF3) + CHR(0x2E)
nResult = GdipSaveImageToFile(nImage, cOutputFileW, cEncoder, 0)
*--- 清理资源 ---
= GdipDeleteGraphics(nGraphics)
= GdipDisposeImage(nImage)
= GdiplusShutdown(nToken)
CLEAR DLLS
IF nResult = 0
? "带框图片已保存: " + cOutputFile
DECLARE INTEGER ShellExecute IN shell32.dll ;
INTEGER hwnd, STRING lpOperation, STRING lpFile, ;
STRING lpParameters, STRING lpDirectory, INTEGER nShowCmd
= ShellExecute(0, "open", cOutputFile, "", "", 1)
RETURN .T.
ELSE
? "? 保存图片失败,错误码: " + STR(nResult)
RETURN .F.
ENDIF
ENDFUNC









