张正友视觉标定算法学习笔记

作者: 开飞机的乔巴 | 来源:发表于2019-08-26 16:48 被阅读3次

    本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。

    传统2D计算机视觉学习笔记目录------->传送门
    传统3D计算机视觉学习笔记目录------->传送门

    前言

    在阅读本篇文章之前,建议首先要了解知道什么是相机的针孔模型,我在之前的文章小孔相机参数学习笔记中有详细的解释,或者说不是建议,而是必须要知道,不然也不知道这篇文章在干什么。本片文章主要来讲张正友视觉标定法,在讲张正友之前,我们先说一下什么是相机标定。

    所谓相机标定,就是通过实验等方法将相机模型中的未知数给解算出来。例如小孔模型我们一般需要节算出内参矩阵 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的计算流程:

    1. 准备标定图片,原理上三张就够,一般在多个角度采集20张左右。
    2. 提取标定板的关键点,并计算出标定板上关键点的实际相对位置,一般将标定板当做XY平面,Z为0,标定板第一个点为坐标原点。
    3. 相机标定,通过张正友标定法计算出内参外参以及畸变。
    4. 对标定结果进行评价,一般通过重投影的误差进行评价。
    5. 查看标定效果,利用标定结果对棋盘图进行矫正

    从上边的过程可以看出,我们其实只有第三步是真正的解算过程,我们现在来看一下大致的方法。

    首先用于标定的棋盘格是三维场景中的一个平面Π,其在成像平面的像是另一个平面𝜋,知道了两个平面的对应点的坐标,就可以求解得到两个平面的单应矩阵𝐻。其中,标定的棋盘格是特制的,其角点的坐标是已知的;图像中的角点,可以通过角点提取算法得到,这样就可以得到棋盘平面Π和图像平面𝜋的单应矩阵𝐻,即: 其中𝑝是像点坐标,𝑃是标定的棋盘坐标,K是相机内参,为了不失一般性,可以在相机的内参矩阵上添加一个扭曲参数𝛾。即: 这样就可以得到下面的等式:

    是不是通过对应的点对解得𝐻后,则可以通过上面的等式得到相机的内参数𝐾,以及外参旋转矩阵𝑅和平移向量𝑡。

    至于怎么解出来,我们接着看。

    设棋盘格所在的平面为世界坐标系中𝑍=0的平面,这样棋盘格的任一角点𝑃的世界坐标为(𝑋,𝑌,0),根据小孔相机模型: 再根据单应性原则: 根据上面两式则有: 将旋转矩阵𝑅的各个列向量和平移向量𝑡使用𝐻的列向量表示: 又因为,𝑅是旋转矩阵,则其是正交矩阵,也就是其任意两个列向量的内积为0,列向量的模为1,则有: 即:

    那么对于一幅棋盘标定版的图像(一个单应矩阵)可以获得两个对内参数的约束等式。

    我们令: 矩阵𝐵是一个对称矩阵,其未知量只有6个,将6个未知量写为向量的形式: 则有: 其中: 则约束等式有: 写成矩阵的形式有: 假如有𝑛幅图像,则:

    其中,𝑉是一个2𝑛×6的矩阵,𝑏是一个6维向量,所以

    • 当𝑛≥3,可以得到𝑏的唯一解;
    • 当𝑛=2,则可以假设扭曲参数𝛾=0作为额外的约束条件
    • 当𝑛=1,则值能计算两个相机的内参数
    对于方程𝑉𝑏=0可以使用SVD求得其最小二乘解。对𝑉𝑇𝑉进行SVD分解,其最小特征值对应的特征向量就是𝑉𝑏=0的最小二乘解,从而求得矩阵𝐵。由于这里得到的𝐵的估计值是在相差一个常量因子下得到的,所以有: 所以则有:

    其中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 棋盘格识别精度有较大提升,但还是建议用圆形。

    重要的事情说三遍:

    如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

    如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

    如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

    传统2D计算机视觉学习笔记目录------->传送门
    传统3D计算机视觉学习笔记目录------->传送门

    任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴

    作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。

    相关文章

      网友评论

        本文标题:张正友视觉标定算法学习笔记

        本文链接:https://www.haomeiwen.com/subject/ddvrectx.html