项目已经及上传github,需者自取。
https://github.com/grey-wood-wolf/Gesture-recognition-mouse-and-keyboard-control
完成人:李政廉 黄鑫杰 傅英伦
实现功能
实现左右手的手势识别,并非触摸控制鼠标的移动和点击,以及键盘上下按键的控制。
所用模型
本项目使用了MediaPipe中的MediaPipe Hands模型以及python的AutoPy GUI工具包。
MediaPipe是一款由 Google Research 开发并开源的多媒体机器学习模型应用框架。MediaPipe的核心框架由C++实现。
MediaPipe Hands为MediaPipe中的手部识别模型,它是一种高保真手和手指跟踪解决方案,采用机器学习 (ML) 从单个帧中推断出手的21个3D地标。
(1) Palm Detection Model(手掌检测模型)
手掌检测模型,对整个图像进行操作并返回一个定向的手部边界框。
使用非极大值抑制(NMS)算法来解决手部自遮挡的问题,使用bounding box来建模。
(2)Hand Landmark Model(手部地标模型)
手部地标模型,对手掌检测器定义的裁剪图像区域进行操作,并返回高保真3D手部关键点。
在训练该模型时,使用人工注释的大约30K个具有 21个3D 坐标的真实世界图像对模型进行训练。
检测手的集合,其中每只手表示为 21 个手部标志的列表,每个标志由x、y和z组成。x和y的范围由图像宽度和高度进行归一化处理为0-1。z表示以手腕深度为原点的地标深度,值越小,地标离相机越近。
AutoPy
AutoPy是一个简单跨平台的 Python GUI工具包,可以控制鼠标,键盘,匹配颜色和屏幕上的位图。
本项目主要使用的autopy方法
autopy.mouse.move()
通过x,y方向的坐标控制鼠标的移动。
autopy.mouse.toggle(None, True/False)
控制鼠标是否点击。值为True时控制鼠标点击,值为False时控制鼠标松开,默认为鼠标左键。
autopy.mouse.click(autopy.mouse.Button.RIGHT)
控制鼠标右键的点击。
autopy.key.toggle(autopy.key.Code.DOWN_ARROW, True/False, [])
控制键盘向下箭头的键入。
autopy.key.toggle(autopy.key.Code.UP_ARROW, True/False, [])
控制键盘向上箭头的键入。
实现流程
主函数
def findHands(self, img, draw=True)def findPosition(self, img, handNo=0, draw=True)def fingersUp(self)def findDistance(self, p1, p2, img, draw=True, r=15, t=3)
第一个函数可以检测手的存在,并且给出21个点位的原始数据。
第二个函数可以将21个点位的数据进行变化,最总得到在显示屏上映射的相对位置信息。
第三个函数可以根据节点的相对位置判断每每根手指的抬起和放下情况。
第四个函数可以得到两根手指末端的间距大小。
跟踪
对象检测和跟踪管道可以实现为一个MediaPipe图,它内部利用了一个对象检测子图、一个对象跟踪子图和一个渲染子图。
一般来说,对象检测子图(在内部执行ML模型推断)仅在请求时运行,例如以任意帧速率或由特定信号触发。更具体地说,在这个特殊的图中,一个PacketResampler计算单元在传入的视频帧被传递到对象检测子图之前,暂时对其进行0.5 fps的子采样。这个帧率可以配置为不同的选项在PacketResampler。
目标跟踪子图在每一帧上实时运行,跟踪被检测到的目标。它扩展了盒跟踪子图,增加了额外的功能:当新的检测到达时,它使用IoU(交叉Union)将当前跟踪的对象/盒与新的检测相关联,以删除过时或重复的目标框。
手部检测
位置处理
手指识别
距离判断
行为控制
模块调用关系
识别代码被分成了5个模块,分别为手部检测模块,位置处理模块,手指识别模块,距离判断模块,行为控制模块。分别实现,手部检测,位置处理,手指识别,距离判断,行为控制的功能。
其中的调用关系为这5个模块都在主函数中被调用,然后各个模块给出对应需要的数据。
如下图:
各模块的对应关系:
手部识别理模块会利用所使用的框架将图片中的手识别,并给出21个点位信息给位置处理模块。
位置处理模块会利用21个点位的信息我,算出屏幕映射位置,并传给手指识别模块和距离判断模块。
之后手指识别模块会利用映射位置判断手指的抬起判断结果,最终返回给行为控制模块。
距离判断模块会利用映射位置算出两个手指间的距离,并返回结果给行为控制模块。、
最终行为控制模块将根据判断,控制鼠标键盘,以及屏幕的显示。
关系如下图:
测试及评估结果
分析:因为利用轻量级的识别,可以实现较高帧率的显示,对于手的识别也成功率十分高,无论实在或明或暗,或者将手藏起来部分,也可以识别出来,并且显示点位,并且对于手势控制鼠标和键盘十分成功,并不会出现太明显的误判和未识别。交互控制过程很流畅,没有出现滞留和延时的情况,但对于手翻转后的手势判定,无法识别,于是我们利用位置信息进行判断,实现手翻面后的识别和控制。
分析:再加入判断后,右手反转或者左手控制也可以实现相对应的功能,没有出现控制判断错误的情况产生,并且,整个人机交互过程也十分的流畅和高效。
代码展示
手部检测和人机交互控制主程序代码
import cv2
import numpy as np
import HandTrackingModule as htm
import time
import autopy##########################
wCam, hCam = 640, 480
frameR = 100 # Frame Reduction
smoothening = 7
#########################pTime = 0
plocX, plocY = 0, 0
clocX, clocY = 0, 0cap = cv2.VideoCapture(0)
cap.set(3, wCam)
cap.set(4, hCam)
detector = htm.handDetector(maxHands=1)
wScr, hScr = autopy.screen.size()
# print(wScr, hScr)flag = 0
while True:# 1. Find hand Landmarkssuccess, img = cap.read()img = detector.findHands(img)lmList, bbox = detector.findPosition(img)# 2. Get the tip of the index and middle fingersif len(lmList) != 0:x1, y1 = lmList[8][1:]x2, y2 = lmList[12][1:]# print(x1, y1, x2, y2)cv2.rectangle(img, (frameR, frameR), (wCam - frameR, hCam - frameR),(255, 0, 255), 2)# 3. Check which fingers are upfingers = detector.fingersUp()if fingers:# print(fingers)# 4. Only Index Finger : Moving Modeif fingers[1] == 1 and fingers[2] == 0:# 5. Convert Coordinatesx3 = np.interp(x1, (frameR, wCam - frameR), (0, wScr))y3 = np.interp(y1, (frameR, hCam - frameR), (0, hScr))# 6. Smoothen ValuesclocX = plocX + (x3 - plocX) / smootheningclocY = plocY + (y3 - plocY) / smoothening# 7. Move Mouseautopy.mouse.move(wScr - clocX, clocY)cv2.circle(img, (x1, y1), 15, (255, 0, 255), cv2.FILLED)plocX, plocY = clocX, clocY# 8. Both Index and middle fingers are up : Clicking Modeif fingers[1] == 1 and fingers[2] == 1:# 9. Find distance between fingerslength, img, lineInfo = detector.findDistance(8, 12, img)if length < 40 and flag == 0:cv2.circle(img, (lineInfo[4], lineInfo[5]),15, (0, 255, 0), cv2.FILLED)autopy.mouse.toggle(None,True)flag = 1elif length >= 40 and flag == 1:cv2.circle(img, (lineInfo[4], lineInfo[5]),15, (0, 255, 0), cv2.FILLED)autopy.mouse.toggle(None,False)flag = 0if fingers[0] == 0 and fingers[1] == 0 and fingers[2] == 0:autopy.mouse.click(autopy.mouse.Button.RIGHT)if fingers[0] == 1 and fingers[1] == 0 and fingers[2] == 0:autopy.key.toggle(autopy.key.Code.DOWN_ARROW, True, [])autopy.key.toggle(autopy.key.Code.DOWN_ARROW, False, [])if fingers[0] == 1 and fingers[1] == 1 and fingers[2] == 1:autopy.key.toggle(autopy.key.Code.UP_ARROW, True, [])autopy.key.toggle(autopy.key.Code.UP_ARROW, False, [])# 11. Frame RatecTime = time.time()fps = 1 / (cTime - pTime)pTime = cTimecv2.putText(img, str(int(fps)), (20, 50), cv2.FONT_HERSHEY_PLAIN, 3,(255, 0, 0), 3)# 12. Displaycv2.imshow("Image", img)cv2.waitKey(1)
手部识别跟踪代码
import cv2
import mediapipe as mp
import time
import mathclass handDetector():def __init__(self, mode=False, maxHands=1, detectionCon=0.5, trackCon=0.5):self.mode = modeself.maxHands = maxHandsself.detectionCon = detectionConself.trackCon = trackConself.mpHands = mp.solutions.handsself.hands = self.mpHands.Hands(static_image_mode=self.mode,max_num_hands=self.maxHands,min_detection_confidence=self.detectionCon,min_tracking_confidence=self.trackCon)self.mpDraw = mp.solutions.drawing_utilsself.tipIds = [4, 8, 12, 16, 20]def findHands(self, img, draw=True):imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)self.results = self.hands.process(imgRGB)#print(self.results.multi_hand_landmarks)if self.results.multi_hand_landmarks:for handLms in self.results.multi_hand_landmarks:if draw:self.mpDraw.draw_landmarks(img, handLms, self.mpHands.HAND_CONNECTIONS)return imgdef findPosition(self, img, handNo=0, draw=True):xList = []yList = []bbox = []self.lmList = []if self.results.multi_hand_landmarks:myHand = self.results.multi_hand_landmarks[handNo]for id, lm in enumerate(myHand.landmark):# print(id, lm)h, w, c = img.shapecx, cy = int(lm.x * w), int(lm.y * h)xList.append(cx)yList.append(cy)self.lmList.append([id, cx, cy])if draw:cv2.circle(img, (cx, cy), 5, (255, 0, 255), cv2.FILLED)xmin, xmax = min(xList), max(xList)ymin, ymax = min(yList), max(yList)bbox = xmin, ymin, xmax, ymaxif draw:cv2.rectangle(img, (xmin - 20, ymin - 20), (xmax + 20, ymax + 20),(0, 255, 0), 2)return self.lmList, bboxdef fingersUp(self):fingers = []if self.lmList:if self.lmList[self.tipIds[0]][1] > self.lmList[self.tipIds[0] - 1][1] \and self.lmList[self.tipIds[1]][1] > self.lmList[self.tipIds[2]][1] \or self.lmList[self.tipIds[0]][1] < self.lmList[self.tipIds[0] - 1][1] \and self.lmList[self.tipIds[1]][1] < self.lmList[self.tipIds[2]][1]:fingers.append(1)else:fingers.append(0)# Fingersfor id in range(1, 5):if self.lmList[self.tipIds[id]][2] < self.lmList[self.tipIds[id] - 2][2]:fingers.append(1)else:fingers.append(0)# totalFingers = fingers.count(1)return fingersdef findDistance(self, p1, p2, img, draw=True, r=15, t=3):x1, y1 = self.lmList[p1][1:]x2, y2 = self.lmList[p2][1:]cx, cy = (x1 + x2) // 2, (y1 + y2) // 2if draw:cv2.line(img, (x1, y1), (x2, y2), (255, 0, 255), t)cv2.circle(img, (x1, y1), r, (255, 0, 255), cv2.FILLED)cv2.circle(img, (x2, y2), r, (255, 0, 255), cv2.FILLED)cv2.circle(img, (cx, cy), r, (0, 0, 255), cv2.FILLED)length = math.hypot(x2 - x1, y2 - y1)return length, img, [x1, y1, x2, y2, cx, cy]def main():pTime = 0cTime = 0cap = cv2.VideoCapture(0)detector = handDetector()while True:success, img = cap.read()img = detector.findHands(img)lmList, bbox = detector.findPosition(img)if len(lmList) != 0:print(lmList[4])cTime = time.time()fps = 1 / (cTime - pTime)pTime = cTimecv2.putText(img, str(int(fps)), (10, 70), cv2.FONT_HERSHEY_PLAIN, 3,(255, 0, 255), 3)cv2.imshow("Image", img)cv2.waitKey(1)if __name__ == "__main__":main()