代碼地址以下:
http://www.demodashi.com/demo/12968.htmlhtml
本文中的手勢識別與控制功能主要採用 OpenCV 庫實現, OpenCV是一個基於BSD許可(開源)發行的跨平臺計算機視覺庫, 能夠運行在Linux, Windows, Android和Mac-OS操做系統上. 它輕量級並且高效---由一系列 C 函數和少許 C++ 類構成, 同時提供了Python, Ruby, MATLAB等語言的接口, 實現了圖像處理和計算機視覺方面的不少通用算法.python
本文主要使用了OpenCV的視頻採集, 圖像色域轉換, 顏色通道分割, 高斯濾波, OSTU自動閾值, 凸點檢測, 邊緣檢測, 餘弦定理計算手勢等功能.web
pip install opencv-python -i https://mirrors.ustc.edu.cn/pypi/web/simple
利用 -i 爲pip指令鏡像源, 這裏使用電子科技大學的源, 速度比官方源更快.算法
pip install numpy -i https://mirrors.ustc.edu.cn/pypi/web/simple
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 或者其餘的來選擇別的攝像頭。以後,你就能夠一幀一幀的捕獲視頻了。可是最後,別忘了中止捕獲視頻。chrome
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 .bash
*** BackgroundSubtractorMOG2*** 是一個以混合高斯模型爲基礎的前景/背景分割算法。它是 P.KadewTraKuPong和 R.Bowden 在 2001 年提出的。它使用 K(K=3 或 5)個高斯分佈混合對背景像素進行建模。使用這些顏色(在整個視頻中)存在時間的長短做爲混合的權重。背景的顏色通常持續的時間最長,並且更加靜止。一個像素怎麼會有分佈呢?在 x,y 平面上一個像素就是一個像素沒有分佈,可是咱們如今講的背景建模是基於時間序列的,所以每個像素點所在的位置在整個時間序列中就會有不少值,從而構成一個分佈。
在編寫代碼時,咱們須要使用函數:*** cv2.createBackgroundSubtractorMOG() *** 建立一個背景對象。這個函數有些可選參數,好比要進行建模場景的時間長度,高斯混合成分的數量,閾值等。將他們所有設置爲默認值。而後在整個視頻中咱們是須要使用 backgroundsubtractor.apply() 就能夠獲得前景的掩模了。網絡
BackgroundSubtractorMOG2 也是以高斯混合模型爲基礎的背景/前景分割算法。它是以 2004 年和 2006 年 Z.Zivkovic 的兩篇文章爲基礎的。這個算法的一個特色是它爲每個像素選擇一個合適數目的高斯分佈。(上一個方法中咱們使用是 K 高斯分佈)。這樣就會對因爲亮度等發生變化引發的場景變化產生更好的適應。
和前面同樣咱們須要建立一個背景對象。但在這裏咱們咱們能夠選擇是否檢測陰影。若是 detectShadows = True(默認值),它就會檢測並將影子標記出來,可是這樣作會下降處理速度。影子會被標記爲灰色。app
咱們這裏使用的就是 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大師代發,拒絕轉載,轉載須要做者受權