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

图像到图像的映射

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

(一)单应性变换

单应性变换是将一个平面内的点映射到另一个平面内的二维投影变换。在这里,平面是指图像或者三维中的平面表面。单应性变换具有很强的实用性,比如图像配准、图像纠正和纹理扭曲,以及创建全景图像。 本质上, 单应性变换 H,按照下面的方程映射二维中的点(齐次坐标意义下):
在这里插入图片描述
对于图像平面内的点,齐次坐标是个非常有用的表示方法。点的齐次坐标是依赖于其尺度定义的, 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 也仅依赖尺度定义,所以,单应性矩阵具有 8 个独立的自由度。我们通常使用 w=1 来归 一化点,这样,点具有唯一的图像坐标 x x x y y y。这个额外的坐标使得可以简单地使用一个矩阵来表示变换。

矩阵H会将一幅图像上的一个点的坐标 a = ( x , y , 1 ) a=(x,y,1) a=(x,y,1)映射成另一幅图像上的点的坐标 b = ( x 1 , y 1 , 1 ) b=(x1,y1,1) b=(x1,y1,1),也就是说,已知a和b,它们是在同一平面上。公式:
b = H a T b=Ha^{T} b=HaT


在这里插入图片描述

在这里插入图片描述在这里插入图片描述
写成一个矩阵与一个向量相乘:
在这里插入图片描述
估计单应性矩阵:
首先,我们假设两张图像中的对应点对齐次坐标为(x’,y’,1)和(x,y,1),单应矩阵H定义为:
在这里插入图片描述
则有:
Python计算机视觉编程学习笔记 三 图像到图像的映射_第1张图片
矩阵展开后有3个等式,将第3个等式代入前两个等式中可得:

Python计算机视觉编程学习笔记 三 图像到图像的映射_第2张图片
最终一个点对对应两个等式。

由于计算使用的是齐次坐标系,通俗来说就是可以进行任意尺度的缩放。假设我们把hij乘以任意一个非零常数k并不改变等式结果,所以单应矩阵H只有8个自由度。

Python计算机视觉编程学习笔记 三 图像到图像的映射_第3张图片
求解H: 每组点对应给出关于H元素的两个独立的方程。给定四组这样的点对应,便获得方程组 Ah=0,其中A是由每组点对应产生的矩阵行AiAi​构成的方程组的系数矩阵,h是H未知元素的矢量,我们只求h的非零解,因为对平凡解h=0毫无兴趣。变换矩阵H一般仅能确定到相差一个尺度,因此解h给出所要求的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-9
    C1 = diag([1 / maxstd, 1 / maxstd, 1])
    C1[0][2] = -m[0] / maxstd
    C1[1][2] = -m[1] / maxstd
    fp = dot(C1, fp)
    # --to points--
    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)
    # create matrix for linear method, 2 rows for each correspondence pair
    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[1][i]]

    U, S, V = linalg.svd(A)
    H = V[8].reshape((3, 3))
    # decondition
    H = dot(linalg.inv(C2), dot(H, C1))

    # normalize and return
    return H / H[2, 2]


对这些点进行归一化操作,其均值为0,方差为1,因为算法的稳定性取决于坐标的表示情况和部分数值计算的问题,所以归一化操作非常重要。
使用对应点来构造矩阵A,最小二乘解即为矩阵SVD分解后所得矩阵V的最后一行。该行经过变形后得到矩阵H。然后对这个矩阵进行处理和归一化,返回输出。

1.2 仿射变换

仿射变换(Affine Transformation或 Affine Map)是一种二维坐标到二维坐标之间的线性变换,它保持了二维图形的“平直性”(即:直线经过变换之后依然是直线)和“平行性”(即:二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变。

仿射变换的功能是从二维坐标到二维坐标之间的线性变换,且保持二维图形的“平直性”和“平行性”。仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,旋转。也就是说,当两幅图像存位移差,就可以通过对应的参数算出平移量,就可实现两张图片的重叠。

由于仿射变换具有6个自由度,因此需要三个对应点对来估计矩阵 H。
Python计算机视觉编程学习笔记 三 图像到图像的映射_第4张图片
简单来说,仿射变换就是允许图像任意倾斜,而且允许图形在两个方向上任意伸缩变换,但是,不能保持原来的线段长度不变,也不能保持原来的夹角角度不变。
Python计算机视觉编程学习笔记 三 图像到图像的映射_第5张图片

仿射变换包含一个可逆矩阵A和一个平移向量t=[tx,ty],仿射变换可以应用于图像扭曲等场景。
相似变换 :
[ x ′ y ′ 1 ] = [ s    c o s ( θ ) − s    s i n ( θ ) t x s    s i n ( θ ) s    c o s ( θ ) t y 0 0 1 ] [ x y 1 ] \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix}= \begin{bmatrix} s\;cos(\theta) & -s\;sin(\theta)& t_x \\ s\;sin(\theta) & s\; cos(\theta)& t_y \\ 0&0& 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} xy1=scos(θ)ssin(θ)0ssin(θ)scos(θ)0txty1xy1

x ′ = [ A t 0 1 ] x x'= \begin{bmatrix} A & t \\ 0 & 1 \end{bmatrix} x x=[A0t1]x
是一个包含尺度变化的二维刚体变换。上式中的向量s指定了变换的尺度,R是角度为θ的旋转矩阵, t = [ t x , t y ] t=[tx,ty] t=[tx,ty]在这里是一个平移向量。如果s=1,那么该变换能够保持距离不变。此时,变换为刚体变换,相似变化可以应用于图像配准等。
仿射变换可以通过一系列的原子变换的复合来实现,包括:平移(Translation)、缩放(Scale)、翻转(Flip)、旋转(Rotation)和剪切(Shear)。

(一) 直接线性变换算法
单应性矩阵可以由两幅图像(或者平面)中对应点对计算出来。每个对应点可以写出两个方程,分别对应与x和y坐标。因此,计算单应性矩阵H需要4个对应点对。
DLT(Direct Linear Transformation,直接线性变换)是给定4个或者更多对应点对矩阵,来计算单应性矩阵H的算法。可以得到下面的方程:
[ − x 1 − y 1 − 1 0 0 0 x 1 x 1 ′ y 1 x 1 ′ x 1 ′ 0 0 0 − x 1 − y 1 − 1 x 1 y 1 ′ y 1 y 1 ′ y 1 ′ − x 2 − y 2 − 1 0 0 0 x 2 x 2 ′ y 2 x 2 ′ x 2 ′ 0 0 0 − x 2 − y 2 − 1 x 2 y 2 ′ y 2 y 2 ′ y 2 ′ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ] [ h 1 h 2 h 3 h 4 h 5 h 6 h 7 h 8 h 9 ] = 0 \left[ \begin{matrix} -x_1 & -y_1 & -1&0&0&0& x_1x_1'& y_1x_1' &x_1' \\ 0 & 0 & 0&-x_1&-y_1&-1& x_1y_1'& y_1y_1' &y_1' \\ -x_2 & -y_2 & -1 & 0 & 0& 0& x_2x_2'& y_2x_2' &x_2' \\ 0 & 0 & 0&-x_2&-y_2&-1& x_2y_2'& y_2y_2' &y_2' \\ \vdots & \vdots & \vdots & \vdots& \vdots& \vdots& \vdots& \vdots& \vdots \end{matrix} \right] \left[ \begin{matrix} h_1 \\ h_2 \\ h_3 \\ h_4 \\ h_5 \\ h_6 \\ h_7 \\ h_8 \\ h_9 \end{matrix} \right]=0 x10x20y10y2010100x10x20y10y20101x1x1x1y1x2x2x2y2y1x1y1y1y2x2y2y2x1y1x2y2h1h2h3h4h5h6h7h8h9=0

或者Ah=0,其中A是一个具有对应点对二倍数量行数的矩阵。将这些对应点对方程的系数堆叠到一个矩阵红,我们可以使用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[1][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。然后对这个矩阵进行处理和归一化,返回输出。

使用对应点对来计算仿射变换矩阵:

from PCV.geometry import warp, homography
from PIL import Image
from pylab import *
from scipy import ndimage
im1 = array(Image.open('s.jpg').convert('L'))
im2 = array(Image.open('7.jpg').convert('L'))
tp = array([[120,260,260,120],[500,500,730,730],[1,1,1,1]])
#tp = array([[675,826,826,677],[55,52,281,277],[1,1,1,1]])
im3 = warp.image_in_image(im1,im2,tp)
figure()
gray()
subplot(141)
axis('off')
imshow(im1)
subplot(142)
axis('off')
imshow(im2)
subplot(143)
axis('off')
imshow(im3)
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 = 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 = 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 = 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 = warp.alpha_for_triangle(tp2,im2.shape[0],im2.shape[1])
im4 = (1-alpha)*im3 + alpha*im1_t
subplot(144)
imshow(im4)
axis('off')
show(

仿射变换示例:

from scipy import ndimage
from PIL import Image
from pylab import *

im = array(Image.open('D:\Data\school.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]))

figure()
gray()
subplot(121)
axis('off')
imshow(im)
subplot(122)
axis('off')
imshow(im2)
show()

效果:
Python计算机视觉编程学习笔记 三 图像到图像的映射_第6张图片

(二)图像扭曲

对图像块应用仿射变换,我们将其称为图像扭曲(或者仿射扭曲)。该操作不仅经常应用在计算机图形学中,而且经常出现在计算机视觉算法中。

扭曲操作可以使用SciPy工具包中的ndimage包来完成:

from scipy.spatial import Delaunay
from numpy import *
from scipy import *
from PIL import *
from PCV.geometry import warp, homography
from scipy import ndimage
import matplotlib
import matplotlib.pyplot as plt
from pylab import *
from PIL import Image
im = array(Image.open('D:\Data\school.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]))
figure()
gray()

subplot(121)
imshow(im)
subplot(122)
imshow(im2)
show()

效果:
Python计算机视觉编程学习笔记 三 图像到图像的映射_第7张图片
分析:
ndimage.affine_transform() 命令输出结果图像中丢失的像素用0来填充(即黑色部分),使用线性H[:2,:2]对图像进行变换,(H[0,2],H[1,2])是平移的向量。输出图像结果中丢失的像素用零来补充。

2.1 图像中的图像

仿射扭曲的一个简单例子是,将图像或者图像的一部分放置在另一幅图像中,使得它们能够和指定的区域或者标记物对齐。
函数image_in_image() ,该函数的输入参数为两幅图像和一个坐标,用于实现上面叙述的例子。
示例:

 #-*- coding: utf-8 -*-
from PCV.geometry import warp, homography
from PIL import Image
from pylab import *
from scipy import ndimage
#example of affine warp of im1 onto im2

im1 = array(Image.open('D:\Data\school.jpg').convert('L'))
im2 = array(Image.open('D:\Data\library.jpg').convert('L'))
#set to points
tp = array([[300,1250,1250,300],[0,0,280,280],[1,1,1,1]])
tp2 = array([[350,1300,1300,350],[380,380,750,750],[1,1,1,1]])
im3 = warp.image_in_image(im1,im2,tp)
im3 = warp.image_in_image(im1,im3,tp2)
figure()
gray()
subplot(131)
axis('off')
imshow(im1)
subplot(132)
axis('off')
imshow(im2)
subplot(133)
axis('off')
imshow(im3)
show()

处理效果:
Python计算机视觉编程学习笔记 三 图像到图像的映射_第8张图片
分析:

将扭曲的图像和第二幅图像融合, Alpha是一个8位的灰度图像通道,该通道用256级灰度来记录图像中的透明度信息,定义透明、不透明和半透明区域,其中黑表示透明,白表示不透明,灰表示半透明。该图像定义了每个像素从各个图像中获取的像素值成分多少。 扭曲的图像是在扭曲区域边界之外以 0 来填充的图像,来创建一个二值的 alpha 图像。严格意义上说,需要在第一幅图像中的潜 在 0 像素上加上一个小的数值,或者合理地处理这些 0 像素。 tp = array([[300,1250,1250,300],[0,0,280,280],[1,1,1,1]])
tp2 = array([[350,1300,1300,350],[380,380,750,750],[1,1,1,1]])可以调整图一映射到图二的位置。

2.2 图像配准

图像配准是对图像进行变换,是变换后的图像能够在常见的坐标系中对齐。配准可以是严格配准,也可以是非严格配准。其目的在于比较或融合针对同一对象在不同条件下获取的图像,例如图像会来自不同的采集设备,取自不同的时间,不同的拍摄视角等等,具体问题具体分析图像的配准问题。具体地说,对于一组图像数据集中的两幅图像,通过寻找一种空间变换把一幅图像映射到另一幅图像,使得两图中对应于空间同一位置的点一一对应起来,从而达到信息融合的目的。

SIFT算法:
SIFT特征匹配算法包括两个阶段:SIFT特征的生成与SIFT特征向量的匹配。
SIFT特征向量的生成算法包括四步:
1.尺度空间极值检测,以初步确定关键点位置和所在尺度。
2.拟和三维二次函数精确确定位置和尺度,同时去除低对比度的关键点和不稳定的边缘响应点。
3.利用关键点领域像素的梯度方向分布特性为每个关键点指定参数方向,使算子具备旋转不变性。
4.生成SIFT特征向量。
SIFT特征向量的匹配
对图像1中的某个关键点,找到其与图像2中欧式距离最近的前两个关键点的距离NN和SCN,如果NN/SCN小于某个比例阈值,则接受这一对匹配点。

import numpy as np
import cv2

def sift_kp(image):
   gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
   sift = cv2.xfeatures2d.SIFT_create()
   kp, des = sift.detectAndCompute(image, None)
   kp_image = cv2.drawKeypoints(gray_image, kp, None)
   return kp_image, kp, des

def get_good_match(des1, des2):
   bf = cv2.BFMatcher()
   matches = bf.knnMatch(des1, des2, k=2)
   good = []
   for m, n in matches:
       if m.distance < 0.75 * n.distance:
           good.append(m)
   return good

def siftImageAlignment(img1, img2):
   _, kp1, des1 = sift_kp(img1)
   _, kp2, des2 = sift_kp(img2)
   goodMatch = get_good_match(des1, des2)
   if len(goodMatch) > 4:
       ptsA = np.float32([kp1[m.queryIdx].pt for m in goodMatch]).reshape(-1, 1, 2)
       ptsB = np.float32([kp2[m.trainIdx].pt for m in goodMatch]).reshape(-1, 1, 2)
       ransacReprojThreshold = 4
       H, status = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, ransacReprojThreshold);
       # 其中H为求得的单应性矩阵矩阵
       # status则返回一个列表来表征匹配成功的特征点。
       # ptsA,ptsB为关键点
       # cv2.RANSAC, ransacReprojThreshold这两个参数与RANSAC有关
       imgOut = cv2.warpPerspective(img2, H, (img1.shape[1], img1.shape[0]),
                                    flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP)
   return imgOut, H, status

img1 = cv2.imread('D:\Data\J01.jpg')
img2 = cv2.imread('D:\Data\Jm02.jpg')
while img1.shape[0] > 1000 or img1.shape[1] > 1000:
   img1 = cv2.resize(img1, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
while img2.shape[0] > 1000 or img2.shape[1] > 1000:
   img2 = cv2.resize(img2, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)

result, _, _ = siftImageAlignment(img1, img2)
allImg = np.concatenate((img1, img2, result), axis=1)
cv2.namedWindow('1', cv2.WINDOW_NORMAL)
cv2.namedWindow('2', cv2.WINDOW_NORMAL)
cv2.namedWindow('Result', cv2.WINDOW_NORMAL)
cv2.imshow('1', img1)
cv2.imshow('2', img2)
cv2.imshow('Result', result)
# cv2.imshow('Result',allImg)
if cv2.waitKey(200000) & 0xff == ord('q'):
   cv2.destroyAllWindows()
   cv2.waitKey(1)

由于SIFT算法有专利版权OpenCV将sift算法移出了,如果使用旧版本可以运行上面代码。下面是参考运行的结果:
Python计算机视觉编程学习笔记 三 图像到图像的映射_第9张图片
Python计算机视觉编程学习笔记 三 图像到图像的映射_第10张图片

Python计算机视觉编程学习笔记 三 图像到图像的映射_第11张图片
总结:
可以看到将图二移动到和图一完全匹配的位置,也就是图三。

ASIFT算法:
通过原始图像来模拟得到场景在各个视角下的图像,再对这些得到的图像提起SIFT特征点,然后进行匹配。其放射性要好于SIFT,具有全仿射不变性。

# Python 2/3 compatibility
from __future__ import print_function

import numpy as np
import cv2 as cv

# built-in modules
import itertools as it
from multiprocessing.pool import ThreadPool

# local modules
from common import Timer
from find_obj import init_feature, filter_matches, explore_match


def affine_skew(tilt, phi, img, mask=None):
    '''
    affine_skew(tilt, phi, img, mask=None) -> skew_img, skew_mask, Ai

    Ai - is an affine transform matrix from skew_img to img
    '''
    h, w = img.shape[:2]
    if mask is None:
        mask = np.zeros((h, w), np.uint8)
        mask[:] = 255
    A = np.float32([[1, 0, 0], [0, 1, 0]])
    if phi != 0.0:
        phi = np.deg2rad(phi)
        s, c = np.sin(phi), np.cos(phi)
        A = np.float32([[c,-s], [ s, c]])
        corners = [[0, 0], [w, 0], [w, h], [0, h]]
        tcorners = np.int32( np.dot(corners, A.T) )
        x, y, w, h = cv.boundingRect(tcorners.reshape(1,-1,2))
        A = np.hstack([A, [[-x], [-y]]])
        img = cv.warpAffine(img, A, (w, h), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_REPLICATE)
    if tilt != 1.0:
        s = 0.8*np.sqrt(tilt*tilt-1)
        img = cv.GaussianBlur(img, (0, 0), sigmaX=s, sigmaY=0.01)
        img = cv.resize(img, (0, 0), fx=1.0/tilt, fy=1.0, interpolation=cv.INTER_NEAREST)
        A[0] /= tilt
    if phi != 0.0 or tilt != 1.0:
        h, w = img.shape[:2]
        mask = cv.warpAffine(mask, A, (w, h), flags=cv.INTER_NEAREST)
    Ai = cv.invertAffineTransform(A)
    return img, mask, Ai


def affine_detect(detector, img, mask=None, pool=None):
    '''
    affine_detect(detector, img, mask=None, pool=None) -> keypoints, descrs

    Apply a set of affine transformations to the image, detect keypoints and
    reproject them into initial image coordinates.
    See http://www.ipol.im/pub/algo/my_affine_sift/ for the details.

    ThreadPool object may be passed to speedup the computation.
    '''
    params = [(1.0, 0.0)]
    for t in 2**(0.5*np.arange(1,6)):
        for phi in np.arange(0, 180, 72.0 / t):
            params.append((t, phi))

    def f(p):
        t, phi = p
        timg, tmask, Ai = affine_skew(t, phi, img)
        keypoints, descrs = detector.detectAndCompute(timg, tmask)
        for kp in keypoints:
            x, y = kp.pt
            kp.pt = tuple( np.dot(Ai, (x, y, 1)) )
        if descrs is None:
            descrs = []
        return keypoints, descrs

    keypoints, descrs = [], []
    if pool is None:
        ires = it.imap(f, params)
    else:
        ires = pool.imap(f, params)

    for i, (k, d) in enumerate(ires):
        print('affine sampling: %d / %d\r' % (i+1, len(params)), end='')
        keypoints.extend(k)
        descrs.extend(d)

    print()
    return keypoints, np.array(descrs)

if __name__ == '__main__':
    print(__doc__)

    import sys, getopt
    opts, args = getopt.getopt(sys.argv[1:], '', ['feature='])
    opts = dict(opts)
    '''
       --feature:  sift/surf/orb/akaze  
    '''
    feature_name = opts.get('--feature', 'brisk-flann')
    try:
        fn1, fn2 = args
    except:
        fn1 = 'D:\\Python\\chapter3\\building.jpg'
        fn2 = 'D:\\Python\\chapter3\\building2.jpg'

    img1 = cv.imread(fn1, 0)
    img2 = cv.imread(fn2, 0)
    detector, matcher = init_feature(feature_name)

    if img1 is None:
        print('Failed to load fn1:', fn1)
        sys.exit(1)

    if img2 is None:
        print('Failed to load fn2:', fn2)
        sys.exit(1)

    if detector is None:
        print('unknown feature:', feature_name)
        sys.exit(1)
    print('using', feature_name)

    pool = ThreadPool(processes=cv.getNumberOfCPUs())
    kp1, desc1 = affine_detect(detector, img1, pool=pool)
    kp2, desc2 = affine_detect(detector, img2, pool=pool)
    print('img1 - %d features, img2 - %d features' % (len(kp1), len(kp2)))

    def match_and_draw(win):
        with Timer('matching'):
            raw_matches = matcher.knnMatch(desc1, trainDescriptors=desc2, k=2) #2
        p1, p2, kp_pairs = filter_matches(kp1, kp2, raw_matches)
        if len(p1) >= 4:
            H, status = cv.findHomography(p1, p2, cv.RANSAC, 5.0)
            print('%d / %d  inliers/matched' % (np.sum(status), len(status)))
            # do not draw outliers (there will be a lot of them)
            kp_pairs = [kpp for kpp, flag in zip(kp_pairs, status) if flag]
        else:
            H, status = None, None
            print('%d matches found, not enough for homography estimation' % len(p1))
        explore_match(win, img1, img2, kp_pairs, None, H)

    match_and_draw('affine find_obj')
    cv.waitKey()
    cv.destroyAllWindows()


参考师姐运行的图片:
Python计算机视觉编程学习笔记 三 图像到图像的映射_第12张图片
总结:
当待配准的图像特征信息比较弱一些的话可以用SIFT算法
当待配准的图像之间存在较大的视角变换的话可以用ASIFT算法

(三)创建全景图

在同一位置(即图像的照相机位置相同)拍摄的两幅或者多幅图像是单应性相关的,我们可以使用该约束将很多图像缝补起来,拼成一个大的图像来创建全景图。

3.1 RANSAC

RANSAC 是“RANdom SAmple Consensus”(随机一致性采样)的缩写。该方法是用来找到正确模型来拟合带有噪声数据的迭代方法。给定一个模型,例如点集之间的单应性矩阵,RANSAC 基本的思想是,数据中包含正确的点和噪声点,合理的模型应该能够在描述正确数据点的同时摒弃噪声点。

RANSAC的基本假设是:
(1)数据由“局内点”组成,例如:数据的分布可以用一些模型参数来解释;
(2)“局外点”是不能适应该模型的数据;
(3)除此之外的数据属于噪声。

局外点产生的原因:噪声的极值;错误的测量方法;对数据的错误假设。

RANSAC也做了以下假设:给定一组(通常很小的)局内点,存在一个可以估计模型参数的过程;而该模型能够解释或者适用于局内点。

RANSAC算法的输入是一组观测数据,一个可以解释或者适应于观测数据的参数化模型,一些可信的参数。
RANSAC算法从匹配数据集中随机抽出4个样本并保证这4个样本之间不共线,计算出单应性矩阵,然后利用这个模型测试所有数据,并计算满足这个模型数据点的个数与投影误差(即代价函数),若此模型为最优模型,则对应的代价函数最小。

RANSAC算法的步骤:

  1. 随机从数据集中随机抽出4个样本数据 (此4个样本之间不能共线),计算出变换矩阵 H H H,记为模型 M M M

  2. 计算数据集中所有数据与模型M的投影误差,若误差小于阈值,加入内点集 I I I

  3. 如果当前内点集 I I I 元素个数大于最优内点集 I b e s t I_best Ibest , 则更新 I b e s t = I I_best = I Ibest=I,同时更新迭代次数k :

  4. 如果迭代次数大于k,则退出 ; 否则迭代次数加1,并重复上述步骤。(注:迭代次数k在不大于最大迭代次数的情况下,是在不断变化的

    k = l o g ( 1 − p ) l o g ( 1 − w m ) k=\frac{log(1−p)}{log(1-w^{m})} k=log(1wm)log(1p)
    其中,p为置信度,一般取0.995;w为"内点"的比例 ; m为计算模型所需要的最少样本数=4。

RANSAC 的标准例子:用一条直线拟合带有噪声数据的点集。简单的最小二乘在该 例子中可能会失效,但是 RANSAC 能够挑选出正确的点,然后获取能够正确拟合的直线。

示例:

import numpy
import scipy  # use numpy if scipy unavailable
import scipy.linalg  # use numpy if scipy unavailable

def ransac(data, model, n, k, t, d, debug=False, return_all=False):
  
    iterations = 0
    bestfit = None
    besterr = numpy.inf
    best_inlier_idxs = None
    while iterations < k:
        maybe_idxs, test_idxs = random_partition(n, data.shape[0])
        maybeinliers = data[maybe_idxs, :]
        test_points = data[test_idxs]
        maybemodel = model.fit(maybeinliers)
        test_err = model.get_error(test_points, maybemodel)
        also_idxs = test_idxs[test_err < t]  # select indices of rows with accepted points
        alsoinliers = data[also_idxs, :]
        if debug:
            print( 'test_err.min()', test_err.min())
            print('test_err.max()', test_err.max())
            print ('numpy.mean(test_err)', numpy.mean(test_err))
            print ('iteration %d:len(alsoinliers) = %d' % (
                iterations, len(alsoinliers)))
        if len(alsoinliers) > d:
            betterdata = numpy.concatenate((maybeinliers, alsoinliers))
            bettermodel = model.fit(betterdata)
            better_errs = model.get_error(betterdata, bettermodel)
            thiserr = numpy.mean(better_errs)
            if thiserr < besterr:
                bestfit = bettermodel
                besterr = thiserr
                best_inlier_idxs = numpy.concatenate((maybe_idxs, also_idxs))
        iterations += 1
    if bestfit is None:
        raise ValueError("did not meet fit acceptance criteria")
    if return_all:
        return bestfit, {'inliers': best_inlier_idxs}
    else:
        return bestfit


def random_partition(n, n_data):
    """return n random rows of data (and also the other len(data)-n rows)"""
    all_idxs = numpy.arange(n_data)
    numpy.random.shuffle(all_idxs)
    idxs1 = all_idxs[:n]
    idxs2 = all_idxs[n:]
    return idxs1, idxs2


class LinearLeastSquaresModel:
    """linear system solved using linear least squares

    This class serves as an example that fulfills the model interface
    needed by the ransac() function.

    """

    def __init__(self, input_columns, output_columns, debug=False):
        self.input_columns = input_columns
        self.output_columns = output_columns
        self.debug = debug

    def fit(self, data):
        A = numpy.vstack([data[:, i] for i in self.input_columns]).T
        B = numpy.vstack([data[:, i] for i in self.output_columns]).T
        x, resids, rank, s = numpy.linalg.lstsq(A, B)
        return x

    def get_error(self, data, model):
        A = numpy.vstack([data[:, i] for i in self.input_columns]).T
        B = numpy.vstack([data[:, i] for i in self.output_columns]).T
        B_fit = scipy.dot(A, model)
        err_per_point = numpy.sum((B - B_fit) ** 2, axis=1)  # sum squared error per row
        return err_per_point


def test():
    # generate perfect input data

    n_samples = 500
    n_inputs = 1
    n_outputs = 1
    A_exact = 20 * numpy.random.random((n_samples, n_inputs))
    perfect_fit = 60 * numpy.random.normal(size=(n_inputs, n_outputs))  # the model
    B_exact = scipy.dot(A_exact, perfect_fit)
    assert B_exact.shape == (n_samples, n_outputs)

    # add a little gaussian noise (linear least squares alone should handle this well)
    A_noisy = A_exact + numpy.random.normal(size=A_exact.shape)
    B_noisy = B_exact + numpy.random.normal(size=B_exact.shape)

    if 1:
        # add some outliers
        n_outliers = 100
        all_idxs = numpy.arange(A_noisy.shape[0])
        numpy.random.shuffle(all_idxs)
        outlier_idxs = all_idxs[:n_outliers]
        non_outlier_idxs = all_idxs[n_outliers:]
        A_noisy[outlier_idxs] = 20 * numpy.random.random((n_outliers, n_inputs))
        B_noisy[outlier_idxs] = 50 * numpy.random.normal(size=(n_outliers, n_outputs))

    # setup model

    all_data = numpy.hstack((A_noisy, B_noisy))
    input_columns = range(n_inputs)  # the first columns of the array
    output_columns = [n_inputs + i for i in range(n_outputs)]  # the last columns of the array
    debug = True
    model = LinearLeastSquaresModel(input_columns, output_columns, debug=debug)

    linear_fit, resids, rank, s = numpy.linalg.lstsq(all_data[:, input_columns], all_data[:, output_columns])

    # run RANSAC algorithm
    ransac_fit, ransac_data = ransac(all_data, model,
                                     5, 5000, 7e4, 50,  # misc. parameters
                                     debug=debug, return_all=True)
    if 1:
        import pylab

        sort_idxs = numpy.argsort(A_exact[:, 0])
        A_col0_sorted = A_exact[sort_idxs]  # maintain as rank-2 array

        if 1:
            pylab.plot(A_noisy[:, 0], B_noisy[:, 0], 'k.', label='data')
            pylab.plot(A_noisy[ransac_data['inliers'], 0], B_noisy[ransac_data['inliers'], 0], 'bx',
                       label='RANSAC data')
        else:
            pylab.plot(A_noisy[non_outlier_idxs, 0], B_noisy[non_outlier_idxs, 0], 'k.', label='noisy data')
            pylab.plot(A_noisy[outlier_idxs, 0], B_noisy[outlier_idxs, 0], 'r.', label='outlier data')
        pylab.plot(A_col0_sorted[:, 0],
                   numpy.dot(A_col0_sorted, ransac_fit)[:, 0],
                   label='RANSAC fit')
        pylab.plot(A_col0_sorted[:, 0],
                   numpy.dot(A_col0_sorted, perfect_fit)[:, 0],
                   label='exact system')
        pylab.plot(A_col0_sorted[:, 0],
                   numpy.dot(A_col0_sorted, linear_fit)[:, 0],
                   label='linear fit')
        pylab.legend()
        pylab.show()
if __name__ == '__main__':
    test()



效果:

Python计算机视觉编程学习笔记 三 图像到图像的映射_第13张图片
分析:
随机取样时只取一部分可以避免估算结果被离群数据影响,所以RANSAC能在有大量噪音情况仍然准确;计算参数时没有一个最大运算时间的顶限,也就是说在迭代次数被限制的情况下,得出来的参数结果有可能并不是最优的,甚至可能不符合真实内群。所以设定 RANSAC参数的时候要根据应用考虑“准确度与效率”哪一个更重要,以此决定做多少次迭代运算。设定与模型的最大误差阈值也是要自己调,因应用而异。还有一点就是RANSAC只能估算一个模型。如果发现了一种足够好的模型(该模型有足够小的错误率),则跳出主循环。这样可能会节约计算额外参数的时间。直接从maybe_model计算this_error,而不从consensus_set重新估计模型。这样可能会节约比较两种模型错误的时间,但可能会对噪声更敏感。

3.2 拼接图像

在同一位置拍摄的两幅或者多幅图片是单应性相关的,经常使用该约束将很多图像缝补起来,拼成一个全景图。

全景图像拼接最重要的两个步骤是:
1.特征匹配;
2.图像拼接。
在本次测试中,使用的是sift特征匹配,其原理见上一章。
在特征匹配之后,接着使用RANSAC算法求解得到单应性矩阵。其基本思想是,数据中包含正确的点和噪声点,合理的模型能够在描述正确数据点的同时摒弃噪声点,在这里的用途就是排除掉不符合大部分几何变换的匹配,而后使正确的点进行匹配,通过单应矩阵来对齐两张图片的内容。

示例:

# -*- coding: utf-8 -*-
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

if __name__ == '__main__':
    top, bot, left, right = 100, 100, 0, 500
    img1 = cv.imread('D:\Data\school.jpg')
    img2 = cv.imread('D:\Data\collage.jpg')
    srcImg = cv.copyMakeBorder(img1, top, bot, left, right, cv.BORDER_CONSTANT, value=(0, 0, 0))
    testImg = cv.copyMakeBorder(img2, top, bot, left, right, cv.BORDER_CONSTANT, value=(0, 0, 0))
    img1gray = cv.cvtColor(srcImg, cv.COLOR_BGR2GRAY)
    img2gray = cv.cvtColor(testImg, cv.COLOR_BGR2GRAY)
    sift = cv.xfeatures2d_SIFT().create()
    # find the keypoints and descriptors with SIFT
    kp1, des1 = sift.detectAndCompute(img1gray, None)
    kp2, des2 = sift.detectAndCompute(img2gray, None)
    # FLANN parameters
    FLANN_INDEX_KDTREE = 1
    index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
    search_params = dict(checks=50)
    flann = cv.FlannBasedMatcher(index_params, search_params)
    matches = flann.knnMatch(des1, des2, k=2)

    # Need to draw only good matches, so create a mask
    matchesMask = [[0, 0] for i in range(len(matches))]

    good = []
    pts1 = []
    pts2 = []
    # ratio test as per Lowe's paper
    for i, (m, n) in enumerate(matches):
        if m.distance < 0.7 * n.distance:
            good.append(m)
            pts2.append(kp2[m.trainIdx].pt)
            pts1.append(kp1[m.queryIdx].pt)
            matchesMask[i] = [1, 0]

    draw_params = dict(matchColor=(0, 255, 0),
                       singlePointColor=(255, 0, 0),
                       matchesMask=matchesMask,
                       flags=0)
    img3 = cv.drawMatchesKnn(img1gray, kp1, img2gray, kp2, matches, None, **draw_params)
    plt.imshow(img3, ), plt.show()

    rows, cols = srcImg.shape[:2]
    MIN_MATCH_COUNT = 10
    if len(good) > MIN_MATCH_COUNT:
        src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
        dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
        M, mask = cv.findHomography(src_pts, dst_pts, cv.RANSAC, 5.0)
        warpImg = cv.warpPerspective(testImg, np.array(M), (testImg.shape[1], testImg.shape[0]),
                                     flags=cv.WARP_INVERSE_MAP)

        for col in range(0, cols):
            if srcImg[:, col].any() and warpImg[:, col].any():
                left = col
                break
        for col in range(cols - 1, 0, -1):
            if srcImg[:, col].any() and warpImg[:, col].any():
                right = col
                break

        res = np.zeros([rows, cols, 3], np.uint8)
        for row in range(0, rows):
            for col in range(0, cols):
                if not srcImg[row, col].any():
                    res[row, col] = warpImg[row, col]
                elif not warpImg[row, col].any():
                    res[row, col] = srcImg[row, col]
                else:
                    srcImgLen = float(abs(col - left))
                    testImgLen = float(abs(col - right))
                    alpha = srcImgLen / (srcImgLen + testImgLen)
                    res[row, col] = np.clip(srcImg[row, col] * (1 - alpha) + warpImg[row, col] * alpha, 0, 255)

        # opencv is bgr, matplotlib is rgb
        res = cv.cvtColor(res, cv.COLOR_BGR2RGB)
        # show the result
        plt.figure()
        plt.imshow(res)
        plt.savefig("pinjie.jpg", dpi=700)
        plt.show()
    else:
        print("Not enough matches are found - {}/{}".format(len(good), MIN_MATCH_COUNT))
        matchesMask = None

Python计算机视觉编程学习笔记 三 图像到图像的映射_第14张图片
由于SIFT算法有专利版权OpenCV将sift算法移出了,旧版本可以运行显示结果。

Python计算机视觉编程学习笔记 三 图像到图像的映射_第15张图片

你可能感兴趣的:(计算机视觉,python,数字图像处理)