齐次坐标系( x , y , w x,y,w x,y,w)与常见的三维空间坐标系( x , y , z x,y,z x,y,z)不同,只有两个自由度,其中 w w w( w w w>0)对应坐标 x x x和 y y y的缩放尺度:
当 w w w=1与 w w w=0时:
从二维平面上看,( x , y , w x,y,w x,y,w)随 w w w的变化在从原点到( x , y x,y x,y)的蓝虚线示意的射线上滑动:
:齐次坐标系在计算机图像将3维物体投影到2维平面中起到很大的作用。
单应性变换是将一个平面内的点映射到另一个平面内的二维投影变换。(平面是指图像或者三维中的平面表面),对应的变换矩阵称为单应性矩阵。
用矩阵的形式可以理解为:
其中( x l x_l xl, y l y_l yl)是Left view图片上的点, ( x r x_r xr, y r y_r yr)是Right view图片上对应的点,H为单应性矩阵 ,使w=1来归一化点。
每一组匹配点( x i x_i xi, y i y_i yi)与( x i ′ x_i' xi′, y i ′ y_i' yi′)可以得出:
上式可以表示为:
变换上式可以得到:
写成矩阵 A H = 0 AH=0 AH=0形式:
根据对应点约束,每个对应点对可以写出两个方程,分别对应于 x x x和 y y y坐标。
由下式可以看出单应性矩阵H与aH其实完全一样(其中a≠0),
即点( x i x_i xi, y i y_i yi)无论经过 H H H还是 a H aH aH映射,变化后都是 ( x i ′ x_i' xi′, y i ′ y_i' yi′)。
如果使令a=1/ h 33 h_{33} h33,那么有:
可以看出单应性矩阵 H H H虽然有9个未知数,但只有8个自由度。
由齐次坐标系也可以看出,点的齐次坐标是依赖于其尺度定义的,所以, x = [ x , y , w ] = [ α x , α y , α w ] = [ x / w , y / w , 1 ] x=[x,y,w]=[αx,αy,αw]=[x/w,y/w,1] x=[x,y,w]=[αx,αy,αw]=[x/w,y/w,1]都表示同一个二维点。因此,单应性矩阵 H H H也仅依赖尺度定义,所以,单应性矩阵具有 8 个独立的自由度。
根据对应点约束,每个对应点对可以写出两个方程,分别对应于 x x x和 y y y坐标。因此,计算单应性矩阵 H H H需要4个对应点对。
DLT(Direct Linear Transformation,直接线性变换)是给定4个或者更多对应点对矩阵,来计算单应性矩阵 H H H的算法。将单应性矩阵 H H H作用在对应点对上,重新写出该方程,我们可以得到下面的方程:
我们显然很难用直接线性的方法来求得H的解,可以用SVD算法找到h的最小二乘解。具体参考奇异值分解
def H_from_points(fp,tp):
"""使用线性DLT方法,计算单应性矩阵H,使fp映射到tp。点自动进行归一化"""
if fp.shape != tp.shape:
raise RuntimeError('number of points do not match')
# 对点进行归一化(对数值计算很重要)
# ---映射起始点---
m = mean(fp[:2],axis=1)
maxstd = max(std(fp[:2],axis=1)) + 1e-9
C1 = diag([1/maxstd,1/maxstd,1])
C1[0][2] = -m[0]/maxstd
C1[1][2] = m[1]/maxstd
fp = dot(C1,fp)
#--- 映射对应点----
m = mean(tp[:2],axis=1)
maxstd = max(std(tp[:2],axis=1)) +1e-9
C2 = diag([1/maxstd,1/maxstd,1])
C2[0][2] = -m[0]/maxstd
C2[1][2] = -m[1]/maxstd
tp = dot(C2,tp)
#创建用于线性方法的矩阵,对于每个对应对,在矩阵中会出现两行数值
nbr_correspondences = fp.shape[1]
A = zeros((2*nbr_correspondences,9))
for i in range(nbr_correspondences):
A[2*i] = [-fp[0][i],-fp[1][i],-1,0,0,0,tp[0][i]*fp[0][i],tp[0][i]*fp[1][i],tp[0][i]]
A[2*i+1] = [0,0,0,-fp[0][i],-fp[1][i],-1,tp[1][i]*fp[0][i],tp[1][i]*fp[1][i],tp[0][i]]
U,S,V = linalg.svd(A)
H = V[8].reshape((3,3))
#反归一化
H = dot(linalg.inv(C2),dot(H,C1))
#归一化,然后返回
return H / H[2,2]
1)检查fp tp两张图像矩阵中行列的数目是否相同。如果不相同,函数将会抛出异常信息。
2)对这些点进行归一化操作,使其均值为 0,方差为 1。因为算法的稳定性取决于坐标的表示情况和部分数值计算的问题,所以归一化操作非常重要。
3)使用对应点来构造矩阵A。最小二乘解即为矩阵SVD分解后得矩阵V的最后一行。改行经过变形后得到矩阵H。
4)对这个矩阵进行反归一化处理。
5)对矩阵H进行反归一化,然后返回。
通俗的来说,仿射变换就是线性变换+平移,变换前是直线的,变换后依然是直线且直线比例保持不变。
不通俗的说,仿射变换是一种二维坐标到二维坐标之间的线性变换(相同平面),它保持了二维图形的“平直性”(直线经过变换之后依然是直线)和“平行性”(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变),但是角度会改变。任意的仿射变换都能表示为乘以一个矩阵(线性变换),再加上一个向量 (平移) 的形式。
由于仿射变换具有 6 个自由度,因此我们需要三个对应点对来估计矩阵 H H H。通过将最后两个元素设置为 0,即 h7=h8=0,仿射变换可以用上面的 DLT 算法估计得出。
def Haffine_from_points(fp, tp):
"""计算H仿射变换,使得tp是fp经过仿射变换H得到的"""
if fp.shape != tp.shape:
raise RuntimeError('number of points do not match')
# 对点进行归一化(对数值计算很重要)
# --- 映射起始点 ---
m = mean(fp[:2], axis=1)
maxstd = max(std(fp[:2], axis=1)) + 1e-9
C1 = diag([1/maxstd, 1/maxstd, 1])
C1[0][2] = -m[0]/maxstd
C1[1][2] = -m[1]/maxstd
fp_cond = dot(C1,fp)
# --- 映射对应点 ---
m = mean(tp[:2], axis=1)
C2 = C1.copy() # 两个点集,必须都进行相同的缩放
C2[0][2] = -m[0]/maxstd
C2[1][2] = -m[1]/maxstd
tp_cond = dot(C2,tp)
# 因为归一化后点的均值为0,所以平移量为0
A = concatenate((fp_cond[:2],tp_cond[:2]), axis=0)
U,S,V = linalg.svd(A.T)
# 如Hartley和Zisserman著的Multiplr View Geometry In Computer,Scond Edition所示,
# 创建矩阵B和C
tmp = V[:2].T
B = tmp[:2]
C = tmp[2:4]
tmp2 = concatenate((dot(C,linalg.pinv(B)),zeros((2,1))), axis=1)
H = vstack((tmp2,[0,0,1]))
# 反归一化
H = dot(linalg.inv(C2),dot(H,C1))
return H / H[2,2]
对图像块应用仿射变换,我们将其称为图像扭曲(或者仿射扭曲)
扭曲操作可以使用SciPy 工具包中的 ndimage 包来简单完成。命令为:
transformed_im = ndimage.affine_transform(im,A,b,size)
使用如上所示的一个线性变换 A 和一个平移向量 b 来对图像块应用仿射变换。选项参数 size 可以用来指定输出图像的大小。默认输出图像设置为和原始图像同样大小。
# -*- coding=utf-8 -*-
# name: nan chen
# date: 2021/4/08 11:11
from numpy import *
from matplotlib.pyplot import *
from scipy import ndimage
from PIL import Image
im = array(Image.open(r'D:\project3image\shangda001.jpg').convert('L'))
H = array([[1.4, 0.05, -100], [0.05, 1.5, -100], [0, 0, 1]])
im2 = ndimage.affine_transform(im, H[:2, :2], (H[0, 2], H[1, 2]))
gray()
subplot(121)
imshow(im)
axis('off')
subplot(122)
imshow(im2)
axis('off')
show()
目标:通过仿射扭曲变换将图像放置到另一幅图像中,使得它们能够和指定的区域或者标记物对齐
手动定位坐标点来调整图像中的位置过于繁琐
import cv2
img = cv2.imread(r'D:\project3image\ggp.jpg')
def on_EVENT_LBUTTONDOWN(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
xy = "%d,%d" % (x, y)
cv2.circle(img, (x, y), 1, (255, 0, 0), thickness=-1)
cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,
1.0, (0, 255, 0), thickness=1)
cv2.imshow("image", img)
cv2.namedWindow("image")
cv2.setMouseCallback("image", on_EVENT_LBUTTONDOWN)
cv2.imshow("image", img)
while (True):
try:
cv2.waitKey(100)
except Exception:
cv2.destroyAllWindows()
break
cv2.waitKey(0)
cv2.destroyAllWindows()
结果:
# -*- coding=utf-8 -*-
# name: nan chen
# date: 2021/4/6 10:36
from PCV.geometry import warp, homography
from PIL import Image
from pylab import *
# 两张图片
im1 = array(Image.open(r'D:\project3image\jmu001.jpg').convert('L'))
im2 = array(Image.open(r'D:\project3image\ggp.jpg').convert('L'))
# 设置映射的目标点
tp = array([[45, 308, 319, 53], [254, 247, 591, 599], [1, 1, 1, 1]])
# 使用仿射变换将im1放置在im2上,使im1图像的角和tp尽可能的靠近
im3 = warp.image_in_image(im1, im2, tp)
# 将图像灰度显示
figure()
gray()
subplot(131)
axis('off')
imshow(im1)
subplot(132)
axis('off')
imshow(im2)
subplot(133)
axis('off')
imshow(im3)
show()
# -*- coding=utf-8 -*-
# name: nan chen
# date: 2021/4/6 10:36
from PCV.geometry import warp, homography
from PIL import Image
from pylab import *
from scipy import ndimage
# 两张图片
im1 = array(Image.open(r'D:\project3image\jmu001.jpg').convert('L'))
im2 = array(Image.open(r'D:\project3image\ggp.jpg').convert('L'))
gray()
# 设置映射的目标点
tp = array([[45, 308, 319, 53], [254, 247, 591, 599], [1, 1, 1, 1]])
# 选定 im1 角上的一些点
m, n = im1.shape[:2]
fp = array([[0, m, m, 0], [0, 0, n, n], [1, 1, 1, 1]])
# 第一个三角形
tp2 = tp[:, :3]
fp2 = fp[:, :3]
# 计算 H
H = homography.Haffine_from_points(tp2, fp2)
im1_t = ndimage.affine_transform(im1, H[:2, :2], (H[0, 2], H[1, 2]), im2.shape[:2])
# 三角形的 alpha
alpha = warp.alpha_for_triangle(tp2, im2.shape[0], im2.shape[1])
im3 = (1 - alpha) * im2 + alpha * im1_t
# 第二个三角形
tp2 = tp[:, [0, 2, 3]]
fp2 = fp[:, [0, 2, 3]]
# 计算 H
H = homography.Haffine_from_points(tp2, fp2)
im1_t = ndimage.affine_transform(im1, H[:2, :2],(H[0, 2], H[1, 2]), im2.shape[:2])
# 三角形的 alpha 图像
alpha = warp.alpha_for_triangle(tp2, im2.shape[0], im2.shape[1])
im4 = (1 - alpha) * im3 + alpha * im1_t
imshow(im4)
axis('equal')
axis('off')
show()
|
|
|
|
第一张图中的p1为集美大学尚大楼的灰度图像,p2为带有广告牌的灰度图像,p3是通过完全图像的仿射扭曲将集美大学尚大楼映射至与p2的广告牌位置对齐,p4为使用两个三角形的仿射弯曲。通过修改tp = array([[45, 308, 319, 53], [254, 247, 591, 599], [1, 1, 1, 1]])
这一行的坐标点可以改变p1映射在p2中的位置,其中前四个数字代表四个角点的纵坐标,中间四个数字代表四个角点的横坐标,四个角点的顺序为从左上角开始按照逆时针方向排序,最后四个数字代表四个角点的α通道,四个1就表示四个角点的透明度均为不透明,将上述用代码获取的坐标修改之后便可以得到上面的效果。通过下面的细节图可以明显看出完全图像的仿射扭曲变换后映射到广告牌上的尚大楼图像边缘不太光滑,使用包含两个三角形的仿射变换的效果更好。
从上面的例子可以看出,三角形图像块的仿射扭曲可以完成角点的精确匹配。三角形图像块越多,则匹配的越精确。分段仿射扭曲是通过给定任意图像的标记点,通过将这些点进行三角剖分,然后使用仿射扭曲来扭曲每个三角形,然后将图像和另一幅图像的对应标记点扭曲对应。
# 三角剖分的函数
def triangulate_points(x, y):
"""二维点的 Delaunay 三角剖分"""
tri = Delaunay(np.c_[x, y]).simplices
return tri
def pw_affine(fromim,toim,fp,tp,tri):
""" Warp triangular patches from an image.
fromim = image to warp
toim = destination image
fp = from points in hom. coordinates
tp = to points in hom. coordinates
tri = triangulation. """
im = toim.copy()
# check if image is grayscale or color
is_color = len(fromim.shape) == 3
# create image to warp to (needed if iterate colors)
im_t = zeros(im.shape, 'uint8')
for t in tri:
# compute affine transformation
H = homography.Haffine_from_points(tp[:,t],fp[:,t])
if is_color:
for col in range(fromim.shape[2]):
im_t[:,:,col] = ndimage.affine_transform(
fromim[:,:,col],H[:2,:2],(H[0,2],H[1,2]),im.shape[:2])
else:
im_t = ndimage.affine_transform(
fromim,H[:2,:2],(H[0,2],H[1,2]),im.shape[:2])
# alpha for triangle
alpha = alpha_for_triangle(tp[:,t],im.shape[0],im.shape[1])
# add triangle to image
im[alpha>0] = im_t[alpha>0]
return im
def plot_mesh(x,y,tri):
""" Plot triangles. """
for t in tri:
t_ext = [t[0], t[1], t[2], t[0]] # add first point to end
plot(x[t_ext],y[t_ext],'r')
上述的三个函数都可以在PCV库中warp.py代码中找到,可以导入包直接调用。下面的代码是对上面的函数进行调用来测试分段仿射扭曲匹配的结果。
代码:
from PCV.geometry import warp
from PIL import Image
from pylab import *
# 打开图像,并将其扭曲
fromim = array(Image.open(r'D:\project3image\jmu001.jpg').convert('L'))
x, y = meshgrid(range(5), range(6))
x = (fromim.shape[1] / 4) * x.flatten()
y = (fromim.shape[0] / 5) * y.flatten()
# 三角剖分
tri = warp.triangulate_points(x, y)
# 打开图像
im = array(Image.open(r'D:\project3image\ggp.jpg').convert('L'))
gray()
imshow(im)
# 手工选取目标点
tp = plt.ginput(30)
for i in range(0, len(tp)):
tp[i] = list(tp[i])
tp[i][0] = int(tp[i][0])
tp[i][1] = int(tp[i][1])
tp = array(tp)
# 将点转换成齐次坐标
fp = vstack((y, x, ones((1, len(x)))))
tp = vstack((tp[:, 1], tp[:, 0], ones((1, len(tp)))))
# 扭曲三角形
im = warp.pw_affine(fromim, im, fp, tp, tri)
# 绘制图像
figure()
imshow(im)
# 绘制三角形
warp.plot_mesh(tp[1], tp[0], tri)
axis('off')
show()
手工选取目标点:(必须按照从左到右 从上往下的顺序整齐的选取目标的,方可得到想要的结果,选取的目标点越准确,匹配的结果越好,如果是随意选取的话将得到错乱的结果)
由上图可以看出多个三角形的选取匹配后的结果要比两个三角形匹配的结果要好,理论上来说只要选取的点足够精确,可以达到很好的效果。
(1)运行代码时遇到ModuleNotFoundError: No module named ‘matplotlib.delaunay’
解决方法:
将import matplotlib.delaunay as md
改为from scipy.spatial import Delaunay
后将wrap.py中的triangulate_points函数中的语句替换为
tri = Delaunay(np.c_[x,y]).simplices
(2)直接运行教材中分段仿射扭曲代码,由于没有txt文件,若直接用数组取值,会出现下图的错误。
解决方法:使用ginput() 函数手工选取。
知乎-单应性变换