跳到主要内容

cv::solvePnP() 实现姿态估计

cv::solvePnP()

PnP 求解器根据给定的几组点,以及相机的内参,求解出相关的位姿信息,其中包括相机坐标系,图像坐标系以及世界坐标系之间的转换关系

solvePnP( InputArray objectPoints,       // 输入的世界坐标点集合
InputArray imagePoints, // 输入的二维图像坐标点集
InputArray cameraMatrix, // 相机的内参
InputArray distCoeffs, // 相机的畸变参数
OutputArray rvec, // 输出的旋转向量 rotation
OutputArray tvec, // 输出的平移向量 translation
bool useExtrinsicGuess = false, // 用于求解PNP_迭代的参数,如果为 true 则会不断优化求解结果
int flags = SOLVEPNP_ITERATIVE // 求解的方法,具体可以查阅文档
);

基本使用

  • 相机的内参矩阵可以表示为
(fx0cx0fycy001)\begin{pmatrix} f_x & 0 & c_x \\\\ 0 & f_y & c_y \\\\ 0 & 0 & 1 \\\\ \end{pmatrix}
  • 畸变参数 k1,k2,k3,p1,p2 分别为径向和切向畸变系数,可以用一个 1x5 的矩阵表示

这两组参数一般通过标定相机获得

求解已知平面

首先定义平面的长宽 wh,然后定义世界坐标系的原点,并得到平面4个定点与原点的关系,假设定义坐标系原点为平面中心

  • 定义世界坐标系下的点集如下
float w, h;
vector<cv::Point3f> object_point;
object_point.clear();
object_point.push_back(cv::Point3f(-w / 2, -h / 2, 0));
object_point.push_back(cv::Point3f(w / 2, -h / 2, 0));
object_point.push_back(cv::Point3f(w / 2, h / 2, 0));
object_point.push_back(cv::Point3f(-w / 2, h / 2, 0));

获取图像坐标系下对应的点集

vector<cv::Point2f> image_point;

创建旋转 rotation 以及平移 translation 向量

cv::Mat rvec = cv::Mat::zeros(3, 3, CV_64FC1);
cv::Mat tvec = cv::Mat::zeros(3, 1, CV_64FC1);
  • 求解
cv::solvePnP(object_point, image_point, camera_matrix, distortion_coeffs, rvec, tvec, cv::SOLVEPNP_IPPE_SQUARE);

坐标转换

从世界坐标转换至图像坐标

OpenCV 中提供了计算点投影的方法 cv::projectPoints()

void projectPoints( InputArray objectPoints,
InputArray rvec, InputArray tvec,
InputArray cameraMatrix, InputArray distCoeffs,
OutputArray imagePoints,
OutputArray jacobian = noArray(),
double aspectRatio = 0 );
  • 参数可以直接使用 solvePnP 求解出的相关参数,imagePoints 即为转换后的图像坐标点集
从图像坐标转换至世界坐标

图像坐标 (u,v) 与世界坐标 (x,y,z) 转换的公式为

[xyz]=R1(Mcamera1S[uv1]t)\begin{bmatrix} x \\ y \\ z \end{bmatrix} = R^{-1}(M_{camera}^{-1} * S * \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} - t)
  • RR 为旋转矩阵,可由旋转向量 rvec 求解
  • McameraM_{camera} 为相机的内参矩阵
  • SS 为目标点在相机坐标系Z方向的值,即目标点到相机平面的距离,可以通过已知参数求解
  • tt 为求解的平移向量 tvec

solvePnP 的求解对象为一个固定的平面,则世界坐标系下的 zz 即固定为 zconstz_{const}

公式简化为

[xyzconst]=R1(Mcamera1S[uv1]t)S[uv1]=Mcamera(R[xyzconst]+t)\begin{aligned} \begin{bmatrix} x \\ y \\ z_{const} \end{bmatrix} &= R^{-1}(M_{camera}^{-1} * S * \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} - t) \\ S \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} &=M_{camera} (R * \begin{bmatrix} x \\ y \\ z_{const} \end{bmatrix} + t) \end{aligned}

变换可得

R1Mcamera1S[uv1]=[xyzconst]+R1tR^{-1}M_{camera}^{-1}S \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \begin{bmatrix} x \\ y \\ z_{const} \end{bmatrix} + R^{-1}t
  • R1Mcamera1R^{-1}M_{camera}^{-1} 记为 M1M_1R1tR^{-1}t 记为 M2M_2

可得

M1[2]1S1=zconstM2[2]S=(zconstM2[2])M1[2]\begin{aligned} M_1[2]^{-1}S * 1 &= z_{const} * M_2[2] \\ S &= (z_{const} * M_2[2]) | M_1[2] \end{aligned}
  • 即求得 SS

已知所有参数,即可求解图像坐标到世界坐标的转换

代码实现如下

cv::Mat rotation_matrix = cv::Mat(3,3,cv::DataType<double>::type);
cv::Rodrigues(this->rvec, rotation_matrix); // 由 rvec 求解 R

cv::Mat uv_pt = cv::Mat::ones(3,1,cv::DataType<double>::type);
uv_pt.at<double>(0,0) = static_cast<double>(img_point.x); // img_point 为图像坐标点
uv_pt.at<double>(1,0) = static_cast<double>(img_point.y);

cv::Mat M_1, M_2;
double S, z_const = 0;
M_1 = rotation_matrix.inv() * camera_matrix.inv() * uv_pt; // M_1
M_2 = rotation_matrix.inv() * tvec; // M_2
/* S = (z_const + M_2[2]) / M_1[2] */
S = z_const + M_2.at<double>(2,0);
S /= M_1.at<double>(2,0);


/* world_pt = R^{-1}(M_{camera}^{-1} * S * uv_pt - t) */
cv::Mat world_pt = rotation_matrix.inv() * (S * camera_matrix.inv() * uv_pt - tvec);

cv::Point3f world_point = cv::Point3f(world_pt.at<double>(0, 0), world_pt.at<double>(1, 0), world_pt.at<double>(2, 0));
  • 此方法求解存在一定误差,尝试用别的方法修正深度值 SS 能减小误差

求解角度

参考