正在上传图片(0/1)

CodeMaster第二次任务(9月27日更新算法详解)

 6
手机看帖 7 2571
先废话两句。斐波那契数列,这个名字太难记,当年我们一帮人都管它叫灰不拉几数列,这多好记:D

任务二有点小坑,十个标签分散太开了,家里不好找一面墙,而且,S1离墙两米,居然抬头到极限都看不全十个标签,于是我让S1高升了:D
这次任务可以说把python的优越性发挥得淋漓尽致,同时又是非常好的python练习,里面有很多小坑,走过一遍,你会对python理解得更深刻
先上视频
随机数给出的是2和3,云台2号和3号灯亮
然后依次击打2,3,5,8,13,21,34,55,89,打完一个数给一个声音,全部打完给另一个声音
代码如下,我逐句解释吧

import random       #这一句import是必须的
global x,y,x0,y0    #定义全局变量,x和y是数组,x0和y0是准星坐标
x = [0.0]*10          #初始化x数组,python的语法非常友好,这样就可以指定长度和初值
y = [0.0]*10          #python的数组是可变长的,如果从空数组开始添加会比较麻烦
x0 = 0.5                #python的变量是自动类型,想要指定它是浮点数,需要这样引导一下
y0 = 0.5
def start():
    global x,y,x0,y0  #需要再次说明全局变量
    marker = RmList()   #初始化标签数组
    sight = RmList()      #初始化准星数组
    j = 0
    robot_ctrl.set_mode(rm_define.robot_mode_free)    #自由模式
    gun_ctrl.set_fire_count(1)                                          #每次一发水弹
    sight = media_ctrl.get_sight_bead_position()            #取得准星坐标
    x0 = sight[0]
    y0 = sight[1]
    gimbal_ctrl.angle_ctrl(0,-3)                      #垫起来的高度略高了一点,云台低头3度正好
    list1 = [1,1,1,1,2,2,2,3,3,4]                        #一共就C(5,2)=10种情况,不折腾了,果断打表
    list2 = [2,3,4,5,3,4,5,4,5,5]
    r = random.randint(0,9)                             #产生0-9的随机数,查表得到n1,n2                           
    n1 = list1[r]
    n2 = list2[r]
#用云台的LED亮绿色表示这两个数,1号灯表示1,……5号灯表示5
    led_ctrl.set_top_led(rm_define.armor_top_all, 0, 255, 0, rm_define.effect_always_off)
    led_ctrl.set_single_led(rm_define.armor_top_all, [n1,n2], rm_define.effect_always_on)
#打开视觉识别
    vision_ctrl.enable_detection(rm_define.vision_detection_marker)
#设置距离为3m
    vision_ctrl.set_marker_detection_distance(3)
#下面三句反复识别标签,直到识别到全部10个标签
    marker = RmList(vision_ctrl.get_marker_detection_info())
    while len(marker) < 51:  #这里用数组长度可以规避一个也没识别到,数组是空的坑
        marker = RmList(vision_ctrl.get_marker_detection_info())
#将标签信息填入x和y数组
    for i in range(10):   #这里注意,i的取值范围是0~9
        j = marker[i*5+2] - rm_define.marker_number_zero #获取标签代表的数字
        x[j] = marker[i*5+3]  #填表
        y[j] = marker[i*5+4]
#这个循环迭代灰不拉几数列
    while n1 < 100:   
        shoot(n1)       #打标签,shoot定义成可以打两位数
        tmp = n2
        n2 = n1 + n2
        n1 = tmp
    media_ctrl.play_sound(rm_define.media_sound_scanning)   #任务完成声音提示

def shoot(nn):   #打两位数
    if nn < 10:    #小于10就打一位数
        shoot1(nn)
    else:             #大于10就打两位数
        shoot1(nn // 10)    #分离十位和个位,注意python的整除是//
        shoot1(nn - nn // 10 * 10) #这里%不能用,应该是个bug,所以用这个算式规避
#打完一个数,声音提示
    media_ctrl.play_sound(rm_define.media_sound_recognize_success,wait_for_complete_flag=True)
    time.sleep(0.5)

def shoot1(n):     #打一位数
    global x
    global y
    xx = x[n]      #获取标签坐标
    yy = y[n]
    yaw = 96*(xx-x0)   #用大师之路的算法得到云台姿态角,这里我加上了准星校正
    pitch = 54*(y0-yy) - 3  #-3是因为云台的初始俯仰角是-3度
    gimbal_ctrl.angle_ctrl(yaw,pitch)    #转动云台对准标签
    led_ctrl.gun_led_on()      #开弹道灯
    gun_ctrl.fire_once()        #终于可以开火
    time.sleep(0.5)                #等待0.5秒,让弹道灯多亮一会儿
    led_ctrl.gun_led_off()    #关弹道灯

~~~~~~~~~~~~~~~~~~~~华丽分界线~~~~~~~~~~~~~~~~~~~~
下面介绍一下我用到的算法
我是懒人,信条是有懒可偷,有懒必偷,所以,追求程序写得尽量简单(键盘敲得少),也追求程序有通用性(这样下次遇到就可以偷懒了)
我是学计算机的,职业习惯加强迫症,追求程序的效率
1)随机数产生算法
题意是随机产生1~5中的两个数,也就是两个数不能一样,这样写显然是不对的
n1=random.randint(1,5)
n2=random.randint(1,5)
比较容易想到的是写成这样
n1=random.randint(1,5)
n2=random.randint(1,5)
while n1==n2:
    n2=random.randint(1,5)
有1/5的机会,n2要多产生一次,1/25的机会,n2要多产生两次,虽然对结果影响甚微,但还是不爽
另一方面,产生了两个数,还需要判断一下顺序
if n1 > n2:
    temp = n2
    n2 = n1
    n1 = temp
那么有没有一种方法可以简单地产生两个不同的随机数,而且顺序捎带搞定呢?
这道题比较简单,1-5中选两个,也就C(5,2)=10种情况,于是我建了两个常数表
list1 = [1,1,1,1,2,2,2,3,3,4]
list2 = [2,3,4,5,3,4,5,4,5,5]
大家竖着看这两个表,同一列的两个数就是我们需要的两个排好了顺序的数,就这10种情况,于是再产生一个0~9的随机数,查表即得
r = random.randint(0,9)
n1 = list1[r]
n2 = list2[r]
一共5行,比传统的8行写法爽了。
不过,这种写法用到了python的机制,scratch是没法写的。而且,也就是这道题规模小,就10种情况可以用常数表,要是规模大一些,比如100个数中取3个数,一共有C(100,3)=161700种情况,建表的方法就不符合偷懒的原则了
提交之后我又琢磨了一种方法
n1 = random.randint(1, 4)
n2 = random.randint(n1+1, 5)
这才是终极偷懒的程序,只有两行!n1显然不能取到5,只能1-4,n1选定之后,n2的范围就缩小到n+1到5。而且,这还是一个scratch也可以用的程序

扩展一下,如果是n个数中取m个,同样可以写出很爽的程序
先定义一个m+1个元素的数组,b[0]浪费掉,因为你事先不知道m的值,就只能用数组存,不能用n1,n2
b = [0]*(m+1)
for i in range(1,m)
    b[ i ] = random.randint(b[i-1]+1,n-m+i)
有兴趣的同学可以研究一下,这样写,每个数被取到的概率仍然是相等的

2)灰不拉几数列产生算法
其实题意的要求仅仅是这样的
    n1            拿去搞事情,然后扔了
    n2            给    n1
    n1 + n2   给    n2
没有必要保存一个数列吧,滚动更新即可
然而后面两个操作在逻辑上是并行的,但软件却有先有后,如果先n2给n1,就把n1给洗了,没法算n1+n2了
在提交的程序中是这样写的
    while n1 < 100:   
        shoot(n1)       #击打n1表示的标签
        tmp = n2       #暂存n2,避免下一句把n2洗了
        n2 = n1 + n2 #n1+n2给n2
        n1 = tmp       #暂存的n2给n1
这就比先算出整个数列保存到列表要简单,不过,利用python的机制,还有更简单的,python真的支持并行赋值
    while n1 < 100:
        shoot(n1)
        [n2, n1] = [n2+n1, n2]
三行搞定,少敲多少字符啊

3)随机数的展示
题目要求初始的两个随机数需要用某种手段展示出来,我头脑风暴了一下
用声音,响几遍就是几,节奏太慢了
用云台旋转的角度,不直观
用云台转的次数,还是太慢
开火几次就是几,太暴力:D
亮灯,好吧,就是它了
S1的云台上左右各有8个灯,而题目只要求显示两个数,那么几号灯亮就是几了,方便,快捷,可以一直亮到程序结束,大家慢慢看,不会错过:D
    led_ctrl.set_top_led(rm_define.armor_top_all, 0, 255, 0, rm_define.effect_always_off)
    led_ctrl.set_single_led(rm_define.armor_top_all, [n1,n2], rm_define.effect_always_on)
就两句,第一句关上所有的云台灯,顺便指定一下颜色
第二句把该亮的两个LED点亮,左右两边都点亮,这样大家不管在哪边都能看到了,[n1,n2]谁发明的?实在是太爽了

4)视觉标签数据的预处理
大师之路的方法是把ID挑出来存到一张表里,然后查找这个表来得到坐标数据。查找的时间复杂度是O(n),不爽啊,我喜欢时间复杂度是O(1)的
根据官方给出的视觉标签格式

第1个数是标签数目,第2个,第7个,第12个......是标签的ID,也就是第(i*5+2)的位置
同样,第(i*5+3)位置是标签的横坐标,第(i*5+4)位置是标签的纵坐标
知道这些,就可以搞事情了。首先,定义x数组纪录标签的横坐标,y数组纪录标签的纵坐标,x[0]就是标签0的横坐标,y[8]就是标签8的纵坐标
x = [0.0]*10
y = [0.0]*10
初值是0.0,意味着这是浮点数
    for i in range(10):   
        j = marker[i*5+2] - rm_define.marker_number_zero #获取标签代表的数字
        x[j] = marker[i*5+3]  #填表
        y[j] = marker[i*5+4]
四句话完成标签信息与坐标列表的转换
第二句,从(i*5+2)的位置得到标签的ID,再减去rm_define.marker_number_zero把ID转换成数字
这个技巧很常用,因为0~9的ID是连续的,所以标签ID减去0的ID,就是ID代表的数字
得到标签代表的数字j之后,x[j]填入横坐标,在标签列表的位置为(i*5+3),同样y[j]填入纵坐标,在标签列表的位置为(i*5+4)

5)击打标签
我用函数来实现的,这样的操作比较通用,从以前的代码拷贝过来略做修改就可以用了,以后还可以继续用
首先,打一个标签的函数
def shoot1(n):     #打一位数
    global x
    global y
    xx = x[n]      #获取标签坐标
    yy = y[n]
    yaw = 96*(xx-x0)   #用大师之路的算法得到云台姿态角,这里我加上了准星校正
    pitch = 54*(y0-yy) - 3  #-3是因为云台的初始俯仰角是-3度
    gimbal_ctrl.angle_ctrl(yaw,pitch)    #转动云台对准标签
    led_ctrl.gun_led_on()      #开弹道灯
    gun_ctrl.fire_once()        #终于可以开火
    time.sleep(0.5)                #等待0.5秒,让弹道灯多亮一会儿
    led_ctrl.gun_led_off()    #关弹道灯
函数带有参数,这是python远远超过scratch的地方。参数n表示打n号标签
这样我们就自然而然地利用参数n去x和y列表中取出标签的坐标,然后用大师之路的巡线出击中的算法算出云台姿态角。没有用PID是因为这是静止目标,直接转云台更简单
对官方算法做了一点点改进,把0.5改成准星坐标,这样更准,pitch的表达式中加上了-3,因为云台的初始俯仰角是-3,这个细节官方程序没有注意到,抑或是故意留着考验大家debug的?
最后就是开弹道灯,开火了。实际操作中,没装子弹,弹道灯点到为止

在shoot1函数中,只能打一位数,要打两位数,简单包装一下
def shoot(nn):   #打两位数
    if nn < 10:    #小于10就打一位数
        shoot1(nn)
    else:             #大于10就打两位数
        shoot1(nn // 10)    #分离十位和个位,注意python的整除是//
        shoot1(nn - nn // 10 * 10) #这里%不能用,应该是个bug,所以用这个算式规避
    media_ctrl.play_sound(rm_define.media_sound_recognize_success,wait_for_complete_flag=True)
    time.sleep(0.5)
这个比较简单,就不解释了。这里包装的思想更重要一些,有了shoot函数,主程序看着比较舒服

最后啰嗦两句,用python还是用scratch?大家觉得scratch简单,可是,这个简单是有限定范围的,仅仅在使用S1的API上简单而已。用python写API的部分确实麻烦,我都是在scratch那边假惺惺地调用一个API,然后利用平台的功能生成python代码,拷贝过去的。
但是,程序不仅仅是API的堆砌,还需要有算法,算法方面,scratch就弱爆了。各种让你抓狂的事情,比如
写一个稍微复杂一点的表达式,比如,pitch = 54*(y0-yy) - 3,python轻轻松松,scratch各种拖拽不到位,要重新拖
函数不支持参数,怎么搞
不支持局部变量,你要随时记住你声明了哪些变量
数组没法方便地初始化
没法大范围的拷贝代码,这一点python优势,文本嘛,随便拷贝,跨平台都可以拷贝
递归貌似没有吧,都不支持局部变量了

强烈建议大家用python,不会用没关系,scratch那边调模块,右上角有个"<|>"图标,点之生成代码,拷过去即可
DJI在这个地方有点不友好哈,拷贝过去的代码,居然是带行号的,这也算是bug哈
评论
上传
你需要登录之后才能回帖    登录 | 注册
CPYCPY  Mavic Mini认证用户 2019-9-20 3#
666,这么快就完成了,赞
夏清  管理员 2019-9-20 4#
微信关注“大疆”,获取最全面的大疆产品教学视频,及时的售后服务动态,最实用的飞行指引和专业的技术支持,都在这里
总督  机甲大师 RoboMaster S1认证用户 2019-9-20 5#
姚兄真是速度啊,效果也很好,必须点赞!!
楼主  Goggles认证用户 2019-9-21 6#
总督机甲大师 RoboMaster S19-20 22:57
姚兄真是速度啊,效果也很好,必须点赞!!
特别感谢你总结的API,一边查一边写,很爽!
CPYCPY  Mavic Mini认证用户 2019-9-27 7#
666,点赞
段多多  机甲大师 RoboMaster EP认证用户 2022-5-16 8#
学习了!
段多多  机甲大师 RoboMaster EP认证用户 2022-5-19 9#
您好,我运行上面程序,总显示 “def shoot(nn):   #打两位数 " 这一行 syntaxerror 错误,是什么原因造成的呢?
取消 点赞 评论
分享至:
回复:
上传
取消 评论
快速回复 返回顶部 返回列表