智能小车的基础调试与控制
前言
此项目是基于视觉识别技术(opencv)智能学习库以及(yolo v3)实时对象检测算法的智能小车
Getting Started
训练你的模型(数据集)
要使小车有视觉识别功能,首先要为其训练一个数据集(材料已经在培训资料中提供,这里不过多赘述)
一般情况下载训练完成后会生成三个文件:
xxx.names (名称文件)
xxx.cfg (训练参数文件)
xxx.weights (训练权重文件)
值得注意的是,在以上三种文件中。后缀名称为.weight的权重文件为不可读文件,训练参数和权重文件均可读
本文目标
我们的任务是让小车识别到标志“停”时停顿三秒后继续前进,识别到左转标志后180度转弯向前行驶到起始点,具体的工作流程如下图所示:
小车向前移动→遇到停止路牌→停顿三秒→继续向前移动→遇到左转路牌→掉头→移动到出发点→停止
在开始之前,我们先来明确几条概念
在三维世界中围绕物体中心做圆周运动
线速度(linear)
在三维世界中围绕轴做匀速直线运动
边界框ID(id)
ID的在模型标注中的主要作用是标注被识别的物体,让标注的名称可以和标注框对应。
在训练模型时,id会存在于后缀名为.name的文件中。该文件可读,但不能直接置于功能包下。在使用你的.names文件时,你应该将其中的所有内容复制到对应的.yaml文件中(目的是将数据序列化,便于程序的读取。这样程序可以直接列出被识别对象的表单)
通过id,他可以知道小车可以识别那些物体。配合python脚本可以告诉小车在识别出这些物体的下一步动作是什么。
注意!在复制的时候请不要对内容进行任何修改,.yaml中的数据列表顺序应与.name中的顺序完全一致,否则可能出现一些奇怪的问题。比如标志物无法正确识别导致小车不能按照预设的逻辑进行下一步动作
让我们把小车看作一个立方体

如果你想要使小车在原地旋转,你应该设置z轴角速度的参数
cmd_msg.angular.z = (设置你的Z的角速度参数)
可以理解为围绕它自身的Z轴旋转
值得注意的是:当速度参数为负值时,小车会按照顺时针方向旋转。当速度参数为正值时,小车会按照逆时针方向旋转。
同时速度设置的最大值不应超过“1”即100%,最小值不应低于“0.1”即10%。超过或低于这两个值可能导致驱动故障(比如小车直接发癫窜出去或者无法启动卡在原地)

如果你想让小车向前行驶,您应该为x轴线速度设置一个参数
cmd_msg.linear.x = (设置X的线速度)
不难看出,这段代码只是换了个轴向,即可实现小车沿X轴移动,可以具象为

如果你想保持某一特定程序执行/停顿你可以更改sleep参数
time.sleep(你想要停顿/保持的时间{秒})
您已经明确了基本的概念,接下来开始实际操作
材料准备
大赛培训资料包中的selfdrive权重相关文件 (路径如图所示)
备赛资料中的ros环境虚拟磁盘文件
智能小车一台
第一部分
修改你的python脚本
准备工作
首先按照路径找到指定的py脚本,路径如下

主文件夹>mnt>src>wheeltec_yolo_action>scripts>tiny_gesture.py
先把文件复制一份,并转存到你找得到的地方。然后使用文本编辑器打开文件
如果你的默认启用的文本编辑器为ubuntu默认文本编辑器的话,请使用更易于使用的subline text文本编辑器打开这个脚本文件。更改打开方式的步骤如下所示:
进入之后点击查看所有应用程序

点击选择,使用sublinetext文本编辑器打开脚本文件,在打开你的脚本文件以后,即可以对其进行修改
代码识读(在本文的示例中我们以智能手势识别的脚本为范本进行识读和修改)
提醒:考虑到在虚拟机中的潜在不稳定性,你应该在完成重大修改后立即保存你的文件修改进度,避免丢失
在打开一个未经过任何修改的手势识别脚本时,你看到的源代码就如下文所示
# 嘿!我是注释,在下文中我将围绕本文目标解释代码中的关键部分
#!/usr/bin/env python
# 第一行通常为程序指明python解释器的路径,在本文件中第一行使用了linux系统下的一个名为python的系统变量来达到同样的功能。当然你也可以直接指明python解释器的绝对路径
# coding=utf-8
# 指示了代码格式为utf-8
import rospy
import threading
import time
# import在这里用于导入运行必需的模块,比如说rospy(ros的python客户端库)
from geometry_msgs.msg import Twist
from darknet_ros_msgs.msg import BoundingBoxes
# 这里指出了接受的两种不同的消息,分别是twist(旋转)和boundingboxes(被识别物体的边界框)
boundingbox_id = -1
# 用于存储当前选择的边界框ID
temp0 = 0
temp1 = 0
temp2 = 0
temp3 = 0
temp4 = 0
temp5 = 0
# temp1-5用来存储每个边界框id的连续计数
count = 2
# 用于设置一个阈值,当某个边界框被连续检测的次数超过阈值时把id设置为boundingbox_id(100-110行的内容为全局变量)
def thread_job():
rospy.spin()
# 这是一个空函数,在此位置仅调用rospy.spin()。这是ROS节点中常用的函数,用于保持节点运行并处理回调。
在本文中这个函数被放置在了一个单独的线程中,但是在这篇代码里面这个线程并没有被启动,所以可以跳过这一部分。
def side_flag_callback(msg):
global boundingbox_id
global temp0
global temp1
global temp2
global temp3
global temp4
global temp5
global count
# for boxes in msg.bounding_boxes:
# if boxes.probability > 0.8:
# boundingbox_id = boxes.id
# print(boxes.Class)
# print("--------------")
for boxes in msg.bounding_boxes:
if boxes.probability > 0.7 and boxes.id == 0:
temp0 = temp0 + 1
temp1 = 0
temp2 = 0
temp3 = 0
temp4 = 0
temp5 = 0
if temp0 > count:
boundingbox_id = boxes.id
temp0 = 0
elif boxes.probability > 0.7 and boxes.id == 1:
temp1 = temp1 + 1
temp0 = 0
temp2 = 0
temp3 = 0
temp4 = 0
temp5 = 0
if temp1 > count:
boundingbox_id = boxes.id
temp1 = 0
elif boxes.probability > 0.7 and boxes.id == 2:
temp2 = temp2 + 1
temp0 = 0
temp1 = 0
temp3 = 0
temp4 = 0
temp5 = 0
if temp2 > count:
boundingbox_id = boxes.id
temp2 = 0
elif boxes.probability > 0.7 and boxes.id == 3:
temp3 = temp3 + 1
temp0 = 0
temp1 = 0
temp2 = 0
temp4 = 0
temp5 = 0
if temp3 > count:
boundingbox_id = boxes.id
temp3 = 0
elif boxes.probability > 0.7 and boxes.id == 4:
temp4 = temp4 + 1
temp0 = 0
temp1 = 0
temp2 = 0
temp3 = 0
temp5 = 0
if temp4 > count:
boundingbox_id = boxes.id
temp4 = 0
elif boxes.probability > 0.7 and boxes.id == 5:
temp5 = temp5 + 1
temp0 = 0
temp1 = 0
temp2 = 0
temp3 = 0
temp4 = 0
if temp5 > count:
boundingbox_id = boxes.id
temp5 = 0
def control_action():
global boundingbox_id
rospy.init_node("control_drive")
add_thread = threading.Thread(target = thread_job)
add_thread.start
# 在这行注释上方的代码在本文中不起作用,因为上一行用于启动线程的add_thread.start少了个括号,导致线程未能正常启动
cmdvel_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=1)
# 从这里开始即为本文内容,这行代码中的cmdvel_pub创建了一个话题发布者,话题名称为/cmd_vel,用于控制小车的移动
side_flag_sub = rospy.Subscriber("/darknet_ros/bounding_boxes", BoundingBoxes,
# 本行用side_flag_sub创建了一个话题订阅者,订阅话题为/darknet_ros/bounding_boxes/,这个话题用于包含了darknet框架检测到的对象边界框
side_flag_callback)
car_mode =rospy.get_param("/if_akm_yes_or_no","no")
# car_mode用于从ros服务(也可能是ros远程主机或者是服务器)获取/if_akm_yes_or_no参数,该参数用于决定小车处于人工控制状态还是自动驾驶状态
cmd_msg = Twist()
# 发送一个消息,类型为twist
rate = rospy.Rate(100)
# 这里设定了一个轮询值,单位是(次/秒)本文中这里被设置为了每秒100次
print("start node!")
# 由于程序会按照顺序执行下去,这里打印一段文本证明程序正常运行。便于你的排障
while not rospy.is_shutdown():
# 这是一个循环语句,用于创造一个无限循环来保证ros节点的运行(通常用在主程序中)
#print(car_mode)
cmd_msg.angular.z = 0
# 小车的z轴角速度,这里的作用是赋予小车一个初始速度
cmd_msg.linear.x = 0.2
# 小车的x轴线速度,这里的作用是在程序启动时赋予小车一个初始速度
cmdvel_pub.publish(cmd_msg)
# 将上文代码中提到的内容以消息的形式发送出去,以确保代码的内容被正确执行
if boundingbox_id == 2:
# 当被识别对象的id为2时就执行下文代码
if car_mode == "yes":
cmd_msg.linear.x = 1
# 小车的X轴线速度
cmd_msg.angular.z = 0
# 小车的z轴角速度
cmdvel_pub.publish(cmd_msg)
# 发布两个轴速度的消息
time.sleep(5)
# 让整个程序暂停执行(阻塞ros节点)本文设定为5秒
cmd_msg.linear.x = 0.1
cmd_msg.angular.z = 0
cmdvel_pub.publish(cmd_msg)
# 参数部分基础概念梳理完毕,下文同理
else:
cmd_msg.angular.z = 0
cmd_msg.linear.x = 0
time.sleep(4)
cmdvel_pub.publish(cmd_msg)
time.sleep(3)
cmd_msg.angular.z = 0
cmd_msg.linear.x = 0.2
cmdvel_pub.publish(cmd_msg)
boundingbox_id = -1
elif boundingbox_id == 0:
if car_mode == "yes":
cmd_msg.linear.x = 0
cmd_msg.angular.z = -1
cmdvel_pub.publish(cmd_msg)
time.sleep(7)
cmd_msg.linear.x = 0
cmd_msg.angular.z = 0
cmdvel_pub.publish(cmd_msg)
else:
cmd_msg.linear.x = 0
cmd_msg.angular.z = 1
time.sleep(3)
cmdvel_pub.publish(cmd_msg)
time.sleep(3.7)
cmd_msg.angular.z = 0
cmd_msg.linear.x = 0.2
cmdvel_pub.publish(cmd_msg)
time.sleep(5)
cmd_msg.angular.z = 0
cmd_msg.linear.x = 0
cmdvel_pub.publish(cmd_msg)
boundingbox_id = -1
break
# break用于跳出上文设定的死循环,即结束程序的运行
if __name__ == '__main__':
# 此条代码确保只有在运行脚本时才会执行其中的代码
try:
control_action()
# 此处调用control_action()函数为了捕获在程序运行中抛出的异常
except rospy.ROSInterruptException:
# 此处命令用于捕获except rospy.ROSInterruptException:错误,由于此故障会在使用外部请求关闭ros节点时弹出,在下一行代码中使用pass命令可使其正常关闭。而不会弹出崩溃/故障报告
pass
在上述代码中,为了达到本文的目标。主要修改参数部分,修改参数部分的代码由以下几类排列组合而成:
cmd_msg.linear.x =(为x轴线速度赋值)
cmd_msg.linear.z =(为z轴角速度赋值)
cmdvel_pub.publish(cmd_msg) (发布消息,一般用于发布线速度和角速度)
time.sleep( ) (让程序休眠,单位为秒)
将上述的四个主要元素重新排列组合,你可以得到一个完美契合本文目标的代码
# 我是注释,这里是重新排列过后的参数部分
cmd_msg = Twist()
# 发送一个类型为Twist的消息
rate = rospy.Rate(100)
# 设定一个每秒100次的轮询频率
print("start node!")
# 当程序按顺序执行的时候,打印一段文本便于排除故障
while not rospy.is_shutdown():
# 创建一个死循环
print(car_mode)
cmd_msg.linear.x = 0.2
# 为x轴线速度赋值,给予一个初始速度,小车前进
cmd_msg.angular.z = 0
# 为z轴角速度赋值,同样给予了一个初始速度
cmdvel_pub.publish(cmd_msg)
# 将上文中赋值的内容发送到ros服务中
if boundingbox_id == 0:
# 如果检测到的边界框ID为0则执行以下部分
if car_mode == "yes":
cmd_msg.linear.x = 0
# x轴线速度赋值0(0%)
cmd_msg.angular.z = 1
# z轴角速度赋值为1(100%),此时小车开始逆时针旋转
cmdvel_pub.publish(cmd_msg)
# 把上文中赋值的两个速度参数以消息的形式发送到ros服务中
time.sleep(5)
# 休眠5秒钟,具体表现为小车原地旋转五秒钟
cmd_msg.linear.x = 0.2
# 为x轴线速度赋值,小车向前行进
cmd_msg.angular.z = 0
# 为z轴角速度赋值,小车停止旋转
cmdvel_pub.publish(cmd_msg)
# 将以上赋值内容发送到ros服务中,以便赋值内容生效
else:
# 如果car_mode不为yes,机器人会停止三秒然后休眠三秒
cmd_msg.linear.x = 0
cmd_msg.angular.z = 0
time.sleep(3)
cmdvel_pub.publish(cmd_msg)
time.sleep(3)
cmd_msg.linear.x = 0.2
cmd_msg.angular.z = 0
cmdvel_pub.publish(cmd_msg)
# 发送角速度和线速度的值
boundingbox_id = -1
# 将边界框id设置为-1,打断原本的循环(使其不再进入)
FAQ
Q: 我逻辑是对的,代码其他地方也没问题,为什么启动就报错?
A:检查符号是不是全部使用了半角符号(英文符号),或者检查你的缩进是否规范。若依然无法解决,给你的小车充电试试
Q:示例代码这么短,能把不需要的部分删去达到精简的的效果吗?
A:可以,但是建议再删之前检查一下。避免删出奇奇怪怪的问题
Q:我代码跑起来了,但是小车纹丝不动。也不报错
A:若不报错可以尝试手动定位错误地点,下面是示例:
else:
# 如果car_mode不为yes,机器人会停止三秒然后休眠三秒
cmd_msg.linear.x = 0
cmd_msg.angular.z = 0
time.sleep(3)
print("sleep1")
# 打印了sleep1在你的终端中
cmdvel_pub.publish(cmd_msg)
time.sleep(3)
print("sleep2")
# 在你的终端中打印了sleep2
cmd_msg.linear.x = 0.2
cmd_msg.angular.z = 0
cmdvel_pub.publish(cmd_msg)
# 发送角速度和线速度的值
boundingbox_id = -1
# 将边界框id设置为-1,打断原本的循环(使其不再进入)
通过终端的输出结果来判定你的程序故障点