OpenCV 4.11.0
开源计算机视觉库
加载中…
搜索中…
无匹配项
使用OpenCV进行相机标定

上一教程: 使用方形棋盘格进行相机标定
下一教程: 纹理物体的实时姿态估计

原作者Bernát Gábor
兼容性OpenCV >= 4.0

相机已经存在很长时间了。然而,随着20世纪后期廉价针孔相机的出现,它们成为我们日常生活中常见的事物。不幸的是,这种廉价是有代价的:明显的畸变。幸运的是,这些是常数,通过标定和一些重新映射,我们可以纠正这一点。此外,通过标定,您还可以确定相机自然单位(像素)和真实世界单位(例如毫米)之间的关系。

理论

对于畸变,OpenCV考虑了径向和切向因素。对于径向因素,使用以下公式

\[x_{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \\ y_{distorted} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)\]

因此,对于坐标为\((x,y)\)的未畸变像素点,其在畸变图像上的位置将为\((x_{distorted},y_{distorted})\)。径向畸变的存在表现为“桶形”或“鱼眼”效应。

切向畸变发生是因为成像镜头与成像平面不完全平行。它可以通过以下公式表示:

\[x_{distorted} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy]\]

因此,我们有五个畸变参数,在OpenCV中表示为一个具有5列的行矩阵

\[distortion\_coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3)\]

现在,对于单位转换,我们使用以下公式

\[\left [ \begin{matrix} x \\ y \\ w \end{matrix} \right ] = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] \left [ \begin{matrix} X \\ Y \\ Z \end{matrix} \right ]\]

这里\(w\)的存在是由齐次坐标系的用法解释的(并且\(w=Z\))。未知参数是\(f_x\)和\(f_y\)(相机焦距)和\((c_x, c_y)\)(以像素坐标表示的光学中心)。如果对于两个轴都使用公共焦距和给定的\(a\)纵横比(通常为1),则\(f_y=f_x*a\),在上式中我们将只有一个焦距\(f\)。包含这四个参数的矩阵称为相机矩阵。虽然畸变系数与使用的相机分辨率无关,但这些系数应与当前分辨率从标定分辨率一起缩放。

确定这两个矩阵的过程就是标定。这些参数的计算是通过基本的几何方程完成的。使用的方程取决于所选择的标定物体。目前OpenCV支持三种类型的标定物体:

  • 经典黑白棋盘格
  • ChArUco棋盘格图案
  • 对称圆形图案
  • 非对称圆形图案

基本上,您需要使用相机拍摄这些图案的快照,并让OpenCV找到它们。每个找到的图案都会产生一个新的方程。要解方程,您至少需要预定数量的图案快照来形成一个适定的方程组。棋盘格图案所需的数字较高,圆形图案所需的数字较低。例如,理论上棋盘格图案至少需要两张快照。但是,在实践中,我们的输入图像中存在大量的噪声,因此为了获得良好的结果,您可能需要至少拍摄10张不同位置的输入图案的良好快照。

目标

示例应用程序将

  • 确定畸变矩阵
  • 确定相机矩阵
  • 从相机、视频和图像文件列表中获取输入
  • 从XML/YAML文件中读取配置
  • 将结果保存到XML/YAML文件中
  • 计算重投影误差

源代码

您也可以在OpenCV源代码库的samples/cpp/tutorial_code/calib3d/camera_calibration/文件夹中找到源代码,或者从这里下载。有关程序的使用,请使用-h参数运行它。程序有一个重要的参数:其配置文件的名称。如果没有给出,它将尝试打开名为“default.xml”的文件。这是一个XML格式的示例配置文件。在配置文件中,您可以选择使用相机作为输入,视频文件或图像列表。如果您选择最后一个选项,则需要创建一个配置文件来枚举要使用的图像。这是一个示例。需要记住的重要一点是,需要使用绝对路径或应用程序工作目录的相对路径来指定图像。您可以在上面提到的samples目录中找到所有这些内容。

应用程序首先从配置文件中读取设置。虽然这是它重要的部分,但它与本教程的主题:相机标定无关。因此,我选择不在这里发布该部分的代码。有关如何执行此操作的技术背景,您可以在使用XML/YAML/JSON文件进行文件输入和输出教程中找到。

解释

  1. 读取设置

    Settings s;
    const string inputSettingsFile = parser.get<string>(0);
    FileStorage fs(inputSettingsFile, FileStorage::READ); // 读取设置
    if (!fs.isOpened())
    {
    cout << "无法打开配置文件:\"" << inputSettingsFile << "\"" << endl;
    parser.printMessage();
    return -1;
    }
    fs["Settings"] >> s;
    fs.release(); // 关闭设置文件

    为此,我使用了简单的OpenCV类输入操作。读取文件后,我还有一个后处理函数来检查输入的有效性。只有当所有输入都正确时,goodInput变量才为true。

  2. 获取下一个输入,如果失败或我们有足够的输入,则进行标定

    此后,我们有一个大的循环,在其中执行以下操作:从图像列表、相机或视频文件中获取下一张图像。如果失败或我们有足够的图像,则运行标定过程。对于图像,我们退出循环,否则剩余帧将通过从DETECTION模式更改为CALIBRATED模式来进行去畸变(如果设置了该选项)。

    for(;;)
    {
    Mat view;
    bool blinkOutput = false;
    view = s.nextImage();
    //----- 如果没有更多图像,或获得足够的图像,则停止标定并显示结果 -------------
    if( mode == CAPTURING && imagePoints.size() >= (size_t)s.nrFrames )
    {
    如果 (runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints, grid_width,
    release_object))
    mode = CALIBRATED;
    否则
    mode = DETECTION;
    }
    如果 (view.empty()) // 如果没有更多图像,则停止循环
    {
    // 如果尚未达到标定阈值,则现在进行标定
    如果 (mode != CALIBRATED && !imagePoints.empty())
    runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints, grid_width,
    release_object);
    中断;
    }

    对于某些相机,我们可能需要翻转输入图像。我们在这里也这样做。

  3. 在当前输入中查找图案

    上面提到的方程式的形成旨在查找输入中的主要模式:对于棋盘格,这些是方格的角点,对于圆形,则是圆形本身。ChArUco棋盘等效于棋盘,但角点由ArUco标记匹配。这些的位置将形成结果,并将写入pointBuf向量中。

    vector<Point2f> pointBuf;
    布尔型 found;
    如果 (!s.useFisheye) {
    // 快速检查在像鱼眼镜头这样的高畸变情况下错误地失败
    chessBoardFlags |= CALIB_CB_FAST_CHECK;
    }
    根据 s.calibrationPattern // 查找输入格式中的特征点
    {
    情况 Settings::CHESSBOARD
    found = findChessboardCorners(view, s.boardSize, pointBuf, chessBoardFlags);
    中断;
    情况 Settings::CHARUCOBOARD
    ch_detector.detectBoard(view, pointBuf, markerIds);
    found = pointBuf.size() == (size_t)((s.boardSize.height - 1)*(s.boardSize.width - 1));
    中断;
    情况 Settings::CIRCLES_GRID
    found = findCirclesGrid(view, s.boardSize, pointBuf);
    中断;
    情况 Settings::ASYMMETRIC_CIRCLES_GRID
    found = findCirclesGrid(view, s.boardSize, pointBuf, CALIB_CB_ASYMMETRIC_GRID);
    中断;
    默认:
    found = false;
    中断;
    }

    取决于输入模式的类型,您可以使用cv::findChessboardCornerscv::findCirclesGrid函数或cv::aruco::CharucoDetector::detectBoard方法。对于所有这些方法,您都传递当前图像和棋盘的大小,您将获得图案的位置。cv::findChessboardCornerscv::findCirclesGrid返回一个布尔变量,该变量表示是否在输入中找到模式(我们只需要考虑这是真的那些图像!)。CharucoDetector::detectBoard可以检测部分可见的图案,并返回可见内角的坐标和ID。

    注意
    棋盘格、圆形网格和ChArUco的棋盘大小和匹配点数不同。所有与棋盘相关的算法都将内角的数量作为棋盘的宽度和高度。圆形网格的棋盘大小只是两个网格维度的圆形数量。ChArUco棋盘大小用方格定义,但检测结果是内角列表,因此在两个维度上都小1。

    然后,对于相机,我们只在经过输入延迟时间后才拍摄相机图像。这样做是为了允许用户移动棋盘并获取不同的图像。相似的图像会导致相似的方程,而标定步骤中的相似方程将形成一个不适定问题,因此标定将失败。对于方形图像,角点的位置只是近似的。我们可以通过调用cv::cornerSubPix函数来改进这一点。(winSize用于控制搜索窗口的边长。它的默认值为11。winSize可以通过命令行参数--winSize=<number>更改。)这将产生更好的标定结果。之后,我们将有效输入的结果添加到imagePoints向量中,以将所有方程式收集到单个容器中。最后,出于可视化反馈的目的,我们将使用cv::findChessboardCorners函数在输入图像上绘制找到的点。

    如果 (found) // 如果成功,
    {
    // 提高棋盘上找到的角点的坐标精度
    如果 (s.calibrationPattern == Settings::CHESSBOARD)
    {
    Mat viewGray;
    cvtColor(view, viewGray, COLOR_BGR2GRAY);
    cornerSubPix(viewGray, pointBuf, Size(winSize,winSize),
    Size(-1,-1), TermCriteria(TermCriteria::EPS+TermCriteria::COUNT, 30, 0.0001));
    }
    如果 (mode == CAPTURING && // 仅对于相机,在延迟时间后才获取新样本
    (!s.inputCapture.isOpened() || clock() - prevTimestamp > s.delay*1e-3*CLOCKS_PER_SEC))
    {
    imagePoints.push_back(pointBuf);
    prevTimestamp = clock();
    blinkOutput = s.inputCapture.isOpened();
    }
    // 绘制角点。
    如果 (s.calibrationPattern == Settings::CHARUCOBOARD)
    drawChessboardCorners(view, cv::Size(s.boardSize.width-1, s.boardSize.height-1), Mat(pointBuf), found);
    否则
    drawChessboardCorners(view, s.boardSize, Mat(pointBuf), found);
    }
  4. 向用户显示状态和结果,以及应用程序的命令行控制

    此部分在图像上显示文本输出。

    字符串 msg = (mode == CAPTURING) ? "100/100"
    mode == CALIBRATED ? "已标定" : "按'g'键开始";
    int baseLine = 0;
    cv::Size textSize = cv::getTextSize(msg, 1, 1, 1, &baseLine);
    cv::Point textOrigin(view.cols - 2*textSize.width - 10, view.rows - 2*baseLine - 10);
    if( mode == CAPTURING )
    {
    if(s.showUndistorted)
    msg = cv::format("%d/%d Undist", (int)imagePoints.size(), s.nrFrames);
    否则
    msg = cv::format("%d/%d", (int)imagePoints.size(), s.nrFrames);
    }
    cv::putText(view, msg, textOrigin, 1, 1, mode == CALIBRATED ? GREEN : RED);
    if( blinkOutput )
    cv::bitwise_not(view, view);

    如果我们进行了标定并获得了带有畸变系数的相机矩阵,我们可能希望使用cv::undistort函数校正图像

    if( mode == CALIBRATED && s.showUndistorted )
    {
    cv::Mat temp = view.clone();
    if (s.useFisheye)
    {
    cv::Mat newCamMat;
    fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
    cv::Matx33d::eye(), newCamMat, 1);
    cv::fisheye::undistortImage(temp, view, cameraMatrix, distCoeffs, newCamMat);
    }
    否则
    cv::undistort(temp, view, cameraMatrix, distCoeffs);
    }

    然后我们显示图像并等待输入按键,如果按键是“u”,我们将切换畸变去除,如果按键是“g”,我们将重新开始检测过程,最后,对于ESC键,我们将退出应用程序

    cv::imshow("Image View", view);
    char key = (char)cv::waitKey(s.inputCapture.isOpened() ? 50 : s.delay);
    if( key == ESC_KEY )
    中断;
    if( key == 'u' && mode == CALIBRATED )
    s.showUndistorted = !s.showUndistorted;
    if( s.inputCapture.isOpened() && key == 'g' )
    {
    mode = CAPTURING;
    imagePoints.clear();
    }
  5. 也显示图像的畸变去除

    当使用图像列表时,无法在循环内去除畸变。因此,必须在循环后执行此操作。现在利用这一点,我将扩展cv::undistort函数,该函数实际上首先调用cv::initUndistortRectifyMap来查找变换矩阵,然后使用cv::remap函数执行变换。因为在成功标定后只需要进行一次地图计算,所以通过使用这种扩展形式,您可以加快应用程序的速度

    if( s.inputType == Settings::IMAGE_LIST && s.showUndistorted && !cameraMatrix.empty())
    {
    cv::Mat view, rview, map1, map2;
    if (s.useFisheye)
    {
    cv::Mat newCamMat;
    fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
    cv::Matx33d::eye(), newCamMat, 1);
    fisheye::initUndistortRectifyMap(cameraMatrix, distCoeffs, cv::Matx33d::eye(), newCamMat, imageSize,
    CV_16SC2, map1, map2);
    }
    否则
    {
    cameraMatrix, distCoeffs, cv::Mat(),
    cv::getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0), imageSize,
    CV_16SC2, map1, map2);
    }
    for(size_t i = 0; i < s.imageList.size(); i++ )
    {
    view = cv::imread(s.imageList[i], cv::IMREAD_COLOR);
    if(view.empty())
    continue;
    cv::remap(view, rview, map1, map2, cv::INTER_LINEAR);
    cv::imshow("Image View", rview);
    char c = (char)cv::waitKey();
    if( c == ESC_KEY || c == 'q' || c == 'Q' )
    中断;
    }
    }

标定和保存

因为每个摄像机只需要进行一次标定,所以在成功标定后保存标定结果是有意义的。这样,以后就可以将这些值加载到程序中。因此,我们首先进行标定,如果标定成功,我们将结果保存到 OpenCV 风格的 XML 或 YAML 文件中,具体取决于您在配置文件中提供的扩展名。

因此,在第一个函数中,我们只是将这两个过程分开。因为我们想保存许多标定变量,所以我们将在此处创建这些变量,并将它们传递给标定和保存函数。同样,我不会展示保存部分,因为这与标定几乎没有共同之处。请浏览源文件以了解如何以及保存什么。

bool runCalibrationAndSave(Settings& s, cv::Size imageSize, cv::Mat& cameraMatrix, cv::Mat& distCoeffs,
std::vector<std::vector<cv::Point2f> > imagePoints, float grid_width, bool release_object)
{
std::vector<cv::Mat> rvecs, tvecs;
std::vector<float> reprojErrs;
double totalAvgErr = 0;
std::vector<cv::Point3f> newObjPoints;
bool ok = runCalibration(s, imageSize, cameraMatrix, distCoeffs, imagePoints, rvecs, tvecs, reprojErrs,
totalAvgErr, newObjPoints, grid_width, release_object);
std::cout << (ok ? "Calibration succeeded" : "Calibration failed")
<< ". avg re projection error = " << totalAvgErr << std::endl;
if (ok)
saveCameraParams(s, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, imagePoints,
totalAvgErr, newObjPoints);
return ok;
}
@ READ
value,打开文件以进行读取
定义 persistence.hpp:266
@ CALIB_USE_INTRINSIC_GUESS
定义 calib3d.hpp:3787
@ CALIB_FIX_K2
定义 calib3d.hpp:3792
@ CALIB_FIX_K4
定义 calib3d.hpp:3794
@ CALIB_FIX_K1
定义 calib3d.hpp:3791
@ CALIB_FIX_PRINCIPAL_POINT
定义 calib3d.hpp:3796
@ CALIB_FIX_K3
定义 calib3d.hpp:3793
#define INVALID
定义 multicalib.hpp:56
#define CV_32FC2
定义 interface.h:119
#define CV_16SC2
定义 interface.h:107

我们借助cv::calibrateCameraRO函数进行标定。它具有以下参数

  • 目标点。这是一个 `Point3f` 向量向量,用于描述每个输入图像中图案应有的外观。如果我们使用的是平面图案(例如棋盘),则可以简单地将所有 Z 坐标设置为零。这是一个这些重要点存在的点的集合。因为我们对所有输入图像使用单一图案,所以我们只需计算一次,然后将其乘以所有其他输入视图。我们使用 `calcBoardCornerPositions` 函数计算角点,如下所示:
    static void calcBoardCornerPositions(Size boardSize, float squareSize, vector<Point3f>& corners,
    Settings::Pattern patternType /*= Settings::CHESSBOARD*/)
    {
    corners.clear();
    switch(patternType)
    {
    情况 Settings::CHESSBOARD
    情况 Settings::CIRCLES_GRID
    for (int i = 0; i < boardSize.height; ++i) {
    for (int j = 0; j < boardSize.width; ++j) {
    corners.push_back(Point3f(j*squareSize, i*squareSize, 0));
    }
    }
    中断;
    情况 Settings::CHARUCOBOARD
    for (int i = 0; i < boardSize.height - 1; ++i) {
    for (int j = 0; j < boardSize.width - 1; ++j) {
    corners.push_back(Point3f(j*squareSize, i*squareSize, 0));
    }
    }
    中断;
    情况 Settings::ASYMMETRIC_CIRCLES_GRID
    for (int i = 0; i < boardSize.height; i++) {
    for (int j = 0; j < boardSize.width; j++) {
    corners.push_back(Point3f((2 * j + i % 2)*squareSize, i*squareSize, 0));
    }
    }
    中断;
    默认:
    中断;
    }
    }
    然后将其乘以
    vector<vector<Point3f> > objectPoints(1);
    calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);
    objectPoints[0][s.boardSize.width - 1].x = objectPoints[0][0].x + grid_width;
    newObjPoints = objectPoints[0];
    objectPoints.resize(imagePoints.size(),objectPoints[0]);
    注意
    如果您的标定板不够精确,未经测量,目标大致为平面(使用现成打印机的纸张上的棋盘格图案是最方便的标定目标,并且大多数都不够精确),则可以使用来自[255] 的方法来显著提高估计的相机内参的精度。如果提供命令行参数 `-d=<number>`,则将调用此新的标定方法。在上面的代码片段中,`grid_width` 实际上是由 `-d=<number>` 设置的值。它是图案网格点左上角 (0, 0, 0) 和右上角 (s.squareSize*(s.boardSize.width-1), 0, 0) 角之间的已测距离。应该用尺子或游标卡尺精确测量。标定后,将使用改进的目标点 3D 坐标更新 newObjPoints。
  • 图像点。这是一个 `Point2f` 向量向量,对于每个输入图像,它包含重要点的坐标(棋盘的角点和圆形图案的圆心)。我们已经从 cv::findChessboardCornerscv::findCirclesGrid 函数中收集了这些信息。我们只需要传递它。
  • 从相机、视频文件或图像获取的图像大小。
  • 要固定的目标点的索引。我们将其设置为 -1 以请求标准校准方法。如果要使用新的目标释放方法,则将其设置为标定板网格右上角点的索引。有关详细说明,请参见 cv::calibrateCameraRO
    int iFixedPoint = -1;
    if (release_object)
    iFixedPoint = s.boardSize.width - 1;
  • 相机矩阵。如果我们使用固定纵横比选项,我们需要设置 \(f_x\)
    cameraMatrix = Mat::eye(3, 3, CV_64F);
    if( !s.useFisheye && s.flag & CALIB_FIX_ASPECT_RATIO )
    cameraMatrix.at<double>(0,0) = s.aspectRatio;
  • 畸变系数矩阵。用零初始化。
    distCoeffs = Mat::zeros(8, 1, CV_64F);
    #define CV_64F
    定义 interface.h:79
  • 对于所有视图,该函数将计算旋转和平移向量,这些向量将目标点(在模型坐标空间中给出)转换为图像点(在世界坐标空间中给出)。第 7 个和第 8 个参数是矩阵的输出向量,在第 i 个位置包含从第 i 个目标点到第 i 个图像点的旋转和平移向量。
  • 更新的校准图案点的输出向量。此参数在标准校准方法中被忽略。
  • 最后一个参数是标志。您需要在此处指定选项,例如修复焦距的纵横比、假设零切向畸变或修复主点。这里我们使用 CALIB_USE_LU 来加快标定速度。
    rms = calibrateCameraRO(objectPoints, imagePoints, imageSize, iFixedPoint,
    cameraMatrix, distCoeffs, rvecs, tvecs, newObjPoints,
    s.flag | CALIB_USE_LU);
  • 该函数返回平均重投影误差。此数字很好地估计了所找到参数的精度。这应该尽可能接近零。给定内参、畸变、旋转和平移矩阵,我们可以使用 cv::projectPoints 将目标点首先变换到图像点来计算一个视图的误差。然后我们计算我们通过变换得到的结果和角点/圆查找算法的结果之间的绝对范数。为了找到平均误差,我们计算所有标定图像计算的误差的算术平均值。
    static double computeReprojectionErrors( const vector<vector<Point3f> >& objectPoints,
    const vector<vector<Point2f> >& imagePoints,
    const vector<Mat>& rvecs, const vector<Mat>& tvecs,
    const Mat& cameraMatrix , const Mat& distCoeffs,
    vector<float>& perViewErrors, bool fisheye)
    {
    vector<Point2f> imagePoints2;
    size_t totalPoints = 0;
    double totalErr = 0, err;
    perViewErrors.resize(objectPoints.size());
    for(size_t i = 0; i < objectPoints.size(); ++i )
    {
    if (fisheye)
    {
    fisheye::projectPoints(objectPoints[i], imagePoints2, rvecs[i], tvecs[i], cameraMatrix,
    distCoeffs);
    }
    否则
    {
    projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, imagePoints2);
    }
    err = norm(imagePoints[i], imagePoints2, NORM_L2);
    size_t n = objectPoints[i].size();
    perViewErrors[i] = (float) std::sqrt(err*err/n);
    totalErr += err*err;
    totalPoints += n;
    }
    return std::sqrt(totalErr/totalPoints);
    }

结果

假设有一个输入的棋盘格图案,大小为 9 X 6。我使用 AXIS IP 摄像头拍摄了棋盘的几张快照,并将其保存到 VID5 目录中。我已经将它放在我的工作目录的images/CameraCalibration文件夹中,并创建了以下VID5.XML文件来描述要使用的图像。

<?xml version="1.0"?>
<opencv_storage>
<images>
images/CameraCalibration/VID5/xx1.jpg
images/CameraCalibration/VID5/xx2.jpg
images/CameraCalibration/VID5/xx3.jpg
images/CameraCalibration/VID5/xx4.jpg
images/CameraCalibration/VID5/xx5.jpg
images/CameraCalibration/VID5/xx6.jpg
images/CameraCalibration/VID5/xx7.jpg
images/CameraCalibration/VID5/xx8.jpg
</images>
</opencv_storage>

然后在配置文件中将images/CameraCalibration/VID5/VID5.XML作为输入。这是应用程序运行时找到的棋盘格图案。

应用畸变去除后,我们得到:

同样的方法也适用于这种非对称圆形图案,将输入宽度设置为 4,高度设置为 11。这次我使用实时摄像机馈送,通过指定其 ID (“1”) 作为输入。检测到的图案应该如下所示:

在这两种情况下,在指定的输出 XML/YAML 文件中,您都会找到相机和畸变系数矩阵。

<camera_matrix type_id="opencv-matrix">
<rows>3</rows>
<cols>3</cols>
<dt>d</dt>
<data>
6.5746697944293521e+002 0. 3.1950000000000000e+002 0.
6.5746697944293521e+002 2.3950000000000000e+002 0. 0. 1.</data></camera_matrix>
<distortion_coefficients type_id="opencv-matrix">
<rows>5</rows>
<cols>1</cols>
<dt>d</dt>
<data>
-4.1802327176423804e-001 5.0715244063187526e-001 0. 0.
-5.7843597214487474e-001</data></distortion_coefficients>

将这些值作为常量添加到您的程序中,调用cv::initUndistortRectifyMapcv::remap 函数来去除畸变,并享受廉价低质量摄像机的无畸变输入。

您可以在YouTube 上观看运行时实例