CodeMaster第二次任务(9月27日更新算法详解)
6
先废话两句。斐波那契数列,这个名字太难记,当年我们一帮人都管它叫灰不拉几数列,这多好记: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哈
|