opencv基础:文档透视(扭曲)矫正

2,343 阅读8分钟

上回说到公司HR姐姐了解我会图像矫正,提供很多倾斜图片让我处理,我都一一克服了。

后来了解到,她是在替他男朋友找源码。

得知真相的我眼泪掉了下来,从此再也不相信漂亮女生的言语。

就在我决定和她绝交的时候。HR姐姐发来信息,说她在宾馆609房间等我。

六、再续前缘

我还是决定去和她见面,探一探虚实,丰富一下人生阅历,即便以后写小说也会有素材。

不过,即便不去,我也已经基本猜到了。

她男朋友以及他的研发团队,在宾馆里封闭开发。这次叫我还是让我写代码的。

我到了宾馆,敲门进入了609房间。见到了她,果然,有很多人,很显眼的是,一个穿黑色皮衣的中年男人坐在椅子上,而她就站在旁边。

她看到我,有点不好意思,说到,又要麻烦你了。这次是我又遇到了一个问题,你看这样的图片如何矫正。

img4_0_perspective.jpg

七、warpPerspective 透视变换

我看到这张图,微微一笑。

我心想:当初为了给你矫正文档,我研究了大量的资料,这种可不是角度的倾斜,这是透视扭曲。

我先看了看那个皮衣男,那是她的男朋友吧,年纪不小了,哼哼,这点知识居然都没有掌握,而且也没有我长得帅。

我问:这是你扫描文件的时候,纸张下面垫了个玻璃瓶子吧?只有这种情况才能扫描成这样!

她说,你怎么看出来的。

我说,这叫透视扭曲。

比如下面这张图。

img1_0_origin.jpg

怎么才能达到你那种效果,只有通过立体感变换才可以,说白了就是远近角度的扭曲,不能直视它,要斜视,哪个角度看着难受哪个角度看。

你看着屏幕上的这张图,跑到天花板上看,或者趴到地板上看。要么你不动,让文件动,就像下面这样,让文件一部分接近你或者远离你,这就是从视觉上的扭曲,绘画上叫“远大近小”。

img4_1.gif

能扭曲,就能矫正。知道怎么扭曲的,倒放的步骤就能矫正。

import numpy as np
import cv2
# 读入图片
img = cv2.imread('img4_0_perspective.jpg')
img_size = (img.shape[1], img.shape[0])
# 确定需要矫正的区域,左上,左下,右下,右上
src = np.float32([[82,90],[82,368],[433,338],[433,124]])
# 确定需要矫正成的形状,和上面一一对应 
dst = np.float32([[82,90],[82,368],[433,368],[433,90]])

# 获取矫正矩阵,也就步骤
M = cv2.getPerspectiveTransform(src, dst)
# 进行矫正,把img
img = cv2.warpPerspective(img, M, img_size)

# 展示校正后的图形
cv2.imshow('output', img)
cv2.waitKey(0)

矫正后的图像如下:

img4_1_perspective.jpg

实现起来主要就是两步。

  • 第一通过cv2.getPerspectiveTransform获取矫正的转换钥匙(矩阵)。传入图像扭曲部分4个点的坐标,然传入矫正到4个点的坐标,坐标顺序没有要求,但是前后要对应(不然会翻车),就可以计算出转换钥匙。
  • 第二步,调用cv2.warpPerspective(img, M, img_size)进行矫正。第一个参数是要矫正的图像数据,第二个参数是转换的钥匙(矩阵),第三个参数是图像的大小。默认矫正后空白填充0值,也就是黑色。当然我们通过cv2.warpPerspective(img, M, img_size, borderValue=(255,255,255))可以设置成白色。

注意下图虚线处就是矫正前和校正后四个点的坐标。 img4_2.gif

八、得寸进尺 findContours、approxPolyDP

皮衣男看到结果后,微微点头,向右看去。

HR姐姐在皮衣男左边,右边是一个秃顶男。

秃顶男收到信号后,问我:传入参数调用方法执行即可,这也不难,敢问矫正前后的四个点,从何获取?你能自动识别吗?

img4_0_perspective.jpg

呜呼呀!这里居然还有懂行的人,实际应用中,哪有人给你手工找坐标点,肯定也得是自动计算。看来简单应付是不行了。

我微微一笑,这有何难!我飞快地用右手敲击键盘,用红绿两种颜色画出了扭曲框和矫正框。

img4_1_perspective.jpg

“怎么样!这效果还满意吧!”,我语气中故意将问号全部换成了叹号。

说吧,我就要走。

“壮士留步!”,秃顶男将我拦下:“可否留下代码,解读一下!”

“没有这个必要吧,你我萍水相逢,何况我还有公务在身,不便久留”,说完要走。

皮衣男瞅了瞅HR姐姐,HR姐姐把我拦了下来:“大郎……不是,大工程师,来都来了,不在乎这一时半会儿了,说说吧,就当帮我了!”

“帮你,凭什么帮你?!”,我心里想,但是没有说出口,我被潜意识控制,说出一个字:好!

img = cv2.imread('img4_0_perspective.jpg')
# 转为灰度单通道 [[255 255],[255 255]]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化图像
ret,img_b=cv2.threshold(gray,200,255,cv2.THRESH_BINARY_INV)

# 图像出来内核大小,相当于PS的画笔粗细   
kernel=np.ones((5,5),np.uint8)
# 图像膨胀
img_dilate=cv2.dilate(img_b,kernel,iterations=8)
# 图像腐蚀
img_erode=cv2.erode(img_dilate,kernel,iterations=3)
# 寻找轮廓
contours, hierarchy = cv2.findContours(img_erode,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) 
# 绘制轮廓
#cv2.drawContours(img,contours,-1,(255,0,255),1)  

# 一般会找到多个轮廓,这里因为我们处理成只有一个大轮廓
contour = contours[0]

# 每个轮廓进行多边形拟合
approx = cv2.approxPolyDP(contour, 150, True)
# 绘制拟合结果,这里返回的点的顺序是:左上,左下,右下,右上 
cv2.polylines(img, [approx], True, (0, 255, 0), 2)

# 寻找最小面积矩形
rect = cv2.minAreaRect(contour)
# 转化为四个点,这里四个点顺序是:左上,右上,右下,左下
box = np.int0(cv2.boxPoints(rect))
# 绘制矩形结果
cv2.drawContours(img, [box], 0, (0, 66, 255), 2)

img_size = (img.shape[1], img.shape[0])
# 同一成一个顺序:左上,左下,右下,右上 
src = np.float32(approx)
dst = np.float32([box[0],box[3],box[2],box[1]])

# 获取透视变换矩阵,进行转换
M = cv2.getPerspectiveTransform(src, dst)
img = cv2.warpPerspective(img, M, img_size, borderValue=(255,255,255))

cv2.imshow('output', img)
cv2.waitKey(0)

代码在此,告辞!

“壮士留步,代码都留了,何不解释一下!”,秃顶男说。

我没有理他,转身要走。HR姐姐又把我留下了。

“这张图,我们如何识别出轮廓。”

img4_0_perspective.jpg

灰度、二值化都是常规操作了,目的是转为黑白单通道,简化计算。

img4_1_threshold.jpg

怎么才能让他们融为一体,有一个方法就是膨胀。cv2.dilate(img_b,kernel,iterations=8)img_b是需要膨胀的图片,kernel是膨胀的内核,上面我们定义了5*5像素大小,iterations是膨胀次数。膨胀之后这样效果。

1633510742226.gif

这样看,他们就连为一体的,但是太胖了,我们还要收缩,收缩用腐蚀cv2.erode(img_dilate,kernel,iterations=3)

1633511561457.gif

为什么要膨胀完了再收缩?膨胀是为了融为一体,为了一体化必然会扩大,收缩是为了接近原始大小。

这样,基本的图形轮廓就出来了,我们利用cv2.findContours找到轮廓的点。

# 寻找轮廓
contours, hierarchy = cv2.findContours(img_erode,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) 
# 绘制轮廓
cv2.drawContours(img,contours,-1,(255,0,255),1)  

contours是图形中能找到的所有闭合的轮廓,如果是下面的图,那就是2个轮廓。

img4_2_dilate_6.jpg

数据是离散的点的集合,样式如下面这样[ [[[ 84, 101]], [[ 84, 103]],[[ 83, 104]]] , [[[ 84, 101]], [[ 84, 103]],[[ 83, 104]]] ],每个轮廓由多个点构成。轮廓之间有可能存在嵌套关系,比如甜甜圈那样,hierarchy就是说明这些之前的嵌套关系,本例子不涉及,此处不做详细说明。

我们的图,经过8次膨胀,已经合成一体是一个轮廓。所以,我们可以先按照contour = contours[0]处理这一个轮廓,轮廓的数据也是点的集合。

img4_4_drawContours.jpg

下面就是用cv2.approxPolyDP(contour, 150, True)做一个多边形拟合。approxPolyDP能够根据传入的多个点,得出一个包裹所有点的多边形,返回多边形的顶点。

未标题-1.jpg

approxPolyDP(curve,epsilon,closed)

  • 第一个参数curve是数据点。
  • 第二个参数epsilon是多边形的精度,数值越小越像曲线。
  • 第三个参数closed是指多边形是否闭合的。
# 每个轮廓进行多边形拟合
approx = cv2.approxPolyDP(contour, 150, True)
# 绘制拟合结果,这里返回的点的顺序是:左上,左下,右下,右上 
cv2.polylines(img, [approx], True, (0, 255, 0), 2)

通过approxPolyDP并绘制之后,结果如下:

6661.jpg

我们可以获得它的4个顶点,顺序是:左上,左下,右下,右上。这4个顶点就是矫正前的点。

我们可以利用minAreaRect获得最小面积矩形,作为期望被矫正后的轮廓。

# 寻找最小面积矩形
rect = cv2.minAreaRect(contour)
# 转化为四个点,这里四个点顺序是:左上,右上,右下,左下
box = np.int0(cv2.boxPoints(rect))
# 绘制矩形结果
cv2.drawContours(img, [box], 0, (0, 66, 255), 2)

矫正后的4个点,这4个点的顺序是:左上,右上,右下,左下。

666.jpg

有了前后的4个点,我们再去调用矫正方法,就可以做到自动矫正了。

1633524057570.gif

九、故事反转

“好好好!”,皮衣男拍着巴掌站了起来:“你讲的很通俗,我居然都听懂了,是个人才!”。

说完,他就走了。紧着着,其他人也都跟着走了,留下我一个人在房间里。

我还在发愣,这是什么情况,搞什么鬼?

我正在思考之际,HR姐姐急忙返回来,跟我说:你发达了!

说完她就急忙走了。