在计算机视觉中,一个基本目标是从静态图像或视频序列中提取有意义的信息。为了理解这些信号,通常有助于对其进行可视化。例如,在跟踪高速公路上行驶的单个汽车时,我们可以围绕它们绘制边界框,或者在检测传送带上产品线中的问题时,我们可以使用不同的颜色来标记异常。但是,如果提取的信息是更具数值性质的,并且你希望可视化该信号的时间动态呢?
仅仅在屏幕上显示数值可能无法提供足够的洞察力,尤其是当信号变化迅速时。在这种情况下,可视化信号的一个好方法是带有时间轴的图表。在本文中,我将向你展示如何结合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