用计算机视觉和神经网络控制鼠标
本文的目标是借助手势控制鼠标。所谓“控制”,我的意思是:平移方向、点击、双击、鼠标滚轮和拖放。

计算机视觉一直吸引着我的注意。我有想法摆脱我的办公桌,以更身体参与的方式与我的电脑互动,而计算机视觉似乎是一个可以实现这一目标的领域,而且所需的硬件最少。既然我已经渴望加入计算机应用中最令人兴奋的领域之一,我决定用它来做点有用的事情。
这里的目标是借助手势控制鼠标。
所谓“控制”,我的意思是:平移方向、点击、双击、鼠标滚轮和拖放。
- 系统必须看到我的手
- 解码命令/动作/手势——这包括清理噪声和错误
- 将此控制转移到鼠标
- 如果有任何学习过程,我希望通过强化学习训练一个自定义模型
1、OpenCV
没有 OpenCV 的计算机视觉是不完整的。
OpenCV 是世界上最大的计算机视觉库。它是开源的,包含超过 2500 种算法,并且自 2000 年 6 月以来由非营利性开源视觉基金会运营。
OpenCV 包含许多常用于处理视觉数据的算法和工具集。
使用 OpenCV,使 Python 代码通过摄像头查看图像非常简单:
import cv2
camera = cv2.VideoCapture(0) # 或者 cv2.VideoCapture(0, cv2.CAP_DSHOW) 用于 windows
if camera.isOpened():
# 从摄像头读取信息
success, frame = camera.read()
camera.release()py
帧是一个表示以网格形式接收的图像的 numpy 数组。虽然图像值看起来像只有宽度和高度的二维信息,但从数据的角度来看,这个二维帧为网格中的每个像素包含 3 个值(红、绿和蓝),这实际上使其成为一个三维数据对象。
OpenCV 使用 BGR 格式而不是 RGB。这个细微的信息在渲染和将数据传输到依赖 RGB 格式的其他系统时是有用的。
为了持续地从摄像头流中读取数据,我们可以设置一个循环并使用 opencv 将帧显示到窗口中。
import numpy as np
import cv2
cam = cv2.VideoCapture(0)
while cap.isOpened(): # 循环
success, frame = cam.read() # 读取帧
# 在名为 Frame 的窗口中显示帧
cv2.imshow('Frame', frame)
# 一种退出循环的方法
if cv2.waitKey(1) & 0xFF == ord('q') or \
cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
break
# 正确清除资源
cap.release()
cv2.destroyAllWindows()
我们现在能够从摄像头流中获取数据。
接下来我们应该研究如何处理这些数据以对其进行智能操作。
2、神经网络模型
深度学习是涉及训练人工神经网络层以高效预测值的技术领域。该领域在 2020 年代呈指数增长,可能性是难以想象的。这种突然的增长归因于多项研究,其结果被公开给公众,使得广泛使用变得容易。
训练这样的神经网络模型需要大量的数据。
尽管数据、研究和架构对所有人都是公开的,但大型模型的训练在小型机器上仍然是一个挑战。
这也是大多数机器学习或人工智能项目依赖预训练模型的原因之一。
神经网络似乎是检测和跟踪我的手的最佳选择。我决定寻找可以帮助我从摄像头画面中检测手部动作的模型,并发现 Google 的 MediaPipe 非常符合我的需求。
MediaPipe 库有助于估计手部骨骼关节的位置,并返回坐标(称为关键点)。
import mediapipe as mp
# 设置手部和工具
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
# 设置预训练 AI 模型
mp_hands = mp.solutions.hands
handyman = mp_hands.Hands(
min_detection_confidence=.8,
min_tracking_confidence=.5)
# 处理图像帧
result = handyman.process(rgb_frame)
if result.multi_hand_landmarks: # 检查是否检测到手部关键点
for hand_lms in result.multi_hand_landmarks: # 取出每只手
# 在帧中绘制圆圈和线条
mp_drawing.draw_landmarks(
rgb_frame, hand_lms, mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style())

将 MediaPipe 过程插入循环中,我们现在能够观察摄像头画面,检测手部关节的像素位置。现在我们应该把注意力转向将这些点转换为我们感兴趣的操作的控制信号。
3、使用 PyAutoGui 控制
PyAutoGui 是一个帮助从 Python 代码控制鼠标和键盘操作的库。我们将使用这个库根据我们构建的逻辑来控制鼠标。PyAutoGui 有以下我们感兴趣的函数:
move
按照一定像素移动鼠标。moveTo
将鼠标移动到屏幕上的特定坐标。scroll
滚动一定距离。mouseDown
按下鼠标按钮,对于长按很有用。mouseUp
点击后释放按钮。doubleClick
执行双击。
MediaPipe 已经为我检测了骨骼的位置,所以我可以从它那里获取关节的位置。
为了根据中指尖移动鼠标,让我们做一些数学计算。
考虑视频流中的两个帧 A 和 B。假设我在帧 A 中的指尖位置是 (x1, y1),在帧 B 中是 (x2, y2)。
我的指尖移动的像素数 (delta_x, delta_y) 可以计算为 (x2-x1, y2-y1)。
调用 pyautogui 来移动鼠标这个 delta 会将鼠标移动到那个方向。
为了方便使用,我编写了一个 Python 函数,它接受之前的地标和当前的地标来移动鼠标。我们可以在循环中使用从处理后的帧中获得的地标来调用这个函数。
import mediapipe as mp
mp_hands = mp.solutions.hands
lms = mp.solutions.hands.HandLandmark
def mouse_control(prev_lms, curr_lms):
# 获取手部坐标,假设至少检测到一只手
curr_index = curr_lms.multi_hand_landmarks[0].landmark[lms.MIDDLE_FINGER_TIP]
last_index = prev_lms.multi_hand_landmarks[0].landmark[lms.MIDDLE_FINGER_TIP]
# 计算差值
delta_x = curr_index.x - last_index.x
delta_y = curr_index.y - last_index.y
# 移动鼠标
pyautogui.move(delta_x, delta_y)
我要指出我遇到的这些错误、原因和解决方法。
- 返回的地标是归一化的值。这意味着我们通常收到的值范围是 (0.0–1.0],而不是 (0–1080] 或适当的屏幕分辨率的绝对像素值。为了解决这个问题,我决定将视频的当前帧传递给函数,并使用它的宽度和高度来将我的差值缩放到更好的值。这不是最好的解决方案,但它是一个进步。
...
h, w, _ = frame.shape
delta_x = w * (curr_index.x - last_index.x)
delta_y = h * (curr_index.y - last_index.y)
- 前置摄像头的视频翻转。视频必须翻转以确保手部运动是直观的。在读取后立即水平翻转帧可以解决这个问题。
success, frame = cam.read()
frame = cv2.flip(frame, 1)
- 多只手导致鼠标跳动。如果第二只手进入屏幕,第一只手的控制就会转移到新手上。此外,当两只手的位置相距较远时,这看起来像是从一个位置到另一个位置的即时跳跃,导致鼠标四处飞舞。这不是理想的。
这是因为我使用列表中第一个手的索引来控制鼠标。MediaPipe 似乎将新手放在数组的开头。这导致跟踪的食指从一个手指跳到新手指。
# 错误
curr_index = curr_lms.multi_hand_landmarks[0].landmark[lms.MIDDLE_FINGER_TIP]
last_index = prev_lms.multi_hand_landmarks[0].landmark[lms.MIDDLE_FINGER_TIP]
# 修复
curr_index = curr_lms.multi_hand_landmarks[-1].landmark[lms.MIDDLE_FINGER_TIP]
last_index = prev_lms.multi_hand_landmarks[-1].landmark[lms.MIDDLE_FINGER_TIP]
在提取检查退出序列和将帧渲染到窗口的功能后,更新后的循环现在如下所示。
# 缓存的手部数据
cached_hands = None
cam = cv2.VideoCapture(0)
while cam.isOpened():
# 观察
success, frame = cam.read()
frame = cv2.flip(frame, 1) # 水平翻转
# 处理手部
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 因为 MediaPipe 需要一个 rgb 帧
hands_found = handyman.process(rgb_frame)
if hands_found.multi_hand_landmarks:
render_hands_to_frame(frame, hands_found)
mouse_control(frame, cached_hands, hands_found)
cached_hands = hands_found # 使用后更新缓存
else:
# 如果在帧中未找到手,则清除缓存
cached_hands = None
cv2.imshow(WINDOW_NAME, frame)
if check_escape():
break
通过这种方式,我们成功地使用中指控制鼠标。
现在,如果发现拇指尖和无名指尖足够接近,我们将让脚本点击鼠标。
“足够接近”是什么意思?拇指尖和无名指尖之间的距离与拇指的最后一个关节相比。这提供了一种相对距离检查,不受像素距离的影响。这种方法的一个缺点是当位置重叠时,可能会触发错误检测,但这种情况很少见。
4、更多控制
我添加了全局布尔变量 IS_MOUSE_CLICKED
来跟踪状态,并修改了 mouse_control
方法以处理点击。
...
t1 = curr_lms.multi_hand_landmarks[-1].landmark[lms.THUMB_TIP]
t2 = curr_lms.multi_hand_landmarks[-1].landmark[lms.THUMB_IP]
r1 = curr_lms.multi_hand_landmarks[-1].landmark[lms.RING_FINGER_TIP]
t1_coord = (int(w * t1.x), int(h * t1.y))
r1_coord = (int(w * r1.x), int(h * r1.y))
t1_t2 = ((t1.x - t2.x) ** 2 + (t1.y - t2.y) ** 2) ** .5 # 拇指尖到拇指关节
t1_r1 = ((t1.x - r1.x) ** 2 + (t1.y - r1.y) ** 2) ** .5 # 拇指尖到无名指尖
# 假设:小于拇指关节大小的距离是有意的点击
if t1_r1 < t1_t2:
if not IS_MOUSE_CLICKED:
IS_MOUSE_CLICKED = True
pyautogui.mouseDown()
elif IS_MOUSE_CLICKED:
IS_MOUSE_CLICKED = False
pyautogui.mouseUp()
当不需要时,鼠标仍然追踪我们的手,这很烦人。必须有一个锁定机制来激活追踪。为此,我们可以简单地复制鼠标点击逻辑来实现锁定。我修改了代码,让它跟踪食指尖和中指尖之间的距离。当食指和中指靠近时,鼠标会跟随我的手。否则,它保持不动。
添加类似的距离检查以触发按钮和滚动功能最终有助于实现更多控制。
按下回车键或点击以全尺寸查看图片!
在 我的 github 上查看完整代码。
5、改进
有很多改进的空间。唯一的问题是,这是否是推动性能前进的正确方法,或者我们应该尝试另一种方法。
- 我的例子中使用了预训练模型来检测手部位置。控制代码仍然是硬编码的。我们可以通过实施自己的训练策略来进一步推进,教脚本识别手势。这将是有机的。
- 在低光条件下,检测变得不稳定,数值开始跳动。平均之前缓存帧的位置是一种平滑和减少故障的方法。
- 当关节位置重叠时可能会触发错误检测。
- 添加更多的距离检查会使系统脆弱,这取决于模型性能和光照条件。克服这一点的最佳方法是使用改进的模型和清晰的硬件。数据增强也可以探索。
原文链接:Mouse Control with Computer Vision and Neural Networks
汇智网翻译整理,转载请标明出处
