【手势识别学习笔记2】利用摄像头获取数据集(下)

上一篇我们讲到了使用OpenCV框架帮助我们调用摄像头进行拍照,今天我们就要开始利用这个方法创建一个小工具,帮助我们产生需要的数据集。

小工具界面设计

在上一篇的讲解中,我们知道OpenCV有一个非常好用的函数:

cv2.waitKey()

通过这个函数,我们能够等待用户从键盘输入数据,那么很自然地,我们可以创建一个实时预览的窗口,并通过提供按键切换功能的方式,为我们创建一个简单的数据集录制工具。

另外,为了方便地把这个工具放进最终的工具包里,我们可以把它封装进class。

选项

主界面的设计比较简单,只需要显示实时的视频(请回顾上一期)和几个必要的选项。

  • 1:RGB模式
  • 2:Binary模式
  • 3:录制手势
  • 0:退出

大致来说这几个就够了。

那么这几个文字放在哪里呢?

cv2为我们提供了

cv2.putText()

函数,函数的作用是在拍摄的frame上显示文字。

为了方便可以把这一部分抽出来做一个函数:

@staticmethod
def show_capturer_options(frame):
 font_face = cv2.FONT_HERSHEY_COMPLEX
 font_size = 0.5
 font_color = (0, 255, 0)
 cv2.putText(frame, 'Options:', (18, 18), font_face, font_size, font_color)
 cv2.putText(frame, '1: RGB Mode', (18, 18 + 18), font_face, font_size, font_color)
 cv2.putText(frame, '2: Binary Mode', (18, 18 + 2 * 18), font_face, font_size, font_color)
 cv2.putText(frame, '3: New Gesture', (18, 18 + 3 * 18), font_face, font_size, font_color)
 cv2.putText(frame, '0: Quit', (18, 18 + 4 * 18), font_face, font_size, font_color)

解释一下代码:

font_face是cv2提供的字体选项,cv2.FONT_HERSHEY_SIMPLEX是正常大小无衬线字体。

font_size是字体缩放

font_color是RGB绿色。

然后依次按顺序把选项文字写在frame上即可。

显示主界面

利用上一篇中讲到的

cv2.imshow()

就可以显示主界面了,然后利用循环就能实时显示录制的内容了。

主循环代码如下:

def mainloop(self):
    while True:
        flag, frame = self.__cap.read()
        frame = cv2.flip(frame, 2)
        
        GestureCapturer.show_capturer_options(frame)
        key = cv2.waitKey(1) & 0xFF
        
        if key is ord('0'):
            self.quit_capturer()
            return
        
        cv2.imshow('Video Capturer', frame)

另外在退出的时候需要关闭窗口,所以再写一个函数用于退出

def quit_capturer(self):
    self.__cap.release()
    cv2.destroyAllWindows()

那么拍摄器的界面就完成了。

看一下全部的代码吧:

class GestureCapturer(object):
    
    def __init__(self):
        self.__cap = cv2.VideoCapture(0)
        
    @staticmethod
    def show_capturer_options(frame):
        font_face = cv2.FONT_HERSHEY_COMPLEX
        font_size = 0.5
        font_color = (0, 255, 0)
        cv2.putText(frame, 'Options:', (18, 18), font_face, font_size, font_color)
        cv2.putText(frame, '1: RGB Mode', (18, 18 + 18), font_face, font_size, font_color)
        cv2.putText(frame, '2: Binary Mode', (18, 18 + 2 * 18), font_face, font_size, font_color)
        cv2.putText(frame, '3: New Gesture', (18, 18 + 3 * 18), font_face, font_size, font_color)
        cv2.putText(frame, '0: Quit', (18, 18 + 4 * 18), font_face, font_size, font_color)
        
    def mainloop(self):
        while True:
            flag, frame = self.__cap.read()
            frame = cv2.flip(frame, 2)
        
            GestureCapturer.show_capturer_options(frame)
            key = cv2.waitKey(1) & 0xFF
        
            if key is ord('0'):
                self.quit_capturer()
                return
        
            cv2.imshow('Video Capturer', frame)
            
    def quit_capturer(self):
        self.__cap.release()
        cv2.destroyAllWindows()

效果也很不错:

需要注意的是,这个界面必须按0退出,点击右上角是不行的。(原因就在mainloop里面,欢迎各位自行修改代码)。

加入录制区域和录制功能

现在我们的主界面已经完成了,但是还只能显示摄像头捕获的画面,我们要为它加上拍摄功能。

在主界面框出录制区域

没有必要录制整个摄像头的画面,主要在于:

    1. 整个摄像头的画面太大了,实际上在使用摄像头的过程中,手出现的位置并不是很大,而且保存很大的图片会提高后期训练的难度。(电脑吃不消啊
    2. 摄像头的分辨率并不统一,这会影响代码的可移植性。

因此要对录制画面进行裁剪。

首先我们要在录制的画面上框出录制区域,cv2提供了一个

cv2.rectangle()

函数,能直接帮助我们画出一个正方形。

GestureCapturer里面添加一个名为draw_saving_area的函数:

@staticmethod
def draw_saving_area(frame):
    cv2.rectangle(frame, (100, 100), (300, 300), (0, 255, 0))

并且在mainloop中调用它(只要在imshow之前进行处理即可)。

现在我们就画出了从(100, 100)到(300, 300)的线段为对角线的正方形,以后就以这个位置作为录制区域了。

处理录制的图像

现在我们框出了录制区域,为了方便,可以把录制区域单独拿出来显示在一个小窗口里,同时,添加显示RGB模式和Binary模式切换的功能。

继续添加函数:

@staticmethod
def binary_mask(frame):
    area = frame[100:300, 100:300]
    gray = cv2.cvtColor(area, cv2.COLOR_RGB2GRAY)
    blur = cv2.GaussianBlur(gray, (5, 5), 2)
    at = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)
    flag, bin = cv2.threshold(at, 70, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    return bin

这段代码可以说相当复杂,我们一条一条来看:

area = frame[100:300, 100:300]

是把我们要录制的区域扣了出来,这一步比较好理解(也就这一步比较好理解……)

gray = cv2.cvtColor(area, cv2.COLOR_RGB2GRAY)

cvtColor提供了一个颜色空间转换的API,指定第二个参数为COLOR_RGB2GRAY,很显然是将RGB空间上的色彩转换为灰度。

但是直接使用这样的转换是有问题的,所以引入了下面的处理方法:

高斯模糊

先说低通滤波器。低通滤波器主要的作用是降低图片的变化率,这样就能平滑并替代那些强度变化明显的区域。低通滤波器的算法可以是将每个像素替换为周围像素的均值。

但是有些时候,需要对一个像素的周围的像素给予更多的重视,这时候就需要重新分配权重,例如使用高斯函数来分配权重。

高斯模糊实际上也是一种低通滤波器。

blur = cv2.GaussianBlur(gray, (5, 5), 2)

其中,参数(5, 5)是高斯模糊矩阵(半径等参数),2是标准差,二者越大,图片的模糊程度越高。

自适应阀值二值化和固定阀值二值化

二值化操作是对一个灰度图片进行二值化处理,亦即转化为黑白两色。

固定阀值二值化是对单个像素超过阀值后进行的处理。

自适应阈值二值化函数根据图片一小块区域的值来计算对应区域的阈值,从而得到新的图片。

先简介一下固定阀值二值化:

固定阀值二值化处理有多种操作类型,cv2提供了:

  • cv2.THRESH_BINARY
  • cv2.THRESH_BINARY_INV
  • cv2.THRESH_TRUNC
  • cv2.THRESH_TOZERO
  • cv2.THRESH_TOZERO_INV

对应的具体效果如下图:

图片非原创,侵删

而自适应阀值二值化提供的方法更为可靠,它的参数是:

cv2.adaptiveThreshold(src, maxval, thresh_type, type, Block Size, C)
  • src: 输入图,只能输入单通道图像,通常来说为灰度图。
  • maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值。
  • thresh_type: 阈值的计算方法,包含以下2种类型:cv2.ADAPTIVE_THRESH_MEAN_C; cv2.ADAPTIVE_THRESH_GAUSSIAN_C.
  • type:二值化操作的类型,与固定阈值函数相同,包含以下5种类型: cv2.THRESH_BINARY; cv2.THRESH_BINARY_INV; cv2.THRESH_TRUNC; cv2.THRESH_TOZERO;cv2.THRESH_TOZERO_INV.
  • Block Size: 图片中分块的大小
  • C :阈值计算方法中的常数项

我们的代码使用了:

at = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)

进行了自适应阀值二值化。

OTSU:基于直方图的二值化

这又是一种二值化方法,简单来看,OTSU进行了这样的过程:

  1. 计算图像直方图
  2. 通过设定的阀值,将图像的像素分为两组。
  3. 分别计算两组内的偏移数,并把偏移数相加。
  4. 重复1-3的步骤,直到得到最小偏移数,其所对应的值即为结果阈值。

有关OTSU的介绍很多,这里就不在赘述了。

flag, bin = cv2.threshold(at, 70, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

最后就得到了二值化后的图像。

完成!

现在我们有了录制窗口,也有了二值化功能,还需要做的事情有:

  1. 单独显示截取的图片
  2. 命名和保存截取的图片

这两个都比较简单。

单独显示截取的区域

很简单,直接上代码:

def mainloop(self):
    is_binary_mode = False
    while True:
        ret, frame = self.__cap.read()
        frame = cv2.flip(frame, 2)
        
        GestureCapturer.show_capturer_options(frame)
        key = cv2.waitKey(1) & 0xFF
        
        if key is ord('0'):
            self.quit_capturer() 
            return
        
        if key is ord('1'):
            is_binary_mode = False
            
        if key is ord('2'):
            is_binary_mode = True
            
        GestureCapturer.draw_saving_area(frame)
        
        if is_binary_mode:
            binary_area = GestureCapturer.binary_mask(frame)
            cv2.imshow('Captured Area', binary_area)
            
        else:
            cv2.imshow('Captured Area', frame[100:300, 100:300])
            
        cv2.imshow('Video Capturer', frame)

设置保存功能

保存功能是当用户按下‘3’键的时候,提示用户选择一个名称,然后保存100帧。

保存功能使用

cv2.imwrite()

即可。(目前在我这里有些bug,保存的部分会另外介绍)

最后贴一下今天全部的代码吧

import cv2
import time
import os


class GestureCapturer(object):

    def __init__(self):
        self.__cap = cv2.VideoCapture(0)
        self.__is_now_saving = False
        self.__save_count = 0
        self.__name = 'Default Name'

    @staticmethod
    def show_capturer_options(frame):
        font_face = cv2.FONT_HERSHEY_SIMPLEX
        font_size = 0.5
        font_color = (0, 255, 0)
        cv2.putText(frame, 'Options:', (18, 18), font_face, font_size, font_color)
        cv2.putText(frame, '1: RGB Mode', (18, 18 + 18), font_face, font_size, font_color)
        cv2.putText(frame, '2: Binary Mode', (18, 18 + 2 * 18), font_face, font_size, font_color)
        cv2.putText(frame, '3: New Gesture', (18, 18 + 3 * 18), font_face, font_size, font_color)
        cv2.putText(frame, '0: Quit', (18, 18 + 4 * 18), font_face, font_size, font_color)

    def mainloop(self):
        is_binary_mode = False
        self.__is_now_saving = False
        while True:
            ret, frame = self.__cap.read()
            frame = cv2.flip(frame, 2)

            GestureCapturer.show_capturer_options(frame)
            key = cv2.waitKey(1) & 0xFF

            if key is ord('0'):
                self.quit_capturer()
                return

            if key is ord('1'):
                is_binary_mode = False

            if key is ord('2'):
                is_binary_mode = True

            if key is ord('3'):
                self.__is_now_saving = True
                self.__name = input('Input gesture name:')
                self.__save_count = 0

            GestureCapturer.draw_saving_area(frame)

            if is_binary_mode:
                binary_area = GestureCapturer.binary_mask(frame)
                cv2.imshow('Captured Area', binary_area)
                if self.__is_now_saving:
                    self.save_image(self, binary_area, self.__name)

            else:
                cv2.imshow('Captured Area', frame[100:300, 100:300])
                if self.__is_now_saving:
                    self.save_image(self, frame[100:300, 100:300], self.__name)

            cv2.imshow('Video Capturer', frame)

    def quit_capturer(self):
        self.__cap.release()
        cv2.destroyAllWindows()

    @staticmethod
    def draw_saving_area(frame):
        cv2.rectangle(frame, (100, 100), (300, 300), (0, 255, 0))

    @staticmethod
    def binary_mask(frame):
        area = frame[100:300, 100:300]
        gray = cv2.cvtColor(area, cv2.COLOR_RGB2GRAY)
        blur = cv2.GaussianBlur(gray, (5, 5), 2)
        at = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)
        flag, bin = cv2.threshold(at, 70, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
        return bin

    @staticmethod
    def save_image(self, image, name):
        if (self.__save_count is 100):
            self.__save_count = 0
            self.__is_now_saving = False
            print('Done!')
            return

        else:
            print('Saving ' + name + ' for #' + str(self.__save_count))
            pic_name = name + str(self.__save_count)
            # 这里的路径需要注意
            path = 'C:\\Users\\LeonWong\\Desktop\\' + name + '\\' + pic_name + '.jpg'
            print('Path: ' + path)
            flag = cv2.imwrite(path, image)
            print('Saved? ' + str(flag))
            self.__save_count += 1
            time.sleep(0.1)


vc = GestureCapturer()
vc.mainloop()

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据