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 plt
def 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 np
def 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 = True
img = 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 plt
def 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 plt
def 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 plt
def 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 plt
def 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.5
M[1, 2] += (new_height - height) * 0.5
height = 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 plt
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
def 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 rect
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
def 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)
运行结果显示如下: