使用opencv实现图像的畸形矫正:仿射变换

news/2024/11/17 21:29:49/

1 仿射变换

1.1 什么是仿射变换

在图像处理中,经常需要对图像进行各种操作如平移、缩放、旋转、翻转等,这些都是图像的仿射变换。图像仿射变换又称为图像仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。通常图像的旋转加上拉升就是图像仿射变换,仿射变换需要一个M矩阵实现,但是由于仿射变换比较复杂,很难找到这个M矩阵.

1.2 仿射变换的数学表达

仿射变换也称仿射投影,是指几何中,对一个向量空间进行线性变换并接上一个平移,变换为另一个向量空间。所以,仿射变换其实也就是再讲如何来进行两个向量空间的变换
假设有一个向量空间k:

还有一个向量空间j:

 如果我们想要将向量空间由k变为j,可以通过下面的公式进行变换

将上式进行拆分可得

 

我们再将上式转换为矩阵的乘法 

通过参数矩阵M就可以实现两个向量空间之间的转换,在进行仿射变换的时候我们也只需要一个矩阵M就可以实现平移、缩放、旋转和翻转变换。

1.3 opencv中的仿射变换

OpenCV中使用warpAffine函数来实现仿射变换

cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]]) → dst
  • src:输入的图像数组
  • M:仿射变换矩阵
  • dsize:变换后图像的大小
  • flags:使用的插值算法
  • borderValue:边界的填充值

1.3.1 图像平移

在平面坐标系有点P(x,y)和点P′(x′,y′),如果我们想要将P点移动到P',通过下面的变换就可以实现

 其中Δx和Δy就是x方向上和y方向上的偏移量,我们将其转换为矩阵的形式

 上面的矩阵M就是仿射变换的平移参数,使用OpenCV中的warpAffine函数实现如下:

import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_cmp_img(original_img,transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()# 定义一个图像平移矩阵
# x向左平移(负数向左,正数向右)100
# y向下平移(负数向上,正数向下)200个像素
M = np.array([[1, 0, -100], [0, 1, 200]], dtype=np.float)# 读取需要平移的图像
img = cv2.imread("../data/girl02.jpg")# 将图片由BGR转为RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 定义平移后图像的大小,保持和原图大小一致
dsize = img.shape[:2][::-1]# 便于大家观察这里采用白色来填充边界
translation_img = cv2.warpAffine(img, M, dsize, borderValue=(255, 255, 255))# 显示图像
show_cmp_img(img, translation_img)

运行结果显示如下:

1.3.2 图像翻转

使用opencv的仿射变换实现图像的水平翻转、垂直翻转、镜像反转(同时进行水平和垂直翻转)

上图中的A、B、C、D表示图像的四个顶点,如果我们需要对图像进行水平翻转,那么我们就需要将 A点和B点进行交换,C点和D点进行交换,沿着x轴的中线进行对称交换位置,通过下面的式子可以实现水平翻转

上式中的w表示图像的宽度,同理可得垂直翻转的实现公式

上式中的h表示的是图像的高图像翻转的变换矩阵:

使用OpenCV中的warpAffine函数实现如下:

import cv2
import matplotlib.pyplot as plt
import numpy as npdef show_cmp_img(original_img,transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()horizontal_flip = True
vertical_flip = Trueimg = cv2.imread("../data/girl02.jpg")# 获取输入图片的宽和高
height,width = img.shape[:2]# 初始化变换矩阵
M = np.array([[0, 0, 0], [0, 0, 0]], dtype=np.float)# 水平翻转
if horizontal_flip:M[0] = [-1, 0, width]# 垂直翻转
if vertical_flip:M[1] = [0, -1, height]# 将图片由BGR转为RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 定义缩放后图片的大小
img_flip = cv2.warpAffine(img, M, (width, height))show_cmp_img(img, img_flip)

运行结果显示如下:

OpenCV的flip函数翻转图像

flip函数参数:

  • src:输入的图像数组
  • flipCode:图像翻转参数,1表示水平翻转,0表示垂直翻转,-1表示镜像翻转
img = cv2.imread("../data/girl02.jpg")#水平翻转
horizontal_flip_img = cv2.flip(img,1)#垂直翻转
vertical_flip_img = cv2.flip(img,0)#镜像翻转
mirror_flip_img = cv2.flip(img,-1)

numpy的索引翻转图像

img = cv2.imread("../data/girl02.jpg")#水平翻转
horizontal_flip_img = img[:,::-1]#垂直翻转
vertical_flip_img = img[::-1]#镜像翻转
mirror_flip_img = img[::-1,::-1]

 1.3.3 图像缩放

如果我们想要对坐标系的P点进行缩放操作,通过下面的公式就可以实现

 通过,在x和y前面添加一个缩放系数即可,同样我们将其转换为矩阵形式

通过上面的矩阵M我们就可以实现对图片的缩放,使用OpenCV中的warpAffine函数实现如下:

import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_cmp_img(original_img,transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()# 定义宽缩放的倍数
fx = 0.5# 定义高缩放的倍数
fy = 2# 定义一个图像缩放矩阵
M = np.array([[fx, 0, 0], [0, fy, 0]], dtype=np.float)# 读取图像
img = cv2.imread("../data/girl02.jpg")# 获取图片的宽和高
height, width = img.shape[:2]# 将图片由BGR转为RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 定义缩放后图片的大小
scale_img = cv2.warpAffine(img, M, (int(width*fx), int(height*fy)))# 显示图像
show_cmp_img(img, scale_img)

结果显示如下:

opencv中的resize函数也能实现一样的效果。

1.3.4 图像旋转

围绕原点旋转:我们先来看看一个二维平面上的点在围绕原点是如何旋转的

上图中点v在围绕原点旋转θ度之后得到了点v′,我们将坐标点用极坐标的形式来表示可以得到 v(rcosϕ,rsinϕ),所以v′(rcos(θ+ϕ),rsin(θ+ϕ))利用正弦和余弦将其展开可得

然后再将上式用矩阵M表示,可得

特别注意:我们在建立直角坐标系的时候是以左下角为原点建立的,然而对于图像而言是以左上角为原点建立的,所以我们需要对角度θ进行取反,结合三角函数的特性,M矩阵的表达式如下

还需要注意的是这里的角度都是弧度制,所以我们还需要对其进行转换,转换代码如下

#将角度转换为弧度制
radian_theta = theta/180 * np.pi

将图片围绕原点进行逆时针旋转θ度,opencv的代码实现如下:

import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_cmp_img(original_img,transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()theta = 30# 将角度转换为弧度制
radian_theta = theta/180 * np.pi# 定义围绕原点旋转的变换矩阵
M = np.array([[np.cos(radian_theta), np.sin(radian_theta), 0],[-np.sin(radian_theta), np.cos(radian_theta), 0]])
# 读取图像
img = cv2.imread("../data/girl02.jpg")# 定义旋转后图片的宽和高
height, width = img.shape[:2]# 将图片由BGR转为RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 围绕原点逆时针旋转\theta度
rotate_img = cv2.warpAffine(img, M, (width, height))# 显示图像
show_cmp_img(img,rotate_img)

 运行结果显示如下:

1.3.5 围绕任意点旋转

下图的v点在围绕点(a,b)旋转90度得到v′。可以将其等价于先将v点平移到v1​点,然后再将v1​点围绕原点旋转90度得到v2​点,最后再将v2​点沿着v点平移的反方向平移相同长度,最终得到v′。这样我们就将围绕任意坐标点旋转的问题转换成了围绕原点旋转的问题

我们来回顾一下,围绕原点旋转坐标的变换公式:

 

在围绕原点旋转变换公式的基础上,我们将其改进为围绕任意点c(a,b)旋转,我们现在原来的坐标进行平移,得到变换后的坐标,最后再沿着之前平移的反方向进行平移,就得到围绕任意点旋转的变换公式:

将其展开可得

 

将上式用矩阵M表示: 

上式中的c(a,b)表示旋转中心,因为坐标系问题需要对θ进行取反,最终M矩阵的表达式如下

使用opencv的代码如下:

import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_cmp_img(original_img,transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()img = cv2.imread("../data/girl02.jpg")theta = 30
height, width = img.shape[:2]# 定义围绕图片的中心旋转
point_x, point_y = int(width/2), int(height/2)# 将角度转换为弧度制
radian_theta = theta / 180 * np.pi# 定义围绕任意点旋转的变换矩阵
M = np.array([[np.cos(radian_theta), np.sin(radian_theta),(1-np.cos(radian_theta))*point_x-point_y*np.sin(radian_theta)],[-np.sin(radian_theta), np.cos(radian_theta),(1-np.cos(radian_theta))*point_y+point_x*np.sin(radian_theta)]])# 将图片由BGR转为RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 定义旋转后图片的宽和高
height, width = img.shape[:2]# 围绕原点逆时针旋转\theta度
rotate_img = cv2.warpAffine(img, M, (width, height))# 显示图像
show_cmp_img(img, rotate_img)

运行结果显示如下:

围绕图像中心旋转后的图片部分被裁剪掉了,如果我们想让旋转之后的图片仍然是完整,代码如下:

import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_cmp_img(original_img,transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()img = cv2.imread("../data/girl02.jpg")theta = 30
is_completed = True
height, width = img.shape[:2]# 定义围绕图片的中心旋转
point_x, point_y = int(width/2), int(height/2)# 将角度转换为弧度制
radian_theta = theta / 180 * np.pi# 定义围绕任意点旋转的变换矩阵
M = np.array([[np.cos(radian_theta), np.sin(radian_theta),(1-np.cos(radian_theta))*point_x-point_y*np.sin(radian_theta)],[-np.sin(radian_theta), np.cos(radian_theta),(1-np.cos(radian_theta))*point_y+point_x*np.sin(radian_theta)]])
# 将图片由BGR转为RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 定义旋转后图片的宽和高
height, width = img.shape[:2]# 判断旋转之后的图片是否需要保持完整
if is_completed:# 增大旋转之后图片的宽和高,防止被裁剪掉new_height = height * np.cos(radian_theta) + width * np.sin(radian_theta)new_width = height * np.sin(radian_theta) + width * np.cos(radian_theta)# 增大变换矩阵的平移参数M[0, 2] += (new_width - width) * 0.5M[1, 2] += (new_height - height) * 0.5height = int(np.round(new_height))width = int(np.round(new_width))
# 围绕原点逆时针旋转\theta度
rotate_img = cv2.warpAffine(img, M, (width, height))
# 显示图像
show_cmp_img(img, rotate_img)

运行结果显示如下:

2 使用opencv实现图像的畸形矫正

在日常处理图片过程中,我们经常遇到扭曲的图片,首先我们要对扭曲的图片进行校正,然后在送入深度模型进行处理,扭曲的图片如下所示:

为实现将倾斜的目标矫正过来,首先,我们需要使用轮廓检测等方法获取到目标的4个关键点坐标值;然后利用相应的变换获取到新的4个坐标点;接着利用这4对关键点计算出仿射变换矩阵M;最后应用仿射变换矩阵到目标中即可。步骤如下:

  • 读取输入图片;
  • 获取原始目标的4个坐标点(左上,左下,右上,右下);
  • 通过4个坐标点计算出新的坐标点;
  • 使用opencv计算仿射变换矩阵M;
  • 应用仿射变换进行变换并进行结果显示。

2.1 获取四个顶点坐标

def get4points(img: np.ndarray, thed, n):# 灰度和二值化gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)ret, binary = cv2.threshold(gray, thed, 255, cv2.THRESH_BINARY)# 搜索轮廓contours, hierarchy = cv2.findContours(binary,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)# 按轮廓长度选取需要轮廓len_list = []for i in range(len(contours)):len_list.append(len(contours[i]))# 选第二长的sy = np.argsort(np.array(len_list))[-n]# 寻找顶点sum_list = []dif_list = []for i in contours[sy]:sum = i[0][0]+i[0][1]sum_list.append(sum)dif_list.append(i[0][0]-i[0][1])id_lb = np.argsort(np.array(sum_list))id_lb2 = np.argsort(np.array(dif_list))lu_id , rd_id = id_lb[0], id_lb[-1]ld_id , ru_id = id_lb2[0], id_lb2[-1]points = np.array([contours[sy][lu_id][0], contours[sy][rd_id][0],contours[sy][ld_id][0], contours[sy][ru_id][0]])return points, contours, sy

2.2 仿射变换

def four_point_transform(image, pts):# 获取坐标点,并将它们分离开来rect = order_points(pts)(tl, tr, br, bl) = rect# 计算新图片的宽度值,选取水平差值的最大值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))# 构建新图片的4个坐标点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

2.3 完整代码

# coding=utf-8
import numpy as np
import cv2
import matplotlib.pyplot as pltdef get4points(img: np.ndarray, thed, n):# 灰度和二值化gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)ret, binary = cv2.threshold(gray, thed, 255, cv2.THRESH_BINARY)# 搜索轮廓contours, hierarchy = cv2.findContours(binary,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)# 按轮廓长度选取需要轮廓len_list = []for i in range(len(contours)):len_list.append(len(contours[i]))# 选第二长的sy = np.argsort(np.array(len_list))[-n]# 寻找顶点sum_list = []dif_list = []for i in contours[sy]:sum = i[0][0]+i[0][1]sum_list.append(sum)dif_list.append(i[0][0]-i[0][1])id_lb = np.argsort(np.array(sum_list))id_lb2 = np.argsort(np.array(dif_list))lu_id , rd_id = id_lb[0], id_lb[-1]ld_id , ru_id = id_lb2[0], id_lb2[-1]points = np.array([contours[sy][lu_id][0], contours[sy][rd_id][0],contours[sy][ld_id][0], contours[sy][ru_id][0]])return points, contours, sydef order_points(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 rectdef four_point_transform(image, pts):# 获取坐标点,并将它们分离开来rect = order_points(pts)(tl, tr, br, bl) = rect# 计算新图片的宽度值,选取水平差值的最大值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))# 构建新图片的4个坐标点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 warpeddef show_cmp_img(original_img, transform_img):_, axes = plt.subplots(1, 2)# 显示图像axes[0].imshow(original_img)axes[1].imshow(transform_img)# 设置子标题axes[0].set_title("original image")axes[1].set_title("transform image")plt.show()# 读取图片
image = cv2.imread('../data/warp01.png')points, _, _ = get4points(image, 127, 1)# 获取原始的坐标点
pts = np.array(points, dtype="float32")# 对原始图片进行变换
warped = four_point_transform(image, pts)show_cmp_img(image, warped)

运行结果显示如下:


http://www.ppmy.cn/news/1215731.html

相关文章

结合大模型进行降本增效之——自动化测试

软件测试中,有哪些步骤能结合大模型的AIGC和数据分析能力? 生成测试用例 利用GPT-3.5 Turbo的自然语言生成能力,让它根据需求自动生成测试用例。例如,你可以向GPT-3.5 Turbo提供关于某个功能或者页面的描述,然后让它生…

C/C++交换输出 2021年9月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C交换输出 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C交换输出 2021年9月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 输入两个整数a,b,将它们交换输出 2、输入输…

Ps:通过显示大小了解图像的打印尺寸

在 Photoshop 中,如果想了解文档窗口中的图像打印出来之后的实质大小,只要知道两个数值即可。 第一个数值是图像分辨率(也称“文档分辨率”)的大小,可在Ps菜单:图像/图像大小 Image Size对话框中查询或设置…

什么是状态机?

什么是状态机? 定义 我们先来给出状态机的基本定义。一句话: 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。 先来解释什么是“状态”( State )。现实事物是有不同状态的,例…

Redis解决缓存问题

目录 一、引言二、缓存三、Redis缓存四、缓存一致性1.缓存更新策略2.主动更新 五、缓存穿透六、缓存雪崩七、缓存击穿1.基于互斥锁解决具体业务2.基于逻辑过期解决具体业务 一、引言 在一些大型的网站中会有十分庞大的用户访问流量,而过多的用户访问对我们的MySQL数…

.net6+aspose.words导出word并转pdf

本文使用net6框架,aspose.word破解激活并兼容net6,导出word模板并兼容识别html并给其设置字体,前端直接浏览器下载,后端保存文件并返回文件流,还有批量导出并压缩zip功能 1、安装Aspose.Words的nuget包选择21.8.0 版本…

Skywalking流程分析_3(服务的准备、启动、关闭)

前文将SkyWalkingAgent.premain中的: SnifferConfigInitializer.initializeCoreConfig(agentArgs)pluginFinder new PluginFinder(new PluginBootstrap().loadPlugins())这两个方法分析完毕,下面继续分析premain方法其余部分 创建byteBuddy final By…

springboot整合openfeign配置微信小程序登录,并解决超时问题

在使用springcloud作为技术栈的时候,在rpc通信方面,有很多框架选择,例如dubbo,openfeign等。 OpenFeign是一个声明式的web服务客户端,它使得编写Web服务客户端变得非常容易。它使用基于注解的方式来定义和实现Web服务客…