-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
写了一半才发现的好东西:
例图:
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
现在获得了一些方框,但我们还要区分左上、右上和左下。通过计算内积判断左上,即内积最小的角。这里感觉如果视角很偏,左上角角度较小会有问题,但我拍了半天也没整出这么张图,不管了。剩下两个方块用外积判断。
然后进行一些暴力计算,给图片旋转一个角度,使得顶部两个定位图形与x轴平行。
找出方框后可以用cv2.moments()求重心,连出一个定位三角形来
参考资料:
计算部分代码: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)
参考:
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)
5. 总结
虽然测了几张图能跑出来,但估计换几张图还有挺多要改的地方的。感觉就是不停地观察图像特征,观察得好,能用数学描述出来,准确率就高。最后第四个点精度还是不够,可能真的要1:1:3:1:1一行行扫才能算对。中间检测定位矩形轮廓也很草率。
忘了要说什么了,反正玩得蛮开心的。以及能写python了,以后可以抄作业的语言又多了一门,不错。