前言
前面也分享过一些opencv的一些图像处理方式,那我今天介绍一个用opencv来提取合同、纸张或者证件的边框并去掉背景,将图像摆正的做法,然后也根据这个思路,介绍下校验是否填写,或者签名的一个思路。话不多说,来看下实现的效果图(图片是我无聊的时候乱写的纸,逃~),具体代码我会放在我的github https://github.com/Wangzg123/fileclipper 上,大家可以去下载
首先来检验介绍一下实现的步骤
1、将图像转为固定高度的图片(这一步是为了适配边缘检测的阈值,下面会介绍)
2、用opencv的边缘检测算法提取边缘
3、找出闭合的轮廓并计算他们的面积,提取最大面积
4、拟合最大面积的的四边形的4个顶点
5、通过opencv的透视变化拟合出长方形的纸张边缘
一、边缘检测
这一步通过opencv的Canny函数将边缘提取出来(具体的功能介绍请自己查阅),值得一提的是在canny前为防止一些噪点必须通过高斯模糊去噪点,然后也膨胀边缘使图像更容易闭合
import cv2
import numpy as np
# 固定尺寸
def resizeImg(image, height=900):
h, w = image.shape[:2]
pro = height / h
size = (int(w * pro), int(height))
img = cv2.resize(image, size)
return img
# 边缘检测
def getCanny(image):
# 高斯模糊
binary = cv2.GaussianBlur(image, (3, 3), 2, 2)
# 边缘检测
binary = cv2.Canny(binary, 60, 240, apertureSize=3)
# 膨胀操作,尽量使边缘闭合
kernel = np.ones((3, 3), np.uint8)
binary = cv2.dilate(binary, kernel, iterations=1)
return binary
path = r'C:\Users\wzg\Desktop\test.jpg'
outpath = r'C:\Users\wzg\Desktop\getCanny.jpg'
img = cv2.imread(path)
img = resizeImg(img)
print('shape =', img.shape)
binary_img = getCanny(img)
cv2.imwrite(outpath, binary_img)
# output: shape = (900, 420, 3)
![](https://i-blog.csdnimg.cn/blog_migrate/23b155c95554af80fcd1d75eadd0e6f9.png)
二、找出纸张边缘
通过findContours拟合出所有的轮廓,然后找出最大的轮廓即是纸质的边缘(因为拍摄的时候,我们的目标图形一般是最大的)
# 代码承接上文
# 求出面积最大的轮廓
def findMaxContour(image):
# 寻找边缘
_, contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# 计算面积
max_area = 0.0
max_contour = []
for contour in contours:
currentArea = cv2.contourArea(contour)
if currentArea > max_area:
max_area = currentArea
max_contour = contour
return max_contour, max_area
path = r'C:\Users\wzg\Desktop\test.jpg'
outpath = r'C:\Users\wzg\Desktop\findMaxContour.jpg'
img = cv2.imread(path)
img = resizeImg(img)
binary_img = getCanny(img)
max_contour, max_area = findMaxContour(binary_img)
cv2.drawContours(img, max_contour, -1, (0, 0, 255), 3)
cv2.imwrite(outpath, img)
![](https://i-blog.csdnimg.cn/blog_migrate/2e642ba3943d37be208726b16c4ccb28.png)
三、找出四边形的四个顶点
拟合最大面积的的四边形的4个顶点
# 代码承接上文
# 多边形拟合凸包的四个顶点
def getBoxPoint(contour):
# 多边形拟合凸包
hull = cv2.convexHull(contour)
epsilon = 0.02 * cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(hull, epsilon, True)
approx = approx.reshape((len(approx), 2))
return approx
path = r'C:\Users\wzg\Desktop\test.jpg'
outpath = r'C:\Users\wzg\Desktop\getBoxPoint.jpg'
img = cv2.imread(path)
img = resizeImg(img)
binary_img = getCanny(img)
max_contour, max_area = findMaxContour(binary_img)
boxes = getBoxPoint(max_contour)
for box in boxes:
cv2.circle(img, tuple(box), 5, (0, 0, 255), 2)
print(boxes)
cv2.imwrite(outpath, img)
![](https://i-blog.csdnimg.cn/blog_migrate/860422dacea1598e17b3cf89651b3b4d.png)
四、透视变换
至此为止,我们都是在resize后的图片上找那四个顶点,我们还必须根据这4个点映射回原图的点。而且我们得到的图像是一个梯形形状,这时我们还要通过透视变化改成长方形的形状。
# 代码承接上文
# 适配原四边形点集
def adaPoint(box, pro):
box_pro = box
if pro != 1.0:
box_pro = box/pro
box_pro = np.trunc(box_pro)
return box_pro
# 四边形顶点排序,[top-left, top-right, bottom-right, bottom-left]
def orderPoints(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
# 计算长宽
def pointDistance(a, b):
return int(np.sqrt(np.sum(np.square(a - b))))
# 透视变换
def warpImage(image, box):
w, h = pointDistance(box[0], box[1]), \
pointDistance(box[1], box[2])
dst_rect = np.array([[0, 0],
[w - 1, 0],
[w - 1, h - 1],
[0, h - 1]], dtype='float32')
M = cv2.getPerspectiveTransform(box, dst_rect)
warped = cv2.warpPerspective(image, M, (w, h))
return warped
path = r'C:\Users\wzg\Desktop\test.jpg'
outpath = r'C:\Users\wzg\Desktop\result.jpg'
image = cv2.imread(path)
ratio = 900 / image.shape[0]
img = resizeImg(image)
binary_img = getCanny(img)
max_contour, max_area = findMaxContour(binary_img)
boxes = getBoxPoint(max_contour)
boxes = adaPoint(boxes, ratio)
boxes = orderPoints(boxes)
# 透视变化
warped = warpImage(image, boxes)
cv2.imwrite(outpath, warped)
![](https://i-blog.csdnimg.cn/blog_migrate/a8e280c3f12c4a38fd50e8ba9c4ba6eb.png)
五、其他
至此为止,我们的纸张边缘提取、摆正已经完成了,可以知道通过opencv可以实现以上的功能,我这里还有一个解决一些校验一些必填选项的是否填写的思路。如下面两种图,我们要检测红框处是否签了名。
解决思路是这样的:
1、将两张合同通过我上面的算法提取出正四边形的纸张
2、通过合同的预设点设置检测区域(比如上面在空白合同的位置标注矩形的左上角和右下角)
3、裁剪出标注区域(同样像素)并做二值化处理(转为只有0 和 1的矩阵)
4、因为写了的和没写的合同像素值有差别,那么如校验的大于一个阈值,那么就签了名,反之亦然
解决效果可以如下示(我就不贴代码了,逃~)
![](https://i-blog.csdnimg.cn/blog_migrate/ba11cb2d554d6463bb7bc0b114bf2aa1.png)
总结
以上用opencv的方法介绍了怎么提取纸张(其实不管白纸都可以,只要是个物体即可)的边缘,然后通过透视变化摆正,最后提了一个是否签名等填写的检测的解决思路。本文具体代码可以到我github https://github.com/Wangzg123/fileclipper 上下载,本人水平有限,欢迎各位大神积极讨论或者给我意见(私信我即可)