02-标定实现

内参标定(棋盘格)

仅考虑棋盘的一个图像的情况下。相机校准所需的重要输入数据是一组3D现实世界点及其对应的2D图像点。可以从图像中轻松找到2D图像点。 (这些图像点是棋盘上两个黑色正方形相互接触的位置)

问题是,如何获取现实世界中的3D点呢? 由于这些图像是从静态相机拍摄的,而棋盘放置在不同的位置和方向。我们需要知道(X,Y,Z)的值。但是换个角度思考,假如我们定义棋盘在X、Y平面上保持静止(Z始终为0),让照相机发生移动了。这种思考仅有助于我们找到X,Y值。现在对于X,Y值,我们可以简单地将点记为(0,0),(1,0),(2,0),...,这表示实际点的位置。在这种情况下,我们得到的结果只是棋盘正方形的大小比例。但是,如果我们知道正方形尺寸(例如20 mm),则可以将值记为(0,0),(20,0),(40,0),...,且得到的结果以mm为单位。(在这种情况下,我们不知道正方形尺寸,因为我们没有拍摄图像,因此我们以正方形尺寸表示)。

计算中,我们将3D对象点取名object points,2D图像点取名image points

1. 加载图片

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

// 保存多张图片对象点列表
vector<vector<Point3f>> objectPoints;
// 保存多张图片的角点列表
vector<vector<Point2f>> cornerPoints;

int main(){
    // 图片像素尺寸
    Size imgSize;
    // 图片路径
    cv::String src_path = "./assets/camerargb_*.jpg";
    std::vector<String> filenames;
    cv::glob(src_path, filenames);//获取路径下所有文件名
    cout << "filenames.size:" << filenames.size() << endl;
    for (auto& imgName : filenames) {
        // 读取图片
        Mat img = imread(imgName, IMREAD_COLOR);
        // 获取图片像素尺寸
        imgSize = img.size();
        std::cout << "name: " << imgName<< " imgSize: " << imgSize << std::endl;
        //...
    }
    return 0;
}

2. 查找角点

由于OpenCV提供的函数参数为灰度图,所以要提前将彩图转为灰度图

  • 首先定义交点查找函数
// 棋盘格的尺寸(宽6,高9)
const Size patternSize(6, 9);
// 黑方格的大小 20mm
const int squareSize = 20;
/**
 * 在指定图片中查找角点,并将结果输出到corners中
 * @param img     待检测图片
 * @param corners 检测到的焦点列表
 * @return 是否检测到角点(两个黑方格的交点)
 */
bool findCorners(Mat &img, vector<Point2f> &corners) {
    Mat gray;
    // 将图片转成灰度图
    cvtColor(img, gray, COLOR_RGB2GRAY);
    // 查找当前图片所有的角点
    bool patternWasFound = findChessboardCorners(gray, patternSize, corners);

    if (patternWasFound) { // 找到角点
        // 提高角点的精确度
        // 原理:https://docs.opencv.org/4.1.0/dd/d1a/group__imgproc__feature.html#ga354e0d7c86d0d9da75de9b9701a9a87e
        cornerSubPix(gray, corners, Size(11, 11), Size(-1, -1),
                     TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 30, 0.1));
    }

    // 将所有的焦点在原图中绘制出来
    drawChessboardCorners(img, patternSize, corners, patternWasFound);
    // 绘制完角点之后,显示原图
    imshow("src", img);

    if (!patternWasFound){
        cout << "角点检测失败!" << endl;
    }
    return patternWasFound;
}
  • 查找角点,创建其对象点
// 保存多张图片对象点列表
vector<vector<Point3f>> objectPoints;
// 保存多张图片的角点列表
vector<vector<Point2f>> cornerPoints;

void calcObjectPoints(vector<Point3f> &objPoint) {
    // 计算uv空间中角点对应的相机坐标系坐标值,设Z为0
    for (int i = 0; i < patternSize.height; ++i)
        for (int j = 0; j < patternSize.width; ++j)
            objPoint.emplace_back(j * squareSize, i * squareSize, 0);
}

// 图片像素尺寸
Size imgSize;

int main(){
    // 图片路径
    cv::String src_path = "./assets/camerargb_*.jpg";
    std::vector<String> filenames;
    cv::glob(src_path, filenames);//获取路径下所有文件名
    cout << "filenames.size:" << filenames.size() << endl;
    for (auto& imgName : filenames) {
        // 读取图片
        Mat img = imread(imgName, IMREAD_COLOR);
        // 获取图片像素尺寸
        imgSize = img.size();
        std::cout << "name: " << imgName<< " imgSize: " << imgSize << std::endl;
        // 声明每张图片的角点
        vector<Point2f> corners;
        bool found = findCorners(img, corners);
        if (found) {
            vector<Point3f> objPoints;
            calcObjectPoints(objPoints);
            // 找到角点,证明这张图是有效的
            objectPoints.push_back(objPoints);
            cornerPoints.push_back(corners);
        }
    }
    return 0;
}

3. 执行相机标定

Mat cameraMatrix; // 相机参数矩阵
Mat disCoffes; // 失真系数 distortion coefficients
Mat rvecs; // 图片旋转向量
Mat tvecs; // 图片平移向量

calibrateCamera(objectPoints, cornerPoints, imgSize, cameraMatrix, disCoffes, rvecs, tvecs);

cout << "标定矩阵:" << cameraMatrix << endl;
cout << "畸变矩阵:" << disCoffes << endl;
// save2xml(cameraMatrix, distCoffes);

waitKey();

4. 保存标定结果

void save2xml(const Mat &cameraMatrix, const Mat &disCoffes) {
    // 获取当前时间
    time_t tm;
    time(&tm);
    struct tm *t2 = localtime(&tm);
    char buf[1024];
    strftime(buf, sizeof(buf), "%c", t2);

    // 写出数据
    String inCailFilePath = "./inCailFilePath.xml";
    FileStorage inCailFs(inCailFilePath, FileStorage::WRITE);
    inCailFs << "calibration_time" << buf;
    inCailFs << "cameraMatrix" << cameraMatrix;
    inCailFs << "distCoffes" << disCoffes;
    inCailFs.release();
}

标定结果示例

  • xml版本
<?xml version="1.0"?>
<opencv_storage>
<calibration_time>"2019年10月15日 星期二 17时25分25秒"</calibration_time>
<cameraMatrix type_id="opencv-matrix">
  <rows>3</rows>
  <cols>3</cols>
  <dt>d</dt>
  <data>
    1.0743566574494685e+03 0. 9.5548426688037193e+02 0.
    1.0758663741535415e+03 5.4946692912295123e+02 0. 0. 1.</data></cameraMatrix>
<disCoffes type_id="opencv-matrix">
  <rows>1</rows>
  <cols>5</cols>
  <dt>d</dt>
  <data>
    7.2163806117565163e-02 -1.1302001810933321e-01
    1.4988711762593146e-03 -3.0609919169149657e-03
    4.2575786627597728e-02</data></disCoffes>
</opencv_storage>
  • yml版本
%YAML:1.0
---
calibration_time: "2019年10月20日 星期日 23时25分26秒"
frame_count: 15
image_width: 640
image_height: 480
board_width: 6
board_height: 9
square_size: 1.9999999552965164e-02
rms: 1.2085572726037488e-01
cameraMatrix: !!opencv-matrix
   rows: 3
   cols: 3
   dt: d
   data: [ 6.0447700370270286e+02, 0., 3.3181544620019122e+02, 0.,
       6.0479737087406238e+02, 2.9128742694561294e+02, 0., 0., 1. ]
distCoeffs: !!opencv-matrix
   rows: 5
   cols: 1
   dt: d
   data: [ 1.9175212705577635e-01, -7.0334052488796239e-01,
       6.9245817187035201e-04, -1.5403756810363311e-03,
       4.2337569654123880e-01 ]

内参标定(圆网格)

“棋盘格”标定中,查找角点的步骤更改为findCirclesGrid即可

bool found = findCirclesGrid(image, board_sz, corners);

摄像头实时标定

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

const int ACTION_ESC = 27;
const int ACTION_SPACE = 32;

static void print_help() {
    printf("通过棋盘格标定相机\n");
    printf("参数:CalibrationChessboard <board_width> n<board_height> <square_size> [number_of_boards] "
           "[--delay=<delay>] [-s=<scale_factor>]\n");
    printf("例子:CalibrationACircle2 6 9 30 500 1.0\n");
}

/**
 * 执行标定并保存标定结果
 * @param square_size   格子尺寸
 * @param board_sz      格子尺寸Size
 * @param image_size    图片尺寸Size
 * @param image_points  图片角点集合
 * @param cameraMatrix  相机参数
 * @param distCoeffs    畸变参数
 */
void
runAndSave(float square_size, const Size board_sz, const Size image_size,
           const vector<vector<Point2f>> &image_points,
           Mat &cameraMatrix, Mat &distCoeffs) {
    vector<vector<Point3f>> object_points;
    vector<Point3f> objPoints;
    for (int i = 0; i < board_sz.height; ++i) {
        for (int j = 0; j < board_sz.width; ++j) {
            // 注意非对称圆网格的对象坐标计算方式
            objPoints.push_back(Point3f(float((2*j + i%2) * square_size),
                                        float(i * square_size), 0));
        }
    }
    object_points.resize(image_points.size(), objPoints);
    vector<Mat> rvecs, tvecs;

    // 执行标定
    double rms = calibrateCamera(object_points, image_points, image_size, cameraMatrix, distCoeffs, rvecs, tvecs);

    // 均方根值(RMS)
    printf("RMS error reported by calibrateCamera: %g\n", rms);
    // 检查标定结果误差
    bool ok = checkRange(cameraMatrix) && checkRange(distCoeffs);


    if (ok) {
        cout << "标定参数:" << endl;
        cout << cameraMatrix << endl;

        cout << "畸变参数:" << endl;
        cout << distCoeffs << endl;

        // 时间、图片个数、图片尺寸、标定板宽高、标定板块尺寸、RMS
        // 标定参数、畸变参数

        // 获取当前时间
        time_t tm;
        time(&tm);
        struct tm *t2 = localtime(&tm);
        char buf[1024];
        strftime(buf, sizeof(buf), "%c", t2);

        // 写出数据
//        String inCailFilePath = "./calibration_in_params.xml";
        String inCailFilePath = "./calibration_in_params2.yml";
        FileStorage inCailFs(inCailFilePath, FileStorage::WRITE);
        inCailFs << "calibration_time" << buf;
        inCailFs << "frame_count" << (int)image_points.size();
        inCailFs << "image_width" << image_size.width;
        inCailFs << "image_height" << image_size.height;
        inCailFs << "board_width" << board_sz.width;
        inCailFs << "board_height" << board_sz.height;
        inCailFs << "square_size" << square_size;
        inCailFs << "rms" << rms;

        inCailFs << "cameraMatrix" << cameraMatrix;
        inCailFs << "distCoeffs" << distCoeffs;
        inCailFs.release();
        std::cout << "标定结果已保存:"<< inCailFilePath << std::endl;
    }else {
        std::cout << "标定结果有误,请重新标定!" << std::endl;
    }
}

int main(int argc, char **argv) {
    std::cout << cv::getVersionString() << std::endl;

    bool flipHorizontal = false;
    // 解析参数
    cv::CommandLineParser parser(argc, argv,
                                 "{@arg1||}{@arg2||}{@arg3|20|}{@arg4|15|}"
                                 "{help h||}{delay d|500|}{scale s|1.0|}{f||}");
    if (parser.has("help")) {
        print_help();
        return 0;
    }
    int board_width = parser.get<int>(0);
    int board_height = parser.get<int>(1);
    float square_size = parser.get<float>(2);
    int num_boards = parser.get<int>(3);
    int delay = parser.get<int>("delay");
    auto image_sf = parser.get<float>("scale");

    if (board_width < 1 || board_height < 1) {
        printf("Command-line parameter error: both image of width and height must be specified\n");
        print_help();
        return -1;
    }
    if (parser.has("f")) {
        flipHorizontal = true;
    }

    std::cout << "board_width: " << board_width << std::endl;
    std::cout << "board_height: " << board_height << std::endl;
    std::cout << "square_size: " << square_size << std::endl;
    std::cout << "num_boards: " << num_boards << std::endl;
    std::cout << "delay: " << delay << std::endl;
    std::cout << "image_sf: " << image_sf << std::endl;


    cv::Size board_sz = cv::Size(board_width, board_height);

    cv::VideoCapture capture(0);
    if (!capture.isOpened()) {
        std::cout << "无法开启摄像头!" << std::endl;
        return -1;
    }

    vector<vector<Point2f>> image_points;
    vector<vector<Point3f>> object_points;

    cv::Size image_size;
    while (image_points.size() < num_boards){
        Mat image0, image;
        capture >> image0;
        image_size = image0.size();
        // 将图像复制到image
        image0.copyTo(image);
        // 缩放
        if (image_sf != 1.0) {
            resize(image0, image, Size(), image_sf, image_sf, cv::INTER_LINEAR);
        }
        // 水平翻转
        if (flipHorizontal) {
            flip(image, image, 1);
        }
        // 查找标定板(不对称圆网格板)
        vector<Point2f> corners;
        bool found = findCirclesGrid(image, board_sz, corners, CALIB_CB_ASYMMETRIC_GRID);

        // 画上去
        drawChessboardCorners(image, board_sz, corners, found);

        int action = waitKey(30) & 255;

        // 判断动作
        if (action == ACTION_SPACE) { // 用户按下了空格
            if (found) {
                // 闪屏
                bitwise_not(image, image);
                // 保存角点
                printf("%s: %d/%d \n", "save角点", (int)image_points.size() + 1, num_boards);
                image_points.push_back(corners);
                // 保存图片
            }else {
                printf("%s\n", "未检测到角点");
            }

        } else if (action == ACTION_ESC) { // 用户按下了ESC
            break;
        }

        cv::imshow("Calibration", image);

    }

    if (image_points.size() < num_boards) {
        printf("角点未达到目标个数,标定已终止!");
        return -1;
    }

    printf("角点收集完毕, 执行标定中... 图片尺寸 %d:%d\n", image_size.width, image_size.height);

    Mat cameraMatrix = Mat::eye(3, 3, CV_64F);
    Mat distCoeffs = Mat::zeros(8, 1, CV_64F);
    runAndSave(square_size, board_sz, image_size, image_points, cameraMatrix, distCoeffs);

    cv::destroyWindow("Calibration");

    return 0;
}

标定参数及优化

findChessboardCorners

通常在执行cv::findChessboardCorners棋盘格角点查找时,会在最后一个参数flags设置一些参数进行角点查找优化,默认参数是CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE,以下是这些参数的意义,这些参数可以单数使用也可以组合使用:

  • CALIB_CB_ADAPTIVE_THRESH:使用自适应阈值将图像转换为黑色和白色,而不是一个固定的阈值水平(从图像的平均亮度计算出来的阈值)。
  • CALIB_CB_NORMALIZE_IMAGE:在自适应二值化之前,对图片的gamma值进行直方图均衡化
  • CALIB_CB_FAST_CHECK:运行一个快速棋盘格角点检查,如果没有找到则尽快返回。这可以大大加快在界面中没有棋盘格时候的查找速度。

  • CALIB_CB_FILTER_QUADS:使用其他条件(例如轮廓区域,周长,正方形形状)过滤掉在轮廓检索阶段提取的假四边形。

推荐使用组合:CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE + CALIB_CB_FAST_CHECK

该功能需要在木板周围留有空白(如正方形的边框,越宽越好),以使检测在各种环境中都更加可靠。否则,如果没有边框且背景较暗,则无法正确分割外部黑色正方形。

cornerSubPix

// 提高角点精度
TermCriteria criteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1);
cv::cornerSubPix(gray, corners, cv::Size(5, 5), cv::Size(-1, -1), criteria);

函数参数说明如下:

  • image:输入图像

  • corners:输入角点的初始坐标以及精准化后的坐标用于输出。

  • winSize:搜索窗口边长的一半,例如如果winSize=Size(5,5),则一个大小为11的搜索窗口将被使用。

  • zeroZone:搜索区域中间的dead region边长的一半,有时用于避免自相关矩阵的奇异性。如果值设为(-1,-1)则表示没有这个区域。

  • criteria:角点精准化迭代过程的终止条件。也就是当迭代次数超过criteria.maxCount,或者角点位置变化小于criteria.epsilon时,停止迭代过程。

calibrateCamera

使用示例:

cv::Mat cameraMatrix = cv::Mat::eye(3, 3, CV_64F);
cv::Mat distCoeffs = cv::Mat::zeros(8, 1, CV_64F);
vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(objectPoints, imagePoints, imgSize, cameraMatrix, distCoeffs, rvecs, tvecs);
  • objectPoints: 对象坐标点列表
  • imagePoints:图像像素点列表
  • imageSize:图像的大小,在计算相机的内参数和畸变矩阵需要用到

  • cameraMatrix:内参矩阵。输入cv::Mat cameraMatrix即可

  • distCoeffs:畸变矩阵。输入cv::Mat distCoeffs即可

  • rvecs:旋转向量vector<cv::Mat> rvecs

  • tvecs:位移向量vector<cv::Mat> tvecs

  • flags为标定是所采用的算法。可如下一个或者多个参数,通过+号连接即可:

  • CV_CALIB_USE_INTRINSIC_GUESS:使用该参数时,在cameraMatrix矩阵中应该有fx,fy,cx,cy的估计值。否则将初始化(cx,cy)图像的中心点,使用最小二乘估算出fx,fy。如果内参矩阵和畸变矩阵已知,应使用标定模块中的solvePnP()函数计算外参数矩阵。
  • CV_CALIB_FIX_PRINCIPAL_POINT:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS参数被设置,光轴点将保持在中心或者某个输入的值。
  • CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只将fy作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS没有被设置,fxfy将会被忽略。只有fx/fy的比值在计算中会被用到。
  • CV_CALIB_ZERO_TANGENT_DIST:设定切向畸变参数(p1,p2)为零。
  • CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6:对应的径向畸变在优化中保持不变。
  • CV_CALIB_RATIONAL_MODEL:计算k4,k5,k6三个畸变参数。如果没有设置,则只计算其它5个参数。

如果对calibrateCamera的详细算法感兴趣,可以阅读张正友的标定算法《A Flexible New Technique for Camera Calibration》