python识别手掌并计算手指个数算法解析

我们今天来做一个有趣的实验,如何用python实时识别手指的个数,并讲解一下识别过程中的算法,效果如下:

python识别手掌并计算手指个数算法解析

如何从混乱的背景中分割前景图像是一个难题。最明显的原因是由于人看图像和计算机看同一图像时存在的差距。人们可以很容易地弄清楚图像中的内容,但是对于计算机而言,图像只是3维矩阵。因此,计算机视觉问题仍然是一个挑战。看下面的图片。

python识别手掌并计算手指个数算法解析

上面这个图像,如果是人看了会在图像中找到不同的区域并标记其相应的标签,如:“天空”,“人”,“树”和“草”。那计算机如何才能识别呢,我们首先需要单独取出手部区域,以去除视频序列中所有不需要的部分。在分割手部区域之后,我们然后对视频序列中显示的手指进行计数,所以我们分两步走。

第一步:从视频序列中找到并分割手部区域。

第二步:从视频序列中分割的手区域计算手指的数量。

▊ 第一步、分割提取手部区域

手势识别的第一步显然是通过消除视频序列中所有其他不需要的部分来找到手部区域。起初这似乎令人恐惧。但是不用担心。使用Python和OpenCV会容易得多!

注意:视频序列只是相对于时间运行的帧集合或图像集合。

在深入探讨细节之前,让我们了解如何确定手部区域。

▶背景扣除

首先,我们需要一种有效的方法来将前景与背景分开。为此,我们使用移动平均值的概念。我们使我们的系统可以查看特定场景的30帧。在此期间,我们计算当前帧和先前帧的运行平均值。通过这样做,我们实质上告诉我们的系统-

好吧,机器人!您凝视的视频序列(这30帧的运行平均值)是背景。

在弄清背景之后,我们举起手来,使系统了解我们的手是进入背景的新条目,这意味着它成为前景对象。但是,我们将如何单独看待这一前景呢?答案是背景减法。

看下面的图片,它描述了背景减法的工作原理。

python识别手掌并计算手指个数算法解析

在使用移动平均值确定背景模型之后,我们使用当前框架以及背景,该框架还包含前景对象(在本例中为hand)。我们计算背景模型(随时间更新)和当前帧(有我们的手)之间的绝对差,以获得包含新添加的前景对象(这就是我们的手)的差异图像。这就是背景减法的全部含义。

▶运动检测和阈值

为了从该差异图像中检测出手部区域,我们需要对差异图像进行阈值处理,以使只有我们的手部区域可见,而所有其他不需要的区域都被涂成黑色。这就是运动检测的全部意义。

注意:阈值是基于特定阈值级别将像素强度分配为0和1,以便仅从图像中捕获我们感兴趣的对象。

▶轮廓提取

对差异图像进行阈值处理后,我们在结果图像中找到轮廓。假定面积最大的轮廓是我们的手。

注意:轮廓线是位于图像中的对象的轮廓或边界。

具体代码

# organize imports
import cv2
import imutils
import numpy as np

# global variables
bg = None

首先,我们导入所有必须使用的软件包并初始化背景模型。如果您的计算机上没有安装这些软件包。
#--------------------------------------------------
# To find the running average over the background
#--------------------------------------------------
def run_avg(image, aWeight):
    global bg
    # initialize the background
    if bg is None:
        bg = image.copy().astype("float")
        return


    # compute weighted average, accumulate it and update the background
    cv2.accumulateWeighted(image, bg, aWeight)


接下来,我们使用函数来计算背景模型和当前帧之间的移动平均值。此函数接受两个参数- 当前帧和aWeight,就像在图像上执行均线的阈值一样。如果背景模型为“ 无”(即,如果它是第一帧),则使用当前帧对其进行初始化。然后,使用cv2.accumulateWeighted()函数计算背景模型和当前帧的移动平均值。使用下面给出的公式计算移动平均值。

dst(x,y)=(1−a).dst(x,y)+a.src(x,y)

s r c (x ,y) -源图像或输入图像(1或3通道,8位或32位浮点)

dst(x ,ÿ) -目标图像或输出图像(与源图像相同的通道,32位或64位浮点)

a-源图像(输入图像)的权重

#---------------------------------------------
# To segment the region of hand in the image
#---------------------------------------------
def segment(image, threshold=25):
    global bg
    # find the absolute difference between background and current frame
    diff = cv2.absdiff(bg.astype("uint8"), image)


    # threshold the diff image so that we get the foreground
    thresholded = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY)[1]


    # get the contours in the thresholded image
    (_, cnts, _) = cv2.findContours(thresholded.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)


    # return None, if no contours detected
    if len(cnts) == 0:
        return
    else:
        # based on contour area, get the maximum contour which is the hand
        segmented = max(cnts, key=cv2.contourArea)
        return (thresholded, segmented)



我们的下一个功能用于从视频序列中分割手部区域。这个函数有两个参数- 当前帧和阈值用于阈值化的差分图像。

首先,我们使用cv2.absdiff()函数找到背景模型与当前帧之间的绝对差异。

接下来,我们对差异图像进行阈值处理以仅显示手部区域。最后,我们对阈值图像进行轮廓提取,并获取面积最大的轮廓(这就是我们的手)。

我们将阈值图像以及分割后的图像作为元组返回。阈值背后的数学非常简单。如果x (n ) 表示输入图像在特定像素坐标下的像素强度,然后 阀值 决定我们将图像分割/阈值化为二进制图像的程度。公式如下:

python识别手掌并计算手指个数算法解析

#-----------------
# MAIN FUNCTION
#-----------------
if __name__ == "__main__":
    # initialize weight for running average
    aWeight = 0.5


    # get the reference to the webcam
    camera = cv2.VideoCapture(0)


    # region of interest (ROI) coordinates
    top, right, bottom, left = 10, 350, 225, 590


    # initialize num of frames
    num_frames = 0


    # keep looping, until interrupted
    while(True):
        # get the current frame
        (grabbed, frame) = camera.read()


        # resize the frame
        frame = imutils.resize(frame, width=700)


        # flip the frame so that it is not the mirror view
        frame = cv2.flip(frame, 1)


        # clone the frame
        clone = frame.copy()


        # get the height and width of the frame
        (height, width) = frame.shape[:2]


        # get the ROI
        roi = frame[top:bottom, right:left]


        # convert the roi to grayscale and blur it
        gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (7, 7), 0)


        # to get the background, keep looking till a threshold is reached
        # so that our running average model gets calibrated
        if num_frames < 30:
            run_avg(gray, aWeight)
        else:
            # segment the hand region
            hand = segment(gray)


            # check whether hand region is segmented
            if hand is not None:
                # if yes, unpack the thresholded image and
                # segmented region
                (thresholded, segmented) = hand


                # draw the segmented region and display the frame
                cv2.drawContours(clone, [segmented + (right, top)], -1, (0, 0, 255))
                cv2.imshow("Thesholded", thresholded)


        # draw the segmented hand
        cv2.rectangle(clone, (left, top), (right, bottom), (0,255,0), 2)


        # increment the number of frames
        num_frames += 1


        # display the frame with segmented hand
        cv2.imshow("Video Feed", clone)


        # observe the keypress by the user
        keypress = cv2.waitKey(1) & 0xFF


        # if the user pressed "q", then stop looping
        if keypress == ord("q"):
            break


# free up memory
camera.release()
cv2.destroyAllWindows()
上面的代码示例是我们程序的主要功能。我们将aWeight初始化为0.5。如移动平均值方程式中更早显示的那样,此阈值意味着如果为该变量设置一个较低的值,则将在大量的先前帧上执行移动平均值,反之亦然。我们使用cv2.VideoCapture(0)引用了我们的网络摄像头,这意味着我们在计算机中获取了默认的网络摄像头实例。

代替从整个视频序列中识别手势,我们将尝试最小化系统必须在其中寻找手部区域的识别区域(或区域)。为了突出显示该区域,我们使用cv2.rectangle()函数,该函数需要顶部,右侧,底部和左侧像素坐标。

为了跟踪帧数,我们初始化一个变量num_frames。然后,我们开始无限循环,并使用camera.read()函数从网络摄像头读取帧。然后,我们使用imutils库将输入帧的大小调整为700像素的固定宽度,以保持宽高比,并翻转该帧以避免产生镜像。

接下来,我们使用简单的NumPy切片仅取出感兴趣的区域(即识别区域)。然后,我们将该ROI转换为灰度图像,并使用高斯模糊来最小化图像中的高频分量。直到超过30帧,我们继续将输入帧添加到run_avg函数并更新背景模型。请注意,在此步骤中,必须使相机保持不动。否则,整个算法将失败。

更新背景模型后,将当前输入帧传递到分割函数中,并返回阈值图像和分割图像。所分割的轮廓绘制在使用帧cv2.drawContours() ,并使用被示出阈值化的输出cv2.imshow() 。

最后,我们在当前帧中显示分段的手部区域,并等待按键退出程序。注意,我们在这里将bg变量维护为全局变量。这很重要,必须加以照顾。

▶执行代码

复制上面给出的所有代码,并将其放在一个名为segment.py的文件中。

然后,打开终端或命令提示符,然后键入python segment.py。

注意:切记通过保持相机静止不动来更新背景模型。5-6秒后,将您的手放在识别区域中以仅露出您的手区域。在下面,您可以看到我们的系统如何从实时视频序列中有效分割手部区域。
python识别手掌并计算手指个数算法解析

▊第二步、识别手指个数

从实时视频序列中分割出手部区域后,我们将使我们的系统对通过摄像头/网络摄像头显示的手指进行计数。我们不能使用任何模板(由OpenCV提供)来执行此操作,因为这确实是一个具有挑战性的问题。

我们通过将分割的手区域假定为框架中的最大轮廓(即具有最大面积的轮廓)来获得该区域。如果您在此框架中引入了一个比您的手大的大物体,则此算法将失败。因此,您必须确保您的手占据框架中大部分区域。

我们将使用在可变手形中获得的分段手形区域。请记住,此手形变量是具有阈值(阈值图像)和分段(分段的手区域)的元组。我们将利用这两个变量来计算所显示的手指。我们该怎么做?

可以使用多种方法来数手指,但是在本教程中我们将看到一种这样的方法。下图显示了计数手指的方法。
python识别手掌并计算手指个数算法解析

从上图可以看到,给定分段的手区域,共有四个中间步骤来计数手指。在执行了特定步骤之后,所有这些步骤都显示了一个对应的输出图像(如左图所示)。

▶步骤一:找到分割的手部区域的凸包(轮廓),并计算凸包中的最极端点(极端顶部,极端底部,极端左侧,极端右侧)。

▶步骤二:使用凸包中的这些极值点找到手掌的中心。

▶步骤三:使用手掌的中心,以最大欧几里德距离(手掌的中心和极点之间)为半径构造一个圆。

▶步骤四:在带阈值的手形图像(帧)和圆形ROI(蒙版)之间执行按位与运算。这显示了手指切片,该切片可以进一步用于计算所示手指的数量。

在下面,我们用代码实现以上几个步骤。

输入- 阈值(阈值图像)和分段(分段的手部区域或轮廓)

输出- 计数(手指数)。
#--------------------------------------------------------------
# To count the number of fingers in the segmented hand region
#--------------------------------------------------------------
def count(thresholded, segmented):
    # find the convex hull of the segmented hand region
    chull = cv2.convexHull(segmented)


    # find the most extreme points in the convex hull
    extreme_top    = tuple(chull[chull[:, :, 1].argmin()][0])
    extreme_bottom = tuple(chull[chull[:, :, 1].argmax()][0])
    extreme_left   = tuple(chull[chull[:, :, 0].argmin()][0])
    extreme_right  = tuple(chull[chull[:, :, 0].argmax()][0])


    # find the center of the palm
    cX = int((extreme_left[0] + extreme_right[0]) / 2)
    cY = int((extreme_top[1] + extreme_bottom[1]) / 2)


    # find the maximum euclidean distance between the center of the palm
    # and the most extreme points of the convex hull
    distance = pairwise.euclidean_distances([(cX, cY)], Y=[extreme_left, extreme_right, extreme_top, extreme_bottom])[0]
    maximum_distance = distance[distance.argmax()]


    # calculate the radius of the circle with 80% of the max euclidean distance obtained
    radius = int(0.8 * maximum_distance)


    # find the circumference of the circle
    circumference = (2 * np.pi * radius)


    # take out the circular region of interest which has 
    # the palm and the fingers
    circular_roi = np.zeros(thresholded.shape[:2], dtype="uint8")
	
    # draw the circular ROI
    cv2.circle(circular_roi, (cX, cY), radius, 255, 1)


    # take bit-wise AND between thresholded hand using the circular ROI as the mask
    # which gives the cuts obtained using mask on the thresholded hand image
    circular_roi = cv2.bitwise_and(thresholded, thresholded, mask=circular_roi)


    # compute the contours in the circular ROI
    (_, cnts, _) = cv2.findContours(circular_roi.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)


    # initalize the finger count
    count = 0


    # loop through the contours found
    for c in cnts:
        # compute the bounding box of the contour
        (x, y, w, h) = cv2.boundingRect(c)


        # increment the count of fingers only if -
        # 1. The contour region is not the wrist (bottom area)
        # 2. The number of points along the contour does not exceed
        #     25% of the circumference of the circular ROI
        if ((cY + (cY * 0.25)) > (y + h)) and ((circumference * 0.25) > c.shape[0]):
            count += 1


    return count


每个中间步骤都需要对图像处理基础知识有一些了解,例如轮廓,按位与,欧氏距离和凸包。

▶Contours等高线

感兴趣对象的轮廓或边界。使用OpenCV的cv2.findContours()函数可以轻松找到该轮廓。在解压缩此函数的返回值时要小心,因为在OpenCV 3.1.0- Contours中,我们需要三个变量来解压缩此元组。

▶Bitwise-AND 按位与

在两个对象之间执行按位逻辑与。您可以从视觉上将其想象为使用遮罩并提取图像中仅位于此遮罩下的区域。OpenCV提供cv2.bitwise_and()函数来执行此操作- 按位与。

▶Euclidean Distance欧氏距离

这是此处所示方程式给出的两点之间的距离。Scikit-learn提供了一个名为pairwise.euclidean_distances ()的函数,用于计算单行代码“ 成对欧几里得距离”中从一个点到多个点的欧几里得距离。在那之后,我们采取了最大的使用与NumPy的所有这些距离argmax()函数。

▶Convex Hull凸包

您可以将凸包视为动态的,可拉伸的信封,将目标对象包裹起来。

▶最后的结果

您可以在此处将整个代码下载到perfom手势识别中。使用克隆该存储库

命令贝壳

git clone https://github.com/Gogul09/gesture-recognition.git


在终端/命令提示符下。然后,进入文件夹并输入

python recognize.py

注意:在30帧的校准期间,请勿摇动网络摄像头。如果在前30帧中晃动,则整个算法将无法达到我们的预期。

之后,您可以将手伸入边界框,显示手势,并相应地显示手指数。

{{collectdata}}

网友评论0