数字图像处理大作业——二维码检测(python)

Posted on 2022-12-23 22:07  Capterlliar  阅读(2664)  评论(7编辑  收藏  举报

-1. 介绍,一点废话

  出于某种神秘原因数图大作业选了二维码识别。本来想随便搜个代码水水的,但直接调zxing的包会使我们的大作业只有五行代码,万一老师真看就寄了,于是开始了痛苦的造轮子之旅。可见搜不到答案是促使大学生学习的根本动力。通常而言,识别包含了检测与解码,但写完检测就想放假,遂摆之。检测也是把别人的代码缝在一起,实在缝不起来就自己写点,中途意识到不仅没学会大学数学,还忘干净了高中数学。感谢各位博客作者,参考放在文中对应位置。为什么非要用传统方法而不上机器学习?因为对我的电脑爱得深沉。

0. 环境

PyCharm+OpenCV

一开始没有代码自动补全,修复方法:解决pycharm安装opencv-python包后,依然无法自动补全OpenCV代码问题

1. 目标与检测流程

先来点二维码介绍:二维码原理简介

平常使用扫一扫的时候,只要角度不是太偏,无论是歪着还是仰视俯视都能识别。于是把这个作为目标。为了减低难度,不考虑鱼眼透视,而且正常情况下贴二维码肯定找平的地方贴。

实现方面,因为要用opencv库,选择只剩c++和python。介于机器上正好有python+opencv的环境,于是选择python。虽然不用定义直接用变量挺方便的,但也用得迷迷糊糊,还是c++对各种存储方式看得清楚一些。

搜了一些参考后将进行以下步骤:

    • 加载图片
    • 检测三个定位图形
    • 角度矫正
    • 检测二维码的四个顶点
    • 透视变换

参考资料:

QR Code Recognition Based On Image Processing

A Simple and Efficient Image Pre-processing for QR Decoder

写了一半才发现的好东西:

OpenCV4 二维码定位识别源码解析

例图:

2. 检测定位图形

检测定位图形,也就是那三个黑框框,主要有两种方法:

1. 利用这个黑框1:1:3:1:1的黑白比例检测。OpenCV4就是用的这种方法。

2. 简化版本:利用cv2.findContours()寻找轮廓,然后找出内部包含两个轮廓的外轮廓;但这样也可能检测到别的什么东西,于是再用cv2.approxPolyDP()判断一下它是否为四边形,增大选中的概率。最后如果检测到的数量超过三个,再根据周长乱搞搞。

 代码:

def getArea(img):
    # 图像灰度化
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 图像二值化
    ret, img2 = cv2.threshold(img_gray, 127, 255, 0)
    # 中值滤波
    img2 = cv2.medianBlur(img2, 3)
    # 寻找轮廓

    contours, hierarchy = cv2.findContours(img2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    base = cv2.imread('base.png')
    height, width, _ = img.shape
    base = cv2.resize(base, (width, height))

    hierarchy = hierarchy[0]
    # 轮廓中有两个子轮廓的轮廓可能为二维码位置探测图形
    points = []
    minlen=100000
    for i in range(0, len(contours)):
        k = i
        c = 0
        while hierarchy[k][2] != -1:
            k = hierarchy[k][2]
            c = c + 1
        if c == 2:
            perimeter = cv2.arcLength(contours[i], True)  # 计算轮廓周长
            if(perimeter<minlen):
                minlen=perimeter
            approx = cv2.approxPolyDP(contours[i], 0.02 * perimeter, True)  # 获取轮廓角点坐标
            if len(approx) == 4:
                points.append(contours[i])
    if len(points)>3:
        rem=[]
        for i in points:
            perimeter = cv2.arcLength(i, True)
            if perimeter>minlen*3:
                rem.append(i)
                continue
        for i in rem:
            points.remove(i)
    for i in points:
        cv2.drawContours(base, [i], -1, (255, 0, 0), 2)
    showImg(base)

    return points
View Code

现在获得了一些方框,但我们还要区分左上、右上和左下。通过计算内积判断左上,即内积最小的角。这里感觉如果视角很偏,左上角角度较小会有问题,但我拍了半天也没整出这么张图,不管了。剩下两个方块用外积判断。

然后进行一些暴力计算,给图片旋转一个角度,使得顶部两个定位图形与x轴平行。

找出方框后可以用cv2.moments()求重心,连出一个定位三角形来

  

参考资料:

图像二值化

cv2.findContours

cv2.approxPolyDP

用OpenCV识别图片中的二维码并截取

计算部分代码:Opencv的使用小教程3——利用轮廓检测实现二维码定位

3. 寻找四个顶点

接下来要进行透视变换,透视变换的重点在于寻找变形二维码的四个顶点。在上一步我们已经获得了三个方框,可以获得三个顶点,现在来找右下角。尝试了以下方法:

1. 寻找外接矩形

由于透视问题矩形无法描述二维码的边界。但扩展一下可以求外接多边形。将图片二值化后腐蚀膨胀,效果不是很好,还是坑坑洼洼的。

(其实这张还行,但有的处理了跟没处理一样)

代码:

    img=cv2.erode(img2,np.ones((5,5),np.uint8),2)
    img = cv2.medianBlur(img, 3)
    img = cv2.dilate(img, np.ones((5, 5), np.uint8), 20)
    showImg(img)
View Code

参考:

腐蚀与膨胀

2. 计算第四个点

对于平行四边形可以算,但透视后大多数是梯形。

3. 霍夫变换

虽然算不了第四个点,但提示我们第四个点是两条直线的交点,于是试图找到这个交点。直接在找到的方框上进行一些霍夫变换,应该比在原图上靠谱。调了半天参,找出直线:

图很漂亮,大喜,开始思考如何求交点,或者求出边缘四条线后取交点。

一开始的想法是将直线分组,分成上下左右四边,然后将多条直线用最小二乘拟合成一条。但一开始分组就没分出来。首先缺乏分界线,没想出来靠左和靠右应该以什么标准区分。如果分成较小较大两组,那两条直线距离多近算作一组也不好说,总体上应该用二维码的大小来估计,但要估计的值好像太多了。或者用二维码中心来分左右,但一个方框会生成两条平行的直线,这两组线又怎么区分呢。总之看起来很美好,但数学太差,没做下去。

然后参考了基于OpenCV实现二维码等图像的检测与矫正的做法,不断合并相似直线,直到只剩四条为止,如果不是四条 ,调整合并标准。看起来很容易死循环,也不知道合并时该留哪条,但可以先试试。试了一下逝世了,作者是用腐蚀膨胀后的图形求的直线,因此只有外部四条,而此处常常会生成8条线;如果能留下这8条也行,但合并标准是斜率相差度数,调了半天总是把二维码拦腰斩断,还有莫名的45°斜线出没,诶那图居然没存,总之很崩溃。不知道把标准调成区域内直线距离会怎么样,不想试,八成会留下一些奇怪的东西。

放弃上述做法后试图对直线形成图像下手,搜索如何检测矩形,但人家的矩形没有延长线,好处理得很。

最后寄希望于玄学,有没有什么能检测尖角的函数。还真找到一个角点检测,cv2.cornerHarris(),蛮好玩的,就是它用灰度变化剧烈程度寻找角点,最后找出来满天星,让人回忆起了被二维偏序支配的恐惧。

看起来只能两两求交点了,但我为什么不在那单纯的方框上求呢。

4. 求右侧和下侧直线交点

回到二维码定位图形,试图遍历方框轮廓寻找顶点。首先枚举每个点对,距离最大的那对一定是对角两个顶点;然后找和这两个顶点能形成三角形最大的那个点,是第三个顶点。最后找和第三个顶点距离最大的点,是第四个顶点。现在来区分左上右下的顺序。先找左上,考虑到已知一对对角顶点,可以求重心了,然后找横纵坐标都比重心小的点为左上点;剩下三个因为已经旋转好了图片,直接计算斜率排序即可。最后处理出每个小方形的四个点,连线求交点,求出来结果如下:

有点歪 挺完美的 离大谱

歪了的主要原因还是小正方形的两个顶点距离太近,斜率误差大,但变换后扫码也能扫出来,那就这样吧。

最终结果

4. 函数说明

findCorners之前的都在暴力计算,计算内容见函数名

findCorners:求三个定位矩形的顺序

findRotateAngle:计算旋转角

getArea:获取三个定位矩形

wrap:霍夫变换求包围直线,没啥用,但挺好看,就没删

dealLittleRects:求每个定位矩形四个顶点位置及排序

perspectTrans:计算交点后透视变换

完整代码:

  1 import math
  2 
  3 import cv2
  4 import numpy as np
  5 
  6 cnt = 3
  7 cnt2 = 0
  8 
  9 
 10 def showImg(img):
 11     # cv2.namedWindow('a', cv2.WINDOW_NORMAL)
 12     # cv2.imshow('a', img)
 13     # cv2.waitKey()
 14     global cnt,cnt2
 15     cnt2=cnt2+1
 16     path="test" + str(cnt)+"\\"+str(cnt2)+".jpg"
 17     cv2.imwrite(path, img)
 18 
 19 
 20 def leftTop(centerPoint):
 21     minIndex = 0
 22     minMultiple = 10000
 23 
 24     multiple = (centerPoint[1][0] - centerPoint[0][0]) * (centerPoint[2][0] - centerPoint[0][0]) + (
 25             centerPoint[1][1] - centerPoint[0][1]) * (centerPoint[2][1] - centerPoint[0][1])
 26     if minMultiple > multiple:
 27         minIndex = 0
 28         minMultiple = multiple
 29 
 30     multiple = (centerPoint[0][0] - centerPoint[1][0]) * (centerPoint[2][0] - centerPoint[1][0]) + (
 31             centerPoint[0][1] - centerPoint[1][1]) * (centerPoint[2][1] - centerPoint[1][1])
 32     if minMultiple > multiple:
 33         minIndex = 1
 34         minMultiple = multiple
 35 
 36     multiple = (centerPoint[0][0] - centerPoint[2][0]) * (centerPoint[1][0] - centerPoint[2][0]) + (
 37             centerPoint[0][1] - centerPoint[2][1]) * (centerPoint[1][1] - centerPoint[2][1])
 38     if minMultiple > multiple:
 39         minIndex = 2
 40 
 41     return minIndex
 42 
 43 
 44 def orderPoints(centerPoint, leftTopPointIndex):
 45     otherIndex = []
 46     waiji = (centerPoint[(leftTopPointIndex + 1) % 3][0] - centerPoint[(leftTopPointIndex) % 3][0]) * (
 47             centerPoint[(leftTopPointIndex + 2) % 3][1] - centerPoint[(leftTopPointIndex) % 3][1]) - (
 48                     centerPoint[(leftTopPointIndex + 2) % 3][0] - centerPoint[(leftTopPointIndex) % 3][0]) * (
 49                     centerPoint[(leftTopPointIndex + 1) % 3][1] - centerPoint[(leftTopPointIndex) % 3][1])
 50     if waiji > 0:
 51         otherIndex.append((leftTopPointIndex + 1) % 3)
 52         otherIndex.append((leftTopPointIndex + 2) % 3)
 53     else:
 54         otherIndex.append((leftTopPointIndex + 2) % 3)
 55         otherIndex.append((leftTopPointIndex + 1) % 3)
 56     return otherIndex
 57 
 58 
 59 def rotateAngle(leftTopPoint, rightTopPoint, leftBottomPoint):
 60     dy = rightTopPoint[1] - leftTopPoint[1]
 61     dx = rightTopPoint[0] - leftTopPoint[0]
 62     k = dy / dx
 63     angle = math.atan(k) * 180 / math.pi
 64     if leftBottomPoint[1] < leftTopPoint[1]:
 65         angle -= 180
 66     return angle
 67 
 68 
 69 def distance(x, y):
 70     return math.sqrt((x[0] - y[0]) ** 2 + (x[1] - y[1]) ** 2)
 71 
 72 
 73 def trianSquare(a, b, c):
 74     return a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])
 75 
 76 
 77 def cross_point(line1, line2):  # 计算交点函数
 78     x1 = line1[0][0]
 79     y1 = line1[0][1]
 80     x2 = line1[1][0]
 81     y2 = line1[1][1]
 82 
 83     x3 = line2[0][0]
 84     y3 = line2[0][1]
 85     x4 = line2[1][0]
 86     y4 = line2[1][1]
 87 
 88     k1 = (y2 - y1) * 1.0 / (x2 - x1)  # 计算k1,由于点均为整数,需要进行浮点数转化
 89     b1 = y1 * 1.0 - x1 * k1 * 1.0  # 整型转浮点型是关键
 90     if (x4 - x3) == 0:  # L2直线斜率不存在操作
 91         k2 = None
 92         b2 = 0
 93     else:
 94         k2 = (y4 - y3) * 1.0 / (x4 - x3)  # 斜率存在操作
 95         b2 = y3 * 1.0 - x3 * k2 * 1.0
 96     if k2 == None:
 97         x = x3
 98     else:
 99         x = (b2 - b1) * 1.0 / (k1 - k2)
100     y = k1 * x * 1.0 + b1 * 1.0
101     return [int(x), int(y)]
102 
103 
104 def getXY(line, len):
105     # 沿着左上角的原点,作目标直线的垂线得到长度和角度
106     rho = line[0][0]
107     theta = line[0][1]
108     # if np.pi / 3 < theta < np.pi * (3 / 4):
109     a = np.cos(theta)
110     b = np.sin(theta)
111     # 得到目标直线上的点
112     x0 = a * rho
113     y0 = b * rho
114 
115     # 延长直线的长度,保证在整幅图像上绘制直线
116     x1 = int(x0 + len * (-b))
117     y1 = int(y0 + len * (a))
118     x2 = int(x0 - len * (-b))
119     y2 = int(y0 - len * (a))
120     return x1, y1, x2, y2
121 
122 
123 def findCorners(points):
124     cent = []
125     temp = []
126     for i in points:
127         # 求重心
128         m = cv2.moments(i)
129         cx = np.int0(m['m10'] / m['m00'])
130         cy = np.int0(m['m01'] / m['m00'])
131         temp.append((cx, cy))
132         cent.append([(cx, cy), i])
133 
134     lefttop = leftTop(temp)
135     other = orderPoints(temp, lefttop)
136     cent = {'ul': cent[lefttop],
137             'ur': cent[other[0]],
138             'dl': cent[other[1]]}
139 
140     return cent
141 
142 
143 def findRotateAngle(points):
144     cent = findCorners(points)
145     angle = rotateAngle(cent['ul'][0], cent['ur'][0], cent['dl'][0])
146     return angle
147 
148 
149 def getArea(img):
150     # 图像灰度化
151     img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
152     # 图像二值化
153     ret, img2 = cv2.threshold(img_gray, 127, 255, 0)
154     # 中值滤波
155     img2 = cv2.medianBlur(img2, 3)
156     # 寻找轮廓
157 
158     contours, hierarchy = cv2.findContours(img2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
159 
160     base = cv2.imread('base.png')
161     height, width, _ = img.shape
162     base = cv2.resize(base, (width, height))
163 
164     hierarchy = hierarchy[0]
165     # 轮廓中有两个子轮廓的轮廓可能为二维码位置探测图形
166     points = []
167     minlen=100000
168     for i in range(0, len(contours)):
169         k = i
170         c = 0
171         while hierarchy[k][2] != -1:
172             k = hierarchy[k][2]
173             c = c + 1
174         if c == 2:
175             perimeter = cv2.arcLength(contours[i], True)  # 计算轮廓周长
176             if(perimeter<minlen):
177                 minlen=perimeter
178             approx = cv2.approxPolyDP(contours[i], 0.02 * perimeter, True)  # 获取轮廓角点坐标
179             if len(approx) == 4:
180                 points.append(contours[i])
181     if len(points)>3:
182         rem=[]
183         for i in points:
184             perimeter = cv2.arcLength(i, True)
185             if perimeter>minlen*3:
186                 rem.append(i)
187                 continue
188         for i in rem:
189             points.remove(i)
190     for i in points:
191         cv2.drawContours(base, [i], -1, (255, 0, 0), 2)
192     showImg(base)
193 
194     return points
195 
196 
197 def wrap(img):
198     base = cv2.imread('base.png')
199     height, width, _ = img.shape
200     base = cv2.resize(base, (width, height))
201     points = getArea(img)
202 
203     base2 = base
204     cv2.drawContours(base2, points, -1, (0, 0, 0), 2)
205     base2 = cv2.Canny(base2, 10, 100, apertureSize=3)
206     lines = cv2.HoughLines(base2, 1, np.pi / 180, 29)
207 
208     for line in lines:
209         x0, y0, x1, y1 = getXY(line, 2000)
210         cv2.line(img, (x0, y0), (x1, y1), (0, 0, 255), 1)
211     showImg(img)
212 
213 
214 def cmp(x, y):
215     return (x[0] - y[0]) / [x[1] - y[1]]
216 
217 
218 def dealLittleRects(rect):
219     littleRect = {}
220     res = []
221     max = 0
222     for i in range(0, len(rect)):
223         for j in range(i + 1, len(rect)):
224             dis = distance(rect[i][0], rect[j][0])
225             if (dis > max):
226                 max = dis
227                 x1 = rect[i][0]
228                 x2 = rect[j][0]
229     corex = min(x1[0], x2[0]) + math.fabs((x1[0] - x2[0]) / 2)
230     corey = min(x1[1], x2[1]) + math.fabs((x1[1] - x2[1]) / 2)
231     max = 0
232     for i in rect:
233         squ = math.fabs(trianSquare(x1, x2, i[0]))
234         if squ > max:
235             max = squ
236             x3 = i[0]
237     max = 0
238     for i in rect:
239         dis = distance(x3, i[0])
240         if dis > max:
241             max = dis
242             x4 = i[0]
243     res.append(x1)
244     res.append(x2)
245     res.append(x3)
246     res.append(x4)
247 
248     for i in range(0, len(res)):
249         if res[i][0] < corex and res[i][1] < corey:
250             lefttop = i
251     littleRect['ul'] = res[lefttop]
252 
253     del res[lefttop]
254     res = sorted(res, key=lambda x: x[1] / x[0])
255     littleRect['ur'] = res[0]
256     littleRect['dr'] = res[1]
257     littleRect['dl'] = res[2]
258     return littleRect
259 
260 
261 def perspectTrans(img):
262     img2 = img.copy()
263     points = getArea(img)
264     cent = findCorners(points)
265 
266     # 定位三角形
267     cv2.line(img2, cent['ul'][0], cent['ur'][0], (0, 255, 0), 3)
268     cv2.line(img2, cent['ul'][0], cent['dl'][0], (0, 255, 0), 3)
269     cv2.line(img2, cent['dl'][0], cent['ur'][0], (0, 255, 0), 3)
270     showImg(img2)
271 
272     pnt = dealLittleRects(cent['dl'][1])
273     pdl = pnt['dl']
274     line1 = (pnt['dl'], pnt['dr'])
275 
276     pnt = dealLittleRects(cent['ur'][1])
277     pur = pnt['ur']
278     line2 = (pnt['ur'], pnt['dr'])
279 
280     pdr = cross_point(line1, line2)
281 
282     pnt = dealLittleRects(cent['ul'][1])
283     pul = pnt['ul']
284 
285     # 四个顶点
286     cv2.circle(img2, pdl, 5, (255, 0, 0), 5)
287     cv2.circle(img2, pdr, 5, (255, 0, 0), 5)
288     cv2.circle(img2, pul, 5, (255, 0, 0), 5)
289     cv2.circle(img2, pur, 5, (255, 0, 0), 5)
290     showImg(img2)
291 
292     plane = np.array([[0, 0], [600, 0], [600, 600], [0, 600]], dtype="float32")
293     source = np.array([pul, pur, pdr, pdl], dtype="float32")
294     M = cv2.getPerspectiveTransform(source, plane)
295     # 进行透视变换
296     img = cv2.warpPerspective(img, M, (600, 600))
297     return img
298 
299 
300 pic = ['t0.jpg', 't1.jpg', 't2.jpg', 't3.png', 't4.png']
301 
302 img = cv2.imread(pic[cnt])
303 showImg(img)
304 points = getArea(img)
305 
306 angle = findRotateAngle(points)
307 height, width, _ = img.shape
308 rotate_matrix = cv2.getRotationMatrix2D((width / 2, height / 2), angle=angle, scale=1)
309 rotated_image = cv2.warpAffine(
310     src=img, M=rotate_matrix, dsize=(max(width, height), max(width, height)))
311 showImg(rotated_image)
312 # wrap(rotated_image)
313 
314 new_img = perspectTrans(rotated_image)
315 showImg(new_img)
View Code

 5. 总结

虽然测了几张图能跑出来,但估计换几张图还有挺多要改的地方的。感觉就是不停地观察图像特征,观察得好,能用数学描述出来,准确率就高。最后第四个点精度还是不够,可能真的要1:1:3:1:1一行行扫才能算对。中间检测定位矩形轮廓也很草率。

忘了要说什么了,反正玩得蛮开心的。以及能写python了,以后可以抄作业的语言又多了一门,不错。