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

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 RGBcv::resize(rgb, rgb, cv::Size(w, h));          // Resizecv::Mat img_float;rgb.convertTo(img_float, CV_32FC3, 1 / 255.0); // Normalize// HWC TO CHWstd::vector<cv::Mat> input_channels(c);cv::split(img_float, input_channels);          // Split based on channelsstd::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 styleworkspace_size = cuda::get_size_aligned<bool>(count);    // flagsworkspace_size += cuda::get_size_aligned<int>(count);    // indicesworkspace_size += cuda::get_size_aligned<int>(count);    // indices_sortedworkspace_size += cuda::get_size_aligned<float>(count);  // scoresworkspace_size += cuda::get_size_aligned<float>(count);  // scores_sortedsize_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 scoresthrust::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 indicesthrust::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 detectionsconst 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 scorescub::DeviceRadixSort::SortPairsDescending(workspace, workspace_size, scores_sorted, scores, indices_sorted, indices, num_detections, 0,sizeof(*scores) * 8, stream);
  1. 最终得到最后的结果
    // Gather filtered scores, boxes, classesnum_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.mzph.cn/bicheng/35520.shtml

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

相关文章

Python22 Pandas库

Pandas 是一个Python数据分析库&#xff0c;它提供了高性能、易于使用的数据结构和数据分析工具。这个库适用于处理和分析输入数据&#xff0c;常见于统计分析、金融分析、社会科学研究等领域。 1.Pandas的核心功能 Pandas 库的核心功能包括&#xff1a; 1.数据结构&#xff…

ODYSSEE加速电机仿真优化

由于对低碳社会的强烈需求&#xff0c;电动汽车(EV)和混合动力汽车(HEV)的数量正在迅速增长。新能源汽车的主要部件是电池、逆变器和电机。电机市场的规模也将不断扩大。为了提高EV的性能&#xff0c;对电机设计工程师的要求越来越高。 除了EV市场&#xff0c;协作机器人市场也…

【Linux】gdb调试器

一、gdb调试器背景 程序的发布方式有两种&#xff0c;debug模式和release模式 Linux gcc/g出来的二进制程序&#xff0c;默认是release模式 要使用gdb调试&#xff0c;必须在源代码生成二进制程序的时候, 加上 -g 选项 二、安装gdb yum install gdb三、使用gdb 在Linux当中g…

Spark运行spark-shell与hive运行时均报错的一种解决方案

环境按照尚硅谷的配置的。 在运行hive的时候&#xff0c;报错代码为30041&#xff0c;无法执行insert语句。 在运行spark-shell的时候&#xff0c;报错&#xff0c;无法进入到shell脚本中。 可能的问题&#xff1a; 对集群设置的域名与集群的主机名称不一致。 例如&#xff1a;…

Cesium入门:Camera的关键知识点

作者: 还是大剑师兰特 ,曾为美国某知名大学计算机专业研究生,现为国内GIS领域高级前端工程师,CSDN知名博主,深耕openlayers、leaflet、mapbox、cesium,canvas,echarts等技术开发,欢迎加微信(gis-dajianshi),一起交流。 查看本专栏目录 - 本文是第 078篇文章 文章目录…

快速高效的菲律宾海运攻略

快速高效的菲律宾海运攻略 【14天送达】菲律宾海运攻略来了&#xff01;你是不是也在为如何将机器发货到菲律宾而烦恼&#xff1f;别担心&#xff0c;今天小编就为大家详细讲解一下整个流程&#xff01; 第一步&#xff1a;准备货物和文件 首先确保你的机器包装完好无损&#x…

使用c++栈刷题时踩坑的小白错误

根据图片中提供的代码&#xff0c;可以发现以下三处错误&#xff1a; 错误原因&#xff1a;条件判断语句的逻辑错误。 代码行&#xff1a;if (res.top() ! e || res.empty())&#xff08;第7行&#xff09; 问题&#xff1a;如果 res 为空&#xff08;res.empty() 为 true&…

mac卡牌游戏:堆叠大陆 Stacklands for Mac 中文安装包

Stacklands 是一款轻松益智的堆叠游戏。玩家需要在游戏中不断堆叠不同形状和大小的方块&#xff0c;使它们尽可能地稳定地堆放在一起。游戏中有多种不同的关卡和挑战&#xff0c;玩家需要通过合理的堆叠方式来完成每个关卡。游戏画面简洁明快&#xff0c;操作简单直观&#xff…

视频分享的二维码怎么做?多种视频可用的二维码制作技巧

视频分享的快捷操作技巧可以在二维码生成器上来制作&#xff0c;与传统分享方式相比用二维码的方法能够更快捷&#xff0c;有利于用户能够在不下载视频占用空间的同时&#xff0c;就能够扫描二维码观看视频内容。视频二维码能够应用于很多的场景下&#xff0c;那么制作一个视频…

Navicat Premium Lite绿色免费版

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl Navicat Premium Lite概述 Navicat 最近推出了一款名为 Navicat Premium Lite 的免费数据库管理开发工具&#xff0c;专为入门级用户设计。这款工具虽然在功能上与 Navicat…

新改进!LSTM与注意力机制结合,性能一整个拿捏住

众所周知&#xff0c;LSTM并不能很好地处理长序列和重要信息的突出&#xff0c;这导致在某些情况下性能不佳。而注意力机制模拟人类视觉注意力机制的特点可以很好地解决这个问题。 说具体点就是&#xff0c;注意力机制通过权重分布来决定应该关注输入序列中的哪些部分&#xf…

程序员学长 | 快速学会一个算法,RNN

本文来源公众号“程序员学长”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;快速学会一个算法&#xff0c;RNN 今天给大家分享一个超强的算法模型&#xff0c;RNN 循环神经网络&#xff08;Recurrent Neural Network, RNN&…

不花一分钱也能制作出高质量的宣传册

在当今竞争激烈的市场环境中&#xff0c;拥有一份高质量的宣传册对于企业或个人来说至关重要。它能帮助您在客户心中留下深刻印象&#xff0c;有效推广您的品牌或服务。但聘请专业设计师和印刷商制作宣传册往往需要不小的开支。那么&#xff0c;有没有既省钱又能做出高质量宣传…

flask水质监测预警系统-计算机毕业设计源码10148

摘 要 近些年来&#xff0c;对河道水位进行实时、准确的监测越来越受到广大人民群众的重视。然而要建立一个稳定的、可靠地、准确的城市河道水位远程监测系统&#xff0c;就必须要解决由人工监测向自动化监测的转变&#xff0c;使用新科技来进行设计。水质监测预警系统是以实际…

ardupilot开发 --- 坐标变换 篇

Good Morning, and in case I dont see you, good afternoon, good evening, and good night! 0. 一些概念1. 坐标系的旋转1.1 轴角法1.2 四元素1.3 基于欧拉角的旋转矩阵1.3.1 单轴旋转矩阵1.3.2 多轴旋转矩阵 2. 齐次变换矩阵3. visp实践 0. 一些概念 相关概念&#xff1a;旋…

charls抓包工具 mumu模拟器抓包apk

1.先安装mumu 官网添加链接描述 2.配置 设置&#xff0c;点进互联网&#xff0c;点编辑&#xff0c;选择手动代理 主机名写自己电脑的ip地址&#xff0c;端口随便&#xff0c;只要不被占用&#xff0c;一般参考其他人都是8888 3.下载charls 参考这个添加链接描述 先官网…

项目验收测试有必要找第三方软件测试机构吗?

在当今信息技术飞速发展的时代&#xff0c;软件测试成为了确保软件质量的重要环节。而在项目的验收测试中&#xff0c;很多企业都面临一个问题&#xff0c;那就是是否有必要找第三方软件测试机构进行验收测试?今天&#xff0c;我们就来探讨一下这个问题。 第三方软件测试机构…

【别再用Excel了!】这款免费可视化工具能帮你轻松提升效率

现代数据分析和展示的需求已经远远超出了传统工具的能力&#xff0c;尤其是在需要快速、直观和高效地处理复杂数据的情况下。山海鲸可视化通过其强大的功能和易用性&#xff0c;成为了设计师以及各类新手用户的理想选择。下面我就以一个可视化设计师的角度&#xff0c;和大家简…

2024年6月计算机视觉论文推荐:扩散模型、视觉语言模型、视频生成等

6月还有一周就要结束了&#xff0c;我们今天来总结2024年6月上半月发表的最重要的论文&#xff0c;重点介绍了计算机视觉领域的最新研究和进展。 Diffusion Models 1、Autoregressive Model Beats Diffusion: Llama for Scalable Image Generation LlamaGen&#xff0c;是一个…

合合信息智能文档抽取:赋能不良资产管理行业的数字化转型

官.网地址&#xff1a;合合TextIn - 合合信息旗下OCR云服务产品 随着数字化浪潮的汹涌澎湃&#xff0c;全球各行各业正经历着前所未有的变革。人工智能技术的快速发展&#xff0c;以其独特的创新能力和应用潜力&#xff0c;正在深刻地改变着业务模式&#xff0c;推动产业效率的…