分类: 知识储备

  • FAST-LIVO(docker)

    一、FAST-LIVO是什么?

    FAST-LIVO(Fast LiDAR-Inertial-Visual Odometry)是香港大学MARS实验室提出的激光-惯性-视觉紧耦合里程计算法。它的核心目标是用激光雷达、IMU和相机三种传感器的数据,实时估计机器人在三维空间中的位姿,并拼接出全局一致的点云地图。

    FAST-LIVO发表于IEEE T-RO 2024(论文: FAST-LIVO2: Fast, Direct LiDAR-Inertial-Visual Odometry),是FAST-LIO的视觉增强版。相比FAST-LIO只使用LiDAR + IMU,FAST-LIVO增加了相机通道,能够在激光退化场景(长走廊、隧道、空旷大厅等)下依靠视觉特征继续稳定建图。

    FAST-LIVO的核心改进包括:

    • 视觉通道:增加相机传感器,提供视觉重投影约束,在激光退化时补充纹理信息
    • VoxelOctoTree:地图存储升级为八叉树体素地图,内存更紧凑,查询更高效
    • 退化场景检测:自动检测几何特征单一的环境,动态调整激光和视觉的权重
    • 曝光补偿:在线估计相机曝光时间,提升视觉特征提取的鲁棒性
    • 多雷达支持:Livox AVIA / Mid-360 / Mid-70 / Velodyne / Hesai / RoboSense等

    二、算法原理详解

    2.1整体架构概览

    FAST-LIVO的数据流可以概括为:

    LiDAR点云 ──┐
                  ├── 点云预处理 ── 特征提取 ──┐
    IMU数据 ─────┤                           ├── ESKF更新 ── 位姿估计 ── 地图更新
                  ├── IMU预积分 ────────────┘
    相机图像 ─────┴── 视觉特征提取 ─────────┘

    整个系统运行在一个ROS节点中(fastlio_mapping),订阅LiDAR、IMU和相机话题,发布里程计和地图。系统支持三种工作模式:

    • LIVO模式:LiDAR + IMU + Camera三传感器融合(默认模式)
    • LIO模式:仅LiDAR + IMU(相机数据不可用时自动切换)
    • LO模式:仅LiDAR(IMU和相机都不可用时)

    2.2三传感器紧耦合框架

    FAST-LIVO的核心创新是将LiDAR、IMU和Camera三个传感器的数据在误差状态卡尔曼滤波(ESKF)层面进行紧耦合融合。每个传感器提供不同类型的观测约束:

    • IMU:提供高频的姿态和速度预测(200-1000Hz),短时间内的相对运动约束
    • LiDAR:提供精确的几何相对位姿约束,对平移和旋转都有良好观测
    • Camera:提供视觉重投影约束,在几何退化场景下补充纹理信息

    在ESKF的更新步骤中,FAST-LIVO根据当前环境动态调整各传感器的权重。当激光点云正常时,主要依赖LiDAR观测;当检测到激光退化时,自动增加视觉观测的权重。这种自适应机制使得系统在多种环境下都能保持鲁棒性。

    FAST-LIVO的状态向量在FAST-LIO的基础上增加了视觉相关状态:

    x = [p, v, R, b_a, b_g, g, T_cam_imu, inv_expo]
        p: 位置 (3D)
        v: 速度 (3D)
        R: 旋转 (四元数)
        b_a: 加速度计偏置 (3D)
        b_g: 陀螺仪偏置 (3D)
        g: 重力向量 (3D)
        T_cam_imu: 相机到IMU的外参 (6D)
        inv_expo: 逆曝光时间 (1D)

    2.3视觉特征融合

    FAST-LIVO的视觉通道采用基于稀疏直接法的轻量级方案。与传统的特征点匹配不同,FAST-LIVO使用光度误差(视觉重投影误差)作为观测残差,直接融合到ESKF中。

    视觉处理的具体流程:

    • 特征提取:将图像划分为网格(grid),在每个网格中提取最亮的角点作为特征点
    • 特征跟踪:通过逆合成光流法(Inverse Compositional Optical Flow)在连续帧之间跟踪特征点
    • 重投影:将地图中的3D特征点投影到当前图像平面,计算光度误差(像素灰度差)
    • ESKF更新:将光度误差作为观测残差,更新状态估计

    FAST-LIVO的视觉特征提取策略非常高效。它不需要对每帧图像进行完整的特征提取,而是只在需要时从体素地图中检索视觉特征点。这大大降低了计算开销,使得视觉通道可以在普通CPU上实时运行。

    视觉通道的关键参数:

    visual:
        enable: true                    # 是否启用视觉通道

    image_topic: "/camera/image_raw" # 相机话题名

    max_iterations: 5               # 视觉ESKF最大迭代次数
    img_point_cov: 100              # 视觉点协方差

    normal_en: true                 # 是否启用法向量约束

    raycast_en: false               # 是否启用光线投射

    exposure_estimate_en: true      # 是否在线估计曝光时间

    grid_size: 5                    # 特征提取网格大小

    grid_n_height: 17               # 特征提取网格高度

    patch_pyrimid_level: 3          # 图像金字塔层数

    patch_size: 8                   # 图像补丁大小

    outlier_threshold: 1000         # 外点阈值

    2.4退化场景检测

    激光SLAM在退化场景中容易失败。典型的退化场景包括:

    • 长直走廊:点云在走廊方向缺乏几何多样性,无法约束横向平移
    • 隧道:与走廊类似,且通常没有视觉纹理
    • 空旷大平层:大面积平面,缺乏角点和平面特征的组合
    • 重复结构:多个相似的环境,容易产生误匹配

    FAST-LIVO通过分析点云的特征值来判断是否退化。具体方法:

    • 对当前帧点云的协方差矩阵进行特征值分解
    • 如果最小特征值小于阈值,说明当前环境在某个方向缺乏几何约束
    • 系统会自动降低该方向的观测权重,同时增加视觉通道的权重

    退化检测的阈值可以通过参数调整。阈值设置过小会导致退化检测不敏感,阈值设置过大则可能误判正常场景为退化。

    2.5 VoxelOctoTree八叉树体素地图

    FAST-LIVO使用VoxelOctoTree(八叉树体素地图)作为全局地图的数据结构。八叉树是一种空间划分数据结构,它将三维空间递归地划分为8个子立方体,每个节点有8个子节点。

    八叉树的工作原理:

    • 根节点代表整个三维空间
    • 每个节点如果包含太多点,就分裂为8个子节点
    • 每个子节点代表父节点空间的1/8
    • 当节点内的点数低于阈值时,可以合并子节点

    相比FAST-LIO的ikd-Tree,VoxelOctoTree的优势:

    • 内存更紧凑:不需要存储分割平面信息,只需要存储体素中心点和子节点指针
    • 查询更快:层次化的结构使得近邻搜索更高效,时间复杂度接近O(log n)
    • 动态更新更灵活:可以方便地增删体素节点,不需要重新平衡树结构
    • 空间划分更均匀:八叉树的规则划分避免了k-d树在数据分布不均时的性能退化

    FAST-LIVO的VoxelOctoTree还维护了每个体素的平面信息(法向量和协方差),用于激光点云的特征匹配和视觉特征的重投影。地图的降采样分辨率由voxel_size参数控制(通常0.5m)。

    2.6曝光补偿

    FAST-LIVO的一个独特设计是在线曝光补偿。相机在自动曝光模式下,曝光时间会随环境光变化而变化,这会影响视觉特征提取的稳定性。FAST-LIVO将逆曝光时间(inverse exposure time)加入状态向量,在ESKF中在线估计:

    • 当环境光变暗时,自动增加曝光时间,保持图像亮度稳定
    • 当环境光变亮时,自动减少曝光时间,避免过曝
    • 曝光估计的协方差由inv_expo_cov参数控制

    曝光补偿使得FAST-LIVO在光照变化较大的环境中(如室内外切换、隧道出入口)也能保持稳定的视觉特征跟踪。

    三、ROS使用指南

    3.1话题与数据流

    FAST-LIVO相比FAST-LIO增加了相机话题:

    话题名消息类型方向说明
    /livox/lidarlivox_ros_driver2/CustomMsg订阅Livox雷达原始数据
    /livox/imusensor_msgs/Imu订阅Livox IMU数据
    /velodyne_pointssensor_msgs/PointCloud2订阅Velodyne雷达数据
    /imu/datasensor_msgs/Imu订阅外部IMU数据
    /camera/image_rawsensor_msgs/Image订阅相机图像数据
    /camera/camera_infosensor_msgs/CameraInfo订阅相机内参数据
    /Odometrynav_msgs/Odometry发布里程计(高频位姿)
    /cloud_registeredsensor_msgs/PointCloud2发布注册到世界坐标系的点云
    /cloud_effectedsensor_msgs/PointCloud2发布有效约束点云
    /pathnav_msgs/Path发布运动轨迹
    /Laser_mapsensor_msgs/PointCloud2发布局部地图点云
    /rgb_imgsensor_msgs/Image发布RGB图像(用于可视化)

    3.2 Launch文件与参数配置

    FAST-LIVO使用ROS的launch文件启动。不同雷达型号对应不同的launch文件:

    Launch文件适用雷达说明
    mapping_avia.launchLivox AVIALivox非重复扫描雷达
    mapping_velodyne.launchVelodyne 16/32/64Velodyne机械旋转雷达
    mapping_mid360.launchLivox Mid-360Livox中距雷达
    mapping_mid70.launchLivox Mid-70Livox短距雷达

    3.3参数配置详解

    FAST-LIVO的参数文件相比FAST-LIO增加了视觉相关配置。以下是完整的参数说明:

    common:
    lid_topic:  "/livox/lidar"      # LiDAR话题名
    imu_topic:  "/livox/imu"        # IMU话题名
    img_topic:  "/camera/image_raw"  # 相机话题名
    img_en: 1                       # 是否启用相机: 1=启用, 0=禁用
    lidar_en: 1                     # 是否启用LiDAR: 1=启用, 0=禁用
    preprocess:
        lidar_type: 1                   # 雷达类型: 1=AVIA, 2=Velodyne
        scan_line: 6                    # 扫描线数: AVIA=6, Velodyne=16/32/64
        blind: 0.5                      # 盲区距离 (m)
    
    vio:
        normal_en: true                 # 是否启用法向量约束
    
    inverse_composition_en: false   # 是否使用逆合成光流法
    
    max_iterations: 5               # 视觉ESKF最大迭代次数
    
    img_point_cov: 100              # 视觉点协方差
    
    raycast_en: false               # 是否启用光线投射
    
    exposure_estimate_en: true      # 是否在线估计曝光时间
    
    inv_expo_cov: 0.2               # 逆曝光时间协方差
    
    grid_size: 5                    # 特征提取网格大小
    
    grid_n_height: 17               # 特征提取网格高度
    
    patch_pyrimid_level: 3          # 图像金字塔层数
    
    patch_size: 8                   # 图像补丁大小
    
    outlier_threshold: 1000         # 外点阈值
    
    time_offset:
    exposure_time_init: 0.0         # 初始曝光时间
    
    img_time_offset: 0.0            # 图像时间偏移
    
    imu_time_offset: 0.0            # IMU时间偏移
    
    lidar_time_offset: 0.0          # LiDAR时间偏移extrin_calib:
        extrinsic_T: [0, 0, 0]          # LiDAR到IMU的平移外参
    
    extrinsic_R: [1,0,0, 0,1,0, 0,0,1]  # LiDAR到IMU的旋转外参
    Pcl: [0, 0, 0]                  # 相机到LiDAR的平移外参
    
    Rcl: [1,0,0, 0,1,0, 0,0,1]      # 相机到LiDAR的旋转外参
    mapping:
        filter_size_surf: 0.5           # 平面特征降采样分辨率 (m)
        max_iteration: 3                # ESKF最大迭代次数
    
    acc_cov: 0.1                    # 加速度噪声协方差
    
    gyr_cov: 0.1                    # 陀螺仪噪声协方差b_acc_cov: 0.0001               # 加速度计偏置噪声
    b_gyr_cov: 0.0001               # 陀螺仪偏置噪声fov_degree: 360                 # 视场角 (度)
        det_range: 100.0                # 最大探测距离 (m)
    pcd_save:
        pcd_save_en: true               # 是否保存PCD
        interval: -1                    # 保存间隔: -1=结束时保存, >0=每N帧保存

    视觉相关参数调优建议:

    • grid_size:特征提取网格大小。越小提取的特征点越多,但计算量越大。建议3-8
    • patch_size:图像补丁大小。越大特征越稳定,但计算量越大。建议6-10
    • patch_pyrimid_level:图像金字塔层数。越多对大运动越鲁棒,但计算量越大。建议2-4
    • exposure_estimate_en:是否在线估计曝光时间。光照变化大的场景建议开启
    • img_point_cov:视觉点协方差。越小视觉约束越强。建议50-200

    3.4相机内参与外参标定

    FAST-LIVO需要准确的相机内参和相机-外参。推荐使用Kalibr工具包进行标定:

    # 安装Kalibr
    sudo apt-get install ros-noetic-kalibr

    # 录制标定数据(多棋盘格标定板)rosbag record /camera/image_raw /camera/camera_info /livox/imu

    # 标定相机内参kalibr_calibrate_cameras --cameras cam0 --target aprilgrid.yaml \
      --bag calibration.bag

    # 标定相机-IMU外参kalibr_calibrate_imu_camera --cam cam0 --imu imu0 \
      --target aprilgrid.yaml --bag calibration.bag --bag-from-to 5 50

    标定完成后,将得到的内参填入相机驱动节点,外参填入YAML配置文件的extrin_calib部分。FAST-LIVO也推荐使用FAST-Calib工具包进行LiDAR-Camera联合标定,其输出参数可以直接填入YAML文件。

    如果标定精度不够,FAST-LIVO也支持在线估计相机-IMU外参,但初始值不能偏差太大,否则会收敛到错误值。

    3.5多雷达型号适配

    FAST-LIVO支持的雷达型号与FAST-LIO相同:

    雷达型号lidar_typescan_line话题名消息类型
    Livox AVIA16/livox/lidarCustomMsg
    Velodyne 16/32/64216/32/64/velodyne_pointsPointCloud2
    Livox Mid-36024/livox/lidarCustomMsg
    Livox Mid-7034/livox/lidarCustomMsg
    Livox Horizon46/livox/lidarCustomMsg
    Livox Tele51/livox/lidarCustomMsg

    3.6使用rosbag离线建图

    FAST-LIVO的离线建图流程与FAST-LIO类似,但需要额外提供相机数据:

    # 终端1: 启动FAST-LIVO
    roslaunch fast_livo mapping_avia.launch rviz:=false

    # 终端2: 播放rosbag(包含LiDAR + IMU + Camera数据)rosbag play -r 10 your_bag_file.bag

    # 播完后按Ctrl+C保存PCD

    注意:如果bag包中不包含相机数据,FAST-LIVO会自动退化为LIO模式,只使用LiDAR和IMU进行建图。如果连IMU数据也没有,则会退化为LO模式。

    四、FAST-LIO vs FAST-LIVO对比

    特性FAST-LIOFAST-LIVO
    传感器LiDAR + IMULiDAR + IMU + Camera
    地图结构ikd-TreeVoxelOctoTree(八叉树)
    退化检测有,自动切换到视觉通道
    曝光补偿有,在线估计逆曝光时间
    运行频率100Hz100Hz(激光为主)
    内存占用较高较低(八叉树更紧凑)
    适用场景一般环境含退化场景的复杂环境
    ROS话题LiDAR + IMULiDAR + IMU + Camera
    参数复杂度较低较高(增加视觉参数)
    工作模式LIOLIVO / LIO / LO(自动切换)

    五、常见问题与排查

    5.1视觉通道不工作

    检查以下几点:

    • 确认bag包中包含相机数据(rostopic list检查)
    • 确认相机话题名与配置一致
    • 确认img_en设为1
    • 确认相机内参正确填写

    5.2退化检测过于敏感

    如果系统频繁切换到视觉通道,可能是退化检测阈值过高。可以调整:

    • 增大退化检测的特征值阈值
    • 降低视觉通道的权重

    5.3视觉特征跟踪丢失

    视觉特征跟踪丢失的常见原因:

    • 相机曝光时间过长,导致运动模糊
    • 环境缺乏纹理(白墙、纯色地面)
    • 相机帧率过低(建议至少20Hz)
    • 光照变化过大(开启exposure_estimate_en可缓解)

    5.4没有生成PCD

    PCD文件保存在退出钩子(Exit Hook)中,只有在按Ctrl+C正常退出时才会触发。如果直接关闭终端或kill进程,PCD不会保存。

    5.5建图漂移严重

    建图漂移通常由以下原因导致:

    • IMU和LiDAR的外参不准确,需要重新标定外参
    • 相机和LiDAR的外参不准确,需要重新标定
    • IMU噪声参数设置不合理,调整acc_cov和gyr_cov
    • 环境缺乏几何特征(长走廊、白墙),开启视觉通道可缓解

    FAST-LIVO论文: Fast and Tightly-coupled Sparse-Direct LiDAR-Inertial-Visual Odometry

  • FAST-LIO(docker)

    一、FAST-LIO是什么?

    FAST-LIO(Fast LiDAR-Inertial Odometry)是香港大学MARS实验室提出的一套激光-惯性紧耦合里程计算法。它的核心目标只有一个:用激光雷达和IMU的高频数据,实时估计机器人在三维空间中的位姿,并拼接出全局一致的点云地图。

    相比传统的滤波方案(如LOAM系列),FAST-LIO的计算效率极高。在普通CPU上就能跑到100Hz以上,同时精度不输甚至超越LOAM。这得益于它采用了基于误差状态卡尔曼滤波(Error-State Kalman Filter, ESKF)的紧耦合框架,以及增量式地图更新机制(ikd-Tree)。

    二、算法原理详解

    2.1整体架构概览

    FAST-LIO的数据流可以概括为:

    LiDAR点云 ──┐

    ├── 点云预处理 ── 特征提取 ── ESKF更新 ── 位姿估计 ── 地图更新IMU数据 ─────┘

    整个系统运行在一个ROS节点中(fastlio_mapping),订阅LiDAR和IMU话题,发布里程计和地图。核心流程如下:

    • IMU预积分:在两帧激光之间,用IMU的高频数据(200-1000Hz)推算相对运动
    • 点云预处理:运动补偿(去畸变)、特征提取(平面特征 / 角点特征)
    • ESKF更新:将激光里程计观测和IMU预测融合,估计误差状态
    • 地图更新:将当前帧点云投影到全局地图中,增量式维护ikd-Tree

    2.2 IMU预积分与运动补偿

    激光雷达的扫描频率通常只有10-20Hz(Livox AVIA是10Hz,Velodyne 32线也是10Hz),而IMU的频率可以达到200-1000Hz。在两次激光扫描之间,IMU能提供大量的高频姿态和速度信息。

    FAST-LIO使用IMU预积分(Preintegration)技术:给定上一帧激光时刻t_k的IMU状态,对t_k到t_{k+1} 之间的所有IMU测量值进行积分,得到两帧之间的相对旋转、速度和位移。这个预积分结果作为ESKF的预测步骤(Prediction)。

    同时,由于激光雷达扫描一帧需要一定时间(例如100ms),而机器人在这期间可能已经运动了相当距离,所以点云会产生运动畸变。FAST-LIO利用IMU数据对每一帧点云做运动补偿(Deskew),把每个点都投影到统一的坐标系下。这是高精度建图的关键步骤。

    2.3误差状态卡尔曼滤波(ESKF)

    FAST-LIO的核心状态估计器是误差状态卡尔曼滤波。这里需要区分两个概念:

    • 名义状态x:由IMU数据直接积分得到,不考虑噪声,包含了大的非线性运动
    • 误差状态delta_x:代表名义状态的微小偏差,是线性的,用卡尔曼滤波估计

    名义状态和误差状态组合成完整的状态向量:

    x = [p, v, R, b_a, b_g, g]
        p: 位置 (3D)
        v: 速度 (3D)
        R: 旋转 (四元数或旋转矩阵)
        b_a: 加速度计偏置 (3D)
        b_g: 陀螺仪偏置 (3D)
        g: 重力向量 (3D)

    ESKF的工作流程:

    • 预测(Predict):用IMU预积分更新名义状态,同时更新误差状态的协方差
    • 更新(Update):用激光里程计观测(当前帧与地图的匹配结果)计算残差,更新误差状态
    • 注入(Inject):将估计的误差状态注入名义状态,然后重置误差状态

    这种设计的优势在于:名义状态可以处理大的非线性运动,而误差状态始终保持线性,可以用标准的卡尔曼滤波高效求解。这也是FAST-LIO能跑到100Hz的重要原因。

    2.4点云特征提取

    FAST-LIO不需要使用全部点云进行匹配,而是提取特征点来降低计算量。特征分为两类:

    • 平面特征(Planar):位于平面上的点,法向量与激光方向接近垂直,对平移敏感
    • 角点特征(Edge):位于边缘或角上的点,对旋转敏感

    特征提取的策略是:将一帧点云按曲率排序,曲率大的作为角点特征,曲率小的作为平面特征。FAST-LIO使用局部邻域(通常取最近的25个点)计算曲率,然后根据阈值分类。

    在ESKF更新步骤中,每个平面特征点与地图中最近的平面片计算点到平面距离,每个角点特征点与地图中最近的边缘线计算点到线距离。这些距离构成观测残差,用于更新状态估计。

    2.5 ikd-Tree增量式地图

    FAST-LIO使用ikd-Tree(incremental k-d Tree)作为全局地图的数据结构。ikd-Tree是一种支持增量插入和删除的k-d树,专门为三维点云设计。

    ikd-Tree的核心优势:

    • 增量更新:新帧点云直接插入地图,不需要重建整棵树
    • 降采样:每个体素格子只保留一个点(或几个点),控制地图大小
    • 近邻搜索:支持快速的最近邻查询,用于特征匹配
    • 删除操作:可以删除旧的或不需要的点,保持地图整洁

    地图的降采样分辨率由filter_size_map参数控制(通常0.5m)。这意味着地图中每个0.5m x 0.5m x 0.5m的体素只保留一个点。这个分辨率直接影响建图精度和内存占用。

    三、ROS使用指南

    3.1话题与数据流

    FAST-LIO作为ROS节点运行,通过话题(Topic)与其他节点通信。理解这些话题是正确使用FAST-LIO的前提。

    话题名消息类型方向说明
    /livox/lidarlivox_ros_driver2/CustomMsg订阅Livox雷达原始数据
    /livox/imusensor_msgs/Imu订阅Livox IMU数据
    /velodyne_pointssensor_msgs/PointCloud2订阅Velodyne雷达数据
    /imu/datasensor_msgs/Imu订阅外部IMU数据
    /Odometrynav_msgs/Odometry发布里程计(高频位姿)
    /cloud_registeredsensor_msgs/PointCloud2发布注册到世界坐标系的点云
    /cloud_registered_bodysensor_msgs/PointCloud2发布注册到IMU坐标系的点云
    /pathnav_msgs/Path发布运动轨迹
    /LaserCloudMapsensor_msgs/PointCloud2发布局部地图点云

    注意:Livox雷达使用自定义消息类型CustomMsg,不是标准的PointCloud2。这是因为Livox雷达的点云数据包含每个点的反射强度、时间戳等额外信息,标准消息无法容纳。FAST-LIO内部会将CustomMsg转换为PointCloud2再处理。

    3.2 Launch文件与参数配置

    FAST-LIO使用ROS的launch文件启动。不同雷达型号对应不同的launch文件:

    Launch文件适用雷达说明
    mapping_avia.launchLivox AVIALivox非重复扫描雷达
    mapping_velodyne.launchVelodyne 16/32/64Velodyne机械旋转雷达
    mapping_mid360.launchLivox Mid-360Livox中距雷达
    mapping_mid70.launchLivox Mid-70Livox短距雷达
    mapping_horizon.launchLivox HorizonLivox单线雷达

    Launch文件的核心作用是加载参数配置文件(YAML)并启动fastlio_mapping节点。参数文件是配置FAST-LIO行为的关键,下面详细介绍。

    3.3参数配置详解(avia.yaml为例)

    参数文件位于config/ 目录下,以YAML格式组织。以下是各参数的含义:

    common:
        lid_topic:  "/livox/lidar"      # LiDAR话题名imu_topic:  "/livox/imu"        # IMU话题名time_sync_en: false             # 是否启用时间同步time_offset_lidar_to_imu: 0.0   # LiDAR到IMU的时间偏移preprocess:
        lidar_type: 1                   # 雷达类型: 1=AVIA, 2=Velodyne
        scan_line: 6                    # 扫描线数: AVIA=6, Velodyne=16/32/64
        scan_rate: 10                   # 扫描频率 (Hz)
        timestamp_unit: 1               # 时间戳单位: 0=秒, 1=毫秒, 2=微秒
    blind: 0.5                      # 盲区距离 (m),过滤近距离噪声mapping:
        filter_size_surf: 0.5           # 平面特征降采样分辨率 (m)
        filter_size_map: 0.5            # 全局地图降采样分辨率 (m)
        max_iteration: 3                # ESKF最大迭代次数
    acc_cov: 0.1                    # 加速度噪声协方差
    gyr_cov: 0.1                    # 陀螺仪噪声协方差
    b_acc_cov: 0.0001               # 加速度计偏置噪声
    b_gyr_cov: 0.0001               # 陀螺仪偏置噪声
    fov_degree: 360                 # 视场角 (度)
        det_range: 100.0                # 最大探测距离 (m)
        extrinsic_est_en: false         # 是否在线估计外参
    extrinsic_T: [0, 0, 0]          # LiDAR到IMU的平移外参
    extrinsic_R: [1,0,0, 0,1,0, 0,0,1]  # LiDAR到IMU的旋转外参
    publish:
        path_en: true                   # 是否发布轨迹
    scan_publish_en: true            # 是否发布注册点云
    dense_publish_en: true          # 是否发布稠密点云scan_bodyframe_pub_en: true     # 是否发布IMU坐标系点云pcd_save:
    pcd_save_en: true               # 是否保存PCD
        interval: -1                    # 保存间隔: -1=结束时保存, >0=每N帧保存

    关键参数调优建议:

    • filter_size_surf / filter_size_map:降低分辨率可以加快建图速度但会损失细节。室内场景0.3-0.5m,室外场景0.5-1.0m
    • max_iteration:ESKF迭代次数。精度要求高可以设为5,实时性要求高设为3
    • blind:盲区距离。需要过滤雷达自身和机器人车体的点,Livox AVIA设0.5m,Velodyne设2.0m
    • extrinsic_T / extrinsic_R:LiDAR和IMU之间的外参。如果安装位置精确已知,直接填入;如果不确定,可以设extrinsic_est_en: true让FAST-LIO在线估计

    3.4多雷达型号适配

    FAST-LIO支持多种雷达型号,切换雷达只需要修改YAML配置文件中的几个参数:

    雷达型号lidar_typescan_line话题名消息类型
    Livox AVIA16/livox/lidarCustomMsg
    Velodyne 16/32/64216/32/64/velodyne_pointsPointCloud2
    Livox Mid-36024/livox/lidarCustomMsg
    Livox Mid-7034/livox/lidarCustomMsg
    Livox Horizon46/livox/lidarCustomMsg
    Livox Tele51/livox/lidarCustomMsg

    注意:修改参数后不需要重新编译,FAST-LIO会在每次启动时读取最新的YAML文件。

    3.5外参标定(Extrinsic Calibration)

    LiDAR和IMU之间的外参(Translation + Rotation)对建图精度至关重要。如果外参不准确,建图会出现明显的重影或漂移。

    FAST-LIO提供两种方式处理外参:

    • 手动填入:如果已知LiDAR和IMU的相对安装位置,直接在YAML中填写extrinsic_T和extrinsic_R
    • 在线估计:设extrinsic_est_en: true,FAST-LIO会在运行过程中自动优化外参。但初始值不能偏差太大,否则会收敛到错误值

    外参的格式:

    extrinsic_T: [t_x, t_y, t_z]           # 平移 (米)
    extrinsic_R: [r11, r12, r13,        # 旋转矩阵 (3×3, 行优先)
                  r21, r22, r23,
                  r31, r32, r33]

    3.6使用rosbag离线建图

    在实际使用中,我们通常先录制rosbag包,再用FAST-LIO离线处理生成点云地图。这样做的好处是:

    • 可以反复调整参数,找到最佳配置
    • 避免实时建图时机器人运动不稳定导致的数据质量问题
    • 可以加速播放(Livox AVIA可以10倍速甚至20倍速)

    离线建图需要两个终端(或两个SSH会话):

    # 终端1: 启动FAST-LIO
    roslaunch fast_lio mapping_avia.launch rviz:=false

    # 终端2: 播放rosbag
    rosbag play -r 10 your_bag_file.bag

    当终端2显示Done. 后,切回终端1按Ctrl+C。FAST-LIO会在退出时自动保存PCD文件。

    注意:Livox AVIA数据量小,可以加速播放(-r 10甚至 -r 20),150秒的数据15秒就能播完。但Velodyne 32线数据量大,必须原速播放(-r 1),否则消息队列会爆满导致数据丢失。

    四、常见问题与排查

    4.1没有生成PCD

    PCD文件保存在退出钩子(Exit Hook)中,只有在按Ctrl+C正常退出时才会触发。如果直接关闭终端或kill进程,PCD不会保存。

    4.2报Error during open!

    这通常是PCD保存路径不存在或权限问题。检查:

    ls -ld /root/fastlio_ws/src/FAST_LIO/PCD
    # 应该显示: PCD -> /workspace (软链接)
    # 如果不是,重建软链接:
    rm -rf /root/fastlio_ws/src/FAST_LIO/PCD
    ln -s /workspace /root/fastlio_ws/src/FAST_LIO/PCD

    4.3卡在No point, skip this scan!

    这说明激光点云数据缺少必要字段。可能的原因:

    • PointCloud2缺少ring(线束ID)或time(单点时间戳)字段
    • bag包中的点云数据格式与配置不匹配
    • 雷达话题名配置错误,FAST-LIO没有接收到数据

    4.4建图漂移严重

    建图漂移通常由以下原因导致:

    • IMU和LiDAR的外参不准确,需要重新标定外参
    • IMU噪声参数设置不合理,调整acc_cov和gyr_cov
    • 环境缺乏几何特征(长走廊、白墙),增加max_iteration或降低filter_size_map
    • 运动补偿不准确,检查IMU频率是否足够高

    4.5内存占用过高

    如果建图范围很大,ikd-Tree会占用大量内存。可以:

    • 增大filter_size_map(如从0.5m改为1.0m)来减少地图点数
    • 设置pcd_save.interval为正值(如200),定期保存并清空地图
    • 限制建图范围(减小det_range参数)

    五、总结

    FAST-LIO是一套高效、实用的激光-惯性紧耦合SLAM方案。它的核心优势在于:

    • 速度快:ESKF + ikd-Tree的设计使其在普通CPU上就能跑到100Hz
    • 精度高:紧耦合框架充分利用了IMU和LiDAR的互补优势
    • 易用性:ROS接口清晰,参数配置简单,支持多种雷达型号

    在实际使用中,关键是理解参数的含义并根据场景调优。希望本文能帮助你更好地使用FAST-LIO进行机器人建图。

  • ROS(docker)

    一、什么是ROS

    ROS(Robot Operating System)是一个开源的机器人软件框架,它不是传统意义上的操作系统,而是一套运行在Linux之上的中间件和工具集。ROS为机器人软件开发提供了统一的通信机制、包管理、构建系统和调试工具。
    目前ROS有两个主要版本:ROS1(如 Noetic、Melodic、Kinetic)和ROS2(如 Humble、Iron、Jazzy)。本文以 ROS Noetic(ROS1的最后一个LTS版本)为基础进行讲解,但核心概念在 ROS 2 中同样适用。

    1.1 ROS 核心概念

    ROS的设计哲学是基于分布式架构,系统由多个独立进程(节点)组成,节点之间通过消息传递进行通信。理解 ROS 的关键是理解以下几个核心概念:

    概念说明类比
    节点(Node)一个独立运行的进程,负责特定功能一个工人
    话题(Topic)节点间异步通信的通道,发布/订阅模式广播频道
    消息(Message)在话题上传输的数据结构广播的内容
    发布(Publish)节点向话题发送消息播音
    订阅(Subscribe)节点从话题接收消息收听
    服务(Service)节点间同步通信,请求/响应模式电话呼叫
    Master节点注册和查找的中间人电话交换机
    Launch文件XML 格式的配置文件,批量启动多个节点启动脚本
    PackageROS 软件的基本组织单位一个项目/模块
    TF坐标变换系统,管理各坐标系之间的关系地图上的坐标网格

    1.2 ROS 文件系统

    ROS项目通常遵循以下目录结构:

    ├── src/                     # 源代码目录
    │   └── my_package/          # 一个ROS包
    │       ├── CMakeLists.txt   # 构建配置
    │       ├── package.xml      # 包描述
    │       ├── src/             # C++ 源文件
    │       ├── include/         # 头文件
    │       ├── scripts/         # Python 脚本
    │       ├── launch/          # Launch 文件
    │       └── config/          # 配置文件
    ├── build/                   # 编译中间产物
    ├── devel/                   # 编译产物(可执行文件、库)
    └── install/                 # 安装目录

    编译安装的通常在/opt/里面,也就是install目录文件夹。内部通常是:

    1.3 ROS 通信机制详解

    ROS 支持两种通信方式:话题(Topic)和服务(Service)。

    话题是异步的、多对多的通信机制。一个节点可以发布消息到某个话题,其他节点可以订阅这个话题接收消息。话题使用发布/订阅模型,发布者(Publisher)和订阅者(Subscriber)之间是解耦的。发布者不需要知道谁在接收,订阅者也不需要知道谁在发送。

    # 话题通信示例(Python)
    # 发布者
    import rospy
    from std_msgs.msg import String
    pub = rospy.Publisher('my_topic', String, queue_size=10)
    pub.publish("Hello ROS!")

    # 订阅者
    def callback(msg):
        rospy.loginfo("Received: %s", msg.data)
    rospy.Subscriber('my_topic', String, callback)

    服务是同步的、一对一的通信机制。客户端(Client)发送请求,服务端(Server)处理后返回响应。适用于需要等待结果的场景,比如查询机器人状态、执行一次性的动作。

    # 服务通信示例(Python)
    # 服务端
    from my_package.srv import AddTwoInts, AddTwoIntsResponse
    def handle_add(req):
        return AddTwoIntsResponse(req.a + req.b)
    rospy.Service('add_two_ints', AddTwoInts, handle_add)
    
    
    # 客户端
    rospy.wait_for_service('add_two_ints')
    add = rospy.ServiceProxy('add_two_ints', AddTwoInts)
    result = add(3, 5)

    1.4 Launch 文件

    Launch 文件是 ROS 中批量启动节点和配置参数的核心工具。使用XML格式编写,可以一次性启动多个节点、设置参数、包含其他Launch文件。

    <!-- 示例:启动两个节点 -->
    <launch>
        <!-- 设置参数 -->
        <arg name="rviz" default="true"/>

        <!-- 启动 FAST-LIO -->
        <node pkg="fast_lio" type="fastlio_mapping"
              name="laserMapping" output="screen">
            <param name="common/lid_topic" value="/livox/lidar"/>
            <param name="common/imu_topic" value="/livox/imu"/>
            <param name="pcd_save/pcd_save_en" value="true"/>
        </node>

        <!-- 启动 RViz 可视化 -->
        <node if="$(arg rviz)" pkg="rviz" type="rviz"
              name="rviz" args="-d $(find fast_lio)/rviz_cfg/loam.rviz"/>
    </launch>

    Launch 文件的关键语法:

    • <launch>:根元素,所有其他元素都包含在其中
    • <node>:启动一个节点,指定包名(pkg)、可执行文件(type)、节点名(name)
    • <param>:设置参数,会加载到ROS参数服务器
    • <arg>:定义变量,可以在运行时通过命令行传入
    • <include>:包含另一个Launch文件
    • <group>:对一组节点进行操作,可以设置命名空间

    1.5 TF 坐标变换系统

    TF(Transform)是ROS中管理坐标变换的核心系统。在机器人系统中,不同传感器、不同部件都有自己的坐标系,TF负责维护这些坐标系之间的变换关系。

    例如,一个典型的移动机器人可能包含以下坐标系:

    • map:全局地图坐标系,固定不动
    • odom:里程计坐标系,以机器人启动位置为原点
    • base_link:机器人本体坐标系,固定在机器人上
    • lidar:激光雷达坐标系,相对于 base_link 有固定偏移
    • imu:IMU 坐标系,相对于 base_link 有固定偏移

    TF树是一个有向树结构,每个坐标系只有一个父节点。通过TF可以方便地将一个坐标系下的点转换到另一个坐标系下。

    二、ROS的Docker基础

    Docker是一个开源的容器化平台,它可以将应用程序及其依赖打包到一个轻量级、可移植的容器中。相比虚拟机,Docker容器共享宿主机的内核,启动更快、资源占用更少。

    2.1 Docker核心概念

    概念说明类比
    镜像(Image)只读的模板,包含运行应用所需的一切光盘/ISO文件
    容器(Container)镜像的运行实例运行中的程序
    Dockerfile定义如何构建镜像的脚本菜谱
    仓库(Registry)存储和分发镜像的服务应用商店
    挂载(Volume)将宿主机目录映射到容器内部共享文件夹
    端口映射将容器端口映射到宿主机端口端口转发

    2.2 Docker基础命令

    以下是Docker最常用的命令:

    docker pull ubuntu:20.04 # 拉取镜像
    
    docker images  # 列出本地镜像
    
    docker rmi <image_id> # 删除镜像
    
    docker save -o my_image.tar <image>   # 导出镜像为tar文件
    
    docker load -i my_image.tar    # 从tar文件导入镜像
    
    docker run -it ubuntu:20.04 /bin/bash   # 启动交互式容器
    
    docker ps  # 列出运行中的容器
    
    docker ps -a  # 列出所有容器(含已停止)
    
    docker stop <container> # 停止容器
    
    docker rm <container>  # 删除容器
    
    docker exec -it <container> bash  # 进入运行中的容器
    
    docker run -v /host/path:/container/path <image>  # 目录挂载
    
    docker run -v my_volume:/container/path <image>   # 命名卷挂载
    
    docker run -p 8080:80 <image>    # 将容器80端口映射到宿主机8080

    2.3 Dockerfile编写

    Dockerfile是构建镜像的脚本文件,定义了镜像的每一层。以下是一个ROS Noetic的示例Dockerfile:

    # 基于ROS Noetic官方镜像FROM osrf/ros:noetic-desktop-full

    # 设置环境变量ENV DEBIAN_FRONTEND=noninteractive

    # 安装常用工具RUN apt-get update && apt-get install -y \
        git wget vim nano \
        ros-noetic-pcl-ros \
        ros-noetic-rviz \
        && rm -rf /var/lib/apt/lists/*

    # 设置工作目录WORKDIR /workspace

    # 设置自动加载ROS环境RUN echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc

    CMD ["/bin/bash"]

    Dockerfile的关键指令:

    • FROM:指定基础镜像
    • RUN:在构建时执行命令
    • COPY/ADD:将文件复制到镜像中
    • ENV:设置环境变量
    • WORKDIR:设置工作目录
    • EXPOSE:声明容器监听的端口
    • CMD/ENTRYPOINT:容器启动时执行的命令

    2.4 Docker Compose

    Docker Compose是一个用于定义和运行多容器应用的工具。通过一个YAML文件配置多个容器,一键启动整个应用栈。

    # docker-compose.yml示例version: '3'
    services:
      fastlio:
        image: fastlio_noetic:v3
        container_name: fastlio_container
        volumes:
          - ./workspace:/workspace
        network_mode: host
        stdin_open: true
        tty: true

      rviz:
        image: osrf/ros:noetic-desktop-full
        container_name: rviz_container
        environment:
          - DISPLAY=${DISPLAY}
        volumes:
          - /tmp/.X11-unix:/tmp/.X11-unix
        network_mode: host
        depends_on:
          - fastlio

    使用Docker Compose启动:

    docker-compose up -d       # 启动所有服务
    
    docker-compose down         # 停止并删除所有服务docker-compose logs -f      # 查看日志

    三、ROS + Docker

    将ROS应用打包到Docker容器中,是目前机器人开发和部署的最佳实践。它解决了“环境依赖地狱”问题。同一套代码在任何机器上都能跑。

    3.1 ROS网络配置

    ROS的节点通信依赖于网络。在Docker中运行ROS时,网络配置是关键。有三种主要方式:

    网络模式说明适用场景
    host容器共享宿主机网络栈ROS节点间通信,最常用
    bridge容器使用独立的虚拟网络隔离环境,需要端口映射
    none容器无网络离线测试

    对于ROS应用,推荐使用host模式。因为ROS节点需要在不同容器之间通过话题通信,host模式让所有容器共享同一网络,节点之间可以直接发现彼此。

    # 使用host网络模式启动ROS容器docker run -it --rm --network host \
      -v $(pwd)/workspace:/workspace \
      fastlio_noetic:v3 /bin/bash

    3.2多容器ROS通信

    在复杂的机器人系统中,通常会将不同功能拆分到不同的容器中。例如:

    • 容器A:运行FAST-LIO建图算法
    • 容器B:运行RViz可视化
    • 容器C:运行数据处理脚本
    • 容器D:运行机器人底盘驱动

    这些容器之间通过ROS话题进行通信。只要使用host网络模式,容器内的ROS节点就像在同一台机器上运行一样,可以自然地发现彼此并通信。

    # 容器1: 启动FAST-LIO
    docker exec -it fastlio_container /bin/bash
    roslaunch fast_lio mapping_avia.launch rviz:=false
    
    # 容器2: 进入同一个容器播放bag
    docker exec -it fastlio_container /bin/bash
    cd /workspace
    rosbag play data.bag
    
    # 容器3: 在另一个容器中查看话题
    docker exec -it rviz_container /bin/bash
    
    rostopic list    # 可以看到FAST-LIO发布的话题rostopic echo /Odometry    # 查看里程计数据

    3.3数据持久化

    Docker容器是临时的。删除容器后,容器内的所有数据都会丢失。因此,必须将重要数据挂载到宿主机上。

    最佳实践:

    • 代码和配置文件:通过 -v挂载到宿主机,方便修改和版本控制
    • 数据文件(bag、PCD):挂载到宿主机目录,容器只负责处理
    • 日志文件:挂载到宿主机,方便后续分析
    # 推荐的数据挂载方式
    
    docker run -it --rm \
      -v $(pwd)/workspace:/workspace \     # 数据和代码
      -v $(pwd)/bags:/bags \             # bag包
      -v $(pwd)/output:/output \         # 输出结果fastlio_noetic:v3 /bin/bash

    3.4 X11转发(GUI应用)

    ROS中很多工具是图形界面的(如RViz、rqt_graph)。在Docker中使用GUI应用需要X11转发。

    # 方法1: 使用host网络 + X11 socket挂载docker run -it --rm --network host \
      -e DISPLAY=$DISPLAY \
      -v /tmp/.X11-unix:/tmp/.X11-unix \
      -v $HOME/.Xauthority:$HOME/.Xauthority \
      osrf/ros:noetic-desktop-full /bin/bash

    # 方法2: 使用xhost允许Docker访问X11
    xhost +local:docker
    docker run -it --rm --network host \
      -e DISPLAY=$DISPLAY \
      -v /tmp/.X11-unix:/tmp/.X11-unix \
      osrf/ros:noetic-desktop-full /bin/bash

    四、实战:构建ROS SLAM容器

    下面通过一个完整的例子,展示如何从零构建一个基于ROS Noetic的激光SLAM Docker容器。

    4.1编写Dockerfile

    FROM osrf/ros:noetic-desktop-full

    ENV DEBIAN_FRONTEND=noninteractive

    # 安装PCL和Eigen
    RUN apt-get update && apt-get install -y \
        libpcl-dev \
        libeigen3-dev \
        ros-noetic-pcl-ros \
        ros-noetic-tf \
        ros-noetic-rviz \
        && rm -rf /var/lib/apt/lists/*

    # 创建工作空间RUN mkdir -p /root/fastlio_ws/src
    WORKDIR /root/fastlio_ws

    # 自动加载ROS环境RUN echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc

    WORKDIR /workspace
    CMD ["/bin/bash"]

    4.2构建和运行

    # 构建镜像docker build -t fastlio_noetic:v1 .

    # 启动容器docker run -it --rm --name fastlio_container \
      -v $(pwd)/workspace:/workspace \
      fastlio_noetic:v1 /bin/bash

    # 在容器内编译FAST-LIO(首次)cd /root/fastlio_ws/src
    git clone https://github.com/hku-mars/FAST_LIO.git
    cd /root/fastlio_ws
    catkin_make

    # 后续使用:直接启动即可,代码已内置echo "source /root/fastlio_ws/devel/setup.bash" >> ~/.bashrc

    4.3镜像版本管理

    随着项目迭代,镜像会不断演进。建议采用语义化版本管理:

    • v1.0:基础环境 + ROS Noetic + PCL

    • v1.1:加入FAST-LIO源码

    • v2.0:代码内置 + 环境自动加载 + PCD直出宿主机(开箱即用版)

    • v2.1:修复PCD保存路径问题

    # 查看镜像历史docker history fastlio_noetic:v2

    # 给镜像打标签docker tag fastlio_noetic:v2 fastlio_noetic:latest
    docker tag fastlio_noetic:v2 fastlio_noetic:20260616

    # 导出镜像用于分发docker save -o fastlio_noetic_v2.tar fastlio_noetic:v2

    # 在新机器上导入docker load -i fastlio_noetic_v2.tar

    五、常见问题与排查

    5.1容器内找不到ROS命令

    进入容器后执行roscore提示command not found。原因是没有source ROS环境。

    # 手动加载
    source /opt/ros/noetic/setup.bash
    
    # 永久生效(写入 .bashrc)
    echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc
    source ~/.bashrc

    5.2多容器间无法通信

    检查以下几点:

    • 确认所有容器使用相同的网络模式(推荐host)
    • 确认ROS_MASTER_URI指向同一个roscore
    • 确认防火墙没有阻止相关端口
    • 使用rostopic list检查话题是否可见

    5.3容器内无法显示GUI

    确保X11 socket已挂载,且宿主机已执行xhost +local:docker。如果使用SSH远程连接,需要启用X11转发:

    ssh -X user@host    # 启用X11转发

    5.4挂载目录权限问题

    Docker容器默认以root用户运行,创建的文件在宿主机上可能无法修改。解决方案:

    # 方法1: 给挂载目录开权限(简单粗暴)chmod 777 ~/workspace

    # 方法2: 在容器内创建非root用户useradd -m rosuser && su - rosuser

    # 方法3: 使用user namespace映射docker run --userns=host ...

    六、总结

    ROS和Docker的结合是机器人开发的最佳实践。ROS提供了强大的通信和工具框架,Docker提供了环境隔离和一键部署能力。两者的结合让机器人软件的开发、测试和部署变得前所未有的简单。

    关键要点:

    • ROS的核心是分布式通信。节点通过话题和服务交换数据
    • Docker的核心是容器化。将应用和依赖打包成可移植的镜像
    • ROS + Docker的关键是网络配置。使用host模式让容器间自然通信
    • 数据持久化靠挂载。重要数据一定要映射到宿主机• 镜像版本化管理。方便团队协作和部署
  • ZIP文件

    一、文件结构

    1、Local File Header — 每个压缩文件开头的本地头,包含文件名、压缩方式、CRC校验等

    2、Compressed Data — 实际的压缩数据

    3、End of Central Directory Record (EOCDR) — 最末尾的一条记录,相当于”目录索引”,记录了:
    – 这个zip里有多少个文件
    – 每个文件在zip里的偏移位置
    – 压缩前后的文件大小
    – 文件名列表


    二、解压过程

    第一步:定位EOCDR

    unzip从文件末尾往前搜索,找到End of Central Directory Record签名(PK\x05\x06)。EOCDR固定在最末尾,长度不固定但通常很小(几十到几百字节)。

    第二步:读取Central Directory

    EOCDR里记录了Central Directory(中央目录)在文件中的偏移位置和大小。unzip跳到那个位置,逐个读取每个文件的Central Directory Entry,得到:

    第三步:逐个解压文件

    对每个文件:

    • 根据偏移找到Local File Header
    • 读取压缩数据
    • 按压缩方式解压(最常用是deflate)
    • 用CRC32校验解压后的数据是否完整
    • 写入磁盘

  • Base64编码(Golang)

    Base64是一种用64个可打印字符来表示二进制数据的方法。叫64是因为用了64个字符。

    一、需求

    计算机底层是二进制(0和1),但很多传输通道只支持文本。比如:

    • 电子邮件:早期SMTP协议只支持ASCII字符,传不了二进制附件
    • URL:有些特殊字符在URL里有特殊含义(? & =),二进制数据传不了
    • HTML/CSS:在网页里直接嵌入图片,不需要额外请求
    • JSON/XML:这些文本格式不能直接塞二进制数据

    Base64就是把二进制数据”翻译”成纯文本,安全地通过这些通道。


    二、原理

    每3个字节(24位)分成4组,每组6位,查表得到4个字符。

    原始数据:Man
    二进制:01001101 01100001 01101110
    6位分组:010011 011000 010110 1110(补00)
    查表:T W F u
    结果:TWFu

    如果原始数据不是3的倍数:

    剩1个字节 → 输出2个字符 + 2个 “=” 填充

    剩2个字节 → 输出3个字符 + 1个 “=” 填充

    任何二进制数据都可以,不只是PNG
    图片:PNG、JPG、GIF、WEBP、SVG、BMP、ICO
    音频:MP3、WAV、AAC、OGG
    视频:MP4、AVI、WEBM
    文档:PDF、ZIP、EXE、DLL
    证书:PEM格式的SSL证书
    任意文件:任何你能想到的文件


    三、其他编码

    Base16(Hex):用16个字符(0-9 A-F),体积膨胀100%
    Base32:用32个字符,体积膨胀约60%
    Base64:用64个字符,体积膨胀约33%
    一张100KB的PNG图片:
    base64编码后约133KB
    解码后恢复为原始的100KB PNG

    Base85:用85个字符,体积膨胀约25%,但字符集更复杂

    Base64是体积和可读性的最佳平衡点

    四、使用示例

    package main
    
    import (
            "encoding/base64"
            "fmt"
            "os"
    )
    
    func main() {
            //1. 基础字符串编解码
            original := "Hello, 世界! "
            fmt.Printf("原始字符串: %s\n", original)
            encoded := base64.StdEncoding.EncodeToString([]byte(original))
            fmt.Printf("Base64编码: %s\n", encoded)
            decoded, err := base64.StdEncoding.DecodeString(encoded)
            if err != nil {
                    fmt.Printf("解码错误: %v\n", err)
            } else {
                    fmt.Printf("Base64解码: %s\n", string(decoded))
            }
    
            // 2. URL安全的Base64
            urlData := "user+name/test=data&key=val"
            fmt.Printf("原始数据: %s\n", urlData)
            stdEncoded := base64.StdEncoding.EncodeToString([]byte(urlData))
            urlEncoded := base64.URLEncoding.EncodeToString([]byte(urlData))
            fmt.Printf("标准Base64: %s\n", stdEncoded)
            fmt.Printf("URL Base64: %s\n", urlEncoded)
    
            // 3. 无填充的Base64
            data := "abc"
            fmt.Printf("原始数据: %s\n", data)
            fmt.Printf("带填充: %s\n", base64.StdEncoding.EncodeToString([]byte(data)))
            fmt.Printf("无填充: %s\n", base64.RawStdEncoding.EncodeToString([]byte(data)))
    
            // 4. 自定义Base64字母表
            custom := base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!")
            customEncoded := custom.EncodeToString([]byte("Hello World"))
            fmt.Printf("自定义编码: %s\n", customEncoded)
            customDecoded, _ := custom.DecodeString(customEncoded)
            fmt.Printf("自定义解码: %s\n", string(customDecoded))
    
            // 5. 模拟图片Base64编码
            // 创建一个假的PNG文件头(实际项目中替换为真实图片文件)
            fakePNG := []byte{
                    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG文件头
                    0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR
            }
            imgEncoded := base64.StdEncoding.EncodeToString(fakePNG)
            fmt.Printf("图片Base64: %s\n", imgEncoded)
    
            // 模拟HTML内嵌(取前min(30, len)个字符)
            prefixLen := 30
            if len(imgEncoded) < prefixLen {
                    prefixLen = len(imgEncoded)
            }
            fmt.Printf("HTML内嵌: <img src=\"data:image/png;base64,%s...\" />\n", imgEncoded[:prefixLen])
    
            // 6. 读真实文件并编码(如果存在)
            filename := "/tmp/test_base64.txt"
            os.WriteFile(filename, []byte("这是一个测试文件内容\n用于演示Base64编码"), 0644)
            content, err := os.ReadFile(filename)
            if err != nil {
                    fmt.Printf("读文件失败: %v\n", err)
            } else {
                    fileEncoded := base64.StdEncoding.EncodeToString(content)
                    fmt.Printf("文件名: %s\n", filename)
                    fmt.Printf("原始大小: %d 字节\n", len(content))
                    fmt.Printf("编码大小: %d 字节\n", len(fileEncoded))
                    fmt.Printf("膨胀率: %.1f%%\n", float64(len(fileEncoded)-len(content))/float64(len(content))*100)
                    fmt.Printf("Base64: %s\n", fileEncoded)
            }
            os.Remove(filename)
    
            // 7. 错误处理 - 非法Base64
            invalidBase64 := "ThisIsNot@Valid#Base64!"
            _, err = base64.StdEncoding.DecodeString(invalidBase64)
            if err != nil {
                    fmt.Printf("非法Base64 '%s' 解码错误: %v\n", invalidBase64, err)
            }
    
            // 8. 严格模式
            // 标准模式允许尾部bits非零
            loose := base64.StdEncoding
            strict := base64.StdEncoding.Strict()
            testData := "SGVsbG8=" // "Hello"
            _, err1 := loose.DecodeString(testData)
            _, err2 := strict.DecodeString(testData)
            fmt.Printf("标准模式解码 '%s': %v\n", testData, err1)
            fmt.Printf("严格模式解码 '%s': %v\n", testData, err2)
    
            // 9. 编解码长度计算
            for _, n := range []int{1, 2, 3, 4, 5, 6, 10, 100} {
                    encLen := base64.StdEncoding.EncodedLen(n)
                    decLen := base64.StdEncoding.DecodedLen(encLen)
                    fmt.Printf("输入%3d字节 → 编码%4d字节 → 解码最多%3d字节\n", n, encLen, decLen)
            }
    }
    
  • Cron定时(Golang)

    Golang的日常定时任务是一种常见需求,例如定期清理缓存、发送通知或同步数据。robfig/cron是Go社区广泛使用的定时任务库,支持秒级精度的cron表达式,并提供了安全启动、停止以及任务查询的接口。本文将从cron表达式的基本原理出发,结合一段实际的生产级代码,详细说明如何封装一个可管理、可监控的定时任务模块,并分析其中关键的并发控制与生命周期管理细节。


    cron 表达式原理

    cron表达式起源于Unix系统的cron守护进程,用于定义任务的执行时间。标准格式通常为五个字段:分、时、日、月、周几,每个字段可指定具体数字、范围(如0-5)、步长(如/5)或通配符。robfig/cron扩展了标准格式,支持可选的秒级字段,即六字段表达式:秒、分、时、日、月、周几。例如”0 30 9 * * 1-5″表示每个工作日上午9点30分执行,其中秒为0。库内部通过解析表达式生成下一次触发时间,并利用定时器(time.Ticker)在精确时刻调用注册的回调函数。其核心是一个调度循环,不断计算最近的下一个执行时间,休眠到该时刻并运行任务,然后重复此过程。这种设计保证了定时任务在单进程内的可靠触发,且不依赖外部守护进程。


    代码整体结构

    # Golang
    import (
    	"net/http"
    	"sync"
    	"time"
    
    	"github.com/gin-gonic/gin"
    	"github.com/robfig/cron/v3"
    )
    
    type CronEntry struct {
    	Cron      *cron.Cron
    	EntryID   cron.EntryID
    	MU        sync.RWMutex
    	StopOnce  sync.Once
    	Remark    string
    	RunStatus string
    }
    
    var (
    	CronJobs   []*CronEntry
    	CronJobsMu sync.RWMutex
    	MyCron     = cron.New(cron.WithSeconds())
    )
    
    func NewCronEntry(spec, remark string, job func() error) (*CronEntry, error) {
    	ce := &CronEntry{
    		Cron:      MyCron,
    		Remark:    remark,
    		RunStatus: "等待中",
    	}
    	id, err := MyCron.AddFunc(spec, func() {
    		err := job()
    		ce.MU.Lock()
    		if err != nil {
    			ce.RunStatus = "失败"
    		} else {
    			ce.RunStatus = "成功"
    		}
    		ce.MU.Unlock()
    	})
    	if err != nil {
    		return nil, err
    	}
    	ce.EntryID = id
    	MyCron.Start()
    	CronJobsMu.Lock()
    	CronJobs = append(CronJobs, ce)
    	CronJobsMu.Unlock()
    	return ce, nil
    }
    func (ce *CronEntry) GetScheduleTime() (prev, next time.Time) {
    	entry := ce.Cron.Entry(ce.EntryID) // cron.Entry() 本身是并发安全的
    	return entry.Prev, entry.Next
    }
    func (ce *CronEntry) Stop() {
    	ce.StopOnce.Do(func() {
    		ce.MU.Lock()
    		defer ce.MU.Unlock()
    		if ce.Cron == nil {
    			return
    		}
    		ce.Cron.Remove(ce.EntryID) // 先移除 job
    		ctx := ce.Cron.Stop()      // 停止调度器
    		<-ctx.Done()
    		ce.Cron = nil // 标记已停
    	})
    }
    func StopAndRemoveCronEntry(ce *CronEntry) {
    	if ce == nil {
    		return
    	}
    	ce.Stop()
    	CronJobsMu.Lock()
    	defer CronJobsMu.Unlock()
    	for i, job := range CronJobs {
    		if job == ce {
    			CronJobs = append(CronJobs[:i], CronJobs[i+1:]...)
    			break
    		}
    	}
    }
    func (ce *CronEntry) GetStatus() string {
    	ce.MU.RLock()
    	defer ce.MU.RUnlock()
    	return ce.RunStatus
    }
    func GetCronJobs(c *gin.Context) {
    	type CronJobInfo struct {
    		Job       int    `json:"job_id"`
    		LastDate  string `json:"上一次执行日期"`
    		NextDate  string `json:"下一次执行日期"`
    		Remark    string `json:"备注"`
    		RunStatus string `json:"上一次执行状态"`
    	}
    	CronJobsMu.RLock()
    	var list []CronJobInfo
    	defer CronJobsMu.RUnlock()
    	for _, entry := range CronJobs {
    		prev, next := entry.GetScheduleTime()
    		prevStr := ""
    		if !prev.IsZero() { // 如果任务还没执行过,Prev 是零值
    			prevStr = prev.Format("2006-01-02 15:04:05")
    		}
    
    		nextStr := ""
    		if !next.IsZero() {
    			nextStr = next.Format("2006-01-02 15:04:05")
    		}
    
    		list = append(list, CronJobInfo{
    			Job:       int(entry.EntryID),
    			LastDate:  prevStr,
    			NextDate:  nextStr,
    			Remark:    entry.Remark,
    			RunStatus: entry.GetStatus(),
    		})
    	}
    	c.AbortWithStatusJSON(http.StatusOK, list)
    }

    代码中定义了一个CronEntry结构体,封装了单个定时任务的完整信息:指向全局调度器的Cron指针、任务在调度器中的唯一EntryID、用于同步的读写锁MU、保证只停止一次的StopOnce、备注 Remark 以及最近一次执行状态RunStatus。全局变量MyCron是使用cron.New(cron.WithSeconds())创建的调度器实例,启用了秒级支持,同时全局切片CronJobs存储所有被管理的任务指针,并由CronJobsMu读写锁保护。这种设计将任务管理与调度器解耦:调度器负责底层触发,而用户代码通过CronEntry获取状态、控制启停。

    创建任务:NewCronEntry

    NewCronEntry函数接收cron表达式(spec)、备注和实际执行函数(返回error)作为参数。函数内部首先创建一个CronEntry实例,初始化运行状态为“等待中”。然后通过MyCron.AddFunc注册任务——这是robfig/cron的核心方法,它解析表达式并在调度器中插入一个entry,返回唯一的EntryID。注册的回调函数先执行用户提供的job,然后根据返回的err通过ce.MU.Lock()安全更新RunStatus为“成功”或“失败”。注意加锁是为了防止后续GetStatus与这里形成数据竞争,因为GetStatus 使用读锁。任务注册成功后,立即启动调度器(MyCron.Start()),这意味着一旦调用NewCronEntry,调度循环就开始运行,后续添加的任务也会被已启动的调度器处理。Start()函数是幂等的,重复调用不会导致多次启动,因此放在每个新任务创建时是安全的。最后,将新entry追加到全局CronJobs切片中,并返回该 entry指针。这里存在一个潜在问题:Start()在每次添加任务时都会调用,虽然不会重复启动,但代码风格上更适合在初始化时仅调用一次。不过考虑到后续可能动态添加任务,如此实现也能工作。

    停止单个任务:Stop方法

    Stop方法使用sync.Once确保一个任务只被停止一次,防止重复调用导致panic或资源泄漏。内部先获取写锁,检查ce.Cron是否为nil(已停止状态),然后依次执行ce.Cron.Remove(ce.EntryID) 从调度器中移除该任务entry,再调用ce.Cron.Stop()停止整个调度器。这里有一个值得注意的细节:Stop()返回一个 channel ctx,调用者需要等待<-ctx.Done()以确保调度器完全停止并清空内部计时器。但问题在于,Stop()是全局行为,它会停止所有任务,而不仅仅是当前 entry。因此,如果一个模块只想停用自己管理的单个任务,使用MyCron.Stop() 会中断其他仍在运行的任务。这应当是设计上的简化,实际项目中往往需要更细粒度的控制,例如使用cron.Cron.Remove(entryID)移除条目后,其他任务仍能继续调度。这里先移除条目再停止整个调度器,可能意味着使用者期望在stop后不再有任何任务执行。如果后续需要单独停止单个任务而不影响其他,更好的做法是在Stop中只调用Remove,而保留调度器运行。但当前代码体现了作者对整体生命周期控制的考量——当某个关键任务停止时,整个调度器也同步停止,避免因剩余任务不再被管理而产生混乱。

    状态查询与调度时间获取

    GetScheduleTime方法通过ce.Cron.Entry(ce.EntryID)获取cron库内部维护的entry 信息,其中包括Prev和Next两个time.Time值,分别表示上一次和下一次执行时间。Cron.Entry()本身是并发安全的,因此无需额外加锁。如果任务尚未执行过,Prev为零值,代码在GetCronJobs中通过IsZero()判断并格式化为空字符串,这样 HTTP响应中就不会显示不存在的日期。GetStatus使用读锁返回当前RunStatus,与回调中写锁对应,保证了数据一致性。

    全局停止与移除:StopAndRemoveCronEntry

    该函数接收一个*CronEntry,先调用其Stop方法,然后从全局CronJobs切片中移除该指针。切片删除采用“先查找再重新拼接”的方式,这种线性搜索在任务数不多时没有问题。注意这里对CronJobsMu加写锁,与NewCronEntry中添加时的锁一致,避免了并发读写切片的风险。

    HTTP 接口:GetCronJobs

    这是一个Gin处理函数,用于返回所有定时任务的信息。它首先通过读锁读取CronJobs切片,遍历每个entry,获取其调度时间和状态,组装成JSON结构。响应中包含了job_id(即EntryID)、上一次执行日期、下一次执行日期、备注和上一次执行状态。日期格式使用config.TimeNowFormat,这应是项目中预定义的时间格式字符串,例如”2006-01-02 15:04:05″。函数最后使用c.AbortWithStatusJSON返回状态码200和序列化后的列表。注意这里使用了AbortWithStatusJSON而不是c.JSON,意味着该函数可能会作为Gin的中间件调用,或者作者希望确保后续中间件不再处理。按照Gin的约定,如果是路由处理函数,更常见的写法是c.JSON,但AbortWithStatusJSON也能正常工作,只是它会设置Abort标志,阻止之后的所有中间件执行。如果该函数是路由的最后一个处理器,两者效果一致;如果有后续中间件,则会导致它们被跳过。这点需要结合具体路由设置来判断。


    总结

    在Go中基于robfig/cron封装一个可管理的定时任务模块。通过CronEntry结构体整合了任务的生命周期(创建、启动、停止、移除)和运行状态,利用读写锁和sync.Once保证了并发安全。同时,提供了HTTP接口用于监控所有任务的状态。在实际使用中,需要注意全局调度器的停止操作会对所有任务产生影响,如果希望实现更细粒度的单任务停止,可以仅调用Remove而不调用Stop()。此外,NewCronEntry中每次添加任务都调用Start()虽然可行,但建议将Start()放在初始化阶段一次调用,使逻辑更清晰。整体设计为中小规模定时任务管理提供了一个良好的模板,具备扩展性,例如可在此基础上增加持久化、错误重试或任务依赖等高级特性。

  • webRTC(Golang)

    在视频监控、工业巡检以及边缘计算等场景中,常见的一个现实问题是协议割裂:前端设备通常通过RTSP推流,而浏览器原生并不支持直接播放RTSP。这就带来了一个工程上的关键挑战——如何在不引入高延迟和复杂转码的前提下,将设备侧的视频流高效地分发到浏览器端。

    一种更直接且高效的思路,是利用WebRTC作为浏览器侧的实时传输协议,同时在服务端完成协议层的桥接,将RTSP流转换为WebRTC可消费的RTP数据流。这种方式避免了传统转码链路(例如FFMPEG+纯接口转发)带来的性能损耗,同时保留了实时性的优势。

    通过golang程序实现一个典型的视频桥接架构:上游通过RTSP拉流(通常来自摄像头或推流工具),服务端将RTP包转发到WebRTC PeerConnection,下游浏览器通过 WebRTC 实时播放视频。整体链路为:RTSP → RTP → Go服务 → WebRTC → 浏览器。


    零、在本机启一个mediamtx作为RTSP服务端,再通过ffmpeg把本机摄像头推流到服务器来模拟摄像头的RTSP流

    ./mediamtx &
    ./ffmpeg -f v4l2 -i /dev/video0 \
    -vcodec libx264 -preset veryfast -tune zerolatency \
    -f rtsp -rtsp_transport tcp \
    rtsp://test:123456@127.0.0.1:8554/test

    一、golang程序入口main中首先确定RTSP地址,并开一个Gin HTTP服务,同时维护一个clients映射,用于保存每个WebRTC客户端对应的Track:

    每个客户端并不是单独拉流,而是共享同一 RTSP输入流,服务端通过fan-out(扇出)机制将RTP包写入多个WebRTC Track,从而实现“一路输入,多路输出”

    import (
    	"github.com/bluenviron/gortsplib/v5"
    	"github.com/bluenviron/gortsplib/v5/pkg/base"
    	"github.com/bluenviron/gortsplib/v5/pkg/description"
    	"github.com/bluenviron/gortsplib/v5/pkg/format"
    	"github.com/gin-gonic/gin"
    	"github.com/pion/rtp"
    	"github.com/pion/webrtc/v3"
    )
    
    rtspURL := os.Getenv("RTSP_URL")
    if rtspURL == "" {
    	rtspURL = "rtsp://test:123456@127.0.0.1:8554/test"
    }
    
    var clientsMu sync.Mutex
    clients := map[string]*webrtc.TrackLocalStaticRTP{}

    二、HTTP部分分为三个路由:

    1)“/”路由返回播放器页面
    2)“/offer”路由处理WebRTC SDP信令交换
    3)“/stats”路由返回实时码率与包速率

    前端页面核心逻辑如下:

    浏览器创建RTCPeerConnection → 生成Offer(SDP)→ 发送给服务端 → 服务端生成Answer → 浏览器设置远端描述。需要注意的是,这里没有使用STUN/TURN(iceServers 为空),意味着该方案默认运行在内网或可直连环境,否则无法穿透NAT。

    pc=new RTCPeerConnection({iceServers:[]});
    pc.addTransceiver('video',{direction:'recvonly'});
    
    const offer=await pc.createOffer();
    await pc.setLocalDescription(offer);
    
    const resp=await fetch('/offer',{
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({sdp:offer.sdp,type:offer.type})
    });
    
    const answer=await resp.json();
    await pc.setRemoteDescription({type:answer.type,sdp:answer.sdp});

    服务端“/offer”路由处理逻辑是WebRTC的核心:

    这里做了两件关键事情:
    1)注册编解码器(H264/H265)
    2)创建 PeerConnection

    WebRTC本质上是RTP的增强版,但浏览器对编码格式要求严格,因此必须显式注册codec,否则SDP协商无法匹配。

    m := webrtc.MediaEngine{}
    _ = m.RegisterCodec(webrtc.RTPCodecParameters{
    	RTPCodecCapability: webrtc.RTPCodecCapability{
    		MimeType: webrtc.MimeTypeH264,
    		ClockRate: 90000,
    		SDPFmtpLine: "packetization-mode=1;profile-level-id=42e01f",
    	},
    	PayloadType: 96,
    }, webrtc.RTPCodecTypeVideo)
    
    api := webrtc.NewAPI(webrtc.WithMediaEngine(&m))
    pc, _ := api.NewPeerConnection(webrtc.Configuration{})

    创建Track,并绑定到PeerConnection:

    Track是WebRTC中媒体发送的抽象,本质上是RTP流的出口。这里使用TrackLocalStaticRTP,意味着可以手动写入RTP包

    track, _ := webrtc.NewTrackLocalStaticRTP(
    	webrtc.RTPCodecCapability{
    		MimeType: webrtc.MimeTypeH264,
    		ClockRate: 90000,
    	},
    	"video", "pion",
    )
    pc.AddTrack(track)

    信令交换部分:

    接收浏览器Offer → 设置远端 SDP → 生成 Answer → 返回给浏览器。GatheringCompletePromise用于等待ICE candidate收集完成,否则SDP不完整。

    offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: req.SDP}
    pc.SetRemoteDescription(offer)
    
    ans, _ := pc.CreateAnswer(nil)
    pc.SetLocalDescription(ans)
    
    <-webrtc.GatheringCompletePromise(pc)

    将Track存入clients,用于后续RTP分发:

    这里通过DESCRIBE + SETUP建立RTSP会话,并解析媒体格式(H264/H265)。

    clientsMu.Lock()
    clients[c.ClientIP()+"_"+time.Now().Format("150405")] = track
    clientsMu.Unlock()

    核心转发逻辑在OnPacketRTP:

    RTSP → 解复用 → 得到RTP包 → 广播写入所有WebRTC Track
    服务端本质上只是一个RTP转发器,并不进行转码,这带来两个重要特性:
    优点:延迟极低(基本无编码延迟)
    CPU占用极小
    架构简单稳定
    限制:浏览器必须支持该编码(通常是H264)
    RTSP输入必须与WebRTC codec兼容

    cli.OnPacketRTP(media, formaH264, func(pkt *rtp.Packet) {
    	clientsMu.Lock()
    	totalBytes += uint64(len(pkt.Payload))
    	totalPackets++
    
    	activeClients := make([]*webrtc.TrackLocalStaticRTP, 0, len(clients))
    	for _, t := range clients {
    		activeClients = append(activeClients, t)
    	}
    	clientsMu.Unlock()
    
    	for _, t := range activeClients {
    		t.WriteRTP(pkt)
    	}
    })

    统计接口“/stats”路由通过简单计数实现码率计算:

    前端每秒拉取一次,实现实时监控。

    kbps := uint64(float64(totalBytes*8) / 1024 / duration)
    pktps := uint64(float64(totalPackets) / duration)

    第一,WebRTC并不负责“获取视频”,它只负责“传输媒体流”。视频源可以来自 RTSP、文件、摄像头等。

    第二,WebRTC的关键不是API,而是:

    • SDP协商
    • ICE建连
    • RTP收发

    第三,这种架构属于典型的“边缘网关模式”:

    RTSP(设备侧协议) → WebRTC(浏览器协议)

    在工业监控、视频巡检、边缘计算中非常常见

  • Load AVG(Linux)

    Load Average】:代表机器在某一时间段内,处于“可运行状态”或“不可中断等待状态”的进程平均数量。

    三个数值统计跨度分别为:1分钟平均 5分钟平均 15分钟平均


    一台4核心的处理器的Load为 1.21 1.34 1.12

    1、CPU压力为 1.21 / 4 ≈ 0.30

    2、CPU压力为 1.34 / 4 ≈ 0.34

    3、CPU压力为 1.12 / 4 ≈ 0.28

    平均只有约1个任务在运行或等待CPU,而系统有4核心,完美胜任服务


    在Linux中,平均负载并非在每个时钟滴答时计算,而是由一个基于HZ频率设置的变量值驱动,并在每个时钟滴答时进行检测。该设置定义了内核时钟滴答速率(单位:赫兹,即每秒次数),默认值为100,对应10毫秒的滴答间隔。内核活动使用这些滴答数进行计时。具体来说,calc_load() 函数(位于loadavg.h文件,原为 sched.h)负责计算平均负载,它大约每LOAD_FREQ(5*HZ+1)个滴答运行一次,即略多于5秒。

  • TOPS和TFLOPS

    选择


    TOPS和TFLOPS代表了计算系统中不同的硬件性能。
    TOPS代表 Tera Operations Per Second
    TFLOPS 代表 Tera Floating Point Operations Per Second

    TOPS衡量芯片每秒能完成多少万亿次整数运算(整数的加法/乘法)。这对于吞吐量比最终精度更重要的任务尤为关键——例如,自动驾驶车辆中的神经处理单元(NPU)或RTX 5070显卡提供数千个TOPS以快速处理传感器数据。 相比之下,TFLOPS计算的是每秒可执行的万亿次浮点(十进制)计算。 浮点数学对于高精度工作至关重要,比如训练神经网络或科学模拟。


    如果工作目标是AI训练或任何需要高精度的任务,选择TFLOPS评分很高的GPU。
    如果是边缘或移动端的实时AI推理,选择高TOPS的NPU或GPU。

    关键区别


    精度与速度:浮点运算涉及小数且精度更高,因此优化TFLOPS(每秒万亿次浮点运算)的硬件用于对精度敏感的任务(如图形处理或气候模型)。整数运算(以TOPS,即每秒万亿次运算衡量)使用整数,更简单快速。正如资料所示,浮点运算“涉及小数点”,适用于高精度场景;而整数运算用于更简单的任务。实践中,GPU(英伟达、AMD独显等)为复杂计算强调TFLOPS,而NPU(神经网络处理器)和数字信号处理单元则强调TOPS以快速处理大量推理任务。

    硬件效率:整数运算比浮点运算简单,硬件通常能实现更高吞吐量。这就是NPU和其他推理加速器宣传极高TOPS值的原因。例如,现代PC CPU如英特尔酷睿Ultra系列或高通骁龙芯片集成了擅长整数运算的NPU,能以低功耗实现每秒数万亿次运算。相比之下,在相同制程下,GPU的浮点单元每秒运算次数少于NPU的整数单元,因为浮点运算更复杂。

    应用适用性:根据工作负载选择合适的指标。若进行AI训练或重型计算,需要高TFLOPS。数据中心GPU和加速器(英伟达A100/H100、AMD Instinct、谷歌TPU)使用FP32、FP16或BF16精度提供巨大的TFLOPS(通常达数十或数百)。TOPS与TFLOPS数值不能直接等同比较。例如,NPU的“130 TOPS”并不天然优于或劣于GPU的“65 TFLOPS”——它们反映不同类型的吞吐能力。TOPS反映通用(通常为低精度)运算,而TFLOPS反映十进制精度运算。高TOPS芯片可能擅长实时运行图像分类器,但在训练需要高浮点精度的模型时可能吃力。反之,高TFLOPS芯片能训练庞大模型,但在相同推理任务上可能功耗更高、成本更大。

  • WebSocket和HTTP Upgrade机制

    HTTP协议用于浏览器和服务器之间传输数据。每次请求是独立的,不记历史,HTTP来轮询服务器更新增加性能支出。

    WebSocket协议旨在取代现有利用HTTP作为传输层的双向通信技术,从而利用现有基础设施(代理、过滤、认证)。但WS是基于HTTP层,而不是为了取代HTTP。

    在HTTP协议中,“Upgrade”机制允许客户端告诉服务器把当前的HTTP连接升级到另一种协议。(RFC 2616)。

    WebSocket协议由开启握手和基础消息框架组成,分层叠加在TCP之上, 该技术的目标是为需要与服务器双向通信的浏览器应用提供一种机制,无需开启多个HTTP连接。WS通过HTTP升级协议握手。一条向服务器的WS握手:

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13

    一条向客户端的WS握手:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    Sec-WebSocket-Protocol: chat

    一旦客户端和服务器都发送了握手,如果握手成功,数据传输阶段才开始。这是一个双向通信通道,双方可以独立地随意发送数据。成功握手后,客户端和服务器以本规范中称为“消息”的概念单元进行数据传递。 在线路上,一条消息由一个或多个组成框架。 WebSocket消息不一定对应于特定的网络层框架,因为分段消息可能被中介合并或拆分。

    Sec-WebSocket-Protocol: chat
    用于防止脚本在网页浏览器中使用 WebSocket API 时未经授权交叉使用该服务器。 服务器会被告知生成WebSocket连接请求的脚本来源。 如果服务器不愿意接受来自该源的连接,可以通过发送相应的HTTP错误代码来拒绝连接。 该头字段由浏览器客户端发送;对于非浏览器客户端,如果在客户端上下文中合理,可能会发送该头字段。

    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    对于该头部字段,服务器必须将头部字段中的值与全局唯一标识符字符串形式“258EAFA5-E914-47DA- 95CA-C5AB0DC85B11”串联起来,这通常不会被不理解WebSocket协议的网络端点使用。 该连接的SHA-1哈希值(160位),以base64编码,随后在服务器握手中返回。

    HTTP/1.1 101 Switching Protocols
    除了101以外的任何状态码,都表示WebSocket握手尚未完成,HTTP的语义仍然适用。头部紧跟状态码。

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    WebSocket客户端会检查这些字段以查找脚本页面。如果 |Sec-WebSocket-Accept|如果缺少头字段,或者 HTTP 状态码不是 101,则连接无法建立,WebSocket帧也不会发送。


    通过升级器,http服务被升级为websocket服务,除非得到websocket握手,否则连接失败:

    #Go
    
    #An upgrader
    var upgrader = websocket.Upgrader{
    	CheckOrigin: func(r *http.Request) bool {
    		return true
    	},
    }
    
    var wsClients   = make(map[*websocket.Conn]bool)
    
    func wsHandler(c *gin.Context) {
    	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    	if err != nil {
    		return
    	}
    	defer conn.Close()
    	wsClients[conn] = true
    	for {
    		_, _, err := conn.ReadMessage()
    		if err != nil {
    			delete(wsClients, conn)
    			break
    		}
    	}
    }
    
    func main() {
            r := gin.Default()
            r.GET("/ws", wsHandler)
    }