比赛内功心法???

前言

临近比赛,恰逢大家期末周也结束了,最后二十天不到的时间正是大家最后冲刺的时间。作为赛事的竞赛相关负责人,我也很关心大家准备的情况,但是和大家私下沟通实际情况发现大家实际的备赛情况没有我预期的效果,所以推出这篇文章希望能够给大家一些帮助。


在设计这个比赛之初,我就希望这个比赛是一个能帮助大家积累一些工程能力和基础知识储备的项目,大家在开发与实践中能够发现自己需要学习什么内容以及在不断的Debug中找到最适合自己的工程开发方法。所以很直接的点是,这个比赛并不像其他比赛一样一开始给到大家太多的源代码,而是很多看不到代码的二进制文件,相信这也给大家造成了很多苦恼,但从我个人的开发经验来看,正是这样才能给到大家一些工程灵感;
所以接下来我从两个部分给大家一些上手的思路,第一部分是基础储备,第二部分是比赛策略;

基础储备

首先基于整个比赛的技术点,我个人感觉是基于两个部分,第一部分是中间件相关的内容,ROS2是一个中间件这个事情我每次开放日或者直播都在和大家做介绍,中间件意味着它会贯通着大家整个开发的环节,具体到DEBUG调试、软件开发与测试、算法结果输出等等,大家都可以通过ROS2来做这个环节;更具体而言,大家不管是使用巡线还是二维码识别亦或者是避障,在这些数据存在 _y=f(x)_ 关系时,大家难免会想到将二维码的输出结果作为巡线的启动/停止输入,等等这些思考需要大家去理解ROS2到底在做什么事情。


基于此,我依旧强烈建议大家务必先把《ROS2 21讲》这个课程看完,课程并没有很长但是浓缩也是精华。我们最终的目标是在机器人开发时我们清楚的知道什么时候需要使用到ROS2的一些概念或者当遇到问题时可以想到使用ROS2的一些基础概念去实现。
例如,当我们在看 origincar_base 这个代码时你是否能看出 cmd_vel 这个话题在做什么:

  Cmd_Vel_Sub = create_subscription<geometry_msgs::msg::Twist>(
      cmd_vel, 1, std::bind(&origincar_base::Cmd_Vel_Callback, this, _1));
void origincar_base::Akm_Cmd_Vel_Callback(const ackermann_msgs::msg::AckermannDriveStamped::SharedPtr akm_ctl)
{
    short  transition;

    Send_Data.tx[0]=FRAME_HEADER;
    Send_Data.tx[1] = 0;
    Send_Data.tx[2] = 0; 

    transition=0;
    transition = akm_ctl->drive.speed*1000;
    Send_Data.tx[4] = transition;
    Send_Data.tx[3] = transition>>8;

    transition=0;
    transition = akm_ctl->drive.steering_angle*1000/2;
    Send_Data.tx[8] = transition;
    Send_Data.tx[7] = transition>>8;

    Send_Data.tx[9]=Check_Sum(9,SEND_DATA_CHECK); 
    Send_Data.tx[10]=FRAME_TAIL;

    try {
      Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx));
    } catch (serial::IOException& e) {
        RCLCPP_ERROR(this->get_logger(),("Unable to send data through serial port"));
    }
}

这里其实在说两个事情,第一个事情是ROS2一般会讲cmd_vel作为速度发布的话题,意味着用户可以将想要机器人执行的速度控制使用cmd_vel这个话题作为发布;第二个事情是整个回调函数是在执行向下位机发布一串二进制的指令,从代码的变量名我们不难看出里面包含着发布的线速度和舵机的转向;
但是这两件事情的本质是什么呢?第一件在告诉我们ROS2被封装的很好,大家基本理解它的常见用法就可以快速上手的,像cmd_vel这样常见的话题是可以让人一眼知道在干什么的;第二件事情在告诉我们,ROS2其实没那么神奇,它的背后无非还是各种代码实现的业务逻辑。
我希望大家可以基于这个案例清楚我们学习ROS2到底在学习什么?其实是在学习一种标准,在学习一种规范的代码思路,它背后做的所有事情都是有迹可循的,那么对应到大家写的代码是不是一样有迹可循的呢?


有了cmd_vel这个话题之后,大家还记得我们提问提到的y=f(x)嘛?我们现在就来给到一个x,假设现在我们希望实现巡线案例,那么输入是传感器的输入,这个传感器可能是相机也可能是超声波等等,但是无疑对应到任何一种传感器,可以实现识别的方式是多样的,可是我们居然惊奇的发现如果想要它们实现巡线这个事情,最终都是把一个结果告诉ROS2中的cmd_vel,这里识别的结果对于cmd_vel最后的结果就是x,f(x)就是拿到这个结果后cmd_vel需要如何去解析。

此处我们以视觉识别为例,也分两种,分别为Opencv和深度学习:


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()

这里我并不关系cmd_vel是如何发布的,因为此时大家足够清楚ROS2是如何将数据进行互相传递的。我们关心的是x的输出是如何输出的,此时我们再一次惊奇的发现如何输出和ROS2竟然没有任何关系,是的,我们再一次印证了ROS2是一个中间件的事情。

那么深度学习呢?

int32_t LineCoordinateParser::Parse(
    std::shared_ptr<LineCoordinateResult> &output,
    std::vector<std::shared_ptr<InputDescription>> &input_descriptions,
    std::shared_ptr<OutputDescription> &output_description,
    std::shared_ptr<DNNTensor> &output_tensor) {
  if (!output_tensor) {
    RCLCPP_ERROR(rclcpp::get_logger("LineFollowerPerceptionNode"), "invalid out tensor");
    rclcpp::shutdown();
  }
  std::shared_ptr<LineCoordinateResult> result;
  if (!output) {
    result = std::make_shared<LineCoordinateResult>();
    output = result;
  } else {
    result = std::dynamic_pointer_cast<LineCoordinateResult>(output);
  }
  DNNTensor &tensor = *output_tensor;
  const int32_t *shape = tensor.properties.validShape.dimensionSize;
  RCLCPP_DEBUG(rclcpp::get_logger("LineFollowerPerceptionNode"),
               "PostProcess shape[1]: %d shape[2]: %d shape[3]: %d",
               shape[1],
               shape[2],
               shape[3]);
  hbSysFlushMem(&(tensor.sysMem[0]), HB_SYS_MEM_CACHE_INVALIDATE);
  float x = reinterpret_cast<float *>(tensor.sysMem[0].virAddr)[0];
  float y = reinterpret_cast<float *>(tensor.sysMem[0].virAddr)[1];

  result->x = (x * 112 + 112) *640.0 / 224.0;
  result->y = 224 - (y * 112 + 112) + 240 - 112;
  RCLCPP_INFO(rclcpp::get_logger("LineFollowerPerceptionNode"),
               "coor rawx: %f,  rawy:%f, x: %f    y:%f", x, y, result->x, result->y);
  return 0;
}

是的,一样是从模型解析中得到了我们想知道的那个具体的x。

说完这里,我再回到我们在讲一个什么事情,我们最终的目标是在机器人开发时我们清楚的知道什么时候需要使用到ROS2的一些概念,或者当遇到问题时可以想到使用ROS2的一些基础概念去实现。以巡线这个案例我们就清楚了,是否使用ROS2取决于你如何去定义它。


第二部分,大家可能会觉得是视觉算法能力,其实我想说的是并不是的。第二部分的技术点我们可以称之为感知规划,视觉部分隶属于这个部分。
当大家使用视觉进行巡线避障是亦或者是定位控制时,其实都是在使用感知规划的部分,我提到这里的原因是,我希望大家把视野放的更加宽阔一些,中间件和算法工程的上下游关系才是大家在考虑技术落地时需要思考的第一步,当我们在讨论实现路径时才会考虑到具体使用到里面的哪一块。
所以我强烈建议把配套的第二门课程《从零开发一款四轮智能小车》看完,这门课程看上去是以origincar作为硬件,但是在思考这门课程的时候并没有固定使用任何一个硬件,而是以一个更大的角度来制作的。
课程讲义:https://j7h4nezmu0.feishu.cn/docx/PD6iduyKfo49Kux41vAcTURsnrJ

比赛策略

关于整个比赛,首先还是感谢大家的积极参与,作为第一年举办的比赛能在开始就有接近300个队伍的数量超出了我们所有人的预期。回归到比赛,大家需要如何进行比赛的开发与调试呢?

首先我需要说的是,基础准备是第一步,当一个大一的新生来做这个比赛需要3-5个月的准备时间,而一个研一的同学来做可能是1个月的时间,大家可能觉得差的是是什么呢?是工程经验,是知识储备,是对待技术的热情和对问题的思考角度;这些内容才是大家真正需要在比赛时去思考学习的地方?
具体到比赛,研究赛规,举例说大家不难发现这个比赛理论上180s是完全足够比赛的,甚至整个比赛的最低成绩是150s的时间,为什么这么说?子任务一、二、三全部失败的罚时一起也才150s,也满足P点出发回到P点的条件,对于这个比赛是完全能完赛的。所以大家的最低预期就应该定在必须小于150s的比赛时间。这是关于比赛策略的思考。
研究技术,大家会发现包括本篇文章在向大家传递的内容是技术及我认为做好技术的路径是什么?网上的资料很多,如何筛选好的资料,减少试错成本是大家需要思考的内容。是一上来直接跟着走还是思考其合理性和是否是你真正想要的内容呢?

后记

和大家聊了一些内容,最后希望大家能取得一些好成绩,咱们赛场见~!