本文将梳理一种单目摄像头标定和矫正的方法,在梳理的过程中,首先使用网上离线的图片数据跑通流程,然后接入自己的camera,手动采集标定图像,实时矫正相机的畸变,然后输出矫正后的图像。全文基于Opencv使用C++实现,文末附带相应的python代码。
1. 基本概念
1.1 什么是畸变
下面两张示意图可以让大家直观的感受摄像头的畸变效果,简单的讲摄像头的畸变会让人们看到的图像出现“拉伸”或“扭曲”的直观感受,出现“横不平,竖不直”的现象。虽然畸变现象改变了图像原有的面貌,但在日常生活中也有很多应用之处,比如转弯路口的凸透镜,汽车的左右后视镜等,利用畸变效果扩大视野。但自动驾驶中,往往需要利用图像进行测量或者识别,为了保证精度,在这种场景下,就需要尽量还原图像,也就是所说的“畸变矫正”。畸变效果示意图一
畸变效果示意图二
1.2 为什么会产生畸变
在进行畸变矫正之前,我们需要简单的理解产生畸变的原因。通常畸变可以分为两种,一种是径向畸变,一种是切向畸变,如下面两张图所示。
径向畸变有两种形态,即桶形畸变和枕形畸变,从效果上看一个突出,一个内凹。产生径像畸变的原因是光学镜头在生产制造的过程中,很难保证厚度的均匀,离透镜中心越远的地方光线弯曲越大,从而产生径向畸变。径向畸变(图片来源https://blog.csdn.net/waeceo/article/details/50580808)
切向畸变如下图所示,从效果上看,一个平直的物体在照片中看上去会有“倾斜”,“大小不一”的现象。出现切向畸变的原因是由于镜头与图像传感器不完全平行造成的(可理解为投影仪与影布不平行)。切向畸变(图片来源https://blog.csdn.net/waeceo/article/details/50580808)
1.3 什么是摄像头参数
1)相机矩阵:包括焦距(fx,fy),光学中心(Cx,Cy),完全取决于相机本身,是相机的固有属性,只需要计算一次,可用矩阵表示如下:[fx, 0, Cx; 0, fy, cy; 0,0,1];
2) 畸变系数:畸变数学模型的5个参数 D = (k1,k2, P1, P2, k3);
3)相机内参:相机矩阵和畸变系数统称为相机内参,在不考虑畸变的时候,相机矩阵也会被称为相机内参;
4) 相机外参:通过旋转和平移变换将3D的坐标转换为相机2维的坐标,其中的旋转矩阵和平移矩阵就被称为相机的外参;描述的是将世界坐标系转换成相机坐标系的过程。
1.4 摄像头标定的流程
相机的标定过程实际上就是在4个坐标系转化的过程中求出相机的内参和外参的过程。这4个坐标系分别是:世界坐标系(描述物体真实位置),相机坐标系(摄像头镜头中心),图像坐标系(图像传感器成像中心,图片中心,影布中心,单位mm),像素坐标系(图像左上角为原点,描述像素的位置,单位是多少行,多少列)。
(1)世界坐标系
相机坐标系:求解摄像头外参(旋转和平移矩阵);
(2)相机坐标系
图像坐标系:求解相机内参(摄像头矩阵和畸变系数);
(3) 图像坐标系
像素坐标系:求解像素转化矩阵(可简单理解为原点从图片中心到左上角,单位厘米变行列)
2. 摄像头标定与矫正实践
2.1 离线图片实现摄像头标定和矫正
1)Cmakelist 配置Opencv
//要求cmake最低版本
cmake_minimum_required(VERSION 3.1)
// 工程名
project(camera_calibration)
set(CMAKE_CXX_STANDARD 11)
add_executable(camera_calibration main.cpp)
// 配置opencv
find_package(OpenCV REQUIRED)
target_link_libraries(camera_calibration ${OpenCV_LIBS}))
2)导入棋盘格图片;
在标定过程中,需要使用棋盘格,拍摄棋盘格在多个角度的图片,这里省去了拍摄的过程,直接使用网上下载的棋盘格图片。
#include
#include
#include
#include "opencv2/imgproc.hpp"
#include "opencv2/calib3d.hpp"
#include
#include
#include
#include
using namespace std;
using namespace cv;
#define PATH "/camera_calibration/image_my/"
#define NUM 30
int main() {
// 定义用来保存导入的图片
Mat image_in;
// 定义用来保存文件路径的容器
vector filelist;
// 定义用来保存旋转和平移矩阵的容器
vector rvecs, tvecs;
// 定义相机矩阵,畸变矩阵
Mat cameraMatrix;
Mat distCoeffs;
int flags = 0;
// 定义保存图像二维角点的容器
vector corners;
// 定义保存图像三维角点的容器
vector > corners2;
// 定义保存图像二维和三维角点的容器
vector worldPoints;
vector > worldPoints2;
//***************读取一个文件夹中的所有图片(所有标定图片)**********************
for(int i=1; i
stringstream str;
str << PATH << setw(2) << setfill('0') << i << ".jpg";
// 保存所有图片的路径,放入容器filelist中
filelist.push_back(str.str());
image_in = imread(str.str());
}棋盘格标定示意图一
棋盘格标定示意图二
3)找角点
标定前需要找到棋盘格中黑白框结合的角点,opencv提供了findChessboardCorners函数来完成这个工作。这个函数的输入参数为:输入图片,图片的内角点数,输出角点,求解方式;
//***************************找角点×××××××××××××××××××××××××××××××× for(int i=0;i
//cout <
// 找图片的角点,参数分别为: // 输入图片,图片内角点数(不算棋盘格最外层的角点),输出角点,求解方式 bool found = findChessboardCorners(image_in, Size(8,6),corners,CALIB_CB_ADAPTIVE_THRESH|CALIB_CB_NORMALIZE_IMAGE);
// 将找到的角点放入容器中; corners2.push_back(corners);
//画出角点 drawChessboardCorners(image_in,Size(9,6),corners, found);
//显示图像 imshow("test",image_in);
// 图像刷新等待时间,单位ms waitKey(100);
// 世界坐标系的二维vector 放入三维vector worldPoints2.push_back(worldPoints);
}角点检测示意图一
角点检测示意图二
4)生成世界坐标系下三维空间点
畸变矫正的本质是通过寻找棋盘格上角点,在图像中和真实世界中的对应关系,来计算相机参数。因此我们需要生成真实世界中的棋盘格坐标点。由于矫正的过程与标定过程的比例尺一样,实际是等比例缩放,因此这些点可以不与真实的尺寸对应,只要成比例就行。
//***********************生成一组object_points*************************
for(int j = 0;j<6;j++){
for(int k = 0; k<8;k++){
worldPoints.push_back(Point3f(j*1.0 ,k*1.0 ,0.0f));
}
}
5)标定
采用calibrateCamera函数能够计算出相应的相机参数,实现相机的标定,这个函数的输入参数依次为:世界坐标系内角点, 图像的角点, 图像的尺寸,相机矩阵,畸变矩阵,旋转矩阵,平移矩阵,求解方式。 其中需要注意的是,世界坐标系内的角点和图像的角点 二者的维度一定要对应,要么全是二维Vector,要么全是三维Vector 即Vector> 或vector
calibrateCamera(worldPoints2,corners2,image_in.size(),cameraMatrix,distCoeffs,
rvecs,tvecs,CV_CALIB_FIX_PRINCIPAL_POINT);
查看对应的相机参数:
//*************************************查看参数*****************************************
cout << " Camera intrinsic: " << cameraMatrix.rows << "x" << cameraMatrix.cols << endl;
cout << cameraMatrix.at(0,0) << " " << cameraMatrix.at(0,1) << " " << cameraMatrix.at(0,2) << endl;
cout << cameraMatrix.at(1,0) << " " << cameraMatrix.at(1,1) << " " << cameraMatrix.at(1,2) << endl;
cout << cameraMatrix.at(2,0) << " " << cameraMatrix.at(2,1) << " " << cameraMatrix.at(2,2) << endl;
cout << distCoeffs.rows << "x" <
cout << distCoeffs << endl;
for(int i = 0;i < distCoeffs.cols;i++)
{
cout << distCoeffs.at(0,i) << " " ;
}
cout <
相机参数
6)矫正
opencv提供了多种畸变矫正的函数,这里使用最基本的undistort, 输入参数分别为:输入图像,矫正后图像,相机矩阵,畸变矩阵。
//*********************畸变矫正**************************
// 导入要矫正的图片
Mat test_image2 = imread("/camera_calibration/test_image.jpg");
Mat show_image;
undistort(test_image2, show_image, cameraMatrix, distCoeffs);矫正前的图片
矫正后的图片
2.2 在线Camera数据采集
打印一张棋盘格,固定在木板上,使用摄像头从各个角度拍摄棋盘格图像。本文使用一个外接Usb罗技摄像头,以下是采集摄像头图像时所使用的程序,按下S键保存当帧的图像。这里需要注意的是waitKey()这个函数,其作用对象是显示的图像窗口,不能对控制台起作用,也就是说在使用waitKey这个函数时,必须在前面显示图片,然后才起作用。
#include
#include
#include
#include
using namespace std;
using namespace cv;
int main() {
VideoCapture capture(1);
Mat img,img_flip;
char filename[200];
int i =0;
int key = 0;
while (capture.isOpened()) {
capture >> img;
flip(img, img_flip, 0);
// imshow("test0",img);
imshow("test1",img_flip);
//char key_board = waitKey(10);
key = waitKey(30);
cout <
if (key == 's') {
sprintf(filename, "%s%d%s", "../image/", i++, ".jpg");
imwrite(filename, img_flip);
}
}
return 0;
}
2.3 在线摄像头畸变矫正
// 当摄像头打开时,实时矫正图片,并实时输出;
while(capture.isOpened()){
capture >> frame;
flip(frame,fz,-1);//1代表水平方向旋转180度
undistort(fz,show, cameraMatrix,distCoeffs);
imshow("raw",fz);
waitKey(30);
imshow("corrected2",show);
waitKey(30);
3. 其他
3.1 python 代码
import numpy as np
import cv2
import glob
import matplotlib.pyplot as plt
%matplotlib
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*8,3), np.float32)
objp[:,:2] = np.mgrid[0:8, 0:6].T.reshape(-1,2)
# Arrays to store object points and image points from all the images.
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane.
# Make a list of calibration images
images = glob.glob('calibration_wide/GO*.jpg')
# Step through the list and search for chessboard corners
for idx, fname in enumerate(images):
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Find the chessboard corners
ret, corners = cv2.findChessboardCorners(gray, (8,6), None)
# If found, add object points, image points
if ret == True:
objpoints.append(objp)
imgpoints.append(corners)
# Draw and display the corners
cv2.drawChessboardCorners(img, (8,6), corners, ret)
#write_name = 'corners_found'+str(idx)+'.jpg'
#cv2.imwrite(write_name, img)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
import pickle
%matplotlib inline
# Test undistortion on an image
img = cv2.imread('calibration_wide/test_image.jpg')
img_size = (img.shape[1], img.shape[0])
# Do camera calibration given object points and image points
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img_size,None,None)
dst = cv2.undistort(img, mtx, dist, None, mtx)
cv2.imwrite('calibration_wide/test_undist.jpg',dst)
# Save the camera calibration result for later use (we won't worry about rvecs / tvecs)
dist_pickle = {}
dist_pickle["mtx"] = mtx
dist_pickle["dist"] = dist
pickle.dump( dist_pickle, open( "calibration_wide/wide_dist_pickle.p", "wb" ) )
#dst = cv2.cvtColor(dst, cv2.COLOR_BGR2RGB)
# Visualize undistortion
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,10))
ax1.imshow(img)
ax1.set_title('Original Image', fontsize=30)
ax2.imshow(dst)
ax2.set_title('Undistorted Image', fontsize=30)
3.2 参考资料
3)OpenCv camera calibration with C++ (Unsupported format or combination of formats error)?stackoverflow.com