机器视觉(三)运动检测

在这篇文章中,我们将探讨机器视觉中的运动检测。有两种非常常见的方法来实现这个目标:第一种称为差分运动检测(motion by difference ),第二种称为光流(optical flow)。差分运动检测是一种简单且计算量不大的方法,但它提供的信息有限。您可以判断是否发生了运动,并确定运动发生的区域,但无法很好地了解运动的方向和速度。另一方面,光流计算量很大,但能提供大量信息。您可以得到图像中不同小区域的运动速度和方向向量。光流需要先了解一种称为遮罩(masking)的机器视觉方法,因此今天我们只讨论差分运动检测。我将首先介绍这种方法的理论背景,然后在我们的Python代码中设置差分运动检测,并进行一些实验以了解其效果。

差分运动检测与我们之前学习的背景减除方法非常相似。唯一的区别是,差分运动检测不是捕获一个长期使用的背景图像,而是连续捕获一个背景图像和一个前景图像,两者在时间上相隔一段时间ΔT。换句话说,这种方法的流程如下:

  • 捕获一个背景图像
  • 等待ΔT时间
  • 捕获一个前景图像
  • 将两个图像相减,然后显示差异

我们还需要包括一些周围的代码,以确保这些像素值不为负数,也不超过255。尽管这种方法非常简单,但通过调整ΔT值,您可以稍微调整其效果以及检测移动物体的速度。让我为您做一个简单的演示。

假设您的第一个图像矩阵是这样的。我将写一个非常低分辨率的图像,以便快速进行计算。假设这是一个黑白图像,像素值为0或1。这个图像矩阵代表位于左上角的一个物体。

图片

假设这个物体在屏幕上移动。然后在等待ΔT时间后,我会得到第二个图像。假设这个ΔT很小。

图片

假设我将第二个图像减去第一个,然后将小于0的值设为0,那么相减后得到的图像会是什么样子?相减和限制后,即将小于0的值设为0,我得到的图像如下:

图片

如果我使用质心方法来找到这个物体的位置,我会发现物体位于第一和第二行之间,并直接在第三列上。

图片

现在,假设我使用较大的ΔT,我们称之为中等ΔT。我们会更具体地量化这个值。由于ΔT较大,物体在两次拍摄之间移动得更远。

图片

现在相减后会发生什么情况?在这种情况下,相减和限制后,我们得到如下图像。

图片

将这两个结果放在一起对比:

图片

左图显示了运动的更亮图像,因为我们有4个代表物体运动的像素,而右图只有2个。这是因为在右边的例子中,ΔT足够小,物体在两幅图像之间重叠。这表明较大的ΔT更好。

然而,在选择较大的ΔT时,您需要考虑几个因素。首先,不能将ΔT设置得太高,否则物体可能在拍摄第二幅图像前移动到屏幕之外。此外,您选择的ΔT值决定了运动的采样率。例如,即使将ΔT设置得足够小,物体不会在拍摄间隔内移动到屏幕之外,如果采样率设置得太低,您可能只能获取到物体位置的少量样本。这对于运动检测应用很重要,例如运动跟踪(motion tracking)。我们可能希望某个物体在移动时进行旋转和指向。如果检测运动的采样率较低,控制旋转物体以跟随移动物体会更加困难。我们希望设置ΔT,使其能清晰捕捉运动,并在物体移动过程中获取尽可能多的样本。

让我们看看如何估计一个合适的ΔT值。我将从估计物体的速度开始,单位为每秒像素(pixels/s)。实际上,速度单位无关紧要,只要所有单位一致即可。但为了便于理解,我们假设速度单位为每秒像素。接下来,我假设物体的尺寸,单位为像素,记为D。完成推导后,我会举个例子帮助理解这些值的含义以及如何获取这些值。我们最亮的运动图像,也是物体移动的最小距离,可以通过物体在ΔT时间内移动D像素来获得。由于我无法控制物体的速度,只能控制ΔT,所以我们将ΔT设为约等于D除以速度V。

让我们来看一个例子。假设我有一台相机正对一条道路,并希望检测汽车在道路上的运动。为了计算合适的ΔT值,我需要估计D,即汽车在图像中的像素大小,以及V,即汽车在这条路上的速度,单位为每秒像素。

为了获得这些值,我只需要一个转换值,即像素单位和物理单位的转换因子。我将相机设置在检测汽车的位置,然后走到汽车通常离相机的距离。我测量相机视野中最左边到最右边的距离,记为W,单位为米。

图片

假设相机分辨率为640×480,那么左右方向上有640个像素。这样我得到一个转换因子k =W/640。我知道平均汽车长度约为4.5米,但需要将其转换为像素。因此,D,单位为像素,等于4.5k,单位为像素/米。米的单位会抵消,最终得到平均汽车长度在像素单位中的值。

接下来需要平均速度V,单位为每秒像素。这并不需要非常准确。假设汽车以限速行驶,即使实际速度稍有不同,我们也不会错过检测。假设这条路的限速为45英里每小时,约等于20米每秒。使用相同的转换因子,将速度转换为像素单位(V=20k)。现在我们计算ΔT,等于D除以V。注意,在这个计算中,转换因子会相互抵消 (ΔT = Dk/Vk = D/V),因此我们可以用米或任何其他单位计算ΔT,只要分子和分母的单位一致。

Python代码 让我们在Python代码中实现这个方法。这里有我们之前写的颜色减法代码,在这个颜色减法代码中,我们只拍摄一张图像并显示在屏幕上。为了进行差分运动检测,我需要拍摄一张图像,等待一段时间T,然后再拍摄另一张图像。首先,我将这些变量改为1,表示这是拍摄的第一张图像。您还需要将这些变量改为red1,green1,blue1,我稍后会注意到这一点并进行更改。

    red1=np.matrix(frame[:,:,2])
    green1=np.matrix(frame[:,:,1])
    blue1=np.matrix(frame[:,:,0])

    red_only1 = np.int16(red) - np.int16(green) - np.int16(blue)

    red_only1[red_only < 0] = 0
    red_only1[red_only > 255] = 255
    red_only1 = np.uint8(red_only)

接下来,我们需要在拍摄图像后等待一段时间。为了在Python中实现这一点,我们需要导入另一个包,叫做time。然后我们可以使用函数time.sleep,在括号中写入我们要等待的秒数。我暂时设置为0.1,即十分之一秒。

 time.sleep(0.1)

现在复制所有这些拍摄图像的代码,并粘贴在sleep后面。将所有这些1改为2,因为这是拍摄的第二张图像。所以在这里我要将所有的1改为2。这些变量也改为2。

    _, frame=cap.read()

    red2=np.matrix(frame[:,:,2])
    green2=np.matrix(frame[:,:,1])
    blue2=np.matrix(frame[:,:,0])

    red_only2 = np.int16(red2) - np.int16(green2) - np.int16(blue2)

    red_only2[red_only2 < 0] = 0
    red_only2[red_only2 > 255] = 255
    red_only2 = np.uint8(red_only2)

下一步是将其中一个图像从另一个图像中减去。我将使用红色图像作为减法图像,因为我会让你通过在相机前挥动手来测试,摄像头通常会将人类皮肤检测为红色。所以我们将使用红色图像作为灰度图像。因此,红色图像的差异等于红色图像2减去红色图像1。让我们重命名这个差异图像,以便记住它是什么。我们将其命名为motion,这将是我们的运动图像。

motion = red_only2-red_only1

我们还可以通过将运动图像添加到矩阵列的和中来计算运动发生的位置,然后将这个变量改为motion。

    column_sum = np.sum(motion, 0)
    column_numbers = np.arange(640)
    column_mult = np.multiply(column_sum, column_numbers)
    total = np.sum(column_mult)
    total_total = np.sum(motion)
    column_location = total / total_total

接下来我们清理显示的图像。我们仍然要显示帧,但第二个图像显示的是motion。暂时注释掉其余的代码行。

    cv2.imshow('rgb', frame)
    cv2.imshow('motion', motion)

在运行代码之前,我们需要添加一些格式化输出的代码。我们需要确保输出的值在0到255之间,并确保运动图像是无符号8位整数。所以在这一行代码中,将motion等于u和8位motion。然后复制这些代码行并粘贴在计算运动图像之后。我们希望将运动矩阵的值限制在0到255之间,与红色图像的限制方式相同。

    motion[motion> 255] = 255   
    motion = np.uint8(motion)

好,现在运行代码。应该是当没有运动时,输出的数值为0。在相机前挥动你的手,应该会看到运动的位置从左到右显示在shell中。当手在屏幕左侧挥动时,数值变小;当手在屏幕右侧挥动时,数值变大。请确保在修改ΔT值时测试代码,以确定适合您的具体运动检测任务的最佳值。

附完整程序代码:

import numpy as np
import cv2
import time

cap=cv2.VideoCapture(0)

while(1):
    #take the 1st image 
    _, frame=cap.read()
    
    red1=np.matrix(frame[:,:,2])
    green1=np.matrix(frame[:,:,1])
    blue1=np.matrix(frame[:,:,0])

    red_only1 = np.int16(red1) - np.int16(green1) - np.int16(blue1)

    red_only1[red_only1 < 0] = 0
    red_only1[red_only1 > 255] = 255
    red_only1 = np.uint8(red_only1)

    time.sleep(0.1)

    #tamke the 2nd image
    _, frame=cap.read()

    red2=np.matrix(frame[:,:,2])
    green2=np.matrix(frame[:,:,1])
    blue2=np.matrix(frame[:,:,0])

    red_only2 = np.int16(red2) - np.int16(green2) - np.int16(blue2)

    red_only2[red_only2 < 0] = 0
    red_only2[red_only2 > 255] = 255
    red_only2 = np.uint8(red_only2)

    motion = red_only2-red_only1
    motion[motion> 255] = 255   
    motion = np.uint8(motion) 


    column_sum = np.sum(motion, 0)
    column_numbers = np.arange(640)
    column_mult = np.multiply(column_sum, column_numbers)
    total = np.sum(column_mult)
    total_total = np.sum(motion)
    column_location = total / total_total    

    print(column_location)
    
    cv2.imshow('rgb', frame)
    cv2.imshow('motion', motion)

    k=cv2.waitKey(5)
    if k==27:
        break

cv2.destroyAllWindows()

print(frame)