ROBOMASTER TT巡线.5(汇总)
预备知识
预备知识分为两块,分别是:软件+硬件。相应的知识体系在下面的思维导图中有所体现。
最重要的是软件的搭建:
Python环境搭建
Robomaster SDK安装
相关Python库的安装
注意事项
思维导图
用浏览器搜索Python
https://www.python.org/
点击如图所示位置
因为SDK不支持3.9以上的Python,注意下载3.8或者更老的版本
将里面的红箭头的位置打对勾
因为Win10的使用频率比较高,所以这里以win10做演示。在小放大镜处搜索cmd
接着输入python,看第二个位置的打印,是否为Python 3.8.x的字样
接着Ctrl+Z可以推出Python环境
接着输入 pip list打印出已经安装的库
如果出现黄色路径错误,这样解决:
找到win菜单里面的Python,右键
继续右键
最终找到这个目录
将脚本这个目录加到变量
先复制这个路径
搜索
安装图中流程加
安装成功
首次先看是不是pip在环境变量里面
我们还要简单的换源
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
在用户文件夹下面建立pip文件夹,里面文件是pip.ini的配置文件
写入上面的URL
点击右键在这个目录下打开这个cmd
http://pypi.doubanio.com/simple/robomaster/
在这个网站内找到自己的版本,3.8就找cp38
这些就是SDK的库
http://pypi.doubanio.com/simple/robomaster/
观察到都是这样的写法,我看了看自己的版本
pip install whl文件
记得用Tab键
pip install --upgrade robomaster
接着用这个命令来更新我们的SDK
如果出现爆红色,就是你的C++库没有装
SDK的一些文件依靠于C++的运行库
https://visualstudio.microsoft.com/zh-hans/downloads/
这里的话我是没有安装推荐的编译工具,我自己也要写C++
就安装VS了
安装前合影留念,C++的东西很大很大
继续
选择这些
安装成功
如果没有什么错误的话,你现在已经是搭建好了Python的环境,已以及正确的安装了SDK.
接下来是安装Python的库,这里因为涉及图像处理,所以我们装opencv的Python版本。
pip install opencv-python
pip install numpy
我的机器装的是最新的版本,pip安装都会安装最新的包
我们虽然现在就搭建好了环境,但是还缺一个编辑器来写代码,虽然自带的记事本也可以去写,但是它的功能有限。而且Python语言对缩进很敏感,所以我们需要找一个编辑器。我这里推荐VSCode。
https://code.visualstudio.com/
下载安装
Ctrl+Shift+N,建立一个文件夹。为了不污染环境
在文件夹上面点右键用code打开
这样情况是安装成功
我们导入如图所示的三个库
SDK库,完成对无人机的连接,初始化,以及赋予使用者控制飞行器上面各种硬件的能力
numpy库,Python中流行的矩阵运算库。图像可以看成一种二维矩阵,那一定就可以被运算,完成各种处理与效果。
time库可以直观的测量一些代码的运行时间,为了对代码进行优化,所以需要寻找来耗时最长的操作。
到目前为止,我们的环境搭建就告一段落。
接下来是硬件的搭建,这个就注意几点:
飞机可以在两种状态下飞机,一种是热点模式,就是自己建立一个热点,控制它的主机直接与之相连。支持较老的网卡直接相连(2.4G频段)。但是需要注意一点的是这样直接相连的情况下,主机不可以联网。在写代码上面也有一些便利,不用指定IP,SDK可以很智能的处理这个连接问题。
一种是station模式,就是TT会接入到一台路由器里面,看下面的示意图
优点首先是不会断开主机的网络,而且支持多个TT接入。缺点是在编码时需要指定对应机器的IP地址。
需要注意的是如果使用扩展件,必须使用支持5.6G的网络设备。
如果你是使用了扩展件,连接得是RMTT这个热点
可以在属性里面获得更多得信息,比如是5G得频段
很多人的机器收不到这个RMTT得名字,那就是你得网卡的毛病,建议更换支持5G频段的网卡或者路由器,另外注意IP地址,这个地址是我们控制的关键
软件与硬件的环境已经搭建完成,接下来就是算法的实现:
算法是我们任务的灵魂
先说图像处理,首先我们了解一下什么是视频:视频(Video)泛指将一系列静态影像以电信号的方式加以捕捉、记录、处理、储存、传送与重现的各种技术。连续的图像变化每秒超过24帧(frame)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面;看上去是平滑连续的视觉效果,这样连续的画面叫做视频。
也就是说,其实视频是一帧一帧的画面,当按照一定的要求(主要是满足1s超24f)就在感官上动了起来。
所以对于我们的巡线来讲,获取地表的数据,从图中解算出相应的线信息,靠这个信息来指导无人机飞行、
所以我们的处理也是一帧一帧的来处理,不停的更新地表信息,下图是处理流程。
这个线性的流程图,是我们对每一帧都要这样处理.
这里注意处理的写法,我当时写的话写错了循环。导致图像处理严重迟钝,达到5s以上的延时,对于一个实时度这么高的项目是不可忍受的。
错误代码,看出来毛病了吗?
每一次循环都是要执行灰度,二值化,计数,中心标定等。而且是按照24f/1
来循环,所以根本处理不过来。这里注意!
要改成,最后的循环只是在处理二值化的图像
下面我会逐条来解释这些操作的含义以及具体在代码中的实现
灰度化,在RGB模型中,如果R=G=B时,则彩色表示一种灰度颜色,其中R=G=B的值叫灰度值,因此,灰度图像每个像素只需一个字节存放灰度值(又称强度值、亮度值),灰度范围为0-255。
可以得到BGR空间到灰度空间的转换公式:
注意这里的2.2次方和2.2次方根,RGB颜色值不能简单直接相加,而是必须用2.2次方换算成物理光功率。因为RGB值与功率并非简单的线性关系,而是幂函数关系,这个函数的指数称为Gamma值,一般为2.2,而这个换算过程,称为Gamma校正。
如果觉得太学术:
灰度化后的R = 处理前的R * 0.3+ 处理前的G * 0.59 +处理前的B * 0.11
灰度化后的G = 处理前的R * 0.3+ 处理前的G * 0.59 +处理前的B * 0.11
灰度化后的B = 处理前的R * 0.3+ 处理前的G * 0.59 +处理前的B * 0.11
这个是比较容易接受的一种算法,下面还有一种:
灰度化后的R=(处理前的R + 处理前的G +处理前的B)/ 3
灰度化后的G=(处理前的R + 处理前的G +处理前的B)/ 3
灰度化后的B=(处理前的R + 处理前的G +处理前的B)/ 3
个人推荐用第一个算法,第二个处理出来的图像有些模糊。
对于更深的理解你需要知道什么是像素点,以及什么是灰度化:
像素点是最小的图像单元,一张图片由好多的像素点构成。就像我们的下视摄像头是320x240的。也就是宽度是320像素,高度是240像素。也就是说这张图片是由一个320 * 240的像素点矩阵构成的(可以把矩阵理解为C语言中的二维数组),这个矩阵是320行,240列,像素是图像的最小单元,这张图片的宽度是320个像素点的长度,高度是240个像素点的长度,共有320 * 240 = 76800个像素点,7.68w。因为一个像素点的颜色是由RGB三个值来表现的,所以一个像素点矩阵对应三个颜色向量矩阵,分别是R矩阵,G矩阵,B矩阵。在理解了一张图片是由一个像素点矩阵构成之后,我们就知道我们对图像的处理就是对这个像素点矩阵的操作,想要改变某个像素点的颜色,我们只要在这个像素点矩阵中找到这个像素点的位置,比如第x行,第y列,所以这个像素点在这个像素点矩阵中的位置就可以表示成(x,y),因为一个像素点的颜色由红、绿、蓝三个颜色变量表示,所以我们通过给这三个变量赋值,来改变这个像素点的颜色,比如改成红色(255,0,0),可以表示为(x,y,(R=255,G=0,B=0))。那么什么叫图片的灰度化呢?其实很简单,就是让像素点矩阵中的每一个像素点都满足下面的关系:R=G=B(就是红色变量的值,绿色变量的值,和蓝色变量的值,这三个值相等,“=”的意思不是程序语言中的赋值,是数学中的相等),此时的这个值叫做灰度值。
在我们程序里面只要一句就完成了
接下来是二值化处理,是在整个计算过程中算性能瓶颈的地方
定义:图像的二值化,就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的只有黑和白的视觉效果。
灰度值0:黑,
灰度值255:白.
一幅图像包括目标物体、背景还有噪声,要想从多值的数字图像中直接提取出目标物体,常用的方法就是设定一个阈值T,用T将图像的数据分成两部分:大于T的像素群和小于T的像素群。这是研究灰度变换的最特殊的方法,称为图像的二值化(Binarization)。作用是去掉噪,例如过滤很小或很大像素值的图像点。这里也就是为什么他要保持赛道的颜色有较大的区分度的原因
我们这里用大津算法对图像进行二值化处理,这样处理的图像二值化使图像中数据量大为减少,从而能凸显出目标的轮廓其次将图像上的像素点的灰度值设置为0或255,这样将使整个图像呈现出明显的黑白效果。
这里有4个参数,图片的源文件,图片的起始阈值,图片的最大值划分的时候使用什么算子。
(x,y)是对应的像素点
这里是选择算子一。小于规定的像素值全为0,就是白色。大于的话就是填充我们上面的值,(0~255)
其实还有很重要的cv2.THRESH_OTSU 作为图像自适应二值化的一个很优的算法Otsu大津算法的参数:
cv2.threshold(img, 0, 255, cv2.THRESH_OTSU )
这些是各种算子处理过的图像,可以对比的看
接下来是图像的腐蚀操作:
这个函数的第一个参数表示内核的形状,有三种形状可以选择。
矩形:MORPH_RECT;
交叉形:MORPH_CROSS;
椭圆形:MORPH_ELLIPSE;
对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心点。
element形状唯一依赖锚点位置,其他情况下,锚点只是影响了形态学运算结果的偏移。
腐蚀操作是对图像进行形态学处理
原理是卷积核沿着图像滑动,如果与卷积核对应的原图像的所有像素值都是1,那么中心元素就保持。根据卷积核的大小靠近前景的所有像素都会被腐蚀掉(变为0),所以前景物体会变小。 这对于去除白噪声很有用,也可以用来断开两个连在一块的物体等。
函数的第一个参数是待处理的图像,第二个是要使用的内核,默认是3x3的矩阵。就是现在指定None.
iterations指的是腐蚀次数,省略是默认为1.
我们现在为止已经获得处理好的二值化图像了,接着就是提取线的特征了。
这里我们插一个小细节,就是图像的方向不对
怎么处理?
可能最牛逼的做法就是,飞机横着飞了,相对的图像就正了。
emmmm,那既然都有这个相对的想法了,我们为什么不调整图像。
我们现在在电脑上面看到的图像是这样的
上面这个算法就是,按照行来稀疏的提取一些像素行
我怕说不明白就画了一张图,emmm触控笔老是飘
首先我们认为飞机:
现在这个摆放的位置是正向
你会觉得飞机拍摄的图像是这样
但它是这样
在这里插一句关于图形的一个坐标,我们规定左上角为坐标的原点
img[x,y],分别是像素点的行与列
那我们直接相对的把行列顺序也就是图像旋转90°来取样
由于运算量的关系,这里只取样5列。后期为了精度,可以取更多的列
这个是最简单的一个示意图
接着看这个,我们可以认为我们画的5个线里面有两条是落到轨迹里面的,那
我们这里用的算法是边缘检测算法
找到目标像素点的个数
记录对应目标像素点的索引(位置)
接着去把中心白线数值输出,接着与标准中心做差
得到的误差作为指导TT控制飞行的变量
def get_line_pos(img):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, img_binary = cv2.threshold(img_gray, 200, 255, cv2.THRESH_BINARY)
img_binary = cv2.erode(img_binary, None, iterations=1)
color = []
color.append(img_binary[:,60])
color.append(img_binary[:,100])
color.append(img_binary[:,160])
color.append(img_binary[:,220])
color.append(img_binary[:,260])
result = []
for i in range(0, 5):
white_sum = np.sum(color[i] == 255)
white_index = np.where(color[i] == 255)
if white_sum > 6:
white_center = (white_index[0][white_sum - 1] + white_index[0][0]) / 2
result.append([1, white_center - 120])
else:
result.append([0, 0])
return result, img_binary
我们现在来总结一下,目前为止的工作:
从下视摄像头获取到的视频流,作为单帧照片去处理
对每帧照片都进行灰度转换
进行二值化处理
对照片取5列像素
计算目前照片的轨迹与标准中线的误差值
我们现在到了控制算法的设计阶段:
我们要用到简单的PID控制:完成对TT的控制,但是我们还需要一些关于飞行器飞行时的姿态描述.
你可以按照这个图形来感觉一下这个相关的方位
Z轴正方向为前进方向
pitch():俯仰,将物体绕X轴旋转(localRotationX)向下的话,会有一个前进的分力,然飞机前进。
yaw():航向,将物体绕Y轴旋转(localRotationY)
就是机头的方向,我们可以认为指向哪里就往哪里飞。
roll():横滚,将物体绕Z轴旋转(localRotationZ),这个是完成侧移动作,就是有点平移飞行的感觉。
也可以这样理解:
如果有一个人站在(0,0,0)点,面向X轴正向,头顶向上方向为Y轴正向,右手方向为Z轴正向,那么旋转角度和方向的计算方法如下:
Yaw是围绕Y轴旋转,站在(0,0,0)点的人脚下是XOZ平面,以正角度为参数是向左转,以负角度为参数是向右转。
Pitch是围绕X轴旋转,站在(0,0,0)点的人脚下是XOY平面,以正角度为参数是向右倒,以负角度为参数是向左倒。
Roll是围绕Z轴旋转,站在(0,0,0)点的人脚下是YOZ平面,以正角度为参数是向后倒,以负角度为参数是向前倒。
首先是对机头方向的调整,就是飞行的方向
如果误差为0,就不要去发送让无人机运动的指令
误差不为零就提取里面的误差数×一个系数(负数),因为已经很偏了要抗拒这种改变,所以是负数
横滚也是一样的控制法,注意系数可以调节。
这种实时的控制方式是:前馈控制系统,其又为前馈控制的一种形式,是控制部分发出指令使受控部分进行某种活动,同时又通过另一快捷途径向受控部分发出前馈信号,受控部分在接受控制部分的指令进行活动时,又及时地受到前馈信号的调控,因此活动可以更加准确。
例如:要求将手伸至某一目标物,脑发出神经冲动指令一定的肌群收缩,同时又通过前馈机制,使这些肌肉的收缩活动能适时地受到一定的制约,因而手不会达不到目标物,也不致伸得过远,整个动作能完成的很准确。在这种调控过程中,前馈控制和反馈控制又是常常互相配合的。例如在脑指挥肌肉活动的过程中,肌肉和关节中的感受器将肌肉活动的信息反馈到脑,因此,脑可以对肌肉实际活动的情况与原先设计的动作要求之间的偏差进行分析,再对前馈信号进行调整,在以后再指令作同样的动作时,发出的前馈信号就更加准确,使完成动作能更接近设计的要求。
与前馈控制相比,反馈控制需要较长的时间,因为控制部分要在接到受控部分活动的反馈信号后才能发出纠正受控部分活动的指令,因此受控部分的活动可能发生较大波动。以神经系统对骨骼肌任意活动的控制为例,如果只有反馈控制而没有前馈控制,则肌肉活动时可出现震颤,动作不能快速、准确、协调地完成。
这个是判断飞机要不要降落,这个地方的逻辑写的不是很好
如果飞机速度快很容易在拐角出降落,后期代码优化
这里需要对控制的动作进行一些限制,防止过载现象
限制函数的写法
最后将实时的运动指令发给飞行器
第一个函数是从主机发送命令给TT
只是第二个函数的使用参数表
发送函数的使用就是这样,直接发送命令字符串
注意中间的延时,是用来让机器进入稳定状态的。因为机器会有一个初始化的过程。但是这个过程中,机器会有小距离漂移现象,目前还在优化。
在SDK的文档内也说明了延时的必要性
import robomaster
from robomaster import robot, flight
from numpy import *
ROLL_PARAM_P = -0.3
YAW_FEEDBACK = -0.7
FORWARD_SPEED = 15
THROTTLE = 0
robomaster.config.LOCAL_IP_STR = "192.168.10.2"
tl_drone = robot.Drone()
tl_drone.initialize()
tl_flight = tl_drone.flight
tl_camera = tl_drone.camera
def send_ctrl_cmd(cmd):
tl_drone.action_dispatcher.send_action(flight.FlightAction(cmd))
def send_rc_cmd(a, b, c, d):
tl_flight.rc(a=a, b=b, c=c, d=d)
def out_limit(val, min, max):
if val > max:
val = max
elif val < min:
val = min
return val
def get_line_pos(img):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, img_binary = cv2.threshold(img_gray, 200, 255, cv2.THRESH_BINARY)
img_binary = cv2.erode(img_binary, None, iterations=1)
color = []
color.append(img_binary[:, 60])
color.append(img_binary[:, 100])
color.append(img_binary[:, 160])
color.append(img_binary[:, 220])
color.append(img_binary[:, 260])
result = []
for i in range(0, 5):
white_sum = np.sum(color[i] == 255)
white_index = np.where(color[i] == 255)
if white_sum > 6:
white_center = (
white_index[0][white_sum - 1] + white_index[0][0]) / 2
result.append([1, white_center - 120])
else:
result.append([0, 0])
return result, img_binary
if __name__ == '__main__':
send_ctrl_cmd('takeoff')
time.sleep(5)
tl_camera.start_video_stream(display=False)
send_ctrl_cmd('downvision 1')
time.sleep(3)
while True:
img = tl_camera.read_cv2_image()
t = time.time()
ret, imgbinary = get_line_pos(img)
cv2.imshow("Drone", img)
cv2.imshow("BIN", imgbinary)
cv2.waitKey(1)
if ret[0][0] == 0:
yaw_out = 0
else:
yaw_out = YAW_FEEDBACK * ret[0][1]
roll_out = ROLL_PARAM_P * ret[2][1]
if ret[0][0] == 0 and ret[2][0] == 0:
send_rc_cmd(0, 0, 0, 0)
send_ctrl_cmd('land')
break
roll_out = out_limit(roll_out, -20, 20)
yaw_out = out_limit(yaw_out, -40, 40)
send_rc_cmd(int(roll_out), int(FORWARD_SPEED),
int(THROTTLE), int(yaw_out))
print('%f, %d, %d' % ((time.time() - t)*1000, roll_out, yaw_out))
time.sleep(0.01)
cv2.destroyAllWindows()
tl_camera.stop_video_stream()
tl_drone.close()
开发的注意事项有:
Python的版本一定不能高于3.8
安装SDK的时候一定要安装VC++的库,使用默认的位置安装
图像处理的使用注意循环的写法,一定是最后将二值化的图像传给图像处理函数
在调试阶段,建议飞机为Statio模式,这样电脑可以一边上网一边调试
在station模式下,记得在代码中指定TT的IP地址
在实地飞行的时候一定要保证地面不反光,且拥有丰富的纹理
保证循迹线与周围的地表具有强烈的颜色反差,最好是处于色轮相对位置的颜色
在首次发送起飞和下视命令之前,不要立刻执行及时控制类指令。让飞机进入稳定的自稳和视频流传输正常的状态