RMYC-合24的一个初步尝试

本文最后更新于 2024年12月29日 凌晨

在各种机缘巧合之下有幸在高中玩了俩年 RoboMaster,但其实在队伍中我一直都是负责工程机的改装及调试,基本上没怎么碰过步兵以及视觉相关的内容

趁着这次搞完了中学生涯中最后一场赛事,就来玩玩视觉组的任务( ̄▽ ̄)*

既然是随便玩玩,那当然要挑一个最基础的来,也就是合 24 了

不过虽然说是最基础的,实际上在小组赛中不知道什么原因也没几个队伍能够合出来,甚至在决赛里也有没合出来的

我寻思这应该不难啊,不过实际上会遇到什么困难也得实践一下才知道

再提一嘴,决赛中我看到有些队伍甚至能够做到秒合成,那我就把这个当作一个小目标吧(逃

背景信息

这里所提到的合 24,官方名称其实叫做能量机关

成功击打能量机关将会为团队带来一些 buff,例如伤害加成

不同赛区以及组别的能量机关任务不同,在中学组的比赛里,任务就是合 24

那什么是合 24 呢?

在能量机关上面有五块电子显示牌,上面能够显示 RoboMaster 可识别的的视觉标签

在合 24 这个任务中,从左到右第一块牌子将会是一个随机的加减乘除运算符,剩下四块是四个数字

例如这样

在图中这个例子,运算符号是减号,数字分别为 8, 6, 6, 2

在这个情况下,机器人将需要按顺序击打 ‘8’, ‘6’, ’-‘, ’6‘, ‘2’

而这个算式的答案也就是 24

值得留意的是,尽管在这个例子中没表示出来,运算符号可以多次重用,但数字只能用一次

也就是说有机会出现 ‘3’, ‘x’, ‘4’, ‘x’, ‘2’ 这种把乘号用俩次的情况

五块显示牌每五秒钟刷新一次,而每次击打完一块牌后必须在一秒内击打下一块,否则会识别为超时无效

问题解剖

观察一下,我们其实可以把这个击打能量机关的任务拆解为 3 个小任务,这样能够更加清晰明了的提供代码思路以及解决方案

这三个任务顺序分别为

  • 识别(视觉,数据结构)

  • 计算(合 24 算式)

  • 射击(包含一部分射击相关的计算)

识别

要完成整个任务,无论是击打还是计算,都离不开识别这一步

Robomaster 内置了视觉标签的视觉模型,所以可以调用内置的库来提取图传中所识别到的标签

1
2
3
4
5
6
7
8
9
10
11
12
def start():
# 启用视觉标签识别
vision_ctrl.enable_detection(rm_define.vision_detection_marker)

# 将识别颜色设置为蓝色
vision_ctrl.marker_detection_color_set(rm_define.marker_detection_color_blue)

# 初始化空列表
raw_vision_info: list = []

# 将列表设置为识别到的标签信息
raw_vision_info = RmList(vision_ctrl.get_marker_detection_info())

最后这行代码中按照官方写法用 RmList()get_marker_detection_info() 的返回值进行了一次封装

经过我的测试,这个函数的原本返回类型就是 list,这个 RmList() 的封装也只是把列表的起始项目从 0 变成 1 而已

虽然不知道有什么用,况且源码也没有公开,那就暂且就先按照官方的来写,避免之后遇到奇奇怪怪的错误

<(_ _)>

再添加一行调试代码

1
print(raw_vision_info)

运行,可见当图传相机的视角里若有视觉标签的话,raw_vision_info 的列表里会出现一些信息

1
[1, 17, 0.64, 0.88, 0.7, 0.6]

参考官方的 EP 编程模块手册

视觉标签信息解释

可见,在返回的列表中第一个元素是表示总共识别到了多少个视觉标签,之后每 5 个元素为一组

组内第一个元素是标签 id,之后分别为在屏幕上的 x, y 坐标值,宽和高

注意,这个坐标值十分奇葩,他并不代表屏幕的像素点,而是以类似比例的形式决定的

屏幕左上角为 (0, 0),右下角为 (1, 1),那么屏幕中心也就是 (0.5, 0.5) 了

这个细节在计算过程中非常要命,因为设置常数的话 x, y 轴要分别处理

回到正题,运行代码的时候应该会发现,这个识别代码只会跑一次

也就是说这个识别只会在运行的那一刻处理图传捕捉到的那一帧

这个逻辑看似没问题,但只要稍微运行多几遍,就能发现这个识别并不稳定

有时候能识别到 5 个,有时候啥都没识别到,又有时候只能识别 3 - 4 个标签

机器人没动过,每次跑程序的结果都不一样,这太不稳定了吧 (#`-_ゝ-)

这种基于计算机视觉的识别在识别方面非常稳定,每次都能识别到标签,问题就在于我们想要的是一次性识别 5 个,这种对数量的要求就表现一般

那咋办?没关系,直接写个循环让他一直识别直到数量达到要求不就得了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def start():
# 启用视觉标签识别
vision_ctrl.enable_detection(rm_define.vision_detection_marker)

# 将识别颜色设置为蓝色
vision_ctrl.marker_detection_color_set(rm_define.marker_detection_color_blue)

# 初始化空列表
raw_vision_info: list = []

# 将列表设置为识别到的标签信息
raw_vision_info = RmList(vision_ctrl.get_marker_detection_info())

# 循环直到识别到起码有 5 个标签
while not raw_vision_info[1] >= 5:
raw_vision_info = RmList(vision_ctrl.get_marker_detection_info())

这段代码中,在循环之前必须将 raw_vision_info 赋值,否则在判断循环条件时会报错

数据结构

识别完成,咱应该想想如何将这些原始数据处理成更快、更容易读取的数据结构

这里以我随便设计的一个数据结构做例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vision_info = {
marker1:
[{
'x': float,
'y': float,
'w': float,
'h': float
}],
marker2:
[{
'x': float,
'y': float,
'w': float,
'h': float
},
{
'x': float,
'y': float,
'w': float,
'h': float
}],
...
}

这个是一个典型的 python 字典序,其中数据由每个键(key) 值(value) 对组成

其中我用了视觉标签的 id 作为键,然后将相对应的信息作为值

注意, 字典序的键和数据库类似,必须是唯一,考虑到视觉标签有机会重复,这里的值使用了列表

参考 marker2,若有多个视觉标签那直接在列表中增加元素即可

视觉标签的信息也可以变成一个字典序,其中 x, y, w, h 作为键,分别代表坐标以及宽高

数据处理

构建完数据结构,我们可以写用于格式化的函数了

首先就是将视觉标签 id 转换为字符串,这个在以后也非常有帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def Id2Marker(marker_id):
""" Marker Translation """
if 10 <= marker_id <= 19:
return str(marker_id - 10)
elif 20 <= marker_id <= 45:
return chr(marker_id + 65 - 20)
elif marker_id == 46:
return "!"
elif marker_id == 47:
return "?"
elif marker_id == 48:
return "#"
elif marker_id == 50:
return "+"
elif marker_id == 51:
return "-"
elif marker_id == 52:
return "/"
elif marker_id == 1:
return "red"
elif marker_id == 2:
return "yellow"
elif marker_id == 3:
return "green"
elif marker_id == 4:
return "left"
elif marker_id == 5:
return "right"
elif marker_id == 6:
return "forward"
elif marker_id == 7:
return "backward"
elif marker_id == 8:
return "heart"
elif marker_id == 9:
return "sword"
else:
#logger.warning("Vision: unsupported marker_id:{0}".format(marker_id))
return ""

直接参考官方文档对视觉标签 id 的释义写即可,十分无脑

然后我们可以开始写主要格式函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def getFormattedVisionInfo(raw_vision_info: list = []) -> dict:
# Null case 返回
if not raw_vision_info:
return raw_vision_info

# 新建空字典序
formatted_vision_info: dict = {}

# 遍历所有视觉信息,注意原始数据的第一项就表示了视觉标签的总数
for i in range(0, raw_vision_info[1]):
# 使用转换函数将 id 变为字符串
marker = Id2Marker(raw_vision_info[i * 5 + 2])
# 基本的列表数学(
info: dict = {
'x': raw_vision_info[i * 5 + 3],
'y': raw_vision_info[i * 5 + 4],
'w': raw_vision_info[i * 5 + 5],
'h': raw_vision_info[i * 5 + 6]
}
if marker in formatted_vision_info:
formatted_vision_info[marker].append(info)
else:
formatted_vision_info[marker] = [info]

return formatted_vision_info

计算

获取算式

在计算之前,我们应该把机器人看到的算式还原回来,这里可能漏了讲,vision_ctrl.get_marker_detection_info() 函数所获取的数据是按照特定方式排序的,并非直接从左到右排

那也就是说我们必须得将获取到的数据进行整理,还原算式

为了方便后面的计算,这里的还原函数就以列表的形式返回算式

例如 ['+', '2', '1', '4', '8'] 这样

1
2
3
4
5
6
7
8
9
10
def getMarkers(raw_vision_info: list = [], vision_info: dict = {}) -> list:
# Null case 返回
if not raw_vision_info or not vision_info:
return []

# 利用用列表生成式从 raw_vision_info 获取识别到的标签数据
markers = [Id2Marker(raw_vision_info[i * 5 + 2]) for i in range(0, raw_vision_info[1])]
# 列表的 sort 方法 + key 参数配合 lambda 函数
markers.sort(key=lambda t: vision_info[t].copy().pop()['x'])
return markers

这里的 markers.sort() 利用了 sort 函数内置的 key 参数

当 python 遍历列表中的元素并作比较时,可以使用 key 这个参数修改比较的值(顾名思义

若不传入 key ,Python 则会直接比较元素本身

若传入 key 参数,并且是个函数的话,sort 函数会把元素传入到 key 函数的参数,并且比较 key 的返回值

这个例子中,我传入了一个 lambda 函数(无名函数),t 作为这个函数的参数,“:” 后的参数为这个 lambda 函数的返回值

此处 sort 函数所使用的无名函数处理逻辑有问题,但在目前测试使用并无大碍

之后有空再改吧 (#`-_ゝ-)

计算击打次序

在上一步中,getMarkers() 函数所返回的是一个列表

那我们同样应该基于这个列表创建一个击打次序的列表

举个例子,假设识别到的是 ['+', '2', '1', '4', '8']

那我们就需要返回一个 ['1', '8', '+', '4', '+', '2'] 的列表以表示击打顺序

关于计算,我认为这个视频已经讲的非常清楚了

这个程序也是采用同样的遍历逻辑,只不过是用 python 写了一次罢了(

简单来说,就是分别处理加、减、乘、除、这四种情况


RMYC-合24的一个初步尝试
https://blissfulalloy79.github.io/13-robomasteryc01/
作者
BlissfulAlloy79
发布于
2024年8月24日
许可协议