В данной статье расскажу про простой Drag-and-Drop на Python+OpenCV.

Немного теории, ведь в наше время без нее никуда????

Drag-and-drop () — способ оперирования элементами интерфейса в интерфейсах пользователя
(как графическим, так и текстовым, где элементы GUI реализованы при помощи псевдографики)
при помощи манипулятора «мышь» или сенсорного экрана.

Простыми словами:

Drag-and-Drop - это возможность захватить мышью элемент и перенести его.

Идея заключается в перемещении созданных квадратов на экране жестами руки. Так как нам потребуется как-то воспроизводить клик, то представим, что кликом будет служить соединение указательного и среднего пальцев. Теперь можно писать код.

Тут, я разберу весь код с нуля, но для полной ясности лучше прочитать мои предыдущие статьи????

Все исходники можно найти на моем Github.

Установим все необходимые библиотеки:

pip install opencv-python
pip install numpy
pip install cvzone

В первую очередь создаем новый файл `HandTrackingModule.py`

import cv2
import mediapipe as mp
import time
import math

class handDetector():
	def __init__(self, mode=False, maxHands=2, modelComplexity=1, detectionCon=0.5, trackCon=0.5):
		self.mode = mode
		self.maxHands = maxHands
		self.modelComplexity = modelComplexity
		self.detectionCon = detectionCon
		self.trackCon = trackCon

		self.mpHands = mp.solutions.hands
		self.hands = self.mpHands.Hands(self.mode, self.maxHands, self.modelComplexity, self.detectionCon, self.trackCon)
		self.mpDraw = mp.solutions.drawing_utils
		self.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(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 img

	def 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.shape
				cx, cy = int(lm.x*w), int(lm.y*h)
				xList.append(cx)
				yList.append(cy)
				#print(id, cx, 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, ymax

			if draw:
				cv2.rectangle(img, (bbox[0]-20, bbox[1]-20), (bbox[2]+20, bbox[3]+20), (0, 255, 0), 2)
		return self.lmList, bbox

	def findDistance(self, p1, p2, img, draw=True):
		x1, y1 = self.lmList[p1][1], self.lmList[p1][2]
		x2, y2 = self.lmList[p2][1], self.lmList[p2][2]
		cx, cy = (x1+x2)//2, (y1+y2)//2

		if draw:
			cv2.circle(img, (x1,y1), 15, (255,0,255), cv2.FILLED)
			cv2.circle(img, (x2,y2), 15, (255,0,255), cv2.FILLED)
			cv2.line(img, (x1,y1), (x2,y2), (255,0,255), 3)
			cv2.circle(img, (cx,cy), 15, (255,0,255), cv2.FILLED)

		length = math.hypot(x2-x1, y2-y1)
		return length, img, [x1, y1, x2, y2, cx, cy]

	def fingersUp(self):
		fingers = []

		# Thumb
		if self.lmList[self.tipIds[0]][1] < self.lmList[self.tipIds[0]-1][1]:
			fingers.append(1)
		else:
			fingers.append(0)

		# 4 Fingers
		for 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)
		return fingers

def main():
	pTime = 0
	cTime = 0
	cap = cv2.VideoCapture(0)
	detector = handDetector()
	while True:
		success, img = cap.read()
		img = detector.findHands(img)
		lmList = detector.findPosition(img)
		if len(lmList) != 0:
			print(lmList[1])

		cTime = time.time()
		fps = 1. / (cTime - pTime)
		pTime = cTime

		cv2.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()

Данный класс мы использовали в Управление громкостью звука жестами на Python.

Можем создать новый файл, назовем его main.py и будем писать всю логику.

Импортируем библиотеки:

import cv2
import HandTrackingModule as htm
import cvzone
import numpy as np

Запускаем камеру и присваиваем глобальным переменным значения:

cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)
detector = htm.handDetector(detectionCon=0.8)
colorR = (255, 0, 255)

cx, cy, w, h = 100, 100, 200, 200

При подключении камеры могут возникнуть ошибки, поменяйте 0 из cap=cv2.VideoCapture(0) на 1 или 2.

cx, cy - координаты левой верхней точки нашего четырехугольника
w, h - ширина и длина (в нашем случае будет квадрат)

Напишем класс, для более удобной дальнейшей работы с блоками:

class DragAndDrogRectangle():
    def __init__(self, posCenter, size=[200, 200]):
        self.posCenter = posCenter
        self.size = size

    def update(self, cursor):
        cx, cy = self.posCenter
        w, h = self.size

        # If the index finger tip is in rectangle region
        if cx - w//2 < cursor[0] < cx + w//2 and cy - h//2 < cursor[1] < cy + h//2:
            self.posCenter = cursor
rectList = []
for x in range(5):
    rectList.append(DragAndDrogRectangle([x*250+150, 150]))

Таким образом у нас будет не один квадрат, а 5. При необходимости можно добавлять или удалять их.

Запускаем бесконечный цикл (можно поставить остановку при необходимости):

while True:
    success, img = cap.read()
    img = cv2.flip(img, 1)
    img = detector.findHands(img)
    lmList, _ = detector.findPosition(img)

Рассмотри теперь изображение, где показаны "Hand Land Marks":

Так как ранее было установлено, что кликом будет соединение указательного и среднего пальцев, то значит нам нужно считывать расстояние между точками 8 и 12.

if len(lmList) != 0:
        length, _, _ = detector.findDistance(8, 12, img, draw=False)
        if length < 40:
            cursor = lmList[8][1:] # index finger tip landmark
            # call the update
            for rect in rectList:
                rect.update(cursor)
    
    # Draw
    imgNew = np.zeros_like(img, np.uint8)
    for rect in rectList:
        cx, cy = rect.posCenter
        w, h = rect.size
        cv2.rectangle(imgNew, (cx - w//2, cy - h//2), (cx + w//2, cy + h//2), colorR, cv2.FILLED)
        cvzone.cornerRect(imgNew, (cx - w//2, cy - h//2, w, h), 20, rt=0)

    out = img.copy()
    alpha = 0.1
    mask = imgNew.astype(bool)
    out[mask] = cv2.addWeighted(img, alpha, imgNew, 1-alpha, 0)[mask]

    cv2.imshow("Image", out)
    cv2.waitKey(1)

Если клик не работает и вы не можете перемещать блоки, тогда замените 40 на большее значение, например на 50, и попробуйте снова:

if length < 40

Эта проблема возникает из-за того, что у каждого человека руки разные и расстояние между пальцами отличается????.

Если хотите поменять клик или указатель на другие пальцы, то на изображении "Hand Land Marks" можно выбрать те точки, которые будут за это отвечать. После чего необходимо поменять эти строки:

length, _, _ = detector.findDistance(8, 12, img, draw=False)

Вместо 8 и 12, вписываем выбранные точки, которые будут отвечать за клик.

cursor = lmList[8][1:] # index finger tip landmark

Вместо 8, вписываем ту точку, которая будет служить указателем (представим, что это курсор мыши).

Для перемещения квадратов (блоков) нужно удерживать клик, т.е. два пальца должны быть соединены все время, пока мы хотим перемещать тот или иной объект.

Запускаем программу и тестируем????:

Ура! Все работает!

Вместо квадратов вы сможете добавить необходимые блоки и использовать Drag-and-Drop для работы с ними =)

Комментарии (15)


  1. shushu
    01.08.2022 15:23

    Мне кажется, или для левшей это работать не будет? :)


    1. Pavel_Dat Автор
      01.08.2022 15:49

      Ошибаешься) Работает также отлично ????


  1. m1n64
    01.08.2022 15:31

    Будущее уже наступило? Жду полноценных ОС, которые полностью управляются жестами рук


    1. Pavel_Dat Автор
      01.08.2022 15:32

      Скоро выпущу????


      1. 0x86
        01.08.2022 18:05
        +1

        FingerOS, где будет находиться этот самый Finger при использовании такой OS не уточняется.


  1. 1fid
    01.08.2022 18:05

    Было бы занятнее измерять расстояния между фалангами пальцев и подушечками на ладони. Перетаскивать настоящим сжатием руки ✋✊


    1. Pavel_Dat Автор
      01.08.2022 18:05

      Тут можно сделать как только душе угодно


  1. VPryadchenko
    01.08.2022 22:24

    А как работает на неоднородном фоне?


    1. Pavel_Dat Автор
      01.08.2022 22:29

      Спокойно. Он работает с положениями объектов, поэтому фон не важен


      1. VPryadchenko
        02.08.2022 07:57

        Это то понятно, весь вопрос в том, как конкретно данный детектор ключевых точек кисти работает в "не идеальных" условиях. Демо на фоне стены натолкнуло на мысль, что может быть и не очень хорошо)


      1. VPryadchenko
        02.08.2022 08:01

        А вообще класс! Вот у меня всё руки не доходили попробовать сделать "мышку" на детекторе кисти - трекать ее положение камерой на столе и детектировать движения пальцами. Теперь может всё-таки дойдут после Ваших демо)


        1. Pavel_Dat Автор
          02.08.2022 08:22

          Жду. Будет интересно почитать!


  1. petlen1
    01.08.2022 23:35

    А если у меня 6 пальцев?


  1. danilovmy
    02.08.2022 00:26

    Я, вероятно, с другой планеты, у нас захват предметов для переноса происходит между большим и указательным пальцами. Не совсем понял, где код исправить? Точнее говоря, что можно добавить, потому как не срабатывает только расстояние между фалангами.


    1. Pavel_Dat Автор
      02.08.2022 01:12
      +1

      Если хотите поменять на другие пальцы, то на изображении "Hand Land Marks" можно выбрать те точки, которые будут за это отвечать. После чего необходимо поменять эти строки:
      1) length, _, _ = detector.findDistance(8, 12, img, draw=False)
      Вместо 8 и 12, вписываем выбранные точки
      2) cursor = lmList[8][1:] # index finger tip landmark
      Вместо 8, вписываем ту точку, которая будет служить указателем (представим, что это курсор мыши)