iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
佛心分享-IT 人技術創業

Berry AI:從零開始打造全美第一的得來速 Vision AI系列 第 14 篇

從巢狀 if 到有限狀態機:得來速 Vision AI 的車輛狀態判斷演進

  • 分享至 

  • xImage
  •  

上一步我們從車輛的物件偵測 (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) 三個要素組成,先看一個簡單的例子。

狀態機範例

優點

  1. 簡單地設定合法合理的狀態轉換:
    只要在合法合理的兩個狀態之間加入轉換,不允許的狀態轉換就不要建立轉換。
    以上述狀態機為例,我們可以限制車輛只能從點餐狀態轉到等餐狀態,沒辦法從排隊狀態轉換到等餐狀態,藉此避免不合理且不允許的狀態轉換。
  2. 方便未來新增及延伸更多狀態:
    維護與新增狀態都比 if condition 方便,只需要新增狀態與連結到該狀態的轉換。例如有店家需要我們增加「延後取餐」的狀態 — 店員先把等餐中的車輛引導至別的區域,等餐點製作完成再由店員親自送餐 — 我們就可以從等餐狀態多拉一條轉換到延後取餐狀態,狀態轉換的限制隨之成立,非等餐狀態的車輛就算碰到延後取餐 RoI 也不會誤判。
    增加新狀態的狀態機範例
  3. 狀態的進入與離開動作 (enter / exit action):
    狀態機除了能控制狀態轉換,也能在各個狀態進入與離開的時候觸發事件,適合用來記下這台車在該狀態的開始與結束時間。

用 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) 這類課程,這次在產品開發上把它拿來用,發現它能直接對應我們的需求,後續功能與需求的開發也進行得非常快速,同時讓我們回頭檢視還有哪些程式碼能用類似的方式重構。

參考資料


本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格。


上一篇
從相機畫面到衛星圖:得來速 Vision AI 的多相機校準與跨相機追蹤
下一篇
Berry AI 的演算法調校平台:streamlit 加 WebRTC 即時預覽,14470 次迭代的調校實戰
系列文
Berry AI:從零開始打造全美第一的得來速 Vision AI 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言