本章中,我们将尝试对照相机进行建模,并有效地使用这些模型。在之前的章节里, 我们已经讲述了图像到图像之间的映射和变换。为了处理三维图像和平面图像之间 的映射,我们需要在映射中加入部分照相机产生图像过程的投影特性。本章中我们 将会讲述如何确定照相机的参数,以及在具体应用中,如增强现实,如何使用图像 间的投影变换。下一章中,我们将使用照相机模型处理其他一些应用,比如多视图 及其映射。

1、针孔相机模型

(1)照相机矩阵

针孔照相机模型(有时称为射影照相机模型)是计算机视觉中广泛使用的照相机模 型。如下图所示:

在针孔照相机中,三维点 X 投影为图像点 x(两个点都是用齐次坐标表示的),如下 所示:

这里,3×4 的矩阵 P 为照相机矩阵(或投影矩阵)。注意,在齐次坐标系中,三维 点 X 的坐标由 4 个元素组成,X=[X, Y, Z, W]。这里的标量 λ 是三维点的逆深度。如 果我们打算在齐次坐标中将最后一个数值归一化为 1,那么就会使用到它。

照相机矩阵可以分解为:

其中,R 是描述照相机方向的旋转矩阵,t 是描述照相机中心位置的三维平移向量,内标定矩阵 K 描述照相机的投影性质。 标定矩阵仅和照相机自身的情况相关,通常情况下可以写成:

图像平面和照相机中心间的距离为焦距 f。当像素数组在传感器上偏斜的时候,需要 用到倾斜参数 s。在大多数情况下,s 可以设置成 0。也就是说:

这里,我们使用了另外的记号 fx 和 fy,两者关系为 fx=αfy。纵横比例参数 α 是在像素元素非正方形的情况下使用的。通常情况下,我们可以默认设置 α=1。经过这些假设,标定矩阵变为:

除焦距之外,标定矩阵中剩余的唯一参数为光心(有时称主点)的坐标 c=[cx,cy], 也就是光线坐标轴和图像平面的交点。因为光心通常在图像的中心,并且图像的坐 标是从左上角开始计算的,所以光心的坐标常接近于图像宽度和高度的一半。特别 强调一点,在这个例子中,唯一未知的变量是焦距 f。

(2)三维点的投影

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

camera.py

from numpy import *
from scipy import linalg

class Camera(object):
    """ Class for representing pin-hole cameras. """
    
    def __init__(self,P):
        """ Initialize P = K[R|t] camera model. """
        self.P = P
        self.K = None # calibration matrix
        self.R = None # rotation
        self.t = None # translation
        self.c = None # camera center
          
    def project(self,X):
        """    Project points in X (4*n array) and normalize coordinates. """
        
        x = dot(self.P,X)
        for i in range(3):
            x[i] /= x[2]    
        return x
               
    def factor(self):
        """    Factorize the camera matrix into K,R,t as P = K[R|t]. """
        
        # factor first 3*3 part
        K,R = linalg.rq(self.P[:,:3])
        
        # make diagonal of K positive
        T = diag(sign(diag(K)))
        if linalg.det(T) < 0:
            T[1,1] *= -1
        
        self.K = dot(K,T)
        self.R = dot(T,R) # T is its own inverse
        self.t = dot(linalg.inv(self.K),self.P[:,3])
        
        return self.K, self.R, self.t
           
    def center(self):
        """    Compute and return the camera center. """
    
        if self.c is not None:
            return self.c
        else:
            # compute c by factoring
            self.factor()
            self.c = -dot(self.R.T,self.t)
            return self.c

# helper functions    
def rotation_matrix(a):
    """    Creates a 3D rotation matrix for rotation
        around the axis of the vector a. """
    R = eye(4)
    R[:3,:3] = linalg.expm([[0,-a[2],a[1]],[a[2],0,-a[0]],[-a[1],a[0],0]])
    return R
    
def rq(A):
    from scipy.linalg import qr
    
    Q,R = qr(flipud(A).T)
    R = flipud(R.T)
    Q = Q.T  
  
    return R[:,::-1],Q[::-1,:]

下面的例子展示如何将三维中的点投影到图像视图中。

import camera
from pylab import *
import numpy as np

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

# 设置照相机参数
P = hstack((eye(3),array([[0],[0],[-10]]))) 
cam = camera.Camera(P)
x = cam.project(points)

# 绘制投影
figure() 
plot(x[0],x[1],'k.') 
show()

# 创建变换
r = 0.05*np.random.rand(3)
rot = camera.rotation_matrix(r)
# 旋转矩阵和投影 figure()
for t in range(20):
	cam.P = dot(cam.P,rot) 
	x = cam.project(points) 
	plot(x[0],x[1],'k.')
	
	show()

投影三维点示例:样本图像(左),图像视图中投影后的点(中);经过照相机旋转后,投影点的轨迹(右)。数据来自于牛津“Model House”数据集 。

(3)照相机矩阵的分解

在示例照相机上运行下面的代码,观察照相机矩阵分解的效果。

import camera
from numpy import *

K = array([[1000,0,500],[0,1000,300],[0,0,1]]) 
tmp = camera.rotation_matrix([0,0,1])[:3,:3] 
Rt = hstack((tmp,array([[50],[40],[30]])))
cam = camera.Camera(dot(K,Rt))

print(K,Rt)
print(cam.factor())
#计算出两组数据不一样,可能是由于rq计算结果具有二义性导致

(4)计算照相机中心

给定照相机投影矩阵 P,我们可以计算出空间上照相机的所在位置。照相机的中心C,是一个三维点,满足约束 PC=0。对于投影矩阵为 P=K[R|t] 的照相机,有: 

照相机的中心可以由下述式子来计算:

注意,如预期一样,照相机的中心和内标定矩阵 K 无关。代码实现详见camera.py中center()函数定义。

2、照相机标定

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

  • 测量你选定矩形标定物体的边长 dX 和 dY;

  • 将照相机和标定物体放置在平面上,使得照相机的背面和标定物体平行,同时物

    体位于照相机图像视图的中心,你可能需要调整照相机或者物体来获得良好的对

    齐效果;

  • 测量标定物体到照相机的距离 dZ;

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

  • 使用像素数来测量标定物体图像的宽度和高度 dx 和 dy。

简单的照相机标定设置:进行标定实验使用的设置情况图像(左);用于标定的图像 (右)。在图像中测量标定物体的宽度和高度,以及设置中标定物体的实际物理尺寸,就可以确定焦距的大小。

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

我们使用一个例子来演示如何进行姿态估计。这里借助book的两幅图像。 我们使用下面的代码来提取两幅图像的 SIFT 特征,然后使用 RANSAC 算法稳健地 估计单应性矩阵,代码实现:

from pylab import *
from PIL import Image
# import pickle

# If you have PCV installed, these imports should work
# from PCV.geometry import homography, camera
# from PCV.localdescriptors import sift
import homography,camera,sift
"""
This is the augmented reality and pose estimation cube example from Section 4.3.
"""


def cube_points(c, wid):
    """ Creates a list of points for plotting
        a cube with plot. (the first 5 points are
        the bottom square, some sides repeated). """
    p = []
    # bottom
    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]) #same as first to close plot
    
    # top
    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]) #same as first to close plot
    
    # vertical sides
    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 array(p).T


def my_calibration(sz):
    """
    Calibration function for the camera (iPhone4) used in this example.
    """
    row, col = sz
    fx = 2555*col/2592
    fy = 2586*row/1936
    K = diag([fx, fy, 1])
    K[0, 2] = 0.5*col
    K[1, 2] = 0.5*row
    return K



# compute features
sift.process_image('../data/book_frontal.JPG', 'im0.sift')
l0, d0 = sift.read_features_from_file('im0.sift')

sift.process_image('../data/book_perspective.JPG', 'im1.sift')
l1, d1 = sift.read_features_from_file('im1.sift')


# match features and estimate homography
matches = sift.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.RansacModel()
H, inliers = homography.H_from_ransac(fp, tp, model)

# camera calibration
K = my_calibration((747, 1000))

# 3D points at plane z=0 with sides of length 0.2
box = cube_points([0, 0, 0.1], 0.1)

# project bottom square in first image
cam1 = camera.Camera(hstack((K, dot(K, array([[0], [0], [-1]])))))
# first points are the bottom square
box_cam1 = cam1.project(homography.make_homog(box[:, :5]))


# use H to transfer points to the second image
box_trans = homography.normalize(dot(H,box_cam1))

# compute second camera matrix from cam1 and H
cam2 = camera.Camera(dot(H, cam1.P))
A = dot(linalg.inv(K), cam2.P[:, :3])
A = array([A[:, 0], A[:, 1], cross(A[:, 0], A[:, 1])]).T
cam2.P[:, :3] = dot(K, A)

# project with the second camera
box_cam2 = cam2.project(homography.make_homog(box))

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

# Rt = dot(linalg.inv(K), cam2.P)
import pickle
with open('../data/ar_camera.pkl','wb') as f: 
    pickle.dump(K,f,0) 
    pickle.dump(dot(linalg.inv(K),cam2.P),f)

print("ar_camera.pkl is generated!")

# plotting
im0 = array(Image.open('book_frontal.JPG'))
im1 = array(Image.open('book_perspective.JPG'))

figure()
subplot(221)
imshow(im0)
plot(box_cam1[0, :], box_cam1[1, :], linewidth=3)
title('2D projection of bottom square')
axis('off')


subplot(222)
imshow(im1)
plot(box_trans[0, :], box_trans[1, :], linewidth=3)
title('2D projection transfered with H')
axis('off')

subplot(223)
imshow(im1)
plot(box_cam2[0, :], box_cam2[1, :], linewidth=3)
title('3D points projected in second image')
axis('off')

show()

运行结果:

4、增强现实

增强现实(Augmented Reality,AR)是将物体和相应信息放置在图像数据上的一 系列操作的总称。最经典的例子是放置一个三维计算机图形学模型,使其看起来属 于该场景;如果在视频中,该模型会随着照相机的运动很自然地移动。如上一节所 示,给定一幅带有标记平面的图像,我们能够计算出照相机的位置和姿态,使用这 些信息来放置计算机图形学模型,能够正确表示它们。在本章的最后一节,我们将 介绍如何建立一个简单的增强现实例子。其中,我们会用到两个工具包:PyGame 和 PyOpenGL。

(1)PyGame和 PyOpenGL

PyGame 是非常流行的游戏开发工具包,它可以非常简单地处理显示窗口、输入设 备、事件,以及其他内容。PyGame 是开源的,可以从 http://www.pygame.org/ 下 载。事实上,它是一个 Python 绑定的 SDL 游戏引擎。你可以查看附录 A 来获取 关于安装的帮助。你还可以查看文献 [21] 来获取关于 PyGame 工具包的更多编程 细节。

PyOpenGL 是 OpenGL 图形编程的 Python 绑定接口。OpenGL 可以安装在几乎所 有的系统上,并且具有很好的图形性能。OpenGL 具有跨平台性,能够在不同的操 作系统之间工作。关于 OpenGL 的更多信息,参见 http://www.opengl.org/。在开 始 页 面(http://www.opengl.org/wiki/Getting_started), 有 针 对 初 学 者 的 很 多 资 源。 PyOpenGL 是开源的,并且易于安装。你可以从附录 A 了解关于 PyOpenGL 更多内 容。你同样可以在项目网页 http://pyopengl.sourceforge.net/ 获取更多细节内容。

为了使用 PyGame 和 PyOpenGL 工具包来完成该应用,需要在脚本的开始部分载入下面的命令:

from OpenGL.GL import *

from OpenGL.GLU import *

import pygame, pygame.image

from pygame.locals import *

(2)从照相机矩阵到OpenGL格式

OpenGL 使用 4×4 的矩阵来表示变换(包括三维变换和投影)。这和我们使用 的 3×4 照相机矩阵略有差别。但是,照相机与场景的变换分成了两个矩阵, GL_PROJECTION 矩阵和 GL_MODELVIEW 矩阵。GL_PROJECTION 矩阵处理图 像成像的性质,等价于我们的内标定矩阵 K。GL_MODELVIEW 矩阵处理物体和照 相机之间的三维变换关系,对应于我们照相机矩阵中的 R 和 t 部分。一个不同之处 是,假设照相机为坐标系的中心,GL_MODELVIEW 矩阵实际上包含了将物体放置 在照相机前面的变换。OpenGL 有很多特性,我们会在下面例子中一一讲解。

(3)在图像中放置虚拟物体

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

(4)综合集成

代码实现:


from OpenGL.GL import * 
from OpenGL.GLU import * 
from OpenGL.GLUT import * 
import pygame, pygame.image 
from pygame.locals import * 
from pylab import *
import pickle
import sift
import homography
import camera

def cube_points(c, wid):
    """ Creates a list of points for plotting
        a cube with plot. (the first 5 points are
        the bottom square, some sides repeated). """
    p = []
    # bottom
    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]) #same as first to close plot
    
    # top
    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]) #same as first to close plot
    
    # vertical sides
    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 array(p).T
    
def my_calibration(sz):
    row, col = sz
    fx = 2555*col/2592
    fy = 2586*row/1936
    K = diag([fx, fy, 1])
    K[0, 2] = 0.5*col
    K[1, 2] = 0.5*row
    return K

def set_projection_from_camera(K): 
	""" 从照相机标定矩阵中获得视图 """
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	fx = K[0,0]
	fy = K[1,1]
	fovy = 2*arctan(0.5*height/fy)*180/pi
	aspect = (width*fy)/(height*fx)
	# 定义近的和远的剪裁平面 
	near = 0.1
	far = 100.0
	# 设定透视 
	gluPerspective(fovy,aspect,near,far) 
	glViewport(0,0,width,height)

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

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)

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)

# # compute features
# sift.process_image('../data/book_frontal.JPG', 'im0.sift')
# l0, d0 = sift.read_features_from_file('im0.sift')

# sift.process_image('../data/book_perspective.JPG', 'im1.sift')
# l1, d1 = sift.read_features_from_file('im1.sift')


# # match features and estimate homography
# matches = sift.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.RansacModel()
# H, inliers = homography.H_from_ransac(fp, tp, model)

# # camera calibration
# K = my_calibration((747, 1000))

# # 3D points at plane z=0 with sides of length 0.2
# box = cube_points([0, 0, 0.1], 0.1)

# # project bottom square in first image
# cam1 = camera.Camera(hstack((K, dot(K, array([[0], [0], [-1]])))))
# # first points are the bottom square
# box_cam1 = cam1.project(homography.make_homog(box[:, :5]))


# # use H to transfer points to the second image
# box_trans = homography.normalize(dot(H,box_cam1))

# # compute second camera matrix from cam1 and H
# cam2 = camera.Camera(dot(H, cam1.P))
# A = dot(linalg.inv(K), cam2.P[:, :3])
# A = array([A[:, 0], A[:, 1], cross(A[:, 0], A[:, 1])]).T
# cam2.P[:, :3] = dot(K, A)

# # project with the second camera
# box_cam2 = cam2.project(homography.make_homog(box))

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

# # Rt = dot(linalg.inv(K), cam2.P)
# import pickle
# with open('../data/ar_camera.pkl','wb') as f: 
#     pickle.dump(K,f,0) 
#     pickle.dump(dot(linalg.inv(K),cam2.P),f)

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('../data/ar_camera.pkl','rb') as f:
	K = pickle.load(f) 
	Rt = pickle.load(f)

setup() 
draw_background('../data/book_perspective.bmp') 
set_projection_from_camera(K) 
set_modelview_from_camera(Rt) 
draw_teapot(0.1)

pygame.display.flip()
while True:
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
       sys.exit()
       pygame.quit()
# pygame.display.flip()


如何不使用之前生成的ar_camera.pkl文件,可将注释的部分代码放开即可重新生成ar_camera.pkl文件。

运行结果:

(5)载入模型

代码实现:


from OpenGL.GL import * 
from OpenGL.GLU import * 
from OpenGL.GLUT import * 
import pygame, pygame.image 
from pygame.locals import * 
from pylab import *
import pickle
import sift
import homography
import camera

def cube_points(c, wid):
    """ Creates a list of points for plotting
        a cube with plot. (the first 5 points are
        the bottom square, some sides repeated). """
    p = []
    # bottom
    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]) #same as first to close plot
    
    # top
    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]) #same as first to close plot
    
    # vertical sides
    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 array(p).T
    
def my_calibration(sz):
    row, col = sz
    fx = 2555*col/2592
    fy = 2586*row/1936
    K = diag([fx, fy, 1])
    K[0, 2] = 0.5*col
    K[1, 2] = 0.5*row
    return K

def set_projection_from_camera(K): 
	""" 从照相机标定矩阵中获得视图 """
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	fx = K[0,0]
	fy = K[1,1]
	fovy = 2*arctan(0.5*height/fy)*180/pi
	aspect = (width*fy)/(height*fx)
	# 定义近的和远的剪裁平面 
	near = 0.1
	far = 100.0
	# 设定透视 
	gluPerspective(fovy,aspect,near,far) 
	glViewport(0,0,width,height)

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

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)

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)

def load_and_draw_model(filename):
	""" 使用 objloader.py,从 .obj 文件中装载模型 假设路径文件夹中存在同名的 .mtl 材料设置文件 """ 
	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.75,1.0,0.0]) 
	glMaterialf(GL_FRONT,GL_SHININESS,0.25*128.0)
	# 从文件中载入
	import objloader
	obj = objloader.OBJ(filename,swapyz=True) 
	glCallList(obj.gl_list)

# # compute features
# sift.process_image('../data/book_frontal.JPG', 'im0.sift')
# l0, d0 = sift.read_features_from_file('im0.sift')

# sift.process_image('../data/book_perspective.JPG', 'im1.sift')
# l1, d1 = sift.read_features_from_file('im1.sift')


# # match features and estimate homography
# matches = sift.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.RansacModel()
# H, inliers = homography.H_from_ransac(fp, tp, model)

# # camera calibration
# K = my_calibration((747, 1000))

# # 3D points at plane z=0 with sides of length 0.2
# box = cube_points([0, 0, 0.1], 0.1)

# # project bottom square in first image
# cam1 = camera.Camera(hstack((K, dot(K, array([[0], [0], [-1]])))))
# # first points are the bottom square
# box_cam1 = cam1.project(homography.make_homog(box[:, :5]))


# # use H to transfer points to the second image
# box_trans = homography.normalize(dot(H,box_cam1))

# # compute second camera matrix from cam1 and H
# cam2 = camera.Camera(dot(H, cam1.P))
# A = dot(linalg.inv(K), cam2.P[:, :3])
# A = array([A[:, 0], A[:, 1], cross(A[:, 0], A[:, 1])]).T
# cam2.P[:, :3] = dot(K, A)

# # project with the second camera
# box_cam2 = cam2.project(homography.make_homog(box))

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

# # Rt = dot(linalg.inv(K), cam2.P)
# import pickle
# with open('../data/ar_camera.pkl','wb') as f: 
#     pickle.dump(K,f,0) 
#     pickle.dump(dot(linalg.inv(K),cam2.P),f)

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('../data/ar_camera.pkl','rb') as f:
	K = pickle.load(f) 
	Rt = pickle.load(f)

setup() 
draw_background('../data/book_perspective.bmp') 
set_projection_from_camera(K) 
set_modelview_from_camera(Rt) 
# draw_teapot(0.1)
load_and_draw_model('../data/toyplane.obj')

pygame.display.flip()
while True:
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
       sys.exit()
       pygame.quit()
# pygame.display.flip()


运行结果:

5、其他

(1)开发环境

         本代码是在mac电脑sublimetxt编辑器python3.7.8下调试出来的,如果你要在windows/linux下编辑器/IDE的其他python版本运行的话,请对代码做相应的调整。

(2)obj和mtl文件

从http://www.oyonale.com/modeles.php网站下载toyplane.obj文件后还缺少mtl文件,如下自制mtl文件toyplane.mtl

newmtl lightblue
Kd 0.5 0.75 1.0
illum 1

运行结果会报错:

mtl = self.mtl[material]
KeyError: 'material2'

故载入模型的代码还有待调试。

(3)源码和图片

        已经调试过的源码和图片详见:

https://github.com/Abonaventure/pcv-book-code.git   或  https://gitlab.com/Abonaventure/pcv-book-code.git

部分图片可能未上传可在https://pan.baidu.com/share/link?shareid=4059473268&uk=3440544532 或者原书目录http://programmingcomputervision.com下载。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐