K230人脸追踪二维云台

运行效果:

c5d637c2560fe3d81c4b7bf722c72de1

源码:

# 导入必要的模块
from libs.PipeLine import PipeLine, ScopedTiming
from libs.AIBase import AIBase
from libs.AI2D import Ai2d
import os
import ujson
from media.media import *
from time import *
import nncase_runtime as nn
import ulab.numpy as np
import time
import utime
import image
import random
import gc
import sys
import aidemo
from machine import Pin, PWM, FPIOA

# === 配置舵机引脚 ===
# 配置引脚42为PWM0功能
fpioa = FPIOA()
fpioa.set_function(42, FPIOA.PWM0)

# 构建舵机PWM对象,通道0,频率为50Hz(对于标准舵机)
S1 = PWM(0, 50, duty=0, enable=True)  # 初始位置设置为中点,即大约0度

# 配置引脚43为PWM1功能
fpioa.set_function(43, FPIOA.PWM1)

# 构建舵机PWM对象,通道1,频率为50Hz(对于标准舵机),负责低头抬头仰头
S2 = PWM(1, 50, duty=0, enable=True)  # 初始位置设置为中点,即大约90度

# 初始化X轴和Y轴当前位置
x_position = 0  # X轴初始位置为中间位置
y_position = 0  # Y轴初始位置为中间位置

# 获取当前帧率
clock = time.clock()

# === 设置舵机角度的函数 ===
def set_servo_angle(servo, angle):
    """设置指定舵机的角度。"""
    angle = max(-90, min(90, angle))
    duty = ((angle + 90) / 180 * 10 + 2.5)
    servo.duty(duty)

# === 增量式PID控制器类 ===
class IncrementalPID:
    """PID控制器类,用于增量式PID控制。"""
    def __init__(self, kp, ki, kd, imax):
        self.kp = kp  # 比例系数
        self.ki = ki  # 积分系数
        self.kd = kd  # 微分系数
        self.imax = imax  # 积分最大值
        self.last_error = 0
        self.integrator = 0
        self.last_derivative = 0
        self.last_output = 0

    def compute(self, error, dt):
        """计算PID输出。"""
        p_term = self.kp * error  # 比例项
        self.integrator += self.ki * error * dt  # 积分项
        self.integrator = max(-self.imax, min(self.imax, self.integrator))

        d_term = (error - self.last_error) / dt  # 微分项
        self.last_error = error

        output = p_term + self.integrator + self.kd * d_term
        self.last_output = output
        return output

    def reset(self):
        """重置PID控制器状态。"""
        self.integrator = 0
        self.last_error = 0

# === 更新舵机位置(使用PID控制) ===
def update_position_pid(servo_x, servo_y, position_x, position_y, target_x, target_y, pid_controller_x, pid_controller_y):
    """更新舵机位置,使目标位置接近当前位置。"""
    global x_position, y_position
    error_x = target_x - position_x
    error_y = target_y - position_y
    dt = 1 / clock.fps()  # 动态时间间隔
    if dt < 0.02:  # 至少20ms
        return

    output_x = pid_controller_x.compute(error_x, dt)
    output_y = pid_controller_y.compute(error_y, dt)

    # 限制每次角度调整的最大值
    delta_angle_x = min(max(output_x, -20), 20)  # 角度限制
    delta_angle_y = min(max(output_y, -30), 30)  # 角度限制

    # 增加死区,仅当输出值大于某个阈值时才进行调整
    if abs(delta_angle_x) < 1:  # 仅当输出值大于1度时才调整
        delta_angle_x = 0
    if abs(delta_angle_y) < 1:  # 仅当输出值大于1度时才调整
        delta_angle_y = 0

    # 只有当坐标变化超过阈值时才更新舵机位置
    if abs(error_x) > 15 or abs(error_y) > 15:
        # 更新舵机位置
        position_x += delta_angle_x
        position_y += delta_angle_y
        position_x = max(-90, min(90, position_x))
        position_y = max(-40, min(40, position_y))  # 限制Y轴舵机角度在-40到40度之间
        set_servo_angle(servo_x, position_x)
        set_servo_angle(servo_y, position_y)
        x_position = position_x
        y_position = position_y

        # 打印调试信息
        print(f" X轴误差: {error_x}, X轴输出: {output_x}, 新X轴位置: {position_x + delta_angle_x}")
        print(f" Y轴误差: {error_y}, Y轴输出: {output_y}, 新Y轴位置: {position_y + delta_angle_y}")

        return False  # 表示坐标变化较大
    else:
        return True  # 表示坐标变化较小

# === 自定义人脸检测类 ===
class FaceDetectionApp(AIBase):
    """自定义人脸检测应用类。"""
    def __init__(self, kmodel_path, model_input_size, anchors, confidence_threshold=0.5, nms_threshold=0.2, rgb888p_size=[224, 224], display_size=[800, 480], debug_mode=0):
        super().__init__(kmodel_path, model_input_size, rgb888p_size, debug_mode)
        self.kmodel_path = kmodel_path
        self.model_input_size = model_input_size
        self.confidence_threshold = confidence_threshold
        self.nms_threshold = nms_threshold
        self.anchors = anchors
        self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]]
        self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]]
        self.debug_mode = debug_mode
        self.ai2d = Ai2d(debug_mode)
        self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8)

    def config_preprocess(self, input_image_size=None):
        """配置预处理参数。"""
        with ScopedTiming("set preprocess config", self.debug_mode > 0):
            ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
            top, bottom, left, right = self.get_padding_param()
            self.ai2d.pad([0, 0, 0, 0, top, bottom, left, right], 0, [104, 117, 123])
            self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
            self.ai2d.build([1, 3, ai2d_input_size[1], ai2d_input_size[0]], [1, 3, self.model_input_size[1], self.model_input_size[0]])

    def postprocess(self, results):
        """后处理检测结果。"""
        with ScopedTiming("postprocess", self.debug_mode > 0):
            post_ret = aidemo.face_det_post_process(self.confidence_threshold, self.nms_threshold, self.model_input_size[1], self.anchors, self.rgb888p_size, results)
            if len(post_ret) == 0:
                return post_ret
            else:
                return post_ret[0]

    def draw_result(self, pl, dets):
        """在屏幕上绘制检测结果。"""
        with ScopedTiming("display_draw", self.debug_mode > 0):
            if dets:
                pl.osd_img.clear()
                faces = []
                for det in dets:
                    x, y, w, h = map(lambda x: int(round(x, 0)), det[:4])
                    x = x * self.display_size[0] // self.rgb888p_size[0]
                    y = y * self.display_size[1] // self.rgb888p_size[1]
                    w = w * self.display_size[0] // self.rgb888p_size[0]
                    h = h * self.display_size[1] // self.rgb888p_size[1]
                    pl.osd_img.draw_rectangle(x, y, w, h, color=(255, 255, 0, 255), thickness=2)
                    text_left_top = f"({x}, {y})"
                    pl.osd_img.draw_string_advanced(x, y - 25, 20, text_left_top, color=(255, 255, 255))
                    faces.append((x, y, w, h))
                return faces
            else:
                pl.osd_img.clear()
                return []

    def get_padding_param(self):
        """获取填充参数。"""
        dst_w = self.model_input_size[0]
        dst_h = self.model_input_size[1]
        ratio_w = dst_w / self.rgb888p_size[0]
        ratio_h = dst_h / self.rgb888p_size[1]
        ratio = min(ratio_w, ratio_h)
        new_w = int(ratio * self.rgb888p_size[0])
        new_h = int(ratio * self.rgb888p_size[1])
        dw = (dst_w - new_w) / 2
        dh = (dst_h - new_h) / 2
        top = int(round(0))
        bottom = int(round(dh * 2 + 0.1))
        left = int(round(0))
        right = int(round(dw * 2 - 0.1))
        return top, bottom, left, right

    def main_loop(self):
        """主循环函数。"""
        clock = time.clock()
        set_servo_angle(S1, 0)  # 初始化X轴舵机
        set_servo_angle(S2, 0)  # 初始化Y轴舵机

        try:
            while True:
                os.exitpoint()
                clock.tick()

                img = pl.get_frame()
                res = self.run(img)

                faces = self.draw_result(pl, res)
                if faces:
                    face = faces[0]
                    face_center_x = face[0] + face[2] / 2
                    face_center_y = face[1] + face[3] / 2

                    screen_center_x = 400  # 已知屏幕中心x坐标
                    screen_center_y = 240  # 已知屏幕中心y坐标

                    error_x = face_center_x - screen_center_x
                    error_y = face_center_y - screen_center_y

                    global target_x, target_y
                    target_x = x_position - error_x / screen_center_x * 90
                    target_y = y_position - error_y / screen_center_y * 60
                    target_x = max(-90, min(90, target_x))
                    target_y = max(-60, min(60, target_y))
                    update_position_pid(S1, S2, x_position, y_position, target_x, target_y, pid_controller_x, pid_controller_y)
                pl.show_image()
                gc.collect()
        except Exception as e:
            sys.print_exception(e)

# === 初始化PID控制器 ===
# 创建增量式PID控制器实例
# P (比例项): 0.10, 对应误差的比例反馈
# I (积分项): 0.00, 积分作用用于消除静态误差,此处设置为0表示暂时不启用积分作用
# D (微分项): 0.00, 微分作用用于预测系统未来的误差,同样设置为0表示不启用
# imax (积分限幅): 0, 控制积分项的最大累积值,防止积分饱和,这里设置为0表示无限制
pid_controller_x = IncrementalPID(0.10, 0.00, 0.00, 0)
pid_controller_y = IncrementalPID(0.08, 0.00, 0.7, 0)

# === 主程序 ===
if __name__ == "__main__":
    display_mode = "lcd"
    display_size = [800, 480]
    kmodel_path = "/sdcard/app/tests/kmodel/face_detection_320.kmodel"
    confidence_threshold = 0.5
    nms_threshold = 0.2
    anchor_len = 4200
    det_dim = 4
    anchors_path = "/sdcard/app/tests/utils/prior_data_320.bin"
    anchors = np.fromfile(anchors_path, dtype=np.float)
    anchors = anchors.reshape((anchor_len, det_dim))
    rgb888p_size = [800, 480]

    pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
    pl.create()
    face_det = FaceDetectionApp(kmodel_path, model_input_size=[320, 320], anchors=anchors, confidence_threshold=confidence_threshold, nms_threshold=nms_threshold, rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
    face_det.config_preprocess()
    try:
        face_det.main_loop()
    except Exception as e:
        sys.print_exception(e)
    finally:
        face_det.deinit()  # 反初始化
        pl.destroy()  # 销毁PipeLine实例

配套使用硬件:

舵机使用了这个店铺得20KG-PWM数字舵机

店铺链接:二维云台

1 Like

:+1: