文章目录

  • 4.1针孔照相机模型
  • 4.1.1 照相机矩阵
  • 4.1.2三维点的投影
  • 4.1.3照相机矩阵的分解
  • 4.2 照相机标定
  • 4.3 以平面和标记物进行姿态估计
  • 4.4 增强现实
  • 4.4.1 PyGame和PyOpenGL
  • 4.4.2 综合集成


4.1针孔照相机模型

针孔照相机模型(有时称为射影照相机模型)是计算机视觉中广泛使用的照相机模

型。对于大多数应用来说,针孔照相机模型简单,并且具有足够的精确度。这个名

字来源于一种类似暗箱机的照相机。该照相机从一个小孔采集射到暗箱内部的光线。

在针孔照相机模型中,在光线投影到图像平面之前,从唯一一个点经过,也就是

照相机中心 C。图 4-1 为从照相机中心前画出图像平面的图解。事实上,在真实的

照相机中,图像平面位于照相机中心之后,但是照相机的模型和图 4-1 的模型是一

样的。

python代码配置basler工业相机回调函数来处理图像 python 相机_相机标定


由图像坐标轴和三维坐标系中的 x 轴和 y 轴对齐平行的假设,我们可以得出针孔照

相机的投影性质。照相机的光学坐标轴和 z 轴一致,该投影几何可以简化成相似三

角形。在投影之前通过旋转和平移变换,对该坐标系加入三维点,会出现完整的投

影变换。

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

所示:

python代码配置basler工业相机回调函数来处理图像 python 相机_相机标定_02


这里,3×4 的矩阵 P 为照相机矩阵(或投影矩阵)。注意,在齐次坐标系中,三维

点 X 的坐标由 4 个元素组成,X=[X, Y, Z, W]。这里的标量 λ 是三维点的逆深度。如

果我们打算在齐次坐标中将最后一个数值归一化为 1,那么就会使用到它。

4.1.1 照相机矩阵

照相机矩阵可以分解为:

python代码配置basler工业相机回调函数来处理图像 python 相机_因子分解_03


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

python代码配置basler工业相机回调函数来处理图像 python 相机_图像平面_04


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

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

4.1.2三维点的投影

# -*- 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()

python代码配置basler工业相机回调函数来处理图像 python 相机_因子分解_05


使用齐次坐标来表示这些点。然后我们使用一个投影矩阵来创建 Camera对象将这些三维点投影到图像平面并执行绘制操作。

4.1.3照相机矩阵的分解

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

齐次坐标下,物体的物理坐标是 [x,y,z,1]′的形式,图像上对应点的坐标是 [u,v,1]′的形式,所以相机矩阵 P作为把物体映射成像的矩阵,应该是一个 3×4 的矩阵。

因此,照相机矩阵可以表示为如下形式:

python代码配置basler工业相机回调函数来处理图像 python 相机_图像平面_06


| 代表的是增广矩阵

M 代表的是可逆的 3×3 的矩阵

C 是列向量,代表世界坐标系中相机的位置要想求得 C, 只需要 P 的前三列组成的M, 求出来 −M−1 再乘以最后一列,就得到了 C矩阵的确可以把 3D的点投影到 2D 空间,但是,这种形式下表达的信息是很粗糙的,比如:没有提供相机的摆放姿态,没有相机内部的几何特征。

为了挖掘这些信息,对矩阵进一步分解为一个内参矩阵和外参矩阵的乘积:

python代码配置basler工业相机回调函数来处理图像 python 相机_图像平面_07


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()

控制台得到的输出:

python代码配置basler工业相机回调函数来处理图像 python 相机_相机标定_08

4.2 照相机标定

标定照相机是指计算出该照相机的内参数。标定照相机的标准方法是,拍摄多幅平面棋盘模式的图像,然后进行处理计算。
求解相机参数的过程就称之为相机标定。
相机模型中的四个平面坐标系:
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)

根据具体情况而定,该坐标系描述环境中任何物体的位置,根据具体情况而定,满足右手法则;

坐标系的关系图:

python代码配置basler工业相机回调函数来处理图像 python 相机_图像平面_09


具体操作步骤:

python代码配置basler工业相机回调函数来处理图像 python 相机_图像平面_10

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

使用一个例子来演示如何进行姿态估计。
首先提取图像的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()

python代码配置basler工业相机回调函数来处理图像 python 相机_因子分解_11


python代码配置basler工业相机回调函数来处理图像 python 相机_图像平面_12


python代码配置basler工业相机回调函数来处理图像 python 相机_因子分解_13

4.4 增强现实

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

4.4.1 PyGame和PyOpenGL

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.whlhttp://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()

python代码配置basler工业相机回调函数来处理图像 python 相机_相机标定_14

4.4.2 综合集成

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()

在这里插入图片描述

python代码配置basler工业相机回调函数来处理图像 python 相机_因子分解_15


首先,该脚本使用 Pickle 载入照相机标定矩阵,以及照相机矩阵中的旋转和平移部

分。假设这些信息已经按照 4.3 节的描述进行保存。setup() 函数会初始化 PyGame,

将窗口设置为图像的大小,绘制图像区域为两倍的 OpenGL 窗口缓存大小。接下来,

载入背景图像,使其与窗口相符。然后,设定照相机和模拟视图矩阵。最后,在正

确的位置上绘制出茶壶。