Pytorch CUDA Reflect Padding 算子实现详解

news2024/9/30 23:32:35

CUDA 简介

  • CUDA(Compute Unified Device Architecture)是由NVIDIA开发的一种并行计算平台和应用编程接口(API),允许软件开发者和软件工程师使用NVIDIA的图形处理单元(GPU)进行通用计算。自2007年推出以来,CUDA已经使得利用GPU的强大计算能力进行高性能计算(HPC)和复杂图形渲染成为可能,广泛应用于科学计算、工程、机器学习和深度学习等领域。
  • CUDA 相关资料
    • 官方文档:https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
    • 入门样例:https://cuda-tutorial.readthedocs.io/en/latest/tutorials/tutorial01/

Reflect Padding 介绍

  • 反射填充是一种常见的图像边缘填充技术,用于卷积神经网络中,特别是在处理图像数据时。它通过镜像边缘像素来扩展图像的尺寸,从而使得边缘信息在卷积操作中得到更好的保留。reflect padding 样例如下图所示:
    reflect padding visualization
  • Q: 反射填充与零填充在实际应用中有何不同?
    • A: 反射填充通过复制边缘像素来扩展图像,保持了图像边缘的自然连续性,而零填充则在边缘添加零值,可能会在卷积后引入人为的边缘效应。

Pytorch Reflect Padding 实现

  • torch reflect padding 文档:https://pytorch.org/docs/stable/generated/torch.nn.ReflectionPad2d.html
>>> import torch.nn as nn
>>> import torch
>>> m = nn.ReflectionPad2d(2)
>>> input = torch.arange(9, dtype=torch.float).reshape(1, 1, 3, 3)
>>> input
tensor([[[[0., 1., 2.],
          [3., 4., 5.],
          [6., 7., 8.]]]])
>>> m(input)
tensor([[[[8., 7., 6., 7., 8., 7., 6.],
          [5., 4., 3., 4., 5., 4., 3.],
          [2., 1., 0., 1., 2., 1., 0.],
          [5., 4., 3., 4., 5., 4., 3.],
          [8., 7., 6., 7., 8., 7., 6.],
          [5., 4., 3., 4., 5., 4., 3.],
          [2., 1., 0., 1., 2., 1., 0.]]]])
>>> # using different paddings for different sides
>>> m = nn.ReflectionPad2d((1, 1, 2, 0))
>>> m(input)
tensor([[[[7., 6., 7., 8., 7.],
          [4., 3., 4., 5., 4.],
          [1., 0., 1., 2., 1.],
          [4., 3., 4., 5., 4.],
          [7., 6., 7., 8., 7.]]]])

CUDA Reflect Padding 代码实现理解

forward
  • reflection_pad2d_out_template 实现,用于执行二维反射填充。
// 定义一个函数,用于对输入Tensor进行二维反射填充,并将结果输出到output Tensor。
void reflection_pad2d_out_template(
    Tensor &output, const Tensor &input_, IntArrayRef padding) {

  // 检查输入Tensor是否可以使用32位索引数学运算。
  TORCH_CHECK(canUse32BitIndexMath(input_),
    "input tensor must fit into 32-bit index math");

  // 初始化一些维度标识符和批次大小。
  int plane_dim = 0;
  int dim_h = 1;
  int dim_w = 2;
  int nbatch = 1;

  // 检查输入Tensor和padding参数是否合法。
  at::native::padding::check_valid_input<2>(input_, padding);

  // 如果输入Tensor是4维的,说明有批次维度,需要相应调整其他维度的索引,并更新批次大小。
  if (input_.ndimension() == 4) {
    nbatch = input_.size(0);
    plane_dim++;
    dim_h++;
    dim_w++;
  }

  // 从padding参数中提取左、右、上、下四个方向的填充大小。
  int64_t pad_l = padding[0];
  int64_t pad_r = padding[1];
  int64_t pad_t = padding[2];
  int64_t pad_b = padding[3];

  // 获取输入Tensor在不同维度上的大小。
  int nplane = input_.size(plane_dim);
  int input_h = input_.size(dim_h);
  int input_w = input_.size(dim_w);

  // 检查左右填充大小是否小于输入宽度,上下填充大小是否小于输入高度。
  TORCH_CHECK(pad_l < input_w && pad_r < input_w, ...);
  TORCH_CHECK(pad_t < input_h && pad_b < input_h, ...);

  // 计算输出Tensor的高度和宽度。
  int output_h = input_h + pad_t + pad_b;
  int output_w = input_w + pad_l + pad_r;

  // 确保计算出的输出Tensor尺寸是有效的。
  TORCH_CHECK(output_w >= 1 || output_h >= 1, ...);

  // 根据输入Tensor的维度,调整输出Tensor的尺寸。
  if (input_.ndimension() == 3) {
    output.resize_({nplane, output_h, output_w});
  } else {
    output.resize_({nbatch, nplane, output_h, output_w});
  }
  // 如果输出Tensor为空,则不执行后续操作。
  if (output.numel() == 0) {
    return;
  }

  // 确保输入Tensor是连续的,便于后续处理。
  Tensor input = input_.contiguous();

  // 计算输出平面的大小,用于配置CUDA核函数的参数。
  int64_t output_plane_size = output_h * output_w;
  dim3 block_size(output_plane_size > 256 ? 256 : output_plane_size);

  // 准备在CUDA核函数中使用的变量。
  int64_t size_y = nplane;
  int64_t size_z = nbatch;

  // 对所有数据类型执行反射填充操作
  AT_DISPATCH_ALL_TYPES_AND_COMPLEX_AND2(kHalf, kBFloat16,
    input.scalar_type(), "reflection_pad2d_out_template", [&] {

      // 遍历所有平面和批次进行填充
      for (int64_t block_y = 0; block_y < size_y; block_y += 65535) {
        int64_t block_y_size = std::min(size_y - block_y, static_cast<int64_t>(65535));
        for (int64_t block_z = 0; block_z < size_z; block_z += 65535) {
          int64_t block_z_size = std::min(size_z - block_z, static_cast<int64_t>(65535));

          // 计算网格大小并启动CUDA核心
          dim3 grid_size(at::ceil_div(output_plane_size, static_cast<int64_t>(256)), block_y_size, block_z_size);

          // 计算网格大小并启动CUDA核心
          // 这里使用了CUDA的核心启动语法,`<<<grid_size, block_size, 0, at::cuda::getCurrentCUDAStream()>>>`,
          // 其中grid_size和block_size是CUDA核心执行时网格和块的维度配置,这里的0表示使用默认的共享内存大小,
          // at::cuda::getCurrentCUDAStream()获取当前CUDA流,用于并行计算。
          reflection_pad2d_out_kernel<<<
            grid_size, block_size, 0, at::cuda::getCurrentCUDAStream()>>>(
              // 传递给核心函数的参数,包括输入和输出张量的数据指针,
              // 输入的宽度和高度,四个方向的填充大小,当前处理的平面和批次索引,以及平面的总数。
              input.const_data_ptr<scalar_t>(), output.mutable_data_ptr<scalar_t>(),
              input_w, input_h,
              pad_t, pad_b, pad_l, pad_r, block_y, block_z, nplane);
          // 检查CUDA核心启动后是否有错误发生
          C10_CUDA_KERNEL_LAUNCH_CHECK();
        }
      }
    }
  );
}

代码的最后部分是关键的,它展示了如何调用CUDA核心函数(reflection_pad2d_out_kernel)来实际执行反射填充操作。这个核心函数利用 CUDA 的并行计算能力,对输入张量的每个元素进行填充处理,确保在 GPU 上高效地完成操作。C10_CUDA_KERNEL_LAUNCH_CHECK() 是用于检测核心启动后是否有任何错误发生。

  • reflection_pad2d_out_kernel 实现:CUDA reflect pad2d 核函数。它接收输入和输出张量的指针、输入尺寸、填充尺寸和平面偏移量,然后计算每个线程应处理的输出张量中的像素位置,并根据输入张量中相应位置的值来填充它。
template<typename scalar_t>
__global__ void reflection_pad2d_out_kernel(
    const scalar_t * input, scalar_t * output,
    int64_t input_dim_x, int64_t input_dim_y,
    int pad_t, int pad_b, int pad_l, int pad_r, int y_shift, int z_shift, int nplane) {
  // 计算当前线程负责的输出位置
  auto output_xy = threadIdx.x + blockIdx.x * blockDim.x;
  // 计算输出维度
  auto output_dim_x = input_dim_x + pad_l + pad_r;
  auto output_dim_y = input_dim_y + pad_t + pad_b;

  // 如果当前线程负责的位置在输出范围内
  if (output_xy < output_dim_x * output_dim_y) {
    // 获取输入和输出索引映射
    auto index_pair = get_index_mapping2d(
      input_dim_x, input_dim_y,
      output_dim_x, output_dim_y,
      pad_l, pad_t,
      output_xy, y_shift, z_shift, nplane);

    // 根据映射关系复制数据
    output[index_pair.second] = input[index_pair.first];
  }
}
  • get_index_mapping2d 函数实现:基于输出像素位置、填充参数和偏移量,计算出反射填充后的输入和输出索引。这个函数利用了 CUDA 的内置函数 abs 来处理反射逻辑,确保输出位置正确地映射到输入张量上
// 定义一个 mapping 函数,用于计算从输出位置到输入位置的索引映射。
__device__
inline thrust::pair<int64_t, int64_t>  get_index_mapping2d(
    int64_t input_dim_x, int64_t input_dim_y,
    int64_t output_dim_x, int64_t output_dim_y,
    int64_t pad_l, int64_t pad_t,
    int64_t output_xy, int y_shift, int z_shift, int nplane) {
  
  // 计算输入和输出的偏移量,考虑了批次和通道的变化。
  auto input_offset =
    ((blockIdx.y + y_shift) + (blockIdx.z + z_shift) * nplane) * input_dim_x * input_dim_y;
  auto output_offset =
    ((blockIdx.y + y_shift) + (blockIdx.z + z_shift) * nplane) * output_dim_x * output_dim_y;

  // 根据线性索引计算输出坐标。
  auto output_x = output_xy % output_dim_x;
  auto output_y = output_xy / output_dim_x;

  // 计算输入和输出坐标的起始点。
  auto i_start_x = ::max(int64_t(0), -pad_l);
  auto i_start_y = ::max(int64_t(0), -pad_t);
  auto o_start_x = ::max(int64_t(0), pad_l);
  auto o_start_y = ::max(int64_t(0), pad_t);

  // 根据反射逻辑计算输入坐标。
  auto input_x = ::abs(output_x - pad_l)
                 - ::abs(output_x - (input_dim_x + pad_l - 1))
                 - output_x
                 + 2 * pad_l + input_dim_x - 1
                 - o_start_x + i_start_x;

  auto input_y = ::abs(output_y - pad_t)
                 - ::abs(output_y - (input_dim_y + pad_t - 1))
                 - output_y
                 + 2 * pad_t + input_dim_y - 1
                 - o_start_y + i_start_y;

  // 返回输入和输出坐标的线性索引对。
  return thrust::make_pair<int64_t, int64_t>(
    input_offset + input_y * input_dim_x + input_x,
    output_offset + output_y * output_dim_x + output_x);
}
backward
  • backward 与 forward 整体实现思路接近,主要是梯度反传时逻辑与前传时需要反过来,代码实现思路基本和之前介绍的 forward 部分一致
  • backward 函数入口
// 定义一个函数,用于计算二维反射填充的梯度输出。
void reflection_pad2d_backward_out_template(
    Tensor &grad_input, const Tensor &grad_output_,
    const Tensor &input, IntArrayRef padding) {

  // 如果梯度输入的元素数为0,则不执行任何操作。
  if (grad_input.numel() == 0) {
    return;
  }

  // 检查输入张量和梯度输出张量是否可以使用32位索引进行数学运算,如果不可以则抛出错误。
  TORCH_CHECK(canUse32BitIndexMath(input),
    "input tensor must fit into 32-bit index math");
  TORCH_CHECK(canUse32BitIndexMath(grad_output_),
    "output gradient tensor must fit into 32-bit index math");

  // 初始化一些维度和批次的变量,用于后续的张量尺寸计算。
  int plane_dim = 0;
  int dim_h = 1;
  int dim_w = 2;
  int nbatch = 1;

  // 如果输入张量的维度是4,说明有一个批次维度,需要相应地调整其他维度的索引,并计算批次大小。
  if (input.ndimension() == 4) {
    nbatch = input.size(0);
    plane_dim++;
    dim_h++;
    dim_w++;
  }

  // 解析padding参数,得到左、右、上、下的填充尺寸。
  int64_t pad_l = padding[0];
  int64_t pad_r = padding[1];
  int64_t pad_t = padding[2];
  int64_t pad_b = padding[3];

  // 计算输入张量在特定维度上的尺寸。
  int nplane = input.size(plane_dim);
  int input_h = input.size(dim_h);
  int input_w = input.size(dim_w);

  // 根据输入尺寸和填充尺寸计算输出尺寸。
  int output_h = input_h + pad_t + pad_b;
  int output_w  = input_w + pad_l + pad_r;

  // 检查梯度输出张量的尺寸是否与预期一致,如果不一致则抛出错误。
  TORCH_CHECK(output_w == grad_output_.size(dim_w), "grad_output width unexpected. Expected: ", output_w, ", Got: ", grad_output_.size(dim_w));
  TORCH_CHECK(output_h == grad_output_.size(dim_h), "grad_output height unexpected. Expected: ", output_h, ", Got: ", grad_output_.size(dim_h));

  // 为了保证数据的连续性,将梯度输出张量转换为连续的。
  Tensor grad_output = grad_output_.contiguous();

  // 计算输出平面的大小,用于后续的CUDA核函数配置。
  int64_t output_plane_size = output_h * output_w;
  // 配置CUDA核函数的线程块大小,取256或输出平面大小的较小者。
  dim3 block_size(output_plane_size > 256 ? 256 : output_plane_size);

  // 准备循环遍历的尺寸变量。
  int64_t size_y = nplane;
  int64_t size_z = nbatch;

  // 对输入张量的数据类型进行分派,支持多种浮点和复数类型。
  AT_DISPATCH_FLOATING_AND_COMPLEX_TYPES_AND2(kHalf, kBFloat16,
    input.scalar_type(), "reflection_pad2d_backward_out_template", [&] {

      // 对每个平面(通道)和批次进行循环,处理大于65535的情况。
      for (int64_t block_y = 0; block_y < size_y; block_y += 65535) {
        int64_t block_y_size = std::min(size_y - block_y, static_cast<int64_t>(65535));
        for (int64_t block_z = 0; block_z < size_z; block_z += 65535) {
          int64_t block_z_size = std::min(size_z - block_z, static_cast<int64_t>(65535));

          // 计算网格大小,用于CUDA核函数的配置。
          dim3 grid_size(at::ceil_div(output_plane_size, static_cast<int64_t>(256)), block_y_size, block_z_size);

          // 调用CUDA核函数,计算梯度输入。
          reflection_pad2d_backward_out_kernel<<<
            grid_size, block_size, 0, at::cuda::getCurrentCUDAStream()>>>(
              grad_input.mutable_data_ptr<scalar_t>(), grad_output.const_data_ptr<scalar_t>(),
              input_w, input_h,
              pad_t, pad_b, pad_l, pad_r, block_y, block_z, nplane);
          // 检查CUDA核函数的启动是否有错误。
          C10_CUDA_KERNEL_LAUNCH_CHECK();
        }
      }
    }
  );
}
  • reflection_pad2d_backward_out_kernel 实现:
// 定义模板函数,用于CUDA内核,处理反射填充的梯度反向传播。
template <typename scalar_t>
__global__ void reflection_pad2d_backward_out_kernel(
    scalar_t * grad_input, // 指向梯度输入的指针,即对应前向传播输入的梯度
    const scalar_t * grad_output, // 指向梯度输出的指针,即损失函数对输出的偏导
    int64_t input_dim_x, // 输入的宽度
    int64_t input_dim_y, // 输入的高度
    int pad_t, // 顶部填充的大小
    int pad_b, // 底部填充的大小
    int pad_l, // 左侧填充的大小
    int pad_r, // 右侧填充的大小
    int y_shift, // 平面(plane)的偏移量,用于多通道数据处理
    int z_shift, // 批量的偏移量,用于批处理
    int nplane) { // 通道数或平面数
  auto output_xy = threadIdx.x + blockIdx.x * blockDim.x; // 计算当前线程处理的输出位置索引
  auto output_dim_x = input_dim_x + pad_l + pad_r; // 计算经过填充后的输出宽度
  auto output_dim_y = input_dim_y + pad_t + pad_b; // 计算经过填充后的输出高度

  // 判断当前线程负责的输出位置是否在有效范围内
  if (output_xy < output_dim_x * output_dim_y) {
    // 计算输出位置对应的输入位置索引
    auto index_pair = get_index_mapping2d(
      input_dim_x, input_dim_y,
      output_dim_x, output_dim_y,
      pad_l, pad_t,
      output_xy, y_shift, z_shift, nplane);

    // 使用原子操作累加计算梯度输入。这里的原子操作确保了多个线程更新同一位置时的正确性。
    gpuAtomicAddNoReturn(&grad_input[index_pair.first], grad_output[index_pair.second]);
  }
}

总结

  • PyTorch 中的 CUDA 反射填充通过两个 CUDA 核函数实现:reflection_pad2d_out_kernelreflection_pad2d_backward_out_kernel
    这两个核函数利用了 CUDA 的并行计算能力,可以高效地执行反射填充操作。
    • 其中 reflection_pad2d_out_kernel 理解了之后 reflection_pad2d_backward_out_kernel 理解起来就比较容易了
    • 代码的核心逻辑主要是在 padding 时输入输出之间的映射关系实现部分,也即 get_index_mapping2d 函数实现需要关注下具体实现细节
  • 通过这篇博客,我们简单介绍了 CUDA 和反射填充的概念和应用,提供了实际的代码实现理解和对应资源的链接,希望能帮助读者更深入地理解并利用这些技术。

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

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

相关文章

2024年C语言最新经典面试题汇总(11-20)

C语言文章更新目录 C语言学习资源汇总&#xff0c;史上最全面总结&#xff0c;没有之一 C/C学习资源&#xff08;百度云盘链接&#xff09; 计算机二级资料&#xff08;过级专用&#xff09; C语言学习路线&#xff08;从入门到实战&#xff09; 编写C语言程序的7个步骤和编程…

Chapter 17 Input Filter Design

Chapter 17 Input Filter Design 在switching converter前面我们总想加一个input filter, 这样可以减少输入电流的谐波EMI(conducted electromagnetic interference). 另外, Input filter可以保护converter和负载不受输入电压瞬态变化的影响, 从而提高了系统稳定性. 如下图所…

BEVFormer v2论文阅读

摘要 本文工作 提出了一种具有透视监督&#xff08;perspective supervision&#xff09;的新型鸟瞰(BEV)检测器&#xff0c;该检测器收敛速度更快&#xff0c;更适合现代图像骨干。现有的最先进的BEV检测器通常与VovNet等特定深度预训练的主干相连&#xff0c;阻碍了蓬勃发展…

C++命名空间和内联函数

目录 命名空间 内联函数 概述 特性&#xff1a; 命名空间 在C/C中&#xff0c;变量&#xff0c;函数和和类这些名称都存在于全局作用域中&#xff0c;可能会导致很多冲突&#xff0c;使用命名空间的目的是对标识符的名称进行本地化&#xff0c;避免命名冲突或名字污染&…

鸿蒙OpenHarmony开发实战:【MiniCanvas】

介绍 基于OpenHarmony的Cavas组件封装了一版极简操作的MiniCanvas&#xff0c;屏蔽了原有Canvas内部复杂的调用流程&#xff0c;支持一个API就可以实现相应的绘制能力&#xff0c;该库还在继续完善中&#xff0c;也欢迎PR。 使用说明 添加MiniCanvas依赖 在项目entry目录执行…

由浅到深认识Java语言(21):Math类

该文章Github地址&#xff1a;https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.c…

UE像素流公网(Windows、Liunx)部署无需GPU服务器

@TOC 前言 之前有个前端地图服务项目要改成UE来渲染3D,有需要在云服务器上多实例运行,所以就先研究了Windows版本的像素流云渲染,后来客户的云服务器是Linux版CectOS系统,加上又有了一些后端服务在上面运行了不能重装成Windows,所以就又着手去研究了Linux系统的云渲染。…

【动手学深度学习】深入浅出深度学习之PyTorch基础

目录 一、实验目的 二、实验准备 三、实验内容 1. 数据操作 2. 数据预处理 3. 线性代数 4. 微积分 5. 自动微分 四、实验心得 一、实验目的 &#xff08;1&#xff09;正确理解深度学习所需的数学知识&#xff1b; &#xff08;2&#xff09;学习一些关于数据的实用…

SLAM算法与工程实践——CMake使用(4)

SLAM算法与工程实践系列文章 下面是SLAM算法与工程实践系列文章的总链接&#xff0c;本人发表这个系列的文章链接均收录于此 SLAM算法与工程实践系列文章链接 下面是专栏地址&#xff1a; SLAM算法与工程实践系列专栏 文章目录 SLAM算法与工程实践系列文章SLAM算法与工程实践…

第28章 ansible的使用

第28章 ansible的使用 本章主要介绍在 RHEL8 中如何安装 ansible 及 ansible的基本使用。 ◆ ansible 是如何工作的 ◆ 在RHEL8 中安装ansible ◆ 编写 ansible.cfg 和清单文件 ◆ ansible 的基本用法 文章目录 第28章 ansible的使用28.1 安装ansible28.2 编写ansible.cfg和清…

springboot+vue考试管理系统

基于springboot和vue的考试管理系统 001 springboot vue前后端分离项目 本文设计了一个基于Springbootvue的前后端分离的在线考试管理系统&#xff0c;采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

113 链接集10--ctrl+左键单击多选

1.ctrl左键单击多选&#xff0c;单击单选 精简代码 <div class"model-list"><divmousedown.prevent"handleClick(item, $event)"class"model-list-item"v-for"item in modelList":key"item.id":class"{ model…

UE5中各类型的英文名称缩写(直接用于文件前缀)

真正开发项目时用到的素材文件是相当巨量的&#xff0c;在资产中查找时由于不区分文件夹&#xff0c;因此查找是比较头疼的&#xff0c;所以很多同类型的文件名命名时要加入缩写&#xff0c;并且同一对象的不同功能文件也需要用不同命名来区分。 本文提供初学者内容包中的缩写…

奇舞周刊第523期:来自 rust 生态的强烈冲击?谈谈 Leptos 在语法设计上的精妙之处...

奇舞推荐 ■ ■ ■ 来自 rust 生态的强烈冲击&#xff1f;谈谈 Leptos 在语法设计上的精妙之处 过去很长一段时间&#xff0c;前端框架们都在往响应式的方向发展。同时又由于 React hooks 的深远影响&#xff0c;函数式 响应式成为了不少前端心中最理想的前端框架模样。Solid …

vue3对openlayers使用(加高德,天地图图层)

OpenLayers认识 WebGIS四大框架&#xff1a; Leaflet、OpenLayers、Mapbox、Cesium OpenLayers 是一个强大的开源 JavaScript 地图库&#xff0c;专注于提供可嵌入网页的交互式地图体验。作为一款地理信息系统&#xff08;GIS&#xff09;的前端开发工具&#xff0c;OpenLaye…

java设计模式(1)---总则

设计模式总则 一、概述 1、什么是设计模式 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。 解释下&#xff1a; 分类编目&#xff1a;就是说可以找到一些特征去划分这些设计模式&#xff0c;从而进行分类。 代码设计经验&#xff1a;这句很重…

CRC计算流程详解和FPGA实现

一、概念 CRC校验&#xff0c;中文翻译过来是&#xff1a;循环冗余校验&#xff0c;英文全称是&#xff1a;Cyclic Redundancy Check。是一种通过对数据产生固定位数的校验码&#xff0c;以检验数据是否存在错误的技术。 其主要特点是检错能力强、开销小&#xff0c;易于电路实…

YOLOv8-ROS-noetic+USB-CAM目标检测

环境介绍 Ubuntu20.04 Ros1-noetic Anaconda-yolov8虚拟环境 本文假设ROS和anaconda虚拟环境都已经配备&#xff0c;如果不知道怎么配备可以参考&#xff1a; https://blog.csdn.net/weixin_45231460/article/details/132906916 创建工作空间 mkdir -p ~/catkin_ws/srccd ~/ca…

【javascript】原型继承

在编程中&#xff0c;我们经常会想获取并扩展一些东西。 例如&#xff0c;我们有一个 user 对象及其属性和方法&#xff0c;并希望将 admin 和 guest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容&#xff0c;而不是复制/重新实现它的方法&#xff0c;而只是在其之…

黑马程序员:C++核心编程——2.引用

引用的作用是给变量起别名&#xff0c;本名和别名都可以操作同一块地址的数据。 注意事项 1&#xff09;引用必须初始化且在初始化后不可改变。大白话是创建时不能不说是谁的别名&#xff0c;更不能在创建之后修改为其他人的别名。 2&#xff09;*重点&#xff1a;函数传参的…