原文:Beginning Robotics with Raspberry Pi and Arduino Using Python and OpenCV
协议:CC BY-NC-SA 4.0
七、组装机器人
在最后一章中,我们制作了 Adafruit 电机帽,这是一个电子设备,允许你用你的树莓派 控制多达四个 DC 电机。我们还研究了一个通用电机控制器,我们在 Arduino 板上运行了这个控制器。现在你知道如何让你的机器人移动,让我们开始建造它。
在这一章中,我们将建造我们的机器人。在这个过程中,我会给出一些我在构建中获得的提示和指示。组装机器人时有很多小事要考虑。你会遇到一些你没有考虑到的奇怪情况。最容易被忽视的是布线和电线管理。像操作顺序和组件放置这样的事情非常重要。在构建早期做出的决策可能会导致以后的复杂化。注意这些事情可以帮助你避免拆卸你的机器人来纠正你早期犯下的错误。
该建筑分为四个独立练习。我们将从构建 Whippersnapper 底盘套件开始。然后我们将安装电子设备,接着是电线。最后,我们来看看如何安装超声波传感器。在每个练习中,我将指出在构建自己的版本时需要考虑的一些事情。
组装底盘
对于这个构建,我选择使用一个商业上可用工具包。套件的好处在于,一个好的套件包含了您开始使用所需的一切。有许多不同价位和不同制造商的产品可供选择。许多低成本的工具包,通常在网上从外国卖家那里找到,不如其他的完整。通常,这些是流行设备的套件,但组装时很少考虑零件如何搭配。所以,如果你打算买一套工具,确保它有所有的硬件,并且零件设计成可以一起工作。
选择材料
选择机箱时,材料是另一个需要考虑的因素。金属底盘不错。它往往比塑料机箱更贵,但也更耐用。就塑料套件而言,请记住,并非所有塑料都是相同的。
丙烯酸是一种便宜且使用方便的材料;然而,它不是大多数应用的合适材料。丙烯酸易碎、不灵活,并且容易刮伤。当它断裂时,通常是锋利的碎片。记住不要在任何类型的高摩擦应用中使用丙烯酸树脂也是明智的,因为它容易分解成粗颗粒,从而放大摩擦。
如果你要用塑料,ABS 是更好的材料。像丙烯酸树脂一样,ABS 也是片状的,而且相当便宜。不像压克力,它更耐用。它不容易开裂或断裂,而且更耐刮擦。ABS 是可钻孔的,比亚克力更容易加工。
另一种选择是聚苯乙烯。苯乙烯是用于塑料模型套件的材料。所以,如果你熟悉使用这些工具,那么苯乙烯是一个简单的选择。它比丙烯酸树脂或 ABS 更柔韧。它往往比其他的贵一点,但是很容易操作。
自以为是的年轻人
Whippersnapper 是一款由激光切割 ABS 板材制成的商用套件。它是 Actobotics 的 Runt Rover 系列的一部分,由 ServoCity 制造。我曾经使用过 Actobotics 系列的几个套件,我知道它们是设计精良的优质产品。除了机器人套件之外,他们还生产一系列可以协同工作的零件。
所有这些都有助于选择自以为是者(见图 7-1 )作为该项目的基地。它是一个好看的机箱,有空间容纳所有的电子产品,并留有一些增长空间,这没有什么坏处。
为了清楚起见,树莓派将安装在机器人的后部。Arduino 会在最前面。这将使布线稍微容易一些。
图 7-1
All the Whippersnapper parts
首先,我想展示一下各个部分。这有助于你确保所有的东西都在那里,并让你熟悉所有的部分。这套工具是按扣合在一起的。事实上,你唯一需要的工具是十字螺丝刀和尖嘴钳。将零件扣合在一起时,要注意配合很紧,需要一些力才能将所有零件合在一起。只要你保持这些部件笔直,它们就不会坏。牢牢抓住零件,均匀施加压力。
- Attach the center support to one of the sides. Make sure that the course side is facing out. Take note of the tabs on the center support. The single pair of tabs attaches to the bottom plate (see Figure 7-2).
图 7-2
Center support attached to an outer plate
- 将第二块侧板安装到中心支架上。再一次,确保路线在机器人的外面。
- Snap the top plate to the assembly. There are six sets of tabs that snap to the top plate (see Figure 7-3).
图 7-3
Top plate added
在接下来的步骤中,我们连接电机。电机的一侧是一个小销钉(见图 7-4 ),它有助于对齐电机并将其保持在适当的位置。
- 安装电机,使轴穿过下面的孔,销钉进入第二个孔。
- Use two screws and nuts to hold the motor in place (see Figure 7-5). Although not included in the kit, some #4 split lock washers would be good to use here. If you don’t have any, use Loctite Threadlocker Blue on the nuts. Without something to lock them into place, the nuts will rattle off.
图 7-5
Mounted motor
- Repeat the process for each of the three remaining motors (see Figure 7-6).
图 7-6
All motors mounted
- Flip the chassis over and attach the bottom plate. There are five sets of tabs holding the bottom plate on (see Figure 7-7).
图 7-7
Bottom plate added
- Feed the wires for each motor into the chassis through the hole behind the motor (see Figure 7-8). This bit of housekeeping keeps the wires from getting tangled in the wheels or caught onto something.
图 7-8
Motor wires fed through the hole behind the motor
- 将电子夹固定在顶板上。这些夹子将用于固定树莓酱。
- 将前马达的电线穿过中心支撑板上的孔。
图 7-4
Motor with tab
现在,机箱已准备好安装电子设备。你的机器人底盘应该如图 7-9 所示。
图 7-9
The completed Whippersnapper
安装电子设备
接下来,我们将把电子设备安装到底盘上。从 Raspberry Pi 开始,我们将连接每个组件,Arduino 和试验板安装在前面。
在这部分制作过程中,经常使用安装胶带和拉链。板子的位置由你决定。有些人将一些电子设备安装在机箱内。然而,我发现下面的安排对我最有效。它可以更容易地接触到电子设备,并为额外的组件节省内部空间。
- Snap the Raspberry Pi into the clips on the top plate (see Figure 7-10). The Pi should be held firmly in place by the top barbs.
图 7-10
Raspberry Pi mounted in the clips The tabs that hold the chassis together (see Figure 7-11) make mounting the Arduino and breadboard a challenge. This is one reason I like to use foam mounting tape—it provides some padding. To clear the tabs, we’ll need to double up on the tape.
图 7-11
Clip protruding from the top plate
- Stack two pieces of foam tape on top of each other and place them on the top plate. Use a second set of stacked foam tape to form a T (see Figure 7-12). This adds stability.
图 7-12
Double layer of mounting tape for the breadboard
- Remove the protective paper from the bottom of the breadboard and press the breadboard firmly into the T-shaped tape on the top plate (see Figure 7-13).
图 7-13
Mounted breadboard . Note that the T-cobbler has been moved forward to allow room for the power pack.
- Repeat the procedure for the Arduino (see Figure 7-14).
图 7-14
Arduino mounted on a double layer of mounting tape When mounting the Arduino, remember to leave room for the USB cable. I offset the Arduino from the center so that the USB plug is clear of the Raspberry Pi (see Figure 7-15).
图 7-15
Leaving clearance for the USB cable
- 将 4 节 AA 电池盒安装在背面的机箱内。如果适用的话,确保安装时能够接触到电池和电源开关。我用泡沫胶带来固定我的。
- 找到一个安全安装 5V 电源组的地方。我发现试验板和树莓皮之间的空间对我使用的小型电源组来说很好。您的位置将由您的电源组的外形决定。
电子设备就位后,是时候开始将各部分连接在一起了。
接线
试图把这一部分写成一步一步的说明是不合适的。如何连接机器人完全取决于你。每个机器人都不一样。布线由组件位置、您使用的电缆和个人偏好决定。相反,我将向您介绍我是如何连接我的机器人的,以及我做出决定背后的思考过程,并包括对您的项目的考虑。
我喜欢尽可能保持我的电缆整洁。有些人很少考虑他们是如何走线的。我在一些机器人的掩护下看到过一些纠结的乱七八糟的东西。对我来说,能够方便地接触零件很重要,这包括电线和电缆。
为 Pi 供电和连接 Arduino 的 USB 电缆比我在大多数项目中喜欢的要长一点。有多种类型的电缆可供选择,包括带有直角插头的电缆,这使得布线相当容易。因为电缆有点长,我用拉链把它们捆成小一点的东西。用于 Arduino 的较重的电缆然后被捆绑到用于 Pi 的安装夹上。从电源组到 Pi 的电缆塞在 Pi 下面(参见图 7-16 )。
图 7-16
USB cables bundled for tidiness
接下来,我将电线从马达连接到马达帽。DC 发动机的发动机罩有四个输出。有四个马达。我可以将马达成对连接到两个不同的输出端:一个用于左侧,一个用于右侧;然而,小而便宜的马达在速度上往往不太稳定。即使两台电机接收到相同的信号,也不能保证它们以相同的速度转动。能够独立调节每个电机的速度是我利用的一个很好的功能。因此,每个电机都有自己的输出(见图 7-17 )。
我在每个马达的速度上加了一个乘数。只要稍微调整一下倍增器,我就能让马达更加稳定地转动。
图 7-17
Motor and external battery pack wires connected to the Motor HAT
一旦马达连接好,我就接通电源。当你连接你的,注意极性。作为标准,红色为正,黑色为负。由于我的电池组是改装的,电线不是红黑相间的。我用伏特计来确定电线的极性,并适当地连接它们。
要连接的最后一根电缆是 T 形补鞋器的带状电缆(见图 7-18 )。只有一种方法将带状电缆连接到 T 形补鞋匠。塞子上的突出部对准塞子上的间隙。在 Pi 上,确保带有白色条纹的导线连接到引脚 1。对于 Pi,这是最靠近拐角的引脚。
图 7-18
Ribbon cable attaches the T-cobbler to the Pi. Note the white stripe .
安装传感器
这是组装机器人最需要创造力的地方。大多数机箱没有传感器安装硬件。如果有,它们是针对您可能不会使用的特定传感器。
安装传感器有许多不同的方法。我发现简单地准备一些不同的材料对我来说很有用。
当我长大的时候,我有一套直立装置。如果你不熟悉 Erector,他们生产一种建筑玩具,包括许多金属部件:横梁、支架、螺丝、螺母、滑轮、皮带等等。我花了数小时制造卡车、拖拉机、飞机,是的,甚至在 20 世纪 80 年代,还有机器人。想象一下我在寻找一些项目中使用的通用零件时的喜悦,我在当地的业余爱好商店遇到了一个安装工。更让我高兴的是,我发现当地一家大型五金店在他们的杂件箱中出售单个零件。
安装工具是许多项目中所需的各种小零件的重要来源。在这种情况下,我使用其中一个横梁和一个支架来安装超声波测距仪(见图 7-19 )。
图 7-19
A bracket and beam from the Erector Set. The beam is bent to provide angles for the sensors.
支架就位后,我使用安装胶带来固定传感器(见图 7-20 )。在这种特殊情况下,磁带有两个用途。首先,它将传感器固定在金属上。第二个目的是绝缘。传感器背面的电子件暴露在外;将它们连接到金属部件上有导致短路的风险。泡沫安装带是很好的绝缘体。
图 7-20
Ultrasonic sensors mounted
我学到的一件事是不要只相信安装带来固定传感器,尤其是金属。过去,胶带会变松,导致传感器出现故障。解决方法是我另一个最喜欢的方法:拉链。胶带将传感器固定到位并提供绝缘;然而,拉链增加了安全性和强度。在这一点上,我很确定事情不会有任何进展。
传感器安装好后,最后要做的就是将它们连接到 Arduino。我使用从传感器到 Arduino 的母到母跳线(参见图 7-21 )。在 Arduino 上,我安装了一个传感器屏蔽。传感器屏蔽为每个数字模拟引脚增加了一个 5V 的接地引脚。其中一些甚至有串行或无线设备专用接口。我用的是一个非常简单的没有很多专业头的。传感器护罩使传感器和其他设备的安装变得更加容易。
图 7-21
Ultrasonic rangefinders secured with zip ties and wired to Arduino
成品机器人
加上传感器,我就有了一个完整的机器人。剩下唯一要做的就是写代码让它动起来。图 7-22 显示了我完成的机器人。
图 7-22
The finished Whippersnapper with electronics
让机器人可以移动
目前,我们有一个非常好的零件集合。没有合适的软件,我们就没有真正的机器人。接下来,我概述了我们希望机器人做什么。我们将把它转化为行为,而这些行为又转化为让这个小机器人活起来所需的代码。
这个计划
在前面的章节中,我们用例子说明了不同的主题。由于这是我们第一次应用工作机器人,让我们花一点时间来概述我们希望机器人做什么。
这个计划是基于我在本章前面制作的机器人。它假设有三个超声波传感器和四个独立工作的电机。电机通过安装在 Pi 上的电机帽控制。传感器通过 Arduino 操作。
传感器
如前所述,我们将操作三个超声波传感器。传感器通过传感器护罩连接到 Arduino。因为我们使用串行与 Pi 通信,所以不能使用引脚 0 和 1。这些是串行端口使用的引脚。因此,我们的第一个传感器,即中间的传感器,位于引脚 2 和 3 上;左侧传感器在针脚 4 和 5 上;右边的传感器在引脚 6 和 7 上。
传感器按顺序触发,从中间开始,接着是左边,然后是右边。每个传感器都会等到前一个传感器完成后再触发。结果以半秒的时间间隔被发送回 Pi,作为一串浮点数,以厘米为单位表示距离每个传感器的距离。
发动机
电机连接到树莓皮上的电机帽。每个电机都连接到控制器上四个电机通道中的一个。电机 1,左前电机,连接到 M1。电机 2,即左后电机,与 M2 相连。电机 3,即右前电机,位于 M3 上。电机 4,右后电机,在 M4。
机器人使用差动转向驱动,也称为坦克驱动或滑动转向。为此,左侧电机一起驱动,右侧电机一起驱动。我称它们为左右声道。因此,同样的命令被发送到 M1 和 M2。同样,M3 和 M4 也接受共同指挥。
每个电机的代码都有乘数。乘数被应用于每个相应的电机以补偿速度差。这意味着我们需要一个缓冲区来容纳这种差异。因此,最高速度设置为 255 分中的 200 分。最初,乘数设置为 1。你需要调整你的乘数来适应你的机器人。
行为
这个机器人是一个简单的随机漫步者。它沿着直线行驶,直到检测到障碍物。然后它调整自己的路线以避免撞到障碍物。这并不是一个特别复杂的解决方案,但它说明了机器人操作的一些基础。
机器人的行为规则如下:
- 它向前行驶。
- 如果它发现左边有物体,它会向右转。
- 如果它发现右边有物体,它就向左转。
- 如果它检测到正前方有物体,它会停下来,转向距离最远的方向。
- 如果两个方向的距离相等,或者两侧的传感器都超过截止值,机器人在继续前进之前会在一个随机的方向上转动一段预定的时间。
这种行为有些基础,但它应该提供一个在房子里自主走动的机器人。
代码
代码分为两部分:Arduino 和 Pi。在 Arduino 上,我们关心的只是操作传感器,并以预定的时间间隔将读数传回 Pi。在这种情况下,每 500 毫秒或半秒。
Raspberry Pi 使用传入的数据来执行行为。它从串行端口读取数据,并将数据解析为变量。Pi 使用这些变量来确定下一步行动。这个动作被翻译成电机的指令,然后被发送到电机控制器执行。
Arduino 伫列
这个程序简单地操作机器人前面的三个超声波传感器。然后,它通过串行连接将这些值作为一串浮点数返回给 Raspberry Pi。代码基本上与第五章中的 Pinguino 示例相同。不同的是,我们使用三个传感器,而不是一个。
- 在 Arduino IDE 中打开一个新的草图。
- 将草图另存为
robot_sensors
。 - 输入以下代码:
int trigMid = 2; int echoMid = 3; int trigLeft = 4; int echoLeft = 5; int trigRight = 6; int echoRight = 7; float distMid = 0.0; float distLeft = 0.0; float distRight = 0.0; String serialString; void setup() { // set the pinModes for the sensors pinMode(trigMid, OUTPUT); pinMode(echoMid, INPUT); pinMode(trigLeft, OUTPUT); pinMode(echoLeft, INPUT); pinMode(trigRight, OUTPUT); pinMode(echoRight, INPUT); // set trig pins to low; digitalWrite(trigMid,LOW); digitalWrite(trigLeft,LOW); digitalWrite(trigRight,LOW); // starting serial Serial.begin(); } // function to operate the sensors // returns distance in centimeters float ping(int trigPin, int echoPin){ // Private variables, not available // outside the function int duration = 0; float distance = 0.0; // operate sensor digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); // get results and calculate distance duration = pulseIn(echoPin, HIGH); distance = duration/58.2; // return the results return distance; } void loop() { // get the distance for each sensor distMid = ping(trigMid, echoMid); distLeft = ping(trigLeft, echoLeft); distRight = ping(trigRight, echoRight); // write the results to the serial port Serial.print(distMid); Serial.print(","); Serial.print(distLeft); Serial.print(","); Serial.println(distRight); // wait 500 milliseconds before looping delay(500); }
- 保存草图并上传到 Arduino。
Arduino 现在应该是 ping 通了,但是因为没有监听,所以我们还不知道。接下来,我们将编写树莓派的代码。
树莓派 代码
现在是时候编写在 Raspberry Pi 上运行的代码了。这是一个相当长的程序,所以我会边走边分解。这其中的绝大多数应该看起来非常熟悉。为了适应这种逻辑,这里或那里做了一些更改,但在大多数情况下,我们以前已经这样做过了。每当我们做新的事情时,我都会花时间带你经历一遍。
- Python 2.7 的 Open IDLE。记住,Adafruit 库在 Python 3 中还不能工作。
- 创建一个新文件。
- 另存为
pi_roamer_01.py
。 - 输入以下代码。我一步一步地看每一部分,以确保你对这一过程中发生的事情有一个坚实的概念。
- 导入您需要的库。
import serial import time import random from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat from Adafruit_MotorHAT import Adafruit_DCMotor as adamo
- 创建电机变量并打开串口。Arduino 设置为以更高的波特率运行,因此 Pi 也需要以更高的波特率运行。
# create motor objects motHAT = amhat(addr=0x60) mot1 = motHAT.getMotor(1) mot2 = motHAT.getMotor(2) mot3 = motHAT.getMotor(3) mot4 = motHAT.getMotor(4) # open serial port ser = serial.Serial('/dev/ttyACM0', )
- 创建所需的变量。它们中的许多都是浮点数,因为我们使用的是小数。
# create variables # sensors distMid = 0.0 distLeft = 0.0 distRight = 0.0 # motor multipliers m1Mult = 1.0 m2Mult = 1.0 m3Mult = 1.0 m4Mult = 1.0 # distance threshold distThresh = 12.0 distCutOff = 30.0
- 设置管理电机所需的变量。您会注意到,我已经创建了许多默认值,然后将这些值赋给了其他变量。
leftSpeed
、rightSpeed
和driveTime
变量应该是我们在代码中真正改变的唯一变量。其余的是在整个程序中提供一致性。如果你想改变默认速度,你可以简单地改变speedDef
,改变在任何地方都适用。# speeds speedDef = 200 leftSpeed = speedDef rightSpeed = speedDef turnTime = 1.0 defTime = 0.1 driveTime = defTime
- 创建
drive
函数。它在程序主体的两个地方被调用。因为涉及到大量的工作,所以最好将代码分解到一个单独的功能块中。def driveMotors(leftChnl = speedDef, rightChnl = speedDef, duration = defTime): # determine the speed of each motor by multiplying # the channel by the motors multiplier m1Speed = leftChnl * m1Mult m2Speed = leftChnl * m2Mult m3Speed = rightChnl * m3Mult m4Speed = rightChnl * m4Mult # set each motor speed. Since the speed can be a # negative number, we take the absolute value mot1.setSpeed(abs(int(m1Speed))) mot2.setSpeed(abs(int(m2Speed))) mot3.setSpeed(abs(int(m3Speed))) mot4.setSpeed(abs(int(m4Speed))) # run the motors. if the channel is negative, run # reverse. else run forward if(leftChnl < 0): mot1.run(amhat.BACKWARD) mot2.run(amhat.BACKWARD) else: mot1.run(amhat.FORWARD) mot2.run(amhat.FORWARD) if (rightChnl > 0): mot3.run(amhat.BACKWARD) mot4.run(amhat.BACKWARD) else: mot3.run(amhat.FORWARD) mot4.run(amhat.FORWARD) # wait for duration time.sleep(duration)
- 通过将代码包装在
try
块中,开始程序的主块。这允许我们干净地退出程序。如果没有它和相应的 except 块,电机将继续执行它们收到的最后一个命令。
```py try: while 1: ```
- 继续主程序块,读取串口并解析接收到的字符串
```py # read the serial port val = ser.readline().decode('utf=8') print val # parse the serial string parsed = val.split(',') parsed = [x.rstrip() for x in parsed] # only assign new values if there are # three or more available if(len(parsed)>2): distMid = float(parsed[0] + str(0)) distLeft = float(parsed[1] + str(0)) distRight = float(parsed[2] + str(0)) ```
- 输入逻辑代码。这是执行前面概述的行为的代码。请注意,中间传感器块(执行停止和转向的那个)写在左右避障代码之外。这样做是因为我们希望对这个逻辑进行评估,而不管左右代码的结果如何。通过将它包含在其他代码之后,中间代码会覆盖左/右代码创建的任何值。
```py # apply cutoff distance if(distMid > distCutOff): distMid = distCutOff if(distLeft > distCutOff): distLeft = distCutOff if(distRight > distCutOff): distRight = distCutOff # reset driveTime driveTime = defTime # if obstacle to left, steer right by increasing # leftSpeed and running rightSpeed negative defSpeed # if obstacle to right, steer to left by increasing # rightSpeed and running leftSpeed negative if(distLeft <= distThresh): leftSpeed = speedDef rightSpeed = -speedDef elif (distRight <= distThresh): leftSpeed = -speedDef rightSpeed = speedDef else: leftSpeed = speedDef rightSpeed = speedDef # if obstacle dead ahead, stop then turn toward most # open direction. if both directions open, turn random if(distMid <= distThresh): # stop leftSpeed = 0 rightSpeed = 0 driveMotors(leftSpeed, rightSpeed, 1) time.sleep(1) leftSpeed = -150 rightSpeed = -150 driveMotors(leftSpeed, rightSpeed, 1) # determine preferred direction. if distLeft > # distRight, turn left. if distRight > distLeft, # turn right. if equal, turn random dirPref = distRight - distLeft if(dirPref == 0): dirPref = random.random() if(dirPref < 0): leftSpeed = -speedDef rightSpeed = speedDef elif(dirPref > 0): leftSpeed = speedDef rightSpeed = -speedDef driveTime = turnTime ```
- 调用我们之前创建的
driveMotors
函数。
```py # drive the motors driveMotors(leftSpeed, rightSpeed, driveTime) ```
- 刷新串行缓冲区中的所有字节。
```py ser.flushInput() ```
- 进入
except
块。它允许我们在退出程序前 Ctrl-C 来关闭电机。
```py except KeyboardInterrupt: mot1.run(amhat.RELEASE) mot2.run(amhat.RELEASE) mot3.run(amhat.RELEASE) mot4.run(amhat.RELEASE) ```
- 保存文件。
- 按 F5 运行程序。
当你看完你的小机器人在房间里漫游时,按 Ctrl-C 结束程序。
恭喜你。您刚刚构建并编程了您的第一个基于 Raspberry Pi 的机器人。
我们在这个项目中做了很多——尽管真的没有什么是你以前没见过的。在程序的第一部分,我们导入了我们需要的库并创建了马达对象。在下一节中,我们定义了所有的变量。程序的一个重要部分是我们在变量之后创建的函数。在此功能中,我们驱动电机。电机速度和驱动时间作为函数的参数传递,用于设置每个电机的速度。我们使用速度的符号来确定电机的方向。之后,我们通过将主块包装在一个try
块中来开始我们的主块。然后我们进入了while
循环,它允许程序无限重复。
在while
循环中,我们从读取串行字符串开始,然后解析它以提取三个浮点值。将字符串转换成浮点数的算法与我们用来转换成整数的算法略有不同。更具体地说,我们不必将结果除以 10。在一个十进制数的末尾加一个 0 不会改变这个值,所以我们可以在它被转换的时候使用它。
距离测量决定了机器人的下一步行动。if/elsif/else
模块评估传感器值。如果左侧或右侧传感器检测到预定义阈值内的障碍物,机器人将转向相反的方向。如果没有探测到障碍物,机器人继续前进。一个单独的if
块确定机器人正前方是否有障碍物。如果有障碍物,机器人会停下来,然后转向。它使用左右传感器值来确定前进的方向。如果不能确定方向,机器人转向随机方向。
所有这些都需要时间,在此期间 Arduino 会愉快地发送串行字符串并填充 Pi 的缓冲区。在继续之前,必须清除这些字符串。我们使用串行对象的flushInput()
方法来实现这一点。这样,我们只使用最新的信息。
最后,我们使用except
块来捕获键盘中断命令。当它被接收时,马达被释放,停止它们。然后程序退出。
摘要
这一章是关于把我们到目前为止学到的所有东西整合到一个工作机器人中。我们组装了机器人底盘套件,并安装了所有的电子设备。一旦所有东西都安装到机器人上,我们就编写一个程序来运行机器人。这是一个相当简单的漫游程序。当你运行它时,你的新机器人应该可以在房间里成功地走动,这取决于房间里家具的拥挤程度。
在接下来的章节中,我们将致力于改进机器人——添加更多的传感器,改进逻辑,并添加一些更高级的功能。具体来说,我们将添加一个摄像头,并学习如何使用 OpenCV 来跟踪颜色和追球。
八、使用红外传感器
到目前为止,你应该已经有了一个可以工作的机器人。在前几章中,我介绍了安装和编程机器人需要知道的一切。您已经使用了电机、传感器以及 Raspberry Pi 和 Arduino 之间的通信。在第 3 和第五章中,你学会了使用 Python 和 Arduino 来操作超声波测距仪。这本书的其余部分介绍了新的传感器,处理算法和计算机视觉。
在本章中,我们将使用红外(IR)传感器。我们看不同类型的传感器。在本章的最后,我们使用了一系列的红外传感器来检测表面和直线的边缘。
红外传感器
红外(IR)传感器是使用针对 IR 光谱调谐的光检测器来检测 IR 信号的任何传感器。通常,红外传感器与红外发射 LED 配对以提供红外信号。测量 LED 的发射强度或存在。
红外传感器的类型
红外线很容易使用。因此,我们发现了许多不同的使用方法。有多种红外传感器可供选择。许多都用在你意想不到的应用中。像在零售店看到的自动门,使用一种叫做 PIR 或被动红外的传感器来检测运动。这种类型的传感器用于自动照明和安全系统。喷墨打印机使用红外传感器和红外发光二极管来测量打印头的精确移动。您的娱乐系统遥控器可能使用红外 LED 将编码脉冲传输到红外接收器。红外敏感摄像机用于制造过程中的质量保证。这样的例子不胜枚举。让我们来看看一些不同类型的红外传感器。
反射传感器
反射传感器包括被设计成检测从目标反射的信号的任何传感器。超声波测距仪是反射传感器,因为它们检测从它们前面的物体反射回来的声音的波长。红外反射传感器以类似的方式工作,它们读取物体反射的红外辐射强度(见图 8-1 )。
图 8-1
Reflectance sensors measure the IR light returned from an IR diode
这种类型的传感器的变体被设计成检测 IR 信号的存在。该传感器使用红外强度阈值来确定附近是否有物体。传感器返回一个低信号,直到超过阈值,这时它返回一个高信号。这些传感器通常以反射或直接配置与发光 LED 配对。
线和边缘检测
红外探测器经常被用于建造探测线或壁架边缘的装置。当表面和线之间的对比度高时,这些传感器用于线检测;例如,白色桌面上的黑线。当传感器位于白色表面上方时,大部分红外信号返回到传感器。当传感器位于暗线上方时,返回的红外信号较少。这些传感器通常返回代表返回光量的模拟信号。
同样,传感器也可以检测表面的边缘。当传感器在表面上方时,传感器接收到更多的红外信号。当传感器超过边缘时,信号大大减弱,导致低值(见图 8-2 )。
图 8-2
Lines and edges can be detected by the difference in reflected light
一些传感器具有可调阈值,允许它们提供数字信号。当反射率高于阈值时,传感器处于高状态。当反射率低于阈值时,传感器为低。
这种类型的传感器面临的挑战是很难设定精确的阈值来获得一致的结果。然后,即使你让它们在一个环境中拨号,一旦条件改变,或者你试图在一个事件中演示,它们必须重新校准。(并不是说这种情况在我身上反复发生过。)正因为如此,我更喜欢使用模拟传感器,这允许我包括一个自动校准程序,以便程序可以设置自己的阈值。
测距仪
与接近传感器非常相似,测距仪测量到物体的距离。测距仪使用光束更窄的更强的 LED,用于确定物体的大致距离。与超声波测距仪不同,红外测距仪是为检测特定范围而设计的。将传感器与应用相匹配非常重要。
中断传感器
中断传感器用于检测红外信号的存在。它们通常与一个发光二极管配对,并被配置为允许物体在发射器和检测器之间通过。当物体存在并阻挡发射器时,接收器返回低信号。当物体不存在,并且允许接收器检测发射器时,信号为高。
这些传感器经常用在被称为编码器的设备中。编码器通常由带有半透明和透明部分的盘或带组成。当磁盘或磁带经过传感器时,信号会不断地从高电平变为低电平。然后,微控制器或其他电子设备可以使用这种交变信号来计数脉冲。因为透明部分的数量是已知的,所以可以高置信度地计算移动。在其最简单的形式中,这些传感器只能为微控制器提供一个脉冲来计数。一些编码器使用许多传感器来提供精确的运动信息,包括方向。
pir 运动探测器
另一种非常常见的传感器称为 PIR 运动检测器(见图 8-3 )。这些传感器有一个多面透镜,将物体发射或反射的红外辐射反射和折射到其内部的红外传感器上。当这些传感器检测到变化时,会产生一个高电平信号。
图 8-3
Common PIR sensor
这些传感器控制当地杂货店的自动门,并控制家中或办公室的自动灯。
使用红外传感器
正如我前面所讨论的,根据您使用的类型,有几种方法可以使用红外传感器。对于我们的项目,我们将使用五个 IR 线传感器,如图 8-4 所示。我更喜欢使用模拟类型的传感器。我们使用的特定传感器实际上可以进行模拟和数字读取。它有一个设置阈值的小电位计;然而,正如我在本章前面所讨论的,这些是出了名的难以拨入。我更喜欢直接使用模拟读数,并用软件计算阈值。
图 8-4
IR sensors for line following
连接红外传感器
我为我的机器人使用的传感器是普通 3 针红外传感器的 4 针变体。3 针传感器是数字式的,对传感器的模拟信号施加阈值,以返回高电平或低电平信号。4 引脚版本使用相同的阈值设置返回数字信号,但它还有一个额外的引脚提供模拟读数。让我们使用这两种信号来演练一下。
我使用的传感器与大多数传感器略有不同。它们是专为循线应用而设计的。因此,返回值是反向的。这意味着当反射率高时,它返回低数值,而不是提供高数值。同样,数字信号也被反相。高值表示存在线条,低值表示空白。当你运行下一个练习时,如果你的结果不同,不要惊讶。我们正在寻找相当一致的行为。
我们将 4 针传感器连接到 Arduino,并使用串行监视器来查看传感器的输出。高/低信号可以使用一个数字引脚,模拟传感器可以使用一个模拟引脚,但为了简化布线,我们使用两个模拟引脚。连接到数字输出的模拟引脚用于数字模式,因此其作用与其它引脚完全一样。
由于 Arduino 现在安装在机器人上,让我们使用传感器屏蔽进行连接。还有,我不会断开超声波测距仪。红外传感器的草图没有使用这些引脚,因此没有理由断开它们。
对于这个练习,您还需要一个测试表面。有大块黑色区域或粗黑线的白纸效果最好。由于大多数循线比赛使用 3/4 英寸的黑色电工胶带作为线,将一条胶带放在一张纸上、白色海报板或泡沫芯板上是理想的。
- 使用母到母跳线,将传感器的接地引脚连接到 A0 3 引脚接头的接地引脚。
- 将传感器的 VCC 引脚连接到 3 引脚接头 A0 的电压引脚。这是中间的针。
- 将模拟引脚连接到 A0 的信号引脚。(在我的传感器上,模拟引脚标记为 A0。)
- 将传感器的数字引脚连接到 A1 的信号引脚。(在我的传感器上,标记为 D0。)
- 在 Arduino IDE 中创建新的草图。
- 将草图另存为 IR_test。
- 输入以下代码:
int analogPin = A0; int digitalPin = A1; float analogVal = 0.0; int digitalVal = 0; void setup() { pinMode(analogPin, INPUT); pinMode(digitalPin, INPUT); Serial.begin(9600); } void loop() { analogVal = analogRead(analogPin); digitalVal = digitalRead(digitalPin); Serial.print("analogVal: "); Serial.print(analogVal); Serial.print(" - digitalVal: "); Serial.println(digitalVal); delay(500); }
- 将传感器移到表面的白色区域。传感器需要非常靠近表面而不接触它。
- 注意返回的值。(我得到了 30 到 45 范围内的模拟值。我的数字值是 0。)
- 将传感器移至直线或表面上的另一个黑色区域。
- 请注意这些值。(我得到了 700 到 900 范围内的模拟值。数字值是 1。)
你应该在表面的亮区和暗区得到非常不同的值。您可以看到这是如何很容易地转化为非常有用的功能。
安装红外传感器
接下来,我们将把传感器安装到机器人上,做一些有用的事情。同样,由于您的构建可能与我的有很大不同,我将介绍我是如何连接传感器的。如果你一直忠实地跟随,那么你应该复制我所做的。如果不是,那么这就是机器人开始变得有创造性的地方。你需要决定如何将传感器安装到你的机器人上。看看我的解决方案,了解你在寻找什么。
为了安装传感器,我转向(再一次)安装机的部件。这些部件非常方便易用。在这种情况下,我使用了一个酒吧和相同的角支架用于安装超声波测距仪。事实上,通过使用角支架,我延长了该组件,使红外传感器更接近地面。
在尝试安装红外传感器时,我遇到了一个问题。用于安装传感器的孔位于两个表面安装电阻之间。这意味着金属支架可能会导致短路。我库存中的尼龙支架太大,无法平放在那个空间。我可以使用垫片和一个长螺丝,但垫片太窄,不会直接对着安装杆上的孔。增加垫圈会使传感器离地面太近。
解决方案是将红外传感器安装在酒吧的顶部。挑战在于引脚的焊点肯定会与金属条短路。但是,通过在传感器背面贴上一片绝缘胶带并戳一个安装螺钉孔,这个问题很容易解决(参见图 8-5 )。
图 8-5
Mounting the IR sensors on a bar. Electrical tape protects the leads from shorting.
一旦传感器安装完毕,我需要将传感器的引线连接到 Arduino 电路板。我只使用了传感器的模拟引脚,所以我需要在 Arduino 上为每个传感器使用一个逻辑引脚。如果我同时使用模拟和数字引脚,我将需要 Arduino 上相应的模拟和数字引脚。因此,我使用了 A0 到 A4 引脚。为了确保导线正确连接,而不会在连接上施加过大的压力,我使用了较短的公母跳线来延长它们。在连接和传感器周围贴上一点胶带即可(见图 8-6 )。
图 8-6
The completed robot with IR sensors mounted and wired
代码
和上一个项目一样,这个项目使用 Arduino 作为 GPIO 设备。大部分逻辑由 Raspberry Pi 执行。我们将以 10 毫秒为间隔读取红外传感器,每秒 100 次。这些值被传递给 Raspberry Pi 来处理。正如您在前面的练习中看到的,读取传感器非常容易,因此 Arduino 代码相当简单。
圆周率方面要复杂得多。首先,我们必须校准传感器。然后,一旦校准,我们必须编写一个算法,使用来自传感器的读数来保持机器人在一条线上。这可能比你想象的要复杂。在这一章的后面,我们将看到一个好的解决方案,但是现在,我们将使用一个更直接的方法。
Arduino 伫列
Arduino 代码对于这个应用来说非常简单。我们将读取每个传感器,并通过串行连接将结果发送到 Pi,每秒 100 次。然而,由于我们需要在校准期间更频繁地读取传感器读数,我们需要知道校准何时运行,因为我们希望每秒更新 100 次,以确保我们获得良好的结果。
- 在 Arduino IDE 中开始一个新的草图。
- 将草图另存为
line_follow1
。 - 输入以下代码:
int ir1Pin = A0; int ir2Pin = A1; int ir3Pin = A2; int ir4Pin = A3; int ir5Pin = A4; int ir1Val = 0; int ir2Val = 0; int ir3Val = 0; int ir4Val = 0; int ir5Val = 0; void setup() { pinMode(ir1Pin, INPUT); pinMode(ir2Pin, INPUT); pinMode(ir3Pin, INPUT); pinMode(ir4Pin, INPUT); pinMode(ir5Pin, INPUT); Serial.begin(9600); } void loop() { ir1Val = analogRead(ir1Pin); ir2Val = analogRead(ir2Pin); ir3Val = analogRead(ir3Pin); ir4Val = analogRead(ir4Pin); ir5Val = analogRead(ir5Pin); Serial.print(ir1Val); Serial.print(","); Serial.print(ir2Val); Serial.print(","); Serial.print(ir3Val); Serial.print(","); Serial.print(ir4Val); Serial.print(","); Serial.println(ir5Val); delay(100); }
- 保存并上传草图。
这个小品很直白。我们所做的就是读取五个传感器中的每一个并将结果打印到串行端口。
Python 代码
大多数处理都是在 Pi 上完成的。我们需要做的第一件事是校准传感器,以获得高值和低值。要做到这一点,我们需要在这条线上来回扫描传感器,同时读取每个传感器的值。我们正在寻找最高值和最低值。一旦我们完成了一些过线,我们应该有好的价值观来工作。
传感器校准后,是时候开始移动了。驱动机器人前进。只要中间传感器检测到线,就一直往前开。如果左侧或右侧的一个传感器读取到该线,则向相反方向稍微校正,以重新对齐。如果一个外部传感器读取到这条线,就进行更剧烈的校正。这使得机器人能够沿着路线前进,并轻松转弯。
要正确运行这段代码,请为它创建一行代码。有几种方法可以做到这一点。如果你刚好有白色瓷砖地板,那么你可以直接在上面贴上电工胶带。电工胶带从瓷砖上剥离,不会损坏瓷砖。否则,你可以使用纸张、海报板或泡沫芯板,就像那些用于科学展览的一样。同样,使用电工胶带标记线路。一定要加一些曲线。
与 roamer 代码一样,我们将分几部分来介绍它。我们正在编写的代码越来越长。
- 在空闲的 IDE 中打开一个新文件。
- 将文件另存为
line_follow1.py
。 - 导入必要的库:
import serial import time from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat from Adafruit_MotorHAT import Adafruit_DCMotor as adamo
- 创建电机对象。为了使代码更加 Pythonic 化,让我们将 motor 对象放在一个列表中。
# create motor objects motHAT = amhat(addr=0x60) mot1 = motHAT.getMotor(1) mot2 = motHAT.getMotor(2) mot3 = motHAT.getMotor(3) mot4 = motHAT.getMotor(4) motors = [mot1, mot2, mot3, mot4]
- 定义控制电机所需的变量。同样,让我们创建列表。
# motor multipliers motorMultiplier = [1.0, 1.0, 1.0, 1.0, 1.0] # motor speeds motorSpeed = [0,0,0,0]
- 打开串行端口。
# open serial port ser = serial.Serial('/dev/ttyACM0', 9600)
- 定义必要的变量。与电机一样,将一些变量定义为列表。(这在代码的后面会有回报。我保证。)
# create variables # sensors irSensors = [0,0,0,0,0] irMins = [0,0,0,0,0] irMaxs = [0,0,0,0,0] irThesh = 50 # speeds speedDef = 200 leftSpeed = speedDef rightSpeed = speedDef corMinor = 50 corMajor = 100 turnTime = 0.5 defTime = 0.01 driveTime = defTime sweepTime = 1000 #duration of a sweep in milliseconds
- 定义驱动电机的函数。虽然类似,但这段代码不同于 roamer 函数。
def driveMotors(leftChnl = speedDef, rightChnl = speedDef, duration = defTime): # determine the speed of each motor by multiplying # the channel by the motors multiplier motorSpeed[0] = leftChnl * motorMultiplier[0] motorSpeed[1] = leftChnl * motorMultiplier[1] motorSpeed[2] = rightChnl * motorMultiplier[2] motorSpeed[3] = rightChnl * motorMultiplier[3]
- 迭代电机列表以设置速度。同样,迭代 motorSpeed 列表。
# set each motor speed. Since the speed can be a # negative number, we take the absolute value for x in range(4): motors[x].setSpeed(abs(int(motorSpeed[x])))
- 启动马达。
```py # run the motors. if the channel is negative, run # reverse. else run forward if(leftChnl < 0): motors[0].run(amhat.BACKWARD) motors[1].run(amhat.BACKWARD) else: motors[0].run(amhat.FORWARD) motors[1].run(amhat.FORWARD) if (rightChnl > 0): motors[2].run(amhat.BACKWARD) motors[3].run(amhat.BACKWARD) else: motors[2].run(amhat.FORWARD) motors[3].run(amhat.FORWARD) # wait for duration time.sleep(duration) ```
- 定义从串行流中读取红外传感器值并解析它们的函数。
```py def getIR(): # read the serial port val = ser.readline().decode('utf-8') # parse the serial string parsed = val.split(',') parsed = [x.rstrip() for x in parsed] ```
- 迭代 irSensors 列表以分配解析的值,然后从串行流中清除任何剩余的字节。
```py if(len(parsed)==5): for x in range(5): irSensors[x] = int(parsed[x]+str(0))/10 # flush the serial buffer of any extra bytes ser.flushInput() ```
- 定义校准传感器的功能。校准经过四个完整的周期,以读取传感器的最小和最大值。
```py def calibrate(): # set up cycle count loop direction = 1 cycle = 0 # get initial values for each sensor # and set initial min/max values getIR() for x in range(5): irMins[x] = irSensors[x] irMaxs[x] = irSensors[x] ```
- 在循环中循环五次,以确保获得四个完整的循环读数。
```py while cycle < 5: #set up sweep loop millisOld = int(round(time.time()*1000)) millisNew = millisOld ```
- 在扫描期间,驱动电机并读取红外传感器。
```py while((millisNew-millisOld)<sweepTime): leftSpeed = speedDef * direction rightSpeed = speedDef * -direction # drive the motors driveMotors(leftSpeed, rightSpeed, driveTime) # read sensors getIR() ```
- 如果传感器值低于或高于当前的
irMins
或irMaxs
值,则更新irMins
和irMaxs
。
```py # set min and max values for each sensor for x in range(5): if(irSensors[x] < irMins[x]): irMins[x] = irSensors[x] elif(irSensors[x] > irMaxs[x]): irMaxs[x] = irSensors[x] millisNew = int(round(time.time()*1000)) ```
- 一个循环后,改变电机方向并增加循环值。
```py # reverse direction direction = -direction # increment cycles cycle += 1 ```
- 循环完成后,驱动机器人前进。
```py # drive forward driveMotors(speedDef, speedDef, driveTime) ```
- 定义
followLine
功能。
```py def followLine(): leftSpeed = speedDef rightSpeed = speedDef getIR() ```
- 根据传感器读数定义行为。如果线被最右边或最左边的传感器检测到,则在另一个方向进行大的校正。如果内侧右侧或内侧左侧传感器检测到该线,则在另一个方向进行轻微校正;否则,直走。
```py # find line and correct if necessary if(irMaxs[0]-irThresh <= irSensors[0] <= irMaxs[0]+irThresh): leftSpeed = speedDef-corMajor elif(irMaxs[1]-irThresh <= irSensors[1] <= irMaxs[1]+irThresh): leftSpeed = speedDef-corMinor elif(irMaxs[3]-irThresh <= irSensors[3] <= irMaxs[3]+irThresh): rightSpeed = speedDef-corMinor elif(irMaxs[4]-irThresh <= irSensors[4] <= irMaxs[4]+irThresh): rightSpeed = speedDef-corMajor else: leftSpeed = speedDef rightSpeed = speedDef # drive the motors driveMotors(leftSpeed, rightSpeed, driveTime) ```
- 输入运行程序的代码。
```py # execute program try: calibrate() while 1: followLine() time.sleep(0.01) except KeyboardInterrupt: mot1.run(amhat.RELEASE) mot2.run(amhat.RELEASE) mot3.run(amhat.RELEASE) mot4.run(amhat.RELEASE) ```
- 保存代码。
- 把机器人放到线上。机器人应该对齐,使线在左右轮之间运行,中心传感器在它的正上方。
- 运行程序。
你的机器人现在应该沿着这条线走,如果它开始偏离这条线就进行纠正。您可能需要使用corMinor
和corMajor
变量来微调行为。
我们在这里执行的是所谓的比例控制。这是最简单的控制算法。背后的基本逻辑是,如果你的机器人有点偏离路线,应用一点修正。如果机器人偏离轨道很多,应用更多的修正。应用于机器人的校正量取决于误差有多大。
仅使用比例控制,机器人会非常努力地跟随生产线。它甚至可能成功;然而,你会注意到它是如何沿着这条线曲折前进的。这种行为可能会随着时间的推移而减少,变得平滑;然而,当你引入一条曲线时,不稳定的行为又开始了。更有可能的是,你的机器人矫枉过正,向一个随机的方向偏离,把线远远地甩在后面。
有更好的方法来控制机器人。事实上,有几种更好的方法,都来自一个叫做控制回路的研究领域。控制回路是提高机器或程序响应的算法。它们中的大多数使用当前状态和期望状态之间的差异来控制机器。这种差异称为误差。
接下来让我们来看一下这样的控制系统。
了解 PID 控制
为了更好地控制机器人,你将学习 PID 控制,我将尝试在不涉及数学的情况下讨论它。PID 控制器是应用最广泛的控制回路之一,因为它的通用性和简单性。我们实际上已经使用了 PID 控制器的一部分:比例控制。其余部分有助于平稳反应并提供更好的响应。
控制回路
PID 控制器是一组称为控制回路的算法中的一员。控制回路的目的是使用来自测量过程的输入来改变一个或多个控制,以补偿当前状态和期望状态之间的差异。有许多不同类型的控制回路。事实上,控制回路是被称为控制理论的整个研究领域。就我们的目的而言,我们实际上只关心一个:比例、积分和微分——或 PID。
比例、积分和微分控制
根据维基百科,“PID 控制器连续计算误差值(e(t))作为期望的设定点和测量的过程变量之间的差,并基于比例、积分和微分项应用校正。PID 是比例-积分-微分的缩写,指的是对误差信号进行运算以产生控制信号的三项
控制器的目的是对某些输出进行增量调节,以达到期望的结果。在我们的应用中,我们使用来自红外传感器的反馈来改变我们的电机。理想的行为是机器人在向前移动时保持以一条线为中心。然而,该过程可以用于任何传感器和输出;例如,PID 用于多旋翼平台,以保持水平和稳定性。
顾名思义,PID 算法实际上由三部分组成:比例、积分和微分。每个部分都是一种控制类型;然而,如果单独使用,产生的行为将是不稳定的和难以预测的。
比例控制
在比例控制中,变化量完全根据误差的大小来设定。误差越大,应用的更改越多。纯比例控制将达到零误差状态,但难以处理剧烈变化,这会导致剧烈振荡。
积分控制
积分控制不仅考虑误差,还考虑误差持续的时间。用于补偿误差的变化量随着时间而增加。纯积分控制可以使器件达到零误差状态,但它的反应很慢,往往会过补偿和振荡。
导数调节
导数控制不考虑误差,因此它永远无法使设备达到零误差状态。然而,它确实试图将误差的变化减小到零。如果应用了过多的补偿,则算法会过冲,然后应用另一个校正。该过程以这种方式继续,产生不断增加或减少修正的模式。尽管振荡减少的状态被认为是“稳定的”,但算法永远不会达到真正的零误差状态。
将他们聚集在一起
PID 控制器是这三种方法的简单组合。通过将它们结合在一起,该算法旨在产生平滑的校正过程,使误差为零。是时候做一点数学了。
让我们从定义一些变量开始。
e(t)是时间上的误差,其中(t)是时间,或现在。
K p 是代表比例增益的参数。当我们开始编码时,这就是比例变量。
K i 为积分增益参数。它也是一个变量。
K d 是微分增益参数。你猜对了,又一个变量。
τ代表一段时间内的积分值。我会说的。
比例项基本上是当前误差乘以 K p 值。)
积分部分稍微复杂一点,因为它考虑了所有已经发生的误差。它是一段时间内的误差和累积校正的总和。
)
导数项是原始误差和当前误差随时间的差值,然后乘以导数参数。
)
综上所述,我们的 PID 方程是这样的:
)
数学就是这样。幸运的是,我们不用自己解决。Python 让这变得非常容易。然而,理解等式内部发生的事情是很重要的。有三个参数需要调整来微调 PID 控制器。通过了解如何使用这些参数,您将能够确定哪些参数需要调整以及何时调整。
实现 PID 控制器
要实现控制器,我们需要知道一些事情。我们想要的结果是什么?我们的输入是什么?我们的产出是什么?
目标是提高我们的循线机器人的性能。因此,我们期望的结果是,当机器人向前行驶时,生产线保持在机器人的中心。
我们的输入是红外传感器。当外部传感器位于黑暗区域(直线)上方时,误差是内部传感器的两倍。这样,我们就知道机器人是有点偏离中心还是很多偏离中心。同样,两个左边的传感器将具有负值,而右边的传感器将具有正值,因此我们将知道哪个方向是关闭的。
最后,我们的输出是电机。更准确地说,我们的输出是左右马达通道之间的速度差。
代码
本练习的代码是对前面代码的修改。事实上,Arduino 代码根本不需要修改。更新的是我们在 Raspberry Pi 上实现的逻辑。
树莓派 代码
我们将修改line_follower1
代码以使用 PID 而不是比例算法。为此,我们需要更新getIR
函数来更新一个名为sensorErr
的新变量。然后我们将用我们的 PID 代码替换followLine
函数中的代码。
- 在空闲的 IDE 中打开文件
line_follower1
。 - 从文件菜单中选择另存为,将文件另存为
line_follower2.py
。 - 在变量部分的
#sensors
下,添加以下代码:# PID sensorErr = 0 lastTime = int(round(time.time()*1000)) lastError = 0 target = 0 kp = 0.5 ki = 0.5 kd = 1
- 创建 PID 函数。
def PID(err): # check if variables are defined before use # the first time the PID is called these variables will # not have been defined try: lastTime except NameError: lastTime = int(round(time.time()*1000)-1) try: sumError except NameError: sumError = 0 try: lastError except NameError: lastError = 0 # get the current time now = int(round(time.time()*1000)) duration = now-lastTime # calculate the error error = target - err sumError += (error * duration) dError = (error - lastError)/duration # calculate PID output = kp * error + ki * sumError + kd * dError # update variables lastError = error lastTime = now # return the output value return output
- 将
followLine
函数替换为:def followLine(): leftSpeed = speedDef rightSpeed = speedDef getIR() prString = '' for x in range(5): prString += ('IR' + str(x) + ': ' + str(irSensors[x]) + ' ') print prString # find line and correct if necessary if(irMaxs[0]-irThresh <= irSensors[0] <= irMaxs[0]+irThresh): sensorErr = 2 elif(irMaxs[1]-irThresh <= irSensors[1] <= irMaxs[1]+irThresh): sensorErr = 1 elif(irMaxs[3]-irThresh <= irSensors[3] <= irMaxs[3]+irThresh): sensorErr = -1 elif(irMaxs[4]-irThresh <= irSensors[4] <= irMaxs[4]+irThresh): sensorErr = -1 else: sensorErr = 0 # get PID results ratio = PID(sensorErr) # apply ratio leftSpeed = speedDef * ratio rightSpeed = speedDef * -ratio # drive the motors driveMotors(leftSpeed, rightSpeed, driveTime)
- 保存文件。
- 把机器人放到线上。
- 运行代码。
再说一次,你的机器人应该试着跟随这条线。如果这样做有问题,开始处理 K p ,K i ,K d 变量。这些变量需要微调以获得最佳结果。每个机器人都不一样。
摘要
在这一章中,我们给机器人增加了一些新的传感器。红外传感器应用于循线应用。它们也可以用于检测表面的边缘。如果你想防止你的机器人从桌子上或楼梯上掉下来,这个功能是很有用的。
我们第一次实现的线跟踪使用一个基本的比例控制来操纵机器人。这是功能,但勉强。一个更好的方法是使用一个称为 PID 控制器的控制回路,它使用几个因素,包括随时间的误差,使校正更平滑。您已经了解到,您可以通过使用我们代码中表示的 PID 参数来调整 ID 设置,其中包含 K p 、K i 和 K d 变量。使用适当的值,可以完全消除振荡,使机器人平稳地跟随生产线。
九、OpenCV 简介
自从第一章介绍树莓派以来,我们已经走过了很长的路。至此,您已经了解了 Pi 和 Arduino。你已经学会了如何对两块电路板进行编程。你和传感器和马达一起工作过。你已经制造了你的机器人,并给它编了程序,让它四处漫游并跟随一条线。
然而,老实说,你并没有真正需要树莓派的功能。事实上,这有点阻碍。你用机器人做的所有事情——漫游和循线,没有 Pi 你也可以用 Arduino 做得很好。现在是时候展示圆周率的真正力量,并了解为什么你想在你的机器人使用它。
在这一章中,我们将做一些你不能单独用 Arduino 做的事情。我们将连接一个简单的网络摄像头,并开始使用通常所说的计算机视觉。
计算机视觉
计算机视觉是允许计算机分析图像并提取有用信息的算法的集合。它应用广泛,并迅速成为日常生活的一部分。如果你有一部智能手机,你很可能至少有一个应用使用计算机视觉。大多数新的中高端相机都内置了面部检测功能。脸书使用计算机视觉进行面部检测。计算机视觉被运输公司用来跟踪他们仓库里的包裹。当然,它被用于机器人导航、物体探测、物体回避和许多其他行为。
这一切都始于一幅图像。计算机分析图像以识别线条、角落和大范围的颜色。这个过程被称为特征提取,它是几乎所有计算机视觉算法的第一步。一旦这些特征被提取出来,计算机就可以将这些信息用于许多不同的任务。
面部识别是通过将特征与包含面部特征数据的 XML 文件进行比较来完成的。这些 XML 文件被称为级联。它们可用于许多不同类型的对象,而不仅仅是面部。同样的技术也可以用于物体识别。您只需向应用提供您感兴趣的对象的特性信息。
计算机视觉也包含视频。运动跟踪是计算机视觉的一个常见应用。为了检测运动,计算机比较来自静止摄像机的单个帧。如果没有运动,特征将不会在帧之间改变。因此,如果计算机识别出帧之间的差异,最有可能的是运动。基于计算机视觉的运动跟踪比红外传感器更可靠,如第八章中讨论的 PIR 传感器。
计算机视觉的一个令人兴奋的最新应用是增强现实。从视频流中提取的特征可用于识别表面上的独特图案。因为计算机知道图案,所以它可以很容易地计算出表面的角度。然后在图案上叠加 3D 模型。这个 3D 模型可以是物理的东西,比如建筑物,也可以是具有二维文本的平面对象。建筑师用这种技术向客户展示一座建筑在地平线上的样子。博物馆用它来提供更多关于展览或艺术家的信息。
所有这些都是现代环境下计算机视觉的例子。但是应用的列表太大了,无法在这里深入讨论,而且还在不断增加。
开放计算机视觉
就在几年前,计算机视觉对业余爱好者来说还不太容易。它需要大量繁重的数学运算,甚至更繁重的处理。计算机视觉项目通常使用笔记本电脑完成,这限制了它的应用。
OpenCV 已经存在一段时间了。1999 年,英特尔研究院建立了一个促进计算机视觉发展的开放标准。2012 年,它被非营利组织 OpenCV 基金会接管。你可以在他们的网站上下载最新版本。然而,要让它在 Raspberry Pi 上运行还需要一点额外的努力。我们很快就会谈到这一点。
OpenCV 是用 C++原生编写的;但是,它可以在 C、Java 和 Python 中使用。我们对 Python 实现感兴趣。因为我们的电机控制器库与 Python 3 不兼容,所以我们需要安装 OpenCV for Python 2.7。
安装 OpenCV
我们将在树莓派 上安装 OpenCV。你要确保你的树莓皮插在充电器上,而不是电池组上,给自己足够的时间来安装。我们将从源代码编译 OpenCV,这意味着我们将从互联网上下载源代码,并直接在 Pi 上构建它。需要注意的是,尽管这个过程并不困难,但确实需要很长时间,并且需要输入许多 Linux 命令。我通常在晚上开始这个过程,让最终的构建运行一整夜。
- 登录您的树莓派。
- 在 Pi 上打开一个终端窗口。
- 确保 Raspberry Pi 已更新。
sudo apt-get update sudo apt-get upgrade sudo rpi-update sudo reboot
- 这些命令安装了构建 OpenCV 的先决条件。
sudo apt-get install build-essential git cmake pkg-config sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev sudo apt-get install libxvidcore-dev libx264-dev sudo apt-get install libgtk2.0-dev sudo apt-get install libatlas-base-dev gfortran
- 下载 OpenCV 源代码和 OpenCV 贡献的文件。贡献的文件包含了许多 OpenCV 主发行版中没有的功能。
cd ~ git clone https://github.com/Itseez/opencv.git cd opencv git checkout 3.1.0 cd ~ git clone https://github.com/Itseez/opencv_contrib.git cd opencv_contrib git checkout 3.1.0
- 安装 Python 开发库和 pip。
sudo apt-get install python2.7-dev wget https://bootstrap.pypa.io/get-pip.py sudo python get-pip.py
- 确保安装了 NumPy。
pip install numpy
- 准备编译的源代码。
cd ~/opencv mkdir build cd build cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D INSTALL_C_EXAMPLES=OFF \ -D INSTALL_PYTHON_EXAMPLES=ON \ -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \ -D BUILD_EXAMPLES=ON ..
- 现在让我们编译源代码。这部分需要一段时间。有些人试图利用树莓派 的 ARM CPU 中的所有四个核心。然而,我发现这很容易出错,而且它从来没有为我工作过。我的建议是咬紧牙关:让 Pi 决定要使用的内核数量,并让它运行。如果您想大胆尝试,您可以通过在下面的行
make
中添加
–j4
开关来强制 Pi 使用四个内核 - 如果您尝试了–j4 开关,但失败了,大约在第四个小时,请输入以下行:
```py make clean make ```
- 编译好源代码后,现在就可以安装它了。
```py sudo make install sudo ldconfig ```
- 通过打开 Python 命令行来测试安装。
```py python ```
- 导入 OpenCV。
```py >>>import cv2 ```
现在,您的 Raspberry Pi 上应该已经安装了 OpenCV 的运行版本。如果导入命令不起作用,您需要确定它没有安装的原因。互联网是您排除故障的指南。
选择摄像机
在我们真正让 OpenCV 在我们的机器人上工作之前,我们需要安装一个摄像头。Raspberry Pi 有几个选项:Pi 摄像头或 USB 网络摄像头。
Pi 摄像机直接连接到专门为其设计的端口。一旦连接上,您需要进入 raspi-config 并启用它。Pi 摄像头的优势在于它比 USB 摄像头快一点,因为它直接连接到电路板。它不通过 USB 串行总线。这让它有了一点优势。
大多数 Pi 摄像机都配有一根短的 6 英寸带状电缆。由于树莓派在我们机器人上的位置,这是不够的。可以订购更长的电缆。Adafruit 有几个选择。但是,对于这个项目,我们将使用一个简单的网络摄像机。
USB 摄像头在任何电子产品零售商处都很容易买到。网上也有很多选择。对于这个基本的应用,我们不需要任何特别健壮的东西。任何能提供像样图像的相机都可以。拥有高分辨率也不是问题。由于我们是在 Raspberry Pi 的有限资源下运行相机,较低的分辨率实际上会有助于性能。记住,OpenCV 逐像素分析每一帧。图像中的像素越多,需要处理的事情就越多。
对于我的机器人,我选择了直播!Cam Sync HD by Creative(见图 9-1 ),这是一款基本的高清网络摄像头,内置麦克风,通过标准 USB 2 端口操作。这个项目我们不需要麦克风,但将来可能会需要。它可以捕捉 720p 高清视频,这对我们的机器人来说可能有点多,但如果性能受到影响,我总是在软件中降低分辨率。
图 9-1
Creative Live! Cam Sync HD
安装摄像机
大多数网络摄像头都安装在显示器的顶部。他们通常有一个折叠夹,为监视器上的摄像机提供支撑。不幸的是,这些夹子通常是作为相机机身的一部分模制而成的,如果不损坏相机就无法拆除。这当然适用于生活!凸轮同步。所以,再一次,一点点创造力开始发挥作用。
我用来安装传感器的支架以 45 度角从机器人的前面脱落。为了让自己轻松一点,我选择不在相机支架上钻孔。相反,我使用我的可靠的安装胶带和一对安装支架。当我爬上去的时候,我想让它爬得相当高,并且稍微向下。这个想法是给它最好的视角,以及任何直接在它前面的物体。我还希望镜头尽可能靠近中心轴,以使软件方面的事情更简单。图 9-2 显示安装摄像头后的机器人。
图 9-2
Camera mounted on robot
OpenCV 基础知识
OpenCV 有许多功能。它拥有 500 多个库和数千种功能。这是一个非常大的主题——太大了,一章无法涵盖。我将讨论在您的机器人上执行一些简单任务所需的基础知识。
我说简单的任务。这些任务非常简单,因为 OpenCV 抽象了后台发生的大量数学运算。当我考虑几年前机器人爱好的状态时,我发现即使是最基本的东西也能轻易获得,这真是令人惊讶。
目标是在本章结束时建造一个能够识别球并向球移动的机器人。我介绍的函数将帮助我们实现这个目标。我强烈建议花时间浏览 OpenCV 网站上的一些教程( https://opencv.org )。
要在 Python 代码中使用 OpenCV,您需要导入它。而且,当您这样做时,您可能还需要导入 NumPy 库。NumPy 增加了许多数学和数字处理功能,使得使用 OpenCV 更加容易。所有与图像相关的代码都应该以此开头:
import cv2 import numpy as np
在本章的代码讨论中,我假设这已经完成了。以cv2
为前缀的函数是 OpenCV 函数。如果以np
为前缀,则为 NumPy 函数。如果你想扩展你在这本书里读到的内容,这一点很重要。OpenCV 和 NumPy 是两个独立的库,但是 OpenCV 经常使用 NumPy。
使用图像
在本节中,您将学习如何从文件中打开图像,以及如何从摄像机中捕捉实时视频。然后,我们将看看如何处理和分析这些图像,从中获取有用的信息。具体来说,我们将研究如何识别特定颜色的球,并跟踪它在帧中的位置。
但是首先,我们有一个先有鸡还是先有蛋的问题。我们需要在所有的练习中看到我们图像处理的结果。为此,我们需要从如何显示图像开始。这是我们会广泛使用的东西,而且非常容易使用。但我想确保在你学习如何捕捉图像之前,我先覆盖它。
显示图像
在 OpenCV 中显示图像实际上非常容易。imshow()
功能提供了这一功能。该函数适用于静态图像和视频图像,并且两者之间的实现不会改变。imshow()
功能打开一个新窗口显示图像。当您调用它时,您必须为窗口以及您想要显示的图像或框架提供一个名称。
这是关于 OpenCV 如何处理视频的重要一点。因为 OpenCV 将视频视为一系列独立的帧,所以几乎所有用于修改或分析图像的功能都适用于视频。这显然包括imshow()
。
如果我们想显示一个加载到img
变量中的图像,它看起来会像这样:
cv2.imshow('img', img)
在这个例子中,第一个参数是窗口的名称。它出现在窗口的标题栏中。第二个参数是保存图像的变量。显示视频的格式完全相同。我通常使用变量 cap 进行视频捕获,因此代码如下所示:
cv2.imshow('cap', cap)
如您所见,代码是相同的。同样,这是因为 OpenCV 将视频视为一系列独立的帧。实际上,视频捕获依靠一个循环来连续捕获下一帧。所以从本质上来说,显示一个文件中的静止图像和一个相机中的单独帧是完全一样的。
还有一个素要显示。为了实际显示图像,imshow()
函数要求也调用waitKey()
。waitKey()
功能等待键盘按键按下指定的毫秒数。许多人用这个来捕捉一个退出键。除非我需要按键,否则我通常将它传递为零。
cv2.waitKey(0)
我们在本章中广泛使用了imshow()
和waitKey()
。
捕捉图像
使用 OpenCV 所需的图像有几个来源,所有这些来源都是两个因素的变体:文件或相机,以及静止或视频。在大多数情况下,我们只关心来自摄像机的视频,因为我们使用 OpenCV 进行导航。但是所有的方法都有优点。
打开一个静止图像文件是学习新技术的一个很好的方法,尤其是当您正在处理计算机视觉的特定方面时。例如,如果您正在学习如何使用滤镜来识别某种颜色的球,那么使用由三个不同颜色的球组成的静止图像(除此之外别无其他)可以让您专注于特定的目标,而不必担心用于捕捉实时视频流的底层框架。哦,这是一个伏笔,如果你没有意识到这一点。
从用相机捕捉静止图像中学到的技术可以应用到真实环境中。它允许您通过使用包含真实世界素的图像来改进或微调代码。
显然,捕捉现场视频是我们在机器人中使用的目标。实时视频流是我们用来识别我们的目标对象,然后导航到它。随着您的计算机视觉经验的增长,您可能会将运动检测或其他方法添加到您的清单中。由于机器人上的摄像头的目的是实时采集环境信息,因此需要实时视频。
文件中的视频对于学习过程也非常有用。您可能希望从机器人捕捉实时视频,并将其保存到文件中供以后分析。比方说,你正在利用一天中你能找到的任何空闲时间从事你的机器人项目。你可以随身携带你的笔记本电脑,但是携带一个机器人却是另一回事。因此,如果你从你的机器人那里录制视频,你可以在没有机器人陪伴的情况下进行你的计算机视觉算法。
请记住,Python 和 OpenCV 的一大优点是它们是抽象的,并且在很大程度上是平台独立的。所以,你在 Windows 机器上写的代码移植到你的 Raspberry Pi 上。
出差,并希望在酒店休息一段时间?去家人那里度假,偶尔需要离开一下?在午餐时间或课间溜进一点机器人程序?将录制的视频与 Python 和 OpenCV 的本地实例一起使用,并使用您的检测算法。当你回家后,你可以把代码转移到你的机器人上,并进行现场测试。
在本节中,我们使用前三种技术。我向您展示了如何保存和打开视频文件,但在大多数情况下,我们将使用静止图像来学习检测算法,使用实时视频来学习跟踪。
打开图像文件
OpenCV 使得处理图像和文件变得非常容易,尤其是考虑到后台发生的事情使得这些操作成为可能。打开图像文件也不例外。我们使用imread()
函数从本地存储器打开图像文件。imread()
函数有两个参数:文件名和颜色类型标志。打开文件显然需要文件名。颜色类型标志决定了是以彩色还是灰度打开图像。
让我们打开并显示一个图像。我将使用三个彩色球的图像,这也是本章后面学习如何检测颜色的图像。如果您已经安装了 Python 和 OpenCV,这个练习可以在 Pi 或您的计算机上完成。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
open_image.py
。 - 输入以下代码:
import cv2 img = cv2.imread('color_balls_small.jpg') cv2.imshow('image',img) cv2.waitKey(0)
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
python open_image.py
并按回车键。
一个窗口打开,在白色背景上显示三个彩球的图像(参见图 9-3 )。按任意键关闭它。
图 9-3
Three colored balls
由于 IDLE 与基于 Linux 的机器上的 GUI 系统的交互方式,如果您直接从 IDLE 运行代码,图像窗口将不会正常关闭。然而,通过从终端运行代码,我们没有这个问题。
捕捉视频
用相机捕捉视频与打开文件略有不同。使用视频还有几个步骤。一个变化是我们必须使用一个循环来获得多个帧;否则,OpenCV 只会捕获单个帧,这不是我们想要的。通常使用开放的while
回路。这会捕捉视频,直到我们主动停止它。
为了使测试更容易,我将球直接放在摄像机的前面(见图 9-4 )。现在,我们只想捕捉图像。
为了从摄像机中捕捉视频,我们将创建一个videoCapture()
对象,然后在一个循环中使用read()
方法来捕捉帧。read()
方法返回两个对象:一个返回值和一个图像帧。返回值只是一个验证读取成功或失败的整数。如果读取成功,则值为 1;否则,读取失败,并返回 0。为了防止导致代码出错的错误,可以测试读取是否成功。
图 9-4
Ball positioned in front of the robot for testing
我们关心图像帧。如果读取成功,则返回一个图像。如果不是,则返回一个空对象来代替它。由于空对象不能访问 OpenCV 方法,所以当您试图修改或操作图像时,您的代码将会崩溃。这就是为什么测试读取操作是否成功是个好主意。
查看摄像机
在下一个练习中,我们将打开之前安装的摄像机来观看视频。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为 view_camera.py。
- 输入以下代码:
import cv2 import numpy as np cap = cv2.VideoCapture(0) while(True): ret,frame = cap.read() cv2.imshow('video', frame) if cv2.waitKey(1) & 0xff == ord('q'): break cap.release() cv2.destroyAllWindows()
- 保存文件。
- 打开终端窗口。
- 导航到保存脚本的工作文件夹。
- 类型
sudo python view_camera.py
。
这将打开一个窗口,显示您的摄像机看到的内容。如果您使用远程桌面会话来处理 Pi,您可能会看到以下警告消息:Xlib:extension RANR missing on display:10。此消息意味着系统正在寻找 vncserver 中不包含的功能。可以忽略。
如果您担心视频图像的刷新率,请记住,当我们通过远程桌面会话运行几个窗口时,我们对 Raspberry Pi 的要求非常高。如果您连接显示器和键盘来访问 Pi,它会运行得更快。如果您在没有可视化的情况下运行视频捕获,它会运行得更快。
录制视频
录制视频是查看摄像机的延伸。要录制,您必须声明您将使用的视频编解码器,然后设置将传入视频写入 SD 卡的VideoWriter
对象。
OpenCV 使用 FOURCC 码来指定编解码器。FOURCC 是视频编解码器的四字符代码。你可以在 www.fourcc.org 找到更多关于 FOURCC 的信息。
当创建VideoWriter
对象时,我们需要提供一些信息。首先,我们必须提供文件名来保存视频。接下来,我们提供编解码器,然后是帧速率和分辨率。一旦创建了VideoWriter
对象,我们只需使用VideoWriter
对象的write()
方法将每个从写到文件中。
让我们录制一些机器人的视频。我们将使用 XVID 编解码器写入一个名为test_video.avi
的文件。我们将使用上一个练习中的视频捕获代码,而不是从头开始。
- 在空闲 IDE 中打开 view_camera.py 文件。
- 选择文件➤另存为并将文件另存为
record_camera.py
。 - 更新代码。在下面,新行以粗体显示:
import cv2 import numpy as np cap = cv2.VideoCapture(0) fourcc = cv2.VideoWriter_fourcc(*'XVID') vidWrite = cv2.VideoWriter('test_video.avi', \ fourcc, 20, (640,480)) while(True): ret,frame = cap.read() vidWrite.write(frame) cv2.imshow('video', frame) if cv2.waitKey(1) & 0xff == ord('q'): break cap.release() vidWrite.release() cv2.destroyAllWindows()
- 保存文件。
- 打开终端窗口。
- 导航到保存脚本的工作文件夹。
- 类型
sudo python record_camera.py
。 - 让视频运行几秒钟,然后按 Q 结束程序并关闭窗口。
现在,您的工作目录中应该有一个视频文件。接下来,我们将看看从文件中读取视频。
代码中有几项需要注意。当我们创建VideoWriter
对象时,我们以组的形式提供了视频分辨率。这是整个 OpenCV 中非常常见的做法。此外,我们必须释放VideoWriter
对象。这关闭了文件的写入。
从文件中读取视频
从文件中回放视频与从摄像机中观看视频完全相同。唯一的区别是,我们提供要播放的文件的名称,而不是向视频设备提供索引。我们将使用ret
变量来测试视频文件的结尾;否则,当没有更多的视频播放时,我们会得到一个错误。
在本练习中,我们将简单地回放我们在上一个练习中录制的视频。代码应该看起来非常熟悉。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
view_video.py
。 - 输入以下代码:
import cv2 import numpy as np cap = cv2.VideoCapture('test_video.avi') while(True): ret,frame = cap.read() if ret: cv2.imshow('video', frame) if cv2.waitKey(1) & 0xff == ord('q'): break cap.release() cv2.destroyAllWindows()
- 保存文件。
- 打开终端窗口。
- 导航到保存脚本的工作文件夹。
- 类型
sudo python view_video.py
。
一个新窗口打开。它显示了我们在上一个练习中记录的视频文件。当到达文件结尾时,视频停止。按 Q 结束程序并关闭窗口。
图像转换
现在你知道了更多关于如何获取图像的知识,让我们来看看我们可以用它们做些什么。我们将看一些非常基本的操作。选择这些操作是因为它们将帮助我们达到跟踪球的目标。OpenCV 非常强大,它拥有比我在这里介绍的更多的功能。
轻弹
很多时候,摄像机在项目中的摆放并不理想。经常,我不得不把相机倒过来装,或者因为这样或那样的原因,我需要翻转图像。
幸运的是,OpenCV 用flip()
方法使这变得非常简单。flip()
方法有三个参数:要翻转的图像、指示如何翻转的代码和翻转图像的目的地。最后一个参数仅在您想要将翻转的图像指定给另一个变量时使用,但是您可以在适当的位置翻转图像。
通过提供 flipCode,可以水平和/或垂直翻转图像。flipCode 为正、负或零。零水平翻转图像,正值垂直翻转图像,负值在两个轴上翻转图像。通常情况下,您会在两个轴上翻转图像,以有效地将其旋转 180 度。
让我们使用之前使用的三个球的图像来说明翻转帧。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
flip_image.py
。 - 输入以下代码:
import cv2 img = cv2.imread('color_balls_small.jpg') h_img = cv2.flip(img, 0) v_img = cv2.flip(img, 1) b_img = cv2.flip(img, -1) cv2.imshow('image', img) cv2.imshow('horizontal', h_img) cv2.imshow('vertical', v_img) cv2.imshow('both', b_img) cv2.waitKey(0)
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
python flip_image.py
并按回车键。
打开四个窗口,每个窗口都有不同版本的图像文件。按任意键退出。
调整大小
您可以调整图像的大小。这有助于减少处理图像所需的资源。图像越大,需要的内存和 CPU 资源就越多。为了调整图像的大小,我们使用了resize()
方法。这些参数是要缩放的图像、作为组的所需维度以及插值。
插值是一种数学方法,用于确定如何处理像素的删除或添加。请记住,在处理图像时,您实际上是在处理一个多维数组,该数组包含组成图像的每个点或像素的信息。缩小图像时,您正在删除像素。当你放大一幅图像时,你是在增加像素。插值是发生这种情况的方法。
有三种插值选项。INTER_AREA 最适合用于归约。INTER_CUBIC 和 INTER_LINEAR 都适合放大图像,INTER_LINEAR 是两者中速度较快的。如果没有提供插值,OpenCV 使用 INTER_LINEAR 作为缩小和放大的缺省值。
三个球的图像目前为 800×533 像素。虽然它不是大号的,但我们会把它做小一点。让我们把两个轴的大小都设为当前大小的一半。为此,我们将使用 INTER_AREA 插值。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
resize_image.py
。 - 输入以下代码:
import cv2 img = cv2.imread('color_balls_small.jpg') x,y = img.shape[:2] resImg = cv2.resize(img, (y/2, x/2), interpolation = cv2.INTER_AREA) cv2.imshow('image', img) cv2.imshow('resized', resImg) cv2.waitKey(0)
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
python resize_image.py
并按回车键。
应该开了两扇窗。第一个有原始图像。第二个显示缩小的图像。按任意键关闭窗口。
使用颜色
颜色显然是处理图像的一个非常重要的部分。因此,它是 OpenCV 中非常重要的一部分。颜色可以做很多事情。我们将重点关注完成我们的最终目标所需的几个关键要素,即识别和用机器人追球。
色彩空间
处理颜色的一个关键要素是颜色空间,它描述了 OpenCV 如何表达颜色。在 OpenCV 中,颜色由一系列数字表示。色彩空间决定了这些数字的含义。
OpenCV 的默认颜色空间是 BGR。这意味着每种颜色都由 0 到 255 之间的三个整数来描述,它们依次对应于三个颜色通道——蓝色、绿色和红色。表示为(255,0,0)的颜色在蓝色通道中具有最大值,绿色和红色都为零。这代表纯蓝色。鉴于此,(0,255,0)为绿色,(0,0,255)为红色。值(0,0,0)代表黑色,没有任何颜色,而(255,255,255)代表白色。
如果你过去曾与图形打交道,BGR 与你可能习惯的截然相反。大多数数字图形都是用 RGB 来描述的,即红色、绿色和蓝色。所以,这可能需要一点时间来适应。
有许多颜色空间。我们关心的是 BGR、RGB、HSV 和灰度。我们已经讨论了默认的颜色空间,BGR,和常见的 RGB 颜色空间。HSV 是色调、饱和度和值。色调代表 0 到 180 范围内的颜色。饱和度表示颜色从 0 到 255 有多白。值是从 0 到 255 的颜色与黑色的距离。如果饱和度和值都为 0,则颜色为灰色。饱和度和值 255 是色调的最亮版本。
色调有点复杂。它在 0 到 180 的范围内,0 和 180 都是红色。这就是记住色轮的重要性。如果 0 和 180 在红色空间中间的轮盘顶部相遇,当你围绕轮盘顺时针移动时,色相= 30 是黄色,色相= 60 是绿色,色相= 90 是蓝绿色,色相= 120 是蓝色,色相= 150 是紫色,色相= 180 将我们带回红色。
你最常遇到的是灰度。灰度就是它听起来的样子:图像的黑白版本。特征检测算法使用它来创建遮罩。我们用它来过滤物体。
要将图像转换到不同的色彩空间,可以使用cvtColor
方法。它需要两个参数:图像和颜色空间常数。颜色空间常量内置于 OpenCV 中。分别是 COLOR_BGR2RGB、COLOR_BGR2HSV、COLOR_BGR2GRAY。你看到那里的模式了吗?如果您想从 RGB 颜色空间转换到 HSV 颜色空间,常量应该是 COLOR_RGB2HSV。
让我们把三个彩球的图像转换成灰度图像。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
gray_image.py
。 - 输入以下代码:
import cv2 img = cv2.imread('color_balls_small.jpg') grayImg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cv2.imshow('img', img) cv2.imshow('gray', grayImg) cv2.waitKey(0)
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
python gray_image.py
并按回车键。
这将打开两个窗口:一个显示原始彩色图像,另一个显示灰度图像。单击任意键退出程序并关闭窗口。
颜色过滤器
对颜色进行过滤需要的代码非常少,但同时,这可能会有点令人沮丧,因为您通常不是在寻找特定的颜色,而是一个颜色范围。颜色很少是纯粹的,只有一种价值。这就是为什么我们希望能够在色彩空间之间转换。当然,我们可以在 BGR 寻找红色。但是要做到这一点,我们需要这三个值的具体范围。在所有色彩空间都是如此的情况下,通常在 HSV 空间中更容易调整到您需要的范围。
用于过滤特定颜色的策略相当简单,但是需要几个步骤,并且需要记住一些事情。
首先,我们将在 HSV 颜色空间中复制图像。然后,我们应用我们的过滤范围,使其成为自己的图像。为此,我们使用inRange()
方法。它有三个参数:我们要应用滤镜的图像、下限值范围和上限值范围。inRange
方法扫描提供的图像中的所有像素,以确定它们是否在指定的范围内。如果是,则返回 true 或 1;否则,它返回 0。这留给我们的是一个黑白图像,我们可以用它作为一个面具。
接下来,我们使用bitwise_and()
方法应用蒙版。该方法获取两幅图像,并返回像素匹配的区域。因为这不是我们想要的,我们需要耍点小花招。出于我们的目的,bitwise_and
需要三个参数:图像 1、图像 2 和一个遮罩。因为我们想要返回遮罩显示的所有内容,所以图像 1 和图像 2 都使用我们的原始图像。然后,我们通过指定遮罩参数来应用遮罩。因为我们忽略了几个可选参数,所以我们需要显式地指定掩码参数,就像这样:mask = mask_image
。结果是图像只显示了我们要过滤的颜色。
演示这一点的最简单的方法是通过它走一走。一旦你知道发生了什么,代码其实很简单。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
blue_filter.py
。 - 输入以下代码:
import cv2 img = cv2.imread("color_balls_small.jpg") imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) lower_blue = np.array([80,120,120]) upper_blue = np.array([130,255,255]) blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue) res = cv2.bitwise_and(img, img, mask=blueMask) cv2.imshow('img', img) cv2.imshow('mask', blueMask) cv2.imshow('blue', res) cv2.waitKey(0)
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
python blue_filter.py
并按回车键。
打开三个窗口,显示我们图像的不同版本。首先是常规图像。第二张是黑白图像,充当我们的面具。第三个是最终的蒙版图像。仅显示蒙版白色区域下的像素。
让我们花点时间浏览一下代码,弄清楚我们在做什么以及为什么做。
我们像处理所有脚本一样开始,先导入 OpenCV 和 NumPy,然后加载图像。
import cv2 import numpy as np img = cv2.imread("color_balls_small.jpg")
接下来,我们复制图像并将其转换到 HSV 颜色空间。
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
一旦进入 HSV 颜色空间,就更容易过滤蓝色球。正如我所讨论的,我们知道纯蓝色的色调值为 120。因为我们过滤的对象不太可能是纯蓝色的,所以我们需要给它一个颜色范围。在这种情况下,我们要寻找从 80(介于绿色和蓝色之间)到 130 的所有内容。我们还想过滤不是接近白色或接近黑色的颜色,所以我们使用值 120 和 255 作为饱和度和值范围。为了确保过滤器范围是 OpenCV 能够理解的格式,我们将它们创建为 NumPy 数组。
lower_blue = np.array([80,120,120]) upper_blue = np.array([130,255,255])
有了指定的滤镜范围,我们可以使用它们和inRange()
方法来确定图像的 HSV 版本中的像素是否在我们正在寻找的蓝色范围内。这将创建遮罩图像以排除所有非蓝色像素。
blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue)
接下来,我们使用bitwise_and()
来应用我们的面具。因为我们希望返回蒙版中图像的所有像素,所以我们将原始图像作为图像 1 和图像 2 传递。这将图像与其自身进行比较,并返回整个图像,因为图像中的每个像素都与其自身匹配。
res = cv2.bitwise_and(img, img, mask=blueMask)
最后,我们显示原始图像、遮罩和过滤后的图像。然后,在我们关闭窗口并退出程序之前,我们等待一个键被按下。
cv2.imshow('img', img) cv2.imshow('mask', blueMask) cv2.imshow('blue', res) cv2.waitKey(0)
正如你所看到的,一旦你知道它是如何工作的,过滤颜色是非常容易的。当你过滤红色时,事情变得有点复杂。红色出现在色调光谱的低端和高端,因此您必须创建两个滤镜并组合生成的遮罩。这可以很容易地用 OpenCV 的add()
方法来完成,它看起来像这样:
combinedMask = cv2.add(redMask1, redMask2)
最后,你得到的图像只有你想要的像素。对人眼来说,它很容易被识别为相关组。对于计算机来说,情况并非如此。本来,计算机不能识别黑色像素和蓝色像素之间的差异。这就是斑点检测发挥作用的地方。
斑点和斑点检测
斑点是相似像素的集合。它们可以是任何东西,从单调的圆形到 jpeg 图像。对计算机来说,像素就是像素,它无法区分球的图像和平面的图像。这就是计算机视觉如此具有挑战性的原因。我们已经开发了许多不同的技术来尝试推断关于图像的信息;每一种都在速度和准确性方面有所取舍。
大多数技术使用称为特征提取的过程,特征提取是一组算法的总称,这些算法对图像中的突出特征进行分类,如线条、边缘、大范围颜色等。一旦提取了这些特征,就可以对它们进行分析或与其他特征进行比较,以确定图像。这就是面部检测和运动检测等功能的工作原理。
我们将使用一种更简单的方法来跟踪一个对象。我们将使用上一节中的颜色过滤技术来识别大面积的颜色,而不是提取细节特征并进行分析。然后,我们将使用内置函数来收集关于像素组的信息。这种更简单的技术被称为斑点检测。
找到一个斑点
OpenCV 使得斑点检测变得相当容易,尤其是在我们过滤掉所有我们不想要的东西之后。一旦图像被过滤,我们可以使用掩模进行干净斑点检测。OpenCV 的SimpleBlobDetector
类识别斑点的位置和大小。
这个类并不像你想象的那么简单。它内置了许多需要启用或禁用的参数。如果启用,您需要确保这些值适用于您的应用。
设置参数的方法是SimpleBlobDetector_Params()
。创建检测器的方法是SimpleBlobDetector_create()
。您将参数传递给 create 方法,以确保一切设置正确。
一旦设置好参数并正确创建了检测器,就可以使用detect()
方法来识别关键点。在简单斑点检测器的情况下,关键点表示任何检测到的斑点的中心和大小。
最后,我们使用drawKeyPoints()
方法在斑点周围画一个圆。默认情况下,这会在斑点的中心绘制一个小圆。然而,可以传递一个标志,使得圆的大小相对于斑点的大小。
让我们看一个例子。我们将使用之前练习中的过滤器代码,并添加斑点检测。在本练习中,我们过滤图像中的蓝色球。然后我们用蒙版找到球的中心,围绕它画一个圆。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
simple_blob_detect.py
。 - 输入以下代码:
import cv2 import numpy as np img = cv2.imread("color_balls_small.jpg") imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # setup parameters params = cv2.SimpleBlobDetector_Params() params.filterByColor = False params.filterByArea = False params.filterByInertia = False params.filterByConvexity = False params.filterByCircularity = False # create blob detector det = cv2.SimpleBlobDetector_create(params) lower_blue = np.array([80,120,120]) upper_blue = np.array([130,255,255]) blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue) res = cv2.bitwise_and(img, img, mask=blueMask) # get keypoints keypnts = det.detect(blueMask) # draw keypoints cv2.drawKeypoints(img, keypnts, img, (0,0,255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) cv2.imshow('img', img) cv2.imshow('mask', blueMask) cv2.imshow('blue', res) # print the coordinates and size of keypoints to terminal for k in keypnts: print k.pt[0] print k.pt[1] print k.size cv2.waitKey(0)
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
python simple_blob_detect.py
并按回车键。
这将打开图像的三个版本。但是,原始图像现在有一个红色的圆圈围绕着蓝色的球。在终端窗口中,我们打印了球的中心坐标和大小。在本章的后面,当我们开始跟踪球时,会用到球的中心。
参数
SimpleBlobDetector
类需要几个参数才能正常工作。强烈建议通过将相应的参数设置为 True 或 False 来显式启用或禁用所有过滤器选项。如果启用了过滤器,您还需要为其设置参数。默认参数被配置为提取黑色圆形斑点。
在上一个练习中,我们简单地禁用了所有过滤器。由于我们正在处理一个球的过滤图像,并且我们在图像中只有一个斑点,我们不需要添加其他过滤器。虽然从技术上讲,您可以单独使用 SimpleBlobDetector 的参数,而不屏蔽所有其他的参数,但是在拨入所有参数以获得我们想要的结果时,这可能会更具挑战性。此外,我们使用的方法允许您更深入地了解 OpenCV 在后台做什么。
理解 SimpleBlobDetector 的工作方式对于更好地理解如何使用过滤器是很重要的。有几个参数可用于微调结果。
首先发生的是通过应用阈值将图像转换成几个二值图像。minThreshold
和maxThreshold
决定整体范围,而thresholdStep
决定阈值之间的距离。
然后使用findContours()
对每个二进制图像进行轮廓处理。这允许系统计算每个斑点的中心。已知中心后,使用minDistanceBetweenBlobs
参数将几个斑点组合成一组。
组的中心作为关键点返回,组的总直径也是如此。计算每个滤波器的参数并应用滤波器。
过滤器
下面列出了过滤器及其相应的参数。
filterByColor
这将过滤每个二进制图像的相对强度。它测量斑点中心的强度值,并将其与参数blobColor
进行比较。如果它们不匹配,则该斑点不合格。强度从 0 到 255 测量;0 为暗,255 为亮。
过滤区域
当单个斑点被分组时,计算它们的总面积。该过滤器寻找minArea
和maxArea
之间的斑点。
过滤循环
圆度通过公式
)
计算
这将返回一个介于 0 和 1 之间的比率,该比率将与minCircularity
和maxCircularity
进行比较。如果该值在这些参数之间,则结果中包含该斑点。
过滤惯性
惯性是对斑点被拉长程度的估计。它是一个介于 0 和 1 之间的比率。如果值在minInertiaRatio
和maxInertiaRatio
之间,则在关键点结果中返回斑点。
filterby 凸性
凸度是一个介于 0 和 1 之间的比值。它测量斑点中凸曲线和凹曲线之间的比率。凸度参数为minConvexity
和maxConvexity
。
斑点跟踪
我们在上一节中看到,斑点中心的 x 和 y 坐标作为关键点的一部分返回,用于跟踪斑点。要跟踪 blob,您需要使用来自机器人摄像机的实时视频流,然后定义跟踪对您的项目意味着什么。最简单的跟踪形式是简单地用斑点移动生成的圆。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
blob_tracker.py
。 - 输入以下代码:
import cv2 import numpy as np cap = cv2.VideoCapture(0) # setup detector and parameters params = cv2.SimpleBlobDetector_Params() params.filterByColor = False params.filterByArea = True params.minArea = 20000 params.maxArea = 30000 params.filterByInertia = False params.filterByConvexity = False params.filterByCircularity = True params.minCircularity = 0.5 params.maxCircularity = 1 det = cv2.SimpleBlobDetector_create(params) # define blue lower_blue = np.array([80,60,20]) upper_blue = np.array([130,255,255]) while True: ret, frame = cap.read() imgHSV = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) blueMask = cv2.inRange(imgHSV,lower_blue,upper_blue) blur= cv2.blur(blueMask, (10,10)) res = cv2.bitwise_and(frame, frame, mask=blueMask) # get and draw keypoint keypnts = det.detect(blur) cv2.drawKeypoints(frame, keypnts, frame, (0,0,255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) cv2.imshow('frame', frame) cv2.imshow('mask', blur) for k in keypnts: print k.size if cv2.waitKey(1) & 0xff == ord('q'): break cap.release() cv2.destroyAllWindows()
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
sudo python blob_tracker.py
并按回车键。
两个窗口打开:一个显示用于过滤颜色的遮罩,另一个显示视频流。应该在斑点周围画一个圆。
我启用了filterByArea
和filterByCircularity
来确保我只得到球。您可能需要调整检测器的参数来微调过滤器。
追球机器人
你现在知道如何用安装在机器人上的网络摄像头跟踪一个斑点了。在第八章中,你学习了一种叫做 PID 控制器的算法。当我们将 PID 控制器与我们的球跟踪程序结合起来时会发生什么?
接下来,让我们给这个小机器人编程,让它去追逐它一直在追踪的那个蓝色的球。为此,您将使用您刚刚学到的关于斑点跟踪的知识以及您在第八章中学到的知识。
PID 控制器期望以偏离期望结果的形式输入。因此,我们需要从定义期望的结果开始。在这种情况下,目标只是将球保持在框架的中间。因此,我们的误差值将是中心的方差,这也意味着我们需要定义帧的中心。一旦我们定义了中心,偏差就是从中心的 x 位置减去球的 x 位置。我们也将从中心的 y 位置减去球的 y 位置。
现在我们可以使用两个 PID 控制器来保持球在框架的中心。第一个控制器操纵机器人。当球在 x 轴上运动时,偏差要么是负的,要么是正的。如果是积极的,转向左边。如果是负数,转向右边。同样,我们可以用 y 轴来控制机器人的速度。正 y 方差驱动机器人前进,而负 y 方差驱动机器人后退。
- 打开空闲的 IDE 并创建一个新文件。
- 将文件另存为
ball_chaser.py
。 - 输入以下代码:
import cv2 import numpy as np import time from Adafruit_MotorHAT import Adafruit_MotorHAT as amhat from Adafruit_MotorHAT import Adafruit_DCMotor as adamo # create motor objects motHAT = amhat(addr=0x60) mot1 = motHAT.getMotor(1) mot2 = motHAT.getMotor(2) mot3 = motHAT.getMotor(3) mot4 = motHAT.getMotor(4) motors = [mot1, mot2, mot3, mot4] # motor multipliers motorMultiplier = [1.0, 1.0, 1.0, 1.0, 1.0] # motor speeds motorSpeed = [0,0,0,0] # speeds speedDef = 100 leftSpeed = speedDef rightSpeed = speedDef diff= 0 maxDiff = 50 turnTime = 0.5 # create camera object cap = cv2.VideoCapture(0) time.sleep(1) # PID kp = 1.0 ki = 1.0 kd = 1.0 ballX = 0.0 ballY = 0.0 x = { 'axis':'X', 'lastTime':int(round(time.time()*1000)), 'lastError':0.0, 'error':0.0, 'duration':0.0, 'sumError':0.0, 'dError':0.0, 'PID':0.0} y = { 'axis':'Y', 'lastTime':int(round(time.time()*1000)), 'lastError':0.0, 'error':0.0, 'duration':0.0, 'sumError':0.0, 'dError':0.0, 'PID':0.0} # setup detector params = cv2.SimpleBlobDetector_Params() # define detector parameters params.filterByColor = False params.filterByArea = True params.minArea = 15000 params.maxArea = 40000 params.filterByInertia = False params.filterByConvexity = False params.filterByCircularity = True params.minCircularity = 0.5 params.maxCircularity = 1 # create blob detector object det = cv2.SimpleBlobDetector_create(params) # define blue lower_blue = np.array([80,60,20]) upper_blue = np.array([130,255,255]) def driveMotors(leftChnl = speedDef, rightChnl = speedDef, duration = defTime): # determine the speed of each motor by multiplying # the channel by the motors multiplier motorSpeed[0] = leftChnl * motorMultiplier[0] motorSpeed[1] = leftChnl * motorMultiplier[1] motorSpeed[2] = rightChnl * motorMultiplier[2] motorSpeed[3] = rightChnl * motorMultiplier[3] # set each motor speed. Since the speed can be a # negative number, we take the absolute value for x in range(4): motors[x].setSpeed(abs(int(motorSpeed[x]))) # run the motors. if the channel is negative, run # reverse. else run forward if(leftChnl < 0): motors[0].run(amhat.BACKWARD) motors[1].run(amhat.BACKWARD) else: motors[0].run(amhat.FORWARD) motors[1].run(amhat.FORWARD) if (rightChnl > 0): motors[2].run(amhat.BACKWARD) motors[3].run(amhat.BACKWARD) else: motors[2].run(amhat.FORWARD) motors[3].run(amhat.FORWARD) def PID(axis): lastTime = axis['lastTime'] lastError = axis['lastError'] # get the current time now = int(round(time.time()*1000)) duration = now-lastTime # calculate the error axis['sumError'] += axis['error'] * duration axis['dError'] = (axis['error'] - lastError)/duration # prevent runaway values if axis['sumError'] > 1:axis['sumError'] = 1 if axis['sumError'] < -1: axis['sumError'] = -1 # calculate PID axis['PID'] = kp * axis['error'] + ki * axis['sumError'] + kd * axis['dError'] # update variables axis['lastError'] = axis['error'] axis['lastTime'] = now # return the output value return axis def killMotors(): mot1.run(amhat.RELEASE) mot2.run(amhat.RELEASE) mot3.run(amhat.RELEASE) mot4.run(amhat.RELEASE) # main program try: while True: # capture video frame ret, frame = cap.read() # calculate center of frame height, width, chan = np.shape(frame) xMid = width/2 * 1.0 yMid = height/2 * 1.0 # filter image for blue ball imgHSV = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) blueMask = cv2.inRange(imgHSV, lower_blue, upper_blue) blur = cv2.blur(blueMask, (10,10)) res = cv2.bitwise_and(frame,frame,mask=blur) # get keypoints keypoints = det.detect(blur) try: ballX = int(keypoints[0].pt[0]) ballY = int(keypoints[0].pt[1]) except: pass # draw keypoints cv2.drawKeypoints(frame, keypoints, frame, (0,0,255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) # calculate error and get PID ratio xVariance = (ballX - xMid) / xMid yVariance = (yMid - ballY) / yMid x['error'] = xVariance/xMid y['error'] = yVariance/yMid x = PID(x) y = PID(y) # calculate left and right speeds leftSpeed = (speedDef * y['PID']) + (maxDiff * x['PID']) rightSpeed = (speedDef * y['PID']) - (maxDiff * x['PID']) # another safety check for runaway values if leftSpeed > (speedDef + maxDiff): leftSpeed = (speedDef + maxDiff) if leftSpeed < -(speedDef + maxDiff): leftSpeed = -(speedDef + maxDiff) if rightSpeed > (speedDef + maxDiff): rightSpeed = (speedDef + maxDiff) if rightSpeed < -(speedDef + maxDiff): rightSpeed = -(speedDef + maxDiff) # drive motors driveMotors(leftSpeed, rightSpeed, driveTime) # show frame cv2.imshow('frame', frame) cv2.waitKey(1) except KeyboardInterrupt: killMotors() cap.release() cv2.destroyAllWindows()
- 保存文件。
- 打开终端窗口。
- 导航到保存文件的文件夹。
- 输入
sudo python ball_chaser.py
并按回车键。
几秒钟后,你的机器人应该开始向前移动。如果框架内有一个蓝色的球,它应该转向它。机器人试图将球保持在框架的中心。
这段代码中的一些内容与我们过去的做法有些不同。最值得注意的是,我们将 x 和 y 轴的值放入字典中。我们这样做是为了在将值传递给 PID 控制器时将它们保持在一起,这是所做的另一项更改。PID 函数已更新为接受单个参数。然而,它所期望的参数是一个字典。它被赋给函数中的轴变量。然后所有的变量引用都被更新以使用字典。结果在 axis 字典中更新,然后分配给主程序中的适当字典。
我还确保消除任何会影响主循环或相机刷新率的延迟。因为整个程序是在单个进程中运行的,所以它没有我们将进程分解成不同线程时那么快。因此,机器人可能会错过球并跑偏。
摘要
在这一章中,我们开始利用 Raspberry Pi 提供的一些令人兴奋的功能。与单独使用微控制器相比,计算机视觉允许我们执行更复杂的任务。
为了准备使用视觉系统,我们在机器人上安装了一个基本的网络摄像头。这需要特别考虑,因为这些网络摄像头不是为安装而设计的。当然,你的解决方案可能与我的不同,所以你可以在安装相机时发挥一些创造力。之后,我们准备安装 OpenCV。
OpenCV 是一个开源社区开发的计算机视觉平台,它使许多视觉功能变得非常简单。在 Raspberry Pi 上安装软件需要相当长的时间,主要是因为我们必须从源代码编译它,尽管它的功能令人印象深刻,但 Raspberry Pi 没有笔记本电脑或 PC 的处理能力,所以编译代码需要一段时间。但是一旦编译和安装,我们就能做一些有趣的事情。
我们用静止图像做了一些练习。这让我们可以学习 OpenCV 的一些基础知识,而不需要处理视频。一旦我们学会了一些基础知识,我们就学会了从摄像机中提取现场视频,并运用我们在静态图像中学到的知识。使用我们在这一章学到的颜色过滤和斑点追踪技术,我们给了我们的机器人看见和跟随一个球的能力。
今天的文章 树莓派和 Arduino 机器人入门手册(三)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/79334.html