OpenCV:解决solvePnP检测问题

20
我在使用OpenCV进行标记的精准检测时遇到了问题。我录制了一段视频来展示这个问题:http://youtu.be/IeSSW4MdyfU。如您所见,我检测的标记在某些摄像机角度下会略微移动。我在网上读到过这可能是相机校准问题,因此我会告诉大家我的相机校准方式,也许您能告诉我哪里出了问题?
首先,我从各种图像中收集数据,并将校准角点存储在_imagePoints向量中,就像这样:
std::vector<cv::Point2f> corners;
_imageSize = cvSize(image->size().width, image->size().height);

bool found = cv::findChessboardCorners(*image, _patternSize, corners);

if (found) {
    cv::Mat *gray_image = new cv::Mat(image->size().height, image->size().width, CV_8UC1);
    cv::cvtColor(*image, *gray_image, CV_RGB2GRAY);

    cv::cornerSubPix(*gray_image, corners, cvSize(11, 11), cvSize(-1, -1), cvTermCriteria(CV_TERMCRIT_EPS+ CV_TERMCRIT_ITER, 30, 0.1));

    cv::drawChessboardCorners(*image, _patternSize, corners, found);
}

_imagePoints->push_back(_corners);

然后,收集足够的数据后,我将使用以下代码计算相机矩阵和系数:

std::vector< std::vector<cv::Point3f> > *objectPoints = new std::vector< std::vector< cv::Point3f> >();

for (unsigned long i = 0; i < _imagePoints->size(); i++) {
    std::vector<cv::Point2f> currentImagePoints = _imagePoints->at(i);
    std::vector<cv::Point3f> currentObjectPoints;

    for (int j = 0; j < currentImagePoints.size(); j++) {
        cv::Point3f newPoint = cv::Point3f(j % _patternSize.width, j / _patternSize.width, 0);

        currentObjectPoints.push_back(newPoint);
    }

    objectPoints->push_back(currentObjectPoints);
}

std::vector<cv::Mat> rvecs, tvecs;

static CGSize size = CGSizeMake(_imageSize.width, _imageSize.height);
cv::Mat cameraMatrix = [_userDefaultsManager cameraMatrixwithCurrentResolution:size]; // previously detected matrix
cv::Mat coeffs = _userDefaultsManager.distCoeffs; // previously detected coeffs
cv::calibrateCamera(*objectPoints, *_imagePoints, _imageSize, cameraMatrix, coeffs, rvecs, tvecs);

结果就像你在视频中看到的那样。

我做错了什么?这是代码问题吗?在校准之前应该使用多少图像(现在我正在尝试获得20-30张图像)。

我应该使用包含错误检测到的棋盘角落的图像,例如:

photo 1

还是只使用正确检测到的棋盘图像,如下所示:

photo 2 photo 3

我已经尝试使用圆形网格代替棋盘,但结果比现在要糟糕得多。

如果有关于我如何检测标记的问题:我正在使用solvepnp函数:

solvePnP(modelPoints, imagePoints, [_arEngine currentCameraMatrix], _userDefaultsManager.distCoeffs, rvec, tvec);

使用如下所示的modelPoints:

    markerPoints3D.push_back(cv::Point3d(-kMarkerRealSize / 2.0f, -kMarkerRealSize / 2.0f, 0));
    markerPoints3D.push_back(cv::Point3d(kMarkerRealSize / 2.0f, -kMarkerRealSize / 2.0f, 0));
    markerPoints3D.push_back(cv::Point3d(kMarkerRealSize / 2.0f, kMarkerRealSize / 2.0f, 0));
    markerPoints3D.push_back(cv::Point3d(-kMarkerRealSize / 2.0f, kMarkerRealSize / 2.0f, 0));

imagePoints 是处理图像中标记角点的坐标(我正在使用自定义算法完成此操作)。


2
我暂时没有时间尝试这个,但是我找到了一个起点(对于我来说是Python,虽然C++和Python接口非常相似)。我在Python中发现了这篇教程(确保检查标定和姿态估计的下一篇),其中提到了您所询问的很多相同问题。也许那里的一些提示/指导对您有用? - KobeJohn
谢谢您提供这个教程的链接,但我不认为它会对我的情况有所帮助。我正在以与教程中写的方式相同的方式校准相机。在校准之后,该教程的作者在输入图像上使用了undistort方法,但我认为如果我在solvePnP中使用该函数的结果图像,它将会出错。我认为这是在向该函数提供cameraMatrix和畸变系数的情况下发生的,因此solvePnP将自行对图像进行校正。 - Axadiw
最后,这是一个相反的问题 - projectPoints 方法用于将3D对象转换为2D对象,而不是相反的情况 :) - Axadiw
我了解了,非常抱歉!我一定会在未来尽力让您的系统正常运行,但需要一些时间。 - KobeJohn
3个回答

5
为了正确调试您的问题,我需要所有的代码 :-)
我假设您正在遵循教程中校准姿态所建议的方法,这些教程由@kobejohn在他的评论中引用,因此您的代码应该按照这些步骤进行。
  1. 收集各种棋盘目标的图像
  2. 在1)中的图像中找到棋盘角落
  3. 使用cv::calibrateCamera校准相机,并得到内部相机参数(称为intrinsic)和镜头畸变参数(称为distortion)作为结果
  4. 收集自己定制目标的图像(目标在0:57 in your video处可见),并在其中找到一些相关点(将在图像中找到的点称为image_custom_target_vertices,对应的3D点称为world_custom_target_vertices
  5. 使用类似于这样的cv::solvePnP调用cv::solvePnP(world_custom_target_vertices,image_custom_target_vertices,intrinsic,distortion,R,t)从自定义目标的图像中估计相机的旋转矩阵(称为R)和平移向量(称为t
  6. 通过调用cv2::projectPoints,例如这样一个函数cv::projectPoints(world_cube_vertices,R,t,intrinsic,distortion,image_cube_vertices),可以通过给出的8个3D立方体顶点(称为world_cube_vertices)得到8个2D图像点(称为image_cube_vertices
  7. 使用自己的draw函数绘制立方体。

现在,抽奖程序的最终结果取决于先前计算的所有数据,我们必须找出问题所在:

校准:正如您在答案中观察到的那样,在3)中,您应该丢弃未正确检测到角落的图像。您需要一个重投影误差阈值,以便丢弃“坏”的棋盘目标图像。引用自校准教程

重投影误差

重投影误差能够很好地估计找到的参数的精确度。这个误差应该尽可能接近于零。给定内部、失真、旋转和平移矩阵,我们首先使用cv2.projectPoints()将对象点转换为图像点。然后,我们计算我们的转换和角点查找算法之间的绝对范数。为了找到平均误差,我们计算所有校准图像计算出的误差的算术平均值。

通常通过实验可以找到适当的阈值。使用这个额外的步骤,您将获得更好的 intrinsicdistortion 值。
寻找自定义目标:在我标记为第4点的步骤中,您似乎没有解释如何找到自己的自定义目标。您是否得到了预期的 image_custom_target_vertices?您是否舍弃那些结果“不好”的图像?
相机的姿态:我认为在第5点中,您使用了第3点中找到的 intrinsic,您确定相机在此期间没有发生任何变化吗?参考Callari's Second Rule of Camera Calibration

相机校准的第二条规则:“在校准后不得触摸镜头”。特别是,您不能重新对焦或更改光圈大小,因为对焦和光圈大小都会影响非线性镜头畸变以及(取决于镜头)视场。当然,您完全可以更改曝光时间,因为它根本不会影响镜头几何形状。

接下来可能会在draw函数中出现一些问题。


1
这是完整的代码:https://bitbucket.org/axadiw/augmentedmgr虽然UI是用波兰语编写的,但所有变量/注释都是用英语编写的,因此应该可以阅读 :)应用程序的大部分部分都是用Objective-C编写的,与OpenCV相关的部分则是用C++编写的。相机校准在AMCalibrator类中进行处理,相机姿态检测在AMCameraPoseDetector中进行处理。标记在AMMarkersDetector中被检测到。 - Axadiw
关于你的问题 - 我正在做与你列出的几乎相同的事情,只有一个区别 - 在标记检测中我使用4个点而不是8个。因为我不是手动绘制每个顶点(我使用由“NinevehGL”渲染的模型 - 简单的iOS 3D引擎) - 直接将相机平移和旋转向量用作3D场景中的相机位置。 - Axadiw

2
所以,我已经对我的代码进行了很多实验,但我仍然没有解决主要问题(移动的物体),但我已经回答了一些校准问题。
首先 - 为了获得良好的校准结果,您必须使用具有正确检测到的网格元素/圆位置的图像!在校准过程中使用所有捕获的图像(即使那些未正确检测到的图像)将导致糟糕的校准结果。
我已经尝试过各种校准模式:
  • 非对称圆形模式CALIB_CB_ASYMMETRIC_GRID)比任何其他模式都要差。我指的是它会产生很多错误检测的角落,如下所示:

photo 1 photo 2

我尝试了CALIB_CB_CLUSTERING,但它并没有帮助太多 - 在某些情况下(不同的光环境)它会变得更好,但不会有太大改善。

  • 对称圆形模式CALIB_CB_SYMMETRIC_GRID) - 比非对称网格好,但仍然比标准网格(棋盘格)差得多。它经常会产生以下错误:

photo 3

  • 棋盘格(使用findChessboardCorners函数找到)- 这种方法产生了最好的可能结果 - 它很少产生未对齐的角落,并且几乎每次校准都会产生与对称圆形网格的最佳结果相似的结果。
对于每个校准,我一直在使用来自不同角度的20-30张图像。我甚至尝试过100多张图像,但它没有产生比较少量的图像更明显的校准结果变化。值得注意的是,测试图像的数量越多,非线性地计算摄像机参数所需的时间就越长(在iPad4上,480x360分辨率的100个测试图像需要25分钟,而使用约50个图像只需4分钟)。
我还尝试了solvePNP参数 - 但它也没有给我任何可接受的结果:我尝试了所有3种检测方法(ITERATIVEEPNPP3P),但我没有看到任何明显的变化。
此外,我尝试将useExtrinsicGuess设置为true,并使用先前检测到的rvectvec,但这样会导致完全消失的检测到的立方体。
我已经想不出其他可能影响这些移动问题的原因了。

不幸的是,“solvePnPRansac”给出了与“solvePnP”相同的结果 - 我尝试了各种参数(迭代次数/“reprojectionError”等),但没有成功。 - Axadiw
从http://docs.opencv.org/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#solvepnp引用,P3P将仅使用四个对象/图像点工作。*CV_P3P方法基于高希声、侯向荣、唐杰、常海峰的论文“透视三点问题的完整解分类”。在这种情况下,该函数需要恰好四个对象和图像点。* - Alessandro Jacopson

1

对于那些仍然感兴趣的人: 这是一个旧问题,但我认为你的问题不在于错误的校准。 我使用OpenCV和SceneKit开发了一款iOS AR应用程序,并且遇到了与您相同的问题。

我认为你的问题在于立方体的渲染位置错误: OpenCV的solvePnP返回标记中心点的X、Y、Z坐标,但你需要在标记上渲染立方体,在标记的Z轴上特定距离处,恰好在立方体边长的一半处。因此,你需要改进标记翻译向量的Z坐标,以便达到这个距离。

事实上,当你从顶部看立方体时,立方体被正确地渲染。 我做了一张图片来解释这个问题,但我的声誉防止我发布它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接