Python手勢識別與控制
概述
本文中的手勢識別與控制功能主要采用 OpenCV 庫實現, OpenCV是一個基於BSD許可(開源)發行的跨平台計算機視覺庫, 可以運行在Linux, Windows, Android和Mac-OS操作系統上. 它輕量級而且高效---由一系列 C 函數和少量 C++ 類構成, 同時提供了Python, Ruby, MATLAB等語言的接口, 實現了圖像處理和計算機視覺方面的很多通用算法.
本文主要使用了OpenCV的視頻采集, 圖像色域轉換, 顏色通道分割, 高斯濾波, OSTU自動閾值, 凸點檢測, 邊緣檢測, 余弦定理計算手勢等功能.
准備工作
安裝 Python-OpenCV 庫
pip install opencv-python -i https://mirrors.ustc.edu.cn/pypi/web/simple
利用 -i 為pip指令鏡像源, 這里使用電子科技大學的源, 速度比官方源更快.
安裝 Numpy 科學計算庫
pip install numpy -i https://mirrors.ustc.edu.cn/pypi/web/simple
安裝 PyAutogui 庫
pip install pyautogui -i https://mirrors.ustc.edu.cn/pypi/web/simple
圖像的基本操作
import numpy as np
import cv2
imname = "6358772.jpg"
# 讀入圖像
'''
使用函數 cv2.imread() 讀入圖像。這幅圖像應該在此程序的工作路徑,或者給函數提供完整路徑.
警告:就算圖像的路徑是錯的,OpenCV 也不會提醒你的,但是當你使用命令print(img)時得到的結果是None。
'''
img = cv2.imread(imname, cv2.IMREAD_COLOR)
'''
imread函數的第一個參數是要打開的圖像的名稱(帶路徑)
第二個參數是告訴函數應該如何讀取這幅圖片. 其中
cv2.IMREAD_COLOR 表示讀入一副彩色圖像, alpha 通道被忽略, 默認值
cv2.IMREAD_ANYCOLOR 表示讀入一副彩色圖像
cv2.IMREAD_GRAYSCALE 表示讀入一副灰度圖像
cv2.IMREAD_UNCHANGED 表示讀入一幅圖像,並且包括圖像的 alpha 通道
'''
# 顯示圖像
'''
使用函數 cv2.imshow() 顯示圖像。窗口會自動調整為圖像大小。第一個參數是窗口的名字,
其次才是我們的圖像。你可以創建多個窗口,只要你喜歡,但是必須給他們不同的名字.
'''
cv2.imshow("image", img) # "image" 參數為圖像顯示窗口的標題, img是待顯示的圖像數據
cv2.waitKey(0) #等待鍵盤輸入,參數表示等待時間,單位毫秒.0表示無限期等待
cv2.destroyAllWindows() # 銷毀所有cv創建的窗口
# 也可以銷毀指定窗口:
#cv2.destroyWindow("image") # 刪除窗口標題為"image"的窗口
# 保存圖像
'''
使用函數 cv2.imwrite() 來保存一個圖像。首先需要一個文件名,之后才是你要保存的圖像。
保存的圖片的格式由后綴名決定.
'''
#cv2.imwrite(imname + "01.png", img)
cv2.imwrite(imname + "01.jpg", img)
攝像頭數據采集
我們經常需要使用攝像頭捕獲實時圖像。OpenCV 為這中應用提供了一個非常簡單的接口。讓我們使用攝像頭來捕獲一段視頻,並把它轉換成灰度視頻顯示出來。從這個簡單的任務開始吧。
為了獲取視頻,你應該創建一個 VideoCapture 對象。他的參數可以是設備的索引號,或者是一個視頻文件。設備索引號就是在指定要使用的攝像頭。一般的筆記本電腦都有內置攝像頭。所以參數就是 0。你可以通過設置成 1 或者其他的來選擇別的攝像頭。之后,你就可以一幀一幀的捕獲視頻了。但是最后,別忘了停止捕獲視頻。
cap.read() 返回一個布爾值(True/False)。如果幀讀取的是正確的,就是 True。所以最后你可以通過檢查他的返回值來查看視頻文件是否已經到了結尾。有時 cap 可能不能成功的初始化攝像頭設備。這種情況下上面的代碼會報錯。你可以使用cap.isOpened(),來檢查是否成功初始化了。如果返回值是True,那就沒有問題。否則就要使用函數 cap.open()。
class Capture(object):
'''
Capture object
:param deviceID: device ID of your capture device, defaults to 0
:type deviceID: :obj:`int`
Example
>>> import pygr
>>> cap = pygr.Capture()
'''
def __init__(self, deviceID=0): # ID為0, 表示從默認的攝像頭讀取視頻數據
self.deviceID = deviceID
self.capture = cv2.VideoCapture(self.deviceID) #
def read(self):
_, frame = self.capture.read() # 調用默認攝像頭捕獲一幀圖像
frame = cv2.bilateralFilter(frame, 5, 50, 100) # 對捕獲到的圖像進行雙邊濾波
image = Image.fromarray(frame) # 轉換圖像數據格式
return image
視頻數據的處理
為了更准確的識別視頻數據中包含的手勢信息, 需要對視頻數據進行預處理, 包括背景減除, 人體皮膚偵測.
背景減除
在很多基礎應用中背景檢出都是一個非常重要的步驟。例如顧客統計,使用一個靜態攝像頭來記錄進入和離開房間的人數,或者是交通攝像頭,需要提取交通工具的信息等。在所有的這些例子中,首先要將人或車單獨提取出來。
技術上來說,我們需要從靜止的背景中提取移動的前景。如果你有一張背景(僅有背景不含前景)圖像,比如沒有顧客的房間,沒有交通工具的道路等,那就好辦了。我們只需要在新的圖像中減去背景就可以得到前景對象了。
但是在大多數情況下,我們沒有這樣的(背景)圖像,所以我們需要從我們有的圖像中提取背景。如果圖像中的交通工具還有影子的話,那這個工作就更難了,因為影子也在移動,僅僅使用減法會把影子也當成前景。真是一件很復雜的事情。為了實現這個目的科學家們已經提出了幾種算法。OpenCV 中已經包含了其中三種比較容易使用的方法: BackgroundSubtractorMOG , BackgroundSubtractorMOG2 , BackgroundSubtractorGMG。這里我們使用的是 BackgroundSubtractorMOG2 .
BackgroundSubtractorMOG 和 BackgroundSubtractorMOG2
*** BackgroundSubtractorMOG2*** 是一個以混合高斯模型為基礎的前景/背景分割算法。它是 P.KadewTraKuPong和 R.Bowden 在 2001 年提出的。它使用 K(K=3 或 5)個高斯分布混合對背景像素進行建模。使用這些顏色(在整個視頻中)存在時間的長短作為混合的權重。背景的顏色一般持續的時間最長,而且更加靜止。一個像素怎么會有分布呢?在 x,y 平面上一個像素就是一個像素沒有分布,但是我們現在講的背景建模是基於時間序列的,因此每一個像素點所在的位置在整個時間序列中就會有很多值,從而構成一個分布。
在編寫代碼時,我們需要使用函數:*** cv2.createBackgroundSubtractorMOG() *** 創建一個背景對象。這個函數有些可選參數,比如要進行建模場景的時間長度,高斯混合成分的數量,閾值等。將他們全部設置為默認值。然后在整個視頻中我們是需要使用 backgroundsubtractor.apply() 就可以得到前景的掩模了。
BackgroundSubtractorMOG2 也是以高斯混合模型為基礎的背景/前景分割算法。它是以 2004 年和 2006 年 Z.Zivkovic 的兩篇文章為基礎的。這個算法的一個特點是它為每一個像素選擇一個合適數目的高斯分布。(上一個方法中我們使用是 K 高斯分布)。這樣就會對由於亮度等發生變化引起的場景變化產生更好的適應。
和前面一樣我們需要創建一個背景對象。但在這里我們我們可以選擇是否檢測陰影。如果 detectShadows = True(默認值),它就會檢測並將影子標記出來,但是這樣做會降低處理速度。影子會被標記為灰色。
我們這里使用的就是 BackgroundSubtractorMOG2 算法, 詳細代碼如下:
# 移除視頻數據的背景噪聲
def _remove_background(frame):
fgbg = cv2.createBackgroundSubtractorMOG2() # 利用BackgroundSubtractorMOG2算法消除背景
# fgmask = bgModel.apply(frame)
fgmask = fgbg.apply(frame)
# kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
# res = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)
kernel = np.ones((3, 3), np.uint8)
fgmask = cv2.erode(fgmask, kernel, iterations=1)
res = cv2.bitwise_and(frame, frame, mask=fgmask)
return res
# 視頻數據的人體皮膚檢測
def _bodyskin_detetc(frame):
# 膚色檢測: YCrCb之Cr分量 + OTSU二值化
ycrcb = cv2.cvtColor(frame, cv2.COLOR_BGR2YCrCb) # 分解為YUV圖像,得到CR分量
(_, cr, _) = cv2.split(ycrcb)
cr1 = cv2.GaussianBlur(cr, (5, 5), 0) # 高斯濾波
_, skin = cv2.threshold(cr1, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # OTSU圖像二值化
return skin
手勢檢測與識別
利用opencv提供的 convexityDefects 凹點檢測函數檢測圖像凹陷的點, 然后利用, 然后根據凹陷點中的 (開始點, 結束點, 遠點)的坐標, 利用余弦定理計算兩根手指之間的夾角, 其必為銳角, 根據銳角的個數判別手勢.
其中,銳角個數為0 ,表示 手勢是 拳頭 或 一,
銳角個數為0 ,表示 手勢是 拳頭 或 一,
銳角個數為1 ,表示 手勢是 剪刀
銳角個數為2 ,表示 手勢是 三,
銳角個數為3 ,表示 手勢是 四,
銳角個數為4 ,表示 手勢是 布
凹陷點計算
對象上的任何凹陷都被成為凸缺陷。OpenCV 中有一個函數 cv.convexityDefect() 可以幫助我們找到凸缺
陷. 函數調用如下. 如果要查找凸缺陷,在使用函數 cv2.convexHull 找凸包時,參數returnPoints一定要是 False.
hull = cv2.convexHull(cnt, returnPoints = False)
defects = cv2.convexityDefects(cnt,hull)
它會返回一個數組,其中每一行包含的值是 [起點,終點,最遠的點,到最遠點的近似距離]。
# 檢測圖像中的凸點(手指)個數
def _get_contours(array):
# 利用findContours檢測圖像中的輪廓, 其中返回值contours包含了圖像中所有輪廓的坐標點
_, contours, _ = cv2.findContours(array, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
return contours
# 根據圖像中凹凸點中的 (開始點, 結束點, 遠點)的坐標, 利用余弦定理計算兩根手指之間的夾角, 其必為銳角, 根據銳角的個數判別手勢.
def _get_defects_count(array, contour, defects, verbose = False):
ndefects = 0
for i in range(defects.shape[0]):
s,e,f,_ = defects[i,0]
beg = tuple(contour[s][0])
end = tuple(contour[e][0])
far = tuple(contour[f][0])
a = _get_eucledian_distance(beg, end)
b = _get_eucledian_distance(beg, far)
c = _get_eucledian_distance(end, far)
angle = math.acos((b ** 2 + c ** 2 - a ** 2) / (2 * b * c)) # * 57
if angle <= math.pi/2 :#90:
ndefects = ndefects + 1
if verbose:
cv2.circle(array, far, 3, _COLOR_RED, -1)
if verbose:
cv2.line(array, beg, end, _COLOR_RED, 1)
return array, ndefects
def grdetect(array, verbose = False):
event = Event(Event.NONE)
copy = array.copy()
array = _remove_background(array) # 移除背景, add by wnavy
thresh = _bodyskin_detetc(array)
contours = _get_contours(thresh.copy()) # 計算圖像的輪廓
largecont = max(contours, key = lambda contour: cv2.contourArea(contour))
hull = cv2.convexHull(largecont, returnPoints = False) # 計算輪廓的凸點
defects = cv2.convexityDefects(largecont, hull) # 計算輪廓的凹點
if defects is not None:
# 利用凹陷點坐標, 根據余弦定理計算圖像中銳角個數
copy, ndefects = _get_defects_count(copy, largecont, defects, verbose = verbose)
# 根據銳角個數判斷手勢, 會有一定的誤差
if ndefects == 0:
event.setType(Event.ZERO)
elif ndefects == 1:
event.setType(Event.TWO)
elif ndefects == 2:
event.setType(Event.THREE)
elif ndefects == 3:
event.setType(Event.FOUR)
elif ndefects == 4:
event.setType(Event.FIVE)
return event
手勢控制
只要能夠檢測到手勢, 相應的控制就簡單很多了, 這里主要模擬手勢控制web頁面滾動, 手勢 五 表示向下滾動, 手勢 四 表示向上滾動. 只要明白了手勢識別的核心原理, 更復雜的手勢控制就完全看個人想象, 比如網絡上有很多人實現 手勢控制chrome瀏覽器中的那只小恐龍, 我也嘗試過, 但是控制起來難度太大了, 還有的是實現手勢播放, 切換歌曲. 只要有python, 這一切都很簡單.
# imports - app imports
import pygr
import cv2
import time
from base import Keycode, Event
import pyautogui
if __name__ == '__main__':
pad = pygr.PyGR(size = (480, 320), verbose = True)
#pad = pygr.PyGR(size = (480, 320))
pad.show()
while cv2.waitKey(10) not in [Keycode.ESCAPE, Keycode.Q, Keycode.q]:
event = pad.get_event()
print("Event:", event.type, "Tip:", event.tip)
if event.type == Event.FIVE : # 手勢五, 向下滾動
pyautogui.scroll(-30) # scroll down 10 "clicks"
elif event.type == Event.FOUR :# 手勢四, 向上滾動
pyautogui.scroll(30) # scroll up 10 "clicks"
time.sleep(0.1)
補充說明
為了demo簡潔明了, 本文中省去了代碼里的無關細節和調試部分, 比如程序運行時的實時識別和標記窗口部分的代碼沒有介紹, 但是代碼中都有詳細注釋. 另外,對於不想研究細節只想 拿來主義 的同學, 只需要仿照上面 main中引入相關模塊, 然后創建並運行PyGR模塊, 不需要任何多余的操作.
運行方法
python main.py
程序運行截圖
手勢識別
手勢控制
項目內文件截圖
Python手勢識別與控制
注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權