一、学习背景
(一)为什么要用状态机
在用python做一个比较复杂的小项目,需要根据不同的输入,控制摄像头采集执行不同的任务。虽然用流程方式实现了,但阅读起来费劲,还容易出错。所以就用了状态机。至于状态机是什么,度娘上一大把。
(二)为什么用transition
一是懒,懒得自己写一个状态机。
二是已经有现成且成熟的库了,为什么不用呢。况且这个库还在持续维护。
(三)transition怎么来
按github上的手册安装就行了,https://github.com/pytransitions/transitions#threading
我的情况,直接用 pip install transitions
可以装上,但import的时候说没有这个库。所以我用第二个方法装了,即克隆下来后用python setup.py install
进行安装。
(四)我要做成什么样的模型
前面说了,我需要根据不同的输入,控制摄像头采集执行不同的任务。实际任务总共有3个:
1.摄像头采集一定数量的视频帧
2.对所采集到的帧进行一些分析和计算(本文不会对怎样做计算展开,仅以把帧保存为图片文件代替)
3.持续实时采集视频,分析当前帧与第2步计算的结果进行比对(本文不会对怎样做分析比对展开,仅以把当前帧保存为图片文件代替)。
二、对目标模型的思考
(一)有多少个状态
针对需求的3个功能,至少应该有3个状态。即:采集状态,基准计算状态和实时跟踪比对状态。我把这3个状态分别命名为:sample,locate和trace。
实际应用中,还有可能让这个系统空转,什么也不干。等有下一步输入的时候再重新开始,因此多设计一个空闲状态:idle。这样,总共就有4个状态了。归结如下:
状态名称 | 功能 |
---|---|
idle | 空闲状态,啥都不干 |
sample | 图像采集 |
locate | 基准定位计算 |
trace | 实时追踪比对 |
(二)如何触发状态切换
根据我的需求,所要实现的目标是:
1.一开始应处于idle状态。
2.当输入接收到start的时候,进行idle->sample切换
3.sample执行完采样后,自动进入locate状态,即进行sample->locate切换
4.locate计算完后,自动进入trace状态,实时追踪比对。除非收到stop、restart信号,否则一直运行。
5.当收到restart信号时回到sample状态,接着进入locate状态和trace状态。即trace->sample->locate->trace。
6.当任何时候收到stop信号时,切换到idle状态。
根据上述需求,显示触发信号和状态切换的对应关系如下:
触发信号 | 原状态 | 目标状态 |
---|---|---|
start | idle | sample |
restart | 任何状态 | sample |
stop | 任何状态 | idle |
sample->locate->trace间的状态切换应为自动切换。
我将一步步实现从手工触发使其切换,到某些状态间自动切换。
三、上状态机
先上定义状态机源码,然后再做小结分析。
import transitions
class tracer_model(object): # 先定义一个类,把它作为基础模型
pass
tracer = tracer_model() #生成一个实例,这时候它和普通的实例没有任何区别
# 定义所有状态的列表
states_lst = [
'idle',
'sample',
'locate',
'trace'
]
# 定义状态切换器
# 也就是当发生什么时从哪个状态转换到哪个状态
transitions_lst = [
['start','*','sample'],
['cal_pos', 'sample', 'locate'],
['live_trace', 'locate', 'trace'],
['stop', '*', 'idle'],
['restart', '*', 'sample']
]
# 生成一个状态机控制器
machine = transitions.Machine(model=tracer, # 控制哪个模型
states=states_lst, # 载入模型可能有的状态
transitions=transitions_lst, # 载入状态切换器
initial='idle' # 这个状态机初始状态是什么
)
transitions库把一个完整的状态机分为执行器和控制器2部分。
执行器:就是在指定状态下分别干什么,各种算法都将装在此处
控制器:就是通过外界的动作出发来切换不同的状态。达到想让程序干啥就干啥的目的。状态切换并非状态1->状态2这么简单,还涉及到触发切换后准备阶段、退出旧状态阶段、进入新状态阶段、处于新状态阶段等等,这个放在后面再说。
刚才的代码中:
class tracer_model(object): # 先定义一个类,把它作为基础模型
pass
tracer = tracer_model()
相当于定义了执行器,只不过现在定义的这个执行器啥都不干。
而这段代码则是定义了控制器:
# 定义所有状态的列表
states_lst = ['idle',
'sample',
'locate',
'trace'
]
# 定义状态切换器
# 也就是当发生什么时从哪个状态转换到哪个状态
transitions_lst = [
['start','*','sample'],
['cal_pos', 'sample', 'locate'],
['live_trace', 'locate', 'trace'],
['stop', '*', 'idle'],
['restart', '*', 'sample']
]
# 生成一个状态机控制器
machine = transitions.Machine(model=tracer,
states=states_lst,
transitions=transitions_lst,
initial='idle'
)
我的理解,一个状态机控制器最起码应包括几个内容:
1.控制器要控制哪个执行器 model=tracer
2.整个状态机都有哪些状态states=states_lst
3.状态间切换的触发条件transitions=transitions_lst
这里对状态切换器做个简单介绍。
首先transitions可以是一个列表(更多的方式请看github),列表中的每一个元素就是怎么切换。以['cal_pos', 'sample', 'locate']
为例,第一位表示触发切换的触发器(怎么用,后面有说),第二位表示从哪个状态切换出去,第三位表示要切换到哪个状态。起始状态和目标状态都需要事先在状态列表中定义,否则实际执行时会出错。
那为什么['stop', '*', 'idle']
中第二位是*号呢?这是transitions库其中一个牛逼的地方,这表示可以从任何当前状态切换到idle状态。
四、状态切换
(一)触发器切换法
通过激活触发器实现状态切换。在上面代码的最后加入以下:
tracer.start() # 激活start触发器
print(tracer.state)
tracer.cal_pos() # 激活cal_pos触发器
print(tracer.state)
可以看到出现这样的结果:
sample
locate
是不是和我之前的定义一致:
当start触发器被触发时,不论当前处于什么状态(此时状态机处于idle状态)都切换到sample状态。此处对应的切换条件为:['start','*','sample']
当前处于sample状态下时,若cal_pos触发器被触发,则切换到locate状态。此处对应的切换条件为['cal_pos', 'sample', 'locate']
不得不说这是transitions库另一个牛逼之处,直接把字符串型定义的触发器转化成执行器的一个方法。
问题来了,如果触发器被触发了,但当前所处状态又不是定义中的原状态,出现什么结果呢?把
#tracer.start()
#print(tracer.state)
注释掉试一下就会发现出错了:transitions.core.MachineError: "Can't trigger event cal_pos from state idle!"
道理很简单,因为cal_pos触发器被触发时是要从sample->locate的,而状态机运行后初始状态为idle,当然出错啦。
(二)目标状态切换法
transitions同时还支持直接切换到目标状态的切换方式。
1、用执行器进行切换
把刚才的触发器切换法语句删掉,用这几句替换,看看是什么效果。
tracer.to_locate()
print(tracer.state)
locate
你可以看到,不论当前处于什么状态,状态机切换到了locate状态。transitions库根据状态定义,在初始化状态机的时候定义了为执行器tracer增加了to_locate()方法。注意观察可以看到所增加的方法为“to_<状态名>”
2、用控制器进行切换
我们还可以用machine.set_state('locate')
强行切换状态。这也是不论当前处于什么状态,都将切换到你想要的目标状态。
这两种分别通过执行器和控制器进行强行切换的方法很重要,在后面的实践中将发挥作用。
本节主要总结了状态机的定义和切换,但仅仅是切换了状态而已,实际上并没有做什么卵事,充其量就是个前戏。下一节将真的来干一炮,也就是对执行器如何执行任务展开讨论。