一、实验内容
基于openCV和python语言实现二维码的图像识别与视频识别,同时实现了根据给定信息生成新二维码。
二、实验环境
使用语言:python 3.9
使用库:openCV ,math, os, numpy , qrcode, pyzbar
三、实验设计
实验共分为三大模块:二维码的图像识别、二维码的视频识别与根据给定信息生成新二维码。我们在本次实验中侧重于手动实现二维码的图像识别模块。
在图像识别模块中,我们着重于手动实现二维码的图像识别的检测定位,通过以下五个流程:加载图片、检测三个定位图形、角度矫正、检测二维码的四个顶点以及透视变换 实现二维码的检测。
在加载图片部分,使用cv2.imread()函数读入图片。
在检测定位图形部分,对原图片先后进行灰度化、二值化和中值滤波,接着使用opencv函数cv2.findContours()、cv2.drawContours()查找并绘制轮廓。
在角度矫正部分,根据斜率手动计算旋转角度,再使用cv2.getRotationMatrix2D()得到旋转矩阵,并使用cv2.warpAffine()进行图片的旋转,使图片与x轴平行
在检测顶点部分,根据之前得到的定位图形,使用cv2.line()绘制定位三角形,根据每个定位矩形的四个顶点位置及排序分别对定位矩阵进行处理;之后使用cv2.circle()得到处理后二维码的四个顶点。
最后在透视变换部分,使用cv2.warpPerspective()得到透视变换后的二维码图像,实现二维码的检测。
得到检测后的二维码后,使用pyzbar库中barcode.data.decode()函数实现二维码的解码处理。得到二维码中包含的信息并输出。
在视频识别模块中,我们使用cv2.VideoCapture()打开摄像头并使用cv2.QRCodeDetector()创建一个二维码检测器。使用cap.read()捕获视频帧,并使用detectAndDecode()实现二维码的检测与解码。
在二维码生成模块中,我们使用qrcode库中qr.make()函数得到包含希望信息的二维码。
四、实验结果与分析
1.二维码的图像识别部分
实验所用图片:
实验代码输出部分:
可见识别后该二维码信息为网址链接。打开后为:
与二维码内容一致。可见识别正确。
实验 中间图像 输出部分:
test1:
test2:
test3:
test4:
test5:
test6:
test7:
按顺序,从test1到test7分别为:读入的图像、绘制三个检测定位框的图像、对读入原函数进行旋转后的图像、旋转处理后重新绘制检测定位框的图像、对旋转后图像绘制三角定位的图像、上一步处理后得到二维码顶点的图像以及最终提取得到的二维码图像。
2.二维码的视频识别部分
3.根据内容生成二维码部分
我们测试中,使用“This is our homework”作为二维码内容,使用“homework”作为二维码名称。
实验代码输出如下:
可以看到,新的二维码已经生成。进入./images查看如下:
homework.jpg:
再使用二维码图片识别对其进行识别得到如下输出:
可见输出内容正是存储的内容。
五、部分代码解释
1.检测三个定位图形
#计算得到三个定位矩阵 def getArea(img): tmp_img = img.copy() # 图像灰度化 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) 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(tmp_img, [i], -1, (255, 0, 0), 2) showImg(tmp_img,"getArea") # 返回找到的二维码区域轮廓 return points
首先对图像进行灰度化、二值化和中值滤波得到可被cv2.findContours()函数处理的图像,之后根据得到轮廓的数量进行处理。只有当轮廓有两个子轮廓时,才认为它可能是二维码所在区域,并使用cv2.arcLength()函数与cv2.approxPolyDP()函数获取角点坐标。当角点数量为4的时候,认为找到了定位矩阵。当发现的轮廓超过三个时,剔除不合条件的轮廓。最后绘制检测的轮廓。
2.检测二维码的四个顶点以及透视变换
#先计算交点,之后透视变换 def perspectTrans(img): img2 = img.copy() # 获取二维码区域 points = getArea(img) cent = findCorners(points) # 定位三角形 cv2.line(img2, cent['ul'][0], cent['ur'][0], (0, 255, 0), 3) cv2.line(img2, cent['ul'][0], cent['dl'][0], (0, 255, 0), 3) cv2.line(img2, cent['dl'][0], cent['ur'][0], (0, 255, 0), 3) showImg(img2, "perspect 1") # 处理左下角的定位点 pnt = dealLittleRects(cent['dl'][1]) pdl = pnt['dl'] line1 = (pnt['dl'], pnt['dr']) # 处理右上角的定位点 pnt = dealLittleRects(cent['ur'][1]) pur = pnt['ur'] line2 = (pnt['ur'], pnt['dr']) pdr = cross_point(line1, line2) # 处理左上角的定位点 pnt = dealLittleRects(cent['ul'][1]) pul = pnt['ul'] # 四个顶点 cv2.circle(img2, pdl, 5, (255, 0, 0), 5) cv2.circle(img2, pdr, 5, (255, 0, 0), 5) cv2.circle(img2, pul, 5, (255, 0, 0), 5) cv2.circle(img2, pur, 5, (255, 0, 0), 5) showImg(img2,"perspect 2") plane = np.array([[0, 0], [600, 0], [600, 600], [0, 600]], dtype="float32") source = np.array([pul, pur, pdr, pdl], dtype="float32") M = cv2.getPerspectiveTransform(source, plane) # 进行透视变换 img = cv2.warpPerspective(img, M, (600, 600)) return img
#计算每个定位矩形四个顶点位置及排序 def dealLittleRects(rect): littleRect = {} # 存储排序后的四个点 res = [] # 存储四个角点 max = 0 # 用于计算距离 for i in range(0, len(rect)): for j in range(i + 1, len(rect)): dis = distance(rect[i][0], rect[j][0]) # 计算两个点之间的距离 if (dis > max): max = dis x1 = rect[i][0] # 记录最大的两个点 x2 = rect[j][0] corex = min(x1[0], x2[0]) + math.fabs((x1[0] - x2[0]) / 2) # 中心点x坐标 corey = min(x1[1], x2[1]) + math.fabs((x1[1] - x2[1]) / 2) # 中心点y坐标 max = 0 for i in rect: squ = math.fabs(trianSquare(x1, x2, i[0])) # 计算面积 if squ > max: max = squ x3 = i[0] max = 0 for i in rect: dis = distance(x3, i[0]) # 计算距离 if dis > max: max = dis x4 = i[0] res.append(x1) res.append(x2) res.append(x3) res.append(x4) for i in range(0, len(res)): if res[i][0] < corex and res[i][1] < corey: lefttop = i littleRect['ul'] = res[lefttop] del res[lefttop] # 按照y/x比值排序 res = sorted(res, key=lambda x: x[1] / x[0]) littleRect['ur'] = res[0] # 右上角 littleRect['dr'] = res[1] # 右下角 littleRect['dl'] = res[2] # 左上角 return littleRect
首先使用getArea()和findCorner()得到定位矩阵以及顺序,之后cv2.line()函数绘制定位三角形。对每个定位矩形的四个顶点进行操作得到其位置及排序,之后得到二维码的四个顶点并使用cv2.getPerspectiveTransform()得到变换矩阵,最后使用 cv2.warpPerspective()得到透视变换后的二维码。
六、源代码
import math import os import cv2 import numpy as np import qrcode as qr from pyzbar.pyzbar import decode steps = 0 #展示图像 def showImg(img,step): if not os.path.exists('images/'): #判断所在目录下是否有该文件名的文件夹 os.mkdir('images/') global steps steps=steps + 1 path="./images"+"/ test"+str(steps) + " " + step +".jpg" cv2.imwrite(path, img) #求左上方点 def leftTop(centerPoint): minIndex = 0 minMultiple = 10000 # 比较三角形的三个点 multiple = (centerPoint[1][0] - centerPoint[0][0]) * (centerPoint[2][0] - centerPoint[0][0]) + ( centerPoint[1][1] - centerPoint[0][1]) * (centerPoint[2][1] - centerPoint[0][1]) if minMultiple > multiple: minIndex = 0 minMultiple = multiple # 计算第二个角点面积 multiple = (centerPoint[0][0] - centerPoint[1][0]) * (centerPoint[2][0] - centerPoint[1][0]) + ( centerPoint[0][1] - centerPoint[1][1]) * (centerPoint[2][1] - centerPoint[1][1]) if minMultiple > multiple: minIndex = 1 minMultiple = multiple # 计算第三个角点面积 multiple = (centerPoint[0][0] - centerPoint[2][0]) * (centerPoint[1][0] - centerPoint[2][0]) + ( centerPoint[0][1] - centerPoint[2][1]) * (centerPoint[1][1] - centerPoint[2][1]) if minMultiple > multiple: minIndex = 2 return minIndex #求剩下的四个点 def otherPoints(centerPoint, leftTopPointIndex): otherIndex = [] # 计算外积来确定剩余两个点 waiji = (centerPoint[(leftTopPointIndex + 1) % 3][0] - centerPoint[(leftTopPointIndex) % 3][0]) * ( centerPoint[(leftTopPointIndex + 2) % 3][1] - centerPoint[(leftTopPointIndex) % 3][1]) - ( centerPoint[(leftTopPointIndex + 2) % 3][0] - centerPoint[(leftTopPointIndex) % 3][0]) * ( centerPoint[(leftTopPointIndex + 1) % 3][1] - centerPoint[(leftTopPointIndex) % 3][1]) if waiji > 0: otherIndex.append((leftTopPointIndex + 1) % 3) otherIndex.append((leftTopPointIndex + 2) % 3) else: otherIndex.append((leftTopPointIndex + 2) % 3) otherIndex.append((leftTopPointIndex + 1) % 3) return otherIndex #计算二维码矩阵的旋转角度 def rotateAngle(leftTopPoint, rightTopPoint, leftBottomPoint): dy = rightTopPoint[1] - leftTopPoint[1] dx = rightTopPoint[0] - leftTopPoint[0] k = dy / dx # 计算斜率并转换为角度 angle = math.atan(k) * 180 / math.pi if leftBottomPoint[1] < leftTopPoint[1]: # 调整角度 angle -= 180 return angle #求距离 def distance(x, y): return math.sqrt((x[0] - y[0]) ** 2 + (x[1] - y[1]) ** 2) #根据划分的三角形计算面积 def trianSquare(a, b, c): return a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]) # 计算交点函数 def cross_point(line1, line2): x1 = line1[0][0] y1 = line1[0][1] x2 = line1[1][0] y2 = line1[1][1] x3 = line2[0][0] y3 = line2[0][1] x4 = line2[1][0] y4 = line2[1][1] k1 = (y2 - y1) * 1.0 / (x2 - x1) # 计算k1,由于点均为整数,需要进行浮点数转化 b1 = y1 * 1.0 - x1 * k1 * 1.0 # 整型转浮点型是关键 if (x4 - x3) == 0: # L2直线斜率不存在操作 k2 = None b2 = 0 else: k2 = (y4 - y3) * 1.0 / (x4 - x3) # 斜率存在操作 b2 = y3 * 1.0 - x3 * k2 * 1.0 if k2 == None: x = x3 else: # 求解x坐标 x = (b2 - b1) * 1.0 / (k1 - k2) # 求解y坐标 y = k1 * x * 1.0 + b1 * 1.0 return [int(x), int(y)] #计算线段的两个端点坐标 def getXY(line, len): # 沿着左上角的原点,作目标直线的垂线得到长度和角度 rho = line[0][0] theta = line[0][1] a = np.cos(theta) b = np.sin(theta) # 得到目标直线上的点 x0 = a * rho y0 = b * rho # 延长直线的长度,保证在整幅图像上绘制直线 x1 = int(x0 + len * (-b)) y1 = int(y0 + len * (a)) x2 = int(x0 - len * (-b)) y2 = int(y0 - len * (a)) return x1, y1, x2, y2 #求定位矩阵的顺序 def findCorners(points): cent = [] temp = [] for i in points: # 求重心 m = cv2.moments(i) cx = np.int0(m['m10'] / m['m00']) cy = np.int0(m['m01'] / m['m00']) temp.append((cx, cy)) cent.append([(cx, cy), i]) lefttop = leftTop(temp) other = otherPoints(temp, lefttop) cent = {'ul': cent[lefttop], 'ur': cent[other[0]], 'dl': cent[other[1]]} # 返回四个角点 return cent #计算旋转角 def findRotateAngle(points): cent = findCorners(points) angle = rotateAngle(cent['ul'][0], cent['ur'][0], cent['dl'][0]) return angle #计算得到三个定位矩阵 def getArea(img): tmp_img = img.copy() # 图像灰度化 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) 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(tmp_img, [i], -1, (255, 0, 0), 2) showImg(tmp_img,"getArea") # 返回找到的二维码区域轮廓 return points #比较函数 def cmp(x, y): return (x[0] - y[0]) / [x[1] - y[1]] #计算每个定位矩形四个顶点位置及排序 def dealLittleRects(rect): littleRect = {} # 存储排序后的四个点 res = [] # 存储四个角点 max = 0 # 用于计算距离 for i in range(0, len(rect)): for j in range(i + 1, len(rect)): dis = distance(rect[i][0], rect[j][0]) # 计算两个点之间的距离 if (dis > max): max = dis x1 = rect[i][0] # 记录最大的两个点 x2 = rect[j][0] corex = min(x1[0], x2[0]) + math.fabs((x1[0] - x2[0]) / 2) # 中心点x坐标 corey = min(x1[1], x2[1]) + math.fabs((x1[1] - x2[1]) / 2) # 中心点y坐标 max = 0 for i in rect: squ = math.fabs(trianSquare(x1, x2, i[0])) # 计算面积 if squ > max: max = squ x3 = i[0] max = 0 for i in rect: dis = distance(x3, i[0])# 计算距离 if dis > max: max = dis x4 = i[0] res.append(x1) res.append(x2) res.append(x3) res.append(x4) for i in range(0, len(res)): if res[i][0] < corex and res[i][1] < corey:# 选出左上角点 lefttop = i littleRect['ul'] = res[lefttop]# 保存左上角 del res[lefttop] # 按照y/x比值排序 res = sorted(res, key=lambda x: x[1] / x[0]) littleRect['ur'] = res[0]# 右上角 littleRect['dr'] = res[1] # 右下角 littleRect['dl'] = res[2] # 左下角 return littleRect # 返回排序后的四个角点 #先计算交点,之后透视变换 def perspectTrans(img): img2 = img.copy() # 获取二维码区域 points = getArea(img) cent = findCorners(points) # 定位三角形 cv2.line(img2, cent['ul'][0], cent['ur'][0], (0, 255, 0), 3) cv2.line(img2, cent['ul'][0], cent['dl'][0], (0, 255, 0), 3) cv2.line(img2, cent['dl'][0], cent['ur'][0], (0, 255, 0), 3) showImg(img2, "perspect 1") # 处理左下角的定位点 pnt = dealLittleRects(cent['dl'][1]) pdl = pnt['dl'] line1 = (pnt['dl'], pnt['dr']) # 处理右上角的定位点 pnt = dealLittleRects(cent['ur'][1]) pur = pnt['ur'] line2 = (pnt['ur'], pnt['dr']) pdr = cross_point(line1, line2) # 处理左上角的定位点 pnt = dealLittleRects(cent['ul'][1]) pul = pnt['ul'] # 四个顶点 cv2.circle(img2, pdl, 5, (255, 0, 0), 5) cv2.circle(img2, pdr, 5, (255, 0, 0), 5) cv2.circle(img2, pul, 5, (255, 0, 0), 5) cv2.circle(img2, pur, 5, (255, 0, 0), 5) showImg(img2,"perspect 2") plane = np.array([[0, 0], [600, 0], [600, 600], [0, 600]], dtype="float32") source = np.array([pul, pur, pdr, pdl], dtype="float32") M = cv2.getPerspectiveTransform(source, plane) # 进行透视变换 img = cv2.warpPerspective(img, M, (600, 600)) return img #视频二维码识别 def Recognize_Cam_QRcode(): # 打开摄像头 cap = cv2.VideoCapture(0) # 检查摄像头是否成功打开 if not cap.isOpened(): print("无法打开摄像头") return # 创建二维码检测器 qr_detector = cv2.QRCodeDetector() print("按下 'q' 键退出程序") while True: # 捕获视频帧 ret, frame = cap.read() if not ret: print("无法读取视频帧") break # 检测二维码 data, bbox, _ = qr_detector.detectAndDecode(frame) if bbox is not None: # 绘制二维码边框 for i in range(len(bbox)): point1 = tuple(bbox[i][0].astype(int)) point2 = tuple(bbox[(i + 1) % len(bbox)][0].astype(int)) cv2.line(frame, point1, point2, color=(0, 255, 0), thickness=2) # 如果检测到数据,显示数据内容 if data: print(f"检测到二维码: {data}") cv2.putText(frame, data, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) # 显示视频帧 cv2.imshow("二维码识别", frame) # 按下 'q' 键退出 if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() if __name__ == '__main__': operate = eval(input("What do you want to do? Sign in the number of operation.\n1.QRcode recognition\n2.QRcode creation\n")) if(operate == 1): operate_of_recognize = eval(input("What is the type of QRcode?\n1.Picture\n2.InVideo\n")) if(operate_of_recognize == 1): print(f"Thank you for using QR code recognition, please sign in the path of picture") pic = input("PATH :") #读取图片,并获取定位矩阵 img = cv2.imread(pic) showImg(img,"read") points = getArea(img) #计算旋转角,将图像旋转到与x轴平行 angle = findRotateAngle(points) height, width, _ = img.shape rotate_matrix = cv2.getRotationMatrix2D((width / 2, height / 2), angle=angle, scale=1) rotated_image = cv2.warpAffine( src=img, M=rotate_matrix, dsize=(max(width, height), max(width, height))) showImg(rotated_image,"rotated") #经过透视变换得到提取出的二维码 new_img = perspectTrans(rotated_image) showImg(new_img, "final") # 解码得到的二维码 for barcode in decode(new_img): mydata = barcode.data.decode() print("The QR code contains : \n" + mydata) else: Recognize_Cam_QRcode() elif(operate == 2): if not os.path.exists('images/'): #判断所在目录下是否有该文件名的文件夹 os.mkdir('images/') message = input("Please sign in the message you want to save\n") name = input("Please sign in the name you want\n") qrc = qr.make(message).save("./images/"+ name +".jpg") print(f"Succefully creation. You can find it in the directory \"images\" in the directory where the program resides ")
七、心得体会
这次实验极大地增强了我的动手能力和解决实际问题的能力,使我更加深入地了解了数字图像处理的魅力所在。整个实验中最令人兴奋的部分是看到自己编写的代码成功识别出二维码的内容,并能准确无误地显示出来。这不仅是对我们理论知识掌握程度的一个验证,更是对我们解决问题能力的一种提升。同时,它也激发了我对计算机视觉领域进一步探索的兴趣,期待未来能够在更多相关项目中应用所学知识。