🧠 Сопровождение человека human_tracking_rknn.py

Сопровождение и распознавание жестов

human_tracking/human_tracking_rknn.py

Распознаёт позу человека, рисует скелет, выполняет команды жестами, делает фото и садится по жесту.

Расширенный пример. Помимо сопровождения, по ключевым точкам скелета COCO-17 классифицируются жесты (вперёд, назад, влево, вправо, фото, посадка). Жест считается командой только после подтверждения несколькими кадрами подряд; для посадки порог строже. Фото сохраняется с задержкой 5 секунд. Кадр зеркалится для удобства жестов.

Как работает

  1. Установить сервокамеру на 25° и загрузить модель YoloPose.
  2. Взлететь и выйти на 1.5 м.
  3. В цикле получать кадр, зеркалить и запускать модель.
  4. Строить позы, рисовать скелет и классифицировать жест по ключевым точкам.
  5. Подтверждать жест несколькими кадрами (STABLE_FRAMES, для посадки — LAND_STABLE_FRAMES).
  6. Отправлять команду движения по жесту или доворачиваться к человеку.
  7. По жесту посадки завершить цикл; фото сохранять после таймера.
  8. В finally посадить дрон и освободить ресурсы.

Используемые методы SDK

Ключевые параметры

ПараметрЗначениеОписание
STABLE_FRAMES10Кадров для подтверждения жеста.
LAND_STABLE_FRAMES18Кадров для подтверждения посадки.
GESTURE_COOLDOWN2.0 сПауза между повторными действиями.
PHOTO_DELAY5.0 сЗадержка перед фото.
MAX_VX / MAX_VY0.4 м/сМаксимальные скорости смещения.
MAX_YAW_RATE0.8 рад/сМаксимальная скорость поворота.

Запуск

cd human_tracking
python3 human_tracking_rknn.py

Результат

  • Поток rtsp://10.42.0.1:8889/human_tracking/ со скелетом и жестом.
  • Фото image_<timestamp>.png в рабочей директории.

Видеопотоки

rtsp://10.42.0.1:8889/human_tracking/

Безопасность

  • Посадка выполняется по подтверждённому жесту или Ctrl+C.
  • Запускайте с малыми скоростями и на свободной площадке.

Примечания

  • Жёлтая подпись — жест-кандидат, зелёная — подтверждённый.
  • Жесты: левая/правая рука вверх — вперёд/назад, в стороны — влево/вправо, руки у груди — фото, руки вниз согнуты — посадка.

Требования

  • pioneer_rknn.
  • Локальная навигация.
  • Драйвер камеры gstreamer.

Полный исходный код

Показать human_tracking_rknn.py
import cv2                                             # библиотека cv2 нужна для обработки изображений
import time                                            # библиотека time содержит функции для работы со временем
import math                                            # библиотека math содержит математические функции
import numpy as np                                     # библиотека numpy нужна для работы с массивами
from collections import deque                          # deque нужен для проверки жеста несколько кадров подряд

from pioneer_sdk2 import Pioneer, Camera, ImageViewer, CameraType # импортируем классы из библиотеки pioneer_sdk2
from pioneer_rknn import YoloPose                                 # импортируем модель YOLO Pose из библиотеки pioneer_rknn

try:                                                   # пробуем импортировать управление сервокамерой
    from pioneer_sdk2 import ServoCamera, ServoPriority # импортируем классы для управления углом камеры
except ImportError:                                    # если текущая конфигурация SDK не поддерживает сервокамеру
    ServoCamera = None                                 # сохраняем None вместо класса ServoCamera
    ServoPriority = None                               # сохраняем None вместо класса ServoPriority


MODEL_NAME = "yolov8n-pose"                             # имя модели YOLO Pose в реестре моделей Pioneer-RKNN
IMG_SIZE = (640, 640)                                   # размер изображения, который подается на вход нейросети
MIN_KPT_CONF = 0.25                                     # минимальная уверенность ключевой точки скелета

CAMERA_TOP_ANGLE = 25                                   # верхнее положение сервокамеры в градусах
FLIGHT_HEIGHT = 1.5                                      # рабочая высота полета в метрах
TAKEOFF_SETTLE_TIME = 0.5                                # пауза после выхода на рабочую высоту

STABLE_FRAMES = 10                                      # сколько кадров подряд нужно видеть жест для подтверждения
LAND_STABLE_FRAMES = 18                                 # посадку подтверждаем дольше, чтобы избежать случайного срабатывания
GESTURE_COOLDOWN = 2.0                                  # пауза между повторным выполнением одного и того же жеста
PHOTO_DELAY = 5.0                                       # задержка перед сохранением фотографии в секундах

COMMAND_INTERVAL = 0.3                                  # время действия команды скорости в секундах
SEND_PERIOD = 0.2                                       # как часто повторять команду скорости, включая нулевую
ZERO_SPEED = (0.0, 0.0, 0.0, 0.0)                       # нулевая команда: vx, vy, vz, yaw_rate

MAX_VX = 0.4                                            # максимальная скорость влево/вправо в м/с
MAX_VY = 0.4                                            # максимальная скорость вперед/назад в м/с
MAX_YAW_RATE = 0.8                                      # максимальная скорость поворота по курсу в рад/с

YAW_KP = 0.004                                          # коэффициент поворота для удержания человека по центру

LEFT_SHOULDER = 5                                       # индекс левого плеча в формате COCO-17
RIGHT_SHOULDER = 6                                      # индекс правого плеча в формате COCO-17
LEFT_ELBOW = 7                                          # индекс левого локтя в формате COCO-17
RIGHT_ELBOW = 8                                         # индекс правого локтя в формате COCO-17
LEFT_WRIST = 9                                          # индекс левого запястья в формате COCO-17
RIGHT_WRIST = 10                                        # индекс правого запястья в формате COCO-17
LEFT_HIP = 11                                           # индекс левого бедра в формате COCO-17
RIGHT_HIP = 12                                          # индекс правого бедра в формате COCO-17

SKELETON = [                                            # пары точек для отрисовки скелета
    (15, 13), (13, 11), (16, 14), (14, 12),
    (11, 12), (5, 11), (6, 12), (5, 6),
    (5, 7), (7, 9), (6, 8), (8, 10),
    (1, 2), (0, 1), (0, 2), (1, 3), (2, 4),
]

photo_timer = -1                                        # переменная хранит время запуска таймера фотографии
last_speed_send_time = 0.0                               # переменная хранит время последней команды скорости
last_sent_speed = None                                   # переменная хранит последнюю отправленную скорость
last_action_time = {}                                   # словарь хранит время последнего выполнения каждого жеста
gesture_history = deque(maxlen=LAND_STABLE_FRAMES)      # история последних распознанных жестов


def limit(value, min_value, max_value):                 # функция ограничивает значение заданным диапазоном
    return max(min_value, min(value, max_value))         # возвращаем значение внутри диапазона min_value..max_value


def resize_for_model(frame):                            # функция подготавливает кадр для нейросети
    image = cv2.resize(frame, IMG_SIZE)                  # изменяем размер кадра до 640x640
    image = np.expand_dims(image, 0)                     # добавляем размерность batch для RKNN
    return image                                         # возвращаем подготовленное изображение


def get_keypoints_array(keypoints):                     # функция преобразует ключевые точки YOLO Pose в массив numpy
    keypoints = np.asarray(keypoints).astype(float)      # преобразуем ключевые точки в массив чисел

    if keypoints.size == 0:                              # проверяем, что массив не пустой
        return None                                      # возвращаем None, если ключевых точек нет

    keypoints = keypoints.reshape(-1, 3)                 # приводим массив к виду: точка = x, y, confidence

    if keypoints.shape[0] < 17:                          # проверяем, что найдено не меньше 17 точек
        return None                                      # возвращаем None, если точек недостаточно

    return keypoints[:17]                                # возвращаем первые 17 ключевых точек скелета


def get_poses(frame, detections):                       # функция преобразует результат YOLO Pose в список людей
    if detections is None:                               # проверяем, что модель вернула результат
        return []                                        # возвращаем пустой список, если результата нет

    image_height, image_width = frame.shape[:2]          # получаем размеры исходного кадра
    scale_x = image_width / IMG_SIZE[0]                  # вычисляем масштаб по ширине
    scale_y = image_height / IMG_SIZE[1]                 # вычисляем масштаб по высоте
    poses = []                                           # создаем пустой список найденных людей

    for detection in detections:                         # перебираем все найденные нейросетью объекты
        keypoints = get_keypoints_array(detection.keypoint) # получаем ключевые точки человека

        if keypoints is None:                            # проверяем, удалось ли получить точки
            continue                                     # пропускаем объект без корректных точек

        keypoints = keypoints.copy()                     # создаем копию массива, чтобы не менять исходные данные модели
        keypoints[:, 0] *= scale_x                       # переводим координаты X к размеру исходного кадра
        keypoints[:, 1] *= scale_y                       # переводим координаты Y к размеру исходного кадра

        bbox = [                                         # создаем рамку человека в размере исходного кадра
            detection.xmin * scale_x,                    # левая граница рамки
            detection.ymin * scale_y,                    # верхняя граница рамки
            detection.xmax * scale_x,                    # правая граница рамки
            detection.ymax * scale_y,                    # нижняя граница рамки
        ]

        poses.append({"bbox": bbox, "keypoints": keypoints}) # сохраняем рамку и ключевые точки

    return poses                                         # возвращаем список найденных людей


def select_main_pose(poses):                             # функция выбирает основного человека в кадре
    if not poses:                                        # проверяем, есть ли найденные люди
        return None                                      # возвращаем None, если людей нет

    def bbox_area(pose):                                 # функция считает площадь рамки человека
        x1, y1, x2, y2 = pose["bbox"]                    # получаем координаты рамки
        return max(0, x2 - x1) * max(0, y2 - y1)         # возвращаем площадь рамки

    return max(poses, key=bbox_area)                     # выбираем человека с самой большой рамкой


def keypoints_are_visible(keypoints, indexes):           # функция проверяет, видны ли нужные точки скелета
    for index in indexes:                                # перебираем индексы нужных точек
        if keypoints[index][2] < MIN_KPT_CONF:           # проверяем уверенность текущей точки
            return False                                 # если точка плохая, жест не определяем
    return True                                          # возвращаем True, если все точки достаточно уверенные


def angle_from_vertical(dx, dy_up):                      # функция считает угол руки относительно вертикали
    return abs(math.degrees(math.atan2(dx, dy_up)))      # возвращаем угол в градусах


def classify_gesture(keypoints):                         # функция определяет жест человека по ключевым точкам
    needed = [                                           # точки, которые нужны для надежного определения жестов
        LEFT_SHOULDER, RIGHT_SHOULDER,
        LEFT_ELBOW, RIGHT_ELBOW,
        LEFT_WRIST, RIGHT_WRIST,
        LEFT_HIP, RIGHT_HIP,
    ]

    if not keypoints_are_visible(keypoints, needed):     # проверяем, что нужные точки хорошо видны
        return "none"                                    # если точки плохие, жест не определяем

    ls = keypoints[LEFT_SHOULDER]                        # координаты левого плеча
    rs = keypoints[RIGHT_SHOULDER]                       # координаты правого плеча
    le = keypoints[LEFT_ELBOW]                           # координаты левого локтя
    re = keypoints[RIGHT_ELBOW]                          # координаты правого локтя
    lw = keypoints[LEFT_WRIST]                           # координаты левого запястья
    rw = keypoints[RIGHT_WRIST]                          # координаты правого запястья
    lh = keypoints[LEFT_HIP]                             # координаты левого бедра
    rh = keypoints[RIGHT_HIP]                            # координаты правого бедра

    shoulder_span = max(1.0, abs(rs[0] - ls[0]))         # ширина плеч в пикселях
    torso_len = max(1.0, abs(((lh[1] + rh[1]) / 2) - ((ls[1] + rs[1]) / 2))) # высота корпуса

    dx_l = lw[0] - ls[0]                                 # смещение левой кисти по X относительно плеча
    dx_r = rw[0] - rs[0]                                 # смещение правой кисти по X относительно плеча
    dy_l_up = ls[1] - lw[1]                              # смещение левой кисти вверх относительно плеча
    dy_r_up = rs[1] - rw[1]                              # смещение правой кисти вверх относительно плеча

    len_l = math.hypot(dx_l, dy_l_up)                    # расстояние от левого плеча до левой кисти
    len_r = math.hypot(dx_r, dy_r_up)                    # расстояние от правого плеча до правой кисти
    angle_l = angle_from_vertical(dx_l, dy_l_up)         # угол левой руки относительно вертикали
    angle_r = angle_from_vertical(dx_r, dy_r_up)         # угол правой руки относительно вертикали

    left_up = (angle_l < 25 and len_l > 0.32 * torso_len) or lw[1] < ls[1] - 0.28 * torso_len  # левая рука поднята вверх
    right_up = (angle_r < 25 and len_r > 0.32 * torso_len) or rw[1] < rs[1] - 0.28 * torso_len # правая рука поднята вверх

    left_side = (abs(angle_l - 90) < 25 and len_l > 0.48 * shoulder_span) or (lw[0] < ls[0] - 0.55 * shoulder_span and abs(lw[1] - ls[1]) < 0.32 * torso_len)  # левая рука вытянута вбок
    right_side = (abs(angle_r - 90) < 25 and len_r > 0.48 * shoulder_span) or (rw[0] > rs[0] + 0.55 * shoulder_span and abs(rw[1] - rs[1]) < 0.32 * torso_len) # правая рука вытянута вбок

    wrists_close = math.hypot(lw[0] - rw[0], lw[1] - rw[1]) < max(40.0, 0.18 * shoulder_span) # кисти сведены близко друг к другу
    wrists_between_shoulders = min(ls[0], rs[0]) - 0.35 * shoulder_span < lw[0] < max(ls[0], rs[0]) + 0.35 * shoulder_span and min(ls[0], rs[0]) - 0.35 * shoulder_span < rw[0] < max(ls[0], rs[0]) + 0.35 * shoulder_span # кисти находятся около центра корпуса
    wrists_near_chest = abs(lw[1] - ((ls[1] + rs[1]) / 2)) < 0.8 * torso_len and abs(rw[1] - ((ls[1] + rs[1]) / 2)) < 0.8 * torso_len # кисти примерно на уровне корпуса

    wrists_below_elbows = lw[1] > le[1] + 0.20 * torso_len and rw[1] > re[1] + 0.20 * torso_len # кисти заметно ниже локтей
    elbows_bent_down = le[1] > ls[1] - 0.15 * torso_len and re[1] > rs[1] - 0.15 * torso_len # локти не подняты высоко вверх
    wrists_not_crossed = math.hypot(lw[0] - rw[0], lw[1] - rw[1]) > 0.45 * shoulder_span     # кисти не сведены вместе
    hands_near_sides = lw[0] < (ls[0] + rs[0]) / 2 and rw[0] > (ls[0] + rs[0]) / 2           # руки находятся по своим сторонам корпуса

    if wrists_close and wrists_between_shoulders and wrists_near_chest: # проверяем жест скрещенных или сведенных рук
        return "photo"                                  # жест для сохранения фотографии
    if wrists_below_elbows and elbows_bent_down and wrists_not_crossed and hands_near_sides: # проверяем жест посадки
        return "land"                                   # жест для посадки
    if left_side and not right_side:                     # проверяем вытянутую левую руку
        return "left"                                   # команда смещения влево
    if right_side and not left_side:                     # проверяем вытянутую правую руку
        return "right"                                  # команда смещения вправо
    if left_up and not right_up:                         # проверяем поднятую левую руку
        return "forward"                                # команда подлета ближе
    if right_up and not left_up:                         # проверяем поднятую правую руку
        return "backward"                               # команда отлета дальше

    return "none"                                       # если жест не найден, возвращаем none


def get_stable_gesture(raw_gesture):                    # функция подтверждает жест несколькими кадрами подряд
    gesture_history.append(raw_gesture)                 # добавляем текущий жест в общую историю

    if raw_gesture == "none":                           # none не нужно подтверждать как команду
        return "none", False                            # возвращаем отсутствие жеста

    required_frames = LAND_STABLE_FRAMES if raw_gesture == "land" else STABLE_FRAMES # для посадки нужен более строгий порог
    recent_gestures = list(gesture_history)[-required_frames:] # берем последние распознанные жесты

    if len(recent_gestures) == required_frames and all(item == raw_gesture for item in recent_gestures): # проверяем стабильность жеста
        return raw_gesture, True                         # возвращаем подтвержденный жест

    return raw_gesture, False                            # жест виден, но еще не подтвержден


def can_execute_action(gesture):                         # функция проверяет задержку между повторными действиями
    now = time.time()                                    # получаем текущее время
    last_time = last_action_time.get(gesture, 0)         # получаем время прошлого выполнения жеста

    if now - last_time < GESTURE_COOLDOWN:               # проверяем, прошла ли пауза
        return False                                     # если пауза не прошла, действие не выполняем

    last_action_time[gesture] = now                      # обновляем время выполнения жеста
    return True                                          # разрешаем выполнить действие


def draw_pose(frame, pose, raw_gesture, stable):         # функция рисует рамку, точки скелета и название жеста
    x1, y1, x2, y2 = map(int, pose["bbox"])              # получаем координаты рамки человека
    keypoints = pose["keypoints"]                        # получаем ключевые точки человека
    color = (0, 255, 0) if stable else (0, 200, 255)     # зеленый цвет - жест подтвержден, желтый - только кандидат

    cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)   # рисуем рамку вокруг человека
    cv2.putText(frame, f"raw: {raw_gesture}", (x1, max(0, y1 - 35)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) # пишем текущий жест
    cv2.putText(frame, f"stable: {stable}", (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) # пишем статус подтверждения

    for first, second in SKELETON:                       # перебираем пары точек скелета
        if keypoints[first][2] > MIN_KPT_CONF and keypoints[second][2] > MIN_KPT_CONF: # проверяем уверенность точек
            pt1 = (int(keypoints[first][0]), int(keypoints[first][1]))   # первая точка линии
            pt2 = (int(keypoints[second][0]), int(keypoints[second][1])) # вторая точка линии
            cv2.line(frame, pt1, pt2, (255, 255, 0), 2, lineType=cv2.LINE_AA) # рисуем линию скелета

    for x, y, confidence in keypoints:                   # перебираем все ключевые точки скелета
        if confidence > MIN_KPT_CONF:                    # проверяем, что точка найдена уверенно
            cv2.circle(frame, (int(x), int(y)), 4, (255, 0, 0), -1) # рисуем точку скелета


def setup_servo_camera():                                # функция устанавливает сервокамеру перед полетом
    if ServoCamera is None:                              # проверяем, доступно ли управление сервокамерой
        print("Сервокамера недоступна в текущей конфигурации SDK") # выводим предупреждение
        return None                                      # возвращаем None вместо объекта сервокамеры

    try:                                                 # пробуем подключиться к сервокамере
        servo_camera = ServoCamera()                     # создаем объект сервокамеры

        if ServoPriority is not None:                    # проверяем, доступен ли класс приоритета сервокамеры
            success = servo_camera.set_angle(CAMERA_TOP_ANGLE, ServoPriority.MEDIUM) # устанавливаем верхнее положение камеры
        else:                                            # если класс приоритета недоступен
            success = servo_camera.set_angle(CAMERA_TOP_ANGLE) # устанавливаем угол без указания приоритета

        if success:                                      # проверяем, удалось ли отправить команду
            print(f"Камера установлена на {CAMERA_TOP_ANGLE}°") # выводим установленный угол
        else:                                            # если команда не была отправлена
            print(f"Не удалось установить камеру на {CAMERA_TOP_ANGLE}°") # выводим предупреждение

        return servo_camera                              # возвращаем объект сервокамеры
    except Exception as error:                           # если сервокамера недоступна
        print("Сервокамера недоступна:", error)          # выводим причину в терминал
        return None                                      # возвращаем None вместо объекта сервокамеры


def wait_for_point(drone):                                # функция ждет, пока дрон долетит до заданной точки
    while not drone.point_reached():                      # ждем событие достижения точки
        time.sleep(0.1)                                   # небольшая пауза между проверками


def start_flight(drone):                                 # функция запускает дрон и поднимает его на рабочую высоту
    if not drone.arm():                                  # включаем двигатели
        raise RuntimeError("Не удалось включить двигатели")

    if not drone.takeoff():                              # выполняем взлет
        raise RuntimeError("Не удалось взлететь")

    drone.go_to_local_point(x=0, y=0, z=FLIGHT_HEIGHT, yaw=0, time=3) # набираем рабочую высоту
    wait_for_point(drone)                                 # ждем, пока дрон поднимется на заданную высоту
    time.sleep(TAKEOFF_SETTLE_TIME)                       # ждем, пока дрон стабилизируется после взлета


def send_speed_command(drone, speed, immediate=True, force=False): # функция отправляет скорость только при необходимости
    global last_speed_send_time, last_sent_speed          # используем общие переменные последней команды

    send_due = time.monotonic() - last_speed_send_time >= SEND_PERIOD # проверяем, пора ли повторить команду
    speed_changed = speed != last_sent_speed              # новую скорость отправляем сразу

    if force or send_due or (immediate and speed_changed): # проверяем, нужно ли отправлять команду скорости
        drone.set_manual_speed_body_fixed(*speed, COMMAND_INTERVAL) # отправляем команду скорости дрону
        last_speed_send_time = time.monotonic()           # запоминаем время отправки команды
        last_sent_speed = speed                           # запоминаем последнюю отправленную скорость


def send_tracking_speed(drone, pose, frame_width):       # функция разворачивает дрон к человеку
    x1, _, x2, _ = pose["bbox"]                          # получаем горизонтальные координаты рамки человека
    box_center_x = (x1 + x2) / 2                         # вычисляем центр рамки по X

    yaw_error = frame_width / 2 - box_center_x           # ошибка положения человека по горизонтали

    vx = 0.0                                             # скорость по оси X корпуса дрона
    vy = 0.0                                             # вперед/назад летим только по жестам
    vz = 0.0                                             # высоту удерживает автопилот после выхода на рабочую высоту
    yaw_rate = limit(YAW_KP * yaw_error, -MAX_YAW_RATE, MAX_YAW_RATE) # скорость поворота по курсу в рад/с

    send_speed_command(drone, (vx, vy, vz, yaw_rate), immediate=False) # отправляем команду скорости дрону


def send_hover_speed(drone):                              # функция отправляет команду зависания
    send_speed_command(drone, ZERO_SPEED)                 # отправляем нулевые скорости для удержания положения


def send_gesture_speed(drone, gesture):                  # функция отправляет команду движения по жесту
    if gesture == "left":                               # если распознан жест движения влево
        send_speed_command(drone, (-MAX_VX, 0, 0, 0))    # смещаемся влево относительно корпуса
    elif gesture == "right":                            # если распознан жест движения вправо
        send_speed_command(drone, (MAX_VX, 0, 0, 0))     # смещаемся вправо относительно корпуса
    elif gesture == "forward":                          # если распознан жест подлета ближе
        send_speed_command(drone, (0, MAX_VY, 0, 0))     # летим вперед относительно корпуса
    elif gesture == "backward":                         # если распознан жест отлета дальше
        send_speed_command(drone, (0, -MAX_VY, 0, 0))    # летим назад относительно корпуса


def update_photo_timer(frame):                           # функция сохраняет фото после задержки
    global photo_timer                                   # используем глобальную переменную таймера фотографии

    if photo_timer == -1:                                # проверяем, был ли уже запущен таймер фотографии
        print("Фото будет сохранено через 5 секунд")     # выводим сообщение пользователю
        photo_timer = time.time()                        # запоминаем время запуска таймера

    if time.time() - photo_timer >= PHOTO_DELAY:         # проверяем, прошла ли задержка перед фотографией
        file_name = f"image_{int(time.time())}.png"      # задаем имя файла с текущим временем
        cv2.imwrite(file_name, frame)                    # сохраняем текущий кадр в файл
        print(f"Фото сохранено: {file_name}")            # выводим сообщение о сохранении фото
        photo_timer = -1                                 # сбрасываем таймер фотографии


def main():                                              # главная функция программы
    global photo_timer, last_speed_send_time, last_sent_speed # используем глобальные переменные таймеров

    drone = None                                         # переменная для подключения к квадрокоптеру
    camera = None                                        # переменная для камеры квадрокоптера
    viewer = None                                        # переменная для RTSP-трансляции
    model = None                                         # переменная для RKNN-модели

    try:                                                 # основной код находится внутри блока try
        setup_servo_camera()                             # устанавливаем камеру в верхнее положение
        camera = Camera(camera_type=CameraType.MAIN)     # создаем объект основной камеры квадрокоптера
        viewer = ImageViewer()                           # создаем объект для RTSP-трансляции обработанного видео
        model = YoloPose(model_name=MODEL_NAME)          # загружаем модель YOLO Pose из реестра моделей
        drone = Pioneer()                                # создаем экземпляр класса Pioneer, устанавливаем соединение

        print("Запущено отслеживание человека")               # выводим сообщение о запуске программы
        print("Желтая подпись - жест-кандидат, зеленая - подтвержденный жест") # объясняем отображение
        print("Остановить программу можно сочетанием Ctrl+C") # выводим подсказку по остановке программы

        start_flight(drone)                              # выполняем взлет и выход на рабочую высоту
        last_sent_speed = None                            # после взлета сразу отправим первую команду скорости
        last_speed_send_time = 0.0                        # сбрасываем таймер отправки скорости

        while True:                                      # запускаем основной цикл программы
            try:                                         # пробуем получить кадр с камеры
                frame = camera.get_cv_frame(timeout=0.05) # получаем кадр с камеры квадрокоптера
            except TimeoutError:                         # если кадр не успел прийти
                send_hover_speed(drone)                  # отправляем команду зависания
                continue                                 # пропускаем текущую итерацию цикла

            if frame is None:                            # проверяем, удалось ли получить кадр
                send_hover_speed(drone)                  # отправляем команду зависания
                continue                                 # пропускаем итерацию, если кадр не получен

            frame = cv2.flip(frame, 1)                   # зеркально отражаем изображение для удобства работы с жестами
            model_input = resize_for_model(frame)        # подготавливаем кадр для нейросети
            detections = model.run([model_input])        # запускаем распознавание человека и ключевых точек
            poses = get_poses(frame, detections)         # преобразуем результат нейросети в список поз
            main_pose = select_main_pose(poses)          # выбираем основного человека в кадре

            if main_pose is not None:                    # проверяем, найден ли человек в кадре
                raw_gesture = classify_gesture(main_pose["keypoints"]) # определяем текущий жест
                stable_gesture, stable = get_stable_gesture(raw_gesture) # проверяем, стабилен ли жест
                draw_pose(frame, main_pose, raw_gesture, stable)       # рисуем скелет и подпись жеста

                if stable and stable_gesture == "photo" and can_execute_action("photo"): # проверяем подтвержденный жест фото
                    update_photo_timer(frame)            # запускаем таймер сохранения фото
                elif photo_timer != -1:                  # если таймер фото уже запущен
                    update_photo_timer(frame)            # продолжаем отсчет до сохранения

                if stable and stable_gesture == "land" and can_execute_action("land"): # проверяем подтвержденный жест посадки
                    print("Распознан подтвержденный жест посадки") # выводим сообщение пользователю
                    break                                # выходим из цикла, посадка выполнится в finally

                elif stable and stable_gesture in ["left", "right", "forward", "backward"]: # проверяем управляющие жесты
                    send_gesture_speed(drone, stable_gesture) # отправляем команду движения по жесту
                else:                                    # если управляющий жест не найден
                    send_tracking_speed(drone, main_pose, frame.shape[1]) # разворачиваем дрон к человеку

            else:                                        # если человек не найден
                cv2.putText(frame, "person not found", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) # пишем сообщение на кадре
                send_hover_speed(drone)                  # отправляем команду зависания

            viewer.imshow(name="human_tracking", frame=frame, fps=30) # отправляем кадр в RTSP-трансляцию

    except KeyboardInterrupt:                            # если пользователь остановил программу сочетанием Ctrl+C
        print("Остановка программы, производится посадка") # выводим сообщение об остановке программы

    except Exception as error:                           # если произошла другая ошибка
        print("Ошибка:", error)                          # выводим текст ошибки

    finally:                                             # блок finally выполнится в любом случае
        if drone is not None:                            # проверяем, что соединение с дроном было создано
            state = drone.get_fly_state().name           # читаем состояние дрона
            if state == "IN_SKY":                        # если дрон в воздухе
                send_speed_command(drone, ZERO_SPEED, force=True) # отправляем нулевую скорость перед посадкой
                drone.land()                             # выполняем посадку перед завершением
            elif state == "ARMED":                       # если двигатели включены, но взлета не было
                drone.disarm()                           # выключаем двигатели
        if viewer is not None:                           # проверяем, что трансляция была создана
            viewer.close()                               # закрываем RTSP-трансляцию ImageViewer
        if camera is not None:                           # проверяем, что камера была создана
            camera.stop()                                # останавливаем получение кадров с камеры
        if drone is not None:                            # проверяем, что соединение было создано
            drone.close_connection()                     # закрываем соединение с квадрокоптером
        if model is not None:                            # проверяем, что модель была создана
            model.release()                              # освобождаем ресурсы RKNN модели


if __name__ == "__main__":                              # проверяем, что файл запущен как основная программа
    main()                                               # запускаем главную функцию программы