目录

引言

一、 针孔照相机模型

1.1 照相机矩阵

1.2 照相机矩阵的分解

二、 照相机标定

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

四、增强现实


引言

        本章学习目标是尝试对照相机进行建模,并有效地使用这些模型。在之前的学习里我们讲述了图像到图像之间的映射和变换,而在处理三维图像和平面图像之间的映射,我们还需在映射中加入部分照相机产生图像过程的投影特性。

一、 针孔照相机模型

        针孔照相机模型是计算机视觉中广泛使用的照相机模型。这个模型来源于一种类似暗照机的照相机,从一个小孔采集射到暗箱内部的光线。在针孔照相机模型中,在光线投影到图像平面之前,从唯一一个点经过,即照相机中心。

        上图中由图像坐标轴和三维坐标系中的x和y轴对齐平行的假设,可以得出针孔照相机的投影性质。考虑到针孔照相机的光学坐标轴于z轴一致,可以将投影几何简化成相似三角形。在投影前通过旋转和平移变换,对该坐标系加入三维点,会出现完整的投影变换。

        针孔照相机中,三维点X投影为图像点x如下所示:

\lambda x=PX

3*4的矩阵P为照相机矩阵(投影矩阵)。在齐次坐标系中,三维点X的坐标由4个元素组成,X=[X,Y,Z,W],\lambda是三维点的逆深度。

1.1 照相机矩阵

        可以将照相机矩阵分解为:

P=K[R|t]

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

f为图像平面和照相机中心间的距离,当像素数组在传感器上偏斜时,需要用到倾斜参数s,一般情况下s可以为0,即:

两个K矩阵中的f_{x},f_{y}存在着f_{x}=af_{y}的关系。a是纵横比例参数,是在像素元素非正方形的情况下使用的,默认为1。

        除焦距外,标定矩阵中剩余的唯一参数为光心的坐标c=[c_{x},c_{y}],也就是光线坐标轴和图像平面的交点。

        使用的例子展示了如何将三维中的点投影到图像视图中,使用的是书中给出的牛津多视图数据集中的Model Housing数据集,可以从Visual Geometry Group - University of Oxford下载使用。下载完成后将需要使用的house.p3d复制到项目中即可。

        在实验开始的一开始,需要创建一个照相机类,用以处理照相机和投影建模所需的全部操作:

from scipy import linalg
from pylab import *

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

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

之后就可以对三维几何图像文件进行处理了:

from cam import Camera
from numpy import *
from pylab import *

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

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

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

 下载得到的原图如下图1所示,在经过投影处理后得到图二的形式:

图1
图2

 当想研究照相机的移动如何改变投影效果时,可以使用以下代码:

from cam import Camera
from pylab import *
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(P)
x = cam.project(points)
# 创建变换
r = 0.05 * random.random(3)
rot = Camera.rotation_matrix(r)

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

# 旋转矩阵和投影
figure()
for t in range(20):
    cam.P = dot(cam.P, rot)
    x = cam.project(points)
    plot(x[0], x[1], 'k.')
show()

这里使用了rotation_matrix()函数,起创建一旋转矩阵其能够围绕向量进行三维旋转。

随机旋转的投影轨迹

因为该图是图2的点围绕一个随机向量旋转后三维点投影的轨迹,可以通过尝试多运行几次程序,观察不同图片之间的联系可以更好的理解投影中的旋转。 

1.2 照相机矩阵的分解

        给定照相机矩阵P,我们需要恢复内参数K以及照相机的位置t和姿势R,将矩阵分块操作称为因子分解,这里使用RQ因子分解。

具体代码如下:

from cam import Camera
from pylab import *
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(dot(K, Rt))
print(K, Rt)
print(cam.factor())

其中factor函数为: 

    def factor(self):
        """
        将照相机矩阵分解为K,R,L,其中,P = K[R|t]
        :return:
        """
        # 分解前3*3的部分
        K, R = linalg.rq(self.P[:, :3])
        # 将K的对角线元素设为正值
        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的逆矩阵为其本身
            self.t = dot(linalg.inv(self.K), self.P[:, 3])
            return self.K, self.R, self.t

运行后得出结果:

因为RQ因子分解的结果并不唯一,其结果往往存在着符号二义性,故需要限制旋转矩阵R为正定的,上面的代码中做到了这一点,因此我们可以看到其输出结果与输入并不一致。

二、 照相机标定

        标定照相机是指计算出该照相机的内参数,即例子中的矩阵K。

        相机标定方法有:传统相机标定法、主动视觉相机标定方法、相机自标定法、零失真相机标定法。

例子

计算手机的内参矩阵与外参函数。

选用标定板宽:12,高:9

使用设备:RedmiK50

使用数据集:

使用手机镜头对标定板从各个方位进行拍照:

 

实现代码为:

# 标定目的建立相机成像几何模型并矫正透视畸变
import cv2
import numpy as np
import glob

# 找棋盘格角点
# 设置寻找亚像素角点的参数,采用的停止准则是最大循环次数30和最大误差容限0.001
# TERM_CRITERIA_EPS控制迭代的精度,设置迭代次数
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 阈值
#棋盘格模板规格
w = 11   # 12 - 1 
h = 8   # 8  - 1 
# 世界坐标系中的棋盘格点,例如(0,0,0), (1,0,0), (2,0,0) ....,(8,5,0),去掉Z坐标,记为二维矩阵
objp = np.zeros((w*h,3), np.float32)  # 创建一个零矩阵
objp[:,:2] = np.mgrid[0:w,0:h].T.reshape(-1,2) 
objp = objp*18.1

# 储存棋盘格角点的世界坐标和图像坐标对
objpoints = [] # 在世界坐标系中的三维点
imgpoints = [] # 在图像平面的二维点
#加载pic文件夹下所有的jpg图像
images = glob.glob('D://vs_prac//computer_version//pic//*.jpg') 
# print(len(images))
i=0
for fname in images:
    img = cv2.imread(fname)
    h1, w1 = img.shape[:2]  
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # print(w1)
    u, v = img.shape[:2]
    # 找到棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, (w,h), None)
    # 如果找到足够点对,将其存储起来
    if ret == True:
        i = i+1
        # 在原角点的基础上寻找亚像素角点
        cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
        # 追加进入世界三维点和平面二维点中
        objpoints.append(objp)
        print(objpoints)
        imgpoints.append(corners)
        # 将角点在图像上显示
        cv2.drawChessboardCorners(img, (w,h), corners, ret)
        cv2.namedWindow('findCorners', cv2.WINDOW_NORMAL)
        cv2.resizeWindow('findCorners', 640, 480)
        cv2.imshow('findCorners',img)
        cv2.waitKey(200)     
cv2.destroyAllWindows() # 标定
    
#计算内参矩阵,畸变系数,旋转向量和平移矩阵
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

print("内参矩阵mtx:\n",mtx)      # 内参数矩阵
print("dist畸变值:\n",dist   )   # 畸变系数   distortion cofficients = (k_1,k_2,p_1,p_2,k_3)
print("rvecs旋转(向量)外参:\n",rvecs)   # 旋转向量  # 外参数
print("tvecs平移(向量)外参:\n",tvecs  )  # 平移向量  # 外参数

最终可得内参,畸变值,平移外参与旋转外参为: 

 

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

        之前学习过从平面间估计单应性矩阵。当图像中有包含平面状的标记物体,并且已经对照相机进行了标记,那么就可以计算出照相机的姿态。标记物可以为任何平坦的物体。

from pylab import *
from PIL import Image
from numpy import *
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):
    """ 创建用于绘制立方体的一个点列表(前5个点是底部的正方形,一些边存在重合) """
    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('D:\\picture\\test2_perspective.jpg', 'im0.sift')
l0, d0 = sift.read_features_from_file('im0.sift')
sift.process_image('D:\\picture\\test2_frontal.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((3024, 4032))
# 位于边长为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('D:\\picture\\test2_perspective.jpg'))
im1 = array(Image.open('D:\\picture\\test2_frontal.jpg'))
figure()
imshow(im0)
plot(box_cam1[0, :], box_cam1[1, :], linewidth=3)
figure()
imshow(im1)
plot(box_trans[0, :], box_trans[1, :], linewidth=3)
figure()
imshow(im1)
plot(box_cam2[0, :], box_cam2[1, :], linewidth=3)
show()

例子中首先对两幅图像提取SIFT特征,并使用RANSAC算法估计单应性矩阵,该单应性矩阵将一幅图像中标记物上的点映射到另一幅图像中的对应点。之后就是定义相应的三维坐标系,使得标记物在X-Y平面上,通过设置分辨率就可以产生第一个标定矩阵,分辨率可以根据图像的来确定。

四、增强现实

        增强现实又称AR,是将物体和相应信息放置在图像数据上的一系列操作的总称,是当前非常热门的一项技术,而最为典型的例子就是放置一个三维计算机图形学模型,并使之看起来属于该场景。在学习这章时,需要使用到PyGame和PyOpenGL工具包。

import math
import pickle
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


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 * 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()
    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, :]
    t = Rt[:, 3]
    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)


width, height = 1000, 747


def setup():
    pygame.init()
    pygame.display.set_mode((width, height), OPENGL | DOUBLEBUF)
    pygame.display.set_caption("OpenGL AR demo")


# compute features
sift.process_image('D:\\picture\\test2_perspective.jpg', 'im0.sift')
l0, d0 = sift.read_features_from_file('im0.sift')

sift.process_image('D:\\picture\\test2_frontal.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)

K = my_calibration((747, 1000))
cam1 = camera.Camera(hstack((K, dot(K, array([[0], [0], [-1]])))))
box = cube_points([0, 0, 0.1], 0.1)
box_cam1 = cam1.project(homography.make_homog(box[:, :5]))
box_trans = homography.normalize(dot(H, box_cam1))
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)

Rt = dot(linalg.inv(K), cam2.P)

setup()
draw_background("D:\\picture\\test2_perspective.bmp")
set_projection_from_camera(K)
set_modelview_from_camera(Rt)
draw_teapot(0.05)

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

实验输出理应如下,借鉴师兄师姐的输出图。 

我自己在实验中程序能够正常运行,但出现了如下的报错:

在查阅网络上提供的资料后,下载了pygame和pyopengl的64位版本,实验仍然报错,将python环境转为anaconda,这时又出现了

还尝试了下载gult64.dll文件并放入syswow64文件夹,均报错,还需进一步实验,也请求各位大佬的帮助。

已解决

修改方式:未修改gult64.dll的情况下,对下面两部分进行更改:

 此时就能成功运行了。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐