针孔照相机模型(有时称为射影照相机模型)是计算机视觉中广泛使用的照相机模
型。对于大多数应用来说,针孔照相机模型简单,并且具有足够的精确度。这个名
字来源于一种类似暗箱机的照相机。该照相机从一个小孔采集射到暗箱内部的光线。
在针孔照相机模型中,在光线投影到图像平面之前,从唯一一个点经过,也就是
照相机中心 C。图 4-1 为从照相机中心前画出图像平面的图解。事实上,在真实的
照相机中,图像平面位于照相机中心之后,但是照相机的模型和图 4-1 的模型是一
样的。
由图像坐标轴和三维坐标系中的 x 轴和 y 轴对齐平行的假设,我们可以得出针孔照
相机的投影性质。照相机的光学坐标轴和 z 轴一致,该投影几何可以简化成相似三
角形。在投影之前通过旋转和平移变换,对该坐标系加入三维点,会出现完整的投
影变换。
在针孔照相机中,三维点 X 投影为图像点 x(两个点都是用齐次坐标表示的),如下
所示:
这里,3×4 的矩阵 P 为照相机矩阵(或投影矩阵)。注意,在齐次坐标系中,三维
点 X 的坐标由 4 个元素组成,X=[X, Y, Z, W]。这里的标量 λ 是三维点的逆深度。如
果我们打算在齐次坐标中将最后一个数值归一化为 1,那么就会使用到它。
照相机矩阵可以分解为:
其中,R 是描述照相机方向的旋转矩阵,t 是描述照相机中心位置的三维平移向量,内标定矩阵 K 描述照相机的投影性质。
标定矩阵仅和照相机自身的情况相关,通常情况下可以写成:
图像平面和照相机中心间的距离为焦距 f。当像素数组在传感器上偏斜的时候,需要用到倾斜参数 s。在大多数情况下,s 可以设置成 0。α通常为1。
除焦距之外,标定矩阵中剩余的唯一参数为光心(有时称主点)的坐标 c=[cx,cy],也就是光线坐标轴和图像平面的交点。因为光心通常在图像的中心,并且图像的坐标是从左上角开始计算的,所以光心的坐标常接近于图像宽度和高度的一半。特别强调一点,在这个例子中,唯一未知的变量是焦距 f。
# -*- coding: utf-8 -*-
from PCV.geometry import camera
from pylab import *
import random
import os
from numpy import *
# 载入点
points = loadtxt('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()
subplot(121)
plot(x[0],x[1],'k.')
# 创建变换
r = 0.05*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()
使用齐次坐标来表示这些点。然后我们使用一个投影矩阵来创建 Camera对象将这些三维点投影到图像平面并执行绘制操作。
如果给定照相机矩阵 P,我们需要恢复内参数 K 以及照相机的位置 t 和姿势 R。矩阵分块操作称为因子分解。这里,我们将使用一种矩阵因子分解的方法,称为 RQ 因子分解。
齐次坐标下,物体的物理坐标是 [x,y,z,1]′的形式,图像上对应点的坐标是 [u,v,1]′的形式,所以相机矩阵 P作为把物体映射成像的矩阵,应该是一个 3×4 的矩阵。
因此,照相机矩阵可以表示为如下形式:
| 代表的是增广矩阵
M 代表的是可逆的 3×3 的矩阵
C 是列向量,代表世界坐标系中相机的位置
要想求得 C, 只需要 P 的前三列组成的M, 求出来 −M−1 再乘以最后一列,就得到了 C矩阵的确可以把 3D的点投影到 2D 空间,但是,这种形式下表达的信息是很粗糙的,比如:没有提供相机的摆放姿态,没有相机内部的几何特征。
为了挖掘这些信息,对矩阵进一步分解为一个内参矩阵和外参矩阵的乘积:
1,矩阵R是rotation矩阵,因此是正交的;K是上三角矩阵. 对P的前三列进行RQ分解就可得到KR
2,T=−RC, 是摄像机坐标系下世界坐标系原点的位置, tx,ty,tz分别代表世界坐标原点在相机的 左右,上下,前后 位置关系。在三维重建中,一组标定好了的图片序列,它们各自的相机矩阵中的T应该是相同的。
3,其中 K 就是内参,R,T 为外参。
RQ 因子分解的结果并不是唯一的。在该因子分解中,分解的结果存在符号二义性。由于我们需要限制旋转矩阵 R 为正定的(否则,旋转坐标轴即可),所以如果需要,我们可以在求解到的结果中加入变换 T 来改变符号。
import camera
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()
标定照相机是指计算出该照相机的内参数。标定照相机的标准方法是,拍摄多幅平面棋盘模式的图像,然后进行处理计算。
求解相机参数的过程就称之为相机标定。
相机模型中的四个平面坐标系:
1.1图像像素坐标系(u,v)
以像素为单位,是以图像的左上方为原点的图像坐标系;
1.2图像物理坐标系(也叫像平面坐标系)(x,y)
以毫米为单位,用物理单位表示图像像素位置,定义坐标系OXY,原点O定义在相机Zc轴与图像平面交点;
1.3相机坐标系(Xc,Yc,Zc)
以毫米为单位,以相机的光心作为原点,Zc轴与光轴重合,并垂直于成像平面,且取摄影方向为正方向,Xc、Yc轴与图像物理坐标系的x,y轴平行,且OcO为摄像机的焦距f;
1.4世界坐标系(Xw,Yw,Zw)
根据具体情况而定,该坐标系描述环境中任何物体的位置,根据具体情况而定,满足右手法则;
坐标系的关系图:(转载自 http://blog.csdn.net/yonger_/article/details/55194602
https://www.cnblogs.com/wyuzl/p/7760601.html)
具体操作步骤:
使用一个例子来演示如何进行姿态估计。
首先提取图像的SIFT特征,然后使用RANSAC算法稳健地估计单应性矩阵;
有了单应性矩阵和照相机的标定矩阵,可以得出两个视图间的相对变换:
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
from pylab import *
from PIL import Image
from numpy import *
# If you have PCV installed, these imports should work
from PCV.geometry import homography, camera
from PCV.localdescriptors import sift
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 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
# 计算特征
sift.process_image('book_frontal.jpg', 'im0.sift')
l0, d0 = sift.read_features_from_file('im0.sift')
sift.process_image('book_perspective.jpg', 'im1.sift')
l1, d1 = sift.read_features_from_file('im1.sift')
# 匹配特征并计算单应性矩阵
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)
# 计算照相机标定矩阵
K = my_calibration((747, 1000))
# 位于边长为0.2 z=0平面的三维点
box = cube_points([0, 0, 0.1], 0.1)
# 投影第一幅图像上底部的正方形
cam1 = camera.Camera(hstack((K, dot(K, array([[0], [0], [-1]])))))
# 底部正方形上的点
box_cam1 = cam1.project(homography.make_homog(box[:, :5]))
# 使用H将点变换到第二幅图像中
box_trans = homography.normalize(dot(H,box_cam1))
# 从cam1和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)
# 使用第二个照相机矩阵投影
box_cam2 = cam2.project(homography.make_homog(box))
# plotting
im0 = array(Image.open('book_frontal.jpg'))
im1 = array(Image.open('book_perspective.jpg'))
figure()
imshow(im0)
plot(box_cam1[0, :], box_cam1[1, :], linewidth=3)
title('2D projection of bottom square')
axis('off')
figure()
imshow(im1)
plot(box_trans[0, :], box_trans[1, :], linewidth=3)
title('2D projection transfered with H')
axis('off')
figure()
imshow(im1)
plot(box_cam2[0, :], box_cam2[1, :], linewidth=3)
title('3D points projected in second image')
axis('off')
show()
增强现实(Augmented Reality,AR)是将物体和相应信息放置在图像数据上的一
系列操作的总称。最经典的例子是放置一个三维计算机图形学模型,使其看起来属
于该场景;如果在视频中,该模型会随着照相机的运动很自然地移动。
PyGame 是非常流行的游戏开发工具包,它可以非常简单地处理显示窗口、输入设
备、事件,以及其他内容。
PyOpenGL 是 OpenGL 图形编程的 Python 绑定接口。OpenGL 可以安装在几乎所
有的系统上,并且具有很好的图形性能。
直接使用PIP可以安装pygame
pip install pygame
使用pip安装 pyopengl则会报错,OpenGL.error.NullFunctionError: Attempt to call an undefined function”
正确操作首先手动下载PyOpenGL-3.1.3b2-cp27-cp27m-win_amd64.whl
http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyopengl 再pip
pip install PyOpenGL-3.1.3b2-cp27-cp27m-win_amd64.whl
# -*- coding: utf-8 -*-
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame,pygame.image
from OpenGL.GLUT import *
def drawFunc():
# 清除之前画面
glClear(GL_COLOR_BUFFER_BIT)
glRotatef(0.1, 5, 5, 0) # (角度,x,y,z)
glutSolidTeapot(0.5) # 实心茶壶
# 刷新显示
glFlush()
# 使用glut初始化OpenGL
glutInit()
# 显示模式:GLUT_SINGLE无缓冲直接显示|GLUT_RGBA采用RGB(A非alpha)
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA)
# 窗口位置及大小-生成
glutInitWindowPosition(0, 0)
glutInitWindowSize(400, 400)
glutCreateWindow(b"first")
# 调用函数绘制图像
glutDisplayFunc(drawFunc)
glutIdleFunc(drawFunc)
# 主循环
glutMainLoop()
OpenGL 使用 4×4 的矩阵来表示变换(包括三维变换和投影)。这和我们使用的 3×4 照相机矩阵略有差别。但是,照相机与场景的变换分成了两个矩阵,GL_PROJECTION 矩阵和 GL_MODELVIEW 矩阵。GL_PROJECTION 矩阵处理图像成像的性质,等价于我们的内标定矩阵 K。GL_MODELVIEW 矩阵处理物体和照相机之间的三维变换关系,对应于我们照相机矩阵中的 R 和 t 部分。一个不同之处是,假设照相机为坐标系的中心,GL_MODELVIEW 矩阵实际上包含了将物体放置在照相机前面的变换。
第一件事是将图像(打算放置虚拟物体的图像)作为背景添加进来。在 OpenGL 中,该操作可以通过创建一个四边形的方式来完成,该四边形为整个视图。完成该操作最简单的方式是绘制出四边形,同时将投影和模拟试图矩阵重置,使得每一维的坐标范围在 -1 到 1 之间。
# -*- coding: utf-8 -*-
import math
import pickle
import sys
from pylab import *
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import pygame, pygame.image
from pygame.locals import *
from PCV.geometry import homography, camera
from PCV.localdescriptors import sift
from numpy import *
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 = []
# 底部
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 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 * math.atan(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)
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)
def draw_background(imname):
# 载入背景图像
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)
glFlush()
def drawFunc(size): # 白色茶壶
glRotatef(0.5, 5, 5, 0) # (角度,x,y,z)
glutWireTeapot(size)
# 刷新显示
glFlush()
width, height = 1000, 747
def setup(): # 设置窗口和pygame环境
pygame.init()
pygame.display.set_mode((width, height), OPENGL | DOUBLEBUF)
pygame.display.set_caption("OpenGL AR demo")
# 计算特征
sift.process_image('book_frontal.jpg', 'im0.sift')
l0, d0 = sift.read_features_from_file('im0.sift')
sift.process_image('book_perspective.jpg', 'im1.sift')
l1, d1 = sift.read_features_from_file('im1.sift')
# 匹配特征,计算单应性矩阵
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)
# 计算照相机标定矩阵
K = my_calibration((747, 1000))
# 位于边长为0.2,z=0平面上的三维点
box = cube_points([0, 0, 0.1], 0.1)
# 投影第一幅图下个上底部的正方形
cam1 = camera.Camera(hstack((K, dot(K, array([[0], [0], [-1]])))))
# 底部正方形上的点
box_cam1 = cam1.project(homography.make_homog(box[:, :5]))
# 使用H将点变换到第二幅图像中
box_trans = homography.normalize(dot(H, box_cam1))
# 从cam1和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)
# 使用第二个照相机矩阵投影
box_cam2 = cam2.project(homography.make_homog(box))
Rt = dot(linalg.inv(K), cam2.P)
setup()
draw_background("book_perspective.bmp")
set_projection_from_camera(K)
set_modelview_from_camera(Rt)
draw_teapot(0.05) # 显示红色茶壶
drawFunc(0.05) # 显示白色空心茶壶
pygame.display.flip()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
在这里插入图片描述
首先,该脚本使用 Pickle 载入照相机标定矩阵,以及照相机矩阵中的旋转和平移部
分。假设这些信息已经按照 4.3 节的描述进行保存。setup() 函数会初始化 PyGame,
将窗口设置为图像的大小,绘制图像区域为两倍的 OpenGL 窗口缓存大小。接下来,
载入背景图像,使其与窗口相符。然后,设定照相机和模拟视图矩阵。最后,在正
确的位置上绘制出茶壶。