如何用python代码实现虚拟拖拽

"""
功能:手势虚拟拖拽
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

SquareManager的Python类,初始化了一些属性。类是对象的模板,它定义了对象可以具有的属性(数据)和方法(操作数据的函数)。

下面是代码中每一部分的详细解释:

  1. class SquareManager::这行代码定义了一个名为SquareManager的新类。

  2. def __init__(self, rect_width)::这行代码定义了类的初始化方法(也称为构造器)。当我们创建类的新实例时,这个方法会被调用。self是对新创建的实例的引用,rect_width是传入的参数,表示矩形的宽度。

  3. self.rect_width = rect_width:这行代码将传入的rect_width赋值给实例属性self.rect_width

  4. self.square_count = 0:这行代码初始化square_count属性为0,它可能用于跟踪管理的方块数量。

  5. self.rect_left_x_list = []self.rect_left_y_list = []self.alpha_list = []:这些代码初始化了几个列表,可能用于存储每个方块的左上角x、y坐标以及其透明度(alpha)。

  6. self.L1 = 0self.L2 = 0:这些代码初始化了L1L2属性,可能用于存储与方块左上角的距离。

  7. self.drag_active = False:这行代码初始化了drag_active属性,表示是否处于拖拽模式。

  8. self.active_index = -1:这行代码初始化了active_index属性,可能用于存储当前活动(被选中或被拖拽)的方块的索引。

总的来说,SquareManager类的构造器接受一个参数rect_width,并初始化了一些属性,这些属性将在后续的方法中用于方块的管理。

create

这个方法用于创建新的方块。这个方法接受三个参数:rect_left_x(方块左上角的x坐标)、rect_left_y(方块左上角的y坐标)和alpha(方块的透明度)。这个方法将这些参数添加到对应的列表中,并增加square_count属性的值。

下面是对每行代码的详细解释:

  1. def create(self, rect_left_x, rect_left_y, alpha=0.4): 这行代码定义了create方法。这个方法接受三个参数,self参数是对实例自身的引用,其他两个参数rect_left_xrect_left_y用于指定新方块的位置,alpha参数用于指定新方块的透明度,默认值为0.4。

  2. self.rect_left_x_list.append(rect_left_x) 这行代码将rect_left_x添加到rect_left_x_list列表的末尾。

  3. self.rect_left_y_list.append(rect_left_y) 这行代码将rect_left_y添加到rect_left_y_list列表的末尾。

  4. self.alpha_list.append(alpha) 这行代码将alpha添加到alpha_list列表的末尾。

  5. self.square_count += 1 这行代码将square_count属性的值加1,以反映已经添加了一个新的方块。

总的来说,这个create方法用于创建新的方块并将其加入到管理的方块列表中,但并没有实现方块的显示,显示方块可能会在其他方法中实现。

display

它用于在图像上绘制并显示所有的方块。该方法接收一个参数class_obj,它应该是包含一个图像属性image的对象。

以下是对代码每一行的详细解释:

  1. def display(self, class_obj): 定义了名为display的方法。它接受一个参数class_obj

  2. for i in range(0, self.square_count): 这是一个循环,将对所有已经创建的方块进行遍历。

  3. x = self.rect_left_x_list[i]y = self.rect_left_y_list[i]alpha = self.alpha_list[i] 这几行将当前方块的左上角x、y坐标和透明度赋值给xyalpha变量。

  4. overlay = class_obj.image.copy() 这行代码创建了class_obj.image的一个副本,命名为overlay。该副本将被用于绘制方块,以保持原始图像不变。

  5. if (i == self.active_index): 这行代码检查当前的方块是否是激活的方块。

  6. 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表示填充)。

  7. cv2.rectangle(overlay, (x, y), (x + self.rect_width, y + self.rect_width), (255, 0, 0), -1) 如果当前方块不是激活的方块,这行代码将在overlay上用蓝色绘制一个矩形。

  8. class_obj.image = cv2.addWeighted(overlay, alpha, class_obj.image, 1 - alpha, 0) 这行代码将透明的矩形叠加到原始图像上。cv2.addWeighted是OpenCV的一个函数,它可以计算两个图像的加权和。在这里,它将overlay(带有透明方块的图像)和class_obj.image(原始图像)进行加权叠加,权重分别为alpha1 - alpha,然后将结果赋值给class_obj.image

总的来说,这个display方法会在class_obj.image上绘制所有的方块,如果方块被激活,颜色

checkOverlay

这个方法的目的是检查一个给定的点(check_x, check_y)是否在任何一个方块内。如果该点在一个方块内,该方法会返回那个方块的索引,并将self.active_index设置为那个索引。如果该点不在任何方块内,该方法将返回-1。

下面是对每一行代码的详细解释:

  1. def checkOverlay(self, check_x, check_y): 定义了名为checkOverlay的方法,该方法接收两个参数:要检查的点的x和y坐标。

  2. for i in range(0, self.square_count): 这是一个循环,将对所有的方块进行遍历。

  3. x = self.rect_left_x_list[i]y = self.rect_left_y_list[i] 这两行将当前方块的左上角x和y坐标赋值给xy变量。

  4. if (x < check_x < (x + self.rect_width)) and (y < check_y < (y + self.rect_width)): 这是一个条件判断,如果check_xxx + self.rect_width之间,并且check_yyy + self.rect_width之间,说明给定的点在当前的方块内。

  5. self.active_index = i 如果给定的点在当前的方块内,这行代码将把self.active_index设置为当前方块的索引。

  6. return i 如果给定的点在当前的方块内,这行代码将返回当前方块的索引。

  7. return -1 如果给定的点不在任何方块内,这行代码将返回-1。

总的来说,checkOverlay方法用于确定一个给定的点是否在方块内,如果在,返回那个方块的索引并设置self.active_index,如果不在,返回-1。

setLen

这个方法的作用是计算给定点(check_x, check_y)到当前活动方块的左上角的水平距离L1和垂直距离L2

这是每一行代码的详细解释:

  1. def setLen(self, check_x, check_y): 定义了名为setLen的方法,该方法接收两个参数:一个是要检查的点的x坐标,另一个是y坐标。

  2. self.L1 = check_x - self.rect_left_x_list[self.active_index] 这行代码计算了给定点的x坐标check_x与当前活动方块左上角的x坐标之间的差,也就是在水平方向上的距离,并将结果赋值给self.L1

  3. self.L2 = check_y - self.rect_left_y_list[self.active_index] 这行代码计算了给定点的y坐标check_y与当前活动方块左上角的y坐标之间的差,也就是在垂直方向上的距离,并将结果赋值给self.L2

总的来说,setLen方法用于计算一个给定的点(check_x, check_y)到当前活动方块左上角的水平距离和垂直距离,并将这两个距离存储在self.L1self.L2中。这可能是为了在之后移动方块时,保持鼠标(或者其他指针设备)在方块上的相对位置不变。

updateSquare

这个方法的作用是更新当前活动方块的位置。

这是每一行代码的详细解释:

  1. def updateSquare(self, new_x, new_y): 定义了名为updateSquare的方法,该方法接收两个参数:新的x坐标new_x和新的y坐标new_y

  2. self.rect_left_x_list[self.active_index] = new_x - self.L1 这行代码将新的x坐标减去self.L1(即该点与方块左上角的水平距离),然后将结果赋值给当前活动方块的左上角x坐标。这意味着当前活动方块将移动到新的x坐标减去self.L1的位置。

  3. self.rect_left_y_list[self.active_index] = new_y - self.L2 这行代码将新的y坐标减去self.L2(即该点与方块左上角的垂直距离),然后将结果赋值给当前活动方块的左上角y坐标。这意味着当前活动方块将移动到新的y坐标减去self.L2的位置。

总的来说,updateSquare方法用于移动当前活动的方块。新的方块位置将根据新的x和y坐标以及与方块左上角的水平和垂直距离进行计算。这样可以保证在移动方块时,鼠标(或其他指针设备)在方块上的相对位置不变。

HandControlVolume

一个类可以被看作是创建对象的蓝图。在这个类中定义了许多属性和方法,这些属性和方法将成为由这个类创建的对象的一部分。

下面是对这段代码的具体解释:

  1. class HandControlVolume: 这行代码定义了一个名为HandControlVolume的类。

  2. def __init__(self): 这行代码定义了类的初始化方法,也就是构造函数。当你创建一个新的HandControlVolume对象时,这个方法会被调用。

  3. self.mp_drawing = mp.solutions.drawing_utils 这行代码创建了一个属性mp_drawing,并将其赋值为mp.solutions.drawing_utils。这将使得HandControlVolume对象能够使用MediaPipe的绘图工具。

  4. self.mp_drawing_styles = mp.solutions.drawing_styles 这行代码创建了一个属性mp_drawing_styles,并将其赋值为mp.solutions.drawing_styles。这将使得HandControlVolume对象能够使用MediaPipe的绘图样式。

  5. self.mp_hands = mp.solutions.hands 这行代码创建了一个属性mp_hands,并将其赋值为mp.solutions.hands。这将使得HandControlVolume对象能够使用MediaPipe的手部识别工具。

  6. self.L1 = 0self.L2 = 0 这两行代码创建了两个属性L1L2,并将它们初始化为0。这可能用于存储中指与矩形左上角点的距离。

  7. self.image = None 这行代码创建了一个属性image,并将其初始化为None。这个属性可能被用来存储一个图像实例,以便另一个类调用。

总的来说,这个类HandControlVolume的主要目的是提供一个方便的方式来使用MediaPipe的手部识别和绘图功能,同时还提供了一些额外的属性来支持其他功能。

你可能感兴趣的:(python,opencv,计算机视觉)