如何开发智能小车的应用功能

工具篇

工欲善其事必先利其器,在正式开发智能小车的应用功能之前,大家需要准备好自己手头上的工具。

当然按照咱们的惯例,同样需要和大家先铺垫一些内容。

  • 镜像版本,大家在使用电脑时一般习惯了看到图形化界面,但是一般而言在机器人开发中为了减少CPU桌面显示损耗让开发板有更好的表现会使用到无图形方式。这就引入了镜像的两个版本,serve版本和desktop版本。

server版推出,就是为了构建7*24小时运行的服务器,故而抛弃了影响性能的界面等,而专注于底层性能。简单来说,server版追求性能,desktop版追求易用。

  • 连接方式,和开发板的连接方式一般可以分为三种,需要有对应的硬件支持。有线连接、串口连接、无线连接。

    • 有线连接一般指通过网口、网线进行PC端和开发板连接,同时需要将PC端静态IP地址与开发板静态IP地址前三位字段调整成一致;

    • 无线连接一般指通过WIFI进行连接,需要板载具备WIFI模块,也分为多个频段。目前主流的开发板一般都会搭载2.4GHZ及以上的频段以供开发者使用。

    • 串口连接,即RX-TX、TX-RX、GND-GND,一般用于调试,可以理解成只有一个终端的调试工具。

  • 中间件:谈到中间件大家可能不一定那么明白是什么,但是在软件架构中可以理ROS/ROS2/Apollo都是一种中间件。

  • 前处理和后处理:

    • 前处理:因为模型推理的输入是Tensor(多维矩阵)数据,但是正常AI应用的输入都是图片,视频,文字等数据,所以前处理就是要将业务的输入数据(图像,视频,文字等)预先处理成模型推理可以接收的数据即Tensor(多维矩阵)。

    • 后处理:将模型推理后的Tensor数据转换成业务可以识别的特征数据

先简短的和大家谈到以上的一些概念,后续有其他需要补充的概念再和大家交流。

终端软件

目前常用终端工具有PuttyMobaXterm等,用户可根据自身使用习惯来选择。不同工具的端口配置流程基本类似,下面以MobaXterm为例,介绍新建SSH连接过程:

  1. 打开MobaXterm工具,点击Session,然后选择SSH

  2. 输入开发板IP地址,例如192.168.1.10

  3. 选中specify username,输入root

  4. 点击OK后,输入用户名(root)、密码(sunrise)即可完成登录

又例如下图中的串口连接。

代码编辑软件

Vscode为例,其支持本地开发代码应用,同时可以通过ssh-remote远程连接开发板。

视觉巡线

机器人巡线是智能机器人比较常见的一个功能,实现的方式也有多种,一般会根据传感器的种类有一些差别。接下来以视觉巡线为例,以OriginCar机器人为载体给大家做介绍。

opencv巡线

OpenCV主要使用C/C++语言编写,执行效率较高,致力于真实世界的实时应用。OpenCV实现了图像处理和计算机视觉方面很多通用算法,这样我们在开发视觉应用的时候,就不需要重新去造轮子,而是基于这些基础库,专注自己应用的优化,同时大家的基础平台一致,在知识传播的时候也更加方便,只要你看得懂OpenCV的函数,就可以很快熟悉别人用OpenCV写的代码,大家交流起来非常方便。

和机器人操作系统一样,一款可以快速传播的开源软件,一般都会选择相对开放的许可证,OpenCV主要采用BSD许可证,我们基于OpenCV写的代码,可以对原生库做修改,不用开源,还可以商业化应用。

OpenCV目前支持的编程语言也非常多,无论你熟悉哪一种,都可以调用OpenCV快速开始视觉开发,比如C ++,PythonJavaMATLAB等语言,而且还支持Windows,Linux,Android和Mac OS等操作系统。

接下来以视觉巡线为例给大家做演示

import rclpy, cv2, cv_bridge, numpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from geometry_msgs.msg import Twist

class Follower(Node):
    def __init__(self):
        super().__init__('line_follower')
        self.get_logger().info("Start line follower.")

        self.bridge = cv_bridge.CvBridge()

        self.image_sub = self.create_subscription(Image, '/image_raw', self.image_callback, 10)
        self.cmd_vel_pub = self.create_publisher(Twist, 'cmd_vel', 10)
        self.pub = self.create_publisher(Image, '/camera/process_image', 10)

        self.twist = Twist()

    def image_callback(self, msg):
        image = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
        hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
        lower_yellow = numpy.array([ 10,  70, 30])
        upper_yellow = numpy.array([255, 255, 250])
        mask = cv2.inRange(hsv, lower_yellow, upper_yellow)

        h, w, d = image.shape
        search_top = int(h/2)
        search_bot = int(h/2 + 20)
        mask[0:search_top, 0:w] = 0
        mask[search_bot:h, 0:w] = 0
        M = cv2.moments(mask)

        if M['m00'] > 0:
            cx = int(M['m10']/M['m00'])
            cy = int(M['m01']/M['m00'])
            cv2.circle(image, (cx, cy), 20, (0,0,255), -1)

            # 基于检测的目标中心点,计算机器人的控制参数
            err = cx - w/2
            self.twist.linear.x = 0.1
            self.twist.angular.z = -float(err) / 400
            self.cmd_vel_pub.publish(self.twist)
            
        self.pub.publish(self.bridge.cv2_to_imgmsg(image, 'bgr8'))

def main(args=None):
    rclpy.init(args=args)    
    follower = Follower()
    rclpy.spin(follower)
    follower.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

深度学习巡线

使用Opencv视觉巡线,我们已经可以让小车跟随黑色的路径线运动,实现了最基础的视觉巡线任务,不过大家可能已经发现,基于OpenCV的图像识别受光线影响较大,更换场地后,就需要重新调整阈值,有没有可能让机器人自主适应环境的变化呢?也就是让机器人自己来学习。

具体流程请参考:http://originbot.org/application/deeplearning_line_follower/

目标检测

目标检测是计算机视觉领域的核心问题之一,其任务是给定一张图片或者是一个视频帧,让计算机定位出这个目标的位置并且知道目标物是什么,即输出目标的Bounding Box(边框)以及标签。

目标检测的定义为:目标检测时图片中对可变数量的目标进行查找和分类。

需要解决的问题包括:1、目标种类与数量;2、目标尺度问题:目标稠密、目标大小等3、外在环境干扰问题:遮挡、光照。

由于各类不同物体有不同的外观,姿态,以及不同程度的遮挡,加上成像是光照等因素的干扰,目标检测一直以来是一个很有挑战性的问题。目标检测的方法有传统方法,也有深度学习的方法。传统方法需要手动设计特征,使用滑动窗口和传统分类器,准确性和实时性比较差。深度学习方法通过深度网络学习特征,Proposal或者直接回归,使用深度网络,支持端到端,准确性高和实时性比较好。

传统的目标检测方法包括:Viola-Jones(采用积分图特征+AdaBoost方法对人脸进行检测等);HOG+SVM(主要用于行人检测,通过对行人目标候选区域提取HOG特征,并结合SVM分类器来进行判定);DPM(是基于HOG特征检测的一种变种,不同的地方在于,DPM中会加入一些额外的策略,来提升检测的精度,而DPM方法是目前非深度学习方法,检测效果最好的,性能最优。在后续的深度学习方法中会用到很多DPM算法中提到的策略,例如包围框策略等)等等。

深度学习方法主要分为两类:一阶段(One Stages)的目标检测算法和两阶段(Two Stages)的目标检测算法。一阶段的目标检测算法直接对输入图像应用算法并输出类别和相应的定位,如YOLO、SSD等。二阶段的目标检测算法先产生候选区域然后进行CNN分类,如R-CNN,Fast R-CNN,Faster R-CNN,Mask R-CNN等。两阶段的目标检测算法精度较高,但速度较慢;一阶段的目标检测算法速度较快,但精度相对较低。

接下来介绍如何在RDK X3上部署YOLOV5目标检测算法:

可以使用conda虚拟环境来搭建Python虚拟环境。Conda官网。conda的安装可以参考Anaconda详细安装及使用教程。这里不做过多介绍,

然后准备好数据集的图片,并使用imageImg来进行标注,首先先安装imageImg

  pip install labelImg

启动的方式可以直接在命令台输入labelImg即可打开软件进行标注。使用方法课参考目标检测---利用labelimg制作自己的深度学习目标检测数据集

本案例训练的为路标识别模型,训练识别的两个路标如下,分别为forward_rightright

训练自己的模型

本次使用的官方的yolov5版本,为了与地平线工具链的转换方式一致,使用了V2.0版本。

git clone https://github.com/ultralytics/yolov5
cd yolov5
git checkout v2.0

创建并虚拟环境

conda create -n yolo_v5 python=3.7
conda activate yolo_v5

然后安装pytorch,但是v2.0版本使用的pytorch版本不能过高,推荐安装以下版本

pip install torch==1.9.0+cu111 torchvision==0.10.0+cu111 torchaudio==0.9.0 -f https://download.pytorch.org/whl/torch_stable.html

然后注释掉requirements.txt中相关的部分,并安装所需的包

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

接下来安装apex,apex是来自英伟达的一个很好用的深度学习加速库。由英伟达开源,完美支持PyTorch框架,用于改变数据格式来减小模型显存占用的工具。

pip install apex -i https://pypi.tuna.tsinghua.edu.cn/simple
#如果上述安装报错,则下载源码进行安装
git clone https://github.com/NVIDIA/apex.git
cd apex
git checkout f3a960f80244cf9e80558ab30f7f7e8cbf03c0a0
python setup.py install

接下来去yolov5项目的官网下载yolov5s.pt放到主目录下即可开始验证了。

python3 detect.py --source ./inference/images/ --weights yolov5s.pt --conf 0.4

在验证的过程中可能会出现以下报错,报错及更正方法如下,如果没有报错就不要修改:

#报错1:AttributeError: 'Hardswish' object has no attribute 'inplace'
根据错误信息提示的路径:你电脑的路径\yolo_v5\lib\site-packages\torch\nn\modules\activation.py。修改activation.py的473行:
def forward(self, input: Tensor) -> Tensor:
    # return F.hardswish(input, self.inplace)
    return F.hardswish(input)
#报错2:AttributeError: 'Detect' object has no attribute 'export'
注释掉yolo.py的25行:
#    self.training |= self.export
#报错3:AttributeError: 'Hardswish' object has no attribute 'inplace'
pip uninstall setuptools
conda install setuptools==58.0.4
#报错4:AttributeError: 'Upsample' object has no attribute 'recompute_scale_factor'
根据错误信息提示的路径:你电脑的路径\yolo_v5\lib\site-packages\torch\nn\modules\upsampling.py 153行的
# return F.interpolate(input, self.size, self.scale_factor, self.mode, self.align_corners,
    #                      recompute_scale_factor=self.recompute_scale_factor)
return F.interpolate(input, self.size, self.scale_factor, self.mode, self.align_corners)

验证完成后就可以在/inference/output看到检测到的图像了,接下来可以开始训练了,将数据集修改为一下目录结构并将其放到data文件夹下:

road_sign
├── images
│   ├── image1.jpg
│   ├── image2.jpg
│    ...
└── labels
    ├── image1.txt
    ├── image2.txt
     ...

然后需要准备自己的配置,首先是模型的配置,将yolov5s.yaml复制为myyolov5s.yaml,并将nc设置为标注的类别的数量,本实验2个路标故设置为2。新建mydata.yaml如下所示:

train: ./data/road_sign/images/
val: ./data/road_sign/images/
# number of classes
nc: 2
# class names
names: ['forward', 'right']

需要修改的train.py几个地方

parser.add_argument('--cfg', type=str, default='models/myyolov5s.yaml', help='model.yaml path')
parser.add_argument('--data', type=str, default='data/mydata.yaml', help='data.yaml path')
parser.add_argument('--epochs', type=int, default=300)
parser.add_argument('--batch-size', type=int, default=32, help="Total batch size for all gpus.")
parser.add_argument('--weights', type=str, default='yolov5s.pt', help='initial weights path')
parser.add_argument('--device', default='0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')

其中--cfg--data为刚才的myyolov5s.yamlmydata.yaml--epochs--batch-size根据自己的电脑配置来选,--weights为刚刚下载的yolov5s.pt路径,--device选择0表示用gpu训练。

python train.py

在训练的过程中可能会出现以下报错,报错及更正方法如下,如果没有报错就不要修改:

#报错1:RuntimeError:a view of a leaf Variable that requires grad is being used in an inplace operation
修改yolo.py的126行,添加with torch.no_grad():
    def _initialize_biases(self, cf=None):  # initialize biases into Detect(), cf is class frequency
        # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
        m = self.model[-1]  # Detect() module
        for mi, s in zip(m.m, m.stride):  #  from
            b = mi.bias.view(m.na, -1)  # conv.bias(255) to (3,85)
            with torch.no_grad():
                b[:, 4] += math.log(8 / (640 / s) ** 2)  # obj (8 objects per 640 image)
                b[:, 5:] += math.log(0.6 / (m.nc - 0.99)) if cf is None else torch.log(cf / cf.sum())  # cls
            mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)

#报错2:TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
根据错误信息提示的路径:你电脑的路径\yolo_v5\lib\site-packages\torch\_tensor.py 的第643、645行,将self.numpy()修改为self.cpu().numpy()如下
if dtype is None:
            # return self.numpy()
            return self.cpu().numpy()
        else:
            # return self.numpy().astype(dtype, copy=False)
            return self.cpu().numpy().astype(dtype, copy=False)

训练完成后,会在\run文件夹下有exp文件夹,里面用来放训练的结果,我们需要用到的即为runs\exp0\weights\best.pt。接下来进行模型转换,首先先将原始的pt模型转onnx模型,先安装onnx。

pip install onnx==1.6 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install coremltools

由于地平线的工具链支持的版本为onnx6 和7,opset版本为10和11,所以要进行对应版本调整,同时由于输出限定为1\*X\*X\*X,需要对维度同步调整,需要做出如下修改:

#改动1:修改版本导出版本:export.py第48行
#torch.onnx.export(model, img, f, verbose=False, opset_version=12, input_names=['images'],
#                  output_names=['classes', 'boxes'] if y is None else ['output'])
torch.onnx.export(model, img, f, verbose=False, opset_version=11, input_names=['images'],
                  output_names=['classes', 'boxes'] if y is None else ['output'])

#改动2:修改输出节点维度信息:yolo.py文件第29行
#x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
x[i]=x[i].permute(0, 2, 3, 1).contiguous()

接下来即可执行模型转换操作

#windows用set(上一条命令),linux用export(下一条命令)
(windows: )set PYTHONPATH=%cd%
(linux: )export PYTHONPATH="$PWD"
python models/export.py --weights runs/exp0/weights/best.pt --img-size 672 --batch-size 1

其中--weights后面为刚刚训练得到的模型的路径,--img-size为672是为了和工具链的图像的分辨率做一个对应,--batch-size需要选择为1,因为目前RDK X3只能为1。此时在\runs\exp0\weights文件夹下会产生best.onnx模型。

模型的量化与部署

接下来就可以进行模型的量化了。

地平线资源中心可以下载芯片算法工具链,下载版本为v2.2.3.a的docker(14G)和算法工具链(7G),放在虚拟机中。使用docker img load将docker环境导入到虚拟机中。

su #需要root用户权限
cd horizon_xj3_open_explorer_v2.2.3a
. run_docker.sh /data
cd ddk/samples/ai_toolchain/horizon_model_convert_sample/04_detection/03_yolov5s/mapper/

然后将01_check.sh中的模型(刚刚训练的模型)路径变成自己的路径即可

onnx_model="../../../01_common/model_zoo/mapper/detection/yolov5_onnx_optimized/YOLOv5s.onnx"

校验模型

sh 01_check.sh

在RDK X3上主要做算力的是bpu,校验模型右边大多数是bpu时,在板端部署后效率会比较高。并且务必记录模型参数.

数据预处理

给模型做一个标定,转化为板端能识别到的一个模型,准备100张之前训练时的图片,修改src_dir的路径为此文件夹的路径,开始进行数据预处理。

sh 02_preprocess.sh
量化模型

运行前需要将onnx_model的路径换成自己模型的路径

sh 03_build.sh

此时会在model_output中产生一个bin文件

量化模型效果查看

接下来可以验证模型的效果,修改infer_image为需要预测的图像,同时也要将quanti_model_fileoriginal_model_file修改为刚刚model_output生成的onnx文件

另外需要更改postprocess.py内容:

#修改一,修改get_yolov5s_config()函数的数量和类别:
   # yolov5s_config.NUM_CLASSES = 80
    yolov5s_config.NUM_CLASSES = 2
    # yolov5s_config.CLASSES = MSCOCODetMetric.class_names
    yolov5s_config.CLASSES = ["forward_right","right"]

#修改二,修改postprocess()函数的模型配置,此模型的参数reshape([1, 21, 21, 3, 7])需要修改为第一步记录的数值,比如第一步的output最后一个是21,这里reshape的最后两个就改为3,7。同理18就改成3,6。
    # model_output[0] = model_output[0].reshape([1, 84, 84, 3,
    #                                            85]).transpose([0, 3, 1, 2, 4])
    # model_output[1] = model_output[1].reshape([1, 42, 42, 3,
    #                                            85]).transpose([0, 3, 1, 2, 4])
    # model_output[2] = model_output[2].reshape([1, 21, 21, 3,
    #                                            85]).transpose([0, 3, 1, 2, 4])
    model_output[0] = model_output[0].reshape([1, 84, 84, 3,
                                             7]).transpose([0, 3, 1, 2, 4])
    model_output[1] = model_output[1].reshape([1, 42, 42, 3,
                                             7]).transpose([0, 3, 1, 2, 4])
    model_output[2] = model_output[2].reshape([1, 21, 21, 3,
                                             7]).transpose([0, 3, 1, 2, 4])

接下来可以验证模型效果了,运行如下指令会在当前目录下得到一个demo.jpg,能看到经过自己的模型标注后的效果。

sh 04_inference.sh

此时,模型的转化结束,接下来就是将得到的model_output中生成的bin文件部署到板端了。使用scp命令将bin文件传输给板端。

接下来可以将模型在板端使用了。

根据自己的模型配置json文件和list文件放在config目录下,本路标实验配置的road_sign_detection.jsonroad_sign.list如下所示:

{
    "model_file": "/userdata/dev_ws/config/yolov5s_roadsign.bin",
    "model_name": "yolov5s_roadsign",
    "dnn_Parser": "yolov5",
    "model_output_count": 3,
    "class_num": 2,
    "cls_names_list": "/userdata/dev_ws/config/road_sign.list",
    "strides": [8, 16, 32],
    "anchors_table": [[[10, 13], [16, 30], [33, 23]], [[30, 61], [62, 45], [59, 119]], [[116, 90], [156, 198], [373, 326]]],
    "score_threshold": 0.7,
    "nms_threshold": 0.45,
    "nms_top_k": 5000
}
forward_right
right

接下来就可以使用自己训练的模型查看效果了,如果是自己训练的模型可以在vision功能包下的road_sign_detection_launch.py修改{"config_file": "/userdata/dev_ws/config/road_sign_detection.json"},将这个改为自己的模型的配置文件。