树莓派,mediapipe,Picamera2利用舵机云台追踪人手(PID控制)

一、项目目标

追踪人手大拇指指尖:
当人手移动时,摄像头通过控制两个伺服电机(分别是偏航和俯仰)把大拇指指尖放到视界的中心位置,本文采用了PID控制伺服电机

  • Mediapipe Hand简介

MediaPipe 手部标志任务可检测图像中手部的标志。 您可以使用此任务来定位手的关键点并在其上渲染视觉效果。 该任务使用机器学习(ML)模型作为静态数据或连续流对图像数据进行操作,并输出图像坐标中的手部标志、世界坐标中的手部标志以及多个检测到的手的惯用手(左/右手)。
树莓派,mediapipe,Picamera2利用舵机云台追踪人手(PID控制)_第1张图片

二、 需要准备的软、硬件

  1. Raspiberry Pi 4b
  2. 两个SG90 180度舵机(注意舵机的角度,最好是180度且带限位的,切勿选360度舵机)
  3. 二自由度舵机云台(如下图)
  4. Raspiberry CSI 摄像头
  5. mediapipe库, 安装方法可以参照此链接
    组装后的效果:
    组装后的效果

三、具体步骤

  1. 创建“hand_tracking_PID.py”文件,代码如下,我在本文中追踪的是大拇指指尖,如果你想追踪其它部位,只须将fingerID参数设置成你想追踪的数字即可。具体数字分布如下图。

#-*- coding: UTF-8 -*-	
# 调用必需库
#hand_tracking_PID.py
from multiprocessing import Manager
from multiprocessing import Process
from handobj import HandObj
from pid import PID
from servo import Servo
import signal
import time
import sys
import cv2
import mediapipe as mp
from picamera2 import Picamera2


# 定义舵机
pan=Servo(pin=19)
tilt=Servo(pin=16)

#定义图像尺寸
dispW=1280
dispH=720

# 定义手指ID
fingerID=4

# 键盘终止函数
def signal_handler(sig, frame):
    # 输出状态信息
    print("[INFO] You pressed `ctrl + c`! Exiting...")

    # 关闭舵机
    pan.stop()
    tilt.stop()

    # 退出
    sys.exit()

def hand_obj(objX,objY,centerX,centerY):
    # ctrl+c退出进程
    signal.signal(signal.SIGINT, signal_handler)

    # 启动视频流并缓冲
    print("[INFO] waiting for camera to warm up...")
    cv2.startWindowThread()
    picam2 = Picamera2()
    preview_config = picam2.create_preview_configuration(main={"size": (dispW, dispH),"format":"RGB888"})
    picam2.configure(preview_config)
    picam2.start()
    time.sleep(2.0)

    #初始化手掌对象探测器
    hand=HandObj(fingerID)

    #进入循环
    while True:
        # 从视频流抓取图像并旋转
        frame = picam2.capture_array()
        frame = cv2.flip(frame, 1)

        # 找到图像中心
        (H, W) = frame.shape[:2]
        centerX.value = W // 2
        centerY.value = H // 2

        # 画出图像中心点
        cv2.circle(frame, (centerX.value, centerY.value), 5, (0, 0, 255), -1)

        # 找到手指对象点
        objectLoc = hand.update(frame, (centerX.value, centerY.value))
        ((objX.value, objY.value), handlms) = objectLoc
        # 画出手指关注的对象点,这是里前面定义的ID:4,即大拇指指尖
        if handlms is not None:      
            cv2.circle(frame, (objX.value, objY.value), 15, (255, 0, 255), cv2.FILLED)
        cv2.imshow('Hand', frame)
        cv2.waitKey(1)

def pid_process(output, p, i, d, objCoord, centerCoord):
    # ctrl+c退出进程
    signal.signal(signal.SIGINT, signal_handler)
    
     # 创建一个PID类的对象并初始化
    p = PID(p.value, i.value, d.value)
    p.initialize()

    # 进入循环
    while True:
        # 计算误差
        error = centerCoord.value - objCoord.value
        # 更新输出值,当error小于50时,误差设为0,以避免云台不停运行。
        if abs(error) < 50:
            error = 0
        output.value = p.update(error)

def set_servos(panAngle, tiltAngle):
    # ctrl+c退出进程
    signal.signal(signal.SIGINT, signal_handler)

    #进入循环
    while True:
        # 偏角变号
        yaw = -1 * panAngle.value
        pitch = -1 * tiltAngle.value

        # 设置舵机角度。
        pan.set_angle(yaw)
        tilt.set_angle(pitch)

# 启动主程序
if __name__ == "__main__":

    # 启动多进程变量管理
    with Manager() as manager:  # 相当于manager=Manager(),with as 语句操作上下文管理器(context manager),它能够帮助我们自动分配并且释放资源。
        # 舵机角度置零
        pan.set_angle(0)
        tilt.set_angle(0)

        # 为图像中心坐标赋初值
        centerX = manager.Value("i", 0)  # "i"即为整型integer
        centerY = manager.Value("i", 0)

        # 为人脸中心坐标赋初值
        objX = manager.Value("i", 0)
        objY = manager.Value("i", 0)

        # panAngle和tiltAngle分别是两个舵机的PID控制输出量
        panAngle = manager.Value("i", 0)
        tiltAngle = manager.Value("i", 0)

       # 设置一级舵机的PID参数
        panP = manager.Value("f", 0.015)  # "f"即为浮点型float
        panI = manager.Value("f", 0.01)
        panD = manager.Value("f", 0.0008)

        # 设置二级舵机的PID参数
        tiltP = manager.Value("f", 0.025)
        tiltI = manager.Value("f", 0.01)
        tiltD = manager.Value("f", 0.008)

        # 创建4个独立进程
        # 1. objectCenter  - 探测人脸
        # 2. panning       - 对一级舵机进行PID控制,控制偏航角
        # 3. tilting       - 对二级舵机进行PID控制,控制俯仰角
        # 4. setServos     - 根据PID控制的输出驱动舵机

        processObjectCenter = Process(
            target=hand_obj, args=(objX, objY, centerX, centerY))
        processPanning = Process(target=pid_process, args=(
            panAngle, panP, panI, panD, objX, centerX))
        processTilting = Process(target=pid_process, args=(
            tiltAngle, tiltP, tiltI, tiltD, objY, centerY))
        processSetServos = Process(
            target=set_servos, args=(panAngle, tiltAngle))

        # 开启4个进程
        processObjectCenter.start()
        processPanning.start()
        processTilting.start()
        processSetServos.start()

        # 添加4个进程
        processObjectCenter.join()
        processPanning.join()
        processTilting.join()
        processSetServos.join()

  1. 创建“handobj.py”,代码如下:
#handobj.py
#-*- coding: UTF-8 -*-
# 调用必需库
import mediapipe as mp

class HandObj:
	def __init__(self,fingerID):
		# 初始化手掌关键点坐标
		self.myHands=mp.solutions.hands
		# 初始化手掌关键点坐标和手掌关键点连接情况
		self.hands=self.myHands.Hands()
		# 初始化手掌关键点绘制库
		self.mpDraw=mp.solutions.drawing_utils
		# 初始化手掌关键点ID
		self.fingerID=fingerID


	def update(self, frame, frameCenter):
		# 处理视频流
		results = self.hands.process(frame)
		if results.multi_hand_landmarks:
			for handLms in results.multi_hand_landmarks:
				# 绘制手掌关键点
				self.mpDraw.draw_landmarks(frame, handLms, self.myHands.HAND_CONNECTIONS)
				for id, lm in enumerate(handLms.landmark):
					h, w, c = frame.shape
					cx, cy = int(lm.x * w), int(lm.y * h)
					if id == self.fingerID:
						#绘制手掌关键点并返回手掌关键点坐标
						return ((cx, cy), handLms)
		return(frameCenter,None)
					

  1. 创建“pid.py”,代码如下:
#-*- coding: UTF-8 -*-
# 调用必需库
import time

class PID:
	def __init__(self, kP=1, kI=0, kD=0):
		# 初始化参数
		self.kP = kP
		self.kI = kI
		self.kD = kD

	def initialize(self):
		# 初始化当前时间和上一次计算的时间
		self.currTime = time.time()
		self.prevTime = self.currTime

		# 初始化上一次计算的误差
		self.prevError = 0

		# 初始化误差的比例值,积分值和微分值
		self.cP = 0
		self.cI = 0
		self.cD = 0

	def update(self, error, sleep=0.5):
		# 暂停
		time.sleep(sleep)

		# 获取当前时间并计算时间差
		self.currTime = time.time()
		deltaTime = self.currTime - self.prevTime

		# 计算误差的微分
		deltaError = error - self.prevError

		# 比例项
		self.cP = error

		# 积分项
		self.cI += error * deltaTime

		# 微分项
		self.cD = (deltaError / deltaTime) if deltaTime > 0 else 0

		# 保存时间和误差为下次更新做准备
		self.prevTime = self.currTime
		self.prevError = error

		# 返回输出值
		return sum([
			self.kP * self.cP,
			self.kI * self.cI,
			self.kD * self.cD])
  1. 上述代码中的from servo import Servo导入servo,这个库是没有的,我们要手动创建这个库,在object_tracking.py所在的目录下新建servo.py文件,复制下面的代码到文件中
#!/usr/bin/env python3
import pigpio
from time import sleep
# Start the pigpiod daemon
import subprocess
result = None
status = 1
for x in range(3):
    p = subprocess.Popen('sudo pigpiod', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    result = p.stdout.read().decode('utf-8')
    status = p.poll()
    if status == 0:
        break
    sleep(0.2)
if status != 0:
    print(status, result)
'''
> Use the DMA PWM of the pigpio library to drive the servo
> Map the servo angle (0 ~ 180 degree) to (-90 ~ 90 degree)

'''

class Servo():
    MAX_PW = 1250  # 0.5/20*100
    MIN_PW = 250 # 2.5/20*100
    _freq = 50 # 50 Hz, 20ms
 
    def __init__(self, pin, min_angle=-90, max_angle=90):

        self.pi = pigpio.pi()
        self.pin = pin 
        self.pi.set_PWM_frequency(self.pin, self._freq)
        self.pi.set_PWM_range(self.pin, 10000)      
        self.angle = 0
        self.max_angle = max_angle
        self.min_angle = min_angle
        self.pi.set_PWM_dutycycle(self.pin, 0)

    def set_angle(self, angle):
        if angle > self.max_angle:
            angle = self.max_angle
        elif angle < self.min_angle:
            angle = self.min_angle
        self.angle = angle
        duty = self.map(angle, -90, 90, 250, 1250)
        self.pi.set_PWM_dutycycle(self.pin, duty)


    def get_angle(self):
        return self.angle
    
    def stop(self):
        self.pi.set_PWM_dutycycle(self.pin, 0)
        self.pi.stop()

    # will be called automatically when the object is deleted
    # def __del__(self):
    #     pass

    def map(self, x, in_min, in_max, out_min, out_max):
        return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min


if __name__ =='__main__':
    from vilib import Vilib
    # Vilib.camera_start(vflip=True,hflip=True) 
    # Vilib.display(local=True,web=True)

    pan = Servo(pin=13, max_angle=90, min_angle=-90)
    tilt = Servo(pin=12, max_angle=30, min_angle=-90)
    panAngle = 0
    tiltAngle = 0
    pan.set_angle(panAngle)
    tilt.set_angle(tiltAngle)
    sleep(1)

    while True:
        for angle in range(0, 90, 1):
            pan.set_angle(angle)
            tilt.set_angle(angle)
            sleep(.01)
        sleep(.5)
        for angle in range(90, -90, -1):
            pan.set_angle(angle)
            tilt.set_angle(angle)
            sleep(.01)
        sleep(.5)
        for angle in range(-90, 0, 1):
            pan.set_angle(angle)
            tilt.set_angle(angle)
            sleep(.01)
        sleep(.5)

  1. 运行效果如下图,如果不想在运行过程中显示网格与关注的手指节点,可以把相应的代码注释掉

树莓派,mediapipe,Picamera2利用舵机云台追踪人手(PID控制)_第2张图片

你可能感兴趣的:(树莓派,Python,python)