在机器视觉领域,摄像头的标定指通过技术手段拿到相机的内参、外参及畸变参数。
相机内参长这样,利用针孔模型,将 3d 物体透视投影到 2d 的相机屏幕上。
畸变参数包括 2 类,径向畸变和切向畸变。
径向畸变最明显的例子就是鱼眼相机的效果。
大家仔细观察上面的图片,它就能很好地介绍径向畸变。越往镜头边缘,线条弯曲的越明显,本来是直线,现在都变成了曲线,消除畸变就是为了把这些曲线尽量还原成本来的样子。
径向畸变可以被纠正,公式如下。
除了径向畸变外,还有一个畸变就是切向畸变。
切向畸变一般来说,是因为相机镜头制造工艺精度不够,透镜和感光器原件没有平行。从而造成了图像的变形。
矫正公式如下:
两个畸变的参数通常用一个向量表示。
但一般只用 4 个参数。
如果用 5 个参数,畸变后的相片就成球状了。
我们的目标就是为了标定出相机内参和外参。
OpenCV 官网上有标定代码示例,但是是基于图片的,并且只有一张图片,我们知道一般要得到一个比较好的标定效果的话,大概需要标定 20 张图片左右。
所以,我想改良一下,我就想到了用相机拍摄视频,然后在视频中完成操作。
标定物我选择了传统的棋盘格,源文件在此。
我用 A4 纸打印了出来,然后粘贴在一张硬纸板上。
接下来就可以编写代码了。
代码的流程其实非常简单。
- 打开摄像头,获取画面,并监听键盘事件。
- 如果检测到空格键,执行棋盘格检测代码。
- 如果检测成功,将棋盘格角点信息绘制在画面上,并将结果保存到列表当中。同时更新棋盘格检测成功次数。
- 如果棋盘格检测成功次数达到指定值,比如 20,又或者是用户按下 Q 键,退出棋盘格的检测。
- 将棋盘格角点信息送入标定函数,获取标定结果并保存。
- 标定结果可以用来去畸变。
def calibrate():
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
Nx_cor = 9
Ny_cor = 6
objp = np.zeros((Nx_cor * Ny_cor, 3), np.float32)
objp[:, :2] = np.mgrid[0:Nx_cor, 0:Ny_cor].T.reshape(-1, 2)
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane.
count = 0 # count 用来标志成功检测到的棋盘格画面数量
while (True):
ret, frame = cap.read()
if cv2.waitKey(1) & 0xFF == ord(' '):
# Our operations on the frame come here
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (Nx_cor, Ny_cor), None) # Find the corners
# If found, add object points, image points
if ret == True:
corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
objpoints.append(objp)
imgpoints.append(corners)
cv2.drawChessboardCorners(frame, (Nx_cor, Ny_cor), corners, ret)
count += 1
if count > 20:
break
# Display the resulting frame
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
global mtx, dist
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
print(mtx, dist)
mean_error = 0
for i in xrange(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
mean_error += error
print "total error: ", mean_error / len(objpoints)
上面是标定的函数。核心是利用了 OpenCV 的几个关键的 API.
# 查找棋盘格角点信息
ret, corners = cv2.findChessboardCorners(gray, (Nx_cor, Ny_cor), None)
# 精细化角点信息
corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
# 绘制查找到的角点
cv2.drawChessboardCorners(frame, (Nx_cor, Ny_cor), corners, ret)
# 标定,mtx 是相机内参,dist 是畸变,rvecs,tvecs 分别是旋转矩阵和平移矩阵代表外参
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
需要注意的是,在这里标定板的的棋盘格是检测内点,所以是横向 9 个,竖向 6 个。
Nx_cor = 9
Ny_cor = 6
标定的时候,还需要角点的物理坐标和图像坐标,这是因为需要通过透视成像的原理,来反向拟合相机的参数,原理比较复杂,这个不做解释,有兴趣的同学可以查看相关书籍和资料。
标定后的结果需要衡量误差,下面是代码。
mean_error = 0
for i in xrange(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
mean_error += error
print "total error: ", mean_error / len(objpoints)
通过 cv2.projectPoints()
方法将角点的物理坐标、标定得到的外参重新投影得到新的角点的图像坐标。
然后将新的图像坐标与之前检测角点时的真实图像坐标对比,以此来衡量标定的精确性。
np.savez('calibrate.npz', mtx=mtx, dist=dist[0:4])
这行代码的用途是为了将标定的结果序列化,保存到本地,以备以后直接使用,畸变参数我只保存了 4 个,原因前面有讲过。
标定得到的相机内参与畸变参数可以用来消除相机原始画面的畸变,代码如下:
def undistortion(img, mtx, dist):
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x, y, w, h = roi
if roi != (0, 0, 0, 0):
dst = dst[y:y + h, x:x + w]
return dst
所以,我们可以编写测试代码,如果本地有标定好的参数,那么就直接加载。如果没有的话,那就标定一次。
拿到内参和畸变参数后,我们可以打开摄像头,然后去畸变,然后你可以直接观察效果。
if __name__ == '__main__':
cap = cv2.VideoCapture(0)
mtx = []
dist = []
try:
npzfile = np.load('calibrate.npz')
mtx = npzfile['mtx']
dist = npzfile['dist']
except IOError:
calibrate()
print('dist', dist[0:4])
while (True):
ret, frame = cap.read()
frame = undistortion(frame, mtx, dist[0:4])
# Display the resulting frame
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
你可以变换姿态,然后按空格键进行图像的抓取。
完整代码如下:
#encoding=utf-8
import numpy as np
import cv2
def undistortion(img, mtx, dist):
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
print('roi ', roi)
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x, y, w, h = roi
if roi != (0, 0, 0, 0):
dst = dst[y:y + h, x:x + w]
return dst
def calibrate():
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
Nx_cor = 9
Ny_cor = 6
objp = np.zeros((Nx_cor * Ny_cor, 3), np.float32)
objp[:, :2] = np.mgrid[0:Nx_cor, 0:Ny_cor].T.reshape(-1, 2)
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane.
count = 0 # count 用来标志成功检测到的棋盘格画面数量
while (True):
ret, frame = cap.read()
if cv2.waitKey(1) & 0xFF == ord(' '):
# Our operations on the frame come here
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (Nx_cor, Ny_cor), None) # Find the corners
# If found, add object points, image points
if ret == True:
corners = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
objpoints.append(objp)
imgpoints.append(corners)
cv2.drawChessboardCorners(frame, (Nx_cor, Ny_cor), corners, ret)
count += 1
if count > 20:
break
# Display the resulting frame
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
global mtx, dist
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
print(mtx, dist)
mean_error = 0
for i in xrange(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
mean_error += error
print "total error: ", mean_error / len(objpoints)
# # When everything done, release the capture
np.savez('calibrate.npz', mtx=mtx, dist=dist[0:4])
def undistortion(img, mtx, dist):
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x, y, w, h = roi
if roi != (0, 0, 0, 0):
dst = dst[y:y + h, x:x + w]
return dst
if __name__ == '__main__':
cap = cv2.VideoCapture(0)
mtx = []
dist = []
try:
npzfile = np.load('calibrate.npz')
mtx = npzfile['mtx']
dist = npzfile['dist']
except IOError:
calibrate()
print('dist', dist[0:4])
while (True):
ret, frame = cap.read()
frame = undistortion(frame, mtx, dist[0:4])
# Display the resulting frame
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()