文章目录

  • 图像到图像的映射
    • 1 单应性变换
      • 1.1 直接线性变换算法
      • 1.2 仿射变换
    • 2 图像扭曲
      • 2.1 图像中的图像
      • 2.2 分段仿射扭曲
      • 2.3 图像配准
    • 3 创建全景图
      • 3.1 RANSAC
      • 3.2 稳健的单应性矩阵估计
      • 3.3 拼接图像

图像到图像的映射

1 单应性变换

单应性变换是将一个平面内的点映射到另一个平面内的二维投影变换。

单应性变换具有很强的实用性,比如图像配准、图像纠正和纹理扭曲,以及创建全景图像。

本质上,单应性变换H,按照下边的方程映射二维中的点(齐次坐标意义下):

对于图像平面内的点,齐次坐标是一个非常有用的表示方式。点的齐次坐标是依赖于其尺度定义的,所以x=[x, y, w]=[αx, αy, αw]=[x/w, y/w,1]都表示同一个二维点。因此,单应性矩阵H也仅仅依赖尺度定义,所以单应性矩阵具有9个独立的自由度。

通常使用w=1来归一化,这样点具有唯一的图像坐标x和y,这样可以简单地使用一个矩阵来表示变换。

创建homography.py文件,使用下边的函数实现对点进行归一化和转换齐次坐标的功能:

def normalize(points):for row in points:row /= points[-1]return pointsdef make_homog(points):return vastack((points,ones((1,points,shape[1]))))

行点和变换处理时,我们会按照列优先的原则存储这些点。因此,n个二维点集将会存储为齐次坐标意义下的一个3 *n数组。这种格式使得矩阵乘法和点的变换操作更加容易。

在这些投影的变换中,有一些特别重要的变换。比如,仿射变换可以应用于图像扭曲、相似变换可以应用于图像配准。

1.1 直接线性变换算法

单应性矩阵可以由两幅图像中对应点对计算出来。一个完全射影变换具有8个自由度,根据对应点约束,每个对应点对可以写出两个方程,分别对应于x和y坐标。因此,计算单应性矩阵H需要4个对应点对。

DLT(直接线性变换)是给定4个或者更多对应点对矩阵,来计算单应性矩阵H的算法。将单应性矩阵H作用于对应点对上,重新写出一个齐次方程Ah=0,其中A是一个具有对应点对二倍数量行数的矩阵。将这些对应点对方程的系数堆叠到一个矩阵中,可以使用奇异值分解找到H的最小二乘解。

下面是实现该算法的代码:

def H_from_points(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-9C1 = diag([1/maxstd,1/maxstd,1])C1[0][2] = -m[0]/maxstdC1[1][2] = m[1]/maxstdfp = dot(C1,fp)m = mean(tp[:2],axis=1)maxstd = max(std(tp[:2],axis=1)) +1e-9C2 = diag([1/maxstd,1/maxstd,1])C2[0][2] = -m[0]/maxstdC2[1][2] = -m[1]/maxstdtp = 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]

上面函数的第一步操作是检查点对的两个数组中点的数目是否相同。如果不相同,函数抛出异常信息。

对这些点进行归一化操作,使其均值为0,方差为1。然后使用对应点对构造矩阵A。最小二乘解即为矩阵SVD分解后所得矩阵V的最后一行。该行经过变形后得到矩阵H。然后对着矩阵进行处理和归一化,返回输出。

1.2 仿射变换

由于仿射变换具有6个自由度,因此我们需要三个对应点对来估计矩阵H。通过将最后两个元素设置为0,即h7 = h8 = 0。仿射变换可以用上面的DLT算法估计得出。

下面的函数使用对应点对来计算仿射变换矩阵:

def Haffine_from_points(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-9C1 = diag([1/maxstd,1/maxstd,1])C1[0][2] = -m[0]/maxstdC1[1][2] = -m[1]/maxstdfp_cond = dot(C1,fp)m = mean(tp[:2],axis=1)C2 = C1.copy() C2[0][2] = -m[0]/maxstdC2[1][2] = -m[1]/maxstdtp_cond = dot(C2,fp)A = concatenate((fp_cond[:2],tp_cond[:2]),axis=0)U,S,V = linalg.svd(A.T)tmp = V[:2].TB = 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]

2 图像扭曲

对图像块应用仿射变换,即为图像扭曲。这一操作可以使用Scipy工具包中的ndimage包来简单完成。命令:

transformed_im = ndimage.affine_transform(im,A,b,size)

使用一个线性变换A和一个平移向量b来对图像块应用仿射变换。size用来指定输出图像的大小。默认输出图像设置为和原始图像同样大小。

了研究该函数是如何工作的,我们可以运行下面的命令:

from PIL import Image
from pylab import *
from scipy import ndimageim = array(Image.open('jimei2.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]))subplot(121)
gray()
axis('off')
imshow(im)subplot(122)
gray()
axis('off')
imshow(im2)show()

得到的结果如下图所示,可见输出图像中丢失的像素用零来填充:

图1 用仿射变换扭曲图像

2.1 图像中的图像

仿射扭曲的一个简单例子是将图像或者图像的一部分放置在另一幅图像中,使得他们能够和指定的区域或者标记物对齐。

以下函数的输入参数为两幅图像和一个坐标。该坐标为将一副图像放置到第二幅图象中的角点位置:

def image_in_image(im1,im2,tp):m,n = im1.shape[:2]fp = array([[0,m,m,0],[0,0,n,n],[1,1,1,1]])H = Haffine_form_points(tp,fp)im1_t = ndimage.affine_transform(im,H[:2,:2],(H[0,2],H[1,2]),im2.shape[:2])alpha = (im1_t>0)return (1-alpha)*im2 + alpha*im1_t

将扭曲的图像和第二幅图像融合,就创建alpha图像。该图像定义了每个像素从各个图像中获取的像素值成分多少。这里基于以下事实:扭曲的图像是在扭曲区域边界之外以0来填充的图像,来创建一个二值的alpha图像。严格意义上,需要在第一幅图象中的潜在0像素上加上一个小的数值,或者合理的处理这些0像素。

下面几行代码会将一幅图像插入另一幅图像:

im4= array(Image.open('jimei.jpg').convert('L'))
im5 = array(Image.open('guanggaopai.jpg').convert('L'))tp = array([[50,300,300,50],[10,10,600,550],[1,1,1,1]])
im6 = image_in_image(im4,im5,tp)
figure()
gray()
imshow(im6)
axis('equal')
axis('off')
show()

图2 用仿射变换将一幅图像放置到另一幅图像中

函数Haffine_from points() 会返回给定对应点对的最有仿射变换。在上面的例子中,对应点对为图像和公告牌的角点。如果透视效应比较弱,那么这种方法会返回很好的结果。

2.2 分段仿射扭曲

给定任意图像的标记点,通过将这些点进行三角剖分,然后使用仿射扭曲来扭曲每个三角形,我们可以将图像和另一幅图像的对应标记点扭曲对应。为了三角化这些点,我们经常使用狄洛克三角剖分方法。在matplotlib中有狄洛克三角剖分:

from PIL import Image
from pylab import *
import numpy as np
from scipy.spatial import Delaunayx,y  = array(np.random.standard_normal((2,100)))
"""centers, edges, tri, neighbors = md.delaunay(x, y"""
tri = Delaunay(np.c_[x,y]).simplicesfigure()for t in tri:t_ext = [t[0],t[1],t[2],t[0]]plot(x[t_ext], y[t_ext], 'r')plot(x,y,'*')
axis('off')
show()

图3 随机二维点集的狄洛克三角
现在让我们将该算法用于一个例子,在该例子中,在5*6的网格上使用30个控制点,将一幅图像扭曲到另一幅图像中的非平坦区域。

首先,我们需要写出一个用于分段仿射图像扭曲通用的扭曲函数。下面的代码实现该功能:

def pw_affine(fromim,toim,fp,tp,tri):im = toim_copy()is_color = len(fromim.shape) == 3 im_t = zeros(im.shape,'unit8')for t in tri:H = 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 = alpha_from_triangle(tp[:,t],im.shape[0],im.shape[1])im[alpha>0] = im_t[alpha>0]return im

在该代码中,我们首先见检查该图像是灰度图像还是彩色图像。如果图像为彩色图像,则对每个颜色通道进行扭曲处理。因为对于每个三角形来说,仿射变换是唯一确定的,所以我们这里使用Haffine_from_points()函数来处理。

2.3 图像配准

图像配准是对图像进行变换,使得变换后的图像能够在常见的坐标系中对齐。配准可以是严格配准,也可以是非严格配准。为了能够进行图像对比和更精细的图像分析,图像配准是一步非常重要的操作。

目前,很难找到一种普适的方法能够应对所有的配准情况,任何一种配准算法都必须考虑图像的成像原理、几何变形、噪声影响、配准精度等因素。不过,从原理上将,配准算法可以大致分为以下四个步骤:

(1)特征提取
采用人工或者自动的方法检测图像中的不变特征,如:闭合区域、边缘、轮廓、角点等。特征提取算法需要满足三个条件
(a)显著性,所提取的特征应该是比较明显的,分布广泛的、易于提取的特征;
(b)抗噪性,具有较强的噪声抑制能力且对成像条件的变化不敏感;
(c)一致性,能准确地检测出两幅图像的共有特征;

(2)特征匹配
通过特征描述算作及相似性度量来建立所提取的特征之间的对应关系。特征匹配常用到的区域灰度、特征向量空间分布和特征符号描述等信息。某些算法在进行特征匹配的同时也完成了变换模型参数的估计;

(3)变换模型估计
指根据待配准图像与参考图像之间的几何畸变的情况,选择能最佳拟合两幅图像之间变化的几何变换模型,可以分为全局映射模型和局部映射模型。其中,全局映射模型利用所有控制点信息进行全局参数估计;局部映射模型利用图像局部的特征分别进行局部参数估计。常见的变换模型包括仿射变换、透视变换、多项式变换等,其中最常用的是仿射变换和多项式变换。

(4)坐标变换与插值
将输入图像做对应的参数变换,使它与参考图像处于同一个坐标系下。由于图像变换后的坐标点不一定是整数,因此,需要考虑一定的插值处理操作。常用的插值方法包括:最近邻插值、双线性插值、双三次插值、B样条插值、高斯插值。

3 创建全景图

3.1 RANSAC

RANSAC是用来找到正确模型来拟合带有噪声数据的迭代方法。给定一个模型,例如点集之间的单应性矩阵,该算法的基本思想是数据中包含正确的点和噪声点,合理的模型能够在描述正确点的同时摒弃噪声点。

3.2 稳健的单应性矩阵估计

在使用RANSAC模块时,需要在相应的类中实现fit()和get_error()方法,剩下的就是正确使用ransac.py。

将下面的代码添加到homography.py中:

class RansacModel(object):  def __init__(self,debug=False):self.debug = debugdef fit(self, data):data = data.Tfp = data[:3,:4]tp = data[3:,:4]return H_from_points(fp,tp)def get_error( self, data, H):data = data.Tfp = data[:3]tp = data[3:]fp_transformed = dot(H,fp)fp_transformed = normalize(fp_transformed)return sqrt( sum((tp-fp_transformed)**2,axis=0) )

可以看到,这个类包含fit() 方法。该方法仅仅接受由ransac.py选择的4个对应点对,然后你和一个单应性矩阵。记住,4个点对是计算单应性矩阵所需的最少数目。由于get_error()方法对于每个对应点对使用该单应性矩阵,然后返回相应的平方距离之和。因此ransac算法能够判定哪些点对是正确的,那些是错误的。在实际中,我们需要在距离上使用一个阙值来决定哪些矩阵是合理的。为了使用方便,将下面的函数添加到homography.py 文件中:

def H_from_ransac(fp,tp,model,maxiter=1000,match_theshold=10):import ransacdata = vstack((fp,tp))H,ransac_data = ransac.ransac(data.T,model,4,maxiter,match_theshold,10,return_all=True)return H,ransac_data['inliers']

该函数同样允许提供阙值和最小期望的点对数目。最重要的参数是最大迭代次数:程序推出太早可能得到一个坏解;迭代次数太多会占用太多空间。函数的返回结果为单应性矩阵和对应该单应性矩阵的正确点对。类似于下面的操作,你可以将RANSAC算法应用于点对上:

def convert_points(j): ndx = matches[j].nonzero()[0]fp = homography.make_homog(l[j+1][ndx,:2].T)ndx2 = [int(matches[j][i]) for i in ndx]tp = homography.make_homog(l[j][ndx2,:2].T)return fp,tpmodel = homography.RansacModel()fp,tp = convert_points(1)
H_12 = homography.H_from_ransac(fp,tp,model)[0] fp,tp = convert_points(0)
H_01 = homography.H_from_ransac(fp,tp,model)[0] tp,fp = convert_points(2)
H_32 = homography.H_from_ransac(fp,tp,model)[0] tp,fp = convert_points(3)
H_43 = homography.H_from_ransac(fp,tp,model)[0]

在该例子中,图像2是中心图像,也是我们希望将其他图像变成的图像。图像0和图像1应该是从右边扭曲,图像3和图像4应该是从左边扭曲。在每个图像对中,由于匹配是从最右边的图像计算出来的。所以我们将对应的顺序进行了颠倒,使其从左边图像开始扭曲。因为我们不关心该扭曲例子中的正确点对,所以仅需要该函数的第一个输出。

3.3 拼接图像

估计出图像间的单应性矩阵,现在我们需要将所有图像扭曲到一个公共的图像平面上。通常,这里的公共平面为中心图像平面。一种方法是创建一个很大的图像,比如图像中全部填充0,使其和中心图像平行,然后将所有的图像扭曲到上面。由于我们所有的图像是由照相机水平旋转拍摄的,因此我们需要一个比较简单的步骤:将中心图像左边或者右边的区域填充0,以便为扭曲的图像腾出空间。

def panorama(H,fromim,toim,padding=2400,delta=2400):is_color = len(fromim.shape) == 3def transf(p):p2 = dot(H,[p[0],p[1],1])return (p2[0]/p2[2],p2[1]/p2[2])if H[1,2]<0: print('warp - right')if is_color:toim_t = hstack((toim,zeros((toim.shape[0],padding,3))))fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))for col in range(3):fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],transf,(toim.shape[0],toim.shape[1]+padding))else:toim_t = hstack((toim,zeros((toim.shape[0],padding))))fromim_t = ndimage.geometric_transform(fromim,transf,(toim.shape[0],toim.shape[1]+padding)) else:print('warp - left')H_delta = array([[1,0,0],[0,1,-delta],[0,0,1]])H = dot(H,H_delta)if is_color:toim_t = hstack((zeros((toim.shape[0],padding,3)),toim))fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))for col in range(3):fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],transf,(toim.shape[0],toim.shape[1]+padding))else:toim_t = hstack((zeros((toim.shape[0],padding)),toim))fromim_t = ndimage.geometric_transform(fromim,transf,(toim.shape[0],toim.shape[1]+padding))if is_color:alpha = ((fromim_t[:,:,0] * fromim_t[:,:,1] * fromim_t[:,:,2] ) > 0)for col in range(3):toim_t[:,:,col] = fromim_t[:,:,col]*alpha + toim_t[:,:,col]*(1-alpha)else:alpha = (fromim_t > 0)toim_t = fromim_t*alpha + toim_t*(1-alpha)return toim_t

对于通用的geometric_transform()函数,我们需要指定能够描述像素到像素间映射的函数。在这个例子中,transf()函数就是该指定函数。干函数通过将像素和H相乘,然后对齐次坐标进行归一化来实现像素间的映射。通过查看H中的平移量,我们可以决定应该将图像填补到左边还是右边。当图像填补到左边时,由于目标图像中点的坐标也变化了,所以在“左边“情况中,需要在单应性矩阵中加入平移。简单起见,我们同样使用0像素的技巧来寻找alpha图。

im1 = np.array(Image.open(imname[1]))
delta = im1.shape[1] im2 = np.array(Image.open(imname[2]))
im_12 = warp.panorama(H_12,im1,im2,delta,delta)im1 = np.array(Image.open(imname[0]))
im_02 = warp.panorama(np.dot(H_12,H_01),im1,im_12,delta,delta)im1 = np.array(Image.open(imname[3]))
im_32 = warp.panorama(H_32,im1,im_02,delta,delta)im1 = np.array(Image.open(imname[4]))
im_42 = warp.panorama(np.dot(H_32,H_43),im1,im_32,delta,2*delta)figure()
imshow(array(im_42))
axis('off')
show()

图4使用SIFT对应点自动创建水平全景图像

计算机视觉编程——图像到图像的映射相关推荐

  1. python计算机视觉编程——基本的图像操作和处理

    python计算机视觉编程--第一章(基本的图像操作和处理) 第1章 基本的图像操作和处理 1.1 PIL:Python图像处理类库 1.1.1 转换图像格式--save()函数 1.1.2 创建缩略 ...

  2. 计算机视觉编程——基本的图像操作和处理

    文章目录 基本的图像操作和处理 1 Python图像处理类库 1.1 转换图像格式 1.2 创建缩略图 1.3 复制和粘贴图像区域 1.4 调整尺寸和旋转 2 Matplotlib 2.1 绘制图像. ...

  3. OpenCV计算机视觉编程之三种图像像素的遍历方法

    为了构建计算机视觉应用程序,需要学会访问图像内容,有时也要修改或创建图像,如何操作图像的像素,就需要遍历一幅图像并处理每一个像素.现在我们就来介绍OpenCV三种图像像素的遍历方法: 一. 用cv:: ...

  4. Python计算机视觉编程第三章——图像到图像的映射

    Python计算机视觉编程 图像到图像的映射 (一)单应性变换 1.1 直接线性变换算法 1.2 仿射变换 (二)图像扭曲 2.1 图像中的图像 2.2 图像配准 (三)创建全景图 3.1 RANSA ...

  5. Python计算机视觉编程学习笔记 三 图像到图像的映射

    图像到图像的映射 (一)单应性变换 1.2 仿射变换 (二)图像扭曲 2.1 图像中的图像 2.2 图像配准 (三)创建全景图 3.1 RANSAC 3.2 拼接图像 (一)单应性变换 单应性变换是将 ...

  6. 三、【python计算机视觉编程】图像到图像的映射

    图像到图像的映射 (一)单应性变换 (1)直接线性变换算法(DLT) (2)仿射变换(affine) (二)图像扭曲 (1)图像中的图像 (2)分段仿射扭曲 (3)图像配准 (三)创建全景图 (1)R ...

  7. Python计算机视觉编程 第三章 图像到图像的映射

    第三章 图像到图像的映射 3.1 单应性变换 3.1.1直接线性变换算法 3.1.2仿射变换 3.2图像扭曲 3.2.1图像中的图像 3.2.2图像配准 3.3创建全景图 3.3.1RANSAC 3. ...

  8. python计算机视觉编程——立体图像之计算视差图

    计算视差图 一.立体图像 1.1概念 1.2关于图像配准算法 二.立体重建之计算视差图 2.1归一化及算法概念 2.2匹配流程 三.实验测试 3.1实验要求 3.2实验代码 3.3实验结果分析 3.4 ...

  9. Python计算机视觉编程第六章——图像聚类(K-means聚类,DBSCAN聚类,层次聚类,谱聚类,PCA主成分分析)

    Python计算机视觉编程 图像聚类 (一)K-means 聚类 1.1 SciPy 聚类包 1.2 图像聚类 1.1 在主成分上可视化图像 1.1 像素聚类 (二)层次聚类 (三)谱聚类 图像聚类 ...

最新文章

  1. javaScript常用知识点有哪些
  2. 【Win 10 应用开发】获取本机的IP地址
  3. SAP系统怎样快速应对2019税改?
  4. Elasticsearch Metric Aggregation指标聚合详解
  5. android studio中创建、切换svn分支
  6. spring之初识IocAop
  7. 百度文库下载器 V2.3.4.3 支持豆丁百度文库道客巴巴
  8. Java飞机大战项目实战
  9. 程序员为什么要写博客?怎么写博客?
  10. excel冻结行和列_Spire.Cloud.Excel 冻结或解除冻结Excel中的行和列
  11. php函数改变图片大小,php实现修改图片大小的方法
  12. 部署Unbound实现DNS服务
  13. adb shell循环命令_android adb实用命令小结
  14. MySQL B+树 BTree原理、增删改(详细)
  15. 三个最好的免费短信发送服务
  16. Android CE DE加密小结
  17. Delphi2010使用QQ邮箱发送邮件
  18. 【案例练习】15—27个网页设计的 HTML 时间线
  19. python中常用英语口语_常用英语口语1000句最全最完整
  20. 用C语言来实现五子棋小游戏

热门文章

  1. 【工具】WPS安卓电脑无广告版
  2. 【PC工具】U盘SD卡测试工具,速度测试,坏块测试查找
  3. 低噪声放大器和高功放的区别
  4. 我要做 Android 之面笔试总结
  5. 68.connect-flash 用法详解 req,flash()
  6. html固定table表头的实现思路
  7. JAVA大数_棋盘覆盖
  8. centos6.5下安装配置ELK及收集nginx日志
  9. iOS 获取网络状态
  10. main_loop()函数解析(1)