学习记录。
事实上很早就接触过视觉定位这东西,但是到现在才返回头学习一下相机的标定,真是可耻啊!我把想法和过程记录一下。
相机的成像原理——小孔成像
然而,在实际由于设计工艺问题、相机安装环境或物体摆放位置等影响,会照成成像与实际图像不一样的现象。
由于设计工艺照成的影响是无法改变的事实,所以这将是相机的内参;
由环境或安装方式照成的影响是可以改变的,这就是相机的外参。
在https://blog.csdn.net/aoulun/article/details/78768570中详细介绍了相机成像原理,相机内、外参数是什么。这里为了保证记录的完整型,把成像平面的像素坐标与实际物体的世界坐标公式写下来。
1.红框就是相机外参,R为旋转矩阵,T为平移向量;如果相机镜头和物体平面平行(室内定位中,有一种基于视觉的室内定位,定位方式就是在移动的小车上安装单目相机,在屋顶安装各种可识别的标签,相机的光轴一直与屋顶是垂直的),在这种情况下,旋转矩阵可以看作是单位向量及R=E,而平移向量T=0。
2.蓝框就是相机的内参,相机的内参从出厂后就被固定了。
f:相机的焦距
(u0,v0):像平面的投影中心点
dx,dy:就是单位像素对应实际距离
Zc:可以认为相机镜头到成像物体的垂直距离
对这部分我就不赘述了,在麻呱智能 的文章中介绍的特别详细,我就不班门弄斧了。
创建自定义的棋盘标定图,这个没啥要说的,就是调用了opencv的画矩形框的函数,代码如下:
#生成想要的标定图,大小自定义
import cv2
import sys
#读入一张空白图片,该图片最好和你想要标定的相机分辨率一致
image = cv2.imread('C:\\Users\\wlx\\Documents\\py_study\\camera calibration\\white.jpg')
#设置图片上黑白方格
dpi = 96 #dpi自己电脑上一英寸显示的像素个数
cm_to_inch = 0.3937 #1cm = 0.3937inch
square_length = 1.5 #黑白方格边长1.5cm
x_nums = 10 #x方向画10个方格
y_nums = 8 #y方向画8个方格
square_pixel = int(square_length * cm_to_inch * dpi) #方格边长的像素
#为了把方格图像放在纸张的中间,设定起始坐标
x0 = 40
y0 = 16
#画方格
def DrawSquare():
flag = -1 #颜色转变标志
#一行一行的画
for i in range(y_nums):
#每画一行先换一次颜色
flag = 0 - flag
for j in range(x_nums):
if flag > 0:
color = [0,0,0] #黑色方格
else:
color = [255,255,255]
#调用opencv中的画方框函数
cv2.rectangle(image,(x0 + j*square_pixel,y0 + i*square_pixel),
(x0 + j*square_pixel+square_pixel,y0 + i*square_pixel+square_pixel),color,-1)
flag = 0 - flag
#保存图像
cv2.imwrite('chess_map.jpg',image)
#判断本程序是独立运行还是被调用
if __name__ == '__main__':
DrawSquare()
上面的代码可以生成自己想要的棋盘标定图,修改x_nums和y_nums参数的值,就能获得任意数量的黑白格子。实现原理就是在图像的某一点开始,先画一个黑色方格(或者白色),画完后将起点坐标和终点坐标都向右移动方格边长的距离,然后改变颜色再画一个方格,依次类推,画完一行后,就转战到第二行,直到全部完成。
cv2.rectangle(img,pt1,pt2,color,thickness=None,line_type=None,shift=None)
img:图像,要画图的图像
pt1:方格的起点坐标(x0,y0)
pt2:方格的终点坐标(x1,y1)
color:方框的颜色
thickness:方框线的宽度(像素),当值为负数时,填充方框
line_type:方框线的样式
我采用离线标定,先把不同角度的标定图采集保存下来,然后再开始标定。下面是采集的代码
import cv2
import os
#标定图像保存路径
photo_path = "C:\\Users\\wlx\\Documents\\py_study\\camera calibration\\image"
#创建路径
def CreateFolder(path):
#去除首位空格
del_path_space = path.strip()
#去除尾部'\'
del_path_tail = del_path_space.rstrip('\\')
#判读输入路径是否已存在
isexists = os.path.exists(del_path_tail)
if not isexists:
os.makedirs(del_path_tail)
return True
else:
return False
#获取不同角度的标定图像
def gain_photo(photo_path):
# 检查输入路径是否存在——不存在就创建
CreateFolder(photo_path)
#开启摄像头
video = cv2.VideoCapture(0)
#显示窗口名称
photo_window = 'calibration'
#保存的标定图像名称以数量命名
photo_num = 0
while video.isOpened():
ok,frame = video.read() #读一帧的图像
if not ok:
break
else:
cv2.imshow(photo_window,frame)
key = cv2.waitKey(10)
#按键盘‘A’保存图像
if key & 0xFF == ord('a'):
photo_num += 1
photo_name = photo_path + '\\' + str(photo_num) + '.jpg'
cv2.imwrite(photo_name,frame)
print('create photo is :',photo_name)
#按键盘‘Q’中断采集
if key & 0xFF == ord('q'):
break
if __name__ == '__main__':
gain_photo(photo_path)
video = cv2.VideoCapture(0)中的0代表的是我的USB相机在我电脑上的驱动位置
if __name__ == '__main__':就是判断这些代码是不是要当作单独的代码执行,如果是就执行if中的内容。
我先上代码吧
import cv2
import sys
import numpy as np
import glob
#标定图像保存路径
photo_path = "C:\\Users\\wlx\\Documents\\py_study\\camera calibration\\image"
#标定图像
def calibration_photo(photo_path):
#设置要标定的角点个数
x_nums = 9 #x方向上的角点个数
y_nums = 7
# 设置(生成)标定图在世界坐标中的坐标
world_point = np.zeros((x_nums * y_nums,3),np.float32) #生成x_nums*y_nums个坐标,每个坐标包含x,y,z三个元素
world_point[:,:2] = np.mgrid[:x_nums,:y_nums].T.reshape(-1, 2) #mgrid[]生成包含两个二维矩阵的矩阵,每个矩阵都有x_nums列,y_nums行
#.T矩阵的转置
#reshape()重新规划矩阵,但不改变矩阵元素
#保存角点坐标
world_position = []
image_position = []
#设置角点查找限制
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,30,0.001)
#获取所有标定图
images = glob.glob(photo_path+'\\*.jpg')
#print(images)
for image_path in images:
image = cv2.imread(image_path)
gray = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY)
#查找角点
ok,corners = cv2.findChessboardCorners(gray,(x_nums,y_nums),None)
if ok:
#把每一幅图像的世界坐标放到world_position中
world_position.append(world_point)
#获取更精确的角点位置
exact_corners = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
#把获取的角点坐标放到image_position中
image_position.append(exact_corners)
#可视化角点
# image = cv2.drawChessboardCorners(image,(x_nums,y_nums),exact_corners,ok)
# cv2.imshow('image_corner',image)
# cv2.waitKey(5000)
#计算内参数
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(world_position, image_position, gray.shape[::-1], None,None)
#将内参保存起来
np.savez('C:\\Users\\wlx\\Documents\\py_study\\camera calibration\\data\\intrinsic_parameters',mtx=mtx,dist=dist)
print(mtx, dist)
#计算偏差
mean_error = 0
for i in range(len(world_position)):
image_position2, _ = cv2.projectPoints(world_position[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(image_position[i], image_position2, cv2.NORM_L2) / len(image_position2)
mean_error += error
print("total error: ", mean_error / len(image_position))
if __name__ == '__main__':
calibration_photo(photo_path)
使用opencv标定这些图像,步骤大致就是:
1、设置想标定角点的个数
2、创建对应角点个数的世界坐标
3、将采集到的标定图读入缓存
4、灰度处理
5、使用findChessboardCorners(img,patternSize,corners,flags=None)函数,查找图像中的内点
6、使用cornerSubPix(image,corners,winSize,zeroZone,criteria)函数,精确查找图像上的角点
image:输入图像,,必须是8位的灰度或者彩色图像
corners:输入角点的初始化坐标,也存储精确的输出角点坐标
winSize:搜索窗口的一半尺度,如winSize=(5,5),则使用(2x5+1, 2x5+1)=(11,11)的搜索窗
zeroZone:死区的一半尺寸,死区为不对搜索区做求和运算的区域,当值为(-1,-1)时,表示没有死区
criteria:搜索角点停止的标志
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,30,0.001)就是一个标志,TERM_CRITERIA_EPS 代表误差也就是精度,TERM_CRITERIA_MAX_ITER代表迭代次数,它两之和就是指两个因素同时作用,这里当迭代次数超过30或误差大于0.001都会停止运算。
7、将图像的世界坐标保存到数组world_position中,将找到的角点坐标保存到数组image_position中
8、使用cv2.calibrateCamera(world_position, image_position, gray.shape[::-1], None,None)计算内外参数
9、计算一下准确度,也就是通过你算出来的内参数,逆运算出角点坐标,然后将这个坐标和识别出来的角点坐标进行误差运算,得到偏差值。
最后,可以将得到的内参数保存到一个文件中,以后在用相机采集图像时,就可以用内参数去矫正图像了,这里我没做图像的矫正,过几天做了再写上来。
另外,用来标定的图像不要太少,我做了实验随着标定图像的增加,最后计算出的偏差会减小;而且拍摄标定图像时,角度要合理,相机固定位置不要发生变化。
下面是我实验,就是为了验证代码是否能运行,所以没有打印标定图纸,而是直接用摄像头拍摄电脑显示器上的图像,摄像头也没固定牢靠,所以结果不是很好。拍摄了20张图像
相机内参:
相机的基础矩阵
M1=[[889.02484885 0. 305.16086021]
[ 0. 874.05128244 296.97147721]
[ 0. 0. 1. ]]
相机的畸变矩阵
D1=[[-1.19429590e-01 1.62553701e+00 -1.23085637e-02 -9.56110534e-03
-9.61452725e+00]]
偏差值:
error: 0.022833192243376606