本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。
前言
在阅读本篇文章之前,建议首先要了解知道什么是相机的针孔模型,我在之前的文章小孔相机参数学习笔记中有详细的解释,或者说不是建议,而是必须要知道,不然也不知道这篇文章在干什么。本片文章主要来讲张正友视觉标定法,在讲张正友之前,我们先说一下什么是相机标定。
所谓相机标定,就是通过实验等方法将相机模型中的未知数给解算出来。例如小孔模型我们一般需要节算出内参矩阵 fx,fy,cx,cy 以及畸变参数。
为什么要标定出这些参数呢?一个是因为每个镜头的畸变程度各不相同,通过相机标定可以校正这种镜头畸变矫正畸变,生成矫正后的图像;另一个是根据获得的图像重构三维场景,在之前文章我们有写过PNP算法,就是通过相机的内参以及畸变解算出目标在相机下的位置,以及双目3D相机也需要相机模型参数进行计算等。
有很多标定方法,比如传统相机标定法、主动视觉相机标定方法、相机自标定法等,但是现在用的最多的就是相机的自标定法,而相机的自标定法的基础就是张正友标定法的解算流程。下面我们一起来看一下张正友标定法。
张正友标定法简介
首先一个问题,张正友是谁?
张正友博士。世界著名的计算机视觉和多媒体技术的专家,ACM Fellow,IEEE Fellow。刚刚从微软研究院视觉技术组离职,加入腾讯AI Lab担任负责人。他在立体视觉、三维重建、运动分析、图像配准、摄像机标定等方面都有开创性的贡献。
那么张正友标定法又是什么?
“张正友标定法”是张正友博士在1999年发表在国际顶级会议ICCV上的论文《Flexible Camera Calibration By Viewing a Plane From Unknown Orientations》中,提出的一种利用平面棋盘格进行相机标定的实用方法。该方法既克服了摄影标定法需要的高精度三维标定物的缺点,又解决了之前自标定法鲁棒性差的难题。
标定过程仅需使用一个打印出来的棋盘格,并从不同方向拍摄几组图片即可,任何人都可以自己制作标定图案,不仅实用灵活方便,而且精度很高,鲁棒性好。因此很快被全世界广泛采用,极大的促进了三维计算机视觉从实验室走向真实世界的进程。
世界正需要这样的发明,张正友标定法也是张正友博士的成名之作。
下面我们看一下,张正友标定法的具体流程,主要是数学计算较多,要细心慢慢看。
张正友标定法计算流程
首先我们看一下张正友标定法使用OpenCV的计算流程:
- 准备标定图片,原理上三张就够,一般在多个角度采集20张左右。
- 提取标定板的关键点,并计算出标定板上关键点的实际相对位置,一般将标定板当做XY平面,Z为0,标定板第一个点为坐标原点。
- 相机标定,通过张正友标定法计算出内参外参以及畸变。
- 对标定结果进行评价,一般通过重投影的误差进行评价。
- 查看标定效果,利用标定结果对棋盘图进行矫正
从上边的过程可以看出,我们其实只有第三步是真正的解算过程,我们现在来看一下大致的方法。
首先用于标定的棋盘格是三维场景中的一个平面Π,其在成像平面的像是另一个平面𝜋,知道了两个平面的对应点的坐标,就可以求解得到两个平面的单应矩阵𝐻。其中,标定的棋盘格是特制的,其角点的坐标是已知的;图像中的角点,可以通过角点提取算法得到,这样就可以得到棋盘平面Π和图像平面𝜋的单应矩阵𝐻,即: 其中𝑝是像点坐标,𝑃是标定的棋盘坐标,K是相机内参,为了不失一般性,可以在相机的内参矩阵上添加一个扭曲参数𝛾。即: 这样就可以得到下面的等式:是不是通过对应的点对解得𝐻后,则可以通过上面的等式得到相机的内参数𝐾,以及外参旋转矩阵𝑅和平移向量𝑡。
至于怎么解出来,我们接着看。
设棋盘格所在的平面为世界坐标系中𝑍=0的平面,这样棋盘格的任一角点𝑃的世界坐标为(𝑋,𝑌,0),根据小孔相机模型: 再根据单应性原则: 根据上面两式则有: 将旋转矩阵𝑅的各个列向量和平移向量𝑡使用𝐻的列向量表示: 又因为,𝑅是旋转矩阵,则其是正交矩阵,也就是其任意两个列向量的内积为0,列向量的模为1,则有: 即:那么对于一幅棋盘标定版的图像(一个单应矩阵)可以获得两个对内参数的约束等式。
我们令: 矩阵𝐵是一个对称矩阵,其未知量只有6个,将6个未知量写为向量的形式: 则有: 其中: 则约束等式有: 写成矩阵的形式有: 假如有𝑛幅图像,则:其中,𝑉是一个2𝑛×6的矩阵,𝑏是一个6维向量,所以
- 当𝑛≥3,可以得到𝑏的唯一解;
- 当𝑛=2,则可以假设扭曲参数𝛾=0作为额外的约束条件
- 当𝑛=1,则值能计算两个相机的内参数
其中fx = 𝛼(1/𝛾),fy = 𝛽(1/𝛾) 。
为了进一步增加标定结果的可靠性,可以使用最大似然估计来优化上面估计得到的结果。
假设同一相机从𝑛个不同的角度的得到了𝑛幅标定板的图像,每幅图像上有𝑚个像点。𝑀𝑖𝑗表示第𝑖幅图像上第𝑗个像点对应的标定板上的三维点,则: 𝑚̂ (𝐾,𝑅𝑖,𝑡𝑖,𝑀𝑖𝑗) 表示𝑀𝑖𝑗的像点。其中,𝑅𝑖,𝑡𝑖表示第𝑖幅图像对应相机的旋转矩阵和平移向量,𝐾是相机的内参数。则像点𝑚𝑖𝑗的概率密度函数是: 构造似然函数: 为了能够让𝐿取得最大值,需要最小化下面的值问题变成了一个非线性优化问题,利用上面得到的解作为初始值,迭代得到最优解。这个过程就是在减少重投影误差的过程。
至此,通过张正友标定法,我们获得了相机的内参以及外参,但是畸变没有获得。张正友标定法只关注了影响较大的径向畸变。畸变的解算有点类似内参解算,暂时先不列举,脑袋有点炸了。
然而,我没有辫子OpenCV 张正友标定流程展示[代码]
#include <iostream>
#include <opencv2/opencv.hpp>
#include <boost/filesystem.hpp>
std::vector<std::string> get_all_image_file(std::string image_folder_path){
boost::filesystem::path dirpath = image_folder_path;
boost::filesystem::directory_iterator end;
std::vector<std::string> files;
for (boost::filesystem::directory_iterator iter(dirpath); iter != end; iter++)
{
boost::filesystem::path p = *iter;
files.push_back(dirpath.string()+ "/"+ p.leaf().string());
}
std::sort(files.begin(),files.end());
return files;
}
std::vector<cv::Mat> get_all_iamge(std::string image_folder_path)
{
std::vector<cv::Mat> images;
std::vector<std::string> image_files_path = get_all_image_file(image_folder_path);
for(int i=0; i< image_files_path.size() ;i++){
cv::Mat image;
image = cv::imread(image_files_path[i]);
images.push_back(image);
}
return images;
}
int find_chessboard(cv::Mat image, std::vector<cv::Point2f> &image_points, cv::Size board_size)
{
if (0 == findChessboardCorners(image,board_size,image_points))
{
std::cout<<"can not find chessboard corners!\n";
return 0;
}
else
{
cv::Mat view_gray;
cv::cvtColor(image,view_gray,cv::COLOR_RGB2GRAY);
cv::find4QuadCornerSubpix(view_gray,image_points,cv::Size(11,11)); //对粗提取的角点进行亚像素精确化
// int nChessBoardFlags = cv::CALIB_CB_EXHAUSTIVE | cv::CALIB_CB_ACCURACY;
// bool bFindResult = findChessboardCornersSB( view_gray,board_size,image_points,nChessBoardFlags );
// Opencv4 识别棋盘格方法,比opencv3有较大提升
}
return 1;
}
int init_chessboard_3dpoints(cv::Size board_size, std::vector<cv::Point3f> &points, float point_size)
{
cv::Size2f square_size = cv::Size2f(point_size,point_size);
for (int i=0;i<board_size.height;i++){
for (int j=0;j<board_size.width;j++){
cv::Point3f realPoint;
realPoint.x = j*square_size.width;
realPoint.y = i*square_size.height;
realPoint.z = 0;
points.push_back(realPoint);
}
}
return 0;
}
void calib_monocular(std::vector<cv::Mat> images){
cv::Size image_szie;
cv::Size board_size(4,11);
std::vector<cv::Mat> images_tvecs_mat;
std::vector<cv::Mat> images_rvecs_mat;
image_szie.width = images[0].cols;
image_szie.height = images[0].rows;
std::vector<std::vector<cv::Point2f> > images_points;
// 识别所有图片的棋盘格
for(int i=0;i<images.size();i++){
std::vector<cv::Point2f> image_points;
if(find_chessboard(images[i],image_points,board_size)>0){
images_points.push_back(image_points);
}
}
std::vector<cv::Point3f> image_points_in3d;
// 计算棋盘格角点在棋盘格坐标系中的位置
init_chessboard_3dpoints(board_size,image_points_in3d,0.045); // 0.045为棋盘格一个格子的大小
std::vector<std::vector<cv::Point3f> > images_points_in3d;
// 生成所有识别出的标定板对应在各自棋盘格坐标系中的位置
for(int i=0;i<images_points.size();i++){
images_points_in3d.push_back(image_points_in3d);
}
cv::Mat intrinsic,distortion;
// 使用张正友标定法计算内参畸变以及外参
cv::calibrateCamera(images_points_in3d,images_points,image_szie,
intrinsic,distortion,
images_rvecs_mat,images_tvecs_mat);
}
int main(int argc, char *argv[])
{
std::string image_file_path = argv[1];
std::vector<cv::Mat> images = get_all_iamge(image_file_path);
calib_monocular(images);
return 0;
}
标定板示例
总结
张正友标定法的思路并不是很难,主要是解算的数学原理较复杂,需要有比较打的耐心看下去,我现在也只能看懂,让自己完全推导一遍还是挺难的。张正友标定法更重要的是将标定这项工作简洁化,不在需要精密高额的设备,而只需要通过打印标定板就可以获得比较好的效果。
在实际的标定项目中,还是需要注意很多的事情,以下是我在标定时用的一些小trick或者一些注意点:
- 比如某个点识别错了,要通过重投影误差将其剔除,然后重新计算标定结果。
- 增加图片的数目,标定板在图片中的各个角落都要有着各个角度的分布。
- 对于畸变不大的图片,opencv 中圆形标定板的效果要比棋盘格的效果要好,opencv4 棋盘格识别精度有较大提升,但还是建议用圆形。
重要的事情说三遍:
如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )
如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )
如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )
任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴
作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。
网友评论