基于 Kalman 濾波的無標簽網(wǎng)球運動員追蹤
近年來,隨著體育追蹤項目的興起,越來越多的體育愛好者開始使用自動化運動員追蹤技術。大多數(shù)方法遵循一個常見的工作流程:收集標注數(shù)據(jù),訓練YOLO模型,將運動員坐標投影到場地或球場的俯視圖中,并利用這些追蹤數(shù)據(jù)生成高級分析,以獲取潛在的競技洞察。然而,在本項目中,我們提供了一種工具,可以繞過對標注數(shù)據(jù)的需求,轉(zhuǎn)而依賴GroundingDINO的無監(jiān)督追蹤能力,并結(jié)合Kalman濾波來克服GroundingDINO輸出的噪聲問題。
我們的數(shù)據(jù)集來源于一組公開的廣播視頻,視頻鏈接:https://github.com/HaydenFaulkner/Tennis。這些數(shù)據(jù)包括2012年溫布爾登奧運會期間多場網(wǎng)球比賽的錄像,我們重點關注了塞雷娜·威廉姆斯(Serena Williams)和維多利亞·阿扎倫卡(Victoria Azarenka)之間的一場比賽。
對于不熟悉GroundingDINO的人來說,它將目標檢測與語言相結(jié)合,允許用戶輸入提示,例如“一個網(wǎng)球運動員”,模型隨后會返回符合描述的候選目標檢測框。RoboFlow提供了一個很好的教程,供有興趣使用的人參考——但我在下面也粘貼了一些非常基礎的代碼。如下所示,你可以通過提示讓模型識別一些在目標檢測數(shù)據(jù)集中很少被標記的對象,比如狗狗和狗的舌頭。
from groundingdino.util.inference import load_model, load_image, predict, annotate
BOX_TRESHOLD = 0.35
TEXT_TRESHOLD = 0.25
# processes the image to GroundingDino standards
image_source, image = load_image("dog.jpg")
prompt = "dog tongue, dog"
boxes, logits, phrases = predict(
model=model,
image=image,
caption=TEXT_PROMPT,
box_threshold=BOX_TRESHOLD,
text_threshold=TEXT_TRESHOLD
)
然而,在職業(yè)網(wǎng)球場上區(qū)分運動員并不像提示“網(wǎng)球運動員”那么簡單。模型經(jīng)常會錯誤識別場上的其他人員,例如線審、球童和其他裁判,導致標注跳躍且不一致。此外,模型有時甚至無法在某些幀中檢測到運動員,導致標注框出現(xiàn)空白或無法持續(xù)出現(xiàn)在每一幀中。
追蹤在第一例中捕捉到線審,在第二例中捕捉到球童。圖片由作者制作
為了解決這些挑戰(zhàn),我們應用了幾種有針對性的方法。首先,我們將檢測框縮小到所有可能框中的前三個概率最高的框。通常,線審的概率得分高于運動員,這就是為什么我們不只過濾到兩個框。然而,這引發(fā)了一個新問題:如何在每一幀中自動區(qū)分運動員和線審?
我們觀察到,線和球工作人員的檢測框通常持續(xù)時間較短,往往只持續(xù)幾幀。基于此,我們假設通過關聯(lián)連續(xù)幀中的框,可以過濾掉那些只短暫出現(xiàn)的人員,從而隔離出運動員。
那么,我們?nèi)绾螌崿F(xiàn)跨幀對象之間的這種關聯(lián)呢?幸運的是,多目標追蹤領域已經(jīng)對這個問題進行了廣泛研究。Kalman濾波器是多目標追蹤中的主要工具,通常與其他識別指標(如顏色)結(jié)合使用。對于我們的目的,一個基本的Kalman濾波器實現(xiàn)就足夠了。簡單來說(更深入的探討可以參考這篇文章),Kalman濾波器是一種基于先前測量結(jié)果概率估計對象位置的方法。它在處理噪聲數(shù)據(jù)時特別有效,但也適用于在視頻中跨時間關聯(lián)對象,即使檢測不一致(例如運動員未被每一幀追蹤到)。我們在這里實現(xiàn)了一個完整的Kalman濾波器,但將在接下來的段落中介紹一些主要步驟。
二維Kalman濾波器的狀態(tài)非常簡單,如下所示。我們只需要跟蹤x和y位置以及對象在兩個方向上的速度(忽略加速度)。
class KalmanStateVector2D:
x: float
y: float
vx: float
vy: float
Kalman濾波器分為兩個步驟:首先預測對象在下一幀中的位置,然后根據(jù)新的測量結(jié)果(在我們的案例中來自目標檢測器)更新預測。然而,在我們的示例中,新幀可能會有多個新對象,甚至可能會丟失前一幀中存在的對象,這就引出了如何將之前看到的框與當前看到的框關聯(lián)起來的問題。
我們選擇使用馬氏距離(Mahalanobis distance)結(jié)合卡方檢驗來評估當前檢測與過去對象匹配的概率。此外,我們保留了一個過去對象的隊列,以便擁有比一幀更長的“記憶”。具體來說,我們的記憶存儲了過去30幀中看到的任何對象的軌跡。然后,對于我們在新幀中找到的每個對象,我們遍歷我們的記憶,找到最可能與當前對象匹配的先前對象,匹配概率由馬氏距離給出。然而,我們也可能看到一個全新的對象,在這種情況下,我們應該將一個新對象添加到我們的記憶中。如果任何對象與記憶中的任何框的關聯(lián)概率小于30%,我們將其作為新對象添加到記憶中。
完整的Kalman濾波器如下:
from dataclasses import dataclass
import numpy as np
from scipy import stats
class KalmanStateVectorNDAdaptiveQ:
states: np.ndarray # for 2 dimensions these are [x, y, vx, vy]
cov: np.ndarray # 4x4 covariance matrix
def __init__(self, states: np.ndarray) -> None:
self.state_matrix = states
self.q = np.eye(self.state_matrix.shape[0])
self.cov = None
# assumes a single step transition
self.f = np.eye(self.state_matrix.shape[0])
# divide by 2 as we have a velocity for each state
index = self.state_matrix.shape[0] // 2
self.f[:index, index:] = np.eye(index)
def initialize_covariance(self, noise_std: float) -> None:
self.cov = np.eye(self.state_matrix.shape[0]) * noise_std**2
def predict_next_state(self, dt: float) -> None:
self.state_matrix = self.f @ self.state_matrix
self.predict_next_covariance(dt)
def predict_next_covariance(self, dt: float) -> None:
self.cov = self.f @ self.cov @ self.f.T + self.q
def __add__(self, other: np.ndarray) -> np.ndarray:
return self.state_matrix + other
def update_q(
self, innovation: np.ndarray, kalman_gain: np.ndarray, alpha: float = 0.98
) -> None:
innovation = innovation.reshape(-1, 1)
self.q = (
alpha * self.q
+ (1 - alpha) * kalman_gain @ innovation @ innovation.T @ kalman_gain.T
)
class KalmanNDTrackerAdaptiveQ:
def __init__(
self,
state: KalmanStateVectorNDAdaptiveQ,
R: float, # R
Q: float, # Q
h: np.ndarray = None,
) -> None:
self.state = state
self.state.initialize_covariance(Q)
self.predicted_state = None
self.previous_states = []
self.h = np.eye(self.state.state_matrix.shape[0]) if h is None else h
self.R = np.eye(self.h.shape[0]) * R**2
self.previous_measurements = []
self.previous_measurements.append(
(self.h @ self.state.state_matrix).reshape(-1, 1)
)
def predict(self, dt: float) -> None:
self.previous_states.append(self.state)
self.state.predict_next_state(dt)
def update_covariance(self, gain: np.ndarray) -> None:
self.state.cov -= gain @ self.h @ self.state.cov
def update(
self, measurement: np.ndarray, dt: float = 1, predict: bool = True
) -> None:
"""Measurement will be a x, y position"""
self.previous_measurements.append(measurement)
assert dt == 1, "Only single step transitions are supported due to F matrix"
if predict:
self.predict(dt=dt)
innovation = measurement - self.h @ self.state.state_matrix
gain_invertible = self.h @ self.state.cov @ self.h.T + self.R
gain_inverse = np.linalg.inv(gain_invertible)
gain = self.state.cov @ self.h.T @ gain_inverse
new_state = self.state.state_matrix + gain @ innovation
self.update_covariance(gain)
self.state.update_q(innovation, gain)
self.state.state_matrix = new_state
def compute_mahalanobis_distance(self, measurement: np.ndarray) -> float:
innovation = measurement - self.h @ self.state.state_matrix
return np.sqrt(
innovation.T
@ np.linalg.inv(
self.h @ self.state.cov @ self.h.T + self.R
)
@ innovation
)
def compute_p_value(self, distance: float) -> float:
return 1 - stats.chi2.cdf(distance, df=self.h.shape[0])
def compute_p_value_from_measurement(self, measurement: np.ndarray) -> float:
"""Returns the probability that the measurement is consistent with the predicted state"""
distance = self.compute_mahalanobis_distance(measurement)
return self.compute_p_value(distance)
在追蹤了過去30幀中檢測到的每個對象后,我們現(xiàn)在可以設計啟發(fā)式方法來精確定位哪些框最有可能代表我們的運動員。我們測試了兩種方法:選擇最靠近底線中心的框,以及選擇在我們記憶中觀察歷史最長的框。經(jīng)驗上,第一種策略在實際運動員遠離底線時經(jīng)常將線審標記為運動員,使其不太可靠。與此同時,我們注意到GroundingDino往往在不同的線審和球童之間“閃爍”,而真正的運動員則保持相對穩(wěn)定的存在。因此,我們的最終規(guī)則是選擇記憶中追蹤歷史最長的框作為真正的運動員。正如你在初始視頻中看到的,對于如此簡單的規(guī)則來說,它的效果出奇地好!
現(xiàn)在,我們的追蹤系統(tǒng)已經(jīng)在圖像上建立,我們可以轉(zhuǎn)向更傳統(tǒng)的分析,從鳥瞰視角追蹤運動員。這種視角可以評估關鍵指標,例如總移動距離、運動員速度和球場位置趨勢。例如,我們可以分析運動員是否經(jīng)常根據(jù)比賽中的位置針對對手的反手。為了實現(xiàn)這一點,我們需要將運動員坐標從圖像投影到標準化的球場模板上,從上方對齊視角以進行空間分析。
這就是單應性變換(homography)發(fā)揮作用的地方。單應性描述了兩個表面之間的映射關系,在我們的案例中,這意味著將原始圖像中的點映射到俯視的球場視圖。通過在原始圖像中識別一些關鍵點(例如球場上的線交叉點),我們可以計算一個單應性矩陣,將任何點轉(zhuǎn)換為鳥瞰圖。為了創(chuàng)建這個單應性矩陣,我們首先需要識別這些“關鍵點”。RoboFlow等平臺上的各種開源、許可寬松的模型可以幫助檢測這些點,或者我們可以在參考圖像上手動標記它們以用于變換。
正如你所看到的,預測的關鍵點并不完美,但我們發(fā)現(xiàn)小的誤差對最終的變換矩陣影響不大
在標記這些關鍵點后,下一步是將它們與參考球場圖像上的對應點匹配,以生成單應性矩陣。使用OpenCV,我們可以用幾行簡單的代碼創(chuàng)建這個變換矩陣:
import numpy as np
import cv2
# order of the points matters
source = np.array(keypoints) # (n, 2) matrix
target = np.array(court_coords) # (n, 2) matrix
m, _ = cv2.findHomography(source, target)
有了單應性矩陣,我們可以將圖像中的任何點映射到參考球場上。對于這個項目,我們的重點是運動員在球場上的位置。為了確定這一點,我們?nèi)∶總€運動員邊界框底部的中心點,將其作為他們在鳥瞰圖中的球場位置。
我們使用框底部的中心點來映射每個運動員在球場上的位置。圖示顯示了通過我們的單應性矩陣將關鍵點轉(zhuǎn)換為鳥瞰圖中的網(wǎng)球球場
總之,本項目展示了如何利用GroundingDINO的無監(jiān)督能力來追蹤網(wǎng)球運動員,而無需依賴標注數(shù)據(jù),將復雜的目標檢測轉(zhuǎn)化為可操作的運動員追蹤。通過解決關鍵挑戰(zhàn)——例如區(qū)分運動員與其他場上人員、確保跨幀的一致追蹤以及將運動員運動映射到球場的鳥瞰圖——我們?yōu)闊o需顯式標簽的穩(wěn)健追蹤管道奠定了基礎。
這種方法不僅解鎖了移動距離、速度和位置等洞察,還為更深入的比賽分析(如擊球目標和戰(zhàn)略覆蓋)打開了大門。通過進一步改進,包括從GroundingDINO輸出中提煉YOLO或RT-DETR模型,我們甚至可以開發(fā)出與現(xiàn)有商業(yè)解決方案相媲美的實時追蹤系統(tǒng),為網(wǎng)球世界的教練和球迷參與提供強大的工具。