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

目录

[4.1 针孔照相机模型](#4.1 针孔照相机模型)

[4.1.1 照相机矩阵](#4.1.1 照相机矩阵)

[4.1.2 三维点的投影](#4.1.2 三维点的投影)

[4.1.3 照相机矩阵的分解](#4.1.3 照相机矩阵的分解)

[4.1.4 计算照相机中心](#4.1.4 计算照相机中心)

[4.2 照相机标定](#4.2 照相机标定)

[4.2.1 一个简单的标定方法](#4.2.1 一个简单的标定方法)

[4.3 以平面和标记物进行姿态估计](#4.3 以平面和标记物进行姿态估计)

[4.4 增强现实](#4.4 增强现实)

[4.4.1 PyGame和PyOenGL](#4.4.1 PyGame和PyOenGL)

[4.4.2 从照相机矩阵到OpenGL格式](#4.4.2 从照相机矩阵到OpenGL格式)

[4.4.3 在图像中放置虚拟物体](#4.4.3 在图像中放置虚拟物体)

[4.4.4 综合集成](#4.4.4 综合集成)


4.1 针孔照相机模型

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

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

4.1.1 照相机矩阵

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

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

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

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

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

4.1.2 三维点的投影

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

python 复制代码
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
python 复制代码
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以及照相机的 位置t和姿势R。矩阵分块操作称为因子分解,我们使用为RQ因子分解。将下面方法加入Camera类中。

python 复制代码
    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。故有

照相机的中心可以由来计算。下面代码是用以计算照相机的中心,将其添加到Camera类中。

python 复制代码
    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 一个简单的标定方法

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

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

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

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

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

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

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

假定,且,带入上述关系表达式可以获得焦距的大小为。此外,需要注意的是,我们现在获取的焦距是在特定图像分辨率下计算出来的,此例图像大小为2592×1936像素,用下面的辅助函数求出K。

python 复制代码
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 以平面和标记物进行姿态估计

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

python 复制代码
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 工具包来完成该应用,需要在脚本的开始部分载入下面的命令:

python 复制代码
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中的投影矩阵:

python 复制代码
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矩阵,如下所示:

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

python 复制代码
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纹理,并将该纹理放 置在四边形上:

python 复制代码
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)

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

python 复制代码
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 综合集成

python 复制代码
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()
相关推荐
985小水博一枚呀24 分钟前
【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法
人工智能·深度学习·神经网络·cnn·transformer
AltmanChan25 分钟前
大语言模型安全威胁
人工智能·安全·语言模型
985小水博一枚呀29 分钟前
【深度学习滑坡制图|论文解读2】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法
人工智能·深度学习·神经网络·cnn·transformer·迁移学习
数据与后端架构提升之路39 分钟前
从神经元到神经网络:深度学习的进化之旅
人工智能·神经网络·学习
爱技术的小伙子44 分钟前
【ChatGPT】如何通过逐步提示提高ChatGPT的细节描写
人工智能·chatgpt
深度学习实战训练营2 小时前
基于CNN-RNN的影像报告生成
人工智能·深度学习
昨日之日20064 小时前
Moonshine - 新型开源ASR(语音识别)模型,体积小,速度快,比OpenAI Whisper快五倍 本地一键整合包下载
人工智能·whisper·语音识别
浮生如梦_4 小时前
Halcon基于laws纹理特征的SVM分类
图像处理·人工智能·算法·支持向量机·计算机视觉·分类·视觉检测
深度学习lover4 小时前
<项目代码>YOLOv8 苹果腐烂识别<目标检测>
人工智能·python·yolo·目标检测·计算机视觉·苹果腐烂识别
热爱跑步的恒川5 小时前
【论文复现】基于图卷积网络的轻量化推荐模型
网络·人工智能·开源·aigc·ai编程