image = cv2.imread(args["image"])
ratio = image.shape[0] / 500.0#这里记住变换的比例
orgi = image.copy()
image = resize(orgi,height=500)#这里我们只传入height参数,剩下的width函数会帮我们计算出来
#将图像调整好大小后,我们要进行预处理
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) #灰度化
gray = cv2.GaussianBlur(gray,(5,5),0) #高斯滤波,剔除干扰项
edges = cv2.Canny(gray,75,200) #边缘检测
print("STEP 1:边缘检测",image)
cv2.imshow("image",image)
cv2.imshow("edges",edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
#至此我已经得到了边缘检测的结果,也就是该图像中内容的大致轮廓,现在在该结果中提取轮廓的话会精确很多
cnts = cv2.findContours(edges.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1]
# 轮廓检测:输入为边缘检测的结果,但是cnts中有好多的轮廓,我只想保留最大的轮廓,按照外接矩形的面积排序,得到前五个最大的轮廓
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:5] #这里的面积不用传入参数,否则会报错,也不知道为啥
for c in cnts:
peri = cv2.arcLength(c,True)
# C表示输入的点集
# epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数,越小就越接近原始轮廓,越大就越接近规则图像(比如说矩形等等)
# True表示封闭的
approx = cv2.approxPolyDP(c,0.02*peri,True)
if len(approx)==4:
screenCnt = approx
break
# 如果我得到近似的结果有四个点,是一个矩形,就代表是我想要的,就break掉
# 这里的原因有二:因为我扫描的图像是一张方形的白纸,我希望将他完整的提取出
# 二是稍后会进行透视变换提取出文字,需要四个点的输入
print("STEP 2:获取轮廓")
cv2.drawContours(image,[screenCnt],-1,(0,0,255),2)
cv2.imshow("outline",image)
cv2.waitKey(0)
cv2.destroyAllWindows()
边缘检测后的结果
筛选出的较大轮廓中的近似轮廓结果为四点者
cv2.GuassianBlur
高斯滤波:cv2.GuassianBlur(img, ksize,sigmaX,sigmaY)
sigmaX,sigmaY分别表示X,Y方向的标准偏差。如果仅指定了sigmaX,则sigmaY与sigmaX相同.如果两者都为零,则根据内核大小计算它们。
特征:核中区域贡献率与距离区域中心成正比,权重与高斯分布相关。作用:高斯模糊在从图像中去除高斯噪声方面非常有用。
边缘检测
边缘检测的目的就是找到图像中亮度变化剧烈的像素点构成的集合,表现出来往往是轮廓。如果图像中边缘能够精确的测量和定位,那么,就意味着实际的物体能够被定位和测量,包括物体的面积、物体的直径、物体的形状等就能被测量。
cv2.canny(img,minval,maxval)
使用高斯滤波器,以平滑图像,滤除噪声。
计算图像中每个像素点的梯度强度和方向。
应用非极大值(Non-Maximum Suppression)抑制,以消除边缘检测带来的杂散响应。
应用双阈值(Double-Threshold)检测来确定真实的和潜在的边缘。
通过抑制孤立的弱边缘最终完成边缘检测。
approxPolyDP( curve,epsilon,closed,approxCurve=None)
求近似轮廓
curve表示输入的点集
epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数,越小就越接近原始轮廓,越大就越接近规则图像(比如说矩形等等),在这里是的精度为轮廓周长的百分之二
True表示封闭的
至此我们得到了原图像中我们所需要的部分的大致轮廓
将原图以及轮廓的四个点的坐标传入函数中
def order_points(pts):
rect = np.zeros((4,2),dtype="float32")
# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
# 计算左上,右下
s = pts.sum(axis=1) #求着四个点横纵坐标之和
rect[0] = pts[np.argmin(s)] #这四个点横纵坐标之和的最小值就对应的是左上角的点
rect[2] = pts[np.argmax(s)] #最大的对应的是右下角的点
# 计算左下,右上
diff = np.diff(pts,axis=1)
rect[3] = pts[np.argmax(diff)]
rect[1] = pts[np.argmin(diff)]
return rect
def four_point_transform(img,pts):
#透视变换的作用是提取出图像中的文字,传入的参数为原图像,与扫描部分的四个点的坐标
rect = order_points(pts) #rect中0123号对应的是左上、右上、右下、左下
(tl,tr,br,bl) = rect
#我们传入的四个点组成的是四边形,但不一定是矩形,所以我们要将四条边的长度分别算出
widthA = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tr[1]) ** 2))
widthB = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
maxWidth = max(int(widthA),int(widthB)) #保留最大值作为宽
heightA = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
heightB = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
maxHeight = max(int(heightA),int(heightB)) #保留最大值作为高
dst = np.array([
[0,0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]
],dtype="float32") #这就是我重新生成的图片的坐标(图片中是我在原图中传入轮廓括住的那些)
M = cv2.getPerspectiveTransform(rect,dst) #求得变换矩阵M
warped = cv2.warpPerspective(img,M,(maxWidth,maxHeight))
# 原始图像img,变换矩阵M,变换后的宽maxWidth和高maxHeight
# warped为变换后的图像
# 返回变换后结果
return warped
#------------------------------------------------------------------
warped = four_point_transform(orgi,screenCnt.reshape(4,2)*ratio)
#orig为原图像(没有经过尺寸变换的图像) screenCnt.reshape将之转换为四个点的坐标,乘ratio是将之还原回原先的比例
变换图像大小的函数
def resize(image,width=None,height=None,inter=cv2.INTER_AREA):
#这里的参数设为None意思就是在调用的时候可以不传这个参数,如果像image一样的话,就是必须要传的参数
dim = None
(h,w) = image.shape[:2] #得到图片的宽和高
if width is None and height is None: #若宽和高都是默认参数,说明转换后的尺寸与原图相同,返回原图
return image
if width is None: #如果只传入了height,我们需要帮他计算一下width
r = height/float(h)
dim = (int(w*r),height)
else: #相反的话我们就需要帮他计算一下height
r = width/float(w)
dim = (width,int(h*r))
resized = cv2.resize(image,dim,interpolation=inter)
return resized
关于python中的None:
- 在定义resize函数中有个参数=None,这里的参数设为None意思就是在调用的时候可以不传这个参数,如果像image一样的话,就是必须要传的参数。下面一段话说的挺清楚:
- 你调用参数的时候没有None,你调用函数必须给他传参,circle(这必须写参数传进去才能成功调用这个参数),如果定义函数的时候,你写了extent=None,说明调用这个函数的时候不传参数也可以调用这个函数,里边的代码也可以被执行,但是里边的代码逻辑你要规避这个参数为空会出现的报错问题。
关于None:
- None是一个特殊的常量。
- None和False不同。
- None不是0。
- None不是空字符串。
- None和任何其他的数据类型比较永远返回False。
- None有自己的数据类型NoneType。
- 你可以将None复制给任何变量,但是你不能创建其他NoneType对象。 Python中的None与NULL(即空字符)的区别:
- (1)是不同的一种数据类型表示该值是一个空对象,空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。
- 注意:[你可以将None赋值给任何变量,也可以将任何变量赋值给一个None值得对象.]
- (2)判断的时候,均是False
- (3)属性不同:使用dir()函数返回参数的属性、方法列表。如果参数包含方法dir(),该方法将被调用。如果参数不包含dir(),该方法将最大限度地收集参数信息。
确定传入四个点的位置
透视变换
下面图片引用xiaowei_cqu的博客【图像处理】透视变换 Perspective Transformation
M = cv2.getPerspectiveTransform(rect,dst)
求得变换矩阵M,rect为原图像中轮廓的四个顶点的坐标,dst为变换后图像的四个顶点的坐标
warped = cv2.warpPerspective(img,M,(maxWidth,maxHeight))
进行透视变换,参数中img为原始图像,M为变换矩阵,第三个参数为变换后的宽maxWidth和高maxHeight。warped为变换后的图像
利用到字符识别插件tesseract
# https://digi.bib.uni-mannheim.de/tesseract/
# 配置环境变量如E:\Program Files (x86)\Tesseract-OCR
# tesseract -v进行测试
# tesseract XXX.png 得到结果
# pip install pytesseract
# anaconda lib site-packges pytesseract pytesseract.py
# tesseract_cmd 修改为绝对路径即可
from PIL import Image
import pytesseract
import cv2
import os
preprocess = 'blur' #thresh
image = cv2.imread('scan.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
if preprocess == "thresh":
gray = cv2.threshold(gray, 0, 255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
if preprocess == "blur":
gray = cv2.medianBlur(gray, 3)
filename = "{}.png".format(os.getpid())
cv2.imwrite(filename, gray)
text = pytesseract.image_to_string(Image.open(filename))
print(text)
os.remove(filename)
cv2.imshow("Image", image)
cv2.imshow("Output", gray)
cv2.waitKey(0)
import numpy as np
import cv2
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("-1","--image",required=True,help="Path to the image to be scanned")
args = vars(ap.parse_args())
def order_points(pts):
rect = np.zeros((4,2),dtype="float32")
# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
# 计算左上,右下
s = pts.sum(axis=1) #求着四个点横纵坐标之和
rect[0] = pts[np.argmin(s)] #这四个点横纵坐标之和的最小值就对应的是左上角的点
rect[2] = pts[np.argmax(s)] #最大的对应的是右下角的点
# 计算左下,右上
diff = np.diff(pts,axis=1)
rect[3] = pts[np.argmax(diff)]
rect[1] = pts[np.argmin(diff)]
return rect
def four_point_transform(img,pts):
#透视变换的作用是提取出图像中的文字,传入的参数为原图像,与扫描部分的四个点的坐标
rect = order_points(pts) #rect中0123号对应的是左上、右上、右下、左下
(tl,tr,br,bl) = rect
#我们传入的四个点组成的是四边形,但不一定是矩形,所以我们要将四条边的长度分别算出
widthA = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tr[1]) ** 2))
widthB = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
maxWidth = max(int(widthA),int(widthB)) #保留最大值作为宽
heightA = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
heightB = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
maxHeight = max(int(heightA),int(heightB)) #保留最大值作为高
dst = np.array([
[0,0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]
],dtype="float32") #这就是我重新生成的图片的坐标(图片中是我在原图中传入轮廓括住的那些)
M = cv2.getPerspectiveTransform(rect,dst) #求得变换矩阵M
warped = cv2.warpPerspective(img,M,(maxWidth,maxHeight))
# 原始图像img,变换矩阵M,变换后的宽maxWidth和高maxHeight
# warped为变换后的图像
# 返回变换后结果
return warped
def resize(image,width=None,height=None,inter=cv2.INTER_AREA):
#这里的参数设为None意思就是在调用的时候可以不传这个参数,如果像image一样的话,就是必须要传的参数
dim = None
(h,w) = image.shape[:2] #得到图片的宽和高
if width is None and height is None: #若宽和高都是默认参数,说明转换后的尺寸与原图相同,返回原图
return image
if width is None: #如果只传入了height,我们需要帮他计算一下width
r = height/float(h)
dim = (int(w*r),height)
else: #相反的话我们就需要帮他计算一下height
r = width/float(w)
dim = (width,int(h*r))
resized = cv2.resize(image,dim,interpolation=inter)
return resized
image = cv2.imread(args["image"])
ratio = image.shape[0] / 500.0#这里记住变换的比例
orgi = image.copy()
image = resize(orgi,height=500)#这里我们只传入height参数,剩下的width函数会帮我们计算出来
#将图像调整好大小后,我们要进行预处理
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) #灰度化
gray = cv2.GaussianBlur(gray,(5,5),0) #高斯滤波,剔除干扰项
edges = cv2.Canny(gray,75,200) #边缘检测
print("STEP 1:边缘检测",image)
cv2.imshow("image",image)
cv2.imshow("edges",edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
#至此我已经得到了边缘检测的结果,也就是该图像中内容的大致轮廓,现在在该结果中提取轮廓的话会精确很多
cnts = cv2.findContours(edges.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1]
# 轮廓检测:输入为边缘检测的结果,但是cnts中有好多的轮廓,我只想保留最大的轮廓,按照外接矩形的面积排序,得到前五个最大的轮廓
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:5] #这里的面积不用传入参数,否则会报错,也不知道为啥
for c in cnts:
peri = cv2.arcLength(c,True)
# C表示输入的点集
# epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数,越小就越接近原始轮廓,越大就越接近规则图像(比如说矩形等等)
# True表示封闭的
approx = cv2.approxPolyDP(c,0.02*peri,True)
if len(approx)==4:
screenCnt = approx
break
# 如果我得到近似的结果有四个点,是一个矩形,就代表是我想要的,就break掉
# 这里的原因有二:因为我扫描的图像是一张方形的白纸,我希望将他完整的提取出
# 二是稍后会进行透视变换提取出文字,需要四个点的输入
print("STEP 2:获取轮廓")
cv2.drawContours(image,[screenCnt],-1,(0,0,255),2)
cv2.imshow("outline",image)
cv2.waitKey(0)
cv2.destroyAllWindows()
warped = four_point_transform(orgi,screenCnt.reshape(4,2)*ratio)
#orig为原图像(没有经过尺寸变换的图像) screenCnt.reshape将之转换为四个点的坐标,乘ratio是将之还原回原先的比例
warped = cv2.cvtColor(warped,cv2.COLOR_BGR2GRAY)
warped = cv2.threshold(warped,100,255,cv2.THRESH_BINARY)[1]
cv2.imwrite('scan_1.jpg',warped)
#展示结果
print("STEP 3:变换")
cv2.imshow('original',resize(orgi,height=500))
cv2.imshow('scanned',resize(warped,height=500))
cv2.waitKey(0)
cv2.destroyAllWindows()
我们要提取图像中我们需要的部分,可以先对其进行滤波、边缘检测,这样可以改善结果。
透视变换的流程,可以将图像重新投影到新的平面上,更加方便处理。
OCR字符识别(不知道为什么效果不好)