上一步我們從車輛的物件偵測 (object detection) 出發,接著透過相機校準 (calibration) 與跨相機追蹤 (cross-camera tracking),把不同畫面、不同時間的車輛接起來;下一步就是判斷車輛的各個狀態。
速食店在意的是這台在得來速消費的車排隊多久才點餐、當下有多少輛車正在排隊、這台車點餐花了多少時間、點完餐後又等了多久才拿到餐點。因此我們把排隊、點餐、等餐這些事件當作車輛的狀態 — 問題就變成如何給予每一台車正確的狀態。
從車輛偵測可以拿到車輛在畫面中的外框座標與參考點 (keypoint),接著在相機的 2D 畫面座標中框出對應的範圍,例如排隊車道涵蓋的範圍、點餐對講機的區域,這些範圍就是 RoI (Region of Interest)。再透過車輛外框與 RoI 的 IoU (Intersection over Union)、或是參考點是否落在 RoI 內,來判斷是否給予這台車特定狀態。
一開始的寫法是用平行與巢狀的 if condition 來判斷狀態,例如:
if 車外框與排隊RoI符合規則且有其他車在點餐且車子停止
車.狀態=排隊
elif 車外框與排隊RoI符合規則但沒有其他車在點餐
車.狀態=待機
elif 車外框與點餐機RoI符合規則且車子停止
車.狀態=點餐
...
得來速車輛的行為在實際情況下會有很多無法預測的特例,如果用多個 if condition 來判斷狀態,車輛可能拿到意料外且錯誤的狀態,也很難擴充與偵錯。
演算法要給予車輛正確狀態,又要避免錯誤且意料外的狀態切換,於是想到用 有限狀態機 (finite state machine) 來實作:狀態機由當前狀態 (current state)、轉換條件 (transition condition) 與目標狀態 (next state) 三個要素組成,先看一個簡單的例子。

優點
python-statemachine 實作將狀態機範例圖寫成虛擬碼 (pseudo code),狀態機的三個要素會一對一對應:方框變成 State、箭頭變成 transition、箭頭上的文字變成事件名稱與 cond。
from statemachine import State, StateMachine
from statemachine.exceptions import TransitionNotAllowed
class 得來速車輛(StateMachine):
# ---- states:圖上的每個方框 ----
待機 = State(initial=True)
排隊 = State()
點餐 = State()
等餐 = State()
取餐 = State()
離開 = State(final=True)
# ---- transitions:圖上的每個箭頭 ----
# 同一個事件可以掛多條轉換,會依「當前狀態 + cond」決定走哪一條
碰到排隊RoI = 待機.to(排隊, cond="有其他車在點餐")
碰到點餐機RoI = 待機.to(點餐) | 排隊.to(點餐)
離開點餐RoI = 點餐.to(等餐)
碰到取餐RoI = 等餐.to(取餐)
離開取餐RoI = 取餐.to(離開)
def __init__(self, vehicle, scene):
self.vehicle = vehicle
self.scene = scene # 拿得到同一幀場上其他車的狀態
super().__init__() # 這行會觸發 initial state 的 enter action
# ---- guard:待機 -> 排隊 才需要看場上其他車 ----
def 有其他車在點餐(self):
return any(other.sm.current_state.id == "點餐"
for other in self.scene.other_vehicles(self.vehicle))
# ---- enter / exit action:自動記下每個 state 的起訖時間 ----
def on_enter_state(self, state):
self.vehicle.enter_time[state.id] = self.scene.current_time
def on_exit_state(self, state):
self.vehicle.exit_time[state.id] = self.scene.current_time
每一幀只負責把偵測到的事件丟進狀態機,合不合法交給狀態機判斷:
def update_frame(scene):
for vehicle in scene.tracked_vehicles:
for 事件, 成立 in [
("碰到排隊RoI", 車外框 與 排隊RoI 的 IoU > 門檻),
("碰到點餐機RoI", 車外框 與 點餐機RoI 的 IoU > 門檻),
("離開點餐RoI", 車外框 與 點餐RoI 的 IoU < 門檻),
("碰到取餐RoI", 車外框 與 取餐RoI 的 IoU > 門檻),
("離開取餐RoI", 車外框 與 取餐RoI 的 IoU < 門檻),
]:
if not 成立:
continue
try:
vehicle.sm.send(事件)
except TransitionNotAllowed:
pass # 當前狀態不接受這個事件,車子維持原狀態
對照前面 if condition 的寫法,有兩個地方值得注意:
TransitionNotAllowed 就是優點 1 的具體展現。等餐中的車就算外框壓到排隊 RoI,碰到排隊RoI 這個事件在等餐狀態下根本沒有對應的 transition,狀態機會直接擋掉,不需要在 if 條件裡再補一堆「而且現在不是等餐」的例外。on_enter_state 與 on_exit_state 是通用的 callback,一次寫好就對所有狀態生效,不用每個狀態各寫一次計時邏輯,這正是優點 3 想要的效果。過去接觸狀態機多半是在編譯器 (compiler) 或計算理論 (computer theory) 這類課程,這次在產品開發上把它拿來用,發現它能直接對應我們的需求,後續功能與需求的開發也進行得非常快速,同時讓我們回頭檢視還有哪些程式碼能用類似的方式重構。
python-statemachine 官方文件:State、transition、cond guard 與 enter / exit action 的完整用法。python-statemachine 原始碼 (GitHub):原始碼與 TransitionNotAllowed 等例外的定義。本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格。