SLAM:即时定位与地图构建的入门指南

什么是SLAM?
基于实时定位与地图生成技术(simultaneous localization and mapping, SLAM),我们能够在环境中并行生成基于机器人的环境模型(map),同时对移动机器人自身状态进行估计。换句话说,SLAM提供了一种实时追踪机器人位置的方法,并能够识别出环境中重要的地标特征(如建筑、树木、岩石等其他世界特征)。除了实现定位外,我们还希望生成一个基于机器人的环境模型(map),从而能够利用这些地图数据确保机器人在全球范围内正确行驶。因此,在利用机器人的运动不确定性来实现对自身位置的追踪方面构成了SLAM的关键作用机制:因为现有的地图是不存在的,并且我们正是通过使用多个机器人进行并行的地图构建工作来完成这一任务。
SLAM的工作原理:
基于机器人传感器数据的空间重建构成了同时定位与地图绘制(SLAM)的基础技术。通过机器人传感器收集的数据以及随时间推移而变化的运动信息来利用这些测量数据构建环境模型
如图,机器人在x方向上的位移为10:

值得注意的是,卡尔曼滤波器作为一种重要的机器人定位技术(参考:https://medium.com/@krunalkshirsagar/the-curious-case-of-kalman-filters-f29c3d17b121),在动态系统中,位置的确定性往往难以保证。因此,在传统的运动模型中,与其假设我们的XY坐标系中的机器人精确地向右移动了10个位置相比,在运动更新后的后验估计通常表现为一个均值位于(10,0)的高斯分布。然而,这种假设忽略了机器人可能出现在其他地方的可能性。
如图:运动更新后,高斯以机器人的位置为中心

这是一个涉及x变量的高斯数学公式:当这两个条件相等时, 与其将x₁设定为等于x₀加10, 不如直接采用高斯函数来进行描述, 此时该高斯函数达到了其最大值. 因此, 如果从这个结果中减去(x₁ - x₀ - 10), 并将其视为一个方波, 然后将其转换成一个符合钟形曲线的形式. 同样地,在处理y时, 我们也可以对其进行类似的转换. 由于我们假设在运动过程中y保持不变, 因此在这种情况下,y₁和y₀会尽可能接近.

这两个高斯分布的乘积构成了我们当前问题中的一个关键限制条件。我们的目标是在固定坐标点(0,0)处最大化变量x1的位置。因此,在应用Graph SLAM算法时,默认情况下它将一系列这样的几何约束转化为概率模型。假设有一个在未知环境中不断移动的机器人传感器系统,则该系统首先自动确定并记录起始基准点作为(0,0)坐标,并将其称为初始基准位姿固定。随后系统持续采集并存储多个基于相对运动关系的几何约束信息;这些相对运动关系被用来构建每一步相对于前一步的姿态变换模型。例如当机器人在不同扫描时刻观察到相同的地标物时,则可以利用两次扫描之间的定位数据来计算两次定位之间的位移量;这种测量过程也被视为一种基于定位差异的信息增广方式。通过整合所有这些测量数据以及相应的几何关系模型,则可以逐步推断出机器人的完整运动轨迹以及环境中标记物的具体三维空间坐标;这一系列运算过程即构成了SLAM系统的本质功能机制

实际使用
生成环境:
我们计划在二维世界网格中加入地标信息,并让机器人在此环境中活动一段时间以完成扫描过程。随后,在一定时间跨度内让机器人在该环境中运动并感知。当实例化后的机器人在其环境中活动时会收集相关数据。为此,请先构建上述数据集,并研究其如何反映机器人通过运动与传感器测量来进行导航与定位。
SLAM输入:
除了数据之外,我们的slam函数还具有:
-
N:机器人将要移动和感应的时间步数。
-
num_landmarks:世界上的地标数量。
-
world_size:世界的大小(w / h)。
-
motion_noise:与运动相关的噪声;运动的更新置信度应为
1.0/motion_noise.
*measurement_noise:与测量/传感相关的噪声;测量的更新权重应为1.0/\text{measurement\_noise}。
import numpy as npfrom helpers import make_data
#slam的实现应该使用以下输入
#请随意更改这些输入值并查看其响应方式!
# 世界参数num_landmarks = 5 # number of landmarksN = 20 # time stepsworld_size = 100.0 # size of world (square)
# 机器人参数measurement_range = 50.0 # range at which we can sense landmarksmotion_noise = 2.0 # noise in robot motionmeasurement_noise = 2.0 # noise in the measurementsdistance = 20.0 # distance by which robot (intends to) move each iteratation
# make_data实例化一个机器人,并为给定的世界大小和给定数量生成的随机地标data = make_data(N, num_landmarks, world_size, measurement_range, motion_noise, measurement_noise, distance)
让我们开发两个主要功能;这些功能将使机器人四处移动,并辅助定位地标及其在2D地图上的间距。
-
Move:尝试按dx,dy移动机器人。
-
Sense:返回可见范围内地标的x和y距离。
class robot:
#move function def move(self, dx, dy):
x = self.x + dx + self.rand() * self.motion_noise y = self.y + dy + self.rand() * self.motion_noise
if x < 0.0 or x > self.world_size or y < 0.0 or y > self.world_size: return False else: self.x = x self.y = y return True
#sense function def sense(self): measurements = []
for landmark_index, landmark in enumerate(self.landmarks): landmark_distance_x = landmark[0] landmark_distance_y = landmark[1] random_noise = self.rand() cal_dx = self.x - landmark_distance_x + random_noise * self.measurement_noise cal_dy = self.y - landmark_distance_y + random_noise * self.measurement_noise is_not_in_measurement_range = self.measurement_range == -1 if(is_not_in_measurement_range) or ((abs(cal_dx) <= self.measurement_range) and (abs(cal_dy) <= self.measurement_range)): measurements.append([landmark_index, cal_dx, cal_dy]) return measurements
Omega 和 Xi:
为了建立Graph SLAM系统, 开发了一个矩阵和一个向量(分别对应ω和ξ)。该矩阵标记了所有机器人位置(ξ)和所有地标。例如, 在观察时两次移动某距离dx, 并关联这两个位置时, 可以将其表示为这些矩阵中的数值关系。让我们开发一个函数来为机器人的起始位置返回ω和ξ约束。我们尚不清楚的所有值都应使用0进行初始化. 我们假设我们的机器人以100%的信心位于世界的正中心开始.
def initialize_constraints(N, num_landmarks, world_size): ''' This function takes in a number of time steps N, number of landmarks, and a world_size, and returns initialized constraint matrices, omega and xi.'''
middle_of_the_world = world_size / 2
## 建议:在变量中定义和存储约束矩阵的大小(行/列) rows, cols = 2*(N + num_landmarks), 2*(N + num_landmarks) ## TODO: 用两个初始“strength”值定义约束矩阵Omega omega = np.zeros(shape = (rows, cols)) ## 我们机器人最初的x,y位置 #omega = [0]
omega[0][0], omega[1][1] = 1,1
## TODO: Define the constraint *vector*, xi ## 假设机器人以100%的置信度在世界的正中间开始。 #xi = [0] xi = np.zeros(shape = (rows, 1)) xi[0][0] = middle_of_the_world xi[1][0] = middle_of_the_world
return omega, xi
通过运动和测量值进行更新:
## slam接受6个参数并返回mu,## mu是机器人穿过的整个路径(所有x,y姿势)和所有地标位置def slam(data, N, num_landmarks, world_size, motion_noise, measurement_noise):
## TODO: 使用你的初始化创建约束矩阵 omega, xi = initialize_constraints(N, num_landmarks, world_size) ## TODO:遍历数据中的每个时间步骤 for time_step in range(len(data)):
## 每次迭代时获取所有的运动和测量数据 measurement = data[time_step][0] motion = data[time_step][1] x dx = motion[0] # 本次沿x移动的距离 dy = motion[1] # 本次沿y移动的距离
#假设机器人在这个时间从(x0,y0)移动到(x1,y1)
#omega的偶数列对应于x值 x0 = (time_step * 2) #x0 = 0,2,4,... x1 = x0 + 2 #x1 = 2,4,6,...
# omega 的奇数列对应于y值 y0 = x0 + 1 #y0 = 1,3,5,... y1 = y0 + 2 #y1 = 3,5,7,...
actual_m_noise = 1.0/measurement_noise actual_n_noise = 1.0/motion_noise ## TODO: 更新约束矩阵/向量(Omega/xi)以解释所有*measurements* ## 这应该是一系列考虑测量噪声的附加值 for landmark in measurement: lM = landmark[0] # 地标 id dx_lM = landmark[1] # 沿x与当前位置分离 dy_lM = landmark[2] # 沿y与当前位置分离
L_x0 = (N*2) + (lM*2) # 偶数列有x个地标值 L_y0 = L_x0 + 1 # 奇数列有y个地标值
# 更新对应于x0和Lx0之间测量值的omega值 omega[x0][x0] += actual_m_noise omega[L_x0][L_x0] += actual_m_noise omega[x0][L_x0] += -actual_m_noise omega[L_x0][x0] += -actual_m_noise
# 更新对应于y0和Ly0之间测量值的omega值 omega[y0][y0] += actual_m_noise omega[L_y0][L_y0] += actual_m_noise omega[y0][L_y0] += -actual_m_noise omega[L_y0][y0] += -actual_m_noise
# 更新X0和LX0之间的测量值对应的xi值 xi[x0] -= dx_lM/measurement_noise xi[L_x0] += dx_lM/measurement_noise
# 更新y0和Ly0之间的测量值对应的xi值 xi[y0] -= dy_lM/measurement_noise xi[L_y0] += dy_lM/measurement_noise
## TODO: 更新约束矩阵/向量(omega/XI),以解释从(x0,y0)到(x1,y1)和运动噪声的所有*运动*。 omega[x0][x0] += actual_n_noise omega[x1][x1] += actual_n_noise omega[x0][x1] += -actual_n_noise omega[x1][x0] += -actual_n_noise
omega[y0][y0] += actual_n_noise omega[y1][y1] += actual_n_noise omega[y0][y1] += -actual_n_noise omega[y1][y0] += -actual_n_noise
xi[x0] -= dx/motion_noise xi[y0] -= dy/motion_noise
xi[x1] += dx/motion_noise xi[y1] += dy/motion_noise
## TODO: 在遍历所有数据之后 ## 计算姿势和地标位置的最佳估计值 ##使用公式,omega_inverse * Xi inverse_of_omega = np.linalg.inv(np.matrix(omega)) mu = inverse_of_omega * xi
return mu
机器人的姿势和地标:
让该函数被用来打印估算姿势与基准点的位置。我们开发了一个用于提取姿势与基准点位置的函数,并将其存储为独立的列表。
def get_poses_landmarks(mu, N): # 创建一个姿势列表 poses = [] for i in range(N): poses.append((mu[2*i].item(), mu[2*i+1].item()))
# 创建一个地标列表 landmarks = [] for i in range(num_landmarks): landmarks.append((mu[2*(N+i)].item(), mu[2*(N+i)+1].item()))
# 返回完成的列表 return poses, landmarks
def print_all(poses, landmarks): print('\n') print('Estimated Poses:') for i in range(len(poses)): print('['+', '.join('%.3f'%p for p in poses[i])+']') print('\n') print('Estimated Landmarks:') for i in range(len(landmarks)): print('['+', '.join('%.3f'%l for l in landmarks[i])+']')
# 调用你的slam实现,并传入必要的参数mu = slam(data, N, num_landmarks, world_size, motion_noise, measurement_noise)
# 打印出地标和姿势结果if(mu is not None): # 获取姿势和地标列表 # 并打印出来 poses, landmarks = get_poses_landmarks(mu, N) print_all(poses, landmarks)
如图:估计的机器人姿势和地标

可视化构建的世界:
# 导入函数from helpers import display_world
# 显示最终世界!
# 定义图形大小plt.rcParams["figure.figsize"] = (20,20)
# 检查姿势是否已创建if 'poses' in locals(): # 打印出最后一个姿势 print('Last pose: ', poses[-1]) # 显示机器人的最后位置和地标位置 display_world(int(world_size), poses[-1], landmarks)
如图:输出量

在Github(https://github.com/Noob-can-Compile/Landmark_Detection_Robot_Tracking_SLAM-) 处查看代码库中的相关代码资源。
Graph Slam: 向机器人同时定位与地图构建入门的新手指南
这篇文章旨在帮助机器人初学者理解并实现基于图的同步定位与建图算法(Simultaneous Localization and Mapping, SLAM)。
它通过简单的步骤和清晰的解释帮助读者快速掌握这一复杂的技术领域。
文中包含详细的代码示例以及完整的项目实现方案供读者参考学习。
如果您对SLAM技术感兴趣,并且希望从基础开始逐步深入学习这一领域,则这篇文章将是您最佳的学习资源之一。
留言送书福利
本次留言的主题围绕着两个问题展开:一是"你觉得这次节日礼物怎么样?"二是"你期待下次能收到什么样的惊喜吗?"
感谢大家的走心留言,每一条小编都认真阅读了,会继续努力哒。
无需感到失望~ 我们定期会不定期地举办留言活动;积极发送评论将有助于提高您获得书籍的机会。
无需感到失望~ 我们定期会不定期地举办留言活动;积极发送评论将有助于提高您获得书籍的机会。
向三名留言的读者表示祝贺,并感谢他们多次参与我们的留言活动。每位 reader 获得一本赠送书籍,请联系小编 mthler 获取详情。




☆ END ☆
当您到达此处时,请表示您对这篇文章有浓厚的兴趣。别忘了转发并点赞。微信搜索「uncle_pn」关注我们,并欢迎添加小编微信「mthler」。我们每天分享一篇高质量的文章(没有广告干扰)。
↓ 扫描二维码添加小编↓

