大家好,我已经把CSDN上的博客迁移到了知乎上,欢迎大家在知乎关注我的专栏慢慢悠悠小马车https://zhuanlan.zhihu.com/duangduangduang。希望大家可以多多交流,互相学习。


目录

1. 处理流程

2. fallback场景

3. pull over场景

4. lane change 场景

 5. regular场景(lane borrow)

6. 结尾


PathBoundsDecider是负责计算道路上可行区域边界的类,其产生的结果如下面代码所示,是对纵向s等间隔采样、横向d对应的左右边界。这样,即限定了s的范围,又限定了d的范围。(注:代码中大多用小写L代表横向,我为了容易辨别,使用d)PathBoundsDecider类继承自Decider类,而Decider类继承自Task类,所以PathBoundsDecider也是Apollo中众多任务的其中之一。文件路径是 apollo\modules\planning\tasks\deciders\path_bounds_decider\path_bounds_decider.cc,基于apollo 5.5.0版本。

// PathBoundPoint contains: (s, l_min, l_max).
using PathBoundPoint = std::tuple<double, double, double>;
// PathBound contains a vector of PathBoundPoints.
using PathBound = std::vector<PathBoundPoint>;

1. 处理流程

PathBoundsDecider类的主要工作在Process()中完成,在该函数中,分4种场景对PathBound进行计算,按处理的顺序分别是fallback、pull over、lane change、regular,regular场景根据是否lane borrow又分为no borrow、left borrow、right borrow,这3种子场景是在一个函数内处理的。之所以要不同场景对应不同的处理方式,我认为是在不同的场景下,自车会有不同的决策和行为,因此要考察的纵向和横向的范围就不一样,在计算时也要考虑不同的环境模型上下文。要注意的是,fallback对应的PathBound一定会生成,其余3个场景只有1个被激活,成功生成PathBound后退出函数。

Status PathBoundsDecider::Process(Frame* const frame,
                                  ReferenceLineInfo* const reference_line_info) {
  ...
  // Initialization.
  InitPathBoundsDecider(*frame, *reference_line_info);
  // Generate the fallback path boundary.
  PathBound fallback_path_bound;
  Status ret = GenerateFallbackPathBound(*reference_line_info, &fallback_path_bound);
  ...
  // Update the fallback path boundary into the reference_line_info.
  std::vector<std::pair<double, double>> fallback_path_bound_pair;
  for (size_t i = 0; i < fallback_path_bound.size(); ++i) {
    fallback_path_bound_pair.emplace_back(std::get<1>(fallback_path_bound[i]),
                                          std::get<2>(fallback_path_bound[i]));
  }
  candidate_path_boundaries.emplace_back(std::get<0>(fallback_path_bound[0]),
                                         kPathBoundsDeciderResolution,
                                         fallback_path_bound_pair);
  candidate_path_boundaries.back().set_label("fallback");
  //1.先生成fallback界限,存入candidate_path_boundaries,并标记为“fallback”

  // If pull-over is requested, generate pull-over path boundary.
  ...
  if (plan_pull_over_path) {
    PathBound pull_over_path_bound;
    Status ret = GeneratePullOverPathBound(*frame, *reference_line_info,
                                           &pull_over_path_bound);
    if (!ret.ok()) {
      AWARN << "Cannot generate a pullover path bound, do regular planning.";
    } else {
      // Update the fallback path boundary into the reference_line_info.
      std::vector<std::pair<double, double>> pull_over_path_bound_pair;
      for (size_t i = 0; i < pull_over_path_bound.size(); ++i) {
        pull_over_path_bound_pair.emplace_back(std::get<1>(pull_over_path_bound[i]),
                                               std::get<2>(pull_over_path_bound[i]));
      }
      candidate_path_boundaries.emplace_back(std::get<0>(pull_over_path_bound[0]),
          kPathBoundsDeciderResolution, pull_over_path_bound_pair);
      candidate_path_boundaries.back().set_label("regular/pullover");
      //2.生成pullover界限,存入candidate_path_boundaries,并标记为“regular/pullover”      
      reference_line_info->SetCandidatePathBoundaries(
          std::move(candidate_path_boundaries));
      return Status::OK();
    }
  }

  // If it's a lane-change reference-line, generate lane-change path boundary.
  if (FLAGS_enable_smarter_lane_change && reference_line_info->IsChangeLanePath()) {
    PathBound lanechange_path_bound;
    Status ret = GenerateLaneChangePathBound(*reference_line_info,
                                             &lanechange_path_bound);    
    ...
    // Update the fallback path boundary into the reference_line_info.
    std::vector<std::pair<double, double>> lanechange_path_bound_pair;
    for (size_t i = 0; i < lanechange_path_bound.size(); ++i) {
      lanechange_path_bound_pair.emplace_back(std::get<1>(lanechange_path_bound[i]),
                                              std::get<2>(lanechange_path_bound[i]));
    }
    candidate_path_boundaries.emplace_back(std::get<0>(lanechange_path_bound[0]),
      kPathBoundsDeciderResolution, lanechange_path_bound_pair);
    candidate_path_boundaries.back().set_label("regular/lanechange");
    //3.生成lanechange界限,存入candidate_path_boundaries,并标记为“regular/lanechange”     
    reference_line_info->SetCandidatePathBoundaries(
        std::move(candidate_path_boundaries));    
    return Status::OK();
  }

  // Generate regular path boundaries.
  std::vector<LaneBorrowInfo> lane_borrow_info_list;
  lane_borrow_info_list.push_back(LaneBorrowInfo::NO_BORROW);
  if (reference_line_info->is_path_lane_borrow()) {
    const auto& path_decider_status =
        PlanningContext::Instance()->planning_status().path_decider();
    for (const auto& lane_borrow_direction :
         path_decider_status.decided_side_pass_direction()) {
      if (lane_borrow_direction == PathDeciderStatus::LEFT_BORROW) {
        lane_borrow_info_list.push_back(LaneBorrowInfo::LEFT_BORROW);
      } else if (lane_borrow_direction == PathDeciderStatus::RIGHT_BORROW) {
        lane_borrow_info_list.push_back(LaneBorrowInfo::RIGHT_BORROW);
      }
    }
  }

  // Try every possible lane-borrow option
  for (const auto& lane_borrow_info : lane_borrow_info_list) {
    PathBound regular_path_bound;
    std::string blocking_obstacle_id = "";
    std::string borrow_lane_type = "";
    Status ret = GenerateRegularPathBound(
        *reference_line_info, lane_borrow_info, &regular_path_bound,
        &blocking_obstacle_id, &borrow_lane_type);
    ...
    // Update the path boundary into the reference_line_info.
    std::vector<std::pair<double, double>> regular_path_bound_pair;
    for (size_t i = 0; i < regular_path_bound.size(); ++i) {
      regular_path_bound_pair.emplace_back(std::get<1>(regular_path_bound[i]),
                                           std::get<2>(regular_path_bound[i]));
    }
    candidate_path_boundaries.emplace_back(std::get<0>(regular_path_bound[0]),
                                           kPathBoundsDeciderResolution,
                                           regular_path_bound_pair);
    std::string path_label = "";
    switch (lane_borrow_info) {
      case LaneBorrowInfo::LEFT_BORROW:
        path_label = "left";
        break;
      case LaneBorrowInfo::RIGHT_BORROW:
        path_label = "right";
        break;
      default:
        path_label = "self";        
        break;
    }    
    //4.生成laneborrow界限,存入candidate_path_boundaries,并标记为“regular/...”
    candidate_path_boundaries.back().set_label(
        absl::StrCat("regular/", path_label, "/", borrow_lane_type));
    candidate_path_boundaries.back().set_blocking_obstacle_id(
        blocking_obstacle_id);
  }

  reference_line_info->SetCandidatePathBoundaries(std::move(candidate_path_boundaries));  
  return Status::OK();
}

2. fallback场景

接下来,我们分析每种场景下的处理细节。fallback是其他3种场景计算PathBound失败时的备选,只考虑自车信息和静态道路信息,不考虑静态障碍物。因此,speed decider负责让自车在障碍物前停车。

Status PathBoundsDecider::GenerateFallbackPathBound(
    const ReferenceLineInfo& reference_line_info, PathBound* const path_bound) {
  // 1. Initialize the path boundaries to be an indefinitely large area.
  if (!InitPathBoundary(reference_line_info, path_bound)) { ... }
  // 2. Decide a rough boundary based on lane info and ADC's position
  std::string dummy_borrow_lane_type;
  if (!GetBoundaryFromLanesAndADC(reference_line_info,
                                  LaneBorrowInfo::NO_BORROW, 0.5, path_bound,
                                  &dummy_borrow_lane_type)) { ... }
  return Status::OK();
}

InitPathBoundary()在合理的s范围内,以kPathBoundsDeciderResolution为采样间隔对s采样,将每个采样点处的横向边界【右边界,左边界】设定为 【最小值,最大值】。

bool PathBoundsDecider::InitPathBoundary(
    const ReferenceLineInfo& reference_line_info, PathBound* const path_bound) {
  ...
  // Starting from ADC's current position, increment until the horizon, and
  // set lateral bounds to be infinite at every spot.
  for (double curr_s = adc_frenet_s_; curr_s < std::fmin(adc_frenet_s_ +
       std::fmax(kPathBoundsDeciderHorizon,
                 reference_line_info.GetCruiseSpeed() * FLAGS_trajectory_time_length),
       reference_line.Length());
       curr_s += kPathBoundsDeciderResolution) {
    path_bound->emplace_back(curr_s, std::numeric_limits<double>::lowest(),
                             std::numeric_limits<double>::max());
  }
  ...
}

GetBoundaryFromLanesAndADC()则包含了根据自车信息、车道信息计算PathBound的具体细节。首先根据当前位置获取当前车道的左右宽度,然后根据左右借道获取相邻车道的宽度(当然,fallback设定不借道),最后综合各因素,更新PathBound。

bool PathBoundsDecider::GetBoundaryFromLanesAndADC( ... ) {
  // Go through every point, update the boundary based on lane info and ADC's position. 
  for (size_t i = 0; i < path_bound->size(); ++i) {
    double curr_s = std::get<0>((*path_bound)[i]);
    // 1. Get the current lane width at current point.    
    reference_line.GetLaneWidth(curr_s, &curr_lane_left_width, &curr_lane_right_width));

    // 2. Get the neighbor lane widths at the current point.    
    //CheckLaneBoundaryType()在向左或向右借道时,返回true
    if (CheckLaneBoundaryType(reference_line_info, curr_s, lane_borrow_info)) {      
      if (lane_borrow_info == LaneBorrowInfo::LEFT_BORROW) {
        // Borrowing left neighbor lane.  //向左侧同向顺行车道借道
        if (reference_line_info.GetNeighborLaneInfo(
                ReferenceLineInfo::LaneType::LeftForward, curr_s,
                &neighbor_lane_id, &curr_neighbor_lane_width)) {
          ADEBUG << "Borrow left forward neighbor lane.";
        } else if (reference_line_info.GetNeighborLaneInfo(
                       ReferenceLineInfo::LaneType::LeftReverse, curr_s,
                       &neighbor_lane_id, &curr_neighbor_lane_width)) {          
          borrowing_reverse_lane = true;  //向左侧反向逆行车道借道
          ADEBUG << "Borrow left reverse neighbor lane.";
        } else {
          ADEBUG << "There is no left neighbor lane.";
        }
      } else if (lane_borrow_info == LaneBorrowInfo::RIGHT_BORROW) {        
        ...  // Borrowing right neighbor lane. 与向左借道类似
      }
    }

    // 3.Calculate the proper boundary based on lane-width, ADC's position, and ADC's velocity.
    static constexpr double kMaxLateralAccelerations = 1.5;
    //给定初始速度和加速度,刹停行驶的距离(横向)
    //对横向来说,就是横向速度变为0的过程行驶的横向位移
    double ADC_speed_buffer = (adc_frenet_ld_ > 0 ? 1.0 : -1.0) *
           adc_frenet_ld_ * adc_frenet_ld_ / kMaxLateralAccelerations / 2.0;
    //如果要向左借道,可行驶道路左边界会变为左侧道路左边界
    double curr_left_bound_lane = curr_lane_left_width +
      (lane_borrow_info == LaneBorrowInfo::LEFT_BORROW ? curr_neighbor_lane_width : 0.0);
    ... //向右借道同上。更新curr_left_bound和curr_right_bound

    // 4. Update the boundary.    
    if (!UpdatePathBoundaryAndCenterLine(i, curr_left_bound, curr_right_bound,
                                         path_bound, &dummy)) { ... }    
  }  
}

3. pull over场景

pull over场景下,要根据停车点的选择,限定PathBound的横向和纵向范围,在该范围内遍历搜索。

Status PathBoundsDecider::GeneratePullOverPathBound( ... ) {
  // 1. Initialize the path boundaries to be an indefinitely large area.
  if (!InitPathBoundary(reference_line_info, path_bound)) { ... }

  // 2. Decide a rough boundary based on road boundary
  if (!GetBoundaryFromRoads(reference_line_info, path_bound)) { ... }  
  
  // 3. Fine-tune the boundary based on static obstacles  
  std::string blocking_obstacle_id;
  if (!GetBoundaryFromStaticObstacles(reference_line_info.path_decision(),
                                      path_bound, &blocking_obstacle_id)) { ... }
  ...
  // If already found a pull-over position, simply check if it's valid.
  int curr_idx = -1;
  if (pull_over_status->has_position()) {
    curr_idx = IsPointWithinPathBound(
        reference_line_info, pull_over_status->position().x(),
        pull_over_status->position().y(), *path_bound);
  }

  // If haven't found a pull-over position, search for one.
  //如果点不在bound限制的可行驶区域内,搜索可以停车的地点
  if (curr_idx < 0) {
    //<pull_over_x, pull_over_y, pull_over_theta, index_in_bounds>
    std::tuple<double, double, double, int> pull_over_configuration;
    if (!SearchPullOverPosition(frame, reference_line_info, *path_bound,
                                &pull_over_configuration)) { ... }
    curr_idx = std::get<3>(pull_over_configuration);
    // If have found a pull-over position, update planning-context
    ...  //设定停车地点的相关信息
  }

  // Trim path-bound properly
  //停车后面的bounds就没用了,删除一部分,修改一部分
  while (static_cast<int>(path_bound->size()) - 1 > curr_idx + kNumExtraTailBoundPoint) {
    path_bound->pop_back();
  }
  //停车后面的bounds全部修改为停车点处的bound
  for (size_t idx = curr_idx + 1; idx < path_bound->size(); ++idx) {
    std::get<1>((*path_bound)[idx]) = std::get<1>((*path_bound)[curr_idx]);
    std::get<2>((*path_bound)[idx]) = std::get<2>((*path_bound)[curr_idx]);
  }
  return Status::OK();
}

SearchPullOverPosition()应该是这个文件中最绕的函数了。它的目的是搜索停车点,为了能够容纳车的尺寸,因为要先搜索可以停车的区域,然后在该区域内取一点作为停车点。搜索区域时,要先确定一个端点,然后向前或向后考察另一个端点,以及考察两端点之间的区域是否符合要求。

bool PathBoundsDecider::SearchPullOverPosition( ... ) {
  // search direction
  bool search_backward = false;  // search FORWARD by default
  //01.先根据不同场景搜索一个考察可停车区域的大致端点,然后从该端点出发确定可停车区域
  double pull_over_s = 0.0;
  if (pull_over_status.pull_over_type() == PullOverStatus::EMERGENCY_PULL_OVER) {
    //紧急情况,前方停车
    if (!FindEmergencyPullOverS(reference_line_info, &pull_over_s)) { ... }
    search_backward = false;  // search FORWARD from target position
  } else if (pull_over_status.pull_over_type() == PullOverStatus::PULL_OVER) {
    if (!FindDestinationPullOverS(frame, reference_line_info, path_bound,
                                  &pull_over_s)) { ... }
    //理想的停车点是route的终点,因此要反向搜索,以找到匹配的bounds
    search_backward = true;  // search BACKWARD from target position
  } else {
    return false;
  }

  int idx = 0;
  if (search_backward) {
    // 1. Locate the first point before destination.
    idx = static_cast<int>(path_bound.size()) - 1;
    while (idx >= 0 && std::get<0>(path_bound[idx]) > pull_over_s) {
      --idx;
    }
    //反向搜索时,idx表示停车区域的末端
  } else {
    // 1. Locate the first point after emergency_pull_over s.
    while (idx < static_cast<int>(path_bound.size()) &&
           std::get<0>(path_bound[idx]) < pull_over_s) {
      ++idx;
    }
    //前向搜索时,idx表示停车区域的开端
  }
  if (idx < 0 || idx >= static_cast<int>(path_bound.size())) { ... }

  // Search for a feasible location for pull-over
  ... //根据一些条件计算pull_over_space_length 和 pull_over_space_width 

  // 2. Find a window that is close to road-edge.(not in any intersection)
  //02.搜索可停车的区域始末。内外2层while循环,外循环控制一个开始搜索的端点idx,因为当
  //考察的区域不符合安全性和尺寸条件时,idx也要变化。内循环控制另一个端点j。
  bool has_a_feasible_window = false;
  while ((search_backward && idx >= 0 &&
          std::get<0>(path_bound[idx]) - std::get<0>(path_bound.front()) >
              pull_over_space_length) ||
         (!search_backward && idx < static_cast<int>(path_bound.size()) &&
          std::get<0>(path_bound.back()) - std::get<0>(path_bound[idx]) >
              pull_over_space_length)) {
    int j = idx;
    bool is_feasible_window = true;

    ... // Check if the point of idx is within intersection.    
    //如果遇到路口,调整开始搜索的端点idx,重新搜索
    if (!junctions.empty()) {
      AWARN << "Point is in PNC-junction.";
      idx = search_backward ? idx - 1 : idx + 1;
      continue;
    }
    
    //搜索一段宽度达标、长度达标的可停车区域。即,搜索合适的端点j
    while ((search_backward && j >= 0 &&
            std::get<0>(path_bound[idx]) - std::get<0>(path_bound[j]) <
                pull_over_space_length) ||
           (!search_backward && j < static_cast<int>(path_bound.size()) &&
            std::get<0>(path_bound[j]) - std::get<0>(path_bound[idx]) <
                pull_over_space_length)) {
      double curr_s = std::get<0>(path_bound[j]);
      double curr_right_bound = std::fabs(std::get<1>(path_bound[j]));      
      reference_line_info.reference_line().GetRoadWidth(
          curr_s, &curr_road_left_width, &curr_road_right_width);      
      if (curr_road_right_width - (curr_right_bound + adc_half_width) >
          config_.path_bounds_decider_config().pull_over_road_edge_buffer()) {
        AERROR << "Not close enough to road-edge. Not feasible for pull-over.";
        is_feasible_window = false;
        break;
      }
      if(std::get<2>(path_bound[j])-std::get<1>(path_bound[j]) < pull_over_space_width) {
        AERROR << "Not wide enough to fit ADC. Not feasible for pull-over.";
        is_feasible_window = false;
        break;
      }

      j = search_backward ? j - 1 : j + 1;
    }
    if (j < 0) {
      return false;
    }
    //03.找到可停车区域后,获取停车目标点的位姿
    if (is_feasible_window) {
      // estimate pull over point to have the vehicle keep same safety distance
      // to front and back      
      ...
      int start_idx = j;
      int end_idx = idx;
      if (!search_backward) {
        start_idx = idx;
        end_idx = j;
      }
      //根据start_idx和end_idx计算pull_over_idx。注意index是相对于bounds的
      ...
      const auto& pull_over_point = path_bound[pull_over_idx];     
      //根据找到的停车点,设定相关信息,并根据参考线计算停车点的朝向角
      ...
      *pull_over_configuration = std::make_tuple(pull_over_x, pull_over_y,
        pull_over_theta, static_cast<int>(pull_over_idx));
      break;  //一旦找到可停车区域,退出最外层while循环,返回结果
    }
    idx = search_backward ? idx - 1 : idx + 1;
  }  
}

4. lane change 场景

Status PathBoundsDecider::GenerateLaneChangePathBound( ... ) {
  // 1. Initialize the path boundaries to be an indefinitely large area.
  if (!InitPathBoundary(reference_line_info, path_bound)) { ... }

  // 2. Decide a rough boundary based on lane info and ADC's position
  if (!GetBoundaryFromLanesAndADC(reference_line_info,
                                  LaneBorrowInfo::NO_BORROW, 0.1, path_bound,
                                  &dummy_borrow_lane_type)) { ... }

  // 3. Remove the S-length of target lane out of the path-bound.
  GetBoundaryFromLaneChangeForbiddenZone(reference_line_info, path_bound);
  if (!GetBoundaryFromStaticObstacles(reference_line_info.path_decision(),
                                      path_bound, &blocking_obstacle_id)) { ... }
  ...
}

计算PathBound的步骤大体类似,不同之处在于GetBoundaryFromLaneChangeForbiddenZone()。我认为该函数是检查【当前位置,变道起始点】区间内的PathBound的,却不明白还没有变道,为何要加入隔壁车道的宽度?

void PathBoundsDecider::GetBoundaryFromLaneChangeForbiddenZone( ... ) {  
  // If there is a pre-determined lane-change starting position, then use it;
  // otherwise, decide one.  
  //1.当前位置可以直接变道
  if (lane_change_status->is_clear_to_change_lane()) {
    ADEBUG << "Current position is clear to change lane. No need prep s.";
    lane_change_status->set_exist_lane_change_start_position(false);
    return;
  }
  double lane_change_start_s = 0.0;
  //2.利用已存在的变道起始点
  if (lane_change_status->exist_lane_change_start_position()) {
    common::SLPoint point_sl;
    reference_line.XYToSL(lane_change_status->lane_change_start_position(),
                          &point_sl);
    lane_change_start_s = point_sl.s();
  } else {
    //3.无变道起始点,就设置为自车前方一段距离的某点,lane_change_start_s是在当前车道上的距离    
    lane_change_start_s = FLAGS_lane_change_prepare_length + adc_frenet_s_;
    // Update the decided lane_change_start_s into planning-context.
    ...  //设定变道起始点的相关信息
  }
  
  if (lane_change_start_s < adc_frenet_s_) {
    // If already passed the decided S, then return.
    return;
  }
  //逐个考察lane_change_start_s之前的bound
  for (size_t i = 0; i < path_bound->size(); ++i) {
    double curr_s = std::get<0>((*path_bound)[i]);
    if (curr_s > lane_change_start_s) {
      break;
    }    
    if (reference_line.GetLaneWidth(curr_s, &curr_lane_left_width,
                                    &curr_lane_right_width)) { ... }    

    //有一点不明白:for循环是在lane_change_start_s范围内执行的,既然还没有到变道起始点,
    //怎么会偏移出本车道呢?可是若没有下面这段对bounds的修改,本函数就没有意义了
    //path_bound中的每个元素是tuple(s, l_min, l_max)
    std::get<1>((*path_bound)[i]) =
        adc_frenet_l_ > curr_lane_left_width    //左变道已跨过车道线,设定右界限
            ? curr_lane_left_width + GetBufferBetweenADCCenterAndEdge()
            : std::get<1>((*path_bound)[i]);
    std::get<1>((*path_bound)[i]) =
        std::fmin(std::get<1>((*path_bound)[i]), adc_frenet_l_ - 0.1);
    std::get<2>((*path_bound)[i]) =
        adc_frenet_l_ < -curr_lane_right_width  //右变道已跨过车道线,设定左界限
            ? -curr_lane_right_width - GetBufferBetweenADCCenterAndEdge()
            : std::get<2>((*path_bound)[i]);
    std::get<2>((*path_bound)[i]) =
        std::fmax(std::get<2>((*path_bound)[i]), adc_frenet_l_ + 0.1);
  }
}

 5. regular场景(lane borrow)

Status PathBoundsDecider::GenerateRegularPathBound( ... ) {
  // 1. Initialize the path boundaries to be an indefinitely large area.
  if (!InitPathBoundary(reference_line_info, path_bound)) { ... }

  // 2. Decide a rough boundary based on lane info and ADC's position
  if (!GetBoundaryFromLanesAndADC(reference_line_info, lane_borrow_info, 0.1,
                                  path_bound, borrow_lane_type)) { ... }

  // 3. Fine-tune the boundary based on static obstacles
  if (!GetBoundaryFromStaticObstacles(reference_line_info.path_decision(),
                                      path_bound, blocking_obstacle_id)) { ... }
  ...
}

处理步骤非常相似,不同之处在于调用GetBoundaryFromLanesAndADC()时,会将3种LaneBorrowInfo传入,即不借道、向左借道、向右借道。而之前的场景在调用该函数时,均设定不借道。

enum class LaneBorrowInfo {
    LEFT_BORROW,
    NO_BORROW,
    RIGHT_BORROW,
  };

6. 结尾

不同场景产生的PathBound都会存入ReferenceLineInfo 类的 candidate_path_boundaries_ 成员变量中,并标记了不同的标签。其他模块通过在candidate_path_boundaries_ 中查找,以取用合适的对应的PathBound。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐