简介面向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 的时间。从那以后我每次拿到新数据集都强制先跑一遍“真值自评估”和“恒速基线”确认数据读取、时间对齐、坐标变换这三层都没问题再往上叠算法。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Python爱心粒子代码:从运行到二次开发全解析 简介:这是一份面向Python初学者与图形编程爱好者的爱心粒子效果实现资料,围绕粒子系统、三次贝塞尔曲线绘制与动画渲染展开,帮助读者理解如何用代码生成动态爱心画面,适合作为课程设计、表白小程序或图形入门练习的参考。压缩包共… · 2026/11/2 19:38:09
在线图片设计源码搭建:在线海报设计系统开发、导出与部署实战 做本地生活号的朋友上周找我,说他每月要做几十张活动海报,外包一张八十,改稿还得等半天,问我有没有办法自己搭一套“拖拽就能出图”的系统。这类需求这两年特别集中:自媒体要日更封面,电商要频繁换主图&… · 2026/11/2 19:38:09
Product Hunt热榜分析:AI与隐私技术的2026趋势 1. 项目概述:Product Hunt每日热榜的价值与意义Product Hunt作为全球知名的产品发现平台,每日热榜汇聚了当天最受关注的新产品、工具和应用。2026年3月23日的这份榜单不仅反映了当下科技产品的流行趋势,更是创业者、产品经理和投资人获取灵感… · 2026/11/2 19:38:09
云沙箱文件通道设计:从Workspace边界到Agent安全隔离 很多第一次接触云沙箱的人都会有个误解:以为 Agent 在云端跑的是一台完整虚拟机,能直接操作任意文件、任意进程。实际上,云沙箱里的 Agent 真正能摸到的只有一块被精心划定出来的 Workspace 目录。文件通道这个概念,就是把这块目录… · 2026/11/2 20:25:45
IDV云桌面技术解析与优化实践 1. IDV云桌面技术概述IDV(Intelligent Desktop Virtualization)是一种分布式虚拟化架构,它将计算任务从集中式服务器转移到终端设备本地执行。与传统的VDI(Virtual Desktop Infrastructure)不同,IDV在保持集… · 2026/11/2 20:25:45
OpenSSL 3.2.0 Windows静态库集成实战:避坑指南与CMake配置 简介:本资源为 OpenSSL 3.2.0 针对 Windows x64 平台编译的静态库 Release 版本,专为 C/C 开发者在 Visual Studio 2019 环境下集成 TLS/SSL 功能提供开箱即用的底层支持,适用于网络通信、加密模块开发、嵌入式安全组件等中高级应用场景。压缩… · 2026/11/2 20:25:45
EDAS Dome:阿里微服务治理能力的本地可验证沙盒 简介:本资源是阿里EDAS(企业级分布式应用服务平台)的典型微服务实践Demo,面向Java开发者、微服务初学者及云原生技术学习者,聚焦解决分布式架构落地难、服务治理门槛高、容器化部署流程复杂等实际问题。压缩包为ZIP格式… · 2026/11/2 20:25:45
GPU服务器集群系统级参数调优与性能优化指南 1. GPU服务器集群系统级参数概述在深度学习和大模型训练领域,GPU服务器集群已成为不可或缺的基础设施。不同于单机GPU工作站,集群环境下的系统级参数调优直接影响着计算资源的利用效率和任务吞吐量。根据实测数据,合理的参数配置可使ResNet50… · 2026/11/2 20:24:44
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* 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 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* 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 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* 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 5:17:18
雷达信号调制技术解析:从LFM到伪线性调频原理与应用 /* 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:01:15
基于MAX14431与TM4C123的工业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:01:15
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:01:15
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* 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 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* 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 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* 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 5:17:18