通常视觉slam中landmark为一个世界系下的点,可以参数化为一个向量\(P = [x,y,z]^T\)或者参数化为第一次观测到的图像坐标加一维逆深度。

对于使用marker的slam来说,通常的做法并不可取。对于一个marker来说,四个特征点的位置是具有特殊的约束(四个坐标点之间的距离固定,并且在一个平面内)。因此在这里将marker作为一个整体参数化为一个三维空间下的旋转加平移。

Bundle Adjustment

相关定义如下:

  • marker特征点在marker坐标系为\(P^m\)
  • marker 在相机坐标系坐标为\([t^c_m, R^c_m]\)
  • 相机在世界坐标系坐标为\([t^w_c, R^w_c]\)

cost function

cost function定义为相机系下的marker特征点投影的预测值和观测值做减法,如下:

\[ f([t^c_m, R^c_m]) = \pi((R^c_m * P^m + t^c_m)) - p^c \]

其中\(\pi()\)为投影函数。

jacobian

jacobian为costfunction对求解变量的导数,可以根据链式法则分解为两部分:

归一化相机坐标\(p=[u, v]^T\)相机坐标系点\(P^c = [x,y,z]^T\)

\[ {\frac{\partial f(p)}{\partial P^c}}^{2\times3} = \ \left(\begin{array}{lll}{\frac{\partial u}{\partial x}} & {\frac{\partial u}{\partial y}} & {\frac{\partial u}{\partial z}} \\ {\frac{\partial v}{\partial x}} & {\frac{\partial v}{\partial y}} & {\frac{\partial v}{\partial z}}\end{array}\right)=\left(\begin{array}{ccc}{\frac{1}{z}} & {0} & {\frac{-x}{z^{2}}} \\ {0} & {\frac{1}{z}} & {\frac{-y}{z^{2}}}\end{array}\right) \]

相机坐标系点\(P^c=[x,y,z]^T\)marker的平移和旋转

\[ \begin{aligned} \frac{\partial f\left(q_{m}^{c}\right)}{\partial q_{m}^{c}} &= \lim _{\delta \theta_{m}^{c} \rightarrow 0} \frac{ {R}_{m}^{c} exp(\left[\delta \theta_{m}^{c}\right]_{\times}) \left( p^m \right) - {R}_{m}^{c} \left( p^m \right) }{\delta \theta_{m}^{c}} \\ &\approx \lim_{\delta \theta_{m}^{c} \rightarrow 0} \frac{ {R}_{m}^{c} (I^{3 \times 3} + \left[ \delta \theta_{m}^{c} \right]_\times) \left( p^m \right) - {R}_{m}^{c} \left( p^m \right) }{\delta \theta_{m}^{c}} (泰勒展开)\\ &= \lim _{\delta \theta_{m}^{c} \rightarrow 0} \frac{ {R}_{m}^{c} \left[ \delta \theta_{m}^{c} \right]_\times \left( p^m \right) }{\delta \theta_{m}^{c}} \\ &= \lim _{\delta \theta_{m}^{c} \rightarrow 0} \frac{ -{R}_{m}^{c} \delta \theta_{m}^{c} \left[ \left( p^m \right)\right]_\times }{\delta \theta_{m}^{c}} (叉乘性质)\\ &= -{R}_{m}^{c} \left[ \left( p^m \right)\right]_\times \\ \end{aligned} \]

Demo



此demo读取一帧图像,首先进行PNP计算当前相机位姿,再通过BundleAdjustment优化。图中黄色为PNP的结果叠加上噪声后重投影,绿色为BundleAdjustment优化的结果重投影。

class MarkerProjectionCostfunction : public ceres::SizedCostFunction<ceres::DYNAMIC, 7, 7>
{
public:
    MarkerProjectionCostfunction(const std::vector<cv::Point2f>& obs,
        std::vector<Eigen::Vector3d, Eigen::aligned_allocator<Eigen::Vector3d>> corners, const Eigen::Matrix2d& information)
        : obs_(obs)
        , corners_(corners)
    {
        set_num_residuals(corners.size() * 2);
    }
    virtual ~MarkerProjectionCostfunction() {}
public:
    virtual bool Evaluate(double const* const* parameters, double* residuals, double** jacobians) const
    {
        //Eigen::Vector3d twb(parameters[0][0], parameters[0][1], parameters[0][2]);
        //Eigen::Quaterniond rwb(parameters[0][6], parameters[0][3], parameters[0][4], parameters[0][5]);
        Eigen::Vector3d tbm(parameters[1][0], parameters[1][1], parameters[1][2]);
        Eigen::Quaterniond rbm(parameters[1][6], parameters[1][3], parameters[1][4], parameters[1][5]);

        Eigen::Map<Eigen::VectorXd> residual(residuals, num_residuals());
        std::vector<Eigen::Matrix<double, 2, 3>> reduces;
        int size = corners_.size();
        for (int pos = 0; pos < size; pos++){
            auto& pm = corners_[pos];
            Eigen::Vector3d pb = rbm * pm + tbm;
            Eigen::Matrix<double, 2, 3> reduce;
            reduce << 1. / pb.z(), 0, -pb.x() / (pb.z() * pb.z()),
                0, 1. / pb.z(), -pb.y() / (pb.z() * pb.z());
            reduces.push_back(reduce);
            residual.segment(pos * 2, 2) = (pb / pb.z()).head<2>() - Eigen::Vector2d(obs_[pos].x, obs_[pos].y);
        }

        if (jacobians) {
            if (jacobians[0]) {
                Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> jacobian(jacobians[0], num_residuals(), 7);
                jacobian.setZero();
            }
            if (jacobians[1]) {
                Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> jacobian(jacobians[1], num_residuals(), 7);
                for (int pos = 0; pos < size; pos++) {
                    Eigen::Matrix<double, 3, 6> jaco;
                    jaco.leftCols<3>() = Eigen::Matrix3d::Identity();
                    jaco.rightCols<3>() = -1. * rbm.toRotationMatrix() *  Sophus::SO3d::hat(corners_[pos]);

                    jacobian.block<2, 7>(pos * 2, 0).leftCols<6>() = reduces[pos] * jaco;
                    jacobian.block<2, 7>(pos * 2, 0).rightCols<1>().setZero();
                }
            }
        }
        return true;
    }
public:
    std::vector<cv::Point2f> obs_;
    std::vector<Eigen::Vector3d, Eigen::aligned_allocator<Eigen::Vector3d>> corners_;
};

bool bundleAdjuster()
{
    // read images
    cv::Mat image = cv::imread("image.png", cv::IMREAD_GRAYSCALE);
    if (image.empty()) {
        return false;
    }
    // debug
    cv::Mat output = image.clone();
    cv::cvtColor(output, output, CV_GRAY2RGB);

    // detect
    auto detector = boost::make_shared<AprilTagDetector>("tag36h11", 0.);
    Markers markers;
    detector->find(image, markers);
    if (markers.empty()) {
        return false;
    }
    for (auto& marker : markers) {
        cv::line(output, marker.corners[0], marker.corners[1], cv::Scalar(0, 255, 0));
        cv::line(output, marker.corners[1], marker.corners[2], cv::Scalar(0, 0, 255));
        cv::line(output, marker.corners[2], marker.corners[3], cv::Scalar(0, 0, 255));
        cv::line(output, marker.corners[3], marker.corners[0], cv::Scalar(255, 0, 0));
        std::string label = cv::format("#%d", marker.id);
        cv::putText(output, label, marker.center, cv::FONT_HERSHEY_COMPLEX_SMALL, 1., cv::Scalar(255, 0, 0), 1);
    }
    // undistort
    double fx = 442.743649;
    double s = 0;
    double cx = 301.858989;
    double fy = 443.371918;
    double cy = 257.307880;

    double k1 = -0.307555;
    double k2 = 0.086253;
    double k3 = 0;
    double p1 = 0.000436;
    double p2 = -0.000926;

    cv::Mat intrinsic_matrix = cv::Mat::eye(3, 3, CV_64F);
    intrinsic_matrix.at<double>(0, 0)/* fx */ = fx;
    intrinsic_matrix.at<double>(0, 1)/* s  */ = s;
    intrinsic_matrix.at<double>(0, 2)/* cx */ = cx;
    intrinsic_matrix.at<double>(1, 1)/* fy */ = fy;
    intrinsic_matrix.at<double>(1, 2)/* cy */ = cy;

    cv::Mat distortion_coeffs = cv::Mat::zeros(5, 1, CV_64F);
    distortion_coeffs.at<double>(0, 0) = k1;
    distortion_coeffs.at<double>(1, 0) = k2;
    distortion_coeffs.at<double>(2, 0) = p1;
    distortion_coeffs.at<double>(3, 0) = p2;
    distortion_coeffs.at<double>(4, 0) = k3;

    std::vector<cv::Point2f> observation;
    cv::undistortPoints(markers[0].corners, observation, intrinsic_matrix, distortion_coeffs, cv::Mat(), intrinsic_matrix);


    for (auto &point : observation) {
        //point.x += (3. * (float)((std::rand() % 10000) - 5000) / 5000.);
        //point.y += (3. * (float)((std::rand() % 10000) - 5000) / 5000.);
        point.x = (point.x - cx) / fx;
        point.y = (point.y - cy) / fy;
    }

    {
        std::vector<cv::Point2f> reprojects;
        std::vector<cv::Point3f> objects;
        cv::convertPointsToHomogeneous(observation, objects);
        cv::projectPoints(objects, cv::Vec3f(0., 0., 0.), cv::Vec3f(0., 0., 0.), intrinsic_matrix, distortion_coeffs, reprojects);
    }
    // pnp
    Eigen::Matrix4d twb = Eigen::Matrix4d::Identity();
    Eigen::Matrix4d tbm = Eigen::Matrix4d::Identity();
    {
        std::vector<cv::Point3f> objects;
        objects.push_back(cv::Point3f(-0.08, -0.08, 0.));
        objects.push_back(cv::Point3f(0.08, -0.08, 0.));
        objects.push_back(cv::Point3f(0.08, 0.08, 0.));
        objects.push_back(cv::Point3f(-0.08, 0.08, 0.));
        cv::Mat r, t, rot;
        cv::Mat intrinsic = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
        if (!cv::solvePnP(objects, observation, intrinsic, cv::Mat(), r, t)) {
            return false;
        }
        cv::Rodrigues(r, rot);
        {
            cv::Mat noise = (cv::Mat_<double>(3, 1) <<
                (0.5 * (float)((std::rand() % 10000) - 5000) / 5000.),
                (0.5 * (float)((std::rand() % 10000) - 5000) / 5000.),
                (0.5 * (float)((std::rand() % 10000) - 5000) / 5000.));
            t += noise;
        }
        {
            cv::Mat noise = (cv::Mat_<double>(3, 1) <<
                (0.5 * (float)((std::rand() % 10000) - 5000) / 5000.),
                (0.5 * (float)((std::rand() % 10000) - 5000) / 5000.),
                (0.5 * (float)((std::rand() % 10000) - 5000) / 5000.));
            r += noise;
        }

        cv::Mat trans = cv::Mat::eye(4, 4, CV_32F);
        rot.copyTo(trans.rowRange(0, 3).colRange(0, 3));
        t.copyTo(trans.rowRange(0, 3).colRange(3, 4));
        cv::cv2eigen(trans, tbm);
        std::cout << "tbm" << std::endl << tbm << std::endl;

        std::vector<cv::Point2f> reprojects;
        cv::projectPoints(objects, r, t, intrinsic_matrix, distortion_coeffs, reprojects);
        std::cout << "reprojection: " << std::endl;
        for (int pos = 0; pos < reprojects.size(); pos ++) {
            auto& point = reprojects[pos];
            auto& origin = markers[0].corners[pos];
            std::printf("reprojection: (%8.5f,%8.5f)->(%8.5f,%8.5f) = (%8.5f,%8.5f).\n", origin.x, origin.y, point.x, point.y, origin.x-point.x, origin.y-point.y);
            cv::circle(output, point, 2, cv::Scalar(0, 255, 255), -1);
        }
    }
    // bundle adjuster
    {
        ceres::Problem problem;
        ceres::LossFunction* loss = new ceres::HuberLoss(1.0);

        std::vector<Eigen::Vector3d, Eigen::aligned_allocator<Eigen::Vector3d>> corners;
        corners.push_back(Eigen::Vector3d(-0.08, -0.08, 0.));
        corners.push_back(Eigen::Vector3d(0.08, -0.08, 0.));
        corners.push_back(Eigen::Vector3d(0.08, 0.08, 0.));
        corners.push_back(Eigen::Vector3d(-0.08, 0.08, 0.));

        Eigen::Matrix<double, 7, 1, Eigen::DontAlign> vtwb;
        vtwb.segment(0, 3) = twb.block<3, 1>(0, 3);
        vtwb.segment(3, 4) = Eigen::Quaterniond(twb.block<3, 3>(0, 0)).coeffs();
        problem.AddParameterBlock(vtwb.data(), 7, new SpecialEuclideanGroupPlus());
        problem.SetParameterBlockConstant(vtwb.data());

        Eigen::Matrix<double, 7, 1, Eigen::DontAlign> vtbm;
        vtbm.segment(0, 3) = tbm.block<3, 1>(0, 3);
        vtbm.segment(3, 4) = Eigen::Quaterniond(tbm.block<3, 3>(0, 0)).coeffs();
        problem.AddParameterBlock(vtbm.data(), 7, new SpecialEuclideanGroupPlus());

        auto factor = new MarkerProjectionCostfunction(observation, corners, Eigen::Matrix2d::Identity());
        problem.AddResidualBlock(factor, loss, vtwb.data(), vtbm.data());

        ceres::Solver::Options options;
        options.linear_solver_type = ceres::DENSE_SCHUR;
        options.trust_region_strategy_type = ceres::DOGLEG;
        options.max_num_iterations = 10000;
        options.max_solver_time_in_seconds = 1.;
        ceres::Solver::Summary summary;
        ceres::Solve(options, &problem, &summary);
        std::printf("%s\n", summary.FullReport().c_str());
        /**/

        Eigen::Matrix4d ttbm = Eigen::Matrix4d::Identity();
        ttbm.block<3, 3>(0, 0) = Eigen::Quaterniond(vtbm.segment(3, 4).data()).normalized().toRotationMatrix();
        ttbm.block<3, 1>(0, 3) = vtbm.segment(0, 3);
        std::cout << "ttbm" << std::endl << ttbm << std::endl;

        {
            std::vector<cv::Point3f> objects;
            for (auto &corner : corners) {
                Eigen::Vector3d object = ttbm.block<3, 3>(0, 0) * corner + ttbm.block<3, 1>(0, 3);
                objects.push_back(cv::Point3f(object.x(), object.y(), object.z()));
            }

            std::vector<cv::Point2f> reprojects;
            cv::projectPoints(objects, cv::Vec3f(0., 0., 0.), cv::Vec3f(0., 0., 0.), intrinsic_matrix, distortion_coeffs, reprojects);
            std::cout << "bundle adjuster reprojection: " << std::endl;
            for (int pos = 0; pos < reprojects.size(); pos++) {
                auto& point = reprojects[pos];
                auto& origin = markers[0].corners[pos];
                std::printf("reprojection: (%8.5f,%8.5f)->(%8.5f,%8.5f) = (%8.5f,%8.5f).\n", origin.x, origin.y, point.x, point.y, origin.x - point.x, origin.y - point.y);
                cv::circle(output, point, 1, cv::Scalar(0, 255, 0), -1);
            }
        }
    }

    cv::imshow("view", output); cv::waitKey(0);
    return true;
}

Reference

[1] <视觉slam十四讲> [2] https://blog.csdn.net/heyijia0327/article/details/60143160