autoware.universe源码略读(3.3)--perception:tensorrt_yolo

news2024/12/24 21:32:59

autoware.universe源码略读3.3--perception:tensorrt_yolo

  • 模块组成
  • cuda_utils(CUDA接口)
  • calibrator(校准器)
    • ImageStream
    • Int8EntropyCalibrator
  • mish(mish激活函数,基于CUDA)
  • mish_plugin(mish激活函数插件)
    • MishPlugin
    • MishPluginCreator
  • nms(非极大值抑制)
  • nms_plugin(NMS插件)
  • yolo_layer(YOLO模型层)
  • yolo_layer_plugin(YOLO层插件)
  • trt_yolo(基于TensorRT的YOLO过程)
  • nodelet
  • 总结

模块组成

这里趁着刚看过YOLO系列的相关知识,直接来看下这里的YOLO模块调用是怎么实现的。首先这里和其他部分没什么区别的地方是,也是一个继承于rclcpp::Node的类,在lib文件夹中则是具体功能的相关实现,比如从名字看起来mish应该是负责激活函数的,nms应该就是非极大值抑制的,当然trt_yolo应该就是yolo的主体部分了,这个部分的组成大概就像下面这张图这样,接下来就分开来简单看下
在这里插入图片描述

cuda_utils(CUDA接口)

这里主要是为了适配CUDA而写的一部分接口功能函数,没有什么特别需要说的。

calibrator(校准器)

这个校准器里面有两个部分,一个是ImageStream,这里应该就是输入的图像流;还有一个部分则是Int8EntropyCalibrator,可以看到这个类是继承自TensorRT中的nvinfer1::IInt8EntropyCalibrator2类的,所以具体执行校准的步骤应该是这里

ImageStream

这里主要是对输入的图像流进行预处理吧。这里的batch_,应该就是指一次处理的数据,那么batch_size_应该对应的就是一次处理几张图片?这里还有的就是这个input_dims_,这里应该是与输入图像的性质关联的,根据构造函数的代码

batch_.resize(batch_size_ * input_dims_.d[1] * input_dims_.d[2] * input_dims_.d[3]);

那我估计这里对应的就是每次输入,后边三个对应的可能就是通道数、图像宽度和图像高度了吧。
图像流的遍历操作是通过next函数实现的,这个其实很简单,就是对calibration_images_进行遍历,然后把经过预处理后的图片插入到batch_尾部。至于预处理的话,在preprocess函数中

  std::vector<float> preprocess(const cv::Mat & in_img, const int c, const int w, const int h) const
  {
    cv::Mat rgb;
    cv::cvtColor(in_img, rgb, cv::COLOR_BGR2RGB);  // BGR to RGB

    cv::resize(rgb, rgb, cv::Size(w, h));          // Resize

    cv::Mat img_float;
    rgb.convertTo(img_float, CV_32FC3, 1 / 255.0); // Normalize

    // HWC TO CHW
    std::vector<cv::Mat> input_channels(c);
    cv::split(img_float, input_channels);          // Split based on channels

    std::vector<float> result(h * w * c);
    auto data = result.data();
    int channel_length = h * w;
    for (int i = 0; i < c; ++i) {
      memcpy(data, input_channels[i].data, channel_length * sizeof(float));
      data += channel_length;
    }

    return result;
  }

Int8EntropyCalibrator

这个类用于实现 TensorRT 的 INT8 校准机制,该机制通过使用校准数据来确定量化后的 INT8 模型在推理时的精度。构造函数就是根据传入的数据流,进行内存的分配,剩下的函数实现其实都是针对于nvinfer1::IInt8EntropyCalibrator2中虚函数的实现吧。

  1. getBatch:这个函数是获取了当前批次的数据(调用了图像流的next函数),然后把内存从CPU复制到了GPU,并且更新bindings数组以指向设备内存。
  2. readCalibrationCache:从缓存文件读取校准数据,返回了校准数据的长度
  3. writeCalibrationCache:将校准数据写入缓存文件

mish(mish激活函数,基于CUDA)

这个文件的作用没什么好说的,就是实现了mish激活函数的计算,这里涉及到了CUDA函数的前缀,可以参考这篇文章,所以__device__前缀的就是在GPU上执行的函数,__global__则表示在GPU上执行,但在CPU上调用的函数,像这个文件就是,核心的计算公式是在__device__ float mish(float x)这里,但是调用的时候又调用的是__global__ void mishKernel(const T * input, T * output, int num_elem)这个方法

另外的一点就是,这里似乎上不是标准的mish函数?mish函数里的计算步骤与标准的mish激活函数中的明显不同,可能是简化版本的吧?

__device__ float mish(float x)
{
  float e = __expf(x);
  float n = e * e + 2 * e;
  if (x <= -0.6f) return x * __fdividef(n, n + 2);

  return x - 2 * __fdividef(x, n + 2);
}

如果是标准的话,应该是下面这样?

__device__ float mish_standard(float x)
{
  return x * tanh(log1p(exp(x)));
}

不太清楚这里使用的这个形式有什么好处,不过毕竟只是在推理的时候使用到,应该大差不差吧

mish_plugin(mish激活函数插件)

这里是为TensorRT添加了一个激活函数的插件,里面也是分成了两个类,一个是MishPlugin应该就是具体的插件类,继承自nvinfer1::IPluginV2DynamicExt,另一个类就是MishPluginCreator,看起来作用是生成一个插件类的

MishPlugin

这个类是一个实现自定义 TensorRT 插件的示例,用于在推理过程中应用 Mish 激活函数。实现了一些 TensorRT 插件接口的方法,使其可以集成到 TensorRT 的推理引擎中。
在这里涉及到的函数里,有很多地方都有这样的代码:

(void)inputTypes;

搜了下这里的作用好像就是先显式地标记某些未使用的参数,这样即使后边没有用到这个参数,编译的时候也不会发出相关的警告。在这个类中,执行mish激活函数的部分在enqueue函数之中,

int MishPlugin::enqueue(
  const PluginTensorDesc * inputDesc, const PluginTensorDesc * outputDesc,
  const void * const * inputs, void * const * outputs, void * workspace,
  cudaStream_t stream) noexcept
{
  (void)inputDesc;
  (void)outputDesc;
  (void)workspace;

  const int input_volume = volume(inputDesc[0].dims);

  int status = -1;

  const float * input = static_cast<const float *>(inputs[0]);
  float * output = static_cast<float *>(outputs[0]);
  status = mish(stream, input, output, input_volume);
  return status;
}

MishPluginCreator

这个类的作用是实现 TensorRT 插件创建器接口,用于创建和反序列化MishPlugin插件。它是 TensorRT 插件机制的一部分,负责提供插件的元数据,并在需要时创建插件实例。这里有两个静态成员变量:

PluginFieldCollection MishPluginCreator::mFC{};
std::vector<PluginField> MishPluginCreator::mPluginAttributes;

mFC是一个PluginFieldCollection,用于存储插件的字段信息(即插件的参数)。mPluginAttributes 是一个 PluginField的向量,用于保存具体的插件字段。构造函数中清空了mPluginAttributes并将其信息设置到mFC中。而在函数createPlugin之中,则是完成了这个插件的生成,具体到代码里就是new了一个MishPlugin并且设置了命名空间。

  MishPlugin * obj = new MishPlugin();
  obj->setPluginNamespace(mNamespace.c_str());
  return obj;

还有一个函数是反序列化的操作,因为通常数据会序列化成类似于二进制的形式用于传输和储存,所以这里将之前经序列化储存过的对象反序列化成了插件实例

  // This object will be deleted when the network is destroyed, which will
  // call MishPlugin::destroy()
  MishPlugin * obj = new MishPlugin(serialData, serialLength);
  obj->setPluginNamespace(mNamespace.c_str());
  return obj;

nms(非极大值抑制)

之前有提到过,非极大值抑制的主要作用是在YOLO检测到物体后,防止一个物体被多次重复检测的步骤。在具体的文件中,定义的是一个__global__前缀的函数来实现核心的计算步骤,这里就是对提取到的boxes进行判断,当然遍历的是检测类别数num_detections和线程数num_per_thread,然后对同一类的进行判断,重叠区域大于设置阈值的化就把得分置为0了(所以应该还有步骤会提前根据得分排序的吧?
这里还涉及到了CUDA里的一个函数:__syncthreads(), 它的作用就是让线程块中的每个线程都执行完 __syncthreads()前面的语句后,才会执行下一条语句。
当然nms的主函数也是和mish激活函数一样是单独写出来的,不过这里的处理逻辑比mish激活函数的难上不少。

  1. 首先,计算工作区的大小,其中使用FlaggedSortPairsDescending分别计算了临时工作区的大小,在代码里选择了较大的那个
  if (!workspace || !workspace_size) {
    // Return required scratch space size cub style
    workspace_size = cuda::get_size_aligned<bool>(count);    // flags
    workspace_size += cuda::get_size_aligned<int>(count);    // indices
    workspace_size += cuda::get_size_aligned<int>(count);    // indices_sorted
    workspace_size += cuda::get_size_aligned<float>(count);  // scores
    workspace_size += cuda::get_size_aligned<float>(count);  // scores_sorted

    size_t temp_size_flag = 0;
    cub::DeviceSelect::Flagged(
      (void *)nullptr, temp_size_flag, cub::CountingInputIterator<int>(count), (bool *)nullptr,
      (int *)nullptr, (int *)nullptr, count);
    size_t temp_size_sort = 0;
    cub::DeviceRadixSort::SortPairsDescending(
      (void *)nullptr, temp_size_sort, (float *)nullptr, (float *)nullptr, (int *)nullptr,
      (int *)nullptr, count);
    workspace_size += std::max(temp_size_flag, temp_size_sort);

    return workspace_size;
  }
  1. 分配临时数据的工作区指针,其中thrust::cuda::par.on(stream)这里是使用了多线程并行的意思吧
  auto on_stream = thrust::cuda::par.on(stream);

  auto flags = cuda::get_next_ptr<bool>(count, workspace, workspace_size);
  auto indices = cuda::get_next_ptr<int>(count, workspace, workspace_size);
  auto indices_sorted = cuda::get_next_ptr<int>(count, workspace, workspace_size);
  auto scores = cuda::get_next_ptr<float>(count, workspace, workspace_size);
  auto scores_sorted = cuda::get_next_ptr<float>(count, workspace, workspace_size);
  1. 接下来比较重要的一步是直接把得分为0的结果过滤掉了
    // Discard null scores
    thrust::transform(
      on_stream, in_scores, in_scores + count, flags, thrust::placeholders::_1 > 0.0f);
  1. 接下来果然有对分数排序的步骤,gather的解释

gather copies elements from a source array into a destination range according to a map. For each input iterator i in the range [map_first, map_last), the value input_first[*i] is assigned to *(result + (i - map_first)). RandomAccessIterator must permit random access.

然后具体排序的话用的是SortPairsDescending

    // Sort scores and corresponding indices
    thrust::gather(on_stream, indices, indices + num_detections, in_scores, scores);
    cub::DeviceRadixSort::SortPairsDescending(
      workspace, workspace_size, scores, scores_sorted, indices, indices_sorted, num_detections, 0,
      sizeof(*scores) * 8, stream);
  1. 接下来就是调用了NMS的核心函数来执行NMS的步骤
    // Launch actual NMS kernel - 1 block with each thread handling n detections
    const int max_threads = 1024;
    int num_per_thread = ceil((float)num_detections / max_threads);
    nms_kernel<<<1, max_threads, 0, stream>>>(
      num_per_thread, nms_thresh, num_detections, indices_sorted, scores_sorted, in_classes,
      in_boxes);
  1. 对执行NMS后的结果重新进行排序
    // Re-sort with updated scores
    cub::DeviceRadixSort::SortPairsDescending(
      workspace, workspace_size, scores_sorted, scores, indices_sorted, indices, num_detections, 0,
      sizeof(*scores) * 8, stream);
  1. 最终得到最后的结果
    // Gather filtered scores, boxes, classes
    num_detections = min(detections_per_im, num_detections);
    cudaMemcpyAsync(
      out_scores, scores, num_detections * sizeof *scores, cudaMemcpyDeviceToDevice, stream);
    if (num_detections < detections_per_im) {
      thrust::fill_n(on_stream, out_scores + num_detections, detections_per_im - num_detections, 0);
    }
    thrust::gather(on_stream, indices, indices + num_detections, in_boxes, out_boxes);
    thrust::gather(on_stream, indices, indices + num_detections, in_classes, out_classes);

函数整体的逻辑还是比较简单的,但是里面涉及到一些CUDA编程的东西以及内存相关的操作,所以读起来可能还是有些吃力

nms_plugin(NMS插件)

这个插件类和之前提到的mish激活函数的插件大差不差,都是分为了插件类自身以及生成插件两个类,这里NMS的核心调用也是放在了enqueue函数中,因为没有什么太多新的东西,所以这里就不再仔细记录了

yolo_layer(YOLO模型层)

这部分应该是涉及到YOLO输出特征转换为边界框、类别和分数,并存储在输出数组中,一上来先定义了两个激活函数,分别是普通的sigmoid和带有尺度的scaleSigmoid

inline __device__ float sigmoid(float x) { return 1.0f / (1.0f + __expf(-x)); }

inline __device__ float scaleSigmoid(float x, float scale)
{
  return scale * sigmoid(x) - (scale - 1.0f) * 0.5f;
}

接下来是yoloLayerKernel,这里应该就是YOLO层的核心部分

  • input: 输入特征图,包含预测数据。
  • out_scores: 输出分数数组,存储每个检测的得分。
  • out_boxes: 输出边界框数组,存储每个检测的边界框坐标。
  • out_classes: 输出类别数组,存储每个检测的类别。
  • grid_width: 特征图的网格宽度。
  • grid_height: 特征图的网格高度。
  • num_classes: 类别的数量。
  • num_anchors: 锚框的数量。
  • anchors: 锚框数组,存储每个锚框的宽高。
  • input_width: 输入图像的宽度。
  • input_height: 输入图像的高度。
  • scale_x_y: 用于坐标缩放的系数。
  • score_thresh: 分数阈值,过滤掉低于该阈值的检测结果。
  • use_darknet_layer: 是否使用 Darknet 层(用于不同版本的 YOLO 实现)。

先是进行了一些数据的准备工作

  int idx = threadIdx.x + TPB * blockIdx.x;
  int total_grids = grid_width * grid_height;
  if (idx >= total_grids * num_anchors) return;
  auto out_score = (out_scores) + idx;
  auto out_box = (out_boxes) + idx;
  auto out_class = (out_classes) + idx;

  int anchor_idx = idx / total_grids;     // 锚框索引
  idx = idx - total_grids * anchor_idx;   // 这里的idx就相当于一个局部索引
  int info_len = 5 + num_classes;
  auto cur_input = static_cast<const float *>(input) + anchor_idx * (info_len * total_grids);

其中,置信度的计算是对输出使用了一层sigmoid激活函数的

int class_id;
float max_cls_logit = -CUDART_INF_F;  // minus infinity
for (int i = 5; i < info_len; ++i) {
  float l = cur_input[idx + i * total_grids];
  if (l > max_cls_logit) {
    max_cls_logit = l;
    class_id = i - 5;
  }
}
float max_cls_prob = sigmoid(max_cls_logit);
float objectness = sigmoid(cur_input[idx + 4 * total_grids]);

然后根据是否用了darknet,来使用不同的方法计算x,y,h和w

  if (use_darknet_layer) {
    x = (col + scaleSigmoid(cur_input[idx + 0 * total_grids], scale_x_y)) / grid_width;    // [0, 1]
    y = (row + scaleSigmoid(cur_input[idx + 1 * total_grids], scale_x_y)) / grid_height;   // [0, 1]
    w = __expf(cur_input[idx + 2 * total_grids]) * anchors[2 * anchor_idx] / input_width;  // [0, 1]
    h = __expf(cur_input[idx + 3 * total_grids]) * anchors[2 * anchor_idx + 1] /
        input_height;  // [0, 1]
  } else {
    x = (col + sigmoid(cur_input[idx + 0 * total_grids]) * 2 - 0.5) / grid_width;   // [0, 1]
    y = (row + sigmoid(cur_input[idx + 1 * total_grids]) * 2 - 0.5) / grid_height;  // [0, 1]
    w = (sigmoid(cur_input[idx + 2 * total_grids]) * 2) *
        (sigmoid(cur_input[idx + 2 * total_grids]) * 2) * anchors[2 * anchor_idx] /
        input_width;  // [0, 1]
    h = (sigmoid(cur_input[idx + 3 * total_grids]) * 2) *
        (sigmoid(cur_input[idx + 3 * total_grids]) * 2) * anchors[2 * anchor_idx + 1] /
        input_height;  // [0, 1]
  }

至于最后,就是把结果赋值给对应的输出了

  *out_box = make_float4(x, y, w, h);
  *out_class = class_id;
  *out_score = objectness < score_thresh ? 0.0 : max_cls_prob * objectness;

关于封装好的YOLO层的函数,yoloLayer的作用是为 YOLO目标检测模型执行推理过程,具体地调用 yoloLayerKernel核函数来处理每个批次的输入数据,生成边界框、类别和分数。

  • batch_size: 批次大小。
  • inputs: 输入数据指针数组。
  • outputs: 输出数据指针数组。
  • grid_width: 特征图的网格宽度。
  • grid_height: 特征图的网格高度。
  • num_classes: 类别的数量。
  • num_anchors: 锚框的数量。
  • anchors: 锚框的宽高列表。
  • input_width: 输入图像的宽度。
  • input_height: 输入图像的高度。
  • scale_x_y: 用于坐标缩放的系数。
  • score_thresh: 分数阈值,过滤掉低于该阈值的检测结果。
  • use_darknet_layer: 是否使用 Darknet 层(用于不同版本的 YOLO 实现)。
  • workspace: 工作空间指针。
  • workspace_size: 工作空间大小。
  • stream: CUDA 流。
  1. 先是判断工作空间是否有效,然后把数据放到GPU上
  if (!workspace || !workspace_size) {
    workspace_size = cuda::get_size_aligned<float>(anchors.size());
    return workspace_size;
  }

  auto anchors_d = cuda::get_next_ptr<float>(anchors.size(), workspace, workspace_size);
  cudaMemcpyAsync(
    anchors_d, anchors.data(), anchors.size() * sizeof *anchors_d, cudaMemcpyHostToDevice, stream);
  1. 计算线程块和网格大小
int num_elements = num_anchors * grid_width * grid_height;
constexpr int block_size = 256;
const int grid_size = (num_elements + block_size - 1) / block_size;

  1. 接下来循环处理每个批次,这里调用了yoloLayerKernel函数
for (int batch = 0; batch < batch_size; ++batch) {
  auto input = static_cast<const float *>(inputs[0]) +
               batch * num_anchors * (num_classes + 5) * grid_width * grid_height;
  auto out_scores = static_cast<float *>(outputs[0]) + batch * num_elements;
  auto out_boxes = static_cast<float4 *>(outputs[1]) + batch * num_elements;
  auto out_classes = static_cast<float *>(outputs[2]) + batch * num_elements;
  yoloLayerKernel<block_size><<<grid_size, block_size, 0, stream>>>(
    input, out_scores, out_boxes, out_classes, grid_width, grid_height, num_classes, num_anchors,
    anchors_d, input_width, input_height, scale_x_y, score_thresh, use_darknet_layer);
}

yolo_layer_plugin(YOLO层插件)

这个和前面两个插件类也没什么太多区别,所以这里也不记录了

trt_yolo(基于TensorRT的YOLO过程)

在这个文件中,看起来是YOLO的主要部分,因为里面有Config这样一个配置文件的结构体,还有一个Net类,首先来看下配置文件的一些参数

struct Config
{
  int num_anchors;                  // 锚框数量
  std::vector<float> anchors;       // 锚框宽高列表
  std::vector<float> scale_x_y;     // 坐标缩放系数列表
  float score_thresh;               // 分数阈值,过滤低于该阈值的检测结果
  float iou_thresh;                 // IOU(交并比)阈值,用于非极大值抑制
  int detections_per_im;            // 每张图片的最大检测数量
  bool use_darknet_layer;           // 是否使用 Darknet 层(用于不同版本的 YOLO 实现)
  float ignore_thresh;              // 忽略阈值,低于该阈值的检测结果将被忽略
};

至于Net类,构造函数分成了两种,第一种的输入参数很简单,这种应该就是直接输入engine情况下的构造函数,另一种输入参数则多了很多,看起来输入的是onnx文件,所以是先经历了一步模型转换的过程
所以如果输入的是engine的话,就很简单,只需要load这个模型就好

  Logger logger(verbose);
  runtime_ = unique_ptr<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger));
  load(path);
  if (!prepare()) {
    std::cout << "Fail to prepare engine" << std::endl;
    return;
  }

不然的话就复杂的很,然后这里没有用到之前在common包里定义的一些模型转换的函数,而是自己又重新写了一遍,具体原因可能是这里对输出层具体进行了一些处理。 模型转换核心部分的代码和之前的差不多

  std::cout << "Building " << precision << " core model..." << std::endl;
  const auto flag =
    1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
  auto network = unique_ptr<nvinfer1::INetworkDefinition>(builder->createNetworkV2(flag));
  if (!network) {
    std::cout << "Fail to create network" << std::endl;
    return;
  }
  auto parser = unique_ptr<nvonnxparser::IParser>(nvonnxparser::createParser(*network, logger));
  if (!parser) {
    std::cout << "Fail to create parser" << std::endl;
    return;
  }

  parser->parseFromFile(
    onnx_file_path.c_str(), static_cast<int>(nvinfer1::ILogger::Severity::kERROR));

当然,这里也添加了插件类,不过没有用自己定义的。。

  auto nmsPlugin = yolo::NMSPlugin(yolo_config.iou_thresh, yolo_config.detections_per_im);
  auto layer = network->addPluginV2(concat.data(), concat.size(), nmsPlugin);
  for (int i = 0; i < layer->getNbOutputs(); i++) {
    auto output = layer->getOutput(i);
    network->markOutput(*output);
  }

最后,生成了engine文件。


至于具体的推理操作的调用,是通过detect实现的,更具体一点的话就是infer函数

这里就是具体的接口了,可以明显看到输入的直接就有图片类型,输出的就是三个:得分,检测框以及类别,这里先是对输入的图片进行了预处理:

const auto input = preprocess(in_img, input_dims.at(0), input_dims.at(2), input_dims.at(1));

预处理的主要操作是把图像从HWC转为了CHW,之后执行infer

  std::vector<void *> buffers = {
    input_d_.get(), out_scores_d_.get(), out_boxes_d_.get(), out_classes_d_.get()};
  try {
    infer(buffers, 1);
  } catch (const std::runtime_error & e) {
    return false;
  }

infer函数中,输入的buffers代表的是包含输入和输出缓冲区的向量,另一个就是一次处理的数据量的大小,这里再次用到了一个函数cudaStreamSynchronize,作用是等待 CUDA 流中的所有操作完成。这样可以确保推理任务已经完成,输出缓冲区中的数据是最新的。

void Net::infer(std::vector<void *> & buffers, const int batch_size)
{
  if (!context_) {
    throw std::runtime_error("Fail to create context");
  }
  auto input_dims = engine_->getBindingDimensions(0);
  context_->setBindingDimensions(
    0, nvinfer1::Dims4(batch_size, input_dims.d[1], input_dims.d[2], input_dims.d[3]));
  context_->enqueueV2(buffers.data(), stream_, nullptr);
  cudaStreamSynchronize(stream_);
}

nodelet

最后我们来看一下这个节点类TensorrtYoloNodelet吧,构造函数一样先加载了很多参数,然后这里对YOLO类进行了初始化

    RCLCPP_INFO(this->get_logger(), "Found %s", engine_file.c_str());
    net_ptr_.reset(new yolo::Net(engine_file, false));

之后也是话题的发布

  objects_pub_ = this->create_publisher<tier4_perception_msgs::msg::DetectedObjectsWithFeature>(
    "out/objects", 1);
  image_pub_ = image_transport::create_publisher(this, "out/image");

  out_scores_ =
    std::make_unique<float[]>(net_ptr_->getMaxBatchSize() * net_ptr_->getMaxDetections());
  out_boxes_ =
    std::make_unique<float[]>(net_ptr_->getMaxBatchSize() * net_ptr_->getMaxDetections() * 4);
  out_classes_ =
    std::make_unique<float[]>(net_ptr_->getMaxBatchSize() * net_ptr_->getMaxDetections());

这里比较有趣的是,图像话题的订阅没有像之前一样直接放在构造函数里,而是在connectCb函数中,然后在构造函数里会有定时器timer_变量对这个进行管理,所以相当于是根据订阅者的数量动态地开启或关闭 image_sub_ 的订阅,从而节省资源。(PS:这个逻辑在很多硬件的驱动上见过

void TensorrtYoloNodelet::connectCb()
{
  using std::placeholders::_1;
  std::lock_guard<std::mutex> lock(connect_mutex_);
  if (objects_pub_->get_subscription_count() == 0 && image_pub_.getNumSubscribers() == 0) {
    image_sub_.shutdown();
  } else if (!image_sub_) {
    image_sub_ = image_transport::create_subscription(
      this, "in/image", std::bind(&TensorrtYoloNodelet::callback, this, _1), "raw",
      rmw_qos_profile_sensor_data);
  }
}

具体的推理过程,当然不出意外是通过回调函数实现的,然后根据结果判断物体的类别,并且进行对应话题的发布,看起来autoware里面是有六种类别:车,行人,公交车,卡车,自行车以及电动车

总结

总体看下来,这个就是针对YOLO模型部署的模块,需要提前训练好模型文件如yolov5x.onnx这种,标签文件如coco.names,之后可以把这个作为一个节点使用,发布的有检测到的物体以及标记出来物体的输出图像两种

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

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

相关文章

旅游收入增速超469%!2.13亿小镇中老年为银发旅游增长提供新动能

下沉市场疯狂“上分”&#xff0c;银发旅游增长迎来新动能 作者&#xff5c;吕娆炜 排版&#xff5c;张思琪 干货抢先看 1. 今年第一季度&#xff0c;我国旅游人数和旅游收入增长率最高的均为三线及以下城市。 2. 独具特色的县域城市成为备受银发族青睐的新兴目的地&#xf…

vue-cli 搭建项目,ElementUI的搭建和使用

vue-cli 官方提供的一个脚手架&#xff0c;用于快速生成一个vue的项目模板&#xff1b;预先定义 好的目录结构及基础代码&#xff0c;就好比咱们在创建Maven项目时可以选择创建一个 骨架项目&#xff0c;这个骨架项目就是脚手架&#xff0c;我们的开发更加的快速&#xff1b; …

第11周 多线程接口并行对数据字典的查询优化

第11周 多线程接口并行对数据字典的查询优化 本章概述1. 多线程的初始化方式1.1 简单实现多线程的启动方式(3种)1. 继承Thread实现2. 实现Runnable接口3. 实现callable接口(返回值)1.2 基于线程池实现多线程的启动方式❤❤❤*************************************************…

「51媒体」浙江地区媒体邀约

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体宣传加速季&#xff0c;100万补贴享不停&#xff0c;一手媒体资源&#xff0c;全国100城线下落地执行。详情请联系胡老师。 浙江地区的媒体邀约资源丰富多样&#xff0c;涵盖了电视台…

进程线程协程

进程、线程和协程 进程 进程是一个正在运行的可执行文件&#xff08;exe&#xff09;&#xff0c;它是系统分配资源的最小单位。好像wx、xhs和wb都是不同的进程。它们各自独立&#xff0c;有自己的资源 线程 一个进程里可以有多个线程&#xff0c;线程是系统进行运行调度的最…

微服务和kafka

一、微服务简介 1.单体架构 分布式--微服务--云原生 传统架构&#xff08;单机系统&#xff09;&#xff0c;一个项目一个工程&#xff1a;比如商品、订单、支付、库存、登录、注册等等&#xff0c;统一部署&#xff0c;一个进程 all in one的架构方式&#xff0c;把所有的…

MyBatis案例

目录 一、配置文件1.数据与环境准备1.1 创建tb_brand表1.2 在Pojo中创建实体类Brand.java1.3 在test文件夹下的java中创建测试类1.4 安装MyBatisX插件 二、增删改查1. 查询 一、配置文件 1.数据与环境准备 1.1 创建tb_brand表 -- 删除tb_brand表 drop table if exists tb_bra…

Nginx 通过代理服务器访问目标接口

需求&#xff1a;某Nginx需要通过一个http代理服务来访问目标URL。 如果下图所示&#xff1a; 服务器B仅提供一个代理出来&#xff0c;不允许在该服务器上安装和进行任何其他配置操作。 在这种需求场景下&#xff0c;网络请求过程和域名解析的工作流程大致如下&#xff1a; 1…

vue3+swiper11 横向的循环切换图片

利用Swiper制作横向循环图片 <template><div class"summaryE"><div class"E_two"><swiper :slidesPerView"5" loop:autoplay"{ delay: 5000, disableOnInteraction: true }":navigation"true" :pagina…

Redis-实战篇-编码解决商铺查询的缓存穿透问题(缓存空对象)

文章目录 1、缓存穿透2、常见的解决方案有两种&#xff1a;2.1、缓存空对象2.2、布隆过滤器 3、编码解决商铺查询的缓存穿透问题3.1、queryById3.2、RedisConstants.java 1、缓存穿透 缓存击穿是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效…

2024年最新 Windows 操作系统安装部署 MongoDB 数据库详细教程(更新中)

MongoDB 概述 MongoDB 是一个基于分布式文件存储的开源数据库系统&#xff0c;由C语言编写&#xff0c;旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产品&#xff0c;是非关系数据库当中功能最丰富&#xff0c;最像关系…

selenium爬取boss直聘招聘岗位数据

selenium爬取boss直聘招聘岗位数据 一、爬取流程二、完整代码一、爬取流程 先来看一下爬取到的数据情况: 再来看一下boss直聘的页面,这时需要我们已经完成了城市的选择,将岗位名称输入到搜索框中,点击搜索之后切换到第2页。这时我们将url复制。第2页的url为:https://www.…

双指针dd d df f

像二分这样的算法&#xff0c;我们甚至可以不用管&#xff0c;直接在问题空间之内搜索&#xff0c;但是双指针也非常好用&#xff0c;帮助我们来减少枚举对象&#xff0c;我们来总结一下这经典的三个题目&#xff1a; 最长上升不重复子序列活动 - AcWings 首先一定要写…

element 问题整合

没关系&#xff0c;凡事发生必有利于我 文章目录 一、el-table 同级数据对齐及展开图标的位置问题二、el-table 勾选框为圆角及只能勾选一个 一、el-table 同级数据对齐及展开图标的位置问题 element 官方提供的扩展tree型数据在表格里默认是靠左边对齐&#xff0c;项目需求需要…

Python在Excel中设置数字格式和获取应用数字格式后的值

目录 安装Python Excel库 Python在Excel中设置数字格式 Python获取Excel中应用数字格式的单元格的显示值 总结 Excel 数字格式是用于控制单元格中数字显示方式的一组规则或代码。通过设置不同的数字格式&#xff0c;可以定义数字的显示方式&#xff0c;如小数位数、货币符号…

智能护栏碰撞监测系统:强化道路安全的智能守卫

智能护栏碰撞监测系统是提升道路安全、加强交通管理智能化的重要技术手段&#xff0c;其为道路安全带来的好处和安装的必要性体现在以下几个方面&#xff1a; 1. 即时事故响应&#xff1a;系统能够实时监测到护栏遭受的碰撞事件&#xff0c;几乎瞬间触发报警机制&#xff0c;将…

ViewBinding的使用(因为kotlin-android-extensions插件的淘汰)

书籍&#xff1a; 《第一行代码 Android》第三版 开发环境&#xff1a; Android Studio Jellyfish | 2023.3.1 问题&#xff1a; 3.2.4在Activity中使用Toast章节中使用到了kotlin-android-extensions插件,但是该插件已经淘汰,根据网上了解,目前使用了新的技术VewBinding替…

电脑的D盘E盘F盘突然消失了 电脑只剩下C盘了其他盘怎么恢复

现如今随着时代的发展&#xff0c;无纸化办公成为主流&#xff0c;这主要归功于电脑&#xff0c;能够通过电脑完成的工作绝不使用纸质文件&#xff0c;这不仅提高了工作效率&#xff0c;也让一些繁杂的工作变的更加简单。不过电脑毕竟是电子产品&#xff0c;不可避免的会出现一…

2024软件设计师笔记之考点版(一考就过):11-25

软件设计师之一考就过:成绩版 考点11:防火墙、入侵检测 真题1:(专家系统、模型检测、简单匹配)属于入侵检测;而漏洞扫描不属于。 真题2:防火墙特性包括(控制进出网络的数据包和数据流向、提供流量信息的日志和审计、隐藏内部IP以及网络结构细节),但不包括提供漏洞扫…

视频共享融合赋能平台LntonCVS统一视频接入平台数字化升级医疗体系

医疗健康事关国计民生&#xff0c;然而&#xff0c;当前我国医疗水平的地区发展不平衡、医疗资源分布不均和医疗信息系统老化等问题&#xff0c;制约了整体服务能力和水平的提升。视频融合云平台作为推动数字医疗的关键工具&#xff0c;在医疗领域的广泛应用和普及&#xff0c;…