本机器人使用了上位机+下位机的架构(大小脑架构),上位机可以为树莓派、Jetson Nano、Orin Nano或其它带有类似40PIN接口的单板电脑,下位机使用ESP32来控制机器人外设、读取传感器信息、电机闭环速度控制(PID控制器)。
上位机与下位机使用 JSON 格式的指令通过串口进行通信,具体的通信内容可以参考下位机的文档,目前作为新手不需要了解那些指令,初期只需要跟随教程文档调用常见的指令或封装好的函数即可。
Jupyter notebooks(.ipynb) 是一种文档,它结合了可直接运行的代码和叙述性文本(Markdown)、方程式(LaTeX)、图片、交互式可视化以及其他丰富的输出内容。
JupyterLab 有两种工作模式,一种是COMMAND模式,一种是EDIT模式
当你在 COMMAND 模式下时,你可以快速地进行笔记本的整体操作,比如增加或删除单元格、移动单元格、更改单元格类型等。在这个模式下,单元格边框呈现灰色。你可以通过按 Esc 键进入 COMMAND 模式。
EDIT 模式允许你在单元格中输入或修改代码或文本。在这个模式下,单元格边框为蓝色。你可以通过在选中的单元格中点击或按下 Enter 键来进入 EDIT 模式。
在 JupyterLab 中,你可以执行以下操作:
选择了正确的 kernel 之后,就可以运行 notebook 里面的代码块了。在 JupyterLab 中,代码块是 notebook 的基本组成部分。运行代码块的操作如下:
print("test text in jupyterlab")
for i in range(0, 10):
print(i)
通过这些基础的操作方法,你可以有效地使用 JupyterLab 来完成各种任务。更多高级功能和详细的使用指南可以在 JupyterLab 的官方文档中找到。
在本章节中我们会写一个Python例程,用于控制机器人底盘运动,你也可以自行使用其它语言来进行机器人底盘的运动控制。
在本例程中,我们使用 JupyterLab 中的代码块,生成一串 JSON 指令,通过 Jetson 的 GPIO 串口(默认与下位机通信的波特率为115200),将这个 JSON 指令发送给下位机,下位机收到指令后开始执行动作。
你可以参考后续的章节来了解都可以给下位机发送什么样的指令,你也可以使用其它语言来实现这一功能,或者自己写一个上位机的应用。
我们使用上位机+下位机的架构可以充分解放上位机的宝贵资源,上位机(树莓派,Jetson 等 SBC)类似人类的大脑,ESP32作为下位机类似人类的小脑,上位机执行视觉处理/决策方面的高阶控制,下位机执行具体的运动控制/插值等低阶控制。这样可以做到大小脑分工合作,下位机负责高频PID控制可保证车轮转速准确,上位机也不需要在这类低复杂度高算力的工作上浪费资源。
项目文件夹中的 app.py,这个是产品的主程序,当你执行过 autorun.sh 后(产品默认出厂是已经配置好自动运行的了),app.py 会在未来每次开机时自动运行,它的运行会占用 GPIO串口 和 摄像头资源,如果你在交互式教程中或者其它程序中需要用到这些资源可能会引发冲突或其它错误,二次开发或学习前,务必关闭掉 app.py 的自动启动。
由于 app.py 中使用了多线程,且开机时它使用 crontab 来自动运行,所以通常不能使用 sudo killall python 这样的指令来关闭 app.py, 你需要在 crontab 中注释掉运行 app.py 的那一行然后再重启机器人产品。
crontab -e
首次使用该命令后,会询问你希望使用什么编辑器来打开这个 crontab 文件,推荐选择 nano,输入 nano 对应的序号即可,然后按回车键确认。
用 "#" 注释掉 ...... app.py 这一行
# @reboot ~/ugv_pt_rpi/ugv-env/bin/python ~/ugv_pt_rpi/app.py >> ~/ugv.log 2>&1
@reboot /bin/bash ~/ugv_pt_rpi/start_jupyter.sh >> ~/jupyter_log.log 2>&1
注意:千万不要注释掉 start_jupyter.sh 这一行,否则开机后你将不能使用 jupyterLab 使用交互式教程。
然后退出并保存变更,具体方法为,编辑 crontab 的内容后,按 ctrl + x,退出 nano,由于你编辑过 crontab 文件了,所以它会问你是否保存变更(Save modified buffer?),输入字母 Y,然后按回车退出,即可保存变更。
再次重启设备开机后产品主程序就不会自动运行了,你可以随意使用 JupyterLab 中的教程了,后续如果你需要再恢复主程序开机自动运行时,可以再使用上面的方法打开 crontab 问价,然后删除掉 @ 前面的 '# ' 符号,退出并保存变更,这样就能恢复主程序的开机自动运行了。
在下面的例程中,你需要使用正确的 GPIO 设备名称,且使用与下位机相同的波特率(默认为115200)。
运行以下代码块之前你需要先将产品架高起量,保持驱动轮全部离地,调用以下代码块后机器人会开始走动,小心不要让机器人从桌面上掉落。
from base_ctrl import BaseController
import time
base = BaseController('/dev/ttyTHS0', 115200)
# 轮子以0.2m/s的速度转动2秒钟后停止
base.send_command({"T":1,"L":0.2,"R":0.2})
time.sleep(2)
base.send_command({"T":1,"L":0,"R":0})
通过调用上面的代码块,Jetson 会首先发送 {"T":1,"L":0.2,"R":0.2} 这条指令(后面章节我们会再具体介绍指令的构成),车轮开始转动,间隔两秒钟后 Jetson 会发送 {"T":1,"L":0,"R":0} 这条指令,车轮会停止转动,这里需要注意的一点是,即使不发送后面的停止车轮转动的指令,如果你没有发送新的指令,车轮依然会停止转动,这是因为下位机内含有心跳函数,心跳函数的做用是在上位机长时间没有新的指令发送给下位机时,下位机自动停止目前的移动指令,改函数的目的是为了避免上位机由于某些原因死机而导致下位机继续运动。
如果你希望机器人一直持续不断地运动下去,上位机需要每隔2秒-4秒循环发送运动控制的指令。
你可能会发现当你输入上面的运动控制指令后,机器人车轮的转动方向或者转动速度并不符合预期,那时因为在控制底盘前,你需要设置底盘的类型(每次执行产品主程序 app.py 时都会自动配置底盘类型,配置参数被存储在 config.yaml 中),这样底盘才会按照正确的参数来进行电机控制(默认每次底盘重新上电都需要由上位机配置一次),你可以向底盘发送以下命令来设置底盘的类型:
其中“main”的值为底盘类型:
“module”的值为模块类型:
使用案例如下,例如你使用的是安装有 云台 的 UGV Rover 产品,你可以通过向下位机发送下面这条指令来设置底盘类型:
{"T":900,"main":2,"module":0} 或 {"T":900,"main":2,"module":2}
你可以根据自己手中产品的类型,更改下面代码块中的JSON参数,来配置底盘类型:
base.send_command({"T":900,"main":2,"module":0})
然后再执行下面的代码块控制底盘,轮子的转动方向和速度就是正确的了。
base.send_command({"T":1,"L":0.2,"R":0.2})
time.sleep(2)
base.send_command({"T":1,"L":0,"R":0})
上面的例程中,你可以控制机器人向前走两秒钟后停止,后续可以通过更改参数来对底盘进行转向控制,底盘采用差速转向原理进行运动控制。
当车辆转弯时,内侧轮(转向方向相同的那一侧)需要行进更短的距离,因此需要旋转得更慢,以保持车辆的稳定性。 差速器通过允许两个驱动轮以不同速度旋转来实现这一目标。通常情况下,外侧轮(转向方向相反的那一侧)旋转得更快,而内侧轮旋转得更慢。 这种不同的旋转速度导致车辆产生转向运动,从而使其沿着预期的方向转向。
你可以给两侧车轮不同的目标线速度来控制车辆的转向,并且可以轻松地调整转向半径。
带有云台的产品上含有两个舵机,分别为水平舵机和倾斜舵机。水平舵机控制云台水平平面的转动,转动范围 ±180°(总范围360°);倾斜舵机控制云台竖直平面的转动,转动范围 -45°~90°(总范围 135°)。
对于没有配有云台的产品,也可以自行在 RaspRover 上扩展云台。
# 导入用于控制底盘的库
from base_ctrl import BaseController
base = BaseController('/dev/ttyTHS0', 115200)
在上面的代码块我们导入并实例化用于底盘控制的库,接下来通过改变云台水平舵机和倾斜舵机的角度来控制云台运动。
改变以下代码中的值,以下代码中:
运行以下代码,可以看见云台会向右且向上分别运动 45° 后停止。
input_x = 45
input_y = 45
input_speed = 0
input_acc = 0
base.gimbal_ctrl(input_x, input_y, input_speed, input_acc)
除了可以通过改变云台中两个舵机的转动角度来控制云台运动,还可以直接控制云台连续运动。
改变以下代码中的值,以下代码中:
其中,只有input_x 和 input_y 的值都为 2 时,云台会自动回归中位所处的位置。
运行以下代码,云台会向左运动,直到运动到 180° 时停止。
input_x = -1
input_y = 0
input_speed = 0
base.gimbal_base_ctrl(input_x, input_y, input_speed)
运行以下代码,云台会向上运动,运动到 90° 时停止。
input_x = 0
input_y = 1
input_speed = 0
base.gimbal_base_ctrl(input_x, input_y, input_speed)
最后,运行以下代码让云台回归至中位。
input_x = 2
input_y = 2
input_speed = 0
base.gimbal_base_ctrl(input_x, input_y, input_speed)
WAVE ROVER和UGV系列产品,驱动板上集成了两路12V开关控制接口(实际最高电压会随着电池电压而改变),分别为由ESP32的IO4和IO5引脚通过MOS管来控制,每路对应有两个接口,一共4个12V开关控制接口,按照默认的组装方式,IO4控制底盘大灯(WAVE ROVER没有底盘大灯),IO5控制头灯,你可以通过向下位机发送相应的指令来控制这两路接口的开关以及控制电压高低(但由于MOS管控制本身存在一定的延时所以ESP32的IO输出的PWM与实际接口输出的电压不成线性关系)。
对于没有配有LED灯的产品,你可以自行在这两路12V开关控制接口上扩展耐压12.6V的LED灯(一般情况下耐压12V的也可以,因为为了安全和保护电池,产品的UPS不会将电池电量充到12V以上),你也可以在剩余的开关控制接口上扩展其它外设——例如耐压12V的水弹枪波箱,直接连接在IO5控制的接口上,即可实现自动瞄准和射击的功能。
如果需要运行代码块内的程序,你可以选中需要运行的代码块,然后按Ctrl+Enter来运行程序,如果你在手机或平板电脑上使用JupyterLab,你可以按上方的三角形播放按键来运行代码块。
在上面的代码块中我们导入并实例化了用于控制底盘的库,接下来控制IO4接口的开关,变量IO4_PWM为ESP32的IO4引脚输出的PWM,变量范围为0-255,当此变量值为0时,IO4控制的接口开关关闭;当此变量为255时,IO4控制的接口开关输出的电压接近UPS的BAT电压(当前UPS内三节锂电池串联的电压)。
运行以下代码块开启IO4控制的接口开关(亮起底盘大灯)。 注意: WAVE ROVE 没有底盘大灯,所以运行以下代码块是没有变化的,需要运行再下一个开启头灯的代码块才会开启头灯,头灯位于摄像头云台上,如果产品没有配有摄像头云台则没有头灯。
IO4_PWM = 255
IO5_PWM = 0
base.lights_ctrl(IO4_PWM, IO5_PWM)
运行以下代码块开启IO5控制的接口开关(亮起云台头灯)。 注意:如果产品没有配有摄像头云台则没有头灯。
IO4_PWM = 255
IO5_PWM = 255
base.lights_ctrl(IO4_PWM, IO5_PWM)
如果你的产品带有LED灯,到目前为止应该已经全部亮起了,你可以运行以下代码块来降低LED灯的亮度。
IO4_PWM = 64
IO5_PWM = 64
base.lights_ctrl(IO4_PWM, IO5_PWM)
最后,运行以下代码块来关闭LED灯。
base.lights_ctrl(0, 0)
这里运行一个整合了云台功能的代码块。
import time
base.gimbal_ctrl(0, 0, 0, 0)
base.lights_ctrl(255, 255)
time.sleep(0.3)
base.gimbal_ctrl(45, 0, 0, 0)
base.lights_ctrl(0, 0)
time.sleep(0.3)
base.gimbal_ctrl(-45, 0, 0, 0)
base.lights_ctrl(255, 255)
time.sleep(0.3)
base.gimbal_ctrl(0, 90, 0, 0)
base.lights_ctrl(0, 0)
time.sleep(0.3)
base.gimbal_ctrl(0, 0, 0, 0)
本教程介绍了如何通过JSON指令控制连接到ESP32模组的OLED显示屏。OLED显示屏是一种常用的显示设备,可以用于显示各种信息,如文本、图像等。
OLED显示屏通过I2C(IIC)接口与下位机ESP32模组进行通信。它可以显示自定义的文本内容,并支持多行显示。
产品上配有一个 OLED 显示屏,该显示屏通过 IIC 与下位机 ESP32 模组进行通信,当开机后会自动显示一些下位机的基础信息,上位机可以通过发送 JSON 指令来更改显示屏上显示的内容。
lineNum 是行数,一条 JSON 指令可以改变一行的内容,下位机收到一条新的指令后,开机默认的 OLED 界面会消失,取而代之的是你新增加的内容,对于大部分产品使用的 0.91英寸的OLED显示屏,lineNum 的数值可以为 0、1、2、3,共四行;Text 是你希望在这一行显示的文字内容,如果你这一行的内容太多,会自动换行,但同时也会挤掉最后一行。
from base_ctrl import BaseController
base = BaseController('/dev/ttyTHS0', 115200)
# 更改 OLED 上面的显示内容
base.send_command({"T":3,"lineNum":0,"Text":"this is line0"})
base.send_command({"T":3,"lineNum":1,"Text":"this is line1"})
base.send_command({"T":3,"lineNum":2,"Text":"this is line2"})
base.send_command({"T":3,"lineNum":3,"Text":"this is line3"})
运行上面的代码块后,OLED上面会显示出来四行文字:
this is line0
this is line1
this is line2
this is line3
在上面的教程中我们实现了在 OLED 屏幕上显示简单文字的方法,接下来我们写一个稍微复杂一些的例程,运行以下代码块后,OLED 屏幕上会显示当前的时间(但是由于树莓派的时间不一定是准确的所以和你当地的实际时间可能会有出入),本例程仅用于演示主程序中更新屏幕的方法,在产品主程序中,我们使用这样的方法来将产品的IP、运行状体等信息即时更新在 OLED 屏幕上。
# 导入 datetime 模块中的 datetime 类,用于获取和处理当前日期和时间
from datetime import datetime
# 导入 time 模块,主要用于程序中的延时处理
import time
# 使用 while True 创建一个无限循环,使程序能够持续运行
while True:
# 使用 datetime.now().strftime("%H:%M:%S") 获取当前时间,并将其格式化为 "小时:分钟:秒" 的形式
current_time = datetime.now().strftime("%H:%M:%S")
# 使用 base.send_command 方法发送一个包含当前时间的命令
base.send_command({"T":3,"lineNum":0,"Text":current_time})
# 使用 time.sleep(1) 让程序暂停1秒,这样可以确保每隔一秒更新一次时间,并发送一次命令
time.sleep(1)
运行上面的代码块后,你可以看到 OLED 屏幕的第一行开始显示当前的时间,每秒钟更新一次,上面这条函数是无限循环的,你可以通过点击上面的 ■ 按键来终止程序运行。
在JupyterLab中构建UI界面通常使用的是ipywidgets库,它提供了一种简单而强大的方法来创建交互式用户界面。下面是详细的步骤:
在我们的产品中已经安装了 ipywidgets 库。如果你运行代码块时提示找不到这个库,可以通过 pip install ipywidgets 来安装实现 UI 界面中所需的库。
选中以下代码块,按 Ctrl + Enter 运行该代码。
import ipywidgets as widgets
from IPython.display import display
我们可以使用ipywidgets库中的各种组件来构建我们的UI界面,比如文本框、按钮、输出框等。例如:
# 创建一个文本框
text = widgets.Text(description='请输入名字:')
# 创建一个按钮
button = widgets.Button(description="打招呼")
# 创建一个输出框
output = widgets.Output()
我们需要定义一个处理函数,用于处理用户交互事件。在本例中,我们将定义一个函数来处理按钮的点击事件,并在输出框中显示问候语。
# 定义一个函数 greet_user,该函数接受一个参数 sender,sender 表示触发事件的对象,比如一个按钮
def greet_user(sender):
# 使用 with 语句和 output 对象来捕获 print 函数的输出,使其显示在预期的输出区域
# output 是已经定义好的输出对象
with output:
# 使用 print 函数输出问候语,其中使用 format 方法将 text 控件的当前值插入到字符串中
# "{}" 是一个占位符,format 函数会将其替换为 text.value 的值
print("你好,{}".format(text.value))
# 使用 on_click 方法将按钮的点击事件与 greet_user 函数关联起来
# 当用户点击这个按钮时,将调用 greet_user 函数
button.on_click(greet_user)
最后,我们将所有的UI组件放在一个布局中,并通过display函数显示出来。
# 将所有组件放在一个垂直布局中
ui = widgets.VBox([text, button, output])
# 显示UI界面
display(ui)
通过这些步骤,我们就可以在JupyterLab中构建一个简单的UI界面了。用户可以在文本框中输入内容,点击按钮后,程序会根据输入内容在输出框中显示相应的问候语。
产品开机后,下位机默认会持续向上位机反馈各类信息,你可以通过这些反馈信息来获取目前产品的工作状态。
通常来说,你需要连续获取下位机反馈的信息,但在本例程中,我们只获取一个由下位机反馈的 JSON 信息(注释掉或删除 break 这行即可连续获取反馈信息)。
选中以下的代码块,使用 Ctrl + Enter 运行该代码块,当它获取第一条完整的且T值为 1001 的 JSON 信息后会跳出循环并输出反馈信息,反馈信息包含当前车轮转速、IMU、云台角度(如果有安装的话)、机械臂角度(如果有安装的话)、电源电压等信息。
from base_ctrl import BaseController
import json
base = BaseController('/dev/ttyTHS0', 115200)
# 使用无限循环来不断监听串口数据
while True:
try:
# 从串口读取一行数据,解码成 'utf-8' 格式的字符串,并尝试将其转换为 JSON 对象
data_recv_buffer = json.loads(base.rl.readline().decode('utf-8'))
# 检查解析出的数据中是否包含 'T' 键
if 'T' in data_recv_buffer:
# 如果 'T' 的值为 1001,则打印接收到的数据,并跳出循环
if data_recv_buffer['T'] == 1001:
print(data_recv_buffer)
break
# 如果在读取或处理数据时发生异常,则忽略该异常并继续监听下一行数据
except:
pass
以下的代码仅用于理解底层的串口读取 JSON 信息的原理,不可以执行下面的代码块。
class ReadLine:
# 构造函数,初始化 ReadLine 类的实例
# s: 传入的串口对象,用于与串口进行通信。
def __init__(self, s):
self.buf = bytearray() # 初始化一个字节数组,用于存储从串口读取但尚未处理的数据
self.s = s # 保存传入的串口对象,后续用于读取串口数据
def readline(self):
i = self.buf.find(b"\n") # 查找缓冲区中是否有换行符
if i >= 0:
r = self.buf[:i+1] # 如果有换行符,将换行符前的数据提取出来
self.buf = self.buf[i+1:] # 更新缓冲区,去除已处理的数据
return r
while True:
i = max(1, min(512, self.s.in_waiting)) # 获取可读取的字节数,最多512个字节
data = self.s.read(i) # 从串口读取数据
i = data.find(b"\n") # 查找换行符
if i >= 0:
r = self.buf + data[:i+1] # 如果找到换行符,将已读取的数据与缓冲区中的数据合并
self.buf[0:] = data[i+1:] # 更新缓冲区,去除已处理的数据
return r
else:
self.buf.extend(data) # 如果未找到换行符,将数据添加到缓冲区
本产品使用大小脑架构开发,上位机通过串口(Jetson 通过 GPIO 串口)将 JSON 格式的命令发送给下位机。 注意:本章课程作为后面介绍下位机JSON指令集的前置课程,内容于前面的 Python 底盘运动控制章节的内容相似,如果你已知了解过那个章节,可以简单了解下 JSON 数据格式的优点后直接学习 JSON 指令集。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它已经成为互联网上数据传输的标准之一。以下是JSON的一些优势:
在下面的例程中,你需要使用正确的 GPIO 设备名称,且使用与下位机相同的波特率(默认为115200)。
运行以下代码块之前你需要先将产品架高起量,保持驱动轮全部离地,调用以下代码块后机器人会开始走动,小心不要让机器人从桌面上掉落。
from base_ctrl import BaseController
import time
base = BaseController('/dev/ttyTHS0', 115200)
# 轮子以0.2m/s的速度转动2秒钟后停止
base.send_command({"T":1,"L":0.2,"R":0.2})
time.sleep(2)
base.send_command({"T":1,"L":0,"R":0})
通过调用上面的代码块,Jetson 会首先发送 {"T":1,"L":0.2,"R":0.2} 这条指令(后面章节我们会再具体介绍指令的构成),车轮开始转动,间隔两秒钟后 Jetson 会发送 {"T":1,"L":0,"R":0} 这条指令,车轮会停止转动,这里需要注意的一点是,即使不发送后面的停止车轮转动的指令,如果你没有发送新的指令,车轮依然会停止转动,这是因为下位机内含有心跳函数,心跳函数的做用是在上位机长时间没有新的指令发送给下位机时,下位机自动停止目前的移动指令,改函数的目的是为了避免上位机由于某些原因死机而导致下位机继续运动。
如果你希望机器人一直持续不断地运动下去,上位机需要每隔2秒-4秒循环发送运动控制的指令。
在上一个章节中我们介绍了一个简单的例程,在例程中我们通过上位机向下位机发送运动控制指令,下位机可以接收的指令非常多,在这个章节中我们会介绍这些指令。
以上一章节我们发送的 {"T":1,"L":0.2,"R":0.2} 指令为例,这个 JSON 数据中的 T 值代表指令的类型(Type),L 值代表左侧(LEFT)车轮的目标线速度,R 值代表右侧(RIGHT)车轮的目标线速度,线速度的单位默认为 m/s,总结起来的意思就是这是一条运动控制指令,运动控制的参数分别为左右两侧车轮的目标线速度。
后面的所有 JSON 指令,都会包含一个 T 值用于定义指令的类型,但是具体的指令参数会根据指令类型的不同而有所区别。
你可以在我们开源的下位机例程的 json_cmd.h 文件中查看这些指令的定义,或自行为其添加新的下位机功能。
这些指令是移动机器人最基础的指令,用于运动相关的功能控制。
下面的指令中,每种指令包含三个部分:案列,简短的介绍和详细的介绍。
L 和 R 分别代表左右侧车轮的目标线速度,单位为 m/s,负值代变反向转动,0 为停止。目标线速度的取值范围取决于产品所使用的电机/减速器/车轮直径,相关的计算公式可以在开源的下位机例程中找到。这里需要注意的是,对于使用碳刷直流电机的底盘来说,当给定的目标速度的绝对值很小时(但不为 0),由于碳刷直流电机的低速性能通常比较差,所以产品在移动过程中速度可能波动比较大。
L 和 R 分别代表左右两侧车轮的 PWM 值,该值的范围为 -255 ~ 255,负值代表反向,当数值的绝对值为 255 时代表 PWM 为 100%,代表让这一侧的车轮满功率运行。
该指令用于 ROS 上位机控制底盘移动的指令,X 代表移动的线速度,单位为 m/s 可以为负值;Z 代表转向的角速度,单位为 rad/s 可以为负值。
该指令用于给PID控制器调参,上面 JSON 案例中的 PID 参数为该产品的默认参数,其中 L 代表 WINDUP_LIMITS,是预留的接口,目前的产品中暂时还没有使用到该参数。
产品上安装有 OLED 显示屏,该显示屏通过 I2C 与下位机主控 ESP32 模组进行通信,上位机可通过发送 JSON 指令来更改显示屏上显示的内容。
lineNum 是行数,一条 JSON 指令可以改变一行的内容,下位机收到一条新的指令后,开机默认的 OLED 界面会消失,取而代之的是你新增加的内容,对于大部分产品使用的 0.91英寸的OLED显示屏,lineNum 的数值可以为 0、1、2、3,共四行;Text 是你希望在这一行显示的文字内容,如果你这一行的内容太多,会自动换行,但同时也会挤掉最后一行。
使用这条指令让 OLED 显示屏显示开机默认的画面。
移动底盘上面会安装不同类型的模块(无/机械臂/云台),使用这条指令来告诉下位机当前所安装的模块类型,这条指令通常会在上位机开机时自动发送给下位机,后面的章节会有这部分的介绍。
cmd 的值代表模块的类型,目前有三种类型可以选择,0:什么都不装,1:机械臂,2:云台
底盘上安装有 IMU 传感器,你可以通过以下命令来获取 IMU 传感器的数据,这里需要注意的是,产品开机后会默认开启底盘信息连续反馈功能(其中包含有IMU的信息),这里的 IMU 相关功能仅在底盘信息连续反馈功能关闭时才有必要使用。
发送该指令后可获取 IMU 的数据。
目前的产品程序不需要执行校准,此命令为预留接口。
使用该指令可反馈当前的 IMU 每个轴的偏移量。
使用该指令可以设置 IMU 每个轴的偏移量,此命令为预留接口,当前的产品不需要执行该指令。
产品开机后通常会默认开启底盘信息反馈,是自动的,如果底盘信息连续反馈功能关闭了,需要单次获取底盘的信息,可以使用该指令,可获取底盘的基础信息。
cmd的值设置为1,开启该功能,此功能默认开启,会持续反馈底盘信息;当cmd的值设置为0,关闭该功能,关闭该功能后,上位机可以通过 CMD_BASE_FEEDBACK 来获取底盘信息。
cmd的值为需要设置的格外间隔时间,单位为ms,通过该指令可以调整底盘反馈信息的频率。
当cmd的值设置为0时,关闭回声;当cmd的值设置为1时,开启回声,当开启指令回声模式后,下位机会输出接收到的指令。
cmd为0时,关闭WIFI功能;1-ap;2-sta;3-ap+sta。
产品下位机上面有两路12V开关接口,每路2个接口共四个接口,你可以通过这个指令来设置这些接口的输出电压,当数值为255时为3S电池电压。 产品默认使用这些接口来控制LED灯,你可以通过这个指令来控制LED灯的亮度。
该指令用于控制云台的朝向。 X为水平方向的朝向,单位为角度,正值向右,负值向左,取值范围为 -180到180。 Y为数值方向的朝向,单位为角度,正值向上,负值向下,取值范围为 -30到90。 SPD为速度,ACC为加速度,当值为0时为最快速度/加速度。
该指令用于连续控制云台朝向。 X为水平方向的朝向,单位为角度,正值向右,负值向左,取值范围为 -180到180。 Y为数值方向的朝向,单位为角度,正值向上,负值向下,取值范围为 -30到90。 SX和SY分别为X轴和Y轴的速度。
当使用上面的命令让云台运动起来后,可以使用该指令让云台可以随时停下来。
s为0时关闭该功能,s为1时开启该功能,该功能开启后云台会自动通过IMU数据来调整云台的竖直角度,y为云台的与地面的目标夹角(即使开启云台自稳功能,摄像头也可以上下看)。
该指令用于UI界面控制云台,X值可以为-1,0和1,-1为向左转动,0为停止,1为向右转动。 Y值可以为-1,0和1,-1为向下转动,0为停止,1为向上转动。 SPD为速度
正常情况下机械臂开机时会自动转动到初始位置。 该指令会引起进程阻塞。
该函数会引发阻塞
该函数不会引发阻塞
tor的值最高可以为1000,代表100%的力量。
cmd单位为ms,可以使用该指令设置心跳函数时间,如果下位机在该时间内没有接收到新的运动指令,会自动停止运动,用于避免使用过程中上位机死机导致下位机一直运动下去。 程中上位机死机导致下位机一直运动下去。
产品使用差速转向原理,当产品左右车轮给相同的目标速度时,产品有可能由于编码器误差或轮胎抓地力误差导致产品不走直线,你可以使用该指令来对左右侧车轮的速度进行微调,例如左边的车轮需要转动慢一些的话,可以将L的数值改为 0.98。L值和R值尽量不要设置大于一的数值。
使用该指令可以获取当前的速度比例
该功能属于下位机的高阶功能,正常带上位机使用时通常不需要进行以下的操作。
raw是舵机的原始ID(新舵机都是1),new是要更改为的ID,最大不超过254,不可为负,255为广播ID。
本章教程用于介绍上位机每次开机后会自动执行指令和向下位机发送一些指令,本章教程的代码块不需要执行(也不能执行),仅用于理解开机后产品的一些自动操作,如果你有需要的话,更改或增加这些指令。
cmd_on_boot() 函数位于产品主程序 app.py 中,这个函数每次开机时会被调用,你可以编辑这个函数来对开机时自动运行的指令进行调参/增加指令。
def cmd_on_boot():
# 定义启动时要执行的命令列表
cmd_list = [
'base -c {"T":142,"cmd":50}', # set feedback interval
'base -c {"T":131,"cmd":1}', # serial feedback flow on
'base -c {"T":143,"cmd":0}', # serial echo off
'base -c {"T":4,"cmd":2}', # select the module - 0:None 1:RoArm-M2-S 2:Gimbal
'base -c {"T":300,"mode":0,"mac":"EF:EF:EF:EF:EF:EF"}', # the base won't be ctrl by esp-now broadcast cmd, but it can still recv broadcast megs.
'send -a -b' # add broadcast mac addr to peer
]
# 遍历命令列表
for i in range(0, len(cmd_list)):
camera.cmd_process(cmd_list[i])
产品上位机可以通过命令行指令来进行一些功能方面的控制,类似上面的 base -c 指令,用于直接将后面写入的 JSON 指令通过 Jetson 的GPIO串口传递给下位机,后面我们会详细解释这里默认的开机自动运行的指令是什么意思。
用于设置下位机连续反馈信息的格外间隔时间,cmd的值的单位为ms,此功能用于降低下位机反馈信息的频率,目的是减轻上位机处理下位机反馈信息的算力压力。
开启下位机连续信息反馈功能,该功能开启后,不需要上位机一问一答地去获取下位机的信息,下位机正常会默认开启该功能,但我们这里还是再发送一次开启该功能的指令,比较保险。
关闭串口指令回声,这样上位机在向下位机发送指令时,下位机不会再将接收到的指令反馈给上位机,这样可以避免上位机处理无用的信息。
设置外置模块的类型,cmd的值为0时,代表没有外接模块;1,机械臂;2,云台,如果你的产品没有安装云台或机械臂,你需要把这里的数值改为0。
避免底盘通过其它设备的ESP-NOW广播控制,但是除mac地址的设备外,你可以自己随意编一个MAC地址,也可以使用你自己的ESP32遥控器的MAC地址。
将广播地址(FF:FF:FF:FF:FF:FF)添加到peer,这样你可以后续直接通过广播信号来将广播信息发送到其它设备。
其它的上位机命令行指令你可以通过后面的 WEB 命令行应用章节来了解。
由于安全方面的原因,你并不能通过 JupyterLab 来直接访问音频设备(环境的限制),我们这里的代码块不供用户运行。
这里的程序来自于产品主程序的 audio_ctrl.py,你可以参考这里的代码来了解产品主程序是如何实现音频文件播放功能的。
产品主程序的文件夹内有一个名称为 sounds 的文件夹,这个文件夹内有很多子文件夹:connected、others、recv_new_cmd、robot_started、searching_for_target、target_detected、target_locked。
在我们提供的默认程序中,只有分别在 connected 和 robot_started 中各放置了一个音频文件。
当机器人主程序运行起来后,会自动随机播放一个 robot_started 内的音频文件。
当有客户端使用浏览器连接到这个 WEB 应用时,会自动随机播放一个 connected 内的音频文件。
你可以在这些文件夹内放入自定义的音频文件当作语音包,来让你的产品更加定制化。
import pygame # 导入pygame库,用于音频播放
import random # 导入random库,用于随机选择音频文件
import yaml # 导入yaml库,用于读取配置文件
import os # 导入os库,用于文件操作
import threading # 导入threading库,用于多线程处理
# 获取配置文件
curpath = os.path.realpath(__file__) # 获取当前脚本的绝对路径
thisPath = os.path.dirname(curpath) # 获取当前脚本所在目录
with open(thisPath + '/config.yaml', 'r') as yaml_file: # 打开配置文件
config = yaml.safe_load(yaml_file) # 使用yaml库加载配置文件
# 初始化pygame.mixer,设置音频输出的默认音量
pygame.mixer.init()
pygame.mixer.music.set_volume(config['audio_config']['default_volume'])
# 创建一个事件对象,用于控制音频播放
play_audio_event = threading.Event()
# 从配置文件中读取最小播放间隔时间
min_time_bewteen_play = config['audio_config']['min_time_bewteen_play']
# 定义播放音频的函数
def play_audio(input_audio_file):
try:
pygame.mixer.music.load(input_audio_file) # 加载音频文件
pygame.mixer.music.play() # 播放音频
except:
play_audio_event.clear() # 出错时清除事件标志
return
while pygame.mixer.music.get_busy(): # 等待音频播放完成
pass
time.sleep(min_time_bewteen_play) # 等待最小播放间隔时间
play_audio_event.clear() # 清除事件
# 定义播放随机音频的函数
def play_random_audio(input_dirname, force_flag):
if play_audio_event.is_set() and not force_flag:
return
# 获取指定目录下的所有音频文件
audio_files = [f for f in os.listdir(current_path + "/sounds/" + input_dirname) if f.endswith((".mp3", ".wav"))]
# 从音频文件列表中随机选择一个音频文件
audio_file = random.choice(audio_files)
play_audio_event.set() # 设置事件
# 创建一个线程来播放音频
audio_thread = threading.Thread(target=play_audio, args=(current_path + "/sounds/" + input_dirname + "/" + audio_file,))
audio_thread.start() # 启动线程
# 定义播放音频的线程函数
def play_audio_thread(input_file):
if play_audio_event.is_set(): # 如果事件已经设置,则返回
return
play_audio_event.set() # 设置事件
# 创建一个线程来播放音频
audio_thread = threading.Thread(target=play_audio, args=(input_file,))
audio_thread.start() # 启动线程
# 定义播放指定文件的函数
def play_file(audio_file):
audio_file = current_path + "/sounds/" + audio_file # 构建音频文件的完整路径
play_audio_thread(audio_file) # 在新线程中播放音频
# 定义设置音频音量的函数
def set_audio_volume(input_volume):
input_volume = float(input_volume) # 将输入音量转换为浮点数
if input_volume > 1: # 如果音量大于1,则设置为1
input_volume = 1
elif input_volume < 0: # 如果音量小于0,则设置为0
input_volume = 0
pygame.mixer.music.set_volume(input_volume) # 设置音量
# 定义设置最小播放间隔时间的函数
def set_min_time_between(input_time):
global min_time_bewteen_play # 使用全局变量
min_time_bewteen_play = input_time # 设置最小播放间隔时间
由于安全方面的原因,你并不能通过 JupyterLab 来直接访问音频设备(环境的限制),我们这里的代码块不供用户运行。
这里的程序来自于产品主程序的 audio_ctrl.py,你可以参考这里的代码来了解产品主程序是如何执行文字转语音功能的。
import pyttsx3 # 导入 pyttsx3 库,用于文本转语音功能
import threading # 导入 threading 模块,用于创建线程
# 初始化 pyttsx3 引擎
engine = pyttsx3.init()
# 创建事件对象,用于控制语音播放的同步
play_audio_event = threading.Event()
# 设置引擎属性,这里设置的是语音播放的速度,数值越大语速越快
engine.setProperty('rate', 180)
# 定义函数,用于播放指定文本的语音
def play_speech(input_text):
engine.say(input_text) # 将文本输入到引擎中
engine.runAndWait() # 等待语音输出完成
play_audio_event.clear() # 清除事件,表示语音播放完成
# 定义函数,用于在新线程中播放语音
def play_speech_thread(input_text):
if play_audio_event.is_set(): # 如果已经有语音在播放中,则直接返回,不重复播放
return
play_audio_event.set() # 设置事件,表示有新的语音播放任务开始
# 创建新线程,调用 play_speech 函数来播放语音
speech_thread = threading.Thread(target=play_speech, args=(input_text,))
speech_thread.start() # 启动新线程,开始播放语音
这段代码使用了 pyttsx3 库来实现文本转语音的功能,并使用 threading 模块创建了一个线程来异步播放语音。play_speech() 函数用于在主线程中播放指定文本的语音,而 play_speech_thread() 函数用于在新线程中播放语音,以避免阻塞主线程。同时,通过 play_audio_event 控制语音播放的同步,确保同一时间只有一个语音在播放。
本章节介绍如何使用 Flask 建立一个 Web 应用,用于显示机器人摄像头的实时画面,由于 Web 应用具有可跨平台的特性,用户可以在手机/PC/平板等设备上通过浏览器来观看摄像头的实时画面,实现无线图传功能。
Flask 是一个轻量级的Web应用框架,用于使用 Python 快速构建Web应用。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
由于 Flask 应用会与 Jupyter Lab 在端口号的使用上产生冲突,所以以下代码不能在 Jupyter Lab 中运行,以下程序存储在 tutorial_cn 和 tutorial_en 中的名为 12 的文件夹内, 在 12 文件夹内还有一个名为 template 的文件夹用于存储网页资源,以下是例程的运行方法。
1. 用上文介绍的方式来打开终端,此时注意左侧的文件夹路径,新打开的终端默认的路径与左侧的文件路径相同,你需要浏览到 tutorial_cn 或 tutorial_en 文件夹内,打开终端后输入 cd 12 浏览到 12 文件夹内。
2. 使用以下命令来启动 Flask Web 应用服务端: python flask_camera.py
3. 然后在同一局域网内的设备(也可以是本设备在浏览器中打开一个新的标签页)中打开浏览器,输入Jetson的IP:5000(例如Jetson的IP是192.168.10.104的话,则打开192.168.10.104:5000这个地址),注意 : 需要为英文的冒号。
4. 在终端中使用 Ctrl + C 来结束运行。
from flask import Flask, render_template, Response # 从flask库导入Flask类,render_template函数用于渲染HTML模板,Response类用于生成响应对象
from picamera2 import Picamera2 # 从picamera2库导入Picamera2类,用于访问和控制摄像头
import time # 导入time模块,可以用来处理时间相关的任务
import cv2 # 导入OpenCV库,用于图像处理
app = Flask(__name__) # 创建Flask应用实例
def gen_frames(): # 定义一个生成器函数,用于逐帧生成摄像头捕获的图像
picam2 = Picamera2() # 创建Picamera2的实例
# 配置摄像头参数,设置视频的格式和大小
picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
picam2.start() # 启动摄像头
while True:
frame = picam2.capture_array() # 从摄像头捕获一帧图像
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
ret, buffer = cv2.imencode('.jpg', frame) # 将捕获的图像帧编码为JPEG格式
frame = buffer.tobytes() # 将JPEG图像转换为字节流
# 使用yield返回图像字节流,这样可以连续发送视频帧,形成视频流
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/') # 定义根路由
def index():
return render_template('index.html') # 返回index.html页面
@app.route('/video_feed') # 定义视频流路由
def video_feed():
# 返回响应对象,内容是视频流,内容类型是multipart/x-mixed-replace
return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True) # 启动Flask应用,监听所有网络接口上的5000端口,开启调试模式
gen_frames(): 这是一个生成器函数,不断从摄像头中捕获帧,将其编码为JPEG格式,并将帧字节作为多部分响应的一部分生成。生成的帧会被实时传输给客户端。
@app.route('/'): 这个装饰器将index()函数与根URL(/)关联起来。当用户访问根URL时,它将呈现名为'12_index.html'的HTML模板。
@app.route('/video_feed'): 这个装饰器将video_feed()函数与'/video_feed' URL关联起来。这个路由用于视频实时传输,帧会作为多部分响应发送。
app.run(host='0.0.0.0', port=5000, debug=True): 这一行启动Flask开发服务器,监听所有可用的网络接口(0.0.0.0)在端口5000上。debug=True选项启用服务器的调试模式。服务器的调试模式。
注释:
<!doctype html>: 声明HTML文档类型。
<html lang="en">: HTML文档的根元素,指定页面语言为英语。
<head>: 包含文档的元信息,如字符集和页面标题。
<!-- Required meta tags -->: HTML注释,提醒这是一些必需的元标签。
<meta charset="utf-8">: 指定文档使用UTF-8字符集。
<title>Live Video Based on Flask</title>: 设置页面标题。
<body>: 包含文档的可见部分。
<!-- The image tag below is dynamically updated with the video feed from Flask -->: HTML注释,说明下面的图像标签会动态更新,显示来自Flask的视频流。
<img src="{{ url_for('video_feed') }}">: 图像标签,使用Flask中定义的video_feed路由获取实时视频流。
在上一章中我们使用 Flask 来显示摄像头的实时画面,那个方法需要在浏览器中格外开启一个新标签页或者使用其它设备打开浏览器来访问,在本章教程中我们使用在 Jupyter Lab 中观看实时视频画面的方案。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
import matplotlib.pyplot as plt # 导入 matplotlib 库用于绘图
import cv2 # 导入 OpenCV 库用于图像处理
from picamera2 import Picamera2 # 导入 Picamera2 库用于访问 Raspberry Pi Camera
import numpy as np # 导入 NumPy 库用于数学计算
from IPython.display import display, Image # 导入 IPython 显示功能
import ipywidgets as widgets # 导入 ipywidgets 库用于创建交互式控件
import threading # 导入 threading 库用于多线程编程
# 创建一个切换按钮作为停止按钮
stopButton = widgets.ToggleButton(
value=False, # 按钮的初始状态为未选中
description='Stop', # 按钮上显示的文本
disabled=False, # 按钮初始为可用状态
button_style='danger', # 按钮样式为红色
tooltip='Description', # 鼠标悬停在按钮上时的提示信息
icon='square' # 按钮上显示的图标
)
# 定义一个函数用于显示视频流
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# 配置摄像头参数,设置视频的格式和大小
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建一个显示句柄,用于更新显示的内容
while True:
# frame = picam2.capture_array() # 从摄像头捕获一帧图像
_, frame = camera.read() # 从摄像头捕获一帧图像
# 如果需要,可以在这里对帧进行处理(例如翻转、颜色转换等)
_, frame = cv2.imencode('.jpeg', frame) # 将图像帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查停止按钮是否被按下
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None) # 清空显示的内容
# 显示停止按钮
display(stopButton)
# 创建并启动一个线程,目标函数是 view 函数,参数是停止按钮
thread = threading.Thread(target=view, args=(stopButton,))
thread.start() # 启动线程
本章教程基于上一章的教程,按照一定的时间间隔来从摄像头获取画面,并将其存储在 ugv_jetson 文件夹下的 /templates/pictures/ 文件夹内。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用CSI摄像头则需要注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。
你可以更改 time_intervel 的数值来更改拍照的间隔时间,单位为秒。你所拍摄的照片会被存储在/ugv_jetson/templates/pictures/文件夹内。
import cv2 # 导入 OpenCV 库,用于图像处理
from picamera2 import Picamera2 # 导入 Picamera2 库,用于访问 Raspberry Pi Camera
import numpy as np
from IPython.display import display, Image # 导入 IPython 显示功能
import ipywidgets as widgets # 导入 ipywidgets 库,用于创建交云端交互式控件
import threading # 导入 threading 库,用于多线程编程
import os, time # 导入 os 和 time 库,用于处理文件操作和时间相关的功能
# 在这里改变拍照间隔时间(秒)
time_intervel = 3 # 每隔3秒拍摄一次
# 设置图片保存的路径
# 可以在这里改变保存的路径
photo_path = '/home/ws/ugv_pt_rpi/static/'
# 创建一个切换按钮作为停止按钮
# ================
stopButton = widgets.ToggleButton(
value=False, # 按钮的初始状态为未选中
description='Stop', # 按钮上显示的文本
disabled=False, # 按钮初始为可用状态
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description', # 鼠标悬停在按钮上时的提示信息
icon='square' # 按钮图标(FontAwesome 名称,不带 `fa-` 前缀)
)
# 定义一个函数,用于显示视频流和定时拍照
# ================
def view(button):
last_picture_time = time.time() # 记录上一次拍照的时间
num_count = 0 # 初始化拍照计数器
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# 配置摄像头参数,设置视频的格式和大小
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建一个显示句柄,用于更新显示的内容
i = 0
while True:
# frame = picam2.capture_array()
# frame = cv2.flip(frame, 1) # 翻转图像
_, frame = camera.read() # 从摄像头捕获一帧图像
# 每隔几秒拍一张照片
if time.time() - last_picture_time >= time_intervel:
num_count = num_count + 1 # 更新拍照计数器
photo_filename = f'{photo_path}photo_{num_count}.jpg' # 定义照片的文件名
cv2.imwrite(photo_filename, frame) # 保存照片到指定路径
last_picture_time = time.time() # 更新上一次拍照的时间
print(f'{num_count} photos saved. new photo: {photo_filename}') # 打印保存照片的信息
_, frame = cv2.imencode('.jpeg', frame) # 将图像帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查停止按钮是否被按下
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None)
# 显示停止按钮并启动视频流显示线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
本章教程使用 OpenCV 来检测画面中的变化,你可以为变化多少设置一个阈值,更改阈值,可以更改运动检测的敏感度。
本章节需要前置章节的基础。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用CSI摄像头则需要注释`frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)`这一句。
你需要更改一些参数来调增 OpenCV 对画面中变化检测的阈值(灵敏度)`threshold`,这个阈值越低,OpenCV 对画面的变化越敏感。
运行代码块是,你可以看到摄像头的实时画面,可以在画面前挥手,本例程会自动将出现变化的部分使用绿色的方框圈起来。
import cv2
from picamera2 import Picamera2
import numpy as np
from IPython.display import display, Image
import ipywidgets as widgets
import threading
import imutils # 用于简化图像处理任务的库
threshold = 2000 # 设置动态检测阈值
# 创建一个“停止”按钮来控制流程
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # 按钮图标(FontAwesome 名称,不带 `fa-` 前缀)
)
# 显示函数定义,用于捕获和处理视频帧,同时进行运动检测
# ================
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 实例
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)})) # 配置摄像头参数
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True)
i = 0
avg = None # 用于存储平均帧
while True:
# frame = picam2.capture_array() # 从摄像头捕获帧
# frame = cv2.flip(frame, 1) # if your camera reverses your image
_, frame = camera.read() # 从摄像头捕获一帧图像
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # 将帧颜色从 RGB 转换为 BGR
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 将帧转换为灰度图
gray = cv2.GaussianBlur(gray, (21, 21), 0) # 对灰度图应用高斯模糊
if avg is None: # 如果平均帧不存在,则创建它
avg = gray.copy().astype("float")
continue
try:
cv2.accumulateWeighted(gray, avg, 0.5) # 更新平均帧
except:
continue
frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg)) # 计算当前帧和平均帧的差值
# 应用阈值,找到差值图像中的轮廓
thresh = cv2.threshold(frameDelta, 5, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.dilate(thresh, None, iterations=2)
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# 遍历轮廓
for c in cnts:
# 如果轮廓太小,则忽略
if cv2.contourArea(c) < threshold:
continue
# 计算轮廓的边界框,将其绘画到矩形框
(mov_x, mov_y, mov_w, mov_h) = cv2.boundingRect(c)
cv2.rectangle(frame, (mov_x, mov_y), (mov_x + mov_w, mov_y + mov_h), (128, 255, 0), 1) # 在移动区域画矩形框
_, frame = cv2.imencode('.jpeg', frame) # 将处理后的帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查是否按下了“停止”按钮
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None) # 清除显示的图像
# 显示停止按钮并启动视频流显示线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
本章教程通过在页面上增加按键来控制摄像头拍照和录像,和之前的教程类似,图片会被默认保存在 /ugv_jetson/templates/pictures/ 文件夹内,视频会被默认保存在 /ugv_jetson/templates/videos 文件夹内。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用USB摄像头则需要取消注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。
当代码块运行时,可以通过点击 PHOTO 拍照。
import cv2 # 导入 OpenCV 库,用于图像处理
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
import numpy as np # 用于数学计算的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
import os, time # 用于文件和目录操作以及时间相关的函数
time_intervel = 3 # 设置定时拍照的时间间隔(秒)
photo_path = '/home/ws/ugv_jetson/static/' # 设置存储照片和视频的目录路径
# 创建“停止”按钮,用户可以通过点击它来停止视频捕获和拍照
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 设置按钮样式 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # 设置按钮图标 (FontAwesome names without the `fa-` prefix)
)
# 创建“拍照”按钮,用户可以通过点击它来即时拍摄一张照片
# ================
photoButton = widgets.ToggleButton(
value=False,
description='Photo',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 设置连续拍照的时间间隔(秒)
time_interval = 3
photo_num_count = 0 #初始化拍摄照片的计数器
capture_lock = threading.Lock()
last_photo_time = time.time() #记录上一次拍照的时间
def photo_button_clicked(change, frame):
global photo_num_count
if change['new']: # 当“拍照”按钮被点击时
photo_num_count = photo_num_count + 1 # 照片计数器加1
photo_filename = f'{photo_path}photo_{photo_num_count}.jpg' # 设置照片的保存路径和文件名
cv2.imwrite(photo_filename, frame) # 保存照片
print(f'{photo_num_count} photos saved. new photo: {photo_filename}') # 打印照片保存信息
photoButton.value = False # 重置“拍照”按钮的状态
# 定义显示函数,用于捕获和显示视频帧,并响应拍照请求
# ================
def view(stop_button, photo_button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)})) # 配置摄像头参数
# picam2.start()
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建显示句柄用于更新显示的图像
i = 0
while True:
# frame = picam2.capture_array() # 从摄像头捕获一帧图像
_, frame = camera.read() # 从摄像头捕获一帧图像
photoButton.observe(lambda change: photo_button_clicked(change, frame), names='value') # 监听“拍照”按钮的点击事件
_, frame = cv2.imencode('.jpeg', frame) # 将图像帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes()))
if stopButton.value==True:
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None)
# 显示“停止”和“拍照”按钮,并启动一个新线程来执行显示函数
# ================
display(stopButton)
display(photoButton)
thread = threading.Thread(target=view, args=(stopButton, photoButton,))
thread.start()
这里需要注意的是,由于该例程使用 JupyterLab 的组件来实现,由于存在一些稳定性方面的问题,所以当你按下 Photo 拍照后,可能会保存下多张照片,你可以 JupyterLab 左边的区域浏览到 /ugv_jetson/templates/pictures/ 内来查看拍摄的照片。
本章节介绍如何使用 OpenCV 来比对特征库,实现人脸识别功能,该功能的效率不如 MediaPipe 的方案高,但是该方案可以通过更换特征库文件来检测其它物体。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
人脸特征库文件与本`.ipynb`处于同一路径内,你可以通过更改 faceCascade 来更改需要检测的内容,你需要使用其它的特征文件来替换当前的 haarcascade_frontalface_default.xml。
当代码块正常运行时,你可以让机器人的摄像头对准人脸,观察画面中会自动圈出人脸所在的位置。
import cv2 # 导入 OpenCV 库,用于图像处理
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
import numpy as np # 用于数学计算的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
# 加载 Haar 特征级联分类器用于面部检测
faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
# 创建一个“停止”按钮,用户可以通过点击它来停止视频流
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 定义显示函数,用于处理视频帧并进行面部检测
# ================
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)})) # 配置摄像头参数
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建显示句柄用于更新显示的图像
i = 0
avg = None
while True:
# frame = picam2.capture_array()
_, frame = camera.read() # 从摄像头捕获一帧图像
# frame = cv2.flip(frame, 1) # if your camera reverses your image
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # 将图像从 RGB 转换为 BGR,因为 OpenCV 默认使用 BGR
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 将图像转换为灰度图,因为面部检测通常在灰度图上进行
# 使用级联分类器进行面部检测
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.2,
minNeighbors=5,
minSize=(20, 20)
)
if len(faces):
for (x,y,w,h) in faces: # 遍历所有检测到的面部
cv2.rectangle(frame,(x,y),(x+w,y+h),(64,128,255),1) # 在检测到的面部周围画一个矩形框
_, frame = cv2.imencode('.jpeg', frame) # 将帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes()))
if stopButton.value==True:
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None)
# 显示“停止”按钮并启动显示函数的线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
本章节介绍如何使用 DNN(深度神经网络)+ OpenCV 来实现常见的物体识别。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
deploy.prototxt 文件与 mobilenet_iter_73000.caffemodel 文件与本 .ipynb 处于同一路径内。
当代码块正常运行时,你可以让机器人的摄像头对准一些常见物体例如:"background", "aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable" "dog", "horse", "motorbike", "person", "pottedplant", "sheep "sofa", "train", "tvmonitor"
画面中会标记出它识别出来的物体,并标记这个物体的名称。
import cv2 # 导入 OpenCV 库,用于图像处理
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
import numpy as np # 用于数学计算的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
# 预定义的分类名,根据 Caffe 模型进行设置
class_names = ["background", "aeroplane", "bicycle", "bird", "boat",
"bottle", "bus", "car", "cat", "chair", "cow", "diningtable",
"dog", "horse", "motorbike", "person", "pottedplant", "sheep",
"sofa", "train", "tvmonitor"]
# 加载 Caffe 模型
net = cv2.dnn.readNetFromCaffe('deploy.prototxt', 'mobilenet_iter_73000.caffemodel')
# 创建一个“停止”按钮,用户可以通过点击它来停止视频流
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 定义显示函数,用于处理视频帧并进行物体检测
# ================
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)})) # 配置摄像头参数
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建显示句柄用于更新显示的图像
i = 0
avg = None
while True:
# frame = picam2.capture_array() # 从摄像头捕获一帧图像
_, frame = camera.read() # 从摄像头捕获一帧图像
# frame = cv2.flip(frame, 1) # if your camera reverses your image
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # 将图像从 RGB 转换为 BGR,因为 OpenCV 默认使用 BGR
(h, w) = img.shape[:2] # 获取图像的高度和宽度
# 生成网络的输入 blob
blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 0.007843, (300, 300), 127.5)
net.setInput(blob) # 将 blob 设置为网络的输入
detections = net.forward() # 进行前向传播,得到检测结果
# 遍历检测到的物体
for i in range(0, detections.shape[2]):
confidence = detections[0, 0, i, 2] # 获取检测到的物体的置信度
if confidence > 0.2: # 如果置信度高于阈值,则处理检测到的物体
idx = int(detections[0, 0, i, 1]) # 获取分类索引
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) # 获取物体的边界框
(startX, startY, endX, endY) = box.astype("int") # 转换边界框为整数
# 在图像上标记物体和置信度
label = "{}: {:.2f}%".format(class_names[idx], confidence * 100)
cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 255, 0), 2)
y = startY - 15 if startY - 15 > 15 else startY + 15
cv2.putText(frame, label, (startX, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
_, frame = cv2.imencode('.jpeg', frame) # 将帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查“停止”按钮是否被按下
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None) # 清空显示的内容
# 显示“停止”按钮并启动显示函数的线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
在本章教程中我们会在 OpenCV 的相关功能中加入一些修改帧画面相关的函数,例如模糊,色彩空间转换,腐蚀和膨胀。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
我们在例程中默认检测蓝色小球,确保画面背景中没有蓝色物体影响颜色识别功能,你也可以通过二次开发来更改检测颜色(HSV色彩空间)。
import cv2
import imutils, math
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
import numpy as np # 用于数学计算的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
# 创建一个“停止”按钮,用户可以通过点击它来停止视频流
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 定义显示函数,用于处理视频帧并识别特定颜色的物体
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# 配置摄像头参数,设置视频的格式和大小
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建显示句柄用于更新显示的图像
i = 0
# 定义要检测的颜色范围
color_upper = np.array([120, 255, 220])
color_lower = np.array([90, 120, 90])
min_radius = 12 # 定义检测物体的最小半径
while True:
# img = picam2.capture_array() # 从摄像头捕获一帧图像
_, img = camera.read() # 从摄像头捕获一帧图像
# frame = cv2.flip(frame, 1) # if your camera reverses your image
blurred = cv2.GaussianBlur(img, (11, 11), 0) # 对图像应用高斯模糊,以去除噪声
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) # 将图像从 BGR 转换为 HSV 颜色空间
mask = cv2.inRange(hsv, color_lower, color_upper) # 创建掩模以便只保留特定颜色范围内的物体
mask = cv2.erode(mask, None, iterations=5) # 对掩模应用腐蚀操作,以去除小的白点
mask = cv2.dilate(mask, None, iterations=5) # 对掩模应用膨胀操作,以使物体区域更加突出
# 查找掩模中的轮廓
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts) # 提取轮廓
center = None # 初始化物体的中心点
if len(cnts) > 0:
# find the largest contour in the mask, then use
# it to compute the minimum enclosing circle and
# centroid
c = max(cnts, key=cv2.contourArea) # 找到最大的轮廓
((x, y), radius) = cv2.minEnclosingCircle(c) # 计算轮廓的最小封闭圆
M = cv2.moments(c) # 计算轮廓的矩
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])) # 根据矩计算轮廓的中心点
if radius > min_radius: # 如果最小封闭圆的半径大于预设的最小半径,则绘制圆圈和中心点
cv2.circle(img, (int(x), int(y)), int(radius), (128, 255, 255), 1) # 绘制最小封闭圆
_, frame = cv2.imencode('.jpeg', img) # 将帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查“停止”按钮是否被按下
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None) # 清空显示的内容
# 显示“停止”按钮并启动显示函数的线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
在本章教程中我们会在 OpenCV 的相关功能中加入一些控制外设的函数,例如,在本章教程中,摄像头云台会转动,确保你的手或其它易碎物品远离摄像头云台的转动半径。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
在本章教程中,摄像头云台会转动,确保你的手或其它易碎物品远离摄像头云台的转动半径。
我们在例程中默认检测蓝色小球,确保画面背景中没有蓝色物体影响颜色识别功能,你也可以通过二次开发来更改检测颜色(HSV色彩空间)。
import matplotlib.pyplot as plt
import cv2
from picamera2 import Picamera2
import numpy as np
from IPython.display import display, Image
import ipywidgets as widgets
import threading
# Stop button
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
def gimbal_track(fx, fy, gx, gy, iterate):
global gimbal_x, gimbal_y
distance = math.sqrt((fx - gx) ** 2 + (gy - fy) ** 2)
gimbal_x += (gx - fx) * iterate
gimbal_y += (fy - gy) * iterate
if gimbal_x > 180:
gimbal_x = 180
elif gimbal_x < -180:
gimbal_x = -180
if gimbal_y > 90:
gimbal_y = 90
elif gimbal_y < -30:
gimbal_y = -30
gimbal_spd = int(distance * track_spd_rate)
gimbal_acc = int(distance * track_acc_rate)
if gimbal_acc < 1:
gimbal_acc = 1
if gimbal_spd < 1:
gimbal_spd = 1
base.base_json_ctrl({"T":self.CMD_GIMBAL,"X":gimbal_x,"Y":gimbal_y,"SPD":gimbal_spd,"ACC":gimbal_acc})
return distance
# Display function
# ================
def view(button):
picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
picam2.start()
display_handle=display(None, display_id=True)
color_upper = np.array([120, 255, 220])
color_lower = np.array([ 90, 120, 90])
min_radius = 12
track_color_iterate = 0.023
while True:
frame = picam2.capture_array()
# frame = cv2.flip(frame, 1) # if your camera reverses your image
# uncomment this line if you are using USB camera
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
blurred = cv2.GaussianBlur(img, (11, 11), 0)
hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, color_lower, color_upper)
mask = cv2.erode(mask, None, iterations=5)
mask = cv2.dilate(mask, None, iterations=5)
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
center = None
height, width = img.shape[:2]
center_x, center_y = width // 2, height // 2
if len(cnts) > 0:
# find the largest contour in the mask, then use
# it to compute the minimum enclosing circle and
# centroid
c = max(cnts, key=cv2.contourArea)
((x, y), radius) = cv2.minEnclosingCircle(c)
M = cv2.moments(c)
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
# only proceed if the radius meets a minimum size
if radius > min_radius:
distance = gimbal_track(center_x, center_y, center[0], center[1], track_color_iterate) #
cv2.circle(overlay_buffer, (int(x), int(y)), int(radius), (128, 255, 255), 1)
_, frame = cv2.imencode('.jpeg', frame)
display_handle.update(Image(data=frame.tobytes()))
if stopButton.value==True:
picam2.close()
display_handle.update(None)
# Run
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
在本章教程中我们会在使用 OpenCV 的基础功能来从画面中检测到画面中黄色(默认颜色)的线条,并通过检测该黄色线条的位置来控制底盘转向(本例程中的底盘不会移动,本例程只在画面中展示 OpenCV 的算法),我们这里由于安全方面的原因不会讲运动控制结合在例程里面,因为该功能受外界因素影响比较大,用户需完整理解代码功能后在增加对应的运动控制功能。
如果你想通过本例程来控制机器人移动,请结合前面的 Python 底盘运动控制 章节来添加相关的运动控制函数(我们的开源例程位于 robot_ctrl.py 中)。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用USB摄像头则需要取消注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。
运行以下代码块后,你可以讲黄色胶带放在摄像头前面,观察黑色的画面中是否有黄色胶带的轮廓,能否使用两条目标检测线来检测到黄色胶带。
import cv2 # 导入 OpenCV 库,用于图像处理
import imutils, math # 辅助图像处理和数学运算的库
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
import numpy as np
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
# Stop button
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# findline autodrive
# 上检测线,0.6代表位置,数值越大
sampling_line_1 = 0.6
# 下检测线,数值需要大于 sampling_line_1 且小于 1
sampling_line_2 = 0.9
# 检测线斜率对转弯的影响
slope_impact = 1.5
# 下检测线检测到的线位置对转弯的影响
base_impact = 0.005
# 当前速度对转弯的影响
speed_impact = 0.5
# 巡线速度
line_track_speed = 0.3
# 斜率对巡线速度的影响
slope_on_speed = 0.1
# 目标线的颜色,HSV色彩空间
line_lower = np.array([25, 150, 70])
line_upper = np.array([42, 255, 255])
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# 配置摄像头参数,设置视频的格式和大小
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True)
while True:
# img = picam2.capture_array()
_, img = camera.read() # 从摄像头捕获一帧图像
# frame = cv2.flip(frame, 1) # if your camera reverses your image
height, width = img.shape[:2]
center_x, center_y = width // 2, height // 2
# 图像预处理,包括转换颜色空间、高斯模糊、颜色范围筛选等
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
line_mask = cv2.inRange(hsv, line_lower, line_upper) # 根据颜色范围筛选出目标线
line_mask = cv2.erode(line_mask, None, iterations=1) # 腐蚀操作去除噪点
line_mask = cv2.dilate(line_mask, None, iterations=1) # 膨胀操作增强目标线
# 根据上下两个采样线的位置进行目标线检测,并根据检测结果计算转向和速度控制信号
sampling_h1 = int(height * sampling_line_1)
sampling_h2 = int(height * sampling_line_2)
get_sampling_1 = line_mask[sampling_h1]
get_sampling_2 = line_mask[sampling_h2]
# 计算上、下采样线处的目标线宽度
sampling_width_1 = np.sum(get_sampling_1 == 255)
sampling_width_2 = np.sum(get_sampling_2 == 255)
if sampling_width_1:
sam_1 = True
else:
sam_1 = False
if sampling_width_2:
sam_2 = True
else:
sam_2 = False
# 获取上下采样线处目标线的边缘索引
line_index_1 = np.where(get_sampling_1 == 255)
line_index_2 = np.where(get_sampling_2 == 255)
# 如果在上采样线处检测到目标线,计算目标线中心位置
if sam_1:
sampling_1_left = line_index_1[0][0] # 上采样线目标线最左侧的索引
sampling_1_right = line_index_1[0][sampling_width_1 - 1] # 上采样线目标线最右侧的索引
sampling_1_center= int((sampling_1_left + sampling_1_right) / 2) # 上采样线目标线中心的索引
# 如果在下采样线处检测到目标线,计算目标线中心位置
if sam_2:
sampling_2_left = line_index_2[0][0]
sampling_2_right = line_index_2[0][sampling_width_2 - 1]
sampling_2_center= int((sampling_2_left + sampling_2_right) / 2)
# 初始化转向和速度控制信号
line_slope = 0
input_speed = 0
input_turning = 0
# 如果在两个采样线处都检测到了目标线,计算线条的斜率,以及根据斜率和目标线位置计算速度和转向控制信号
if sam_1 and sam_2:
line_slope = (sampling_1_center - sampling_2_center) / abs(sampling_h1 - sampling_h2) # 计算线条斜率
impact_by_slope = slope_on_speed * abs(line_slope) # 根据斜率计算对速度的影响
input_speed = line_track_speed - impact_by_slope # 计算速度控制信号
input_turning = -(line_slope * slope_impact + (sampling_2_center - center_x) * base_impact) #+ (speed_impact * input_speed) # 计算转向控制信号
elif not sam_1 and sam_2: # 如果只在下采样线处检测到了目标线
input_speed = 0 # 设置速度为0
input_turning = (sampling_2_center - center_x) * base_impact # 计算转向控制信号
elif sam_1 and not sam_2: # 如果只在上采样线处检测到了目标线
input_speed = (line_track_speed / 3) # 减慢速度
input_turning = 0 # 不进行转向
else: # 如果两个采样线都没有检测到目标线
input_speed = - (line_track_speed / 3) # 后退
input_turning = 0 # 不进行转向
# base.base_json_ctrl({"T":13,"X":input_speed,"Z":input_turning})
cv2.putText(line_mask, f'X: {input_speed:.2f}, Z: {input_turning:.2f}', (center_x+50, center_y+0), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
# 可视化操作,包括在采样线位置绘制直线,标记采样结果,以及显示转向和速度控制信号
cv2.line(line_mask, (0, sampling_h1), (img.shape[1], sampling_h1), (255, 0, 0), 2)
cv2.line(line_mask, (0, sampling_h2), (img.shape[1], sampling_h2), (255, 0, 0), 2)
if sam_1:
# 在上采样线处的目标线两端绘制绿色的标记线
cv2.line(line_mask, (sampling_1_left, sampling_h1+20), (sampling_1_left, sampling_h1-20), (0, 255, 0), 2)
cv2.line(line_mask, (sampling_1_right, sampling_h1+20), (sampling_1_right, sampling_h1-20), (0, 255, 0), 2)
if sam_2:
# 在下采样线处的目标线两端绘制绿色的标记线
cv2.line(line_mask, (sampling_2_left, sampling_h2+20), (sampling_2_left, sampling_h2-20), (0, 255, 0), 2)
cv2.line(line_mask, (sampling_2_right, sampling_h2+20), (sampling_2_right, sampling_h2-20), (0, 255, 0), 2)
if sam_1 and sam_2:
# 如果上下采样线处都检测到目标线,绘制一条从上采样线中心到下采样线中心的红色连线
cv2.line(line_mask, (sampling_1_center, sampling_h1), (sampling_2_center, sampling_h2), (255, 0, 0), 2)
_, frame = cv2.imencode('.jpeg', line_mask)
display_handle.update(Image(data=frame.tobytes()))
if stopButton.value==True:
# picam2.close()
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None)
# 显示“停止”按钮并启动显示函数的线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
本章节介绍如何使用 MediaPipe + OpenCV 来实现手势识别。
MediaPipe 是 Google 开发的一种开源框架,用于构建基于机器学习的多媒体处理应用程序。它提供了一组工具和库,可以用于处理视频、音频和图像数据,并应用机器学习模型来实现各种功能,如姿态估计、手势识别、人脸检测等。MediaPipe 的设计目标是提供高效、灵活和易用的解决方案,使开发者能够快速构建出各种多媒体处理应用。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用USB摄像头则需要取消注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。
当代码块正常运行时,你可以把自己手放在摄像头前面,实时视频画面中会标注出人手的关节,标注出的关节会随人手的变化而变化,同时也会输出各个关节的位置,方便进行手势控制方面的二次开发。
MediaPipe 的手势识别过程采用不同的名称来对应不同的关节,你可以通过调用对应的编号来获取该关节的位置信息。
import cv2
import imutils, math
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
import mediapipe as mp # 导入 MediaPipe 库,用于手部关键点检测
# 创建一个“停止”按钮,用户可以通过点击它来停止视频流
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 初始化 MediaPipe 绘图工具和手部关键点检测模型
mpDraw = mp.solutions.drawing_utils
mpHands = mp.solutions.hands
hands = mpHands.Hands(max_num_hands=1) # 初始化手部关键点检测模型,最多检测一只手
# 定义显示函数,用于处理视频帧并进行手部关键点检测
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 的实例
# 配置摄像头参数,设置视频的格式和大小
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True) # 创建显示句柄用于更新显示的图像
while True:
# frame = picam2.capture_array()
_, frame = camera.read() # 从摄像头捕获一帧图像
# frame = cv2.flip(frame, 1) # if your camera reverses your image
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
results = hands.process(img)
# 如果检测到手部关键
if results.multi_hand_landmarks:
for handLms in results.multi_hand_landmarks: # 遍历检测到的每只手
# 绘制手部关键点
for id, lm in enumerate(handLms.landmark):
h, w, c = img.shape
cx, cy = int(lm.x * w), int(lm.y * h) # 计算关键点在图像中的位置
cv2.circle(img, (cx, cy), 5, (255, 0, 0), -1) # 在关键点位置绘制圆点
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
mpDraw.draw_landmarks(frame, handLms, mpHands.HAND_CONNECTIONS) # 绘制手部骨架连接线
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
target_pos = handLms.landmark[mpHands.HandLandmark.INDEX_FINGER_TIP]
_, frame = cv2.imencode('.jpeg', frame)
display_handle.update(Image(data=frame.tobytes()))
if stopButton.value==True:
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None)
# 显示“停止”按钮并启动显示函数的线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
本章节介绍如何使用 MediaPipe + OpenCV 来实现人脸识别。
MediaPipe 是 Google 开发的一种开源框架,用于构建基于机器学习的多媒体处理应用程序。它提供了一组工具和库,可以用于处理视频、音频和图像数据,并应用机器学习模型来实现各种功能,如姿态估计、手势识别、人脸检测等。MediaPipe 的设计目标是提供高效、灵活和易用的解决方案,使开发者能够快速构建出各种多媒体处理应用。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用USB摄像头则需要取消注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。
当代码块正常运行时,当画面中出现人脸,MediaPipe 会自动框选出人脸在画面中的位置,同时也会标记出五官。
import cv2 # 导入 OpenCV 库,用于图像处理
import imutils, math # 辅助图像处理和数学运算的库
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
import mediapipe as mp # 导入 MediaPipe 库,用于人脸检测
# 创建一个“停止”按钮,用户可以通过点击它来停止视频流
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 初始化 MediaPipe 的人脸检测模型
mpDraw = mp.solutions.drawing_utils
# MediaPipe Hand GS
mp_face_detection = mp.solutions.face_detection
face_detection = mp_face_detection.FaceDetection(model_selection=0, min_detection_confidence=0.5)
# 定义显示函数,用于处理视频帧并进行人脸检测
def view(button):
picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
picam2.start()
display_handle=display(None, display_id=True)
while True:
frame = picam2.capture_array() # 从摄像头捕获一帧图像
# frame = cv2.flip(frame, 1) # if your camera reverses your image
# uncomment this line if you are using USB camera
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
results = face_detection.process(img)
# 如果检测到人脸
if results.detections:
for detection in results.detections: # 遍历检测到的每张人脸
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
mpDraw.draw_detection(frame, detection) # 使用 MediaPipe 的绘图工具绘制人脸标记
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
_, frame = cv2.imencode('.jpeg', frame) # 将处理后的帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查“停止”按钮是否被按下
picam2.close() # 如果是,则关闭摄像头
display_handle.update(None) # 清空显示的内容
# 显示“停止”按钮并启动显示函数的线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()
本章节介绍如何使用 MediaPipe + OpenCV 来实现姿态检测。
MediaPipe 是 Google 开发的一种开源框架,用于构建基于机器学习的多媒体处理应用程序。它提供了一组工具和库,可以用于处理视频、音频和图像数据,并应用机器学习模型来实现各种功能,如姿态估计、手势识别、人脸检测等。MediaPipe 的设计目标是提供高效、灵活和易用的解决方案,使开发者能够快速构建出各种多媒体处理应用。
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: sudo killall -9 python
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果使用USB摄像头则需要取消注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。
当代码块正常运行时,当画面中出现人脸,MediaPipe 会自动标记出人肢体的关节。
import cv2 # 导入 OpenCV 库,用于图像处理
import imutils, math # 辅助图像处理和数学运算的库
from picamera2 import Picamera2 # 用于访问 Raspberry Pi Camera 的库
from IPython.display import display, Image # 用于在 Jupyter Notebook 中显示图像
import ipywidgets as widgets # 用于创建交互式界面的小部件,如按钮
import threading # 用于创建新线程,以便异步执行任务
import mediapipe as mp # 导入 MediaPipe 库,用于姿态检测
# 创建一个“停止”按钮,用户可以通过点击它来停止视频流
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # (FontAwesome names without the `fa-` prefix)
)
# 初始化 MediaPipe 的绘图工具和姿态检测模型
mpDraw = mp.solutions.drawing_utils
# MediaPipe Hand GS
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(static_image_mode=False,
model_complexity=1,
smooth_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
# 定义显示函数,用于处理视频帧并进行姿态检测
def view(button):
picam2 = Picamera2() # 创建 Picamera2 的实例
picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)})) # 配置摄像头参数
picam2.start() # 启动摄像头
display_handle=display(None, display_id=True) # 创建显示句柄用于更新显示的图像
while True:
frame = picam2.capture_array()
# frame = cv2.flip(frame, 1) # if your camera reverses your image
# uncomment this line if you are using USB camera
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
results = pose.process(img) # 使用 MediaPipe 处理图像,获取姿态检测结果
# 如果检测到姿态关键点
if results.pose_landmarks:
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # 将图像从 RGB 转换为 BGR 以供绘制使用
mpDraw.draw_landmarks(frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS) # 使用 MediaPipe 的绘图工具绘制姿态关键点和连接线
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 将图像从 BGR 转换回 RGB 以供显示
_, frame = cv2.imencode('.jpeg', frame) # 将处理后的帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查“停止”按钮是否被按下
picam2.close() # 如果是,则关闭摄像头
display_handle.update(None) # 清空显示的内容
# 显示“停止”按钮并启动显示函数的线程
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start() # 启动线程
在之前的章节中我们介绍了如何使用 Flask 实现低延时图传,用于将摄像头画面传递到 WEB 应用界面上,在这里我们介绍如何将 WEB 应用界面输入的信息,传递到 WEB 应用的后端,这个功能用于使用 WEB 应用来控制机器人。
from flask import Flask, request # 从 flask 包导入 Flask 类和 request 对象
app = Flask(__name__) # 创建 Flask 应用实例
@app.route('/', methods=['GET', 'POST']) # 定义根路由,并允许 GET 和 POST 方法
def index():
if request.method == 'POST': # 检查当前请求是否为 POST
# 获取表单数据
form_data = request.form
# 打印表单数据
print(form_data)
# 返回一个简单的响应
return 'Received data: {}'.format(form_data)
else:
# 如果是 GET 请求,返回一个简单的表单页面
return '''
<form method="post">
<label for="input_data">Input data:</label><br>
<input type="text" id="input_data" name="input_data"><br>
<input type="submit" value="Submit">
</form>
'''
if __name__ == '__main__':
app.run(host='0.0.0.0')
你可以选中上面的代码块,按 Ctrl + Enter 来运行该代码块,如果提是端口已经被占用说明你之前已经运行过该代码块。你需要点击 jupyterLab 上方的 Kernel -> Shut Down All Kernel ,这样会释放之前运行代码块所占用的资源(包含网络端口资源),然后你可以重新运行该代码块来运行这个 Flask 应用。
当你运行这个代码块后,你可以看到 Running on http://127.0.0.1:5000, Running on http://[IP]:5000 的字样,通常这个 [IP] 是你的路由给你的Jetson 分配的 IP 地址,你可以在同一局域网内的设备上打开浏览器,访问 [IP]:5000 这个地址,注意这里的 ':' 符号一定要是英文的冒号,它指代你要访问这个IP地址的5000端口号,访问这个页面后,你可以看到页面上有一个输入框,下面还有一个 Submit 按键,你可以在输入框中输入一些内容,然后点击 Submit 按键,之后你可以在 JupyterLab 的代码块下方看到你在网页上输入的内容,且网页也会显示出后端所接收到的内容。
在项目文件夹中有一个名为 setup.sh 的文件,使用 shell 编写,可以帮助自动配置机器人产品的上位机,包括设置串口、设置摄像头、创建项目虚拟环境和安装依赖库等。这些步骤在我们出厂的SD卡的镜像中都是已经配置好的了。
安装脚本使用方法,安装过程需要从网络下载并安装很多依赖库,对于网络环境比较特殊的地区我们推荐你使用直接从我们官网下载镜像文件的方法来安装产品。
setup.sh 需要使用 root 权限来运行。
cd ugv_jetson/
sudo chmod +x setup.sh
sudo ./setup.sh
项目文件夹中的 autorun.sh 用于配置产品主程序(app.py)和 jupyterlab(start_jupyter.sh)的开机自动运行(以用户身份而非 root),同时生成 jupyterlab 的配置文件。
autorun.sh 需要使用用户权限来运行。
sudo chmod +x autorun.sh ./autorun.sh
本项目会长期更新,当你需要更新是,只需要导航到项目文件夹内即可使用 git pull 命令来更新,注意该操作会替换掉之前你所做的变更,更新后你需要更改 config.yaml 内的参数,设置底盘类型和模块类型,你也可以使用 WEB 端控制界面的命令行工具来配置产品类型。
cd ugv_jetson/
git pull
设备重启后,打开 WEB 端控制界面,输入用来设置产品类型的指令,例如:s 20
第一位数字2代表底盘类型,第二位数字0代表没有模块或云台模块。
第一位数字:
第二位数字:
该配置更新后或重装后只需要配置一次,会被保存在 config.yaml 中。
本项目以 GPL-3.0 协议开源,你可以根据我们的项目来实现自己的项目,我们同时也会在后续持续更新更多功能。
YAML(YAML Ain't Markup Language)是一种人类可读的数据序列化格式,用于表示复杂数据结构。它的主要用途包括配置文件、数据交换和存储,以及将数据传递给程序。
YAML 使用缩进和可读性良好的结构,使得文件易于阅读和理解。它不像 XML 或 JSON 那样显得冗长,更接近自然语言。
YAML 的语法简洁清晰,不需要额外的标记符号(如 XML 的标签或 JSON 的大括号),因此更容易编写和编辑。
YAML 支持嵌套、列表、字典等复杂数据结构,可以轻松表示各种类型的数据。
YAML 允许使用标签和锚点来表示对象之间的关系,从而实现数据的复用和引用,提高了数据的可扩展性。
YAML 是一种通用的数据序列化格式,不依赖于特定的编程语言,因此可以被多种编程语言轻松解析和生成。
在本产品的config.yaml中,我们配置了一些机器人相关的关键参数:
这些代号与对应的指令与下位机程序中的指令定义相关,用于上位机下位机通信联调,更改这些涉及到更改下位机程序。
这些代号与功能对应,前端页面也需要加载这个 .yaml 文件来获取这些配置,这样前端 WEB 应用再与后端进行通信时才会让不同的按键对应不同的功能,如无必要不需要更改。
这些代号与反馈的信息类型对应,这些反馈信息有些是底盘反馈给上位机的,有些是后端反馈给前端的,使用后端与前端使用这同一个.yaml文件来统一这些序号与反馈信息的类型,如无必要不需要更改。
在之前的教程中,我们简单介绍了如果关闭产品主程序的自动运行,所以用的方法就是在 Crontab 文件中注释掉运行产品主程序的命令,在本章教程中,你将会了解到更多有关 Crontab 的信息,以及为什么我们采用 Crontab 而不采用 Services 来实现开机自动运行。
Crontab 是 Linux 系统中用于周期性执行任务的工具。通过 Crontab,用户可以设置在特定的时间点、日期或者周期性地执行特定的命令或脚本。我们这里介绍 Crontab 的一些重要概念和用法:
Crontab 文件是存储周期性任务调度信息的文件。每个用户都有自己的 Crontab 文件,用于存储他们自己的任务调度信息。Crontab 文件通常存储在 /var/spool/cron 目录中,以用户的用户名命名。
Crontab 文件中的每一行代表一个任务调度。每行由五个字段组成,分别表示分钟、小时、日期、月份和星期。你可以使用 # 号来注释掉某一行来关闭对应的任务调度。
要编辑 Crontab 文件,可以使用 crontab -e 命令。此命令会打开一个文本编辑器,允许用户编辑自己的 Crontab 文件。编辑完成后,保存并退出编辑器,Crontab 文件就会更新。
常用选项:
相比之下,使用 services 实现开机自动运行通常是通过在系统启动时执行一系列预定义的服务或脚本来实现的。这些服务可以在 /etc/init.d/ 目录下找到,并通过系统的服务管理工具(如 systemctl)来启动、停止或重启。
更低的资源占用,经过我们的测试对比,同样的python脚本使用 Crontab 的CPU资源占用是 services 的 1/4,对于复杂度比较高的机器人主程序这样的应用而言,使用 Crontab 来实现开机自动运行是更优的选择。ervices 则更适用于需要在系统启动时执行的重要服务或应用程序。
为了方便对产品进行二次开发,我们在 WEB 应用中添加了一个命令行输入窗口,你可以在这个窗口里面输入命令,点击 SEND 按键后可以将这条命令发送给上位机应用,上位机应用根据你所发送的指令来执行相应的功能或调参。
我们有一共一些现成的命令,你可以参考后续的 WEB 命令行应用章节来学习那些命令,在本章节中,我们会在介绍如何实现自定义命令行功能的同时,介绍这个功能是如何实现的,这样你可以更容易理解后续的章节。
命令行功能的例程写在产品主程序的 robot_ctrl.py 中,由 cmd_process() 函数来处理命令行指令。以下是我们默认的命令行指令处理函数,这个函数是不完整的,因为产数后面的内容是其它功能的处理,省略掉那部分不影响对函数本身的理解。
注意:下面的代码块不能在 JupyterLab 中运行,仅用于原理展示。
def cmd_process(self, args_str):
global show_recv_flag, show_info_flag, info_update_time, mission_flag
global track_color_iterate, track_faces_iterate, track_spd_rate, track_acc_rate
# 将输入的参数字符串分割成一个列表 args
args = args_str.split()
if args[0] == 'base':
self.info_update("CMD:" + args_str, (0,255,255), 0.36)
if args[1] == '-c' or args[1] == '--cmd':
base.base_json_ctrl(json.loads(args[2]))
elif args[1] == '-r' or args[1] == '--recv':
if args[2] == 'on':
show_recv_flag = True
elif args[2] == 'off':
show_recv_flag = False
elif args[0] == 'info':
info_update_time = time.time()
show_info_flag = True
elif args[0] == 'audio':
self.info_update("CMD:" + args_str, (0,255,255), 0.36)
if args[1] == '-s' or args[1] == '--say':
audio_ctrl.play_speech_thread(' '.join(args[2:]))
elif args[1] == '-v' or args[1] == '--volume':
audio_ctrl.set_audio_volume(args[2])
elif args[1] == '-p' or args[1] == '--play_file':
audio_ctrl.play_file(args[2])
我们以 audio -s hey hi hello 为例,这条指令用于文字转语音功能,audio代表这是一个音频相关的功能,-s 或 --say 是音频的文字转语音,后面接的参数是你想让它说话的内容,发送上面的指令后机器人会说 hey hi hello。
首先当该函数接收到命令行指令后,由于命令行指令是一串字符串,所以需要先使用 args = args_str.split() 来将这个字符串转换为列表,再去判断列表中的每个值来执行相应的功能。
如果你需要扩展其它的自定义功能,只需要再增加一个 eilf args[0] == 'newCmd' 即可。
为了让产品的参数更容易配置,同时更加方便用户为产品添加自定义的功能,我们为产品设计了命令行参数的功能,你可以通过在网页的命令行工具中输入指令来实现对应的功能,本章节会详细介绍这些功能。