目录
为什么进行相机标定
相机标定原理推导
坐标系的转换
世界坐标系->相机坐标系
相机坐标系->图像坐标系
图像坐标系->像素坐标系
为什么使用齐次坐标系
图像畸变和畸变矫正
径向畸变
切向畸变
相机内外参数的应用
相机标定具体实现
步骤:
1. 准备标定图片
2. 对每张标定图片,提取角点信息
3. 对每张标定图片,进一步提取亚像素角点信息
4. 在标定图上绘制找到的内角点
5. 相机标定
6. 利用标定结果对测试图进行矫正
完整代码(python+opencv):
参考
首先谈谈相机为什么需要标定?任何理论物理模型都是在特定假设上对真实事物的近似,然而在实际应用中存在误差。实际应用中,普通相机成像误差的主要来源有两部分,第一是sensor制造产生的误差,比如sensor成像单元不是正方形,sensor歪斜;第二是镜头制造和安装产生的误差,镜头一般存在非线性的径向畸变;镜头与相机sensor安装不平行,还会产生切向畸变。
为确定空间物体表面某点的三维几何位置与其在图像中对应点之间的相互关系,必须建立相机成像的几何模型,这些几何模型参数就是相机参数。在大多数条件下这些参数必须通过实验与计算才能得到,这个求解参数的过程就称之为相机标定(或摄像机标定),标定结果的精度及算法的稳定性直接影响相机工作产生结果的准确性。
相机标定的方法有自标定(找图像中特征点)、标定板标定(特征点易求,稳定性好),一般采用标定板标定。相机标定按照相机是否静止,可分为静态相机标定(标定板动,相机静止),动态相机标定(标定板静止,相机运动)。
坐标系转换就是为了将空间的三维世界坐标系转换至图像处理的二维像素坐标系。坐标系有:世界坐标系,相机坐标系,图像坐标系,像素坐标系。
世界坐标系(world coordinate)(xw,yw,zw):是实际物体再三维世界中的坐标系,可以表示任何物体,是由于相机而引入的。单位m。
相机坐标系(camera coordinate)(xc,yc,zc):是一个三维直角坐标系,以摄像机光心为原点,z轴与光轴重合也就是z轴指向相机的前方(也就是与成像平面垂直),x轴与y轴的正方向与物体坐标系平行。
图像坐标系(也叫平面坐标系)(image coordinate)(x,y):是像平面上的二维直角坐标系。坐标原点为摄像机光轴与图像物理坐标系的交点位置。它的x轴与相机坐标系的xc轴平行,它的y轴与相机坐标系的yc轴平行,单位是mm。
像素坐标系:(pixel coordinate)(u,v),以像素为单位,坐标原点在左上角。像素坐标系到平面坐标系涉及到的是单位的转换和平移。传感器以mm单位到像素中间有转换。举个例子,CCD传感上上面的8mm x 6mm,转换到像素大小是640x480。dx,dy表示像素坐标系中每个像素的物理大小。像素坐标系的x轴与图像坐标系的u轴平行,像素坐标系的y轴与图像坐标系的v轴平行。
相机坐标系是以相机位置作为原点,世界坐标系是以物体的中心作为原点,世界坐标系到相机坐标系的转换包括三个轴的平移,用表示;因为相机不一定和物体是平齐的姿态,所以坐标转换还包括以轴为轴的旋转,用来表示。
红色的点为世界坐标系中的点,黑色为世界坐标系,蓝色为相机坐标系;绕轴旋转后,通过相似三角形定理:
写成矩阵的形式:
同理,绕轴旋转后:
绕轴旋转后:
所以,世界坐标系和相机坐标系的转换可以这样表达:
简化为:
相机坐标系到图像坐标系可以使用小孔成像的原理进行推导,涉及的是相机本身的参数。
为了便于计算,我们将像平面对称反转过来,如下图所示:
点是相机坐标系下的点,映射到图像坐标系上为,通过相似三角形定理,可以得出:
,为焦距;
进一步地:
进一步地:
图像坐标系到像素坐标系是在轴上的偏移,但是图像坐标系的单位为mm,像素坐标系的单位为像素pt,所以需要进行单位的转换。
假设每个像素代表的物理尺寸为,则:
从而可以得到像素坐标系和相机坐标系的关系式:
从而可以得到像素坐标系和世界坐标系的关系式:
注:不同的矩阵符号是为了便于区分。
大多数情况下是相机坐标系到像素坐标系的转换:
为轴的尺度因子,为轴的尺度因子;
为相机内参;为相机外参;
所以,像素坐标系->世界坐标系可以表示为:
外参就是相机相对于世界坐标系的旋转和平移变换关系。内参是相机固有的属性,即焦距,像元尺寸。公式中有一个,它表示物体离光学中心的距离。因此,在标定的时候,如果物体在距离相机的不同位置,那么我们就必须在不同的位置对相机做标定。当物体离相机远的时候,在相机上成像小,一个像素代表的实际尺寸就大,当物体离相机近的时候,那么成像大,一个像素代表的实际物体尺寸就小。因此,不同的位置都需要去标定。
齐次坐标表示是计算机图形学的重要手段之一,它既能够用来明确区分向量和点,同时也更易用于进行仿射几何变换”—— F.S. Hill, JR。
齐次坐标易于进行仿射变换。
首先:齐次坐标就是用N+1维来代表N维坐标;
将三维坐标视为一个列向量,那么矩阵*列向量得到的新向量的每一个分量,都是旧的列向量的线性函数,因而三维笛卡尔坐标与矩阵的乘法只能实现三维坐标的缩放和旋转,而无法实现坐标平移。如下所示:
将三维的笛卡尔坐标添加一个额外坐标,就可以实现坐标平移了,而且保持了三维向量与矩阵乘法具有的缩放和旋转操作。这个就称为齐次坐标。而这种变换也称为仿射变换(affine transformation),不属于线性变换。仿射变换是:“线性变换”+“平移”。
齐次坐标能够用来明确区分向量和点。
(9条消息) 深入探索透视投影变换_popy007的博客-CSDN博客_投影变换
畸变(distortion)是对直线投影(rectilinear projection)的一种偏移。直线投影是场景内的一条直线投影到图片上也保持为一条直线。畸变就是一条直线投影到图片上不能保持为一条直线了,这是一种光学畸变(optical aberration。
图像畸变一般分为两种:径向畸变和切向畸变。径向畸变发生在相机坐标系转图像坐标系的过程中;切向畸变是发生在相机制作过程,原因是感光元平面跟透镜不平行。
畸变还有其他类型的畸变,但是没有径向畸变、切向畸变显著。
径向畸变(枕形、桶形):光线在远离透镜中心的地方比靠近中心的地方更加弯曲。当光线越靠近中心的位置,畸变越小,沿着半径方向远离中心的时候,畸变越大。桶形畸变虽然不影响成像清晰度,但却影响成像的位置精度,这会给图像分析和图像测量带来误差。
径向畸变都是随着与中心之间的距离增加而增加,因此可以用一个多项式函数来描述畸变前后的坐标变化:
注: 是径向畸变系数;是畸变后点的坐标,是理想的点的坐标,。
对于畸变较小的图像中心区域,畸变纠正主要是 起作用;对于畸变较大的边缘区域,主要是起作用。根据所用镜头,可以适当使用合适的校正系数。
切向畸变:透镜不完全平行于图像平面,即sensor装配时与镜头间的角度不准。
注:是切向畸变系数;
综上,我们一共需要5个畸变参数(k1、k2、k3、p1、p2 )来描述镜头畸变。
针孔相机模型中,只要确定相机参数和畸变参数就可以唯一的确定针孔相机模型, 这个过程就称为「相机标定」。
一旦相机结构固定,包括镜头结构固定,对焦距离固定,我们就可以用这些参数去近似这个相机。相机参数标定结果的精度会直接影响相机工作中产生结果的准确性。因此做好相机标定是后续工作的重要前提。
对单目视觉而言,求得内参和畸变参数后,就可以对拍摄的图像做变换和矫正。矫正完拍摄的图像之后,对图像做其他处理。
对于双目视觉而言,需要用到世界坐标系。对单目视觉做完内参和畸变参数的矫正之后,就可以用这些变换后的图像,同时结合世界坐标系实现定位或者其他用途了。
使用python+opencv进行相机标定。
1. 准备标定图片(棋盘图)
2. 对每张标定图片,提取角点信息;
3. 对每张标定图片,进一步提取亚像素角点信息;
4. 在标定图上绘制找到的内角点
5. 相机标定(得到相机内外参)
6. 利用标定结果对棋盘图进行矫正
标定图片需要使用标定板在不同位置、不同角度、不同姿态下拍摄,最少需要3张,以10~20张为宜。标定板需要是黑白相间的矩形构成的棋盘图。
棋盘图PDF文件链接:https://pan.baidu.com/s/1V1nJYyKDd0ZmIM0BtfyTSg
提取码:alth
我实验的时候使用的是别人的图片,我的相机拍的图片畸变不明显。在此非常感谢这位博主,数据集在这里:
链接:https://pan.baidu.com/s/1K1SsxV8H_F5BG0avBdbOiQ
提取码:v6en
使用FindChessboardCorners()函数提取角点信息。
使用cornerSubPix函数在角点检测中精确化角点位置。
drawChessboardCorners函数用于绘制被成功标定的角点。
获取到棋盘标定图的内角点图像坐标之后,使用calibrateCamera()函数进行标定,计算相机内参和外参系数。
使用undistort()函数对测试的图片进行标定。结果如下图:
import argparse
from argparse import RawDescriptionHelpFormatter
from email import parser
import numpy as np
import cv2
def findCorners(img,srcPath,imgId,col,row):
srcGray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,corners = cv2.findChessboardCorners(srcGray,(col,row),None)
if ret:
criteria = (cv2.TermCriteria_EPS + cv2.TermCriteria_COUNT,10,0.001)
cornersSubPix = cv2.cornerSubPix(srcGray,corners,(5,5),(-1,-1),criteria)
savePath = srcPath + "\\" + str(imgId) + "_corner.jpg"
cv2.drawChessboardCorners(img,(row,col),cornersSubPix,ret)
cv2.imwrite(savePath,img)
print("保存找到角点的图像,地址:",savePath)
return (ret,cornersSubPix)
def camCalibration(imgDir,srcPath,imgNums,row,col):
w,h = 0,0
allCornersList = []
patterns = []
# 相机标定
x,y = np.meshgrid(range(row),range(col))
prod = row * col
patternPoints = np.hstack((x.reshape(prod,1),y.reshape(prod,1),
np.zeros((prod,1)))).astype(np.float32)
for i in range(1,imgNums+1):
imgPath = imgDir + "\\" + str(i) + ".jpg"
print("imgPath:",imgPath)
# 读取图像
img = cv2.imread(imgPath)
(h,w) = img.shape[:2]
print("(h:w):",(h,w))
# 提取角点
ret,cornres = findCorners(img,srcPath,i,row,col)
if ret:
allCornersList.append(cornres)
patterns.append(patternPoints)
rms,camMatrix,distcoeffs,rvecs,thecs = cv2.calibrateCamera(patterns,allCornersList,(w,h),None,None)
print("rms:",rms)
print("distcoeffs:",distcoeffs)
print("camMatrix:",camMatrix)
print("rvecs:",rvecs)
print("thecs:",thecs)
return (camMatrix,distcoeffs)
def camCorrect(testDir,camMatrix2,distCoffs2):
for i in range(1,8):
corrImgPath = testDir + "\\" + str(i) + ".jpg"
img = cv2.imread(corrImgPath)
(w1,h1) = img.shape[:2]
print("(w1,h1):",(w1,h1))
# 对参数处理,去除不必要的边缘
newCamMatrix,roi = cv2.getOptimalNewCameraMatrix(camMatrix2,distCoffs2,(w1,h1),1,(w1,h1))
# 矫正图像
dst = cv2.undistort(img,camMatrix2,distCoffs2,None,newCamMatrix)
# 保存校正后的图像
# x,y,w,h = roi
# dst = dst[y:y+h,x:x+w]
corPath = testDir + "\\" + str(i) + "_crct.jpg"
cv2.imwrite(corPath,dst)
print("保存校正后的图像,地址:",corPath)
if __name__ == "__main__":
parser = argparse.ArgumentParser(formatter_class=RawDescriptionHelpFormatter)
parser.add_argument("--imgDir",help="标定图片路径",type=str,metavar='',default="E:\\09tmp\\src")
parser.add_argument("--srcPath",help="图片保存途径",type=str,metavar='',default="E:\\09tmp\\res")
parser.add_argument("--testDir",help="待测试图片路径",type=str,metavar='',default="E:\\09tmp\\src")
parser.add_argument("--row",help="每一行有多少角点,边缘处不算",type=int,metavar='',default="7")
parser.add_argument("--col",help="每一列有多少角点,边缘处不算",type=int,metavar='',default="4")
parser.add_argument("--imgNum",help="多少幅图像",type=int,metavar='',default="7")
args=parser.parse_args()
# 相机标定
camMatrix,distCoffs = camCalibration(args.imgDir,args.srcPath,args.imgNum,args.row,args.col)
# 矫正图片
camCorrect(args.testDir,camMatrix,distCoffs)
(37 封私信 / 81 条消息) 世界坐标系 - 搜索结果 - 知乎 (zhihu.com)
为什么要相机标定?你想知道的都在这!_坐标系 (sohu.com)
相机参数标定(camera calibration)及标定结果如何使用_Aoulun的博客-CSDN博客_相机标定后如何使用
(37 封私信 / 81 条消息) 如何通俗地讲解「仿射变换」这个概念? - 知乎 (zhihu.com)
(12条消息) 第三更,单目相机标定实践(完整过程)_Aoulun的博客-CSDN博客
图像处理——相机标定(Camera calibration)_fengye2two的博客-CSDN博客_图像标定