本网页已闲置超过3分钟,按键盘任意键或点击空白处,即可回到网页

基于树莓派的立体视觉和激光雷达驱动驴车

发布时间:2022-01-08
分享到:

基于树莓派的立体视觉和激光雷达驱动驴车

发布时间:2022-01-08
分享到:

本项目 Donkey Car 配备 Ultra96 板、Raspberry Pi、FPGA 加速立体视觉、MIPI CSI-2 图像采集、LiDAR 传感器和 AI。

在这个项目中,我将记录使用FPGA 加速立体视觉和LiDAR的高级视觉系统增强驴车的构建。

该项目由以下主要组成部分组成:

  • 具有 4 个 Cortex-A53 内核的Raspberry Pi 3 - 运行驱动汽车的 AI
  • Avnet / Xilinx Ultra96 Board - 用于视频采集和处理。两个 OV5647 相机的高速 MIPI CSI-2 输入是使用在 Xilinx Zynq UltraScale+ MPSoC 的可编程逻辑 (FPGA) 部分中实现的 2 个视频管道捕获的。捕获的图像由 OpenCV / xfOpenCV 应用程序处理。使用硬件加速立体匹配算法 ( xfOpenCV StereoBM)应用于输入图像并实时计算场景深度
  • Donkey Car - 用于 PoC 的遥控车
  • 2 x OV5647 5 百万像素 NoIR相机
  • TFMini LiDAR 传感器

第 1 部分:入门
1.驴车入门
驴车的建造有很好的记录。

我们可以从硬件部分开始:

  • 首先,我们需要从我们的遥控车上拆下装饰性塑料外壳和外壳上的一些零件。
  • 然后我们可以继续标准的构建过程。
  • 结果应如下所示:

该软件部分包括:

  • 将软件安装到 SD 卡。
  • 校准油门和转向的 PWM 值。
  • 启动 Web 界面并获得试驾。

2. Ultra96 入门
我尝试使用 Avnet Ultra96 板的第一件事(当然没有阅读入门指南 :D),是将电缆插入其微型 USB 源,然后查看它是否通电。它没有。

原来,Ultra96必须通过 12V 2A 电源的插孔连接器供电。插孔连接器是EIAJ-03 (外径为 4.75 毫米,内径为 1.7 毫米),这显然不是一个非常常见的连接器。由于我在房子里没有找到这样的连接器,而且(8 月)当地的电子商店关门了,我决定“暂时”在电路板底部的连接器引脚上焊接两根电线。

在仔细检查没有短路之后,我能够从工作台电源(具有 12V 和 2.10A 的电流限制)为电路板供电:

给它加电,Ultra96 板子在大约 30-40 时启动并创建一个名为 的 WiFi 网络Ultra96_<Mac_Address>。

要访问 Ultra96,我们需要连接此网络并从浏览器访问192.168.2.1 。应显示如下用户界面。这允许:

  • 运行示例 - 例如,控制板载 LED
  • 更改设置 - 例如,更改 WiFi 设置并连接到我的家庭网络:

但是,Ultra96 具有比这更有趣的功能。它有一个 FPGA!

Vivado 设计套件

要创建一个FPGA的基础项目,我们需要安装赛灵思Vivado设计套件。这需要一些时间,但结果应该如下所示:

现在我们可以开始创建一个示例项目。

在赛灵思SDK用于构建引导映像:

结果将是一个 BOOT.bin 文件,应将其复制到 SD 卡。Ultra96 将从 SD 卡启动,并使用提供的映像对 FPGA 进行编程。

闪烁 LED 示例应向低速接头的引脚 3 输出大约 2-3 Hz 的方波。

由于输出为 1.8V 逻辑电平,而且我还没有带电平转换器的扩展板,我使用万用表和示波器检查信号。

第 2 部分:添加 LiDaR 模块
3. TFmini LiDaR 传感器
我使用的LiDAR (光检测和测距)传感器是Benewake的TFMini。

传感器通过测量被测量物体反射的光信号的飞行时间来测量距离。然后使用光速计算到物体的距离。

Benewake 有一个演示应用程序,可用于测试传感器:

此外,建议将固件更新到最新版本(如果尚未更新):

4. 构建 LiDaR 模块
传感器测量单个方向的距离。这不是很有用,所以我决定建立一个简单的旋转平台,允许在180° 度测量距离。传感器安装在伺服电机上,允许180°旋转。通过这种方式,LiDAR 传感器能够测量到驴车周围物体的距离。使用收集的数据可以构建楼层地图。

所述TFmini传感器上方连通串行连接(115200波特)使用自定义协议。传感器的VCC (5V) 、GND 、TX和RX (3.3V) 电缆连接到Raspberry Pi的相应引脚。

在之后的硬件串口的的树莓派的启用从raspi-config,我们应该能够在接收数据/dev/ttyS0端口。

TFmini使用的协议和数据格式是自定义的。读数以8 字节的数据包形式出现,格式如下:

这个指令有一个示例代码,可用于测试传感器。

所述伺服电机连接至信道15的的伺服控制器板。该donkey calibrate命令可用于测试伺服并找出最小和最大脉冲值:

有了最小值和最大值,我创建了一个小脚本来移动伺服并打印出从 TFMini 传感器读取的值。

5. 创建驴车软件部分
下一步是将我们的新传感器集成到 Donkey 汽车软件中。为此,我们需要实现一个新的 Vehicle 部分(一个线程部分),它基本上是一个 Python 类,TFMiniLidar在我的例子中,使用以下方法:

  • update(self)- 此方法在开始时被调用,它将成为传感器线程的入口点。它基本上是一个无限循环,控制伺服电机并收集传感器数据。该传感器数据被存储在阵列90层的元件叫做。每个值代表2° 切片的距离读数。frame
  • run_threaded(self)-定期调用此方法并应返回部件的输出。在我们的例子中,我们返回完整frame数组的副本。利用这种技术的驴车软件总是得到的值的完整180°视口,而不管传感器的当前旋转的。

有了我们的TFMiniLidar一部分,我们现在可以更新manage.py以使用新的传感器:

该网络控制器也被更新,以显示该激光雷达传感器数据:

  • 该WebController部分已更新lidar/dist_array为输入
  • 一个LidarHandler加入到服务/lidar路径-这是实现的Web套接字处理程序,它发送该传感器数据作为JSON阵列周期性地
  • vehicle.html并style.css进行了修改,通过摄像机图像添加透明HTML5画布
  • main.js更新为打开 WebSocket 连接/lidar并使用传感器数据。然后将传感器数据以半圆形图案绘制在 HTML5 画布上

现在我们可以开始开车记录一些数据了

6. 训练自动驾驶仪
现在,我们想要创建一个使用我们的LiDAR 传感器收集的数据的自动驾驶仪。我们将传感器数据保存在tub 文件中,但我们还需要调整训练模型以使用新数据。

Donkey 汽车使用的默认模型是. 我们将添加一个新的输入。将包括:default_categoricaldefault_categorical

  • 一个输入层,相机图像
  • 四个2D 卷积层- 这些在图像中进行模式识别
  • 一个扁平化层-转换先前层1D的输出
  • 两个密集连接的 NN 层,每个层结合一个dropout (10%)层

我们可以在flatten 层之后添加一个LiDAR 输入层。我们可以使用该函数将LiDAR 输入附加到前一层的输出:concatenate()

第 3 部分:立体视觉
7. 第二个摄像头模块

Donkey Car Kit 带有一个无红外广角 FOV160° 5 兆像素摄像头模块,这是一个基于 Omnivision OV5647 传感器的摄像头模块。

对于立体视觉,我们需要两个摄像头,所以我又购买了一个模块。

该OV5647传感器使用MIPI相机串行接口(版本2)进行通信。Ultra96 板卡最多支持两个 CSI-2 摄像机。Avnet 的 Ultra96 硬件用户指南似乎也证实了这一点。

8. Ultra96 的 MIPI 适配器
为了连接两个摄像头,我使用了 AISTARVISION MIPI Adapter (v2.1) 96Boards适配器板。

该板允许连接两个 CSI-2 摄像机。它接受具有不同类型连接器的相机,包括 Raspberry Pi 相机使用的柔性电缆类型。

9. 相机支架
对于立体视觉,需要并排放置两个摄像头。对于第一次测试,我决定尝试在它们之间保持 50 毫米的距离。

为了在 Donkey Car 上安装两个摄像头,我设计并打印了一个适合原始摄像头安装座的双摄像头安装座。

10. 构建 MIPI CSI-2 视频管道
MIPI CSI-2 是移动设备中使用的相机设备的行业标准。它的心理接口,即所谓的 D-PHY,使用高速差分信号。一个时钟通道和多达 4 个数据通道(OV5647 使用两个)用于传输图像帧。相机的配置是使用 I2C 兼容接口完成的。

Xilinx UltraScale+ MPSoC-s,包括 Ultra96 板中使用的 ZU3EG,具有能够进行差分信号传输的 I/O 引脚。在 Ultra96 上,这些路由到高速连接器的 MIPI CSI-2 和 DSI 引脚,如 96boards CE 规范中所定义。

为了能够接收图像,我们需要在可编程逻辑中实现一个视频管道。为了构建视频管道,我们将使用 Xilinx 的一些预先构建的视频 IP。图像数据使用 AXI Stream 链接在组件之间传输,而对于配置,每个组件都有一个 AXI Lite 从接口。

视频管道具有以下组件:

  • MIPI CSI-2 Rx 子系统——一个完整的 MIPI CSI-2 接收器实现,提供 D-PHY 信号处理并管理所有 MIPI 相关的东西——它输出图像接收的原始图像(OV5647 的情况下为 RAW8 格式)数据传感器
  • Sensor Demosaic - 对 RAW8 数据进行 debayering - 输出 24 位 RGB 图像 -图像传感器具有检测红、绿或蓝光的像素。这些排列成所谓的“拜耳”(象棋盘)图案。传感器以 RAW8 格式输出原始数据。这意味着每个像素只有一种颜色(红色、绿色或蓝色)的信息。这对于图像处理来说不太方便,因此可能需要对图像数据进行插值以具有每个像素的所有树颜色。这个过程称为去拜耳化。
  • Gamma LUT - 基于查找表进行 Gamma 校正 - 在输入和输出中使用 24 位 RGB
  • 执行色彩空间转换的视频处理子系统 - 能够执行包括对比度、亮度和红/绿/蓝增益控制在内的色彩校正任务 - 在输入和输出中也使用 24 位 RGB
  • 仅缩放配置中的视频处理子系统 - 提供缩放、色彩空间转换和色度重采样功能 - 还将像素从 24 位 RGB 转换为更紧凑的 YUV 4:2:2 格式(24 位与 8 位)每个像素)
  • 帧缓冲区写入 - 将图像流式传输到 DDR 内存
  • 此外,视频管道组件的复位引脚连接到 EMIO GPIO 引脚

组件配置如下:

11. 构建 PetaLinux
为了能够使用视频管道,我们需要使用 PetaLinux,并且需要使用自定义硬件来构建它。

我们将使用 V4L2(Linux 视频)驱动程序基础设施来控制我们的视频管道,并将捕获的图像作为节点在标准接口中公开/dev/video0。

为了创建自定义硬件,我从官方 Ultra96 BSP 开始,并进行了一些其他更改,添加了上述视频管道。设计如下:

之后,我们需要打开 Elaborated Design 并在 I/O Ports 面板中将 MIPI D-PHY 引脚配置为clk_lane = N2, data_lane_0 = N5, data_lane_1 = M2。

在此之后,我们可以运行综合和实现。结果应如下所示:

如果这些成功,我们可以生成一个比特流,我们可以为 PetaLinux导出硬件平台(包括比特流)。

下一步是在 Ultra96 PetaLinux 项目中导入新的硬件平台:

为了被 Linux 识别,我们需要将我们的 Video Pipeline 组件添加到设备树中。在 PetaLinux 构建中,我们通过编辑来实现system-user.dtsi:

/*
Notes:
- EMIO = &gpio0 + 78
- AXI Clock = clocking_wizard_clk2
*/
/{
  cam_clk: cam_clk {
      #clock-cells = <0>;
      compatible = "fixed-clock";
      clock-frequency = <25000000>;
  };
  clocking_wizard_clk2: clocking_wizard_clk2@0 {
      #clock-cells = <0>;
      compatible = "fixed-factor-clock";
      clocks = <&clk 71>; /* fclk0 */
      clock-div = <6>;
      clock-mult = <12>;
  };
};
&fclk0 {
  status = "okay";
};
&i2csw_2 {
  ov5647_0: camera@36 {
      compatible = "ovti,ov5647";
      reg = <0x36>;
      clocks = <&cam_clk>;
      status = "okay";
      port {
          ov5647_0_to_mipi_csi2_rx_0: endpoint {
              remote-endpoint = <&mipi_csi2_rx_0_from_ov5647_0>;
              clock-lanes = <0>;
              data-lanes = <1 2>;
          };
      };
  };
};
&mipi_csi2_rx0_mipi_csi2_rx_subsyst_0 {
  compatible = "xlnx,mipi-csi2-rx-subsystem-3.0";   
  reset-gpios = <&gpio 90 GPIO_ACTIVE_LOW>;
  xlnx,max-lanes = <0x2>;
  xlnx,vc = <0x4>;
  xlnx,csi-pxl-format = "RAW8";
  xlnx,vfb;
  xlnx,dphy-present;
  xlnx,ppc = <0x2>;
  xlnx,axis-tdata-width = <0x20>;
  ports {
      #address-cells = <1>;
      #size-cells = <0>;
      port@0 {
          reg = <0>;
          xlnx,video-format = <XVIP_VF_RBG>;
          xlnx,video-width = <8>;
          mipi_csi2_rx_0_to_demosaic_0: endpoint {
              remote-endpoint = <&demosaic_0_from_mipi_csi2_rx_0>;
          };
      };
      port@1 {
          reg = <1>;
          xlnx,video-format = <XVIP_VF_RBG>;
          xlnx,video-width = <8>;
          mipi_csi2_rx_0_from_ov5647_0: endpoint {
              data-lanes = <1 2>;
              remote-endpoint = <&ov5647_0_to_mipi_csi2_rx_0>;
          };
      };
  };
};
&mipi_csi2_rx0_v_demosaic_0 {
  compatible = "xlnx,v-demosaic";
  clocks = <&clocking_wizard_clk2>;
  reset-gpios = <&gpio 85 GPIO_ACTIVE_LOW>;
  ports {
      #address-cells = <1>;
      #size-cells = <0>;
      port@0 {
          reg = <0>;
          xlnx,video-width = <8>;
          demosaic_0_from_mipi_csi2_rx_0: endpoint {
              remote-endpoint = <&mipi_csi2_rx_0_to_demosaic_0>;
          };
      };
      port@1 {
          reg = <1>;
          xlnx,video-width = <8>;
          demosaic_0_to_gamma_lut_0: endpoint {
              remote-endpoint = <&gamma_lut_0_from_demosaic_0>;
          };
      };
  };
};
&mipi_csi2_rx0_v_gamma_lut_0 {
  compatible = "xlnx,v-gamma-lut";
  clocks = <&clocking_wizard_clk2>;
  reset-gpios = <&gpio 86 GPIO_ACTIVE_LOW>;
  ports {
      #address-cells = <1>;
      #size-cells = <0>;
      port@0 {
          reg = <0>;
          xlnx,video-width = <8>;
          gamma_lut_0_from_demosaic_0: endpoint {
              remote-endpoint = <&demosaic_0_to_gamma_lut_0>;
          };
      };
      port@1 {
          reg = <1>;
          xlnx,video-width = <8>;
          gamma_lut_0_to_csc_0: endpoint {
              remote-endpoint = <&csc_0_from_gamma_lut_0>;
          };
      };
  };
};
&mipi_csi2_rx0_v_proc_ss_csc_0 {
  compatible = "xlnx,v-vpss-csc";
  clocks = <&clocking_wizard_clk2>;
  reset-gpios = <&gpio 87 GPIO_ACTIVE_LOW>;
  ports {
      #address-cells = <1>;
      #size-cells = <0>;
      port@0 {
          reg = <0>;
          xlnx,video-format = <XVIP_VF_RBG>;
             xlnx,video-width = <8>;
          csc_0_from_gamma_lut_0: endpoint {
              remote-endpoint = <&gamma_lut_0_to_csc_0>;
          };
      };
      port@1 {
          reg = <1>;
          xlnx,video-format = <XVIP_VF_RBG>;
          xlnx,video-width = <8>;
          csc_0_to_scaler_0: endpoint {
              remote-endpoint = <&scaler_0_from_csc_0>;
          };
      };
  };
};
&mipi_csi2_rx0_v_proc_scaler_0 {
  compatible = "xlnx,v-vpss-scaler";
  clocks = <&clocking_wizard_clk2>;
  reset-gpios = <&gpio 88 GPIO_ACTIVE_LOW>;
  xlnx,num-hori-taps = <8>;
  xlnx,num-vert-taps = <8>;
  xlnx,pix-per-clk = <2>;
  ports {
      #address-cells = <1>;
      #size-cells = <0>;
      port@0 {
          reg = <0>;
          xlnx,video-format = <XVIP_VF_RBG>;
             xlnx,video-width = <8>;
          scaler_0_from_csc_0: endpoint {
              remote-endpoint = <&csc_0_to_scaler_0>;
          };
      };
      port@1 {
          reg = <1>;
          xlnx,video-format = <XVIP_VF_YUV_422>;
          xlnx,video-width = <8>;
          scaler_0_to_vcap_0: endpoint {
              remote-endpoint = <&vcap_0_from_scaler_0>;
          };
      };
  };
};
&mipi_csi2_rx0_v_frmbuf_wr_0 {
  #dma-cells = <1>;
  compatible = "xlnx,axi-frmbuf-wr-v2.1";
  reset-gpios = <&gpio 89 GPIO_ACTIVE_LOW>;
  xlnx,dma-addr-width = <32>;
  xlnx,vid-formats = "yuyv","uyvy","y8";
  xlnx,pixels-per-clock = <2>;
};
&amba_pl {
  vcap0: video_cap {
      compatible = "xlnx,video";
      dmas = <&mipi_csi2_rx0_v_frmbuf_wr_0 0>;
      dma-names = "port0";
      ports {
          #address-cells = <1>;
          #size-cells = <0>;
          port@0 {
              reg = <0>;
              direction = "input";
              vcap_0_from_scaler_0: endpoint {
                  remote-endpoint = <&scaler_0_to_vcap_0>;
              };
          };            
      };
  };
};

基于设备树,Linux 将为 OV5647 和 Xilinx 视频 IP 组件加载适当的内核模块。我们可以使用以下方法构建 PetaLinux:

成功的构建在文件夹中生成了以下文件images/linux:

  • 的BOOT.BIN和image.ub是U-Boot的引导加载程序和Linux内核映像-应将文件复制到SD卡的引导分区
  • 这rootfs.ext4是根文件系统映像 - 我们应该使用命令将其写入 SD 卡的根分区sudo dd if=images/linux/rootfs.ext4 of=/dev/mmcblk0p2

现在,我们可以在 Ultra96 中启动了。我一切正常,我们应该看到一个/dev/video0,一个/dev/media0和一些/dev/v4l-subdev-*设备文件。

使用media-ctl我们可以检查我们的视频管道:

root@Ultra96:~# media-ctl -p
Media controller API version 4.14.0
Media device information
------------------------
driver          xilinx-video
model           Xilinx Video Composite Device
serial          
bus info        
hw revision     0x0
driver version  4.14.0
Device topology
- entity 1: video_cap output 0 (1 pad, 1 link)
           type Node subtype V4L flags 0
           device node name /dev/video0
       pad0: Sink
               <- "b0000000.v_proc_ss":1 [ENABLED]
- entity 5: ov5647 4-0036 (1 pad, 1 link)
           type V4L2 subdev subtype Sensor flags 0
           device node name /dev/v4l-subdev0
       pad0: Source
               -> "80120000.mipi_csi2_rx_subsystem":1 [ENABLED]
- entity 7: 80120000.mipi_csi2_rx_subsystem (2 pads, 2 links)
           type V4L2 subdev subtype Unknown flags 0
           device node name /dev/v4l-subdev1
       pad0: Source
               [fmt:RBG24/1920x1080 field:none]
               -> "b0050000.v_demosaic":0 [ENABLED]
       pad1: Sink
               [fmt:RBG24/1920x1080 field:none]
               <- "ov5647 4-0036":0 [ENABLED]
- entity 10: b0050000.v_demosaic (2 pads, 2 links)
            type V4L2 subdev subtype Unknown flags 0
            device node name /dev/v4l-subdev2
       pad0: Sink
               [fmt:SRGGB8/1280x720 field:none]
               <- "80120000.mipi_csi2_rx_subsystem":0 [ENABLED]
       pad1: Source
               [fmt:RBG24/1280x720 field:none]
               -> "b0070000.v_gamma_lut":0 [ENABLED]
- entity 13: b0070000.v_gamma_lut (2 pads, 2 links)
            type V4L2 subdev subtype Unknown flags 0
            device node name /dev/v4l-subdev3
       pad0: Sink
               [fmt:RBG24/1280x720 field:none]
               <- "b0050000.v_demosaic":1 [ENABLED]
       pad1: Source
               [fmt:RBG24/1280x720 field:none]
               -> "b0040000.v_proc_ss":0 [ENABLED]
- entity 16: b0040000.v_proc_ss (2 pads, 2 links)
            type V4L2 subdev subtype Unknown flags 0
            device node name /dev/v4l-subdev4
       pad0: Sink
               [fmt:RBG24/1280x720 field:none]
               <- "b0070000.v_gamma_lut":1 [ENABLED]
       pad1: Source
               [fmt:RBG24/1280x720 field:none]
               -> "b0000000.v_proc_ss":0 [ENABLED]
- entity 19: b0000000.v_proc_ss (2 pads, 2 links)
            type V4L2 subdev subtype Unknown flags 0
            device node name /dev/v4l-subdev5
       pad0: Sink
               [fmt:RBG24/1280x720 field:none]
               <- "b0040000.v_proc_ss":1 [ENABLED]
       pad1: Source
               [fmt:UYVY/1920x1080 field:none]
               -> "video_cap output 0":0 [ENABLED]

V4L2 a 成功初始化。最初,它们配置了错误的格式/分辨率,因此我们需要设置正确的格式/分辨率:

root@Ultra96:~# # MIPI RX:
root@Ultra96:~# media-ctl -v -d /dev/media0 -V '"80120000.mipi_csi2_rx_subsystem":0 [fmt:SBGGR8/640x480]'
Opening media device /dev/media0
Enumerating entities
Found 7 entities
Enumerating pads and links
Setting up format SBGGR8 640x480 on pad 80120000.mipi_csi2_rx_subsystem/0
Format set: SBGGR8 640x480
Setting up format SBGGR8 640x480 on pad b0050000.v_demosaic/0
Format set: SBGGR8 640x480
root@Ultra96:~# media-ctl -v -d /dev/media0 -V '"80120000.mipi_csi2_rx_subsystem":1 [fmt:SBGGR8/640x480]'
Opening media device /dev/media0
Enumerating entities
Found 7 entities
Enumerating pads and links
Setting up format SBGGR8 640x480 on pad 80120000.mipi_csi2_rx_subsystem/1
Format set: SBGGR8 640x480
root@Ultra96:~# 
root@Ultra96:~# # Demosaic
root@Ultra96:~# media-ctl -v -d /dev/media0 -V '"b0050000.v_demosaic":1 [fmt:RBG24/640x480]'
Opening media device /dev/media0
Enumerating entities
Found 7 entities
Enumerating pads and links
Setting up format RBG24 640x480 on pad b0050000.v_demosaic/1
Format set: RBG24 640x480
Setting up format RBG24 640x480 on pad b0070000.v_gamma_lut/0
Format set: RBG24 640x480
root@Ultra96:~# 
root@Ultra96:~# # Gamma LUT
root@Ultra96:~# media-ctl -v -d /dev/media0 -V '"b0070000.v_gamma_lut":1 [fmt:RBG24/640x480]'
Opening media device /dev/media0
Enumerating entities
Found 7 entities
Enumerating pads and links
Setting up format RBG24 640x480 on pad b0070000.v_gamma_lut/1
Format set: RBG24 640x480
Setting up format RBG24 640x480 on pad b0040000.v_proc_ss/0
Format set: RBG24 640x480
root@Ultra96:~# 
root@Ultra96:~# # SS CSC
root@Ultra96:~# media-ctl -v -d /dev/media0 -V '"b0040000.v_proc_ss":1 [fmt:RBG24/640x480]'
Opening media device /dev/media0
Enumerating entities
Found 7 entities
Enumerating pads and links
Setting up format RBG24 640x480 on pad b0040000.v_proc_ss/1
Format set: RBG24 640x480
Setting up format RBG24 640x480 on pad b0000000.v_proc_ss/0
Format set: RBG24 640x480
root@Ultra96:~# 
root@Ultra96:~# # SS SCALER
root@Ultra96:~# media-ctl -v -d /dev/media0 -V '"b0000000.v_proc_ss":1 [fmt:UYVY/640x480]'
Opening media device /dev/media0
Enumerating entities
Found 7 entities
Enumerating pads and links
Setting up format UYVY 640x480 on pad b0000000.v_proc_ss/1
Format set: UYVY 640x480

现在我们可以尝试使用该实用程序捕获一些帧yavta:

root@Ultra96:~# width=640
root@Ultra96:~# height=480
root@Ultra96:~# size=${width}x${height}
root@Ultra96:~# frames=8
root@Ultra96:~# skip=0
root@Ultra96:~# root@Ultra96:~# yavta -c$frames -p -F --skip $skip -f UYVY -s $size /dev/video0
Device /dev/video0 opened.
Device `video_cap output 0' on `platform:video_cap:0' is a video output (without mplanes) device.
Video format set: UYVY (59565955) 640x480 field none, 1 planes: 
* Stride 1280, buffer size 614400
Video format: UYVY (59565955) 640x480 field none, 1 planes: 
* Stride 1280, buffer size 614400
8 buffers requested.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 0/0 mapped at address 0x7f9e00b000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 1/0 mapped at address 0x7f9df75000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 2/0 mapped at address 0x7f9dedf000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 3/0 mapped at address 0x7f9de49000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 4/0 mapped at address 0x7f9ddb3000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 5/0 mapped at address 0x7f9dd1d000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 6/0 mapped at address 0x7f9dc87000.
length: 1 offset: 4278322640 timestamp type/source: mono/EoF
Buffer 7/0 mapped at address 0x7f9dbf1000.
Press enter to start capture
0 (0) [-] none 0 0 B 209.798257 209.798333 23.472 fps ts mono/EoF
1 (1) [-] none 1 0 B 209.823763 209.823780 39.206 fps ts mono/EoF
2 (2) [-] none 2 0 B 209.849348 209.849361 39.085 fps ts mono/EoF
3 (3) [-] none 3 0 B 209.874935 209.874947 39.082 fps ts mono/EoF
4 (4) [-] none 4 0 B 209.900520 209.900532 39.085 fps ts mono/EoF
5 (5) [-] none 5 0 B 209.926106 209.926185 39.084 fps ts mono/EoF
6 (6) [-] none 6 0 B 209.951693 209.951764 39.082 fps ts mono/EoF
7 (7) [-] none 7 0 B 209.977279 209.977293 39.084 fps ts mono/EoF
Captured 8 frames in 0.221639 seconds (36.094572 fps, 0.000000 B/s).
8 buffers released.

这将保存带有名称的原始 (UYVY) 文件frame-000xx.bin。我使用在线工具检查了他们的内容:

12. 添加第二个视频管道
为了能够使用第二个摄像头,我们需要在 Vivado 设计中添加第二个 MIPI CSI-2 接口。还需要做以下几步:

  • 复制并粘贴mipi_phy_if_0输入端口 - (将自动添加一个mipi_phy_if_1端口)
  • 复制并粘贴mipi_csi2_rx0块(一个blockwill被自动添加)mipi_csi2_rx1
  • 连接mipi_phy_if_1到mipi_phy_if引脚mipi_csi2_rx1
  • 连接时钟,复位,AXI,中断和GPIO端口mip_csi2_rx1,类似于mipi_csi2_rx0那些
  • 打开mipi_csi2_rx1并编辑 MIPI CSI-2 Rx 子系统并将 D-PHY 引脚更改为clk_lane = T3, data_lane_0 = P3,data_lane_1 = U2
  • 更改复位引脚的 Slice 块以使用 GPIO-s 引脚 12-16 (而不是 7-11)

更改后的设计应如下所示:

在地址编辑器选项卡中,我们需要为新条目分配地址范围。

在仔细检查 I/O 端口是否正确后,我们可以运行 Synthesis and Implementation。

如果这些成功,我们需要生成比特流并导出硬件。

然后我们需要使用新的硬件定义文件更新 PetaLinux 项目。在设备树中,我们将为第二个视频管道添加必要的节点。之后我们就可以构建项目了。

分析一些 RAW 帧(UYVY 格式)显示,Chroma(U,V)通道存在噪点,但 Luma(Y)通道似乎清晰:

由于噪声仅出现在色度 (U, V) 通道中,这意味着我们应该能够获得一些清晰的灰度图像,只保留亮度 (Y) 通道。为此,我编写了一个 V4L2 图像捕获模块,它从相机捕获 UYVY 帧,丢弃 U、V 通道并输出灰度图像:

所以,通过这种方式,我得到了一些可以使用的图像。

13.OpenCV
为了进行图像处理,我们将使用 OpenCV。要在 Ultra96 上安装它,我们可以使用智能包管理器:

然后我们可以使用以下 Python 代码片段从相机中捕获一些帧:

这会以 PNG 格式保存 10 帧:

此外,我们可以对捕获的图像进行图像处理。例如,我们可以很容易地应用 Sobel 过滤器:

14. OV5647:分辨率和视口
使用 OpenCV 进行一些测试时,我观察到 OV5647 的输出图像与镜头轴不对齐。我觉得相机总是向上看。

结果证明,OV5647 内核驱动程序有点初级。唯一支持的分辨率是 640x480,这仅使用传感器区域的左上角。这意味着输出图像的中心和镜头轴相互偏移,给人的印象是相机总是向上看。

(注:OV5647 摄像头连接树莓派时,由 Broadcom SoC 视频核心的视频核心控制。不幸的是,视频核心运行专有代码,因此我们无法从中获得启发)

为了解决这个问题,我尝试使用其他配置扩展 OV5647 内核驱动程序。5647 的输出分辨率和传感器使用面积由一组寄存器控制:

和其他几个控制诸如子采样、时钟速度等的寄存器。

我尝试了多种分辨率,但并非所有分辨率都有效,主要是因为视频管道的稳定性问题:

  • - 1920×1080(全高清)由MIPI RX接收帧,但不会获得通过视频管线
  • 2560x1920,被 VPSS Scaler 缩小到 1280x960 - 与上述相同的问题
  • 640x480,成像窗口移动到中心(蓝色框)- 这在几次尝试后有效(注意:分辨率在传感器中从 1280x960 进行子采样)
  • 1280x960,由 VPSS Scaler 从 2560x1920 缩小到 640x480 的子采样 - 这有效

以上配置的区别如下图:

初始 640x480 分辨率和 640x480 分辨率(从 1280x960 缩小)之间的视口差异如下所示:

最新配置输出图像和镜头对齐,几乎使用了整个传感器区域。这意味着驴车有一个大的视口(比 Raspberry Pi 设置好一点)。

注:内核补丁中存在的的PetaLinux文件。

15.立体视觉
在图像处理中,立体视觉是从两个 2D 图像中提取 3D 信息的过程。通常,使用两个水平位移的相机来获取场景的两个视图。

在此之后,立体匹配算法将尝试匹配两个图像中的对应点。使用来自左侧 (x1, y1) 和右侧 (x2, y2) 图像的图像坐标计算点 (x, y, z) 的真实世界坐标。立体匹配算法的输出是视差图,表示相应图像像素的水平坐标差异。视差值与场景中物体的距离成反比。

16. 相机校准
要进行立体视觉,我们需要进行相机校准。相机标定的范围是确定两个相机的内在(场景无关)和外在(现实世界与相机坐标系)参数,以及两个相机的相对位置和旋转。

校准是通过在不同位置和方向拍摄一组打印出来的棋盘图案的照片来完成的:

为了成功校准大约需要 40-60 个图像对(左 + 右):

校准分为三个主要步骤:

  • 识别每个图像中棋盘图案的角 -cv::findChessboardCorners可用于执行此操作
  • 校准两个摄像头并计算两个摄像头之间的转换 - 使用函数完成cv::stereoCalibrate
  • 通过调用完成-计算旋转对两个摄像机的投影矩阵cv::stereoRectify函数与的结果cv::stereoCalibrate函数的输出cv::stereoRectify是两个旋转矩阵 R1、R2 和两个投影矩阵 P1、P2。将这些应用于输入图像使极线平行,从而简化了立体对应问题。

cv::命名空间中的校准功能并不总是与 Donkey Car 中使用的鱼眼镜头式广角镜头配合得很好。幸运的是,上述功能的 fiseye 镜头优化变体有cv::fisheye::命名空间。

立体校准过程在以下 GitHub 存储库中得到了很好的演示sourishg:

  • https://github.com/sourishg/stereo-calibration
  • https://github.com/sourishg/fisheye-stereo-calibration

我使用这些代码来校准相机。校准产生的参数保存在cam_stereo.yml文件中。

这些参数可用于在进行立体匹配之前校正输入图像:

  • 首先从cam_stereo.yml文件中加载输入参数,并cv::fisheye::initUndistortRectifyMap为每个摄像机调用。对于每个相机,函数的输出是一个 X 和 Y 变换矩阵,可用作函数的参数remap

修改的结果是这样的:

17.立体匹配
校正后的图像可用于计算视差图。OpenCV 中有多种立体匹配算法可用。我尝试了 StereoBM 和 StereoSGBM 算法:

int stereo_run(int num_imgs, char* img_dir, char* leftimg_filename, char* rightimg_filename)
{
    string calib_file = "cam_stereo.yml";
    Mat R1, R2, P1, P2, Q;
    Mat K1, K2, R;
    Vec3d T;
    Mat D1, D2;
    Size imgSize(640, 480);
    cv::FileStorage fs1(calib_file, cv::FileStorage::READ);
    cout << "K1" << endl;
    fs1["K1"] >> K1;
    cout << "K2" << endl;
    fs1["K2"] >> K2;
    cout << "D1" << endl;
    fs1["D1"] >> D1;
    cout << "D2" << endl;
    fs1["D2"] >> D2;
    cout << "R" << endl;
    fs1["R"] >> R;
    cout << "T" << endl;
    fs1["T"] >> T;
    cout << "R1" << endl;
    fs1["R1"] >> R1;
    cout << "R2" << endl;
    fs1["R2"] >> R2;
    cout << "P1" << endl;
    fs1["P1"] >> P1;
    cout << "P2" << endl;
    fs1["P2"] >> P2;
    cout << "Q" << endl;
    fs1["Q"] >> Q;
    cv::Mat lmapx, lmapy, rmapx, rmapy;
    cv::Mat imgU1, imgU2;
    cv::Mat r;
    cv::fisheye::initUndistortRectifyMap(K1, D1, R1, P1, imgSize, CV_32F,
            lmapx, lmapy);
    cv::fisheye::initUndistortRectifyMap(K2, D2, R2, P2, imgSize, CV_32F,
            rmapx, rmapy);
    Ptr<StereoBM> stereoBM = StereoBM::create(128, 21);
    Ptr<StereoSGBM> stereoSGBM = StereoSGBM::create(0,    //int minDisparity
           96,     //int numDisparities
           21,      //int SADWindowSize
           600,    //int P1 = 0
           2400,   //int P2 = 0
           10,     //int disp12MaxDiff = 0
           16,     //int preFilterCap = 0
           2,      //int uniquenessRatio = 0
           20,    //int speckleWindowSize = 0
           30,     //int speckleRange = 0
           true);  //bool fullDP = false
    Mat dispOut, dispNorm;
     for (int i = 72; i <= 78; i++) {
        char left_img[100], right_img[100];
        sprintf(left_img, "%s%s%s%d.png", img_dir, "stereo/", leftimg_filename, i);
        sprintf(right_img, "%s%s%s%d.png", img_dir, "stereo/", rightimg_filename, i);
        img1 = imread(left_img, CV_LOAD_IMAGE_GRAYSCALE);
        img2 = imread(right_img, CV_LOAD_IMAGE_GRAYSCALE);
        cv::remap(img1, imgU1, lmapx, lmapy, cv::INTER_LINEAR);
        cv::remap(img2, imgU2, rmapx, rmapy, cv::INTER_LINEAR);
        sprintf(left_img, "%s%s%s%d.png", img_dir, "stereo/rect_", leftimg_filename, i);
        sprintf(right_img, "%s%s%s%d.png", img_dir, "stereo/rect_", rightimg_filename, i);
        imwrite(left_img, imgU1);
        imwrite(right_img, imgU2);
        double minVal; double maxVal;
        stereoBM->compute(imgU1, imgU2, dispOut);
        minMaxLoc( dispOut, &minVal, &maxVal );
        dispOut.convertTo(dispNorm, CV_8UC1, 255/(maxVal - minVal));
        sprintf(left_img, "%s%s%s%d.png", img_dir, "stereo/disp_BM_", leftimg_filename, i);
        imwrite(left_img, dispNorm);
        stereoBM->compute(imgU1, imgU2, dispOut);
        minMaxLoc( dispOut, &minVal, &maxVal );
        dispOut.convertTo(dispNorm, CV_8UC1, 255/(maxVal - minVal));
        stereoSGBM->compute(imgU1, imgU2, dispOut);
        minMaxLoc( dispOut, &minVal, &maxVal );
        dispOut.convertTo(dispNorm, CV_8UC1, 255/(maxVal - minVal));
        sprintf(left_img, "%s%s%s%d.png", img_dir, "stereo/disp_SGBM_", leftimg_filename, i);
        imwrite(left_img, dispNorm);
     }
 return 0;
}

原始输入图像:

校正后的输入图像:

StereoBM 和 StereSGBM 算法的输出如下所示:

较亮的值代表较小的距离,而较暗的值代表较高的距离。

输出有点嘈杂,但可以通过更改参数和对输入图像应用过滤来增强结果。

18. Xilinx ReVision 和 xfOpenCV
Xilinx reVISION 是一个框架,集合了平台、算法和应用开发的开发资源。它拥有硬件加速机器学习和计算机视觉算法的资源。

xFOpenCV 是赛灵思对 OpenCV 库的扩展。基于关键的 OpenCV 功能,将允许您通过 SDx 或 HLx 环境在 FPGA 架构中轻松组合和加速计算机视觉功能。

它还具有对上面使用的 StereoBM 算法的硬件加速支持。

19. 创建 SDSoC 硬件平台
为了能够在 SDx 项目中使用新的硬件设计,我们首先需要创建一个 SDSoc 平台。

本教程有 3 个主要部分:

1.从 Vivado 将项目导出为 DSA 文件- 这包括:

  • 在不同的组件上设置一些 PFM 属性 - 基本上我们需要指定一个平台名称和时钟、主从 AXI 接口以及将在导出的平台中可用的中断
  • 使用全局综合选项生成 HDL 输出产品
  • 导出和验证 DSA 文件
  • 导出硬件并启动 Xilinx SDK

2.使用 Xilinx SDK 创建 FSBL 项目和链接器脚本- 这包括:

  • 创建 FSBL 项目并为其生成.bif文件
  • 创建一个空的应用程序并生成一个链接描述文件
  • 准备一个文件夹,里面有一些将在 Xiling SDx 平台中使用的文件

3.创建自定义平台 Xilinx SDx - 这包括:

  • 使用从 Vivado 导出的 DSA 文件创建平台项目
  • 在平台项目上设置.bif、链接器脚本和 BSP 设置文件
  • 生成新平台并上传到自定义存储库

在此之后,我们应该能够使用我们新创建的平台创建一个应用程序项目:

为了测试平台,我使用了阵列分区示例。如果我们构建项目,就会创建一个sd_card文件夹。我们可以通过将其内容复制到 SD 卡来运行该应用程序:

20. PetaLinux 上的 SDSoC 应用
为了能够在摄像头上进行硬件加速视频处理,我们需要 Linux 的 V4L2 框架,因此我们需要在 PetaLinux 上运行 SDSoC。

为了能够做到这一点,我们需要向我们的平台项目添加一个新的 Linux 系统配置:

在此之后,我们应该能够使用我们的自定义硬件平台创建针对 Linux 的应用程序。

构建会生成一些应复制到 SD 卡和.elf可执行文件的文件。运行它,我们应该看到如下内容:

21. 使用 SDSoC、xfOpenCV 和 reVision 进行图像处理
运行 SDSoC 示例后,我尝试了一些硬件 xfOpenCV 示例。

首先,我尝试了 Harris 角点检测示例。项目通过硬件加速功能编译成功。角落检测的软件版本工作正常,但启用硬件加速后,Ultra96 挂起。

StereoBM 算法也有 xfOpenCV 版本,所以尝试编译一个启用硬件加速的项目xf::StereoBM。稍微减少NO_OF_DISPARITIESan参数后PARALLEL_UNITS,项目编译成功。

带有硬件加速 xfOpenCV函数的SDx 项目StereoBM:

Vivado项目的项目总结:

不幸的是,当我尝试在 Ultra96 上运行硬件加速示例时,应用程序挂起。可能问题与可编程逻辑/视频管道的不稳定问题有关。

成功!

如果您对此项目有任何想法、意见或问题,请在下方留言。

原文链接丨以上内容来源网络,如涉及侵权可联系删除。

加入微信技术交流群

技术交流,职业进阶

关注与非网服务号

获取电子工程师福利

加入电路城 QQ 交流群

与技术大牛交朋友

讨论