基于 OpenCV 和 Matplotlib 的物體移動可視化
在計算機視覺中,一個基本目標是從靜態圖像或視頻序列中提取有意義的信息。為了理解這些信號,通常有助于對其進行可視化。例如,在跟蹤高速公路上行駛的單個汽車時,我們可以圍繞它們繪制邊界框,或者在檢測傳送帶上產品線中的問題時,我們可以使用不同的顏色來標記異常。但是,如果提取的信息是更具數值性質的,并且你希望可視化該信號的時間動態呢?
僅僅在屏幕上顯示數值可能無法提供足夠的洞察力,尤其是當信號變化迅速時。在這種情況下,可視化信號的一個好方法是帶有時間軸的圖表。在本文中,我將向你展示如何結合OpenCV和Matplotlib的強大功能,創建此類信號的實時動畫可視化。
繪制球的運動軌跡
讓我們從一個簡單的示例問題開始,我錄制了一個球垂直向上拋出的視頻。目標是跟蹤視頻中的球,并繪制其位置p(t)、速度v(t)和加速度a(t)隨時間的變化。
輸入視頻截圖
讓我們將參考坐標系定義為攝像機,為了簡單起見,我們只跟蹤圖像中球的垂直位置。我們期望位置呈拋物線形狀,速度線性減小,加速度保持恒定。
預期圖表的草圖
球體分割
首先,我們需要在視頻序列的每一幀中識別球體。由于攝像機保持靜止,檢測球的一個簡單方法是使用背景減除模型,并結合顏色模型來去除畫面中的手。
首先,讓我們使用OpenCV的VideoCapture簡單循環顯示視頻片段。我們只需在視頻片段結束時重新開始播放。我們還通過根據視頻的FPS計算sleep_time(以毫秒為單位)來確保以原始幀速率播放視頻。最后,確保釋放資源并關閉窗口。
輸入視頻的可視化代碼:
import cv2
cap = cv2.VideoCapture("ball.mp4")
fps = int(cap.get(cv2.CAP_PROP_FPS))
while True:
ret, frame = cap.read()
if not ret:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
continue
cv2.imshow("Frame", frame)
sleep_time = 1000 // fps
key = cv2.waitKey(sleep_time) & 0xFF
if key & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
讓我們先提取球的二值分割掩碼。這基本上意味著我們希望創建一個掩碼,該掩碼對球的像素激活,對所有其他像素不激活。為此,我將結合兩個掩碼:運動掩碼和顏色掩碼。運動掩碼提取移動的部分,而顏色掩碼主要去除畫面中的手。對于顏色過濾器,我們可以將圖像轉換為HSV顏色空間,并選擇包含球體綠色但不含膚色色調的特定色調范圍(20–100)。我不對飽和度或亮度值進行過濾,因此我們可以使用全范圍(0–255)。
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask_color = cv2.inRange(hsv, (20, 0, 0), (100, 255, 255))
要創建運動掩碼,我們可以使用簡單的背景減除模型。我們使用視頻的第一幀作為背景,將學習率設置為1。在循環中,我們應用背景模型以獲取前景掩碼,但通過將學習率設置為0,不將新幀集成到其中。
bg_sub = cv2.createBackgroundSubtractorMOG2(varThreshold=50, detectShadows=False)
ret, frame0 = cap.read()
if not ret:
print("Error: cannot read video file")
exit(1)
bg_sub.apply(frame0, learningRate=1.0)
while True:
...
mask_fg = bg_sub.apply(frame, learningRate=0)
接下來,我們可以結合這兩個掩碼,并應用開運算形態學操作以去除小噪聲,最終得到球的完美分割。
mask = cv2.bitwise_and(mask_color, mask_fg)
mask = cv2.morphologyEx(
mask, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (13, 13))
)
跟蹤球體
現在我們只剩下掩碼中的球體。為了跟蹤球的中心,我首先提取球的輪廓,然后將其邊界框的中心作為參考點。如果某些噪聲通過了我們的掩碼,我通過大小過濾檢測到的輪廓,只關注最大的一個。
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
largest_contour = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(largest_contour)
center = (x + w // 2, y + h // 2)
我們還可以在幀中添加一些注釋以可視化檢測結果。我打算繪制兩個圓圈,一個用于中心,一個用于球的周長。
cv2.circle(frame, center, 30, (255, 0, 0), 2)
cv2.circle(frame, center, 2, (255, 0, 0), 2)
為了跟蹤球的位置,我們可以使用一個列表。每當檢測到球時,我們只需將中心位置添加到列表中。我們還可以通過在跟蹤位置列表的每個段之間繪制線條來可視化軌跡。
tracked_pos = []
while True:
...
if len(contours) > 0:
...
tracked_pos.append(center)
# draw trajectory
for i in range(1, len(tracked_pos)):
cv2.line(frame, tracked_pos[i - 1], tracked_pos[i], (255, 0, 0), 1)
球體軌跡的可視化
創建圖表
現在我們可以跟蹤球了,讓我們開始探索如何使用matplotlib繪制信號。首先,我們可以在視頻結束時創建最終圖表,然后在第二步中考慮如何實時動畫化它。為了顯示位置、速度和加速度,我們可以使用三個水平對齊的子圖:
fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(10, 2), dpi=100)
axs[0].set_title("Position")
axs[0].set_ylim(0, 700)
axs[1].set_title("Velocity")
axs[1].set_ylim(-200, 200)
axs[2].set_title("Acceleration")
axs[2].set_ylim(-30, 10)
for ax in axs:
ax.set_xlim(0, 20)
ax.grid(True)
我們只對圖像中的y位置(數組索引1)感興趣,為了獲得零偏移的位置圖,我們可以減去第一個位置。
pos0 = tracked_pos[0][1]
pos = np.array([pos0 - pos[1] for pos in tracked_pos])
對于速度,我們可以使用位置的差值作為近似值,對于加速度,我們可以使用速度的差值。
vel = np.diff(pos)
acc = np.diff(vel)
現在我們可以繪制這三個值:
位置、速度和加速度的靜態圖表
動畫化圖表
現在進入有趣的部分,我們希望使這個圖表動態化!由于我們正在OpenCV的GUI循環中工作,我們不能直接使用matplotlib的show函數,因為這會阻塞循環并且不會運行我們的程序。相反,我們需要使用一些技巧。主要思想是將圖表繪制到內存中的緩沖區,然后在OpenCV窗口中顯示該緩沖區。通過手動調用畫布的draw函數,我們可以強制將圖形渲染到緩沖區。然后我們可以獲取該緩沖區并將其轉換為數組。由于緩沖區是RGB格式,而OpenCV使用BGR,我們需要轉換顏色順序。
fig.canvas.draw()
buf = fig.canvas.buffer_rgba()
plot = np.asarray(buf)
plot = cv2.cvtColor(plot, cv2.COLOR_RGB2BGR)
確保axs.plot調用現在位于幀循環內:
while True:
...
axs[0].plot(range(len(pos)), pos, c="b")
axs[1].plot(range(len(vel)), vel, c="b")
axs[2].plot(range(len(acc)), acc, c="b")
...
現在我們可以使用OpenCV的imshow函數簡單地顯示圖表。
cv2.imshow("Plot", plot)
為了提高性能,我們需要使用blitting技術。這是一種高級渲染技術,將圖表的靜態部分繪制到背景圖像中,只重新繪制變化的動態元素。要設置此功能,我們首先需要在幀循環之前為每個圖表定義一個引用。
pl_pos = axs[0].plot([], [], c="b")[0]
pl_vel = axs[1].plot([], [], c="b")[0]
pl_acc = axs[2].plot([], [], c="b")[0]
然后,我們需要在循環之前繪制一次圖形的背景,并獲取每個軸的背景。
fig.canvas.draw()
bg_axs = [fig.canvas.copy_from_bbox(ax.bbox) for ax in axs]
在循環中,我們現在可以更改每個圖表的數據,然后對于每個子圖,我們需要恢復區域的背景,繪制新圖表,然后調用blit函數以應用更改。
# Update plot data
pl_pos.set_data(range(len(pos)), pos)
pl_vel.set_data(range(len(vel)), vel)
pl_acc.set_data(range(len(acc)), acc)
# Blit Pos
fig.canvas.restore_region(bg_axs[0])
axs[0].draw_artist(pl_pos)
fig.canvas.blit(axs[0].bbox)
# Blit Vel
fig.canvas.restore_region(bg_axs[1])
axs[1].draw_artist(pl_vel)
fig.canvas.blit(axs[1].bbox)
# Blit Acc
fig.canvas.restore_region(bg_axs[2])
axs[2].draw_artist(pl_acc)
fig.canvas.blit(axs[2].bbox)
完整代碼:https://github.com/trflorian/ball-tracking-live-plot/blob/main/src/tracker.py