OpenCV中的透视变换方法详解
文章目录
- 引言
- 1. 什么是透视变换
- 2. 透视变换的数学原理
- 3. OpenCV中的透视变换代码实现
- 3.1 首先定义四个函数
- 3.1.1 cv_show() 函数
- 3.1.2 def resize() 函数
- 3.1.3 order_points() 函数
- 3.1.4 four_point_transform() 函数
- 3.2 读取图片并做预处理
- 3.3 轮廓检测
- 3.4 获取最大轮廓
- 3.5 透视变换矫正
- 3.6 保存矫正后的图片并显示窗口属性
- 4. 结语
引言
透视变换是计算机视觉中一项重要的图像处理技术,它能够将图像从一个视角投影到另一个视角,广泛应用于文档校正、增强现实、视角转换等场景。本文将深入探讨OpenCV中透视变换的原理与实现方法。
1. 什么是透视变换
透视变换(Perspective Transformation)是一种将图像从一个平面投影到另一个平面的变换方式。与仿射变换不同,透视变换能够处理"近大远小"的透视效果,更真实地模拟人眼观察世界的视角变化。
2. 透视变换的数学原理
透视变换可以用一个3×3的变换矩阵表示:
[a11 a12 a13]
[a21 a22 a23]
[a31 a32 a33]
对于原始图像中的点(x,y),变换后的坐标(x’,y’)计算方式为:
x' = (a11*x + a12*y + a13) / (a31*x + a32*y + a33)
y' = (a21*x + a22*y + a23) / (a31*x + a32*y + a33)
3. OpenCV中的透视变换代码实现
3.1 首先定义四个函数
3.1.1 cv_show() 函数
def cv_show(name,img):cv2.imshow(name, img)cv2.waitKey(0)
这段代码是用来输入图像并将其显示出来
3.1.2 def resize() 函数
def resize(image,width=None,height=None,inter=cv2.INTER_AREA):dim = None(h,w)=image.shape[:2]if width is None and height is None:return imageif width is None:r = height/float(h)dim = (int(w*r),height)else:r = width/float(w)dim = (width,int(h*r))resized = cv2.resize(image,dim,interpolation=inter)return resized
这段代码定义了一个名为 resize 的函数,用于调整图像的大小(缩放),同时保持图像的宽高比例(aspect ratio)。它使用 OpenCV(cv2)来实现图像缩放。
函数参数说明:
- image: 输入的图像(NumPy 数组格式,OpenCV 默认读取的图像)。
- width (可选): 目标宽度(如果提供,则按宽度缩放)。
- height (可选): 目标高度(如果提供,则按高度缩放)。
- inter (可选): 插值方法(默认 cv2.INTER_AREA,适用于缩小图像)。
函数逻辑解析
1.获取原始尺寸:
(h, w) = image.shape[:2] # 获取图像的高度和宽度
2.检查是否传入 width 或 height:
- 如果两者都未提供(width is None and height is None),直接返回原图。
- 如果只提供 height(width is None),则按高度缩放,宽度按比例计算:
r = height / float(h) # 计算缩放比例
dim = (int(w * r), height) # 新尺寸:(宽度按比例缩放, 目标高度)
- 如果提供 width(不管 height 是否提供),则按宽度缩放,高度按比例计算:
r = width / float(w) # 计算缩放比例
dim = (width, int(h * r)) # 新尺寸:(目标宽度, 高度按比例缩放)
3.执行缩放:
resized = cv2.resize(image, dim, interpolation=inter) # 使用 OpenCV 进行缩放
4.返回缩放后的图像:
return resized
总结
- 用途:保持宽高比的情况下缩放图像,避免直接 cv2.resize 可能导致的变形。
适用场景:
- 需要固定宽度或高度,但保持比例不变。
- 适用于图像预处理(如深度学习输入尺寸调整)。
注意:
- 如果同时传入 width 和 height,此函数仍然只会按其中一个参数缩放(优先 width)。
- 如果要强制指定宽高(可能变形),直接用 cv2.resize(image, (width, height))
3.1.3 order_points() 函数
def order_points(pts):# 一共4个坐标点rect = np.zeros((4,2),dtype="float32") # 用来存储排序之后的坐标位置# 按顺序找到对应坐标0123分别是 左上、右上、右下、左下s = pts.sum(axis=1) #对pts矩阵的每一行进行求和操作,(x+y)rect[0] = pts[np.argmin(s)]rect[2] = pts[np.argmax(s)]diff = np.diff(pts,axis=1) #对pts矩阵的每一行进行求差操作,(y-x)rect[1] = pts[np.argmin(diff)]rect[3] = pts[np.argmax(diff)]return rect
这段代码定义了一个名为 order_points 的函数,用于对给定的 4个二维坐标点 进行排序,使其按照 左上、右上、右下、左下 的顺序排列。
逐步解析
输入:
pts:一个形状为 (4, 2) 的 NumPy 数组,表示 4 个点的 (x, y) 坐标。例如:
pts = np.array([[x1, y1], [x2, y2], [x3, y3], [x4, y4]])
输出:
rect:排序后的 (4, 2) 数组,顺序为 左上、右上、右下、左下。
排序逻辑
1.初始化存储数组
rect = np.zeros((4, 2), dtype="float32") # 存储排序后的坐标
2.计算 x + y 并找到左上和右下点
- 左上点(rect[0]):x + y 最小的点(因为左上角的 x 和 y 都较小)。
- 右下点(rect[2]):x + y 最大的点(因为右下角的 x 和 y 都较大)。
s = pts.sum(axis=1) # 计算每个点的 x + y
rect[0] = pts[np.argmin(s)] # 左上点
rect[2] = pts[np.argmax(s)] # 右下点
3.计算 y - x 并找到右上和左下点
- 右上点(rect[1]):y - x 最小的点(因为右上角的 y 较小,x 较大)。
- 左下点(rect[3]):y - x 最大的点(因为左下角的 y 较大,x 较小)。
diff = np.diff(pts, axis=1) # 计算 y - x
rect[1] = pts[np.argmin(diff)] # 右上点
rect[3] = pts[np.argmax(diff)] # 左下点
4.返回排序后的坐标
return rect
注意事项
- 输入必须是 4 个点,否则会报错。
- 适用于凸四边形,如果点排列异常(如交叉),可能排序错误。
- 如果 4 个点本身是 旋转的矩形(如 45° 倾斜),该方法仍然有效。
3.1.4 four_point_transform() 函数
def four_point_transform(image,pts):# 获取输入坐标点rect = order_points(pts)(tl,tr,br,bl) = rect# 计算输入的w和h值widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))maxWidth = max(int(widthA),int(widthB))heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[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)warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))# 返回变换后的结果return warped
这段代码定义了一个名为 four_point_transform 的函数,用于对图像进行 透视变换(Perspective Transformation),将图像中的任意四边形区域 矫正为一个 矩形。
代码解析
输入参数
- image:输入的图像(OpenCV 格式,即 NumPy 数组)。
- pts:一个形状为 (4, 2) 的 NumPy 数组,表示待矫正的 4 个角点坐标(顺序任意)。
输出
- warped:矫正后的图像(矩形视角)。
执行步骤
1.对 4 个点进行排序(使用 order_points 确保顺序为 左上、右上、右下、左下):
rect = order_points(pts) # 排序后的 4 个点:[tl, tr, br, bl]
(tl, tr, br, bl) = rect # tl: 左上, tr: 右上, br: 右下, bl: 左下
2.计算矫正后的目标宽度 maxWidth:
计算底边宽度(br 到 bl 的距离):
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
计算顶边宽度(tr 到 tl 的距离):
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
取最大值作为最终宽度:
maxWidth = max(int(widthA), int(widthB))
3.计算矫正后的目标高度 maxHeight:
计算右侧高度(tr 到 br 的距离):
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
计算左侧高度(tl 到 bl 的距离):
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
取最大值作为最终高度:
maxHeight = max(int(heightA), int(heightB))
4.定义目标矩形坐标 dst:
dst = np.array([[0, 0], # 左上[maxWidth - 1, 0], # 右上[maxWidth - 1, maxHeight - 1], # 右下[0, maxHeight - 1] # 左下
], dtype="float32")
5.计算透视变换矩阵 M:
M = cv2.getPerspectiveTransform(rect, dst)
- rect:原始 4 个点(四边形)。
- dst:目标 4 个点(矩形)。
6.执行透视变换:
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
- image:输入图像。
- M:透视变换矩阵。
- (maxWidth, maxHeight):输出图像的尺寸。
7.返回矫正后的图像:
return warped
总结
four_point_transform 的作用是:
- 1.对 4 个点进行排序(order_points)。
- 2.计算目标矩形的尺寸(maxWidth 和 maxHeight)
- 3.计算透视变换矩阵(cv2.getPerspectiveTransform)
- 4.执行透视变换(cv2.warpPerspective),将任意四边形矫正为矩形
这样,我们就可以将 倾斜拍摄的物体 转换为 正面视角,便于后续处理。
3.2 读取图片并做预处理
#读取输入
image = cv2.imread('fapiao.jpg')
cv_show('image',image)#图片过大,进行缩小处理
ratio = image.shape[0] / 500.0 #计算最小比率
orig = image.copy()
image = resize(orig,height=500)
cv_show('1',image)
读取一张发票图片,并进行缩小处理。
图片如下:
3.3 轮廓检测
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)edged = cv2.threshold(gray,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cnts= cv2.findContours(edged.copy(),cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[-2]
image_contours = cv2.drawContours(image.copy(),cnts,-1,(0,0,255),1)
cv_show('image_contours',image_contours)
这段代码的作用是 对输入图像进行边缘检测并绘制轮廓,以下是逐步解析:
1. 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- 功能:将彩色图像(BGR格式)转换为灰度图(单通道)。
- 为什么需要:简化后续处理,减少计算量。
2.二值化处理(阈值分割
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
功能:使用 Otsu算法 自动计算最佳阈值,将灰度图转换为黑白二值图。
- cv2.THRESH_BINARY:二值化(大于阈值的设为255,否则为0)。
- cv2.THRESH_OTSU:自动确定最佳阈值(适合双峰直方图的图像)。
输出:edged 是一个二值图像,白色(255)代表目标,黑色(0)代表背景。
3. 查找轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
功能:在二值图像中查找所有轮廓。
- edged.copy():确保不修改原始二值图像。
- cv2.RETR_LIST:检测所有轮廓,不建立层级关系。
- cv2.CHAIN_APPROX_SIMPLE:压缩轮廓点(例如直线只保留端点)。
返回值:cnts 是一个列表,每个元素是一个轮廓(点的集合)。
4. 绘制轮廓
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
功能:在原图的副本上绘制所有轮廓。
- image.copy():避免修改原图。
- -1:绘制所有轮廓(若为 0 则只绘制第一个轮廓)。
- (0, 0, 255):轮廓颜色(红色,BGR格式)。
- 1:轮廓线宽(像素)。
5. 显示结果
cv_show('image_contours', image_contours)
功能:显示带轮廓的图像(假设 cv_show 是自定义的显示函数,等同于 cv2.imshow + cv2.waitKey)。
3.4 获取最大轮廓
screenCnt = sorted(cnts,key=cv2.contourArea,reverse=True)[0]peri = cv2.arcLength(screenCnt,True) #计算轮廓周长
screenCnt = cv2.approxPolyDP(screenCnt,0.05 * peri,True) #轮廓近似
print(screenCnt.shape)
image_contours = cv2.drawContours(image.copy(),[screenCnt],-1,(0,255,0),2)cv2.imshow("image_contours",image_contours)
cv2.waitKey(0)
这段代码的作用是从所有检测到的轮廓中筛选出面积最大的轮廓,并进行多边形近似,最终绘制出这个近似后的轮廓。以下是逐步解析:
1. 筛选面积最大的轮廓
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
功能:对轮廓列表 cnts 按面积从大到小排序,并选择面积最大的一个。
- cv2.contourArea:计算轮廓的面积。
- reverse=True:降序排列。
- [0]:取第一个(即最大轮廓)。
用途:假设图像中只有一个主要目标,直接取最大轮廓可排除噪声。
2. 计算轮廓周长
peri = cv2.arcLength(screenCnt, True)
功能:计算轮廓的周长。
- screenCnt:输入的轮廓点集。
- True:表示轮廓是闭合的(首尾相连)。
3.多边形近似(轮廓简化)
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, True)
功能:用更少的点近似轮廓,减少冗余点(如将弯曲边缘近似为直线)。
- screenCnt:原始轮廓点集。
- 0.05 * peri:近似精度(周长的5%作为阈值,值越小越接近原始形状)。
- True:轮廓闭合。
输出:近似后的轮廓点集(如果是矩形,会返回4个角点)。
示例:
- 输入:复杂轮廓(如几十个点组成的弯曲边缘)。
- 输出:简化后的多边形(如4个点组成的矩形)。
4.检查近似结果
print(screenCnt.shape)
用途:打印近似后轮廓的形状(如 (4, 1, 2) 表示4个点,每个点是 (x, y) 坐标)。
典型输出:
- 矩形:(4, 1, 2)
- 三角形:(3, 1, 2)
- 若点数过多(如 (10, 1, 2)),可能需要调整 0.05 参数。
5.绘制近似后的轮廓
image_contours = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 255, 0), 2)
功能:在原图的副本上绘制绿色(BGR格式 (0,255,0))的近似轮廓。
- [screenCnt]:将轮廓包装为列表(因 drawContours 接受列表输入)。
- -1:绘制所有轮廓(此处只有一个)。
- 2:线宽(像素)。
6.显示结果
cv2.imshow("image_contours", image_contours)
cv2.waitKey(0)
效果:显示带绿色轮廓的图像,标识出检测到的主要目标。
完整流程总结
- 输入:所有检测到的轮廓 cnts(来自 cv2.findContours)。
- 筛选:选择面积最大的轮廓。
- 简化:用多边形近似轮廓(如提取矩形角点)。
- 绘制:在原图上标出简化后的轮廓。
- 输出:可视化结果。
3.5 透视变换矫正
warped = four_point_transform(orig,screenCnt.reshape(4,2) * ratio)
功能:将原始图像 orig 中的四边形区域(screenCnt)矫正为正面视角的矩形。
- screenCnt.reshape(4, 2):将轮廓点从 (4, 1, 2) 转换为 (4, 2) 格式(4个点的x,y坐标)。
- ratio:如果预处理时图像被缩小过(如为了加速轮廓检测),需通过比例 ratio 将坐标映射回原始图像尺寸。
- four_point_transform:自定义函数(基于 cv2.getPerspectiveTransform 和
cv2.warpPerspective)。
3.6 保存矫正后的图片并显示窗口属性
cv2.imwrite('invoice_new.jpg', warped)
cv2.namedWindow('xx',cv2.WINDOW_NORMAL)
cv2.resizeWindow('xx',800,600)cv_show('xx',warped)
cv2.waitKey(0)
cv2.destroyAllWindows()
代码解析:
cv2.imwrite('invoice_new.jpg', warped)
功能:将矫正后的图像 warped 保存为 invoice_new.jpg。
用途:持久化处理结果,便于后续使用(如OCR识别、打印等)。
cv2.namedWindow('xx', cv2.WINDOW_NORMAL)
cv2.resizeWindow('xx', 800, 600)
功能:创建一个可调整大小的窗口,并设置初始尺寸为 800x600 像素。
- cv2.WINDOW_NORMAL:允许手动调整窗口大小(默认是固定大小的 cv2.WINDOW_AUTOSIZE)。
适用场景:当图像较大时,可通过缩放窗口查看完整内容。
cv2.imshow('xx', warped)
cv2.waitKey(0)
功能:在窗口 ‘xx’ 中显示矫正后的图像 warped,并等待用户按键关闭窗口。
注意:cv_show 可能是封装好的函数,若未定义需替换为 cv2.imshow + cv2.waitKey。
4. 结语
本篇博客到这里就结束啦,希望能帮助大家更好的理解和和使用OpenCV中的透视变换方法。其实方法并不难,只要我们能沉下心来慢慢学习,一定会有收获的。我一直坚信,努力会让我们收获一个更好的自己,加油!!!