机器人编程学习指南(四)
原文:
zh.annas-archive.org/md5/6f86c02400b44f7c565792c9032673f8译者:飞龙
第十七章:第14章:使用Python进行相机线跟踪
在上一章中,我们学习了如何使用相机来追踪和跟踪物体。在本章中,我们将扩展相机代码以创建线感应行为。
我们将探讨机器人使用线跟踪的地方以及它的有用性。我们还将了解不同机器人中跟踪路径的不同方法,以及它们的权衡。你将看到如何构建一个简单的线跟踪轨道。
我们将了解一些不同的算法来使用,然后选择一个简单的算法。我们将制作一个数据流图来查看其工作原理,收集样本图像进行测试,并根据样本图像调整其性能。在这个过程中,我们将看到更多处理计算机视觉和从中提取有用数据的方法。
我们将增强我们的PID代码,将我们的线检测算法集成到机器人驾驶行为中,并看到机器人使用这种方式运行。本章结束时,我们将讨论如何进一步发展这一想法。
在本章中,我们将涵盖以下主要主题:
-
线跟踪简介
-
制作线跟踪测试轨道
-
线跟踪计算机视觉流程
-
使用测试图像尝试计算机视觉
-
使用PID算法进行线跟踪
-
再次找到线条
技术要求
对于本章,你需要以下物品:
-
机器人及其代码来自第13章,机器人视觉 – 使用Pi相机和OpenCV
-
一些白色或黑色绝缘胶带
-
一些A2纸或板子 – 与绝缘胶带相反的颜色
-
一把剪刀
-
良好的照明
本节代码可在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter14找到。
查看以下视频以查看代码的实际应用:https://bit.ly/3slLzbQ
线跟踪简介
在我们开始编写代码之前,让我们了解一下线跟踪机器人的行为,这些系统在哪里以及如何使用它们,以及不同的实现技术。
什么是线跟踪?
一些机器人需要在任务中保持特定的路径。对于机器人来说,导航线条比规划整个房间或建筑物的地图要简单。
简而言之,线跟踪是指能够自主地跟随标记路径。这些可以是视觉标记,例如蓝色胶带或黑色道路上的一条白色线条。当机器人沿着线条行驶时,它将不断寻找前方线条的位置,并纠正航向以跟随该线条。
在机器人竞赛中,沿着线条进行比赛是一个常见的挑战,准确性之后速度至关重要。
工业应用
线路跟踪行为最常见的使用场景是工业领域。被称为自动导引车(AGVs)的机器人需要因多种原因遵循既定路径。这些任务可以是仓库机器人保持在堆叠产品通道之间的轨道上,或者工厂机器人保持在远离其他工作区域的路径上。线路可能标记了存储货架和装货区或机器人充电站和机器人工作区之间的路线:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_01.jpg
图14.1 – IntellCart – Mukeshhrs的线路跟踪工业机器人[公有领域]
如图14.1所示的IntelliCart使用明亮的蓝色引导胶带,尽管在大多数工业应用中,机器人使用地板下的磁轨。
路线可能包括选择点,从特定位置发出多条线路。根据其任务,机器人可能需要额外的线索来感知它已经到达这些点。工程师可以为完全自动化的系统设置重复路径。
这些明确的标记意味着你可以设置安全边界,并清楚地了解人类和机器人是否互动;这意味着机器人很少会在人们不太了解的区域外操作。
线路跟踪类型
线路跟踪和相关系统有几个主要分支。
视觉线路跟踪是目前最常见且易于设置的线路跟踪技术。它由机器人检测到的涂画、绘制或贴上的视觉线路组成。光学线路简单,但表面污垢和光照条件可能会使其不可靠。检测方式主要分为以下几类:
-
通过光传感器检测:在这种情况下,我们将小型传感器附着在机器人底部靠近线路的位置。它们被调整以输出二进制开/关信号或模拟信号。它们通常有灯光照射到表面。这些传感器体积小且价格低廉,但需要额外的I/O。
-
通过摄像头检测:如果你已经使用摄像头,并且有I/O引脚,这将节省空间。它简化了安装和布线的复杂性。然而,这需要在软件复杂性上做出权衡,因为你的机器人需要计算机视觉算法来分析这些。
磁性线路跟踪用于需要保护线路免受外界因素影响的情况。对于这种变化的某些变体,你可以引导机器人在多条路径上。以下是一些变体:
-
在地板上运行磁条,可以让霍尔效应传感器(如第12章,使用Python进行IMU编程)检测磁条的位置。一系列这样的传感器可以确定线路的方向并跟随它。这比画线路更容易更改,但可能成为绊脚石。
-
在地板上或其下方运行带有电流的电线,可以达到相同的效果。通过多条电线和一些不同的电路,系统可以将机器人引导到不同的路径上。
-
在地板下隐藏线条消除了绊倒危险,但意味着您需要在工业机器人跟随的路径上为人类画上警告标记。
现在,您已经看到了两种主要的线跟踪类型;值得提一下其他一些在现实世界中确定机器人路径的方法:
-
信标:超声波、发光或无线电发射的信标可以放置在环境中以确定机器人的路径。这些可能是激光或其他光线的反射器。
-
视觉线索:如果您在墙壁和柱子上放置二维码或其他可见标记,它们可以编码一个精确的位置。
您已经看到了机器人如何通过可见线条和隐藏线条(如地板下的电线和磁传感器)进行线感应。因为它更容易,我们将使用可见线条。
简单的光学传感器需要额外的布线,但如果我们已经有一个能够做到这一点的相机,为什么不利用它呢?
在本章中,我们将专注于使用我们已有的相机和视觉轨道进行跟踪,并跟随那里的线条。我们将接受代码的复杂性,同时简化我们机器人的硬件方面。
现在您已经对不同的线跟踪类型及其使用场合有了些了解,让我们创建一个机器人可以跟随的测试轨道。
制作一条线跟踪测试轨道
由于您将使您的机器人跟随一条线,我们需要从一个要跟随的线条部分开始。该轨道最初将用于测试我们的线检测算法,然后当我们打开电机并开始沿着线条行驶时,可以将其扩展到更令人兴奋的轨道。在本节中,我将向您展示的是易于制作和可扩展的。它允许您尝试不同的线条形状和曲线,并观察机器人如何响应。
您甚至可以尝试不同的颜色和对比度选项。
准备测试轨道材料
以下照片显示了所需的主要材料:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_02.jpg
图14.2 – 制作测试轨道的材料
图14.2中的照片显示了一卷黑色电气胶带铺在一张大白纸上。对于本节,您需要以下材料:
-
一些A2纯白色纸张或板。
-
一些黑色电气绝缘胶带或油漆工胶带。确保这种胶带是不透明的。
-
一把剪刀。
如果它们是白色涂漆的,您可以用板子替换纸张。
您也可以通过使用黑色或深色纸张和白色胶带来更换物品。这种胶带必须是半透明的,以便与背景形成良好的强烈对比。
制作一条线
将纸张平铺。然后,用胶带在纸张中间画一条线:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_03.jpg
图14.3 – 在纸张上平滑胶带
图14.3中的照片显示了带有胶带线条的纸张和我用手指抚平胶带的情景。确保将胶带抚平。你不需要担心使其完全笔直,因为这个系统的整个目的就是在线条弯曲时也能跟随线条。
一旦你在纸上贴上几段这样的胶带,为什么不制作一些有趣的部件,比如以下图中的那些:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_04.jpg
图14.4 – 一些与直线相邻的不同形状
如图14.4所示,你可以尝试曲线和故意不完全直的线条。你可以将这些线条与直线结合起来,形成整个区域,就像一个机器人火车套装!这些线条在后续调整和玩转跟随代码时将会很有趣。
现在我们已经准备好了测试轨道,我们可以考虑如何视觉处理线条。
跟随线条的计算机视觉流程
正如我们在之前的计算机视觉任务中所做的那样,我们将将其视为一个流程。在我们这样做之前,有许多使用计算机视觉跟踪线条的方法。
摄像头线条跟踪算法
我们有兴趣选择其中最简单的一个,但就像往常一样,总有一个权衡,那就是其他算法可以处理更复杂的情况或比我们的算法更好地预测曲线。
这里是一些我们可以使用的方法:
-
使用边缘检测: 可以运行边缘检测算法,例如Canny边缘检测器,对图像进行处理,将找到的任何过渡转换为边缘。如果我们想使用这个系统,OpenCV内置了边缘检测系统。该系统可以检测从暗到亮和从亮到暗的边缘。它对不太尖锐的边缘更加宽容。
-
沿线条寻找差异: 这就像 cheeky 边缘检测,但只针对特定的一行。通过在图像中每一行的每个像素之间找到差异,任何边缘都会显示出显著差异。它比Canny算法简单且成本低;它可以处理任意方向的边缘,但需要尖锐的对比度。
-
寻找亮度和使用绝对亮度区域作为线条: 这非常简单,但过于简单,难以给出好的结果。它对反转不敏感,但不需要跟踪边缘,因此不需要尖锐的对比度。
使用前面三种方法中的一种,你可以在一个图片区域找到线条,并直接瞄准那里。这意味着你将无法预先预测路线变化。这是最简单的方法。所选区域可能是屏幕底部的单行。
或者,你可以使用前面的方法来检测摄像头图像中的线条并为其绘制轨迹。这更复杂,但更能应对更陡峭的转弯。
值得注意的是,我们可以使用Pi摄像头的原始YUV数据制作一个更高效但更复杂的算法。为了简单起见,我们将坚持使用简单的算法。随着你在复杂性和理解上的进一步交易,你可以找到更快、更准确的方法。
我们系统的另一个主要限制是摄像头的视野宽度。你可以使用镜头让摄像头捕捉更宽的视觉范围,这样机器人就不会经常丢失线条。
我们将使用的方法是沿着线条寻找差异,因为它简单且能够应对不同的线条颜色。我们还将简单地沿着单列查找,这导致数学更加直接。
管道
我们可以将数据处理方式视为一个管道。在我们这样做之前,让我们快速解释一下离散差异。每个像素的亮度是一个介于0(黑色)和255(白色)之间的数字。为了得到差异,你从每个像素减去它右侧的像素:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_05.jpg
图14.5 – 像素之间的离散差异
在图14.5中,有六组不同色调的像素:
-
第一幅图显示了两个白色像素。它们之间没有差异。
-
当灰色像素后面跟着白色像素时,会产生一个小的差异。
-
黑色像素后面跟着白色像素会产生一个大的差异。
-
白色像素后面跟着黑色像素会产生一个大的负差异。
-
黑色像素后面跟着黑色像素不会产生差异。
-
灰色像素后面跟着黑色像素会产生一个小的负差异。
应该很容易看出,对比度高的线条边缘会产生最大的差异,无论是正的还是负的。我们的代码将寻找这些差异。
下面的图示展示了我们如何处理该方法的摄像头数据:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_06.jpg
图14.6 – 寻找线条的图像处理管道
在图14.6中,我们展示了寻找跟随线条的过程。它从摄像头开始,我们从320×240像素分辨率的摄像头捕获图像。管道中的下一步是将图像转换为灰度 – 我们现在只对亮度感兴趣。
由于图像可能有噪声或颗粒,我们模糊它;这并不是严格必要的,这取决于你从哪个清晰的环境中拍照。
我们从这个图像中切出候选行;这个行不应该太高,因为那条线可能太远,或者根据摄像头的位置,地平线以上可能有随机的东西。行也不应该太低,因为它会离机器人太近,以至于它无法及时反应。在切出候选行框上方是一个示例,展示了切出的行和它来自的图像。
我们然后将这一行视为一组数字,并获取它们之间的离散差异。在离散差异框上方的图表显示,当行从浅灰色变为黑色时,出现一个大的负峰值,随后当行再次从黑色变为浅灰色时,出现一个大的正峰值。注意,图表的大部分显示为零线,因为颜色块之间没有差异。
下一步是找到最大和最小位置,具体来说是在行中的哪个位置。我们想要的是高于零的最高点的位置/索引和低于零的最低点的位置/索引。现在我们知道了我们线条的边界可能在哪里。
我们可以通过将这些边界之间的位置相加并除以2来找到线条的中心,这将是一个相对于相机图像中间的X位置。
现在,您已经看到了一些测试图像的流程。是时候获取一些自己的测试图像,并尝试使用一些代码来运行这个算法了。
尝试使用测试图像进行计算机视觉
在本节中,我们将探讨如何以及为什么使用测试图像。我们将为这种行为编写我们的第一段代码,并在机器人的相机测试图像上尝试它。这些测试将为我们使用代码来控制机器人做准备。
为什么使用测试图像?
到目前为止,我们的计算机视觉工作都是直接与机器人行为一起编写的;这是它们的目标,但有时,您可能想要单独尝试视觉处理代码。
可能您想要让它工作或找出其中的错误,或者您可能想要看看您是否可以使代码更快并对其进行计时。为此,在机器人控制系统之外运行该特定代码是有意义的。
使用测试图像也是合理的。因此,您不必运行相机并需要光照条件,您可以使用您已经捕捉到的测试图像,并将它们与您从它们期望的结果进行比较。
对于性能测试,尝试同一图像100次或同一组图像将给出一致的结果,以便性能指标有意义。避免每次都使用新数据,因为这些可能会导致意外或可能是有噪声的结果。然而,添加新的测试图像以查看会发生什么是非常有趣的。
既然我们现在知道了为什么使用它们,让我们尝试捕捉一些测试图像。
捕捉测试图像
您可能还记得,在前一章中,我们使用raspistill来捕捉图像。我们在这里也将这样做。首先,我们想要将我们的相机放置到一个新的位置,朝下,这样我们就可以俯视线条。
本节需要从第第13章的设置中获取,使用Pi相机和OpenCV进行机器人视觉,以及从第9章的代码,在Python中编程RGB条带。
将电机功率打开到Raspberry Pi上,然后在机器人上的Raspberry Pi上通过ssh会话,输入以下命令:
-
我们通过键入
python3开始Python:pi@myrobot:~ $ python3 Python 3.7.3 (default, Dec 20 2019, 18:57:59) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> -
现在,我们需要导入我们的机器人对象并创建它,以便我们可以与之交互:
>>> import robot -
让我们创建机器人对象:
>>> r = robot.Robot() -
现在,使用此方法将水平伺服机构设置为中间位置:
>>> r.set_pan(0)水平伺服机构应该使相机居中。
-
接下来,我们将倾斜伺服机构设置为向下,以便观察线条:
>>> r.set_tilt(90)伺服机构应该在这里直视下方。它不应该过度拉伸或发出咔哒声。
-
现在,您可以通过按Ctrl + D退出Python(并释放电机)。
相机面向下方。现在您可以关闭电机开关,并将这个机器人放到您的测试轨道上。尽量将机器人定位,使相机正好位于线条上方。
-
在
ssh终端中,键入以下内容以捕获测试图像:$ raspistill -o line1.jpg
您现在可以使用FileZilla(正如本书前面的章节所讨论的)将此图像下载到您的PC上。下一张图显示了一个测试图像,也用于前面的示例:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_07.jpg
图14.7 – 一条线的测试图像
图14.7 展示了我的一张测试图像。请注意,线条大致从图片中间开始,但并不精确,也不需要精确。还要注意,光线有点粗糙,产生了阴影。这些值得注意,因为它们可能会使系统混淆。
从不同的角度捕捉机器人几条线的图像,稍微向左或向右偏离相机。
现在我们有了测试图像,我们可以编写代码来测试它们!
编写Python代码以找到线条的边缘
我们准备开始编写代码,使用我们的测试图像和前面的流程图。我们可以使结果非常直观,以便我们可以看到算法正在做什么。
提示
在计算机视觉中,使用最低的分辨率来完成工作是有用的。每个额外的像素都会增加更多的内存和处理能力。在320*200的分辨率下,这是76,800个像素。树莓派相机可以以1920 x 1080的分辨率记录 – 2,073,600个像素 – 这是27倍的数据!我们需要它快速,所以我们保持低分辨率。
本节中的代码将在树莓派上运行,但您也可以在安装了Python 3、NumPy、Matplotlib和Python OpenCV的PC上运行:
-
创建一个名为
test_line_find.py的文件。 -
我们将需要导入NumPy以数值处理图像,OpenCV以操作图像,以及Matplotlib以绘制结果:
import cv2 import numpy as np from matplotlib import pyplot as plt -
现在,我们加载图像。OpenCV可以加载
jpg图像,但如果在加载过程中出现问题,它会产生一个空图像。因此,我们需要检查它是否加载了某些内容:line1.jpg and is in the same directory that we will run this file from. -
捕获的图像将是相机的大默认分辨率。为了保持快速,我们将它调整到更小的图像:
resized = cv2.resize(image, (320, 240)) -
我们还只想使用灰度;对于这个练习,我们不感兴趣的其他颜色:
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) -
现在,我们将选择行;目前,我们将使用180,因为这在240像素高的图像上相当低。图像是存储的,使得行0是顶部。请注意,我们正在告诉NumPy将其转换为
int32类型:int32 with a sign (plus or minus) so that our differences can be negative. -
我们可以为这一行的每个像素获取差异列表。NumPy使这变得容易:
diff = np.diff(row) -
我们将要绘制这个
diff列表。我们需要将x轴设置为像素编号;让我们创建一个从0到该范围的NumPy范围:x = np.arange(len(diff)) -
让我们绘制
diff变量与像素索引(x)的关系图,并将结果保存:not_blurred. This is because we've not added the optional blurring step. With the graph, we'll be able to see the difference.
指向我的测试图片,我得到了以下图表:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_08.jpg
图14.8 – 无模糊差异图
图14.8中的图表以列数作为x轴,差异作为y轴。线条中有很多噪声。有两个明显的峰值 – 一个在约列145的零线以下,一个在约240的线上方。这里的噪声不会对此产生太大影响,因为峰值非常明显:
-
让我们尝试添加模糊以查看这如何改变事情。对代码进行以下更改。粗体区域显示更改的部分:
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) blurred = cv2.blur(gray, (5, 5)) row = blurred[180].astype(np.int32) diff = np.diff(row)在此代码中,我们添加了额外的模糊步骤,模糊5x5块稍微一点。
-
为了我们可以看到不同的图表,让我们更改输出文件的名称:
plt.savefig("blurred.png")
稍微模糊应该可以减少噪声,同时不会太多地影响我们的尖锐峰值。确实,以下图显示了这是多么有效:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_09.jpg
图14.9 – 模糊后的diff图
图14.9中的图表与图14.8相似。坐标轴相同,并且有相同的峰值。然而,在0附近的线条周围的噪声少得多,这表明模糊使差异更清晰。关于这个问题,我认为不会改变结果太多。从峰值的位置和大小来看,我会说不会太多。因此,我们可以将其从最终跟踪中省略,以获得额外的速度。每个操作都会花费一点时间。
现在我们有了两个峰值,让我们使用它们来找到线条的位置。
从边缘定位线条
那些峰值是我们线条边缘的标记。要找到某物的中间位置,你将其左右坐标相加,然后除以2:
-
首先,我们必须获取坐标。让我们编写代码来请求最大值和最小值。我们将在分析代码和图表输出代码之间添加此代码:
diff = np.diff(row) min_d and max_d, abbreviating the difference as d. Note that they cannot be called min and max as those names already belong to Python. -
这些是值,但不是位置。我们现在需要找到位置索引。NumPy有一个
np.where函数可以从数组中获取索引:where function returns an array of answers for each dimension – so, although diff is a one-dimensional array, we will still get a list of lists. The first [0] selects this first dimension's results list, and the second [0] selects the first item in the results. Multiple results mean it's found more than one peak, but we assume that there's only one for now. -
要找到中间位置,我们需要将它们相加然后除以2:
middle = (highest + lowest) // 2 -
现在我们已经找到了它,我们应该以某种方式显示它。我们可以在图上用三条线绘制这个图表。Matplotlib可以为图表指定颜色和样式。让我们从中间线开始:
max_d and min_d for the Y coordinates, so the line draws from the highest peak to the lowest. The r- style specifier means to draw a solid red line. -
我们可以为
highest和lowest位置做同样的事情,这次使用g--为绿色虚线:plt.plot([lowest, lowest], [max_d, min_d], "g--") plt.plot([highest, highest], [max_d, min_d], "g--") -
就像我们对模糊处理所做的那样,让我们更改输出图的名称,以便我们可以进行比较:
plt.savefig("located_lines.png")
运行此代码应输出以下图:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_10.jpg
图14.10 – 显示最高、最低和中间线的图
图14.10中的图表显示我们已经找到了中线以及两个清晰的峰值。这段代码看起来对我们的机器人是可用的。
然而,当事情并不那么清晰时会发生什么?
尝试没有清晰线条的测试图片
让我们看看我们的线条查找代码在非常不同的测试图片上的表现。我们将看到这里会发生什么,这样我们就不那么惊讶于机器人的行为,并排除一些简单的错误。
例如,把我们的线条放在一个非常嘈杂的表面上,比如地毯上?或者没有线条的纸张,或者没有线条的地毯呢?
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_11.jpg
图14.11 – 在更嘈杂条件下的差分图
通过像图14.11中的一系列图表,我们对我们系统了解了很多。
最上面的三张图片显示了原始照片。接下来的三张图表显示了在没有模糊的情况下寻找差异和中线时的图像外观。最下面的三张图表显示了启用模糊时会发生什么。
首先,当事情变得像第一张图片那样嘈杂(这已经超出了循线应该应对的范围),模糊使得找到线条和随机伪影之间的差异;尽管如此,在第二个图表中,一个具有类似向下峰值大小的随机伪影是一个接近的竞争者。在这种情况下,制作更大的Y模糊可能会平滑掉那个伪影,只留下线条。
仔细观察,那些图表的刻度尺也不相同。普通纸张图表在没有模糊的情况下测量+10/-10的峰值,而在模糊的情况下为+1/-1。因此,当差异如此之低时,我们甚至应该寻找峰值吗?地毯-only图表中的情况也类似。
我们可以对系统进行一些更改,使其将这些视为非线条。最简单的是添加一个条件,过滤掉-5以上的最小值和10以下的最大值。我说-5,因为否则会完全过滤掉第一张图中的线条。然而,更大的模糊区域可能会有所帮助。
根据条件的嘈杂程度,我们可能需要启用模糊。在光线充足的轨道上,模糊可能不是必需的。
下一个图表显示了我们的线条在地毯上,模糊设置为(5,40),进一步在行之间模糊并过滤掉更多的噪声:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_12.jpg
图14.12 – 带有更大模糊的地毯上的线条
图14.12中的图表比之前有更少的噪声,模糊处理大大平滑了噪声尖峰,而实际的线条尖峰仍然存在。我们只会在嘈杂的环境中这样做,因为它可能会使速度变慢。
如您所见,在测试图像上测试代码使我们对我们系统了解了很多。通过拍摄相同的照片并尝试不同的参数和管道更改,您可以针对不同的场景进行优化。随着您在计算机视觉方面的实验越来越多,养成这个习惯。
现在我们已经尝试在我们的测试图像上运行我们的视觉处理代码,是时候将其应用于机器人行为上了!
使用PID算法的循线
在本节中,我们将结合之前看到的视觉处理、PID控制循环和相机流,以及在第13章中看到的第13章,机器人视觉 - 使用Pi相机和OpenCV。请从该章节中的代码开始。
你将需要的文件如下:
-
pid_controller.py -
robot.py -
servos.py -
camera_stream.py -
image_app_core.py -
leds_led_shim.py -
encoder_counter.py -
模板文件夹
我们将使用相同的模板来显示这个,但我们将添加一种快速而巧妙的方法,将OpenCV上的diff图渲染到输出帧上。Matplotlib对于这个来说太慢了。
创建行为流程图
在我们构建新的行为之前,创建一个数据流图将帮助我们了解处理数据后数据发生了什么。
系统看起来很熟悉,因为它与我们第13章中制作的非常相似第13章,机器人视觉 - 使用Pi相机和OpenCV。看看下面的图:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_13_NEW.jpg
图14.13 – 跟线行为
在图14.13中,相机图像通过到从图像获取线块。该块输出对象的X位置(线的中间),它进入我们的PID。请注意,图像数据也从获取线到图像队列,这样你就可以在浏览器中看到这些数据。
PID控制还取一个参考中间点,即相机的中间。它使用这些之间的误差来计算偏移,并使用该偏移来驱动电机。
该图显示了带有反馈线到摄像头的电机,因为这些移动的间接影响是视图发生变化,所以我们将看到不同的线。
在此之前,我们将使我们的PID控制器变得更聪明一些。
将时间添加到我们的PID控制器
我们的机器人行为包括处理帧,然后在过程完成一个周期时将错误数据发送到PID。一个周期中有很多事情发生,时间可能会变化。当我们创建积分时,我们一直像时间恒定一样添加数据。为了得到一个更准确的图像,我们应该乘以时间:
-
打开
pid_controller.py文件。 -
在
handle_integral方法中,将参数更改为接受delta_time:def handle_integral(self, error, delta_time): -
然后我们将使用这个方法来添加积分项:
self.integral_sum += error * delta_time -
我们通常使用
get_value方法来更新PID;然而,由于我们已经有使用这个方法的代码,我们应该让它对这些代码的行为保持不变。为此,我们将添加一个delta_time参数,但默认值为1:def get_value(self, error, delta_time=1): -
当这个
get_value方法调用handle_integral时,它应该始终传递新的delta_time参数:p = self.handle_proportional(error) i = self.handle_integral(error, delta_time) logger.debug(f"P: {p}, I: {i:.2f}") return p + i
虽然这不是一个很大的变化,但它意味着我们可以计算PID代码更新之间的时间变化。
我们现在可以在我们的行为中使用这个方法。
编写初始行为
我们可以将所有这些元素组合起来,创建我们的循线行为:
-
创建一个名为
line_follow_behavior.py的文件。 -
首先,我们需要导入
image_app_core、NumPy、OpenCV、摄像头流、PID控制器和机器人。我们还有time模块,这样我们就可以稍后计算时间差:import time from image_app_core import start_server_process, get_control_instruction, put_output_image import cv2 import numpy as np import camera_stream from pid_controller import PIController from robot import Robot -
让我们创建行为类。构造函数,就像之前一样,接受机器人作为参数:
class LineFollowingBehavior: def __init__(self, robot): self.robot = robot -
现在,我们需要在构造函数中添加变量来跟踪我们的行为。首先,我们应该设置我们将要查找差异的行和阈值(低于该阈值我们不将其视为线条):
self.check_row = 180 self.diff_threshold = 10 -
就像我们之前的摄像头行为一样,我们有一个中心点的设定值,一个变量来表示电机是否应该运行,以及一个前进的速度:
self.center = 160 self.running = False self.speed = 60 -
我们将要创建一些有趣的显示。我们也会在这里存储我们计划使用的颜色——一个绿色的十字准线,红色用于中线,浅蓝色用于图表。这些是BGR格式,因为OpenCV期望这样:
self.crosshair_color = [0, 255, 0] self.line_middle_color = [128, 128, 255] self.graph_color = [255, 128, 128]这就是行为构造函数的完整内容。
-
现在,我们需要控制变量来表示系统是否正在运行或应该退出。这段代码应该很熟悉,因为它与其他摄像头控制行为相似:
def process_control(self): instruction = get_control_instruction() if instruction: command = instruction['command'] if command == "start": self.running = True elif command == "stop": self.running = False if command == "exit": print("Stopping") exit() -
接下来,我们将创建
run方法,它将执行主要的PID循环并驱动机器人。我们将倾斜伺服器设置为90,将俯仰伺服器设置为0,这样它就会直视下方。我们也会设置摄像头:def run(self): self.robot.set_pan(0) self.robot.set_tilt(90) camera = camera_stream.setup_camera() -
现在,我们为方向设置PID。这些值不是最终的,可能需要调整。我们有一个低比例值,因为方向误差与电机速度相比可能相当大:
direction_pid = PIController( proportional_constant=0.4, integral_constant=0.01, windup_limit=400) -
我们暂停一秒钟,以便摄像头可以初始化,伺服器达到其位置:
time.sleep(1) self.robot.servos.stop_all() print("Setup Complete")我们停止伺服器,这样一旦它们达到位置,就不会再消耗更多电力。
-
由于我们将跟踪时间,我们在这里存储最后的时间值。时间是秒为单位的浮点数:
last_time = time.time() -
我们启动摄像头循环并将帧传递给
process_frame方法(我们很快就会编写)。我们还可以处理控制指令:for frame in camera_stream.start_stream(camera): x, magnitude = self.process_frame(frame) self.process_control()从处理一个帧中,我们期望得到一个X值,幅度是差异中的最高值和最低值之间的差异。峰值之间的差距有助于检测它是否真的是线条而不是噪声。
-
现在,对于移动,我们需要检查机器人是否正在运行,以及我们找到的幅度是否大于阈值:
if self.running and magnitude > self.diff_threshold: -
如果是这样,我们开始PID行为:
dt. This error and time delta are fed to the PID, getting a new value. So, we are ready for the next calculation: last_time now gets the new_time value. -
我们现在记录这个信息并使用这个值来改变机器人的航向。我们将电机速度设置为基本速度,然后添加或减去电机的PID输出:
print(f"Error: {direction_error}, Value:{direction_value:2f}, t: {new_time}") self.robot.set_left(self.speed - direction_value) self.robot.set_right(self.speed + direction_value) -
现在我们已经处理了检测到线条时会发生什么。那么,当我们没有检测到线条时呢?
else会停止电机运行并重置PID,这样它就不会累积异常值:else: self.robot.stop_motors() if not self.running: direction_pid.reset() last_time = time.time()注意我们在这里仍然在更新最后的时间。否则,在停止和开始之间会有很大的差距,这会导致PID输入异常值。
-
接下来,我们需要填写处理框架时发生的事情。让我们添加我们的
process_frame方法:def process_frame(self, frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blur = cv2.blur(gray, (5, 5)) row = blur[self.check_row].astype(np.int32) diff = np.diff(row) max_d = np.amax(diff, 0) min_d = np.amin(diff, 0)这段代码看起来都应该很熟悉;这是我们之前为测试代码编写的代码。
-
我们应该测试看看我们的读数是否使我们位于零线的两侧,并且我们找到了两个不同的位置。最大值不应该低于零,最小值也不应该高于它。如果它们失败了,就停止在这里——主循环将考虑这不是一条线:
if max_d < 0 or min_d > 0: return 0, 0 -
我们将像之前一样找到行上的位置,以及它们的中间点:
highest = np.where(diff == max_d)[0][0] lowest = np.where(diff == min_d)[0][0] middle = (highest + lowest) // 2 -
这样我们就可以用它来确定我们得到了一个正匹配,我们将计算最小值和最大值之间差异的幅度,确保我们没有捕捉到微弱的东西:
mag = max_d - min_d -
我们希望在这里向用户展示一些有用的信息。因此,这个方法调用了一个
make_display方法,就像其他相机行为一样。我们传递一些变量到这个显示上进行绘图:self.make_display(frame, middle, lowest, highest, diff) -
然后,我们返回中间点和幅度:
return middle, mag -
这段代码将驱动我们的机器人,但如果我们不能看到正在发生的事情,我们将很难调整它。所以,让我们创建一个
make_display方法来处理这个问题:frame, the middle position for the line, the lowest difference position in the line, the highest difference position, and diff as the whole difference row. -
我们在显示中首先想要的是中心参考。让我们在中心和选定的行上制作一个十字准线:
cv2.line(frame, (self.center - 4, self.check_row), (self.center + 4, self.check_row), self.crosshair_color) cv2.line(frame, (self.center, self.check_row - 4), (self.center, self.check_row + 4), self.crosshair_color) -
接下来,我们用另一种颜色显示我们找到的中间位置:
cv2.line(frame, (middle, self.check_row - 8), (middle, self.check_row + 8), self.line_middle_color) -
为了找到它,我们还围绕它绘制了
lowest和highest的条形图,颜色不同:cv2.line(frame, (lowest, self.check_row - 4), (lowest, self.check_row + 4), self.line_middle_color) cv2.line(frame, (highest, self.check_row - 4), (highest, self.check_row + 4), self.line_middle_color) -
现在,我们将要在一个新的空框架上绘制
diff。让我们创建一个空框架——这只是一个 NumPy 数组:graph_frame = np.zeros((camera_stream.size[1], camera_stream.size[0], 3), np.uint8)数组维度是行然后是列,所以我们将摄像机的 X 和 Y 值交换。
-
然后,我们将使用一个方法来制作一个简单的图表。我们将在下面进一步实现它。它的参数是要绘制图表的框架和图表的 Y 值。简单的图表方法暗示了 X 值为列号:
self.make_cv2_simple_graph(graph_frame, diff) -
现在我们有了框架和图表框架,我们需要将它们连接起来,就像我们在颜色检测代码中的框架一样:
display_frame = np.concatenate((frame, graph_frame), axis=1) -
我们现在可以对这些字节进行编码并将它们放入输出队列:
encoded_bytes = camera_stream.get_encoded_bytes_for_frame(display_frame) put_output_image(encoded_bytes) -
我们接下来需要实现的是这个
make_cv2_simple_graph方法。它有点厚颜无耻,但在 x 轴上绘制了 Y 点之间的线条:def make_cv2_simple_graph(self, frame, data): -
我们需要存储我们最后所在的位置,这样代码就可以相对于这个位置绘制下一个值,从而得到一个线图。我们从项目 0 开始。我们还为图表设置了一个稍微任意的中间 Y 点。记住,我们知道
diff值可以是负数:last = data[0] graph_middle = 100 -
接下来,我们应该枚举要绘制的数据,以便绘制每个项目:
for x, item in enumerate(data): -
现在,我们可以从上一个项目 Y 位置绘制到下一个 X 位置的当前位置的线条。注意我们是如何通过图表中间偏移每个项目的:
cv2.line(frame, (x, last + graph_middle), (x + 1, item + graph_middle), self.graph_color) -
然后我们需要更新最后一个项目到当前这个:
last = item好了——几乎完成了;这将把图表绘制在我们的框架上。
-
我们的行为已经完成;我们只需要外部代码来运行它!这段代码也应该类似于之前的相机示例:
color_track_behavior.html template here.
您现在可以将其上传到您的机器人。然后,打开电机并运行它。因为这是基于网络的,请将浏览器指向 http://myrobot.local:5001。
您应该看到以下内容:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_14_14.jpg
图 14.14 – 跟随线条行为输出的截图
图 14.14 中的截图显示了上方两张图片的标题。在左边是线条的相机图片。在这个框架上画了一个绿色的十字线,显示了中心点位置。还有一个大红色的条形,显示了线条的中间,以及这个条形两侧的两个较短的红色条形,显示了线条的两侧。在右边是绘制模糊后强度差异的图表。图表中可见上峰值和下峰值。
在此之下是 开始、停止 和 退出 按钮。
将机器人放置在直线上,光线充足。如果看起来像前面的显示,请按 开始 按钮以查看它运行。它应该开始不稳定地沿着线条行驶。
调整 PID
在尝试跟踪曲线线条并找到其极限时,您可以稍微大胆一些。机器人有时会超调或欠调,这就是 PID 调节发挥作用的地方:
-
如果它似乎转得太慢,尝试稍微增加比例常数。相反,如果它过度转向,尝试稍微降低比例常数。
-
如果存在轻微的连续误差,尝试增加积分常数。
PID 调节是一个重复的过程,需要大量的耐心和测试。
故障排除
如果行为不太正常,请尝试以下步骤:
-
如果将倾斜伺服设置为 90 度时看起来不垂直向下,可能没有正确校准。将
deflect_90_in_ms值参数更改为Servos对象 – 以 0.1 的增量增加,以使其达到 90 度。 -
如果它难以获得清晰的线条,请确保光线充足,它所在的表面是平坦的,例如纸张,并且线条对比度良好。
-
如果它仍然难以找到线条,以 5 为增量增加垂直模糊量。
-
如果它难以按时转向直线,尝试以 10 为增量降低速度。
-
如果您发现相机在水平方向上晃动,可以从
line_follow_behavior中移除self.robot.servos.stop_all()这一行。注意:这会以牺牲电机电池寿命为代价。 -
如果机器人发现太多其他随机垃圾而不是线条,尝试增加垂直模糊度。此外,尝试以 1 或 2 的步长增加阈值。亮度对比度越尖锐,您需要做的就越少。
-
确保您已经双重检查了代码,并且您已经在这里以及从 第 13 章,机器人视觉 – 使用 Pi 相机和 OpenCV 中找到了之前的例子。
再次找到线条
需要考虑的一个重要问题是,如果机器人失去了线条,它应该做什么。回到我们工业环境的例子,这可能是一个安全措施。
我们当前的机器人会停止。这需要你将其放回线路。然而,当你这样做时,机器人会立即开始移动。这种行为对我们的小型机器人来说很好,但对于大型机器人来说可能是一个危险隐患。
你可以考虑的另一种行为是旋转,直到机器人再次找到线路。丢失线路可能是由于机器人不足/过度转向而偏离线路且无法再次找到它,或者可能是由于机器人已经超过了线路的末端。这种行为可能适合小型机器人竞赛。
我们需要仔细考虑这类事情以及你会在哪里使用这个机器人。请注意,对于竞赛型机器人或工业机器人,它们将具有多个不同角度的传感器或更宽角度的传感器——因此,它们不太可能像我们这样丢失线路。此外,对于更大的机器人来说,即使缓慢旋转也可能非常危险。因此,让我们实现一个简单的附加安全特性。
当它找不到线路时,它不会只是停止电机;它将运行标志设置为false,因此你需要手动再次启动它:
-
再次打开
line_follow_behavior.py文件。 -
前往
run方法并找到else:语句。 -
现在,我们可以修改这个声明的内容:
else: self.robot.stop_motors() running is false, we now set running to False every time. We also reset the PID every time.
将代码保存到机器人中,并运行它直到它丢失线路。这可能是因为偏离了路线或到达了线路的末端。机器人应该停止。它应该在尝试再次移动之前等待你按下启动按钮。请注意,你需要将其放回线路并按下启动按钮才能再次移动。
这个机器人现在处理丢失线路条件更加可预测。
概述
在本章中,你看到了如何使用摄像头检测线路以及如何绘制显示它所发现的数据的图表。然后你看到了如何将数据放入驾驶行为中,以便机器人跟随线路。你增加了你的OpenCV知识,并且我向你展示了一种将图表放入摄像头流输出渲染的帧中的巧妙方法。你看到了如何调整PID以使线路跟随更准确,以及如何确保当机器人丢失线路时它能够可预测地停止。
在下一章中,我们将看到如何通过语音代理Mycroft与我们的机器人进行通信。你将向树莓派添加麦克风和扬声器,然后添加语音识别软件。这将使我们能够向树莓派发出语音命令以发送到机器人,而Mycroft将响应以让我们知道它所做的一切。
练习
现在我们已经使这个系统工作,我们可以通过以下方式增强系统并使其更有趣:
-
你能否在
make_display方法中使用cv2.putText在帧上绘制如PID数据之类的值? -
考虑将PID和误差数据与时间写入文件,然后将其加载到另一个Python文件中,使用Matplotlib显示发生了什么。这种变化可能会使回顾时的不足/过度转向更加清晰。
-
你可以修改电机处理代码,当线条靠近中间时加速,远离中间时减速。
-
一个重要的改进是检查两行并找出它们之间的角度。这样你就知道线条离中间有多远,同时你也知道线条的方向,可以利用这一点来进一步引导你的转向。
这些练习应该会给你一些有趣的玩法和实验你在这章中学到的构建和知识的方法。
进一步阅读
以下内容可以帮助你进一步了解跟随线条:
-
在Pi Wars传奇人物布莱恩·斯塔基的博客中了解Go语言在树莓派上处理线的替代方法:https://blog.usedbytes.com/2019/02/autonomous-challenge-blast-off/.
-
这里还有一个使用类似我们方法但更为复杂的跟随线条的机器人:https://www.raspberrypi.org/blog/an-image-processing-robot-for-robocup-junior/.
第十八章:第 15 章:使用 Mycroft 与机器人进行语音通信
使用我们的语音请求机器人做某事并接收语音响应长期以来被视为智能的标志。我们周围的设备,如使用 Alexa 和 Google Assistant 的设备,都有这些工具。能够编程我们的系统以集成这些工具,使我们能够访问强大的语音助手系统。Mycroft 是一个基于 Python 的开源语音系统。我们将通过将其连接到扬声器和麦克风来在树莓派上运行它,然后我们将根据我们说的词语在机器人上运行指令。
在本章中,我们将概述 Mycroft,然后学习如何将扬声器/麦克风板添加到树莓派上。然后我们将安装和配置树莓派以运行 Mycroft。
我们还将扩展我们对 Flask 编程的使用,构建具有更多控制点的 Flask API。
在本章的末尾,我们将创建我们自己的技能代码以将语音助手连接到我们的机器人。你将能够利用这些知识来创建更多的语音代理技能。
本章涵盖了以下主题:
-
介绍 Mycroft – 理解语音代理术语
-
在机器人上监听语音的限制
-
如何将扬声器/麦克风板添加到树莓派上
-
如何安装和配置树莓派以运行 Mycroft
-
编程 Flask 控制API
-
如何创建我们自己的技能代码以将语音助手连接到我们的机器人
技术要求
为了本章,你需要以下硬件:
-
另一个 Raspberry Pi 4(型号 B)。
-
一张 SD 卡(至少 8 GB)。
-
一台可以写入卡的 PC(带有 balenaEtcher 软件)。
-
ReSpeaker 2-Mics Pi HAT。
-
Mini Audio Magnet Raspberry Pi Speaker——一个带有 JST 连接器的微型扬声器或带有 3.5 毫米插孔的扬声器。
-
拥有一条 Micro-HDMI 到 HDMI 的电缆可能有助于故障排除。
-
Micro USB 电源。
-
上一章中的机器人(毕竟,我们打算让它动起来)。
本章的代码可在 GitHub 上找到:https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter15。
查看以下视频以查看代码的实际应用:https://bit.ly/2N5bXqr
介绍 Mycroft – 理解语音代理术语
Mycroft 是一个名为 语音助手 的软件套件。Mycroft 监听语音命令并根据这些命令采取行动。Mycroft 代码是用 Python 编写的,是开源和免费的。它的大部分语音处理都在云端进行。在命令处理完毕后,Mycroft 将使用语音对人类进行回应。
Mycroft 在线有文档和用户社区。在尝试了 Mycroft 之后,你可以考虑其他替代方案——例如 Jasper、Melissa-AI 和 Google Assistant。
那么,语音助手的概念是什么呢?让我们在以下小节中看看它们。
语音转文本
语音转文本(STT)描述的是将包含人类语音的音频转换为计算机可以处理的单词序列的系统。
这些可以在本地运行,也可以在更强大的机器上云上运行。
唤醒词
语音助手通常有一个唤醒词——一个在命令其余部分之前说出的短语或单词,以引起语音助手的注意。例如,有“嘿,Siri”,“嗨,Google”和“Alexa”的表达。Mycroft使用单词“Mycroft”或短语“嘿,Mycroft”,但这可以更改。
语音助手通常只监听唤醒词,直到被唤醒才会忽略所有其他音频输入。唤醒词在设备上本地识别。唤醒词之后的样本声音被发送到语音转文本系统进行识别。
表达
表达是用户说的东西的术语。语音助手使用你定义的词汇来匹配表达与技能。特定的词汇将导致Mycroft调用意图处理器。
Mycroft中的词汇包括一个文件中可互换短语的列表。
一个好的例子是询问Mycroft关于天气:“嘿,Mycroft,天气怎么样?”
意图
意图是语音助手可以执行的任务,例如找出今天的天气。我们将构建意图与我们的机器人交互。意图是技能的一部分,定义了执行它的处理代码并选择一个对话框来响应。
以天气技能为例,表达“天气怎么样?”会触发获取配置位置当前天气的意图,然后向用户说出这些细节。对于我们机器人来说,一个例子是“让机器人测试LED”,这个意图会在机器人上启动LED彩虹行为(来自第9章,在Python中编程RGB条带)。
对话
在Mycroft术语中,对话是Mycroft对用户说的短语。例如,“好的,机器人已经启动了”,或者“今天,天气晴朗”。
技能包含一系列对话。这些对话有可以说的同义词组,并且可以使用不同的语言。
词汇
你说的表达,一旦转换为文本,就会与词汇相匹配。词汇文件,就像对话一样,是意图的一部分,将表达与动作相匹配。词汇文件包含同义词组,并且可以组织成语言集,使你的技能多语言。
这将使像“天气怎么样?”,“天气晴朗吗?”,“我需要伞吗?”或“会下雨吗?”这样的短语具有同义性。你可以将某些内容分开——例如,“让机器人做”作为一个词汇项,而“向前行驶”作为另一个。
技能
技能是用于话语、对话和意图的整个词汇表的容器。一个用于闹钟的技能可能包含设置闹钟、列出闹钟、删除闹钟或更改闹钟的意图。它将包含一个用于说闹钟设置完成或确认每个闹钟的对话。
在本章的后面部分,我们将构建一个带有使机器人移动和停止意图的MyRobot技能。
现在你已经了解了一些关于语音代理术语和部件的知识。接下来,我们需要考虑我们将要构建什么。扬声器和麦克风将放在哪里?
在机器人上监听语音的限制
在我们开始构建这个之前,我们应该考虑我们将要制作什么。扬声器和麦克风应该放在机器人上还是其他地方?处理是本地还是云端?
这里有一些需要考虑的事项:
-
噪音:带有电机的机器人是一个嘈杂的环境。如果麦克风靠近电机,它几乎就毫无用处。
-
电源:语音助手一直在监听。机器人已经有很多传感器在运行,对电源的需求已经很大。这种电源需求既包括电池功率,也包括CPU功率。
-
尺寸和物理位置:扬声器和语音HAT将增加机器人高度和布线复杂性,而这个机器人本身已经非常繁忙。
一个麦克风和扬声器的组合可以放在大型机器人的茎上——一个高支架上,那里还有一个第二个树莓派。但这对这个小而简单的机器人来说不合适。我们将创建一个独立的语音助手板,它将与我们的机器人通信,但我们不会直接将其放在机器人上。语音助手将是一个第二个树莓派。
我们还将使用一个发送到云端进行语音处理系统。虽然一个完全本地的系统会有更好的隐私性并且可以更快地响应,但在撰写本文时,还没有一个完整的打包语音助手适用于树莓派。Mycroft软件让我们在使用自己的技能时具有灵活性,并且有一个可插拔的后端用于语音处理,因此有一天它可能可以在本地运行。
现在我们已经选择了如何使用Mycroft和第二个树莓派来制作我们的语音代理,现在是时候开始构建它了。
为树莓派添加声音输入和输出
在我们能够使用语音处理/语音助手之前,我们需要给树莓派一些扬声器和麦克风。一些树莓派附加组件可以提供这些设备。我的推荐是带有麦克风阵列(以提高识别率)和扬声器连接的ReSpeaker 2-Mics Pi HAT,它非常容易获得。
下一张照片显示了ReSpeaker 2-Mics Pi HAT:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_15_01.jpg
图15.1 – ReSpeaker 2-Mics Pi HAT
图 15.1 展示了 ReSpeaker 2-Mics Pi HAT 安装在 Raspberry Pi 上的照片。在左侧,我标记了左侧麦克风。该帽子有两个麦克风,每个侧面都有两个微小的矩形金属部件。下一个标签是 3 个 RGB LED 和一个连接到 GPIO 引脚的按钮。接下来是连接扬声器的两种方式 – 3.5mm 插孔或 JST 连接器。我建议您连接一个扬声器来听从这个 HAT 的输出。然后,最后一个标签突出了右侧麦克风。
我选择 ReSpeaker 2-Mic Pi HAT,因为它是一个便宜的设备,可以开始语音识别。非常便宜的 USB 麦克风对此不起作用。在 Mycroft 中有更好的支持的昂贵设备,但它们不会像帽子一样放在 Pi 上。这个 ReSpeaker 2-Mics Pi HAT 是一种权衡 – 对于硬件简单性和成本来说很好,但需要更多的软件设置。现在让我们看看如何物理安装这个 HAT。
硬件安装
ReSpeaker 2-Mics HAT 将直接安装在 Raspberry Pi 4 头部,板子悬在 Pi 上方。
扬声器将有一个微小的两针连接器(JST)类型,适合板上的单个两针插座,或者一个 3.5 mm 插孔。下一张照片显示了扬声器插入到其中:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_15_02.png
图 15.2 – Mycroft 语音助手 ReSpeaker 设置
图 15.2 展示了我的 Mycroft 设置,ReSpeaker 2-Mics Pi HAT 已设置在我的桌子上。它已通电,Raspberry Pi 已点亮。我还连接了一个扬声器。
您可以使用 Raspberry Pi 外壳或项目箱,但确保麦克风没有被遮挡。
您还需要一个 SD 卡和一个电源供应器。
重要提示
在接下来的几节中,我建议使用市电电源。不要插上电源并启动它。
现在我们已经准备好了硬件,它配备了扬声器和麦克风。在下一节中,我们将设置 Raspbian 和语音代理软件。
在 Raspberry Pi 上安装语音代理
Mycroft 为此准备了 Raspbian 发行版。让我们将其放入 SD 卡中:
-
前往 Mycroft 网站,下载 Picroft 镜像:https://mycroft-ai.gitbook.io/docs/using-mycroft-ai/get-mycroft/picroft – 该镜像基于 Raspbian Buster。选择 稳定磁盘镜像。
-
将 SD 卡插入您的计算机。使用 第 3 章 中的程序,探索 Raspberry Pi 的 在 balenaEtcher 中闪存卡 部分。务必选择 Picroft 镜像而不是 Raspbian。
-
确保此镜像可以无头运行,启用 SSH 和 Wi-Fi,就像我们在 第 4 章 中所做的那样,在 为机器人准备无头 Raspberry Pi 的 在 Raspberry Pi 上设置无线并启用 SSH 部分中。
准备好这张 SD 卡后,是时候尝试使用了。将其插入语音助手 Raspberry Pi,并使用 ReSpeaker 2-Mics Pi HAT 上的 USB 微型插座(而不是 Pi)供电。
重要提示
确保您通过 ReSpeaker 2-Mics Pi HAT 提供电源,而不是 Pi。该板需要电源来驱动其扬声器。该板的文档建议,如果您通过 Pi 为其供电,则扬声器不会输出声音。有关详细信息,请参阅 https://wiki.seeedstudio.com/ReSpeaker_2_Mics_Pi_HAT/#hardware-overview。
其主机名最初为 picroft.local。您使用用户名 pi 和密码 mycroft。确保它已连接到 Wi-Fi,并且您可以通过 SSH(PuTTY)访问它。启动 Raspberry Pi 后,您就可以开始设置 Mycroft。
安装 ReSpeaker 软件
当您登录时,Mycroft 将显示安装指南。这将向您列出的问题进行提问:
-
当询问是否要进行引导设置时,按 Y 表示是。Mycroft 安装将下载大量更新。留出 30 分钟到 1 小时的时间来完成。
-
Mycroft 现在将询问您的音频输出设备:
3, to select USB speakers, which sets some basic defaults. -
按 Ctrl + C 退出引导设置并返回到
$提示符。 -
为了使安装生效,我们需要更新 SD 卡上的软件。在提示符下,输入
sudo apt update -y && sudo apt upgrade -y。更新可能需要一些时间。 -
使用
sudo reboot重启 Pi 以使更新生效。重启 Pi 后,使用ssh登录。您将再次处于引导设置中。再次按 Ctrl + C。 -
使用以下命令安装 ReSpeaker 2-Mics Pi HAT 的音频驱动程序:
$ git clone https://github.com/waveshare/WM8960-Audio-HAT.git $ cd WM8960-Audio-HAT $ sudo ./install.shGit 克隆可能需要一分钟左右。该板使用 WM8960 音频芯片。安装脚本将花费 20-30 分钟完成。
-
再次重启。在退出引导模式后按 Ctrl + C。
在我们继续之前,测试我们是否真的得到了音频是一个好主意。
-
输入
aplay -l以列出播放设备。在输出中,您应该看到以下内容:card 1: wm8960soundcard [wm8960-soundcard], device 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]这表明它已经找到了我们的卡。
-
我们现在可以通过让这张卡播放一个音频文件来测试它是否能够播放音频。使用以下命令:
aplay -Dplayback /usr/share/sounds/alsa/Front_Left.wav。此命令指定了带有设备
-D标志的名为playback的设备,然后是要播放的文件。playback设备是一个默认的 ALSA 处理器,它确保了混音的完成,并避免了比特率和通道数不匹配的问题。在/usr/share/sounds/alsa中还有其他测试音频文件。 -
然后,我们可以使用
arecord -l命令检查录音设备。在以下输出中,我们可以看到arecord已经找到了这张卡:
card 1: wm8960soundcard [wm8960-soundcard], device 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]
该卡现在已准备好使用。接下来,我们需要向 Mycroft 系统展示如何选择这张卡进行使用。
故障排除
如果您没有音频输出,有一些事情您可以检查:
-
首先,输入
sudo poweroff来关闭Raspberry Pi。当它关闭时,检查连接。确保电路板已完全连接到Pi的GPIO引脚。确保你已经将扬声器连接到ReSpeaker 2-Mics Pi HAT的正确端口。 -
当你再次开机时,确保你使用的是ReSpeaker 2-Mics Pi HAT上的电源连接器,而不是Raspberry Pi。
-
如果你使用的是耳机插槽而不是扬声器插槽,你可能需要增加音量。输入
alsamixer,选择WM8960声卡,并将耳机音量调高。然后再次尝试播放测试。 -
确保你已经执行了
apt update和apt upgrade步骤。没有这些步骤,驱动程序的安装将无法进行。你需要在此之后重新启动,然后尝试重新安装驱动程序。 -
在安装驱动程序时,如果Git步骤失败,请仔细检查你获取的地址。
-
在尝试播放时,
-D标志是区分大小写的。小写的d在这里不起作用。
如果这些步骤仍然没有帮助,请访问https://github.com/waveshare/WM8960-Audio-HAT网站,阅读他们的文档,或者提出一个问题。
现在我们已经检查了这一点,让我们尝试将声卡与Mycroft链接。
让Mycroft与声卡通信
现在你需要将Mycroft和声卡连接起来。通过编辑Mycroft配置文件来完成此操作:
-
使用
sudo nano /etc/mycroft/mycroft.conf以root权限打开Mycroft配置文件。 -
文件中有描述Mycroft各个方面的行。然而,我们只对其中两行感兴趣:
aplay command on device hardware 0,0 (the Pi headphone jack) – written as hw:0,0. This will be the wrong device. The second specifies it will play mp3 files using the mpg123 command and on the same incorrect device. Using a direct hardware device may make assumptions about the format of the sound being played, so it needs to go through the mixer device. Let's fix these. -
将两个
hw:0,0的出现更改为术语playback。这两行应该看起来像这样:"play_wav_cmdline": "aplay -Dplayback %1", "play_mp3_cmdline": "mpg123 -a playback %1", -
按Ctrl + X来保存并退出。当被询问是否保存文件时,输入Y表示是。
-
再次重启;当你回来时,不要退出指导模式。
-
Mycroft会要求测试设备。按T来测试扬声器。可能需要几秒钟,但你将听到Mycroft对你说话。如果声音有点小,尝试输入数字
9,然后再次测试。这是一个激动人心的时刻!按D表示你已经完成了测试。 -
指导安装程序接下来会询问麦克风。选择4代表其他USB麦克风并尝试声音测试。安装程序会要求你对着麦克风说话,并且应该会回放你的声音。如果听起来不错,请按1。
-
指导安装程序会询问你是否使用推荐设置;选择1以确认你想要使用它。将有一系列关于密码设置的问题。我建议不要添加sudo密码,而是将Pi的默认密码改为一个独特的密码。
-
Mycroft将以大量紫色安装文本启动。
你已经配置并启动了Mycroft。它可以记录你的声音并回放给你听,你也已经听到了它的测试词。现在,是时候开始使用Mycroft并看看它能做什么了。
开始使用Mycroft
让我们稍微了解一下Mycroft,然后尝试与它交谈。我们将从调试界面、Mycroft客户端开始,它显示了系统正在发生的事情,然后我们将开始与它交谈。
Mycroft客户端
当你连接到Mycroft时,你会看到如下所示的显示界面:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_15_03.jpg
图15.3 – Mycroft客户端界面
图15.3中的截图是Mycroft客户端。它允许你看到Mycroft正在做什么,但你不需要连接到这个客户端,Mycroft才能听到你的声音。右上角显示了有多少消息以及你能看到多少。在截图上,你可以看到0-10条消息,总共10条消息。
主要的中间部分显示消息。红色和紫色消息来自Mycroft系统和插件。如果许多紫色消息快速闪过,说明Mycroft正在安装插件和更新,你可能需要等到它完成后再操作。绿色消息显示Mycroft与用户的交互。它显示当它检测到唤醒词、开始录音、结束录音以及它认为你所说的表述。如果它没有完全响应,你可以检查它是否正在拾取唤醒词以及表述是否与你试图说的相符。
在此之下,左侧是历史记录。在历史记录中,Mycroft从你的表述中处理的内容以蓝色显示。Mycroft所说的对话以黄色显示。你应该在扬声器上听到黄色文本的重复;然而,如果它非常忙碌,这可能需要一段时间。在右侧,它显示一个与日志文件颜色匹配的图例。更右边是一个麦克风扬声器电平计,除非Mycroft正在忙碌,或者你非常安静,否则你应该看到它在上下移动,因为它正在拾取房间内的噪音。注意 – 噪音太多,你可能会有困难与它交谈。
屏幕底部是一个输入区域,你可以在这里为Mycroft输入命令。
给系统大约30-40分钟的时间来完成所有安装。如果它没有响应,并不是卡住了,通常是在安装和编译额外的组件。
然后,Mycroft会告诉你需要在mycroft.ai进行配对。你需要使用它给出的代码来注册设备;这可以在Mycroft安装时完成。你需要在那里创建一个账户(或者如果这是一个第二设备/尝试,则登录)。请在继续之前完成此操作。
当你配对好Mycroft,并且它完成安装后,你就可以开始交互了。
与Mycroft交谈
现在你应该能够与你的语音助手交谈了:
-
首先,为了引起它的注意,你必须使用唤醒词Hey Mycroft。如果它准备好了(并且没有还在忙碌),它将发出一个扬声器音调来显示Mycroft正在倾听。你需要站在大约一米的Raspberry Pi麦克风附近。它可能会回应说请稍等,我正在完成启动。给它一分钟,然后再次尝试。
-
如果你听到声音,你现在可以要求它做某事。一个好的起点是告诉它:说你好。大约10秒后,Mycroft 应该会通过扬声器回应你好。你需要尽可能清晰地说话。我发现它需要你逐个发音;那些t和n的音是必不可少的。
现在既然它已经工作,你可以用它来玩一些有趣的事情!你可以将嘿 Mycroft缩短为Mycroft。你还可以说的其他事情包括以下内容:
-
嘿 Mycroft,天气怎么样?:这将使用天气技能并告诉你天气情况。可能不是你所在位置的天气;请使用 mycroft.ai 网站配置你的设备以适应你的位置。
-
Mycroft,23 乘以 76 是多少:这将使用 Wolfram 技能,它可以处理数学问题。
-
Mycroft,wiki banana:这将使用维基百科技能,Mycroft 将告诉你它关于香蕉的发现。
尝试这些来习惯与 Mycroft 交谈,以便它做出响应。它可能会说我不明白,日志会告诉你它听到了什么,这可以帮助你尝试调整发音以便于它理解。
我们现在可以创建一个技能,将 Mycroft 连接到我们的机器人。但首先,让我们检查是否有问题。
故障排除
如果无法让 Mycroft 说话或识别说话,请尝试以下方法:
-
确保你离麦克风足够近或声音足够大。这可以通过观察麦克风(麦克风)水平是否超过 Mycroft 控制台中的虚线来检查。
-
确保你的 Raspberry Pi 有良好的网络连接。Mycroft 只能在你可以访问互联网的地方工作。请参阅 Mycroft 文档了解如何处理代理。如果互联网连接不佳,Mycroft 可能无法正确启动。修复连接并重新启动可能会有所帮助。
-
在 Pi 启动时连接监视器可能会显示错误消息。
-
Mycroft 有一个以 故障排除和已知错误 开始的故障排除系统(https://mycroft.ai/documentation/troubleshooting/)。
-
Mycroft 正在积极开发中。使用最新的 Picroft 图像并应用 ReSpeaker 驱动程序可能会有所帮助。简而言之,安装和运行此软件可能会发生变化。
在 Mycroft 说话并做出响应的情况下,我们需要为 Mycroft 准备机器人以便与之交谈。
编程 Flask API
本章旨在使用 Mycroft 控制我们的机器人。为此,我们需要为我们的机器人提供一种接收来自其他系统命令的方式。服务器上的 应用程序编程接口(API)允许我们将此类系统解耦,以便通过网络向另一个系统发送命令并接收响应。Flask 系统非常适合构建此类系统。
基于网络的API有端点,其他系统会向其发送请求,并大致映射到Python模块中的函数或方法。正如你将看到的,我们将我们的API端点直接映射到Python robot_modes 模块中的函数。
在我们开始构建之前,让我们看看这个系统的设计——它还将揭示 Mycroft 的工作方式。
Mycroft 控制机器人的概述
下面的图示展示了用户如何通过 Mycroft 控制机器人:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_15_04.jpg
图15.4 – 机器人技能概述
图15.4 中的图示展示了数据在这个系统中的流动:
-
在左侧,它从用户对 Mycroft 发出指令开始。
-
在识别唤醒词后,Mycroft 将声音发送到 Google STT 引擎。
-
Google STT 返回文本,一个语音输入,Mycroft 会将其与技能/意图中的词汇进行匹配。我们稍后会更深入地探讨这些内容。
-
这将在机器人技能中触发意图,我们将构建这个技能。机器人技能将向机器人右侧的 Raspberry Pi 发送请求,作为对 Flask 控制API(网络)服务器的请求。
-
那个控制 API 服务器将启动机器人进程,并响应表示已完成。
-
机器人技能将选择对话来说明它已完成,并将此信息发送给 Mycroft。
-
Mycroft 将然后向用户说出这个响应。
在这一点上,我们将在机器人上构建 Flask 服务器。你之前在视觉处理章节中已经见过 Flask,并且已经安装了这个库。
远程启动行为
我们将使用 HTTP 和一个网络服务器来完成这项工作,因为向其发送请求很简单,因此我们可以构建其他远程控制机器人的方法。HTTP 在 URL 中发送请求——首先,http:// 协议标识符;服务器主机名,myrobot.local;路径,/mode/foo;之后可能还有额外的参数。我们使用 URL 的路径来确定机器人做什么。
就像我们处理其他系统一样,我们创建了一些逻辑部分和块来处理这个系统的不同方面:
-
管理机器人模式和启动/停止已知脚本的代码。它还可以给我们一个已知脚本的列表。
-
一个网络服务器来处理网络请求。
我们首先需要构建模式管理器。
管理机器人模式
我们可以通过启动和停止我们的行为脚本作为子进程来管理模式。让我们创建一个配置来告诉模式管理器关于模式的信息。这个配置将模式名称映射到一个文件——一个 Python 文件。请注意,我们指定了一个文件列表,而不是推断它。尽管我们可以将模式/路径部分添加 .py 来获取一个文件,但这有两个原因是不好的:
-
它将直接与脚本名称耦合;如果我们可以更改相同模式名称的底层脚本,那将很棒。
-
尽管机器人不是一个安全的环境,允许任意子进程运行是非常糟糕的;限制它可以使机器人更加安全。
让我们开始构建它:
-
创建一个名为
robot_modes.py的文件。这个文件包含一个名为RobotModes的类,用于处理机器人进程。 -
文件以一些导入和一些类定义的顶部开始:
import subprocess class RobotModes(object): -
接下来,我们创建一些模式映射,将模式名称映射到文件名:
mode_config = { "avoid_behavior": "avoid_with_rainbows.py", "circle_head": "circle_pan_tilt_behavior.py", "test_rainbow": "test_rainbow.py" }模式名称是一个简短名称,也称为 slug,是可读性和机器可读性之间的折衷方案 – 它们通常仅限于小写和下划线字符,并且比完整的英文描述要短。我们的文件名已经相对接近 slug 名称。
-
在固定配置之外,这个类还在管理作为进程的运行行为。它一次只能运行一个。因此,我们需要一个成员变量来跟踪当前进程并检查它是否正在运行:
def __init__(self): self.current_process = None -
我们应该能够检查是否有东西已经在运行或已完成:
subprocess is a way of running other processes and apps from within Python. We check whether we have a current process, and if so, whether it is still running. Processes have a return code, usually to say whether they completed or failed. However, if they are still running, it will be None. We can use this to determine that the robot is currently running a process. -
下一个函数是运行一个进程。函数参数包括一个模式名称。该函数检查进程是否正在运行,如果不是,则启动一个进程:
self.mode_config to map mode_name to a script name. We then use subprocess to start this script with Python. Popen creates a process, and the code stores a handle for it in self.current_process. This method returns True if we started it, and False if one was already running. -
该类需要一种方法来请求它停止一个进程。请注意,当进程未运行时,它不会尝试停止进程。当我们停止脚本时,我们可以使用 Unix 信号,这允许我们以允许它们运行
atexit代码的方式请求它们停止。它发送SIGINT信号,这是 Ctrl + C 键盘组合的等效信号:def stop(self): if self.is_running(): self.current_process.send_signal( subprocess.signal.SIGINT) self.current_process = None
在我们发出信号后,我们将当前进程设置为 None – 丢弃句柄。
我们现在有了启动和停止进程的代码,它还将名称映射到脚本。我们需要将其封装在一个语音代理可以使用的网络服务中。
编程 Flask 控制API服务器
我们之前使用 Flask 创建了我们的视觉处理行为的网络服务器。这次我们将用它来做一些更简单的事情。
正如我们在图像服务器中的启动和停止按钮所看到的,Flask 允许我们为链接设置处理程序以执行任务。让我们编写一个脚本,作为我们的控制网络服务,它使用 Flask 和我们的 RobotModes 对象。
让我们按照以下步骤构建它:
-
创建一个名为
control_server.py的脚本。我们可以从导入 Flask 和我们的机器人模式开始:from flask import Flask from robot_modes import RobotModes -
现在,我们创建一个 Flask 应用程序来包含路由和之前创建的
RobotModes类的实例:app = Flask(__name__) mode_manager = RobotModes() -
接下来,我们需要一个路由,或 API 端点,来运行应用程序。它将模式名称作为路由的一部分:
@app.route("/run/<mode_name>", methods=['POST']) def run(mode_name): mode_manager.run(mode_name) return "%s running"我们返回一个运行确认。
-
我们还需要另一个 API 端点来停止正在运行的过程:
@app.route("/stop", methods=['POST']) def stop(): mode_manager.stop() return "stopped" -
最后,我们需要启动服务器:
http for a web (hypertext) service. This is followed by a colon (:) and then two slashes // with a hostname or host address—the network address of the Raspberry Pi the resource will be on. As a host can have many services running, we can then have a port number, with a colon as a separator—in our case, :5000. After this, you could add a slash / then select a specific resource in the service.We can test this now: -
启动机器人并复制
control_server.py和robot_modes.py文件到其中。 -
通过 SSH 连接到机器人,并使用
python3 control_server.py启动控制服务器。你应该看到以下内容:$ python3 control_server.py * Serving Flask app "control_server" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: on * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) -
现在创建另一个
ssh窗口进入 Mycroft Raspberry Pi – 我们可以测试它是否与另一个窗口通信。在pi@picroft.local中按一次 Ctrl + C 以进入 Linux 命令行($提示符)。 -
curl命令在像 Raspberry Pi 这样的 Linux 系统上经常被用来测试此类服务器。它向网络服务器发送请求,发送/接收数据,并显示结果。它非常适合测试此类 HTTP 控制API。我们打算发送一个
post请求。输入以下命令:curl -X POST http://myrobot.local:5000/run/test_rainbow这应该会启动彩虹灯的开关,使用来自第9章,在Python中编程RGB灯带的代码。
curl命令指定我们使用POST方法发送请求,然后是一个包含端口号、机器人主机名、指令run和模式名称的URL。 -
你可以用
curl -X POST http://myrobot.local:5000/stop停止LED灯。这个URL有stop指令。机器人的LED彩虹灯应该会停止。注意这两个URL都是以
http://myrobot.local:5000/开头的。这个地址可能因你的主机名而异。这是这个控制服务器的基准URL。 -
你可以按Ctrl + C来停止它。
我们可以用这个来构建我们的 Mycroft 行为,但在继续之前,让我们检查一下是否有任何问题。
故障排除
如果这对你不起作用,我们可以检查一些事情来看看发生了什么:
-
如果你收到任何语法错误,请检查你的代码并再次尝试。
-
请验证你的机器人和你正在测试的设备都有互联网连接。
-
注意,当我们启动子进程时,我们正在启动Python 3。如果没有
3,可能会发生一些意外的事情。 -
首先,记住控制服务器正在机器人的Raspberry Pi 3A+上运行。你需要在
curl命令中用你的机器人地址替换它。 -
确保你已经安装了Flask,如第13章,机器人视觉 – 使用Pi摄像头和OpenCV中所示。
-
确保你已经将控制服务器和机器人模式脚本复制到了机器人上。你还需要在机器人上安装来自第9章,在Python中编程RGB灯带的代码来运行这个测试。
现在我们已经测试了控制服务器,你可以关闭Pi的电源。还有一些代码要写!让我们将其与Mycroft连接起来。
在Raspberry Pi上使用Mycroft编程语音代理
Flask控制系统提供的机器人后端足够我们创建Mycroft技能。
在图15.4中,你看到在你说出唤醒词后,Mycroft会将你发出的声音传输到Google STT系统。Google STT随后会返回文本。
Mycroft会将这些与你在该地区使用的词汇文件进行匹配,并将其与技能中设置的意图相匹配。一旦匹配,Mycroft将在技能中调用一个意图。我们的机器人技能有意图,会向为我们机器人创建的Flask控制服务器发送网络(HTTP)请求。当Flask服务器响应表示它已处理请求(可能是行为已启动)时,机器人技能将选择一个对话框对用户说话,以确认它已成功执行请求或发现问题。
我们将从简单的技能开始,有一个基本的意图,然后你可以扩展它以执行更多操作。我选择了彩虹 LED 测试(来自 第 9 章,在 Python 中编程 RGB 条带),因为它很简单。
值得注意的是,Google 处理语音所需的时间意味着这不适合紧急停止机器人;语音识别可能需要一些时间。你可以在意图中使用 GPIOZero 和 when_pressed 处理器来触发控制服务器的停止处理器。
构建意图
我们可以从意图开始,然后看看一些词汇。为了构建它,我们将使用 Mycroft 内置的名为 adapt 的库:
-
创建一个名为
my-robot-skill的文件夹,我们将在此文件夹中构建 Mycroft 技能。 -
主要意图文件将是此文件夹中的
__init__.py文件。此文件名意味着 Python 将将整个文件夹视为一个 Python 库,称为my-robot-skill/__init__.py:IntentBuilder to build and define intents around vocabulary. MycroftSkill is a base class to plug our code into Mycroft. intent_handler marks which parts of our code are intents, associating the code with IntentBuilder. We import LOG to write information out to the Mycroft console and see problems there.The last import, `requests`, is a tool to talk to our control server in Python remotely. -
接下来,我们将从
MycroftSkill基类定义我们的技能。它需要设置其父类并准备设置:super calls a method from a class we've made our base; in this case, __init__ so we can let it set things up.The only setting we have is a `base_url` member for our control server on the robot. It is consulting a settings file, which we'll see later. It's usually a good idea to separate the configuration from the code. -
下一步我们需要定义一个意图。我们通过一个
handle_test_rainbow方法来实现,但你需要使用@intent_handler装饰器。在 Python 中,装饰器会将方法包装在进一步的处理中——在这种情况下,使其适合 Mycroft:intent_handler decorator takes some parameters to configure the vocabulary we will use. We will define vocabulary in files later. We require a vocabulary matching *robot* first, then another part matching *TestRainbow* – which could match a few phrases. -
下一步,这个技能应该使用
requests.post向机器人发送请求:base_url variable, plus the run instruction and the test_rainbow mode. -
我们需要 Mycroft 说出一些话,表明它已经告诉机器人在这里做些什么:
speak_dialog method tells Mycroft to pick something to say from dialog files, which allows it to have variations on things to say. -
此请求可能因几个原因而失败,因此在代码片段的最后之前有
try。我们需要一个except来处理这种情况,并为用户说话一个对话框。我们还将LOG一个异常到 Mycroft 控制台:Unable to reach the robot, while not inspecting the result code from the server other than if the voice skill contacted the robot. -
此文件需要提供一个
create_skill函数,该函数位于类外部,Mycroft 预期在技能文件中找到:def create_skill(): return MyRobot()
代码是这个系统的一部分,但在使用它之前我们需要配置它。
设置文件
我们的意图首先加载一个设置。我们将将其放在 my-robot-skill/settingsmeta.json 中,并定义我们的控制服务器的基准 URL。
如果你的机器人树莓派的域名/地址不同,请使用该域名/地址。此文件对于这个设置来说有点长,但意味着你可以稍后配置 URL:
{
"skillMetadata": {
"sections": [
{
"name": "Robot",
"fields": [
{
"name": "base_url",
"type": "text",
"label": "Base URL for the robot control server",
"value": "http://myrobot.local:5000"
}
]
}
]
}
}
我们已经设置了要使用的基准 URL,但我们需要配置 Mycroft 以加载我们的技能。
要求文件
我们的技能使用 requests 库。当 Mycroft 遇到我们的技能时,我们应该告诉它期待这个。在 Python 中,要求文件是这样做的一种标准方式。将以下内容放入 my-robot-skill/requirements.txt:
requests
此文件并非仅限于 Mycroft,它还与许多 Python 系统一起使用,以安装应用程序所需的库。
现在,我们需要告诉 Mycroft 它需要监听什么,即词汇。
创建词汇文件
要定义词汇,我们需要定义词汇文件。你需要将它们放入一个遵循格式my-robot-skill/vocab/<IETF语言和区域设置>的文件夹中。语言/区域设置意味着我们应该能够为变体定义词汇,例如en-us代表美式英语和zn-cn代表简体中文;然而,在撰写本文时,en-us是最受支持的Mycroft语言。社区的一部分正在努力支持其他语言。
你使用一个或多个与词汇文件匹配的词汇部分来定义每个意图。词汇文件有代表预期话语方式的行。这允许人类自然地变化他们说话的方式,当机器无法以稍微不同的方式请求某事时,人们会注意到这一点。在为词汇文件想出类似短语时,有一些技巧。
我们需要为我们的意图定义两个词汇文件——一个用于robot同义词,另一个用于TestRainbow同义词:
-
在
my-robot-skill下创建vocab文件夹,然后在其中创建en-us文件夹。 -
在那里创建一个路径和名称为
my-robot-skill/vocab/en-us/robot.voc的文件。 -
添加一些用于请求机器人做某事的短语:
robot in the intent handler. -
让我们创建用于测试彩虹的词汇。将其放入
my-robot-skill/vocab/en-us/TestRainbow.voc:test rainbow test the leds deploy rainbows turn on the lights重要提示
注意,词汇文件名的首字母大小写必须与意图构建器匹配;我随后使用了将非共享词汇部分首字母大写的惯例。
在测试这个时,你不可避免地会尝试说出一个听起来合理的短语,但这个短语并不在那里。Mycroft会告诉你“抱歉,我不明白”,然后你会在上面的词汇表中添加另一个表达。
对话文件
我们还想要定义Mycroft会对你说的短语。到目前为止,我们有三个短语是我们意图所需的。这些短语将放入与词汇文件类似结构的my-robot-skill/dialog/en-us文件夹中。让我们来构建它们:
-
在
my-robot-skill下创建dialog文件夹,然后在其中创建en-us文件夹。 -
在该文件夹中创建路径为
my-robot-skill/dialog/en-us/Robot.dialog的文件。我们可以在其中添加一些短语:The Robot Robot -
下一个对话我们需要的是同一文件夹中的
TestRainbow.dialog:is testing rainbows. is deploying rainbows. is starting rainbows. is lighting up. -
由于我们有一个错误处理器,我们也应该创建
UnableToReach.dialog:Sorry I cannot reach the robot. The robot is unreachable. Have you turned the robot on? Is the control server running on the robot?
通过定义多个可能的对话,Mycroft会随机选择一个,使其不那么重复。我们已经看到了如何制作词汇短语和对话短语。让我们简要回顾一下我们应该有什么。
当前技能文件夹
我们的技术文件夹应该看起来像以下截图:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_15_05.jpg
图15.5 – 机器人技能文件夹截图
在 图 15.5 中,我们看到一个截图显示了名为 my-robot-skill 的文件夹中的技能。这个技能文件夹包含 dialog 文件夹,其中包含 en-us 子文件夹和这里的三个对话文件。下面是 vocab 文件夹,包含 en-us 文件夹和两个词汇文件。在 vocab 文件夹下面,我们有 __init__.py 文件,它定义了意图、Mycroft 安装它的要求和一个设置文件。哇 - 我们在这里创建了很多东西,但这一切都将是值得的!
我们现在需要将整个文件夹结构上传到我们的机器人:
-
使用 SFTP(FileZilla),将此文件夹上传到你的 Mycroft Pi,在
/opt/mycroft/skills文件夹中。 -
Mycroft 将自动加载此技能;你将看到紫色文本在安装过程中闪烁。
-
如果你需要更新代码,再次上传文件到这个位置将导致 Mycroft 重新加载它。
加载或使用技能时遇到的问题将在 Mycroft 输出中显示。你还可以在
/var/log/mycroft/skills.log中找到结果——lessLinux 工具对于查看此类日志输出很有用,使用 Shift + G 跳到文件末尾或输入/myrobot跳到其输出。你也可以使用
tail -f /var/log/mycroft/skills.log来查看问题发生时的状态。使用 Ctrl + C 来停止。 -
现在,打开机器人的电源,使用
ssh登录,并使用python3 control_server.py启动控制服务器。 -
然后,你可以尝试使用 Mycroft 测试你的技能:告诉机器人打开灯光。
-
Mycroft 应该发出哔哔声以向用户显示它已唤醒,一旦它从语音到文本转换中获取了单词,它将向机器人上的控制服务器发送
/run/test_rainbow。你应该听到 Mycroft 说出其中一个对话短语,例如 机器人正在测试彩虹,并看到 LED 灯亮起。
故障排除
如果你遇到意图响应的问题,请尝试以下操作:
-
首先,检查之前 Python 代码的语法和缩进。
-
确保你的机器人和语音助手 Raspberry Pi 在同一网络中;我发现一些 Wi-Fi 扩展器存在这个问题,需要使用 IP 地址而不是
myrobot.local。使用settingsmeta.json文件来配置此设置。 -
确保你已经将整个结构 - 包括
vocab、dialog、settingsmeta.json和__init__.py- 复制到语音助手 Raspberry Pi 的/opt/mycroft/skills文件夹中。 -
如果你的设置不正确,你需要在 https://account.mycroft.ai/skills 页面上更改它们。查找
My Robot技能并在此处更改。你需要保存更改,可能需要重新启动 Mycroft 或等待几分钟以使更改生效。 -
确保你与 Mycroft 对话的方式与你的词汇文件相匹配 - 否则它将无法识别你的单词。
-
如果你遇到它识别你的声音的问题,你也可以在 Mycroft 控制台中输入短语。
我们的第一种意图已经生效!你能够对语音助手说话,并且它已经指导机器人执行操作。然而,我们现在开始闪烁 LED,唯一停止它们的方法是使用那个不方便的 curl 命令。我们可能需要通过添加另一个意图来修复这个问题。
添加另一个意图
现在我们有了自己的技能,为它添加一个停止的第二个意图变得相对容易,使用机器人控制服务器中的另一个端点。
词汇和对话
我们需要添加词汇和对话,以便我们的新意图能够理解我们所说的话,并且有一些话可以回应:
-
我们需要创建
stop词汇;我们可以将其放在my-robot-skill/vocab/en-us/stop.voc中:stop cease turn off stand down -
我们需要为 Mycroft 创建一个对话文件,以便在
my-robot-skill/dialog/en-us/stopping.dialog中告诉机器人它正在停止:is stopping. will stop.
这些就足够了,但如果你想到了更多的同义词,也可以添加。
添加代码
现在我们需要将意图代码添加到我们的技能中:
-
我们将此放入
my-robot-skill/__init__.py中的MyRobot类:stop vocabulary, the handler name (which could be anything – but must not be the same as another handler), and the URL endpoint. Identical code like that is ripe for refactoring. Refactoring is changing the appearance of code without affecting what it does. This is useful for dealing with common/repeating code sections or improving how readable code is. Both the intents have the same try/catch and similar dialog with some small differences. -
在同一文件中,添加以下内容:
end_point as a parameter and uses that in its request. It takes a dialog_verb parameter to say after the Robot bit. All of the other dialog and error handling we saw before is here. -
这两个意图现在变得更为简单。将它们更改为以下内容:
@intent_handler(IntentBuilder("") .require("Robot") .require("TestRainbow")) def handle_test_rainbow(self, message): self.handle_control('/run/test_rainbow', 'TestingRainbow') @intent_handler(IntentBuilder("") .require("Robot") .require("stop")) def handle_stop(self, message): self.handle_control('/stop', 'stopping')
添加新的意图现在更容易,因为我们能够重用 handle_control。
使用新意图运行
你现在可以再次上传文件夹结构——因为 vocab、dialog 和 __init__ 文件已经更改。当你这样做时,请注意 Mycroft 将自动重新加载更改后的技能(或显示尝试这样做时出现的任何问题),因此它立即可以投入使用。
通过说出 Mycroft,告诉机器人停止 来尝试一下。
你现在已向系统中添加了第二个意图,定义了更多的词汇和对话。你也已经重构了这段代码,因为看到了一些重复。现在,你的机器人已经开始具备语音控制功能。
摘要
在本章中,你学习了语音助手术语、语音转文本、唤醒词、意图、技能、话语、词汇和对话。你考虑了麦克风和扬声器的安装位置,以及它们是否应该安装在机器人上。
你随后看到了如何将扬声器/麦克风组合物理安装在 Raspberry Pi 上,然后准备软件以便 Pi 使用它。你安装了 Picroft – 一个 Mycroft Raspbian 环境,获取了语音代理软件。
你随后能够与 Mycroft 互动,让它响应不同的语音命令,并将其与基础注册。
你还看到了如何使机器人准备好接受外部代理,例如使用 Flask API 的语音代理来控制它。你能够创建多个与机器人通信的技能,为创建更多技能提供了良好的起点。
在下一章中,我们将回顾在第 12 章第 12 章中引入的 IMU,使用 Python 进行 IMU 编程,并让它做更多有趣的事情——我们将平滑和校准传感器,然后将它们组合起来为机器人获取航向,编程机器人始终朝北转。
练习
尝试这些练习以充分利用本章内容并扩展你的经验:
-
尝试从Mycroft网站安装一些其他Mycroft技能并与之互动。提示:说“Hey Mycroft,install pokemon”。
-
机器人模式系统有一个缺陷;它假设你要求停止的过程确实停止了。它应该等待并检查返回代码以查看是否已停止吗?
-
实现机器人模式的另一种方法可能是更新所有行为以干净地退出,这样你就可以导入它们而不是在子进程中运行。这会多么棘手?
-
在测试交互时,你是否觉得词汇量不足?也许你可以用更自然的短语来开始不同的行为。同样,你也可以使对话更有趣。
-
向技能添加更多意图,例如,避障。你可以添加一个停止意图,尽管响应时间可能会使这不太理想。
-
ReSpeaker 2-Mics Pi HAT上的RGB LED可以使用吗?项目https://github.com/respeaker/mic_hat有一个LED演示。
有这些想法,有足够的空间来进一步探索这个概念。进一步的阅读也会有所帮助。
进一步阅读
请参阅以下信息以获取更多信息:
-
《树莓派机器人项目》,理查德·格里姆特博士,Packt Publishing,有一章介绍语音输入和输出。
-
《语音用户界面项目》,亨利·李,Packt Publishing,专注于系统的语音界面。它展示了如何使用Alexa和Google Home语音代理构建聊天机器人和应用程序。
-
《Mycroft AI – 语音栈介绍》 – Mycroft AI的一份白皮书提供了更多关于Mycroft栈如何工作及其组件的详细信息。
-
Mycroft有一个庞大的社区支持并讨论这项技术,请参阅https://community.mycroft.ai/。我建议咨询这个社区的故障排除信息。Mycroft正在积极开发中,既有许多怪癖,也有许多新功能。它也是一个分享你为它构建的技能的绝佳地方。
-
查看https://github.com/respeaker/seeed-voicecard以获取Seeed Studio,ReSpeaker 2-Mics Pi HAT的创造者,提供的关于此设备的文档和代码,以及更大型的四麦克风和六麦克风版本。
第十九章:第16章: 使用IMU深入探索
在第12章的使用Python进行IMU编程中,我们读取了来自惯性测量单元(IMU)的数据。我们现在对处理传感器数据、使用数学和管道进行决策有了更多的了解。
在本章中,我们将学习如何从IMU获取校准数据,结合传感器数据,并使用这些数据使机器人具有基于绝对方向的行性行为。在这个过程中,我们将看到用于提高精度/速度或准确性的算法。
到本章结束时,你将能够检测机器人的绝对方向,并在屏幕上显示,并将其与比例-积分-微分(PID)行为相结合。
本章我们将涵盖以下主要内容:
-
编程虚拟机器人
-
使用陀螺仪检测旋转
-
使用加速度计检测俯仰和滚转
-
使用磁力计检测航向
-
从磁力计获取大致航向
-
结合传感器进行方向检测
-
使用IMU数据驱动机器人
技术要求
对于本章,你需要以下物品:
-
来自至少第14章的机器人代码,使用Python进行基于摄像头的循线
-
来自第14章的机器人代码,使用Python进行基于摄像头的循线,在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter14
-
来自第12章的IMU代码,使用Python进行IMU编程,在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter12
-
在没有许多磁铁的广阔驾驶空间
-
磁罗盘
本章的完整代码请访问https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter16。
查看以下视频,了解代码的实际应用:https://bit.ly/2LztwOO
编程虚拟机器人
我们首先将检测我们的机器人方向;将其显示为3D机器人模型将很有用。这部分基于第12章中的表示坐标和旋转系统部分,使用Python进行IMU编程。在本节中,我们将使用VPython构建我们机器人的简单模型。
在VPython中建模机器人
我们将使用称为 原语 的形状来模拟机器人。它们具有位置、旋转、大小和颜色。高度和宽度参数与 VPython 世界坐标系匹配(参见 第 12 章 中的 图 12.14 – 机器人身体坐标系,使用 Python 进行 IMU 编程),因此我们必须旋转物体以匹配机器人身体坐标系。
首先,我们需要收集一些机器人测量值。以下图表显示了它们的位置。一旦完成主要测量,可以使用估计值进行较小测量:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_01.jpg
图 16.1 – 虚拟机器人的测量值
图 16.1 展示了机器人的测量值。俯视图和左视图展示了覆盖不同方面的内容。这包括底座的宽度和高度——注意,我们在此目的将其视为矩形。需要测量或猜测车轮的大小和位置,以及万向轮的大小和位置。为你的机器人测量或猜测这些值。对我们来说,猜测就足够了。位置来自底盘的中间。
让我们编写代码来创建基本形状,如下所示:
-
创建一个名为
virtual_robot.py的文件,并添加vpython导入和我们的机器人视图来启动它,如下所示:import vpython as vp from robot_pose import robot_view -
我们将虚拟机器人放入一个函数中,以便在几种不同的行为中使用,如下所示:
def make_robot(): -
我们将 图 16.1 中的机器人测量值放入变量中。我使用了 毫米(mm)来表示所有这些值。代码如下所示:
chassis_width = 155 chassis_thickness = 3 chassis_length = 200 wheel_thickness = 26 wheel_diameter = 70 axle_x = 30 axle_z = -20 castor_position = vp.vector(-80, -6, -30) castor_radius = 14 castor_thickness = 12 -
底座是一个默认位置为 (
0,0,0) 的盒子。代码如下所示:base = vp.box(length=chassis_length, height=chassis_thickness, width=chassis_width) -
将此盒子绕
x轴旋转 90 度以匹配身体坐标系,将z轴向上,如下所示:base.rotate(angle=vp.radians(90), axis=vp.vector(1, 0, 0)) -
我们将使用两个圆柱体作为车轮。每个车轮到中间的距离大约是底盘宽度的一半。让我们用它来创建车轮的
y位置,如下所示:wheel_dist = chassis_width/2 -
我们设置车轮位置以与电机轴的末端对齐。左轮有一个
y坐标;-wheel_dist将其移动到平台左侧,如下代码片段所示:cylinder axis says which way it is pointing. We set *y* to *-1* to point it left. -
现在,我们设置右轮,
wheel_dist为正值,y轴为1,以便它指向右侧,如下代码片段所示:wheel_r = vp.cylinder(radius=wheel_diameter/2, length=wheel_thickness, pos=vp.vector(axle_x, wheel_dist, axle_z), axis=vp.vector(0, 1, 0)) -
我已经使用了一个圆柱体作为后轮的万向轮,如下代码片段所示:
castor = vp.cylinder(radius=castor_radius, length=castor_thickness, pos=castor_position, axis=vp.vector(0, 1, 0)) -
现在,我们将所有这些部分组合成一个复合对象——一个单一的 3D 对象,如下所示:
return vp.compound([base, wheel_l, wheel_r, castor]) -
为了测试它,让我们创建一个微小的
main部分。此代码检查你是否直接启动了它,因此以下代码在将虚拟机器人作为库导入时不会运行:if __name__ == "__main__": -
设置视图,将摄像机放置在机器人前方,如下所示:
robot_view() -
我们将添加轴来显示物品的位置,如下所示:
x_arrow = vp.arrow(axis=vp.vector(200, 0, 0), color=vp.color.red) y_arrow = vp.arrow(axis=vp.vector(0, 200, 0), color=vp.color.green) z_arrow = vp.arrow(axis=vp.vector(0, 0, 200), color=vp.color.blue) -
然后,我们将绘制机器人,如下所示:
make_robot() -
使用
vpython virtual_robot.py上传并测试此代码。 -
打开浏览器,查看您机器人上的端口
9020以查看您的虚拟机器人。您应该看到以下类似的图像!https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_02.jpg图16.2 – VPython 3D虚拟机器人的截图
在 图16.2 中,我们可以看到红色表示的 x 轴向前,绿色表示的 y 轴向右,蓝色表示的 z 轴向上。这遵循右手定则坐标系。它显示了从前方看到的虚拟机器人,两侧各有一个轮子。它是灰色、方形的,但对我们剩余的实验来说足够了。
-
您可以右键单击并拖动它以获得另一个视角。鼠标滚轮也可以放大或缩小。以下截图显示了后轮:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_03.jpg
图16.3 – 虚拟机器人的不同视角
图16.3 展示了该虚拟机器人的左侧视角。
关闭浏览器标签页,然后按 Ctrl + C 完成此程序。让我们检查一下你是否能跟上。
故障排除
如果您没有让这个工作,让我们检查以下几点:
-
如果您收到显示 没有这样的模块 vpython 的错误,请确保您已遵循 第12章 中的 简化 VPython 命令行 部分,使用 Python 进行 IMU 编程,在 读取温度 部分。您需要 第12章 中的整个代码,使用 Python 进行 IMU 编程,以便本章工作。
-
如果您收到显示 没有这样的命令 vpython 的错误,请确保您已遵循 第12章 中的 简化 VPython 命令行 部分。VPython 的别名对于能够看到显示是必要的。
-
如果您看到语法错误,请检查您的代码是否有误。
-
如果您无法到达显示屏(并且已经检查了 步骤1),请确保您在机器人上使用端口
9020(我的机器人是 http://myrobot.local:9020)。 -
请耐心等待——VPython 启动可能需要一分钟或两分钟。
现在我们有一个可以玩耍的视觉机器人,我们可以重新审视陀螺仪,并尝试让屏幕上的机器人像我们的真实机器人一样移动。
使用陀螺仪检测旋转
我们已经从陀螺仪获得了一些原始数据,但为了更有效地使用它,我们需要执行两个操作,校准陀螺仪,然后将其积分,如下图中所示:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_04.jpg
图16.4 – 陀螺仪数据流
图16.4 展示了数据流,我们将在本节稍后更详细地探讨这些概念。第一个操作显示在顶部,显示了陀螺仪数据经过偏移校准以去除误差。这给我们一个校准后的陀螺仪,以每秒度数的变化率(每轴)显示——由圆圈周围的箭头表示。陀螺仪进行相对测量。
图表的下半部分是第二个操作,将时间差与校准后的陀螺仪(gyro)结合。我们需要积分这个值以找到绝对测量值。积分器将输入值乘以时间差并添加到先前结果中。在这种情况下,我们将陀螺仪速率乘以时间差以产生该期间的移动(由框中的乘法符号表示)。上面的圆圈有一个小扇形,用虚线表示移动的量。
我们将移动量加到该轴的最后一个值上,由加号符号框表示。上面的圆圈显示了一个实心灰色扇形表示现有位置,以及一个带有虚线的新的扇形段。当它们相加时,它们构成了该轴的总值——由一个带有大实心灰色扇形的圆圈表示,表示加法的结果。系统将俯仰、滚转或偏航结果反馈到下一个循环。
在我们这样做之前,我们需要纠正陀螺仪的错误。
校准陀螺仪
由于它们是从工厂出来的,微机电系统(MEMS)陀螺仪通常有一些微小的缺陷,导致它们给出略微不准确的读数。这些缺陷将导致我们的积分出现漂移。
我们可以编写代码来检测这些并补偿;我们称之为校准。按照以下步骤进行:
-
创建一个名为
calibrate_gyro.py的文件。 -
我们需要VPython来处理向量,time来稍微休息一下,以及设置IMU,如下面的代码片段所示:
from robot_imu import RobotImu import time import vpython as vp imu = RobotImu() -
我们需要向量来保存陀螺仪的最小和最大值,如下面的代码片段所示:
gyro_min = vp.vector(0, 0, 0) gyro_max = vp.vector(0, 0, 0) -
现在,对于循环,我们将在一段时间内进行多次读数,如下所示:
for n in range(500): gyro = imu.read_gyroscope() -
为了校准,我们需要测量一段时间以获取每个轴的最小和最大值。Python的
min函数返回它给出的两个值中的较小值,如下所示:gyro_min.x = min(gyro_min.x, gyro.x) gyro_min.y = min(gyro_min.y, gyro.y) gyro_min.z = min(gyro_min.z, gyro.z) -
我们对最大值也做同样的处理,使用Python的
max函数,如下所示:gyro_max.x = max(gyro_max.x, gyro.x) gyro_max.y = max(gyro_max.y, gyro.y) gyro_max.z = max(gyro_max.z, gyro.z) -
这些中间部分是我们距离零的估计值。我们可以通过将向量相加然后除以2来计算这个值,如下所示:
offset = (gyro_min + gyro_max) / 2 -
在下一个循环之前稍微休息一下,如下所示:
time.sleep(.01) -
我们打印这些值以便使用,如下所示:
print(f"Zero offset: {offset}.") -
此代码已准备好运行。使用Python 3上传并运行此代码,直到程序退出之前,请确保机器人仍然位于平坦、稳定的表面上。
-
你应该看到控制台输出以类似以下内容结束:
pi@myrobot:~ $ python3 calibrate_gyro.py Zero offset: <-0.583969, 0.675573, -0.530534>.
我们在这里测量的是当静止时陀螺仪平均变化了多少。这被计算为每个轴的偏移量。通过从测量值中减去这个值,我们将主要抵消陀螺仪的连续误差。让我们把它放在我们可以使用的地方,如下所示:
-
创建一个名为
imu_settings.py的文件。 -
我们将导入
vector类型,然后设置我们的校准读数。你可能只需要运行一次,或者如果你更换了IMU设备。请使用从你的机器人获得的读数。运行以下代码:from vpython import vector gyro_offsets = vector(-0.583969, 0.675573, -0.530534) -
接下来,我们将我们的
RobotImu类升级以处理这些偏移量——打开robot_imu.py。 -
如果我们传递偏移量,我们的类将接受它们,或者如果我们不传递它们,则使用零。对
RobotImu的__init__方法进行以下突出显示的更改:def __init__(self, gyro_offsets=None): self._imu = ICM20948() self.gyro_offsets = gyro_offsets or vector(0, 0, 0) -
我们需要修改
read_gyroscope方法来考虑这些偏移量,如下所示:def read_gyroscope(self): _, _, _, gyro_x, gyro_y, gyro_z = self._imu.read_accelerometer_gyro_data() return vector(x, y, z) - self.gyro_offsets
现在,为了看看这行不行,让我们用它来移动一个虚拟机器人。
使用陀螺仪旋转虚拟机器人
我们提到了如何将陀螺仪测量值进行积分。看看以下图表,看看这是如何对单个轴起作用的:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_05.jpg
图16.5 – 整合陀螺仪轴
图16.5显示了一个虚线圆圈,表示轴的旋转圆。穿过圆的十字线表示其中心。圆圈上方和左边的粗箭头表示当前的航向。阴影区域表示一段时间内旋转的变化(以度为单位),我们将这些变化加到当前航向上,以得到新的航向估计——另一个粗箭头。
我们将旋转速率乘以时间以获得移动;这是一个估计,因为我们没有中间值。
时间自上次测量以来的概念是一个重要的概念,在第14章中可以看到,即“Python中用摄像头进行路径跟踪”。它更常被称为时间差。
我们可以将我们对陀螺仪的了解与虚拟机器人结合起来,使其在屏幕上旋转。让我们用这个来旋转我们的虚拟机器人,如下所示:
-
创建一个名为
visual_gyroscope.py的新文件。我们在这里有很多导入,以便将组件组合在一起,如下面的代码片段所示:import vpython as vp from robot_imu import RobotImu import time import imu_settings import virtual_robot -
这次,当我们设置
RobotImu时,我们将使用我们设置的设置,如下所示:imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) -
我们将要整合三个轴:
俯仰角、横滚角和偏航角。让我们从零开始,如下所示:pitch = 0 roll = 0 yaw = 0 -
我们现在应该设置虚拟机器人和它的视图,如下所示:
model = virtual_robot.make_robot() virtual_robot.robot_view() -
我们将要跟踪时间差,所以我们首先获取最新的时间,如下所示:
latest = time.time() -
然后我们开始这个行为的主体循环。由于这是在VPython中动画化,我们需要设置循环速率并告诉它更新,如下所示:
while True: vp.rate(1000) -
我们现在计算时间差(
dt),存储最新的时间,如下所示:current = time.time() dt = current - latest latest = current -
代码读取
gyro向量中的陀螺仪,如下所示:gyro = imu.read_gyroscope() -
我们将当前速率(每秒度数)乘以
dt(秒)进行积分,如下面的代码片段所示:roll += gyro.x * dt pitch += gyro.y * dt yaw += gyro.z * dt -
我们将模型的朝向重置为准备旋转,如下所示:
model.up = vp.vector(0, 1, 0) model.axis = vp.vector(1, 0, 0) -
我们通过每个轴进行旋转。我们必须将这些转换为弧度,如下所示:
1, 0, 0). -
这段代码现在可以运行了;这将使虚拟机器人在现实世界中旋转时改变位置!上传文件并使用
vpython visual_gyroscope.py运行。 -
如前所述,等待一分钟或更长时间,并将您的浏览器指向
myrobot.local:9020。您应该看到以下显示:https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_06.jpg图16.6 – 旋转的机器人
图16.6显示了通过移动真实机器人使虚拟机器人旋转到一定角度。移动一下你的机器人——尽量接近你在这里看到的样子。
-
你会注意到,当你移动机器人并将其放回原位时,它将不再正确对齐——这是陀螺仪积分造成的累积误差或漂移。
在这个实验中,虽然看到了一些很好的运动,但你同时也注意到仅使用陀螺仪无法准确跟踪旋转。我们将需要利用IMU设备中的其他传感器来提高这一点。
在继续之前,让我们检查它是否正常工作。
故障排除
如果它还没有完全工作,尝试以下步骤:
-
此代码需要使用
vpython命令和浏览器来查看结果。 -
如果机器人静止时仍在移动,请重试校准和偏移。陀螺仪的特性是这不会完美——我们将在后面修复它。
-
如果机器人看起来无法控制地旋转或跳跃,请确保你已经记得将其转换为弧度。
-
如果机器人旋转的方向不正确(左右而不是上下),请检查旋转轴的参数。
现在你已经使它工作,让我们继续到加速度计,这样我们就可以看到作用在我们机器人上的力了!
使用加速度计检测俯仰和滚转
在第12章,“使用Python进行IMU编程”中,我们得到了加速度计的矢量,但我们需要计算角度以考虑与陀螺仪和磁力计一起使用。为了使用这个来旋转物体,我们需要将这个矢量转换为俯仰和滚转角度。
从加速度计矢量获取俯仰和滚转
加速度计描述了在笛卡尔坐标系中发生的事情。我们需要将这些转换为相互垂直的一对俯仰和滚转角度。在第12章,“使用Python进行IMU编程”中,坐标和旋转系统部分显示滚转发生在x轴周围,俯仰发生在y轴周围。
考虑这种方法的粗略但有效的方式是将其视为两个平面。当围绕x轴旋转时,你可以在yz平面取一个矢量并找到其角度。当围绕y轴旋转时,则考虑xz平面。看看下一个图:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_07.jpg
图16.7 – 加速度计矢量和角度
在图16.7中,背景有x、y和z轴以及一个球体,周围有围绕xz和yz平面的圆圈。
加速度计矢量为A。仅使用xz分量,我们将此矢量投影到点C处的xz圆上;因此,从z轴到C的角度是俯仰。我们再次将A投影到点B处的yz圆上;从z轴到B的角度是滚转。
当我们在一个平面上有两个组件(例如 x 和 z)时,它们可以用在 atan2 函数(存在于大多数编程语言中)中以获取一个角度。这里的一个小问题是不同传感器组件的朝向意味着我们必须取俯仰的相反数。以下图表显示了这个过程:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_08.jpg
图16.8 – 加速度计数据流
图16.8 显示了原始加速度计数据进入反正切函数以获取角度,并输出加速度计俯仰/滚转值。
让我们把加速度计读数转换成俯仰和滚转,然后放入图表中,如下所示:
-
首先,打开
robot_imu.py。 -
将导入扩展到包括三角函数,如下所示:
from vpython import vector, degrees, atan2 -
在
read_accelerometer方法之后,添加以下代码以执行所需的数学运算:def read_accelerometer_pitch_and_roll(self): accel = self.read_accelerometer() pitch = degrees(-atan2(accel.x, accel.z)) roll = degrees(atan2(accel.y, accel.z)) return pitch, roll -
让我们在图表上显示这些角度,这将同时揭示使用加速度计时存在的重大缺陷。创建一个
plot_pitch_and_roll.py文件。 -
从导入开始,如下所示:
import vpython as vp import time from robot_imu import RobotImu imu = RobotImu() -
我们创建图表,如下所示:
vp.graph(xmin=0, xmax=60, scroll=True) graph_pitch = vp.gcurve(color=vp.color.red) graph_roll = vp.gcurve(color=vp.color.green) -
现在,我们设置一个起始时间,以便我们可以创建一个基于时间的图表,如下所示:
start = time.time() while True: vp.rate(100) elapsed = time.time() - start -
我们现在可以获取新的俯仰和滚转值,如下所示:
pitch, roll = imu.read_accelerometer_pitch_and_roll() -
然后,我们可以将这两组数据放入图表中,如下所示:
graph_pitch.plot(elapsed, pitch) graph_roll.plot(elapsed, roll) -
上传
robot_imu.py和plot_pitch_and_roll.py文件。使用vpython plot_accel_pitch_and_roll.py运行,并将浏览器指向机器人的端口9020。这应该会产生以下结果:https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_09.jpg
图16.9 – 加速度计俯仰和滚转图
图16.9 显示了图表的截图。图表中的红色曲线代表俯仰,围绕 y 轴,而绿色曲线代表滚转,围绕 x 轴。虽然它显示了+90和-90度之间的摆动,但也很明显的是,图表有很多噪声,以至于不到一秒的移动都被它抹去了。
我们需要清理一下。一个常见的做法是通过互补滤波器,将新值与旧值结合以过滤掉快速振动噪声。我们将创建这样的滤波器,但这会使采样变慢。
让我们检查一下这是否有效。
故障排除
如果它还没有完全工作,让我们尝试一些修复方法,如下所示:
-
如果非常嘈杂,尝试更剧烈的转弯,并尽量保持双手稳定。这张图会因为加速度计本身的特性而嘈杂。
-
如果你看到图表在0-90度范围外断裂或行为异常,请确保你正在使用
atan2函数——这个函数在大多数编程语言中执行三角函数的 CAST 规则。 -
注意,
read_accelerometer_pitch_and_roll方法在atan2函数前有一个负号。 -
如果在180度处出现问题——这是这个系统已知且预期的问题——尽量避免触及这一点。
现在,我们有了俯仰和滚转,但它们相当粗糙——一个合适的修复方法是结合传感器通过滤波器。我们还有一个传感器,它给我们一个综合的俯仰和滚转值:陀螺仪。
平滑加速度计
我们可以将我们对积分陀螺仪和加速度计的知识结合起来,以实现平滑的组合。
由于我们将更多地使用时间差概念,一个帮助的类将节省我们以后的工作。
时间差
我们之前看到如何跟踪绘图所需的时间差和更新之间的时间差以进行积分。让我们创建以下代码来帮助:
-
创建一个
delta_timer.py文件,并首先导入time,如下所示:import time -
我们将创建一个
DeltaTimer类来跟踪事物,如下所示:last and start variables with the current time. -
这里的核心是一个
update方法。每个循环都会调用这个方法;让我们先获取当前时间,如下所示:def update(self): current = time.time() -
时间差将是当前时间和上次时间之间的差值,如下面的代码片段所示:
dt = current - self.last -
经过的时间是当前时间和开始时间之间的差值,如下面的代码片段所示:
elapsed = current - self.start -
现在,我们需要更新最后的时间以获取时间差并返回这些部分,如下所示:
self.last = current return dt, elapsed
我们可以在需要用于绘图的时间差和经过的时间时使用这个类。让我们先使用它来组合加速度计和陀螺仪。
融合加速度计和陀螺仪数据
通过组合传感器,我们可以让每个传感器补充对方的弱点。加速度计作为俯仰角和横滚角的绝对测量值,以抵消陀螺仪看到的漂移。陀螺仪不像加速度计那样经历相同的噪声,但可以进行快速测量。让我们看看如何在以下图中将它们组合起来:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_10.jpg
图16.10 – 陀螺仪和加速度计融合数据流
图16.10 展示了数据流图,使用互补滤波器融合陀螺仪和加速度计数据。以俯仰角为例。首先,系统将陀螺仪数据和时间差输入到积分器中。积分器将这个值加到之前的位置上。然后我们可以用这个值的95%来解释较大的运动变化。滤波器的其他5%是加速度计的测量值。这5%将在滤波过程中将测量值拖到平均加速度计读数,同时滤除混沌噪声元素。输出是一个滤波后的俯仰角或横滚角,然后反馈到积分器进行下一周期。
让我们将这些放入代码中,从滤波器开始,如下所示:
-
打开
robot_imu.py。 -
添加
ComplementaryFilter类,如下所示:class ComplementaryFilter: -
我们可以使用左侧的值构建这个滤波器,存储这个值并计算其补数(左侧减一)来形成右侧,如下所示:
def __init__(self, filter_left=0.9): self.filter_left = filter_left self.filter_right = 1.0 - filter_left -
这个类有一个
filter方法,它接受两个值并使用滤波器值将它们结合起来,如下所示:def filter(self, left, right): return self.filter_left * left + \ self.filter_right * right这样就完成了滤波器。
-
下一步,我们想要的是通过滤波器组合IMU传感器的代码 – 将它们融合在一起。我们将在
robot_imu.py中添加一个类来实现这一点,如下所示:class ImuFusion: -
在构造函数中,我们将存储
RobotImu实例,创建一个滤波器,并初始化俯仰角和横滚值,如下所示:def __init__(self, imu, filter_value=0.95): self.imu = imu self.filter = ComplementaryFilter(filter_value).filter self.pitch = 0 self.roll = 0 -
代码的核心部分是一个
update函数。该函数接受一个dt(时间差)参数。它不会返回任何内容,只是更新俯仰/滚转成员,如下所示:def update(self, dt): -
我们首先从加速度计获取俯仰和滚转值,如下所示:
accel_pitch, accel_roll = self.imu.read_accelerometer_pitch_and_roll() -
我们还想要陀螺仪的值,所以我们运行以下命令:
gyro = self.imu.read_gyroscope() -
现在,我们将陀螺仪的y读数和加速度计的俯仰值结合起来得到俯仰值,如下所示:
self.pitch = self.filter(self.pitch + gyro.y * dt, accel_pitch)注意这里来自先前数据流的乘法和加法操作。
-
我们对滚转也做同样的处理,如下:
self.roll = self.filter(self.roll + gyro.x * dt, accel_roll)
现在,我们已经准备好了带有滤波器的RobotImu类,并融合了传感器。让我们用以下代码进行一次图形测试:
-
在
plot_pitch_and_roll.py文件中,我们将添加DeltaTimer、ImuFusion和陀螺仪校准导入。注意以下代码片段中已经移除了import time:import vpython as vp from robot_imu import RobotImu, ImuFusion from delta_timer import DeltaTimer import imu_settings -
接下来,我们使用陀螺仪设置设置
RobotImu,然后创建fusion实例,如下代码片段所示:imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) fusion = ImuFusion(imu) -
我们需要一个
dt(时间差)用于融合计算和一个用于图形的已过时间。DeltaTimer类提供了这些。我们在循环开始前将其放在这里,替换了start的赋值,如下所示:timer = DeltaTimer() -
现在,在计算
elapsed的循环中,我们使用delta定时器,如下所示:while True: vp.rate(100) dt, elapsed = timer.update() -
现在,用代码替换加速度计的读数,以更新融合的delta时间,使其进行计算,如下所示:
fusion.update(dt) -
我们现在可以从
fusion对象中获取俯仰和滚转值,如下所示:graph_pitch.plot(elapsed, fusion.pitch) graph_roll.plot(elapsed, fusion.roll) -
将
robot_imu.py、delta_timer.py和plot_pitch_and_roll.py上传到机器人。 -
再次运行
vpython plot_pitch_and_roll.py,并将你的浏览器指向机器人的9020端口。
表面上,它应该看起来与图16.9中的加速度计俯仰和滚转图相似。然而,当你移动机器人时,你应该注意到噪声少得多——图形更平滑——并且当你放下机器人或保持它静止时,它会水平。它应该能够快速处理快速转弯。系统平稳且准确!
故障排除
如果你有问题,尝试以下故障排除检查:
-
总是如此,如果你看到语法错误或异常行为,请仔细检查代码。
-
如果出现异常移动,确保你使用的是
0.95(而不是95)作为滤波器值。 -
确保你已经上传了所有文件。
-
这个系统在图形开始后需要一两秒钟才能稳定下来。
你现在已经看到了如何从这些传感器获取准确和平滑的俯仰和滚转。一个轮式机器人可能不会遇到很多需要使用俯仰和滚转的原因,但其中之一将是使指南针工作。让我们进一步探讨磁力计。
使用磁力计检测航向
我们在 第12章,使用Python进行IMU编程中看到了如何从磁力计绘制向量,以及磁性金属(如钢和铁的碎片)如何干扰它。甚至IMU板上的引脚头也会干扰。我们可以校准以补偿这一点。
获取 X,Y 和 Z 分量并不那么有用;我们想要一个相对于磁北的航向。我们可以看到如何使用这个来进行精确的转向。
这个部分需要留出空间,因为磁铁非常少。笔记本电脑、手机、扬声器和磁盘驱动器会干扰这个传感器。使用指南针来揭示你空间中的磁场。我建议将机器人的支架茎尽可能长,允许的话多放一些支架;机器人的电机本身就有很强的磁场。
请避免以机器人面向南方开始——这会导致一些奇怪的结果,我们将在以后进行调查和修复。以机器人大致面向北方开始是个好主意。
校准磁力计
我们将执行一个称为硬铁偏移计算的校准。硬铁指的是任何与磁力计一起移动的磁性物体。我们将机器人移动到各个轴上来采样场强。我们将使用轴上所有读数的中间值来进行补偿,并将其添加到IMU设置中;这看起来与陀螺仪校准相似,但需要你移动机器人。
让我们编写代码,如下所示:
-
创建一个名为
magnetometer_calibration.py的文件,从导入和RobotImu设置开始,如下所示:import vpython as vp from robot_imu import RobotImu imu = RobotImu() -
我们将找到最小和最大向量,就像我们在陀螺仪中做的那样,如下面的代码片段所示:
mag_min = vp.vector(0, 0, 0) mag_max = vp.vector(0, 0, 0) -
我们将展示系统作为一组带有彩色点集群的三维散点图。这三个集群中的每一个都是一个结合了两个轴的图表:xy,yz和zx。我们的目标是通过对设备进行校准使这些集合对齐,如下所示:
scatter_xy = vp.gdots(color=vp.color.red) scatter_yz = vp.gdots(color=vp.color.green) scatter_zx = vp.gdots(color=vp.color.blue) -
我们开始主循环并读取磁力计,如下所示:
while True: vp.rate(100) mag = imu.read_magnetometer() -
现在,我们以与陀螺仪相同的方式更新最小值,如下所示:
mag_min.x = min(mag_min.x, mag.x) mag_min.y = min(mag_min.y, mag.y) mag_min.z = min(mag_min.z, mag.z) -
然后,我们以同样的方式更新最大值,如下所示:
mag_max.x = max(mag_max.x, mag.x) mag_max.y = max(mag_max.y, mag.y) mag_max.z = max(mag_max.z, mag.z) -
我们随后以与陀螺仪相同的方式计算偏移量,如下所示:
offset = (mag_max + mag_min) / 2 -
这个
print语句显示了当前值和偏移量:print(f"Magnetometer: {mag}. Offsets: {offset}.") -
现在,我们创建图表。它们将指导你获取足够的校准数据,并显示轴不对齐的地方。代码如下所示:
scatter_xy.plot(mag.x, mag.y) scatter_yz.plot(mag.y, mag.z) scatter_zx.plot(mag.z, mag.x) -
上传此文件并使用VPython运行它。你应该看到以下屏幕:https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_11.jpg
图16.11 – 初始磁力计校准屏幕
图16.11 展示了集群作为三个彩色块——右下角是红色(代表 xy),顶部是蓝色(代表 yz),右边是绿色(代表 zx)。这些集群在你这里将从一个不同的位置开始,这取决于你机器人的方向。
-
你需要移动机器人,慢慢地围绕整个 y 轴(围绕轮子)旋转。绿色图表应该更像一个椭圆,如下面的截图所示!
图16.12 – 磁力计部分校准
图16.12 展示了绿色值的椭圆,以及红色和蓝色散点图的更多数据。你越慢,数据越好。
-
将机器人绕 x 轴(机器人的长度)旋转,然后绕 z 轴(其高度)旋转。你移动的角度越多,越好。通过制作 8 的三维图形来填补空白。最终,它应该看起来像以下截图中的图形!
图16.13 – 磁力计校准:良好的组合
图16.13 展示了良好的数据集合应该看起来像什么,有红色、绿色和蓝色的圆圈。注意,由于机器人太靠近其他磁铁而摆动,存在异常值——小心这些!
-
你现在可以关闭浏览器了,因为你已经收集了大量校准数据。
-
控制台将显示校准偏移量,如下所示:
Magnetometer: <30.6, -36.9, 10.35>. Offsets: <21.15, 3.15, 0.225>. Magnetometer: <31.5, -36.75, 11.25>. Offsets: <21.15, 3.15, 0.225>.开始时,这些偏移量变化很大;然而,随着你收集更多数据,它们将稳定下来,即使磁力计读数在变化。
我们现在有一些校准数字;我的例子给出了 21.15, 3.15, 0.225。让我们确保每个人都有一组值。
故障排除
这种校准可能不起作用——让我们看看原因如下:
-
如果数字看起来没有稳定下来,继续移动机器人。你必须尝试用它进行完整的 360 度运动以获得完整的范围。
-
如果奇怪的点出现在圆圈之外,请移动到其他地方并重新开始校准——这很可能是你测试时的磁场,并会影响你的结果。
-
有可能你的浏览器在尝试这样做时会变慢或耗尽内存——虽然我说要慢慢来,但你不能在运行时将其搁置一边,因为它会继续添加点。
-
如果你完全没有得到圆圈——线条或小块——请确保你有正确的 xy、yz 和 zx 绘图组合。
你现在应该正在获取校准偏移量。让我们使用这些值来对齐散点图。
测试校准值
为了看看这些是否有效,我们将它们放回代码中,并且相同的操作应该显示点簇对齐。它首先允许我们在 RobotImu 接口中设置偏移量。按照以下步骤进行:
-
打开
robot_imu.py文件。 -
在
__init__方法中,我们需要存储偏移量。我已经突出显示了新的代码,如下所示:def __init__(self, gyro_offsets=None, mag_offsets=None): self._imu = ICM20948() self.gyro_offsets = gyro_offsets or vector(0, 0, 0) self. mag_offsets = mag_offsets or vector(0, 0, 0) -
read_magnetometer方法需要减去磁力计偏移量,如下所示:def read_magnetometer(self): mag_x, mag_y, mag_z = self._imu.read_magnetometer_data() return vector(mag_x, -mag_z, -mag_y) - self. mag_offsets
我们现在可以将磁力计的偏移量包含在我们的脚本中。我们将把这些放在我们用于陀螺仪校准的相同设置文件中。按照以下步骤进行:
-
打开
imu_settings.py。 -
按照以下方式添加你的机器人上的磁力计校准读数:
from vpython import vector gyro_offsets = vector(0.557252, -0.354962, -0.522901) mag_offsets = vector(21.15, 3.15, 0.225) -
现在,我们可以在散点图中使用它们。打开
magnetometer_calibration.py,并将我们的设置添加到导入中,如下所示:import vpython as vp from robot_imu import RobotImu from imu_settings import mag_offsets -
当我们创建了我们的
RobotImu后,我们可以应用偏移量,如下所示:imu = RobotImu(mag_offsets=mag_offsets) -
将文件发送到机器人,并重新运行
magnetometer_calibration.py。你需要再次进行旋转和8字形图样,以在不同方向上获得许多样本点。当你收集了数据后,你应该有重叠的圆圈,如下面的截图所示:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_14.jpg
图16.14 – 校准磁力计
图16.14显示了叠加的红色、蓝色和绿色圆圈。恭喜你——你已经校准了你的磁力计!
使用你的校准磁力计,我们可以尝试进行更多有用值的进一步实验。但首先,让我们排除任何问题。
如果圆圈没有在一起,应该怎么做
你可能已经到达了这个点,但圆圈没有收敛。如果出现这种情况,请尝试以下故障排除步骤:
-
你需要重新运行校准代码。在这样做之前,请注释掉应用于
RobotImu类的偏移量应用(设置)行。在偏移量激活时运行校准代码会导致它偏移错误。 -
仔细检查你的校准和IMU代码,确保没有错误。
-
确保在机器人附近没有强磁铁或大块金属——例如扬声器或硬盘。尽量在离这些物体大约一米的地方做这个。甚至你的笔记本电脑或手机也可能产生干扰。
-
确保你缓慢地通过每个轴,并尝试8字形图样。继续这样做,直到你能看到三个椭圆。
-
你可以使用控制台输出,旋转机器人,并在每个方向上移动,然后看看这里输出的偏移值是否稳定。
-
当输出稳定后,再次尝试应用偏移量,并运行校准以查看圆圈是否收敛。
经过这些步骤后,你应该有了继续所需的校准值。让我们将其放回我们已有的矢量输出中,并确定航向。
从磁力计获取粗略航向
现在我们已经获得了校准设置,我们可以开始使用磁力计读数来估计北方在哪里,就像指南针一样。航向和yaw意味着同一件事——相对于参考点的方向——在这种情况下,是磁北。让我们看看我们如何做到这一点。看看下面的截图:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_15.jpg
图16.15 – 从磁力计获取近似航向
图16.15显示了我们将构建的方法。它使用校准数据应用了磁力计,并使用atan2,就像我们使用陀螺仪来近似航向一样。我们也可以添加一个粗略的指南针。
让我们这样做:
-
创建一个
plot_mag_heading.py文件。从导入开始,如下所示:import vpython as vp from robot_imu import RobotImu from delta_timer import DeltaTimer import imu_settings -
我们可以用设置初始化
RobotImu,如下所示:imu = RobotImu(mag_offsets=imu_settings.mag_offsets) -
要制作指南针显示,我们需要一个红色刻度盘(圆柱体)和指针(箭头),如下所示:
vp.cylinder(radius=1, axis=vp.vector(0, 0, 1), pos=vp.vector(0, 0, -1)) needle = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) -
接下来,让我们制作一个图表来显示航向,以及一个用于显示经过时间的delta计时器,如下所示:
vp.graph(xmin=0, xmax=60, scroll=True) graph_yaw = vp.gcurve(color=vp.color.blue) timer = DeltaTimer() -
我们以速率启动主循环并获取经过时间,如下所示:
while True: vp.rate(100) dt, elapsed = timer.update() -
现在,我们通过运行以下命令来读取磁力计:
mag = imu.read_magnetometer() -
我们可以取xy平面,并找到这些值的
atan2函数以获得航向,如下所示:yaw = -vp.atan2(mag.y, mag.x) -
然后,我们以度为单位在图表上绘制这个,如下所示:
graph_yaw.plot(elapsed, vp.degrees(yaw)) -
我们还可以使用
sin/cos将针轴设置为我们的方向,将其转换回一个单位方向,如下所示:needle.axis = vp.vector(vp.sin(yaw), vp.cos(yaw), 0) -
保存,上传并在VPython中运行此代码。将你的浏览器发送到机器人的
9020端口。 -
如果你围绕机器人旋转,你会看到如下显示:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_16.jpg
图16.16 – 磁力计航向估计
图16.16显示了一个指南针,顶部是机器人感知的北方,以及一个红色箭头。下面是一个蓝色图表,范围在+和-180度之间。当你移动机器人时,你会看到这个移动,0度是北方。不过,你需要确保机器人位于一个平坦的表面上。
注意,指南针是读取相对于机器人的北方,而不是机器人相对于北方的位置!
这个输出开始看起来合理。它可以指向北方并进行一些指南针测量,我们有一个航向。
这有点混乱,任何俯仰或横滚都可能使其错误。再次,通过将此数据与其他传感器的数据融合,我们可以改进这一点。
结合传感器进行定位
我们已经看到如何将加速度计和陀螺仪结合起来,以获得平稳的俯仰和横滚读数。我们还可以再次结合这些传感器,以正确地定位并平滑磁力计的读数。这个系统使我们能够近似机器人的绝对方向。
查看以下数据流以了解我们在做什么——它是基于之前阶段的:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_17.jpg
图16.17 – 融合所有三个传感器
图16.17从左侧开始,显示我们之前阶段的数据。我们有灰色的滤波俯仰和横滚,因为它也是一个输出。有校准后的陀螺仪偏航、delta时间和校准后的磁力计作为输入。滤波后的俯仰和横滚通过倾斜补偿框,在那里我们旋转磁力计向量。磁力计数据然后通过一个xy-到极坐标框,使用atan2函数来获得航向。
在此之上,校准后的陀螺仪偏航和delta时间进入一个积分器,它将添加到之前的偏航读数。积分器和磁力计航向输出进入一个互补滤波器,其中积分器的输出占主导地位。这个滤波器的输出是一个航向/偏航输出,它将稳定且快速响应,并返回到绝对航向。我们现在有三个角度——俯仰、横滚和偏航!
让我们修改代码来实现这一点,如下所示;
-
打开
robot_imu.py并转到ImuFusion类。 -
我们需要将其转换回弧度,因此我们需要将此添加到从VPython导入的内容中,如下所示:
from icm20948 import ICM20948 from vpython import vector, degrees, atan2, radians -
在
__init__方法中,我们应该添加一个变量来存储偏航,如下所示:def __init__(self, imu, filter_value=0.95): self.imu = imu self.filter = ComplementaryFilter(filter_value).filter self.pitch = 0 self.roll = 0 self.yaw = 0我们现在将使用相同的滤波器。
-
在
update方法中,在计算俯仰和滚转之后,添加以下行以获取磁力计读数:mag = self.imu.read_magnetometer() -
mag变量是一个向量。我们使用俯仰和倾斜来旋转这个向量,以使xy分量水平,如下所示:mag = mag.rotate(radians(self.pitch), vector(0, 1, 0)) mag = mag.rotate(radians(self.roll), vector(1, 0, 0)) -
我们现在可以从这个计算磁力计偏航,如下所示:
mag_yaw = -degrees(atan2(mag.y, mag.x)) -
为了稳定它,我们现在可以使用互补滤波器与陀螺仪,如下所示:
self.yaw = self.filter(self.yaw + gyro.z * dt, mag_yaw)
self.yaw值现在将具有补偿后的偏航(或航向)值,使这个IMU可以作为指南针使用。为了使用它,让我们以三种方式可视化它——作为图形、指南针和机器人的运动。按照以下步骤进行:
-
将此代码放入一个名为
visual_fusion.py的新文件中。代码将非常熟悉。只有磁力计偏移量和偏航值是新的。导入如下代码片段所示:import vpython as vp from robot_imu import RobotImu, ImuFusion from delta_timer import DeltaTimer import imu_settings import virtual_robot -
准备带有磁力计偏移量的
RobotImu,并初始化fusion,如下所示:imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets, mag_offsets=imu_settings.mag_offsets) fusion = ImuFusion(imu) -
我们将使用一个VPython画布来表示虚拟机器人,并为指南针使用一个单独的画布。每个画布让我们包含一个3D场景。让我们将当前画布设置为机器人视图并将其放在左侧。机器人模型将与这个视图相关联。代码如下所示:
robot_view = vp.canvas(align="left") model = virtual_robot.make_robot() virtual_robot.robot_view() -
为了配合机器人视图,我们将创建一个
指南针画布,使用与之前相同的圆柱和箭头。请注意,最新的画布与之后创建的形状相关联。代码如下所示:compass = vp.canvas(width=400, height=400) vp.cylinder(radius=1, axis=vp.vector(0, 0, 1), pos=vp.vector(0, 0, -1)) needle = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) -
按如下设置俯仰、滚动和偏航的图形:
vp.graph(xmin=0, xmax=60, scroll=True) graph_roll = vp.gcurve(color=vp.color.red) graph_pitch = vp.gcurve(color=vp.color.green) graph_yaw = vp.gcurve(color=vp.color.blue) -
创建一个delta计时器,启动循环,并获取时间更新,如下所示:
timer = DeltaTimer() while True: vp.rate(100) dt, elapsed = timer.update() -
现在,我们使用时间更新
fusion(它将读取IMU并执行计算),如下所示:fusion.update(dt) -
现在,在我们旋转它之前,我们需要重置虚拟机器人模型,如下所示:
model.up = vp.vector(0, 1, 0) model.axis = vp.vector(1, 0, 0) -
然后,我们需要执行三个旋转——滚动、俯仰和偏航,如下所示:
model.rotate(angle=vp.radians(fusion.roll), axis=vp.vector(1, 0, 0)) model.rotate(angle=vp.radians(fusion.pitch), axis=vp.vector(0, 1, 0)) model.rotate(angle=vp.radians(fusion.yaw), axis=vp.vector(0, 0, 1)) -
我们定位指南针针——注意我们的偏航是以度为单位,因此我们需要将其转换,如下所示:
needle.axis = vp.vector( vp.sin(vp.radians(fusion.yaw)), vp.cos(vp.radians(fusion.yaw)), 0) -
然后,我们绘制三个图形轴,如下所示:
graph_roll.plot(elapsed, fusion.roll) graph_pitch.plot(elapsed, fusion.pitch) graph_yaw.plot(elapsed, fusion.yaw) -
将
robot_imu.py和visual_fusion.py上传到机器人。首先使用vpython visual_fusion.py,并将你的浏览器指向机器人的9020端口。
你应该能看到可视化的机器人、指南针以及所有三个轴的图形显示,每个都应该既相对稳定又响应迅速,如以下截图所示:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_18.jpg
图16.18 – 俯仰、滚动和偏航图形
图16.18中的图形是显示的截图。左上角是虚拟机器人——你可以通过右键点击来更改其视图。左上角显示指南针。下面是滚转、俯仰和偏航的滚动图形。滚转用红色表示,俯仰用绿色表示,偏航用蓝色表示。图形最初会稳定,然后匹配你的机器人运动。当在一个轴上移动时,对其他轴有轻微的影响,但它们可以独立移动。
在±180度时,图表会表现不佳。让我们看看如何解决这个问题。
解决180度问题
需要认识到的是,圆上的角度是循环的;200度和-160度是等效的,-180度和180度也是相等的。我们没有让过滤器或代码意识到这一点,所以当我们达到180度点,atan2函数在-179.988和179.991(或一些非常接近的标记)之间翻转时,图表变得混乱,将小于1度的差异视为360度,然后尝试在这两者之间进行过滤。
为了解决这个问题,我们需要进行一些更改。首先,我们可以声明我们希望角度数值在-180度以下和180度以上,并以这种方式约束它们。由于我们打算使用角度的互补滤波器,我们可以对其进行特殊化,如下所示:
-
在
robot_imu.py的顶部,在ComplementaryFilter类内部,让我们添加一个格式化角度的方法,如下所示:@staticmethod def format_angle(angle): -
如果角度低于-180度,我们希望通过加上
360将其绕回,如下所示:if angle < -180: angle += 360 -
如果角度大于180度,我们将通过减去
360将其绕回,如下所示:if angle > 180: angle -= 360 return angle -
我们将用某种更智能地约束这些角度的方法来替换
filter函数的内部。当我们进行过滤时,我们首先按照以下方式格式化传入的角度:def filter(self, left, right): left = self.format_angle(left) right = self.format_angle(right) -
我们还希望将过滤后的角度放在相同的范围内。如果差异超过350度,我们可以假设某些东西已经绕回;因此,我们将最低的一个加上360度来过滤它们,如下所示:
if left - right > 350: right += 360 elif right - left > 350: left += 360 filtered = self.filter_left * left + \ self.filter_right * right -
这个操作可能会得到一个超出范围的答案。因此,我们将其格式化回,如下所示:
return format_angle(filtered) -
这个过滤器已经在使用中,所以我们可以重新运行
visual_fusion.py并尝试再次通过180度。当你将浏览器指向端口时,在稳定后,那里的机器人应该会随着你的旋转而旋转——并且稳定,不会漂移!
注意,这个系统在启动时处理面向南方的情况仍然不太好。我们已经至少解决了系统的一个问题,并平滑了其缺陷。
这种行为很令人兴奋:你现在可以在屏幕上得到一个机器人来镜像你的旋转。然而,尽管在屏幕上移动很有趣,但我们希望看到它在现实世界中得到应用。让我们启动一些电机!
从IMU数据驱动机器人
在前面的章节中,我们看到了如何使用PID算法,在本章中,我们将学习如何从磁力计检测俯仰、滚转和偏航。我们的机器人不能改变其俯仰或滚转,但它可以改变其航向。
在这个演示中,我们将让机器人保持在航向上——尝试无论我们将其转向何方都能追踪北方。让我们看看如何。看一下以下图表:
https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_16_19.jpg
图16.19 – 航向驱动行为
图16.19显示了数据流。图的左侧从测量的航向开始,航向设定点进入PID——误差值将是两者的差值。测量的航向来自IMU + 融合算法。我们使用PID输出来驱动电机,使它们以固定速度加减该值移动,这样机器人就会转向以减少误差。机器人的移动将反馈到IMU + 融合算法中,通过PID循环。
让我们使用前面的流程来构建代码,如下所示:
-
以以下导入开始
drive_north_behavior.py文件:from robot_imu import RobotImu, ImuFusion from delta_timer import DeltaTimer from pid_controller import PIController from robot import Robot import imu_settings -
我们现在初始化
RobotImu、fusion和DeltaTimer,如下所示:imu = RobotImu(mag_offsets=imu_settings.mag_offsets, gyro_offsets=imu_settings.gyro_offsets) fusion = ImuFusion(imu) timer = DeltaTimer() -
我们可以设置PID(或PI)控制器和机器人,如下所示:
pid = PIController(0.7, 0.01) robot = Robot() -
然后,几个常数——机器人的基本速度,以及从北方起度的航向设定点,如下面的代码片段所示:
base_speed = 70 heading_set_point = 0 -
这里的主要循环更新计时器和IMU融合。注意在下面的代码片段中,这里没有视觉速率:
while True: dt, elapsed = timer.update() fusion.update(dt) -
我们现在计算误差,并将该误差和delta时间输入PID,如下所示:
heading_error = fusion.yaw - heading_set_point steer_value = pid.get_value(heading_error, delta_time=dt) -
我们打印值以进行调试,并设置电机速度,如下所示:
print(f"Error: {heading_error}, Value:{steer_value:2f}, t: {elapsed}") robot.set_left(base_speed + steer_value) robot.set_right(base_speed - steer_value)
将此上传到机器人,打开电机,用常规Python 3运行。机器人将尝试向北行驶。如果你偏离航线,它将纠正回北方,而你转得越多,电机尝试转回的速度就越快。玩这个行为相当有趣!
完成后按 Ctrl + C 停止,并尝试不同的航向设定点。
在本节中,你通过构建数据流图和从它们编写代码来巩固了这些技能。你通过将传感器数据转换为这样的数字,展示了如何使用它构建基于PID的行为。然后你使用了我们计算出的航向,并用它和PID一起创建从你的机器人来的基于指南针的运动。
摘要
在本章中,你看到了如何将IMU传感器组合起来以近似空间中的绝对方向。你看到了如何在图表中呈现这些信息,以及如何使用虚拟机器人将它们显示在屏幕上。然后你看到了如何将这个传感器系统连接到PID控制器和电机,以使机器人行驶。
你已经学到了一些必要的数学知识,用于在3D空间中转换向量分量和角度,以及如何使用互补滤波器来补偿一个系统中的噪声和另一个系统中的漂移。你已经开始看到多个传感器融合在一起,以对世界做出推断。你的框图和数据流技能得到了锻炼,你在PID算法方面也有了更多的实践。
在下一章中,我们将探讨如何使用智能手机控制你的机器人,并从菜单中选择行为。
练习
这里有一些想法可以帮助你进一步理解,并给你一些关于如何使用本章概念做更有趣的事情的想法:
-
读者可以使用更多颜色和复杂的形状来制作更好的机器人模型。这并非本章的目的,但这是一个有趣且有益的方法,可以让你更熟悉VPython。
-
我们的磁力计设置是硬编码的,直接写入Python文件中。从数据文件中加载设置是一种良好的实践。一个良好的起点可以在http://zetcode.com/python/yaml/找到。
-
能否使用视觉机器人来显示或调试其他传感器和集成?
-
你能否将这里的绝对定位与编码器结合起来,以实现非常精确的转弯的方形?
进一步阅读
关于本章涵盖的主题的更多信息,请参考以下内容:
-
万维网联盟(W3C)有一份关于浏览器中磁力计设备的指南,它不仅对技术进行了有趣的阅读,还介绍了智能手机上的代码可能如何执行这些相同的算法以获取手机方向:https://www.w3.org/TR/magnetometer.
-
我多次提到了
atan2函数;这个页面有关于它的更多信息:https://en.wikipedia.org/wiki/Atan2. -
我推荐保罗·麦克沃特(Paul McWhorter)的基于IMU的Arduino实验,以及他对VPython的介绍——他的指南是本书研究过程中的一个关键部分:https://toptechboy.com/arduino-based-9-axis-inertial-measurement-unit-imu-based-on-bno055-sensor/.
-
本文将内容进一步拓展,引入了一个全球定位系统(GPS)以实现更进一步的传感器融合:https://www.researchgate.net/publication/51873462_Data_Fusion_Algorithms_for_Multiple_Inertial_Measurement_Units.
-
如果你想深入了解传感器融合,以及如何在滤波误差的同时将它们结合起来的算法,卡尔曼滤波器是最佳选择。这篇文章是一个起点:https://towardsdatascience.com/sensor-fusion-part-1-kalman-filter-basics-4692a653a74c.
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)