Автономный дрон на Vision-Language Models: сборка своими руками | AiManual
AiManual Logo Ai / Manual.
26 Янв 2026 Гайд

End-to-End беспилотник на VLM: когда дрон сам понимает, куда лететь

Пошаговый гайд по созданию беспилотника с VLM для автономного полета. Аппаратная часть, софт, обучение модели, интеграция.

Зачем дрону языковая модель? (Потому что классическое CV уже не катит)

Пять лет назад автономный дрон означал кучу кода на OpenCV, детекторы ArUco маркеров и попытки заставить его не врезаться в стену. Сегодня все проще и сложнее одновременно. Vision-Language Models (VLM) - это не просто "распознавание объектов". Это понимание сцены. Дрон видит не "квадратный объект коричневого цвета", а "коричневая дверь, приоткрытая на 30 градусов". Разница колоссальная.

Важно: на момент написания (январь 2026) VLM-модели переживают взрывной рост. Если в 2024 году мы радовались BLIP-2, то сейчас уже есть десятки специализированных моделей для робототехники. В этом гайде я буду использовать LLaVA-NeXT-Video - потому что она умеет работать с видеопотоком, а не с одиночными кадрами.

Что получится в итоге (без прикрас)

Дрон, который:

  • Понимает голосовые команды: "облети квартиру по периметру и найди черную кошку"
  • Сам строит маршрут, избегая препятствий
  • Умеет импровизировать: если на пути внезапно появился человек, дрон остановится и спросит (текстом), можно ли продолжить
  • Работает полностью локально - никаких облаков, никаких подписок
  • Стоит в 3-4 раза дешевле коммерческих решений с аналогичными возможностями

Звучит как фантастика? Это потому что вы еще не видели, как современные VLM справляются с пространственным reasoning. Они реально понимают "слева от", "над", "между". И это меняет все.

Железо: что покупать и почему именно это

Здесь главная ошибка - пытаться запихнуть на дрон полноценную RTX 4090. Не надо. Наша архитектура распределенная:

Компонент Модель Цена (руб) Зачем
Дрон DJI Tello (б/у) 8 000-10 000 Дешевый, программируемый, стабильный в полете
Одноплатник на дроне Jetson Nano 4GB 15 000 Запускает легкую модель для экстренных решений
Носимый компьютер Intel NUC 13 Pro 45 000 Основная VLM, коммуникация с дроном по Wi-Fi
Камера доп. Raspberry Pi HQ Camera 6 000 Высокое разрешение для детального анализа

Почему такая схема? Потому что latency. Если вся обработка будет на NUC, а дрон только передает видео, мы получим задержку 200-300 мс. Критично для полета в помещении. Поэтому на Jetson Nano стоит крошечная модель (например, MobileVLM 1.7B), которая занимается только одним: "впереди препятствие? Да/Нет". Основная же VLM на NUC получает видео с задержкой, но зато может планировать сложные маршруты.

💡
Если у вас уже есть мощный домашний компьютер с GPU, можно использовать его вместо NUC. В статье про локальную LLM-инфраструктуру я подробно разбирал настройку. Главное - стабильный Wi-Fi 6 между дроном и компьютером.

Софтверный стек: от прошивки до нейросети

Собираем по слоям, как бутерброд:

1 Базовый слой: прошивка дрона

DJI Tello работает на проприетарной прошивке, но есть хаки. Качаем DJITelloPy - это Python библиотека, которая дает низкоуровневый доступ к контроллерам полета. Без этого дальше не сдвинемся.

# Устанавливаем на Jetson Nano
sudo apt update
sudo apt install python3-pip
pip3 install djitellopy opencv-python

Тестовая программа для проверки связи:

from djitellopy import Tello
import cv2

# Подключаемся к дрону
tello = Tello()
tello.connect()

# Включаем видеопоток
tello.streamon()
frame_read = tello.get_frame_read()

# Просто взлетаем и садимся
print(f"Батарея: {tello.get_battery()}%")
tello.takeoff()
tello.land()

2 Средний слой: коммуникация

Jetson Nano (на дроне) и NUC (на земле) общаются через ZeroMQ. Почему не ROS2? Потому что ROS2 для такой простой связки - это как пулемет для охоты на мух. К тому же, в моем гайде про мобильного робота-манипулятора я уже показывал, как ROS2 может усложнить жизнь.

# На Jetson Nano (паблишер видео)
import zmq
import cv2

context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://*:5555")

cap = cv2.VideoCapture(0)  # Камера дрона
while True:
    ret, frame = cap.read()
    if ret:
        # Ресайзим для экономии трафика
        frame_small = cv2.resize(frame, (640, 480))
        socket.send(frame_small.tobytes())
# На NUC (сабскрайбер)
import zmq
import cv2
import numpy as np

context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://[IP_JETSON]:5555")
socket.setsockopt_string(zmq.SUBSCRIBE, '')

while True:
    frame_bytes = socket.recv()
    frame = np.frombuffer(frame_bytes, dtype=np.uint8)
    frame = frame.reshape((480, 640, 3))
    # Теперь frame готов для обработки VLM

3 Верхний слой: VLM и принятие решений

Вот здесь начинается магия. Устанавливаем LLaVA-NeXT-Video - на январь 2026 это одна из лучших open-source моделей для видеоанализа.

Внимание: LLaVA-NeXT-Video требует минимум 16GB VRAM для комфортной работы в 7B параметров. Если GPU слабее - используйте Qwen2-VL-2B, она менее точная, но работает на 8GB.

# На NUC с NVIDIA GPU
git clone https://github.com/haotian-liu/LLaVA-NeXT
cd LLaVA-NeXT
pip install -e .

# Скачиваем веса модели (7B параметров)
huggingface-cli download llava-hf/llava-v1.6-vicuna-7b --local-dir ./weights

Базовый скрипт для анализа сцены:

from llava.model.builder import load_pretrained_model
from llava.mm_utils import process_images, tokenizer_image_token
from llava.constants import IMAGE_TOKEN_INDEX
import torch

# Загружаем модель
model_name = "./weights"
tokenizer, model, image_processor, context_len = load_pretrained_model(
    model_name=model_name,
    model_base=None,
    tokenizer_name=model_name
)
model = model.cuda()  # На GPU

# Функция для анализа кадра
def analyze_scene(image_frame, question):
    """
    image_frame: numpy array (H, W, 3) от камеры
    question: строка, например "Что находится в центре кадра?"
    """
    # Подготовка изображения
    image_tensor = process_images([image_frame], image_processor, model.config)
    image_tensor = [t.cuda() for t in image_tensor]
    
    # Формируем промпт
    prompt = f"USER: \n{question}\nASSISTANT:"
    
    # Токенизация
    input_ids = tokenizer_image_token(
        prompt, tokenizer, IMAGE_TOKEN_INDEX, return_tensors='pt'
    ).unsqueeze(0).cuda()
    
    # Генерация ответа
    with torch.inference_mode():
        output_ids = model.generate(
            input_ids,
            images=image_tensor,
            do_sample=True,
            temperature=0.2,
            max_new_tokens=100
        )
    
    # Декодируем ответ
    answer = tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0]
    return answer

Архитектура принятия решений: как дрон думает

Самая интересная часть. У нас не одна нейросеть, а целый конвейер:

  1. Экстренный контроллер (на Jetson Nano): MobileVLM 1.7B, вопрос всегда один: "Есть ли прямо по курсу препятствие на расстоянии менее 2 метров? Ответ только YES или NO." Задержка: 15-20 мс.
  2. Планировщик маршрута (на NUC): LLaVA-NeXT 7B, получает стоп-кадр раз в секунду, анализирует: "Какие пути свободны для движения? Опиши их относительно текущего направления."
  3. Интерпретатор команд (тоже на NUC): та же LLaVA, но для текста. Превращает "найди кошку" в последовательность действий: "1. Осмотреть комнату по секторам 2. Искать небольшие движущиеся объекты 3. Приблизиться для идентификации"
💡
Ключевой трюк: мы НЕ используем VLM для непосредственного управления моторами. VLM выдает высокоуровневые команды ("поверни налево на 30 градусов"), которые потом преобразуются в низкоуровневые команды для DJITelloPy. Так надежнее.

Fine-tuning на своих данных: когда стандартной модели недостаточно

LLaVA-NeXT тренировалась на общих данных. Но ваша квартира - особенная. Нужно научить модель:

  • Узнавать вашу мебель ("это диван Ивановых, а не просто диван")
  • Понимать планировку ("коридор ведет в спальню, а не в стену")
  • Избегать опасных зон ("рядом с аквариумом не летать"

Делаем так:

# 1. Собираем датасет
import json
from pathlib import Path

# Структура для supervised fine-tuning
dataset = []

# Для каждого помещения делаем 10-15 снимков
for room in ["kitchen", "living_room", "bedroom"]:
    for i in range(15):
        # Запускаем дрон, делаем снимок
        # Вручную размечаем (или используем автоматическую разметку через GPT-4)
        item = {
            "id": f"{room}_{i}",
            "image": f"images/{room}_{i}.jpg",
            "conversations": [
                {
                    "from": "human",
                    "value": "Опиши, что ты видишь. Где можно пролететь?"
                },
                {
                    "from": "gpt",
                    "value": "Я вижу кухню. Слева - стол, справа - окно. Свободное пространство в центре комнаты. Можно лететь прямо или немного влево."
                }
            ]
        }
        dataset.append(item)

# Сохраняем
with open("drone_dataset.json", "w", encoding="utf-8") as f:
    json.dump(dataset, f, ensure_ascii=False, indent=2)
# 2. Fine-tuning с LoRA (чтобы не перетренить всю модель)
python llava/train/train_mem.py \
    --model_name_or_path ./weights \
    --version v1 \
    --data_path drone_dataset.json \
    --image_folder images/ \
    --vision_tower openai/clip-vit-large-patch14-336 \
    --mm_projector_type mlp2x_gelu \
    --tune_mm_mlp_adapter True \
    --bf16 True \
    --output_dir ./checkpoints \
    --num_train_epochs 3 \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --save_steps 100 \
    --save_total_limit 3 \
    --learning_rate 2e-4 \
    --weight_decay 0. \
    --warmup_ratio 0.03 \
    --lr_scheduler_type cosine \
    --logging_steps 1 \
    --model_max_length 2048 \
    --gradient_checkpointing True \
    --lazy_preprocess True

После 3 эпох (примерно 6 часов на RTX 4070) модель уже будет знать вашу квартиру лучше, чем вы сами. Шутка. Но ориентироваться будет точно лучше.

Интеграция всего: код, который все связывает

Теперь собираем все компоненты в одну систему. Главный скрипт на NUC:

import threading
import queue
import time
from collections import deque

class AutonomousDrone:
    def __init__(self):
        # Очереди для межпоточного обмена
        self.video_queue = queue.Queue(maxsize=10)
        self.command_queue = queue.Queue()
        
        # История кадров для видеоанализа (последние 5 кадров)
        self.frame_buffer = deque(maxlen=5)
        
        # Состояние дрона
        self.state = {
            "battery": 100,
            "position": (0, 0, 0),  # x, y, z
            "obstacle_near": False
        }
        
    def video_receiver_thread(self):
        """Получает видео от Jetson Nano"""
        while True:
            frame = receive_frame_from_zmq()  # Функция из примера выше
            if frame is not None:
                try:
                    self.video_queue.put_nowait(frame)
                except queue.Full:
                    pass  # Пропускаем кадр, если очередь переполнена
    
    def emergency_check_thread(self):
        """Быстрая проверка препятствий (на основе MobileVLM на Jetson)"""
        while True:
            if not self.video_queue.empty():
                frame = self.video_queue.get()
                
                # Отправляем на Jetson для быстрого анализа
                # (реализация через REST API к Jetson)
                response = requests.post(
                    "http://jetson.local:8080/check_obstacle",
                    json={"frame": frame.tolist()}
                )
                
                if response.json().get("obstacle"):
                    self.state["obstacle_near"] = True
                    # Немедленная команда стоп
                    self.send_emergency_stop()
                else:
                    self.state["obstacle_near"] = False
    
    def planning_thread(self):
        """Планирование маршрута с помощью LLaVA"""
        while True:
            time.sleep(1)  # Планируем раз в секунду
            
            if not self.video_queue.empty():
                # Берем последний кадр
                frames = list(self.video_queue.queue)
                latest_frame = frames[-1] if frames else None
                
                if latest_frame is not None:
                    # Анализируем сцену
                    prompt = """
                    Ты - пилот дрона. Проанализируй сцену:
                    1. Какие препятствия видишь?
                    2. Какие свободные пути для движения?
                    3. Если бы тебе нужно было лететь вперед, насколько это безопасно?
                    Ответ дай в формате JSON.
                    """
                    
                    analysis = self.llava_analyze(latest_frame, prompt)
                    
                    # Парсим ответ и планируем
                    free_paths = analysis.get("free_paths", [])
                    
                    if free_paths:
                        # Выбираем лучший путь
                        best_path = self.choose_best_path(free_paths)
                        # Конвертируем в команды дрону
                        commands = self.path_to_commands(best_path)
                        
                        for cmd in commands:
                            self.command_queue.put(cmd)
    
    def command_executor_thread(self):
        """Исполнитель команд"""
        while True:
            if not self.command_queue.empty():
                cmd = self.command_queue.get()
                
                # Проверяем, нет ли экстренной ситуации
                if not self.state["obstacle_near"]:
                    self.execute_command(cmd)
                
                time.sleep(0.1)  # 10 команд в секунду максимум
    
    def voice_command_listener(self):
        """Слушатель голосовых команд"""
        # Используем whisper.cpp для распознавания
        # или более легкую модель для локальной работы
        while True:
            command = listen_for_voice_command()
            
            if command:
                # Интерпретируем команду через VLM
                prompt = f"""
                Пользователь сказал: "{command}"
                
                Ты - система управления дроном. Преобразуй эту команду в последовательность действий.
                Формат ответа:
                {
                    "goal": "цель команды",
                    "steps": ["шаг1", "шаг2", ...],
                    "constraints": ["ограничение1", ...]
                }
                """
                
                plan = self.llava_text_only(prompt)
                
                # Добавляем шаги в очередь команд
                for step in plan["steps"]:
                    self.command_queue.put(step)
    
    def run(self):
        """Запуск всех потоков"""
        threads = [
            threading.Thread(target=self.video_receiver_thread),
            threading.Thread(target=self.emergency_check_thread),
            threading.Thread(target=self.planning_thread),
            threading.Thread(target=self.command_executor_thread),
            threading.Thread(target=self.voice_command_listener)
        ]
        
        for thread in threads:
            thread.daemon = True
            thread.start()
        
        # Главный цикл
        try:
            while True:
                time.sleep(1)
                # Мониторинг состояния
                self.log_state()
        except KeyboardInterrupt:
            print("\nЗавершение работы...")
            self.land_and_disconnect()

Типичные грабли (чтобы вы не наступили)

Грабли №1: Wi-Fi лагает
Решение: используйте Wi-Fi 6 роутер в одном помещении с дроном. Или лучше - выделенную точку доступа на NUC. В статье про локальный умный дом я показывал, как настроить стабильную локальную сеть.

Грабли №2: VLM глючит на однородных текстурах
Белые стены, однотонный ковер - модель теряется. Решение: добавляем в fine-tuning данные с такими сценами, учим модель говорить "не вижу ориентиров, нужна осторожность" вместо случайных ответов.

Грабли №3: Батарея садится за 10 минут
Дополнительная камера и Jetson Nano жрут энергию. Решение: внешний power bank на 10000 mAh, примотанный скотчем к дрону. Неэлегантно, но работает.

Что дальше? (Когда базовый вариант уже работает)

Самые интересные апгрейды:

  • Мультиагентность: несколько дронов, которые координируются через общую VLM. Один летает, второй страхует, третий снимает видео.
  • Обучение с подкреплением: вместо fine-tuning на статических данных, даем дрону возможность учиться на своих ошибках. Ударился о стену - запомнил, что так делать не надо.
  • Интеграция с домашней автоматизацией: дрон не просто летает, а взаимодействует с умным домом. "Найди кошку и включи свет в той комнате, где она сидит" - вот это уже уровень.
  • Лонгриды на борту: сохранять не просто логи, а полноценные отчеты о полете с анализом сцен. Потом можно анализировать, как менялась обстановка в квартире за день.
💡
Самый безумный эксперимент, который у меня получился: дрон, который учится у другого дрона. Один налетал 10 часов, собрал датасет, второй fine-tun'ится на этих данных. Получается что-то вроде коллективного разума. Если интересно - напишите в комментариях, сделаю отдельный гайд.

Финальный совет (который сэкономит вам неделю)

Не пытайтесь сделать все идеально с первого раза. Сначала добейтесь, чтобы дрон просто взлетел и передавал видео. Потом добавьте экстренную остановку при препятствиях. Потом - простой планировщик маршрута. И только потом - полноценную VLM.

Каждый этап тестируйте отдельно. VLM без дрона (просто анализируйте сохраненные видео). Дрон без VLM (ручное управление). Иначе вы никогда не поймете, где именно проблема: в железе, в коде или в модели.

И последнее: ваш дрон будет глупым. Первые недели он будет врезаться в стены, путать дверь с окном и садиться на кошку. Это нормально. Главное - чтобы вы понимали, ПОЧЕМУ он так делает. А с VLM это понимание приходит гораздо быстрее, чем с классическим CV. Потому что вместо "ошибка в строке 243" вы получаете "я думал, что эта тень - это проход, но оказалось, что это просто тень". И это - огромный прогресс.