SLAM评估避坑指南:TUM、KITTI、EuRoC真值格式与对齐详解

发布时间:2026/11/2 19:39:14

SLAM评估避坑指南:TUM、KITTI、EuRoC真值格式与对齐详解 简介面向SLAM、视觉里程计与自动驾驶方向的学习者与研究者这份文档系统梳理了TUM RGB-D、KITTI与EuRoC三大主流数据集的核心信息。内容涵盖TUM数据集的Kinect采集方式、彩色与深度图存储规格、深度值5000倍缩放规则、groundtruth轨迹的timestamp与四元数格式以及ATE、RPE两项评价准则和点云生成脚本同时介绍KITTI采集平台的大众帕萨特B6改装车、GPS/IMU与Velodyne激光雷达等传感器配置、raw data与object等数据组织方式并简述EuRoC微型无人机多传感器数据构成。资源包共1个docx文件约1.93MB以结构化文字整理为主便于按数据集快速检索与对照。已有9650人学习下载适合需要横向比较数据集格式、搭建评测流程或撰写论文实验部分的读者作为速查参考。1. 三个数据集三种“真值”为什么 SLAM 评估的坑从数据格式就开始了跑通一个视觉 SLAM 或 VIO 系统很多人第一反应是“算法收敛了、轨迹画出来了”可一旦把轨迹和真值叠在一起ATE 动辄零点几米RPE 曲线抖成锯齿于是开始怀疑前端特征、后端优化、IMU 预积分。血泪经验是问题往往不在算法而在你根本没搞懂手里这份数据的真值是怎么来的、坐标系怎么定义的、时间戳怎么对齐的。TUM RGB-D、KITTI、EuRoC 这三份被引用到烂的数据集恰好代表了三种完全不同的真值获取范式——运动捕捉系统、GPS/IMU 融合加激光雷达、动作捕捉加 IMU 紧耦合。搞懂它们的底层机制你才知道自己评估的到底是算法精度还是数据本身的玄学。这篇就把这三份数据集的格式约定、坐标系变换、评估脚本的实现逻辑拆开讲透让你下次看到 ATE 异常时能第一时间定位到是数据关联错了还是对齐方式错了。2. TUM RGB-D深度值为什么要乘 5000ATE 的对齐到底在做什么TUM RGB-D 是理解 RGB-D SLAM 评估最好的入口因为它把“数据格式”和“评估准则”绑得很紧。它的采集平台用 Kinect 拿彩色图和深度图同时用高精度运动捕捉系统给出真值轨迹所以真值的精度远高于视觉估计适合做量化基准。但真正让新手翻车的是深度图的编码方式和轨迹对齐的数学。2.1 深度图的 16 位编码与 5000 倍缩放TUM 的深度图是 640x480 的 16 位单色 PNG像素值不是直接的米数而是按 5000 倍缩放像素值 5000 对应 1 米像素值 0 表示缺失。这个设计不是随便定的16 位无符号整数最大 65535除以 5000 约等于 13.1 米正好覆盖 Kinect 的有效深度范围同时保留了毫米级的量化精度。如果你直接把 PNG 读成 8 位或者忘了除 5000生成的点云会整体缩放 5000 倍轨迹评估自然全错。import cv2 import numpy as np # 读取深度图必须用 IMREAD_UNCHANGED 保留 16 位 depth cv2.imread(depth/1305031102.175304.png, cv2.IMREAD_UNCHANGED) # 检查数据类型确认是 uint16 assert depth.dtype np.uint16 # 转换为米0 值保持为 0 表示无效 depth_in_meters depth.astype(np.float32) / 5000.0 # 无效值掩码 valid_mask depth 0这段代码的关键在IMREAD_UNCHANGEDOpenCV 默认会把 16 位图截断成 8 位深度信息直接丢失。除以 5000 之后valid_mask用来过滤缺失值后续生成点云或做 ICP 时都要用它剔除无效像素。常见的实现思路是在把深度反投影到相机坐标系之前先做一次有效性过滤否则无效的 0 值会被当成距离相机 0 米的点点云里出现一大团噪声。2.2 轨迹文件格式与四元数的坑真值轨迹是 txt 文本每行timestamp tx ty tz qx qy qz qw。timestamp 是 Unix 纪元秒数平移是彩色相机光心相对运动捕捉系统世界原点的位置旋转是单位四元数。这里有两个容易踩的点第一四元数顺序是qx qy qz qw不是常见的qw qx qy qz很多库默认后者读进来旋转全反第二这个位姿是相机光心相对世界原点不是相机坐标系相对世界坐标系做手眼标定时要注意外参的叠加顺序。def load_tum_trajectory(path): data [] with open(path, r) as f: for line in f: if line.startswith(#): continue parts line.strip().split() if len(parts) 8: continue ts float(parts[0]) t np.array([float(x) for x in parts[1:4]]) q np.array([float(x) for x in parts[4:8]]) # qx qy qz qw data.append((ts, t, q)) return data读进来之后四元数要转成旋转矩阵才能和估计轨迹做对齐。注意q的顺序如果直接丢给scipy.spatial.transform.Rotation.from_quat它期望的是x y z w正好和 TUM 一致但如果你用的是别的库务必确认顺序。2.3 ATE 的 SVD 对齐为什么必须先对齐再算误差ATE 的计算不是简单地把两条轨迹逐点相减。因为估计轨迹和真值轨迹可能处在不同的坐标系下比如估计轨迹从原点出发真值轨迹在世界坐标系里有偏移直接减会得到一个巨大的常数误差。TUM 的评估脚本先用时间戳做关联把估计位姿和真值位姿配对然后用奇异值分解求一个最优的刚体变换旋转加平移把估计轨迹对齐到真值轨迹上再计算每对位姿之间的差异输出均值、中值、标准差。这个对齐的数学本质是求解一个最小二乘问题找旋转 R 和平移 t使得对齐后的估计轨迹和真值轨迹的欧氏距离平方和最小。SVD 的解法是先算两组点的质心去中心化构造协方差矩阵 H对 H 做 SVD从 U 和 V 得到最优旋转再算平移。常见的实现思路是对齐只做一次全局刚体变换不改变轨迹的形状所以 ATE 衡量的是整体形状的偏差而不是逐帧的漂移。这也是为什么 ATE 适合评估 SLAM 系统的整体性能而 RPE 适合评估里程计的漂移。提示TUM 的评估脚本默认会对估计轨迹做尺度对齐吗不会。ATE 的对齐是刚体变换不含尺度。如果你的单目 SLAM 估计轨迹和真值差一个尺度ATE 会很大这时候应该先做尺度对齐或者用 RPE 的固定窗口评估。2.4 RPE 的窗口机制与下采样RPE 计算的是时间戳对之间的相对运动误差。默认情况下脚本会计算估计轨迹中所有时间戳对之间的误差但时间戳对的数量随轨迹长度是二次增长的所以脚本提供了-max_pairs参数做下采样或者用-fixed_delta加-delta和-delta_unit指定固定窗口。这个设计背后的动机是RPE 关心的是局部漂移比如每秒漂移多少而不是全局形状。固定窗口让每个位姿只和稍后的一个位姿比较误差的统计更有物理意义。# 计算固定窗口 1 秒的 RPE python evaluate_rpe.py groundtruth.txt estimated.txt \ --fixed_delta --delta 1.0 --delta_unit s \ --plot result.pdf--fixed_delta开启固定窗口模式--delta 1.0表示窗口 1 秒--delta_unit s指定单位是秒。这样每个位姿和 1 秒后的位姿做相对运动比较误差反映的是每秒的漂移量。如果你的系统是视觉里程计这个指标比 ATE 更能说明问题。3. KITTI激光雷达 10Hz 同步、相机矫正与坐标系链KITTI 的复杂度不在单个传感器的数据格式而在多传感器的时间同步和坐标系变换链。它的采集车装了 GPS/IMU、64 线激光雷达、两对灰度相机和彩色相机所有传感器以激光雷达的 10Hz 为基准同步相机在激光雷达朝向正前方时释放快门。这个同步机制决定了你在做多传感器融合时时间戳对齐的精度上限。3.1 raw data 的组织方式与 syncedrectified 的含义KITTI 的 raw data 按场景分为 Road、City、Residential、Campus、Person每个序列提供原始数据和同步矫正后的数据。原始数据带畸变、没有同步同步矫正后的数据消除了畸变、做了双目矫正并且和其他传感器对应好。一般用 syncedrectified 数据因为双目矫正后极线是水平的视差计算和三角化都简单。但要注意矫正后的图像尺寸会变小内参也变了标定文件里的参数对应的是矫正后的相机。激光雷达数据是.bin格式每帧 10 万多个点按 float 矩阵存储每行是x y z reflectance。读的时候要按 float32 解析四个一组。import numpy as np def load_kitti_lidar(bin_path): # 每点 4 个 float32x y z reflectance scan np.fromfile(bin_path, dtypenp.float32) scan scan.reshape((-1, 4)) points scan[:, :3] # 只取 xyz return points这个读取方式看起来简单但坑在于如果你用np.fromfile不指定 dtype默认是 float64解析出来的点数和坐标全错。另外激光雷达坐标系和相机坐标系之间有一个固定的外参标定文件里给了相机到激光雷达的变换矩阵做点云投影时必须先乘这个矩阵。3.2 标定文件与坐标系变换链KITTI 的标定文件是 txt包含相机内参、相机之间的外参、相机到 GPS/IMU 的外参、相机到激光雷达的外参。这些变换构成一条链激光雷达坐标系 → 相机坐标系 → 图像像素坐标系。做 3D 目标检测或者点云投影时顺序不能乱。常见的实现思路是先把激光雷达点变换到相机坐标系再用相机内参投影到图像平面最后用图像尺寸做裁剪。def project_lidar_to_image(points_lidar, calib): # calib 包含 R0_rect, P2, Tr_velo_to_cam # 激光雷达 - 相机坐标系 points_cam calib[Tr_velo_to_cam] np.hstack( (points_lidar, np.ones((points_lidar.shape[0], 1)))).T # 矫正矩阵 points_cam calib[R0_rect] points_cam[:3, :] # 投影到图像 points_img calib[P2] np.vstack( (points_cam, np.ones((1, points_cam.shape[1])))) # 归一化 points_img points_img[:2, :] / points_img[2, :] return points_img.T这段代码里Tr_velo_to_cam是 4x4 变换R0_rect是 3x3 矫正旋转P2是 3x4 投影矩阵。顺序是激光雷达 → 相机 → 矫正 → 投影少一步结果就偏。参数说明points_lidar是 Nx3输出是 Nx2 的像素坐标。如果投影后点跑到图像外面先检查R0_rect有没有乘再检查P2对应的是哪个相机。3.3 object 与 tracking 数据的区别object 数据集用于目标检测数据是单帧的不连续每个下载包分 training 和 testingtraining 带标注。tracking 数据集用于目标跟踪序列是连续的组织方式和 object 类似但多了时间维度。这个区别决定了你不能拿 object 的数据做跟踪评估因为帧间没有连续性保证。常见的误用是用 object 的标注训练跟踪器结果帧间关联全乱因为 object 的帧本来就不是按时间顺序连续采的。4. EuRoC双目IMU 的传感器坐标系与 T_BS 变换EuRoC 是 VIO 评估绕不开的数据集它用四旋翼无人机采集装双目相机和 IMU真值来自 Vicon 动作捕捉系统100Hz、毫米精度。它的核心设计是所有传感器数据都相对于各自的传感器坐标系每个传感器文件夹里有一个sensor.yaml描述该传感器相对于 Body 坐标系的变换T_BS和内参。Body 坐标系以 IMU 中心为原点这个约定直接决定了 IMU 预积分和相机投影的变换顺序。4.1 四个传感器文件夹与 T_BS 的含义数据集结构里有cam0、cam1、imu0、leica0四个文件夹分别对应左目、右目、IMU、激光跟踪仪。每个文件夹的sensor.yaml里有T_BS表示从 Sensor 坐标系到 Body 坐标系的变换。注意方向T_BS是把 Sensor 坐标系下的向量变到 Body 坐标系不是反过来。这个方向搞反IMU 和相机的外参就全错VIO 初始化直接发散。# cam0/sensor.yaml 片段 T_BS: cols: 4 rows: 4 data: [0.0148655429818, -0.999880929698, 0.00414029679422, -0.0216401454975, 0.999557249008, 0.0149672133247, 0.025715529948, -0.064676986768, -0.0257744366974, 0.00375618835797, 0.999660727178, 0.00981073058949, 0.0, 0.0, 0.0, 1.0]这个 4x4 矩阵按行展开前三个是旋转第四列是平移。读的时候要按行 reshape 成 4x4不能按列。常见的实现思路是在 VIO 系统里先把相机坐标系下的特征点变换到 Body 坐标系再和 IMU 预积分的结果做优化变换顺序就是T_BS左乘。4.2 时间戳格式与 groundtruth 的 18 位纳秒EuRoC 的时间戳是 18 位纳秒整数不是秒的浮点数。这个设计是为了避免浮点精度损失因为纳秒级的时间戳用 float64 表示会有舍入误差。groundtruth 输出也是 18 位时间戳加位姿。做时间对齐时要把图像、IMU、真值的时间戳统一到同一时间基准通常是纳秒转秒再插值。坑在于如果你用 float32 存时间戳18 位纳秒直接溢出对齐全错。def ns_to_sec(ts_ns): # 18 位纳秒转秒用整数除法避免浮点误差 return ts_ns / 1e9 # 对齐时用线性插值 def interpolate_pose(ts_query, ts_list, pose_list): idx np.searchsorted(ts_list, ts_query) # 边界处理 if idx 0: return pose_list[0] if idx len(ts_list): return pose_list[-1] t0, t1 ts_list[idx-1], ts_list[idx] alpha (ts_query - t0) / (t1 - t0) return pose_list[idx-1] * (1 - alpha) pose_list[idx] * alpha这段插值代码假设位姿是向量形式实际用的时候四元数要单独做球面插值不能直接线性插值。参数说明ts_query是查询时间戳ts_list是已排序的时间戳列表pose_list是对应的位姿。边界处理很重要查询时间超出范围时直接取端点避免索引越界。4.3 旋转坐标系变换约定RWC 的方向EuRoC 文档里特别强调了旋转坐标系变换的方向RWC表示把一个在 C 坐标系下表示的点的坐标变换成以 W 坐标系下表示的坐标。这个约定和很多教科书的写法相反教科书里Rwc常常表示从世界到相机的旋转。方向搞反轨迹评估的误差会大一个数量级。常见的实现思路是在写变换代码时变量名直接带上方向比如R_world_cam避免歧义。5. 避坑与排查评估脚本跑通了结果却不可信5.1 ATE 异常大但轨迹形状看起来对现象把估计轨迹和真值画在一起形状几乎重合但 ATE 输出好几米。原因估计轨迹和真值的尺度不一致或者时间戳关联错了。单目 SLAM 的估计轨迹和真值差一个尺度ATE 的刚体对齐不含尺度所以误差被尺度放大。解决先检查是不是单目系统如果是做尺度对齐或者改用 RPE再检查时间戳关联TUM 的脚本用时间戳最近邻关联如果估计轨迹的时间戳和真值差太多关联会错位。5.2 深度图读进来全是 0 或者全白现象生成的点云要么空要么一片噪声。原因OpenCV 默认把 16 位 PNG 读成 8 位深度值被截断。解决用cv2.IMREAD_UNCHANGED读确认 dtype 是 uint16再除以 5000。另外检查深度图的路径TUM 的深度图和彩色图是分开的文件夹文件名时间戳要对上。5.3 KITTI 点云投影到图像上偏移很大现象激光雷达点投影到图像和目标框对不上。原因标定文件的变换链顺序错了或者用了原始图像的内参去投影矫正后的图像。解决确认用的是 syncedrectified 数据标定文件里的P2对应矫正后的相机变换顺序是Tr_velo_to_cam→R0_rect→P2少一步都不行。5.4 EuRoC 的 IMU 和相机外参方向反了现象VIO 初始化后轨迹直接飞掉或者优化不收敛。原因T_BS的方向搞反了把 Body 到 Sensor 当成了 Sensor 到 Body。解决记住T_BS是把 Sensor 坐标系下的向量变到 Body 坐标系代码里用T_BS左乘 Sensor 坐标系的点不要取逆。如果不确定用单位矩阵测试看轨迹是否合理。5.5 时间戳对齐用了浮点秒精度丢失现象高频 IMU 和低频图像对齐时插值结果抖动。原因18 位纳秒用 float32 存精度不够。解决时间戳全程用 int64 纳秒只在插值时转 float64并且插值前做排序和去重。6. 真值不是免费的三种评估范式的取舍与验证习惯这三份数据集放在一起看真正的设计取舍在于真值的获取方式和它带来的评估边界。TUM 用运动捕捉系统给真值精度高但场景受限只在室内小范围有效所以它的 ATE 适合评估 RGB-D SLAM 的整体形状RPE 适合评估漂移。KITTI 用 GPS/IMU 加激光雷达覆盖室外大场景但真值本身是融合结果存在累积误差所以它的评估更依赖标定和同步的精度。EuRoC 用动作捕捉加 IMU 紧耦合给 VIO 提供了高频真值但T_BS的方向约定和纳秒时间戳是两道门槛。数据集真值来源时间戳单位核心变换适用评估TUM RGB-D运动捕捉系统秒float相机光心到世界ATE 整体形状RPE 漂移KITTIGPS/IMU 激光雷达秒float雷达到相机链室外里程计、目标检测EuRoCVicon 动作捕捉纳秒intT_BS 传感器到 BodyVIO 紧耦合、IMU 预积分验证方法上我一般会这么做拿到一份新数据先不跑算法直接用真值自己和自己算一遍 ATE 和 RPE确认评估脚本的输出是 0 附近。如果自己和自己算都出问题那一定是格式读错了或者对齐方式错了。然后再用一条简单的恒速模型轨迹去评估看误差是否符合预期。这个习惯帮我省了很多次在算法里瞎找 bug 的时间。从那以后我每次拿到新数据集都强制先跑一遍“真值自评估”和“恒速基线”确认数据读取、时间对齐、坐标变换这三层都没问题再往上叠算法。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/2 19:34:14

社交媒体成瘾机制与合规设计解析

1. 社交媒体平台成瘾机制的本质剖析 社交媒体平台的设计核心在于一套精密的行为激励机制,这套机制融合了心理学、神经科学和行为经济学原理。最典型的当属"可变奖励"机制——用户每次刷新内容时获得的是不确定的反馈组合(可能是朋友点赞、热门…

2026/11/2 19:34:14

Mangos物品任务BOSS NPC编辑软件安装配置与排错指南

简介:Mangos服务端数据维护工具包,面向私服/单机服搭建者、数据库管理员以及希望熟悉WoW类服务端数据结构的进阶学习者,重点解决物品、任务、BOSS、NPC等条目在数据库中录入、查询和批量调整时效率低、字段易填错的问题。压缩包共66个文件&am…

2026/11/2 19:34:14

SOMBP多分类模型:融合自组织映射与BP神经网络的混合架构

1. SOMBP多分类模型概述 在机器学习领域,将不同算法的优势进行组合往往能产生意想不到的效果。SOMBP模型正是这种思路的典型代表——它巧妙地将自组织映射(SOM)的无监督学习特性与BP神经网络强大的监督学习能力相结合。这种混合架构特别适合处理那些既有大量未标记数…

2026/11/2 22:39:32

PanabitFREE FreeBSD 9.2流控网关:DPI应用识别与网桥限速

简介:PanabitFREE_SANGUOr10_20150513_FreeBSD9.2_dev.tar.gz 是面向网络管理员、运维人员和二次开发者的一款免费网络行为审计与流量管理开发包,基于 FreeBSD 9.2 构建,可用于流量监控、用户行为审计、限速与 QoS 策略控制以及异常流量排查。…

2026/11/2 22:39:32

OpenHarmony中Flutter UI自动化测试优化实践

1. 项目背景与核心价值在OpenHarmony生态中构建具备复杂交互链路的大规模应用时,UI自动化测试的效率直接决定了产品迭代速度。传统Flutter Driver方案存在两个致命缺陷:一是测试环境初始化耗时过长,二是缺乏并行调度能力导致CI流水线阻塞。fa…

2026/11/2 22:39:32

uni-app安卓侧滑返回禁用与返回键控制全指南

1. 为什么uni-app在安卓上“侧滑返回”成了默认行为,而你根本没写过这行代码?很多人第一次在真机调试时都懵了:明明只写了页面跳转逻辑,没碰过任何手势配置,结果在安卓手机上一左滑,页面就嗖一下退回到上一…

2026/11/2 22:39:32

SR830锁相放大器LabVIEW开发包:GPIB/VISA采集与落盘实践

简介:面向使用Stanford Research Systems SR830锁相放大器进行精密测量的科研人员与LabVIEW开发者,这套开发包提供锁相放大器与LabVIEW无缝对接所需的VI、函数库及工程文件,用于参数控制、实时数据采集与结果分析。压缩包共48个文件、约568KB…

2026/11/2 22:39:32

Agent-Based架构在车路云协同中的优势与实践

1. 论文核心价值解析:为什么Agent-Based架构适合车路云协同?这篇论文探讨的Agent-Based网络架构与资源管理方案,本质上是在解决智能交通系统中三个关键矛盾:车辆移动的高动态性、路侧设备计算能力的有限性,以及云端全局…

2026/11/2 22:34:32

米勒阀门选型与应用全解析:从参数计算到行业实践

1. 美国米勒阀门产品线概述作为全球领先的工业阀门制造商,美国米勒阀门(Miller Valve)拥有超过60年的专业经验,其产品线覆盖了从常规工况到极端环境的全系列解决方案。在石油化工、电力能源、水处理等行业,米勒阀门以其…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑