slam一些面试问题(一)

视觉SLAM的框架以及各个模块的作用

1.传感器信息读取 在视觉SLAM中主要为相机图像信息的读取和预处理。如果在机器人中,还可能有码盘,惯性传感器等信息的读取和同步。
2.视觉里程计 (visual odometry,VO) 视觉里程计的任务是估算相邻图像间相机运动,以及局部地图的样子。vo又称为前端。
3. 后端优化(optimization)。后端接受不同时刻视觉里程计测量的相机位姿,以及回环检测的信息,对他们进行优化,得到全局一致的轨迹和地图。由于在VO之后,又称为后端。
4. 回环检测(loop closing)。 回环检测判断机器人是否到达过去先前的位置,如果检测到回环,它会把信息提供给后端进行检测。
5. 建图(mapping)。它根据估计的轨迹,建立与任务要求对应的地图。

slam前端后端怎么连接在一起,数据流怎么传递的
前端实时的做帧间匹配,然后帧间匹配会得到一个位姿,这个位姿传给后端再做一次优化,后端优化过的位姿才是我们最后得到的位姿,然后用最后这个比较准确的位姿把点云投影到地图上。这个就是我们最后形成的点云地图。数据流通过ROS通信机制进行传输,前端publish里程计,后端subscriber。

当前帧和局部地图的数据关联是如何做的?
仅使用帧与帧之间运动估计运动,一般精度还不够。一般会维护局部地图来提高跟踪精度和鲁棒性。为了权衡精度和实时性,为了保证实时性,只能使用当前帧跟踪一部分帧,一般使用固定滑动窗口或ORB-SLAM2中使用的共视帧窗口,它们与当前帧共视点相对其他帧多很多。前端主要用于跟踪(得到当前帧位姿),据此选择关键帧交给后端,后端再进行三角化新的路标点、删错冗余和错误的路标点和帧、优化各帧位姿和路标点位姿(一般在世界坐标系表示)。

SLAM的回环检测方法
激光: Scan Context
首先使用Scan Context进行回环帧检测,确定历史帧中的回环帧后,将回环帧与当前点云帧进行点云配准,获取回环精确位姿。也就是说回环检测的本质是利用当前点云和历史点云做相似度检测,如果历史中有对应的点云相似度较高,我们就把这个历史帧确定为回环帧,用当前点云和历史帧去做配准得到精确位姿;由于累计误差的存在,激光里程计连续下来求得本时刻与那个历史时刻的位姿存在一定的偏差,而回环检测没有累计误差;因此构建了一条G2O的回环边。

  • 传统的领域距离搜索+ICP匹配

  • 基于scan context系列的粗匹配+ICP精准匹配的回环检测

  • 基于scan context的回环检测

  • 基于Intensity scan context+ICP的回环检测
    slam一些面试问题(一)_第1张图片

  • 视觉的话,回环检测目前多采用词袋模型(Bag-of-Word)

重载、隐藏、重写(覆盖)三者的区别?

  • 重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型(统一为void,否则报错)。
    运算符重载:使用户自定义的数据以更简洁的方式工作
  • 隐藏(重定义):是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
  • 重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

重载的参数不同,函数体不同(同一个类中,同一作用域);
隐藏的参数可以不同,函数体不同;
重写或者覆盖仅仅函数体不同(花括号内)不同的范围(分别位于派生类与基类)。

智能指针

  • 动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。
  • std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是 std::unique_ptr 不可以拷贝或赋值给其他对象,其拥有的堆内存仅自己独占,std::unique_ptr 对象销毁时会释放其持有的堆内存。
  • std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。
  • std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。

虚函数与纯虚函数

  • 虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。
  • 虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。
  • 虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。
  • 虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。
  • 虚函数的定义形式:virtual {method body}
    纯虚函数的定义形式:virtual { } = 0;
  • 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样

能用纯虚函数的类声明对象么

  • 普通类具有成员函数,构造类的对象时,会对成员变量和成员函数分配内存。含有纯虚函数的类,定义了成员函数的地址是空,无法分配内存,该成员函数对类是没有意义的,失去了普通类的数据和方法绑定于同一对象中的意义,因此无法构造对象,只能通过派生类继承这些成员函数并实现,才能构造派生类对象。此时抽象类就起到了定义接口的作用。

KD树工作原理:

Kd-Tree是从BST(Binary search tree)发展而来,是一种高维索引树形数据结构,常用于大规模高维数据密集的查找比对的使用场景中,主要是最近邻查找(Nearest Neighbor)以及近似最近邻查找(Approximate Nearest Neighbor)

  • 一维:
    在现有的数据中选定一个数据作为根节点的存储数值。(要求尽可能保证左右子树的集合的数量相等,优化查找速度)
    将其它数据按照左小右大的规则往深层递归,直到叶节点,然后开辟新的叶节点,并存储当前值。
    新的数据按照上一条进行存储。
  • 多维
    Kd-tree的构造是在BST的基础上升级:
    选定数据X1的Y1维数值a1做为根节点比对值,对所有的数值在Y1维进行一层BST排列。相当于根据Y1维数值a1对数据集进行分割。
    选定数据X2的Y2维数值a2做为根节点比对值,对所有的数值在Y2维进行一层BST排列。也即将数据集在Y2维上又做了一层BST。
    slam一些面试问题(一)_第2张图片

原文链接:https://blog.csdn.net/u012423865/article/details/77488920

map与unordered_map

  • map:
    map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。查找的复杂度基本是Log(N)
  • unordered_map:
    unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。

多线程

  • 线程休眠(sleep方法)
    线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
public static native void sleep(long millis) throws InterruptedException

线程休眠会交出CPU(变回阻塞状态),让CPU去执行其他的任务,睡眠结束后返回就绪状态。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。休眠时间使用毫秒作为单位。

  • 线程让步(yield()方法)
    暂停当前正在执行的线程对象,并执行其他线程。意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

  • join()方法
    等待该线程终止。意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run方法先执行完毕之后在开始执行主线程。
    join()方法只是对Object提供的wait()做的的一层包装而已。
    join()方法变回阻塞状态,结束后返回就绪状态,会释放锁

  • 多线程中有三种方式可以停止线程。
    设置标记位,可以是线程正常退出。
    使用stop方法强制使线程退出,但是该方法不太安全所以已经被废弃了。
    使用Thread类中的一个interrupt() 可以中断线程。

  • 对于优先级设置的内容可以通过Thread类的几个常量来决定
    高优先级:public final static int MAX_PRIORITY = 10;
    中等优先级:public final static int NORM_PRIORITY = 5;
    低优先级:public final static int MIN_PRIORITY = 1;
    主方法只是一个中等优先级,为5

线程间的同步与互斥:

  • 线程间的同步
    线程的同步是指在一些确定点上需要线程之间相互合作,协同工作。在访问同一个临界资源(互斥资源时,两个线程间必须有一个先后顺序,因为临界资源一次只能供一个线程使用。

举例:假如程序中有一个静态变量,static int a;线程1负责往里写入数据,线程2需要读取其中的数据,那么线程2在读数据之前必须是线程1写入了数据,如果不是,那么线程2必须停下来等待线程1的操作结束。这就是线程之间在某些地方上的合作关系,协同工作嘛!

  • 线程间的互斥
    线程的互斥是指同一进程中的多个线程之间因争用临界资源而互斥的执行,即一次只能有一个线程访问临界资源。

举例:还是假如程序中有一个静态变量,static int b;线程1想要往里写入数据,线程2也想要往里写入数据,那么此时静态变量b就是一个临界资源(互斥资源),即一次只能被一个线程访问。想一想,如果线程1和线程2同时往b中写入数据,那怎么能行,计算机是不允许的!所以,要么是线程1占用b,此时线程2要等待;要么是线程2占用b,此时线程1等待。这就是所谓的线程间的互斥,这里可以通过加锁的方式来实现。

  • 多线程中实现同步互斥的两种方法
    C++11中实现多线程间同步与互斥有两种方法:信号量机制和锁机制,其中锁机制最为常用。

join()和detach()

thread first(GetSumT,largeArrays.begin(),largeArrays.begin()+20000000,std::ref(result1)); 
first.join();
  • 这意味着主线程和子线程之间是同步的关系,即主线程要等待子线程执行完毕才会继续向下执行,join()是一个阻塞函数。*
  • first.detach(),上面示例中并没有应用到,表示主线程不用等待子线程执行完毕,两者脱离关系,完全放飞自我。这个一般用在守护线程上:有时候我们需要建立一个暗中观察的线程,默默查询程序的某种状态,这种的称为守护线程。这种线程会在主线程销毁之后自动销毁。
  • 我们为什么需要多线程,因为我们希望能够把一个任务分解成很多小的部分,各个小部分能够同时执行,而不是只能顺序的执行,以达到节省时间的目的。对于求和,把所有数据一起相加和分段求和再相加没什么区别。

原文链接:https://blog.csdn.net/lizun7852/article/details/88753218

简单工厂模式

  • 简单工厂模式有一个工厂,可以生产多个产品,包含两个接口,一个是产品类的,一个是工厂类的。产品类需要有一个基类,基类中的具体产品实现需要是个纯虚函数,这样一来,产品的子类必须要重写具体的产品实现,实现不同的功能。
  • 产品类封装完成后,还需要一个工厂类,工厂类对产品类再次封装,最终实现由一个工厂对象决定创建出哪一种产品类的实例。
//简单工厂模式
#include
using namespace std;

//产品的基类
class Product{
public:
   //基类中的纯虚函数
	virtual int operation(int a, int b) = 0;
};
//产品的子类Add
class Product_Add : public Product{
public:
	int operation(int a, int b){
		return a + b;
	}
};
//产品的子类Mul
class Product_Mul : public Product{
public:
	int operation(int a, int b){
		return a * b;
	}
};
//工厂
class Factory{
public:
	Product* Create(int i){
		switch (i){
		case 1:
			return new Product_Add;
			break;
		case 2:
			return new Product_Mul;
			break;
		default:
			break;
		}
	}
};

int main()
{
	Factory *factory = new Factory();
	int add_result = factory->Create(1)->operation(1, 2);
	int mul_result = factory->Create(2)->operation(1, 2);
	cout <<"op_add:" <<add_result << endl;
	cout <<"op_multiply:" << mul_result << endl;
		getchar();
	return 0;
}

一些好的博客:
视觉SLAM的前段后端最详细的梳理(硕士入门知识框架更新)
C++笔记——多线程编程(1)
SLAM从0到1——13.SLAM中的多线程编程(2)
一些面试问题
视觉SLAM总结——视觉SLAM面试题汇总

你可能感兴趣的:(面试相关,c++,算法)