二维ICP配准算法-C++ 直接法实现

1. 原理

1.1 2D的ICP算法

2D的ICP算法的原理与3D的一致,区别在于其SO(2)的自由度只有1。因此并不需要借用SVD来进行求解,只需要直接求导即可。

1.2 目标函数

min ⁡ D ( R , T ) = 1 n ∑ i = 1 n ∣ ∣ P i − ( R ⋅ Q i + T ) ∣ ∣ 2 \min D(R,T) = \frac{1}{n} \sum_{i=1}^n||P_i-(R \cdot Q_i+T)||^2 minD(R,T)=n1i=1n∣∣Pi(RQi+T)2

1.3 简化目标函数

上述目标函数包含两个变量比较难以求解
可以通过对点集进行归一化,不考虑T
P m e a n = 1 n ⋅ ∑ i = 1 n P ( i ) , Q m e a n = 1 n ⋅ ∑ i = 1 n Q ( i ) P i = P i − P m e a n Q i = Q i − Q m e a n P_{mean} = \frac{1}{n} \cdot \sum_{i=1}^{n} P_{(i)}, Q_{mean} = \frac{1}{n} \cdot \sum_{i=1}^{n} Q_{(i)} \\ P_i = Pi - P_{mean} \quad Q_i = Q_i - Q_{mean} Pmean=n1i=1nP(i),Qmean=n1i=1nQ(i)Pi=PiPmeanQi=QiQmean
这时目标函数可以简化为
min ⁡ D ( R , T ) = 1 n ∑ i = 1 n ∣ ∣ P i − R ⋅ Q i ∣ ∣ 2 \min D(R,T) = \frac{1}{n} \sum_{i=1}^n||P_i-R \cdot Q_i||^2 minD(R,T)=n1i=1n∣∣PiRQi2
对目标函数进行展开可得
min ⁡ D ( R , T ) = 1 n ∑ i = 1 n ( P i 2 − 2 P i t R Q i + Q i 2 ) \min D(R,T) = \frac{1}{n} \sum_{i=1}^n (P_i^2 - 2P_i^tRQ_i + Q_i^2) minD(R,T)=n1i=1n(Pi22PitRQi+Qi2)
由于第一项和第三项为常量,则目标函数可以转化为
max ⁡ G ( R , T ) = 1 n ∑ i = 1 n P i t R Q i \max G(R,T) = \frac{1}{n} \sum_{i=1}^n P_i^tRQ_i maxG(R,T)=n1i=1nPitRQi

1.4 求解SO(2)

因SO(2)有如下定义
R = [ c o s ( θ ) − s i n ( θ ) s i n ( θ ) c o s ( θ ) ] R=\left[ \begin{matrix} cos(\theta) & -sin(\theta)\\ sin(\theta) & cos(\theta) \end{matrix} \right] R=[cos(θ)sin(θ)sin(θ)cos(θ)]
则目标函数G可以表达为如下形式
G ( R , T ) = 1 n ∑ i = 1 n [ p i x , p i y ] [ c o s ( θ ) − s i n ( θ ) s i n ( θ ) c o s ( θ ) ] [ q i x q i y ] ] = 1 n ∑ i = 1 n c o s ( θ ) ⋅ ( p i x q i x + p i y q i y ) + s i n ( θ ) ⋅ ( p i y q i x − p i x q i y ) G(R,T) = \frac{1}{n} \sum_{i=1}^n [p_i^x,p_i^y] \left[ \begin{matrix} cos(\theta) & -sin(\theta)\\ sin(\theta) & cos(\theta) \end{matrix} \right] \left[ \begin{matrix} q_i^x \\ q_i^y] \end{matrix} \right] = \\ \frac{1}{n} \sum_{i=1}^n cos(\theta) \cdot (p_i^xq_i^x+p_i^yq_i^y)+sin(\theta)\cdot(p_i^yq_i^x-p_i^xq_i^y) G(R,T)=n1i=1n[pix,piy][cos(θ)sin(θ)sin(θ)cos(θ)][qixqiy]]=n1i=1ncos(θ)(pixqix+piyqiy)+sin(θ)(piyqixpixqiy)
θ \theta θ求导,令导数等于0
0 = 1 n ∑ i = 1 n − s i n ( θ ) ⋅ ( p i x q i x + p i y q i y ) + c o s ( θ ) ⋅ ( p i y q i x − p i x q i y ) 0=\frac{1}{n} \sum_{i=1}^n -sin(\theta) \cdot (p_i^xq_i^x+p_i^yq_i^y)+cos(\theta)\cdot(p_i^yq_i^x-p_i^xq_i^y) 0=n1i=1nsin(θ)(pixqix+piyqiy)+cos(θ)(piyqixpixqiy)
移项得
t a n ( θ ) = 1 n ∑ i = 1 n p i y q i x − p i x q i y 1 n ∑ i = 1 n p i x q i x + p i y q i y = a   θ = a t a n ( a ) tan(\theta) = \frac{\frac{1}{n} \sum_{i=1}^np_i^yq_i^x-p_i^xq_i^y}{\frac{1}{n} \sum_{i=1}^np_i^xq_i^x+p_i^yq_i^y} = a \\\ \theta = atan(a) tan(θ)=n1i=1npixqix+piyqiyn1i=1npiyqixpixqiy=a θ=atan(a)
求出 θ \theta θ,根据SO(2)的形式就可求出R

1.5求解T

T = P m e a n − R ⋅ Q m e a n T=P_{mean}-R \cdot Q_{mean} T=PmeanRQmean

2. C++实现

// ICP2D.h
#ifndef ICP2D_H_
#define ICP2D_H_

#include <opencv2/opencv.hpp>

class ICP2D
{
public:
    ICP2D() = default;
    ~ICP2D() = default;
    
    // ICP配准
    double registration(const std::vector<cv::Point2f> &source,
                        const std::vector<cv::Point2f> &target,
                        const int &iter_num, const double &eps);

private:
    // 暴力搜索最近邻
    int bfnn_point_(const std::vector<cv::Point2f> &points,
                        const cv::Point2f &point);
    
    // 点集去中心化
    cv::Point2f normalize_(std::vector<cv::Point2f> &points);
};

#endif
// ICP2D.cc
#include "ICP2D.h"

double ICP2D::registration(const std::vector<cv::Point2f> &source,
                           const std::vector<cv::Point2f> &target,
                           const int &iter_num, const double &eps)
{
    auto source_ = source;
    auto target_ = target;

    auto source_mean = normalize_(source_);

    cv::Matx22f R = cv::Matx22f::eye();
    double res_angle = 0.f;
    cv::Point2f T(0.f, 0.f);

    for (int i = 0; i < iter_num; ++i)
    {
        auto target_mean = normalize_(target_);

        double t_nume = 0.f, t_deno = 0.f;

        for (int j = 0; j < target_.size(); ++j)
        {
            int closet_index = bfnn_point_(source_, target_[j]);

            t_nume += source_[closet_index].y * target_[j].x - source_[closet_index].x * target_[j].y;
            t_deno += source_[closet_index].x * target_[j].x + source_[closet_index].y * target_[j].y;
        }

        double theta = std::atan2(t_nume, t_deno);
        res_angle += theta / CV_PI * 180.f;

        cv::Matx22f R_iter;
        R_iter << std::cos(theta), -std::sin(theta),
            std::sin(theta), std::cos(theta);

        cv::Point2f T_iter = source_mean - R_iter * target_mean;

        for (int j = 0; j < target_.size(); ++j)
        {
            target_[j] = R_iter * target_[j] + T_iter;
        }

        R = R_iter * R;
        T = R_iter * T + T_iter;

        std::cout << "R iter: " << theta / CV_PI * 180.f << std::endl;
        std::cout << "T iter: " << T_iter << std::endl;

        if (std::abs(theta / CV_PI * 180.f) < eps)
        {
            break;
        }
    }

    return res_angle;
}

int ICP2D::bfnn_point_(const std::vector<cv::Point2f> &points,
                                   const cv::Point2f &point)
{
    return std::min_element(points.begin(), points.end(),
                            [&point](const cv::Point2f &p1, const cv::Point2f &p2)
                            { return cv::norm(p1 - point) < cv::norm(p2 - point); }) -
           points.begin();
}

cv::Point2f ICP2D::normalize_(std::vector<cv::Point2f> &points)
{
    cv::Point2f mean(0.f, 0.f);
    for (const auto &p : points)
    {
        mean += p;
    }

    mean.x /= points.size();
    mean.y /= points.size();

    for (auto &p : points)
    {
        p -= mean;
    }

    return mean;
}

3. 性能测试

// test.cc
#include <opencv2/opencv.hpp>
#include "ICP2D.h"

// 运行速度测试
template <typename FuncT>
void evaluate_and_call(FuncT &&func, int run_num)
{
    double total_time = 0.f;
    for (int i = 0; i < run_num; ++i)
    {
        auto t1 = std::chrono::high_resolution_clock::now();
        func();
        auto t2 = std::chrono::high_resolution_clock::now();
        total_time += std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1).count() * 1000;
        ;
    }

    std::cout << "run cost: " << total_time / run_num << " ms" << std::endl;
}

void test_icp()
{
    ICP2D icp;
    std::vector<cv::Point2f> points = {{1, 1}, {2, 2}, {3, 3}};
    cv::Matx22f R;
    double angle = 30.f;
    R << std::cos(angle / 180.f * CV_PI), -std::sin(angle / 180.f * CV_PI),
        std::sin(angle / 180.f * CV_PI), std::cos(angle / 180.f * CV_PI);
    cv::Point2f T(10, 20);

    std::vector<cv::Point2f> points_RT;
    for (int i = 0; i < points.size(); ++i)
    {
        points_RT.push_back(R * points[i] + T);
    }

    double angle_final = icp.registration(points, points_RT, 100, 0.001);
    std::cout << "angle final: " << angle_final << std::endl;
}

int main()
{
    evaluate_and_call(test_icp, 10);

    return 0;
}

/* Terminal print
R iter: -30
T iter: [-18.6603, -12.3205]
R iter: 1.36604e-05
T iter: [20.6602, 14.3205]
angle final: -30
run cost: 1.1759 ms
*/

  • 笔者在项目中只用其求解角度,所以registration函数的返回值只有角度,且收敛条件设置为 Δ \Delta ΔR足够小
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值