上位机使用教程

Jetson 01 JupyterLab 基础和机器人介绍

机器人基础信息介绍

控制架构

本机器人使用了上位机+下位机的架构(大小脑架构),上位机可以为树莓派、Jetson Nano、Orin Nano或其它带有类似40PIN接口的单板电脑,下位机使用ESP32来控制机器人外设、读取传感器信息、电机闭环速度控制(PID控制器)。

上位机与下位机使用 JSON 格式的指令通过串口进行通信,具体的通信内容可以参考下位机的文档,目前作为新手不需要了解那些指令,初期只需要跟随教程文档调用常见的指令或封装好的函数即可。

采用大小脑的架构优势

  1. Jetson 或其它单板电脑作为上位机处理复杂任务,如视觉信息处理,而ESP32下位机负责控制外设和传感器,这种模块化设计提高了系统的灵活性和可扩展性。
  2. Jetson 或其它单板电脑专注于高级处理和决策制定,而ESP32负责实时低级任务,如电机控制,这样的分工使得每个组件都可以专注于其擅长的领域。
  3. 这种架构可以有效分配处理能力和 IO 资源,减轻单一系统的负担,提高整体效率。
  4. 通过串口使用 JSON 格式进行通信,提高了数据传输的效率和可读性,便于调试和扩展。
  5. 对于预算有限的创客和爱好者来说,这种架构能够在保持较高性能的同时,降低成本和系统复杂度。


使用 JupyterLab 交互式开发基础教程

JupyterLab是什么?

  • 交互式开发环境:JupyterLab 是一个开源的交互式开发环境,它提供了一个易于使用的界面,用于编写代码、运行实验和查看数据。
  • 适合数据科学和机器学习:虽然最初设计用于数据科学和机器学习,但它的灵活性和易用性使其成为机器人编程和实验的理想选择。
  • 基于网页的工具:作为一个基于浏览器的应用,JupyterLab 不需要复杂的安装过程,用户可以在几乎任何操作系统上使用它。

使用JupyterLab开发的优势

  1. 易于上手的编程环境:JupyterLab 提供的简洁、直观的用户界面,使得编程和实验对于初学者更加友好。通过交互式笔记本轻松编写和测试代码,适合初学者逐步探索和学习。
  2. 即时反馈和结果可视化:即时反馈,用户可以立即看到代码更改的效果,这对于调试和学习非常有用。JupyterLab 可以方便地进行数据可视化,帮助理解机器人的行为和性能。
  3. 支持多种编程语言:JupyterLab 支持 Python 等多种编程语言,为各种技能水平的用户提供灵活性。
  4. 自定义和扩展功能:JupyterLab 具有的高度可定制性和扩展性,用户可以根据自己的需要添加新功能或工具。
  5. 跨平台和可访问性:JupyterLab 是基于网页的工具,具有出色的跨平台能力,它可以在不同操作系统上运行,通过浏览器即可访问。

JupyterLab 基础使用方法

什么是 Jupyter notebooks(.ipynb) 文档

Jupyter notebooks(.ipynb) 是一种文档,它结合了可直接运行的代码和叙述性文本(Markdown)、方程式(LaTeX)、图片、交互式可视化以及其他丰富的输出内容。

切换文档的主题

  • 我们默认的主题是亮色的 Jupyter Dark。
  • 你可以根据自己的喜好来切换为暗色的主题:依次点击界面上方的 Settings - Theme - JupyterLab Dark。

COMMAND / EDIT MODE

JupyterLab 有两种工作模式,一种是COMMAND模式,一种是EDIT模式

  • COMMAND 模式

当你在 COMMAND 模式下时,你可以快速地进行笔记本的整体操作,比如增加或删除单元格、移动单元格、更改单元格类型等。在这个模式下,单元格边框呈现灰色。你可以通过按 Esc 键进入 COMMAND 模式。

  • EDIT 模式

EDIT 模式允许你在单元格中输入或修改代码或文本。在这个模式下,单元格边框为蓝色。你可以通过在选中的单元格中点击或按下 Enter 键来进入 EDIT 模式。

单元格操作

在 JupyterLab 中,你可以执行以下操作:

  • 在 COMMAND 模式下,使用方向键的上下来选择单元格。
  • 在下方添加单元格:你可以通过点击工具栏的 "+" 按钮或使用快捷键 B(在 COMMAND 模式下)来在当前单元格下方添加新单元格。
  • 在上方添加单元格:你可以通过点击工具栏的 "+" 按钮或使用快捷键 A(在 COMMAND 模式下)来在当前单元格下方添加新单元格。
  • 删除单元格:使用 D,D(连续两次按 D 键,在 COMMAND 模式下)可以删除当前选中的单元格。
  • 复制单元格:(在 COMMAND 模式下)使用快捷键 C。
  • 粘贴单元格:(在 COMMAND 模式下)使用快捷键 V。
  • 剪切单元格:(在 COMMAND 模式下)使用快捷键 X。
  • 撤销:(在 COMMAND 模式下)使用快捷键 Z。
  • 重做:(在 COMMAND 模式下)使用快捷键 Shift + Z。
  • 将当前单元格转换为代码块:(在 COMMAND 模式下)使用快捷键 Y。
  • 将当前单元格转换为Markdown:(在 COMMAND 模式下)使用快捷键 M。
  • 切换单元格类型:你可以将单元格设置为代码单元格、Markdown 单元格或原始单元格。这可以通过工具栏的下拉菜单或快捷键 Y(代码单元格)、M(Markdown 单元格)在 COMMAND 模式下进行切换。
  • 运行单元格:你可以点击工具栏的 "运行" 按钮或使用快捷键 Shift + Enter 来执行当前单元格并自动运行后续单元格。

保存和导出

  • 保存 notebook:你可以点击工具栏的 "报" 按钮或使用快捷键 S(在 COMMAND 模式下)来保存你的 notebook。
  • 导出 notebook:JupyterLab 支持将 notebook 导出为多种格式,包括 HTML、PDF、Markdown 等。这可以通过 File > Export Notebook As... 菜单选项来实现。

什么是 JupyterLab 的 Kernel

  • JupyterLab 的 Kernel 是一个计算引擎,它执行用户在 notebook 中编写的代码。
  • 每个 notebook 都与一个 Kernel 关联,这个 Kernel 可以是不同的编程语言,如 Python、R 或 Julia。Kernel 也能访问资源,比如内存或 CPU。

将Kernel设置为机器人的虚拟环境

  • 当你打开后续的 .ipynb 教程文档时,你需要手动选择 notebook 中的 kernel 以便可以正确执行机器人相关的代码块。
  • 具体方法如下:点击 notebook 选项卡右上角"⭕"旁边的 Kernel 选项,从下拉菜单中选择 Python 3 (ipykernel)。

Kernel管理

  • 启动:当打开一个 Jupyter 笔记本时,相关联的 Kernel 会自动启动,启动后会在文件列表中对应笔记的前方出现一个小绿点。
  • 重启:如果 Kernel 崩溃或者需要清除当前会话的状态,可以通过 "Kernel" -> "Restart Kernel..." 选项来重启 Kernel。
  • 停止:在 Kernel 正在运行的笔记界面中,通过 "Kernel" -> "Shut Down Kernel" 来停止当前笔记的 Kernel;也可以通过 "Kernel" -> "Shut Down All Kernels" 来停止所有笔记的 Kernel。
  • 注意:如果你在某个教程的 Kernel 中调用了摄像头,没有停止该笔记的 Kernel,则会一直占用这个资源,其它教程则不能正常使用,需要停止该教程的 Kernel 之后其它教程才能正常使用。

运行代码块

选择了正确的 kernel 之后,就可以运行 notebook 里面的代码块了。在 JupyterLab 中,代码块是 notebook 的基本组成部分。运行代码块的操作如下:

  • 运行单个代码块:选择你想运行的代码块,然后点击工具栏上的 "▶︎" 按钮或使用快捷键 Shift + Enter。这将执行当前代码块并选中下一个代码块。
print("test text in jupyterlab")
  • 运行所有代码块:你也可以运行整个 notebook 中的所有代码块。为此,点击工具栏的 Run 菜单,然后选择 Run All Cells。
for i in range(0, 10):
    print(i)
  • 停止代码块运行:如果需要停止正在运行的代码块,可以点击工具栏上的 "■" 按钮。

通过这些基础的操作方法,你可以有效地使用 JupyterLab 来完成各种任务。更多高级功能和详细的使用指南可以在 JupyterLab 的官方文档中找到。

删除代码块输出

  • 删除单个代码块输出:选择一个代码块,依次点击上方的 Edit - Clear Cell Output
  • 删除全部代码块输出: 依次点击上方的 Edit - Clear Outputs of All Cells

更多高阶内容



Jetson 02 Python 底盘运动控制

Python 底盘运动控制

在本章节中我们会写一个Python例程,用于控制机器人底盘运动,你也可以自行使用其它语言来进行机器人底盘的运动控制。


底盘控制原理

在本例程中,我们使用 JupyterLab 中的代码块,生成一串 JSON 指令,通过 Jetson 的 GPIO 串口(默认与下位机通信的波特率为115200),将这个 JSON 指令发送给下位机,下位机收到指令后开始执行动作。

你可以参考后续的章节来了解都可以给下位机发送什么样的指令,你也可以使用其它语言来实现这一功能,或者自己写一个上位机的应用。


这样设计的优点

我们使用上位机+下位机的架构可以充分解放上位机的宝贵资源,上位机(树莓派,Jetson 等 SBC)类似人类的大脑,ESP32作为下位机类似人类的小脑,上位机执行视觉处理/决策方面的高阶控制,下位机执行具体的运动控制/插值等低阶控制。这样可以做到大小脑分工合作,下位机负责高频PID控制可保证车轮转速准确,上位机也不需要在这类低复杂度高算力的工作上浪费资源。


主程序 app.py

项目文件夹中的 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”的值为底盘类型:

  • 1:RaspRover(四轮四驱底盘)
  • 2: UGV Rover(六轮四驱底盘)
  • 3:UGV Beast(履带底盘)

“module”的值为模块类型:

  • 0:没有安装模块和安装有云台模块都可以设置为该值
  • 1:机械臂模块(如果没有安装机械臂会导致底盘 Ping 不通关节报错)
  • 2:云台模块(如果没有安装云台会导致底盘Ping不同关节报错)

使用案例如下,例如你使用的是安装有 云台 的 UGV Rover 产品,你可以通过向下位机发送下面这条指令来设置底盘类型:

{"T":900,"main":2,"module":0} 或 {"T":900,"main":2,"module":2}
  • 上面这两条指令都可以用来配置带有云台的 UGV Rover 产品,如果你不需要底盘反馈云台的角度信息(仅针对用户的二次开发,例程中暂不包含这方面应用),推荐使用前者。

你可以根据自己手中产品的类型,更改下面代码块中的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})


底盘转向原理

上面的例程中,你可以控制机器人向前走两秒钟后停止,后续可以通过更改参数来对底盘进行转向控制,底盘采用差速转向原理进行运动控制。

当车辆转弯时,内侧轮(转向方向相同的那一侧)需要行进更短的距离,因此需要旋转得更慢,以保持车辆的稳定性。 差速器通过允许两个驱动轮以不同速度旋转来实现这一目标。通常情况下,外侧轮(转向方向相反的那一侧)旋转得更快,而内侧轮旋转得更慢。 这种不同的旋转速度导致车辆产生转向运动,从而使其沿着预期的方向转向。

你可以给两侧车轮不同的目标线速度来控制车辆的转向,并且可以轻松地调整转向半径。



Jetson 03 云台控制和 LED 灯控制

云台控制和 LED 灯控制

云台控制

基础介绍

带有云台的产品上含有两个舵机,分别为水平舵机和倾斜舵机。水平舵机控制云台水平平面的转动,转动范围 ±180°(总范围360°);倾斜舵机控制云台竖直平面的转动,转动范围 -45°~90°(总范围 135°)。

对于没有配有云台的产品,也可以自行在 RaspRover 上扩展云台。

# 导入用于控制底盘的库
from base_ctrl import BaseController

base = BaseController('/dev/ttyTHS0', 115200)

在上面的代码块我们导入并实例化用于底盘控制的库,接下来通过改变云台水平舵机和倾斜舵机的角度来控制云台运动。

改变以下代码中的值,以下代码中:

  • input_x 为云台水平舵机的转动角度,水平舵机平面 PAN 轴转动范围 ±180°(总范围360°);
  • input_y 为云台倾斜舵机的转动角度,倾斜舵机竖直平面 TILT 轴转动范围 -45°~90°(总范围135°);
  • input_speed 为云台转动的速度,当速度值为 0 时,转动速度最快;
  • input_acc 为云台转动开始和结束时的加速度,数值越小启停越平滑。当加速度值为 0 时,则按照最大的加速度转动。

运行以下代码,可以看见云台会向右且向上分别运动 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 为云台水平舵机的转动模式。当值为 -1 时,则表示为云台连续向左转动(顺时针转动);当值为 1 时,则表示为云台连续向右转动(逆时针转动);当值为0时,则表示停止转动。
  • input_y 为云台倾斜舵机的转动模式。当值为 -1 时,则表示为云台倾斜舵机连续向下转动(低头);当值为 1 时,则表示为云台倾斜舵机连续向上转动(抬头);当值为0时,则表示停止转动。
  • input_speed 为云台转动的速度。

其中,只有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)


LED灯光控制

基础介绍

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)



Jetson 04 OLED 屏幕控制

OLED 屏幕控制

本教程介绍了如何通过JSON指令控制连接到ESP32模组的OLED显示屏。OLED显示屏是一种常用的显示设备,可以用于显示各种信息,如文本、图像等。

OLED 屏幕基础信息

OLED显示屏通过I2C(IIC)接口与下位机ESP32模组进行通信。它可以显示自定义的文本内容,并支持多行显示。

产品上配有一个 OLED 显示屏,该显示屏通过 IIC 与下位机 ESP32 模组进行通信,当开机后会自动显示一些下位机的基础信息,上位机可以通过发送 JSON 指令来更改显示屏上显示的内容。

OLED 屏幕控制 JSON 指令

  • {"T":3,"lineNum":0,"Text":"putYourTextHere"}
    • 控制显示屏显示自定义内容
    • lineNum 是行数,一条JSON指令可以改变一行的内容。针对大部分产品使用的0.91英寸的OLED显示屏,lineNum的数值可以为0、1、2、3,共四行。
    • Text 是你希望在这一行显示的文字内容。如果内容太多,会自动换行,但同时也会挤掉最后一行。

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 屏幕上显示简单文字的方法,接下来我们写一个稍微复杂一些的例程,运行以下代码块后,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 屏幕的第一行开始显示当前的时间,每秒钟更新一次,上面这条函数是无限循环的,你可以通过点击上面的 ■ 按键来终止程序运行。


05 在 JupyterLab 中构建 UI 界面

在 JupyterLab 中构建 UI 界面

在JupyterLab中构建UI界面通常使用的是ipywidgets库,它提供了一种简单而强大的方法来创建交互式用户界面。下面是详细的步骤:


导入所需要的库

在我们的产品中已经安装了 ipywidgets 库。如果你运行代码块时提示找不到这个库,可以通过 pip install ipywidgets 来安装实现 UI 界面中所需的库。

选中以下代码块,按 Ctrl + Enter 运行该代码。

import ipywidgets as widgets
from IPython.display import display


创建 UI 组件

我们可以使用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界面

最后,我们将所有的UI组件放在一个布局中,并通过display函数显示出来。

# 将所有组件放在一个垂直布局中
ui = widgets.VBox([text, button, output])

# 显示UI界面
display(ui)

通过这些步骤,我们就可以在JupyterLab中构建一个简单的UI界面了。用户可以在文本框中输入内容,点击按钮后,程序会根据输入内容在输出框中显示相应的问候语。


Jetson 06 获取底盘反馈信息

获取底盘反馈信息

产品开机后,下位机默认会持续向上位机反馈各类信息,你可以通过这些反馈信息来获取目前产品的工作状态。

通常来说,你需要连续获取下位机反馈的信息,但在本例程中,我们只获取一个由下位机反馈的 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 信息

以下的代码仅用于理解底层的串口读取 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) # 如果未找到换行符,将数据添加到缓冲区
  • 该方法用于从串口读取数据,并返回一行完整的JSON数据(以换行符\n为分隔)。
  • 如果缓冲区中已经存在完整的一行数据,则直接返回该行数据。
  • 如果缓冲区中没有完整的一行数据,则通过 in_waiting 方法获取串口缓冲区中可读取的字节数,最多读取512字节。
  • 从串口读取数据,并将其与缓冲区中的数据合并。
  • 查找新读取数据中是否有换行符,如果有则提取完整的一行数据,并更新缓冲区。
  • 如果没有换行符,则将新读取的数据添加到缓冲区,继续读取直到找到换行符为止。行符为止。

函数特性

  • 非阻塞: 这个函数使用了非阻塞的读取方式,即使串口没有数据可读,也不会阻塞程序的执行,而是等待有数据可读时才读取。
  • 高效: 采用了较小的缓冲区,每次最多读取512字节,减少了内存消耗,并且及时处理数据,避免了缓冲区溢出。
  • 灵活性: 可以灵活地读取任意长度的数据,并且自动处理换行符分割的数据,非常适用于读取JSON等结构化数据。
  • 可靠性: 在处理读取过程中考虑了各种情况,如缓冲区数据不足、读取数据中不含换行符等情况,保证了读取的准确性和稳定性。
  • 这个函数非常适合用于实时读取串口中的JSON数据,尤其是在需要非阻塞读取的情况下。情况下。



Jetson 07 使用 JSON 指令控制下位机

使用 JSON 指令控制下位机

本产品使用大小脑架构开发,上位机通过串口(Jetson 通过 GPIO 串口)将 JSON 格式的命令发送给下位机。 注意:本章课程作为后面介绍下位机JSON指令集的前置课程,内容于前面的 Python 底盘运动控制章节的内容相似,如果你已知了解过那个章节,可以简单了解下 JSON 数据格式的优点后直接学习 JSON 指令集。

JSON 数据格式的优点

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它已经成为互联网上数据传输的标准之一。以下是JSON的一些优势:

  • 可读性强:JSON采用了一种易于人类理解和编写的文本格式,使用键值对的形式组织数据,这使得数据在传输和存储时更加易于阅读和理解。
  • 轻量级:相比于XML等其他数据格式,JSON的语法更加简洁紧凑,因此它更加轻量级,能够减少数据传输的大小和网络带宽的占用,提高传输效率。
  • 易于解析:JSON数据结构简单明了,易于解析和序列化,几乎所有的编程语言都提供了JSON的解析和生成库,使得开发人员可以方便地处理JSON数据。
  • 与各种语言兼容性好:JSON几乎在所有编程语言中都有支持,因此可以很方便地在不同的平台和系统中进行数据交换和通信。
  • 支持多种数据类型:JSON支持多种数据类型,包括字符串、数字、布尔值、数组和对象等,这使得它可以灵活地表示各种类型的数据结构。
  • 与Web技术无缝结合:JSON最初是由JavaScript发展而来,因此与Web技术的集成非常紧密,它与JavaScript语言的兼容性非常好,可以很方便地在Web应用中使用。方便地在Web应用中使用。


简单的 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秒循环发送运动控制的指令。


08 下位机 JSON 指令集

下位机 JSON 指令集

在上一个章节中我们介绍了一个简单的例程,在例程中我们通过上位机向下位机发送运动控制指令,下位机可以接收的指令非常多,在这个章节中我们会介绍这些指令。


JSON 指令的构成

以上一章节我们发送的 {"T":1,"L":0.2,"R":0.2} 指令为例,这个 JSON 数据中的 T 值代表指令的类型(Type),L 值代表左侧(LEFT)车轮的目标线速度,R 值代表右侧(RIGHT)车轮的目标线速度,线速度的单位默认为 m/s,总结起来的意思就是这是一条运动控制指令,运动控制的参数分别为左右两侧车轮的目标线速度。

后面的所有 JSON 指令,都会包含一个 T 值用于定义指令的类型,但是具体的指令参数会根据指令类型的不同而有所区别。


JSON 指令集

你可以在我们开源的下位机例程的 json_cmd.h 文件中查看这些指令的定义,或自行为其添加新的下位机功能。

运动控制类指令

这些指令是移动机器人最基础的指令,用于运动相关的功能控制。

下面的指令中,每种指令包含三个部分:案列,简短的介绍和详细的介绍。

CMD_SPEED_CTRL

  • {"T":1,"L":0.5,"R":0.5}
  • 设置两侧车轮目标线速度(速度闭环控制)
L 和 R 分别代表左右侧车轮的目标线速度,单位为 m/s,负值代变反向转动,0 为停止。目标线速度的取值范围取决于产品所使用的电机/减速器/车轮直径,相关的计算公式可以在开源的下位机例程中找到。这里需要注意的是,对于使用碳刷直流电机的底盘来说,当给定的目标速度的绝对值很小时(但不为 0),由于碳刷直流电机的低速性能通常比较差,所以产品在移动过程中速度可能波动比较大。

CMD_PWM_INPUT

  • {"T":11,"L":164,"R":164}
  • 设置两侧驱动轮的PWM值(速度开环控制)
L 和 R 分别代表左右两侧车轮的 PWM 值,该值的范围为 -255 ~ 255,负值代表反向,当数值的绝对值为 255 时代表 PWM 为 100%,代表让这一侧的车轮满功率运行。

CMD_ROS_CTRL

  • {"T":13,"X":0.1,"Z":0.3}
  • ROS控制(速度闭环控制)
该指令用于 ROS 上位机控制底盘移动的指令,X 代表移动的线速度,单位为 m/s 可以为负值;Z 代表转向的角速度,单位为 rad/s 可以为负值。

CMD_SET_MOTOR_PID

  • {"T":2,"P":20,"I":2000,"D":0,"L":255}
  • PID控制器设置
该指令用于给PID控制器调参,上面 JSON 案例中的 PID 参数为该产品的默认参数,其中 L 代表 WINDUP_LIMITS,是预留的接口,目前的产品中暂时还没有使用到该参数。


OLED 显示屏控制指令

产品上安装有 OLED 显示屏,该显示屏通过 I2C 与下位机主控 ESP32 模组进行通信,上位机可通过发送 JSON 指令来更改显示屏上显示的内容。

CMD_OLED_CTRL

  • {"T":3,"lineNum":0,"Text":"putYourTextHere"}
  • 控制显示屏显示自定义内容
lineNum 是行数,一条 JSON 指令可以改变一行的内容,下位机收到一条新的指令后,开机默认的 OLED 界面会消失,取而代之的是你新增加的内容,对于大部分产品使用的 0.91英寸的OLED显示屏,lineNum 的数值可以为 0、1、2、3,共四行;Text 是你希望在这一行显示的文字内容,如果你这一行的内容太多,会自动换行,但同时也会挤掉最后一行。

CMD_OLED_DEFAULT

  • {"T":-3}
  • 控制显示屏显示自定义内容
使用这条指令让 OLED 显示屏显示开机默认的画面。


模块类型

移动底盘上面会安装不同类型的模块(无/机械臂/云台),使用这条指令来告诉下位机当前所安装的模块类型,这条指令通常会在上位机开机时自动发送给下位机,后面的章节会有这部分的介绍。

CMD_MODULE_TYPE

  • {"T":4,"cmd":0}
  • 设置模块类型
cmd 的值代表模块的类型,目前有三种类型可以选择,0:什么都不装,1:机械臂,2:云台


IMU 相关功能

底盘上安装有 IMU 传感器,你可以通过以下命令来获取 IMU 传感器的数据,这里需要注意的是,产品开机后会默认开启底盘信息连续反馈功能(其中包含有IMU的信息),这里的 IMU 相关功能仅在底盘信息连续反馈功能关闭时才有必要使用。

CMD_GET_IMU_DATA

  • {"T":126}
  • 获取 IMU 数据
发送该指令后可获取 IMU 的数据。

CMD_CALI_IMU_STEP

  • {"T":127}
  • IMU 校准(预留接口)
目前的产品程序不需要执行校准,此命令为预留接口。

CMD_GET_IMU_OFFSET

  • {"T":128}
  • 获取目前 IMU 的偏移量(预留接口)
使用该指令可反馈当前的 IMU 每个轴的偏移量。

CMD_SET_IMU_OFFSET

  • {"T":129,"x":-12,"y":0,"z":0}
  • 设置 IMU 的偏移量(预留接口)
使用该指令可以设置 IMU 每个轴的偏移量,此命令为预留接口,当前的产品不需要执行该指令。


底盘信息反馈

CMD_BASE_FEEDBACK

  • {"T":130}
  • 底盘信息反馈
产品开机后通常会默认开启底盘信息反馈,是自动的,如果底盘信息连续反馈功能关闭了,需要单次获取底盘的信息,可以使用该指令,可获取底盘的基础信息。

CMD_BASE_FEEDBACK_FLOW

  • {"T":131,"cmd":1}
  • 连续底盘信息反馈
cmd的值设置为1,开启该功能,此功能默认开启,会持续反馈底盘信息;当cmd的值设置为0,关闭该功能,关闭该功能后,上位机可以通过 CMD_BASE_FEEDBACK 来获取底盘信息。

CMD_FEEDBACK_FLOW_INTERVAL

  • {"T":142,"cmd":0}
  • 设置连续反馈信息的格外间隔时间
cmd的值为需要设置的格外间隔时间,单位为ms,通过该指令可以调整底盘反馈信息的频率。

CMD_UART_ECHO_MODE

  • {"T":143,"cmd":0}
  • 设置指令回声模式
当cmd的值设置为0时,关闭回声;当cmd的值设置为1时,开启回声,当开启指令回声模式后,下位机会输出接收到的指令。


WIFI 相关设置

CMD_WIFI_ON_BOOT

  • {"T":401,"cmd":3}
  • 设置开机时的 WIFI 模式。
cmd为0时,关闭WIFI功能;1-ap;2-sta;3-ap+sta。

CMD_SET_AP

  • {"T":402,"ssid":"UGV","password":"12345678"}
  • 设置AP模式的名称和密码。(ESP32建立热点)

CMD_SET_STA

  • {"T":403,"ssid":"WIFI_NAME","password":"WIFI_PASSWORD"}
  • 设置STA模式的名称和密码。(ESP32去连接已知热点)

CMD_WIFI_APSTA

  • {"T":404,"ap_ssid":"UGV","ap_password":"12345678","sta_ssid":"WIFI_NAME","sta_password":"WIFI_PASSWORD"}
  • 设置AP和STA模式的名称和密码。(AP+STA模式)

CMD_WIFI_INFO

  • {"T":405}
  • 获取当前WIFI的信息。

CMD_WIFI_CONFIG_CREATE_BY_STATUS

  • {"T":406}
  • 使用当前的设置新建一个WIFI配置文件。

CMD_WIFI_CONFIG_CREATE_BY_INPUT

  • {"T":407,"mode":3,"ap_ssid":"UGV","ap_password":"12345678","sta_ssid":"WIFI_NAME","sta_password":"WIFI_PASSWORD"}
  • 使用输入的设置新建一个 WIFI 配置文件。

CMD_WIFI_STOP

  • {"T":408}
  • 断开 WIFI 连接。


12V开关和云台设置

CMD_LED_CTRL

  • {"T":132,"IO4":255,"IO5":255}
  • 12V开关输出设置
产品下位机上面有两路12V开关接口,每路2个接口共四个接口,你可以通过这个指令来设置这些接口的输出电压,当数值为255时为3S电池电压。
产品默认使用这些接口来控制LED灯,你可以通过这个指令来控制LED灯的亮度。

CMD_GIMBAL_CTRL_SIMPLE

  • {"T":133,"X":0,"Y":0,"SPD":0,"ACC":0}
  • 云台基础控制指令
该指令用于控制云台的朝向。
X为水平方向的朝向,单位为角度,正值向右,负值向左,取值范围为 -180到180。
Y为数值方向的朝向,单位为角度,正值向上,负值向下,取值范围为 -30到90。
SPD为速度,ACC为加速度,当值为0时为最快速度/加速度。

CMD_GIMBAL_CTRL_MOVE

  • {"T":134,"X":45,"Y":45,"SX":300,"SY":300}
  • 云台连续控制指令
该指令用于连续控制云台朝向。
X为水平方向的朝向,单位为角度,正值向右,负值向左,取值范围为 -180到180。
Y为数值方向的朝向,单位为角度,正值向上,负值向下,取值范围为 -30到90。
SX和SY分别为X轴和Y轴的速度。

CMD_GIMBAL_CTRL_STOP

  • {"T":135}
  • 云台停止指令
当使用上面的命令让云台运动起来后,可以使用该指令让云台可以随时停下来。

CMD_GIMBAL_STEADY

  • {"T":137,"s":0,"y":0}
  • 云台自稳功能
s为0时关闭该功能,s为1时开启该功能,该功能开启后云台会自动通过IMU数据来调整云台的竖直角度,y为云台的与地面的目标夹角(即使开启云台自稳功能,摄像头也可以上下看)。

CMD_GIMBAL_USER_CTRL

  • {"T":141,"X":0,"Y":0,"SPD":300}
  • 云台UI控制
该指令用于UI界面控制云台,X值可以为-1,0和1,-1为向左转动,0为停止,1为向右转动。
Y值可以为-1,0和1,-1为向下转动,0为停止,1为向上转动。
SPD为速度


机械臂控制

CMD_MOVE_INIT

  • {"T":100}
  • 机械臂运动到初始姿态
正常情况下机械臂开机时会自动转动到初始位置。 该指令会引起进程阻塞。

CMD_SINGLE_JOINT_CTRL

  • {"T":101,"joint":0,"rad":0,"spd":0,"acc":10}
  • 机械臂单轴运动控制
  • joint:关节序号。
    • 1:BASE_JOINT基础关节。
    • 2:SHOULDER_JOINT肩关节。
    • 3:ELBOW_JOINT肘关节。
    • 4:EOAT_JOINT手腕/夹爪关节。
  • rad:为需要转动到的角度(以弧度制形式来显示),以各个关节的初始位置为准,各个关节的默认角度和转动方向如下:
    • BASE_JOINT的初始位置默认角度为 0,角度转动范围为 3.14 至 -3.14 之间。角度增加时,基础关节向左转动;角度减少时,基础关节向右转动。
    • SHOULDER_JOINT的初始位置默认角度为 0,角度转动范围为 1.57 至 -1.57 之间。角度增加时,肩关节向前转动;角度减少时,肩关节会向后转动。
    • ELBOW_JOINT的初始位置默认角度为 1.570796,角度转动范围为 3.14 至 -1.11 之间。角度增加时,肘关节向下转动;角度减少时,肘关节会往反方向转动。
    • EOAT_JOINT的初始位置默认角度为 3.141593。产品默认的是夹爪关节,角度转动范围为 1.08 至 3.14 之间,角度减少时,夹爪关节会张开。若更换为手腕关节,角度转动范围为 1.08 至 5.20 之间,角度增加时,手腕关节向下转动;角度减少时,手腕关节向上转动。
  • spd:转动的速度,速度单位为步/秒,舵机的一圈为 4096 步,数值越大速度越快,当速度值为 0 时,以最大速度转动。
  • acc:转动开始和结束时的加速度,数值越小启停越平滑,数值可以为 0-254 ,单位为 100 步/秒^2。如设置为 10 时,则按照 1000 步每秒的平方加速度变速。当加速度值为 0 时,则按照最大的加速度运行。

CMD_JOINTS_RAD_CTRL

  • {"T":102,"base":0,"shoulder":0,"elbow":1.57,"hand":1.57,"spd":0,"acc":10}
  • 以弧度制形式来控制机械臂全部关节的转动
  • base:基础关节的角度,角度转动范围见上方“CMD_SINGLE_JOINT_CTRL”指令中rad键的说明。
  • shoulder:肩关节的角度。
  • elbow:肘关节的角度。
  • hand:夹爪/手腕关节的角度。
  • spd:转动的速度,速度单位为步/秒,舵机的一圈为4096步,数值越大速度越快,当速度值为0时,以最大速度转动。
  • acc:转动开始和结束时的加速度,数值越小启停越平滑,数值可以为0-254,单位为100步/秒^2。如设置为10时,则按照1000步每秒的平方加减速度变速。当加速度值为0时,则按照最大的加速度运行。

CMD_SINGLE_AXIS_CTRL

  • {"T":103,"axis":2,"pos":0,"spd":0.25}
  • 单轴坐标控制
  • axis,1-x,2-y,3-z,4-t。除T轴外,其它轴的pos参数为mm,T轴的单位为rad。spd是速度系数,数值越高速度越快。

CMD_XYZT_GOAL_CTRL

  • {"T":104,"x":235,"y":0,"z":234,"t":3.14,"spd":0.25}
  • 机械臂坐标运动控制(逆运动学控制)
该函数会引发阻塞

CMD_XYZT_DIRECT_CTRL

  • {"T":1041,"x":235,"y":0,"z":234,"t":3.14}
  • 机械臂坐标运动控制(逆运动学控制)
该函数不会引发阻塞

CMD_SERVO_RAD_FEEDBACK

  • {"T":105}
  • 反馈机械臂的坐标信息

CMD_EOAT_HAND_CTRL

  • {"T":106,"cmd":1.57,"spd":0,"acc":0}
  • 末端关节控制(弧度制)
  • cmd:为需要转动到的角度(以弧度制来显示)。EOAT_JOINT的初始位置默认角度为3.141593。
    • 产品默认的是夹爪关节,角度转动范围为1.08至3.14之间,角度减少时,夹爪关节会张开。
    • 若更换为手腕关节,角度转动范围为1.08至5.20之间,角度增加时,手腕关节向下转动;角度减少时,手腕关节向上转动。
  • spd:转动的速度,速度单位为步/秒,舵机的一圈为4096步,数值越大速度越快,当速度值为0时,以最大速度转动。
  • acc:转动开始和结束时的加速度,数值越小启停越平滑,数值可以为0-254,单位为100步/秒^2。如设置为10时,则按照1000步每秒的平方加减速度变速。当加速度值为0时,则按照最大的加速度运行。

CMD_EOAT_GRAB_TORQUE

  • {"T":107,"tor":200}
  • 夹爪力量控制
tor的值最高可以为1000,代表100%的力量。

CMD_SET_JOINT_PID

  • {"T":108,"joint":3,"p":16,"i":0}
  • 关节PID设置

CMD_RESET_PID

  • {"T":109}
  • 重置关节PID

CMD_SET_NEW_X

  • {"T":110,"xAxisAngle":0}
  • 设置新的X轴方向

CMD_DYNAMIC_ADAPTATION

  • {"T":112,"mode":0,"b":1000,"s":1000,"e":1000,"h":1000}
  • 动态外力自适应控制


其它设置

CMD_HEART_BEAT_SET

  • {"T":136,"cmd":3000}
  • 设置心跳函数时间
cmd单位为ms,可以使用该指令设置心跳函数时间,如果下位机在该时间内没有接收到新的运动指令,会自动停止运动,用于避免使用过程中上位机死机导致下位机一直运动下去。
程中上位机死机导致下位机一直运动下去。

CMD_SET_SPD_RATE

  • {"T":138,"L":1,"R":1}
  • 设置左右速度比率
产品使用差速转向原理,当产品左右车轮给相同的目标速度时,产品有可能由于编码器误差或轮胎抓地力误差导致产品不走直线,你可以使用该指令来对左右侧车轮的速度进行微调,例如左边的车轮需要转动慢一些的话,可以将L的数值改为 0.98。L值和R值尽量不要设置大于一的数值。

CMD_GET_SPD_RATE

  • {"T":139}
  • 获取当前的速度比率
使用该指令可以获取当前的速度比例


ESP-NOW 相关设置

CMD_BROADCAST_FOLLOWER

  • {"T":300,"mode":1}
  • {"T":300,"mode":0,"mac":"CC:DB:A7:5B:E4:1C"}
  • 设置ESP-NOW被广播控制的模式
    • mode为1时,可以让其它设备通过广播指令控制;mode为0时只能由mac地址的设备控制。

CMD_GET_MAC_ADDRESS

  • {"T":302}
  • 获取当前设备的MAC地址

CMD_ESP_NOW_ADD_FOLLOWER

  • {"T":303,"mac":"FF:FF:FF:FF:FF:FF"}
  • 将MAC地址添加到被控设备(PEER)

CMD_ESP_NOW_REMOVE_FOLLOWER

  • {"T":304,"mac":"FF:FF:FF:FF:FF:FF"}
  • 将MAC地址从PEER中删除

CMD_ESP_NOW_GROUP_CTRL

  • {"T":305,"dev":0,"b":0,"s":0,"e":1.57,"h":1.57,"cmd":0,"megs":"hello!"}
  • ESP-NOW 组播控制

CMD_ESP_NOW_SINGLE

  • {"T":306,"mac":"FF:FF:FF:FF:FF:FF","dev":0,"b":0,"s":0,"e":1.57,"h":1.57,"cmd":0,"megs":"hello!"}
  • ESP-NOW 单播/组播控制


任务文件相关功能

该功能属于下位机的高阶功能,正常带上位机使用时通常不需要进行以下的操作。

CMD_SCAN_FILES

  • {"T":200}
  • 扫描当前的任务文件

CMD_CREATE_FILE

  • {"T":201,"name":"file.txt","content":"inputContentHere."}
  • 新建任务文件

CMD_READ_FILE

  • {"T":202,"name":"file.txt"}
  • 读取任务文件

CMD_DELETE_FILE

  • {"T":203,"name":"file.txt"}
  • 删除任务文件

CMD_APPEND_LINE

  • {"T":204,"name":"file.txt","content":"inputContentHere."}
  • 在任务文件末尾添加新的指令

CMD_INSERT_LINE

  • {"T":205,"name":"file.txt","lineNum":3,"content":"content"}
  • 在任务文件中间插入新的指令

CMD_REPLACE_LINE

  • {"T":206,"name":"file.txt","lineNum":3,"content":"Content"}
  • 替换任务文件中的某一个指令


总线舵机设置

CMD_SET_SERVO_ID

  • {"T":501,"raw":1,"new":11}
  • 更改舵机ID
raw是舵机的原始ID(新舵机都是1),new是要更改为的ID,最大不超过254,不可为负,255为广播ID。

CMD_SET_MIDDLE

  • {"T":502,"id":11}
  • 将该舵机的目前位置设置为舵机中位(仅ST系列舵机有效)。

CMD_SET_SERVO_PID

  • {"T":503,"id":14,"p":16}
  • 设置舵机的PID的P值。


ESP32 相关功能

CMD_REBOOT

  • {"T":600}
  • 重启。

CMD_FREE_FLASH_SPACE

  • {"T":601}
  • 获取 FLASH 的剩余空间大小。

CMD_BOOT_MISSION_INFO

  • {"T":602}
  • 输出当前的开机任务文件。

CMD_RESET_BOOT_MISSION

  • {"T":603}
  • 重置开机任务文件。

CMD_NVS_CLEAR

  • {"T":604}
  • 清理 ESP32 的 NVS 区,如果不能成功WIFI连接,可以尝试调用该指令,然后重启。

CMD_INFO_PRINT

  • {"T":605,"cmd":1}
  • 设置信息反馈模式。
  • cmd为1时,打印输出调试信息;2,连续的底盘信息反馈;0,什么都不反馈。


Jetson 09 开机自动发送指令

开机自动发送指令

本章教程用于介绍上位机每次开机后会自动执行指令和向下位机发送一些指令,本章教程的代码块不需要执行(也不能执行),仅用于理解开机后产品的一些自动操作,如果你有需要的话,更改或增加这些指令。

cmd_on_boot() 函数

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串口传递给下位机,后面我们会详细解释这里默认的开机自动运行的指令是什么意思。

  • base -c {"T":142,"cmd":50}

用于设置下位机连续反馈信息的格外间隔时间,cmd的值的单位为ms,此功能用于降低下位机反馈信息的频率,目的是减轻上位机处理下位机反馈信息的算力压力。

  • base -c {"T":131,"cmd":1}

开启下位机连续信息反馈功能,该功能开启后,不需要上位机一问一答地去获取下位机的信息,下位机正常会默认开启该功能,但我们这里还是再发送一次开启该功能的指令,比较保险。

  • base -c {"T":143,"cmd":0}

关闭串口指令回声,这样上位机在向下位机发送指令时,下位机不会再将接收到的指令反馈给上位机,这样可以避免上位机处理无用的信息。

  • base -c {"T":4,"cmd":2}

设置外置模块的类型,cmd的值为0时,代表没有外接模块;1,机械臂;2,云台,如果你的产品没有安装云台或机械臂,你需要把这里的数值改为0。

  • base -c {"T":300,"mode":0,"mac":"EF:EF:EF:EF:EF:EF"}

避免底盘通过其它设备的ESP-NOW广播控制,但是除mac地址的设备外,你可以自己随意编一个MAC地址,也可以使用你自己的ESP32遥控器的MAC地址。

  • send -a -b

将广播地址(FF:FF:FF:FF:FF:FF)添加到peer,这样你可以后续直接通过广播信号来将广播信息发送到其它设备。


其它的上位机命令行指令你可以通过后面的 WEB 命令行应用章节来了解。


10 播放音频文件

播放音频文件

由于安全方面的原因,你并不能通过 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  # 设置最小播放间隔时间


11 文字转语音 (TTS)

文字转语音(TTS)

由于安全方面的原因,你并不能通过 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 控制语音播放的同步,确保同一时间只有一个语音在播放。


Jetson 12 使用 Flask 实现低延时图传

使用 Flask 实现低延时图传

本章节介绍如何使用 Flask 建立一个 Web 应用,用于显示机器人摄像头的实时画面,由于 Web 应用具有可跨平台的特性,用户可以在手机/PC/平板等设备上通过浏览器来观看摄像头的实时画面,实现无线图传功能。

什么是 Flask?

Flask 是一个轻量级的Web应用框架,用于使用 Python 快速构建Web应用。

  • 轻量级:Flask 是一个轻量级框架,它的核心库相对较小,但具有足够的灵活性和可扩展性,使得开发者可以选择添加需要的扩展和库。
  • 简单易用:Flask 设计简单,容易上手。它的API清晰明了,文档详尽,使得开发者能够迅速开始并快速构建Web应用。
  • 路由系统:Flask使用装饰器来定义URL路由,将请求映射到相应的处理函数。这使得创建不同页面和处理不同请求变得直观而简单。
  • 模板引擎:Flask集成了 Jinja2 模板引擎,使得在应用中构建动态内容变得更加容易。模板引擎允许你在HTML中嵌入动态生成的内容。
  • 集成开发服务器:Flask带有一个简单的集成开发服务器,方便开发和调试。然而,在生产环境中,建议使用更强大的Web服务器,如 Gunicorn 或 uWSGI。
  • 插件和扩展:Flask支持许多插件和扩展,以便添加额外的功能,如数据库集成、身份验证、表单处理等。
  • RESTful支持:Flask对RESTful风格的API提供了良好的支持,使得构建和设计RESTful API变得简单。
  • WSGI兼容:Flask 是基于WSGI(Web Server Gateway Interface)的,这使得它能够在许多符合WSGI标准的Web服务器上运行。
  • 社区活跃:Flask拥有庞大且活跃的社区,这意味着你可以轻松地找到大量的文档、教程和第三方扩展,以及得到支持。

准备工作

由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。

这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。

如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。

结束主程序

1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。

2. 点击 Other 内的 Terminal,打开终端窗口。

3. 在终端窗口内输入 bash 后按回车。

4. 现在你可以使用 Bash Shell 来控制机器人了。

5. 输入命令: sudo killall -9 python


Web 应用例程

注意,不能在 Jupyter Lab 中运行下面的代码块

由于 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 来结束运行。

Flask 的程序介绍

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路由获取实时视频流。


Jetson 13 在 Jupyter Lab 中显示实时画面

在 Jupyter Lab 中显示实时视频画面

在上一章中我们使用 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()  # 启动线程


Jetson 14 延时摄影

延时摄影

本章教程基于上一章的教程,按照一定的时间间隔来从摄像头获取画面,并将其存储在 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 15 OpenCV 运动检测

OpenCV 运动检测

本章教程使用 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 16 通过按键控制拍照

通过按键控制拍照

本章教程通过在页面上增加按键来控制摄像头拍照和录像,和之前的教程类似,图片会被默认保存在 /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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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/ 内来查看拍摄的照片。


Jetson 17 OpenCV 人脸识别

基于 OpenCV 的人脸识别

本章节介绍如何使用 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 18 基于 DNN(深度神经网络)的物体识别

基于 DNN(深度神经网络)的物体识别

本章节介绍如何使用 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 19 基于 OpenCV 的颜色识别

基于 OpenCV 的颜色识别

在本章教程中我们会在 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 20 基于 OpenCV 的颜色追踪

基于 OpenCV 的颜色追踪

在本章教程中我们会在 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 21 基于 OpenCV 的巡线自动驾驶

基于 OpenCV 的巡线自动驾驶

在本章教程中我们会在使用 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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 22 基于 MediaPipe 的手势识别

基于 MediaPipe 的手势识别

本章节介绍如何使用 MediaPipe + OpenCV 来实现手势识别。

什么是 MediaPipe?

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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 STOP 释放摄像头资源后重新运行代码块
  • 重启设备

注意事项

如果使用USB摄像头则需要取消注释 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 这一句。

本章节特性

当代码块正常运行时,你可以把自己手放在摄像头前面,实时视频画面中会标注出人手的关节,标注出的关节会随人手的变化而变化,同时也会输出各个关节的位置,方便进行手势控制方面的二次开发。

MediaPipe 的手势识别过程采用不同的名称来对应不同的关节,你可以通过调用对应的编号来获取该关节的位置信息。

MediaPipe Hand

  • WRIST
  • THUMB_CMC
  • THUMB_MCP
  • THUMB_IP
  • THUMB_TIP
  • INDEX_FINGER_MCP
  • INDEX_FINGER_PIP
  • INDEX_FINGER_DIP
  • INDEX_FINGER_TIP
  • MIDDLE_FINGER_MCP
  • MIDDLE_FINGER_PIP
  • MIDDLE_FINGER_DIP
  • MIDDLE_FINGER_TIP
  • RING_FINGER_MCP
  • RING_FINGER_PIP
  • RING_FINGER_DIP
  • RING_FINGER_TIP
  • PINKY_MCP
  • PINKY_PIP
  • PINKY_DIP
  • PINKY_TIP
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()


Jetson 23 基于 MediaPipe 的人脸识别

基于 MediaPipe 的人脸识别

本章节介绍如何使用 MediaPipe + OpenCV 来实现人脸识别。

什么是 MediaPipe?

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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()


Jetson 24 基于 MediaPipe 的姿态检测

基于 MediaPipe 的姿态检测

本章节介绍如何使用 MediaPipe + OpenCV 来实现姿态检测。

什么是 MediaPipe?

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 关闭实时视频,释放摄像头资源

如果运行时不能看到摄像头实时画面

  • 需要点击上方的 Kernel - Shut down all kernels
  • 关闭本章节选项卡,再次打开
  • 点击 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()  # 启动线程


Jetson 25 简易的 WEB 应用

简易的 WEB 应用

在之前的章节中我们介绍了如何使用 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 的代码块下方看到你在网页上输入的内容,且网页也会显示出后端所接收到的内容。


Jetson 26 主程序架构介绍

主程序架构介绍

文件结构和功能介绍

  • ugv_jetson
    • [文件夹] media (README.MD 素材)
    • [文件夹] models (预训练的模型)]
    • [文件夹] sounds (用于存储音频文件,可以在这里配置语音包)
    • [文件夹] templates (WEB 应用的相关资,照片与视频也存在这里面源)
    • [文件夹] tutorial_cn (中文版交互式教程)
    • [文件夹] tutorial_en (英文版交互式教程
    • LICENSE (GPL-3.0 开源协议))
    • app.py (产品主程序,包 webRTC、 web-socket 和 flask 相关功能)
    • asound.conf (声卡配置文件)
    • audio_ctrl.py (音频功能相关的库)
    • autorun.sh (配置产品主程序和 jupyterlab 开机自动运行的脚本
    • base_camera.py (flask 实时视频的底层多线程采集的库,原项目为 flask-video-streaming)
    • base_ctrl.py (与下位机通信的库,通过串口与下位机进行通信)
    • config.yaml (配置文件,用于配置一些参数
    • cv_ctrl.py (OpenCV相关功能)
    • os_info.py (用于获取系统信息的库)
    • requirements.txt (python项目依赖库)
    • setup.sh (自动安装脚本)
    • start_jupyter.sh (开启 jupyterlab 服务器
    • sunny (sunny-ngrok 程序))

安装脚本

在项目文件夹中有一个名为 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代表没有模块或云台模块。

第一位数字:

  • 1:RaspRover
  • 2:UGV Rover
  • 3:UGV Beast

第二位数字:

  • 0;None/PT
  • 1: RoArm
  • 2: Pan-Tilt

该配置更新后或重装后只需要配置一次,会被保存在 config.yaml 中。


开源项目

本项目以 GPL-3.0 协议开源,你可以根据我们的项目来实现自己的项目,我们同时也会在后续持续更新更多功能。

项目地址:https://github.com/waveshareteam/ugv_jetson


27 YAML 配置文件设置

YAML 配置文件设置

什么是 YAML?

YAML(YAML Ain't Markup Language)是一种人类可读的数据序列化格式,用于表示复杂数据结构。它的主要用途包括配置文件、数据交换和存储,以及将数据传递给程序。

YAML 的优势

人类可读性

YAML 使用缩进和可读性良好的结构,使得文件易于阅读和理解。它不像 XML 或 JSON 那样显得冗长,更接近自然语言。

易于编写和编辑

YAML 的语法简洁清晰,不需要额外的标记符号(如 XML 的标签或 JSON 的大括号),因此更容易编写和编辑。

支持复杂数据结构

YAML 支持嵌套、列表、字典等复杂数据结构,可以轻松表示各种类型的数据。

可扩展性

YAML 允许使用标签和锚点来表示对象之间的关系,从而实现数据的复用和引用,提高了数据的可扩展性。

语言无关性

YAML 是一种通用的数据序列化格式,不依赖于特定的编程语言,因此可以被多种编程语言轻松解析和生成。


本产品的 config.yaml

在本产品的config.yaml中,我们配置了一些机器人相关的关键参数:

audio_config 音频相关设置

  • audio_output 是否使用音频输出
  • default_volume 默认音量大小
  • min_time_bewteen_play 最小的音频播放间隔时间
  • speed_rate TTS语速

base_config 基础信息

  • robot_name 产品名称
  • module_type 模块类型(0-无模块,1-机械臂,2-云台)
  • sbc_version 上位机版本

sbc_config 上位机设置

  • feedback_interval 接收反馈信息的间隔时间
  • disabled_http_log 禁用http服务器log信息

args_config 机器人参数设置

  • max_speed 最大速度
  • slow_speed 低速速度
  • max_rate 最大速度的比率
  • mid_rate 中等速度的比率
  • min_rate 低速速度的比率

cv OpenCV参数设置

  • default_color 颜色识别的默认目标颜色
  • color_lower 目标颜色的 HSV LOWER 值
  • color_upper 目标颜色的 HSV UPPER 值
  • min_radius 目标区域的半径阈值
  • sampling_rad 采样区半径
  • track_color_iterate 颜色跟踪(云台)速度比率
  • track_faces_iterate 人脸跟踪(云台)速度比率
  • track_spd_rate 云台转动速度
  • track_acc_rate 云台转读加速度
  • aimed_error 瞄准锁定判定阈值

cmd_config 指令类型代号

这些代号与对应的指令与下位机程序中的指令定义相关,用于上位机下位机通信联调,更改这些涉及到更改下位机程序。

code 功能代号

这些代号与功能对应,前端页面也需要加载这个 .yaml 文件来获取这些配置,这样前端 WEB 应用再与后端进行通信时才会让不同的按键对应不同的功能,如无必要不需要更改。

fb 反馈信息代号

这些代号与反馈的信息类型对应,这些反馈信息有些是底盘反馈给上位机的,有些是后端反馈给前端的,使用后端与前端使用这同一个.yaml文件来统一这些序号与反馈信息的类型,如无必要不需要更改。


28 Crontab 开机自动运行脚本

Crontab 开机自动运行脚本

在之前的教程中,我们简单介绍了如果关闭产品主程序的自动运行,所以用的方法就是在 Crontab 文件中注释掉运行产品主程序的命令,在本章教程中,你将会了解到更多有关 Crontab 的信息,以及为什么我们采用 Crontab 而不采用 Services 来实现开机自动运行。

Crontab 是 Linux 系统中用于周期性执行任务的工具。通过 Crontab,用户可以设置在特定的时间点、日期或者周期性地执行特定的命令或脚本。我们这里介绍 Crontab 的一些重要概念和用法:

Crontab 文件

Crontab 文件是存储周期性任务调度信息的文件。每个用户都有自己的 Crontab 文件,用于存储他们自己的任务调度信息。Crontab 文件通常存储在 /var/spool/cron 目录中,以用户的用户名命名。

Crontab 格式

Crontab 文件中的每一行代表一个任务调度。每行由五个字段组成,分别表示分钟、小时、日期、月份和星期。你可以使用 # 号来注释掉某一行来关闭对应的任务调度。

使用方法

要编辑 Crontab 文件,可以使用 crontab -e 命令。此命令会打开一个文本编辑器,允许用户编辑自己的 Crontab 文件。编辑完成后,保存并退出编辑器,Crontab 文件就会更新。

常用选项:

  • -e:编辑用户的 Crontab 文件。
  • -l:列出用户的 Crontab 文件内容。
  • -r:删除用户的 Crontab 文件。
  • -u:指定要操作的用户。

与 services 的对比

相比之下,使用 services 实现开机自动运行通常是通过在系统启动时执行一系列预定义的服务或脚本来实现的。这些服务可以在 /etc/init.d/ 目录下找到,并通过系统的服务管理工具(如 systemctl)来启动、停止或重启。

Crontab 的优点:

  • 灵活性:可以非常灵活地设置任务的执行时间,包括分钟、小时、日期、月份和星期等。
  • 简单易用:Crontab 的配置相对简单,对于一些简单的周期性任务调度非常方便。
  • 用户独立性:每个用户都有自己的 Crontab 文件,可以管理自己的任务,不会影响其他用户。

services 的优点:

  • 可靠性:通过 services 实现的开机自动运行通常更稳定可靠,因为它们是系统级别的服务,会在系统启动时自动加载和运行。
  • 管理性:系统管理员可以更轻松地管理服务,包括启动、停止、重启和查看状态等操作。
  • 控制权限:对于一些需要特权执行的任务,使用 services 可以更好地控制权限,确保安全性。

本产品中 Crontab 的特殊优势

更低的资源占用,经过我们的测试对比,同样的python脚本使用 Crontab 的CPU资源占用是 services 的 1/4,对于复杂度比较高的机器人主程序这样的应用而言,使用 Crontab 来实现开机自动运行是更优的选择。ervices 则更适用于需要在系统启动时执行的重要服务或应用程序。


29 自定义命令行功能

自定义命令行功能

为了方便对产品进行二次开发,我们在 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' 即可。


30 WEB 命令行应用

WEB 命令行应用

为了让产品的参数更容易配置,同时更加方便用户为产品添加自定义的功能,我们为产品设计了命令行参数的功能,你可以通过在网页的命令行工具中输入指令来实现对应的功能,本章节会详细介绍这些功能。

通过 ESP-NOW 发送信息

添加广播MAC地址到peer

  • send -a -b
  • send --add --broadcast
  • send -a FF:FF:FF:FF:FF:FF
  • send --add FF:FF:FF:FF:FF:FF

添加特定MAC地址到peer

  • send -a AA:BB:CC:DD:EE:FF
  • send -add AA:BB:CC:DD:EE:FF

从peer中删除广播MAC地址

  • send -rm -b
  • send --remove --boardcast
  • send -rm FF:FF:FF:FF:FF:FF
  • send --remove FF:FF:FF:FF:FF:FF

从peer中删除特定MAC地址

  • send -rm AA:BB:CC:DD:EE:FF
  • send --remove AA:BB:CC:DD:EE:FF

广播发送(初次使用前需将广播MAC添加到peer)

  • send -b what's up bro
  • send --broadcast what's up bro

单播发送(初次使用前需将目标MAC添加到peer)

  • send AA:BB:CC:DD:EE:FF what's up bro

组播发送(初次使用前需将目标MAC添加到peer,可添加多个,且不能包含广播MAC地址:FF:FF:FF:FF:FF:FF)

  • send -g what's up bro
  • send --group what's up bro


播放音频

TTS(文字转语音,初次使用初始化时间较长)

  • audio -s what's up bro
  • audio --say what's up bro

设置音量大小(范围为0-1.0之间)

  • audio -v 0.9
  • audio --volume 0.9

播放某个 sounds 文件夹内的音频文件(.mp3 .wav 格式,可以播放其它文件夹内的文件)

  • audio -p file.mp3
  • audio --play_file file.mp3
  • audio -p others/file.mp3


底盘

直接给底盘发JSON指令,具体指令需参考相关WIKI

  • base -c {'T':1,'L':0,'R':0}
  • base --cmd {'T':1,'L':0,'R':0}

开启在屏幕上显示来自底盘的信息

  • base -r on
  • base --recv on

关闭在屏幕上显示来自底盘的信息

  • base -r off
  • base --recv off

默认可以显示10秒命令行/ESP-NOW信息

  • info


OpenCV

设置目标颜色区间

  • cv -r [90,120,90] [120,255,200]
  • cv -range [90,120,90] [120,255,200]

选择颜色(默认只有红绿蓝)

  • cv -s red
  • cv --select red

设置云台追踪参数

颜色追踪迭代比例

  • track -c 0.023

人脸/手势追踪迭代比例

  • track -f 0.068

追踪速度

  • track -s 60

动作加速度比例

  • track -a 0.4




[教程导航]