VINS-Mono+Fusion源码解析系列(二):视觉前端的图像处理

本文详细解析VINS-Mono视觉前端的图像处理,包括图片回调函数`img_callback`的图像时间戳检查、频率控制、光流追踪、去畸变以及特征点处理。重点介绍了对极约束剔除outlier、特征点均匀化、去畸变计算特征点速度的过程,深入探讨了逐次逼近法去除相机畸变的原理和实现。
摘要由CSDN通过智能技术生成

1. 图片的回调函数(img_callback)

(1)大致流程

  • 检查时间戳是否异常:图像时间间隔超过1s,或者时间差错乱:当前帧时间戳 < 上一帧时间戳

  • 检查频率:控制向后端发送频率,以及运行时频率隐性异常处理

  • 读取图片并进行相应处理(readImage):光流追踪 + 去畸变 + 均匀化等

  • 更新特征点ID(updateID)

  • 向后端发送特征点信息:特征点的id + 去畸变后的像素坐标 + 特征点的速度

(2) 整体代码

// /feature_tracker/src/feature_tracker_node.cpp
// 图片的回调函数    传入的参数为图像的消息类型
void img_callback(const sensor_msgs::ImageConstPtr &img_msg)
{
    // Step 1: 检查时间戳
    if(first_image_flag) // 对第一帧图像的基本操作(时间戳)
    {
        first_image_flag = false;
        first_image_time = img_msg->header.stamp.toSec();
        last_image_time = img_msg->header.stamp.toSec();
        return;
    }
    
    // 检查时间戳是否正常,这里认为超过一秒或者错乱就异常
    // 图像时间差太多光流追踪就会失败,这里没有描述子匹配,因此对时间戳要求就高
    if (img_msg->header.stamp.toSec() - last_image_time > 1.0 || 
        img_msg->header.stamp.toSec() < last_image_time)
    {
        // 一些常规的reset操作
        ROS_WARN("image discontinue! reset the feature tracker!");
        first_image_flag = true; 
        last_image_time = 0;
        pub_count = 1;
        std_msgs::Bool restart_flag;  // 复位标志
        restart_flag.data = true;
        pub_restart.publish(restart_flag);  // 告诉其他模块要重启了
        return;
    }
    
    last_image_time = img_msg->header.stamp.toSec();
/*
  Step 2: 检查频率
  (a) 控制向后端发送的频率
  因为如果送给后端太频繁,后端运算时间会跟不上,可能会崩掉
  pub_count表示发送帧数  img_msg->header.stamp.toSec()-first_image_time表示发送的时间间隔    
  发送帧数 / 时间间隔 = 频率  下面的判断条件是保证后端发送的频率不超过所设的FREQ
*/
    if (round(1.0 * pub_count / (img_msg->header.stamp.toSec() - first_image_time)) <= FREQ)  
    {
        // 满足频率要求才将发布标志位置为true,不满足频率要求的帧就不发布  
        PUB_THIS_FRAME = true;  

        // (b) 运行时的频率隐性异常  需要及时清空,下面做进一步解释
        if (abs(1.0 * pub_count / (img_msg->header.stamp.toSec() - first_image_time) - FREQ) < 0.01 * FREQ)
        {
            // 当前时间赋给起始计数时间
            first_image_time = img_msg->header.stamp.toSec();  
            pub_count = 0;
        }
    }
    else
        PUB_THIS_FRAME = false;

    cv_bridge::CvImageConstPtr ptr;
    // Step 3: 通过cv_bridge把ros message转成cv::Mat
    if (img_msg->encoding == "8UC1")
    {
        sensor_msgs::Image img;
        img.header = img_msg->header;
        img.height = img_msg->height;
        img.width = img_msg->width;
        img.is_bigendian = img_msg->is_bigendian;
        img.step = img_msg->step;
        img.data = img_msg->data;
        img.encoding = "mono8";
        ptr = cv_bridge::toCvCopy(img, sensor_msgs::image_encodings::MONO8);
    }
    else
        ptr = cv_bridge::toCvCopy(img_msg, sensor_msgs::image_encodings::MONO8);

    TicToc t_r;
    // Step 5: readImage读取图片,进行光流追踪与去畸变操作
    // 单目情况下,NUM_OF_CAM=1
    for (int i = 0; i < NUM_OF_CAM; i++)
    {
        ROS_DEBUG("processing camera %d", i);
        if (i != 1 || !STEREO_TRACK)
            trackerData[i].readImage(ptr->image.rowRange(ROW * i, ROW * (i + 1)), 
                                     img_msg->header.stamp.toSec());
        else
        {
            if (EQUALIZE)
            {
                cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
                clahe->apply(ptr->image.rowRange(ROW * i, ROW * (i + 1)), 
                             trackerData[i].cur_img);
            }
            else
                trackerData[i].cur_img = ptr->image.rowRange(ROW * i, ROW * (i + 1));
        }
    }
    
    for (unsigned int i = 0;; i++)
    {
        bool completed = false;
        for (int j = 0; j < NUM_OF_CAM; j++)
            if (j != 1 || !STEREO_TRACK)
                // updateID中是对新提取到特征点的id进行更新
                completed |= trackerData[j].updateID(i);    
        if (!completed)
            break;
    }
    
   // 若需要向后端发送数据,则要进行如下操作
   if (PUB_THIS_FRAME)
   {
        pub_count++;    // 计数器更新
        // 定义发送的数据类型
        sensor_msgs::PointCloudPtr feature_points(new sensor_msgs::PointCloud);  
        // 定义要发送的信息
        sensor_msgs::ChannelFloat32 id_of_point;  // 特征点的id
        sensor_msgs::ChannelFloat32 u_of_point;   // 特征点的x像素坐标
        sensor_msgs::ChannelFloat32 v_of_point;   // 特征点的y像素坐标
        sensor_msgs::ChannelFloat32 velocity_x_of_point;  // 特征点的x方向速度
        sensor_msgs::ChannelFloat32 velocity_y_of_point;  // 特征点的y方向速度

        feature_points->header = img_msg->header;
        feature_points->header.frame_id = "world";

        vector<set<int>> hash_ids(NUM_OF_CAM);
       for (int i = 0; i < NUM_OF_CAM; i++)  // 单目相机情况下,NUM_OF_CAM=1
       {
            // 向后端发送的内容
            auto &un_pts = trackerData[i].cur_un_pts;   // 每一帧去畸变后的归一化坐标
            auto &cur_pts = trackerData[i].cur_pts;     // 像素坐标
            auto &ids = trackerData[i].ids;             // id
            auto &pts_velocity = trackerData[i].pts_velocity;   // 归一化坐标下的速度
            for (unsigned int j = 0; j < ids.size(); j++)
            {
                // 只发布追踪数大于1的,因为等于1没法构成重投影约束,也没法三角化
                if (trackerData[i].track_cnt[j] > 1)
                {
                    int p_id = ids[j];
                    hash_ids[i].insert(p_id);   // 这个并没有用到
                    geometry_msgs::Point32 p;   // 去畸变后的归一化坐标
                    p.x = un_pts[j].x;
                    p.y = un_pts[j].y;
                    p.z = 1;
                    // 利用这个ros消息的格式进行信息存储
                    feature_points->points.push_back(p);  // 去畸变后的归一化坐标
                    id_of_point.values.push_back(p_id * NUM_OF_CAM + i);  // 特征点的id
                    // 像素坐标
                    u_of_point.values.push_back(cur_pts[j].x);
                    v_of_point.values.push_back(cur_pts[j].y);
                    // 特征点的速度
                    velocity_x_of_point.values.push_back(pts_velocity[j].x);
                    velocity_y_of_point.values.push_back(pts_velocity[j].y);
                }
            }
       }
       
        // 最终发送给后端的是:特征点的id,去畸变后的像素坐标,以及特征点的速度
        // 去畸变后特征点的归一化坐标个人认为是没有发送给后端的,要确认一下
        // 若真的没有发送给后端,那for循环中为什么要有归一化坐标    疑问???
        feature_points->channels.push_back(id_of_point);
        feature_points->channels.push_back(u_of_point);
        feature_points->channels.push_back(v_of_point);
        feature_points->channels.push_back(velocity_x_of_point);
        feature_points->channels.push_back(velocity_y_of_point);
        // skip the first image; since no optical speed on frist image
        if (!init_pub)
        {
            init_pub = 1;
        }
        else
            pub_img.publish(feature_points);    // 调用接口,将feature_points发送给后端
   }
}

(4) 运行时的频率隐性异常

​ 频率计算公式如下:
f r e q = f r a m e _ c o u n t Δ t freq = \frac{frame\_count}{\Delta t} freq=Δtframe_count
​ 随着程序的运行,帧数frame_count和时间差 Δ t \Delta t Δt都会增加。假设预设的频率阈值FREQ=10Hz,当frame_count=900, Δ t = 100 \Delta t=100 Δt=100时, f r e q = 9 H z < 10 H z freq=9Hz < 10Hz freq=9Hz<10Hz,满足要求。之前1s内收到30帧图像,若此时出现异常,1s内收到50帧图像,则频率 f r e q = 950 / 101 < 10 H z freq = 950/101 < 10Hz freq=950/101<10Hz,显示仍然满足频率阈值要求,但实际上此时已经出现了异常。

​ 这里的解决办法是:当实际频率freq和预设频率阈值FREQ很接近时,将frame_count和 Δ t \Delta t Δt都清空掉。此时若出现上述异常,则频率计算为: f r e q = 50 H z > 10 H z freq = 50Hz > 10Hz freq=50Hz>10Hz,不满足频率阈值要求。

(5)对新提取到的特征点被追踪的次数进行更新updateID

/**
 * @brief 
 * 
 * @param[in] i 
 * @return true 
 * @return false 
 *  给新的特征点赋上id,越界就返回false
 */
bool FeatureTracker::updateID(unsigned int i)
{
    if (i < ids.size())
    {
        if (ids[i] == -1)
            // n_id初始值为-1, 表示当前特征点为新特征点, 因此特征点id从0开始,
            // 之后每来一个特征点, 都是根据n_id来更新其id
            ids[i] = n_id++;
        return true;
    }
    else
        return false;
}

2. readImage

(1)readImage大致流程

  • 图像均衡化处理:防止图像亮暗影响光流追踪
  • 光流追踪:调用opencv接口对上一帧的特征点进行光流追踪
  • 根据光流追踪的状态位status和追踪点的坐标是否在图像范围内(inBorder)对特征点进行剔除
  • 若向后端发布特征点,则需要依次执行如下操作:
    • 通过对极约束来剔除outlier
    • 通过在提取到的特征点周围设置mask对其进行均匀化处理
    • 通过opencv提取剩余特征点
  • undistortedPoints:通过逐次逼近法对特征点进行去畸变,并计算特征点速度

(3)readImage代码实现

// /feature_tracker/src/feature_tracker.cpp
/**
 * @brief 
 * 
 * @param[in] _img 输入图像
 * @param[in] _cur_time 图像的时间戳
 * 1、图像均衡化预处理
 * 2、光流追踪
 * 3、提取新的特征点(如果发布)
 * 4、所有特征点去畸变,计算速度
 * 5、外点剔除情况:光流追踪后剔除一次外点,若要向后端发送,则根据对极约束再剔除一次外点
 */
void FeatureTracker::readImage(const cv::Mat &_img, double _cur_time)
{
    cv::Mat img;
    TicToc t_r;
    cur_time = _cur_time;

    // Step 1: 图像均衡化处理
    if (EQUALIZE)
    {
        // 图像太暗或者太亮,提特征点比较难,所以均衡化一下
        cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(3.0, cv::Size(8, 8));
        TicToc t_c;
        clahe->apply(_img, img);  // 通过均衡化得到img
        ROS_DEBUG("CLAHE costs: %fms", t_c.toc());
    }
    else
        img = _img;  // 若不进行均衡化,则直接赋给img

    // 获取相邻两帧图像及特征点, 这里forw表示当前, cur表示上一帧
    if (forw_img.empty())   // 第一次输入图像,prev_img为空
    {
        // 第一次输入图像时,均为当前帧
        prev_img = cur_img = forw_img = img;  
    }
    else
    {
        // 后面在输入图像时,对forw_img进行更新,所以forw_img表示当前帧
        // 而由于没有对cur_img进行更新,故cur_img表示上一帧
        forw_img = img;
    }

    // forw_pts中存放当前帧的特征点信息
    // 当前帧刚送进来时,还没有对其进行特征提取,所以存储当前帧特征点的forw_pts要清空
    // 后面提取到的当前帧特征点就存放在forw_pts中
    forw_pts.clear();

    // cur_pts中存放上一帧的特征点信息
    if (cur_pts.size() > 0) // 上一帧有特征点,就可以进行光流追踪了
    {
        TicToc t_o;
        vector<uchar> status;
        vector<float> err;
        // Step 2 调用opencv函数(带图像金字塔)进行光流追踪
        // cur_img: 上一帧图像  forw_img: 当前帧图像
        // cur_pts: 上一帧图像上的特征点  
        // forw_pts: 上一帧图像上的特征点通过光流追踪在当前图像上的特征点
        // status: 上一帧图像上的特征点是否被成功追踪到的状态位  
        cv::calcOpticalFlowPyrLK(cur_img, forw_img, cur_pts, forw_pts, 
                                 status, err, cv::Size(21, 21), 3);

        // Step 3 根据状态位status和追踪点的坐标是否在图像范围内进行剔除
        for (int i = 0; i < int(forw_pts.size()); i++)
            if (status[i] && !inBorder(forw_pts[i]))    
                status[i] = 0;
   
/*
   根据状态位status,使用双指针去除outlier进行瘦身
   status中保存的是通过光流追踪得到的每个点的状态,追踪到的status为1,每追踪到的status为0
   reduceVector就是根据status的值,将status为0的点去掉,保留下来的是成功追踪到的特征点
*/
        reduceVector(prev_pts, status); // 没用到
        reduceVector(cur_pts, status);
        reduceVector(forw_pts, status);
        reduceVector(ids, status);  // 特征点的id
        reduceVector(cur_un_pts, status);   // 去畸变后的坐标
        reduceVector(track_cnt, status);    // 保留下来的特征点的追踪次数
        ROS_DEBUG("temporal optical flow costs: %fms", t_o.toc());
    }
    // 被追踪到的是上一帧就存在的,因此追踪数+1  
    // 这里n取的是引用,因此n自增后track_cnt中对应值也自增
    for (auto &n : track_cnt)
        n++;

    // 当向后端发布时(PUB_THIS_FRAME == true),需要执行以下步骤:
    // 1. 通过对极约束来剔除outlier
    // 2. 对提取到的特征点进行均匀化处理
    // 3. 为防止提取到的特征点数目不够,均匀化处理后再提取剩余数目的特征点
    if (PUB_THIS_FRAME) 
    {
        // Step 4 通过对极约束来剔除outlier
        rejectWithF();
        ROS_DEBUG("set mask begins");
        TicToc t_m;
        // Step 5 进行特征点均匀化
        setMask();  
        ROS_DEBUG("set mask costs %fms", t_m.toc());

/*
  Step 6: 提取剩余特征点
  前面经过光流追踪、对极约束以及对特征点进行均匀化提取,可能会使得提取到的特征点不够
  下面提取剩余的特征点  MAX_CNT表示最多提取的特征点数,forw_pts.size()表示当前提取到的特征点数,二者之差表示还需要提取的特征点数
*/
        ROS_DEBUG("detect feature begins");
        TicToc t_t;
        // 要提取的剩余特征点数目
        int n_max_cnt = MAX_CNT - static_cast<int>(forw_pts.size());
        if (n_max_cnt > 0)
        {
            if(mask.empty())
                cout << "mask is empty " << endl;
            if (mask.type() != CV_8UC1)
                cout << "mask type wrong " << endl;
            if (mask.size() != forw_img.size())
                cout << "wrong size " << endl;
/*
   通过opencv的cv::goodFeaturesToTrack()提取剩余特征点时,提取到的特征点可能比较集中,但没关系, 因为当来下一帧时,当前提取到的集中特征点还会进行上述均匀化处理
   另外,cv::goodFeaturesToTrack()提取剩余特征点时,由于设置了mask,因此不会在之前已均匀化的特征点周围来提取
*/
            cv::goodFeaturesToTrack(forw_img, n_pts, MAX_CNT - forw_pts.size(), 0.01, MIN_DIST, mask);
        }
        else
            n_pts.clear();
        ROS_DEBUG("detect feature costs: %fms", t_t.toc());

        ROS_DEBUG("add feature begins");
        TicToc t_a;
        // 将新提取的特征点的相关信息(坐标、id、被追踪次数)添加到相应容器中
        addPoints();
        ROS_DEBUG("selectFeature costs: %fms", t_a.toc());
    }
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值