基于 OpenCV 和 Matplotlib 的物體移動(dòng)可視化
在計(jì)算機(jī)視覺(jué)中,一個(gè)基本目標(biāo)是從靜態(tài)圖像或視頻序列中提取有意義的信息。為了理解這些信號(hào),通常有助于對(duì)其進(jìn)行可視化。例如,在跟蹤高速公路上行駛的單個(gè)汽車(chē)時(shí),我們可以圍繞它們繪制邊界框,或者在檢測(cè)傳送帶上產(chǎn)品線(xiàn)中的問(wèn)題時(shí),我們可以使用不同的顏色來(lái)標(biāo)記異常。但是,如果提取的信息是更具數(shù)值性質(zhì)的,并且你希望可視化該信號(hào)的時(shí)間動(dòng)態(tài)呢?
僅僅在屏幕上顯示數(shù)值可能無(wú)法提供足夠的洞察力,尤其是當(dāng)信號(hào)變化迅速時(shí)。在這種情況下,可視化信號(hào)的一個(gè)好方法是帶有時(shí)間軸的圖表。在本文中,我將向你展示如何結(jié)合OpenCV和Matplotlib的強(qiáng)大功能,創(chuàng)建此類(lèi)信號(hào)的實(shí)時(shí)動(dòng)畫(huà)可視化。
繪制球的運(yùn)動(dòng)軌跡
讓我們從一個(gè)簡(jiǎn)單的示例問(wèn)題開(kāi)始,我錄制了一個(gè)球垂直向上拋出的視頻。目標(biāo)是跟蹤視頻中的球,并繪制其位置p(t)、速度v(t)和加速度a(t)隨時(shí)間的變化。
輸入視頻截圖
讓我們將參考坐標(biāo)系定義為攝像機(jī),為了簡(jiǎn)單起見(jiàn),我們只跟蹤圖像中球的垂直位置。我們期望位置呈拋物線(xiàn)形狀,速度線(xiàn)性減小,加速度保持恒定。
預(yù)期圖表的草圖
球體分割
首先,我們需要在視頻序列的每一幀中識(shí)別球體。由于攝像機(jī)保持靜止,檢測(cè)球的一個(gè)簡(jiǎn)單方法是使用背景減除模型,并結(jié)合顏色模型來(lái)去除畫(huà)面中的手。
首先,讓我們使用OpenCV的VideoCapture簡(jiǎn)單循環(huán)顯示視頻片段。我們只需在視頻片段結(jié)束時(shí)重新開(kāi)始播放。我們還通過(guò)根據(jù)視頻的FPS計(jì)算sleep_time(以毫秒為單位)來(lái)確保以原始幀速率播放視頻。最后,確保釋放資源并關(guān)閉窗口。
輸入視頻的可視化代碼:
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()
讓我們先提取球的二值分割掩碼。這基本上意味著我們希望創(chuàng)建一個(gè)掩碼,該掩碼對(duì)球的像素激活,對(duì)所有其他像素不激活。為此,我將結(jié)合兩個(gè)掩碼:運(yùn)動(dòng)掩碼和顏色掩碼。運(yùn)動(dòng)掩碼提取移動(dòng)的部分,而顏色掩碼主要去除畫(huà)面中的手。對(duì)于顏色過(guò)濾器,我們可以將圖像轉(zhuǎn)換為HSV顏色空間,并選擇包含球體綠色但不含膚色色調(diào)的特定色調(diào)范圍(20–100)。我不對(duì)飽和度或亮度值進(jìn)行過(guò)濾,因此我們可以使用全范圍(0–255)。
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask_color = cv2.inRange(hsv, (20, 0, 0), (100, 255, 255))
要?jiǎng)?chuàng)建運(yùn)動(dòng)掩碼,我們可以使用簡(jiǎn)單的背景減除模型。我們使用視頻的第一幀作為背景,將學(xué)習(xí)率設(shè)置為1。在循環(huán)中,我們應(yīng)用背景模型以獲取前景掩碼,但通過(guò)將學(xué)習(xí)率設(shè)置為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)
接下來(lái),我們可以結(jié)合這兩個(gè)掩碼,并應(yīng)用開(kāi)運(yùn)算形態(tài)學(xué)操作以去除小噪聲,最終得到球的完美分割。
mask = cv2.bitwise_and(mask_color, mask_fg)
mask = cv2.morphologyEx(
mask, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (13, 13))
)
跟蹤球體
現(xiàn)在我們只剩下掩碼中的球體。為了跟蹤球的中心,我首先提取球的輪廓,然后將其邊界框的中心作為參考點(diǎn)。如果某些噪聲通過(guò)了我們的掩碼,我通過(guò)大小過(guò)濾檢測(cè)到的輪廓,只關(guān)注最大的一個(gè)。
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)
我們還可以在幀中添加一些注釋以可視化檢測(cè)結(jié)果。我打算繪制兩個(gè)圓圈,一個(gè)用于中心,一個(gè)用于球的周長(zhǎng)。
cv2.circle(frame, center, 30, (255, 0, 0), 2)
cv2.circle(frame, center, 2, (255, 0, 0), 2)
為了跟蹤球的位置,我們可以使用一個(gè)列表。每當(dāng)檢測(cè)到球時(shí),我們只需將中心位置添加到列表中。我們還可以通過(guò)在跟蹤位置列表的每個(gè)段之間繪制線(xiàn)條來(lái)可視化軌跡。
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)
球體軌跡的可視化
創(chuàng)建圖表
現(xiàn)在我們可以跟蹤球了,讓我們開(kāi)始探索如何使用matplotlib繪制信號(hào)。首先,我們可以在視頻結(jié)束時(shí)創(chuàng)建最終圖表,然后在第二步中考慮如何實(shí)時(shí)動(dòng)畫(huà)化它。為了顯示位置、速度和加速度,我們可以使用三個(gè)水平對(duì)齊的子圖:
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)
我們只對(duì)圖像中的y位置(數(shù)組索引1)感興趣,為了獲得零偏移的位置圖,我們可以減去第一個(gè)位置。
pos0 = tracked_pos[0][1]
pos = np.array([pos0 - pos[1] for pos in tracked_pos])
對(duì)于速度,我們可以使用位置的差值作為近似值,對(duì)于加速度,我們可以使用速度的差值。
vel = np.diff(pos)
acc = np.diff(vel)
現(xiàn)在我們可以繪制這三個(gè)值:
位置、速度和加速度的靜態(tài)圖表
動(dòng)畫(huà)化圖表
現(xiàn)在進(jìn)入有趣的部分,我們希望使這個(gè)圖表動(dòng)態(tài)化!由于我們正在OpenCV的GUI循環(huán)中工作,我們不能直接使用matplotlib的show函數(shù),因?yàn)檫@會(huì)阻塞循環(huán)并且不會(huì)運(yùn)行我們的程序。相反,我們需要使用一些技巧。主要思想是將圖表繪制到內(nèi)存中的緩沖區(qū),然后在OpenCV窗口中顯示該緩沖區(qū)。通過(guò)手動(dòng)調(diào)用畫(huà)布的draw函數(shù),我們可以強(qiáng)制將圖形渲染到緩沖區(qū)。然后我們可以獲取該緩沖區(qū)并將其轉(zhuǎn)換為數(shù)組。由于緩沖區(qū)是RGB格式,而OpenCV使用BGR,我們需要轉(zhuǎn)換顏色順序。
fig.canvas.draw()
buf = fig.canvas.buffer_rgba()
plot = np.asarray(buf)
plot = cv2.cvtColor(plot, cv2.COLOR_RGB2BGR)
確保axs.plot調(diào)用現(xiàn)在位于幀循環(huán)內(nèi):
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")
...
現(xiàn)在我們可以使用OpenCV的imshow函數(shù)簡(jiǎn)單地顯示圖表。
cv2.imshow("Plot", plot)
為了提高性能,我們需要使用blitting技術(shù)。這是一種高級(jí)渲染技術(shù),將圖表的靜態(tài)部分繪制到背景圖像中,只重新繪制變化的動(dòng)態(tài)元素。要設(shè)置此功能,我們首先需要在幀循環(huán)之前為每個(gè)圖表定義一個(gè)引用。
pl_pos = axs[0].plot([], [], c="b")[0]
pl_vel = axs[1].plot([], [], c="b")[0]
pl_acc = axs[2].plot([], [], c="b")[0]
然后,我們需要在循環(huán)之前繪制一次圖形的背景,并獲取每個(gè)軸的背景。
fig.canvas.draw()
bg_axs = [fig.canvas.copy_from_bbox(ax.bbox) for ax in axs]
在循環(huán)中,我們現(xiàn)在可以更改每個(gè)圖表的數(shù)據(jù),然后對(duì)于每個(gè)子圖,我們需要恢復(fù)區(qū)域的背景,繪制新圖表,然后調(diào)用blit函數(shù)以應(yīng)用更改。
# 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