(注:部分代码参考作者 skyblue NG 博客修改,部分代码在新的版本不适用,所以作了修改,在3.7上可以正常运行。)
上节谈到如何通过自动查找边缘,获取文档轮廓来进行透视转换的问题,在图像文件比较清晰的情况下,这个转换还是很简单的,但是也存在先天的缺陷,就是实际应用所获得的图像并不是那么好获取文档轮廓,所以我们得另辟蹊径来解决这个问题,方法就是通过人工鼠标选取需要转换的区域来实现。这样可以适用于大多数情况。
整体的思路是这样的:
第一步:需要用到tkinter图形界面,构建一个GUI用于图像导入等操作
第二步:定义鼠标事件,对导入的图像选取ROI区域,这里通过鼠标标注ROI的四个角点坐标
第三步:调用上一节使用到的transform and scan 程序,把scan程序修改成一个函数
第四步:显示结果
看起来也不是很难,当然你得对tkinter有所了解,不过参照文档理解这些代码不难,而且我做了大量的注释,尽量减少代码理解的难度。关键是一定要自己动手验证一下。
马上进入第一步:构建一个GUI窗体
# -*- coding: utf-8 -*- from tkinter import * from tkinter.filedialog import askopenfilename #需要用到文件对话框 import cv2 import numpy as np from tkinter import ttk import win32clipboard as wcld import os from scan_module import wrapped root = Tk() # 创建根窗体 frm1 = Frame(root) # 把frm1加载到根窗体中 frm1.pack(side='top',anchor='e',ipadx=1,ipady=1) # 放置frm1框架的为止 frm2 = Frame(root) # 加载第二个框架到根窗体 frm2.pack(side='top',anchor='w',ipadx=1,ipady=3) root.title("ROI截取") Label(frm1,text="ROI截取 第一版",fg ="gray").pack(side="right") # 在框架1中加载一个标签 ttk.Button(frm2,text="打开文件",command=myopen).pack(side="left",ipadx=8) # 在框架2左边加载一个按钮并赋予按钮事件 ttk.Label(frm2,text=" 坐标:").pack(side="left",ipadx=0) # 在框架2左边再加载一个标签 msg = StringVar() ttk.Entry(frm2,width=60, textvariable = msg).pack(side="left",ipadx=0) # 紧接着在框架2左边再加载一个文本输入框 ttk.Button(frm2,text="复制",command=send_to_clibboard).pack(side="right",ipadx=3) # 在框架2右边加载一个按钮并赋予按钮事件 # 进入消息循环 root.mainloop() # tkinter一般只有执行mainloop()方法才能运行,才能创建窗口
构建完是这个样子的:
第二步:定义按钮 ‘打开文件’ 事件,myopen() 函数
filename = "" def myopen(): global filename,img,ROI # 通过askopenfilename()方法直接获取文档名称 filename = askopenfilename(filetypes=(("Template files", "*.tplate"), ("HTML files", "*.html;*.htm"), ("All files", "*.*") )) print (filename) img = cv2.imread(filename) ROI = img.copy() # setMouseCallback()创建了一个鼠标回调函数,每次在图像上单击鼠标左键再抬起的过程,都会分3次调用鼠标响应函数 # 这里调用的回调函数就是上面定义的on_mouse()函数,当鼠标激活打开的图像时,执行相应的操作。 cv2.namedWindow('src') cv2.setMouseCallback('src', on_mouse) cv2.imshow('src', img) cv2.waitKey(0) cv2.destroyAllWindows() 定义on_mouse() 函数: # -----------------------鼠标操作相关------------------------------------------ lsPointsChoose = [] #选取点的坐标列表 tpPointsChoose = [] pointsCount = 0 #鼠标点击的次数 count = 0 pointsMax = 4 #初始化选取点的数量 def on_mouse(event, x, y, flags, param): global img, point1, point2, count, pointsMax global lsPointsChoose, tpPointsChoose # 存入选择的点 global pointsCount # 对鼠标按下的点计数 global img2, ROI_bymouse_flag img2 = img.copy() # 此行代码保证每次都重新再原图画 避免画多了 if event == cv2.EVENT_LBUTTONDOWN: # 左键点击 pointsCount = pointsCount + 1 print('pointsCount:', pointsCount) point1 = (x, y) print (x, y) # 画出点击的点 cv2.circle(img2, point1, 10, (0, 255, 0), 2) # 将选取的点保存到list列表里 lsPointsChoose.append([x, y]) # 用于转化为array 提取多边形ROI tpPointsChoose.append((x, y)) # 用于画点 # ---------------------------------------------------------------------- # 将鼠标选的点用直线连起来 print(len(tpPointsChoose)) for i in range(len(tpPointsChoose) - 1): print('i', i) cv2.line(img2, tpPointsChoose[i], tpPointsChoose[i + 1], (0, 0, 255), 2) # ----------点击到pointMax时可以提取去绘图---------------- if (pointsCount == pointsMax): # -----------绘制感兴趣区域----------- cv2.line(img2, tpPointsChoose[0], tpPointsChoose[pointsMax-1], (0, 0, 255), 2) # 把多边形封闭,最后一个点和起始点连接 ROI_byMouse() # 调用绘制函数画出感兴趣区域 ROI_bymouse_flag = 1 i = 0 pointsCount = 0 tpPointsChoose = [] lsPointsChoose = [] cv2.imshow('src', img2) # -------------------------右键按下清除轨迹----------------------------- if event == cv2.EVENT_RBUTTONDOWN: # 右键点击 print("right-mouse") pointsCount = 0 tpPointsChoose = [] lsPointsChoose = [] cv2.imshow('src', img2) 再定义ROI_byMouse()函数 def ROI_byMouse(): global src, ROI, ROI_flag, mask2, lsPointsChoose, msg mask = np.zeros(img.shape, np.uint8) #print(lsPointsChoose) msg.set(lsPointsChoose)# 储存选择点坐标的列表到msg变量 #print(msg.get()) pts = np.array([lsPointsChoose], np.int32) # pts是多边形的顶点列表(顶点集) pts = pts.reshape((-1, 1, 2)) #print(pts) # 这里 reshape 的第一个参数为-1, 表明这一维的长度是根据后面的维度的计算出来的。 # OpenCV中需要先将多边形的顶点坐标变成顶点数×1×2维的矩阵,再来绘制 # --------------画多边形--------------------- #mask = cv2.polylines(mask, [pts], True, (255, 255, 255)) # -------------填充多边形--------------------- mask2 = cv2.fillPoly(mask, [pts], (255, 255, 255)) #cv2.imshow('mask', mask2) cv2.imwrite('mask.bmp', mask2) ROI = cv2.bitwise_and(mask2, img) wrap_img = wrapped(ROI) cv2.imwrite('ROI.bmp', ROI) cv2.imshow('ROI', ROI) cv2.imshow('wrap_image',wrap_img)
本例中还定义了一个 send_to_clibboard() 复制到剪切板函数用于查看选取的四个角点的坐标,主要用来验证效果,其实可以不用。不过你可以参考。
def send_to_clibboard(): wcld.OpenClipboard() wcld.EmptyClipboard() wcld.SetClipboardData(wcld.CF_UNICODETEXT, msg.get()) # 此处使用.CF_UNICODETEXT方法才能正常解析 wcld.CloseClipboard()
把上一篇的scan.py修改成scan_module()模块用来调用:
# 导入必要的库 #导入上一节构建的模块和函数 from transform import four_point_transform #记得安装scikit-image包,threshold-local函数帮助我们处理黑白图像 from skimage.filters import threshold_local import numpy as np import argparse import cv2 #imutils是一个很实用的图像处理库,比如resize/cropping/rotate等图像基本编辑 import imutils #这个module直接返回转换后的图像,用于其它程序调用 def wrapped(image): # 第一步 # 加载图像并计算新旧图像高度的比例,并拷贝一份,修改大小。 # 为了加快图像处理速度,同时使边缘检测步骤更加准确, # 我们将扫描图像的高度调整为500像素。 # 我们还特别注意跟踪图像的原始高度与新高度的比值, # 这将允许我们对原始图像而不是调整大小的图像执行扫描。 #img = cv2.imread(image) img = image ratio = img.shape[0] / 500.0 orig = img.copy() img = imutils.resize(img, height = 500) # 把图象转化为灰度, 并加模糊处理,然后查找边缘 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) edged = cv2.Canny(gray, 70, 250) # 显示原始图像和检测到的边缘图像,不需要显示 #print("STEP 1: Edge Detection") #cv2.imshow("Image", img) #cv2.imshow("Edged", edged) #cv2.waitKey(0) #cv2.destroyAllWindows() # 第二步 # 在边缘图像的基础上查找轮廓保留最大的一个,并在图像中标识出来 cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5] # 循环处理 for c in cnts: # 大致轮廓 peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True) # 如果查找的大致轮廓有四个角点,即假设为我们需要查找的 if len(approx) == 4: screenCnt = approx break # 显示文档的轮廓在这个模块中不需要显示 #print("STEP 2: Find contours of paper") #cv2.drawContours(img, [screenCnt], -1, (0, 255, 0), 2) #cv2.imshow("Outline", img) #cv2.waitKey(0) #cv2.destroyAllWindows() # 第三步 # 应用四点转换生成鸟瞰图 warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio) # 把变形的图像转化成灰度 warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) T = threshold_local(warped, 11, offset = 10, method = "gaussian") warped = (warped > T).astype("uint8") * 255 return warped
该函数返回转换后的图像。
好了,我们测试一下效果
查看一下视频,演示测试过程:
效果非常好。赶紧动起手来,设计一个属于自己的文档扫描仪吧。下一次尝试一下OCR,文本识别和输出。这个比较有挑战。