"""
功能:手势虚拟拖拽
1、使用OpenCV读取摄像头视频流;
2、识别手掌关键点像素坐标;
3、根据食指和中指指尖的坐标,利用勾股定理计算距离,当距离较小且都落在矩形内,则触发拖拽(矩形变色);
4、矩形跟着手指动;
5、两指放开,则矩形停止移动
"""
# 导入OpenCV
import cv2
# 导入mediapipe
import mediapipe as mp
# 导入其他依赖包
import time
import math
# 方块管理类
class SquareManager:
def __init__(self, rect_width):
# 方框长度
self.rect_width = rect_width
# 方块list
self.square_count = 0
self.rect_left_x_list = []
self.rect_left_y_list = []
self.alpha_list = []
# 中指与矩形左上角点的距离
self.L1 = 0
self.L2 = 0
# 激活移动模式
self.drag_active = False
# 激活的方块ID
self.active_index = -1
# 创建一个方块,但是没有显示
def create(self, rect_left_x, rect_left_y, alpha=0.4):
self.rect_left_x_list.append(rect_left_x)
self.rect_left_y_list.append(rect_left_y)
self.alpha_list.append(alpha)
self.square_count += 1
# 更新位置
def display(self, class_obj):
for i in range(0, self.square_count):
x = self.rect_left_x_list[i]
y = self.rect_left_y_list[i]
alpha = self.alpha_list[i]
overlay = class_obj.image.copy()
if (i == self.active_index):
cv2.rectangle(overlay, (x, y), (x + self.rect_width, y + self.rect_width), (255, 0, 255), -1)
else:
cv2.rectangle(overlay, (x, y), (x + self.rect_width, y + self.rect_width), (255, 0, 0), -1)
# Following line overlays transparent rectangle over the self.image
class_obj.image = cv2.addWeighted(overlay, alpha, class_obj.image, 1 - alpha, 0)
# 判断落在哪个方块上,返回方块的ID
def checkOverlay(self, check_x, check_y):
for i in range(0, self.square_count):
x = self.rect_left_x_list[i]
y = self.rect_left_y_list[i]
if (x < check_x < (x + self.rect_width)) and (y < check_y < (y + self.rect_width)):
# 保存被激活的方块ID
self.active_index = i
return i
return -1
# 计算与指尖的距离
def setLen(self, check_x, check_y):
# 计算距离
self.L1 = check_x - self.rect_left_x_list[self.active_index]
self.L2 = check_y - self.rect_left_y_list[self.active_index]
# 更新方块
def updateSquare(self, new_x, new_y):
# print(self.rect_left_x_list[self.active_index])
self.rect_left_x_list[self.active_index] = new_x - self.L1
self.rect_left_y_list[self.active_index] = new_y - self.L2
# 识别控制类
class HandControlVolume:
def __init__(self):
# 初始化medialpipe
self.mp_drawing = mp.solutions.drawing_utils
self.mp_drawing_styles = mp.solutions.drawing_styles
self.mp_hands = mp.solutions.hands
# 中指与矩形左上角点的距离
self.L1 = 0
self.L2 = 0
# image实例,以便另一个类调用
self.image = None
# 主函数
def recognize(self):
# 计算刷新率
fpsTime = time.time()
print(fpsTime)
# OpenCV读取视频流
cap = cv2.VideoCapture(0)
# 视频分辨率
resize_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
resize_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(resize_w)
print(resize_h)
# 画面显示初始化参数
rect_percent_text = 0
# 初始化方块管理器
squareManager = SquareManager(150)
# 创建多个方块
for i in range(0, 5):
squareManager.create(200 * i + 20, 200, 0.6)
with self.mp_hands.Hands(min_detection_confidence=0.7,
min_tracking_confidence=0.5,
max_num_hands=2) as hands:
while cap.isOpened():
# 初始化矩形
success, self.image = cap.read()
self.image = cv2.resize(self.image, (resize_w, resize_h))
if not success:
print("空帧.")
continue
# 提高性能
self.image.flags.writeable = False
# 转为RGB
self.image = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
# 镜像
self.image = cv2.flip(self.image, 1)
# mediapipe模型处理
results = hands.process(self.image)
self.image.flags.writeable = True
self.image = cv2.cvtColor(self.image, cv2.COLOR_RGB2BGR)
# 判断是否有手掌
if results.multi_hand_landmarks:
# 遍历每个手掌
for hand_landmarks in results.multi_hand_landmarks:
# 在画面标注手指
self.mp_drawing.draw_landmarks(
self.image,
hand_landmarks,
self.mp_hands.HAND_CONNECTIONS,
self.mp_drawing_styles.get_default_hand_landmarks_style(),
self.mp_drawing_styles.get_default_hand_connections_style())
# 解析手指,存入各个手指坐标
landmark_list = []
# 用来存储手掌范围的矩形坐标
paw_x_list = []
paw_y_list = []
for landmark_id, finger_axis in enumerate(
hand_landmarks.landmark):
landmark_list.append([
landmark_id, finger_axis.x, finger_axis.y,
finger_axis.z
])
paw_x_list.append(finger_axis.x)
paw_y_list.append(finger_axis.y)
if landmark_list:
# 比例缩放到像素
ratio_x_to_pixel = lambda x: math.ceil(x * resize_w)
ratio_y_to_pixel = lambda y: math.ceil(y * resize_h)
# 设计手掌左上角、右下角坐标
paw_left_top_x, paw_right_bottom_x = map(ratio_x_to_pixel,
[min(paw_x_list), max(paw_x_list)])
paw_left_top_y, paw_right_bottom_y = map(ratio_y_to_pixel,
[min(paw_y_list), max(paw_y_list)])
# 给手掌画框框
cv2.rectangle(self.image, (paw_left_top_x - 30, paw_left_top_y - 30),
(paw_right_bottom_x + 30, paw_right_bottom_y + 30), (0, 255, 0), 2)
# 获取中指指尖坐标
middle_finger_tip = landmark_list[12]
middle_finger_tip_x = ratio_x_to_pixel(middle_finger_tip[1])
middle_finger_tip_y = ratio_y_to_pixel(middle_finger_tip[2])
# 获取食指指尖坐标
index_finger_tip = landmark_list[8]
index_finger_tip_x = ratio_x_to_pixel(index_finger_tip[1])
index_finger_tip_y = ratio_y_to_pixel(index_finger_tip[2])
# 中间点
between_finger_tip = (middle_finger_tip_x + index_finger_tip_x) // 2, (
middle_finger_tip_y + index_finger_tip_y) // 2
# print(middle_finger_tip_x)
thumb_finger_point = (middle_finger_tip_x, middle_finger_tip_y)
index_finger_point = (index_finger_tip_x, index_finger_tip_y)
# 画指尖2点
circle_func = lambda point: cv2.circle(self.image, point, 10, (255, 0, 255), -1)
self.image = circle_func(thumb_finger_point)
self.image = circle_func(index_finger_point)
self.image = circle_func(between_finger_tip)
# 画2点连线
self.image = cv2.line(self.image, thumb_finger_point, index_finger_point, (255, 0, 255), 5)
# 勾股定理计算长度
line_len = math.hypot((index_finger_tip_x - middle_finger_tip_x),
(index_finger_tip_y - middle_finger_tip_y))
# 将指尖距离映射到文字
rect_percent_text = math.ceil(line_len)
# 激活模式,需要让矩形跟随移动
if squareManager.drag_active:
# 更新方块
squareManager.updateSquare(between_finger_tip[0], between_finger_tip[1])
if (line_len > 100):
# 取消激活
squareManager.drag_active = False
squareManager.active_index = -1
elif (line_len < 100) and (squareManager.checkOverlay(between_finger_tip[0],
between_finger_tip[1]) != -1) and (
squareManager.drag_active == False):
# 激活
squareManager.drag_active = True
# 计算距离
squareManager.setLen(between_finger_tip[0], between_finger_tip[1])
# 显示方块,传入本实例,主要为了半透明的处理
squareManager.display(self)
# 显示距离
cv2.putText(self.image, "Distance:" + str(rect_percent_text), (10, 120), cv2.FONT_HERSHEY_PLAIN, 3,
(255, 0, 0), 3)
# 显示当前激活
cv2.putText(self.image, "Active:" + (
"None" if squareManager.active_index == -1 else str(squareManager.active_index)), (10, 170),
cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
# 显示刷新率FPS
cTime = time.time()
fps_text = 1 / (cTime - fpsTime)
fpsTime = cTime
cv2.putText(self.image, "FPS: " + str(int(fps_text)), (10, 70),
cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
# 显示画面
# self.image = cv2.resize(self.image, (resize_w//2, resize_h//2))
cv2.imshow('virtual drag and drop', self.image)
if cv2.waitKey(5) & 0xFF == 27:
break
cap.release()
# 开始程序
control = HandControlVolume()
control.recognize()
SquareManager
的Python类,初始化了一些属性。类是对象的模板,它定义了对象可以具有的属性(数据)和方法(操作数据的函数)。
下面是代码中每一部分的详细解释:
class SquareManager:
:这行代码定义了一个名为SquareManager
的新类。
def __init__(self, rect_width):
:这行代码定义了类的初始化方法(也称为构造器)。当我们创建类的新实例时,这个方法会被调用。self
是对新创建的实例的引用,rect_width
是传入的参数,表示矩形的宽度。
self.rect_width = rect_width
:这行代码将传入的rect_width
赋值给实例属性self.rect_width
。
self.square_count = 0
:这行代码初始化square_count
属性为0,它可能用于跟踪管理的方块数量。
self.rect_left_x_list = []
,self.rect_left_y_list = []
,self.alpha_list = []
:这些代码初始化了几个列表,可能用于存储每个方块的左上角x、y坐标以及其透明度(alpha)。
self.L1 = 0
和self.L2 = 0
:这些代码初始化了L1
和L2
属性,可能用于存储与方块左上角的距离。
self.drag_active = False
:这行代码初始化了drag_active
属性,表示是否处于拖拽模式。
self.active_index = -1
:这行代码初始化了active_index
属性,可能用于存储当前活动(被选中或被拖拽)的方块的索引。
总的来说,SquareManager
类的构造器接受一个参数rect_width
,并初始化了一些属性,这些属性将在后续的方法中用于方块的管理。
这个方法用于创建新的方块。这个方法接受三个参数:rect_left_x
(方块左上角的x坐标)、rect_left_y
(方块左上角的y坐标)和alpha
(方块的透明度)。这个方法将这些参数添加到对应的列表中,并增加square_count
属性的值。
下面是对每行代码的详细解释:
def create(self, rect_left_x, rect_left_y, alpha=0.4):
这行代码定义了create
方法。这个方法接受三个参数,self
参数是对实例自身的引用,其他两个参数rect_left_x
和rect_left_y
用于指定新方块的位置,alpha
参数用于指定新方块的透明度,默认值为0.4。
self.rect_left_x_list.append(rect_left_x)
这行代码将rect_left_x
添加到rect_left_x_list
列表的末尾。
self.rect_left_y_list.append(rect_left_y)
这行代码将rect_left_y
添加到rect_left_y_list
列表的末尾。
self.alpha_list.append(alpha)
这行代码将alpha
添加到alpha_list
列表的末尾。
self.square_count += 1
这行代码将square_count
属性的值加1,以反映已经添加了一个新的方块。
总的来说,这个create
方法用于创建新的方块并将其加入到管理的方块列表中,但并没有实现方块的显示,显示方块可能会在其他方法中实现。
它用于在图像上绘制并显示所有的方块。该方法接收一个参数class_obj
,它应该是包含一个图像属性image
的对象。
以下是对代码每一行的详细解释:
def display(self, class_obj):
定义了名为display
的方法。它接受一个参数class_obj
。
for i in range(0, self.square_count):
这是一个循环,将对所有已经创建的方块进行遍历。
x = self.rect_left_x_list[i]
,y = self.rect_left_y_list[i]
,alpha = self.alpha_list[i]
这几行将当前方块的左上角x、y坐标和透明度赋值给x
,y
和alpha
变量。
overlay = class_obj.image.copy()
这行代码创建了class_obj.image
的一个副本,命名为overlay
。该副本将被用于绘制方块,以保持原始图像不变。
if (i == self.active_index):
这行代码检查当前的方块是否是激活的方块。
cv2.rectangle(overlay, (x, y), (x + self.rect_width, y + self.rect_width), (255, 0, 255), -1)
如果当前方块是激活的方块,这行代码将在overlay
上用紫色绘制一个矩形。矩形的左上角坐标是(x, y)
,右下角坐标是(x + self.rect_width, y + self.rect_width)
,颜色是紫色(在BGR颜色空间中,紫色表示为(255, 0, 255)),并且填充整个矩形(线宽为-1表示填充)。
cv2.rectangle(overlay, (x, y), (x + self.rect_width, y + self.rect_width), (255, 0, 0), -1)
如果当前方块不是激活的方块,这行代码将在overlay
上用蓝色绘制一个矩形。
class_obj.image = cv2.addWeighted(overlay, alpha, class_obj.image, 1 - alpha, 0)
这行代码将透明的矩形叠加到原始图像上。cv2.addWeighted
是OpenCV的一个函数,它可以计算两个图像的加权和。在这里,它将overlay
(带有透明方块的图像)和class_obj.image
(原始图像)进行加权叠加,权重分别为alpha
和1 - alpha
,然后将结果赋值给class_obj.image
。
总的来说,这个display
方法会在class_obj.image
上绘制所有的方块,如果方块被激活,颜色
这个方法的目的是检查一个给定的点(check_x, check_y)
是否在任何一个方块内。如果该点在一个方块内,该方法会返回那个方块的索引,并将self.active_index
设置为那个索引。如果该点不在任何方块内,该方法将返回-1。
下面是对每一行代码的详细解释:
def checkOverlay(self, check_x, check_y):
定义了名为checkOverlay
的方法,该方法接收两个参数:要检查的点的x和y坐标。
for i in range(0, self.square_count):
这是一个循环,将对所有的方块进行遍历。
x = self.rect_left_x_list[i]
,y = self.rect_left_y_list[i]
这两行将当前方块的左上角x和y坐标赋值给x
和y
变量。
if (x < check_x < (x + self.rect_width)) and (y < check_y < (y + self.rect_width)):
这是一个条件判断,如果check_x
在x
和x + self.rect_width
之间,并且check_y
在y
和y + self.rect_width
之间,说明给定的点在当前的方块内。
self.active_index = i
如果给定的点在当前的方块内,这行代码将把self.active_index
设置为当前方块的索引。
return i
如果给定的点在当前的方块内,这行代码将返回当前方块的索引。
return -1
如果给定的点不在任何方块内,这行代码将返回-1。
总的来说,checkOverlay
方法用于确定一个给定的点是否在方块内,如果在,返回那个方块的索引并设置self.active_index
,如果不在,返回-1。
这个方法的作用是计算给定点(check_x, check_y)
到当前活动方块的左上角的水平距离L1
和垂直距离L2
。
这是每一行代码的详细解释:
def setLen(self, check_x, check_y):
定义了名为setLen
的方法,该方法接收两个参数:一个是要检查的点的x坐标,另一个是y坐标。
self.L1 = check_x - self.rect_left_x_list[self.active_index]
这行代码计算了给定点的x坐标check_x
与当前活动方块左上角的x坐标之间的差,也就是在水平方向上的距离,并将结果赋值给self.L1
。
self.L2 = check_y - self.rect_left_y_list[self.active_index]
这行代码计算了给定点的y坐标check_y
与当前活动方块左上角的y坐标之间的差,也就是在垂直方向上的距离,并将结果赋值给self.L2
。
总的来说,setLen
方法用于计算一个给定的点(check_x, check_y)
到当前活动方块左上角的水平距离和垂直距离,并将这两个距离存储在self.L1
和self.L2
中。这可能是为了在之后移动方块时,保持鼠标(或者其他指针设备)在方块上的相对位置不变。
这个方法的作用是更新当前活动方块的位置。
这是每一行代码的详细解释:
def updateSquare(self, new_x, new_y):
定义了名为updateSquare
的方法,该方法接收两个参数:新的x坐标new_x
和新的y坐标new_y
。
self.rect_left_x_list[self.active_index] = new_x - self.L1
这行代码将新的x坐标减去self.L1
(即该点与方块左上角的水平距离),然后将结果赋值给当前活动方块的左上角x坐标。这意味着当前活动方块将移动到新的x坐标减去self.L1
的位置。
self.rect_left_y_list[self.active_index] = new_y - self.L2
这行代码将新的y坐标减去self.L2
(即该点与方块左上角的垂直距离),然后将结果赋值给当前活动方块的左上角y坐标。这意味着当前活动方块将移动到新的y坐标减去self.L2
的位置。
总的来说,updateSquare
方法用于移动当前活动的方块。新的方块位置将根据新的x和y坐标以及与方块左上角的水平和垂直距离进行计算。这样可以保证在移动方块时,鼠标(或其他指针设备)在方块上的相对位置不变。
一个类可以被看作是创建对象的蓝图。在这个类中定义了许多属性和方法,这些属性和方法将成为由这个类创建的对象的一部分。
下面是对这段代码的具体解释:
class HandControlVolume:
这行代码定义了一个名为HandControlVolume
的类。
def __init__(self):
这行代码定义了类的初始化方法,也就是构造函数。当你创建一个新的HandControlVolume
对象时,这个方法会被调用。
self.mp_drawing = mp.solutions.drawing_utils
这行代码创建了一个属性mp_drawing
,并将其赋值为mp.solutions.drawing_utils
。这将使得HandControlVolume
对象能够使用MediaPipe的绘图工具。
self.mp_drawing_styles = mp.solutions.drawing_styles
这行代码创建了一个属性mp_drawing_styles
,并将其赋值为mp.solutions.drawing_styles
。这将使得HandControlVolume
对象能够使用MediaPipe的绘图样式。
self.mp_hands = mp.solutions.hands
这行代码创建了一个属性mp_hands
,并将其赋值为mp.solutions.hands
。这将使得HandControlVolume
对象能够使用MediaPipe的手部识别工具。
self.L1 = 0
和 self.L2 = 0
这两行代码创建了两个属性L1
和L2
,并将它们初始化为0。这可能用于存储中指与矩形左上角点的距离。
self.image = None
这行代码创建了一个属性image
,并将其初始化为None
。这个属性可能被用来存储一个图像实例,以便另一个类调用。
总的来说,这个类HandControlVolume
的主要目的是提供一个方便的方式来使用MediaPipe的手部识别和绘图功能,同时还提供了一些额外的属性来支持其他功能。