(02)Cartographer源码无死角解析-(75) 2D后端优化→整体复盘,理解后端优化核心

news2024/9/23 19:23:12

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下:
(02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

再前面的博客中,本人有提出过自己对于后端优化的讲解如下:

个人理解 : \color{red}个人理解: 个人理解: 这里,本人抛砖引为引玉,谈一下个人对后端优化的理解。首先后端优化是从一个整理来考虑的,其基于 global 系。利用到到子图与子图之间的变换关系,可是在前端(基于local系)过程中,子图位姿是单个相对于 local系 的位姿,可以说多个子图之间他们是没有直接联系到一起的。除了子图,节点也一样,前端的节点都是根据活跃子图计算出相对于 local系 的位姿,但是却没有和前面已经完成的子图联系到一起,也显得比较孤立。

但是在后端优化中,这些都被考虑了进去,其估算出来的节点位姿,不仅希望其在活跃的子图位姿比较准确,还希望其相对于其他子图的位姿也比较准确,并且子图与子图之间的位姿也比较准确。同时呢,还要这些准确的位姿传递给到前端,因为全局优化是以一定频率持续优化,基于前面的优化再优化,而不是只优化一次。

优化的过程,就是对位姿进行调整,让约束(精确解)不变的情况下,对节点与子图的global位姿进行调整优化,优化过的位姿节点位姿与子图global位姿分别保存在 PoseGraph2D::data_::trajectory_nodes 与 PoseGraph2D::data_::global_submap_poses_2d 之中。另外,后端优化中还可以结合了 OdometryData、FixedFramePoseData、ImuData、LandmarkData 等数据对位姿进行优化,也可以理解为约束。

总的来说,就是保证约束不变的前提下,对其他的位姿进行调整,就好像有一个渔网,关系错综复杂,现在我们要把这个网展开。应该怎么做呢?可以找其中有特点的地方,比如四个角,或者之前打过标识的地方,先把这些位置固定好,固定之后再一点一点的调整其他的位置。直到把网铺平,展开为止。

总结: \color{red}总结: 总结: 后端优化是在图优化的框架下,通过调整节点和子图的位姿,以最大程度地满足各种约束条件,使得整个系统的位姿估计更加准确和一致。

在后端优化中,节点和子图的位姿是通过最小化误差函数来进行调整的。这个误差函数可以包含多种约束,如里程计测量、IMU数据、固定的参考帧位姿、特征点观测等等。优化的目标是找到一组位姿,使得所有的约束都得到满足,并且使得整个系统的一致性最好。

在优化的过程中,为了保证约束的准确性,需要对位姿进行调整,同时也要考虑到不同节点和子图之间的关系,以及它们与前端位姿估计的一致性。这样可以使得后端优化得到的位姿既在局部子图中准确,也在全局子图中准确,并且能够传递给前端进行实时的位姿更新。

上一篇博客中,将优化过程比喻为展开一个错综复杂的渔网是一种形象的描述。通过固定一些已知的位置(类似于固定特定的节点或子图),然后逐步调整其他位置(调整其他节点和子图的位姿),最终将整个系统的位姿展开为一个一致的整体。不过描述中可能存在一些技术上的细节或理解上的差异。

二、残差优化效果

1、基于节点与子图(子图内,子图间)约束的残差
2、基于Landmark的残差
3、基于odometry里程计的残差
4、基于节点local系下的残差
5、基于GPS数据的残差

( 1 ): \color{blue}(1): 1): 对于第1种约束,其主要用于优化global系下的节点、子图的位姿。

( 2 ): \color{blue}(2): 2): 对于第2种约束,其主要用于优化global系下两个节点之间的插值位姿,进而达到优化两节点的位姿。

( 3 ): \color{blue}(3): 3): 对于第3种约束,主要对单个节点位姿进行优化。

( 4 ): \color{blue}(4): 4): 对相邻的两个节点位姿进行优化

( 5 ): \color{blue}(5): 5):对单个节点进行优化

注意: \color{blue}注意: 注意:通过循环以及插值的方式,上诉都可以实现对多个节点的位姿优化。

三、优化图示

在这里插入图片描述
为了象形表达优化的过程,本人把子图内约束、子图间约束、local约束画了出来,其优化的过程就是保持浅绿、深绿、红色线段不变,逐步调整黑色线段的过程。上图种,把子图的原点绘画在右上角本人目前是无法确定是否正确。后续有机会再为大家分析 Cartographer 的各种坐标系的朝向,不过子图的x,y轴理论上来说应该是与 local 系的x,y轴 平行的,方向目前不是很确定。另外关于odometry里程计、GPS、Landmark 的数据都没有绘画,因为似乎不好共同体现出来,有兴趣的朋友可以单独绘画一下。

四、对外接口

后端优化的变量,主要在 OptimizationProblem2D::Solve() 函数的末尾体现出来,

  // 将优化后的所有数据进行更新 Store the result.
  for (const auto& C_submap_id_data : C_submaps) {
    submap_data_.at(C_submap_id_data.id).global_pose =
        ToPose(C_submap_id_data.data);
  }
  for (const auto& C_node_id_data : C_nodes) {
    node_data_.at(C_node_id_data.id).global_pose_2d =
        ToPose(C_node_id_data.data);
  }
  for (const auto& C_fixed_frame : C_fixed_frames) {
    trajectory_data_.at(C_fixed_frame.first).fixed_frame_origin_in_map =
        transform::Embed3D(ToPose(C_fixed_frame.second));
  }
  for (const auto& C_landmark : C_landmarks) {
    landmark_data_[C_landmark.first] = C_landmark.second.ToRigid();
  }

从这里可以看出,后端主要优化的变量就是子图、节点在globl系下的位姿,GPS初始帧在global系下的位姿,以及 landmark 数据,这些数据虽然更新,但是其是存储于 c 的私有成员之中,如何传递到外部呢?首先在 PoseGraph2D::RunOptimization() 函数中,在调用 optimization_problem_->Solve() 之后,执行了如下代码:

  // 获取优化后的结果
  // submap_data的类型是 MapById<SubmapId, optimization::SubmapSpec2D> 
  const auto& submap_data = optimization_problem_->submap_data();

  // node_data的类型是 MapById<NodeId, NodeSpec2D>
  // node_data是优化后的所有节点的新位姿
  const auto& node_data = optimization_problem_->node_data();

  // 更新轨迹内的节点位置
  for (const int trajectory_id : node_data.trajectory_ids()) {

    // Step: 根据优化后的结果对data_.trajectory_nodes的global_pose进行更新
    for (const auto& node : node_data.trajectory(trajectory_id)) {
	......

  } // end for trajectory_id

  // 更新data_.landmark_nodes
  for (const auto& landmark : optimization_problem_->landmark_data()) {
    data_.landmark_nodes[landmark.first].global_landmark_pose = landmark.second;
  }

  // 更新所有submap的位姿
  data_.global_submap_poses_2d = submap_data;

也就是说,其已经将部分数据传递到了 PoseGraph2D 之中,如 landmark 数据,子图global位姿以及节点的 global 位姿。但是这里依旧还存在一个问题,无论 PoseGraph2D 还是 OptimizationProblem2D 其是属于src/cartographer 下的代码,这些即使有了这些接口数据,又是如何与 src/cartographer_ros 的代码联系到一起的呢?在src/cartographer_ros/cartographer_ros/cartographer_ros/map_builder_bridge.cc 文件中搜索,可以找到如下代码,后端优化与被 cartographer_ros 中代码查询或者调用的过程。

MapBuilderBridge::GetSubmapList()
	map_builder_->pose_graph()->GetAllSubmapPoses();
	
MapBuilderBridge::GetLocalTrajectoryData()
	map_builder_->pose_graph()->GetLocalToGlobalTransform(trajectory_id),
	
MapBuilderBridge::HandleTrajectoryQuery
	map_builder_->pose_graph()->GetTrajectoryNodePoses()

MapBuilderBridge::GetTrajectoryNodeList()
	map_builder_->pose_graph()->GetTrajectoryNodePoses()
	map_builder_->pose_graph()->constraints()
	
MapBuilderBridge::GetLandmarkPosesList()
	map_builder_->pose_graph()->GetLandmarkPoses()
	
MapBuilderBridge::GetConstraintList()
	pose_graph()->GetTrajectoryNodePoses()
	map_builder_->pose_graph()->GetAllSubmapPoses()
	map_builder_->pose_graph()->constraints()

五、优化过程不变量

在后端优化过程中,有些参数是一致保持不变的,首先前端相关的数据,如点云、节点local位姿,子图local位姿,其虽然有传递给后端,但是后端优化过程中并没有对这些前端的位姿以及数据进行改变,可以说他们是相互独立的。

除此之外即使有些被传递到了后端,也没有被修改,如点云数据、被冻结轨迹的子图,以及节点位姿等。对于第一个子图的位姿,无论其对应轨迹是否冻结,其在 global 坐标系下的位姿不会被优化所改变,且与 local 系的第一个子图位姿保持一致。

六、回环检测

PoseGraph2D::ComputeConstraintsForNode() 函数计算子图间约束的代码:

  // Step: 当前节点与所有已经完成的子图进行约束的计算---实际上就是回环检测
  for (const auto& submap_id : finished_submap_ids) {
    // 计算旧的submap和新的节点间的约束
    ComputeConstraint(node_id, submap_id);
  }

  // Step: 计算所有节点与刚完成子图间的约束---实际上就是回环检测
  if (newly_finished_submap) {
    const SubmapId newly_finished_submap_id = submap_ids.front();
    // We have a new completed submap, so we look into adding constraints for
    // old nodes.
    for (const auto& node_id_data : optimization_problem_->node_data()) {
      const NodeId& node_id = node_id_data.id;
      // 刚结束的子图内部的节点, 不再与这个子图进行约束的计算
      if (newly_finished_submap_node_ids.count(node_id) == 0) {
        // 计算新的submap和旧的节点间的约束
        ComputeConstraint(node_id, newly_finished_submap_id);
      }
    }
  }

本质上来说就是回环检测,为什么呢?首先来说局部约束,在建图的时候会调用到 ConstraintBuilder2D::MaybeAddConstraint() 函数,就是建立节点与一定范围之间的约束,如果之前机器人经过某个区域,那么该区域就是 就那么机器再次靠近该区域时,其距离肯定比较近的,那么就会建立约束,用于后端优化。全局回环就是节点子图建立约束。

核心理解 : \color{red}核心理解: 核心理解: 节点与子图建立约束的时候,其先构建多分辨率地图,然后通过分支定界算法进行粗匹配,最后使用 Ceres 进行精匹配。总的来说,局部约束本质就是局部匹配,全局约束就是全局匹配,前端中使用的匹配,是在节点所属子图内进行匹配。

七、子图修剪

该部分的代码,起始主要与重定位模式相关,这里进行一个初步的讲解,后续会单独出一篇博客进行详细的分析。首先在 PoseGraph2D::HandleWorkQueue() 函数中可以看到如下代码段:

    TrimmingHandle trimming_handle(this);
    // 进行子图的裁剪, 如果没有裁剪器就不裁剪
    for (auto& trimmer : trimmers_) {
      trimmer->Trim(&trimming_handle); // PureLocalizationTrimmer::Trim()
    }
    // 如果裁剪器处于完成状态, 就把裁剪器删除掉
    trimmers_.erase(
        // c++11: std::remove_if 如果回调函数函数返回真,则将当前所指向的参数移到尾部,返回值是被移动区域的首个元素
        std::remove_if(trimmers_.begin(), trimmers_.end(),
                       [](std::unique_ptr<PoseGraphTrimmer>& trimmer) {
                         return trimmer->IsFinished(); // 调用PureLocalizationTrimmer::IsFinished()
                       }),
        trimmers_.end());

该段代码主要的作用是对子图进行修剪,直白的说就是移除那些比较小的子图,或者与其他子图重叠部分很少的子图,认为这些子图没有太大的意义,所以从 PoseGraph2D::data_.submap_data 中删除这些子图,后续节点不与这些子图建立约束。

trimmer->Trim(&trimming_handle); 这句代码,起始最终会调用到 PoseGraph2D::TrimmingHandle::TrimSubmap() 这个函数,代码注释如下(简单看一下,后续重定位章节会进行重点分析):

// 删除指定id的子图, 并删除相关的约束,匹配器,与节点
void PoseGraph2D::TrimmingHandle::TrimSubmap(const SubmapId& submap_id) {
  // TODO(hrapp): We have to make sure that the trajectory has been finished
  // if we want to delete the last submaps.
  // 只有kFinished状态的子图才能够裁剪
  CHECK(parent_->data_.submap_data.at(submap_id).state ==
        SubmapState::kFinished);

  // Compile all nodes that are still INTRA_SUBMAP constrained to other submaps
  // once the submap with 'submap_id' is gone.
  // We need to use node_ids instead of constraints here to be also compatible
  // with frozen trajectories that don't have intra-constraints.
  // 获取除submap_id外的所有子图的所有节点的id
  std::set<NodeId> nodes_to_retain;
  for (const auto& submap_data : parent_->data_.submap_data) {
    if (submap_data.id != submap_id) {
      nodes_to_retain.insert(submap_data.data.node_ids.begin(),
                             submap_data.data.node_ids.end());
    }
  }

  // Remove all nodes that are exlusively associated to 'submap_id'.
  // 找到在submap_id的子图内部同时不在别的子图内的节点, 这些节点需要删除
  std::set<NodeId> nodes_to_remove;
  // c++11: std::set_difference 求set的差集, 在first_set中出现, 在second_set中不出现的元素
  std::set_difference(parent_->data_.submap_data.at(submap_id).node_ids.begin(),
                      parent_->data_.submap_data.at(submap_id).node_ids.end(),
                      nodes_to_retain.begin(), nodes_to_retain.end(),
                      std::inserter(nodes_to_remove, nodes_to_remove.begin()));

  // Remove all 'data_.constraints' related to 'submap_id'.
  {
    // Step: 1 删除submap_id相关的约束
    std::vector<Constraint> constraints;
    for (const Constraint& constraint : parent_->data_.constraints) {
      if (constraint.submap_id != submap_id) {
        constraints.push_back(constraint);
      }
    }
    parent_->data_.constraints = std::move(constraints);
  }

  // Remove all 'data_.constraints' related to 'nodes_to_remove'.
  // If the removal lets other submaps lose all their inter-submap constraints,
  // delete their corresponding constraint submap matchers to save memory.
  {
    std::vector<Constraint> constraints;
    std::set<SubmapId> other_submap_ids_losing_constraints;
    // Step: 2 删除与nodes_to_remove中节点相关联的约束, 并对submap_id进行标记
    for (const Constraint& constraint : parent_->data_.constraints) {
      if (nodes_to_remove.count(constraint.node_id) == 0) {
        constraints.push_back(constraint);
      } else {
        // A constraint to another submap will be removed, mark it as affected.
        other_submap_ids_losing_constraints.insert(constraint.submap_id);
      }
    }
    parent_->data_.constraints = std::move(constraints);
    // Go through the remaining constraints to ensure we only delete scan
    // matchers of other submaps that have no inter-submap constraints left.
    // 检查剩余的约束以确保我们只删除没有子图间约束的其他子图的扫描匹配器
    for (const Constraint& constraint : parent_->data_.constraints) {
      if (constraint.tag == Constraint::Tag::INTRA_SUBMAP) {
        continue;
      } 
      // 只要other_submap_ids_losing_constraints内submap_id还存在其他的子图间约束
      // 就把这个子图id从other_submap_ids_losing_constraints中删除, 可以留着
      else if (other_submap_ids_losing_constraints.count(
                     constraint.submap_id)) {
        // This submap still has inter-submap constraints - ignore it.
        other_submap_ids_losing_constraints.erase(constraint.submap_id);
      }
    }
    // Delete scan matchers of the submaps that lost all constraints.
    // TODO(wohe): An improvement to this implementation would be to add the
    // caching logic at the constraint builder which could keep around only
    // recently used scan matchers.
    // Step: 3 删除这些子图id的匹配器
    for (const SubmapId& submap_id : other_submap_ids_losing_constraints) {
      parent_->constraint_builder_.DeleteScanMatcher(submap_id);
    }
  }

  // Mark the submap with 'submap_id' as trimmed and remove its data.
  CHECK(parent_->data_.submap_data.at(submap_id).state ==
        SubmapState::kFinished);
  // Step: 4 删除这个子图的指针
  parent_->data_.submap_data.Trim(submap_id);
  // Step: 5 删除这个子图的匹配器, 与多分辨率地图
  parent_->constraint_builder_.DeleteScanMatcher(submap_id);
  // Step: 6 删除optimization_problem_中的这个子图
  parent_->optimization_problem_->TrimSubmap(submap_id);

  // We have one submap less, update the gauge metrics.
  kDeletedSubmapsMetric->Increment();
  if (parent_->IsTrajectoryFrozen(submap_id.trajectory_id)) {
    kFrozenSubmapsMetric->Decrement();
  } else {
    kActiveSubmapsMetric->Decrement();
  }

  // Remove the 'nodes_to_remove' from the pose graph and the optimization
  // problem.
  // Step: 7 删除节点
  for (const NodeId& node_id : nodes_to_remove) {
    parent_->data_.trajectory_nodes.Trim(node_id);
    parent_->optimization_problem_->TrimTrajectoryNode(node_id);
  }
}

八、结语

到目前为止,关于 Cartographer 数据处理,前端,后端都讲解完成了,也就是说,目前已经能够获得比较准确的地图以及位姿了。那么这些位姿外界 cartographer_ros 是如何获取到的,且进行可视化发布到 rviz 的呢? 接下来,我们会从 src/cartographer 跳出来,回到最初的 cartographer_ros 部分继续分析源码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/732368.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IIC接口隔离电路ISO

IIC为例 为什么需要隔离—隔离电路电源和数据线之间的隔离 隔离电性干扰&#xff0c;增强抗干扰能力&#xff0c;保护隔离总线iic确保系统的稳定型和可靠性。避免电源串扰以及避免数字信号对模拟信号的干扰&#xff0c;就需要总线进行信号隔离。 就IIC而言&#xff0c;让master…

SQL-每日一题【584.寻找用户推荐人】

题目 给定表 customer &#xff0c;里面保存了所有客户信息和他们的推荐人。 写一个查询语句&#xff0c;返回一个客户列表&#xff0c;列表中客户的推荐人的编号都 不是 2。 对于上面的示例数据&#xff0c;结果为&#xff1a; 解题思路 1.题目要求查询列表中客户的推荐人的…

【剑指offer】4.从尾到头打印链表(java)

文章目录 从尾到头打印链表描述示例1示例2思路完整代码 从尾到头打印链表 描述 输入一个链表的头节点&#xff0c;按链表从尾到头的顺序返回每个节点的值&#xff08;用数组返回&#xff09;。 如输入{1,2,3}的链表如下图: 返回一个数组为[3,2,1] 0 < 链表长度 < 100…

分享 Eclipse 常用插件(持续更新)

NO1&#xff1a; sts 点评&#xff1a;不用多说了&#xff0c;springboot/springcloud 家族必备插件 NO2&#xff1a;Eclipse Color Theme 点评&#xff1a;是时候换个主题了&#xff01; 总有一款适合你&#xff01; 这里贴上按照步骤&#xff1a; 通过 help-> Eclips…

7.5_2散列查找(下)

不同于拉链法&#xff0c;他是实实在在存储在这个位置&#xff0c;而不是用指针去指向。 发生冲突时&#xff0c;每次既可以往后探测相邻的下一个单元是否为空。 发生冲突了 序号7也有其他元素了&#xff0c;所以只能存到序号为8的位置上 也就是或线性探测法如果发生冲突了&am…

C++多线程学习(十二、特殊的原子类型atomic_flag,自旋锁)

目录 atomic_flag 自旋锁 自旋锁与互斥锁的不同 1. 等待方式不同&#xff1a; 2. 资源消耗不同&#xff1a; 3. 适用场景不同&#xff1a; 简单案例 其他的原子类型是可以通过is_lock_free()来判定是否无锁 atomic_flag atomic_flag&#xff1a;是无锁的 atomic_flag的…

25岁的我被辞了转行做软件测试,5个月靠体系化自学“跳进”阿里

前言 大学学的是物流管理&#xff0c;毕业之后到现在的两年时间内也是做的物流相关的岗位&#xff0c;但是现在想做个有技术含量的工作&#xff0c;所以想学软件测试&#xff0c;大学的时候学过VB&#xff0c;前几天自己网上看了讲软件测试入门的视频觉得还能听懂所以我说一下…

Kubernetes_KubeProxy_Service找到Pod与DNS解析Service/Pod

文章目录 前言一、Service找到Pod(Iptables)二、Service找到Pod(IPVS)2.1 IPVS模式原理2.2 IPVS模式实践修改为 IPVS 模式 之前修改为 IPVS 模式之中修改为 IPVS 模式之后 三、Service和Pod的DNS域名尾声 前言 一、Service找到Pod(Iptables) 在前面的文章中&#xff0c;我们已…

【动态规划算法】第十题:174.地下城游戏

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你…

Java中如何定义一个线程工厂?

线程工厂官方文档&#xff1a; 在Java中&#xff0c;可以通过实现ThreadFactory接口来定义一个线程工厂。线程工厂用于创建新的线程对象&#xff0c;并可以自定义线程的属性、命名规则等。 下面是一个简单的示例代码&#xff0c;展示如何定义一个线程工厂&#xff1a; import…

路径规划算法:基于学校优化优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于学校优化优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于学校优化优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

抖音账号矩阵系统|源码|开源代码独立部署难度-开发者分享

抖音账号矩阵系统&#xff0c;短视频账号矩阵系统源码&#xff0c; 短视频矩阵是一种常见的视频编码标准&#xff0c;它通过将视频分成多个小块并对每个小块进行压缩来实现高效的视频传输。短视频多账号矩阵系统&#xff0c;通过多账号一键授权管理的方式&#xff0c;为运营人员…

学生信息管理系统——C语言版

C语言版学生信息管理系统 一&#xff0c;开发环境 操作系统&#xff1a;windows10, windows11, linux, mac等。开发工具&#xff1a;Qt, vscode, visual studio等开发语言&#xff1a;c语言 二&#xff0c;功能需求 用户界面: 提供一个简洁的文本界面&#xff0c;用户可以通…

EA编写的十大注意事项:避免常见的错误和陷阱

EA编写是一项复杂而有挑战性的任务&#xff0c;需要投资者具备一定的编程技能及丰富的交易经验。下面我将分享EA编写过程中的十大注意事项&#xff0c;以及如何避免常见的错误和陷阱。 设定明确的目标和策略 在编写EA之前&#xff0c;一定要明确交易目标和策略。这包括交易工具…

从OVF矢量场文件中获取磁斯格明子的位置和半径的粗略方法(trace skyrmion)

文章目录 前言一、使用oommf的avf2odt命令行程序获取斯格明子中心位置的示例二、当磁体系的单个xy平面层仅有一个斯格明子的情况1.读取所有磁化文件中的指定磁化分量2.筛选出每一个xy平面层中位于磁化分量阈值范围内的单元格3.计算组成磁结构的所有单元格的平均坐标和平均距离 …

今年想考CISP,速看这篇

关键词&#xff1a;CISP考试&#xff0c;CISP认证&#xff0c;CISP报名条件&#xff0c;CISP培训&#xff0c;CISP考试费用&#xff0c;CISP考试大纲 注册信息安全专业人员&#xff08;Certified Information Security Professional&#xff0c;简称“CISP"&#xff09;&…

如何处理Long类型精度丢失问题?

如何处理Long类型精度丢失问题?_拒绝画大饼的博客-CSDN博客 解决问题&#xff1a;long型数据精度丢失_long精度丢失_YOLO小蜗的博客-CSDN博客 原因分析 通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致&#xff0c;这是怎么回事呢&#xff1f; 在…

Node中的模块引擎EJS模块渲染

1.导入 const ejsrequire("ejs") 2.声明数组 const group["张三","李四","王二","麻子"] 3.EJS实现 let resultejs.render(<ul> <% group.forEach(item>{ %> <li><%item%></li> <% }) …

ionic实现滑动的三种方式

ionic实现滑动的三种方式 在移动端受屏幕大小所限&#xff0c;展示内容很多的时候&#xff0c;就要使部分区域进行滑动。本文展示项目中所有到的几种方式&#xff0c;大家可以看自己的需求选择合适的滑动方式。实现滑动的基本原理&#xff0c;有两个容器A、B,假如A在外层&#…

[element-ui] el-descriptions站位,换行用法

使用element-ui组件el-descriptions element-ui组件el-descriptions官方文档 需要将el-descriptions-item换行用法&#xff1a;使用span &#xff08;1&#xff09;span 代表占位&#xff0c;当span 的值大于 column的值&#xff0c;就会自动换一行 &#xff08;2&#xff0…