计算机视觉 第四章照相机模型与增强现实

目录

4.1  针孔照相机模型

        4.1.1  照相机矩阵

        4.1.2  三维点的投影

        4.1.3  照相机矩阵的分解

        4.1.4  计算照相机中心

4.2  照相机标定

        4.2.1  一个简单的标定方法

4.3  以平面和标记物进行姿态估计

4.4  增强现实

        4.4.1  PyGame和PyOenGL

        4.4.2  从照相机矩阵到OpenGL格式

        4.4.3  在图像中放置虚拟物体

         4.4.4  综合集成


4.1  针孔照相机模型

        针孔照相机模型是计算机视觉中广泛使用的照相机模型,针孔照相机模型简单,并且具有足够的精确度。该照相机从一个小孔采集射到暗箱内部的光线。从照相机中心前画出图像平面的图解如图4-1所示。

图4-1  针孔照相机模型

        在针孔照相机中,三维点X投影为图像点x(两个点都是用齐次坐标表示的)\lambda x=PX,其中,3\times 4的矩阵P为照相机矩阵(或投影矩阵),标量\lambda是三维点的逆深度。

        4.1.1  照相机矩阵

        照相机矩阵可以分解为:P =K[R|t]。其中,R是描述相机方向的旋转矩阵,t是描述相机中心位置的三维平移向量,内标定矩阵K描述照相机的投影性质。

        标定矩阵仅和照相机自身情况相关,可写成:

K=\begin{bmatrix} \alpha f& s & c_{x} \\ 0& f & c_{y}\\ 0& 0 & 1 \end{bmatrix}

图像平面和照相机中心间的距离为焦距f。当像素数组在传感器上偏斜的时候,需要用到倾斜参数s,通常s=0,即:

K=\begin{bmatrix} f_{x}& 0 & c_{x} \\ 0& f_{y} & c_{y}\\ 0& 0 & 1 \end{bmatrix}

这里f_{x}=\alpha f_{y}。纵横比例参数α是在像素元素非正方形的情况下使用的,通常默认\alpha =1,标定矩阵为:

K=\begin{bmatrix} f& 0 & c_{x} \\ 0& f & c_{y}\\ 0& 0 & 1 \end{bmatrix}

        除焦距之外,标定矩阵中剩余的唯一参数为光心(有时称主点)的坐标c=[c_{x},c_{y}],也就是光线坐标轴和图像平面的交点。因为光心通常在图像的中心,并且图像的坐 标是从左上角开始计算的,所以光心的坐标常接近于图像宽度和高度的一半。

        4.1.2  三维点的投影

        首先创建照相机类,用来处理对照相机和投影建模所需要的全部操作:

        

from scipy import linalg
import numpy as np

class Camera(object):
    """表示针孔照相机的类"""
    def __init__(self,P):
        """初始化P=K[R|t]照相机模型"""
        self.P = P
        self.K = None #标定矩阵
        self.R = None #旋转
        self.t = None #平移
        self.c = None # 照相机中心

    def project(self,X):
        """X(4*n的数组)的投影点,并且进行坐标归一化"""
        x = np.dot(self.P,X)
        for i in range(3):
            x[i] /= x[2]
        return x

def rotation_matrix(a):
    """创建一个用于围绕向量a轴旋转的三维旋转矩阵"""
    R = np.eye(4)
    R[:3,:3] = linalg.expm([[0,-a[2],a[1]],[a[2],0,-a[0]],[-a[1],a[0],0]])
    return R


import camera
from PIL import Image
from numpy import *
import matplotlib.pyplot as plt

# 载入图像(未使用此图像进行绘制,但可以在必要时使用)
im = array(Image.open('house.000.pgm').convert('L'))

# 载入3D点
points = loadtxt('house.p3d').T
points = vstack((points, ones(points.shape[1])))

# 定义投影矩阵 P
P = hstack((eye(3), array([[0], [0], [-10]])))
cam = camera.Camera(P)

# 创建图形与子图
fig, (ax3, ax1, ax2) = plt.subplots(1, 3, figsize=(12, 4))

#绘制原始图像
ax3.imshow(im, cmap='gray')
ax3.set_title("Original Image")
ax3.axis('off')

# 绘制初始投影
x = cam.project(points)
ax1.plot(x[0], x[1], 'k.')
ax1.set_title("Initial Projection")
ax1.grid()

# 创建旋转变换
r = 0.05 * random.rand(3)
rot = camera.rotation_matrix(r)

# 绘制旋转后的投影
for t in range(20):
    cam.P = dot(cam.P, rot)  # 更新相机的投影矩阵
    x = cam.project(points)
    ax2.plot(x[0], x[1], 'k.')

ax2.set_title("Rotated Projections")
ax2.grid()

# 显示图形
plt.tight_layout()  # 自动调整子图间距
plt.show()

运行结果如下所示:

        4.1.3  照相机矩阵的分解

        如果给定方程为P =K[R|t] 中的照相机矩阵P,我们需要恢复内参数K以及照相机的 位置t和姿势R。矩阵分块操作称为因子分解,我们使用为RQ因子分解。将下面方法加入Camera类中。

    def factor(self):
        """将照相机矩阵分解为K、R、t,其中,P=K[R|t]"""

        #分解前3*3的部分
        K,R=linalg.rq(self.P[:,:3])

        #将K的对角线元素设为正值
        T = np.diag(np.sign(np.diag(K)))
        if linalg.det(T) < 0:
            T[1,1] *= -1
        self.K = np.dot(K,T)
        self.R = np.dot(T,R)  #T的逆矩阵为其自身
        self.t = np.dot(linalg.inv(self.K),self.P[:,3])
        return self.K, self.R, self.t

        值得注意的是,RQ因子分解的结果并不是唯一的,需要限制旋转矩阵R为正定的,故在求解到的结果中加入变换T来改变符号。

        4.1.4  计算照相机中心

        假定投影矩阵P,可以计算出空间上照相机的所在位置。照相机的中心位置C,是一个三维点,满足约束PC=0。故有

K[R|t]C=KRC+Kt=0

照相机的中心可以由C=-R^{T}t来计算。下面代码是用以计算照相机的中心,将其添加到Camera类中。

    def center(self):
        """计算并返回照相机的中心"""
        
        if self.c is not None:
            return self.c
        else:
            #通过因子分解计算c
            self.factor()
            self.c = -np.dot(self.R.T,self.t)
            return self.c

4.2  照相机标定

        标定照相机是指计算出该照相机的内参数,即值计算矩阵K。标定照 相机的标准方法是,拍摄多幅平面棋盘模式的图像,然后进行处理计算。

        4.2.1  一个简单的标定方法

        大多数参数可以使用基本的假设来 设定(正方形垂直的像素,光心位于图像中心),比较难处理的是获得正确的焦距。你需要准备一个平面矩形的标定物体(一个书本即可)、用于测 量的卷尺和直尺,以及一个平面,下面是具体操作步骤:

        \bullet测量你选定矩形标定物体的边长dXdY

        \bullet将照相机和标定物体放置在平面上,使得照相机的背面和标定物体平行,同时物 体位于照相机图像视图的中心;

        \bullet测量标定物体到照相机的距离dZ

        \bullet拍摄一副图像来检验该设置是否正确,即标定物体的边要和图像的行和列对齐;

        \bullet使用像素数来测量标定物体图像的宽度和高度dxdy

        实验设置情况如图所示,使用相似三角形关系可以获得焦距:

f_{x}=\frac{dx}{dX}dZ,     f_{y}=\frac{dy}{dY}dZ

        假定dX=130,dY=185,dZ=460,且dx=722,dy=1040,带入上述关系表达式可以获得焦距的大小为f_{x}=2555,f_{y}=2586。此外,需要注意的是,我们现在获取的焦距是在特定图像分辨率下计算出来的,此例图像大小为2592×1936像素,用下面的辅助函数求出K。

def my_calibration(sz):
    row,col = sz
    fx = 2555 * col / 2592
    fy = 2586 * row / 1936
    K = np.diag([fx,fy,1])
    K[0,2] = 0.5 * col
    K[1,2] = 0.5 * row
    return K

        该函数的输入参数为表示图像大小的元组,返回参数为标定矩阵。

4.3  以平面和标记物进行姿态估计

        如果图像中包含平面状的 标记物体,并且已经对照相机进行了标定,那么我们可以计算出照相机的姿态(旋 转和平移)。这里的标记物体可以为对任何平坦的物体。

import homography
import camera
import sift2
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

#计算特征
sift2.process_image('eg1.png','im0.sift2')
l0,d0 = sift2.read_features_from_file('im0.sift2')

sift2.process_image('eg2.png','im0.sift2')
l1,d1 = sift2.read_features_from_file('im1.sift2')

#匹配特征,并计算单应性矩阵
matches = sift2.match_twosided(d0,d1)
ndx = matches.nonzero()[0]
fp = homography.make_homog(l0[ndx,:2].T)
ndx2 = [int(matches[i]) for i in ndx]
tp = homography.make_homog(l1[ndx2,:2].T)

model = homography.RansancModel()
H = homography.H_from_ransac(fp,tp,model)

def cube_points(c,wid):
    """创建用于绘制立方体的一个点列表(前5个点是底部的正方形,一些边重合了)"""
    p = []
    #底部
    p.append([c[0] - wid, c[1] - wid, c[2] - wid])
    p.append([c[0] - wid, c[1] + wid, c[2] - wid])
    p.append([c[0] + wid, c[1] + wid, c[2] - wid])
    p.append([c[0] + wid, c[1] - wid, c[2] - wid])
    p.append([c[0] - wid, c[1] - wid, c[2] - wid])#为了绘制闭合图像,和第一个相同

    #顶部
    p.append([c[0] - wid, c[1] - wid, c[2] + wid])
    p.append([c[0] - wid, c[1] + wid, c[2] + wid])
    p.append([c[0] + wid, c[1] + wid, c[2] + wid])
    p.append([c[0] + wid, c[1] - wid, c[2] + wid])
    p.append([c[0] - wid, c[1] - wid, c[2] + wid])  # 为了绘制闭合图像,和第一个相同

    #竖直边
    p.append([c[0] - wid, c[1] - wid, c[2] + wid])
    p.append([c[0] - wid, c[1] + wid, c[2] + wid])
    p.append([c[0] - wid, c[1] + wid, c[2] - wid])
    p.append([c[0] + wid, c[1] + wid, c[2] - wid])
    p.append([c[0] + wid, c[1] + wid, c[2] + wid])
    p.append([c[0] + wid, c[1] - wid, c[2] + wid])
    p.append([c[0] + wid, c[1] - wid, c[2] - wid])

    return np.array(p).T

def my_calibration(sz):
    row,col = sz
    fx = 2555 * col / 2592
    fy = 2586 * row / 1936
    K = np.diag([fx,fy,1])
    K[0,2] = 0.5 * col
    K[1,2] = 0.5 * row
    return K

# 计算照相机标定矩阵
K = my_calibration((747,1000))

# 位于边长为0.2,z=0平面上的三维点
box = cube_points([0,0,0.1],0.1)

#投影第一幅图像上底部的正方形
cam1 = camera.Camera(np.hstack((K,np.dot(K,np.array([[0],[0],[-1]])))))
#底部正方形上的点
box_cam1 = cam1.project(homography.make_homog(box[:,:5]))

#使用H将点变换到第二幅图像中
box_trans = homography.normalize(np.dot(H,box_cam1))

#从cam1和H中计算第二个照相机矩阵
cam2 = camera.Camera(np.dot(H,cam1.P))
A = np.dot(np.linalg.inv(K),cam2.P[:,:3])
A = np.array([A[:,0],A[:,1],np.cross(A[:,0],A[:,1])]).T
cam2.P[:,:3] = dot(K,A)

#使用第二个照相机矩阵投影
box_cam2 = cam2.project(homography.make_homog(box))

#测试:将点投影在z=0上,应该能够得到相同的点
point = np.array([1,1,0,1]).T
print(homography.normalize(np.dot(np.dot(H,cam1.P),point)))
print(cam2.project(point))

im0 = np.array(Image.open('book_frontal.jpg'))
im1 = np.array(Image.open('book_perspective.jpg'))

#底部正方形的二维投影
figure()
imshow(im0)
plt.plot(box_cam2[0,:],box_cam1[1,:],linewidth=3)

#使用H对二维投影进行变换
figure()
imshow(im1)
plt.plot(box_trans[0,:],box_trans[1,:],linewidth=3)

#三维立方体
figure()
imshow(im1)
plt.plot(box_cam2[0,:],box_cam2[1,:],linewidth=3)
show()

        由于sift2.py和homography.py中的函数并不完善,所以以上代码无法实现预期效果。

姿态估计用到的sift2和homography文件中的函数
homography中的所有函数
sift2中所有的函数

        书上运行效果图如下

4.4  增强现实

        增强现实(AR)是将物体和相应信息放置在图像数据上的一系列操作的总称。最经典的例子是放置一个三维计算机图形学模型,使其看起来属于该场景;如果在视频中,该模型会随着照相机的运动很自然地移动。

        4.4.1  PyGame和PyOenGL

        PyGame 是非常流行的游戏开发工具包,它可以非常简单地处理显示窗口、输入设备、事件,以及其他内容。

        PyOpenGL 是 OpenGL 图形编程的Python绑定接口。OpenGL可以安装在几乎所 有的系统上,并且具有很好的图形性能。为了使用PyGame和 PyOpenGL 工具包来完成该应用,需要在脚本的开始部分载入下面的命令:

from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, pygame.image
from pygame.locals import *

        pygame部分用 来设置窗口和事件控制;其中pygame.image用来载入图像和创建OpenGL的纹理, pygame.locals 用来设置 OpenGL的显示区域。

        4.4.2  从照相机矩阵到OpenGL格式

        OpenGL 使用4×4的矩阵来表示变换(包括三维变换和投影),但是,照相机与场景的变换分成了两个矩阵。GL_PROJECTION矩阵处理图 像成像的性质,等价于我们的内标定矩阵K。GL_MODELVIEW矩阵处理物体和照 相机之间的三维变换关系,对应于我们照相机矩阵中的R和t部分。

        将照相 机参数转换为OpenGL中的投影矩阵:

def set_projection_from_camera(K):
    """从照相机标定矩阵中获得试图"""

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()

    fx = K[0,0]
    fy = K[1,1]
    fovy = 2*math.tan(0.5 * height / fy) * 180/math.pi
    aspect = (width * fy) / (height * fx)

    #定义近的和远的剪裁平面
    near = 0.1
    far = 100.0

    #设定透视
    gluPerspective(fovy,aspect,near,far)
    glViewport(0,0,width,height)

        模拟视图矩阵能够表示相对的旋转和平移,该变换将该物体放置在照相机前(效果 是照相机在原点上)。模拟视图矩阵是个典型的4×4矩阵,如下所示:

\begin{bmatrix} R & t \\ 0 & 1 \end{bmatrix}

其中,R是旋转矩阵,列向量表示3个坐标轴的方向,t是平移向量。下面的函数实现如何获得移除标定矩阵后的3×4针孔照相机矩阵(将P和K-1相 乘),并创建一个模拟视图:

def set_modelview_from_camera(Rt):
    """从照相机姿态中获得模拟试图矩阵"""
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    #围绕x轴将茶壶旋转90度,使z轴向上
    Rx = np.array([[1,0,0],[0,0,-1],[0,1,0]])
    #获得旋转的最佳逼近
    R = Rt[:,:3]
    U,S,V = np.linalg.svd(R)
    R = np.dot(U,V)
    R[0,:] = -R[0,:] #改变x轴的符号
    #获得平移量
    t = Rt[:,3]
    #获得4*4的模拟视图矩阵
    M = np.eye(4)
    M[:3,:3] = np.dot(R,Rx)
    M[:3,3] = t
    #转置并压平以获取列序数值
    M = M.T
    m = M.flatten()
    #将模拟视图矩阵替换为新的矩阵
    glLoadMatrixf(m)

        4.4.3  在图像中放置虚拟物体

        我们需要做的第一件事是将图像(打算放置虚拟物体的图像)作为背景添加进来。在OpenGL中,该操作可以通过创建一个四边形的方式来完成,该四边形为整个视图。同时将投影和模拟试图矩阵重置, 使得每一维的坐标范围在-1到1之间。载入一幅图像,然后将其转换成一个OpenGL纹理,并将该纹理放 置在四边形上:

def draw_background(imname):
    """ 使用四边形绘制背景图像"""
    # 载入背景图像(应该是.bmp格式),转换为OpenGL纹理
    bg_image = pygame.image.load(imname).convert()
    bg_data = pygame.image.tostring(bg_image, "RGBX", 1)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    #绑定纹理
    glEnable(GL_TEXTURE_2D)
    glBindTexture(GL_TEXTURE_2D, glGenTextures(1))
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bg_data)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_fiLTER, GL_NEAREST)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_fiLTER, GL_NEAREST)

    #创建四方形填充整个窗口
    glBegin(GL_QUADS)
    glTexCoord2f(0.0, 0.0);
    glVertex3f(-1.0, -1.0, -1.0)
    glTexCoord2f(1.0, 0.0);
    glVertex3f(1.0, -1.0, -1.0)
    glTexCoord2f(1.0, 1.0);
    glVertex3f(1.0, 1.0, -1.0)
    glTexCoord2f(0.0, 1.0);
    glVertex3f(-1.0, 1.0, -1.0)
    glEnd()
    #清除纹理
    glDeleteTextures(1)

       下面的函数将会设置颜色和其他特性,产生一个红色的漂亮茶壶:

from OpenGL.GLUT import *

def draw_teapot(size):
    """ 在原点处绘制红色茶壶"""
    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)
    glEnable(GL_DEPTH_TEST)
    glClear(GL_DEPTH_BUFFER_BIT)

    #绘制红色茶壶
    glMaterialfv(GL_FRONT, GL_AMBIENT, [0, 0, 0, 0])
    glMaterialfv(GL_FRONT, GL_DIFFUSE, [0.5, 0.0, 0.0, 0.0])
    glMaterialfv(GL_FRONT, GL_SPECULAR, [0.7, 0.6, 0.6, 0.0])
    glMaterialf(GL_FRONT, GL_SHININESS, 0.25 * 128.0)
    glutSolidTeapot(size)

         4.4.4  综合集成

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import pygame, pygame.image
from pygame.locals import *
import pickle
width,height = 1000,747
def setup():
    """ 设置窗口和pygame 环境"""
    pygame.init()
    pygame.display.set_mode((width,height),OPENGL | DOUBLEBUF)
    pygame.display.set_caption('OpenGL AR demo')

# 载入照相机数据
with open('ar_camera.pkl','r') as f:
  K = pickle.load(f)
  Rt = pickle.load(f)
  setup()
  draw_background('book_perspective.bmp')
  set_projection_from_camera(K)
  set_modelview_from_camera(Rt)
  draw_teapot(0.02)
  while True:
      event = pygame.event.poll()
      if event.type in (QUIT, KEYDOWN):
          break
  pygame.display.flip()
  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值