Содержание статьи
Мы будем управлять ботом так же, как это делает обычный игрок: отправлять ему команды нажатия клавиш (WASD и других). Получать информацию от бота будем через встроенную в игру функцию сохранения скриншотов. Например, отправили боту команду нажать F12 — получили снимок того, что он видит в данный момент. Проанализировали изображение, отправили следующую команду — и так по кругу.
Важный момент: у нас не будет цели делать в игре что‑то конкретное. Мы просто подготовим основу и заодно изучим принципы работы с машинным зрением.
Для распознавания объектов мы будем использовать модели YOLO (You Only Look Once). Наша задача — обучить модель находить булыжник и двигаться по дорожке, вымощенной им.
Обучение будет проходить в два этапа.
- Первый этап. Обучим модель YOLOv8n находить наш объект на снимках. Эта модель сможет определить, содержится ли на изображении искомый объект, и выделить его с помощью ограничивающего прямоугольника (bounding box).
- Второй этап. Подготовим обучающий материал для второй модели — YOLOv8-seg, которая сможет обводить объекты по контуру. Это даст нам возможность анализировать их размер, форму и положение. Это именно то, что нужно для точной ориентации бота в пространстве.
Что нам понадобится
-
Обычный компьютер. Мы используем легковесные модели, с которыми справится даже средний по мощности ПК. Весь код был написан и протестирован на машине со следующими характеристиками:
- процессор — восьмиядерный AMD Ryzen 7 5700U с картой Radeon;
- оперативная память — 8 Гбайт.
-
Свободное место на диске. Потребуется около 10 Гбайт:
- 400–500 Мбайт для хранения снимков (зависит от их количества);
- ~400 Мбайт для скачивания предобученных моделей;
- ~8,6 Гбайт для установки библиотек Python;
- 38,8 Кбайт для нашего кода!
- Хороший фильм или даже сериал. Обучение модели занимает несколько часов, так что стоит запастись терпением и развлечениями.
- Операционная система. Я использовал Ubuntu, но подойдет любой дистрибутив Linux. Нам понадобятся несколько дополнительных программ, которые, скорее всего, уже есть в официальных репозиториях твоего дистра.
Ставим приложения для работы с буфером обмена:
sudo apt install xclip xsel
И для работы с окнами:
sudo apt install wmctrl xdotool
Тестировать я буду на игре Luanti (также известна под старым названием Minetest) — она попроще, чем Minecraft, и есть в стандартных репозиториях. Однако попутно дам рекомендации для работы с Minecraft — отличия будут минимальными.
Мы воспользуемся своим набором текстур для игры, чтобы наш бот заработал на любом сервере. Скачать текстурки можно из репозитория проекта.
В качестве языка возьмем Python — как один из наиболее популярных и имеющий множество удобных библиотек.
Весь процесс можно разбить на три основных этапа: подготовка, обучение, использование.
Подготовка
У нас будет много разных файлов, поэтому, чтобы ничего не потерять, создадим каталог для проекта.
Поскольку нам нужно будет ставить модули Python, сразу сделаем виртуальное окружение.
python -m venv minebot
Теперь попробуем покомандовать нашим ботом. Для этого воспользуемся библиотекой PyAutoGUI. С ее помощью мы можем отправлять окну в фокусе нажатие клавиш, движение мыши и нажатие ее кнопок.
Активируем наше виртуальное окружение:
source minebot/bin/activate
Установим библиотеки.
pip install pyautogui pyperclip opencv-python numpy ultralytics
Команды управления ботом я собрал в одном файле.
luanti/controller.py
import pyautoguiimport pyperclipimport timeimport mathclass LuantiController: """Класс для управления персонажем в Luanti с использованием библиотеки PyAutoGUI.""" def __init__(self, window_width, window_height): """Инициализирует контроллер, получает размеры экрана.""" self.screen_width = window_width self.screen_height = window_height self.fov = 72 # Угол обзора в градусах def take_screenshot(self): """Делает скриншот (нажатие F12).""" pyautogui.press('f12') time.sleep(0.1) def _move(self, key, duration): """Вспомогательная функция для перемещения.""" pyautogui.keyDown(key) time.sleep(duration) pyautogui.keyUp(key) def move_forward(self, duration): """Перемещает персонажа вперед (нажатие W).""" self._move('w', duration) def move_backward(self, duration): """Перемещает персонажа назад (нажатие S).""" self._move('s', duration) def move_left(self, duration): """Перемещает персонажа влево (нажатие A).""" self._move('a', duration) def move_right(self, duration): """Перемещает персонажа вправо (нажатие D).""" self._move('d', duration) def jump(self): """Прыжок (нажатие пробела).""" pyautogui.keyDown('space') time.sleep(0.1) pyautogui.keyUp('space') def run(self, direction, duration): """Бег в указанном направлении.""" pyautogui.keyDown('e') time.sleep(0.1) if direction == 'forward': self._move('w', duration) elif direction == 'backward': self._move('s', duration) elif direction == 'left': self._move('a', duration) elif direction == 'right': self._move('d', duration) pyautogui.keyUp('e') def crouch_and_move(self, direction, duration): """Перемещение при приседании.""" pyautogui.keyDown('shift') time.sleep(0.1) if direction == 'forward': self._move('w', duration) elif direction == 'backward': self._move('s', duration) elif direction == 'left': self._move('a', duration) elif direction == 'right': self._move('d', duration) pyautogui.keyUp('shift') def open_inventory(self): """Открывает инвентарь (нажатие I).""" pyautogui.press('i') def close_inventory(self): """Закрывает инвентарь (нажатие Esc).""" pyautogui.press('esc') def change_camera(self, clicks): """Меняет вид камеры (нажатие C).""" for _ in range(clicks): pyautogui.press('c') time.sleep(0.5) def select_inventory_slot(self, slot_number): """Выбирает слот в инвентаре (нажатие клавиш 1–8).""" if 1 <= slot_number <= 8: pyautogui.press(str(slot_number)) else: print("Недопустимый номер слота: " + str(slot_number)) def place_block(self): """Ставит блок (правый клик мыши).""" pyautogui.click(button='right') def mine_block(self): """Ломает блок (удержание левого клика мыши).""" pyautogui.mouseDown(button='left') time.sleep(2) # Удерживаем кнопку мыши 2 секунды pyautogui.mouseUp(button='left') def send_chat_message(self, message): """Отправляет сообщение в чат с поддержкой кириллицы.""" # Открываем чат pyautogui.press('t') time.sleep(0.1) # Копируем текст в буфер обмена pyperclip.copy(message) # Вставляем текст из буфера обмена (Ctrl + V) pyautogui.hotkey('ctrl', 'v') # Отправляем сообщение pyautogui.press('enter') def _calculate_pixels_for_degrees(self, degrees): """Вычисляет количество пикселей для сдвига мыши, необходимое для имитации поворота на заданный угол.""" pixels_per_degree = self.screen_width / self.fov return int(degrees * pixels_per_degree) def turn(self, degrees_x, degrees_y): """Поворачивает персонажа на указанное количество градусов.""" pixel_offset_x = self._calculate_pixels_for_degrees(degrees_x) pixel_offset_y = self._calculate_pixels_for_degrees(degrees_y) pyautogui.moveRel(pixel_offset_x, pixel_offset_y, duration=0.0) time.sleep(0.05) def drop_item(self): """Выбрасывает предмет (нажатие Q).""" pyautogui.press('q') time.sleep(0.1)
Управление в Minecraft будет отличаться несколькими клавишами, код файла можно взять в репозитории проекта.
Дальше — демонстрационный код, который поочередно отправляет все команды боту, чтобы проверить, как он их выполняет.
Вот как это работает.
Мы подключаем к нашему файлу команды управления ботом из предыдущего листинга.
Важно: при подключении нужно указывать размеры окна игры! Если размер неверный, то бот будет неправильно рассчитывать углы для поворота. Для упрощения и сокращения кода в этом файле они указаны вручную, ты можешь их исправить на свои значения. Далее мы будем определять эти параметры автоматически. Сейчас же нам надо проверить, что мы можем управлять персонажем.
Зайди в игру и найди ровную поверхность, запусти experiment_1.
в терминале и кликай по окну с игрой, чтобы оно получило фокус.
Для Minecraft нужно будет закомментировать одну строку и раскомментировать другую — они помечены в коде.
experiment_1.py
import pyautoguiimport timeimport cv2import osimport datetimeimport re# Для Luantifrom luanti.controller import LuantiController as Controller# Для Minecraft# from minecraft.controller import MinecraftController as Controller# Лучше сделать размер окна игры такого размераwindow_width, window_height = 640, 640def run_bot_actions(controller): """Выполняет все действия бота.""" normal_move_time = 2 run_move_time = 1 methods = [ ("устанавливает направление взгляда", lambda: ( controller.turn(0, 90), controller.turn(0, -25) ) ), ("идет вперед", lambda: controller.move_forward(normal_move_time)), ("идет назад", lambda: controller.move_backward(normal_move_time)), ("идет влево", lambda: controller.move_left(normal_move_time)), ("идет вправо", lambda: controller.move_right(normal_move_time)), ("прыгает", controller.jump), ("бежит вперед", lambda: controller.run("forward", run_move_time)), ("бежит назад", lambda: controller.run("backward", run_move_time)), ("бежит влево", lambda: controller.run("left", run_move_time)), ("бежит вправо", lambda: controller.run("right", run_move_time)), ("приседает и идет вперед", lambda: controller.crouch_and_move("forward", normal_move_time)), ("приседает и идет назад", lambda: controller.crouch_and_move("backward", normal_move_time)), ("приседает и идет влево", lambda: controller.crouch_and_move("left", normal_move_time)), ("приседает и идет вправо", lambda: controller.crouch_and_move("right", normal_move_time)), ("открывает инвентарь", controller.open_inventory), ("закрывает инвентарь", controller.close_inventory), ("меняет камеру", lambda: controller.change_camera(3)), ("выбирает слот 8 и ставит блок", lambda: ( controller.select_inventory_slot(8), controller.place_block() ) ), ("выбирает слот 5 и копает блок", lambda: ( controller.select_inventory_slot(5), controller.mine_block() ) ), ("выбирает слот 1 и кушает :)", lambda: ( controller.select_inventory_slot(1), time.sleep(0.2), pyautogui.mouseDown(button='left'), time.sleep(0.1), pyautogui.mouseUp(button='left') ) ), ("поворачивается на 10 градусов вправо", lambda: controller.turn(10, 0)), ("поворачивается на 10 градусов влево", lambda: controller.turn(-10, 0)), ("поворачивается на 10 градусов влево",lambda: controller.turn(-10, 0)), ("поворачивается на 10 градусов вправо",lambda: controller.turn(10, 0)), ("Делает скриншот", controller.take_screenshot), ("пишет в чат", lambda: controller.send_chat_message("Тест пройден, мои поздравления!")), ] for description, method in methods: print(f"[БОТ] {description}") if isinstance(method, tuple): for action in method: action() else: method() time.sleep(0.2)if __name__ == "__main__": print("Начинаем тестирование всех методов...") print("Пожалуйста, переключите фокус на окно с игрой...") time.sleep(5) if window_width is not None and window_height is not None: controller = Controller(window_width, window_height) run_bot_actions(controller) print(f"[БОТ] работу закончил.")
Если бот зашевелился, значит, есть контроль над ним и можно двигаться дальше.
Чтобы хранить все настройки в одном месте, напишем конфигурационный файл.
import os# Базовое имя каталога, по умолчанию "minebot"BASE_DIR_NAME = "minebot"# Полный путь к базовой директорииBASE_DIR = os.path.join(os.path.expanduser("~"), BASE_DIR_NAME)# Путь к моделиMODEL_PATH = os.path.join(BASE_DIR, "runs", "segment", "train", "weights", "best.pt")# Luanti# Путь к скриншотам для LuantiSCREENSHOT_PATH = os.path.join(os.path.expanduser("~"), "snap/minetest/current/screenshots/")# Шаблон имени скриншота для LuantiSCREENSHOT_PATTERN = r"screenshot_(\d{8}_\d{6})\.png"# Minecraft# Путь к скриншотам для Minecraft, исправь путь к твоему майнкрафту# SCREENSHOT_PATH = os.path.join(os.path.expanduser("~"), "Minecraft_1.20.1/screenshots/")# Шаблон имени скриншота для Minecraft# SCREENSHOT_PATTERN = r"^\d{4}-\d{2}-\d{2}_\d{2}\.\d{2}\.\d{2}\.png$"
info
Путь к каталогу снимков указан для варианта установки Luanti при использовании Snap в качестве источника.
Опять же, если ты хочешь использовать бота в Minecraft, закомментируй строки для Luanti и раскомментируй для Minecraft и укажи правильный путь к каталогу, в котором у тебя установлен Minecraft.
Если ты устанавливал Luanti из источника Snap, то путь к каталогу скриншотов будет такой:
~/snap/minetest/current/screenshots/
Если устанавливал из пакета .deb, то такой:
~/.minetest/screenshots/
RAM-диск
Частая запись снимков на диск — ресурсоемкий процесс. К тому же скрины нам нужны для анализа, а не для хранения. Поэтому, если ты будешь развивать своего бота, рекомендую записывать снимки на виртуальный диск в оперативной памяти. Вот как это можно сделать.
Сначала создаем RAM-диск с помощью tmpfs.
Делаем директорию для него:
sudo mkdir /mnt/ramdisk
Монтируем tmpfs в эту директорию:
sudo mount -t tmpfs -o size=56M tmpfs /mnt/ramdisk
Здесь size=56M
— это размер RAM-диска. Ты можешь изменить его в зависимости от объема доступной оперативной памяти. Мы стираем файлы после использования, поэтому 56 Мбайт — это даже очень много.
Теперь создадим символическую ссылку.
Можешь переместить текущий каталог снимков в другое место (если нужно сохранить существующие снимки):
mv ~/minecraft/screenshots ~/minecraft/screenshots_backup
И создадим символическую ссылку из ~/
на RAM-диск:
ln -s /mnt/ramdisk ~/minecraft/screenshots
Теперь, когда игра будет сохранять снимки в ~/
, они фактически будут записываться в оперативную память (в /
).
Вместо ~/
используй правильный каталог снимков твоей игры.
Если ты устанавливал Minetest через Snap, то наш кастомный набор текстур нужно положить вот в этот каталог:
~/snap/minetest/current/textures/minebot
Если ставил из пакета deb, то вот сюда:
~.minetest/textures/minebot
У меня — первый вариант. В этом каталоге лежит файл, который нам нужен для распознавания блоков булыжника: default_cobble.
В Minetest использование собственных текстур включается на вкладке «Контент», напротив выбранных текстур должно появиться слово «Включено».
В Minecraft пакеты текстур хранятся в каталоге ~/
, если игра установлена в ~/
.
Подготовленный набор (из целой одной текстуры!) можно скачать из репозитория проекта. Именно на ее распознавание натренирован бот.
Теперь нужно настроить игру на нужное разрешение. Лучше, чтобы размеры снимков для обучения и размеры снимков, которые мы потом будем распознавать в игре, были одинаковыми — это повысит качество распознавания. При этом размер снимков прямо влияет на требуемые для распознавания ресурсы и на скорость обработки снимков.
Я для себя решил, что мой бот будет играть сам, автономно, поэтому я выбрал разрешение пониже — 640 на 640 пикселей. Настроить разрешение можно в настройках, в разделе «Графика». Нужно задать ширину и высоту экрана, и не забудь нажать кнопку «Назначить» для сохранения изменений.
На той же странице настроек ниже есть параметр «Угол обзора». Проверь, что его значение совпадает со значением self.
в файле controller.
(по умолчанию 72). Заодно можно отключить все тени и все лишние прелести освещения и сглаживания, чтобы текстуры меньше искажались и распознавание образов работало лучше. К тому же игра будет потреблять меньше ресурсов.
В общем, выключаем вообще все, что выключается!
Для Minecraft нужен чуть более сложный подход. При каждом запуске игры надо будет вручную из терминала менять разрешение окна на нужное нам.
Выполним команду
wmctrl -l
Она выведет список всех открытых окон. Ищи строку с названием Minecraft (например, Minecraft 1.20.1).
Теперь используй xdotool, чтобы изменить размер окна. Например:
xdotool search --name "Minecraft Forge*" windowsize 640 640
Здесь Minecraft
— это часть названия окна, которое ты нашел с помощью wmctrl
.
Также для Minecraft дополнительно нужно выключить настройку «Необработанный ввод» в настройках мыши. Если этого не сделать, то при попытке изменить направление взгляда бота персонаж будет опускать взгляд в землю.
С настройкой самой игры на этом все. Можно переходить к съемкам набора для обучения модели.
Обучение
Для обучения нам потребуется порядка 150–200 снимков. Я делал их в двух локациях: одну создал специально и построил там дорогу шириной три блока. Неподалеку за холмом поставил столб и арку — для разнообразия. Второй локацией был город, в котором уже есть вымощенные булыжником дорожки.
Перед выбором места хорошо его осмотри, чтобы нигде неожиданно не торчал булыжник. При разметке снимков тебе надо будет отметить все совпадения.
Особых рекомендаций по технике съемки нет, главное — разнообразие. И учитывай, что потом на каждом снимке нужно будет выделить прямоугольником всю замощенную булыжником поверхность. Если дорога будет идти по диагонали через весь экран, придется долго и утомительно размечать ее лесенкой. Я в таких случаях шел от самого большого участка, потом следующий по величине и дальше все меньшие и меньшие кусочки.

Напомню, снимок делается кнопкой F12 (или F2 в Minecraft), нам надо порядка 150–200. Вперед!
Впрочем, следующий этап — еще более утомительный. Запасайся терпением! Нам нужно на каждом снимке обвести булыжник прямоугольником. Сделать это надо точно и аккуратно, не захватывая лишнего и не оставляя части булыжника неразмеченными. Качество разметки очень сильно скажется на качестве обучения модели.
Вот несколько простых инструментов, которые подойдут для аннотации:
- Онлайновый сервис — простой и понятный. Из минусов: файлы с аннотациями он выдает в формате CSV. Это не наш формат, но конвертировать нетрудно.
-
YOLO-Annotation-Tool — набор из файлов JavaScript, работающий в браузере локально. В поле
Classes
загружается текстовый файл с именами всех классов. В нашем случае — файл, содержащий строку0
.cobblestone - Label Studio — программа, которая работает локально и поднимает свой веб‑интерфейс. Из минусов — нужно ставить много зависимостей, из плюсов — есть возможность совместной работы над аннотацией файлов.
После разметки при помощи YOLO-Annotation-Tool у меня на руках появился каталог со снимками в формате PNG и файлы с аннотациями в формате YOLO. Образец можно посмотреть в репозитории проекта.
Формат аннотаций YOLO
Каждая строка файла содержит информацию об одном объекте на изображении. Есть тип border box — это когда обозначается область, в которой находится объект. Эта информация используется для детекта моделей.
Пример:
<class_id> <x_center> <y_center> <width> <height>
Здесь
-
class_id
— целое число, представляющее номер класса объекта. Нумерация классов начинается с 0. Например, если у тебя есть два класса — «дорога» и «препятствие», то класс «дорога» может быть представлен как 0, а «препятствие» как 1; -
x_center
иy_center
— координаты центра объекта, нормализованные относительно ширины и высоты изображения. Эти значения лежат в диапазоне от 0 до 1; -
width
иheight
— схожие значения для ширины и высоты изображения.
Наш class_id
во всех файлах равен нулю, потому что у нас только один класс — булыжник. Если на снимках будут объекты разных классов, то для каждого класса будет свой id. Например:
0 cactus
1 cobblestone
2 sky
3 sun
На всякий случай мы сделаем проверку наших файлов аннотации (это не обязательно, но интересно).
Давай просто наложим на наши изображения маски, записанные в файлах с аннотациями. Для этого мы воспользуемся небольшим скриптом.
visualize_bbox_mask.py
import cv2import osimport numpy as npimport re# Импортируем переменные из config.pyfrom config import BASE_DIRdef visualize_bbox(image_path, labels_path, output_path): """Визуализирует bounding box маски на изображении.""" img = cv2.imread(image_path) if img is None: print(f"Ошибка: Не удалось загрузить изображение {image_path}") return height, width, _ = img.shape # Создаем пустую маску mask_img = np.zeros_like(img, dtype=np.uint8) if not os.path.exists(labels_path): print(f"Предупреждение: Файл меток {labels_path} не найден.") return with open(labels_path, "r") as f: for line in f: parts = line.strip().split() # Проверяем, что есть ровно пять элементов if len(parts) != 5: print(f"Предупреждение: Неверное кол-во элементов в файле {labels_path}: {line}") continue try: class_id = int(parts[0]) x_center, y_center, box_width, box_height = map(float, parts[1:]) # Преобразуем координаты в пиксели x1 = int((x_center - box_width / 2) * width) y1 = int((y_center - box_height / 2) * height) x2 = int((x_center + box_width / 2) * width) y2 = int((y_center + box_height / 2) * height) # Рисуем прямоугольник (bounding box) на маске cv2.rectangle(mask_img, (x1, y1), (x2, y2), (0, 255, 0), -1) except ValueError as e: print(f"Ошибка при обработке строки в файле {labels_path}: {line}, ошибка: {e}") # Объединяем исходное изображение и маску overlay = cv2.addWeighted(img, 0.7, mask_img, 0.3, 0) cv2.imwrite(output_path, overlay) print(f"Визуализация сохранена в {output_path}")if __name__ == "__main__": # Директория с изображениями images_dir = os.path.join(BASE_DIR, "dataset", "check_mask") # Директория с метками (в том же месте, что и изображения) labels_dir = images_dir if not os.path.exists(images_dir): print(f"Ошибка: Папка '{images_dir}' не найдена.") exit() # Регулярное выражение для проверки имени файла screenshot_regex = re.compile(r"(\w+)\.png") for filename in os.listdir(images_dir): match = screenshot_regex.match(filename) if match: image_path = os.path.join(images_dir, filename) # Формируем имя файла меток labels_name = os.path.splitext(filename)[0] + ".txt" # Путь к файлу меток labels_path = os.path.join(labels_dir, labels_name) output_name = os.path.splitext(filename)[0] + "_masked.png" output_path = os.path.join(images_dir, output_name) visualize_bbox(image_path, labels_path, output_path)
Результат должен выглядеть примерно как на скриншотах.


Создадим такую структуру каталогов:
/dataset
├──/ train_images
├──/ test_images
├──/ val_images
└─── dataset.yaml
Снимки надо распределить по папкам в таком соотношении: в каталоги test_images
и val_images
по 10–15% от общего количества снимков, остальные — в каталог train_images
. Это основной набор снимков для обучения.
Названия файлов могут быть любыми, но на всякий случай лучше не использовать кириллицу.
Файлы с аннотациями должны быть в том же каталоге, что и снимки. У каждого снимка должен быть свой файл с аннотацией, с тем же названием, но с расширением .txt. В каталоге test_images
файлы аннотаций не нужны, он необходим для проверки того, как модель обучается. Когда будем делать автоматическую разметку для второй модели, советую скинуть сюда файлы с самой трудной или плохой разметкой, если такие найдутся.
Еще нам нужен конфиг для описания пути к нашим каталогам со снимками.
dataset/dataset.yaml
# train и val пути к изображениямtrain: /home/ksandr/minebot/dataset/train_imagesval: /home/ksandr/minebot/dataset/val_imagestest: /home/ksandr/minebot/dataset/test_images# Количество классовnc: 1# Имена классовnames: ['cobblestone']
В файле указываются абсолютные пути, так что исправь их на свои.
Итак, у нас все готово для запуска обучения первой модели YOLO detect.
info
YOLO в названии модели означает You Only Look Once — «Ты смотришь только один раз». Более ранние методы детектирования объектов делали по несколько проходов, но потом удалось обойтись одним.
Для обучения модели запусти в терминале файл train_detect_model.
. И можешь идти заваривать чай, выгуливать собаку, звонить самому болтливому приятелю или смотреть сериал — конкретный выбор зависит от того, насколько мощное у тебя железо.
train_detect_model.py
import osfrom ultralytics import YOLO# Импортируем переменные из config.pyfrom config import BASE_DIR# 1. Загрузка модели# Загрузка предобученной модели YOLOv8nmodel = YOLO("yolov8n.pt")# Путь к файлу dataset.yamldata_path = os.path.join(BASE_DIR, "dataset", "dataset.yaml")# 2. Обучение моделиmodel.train( data = data_path, # Количество эпох обучения epochs = 100, # Размер пакета batch = 16, # Размер изображений imgsz = 640,)

Результатом выполнения скрипта выше будет запуск обучения модели: она будет тренироваться распознавать на снимках объекты с нашей текстурой «булыжник». Точнее, сначала скачается предобученная модель yolov8n.
, а затем она дообучится на нашем наборе снимков.
В результате выполнения скрипта будут созданы каталоги ~/
(возможно, train2
, train3
и так далее — если обучение запускалось несколько раз). В нем будут две готовые к использованию модели. Далее мы станем использовать модель ~/
.
Конечно, сразу хочется проверить, как это работает. Давай попробуем найти наши булыжники в виртуальной среде. Будем делать снимок каждые пять секунд, рассматривать его и выводить в терминале сообщение о том, есть булыжник или нет. А потом попробуем повернуть взгляд бота в его сторону.
Запустим игру. В терминале запусти скрипт experiment_2.
и переведи фокус на окно с игрой. Если у тебя Minecraft, то закомментируй в начале файла строку для Luanti и раскомментируй вариант для Minecraft. То же сделай в начале метода def
.
experiment_2.py
import pyautoguiimport timeimport cv2import numpy as npimport osimport refrom ultralytics import YOLO# Импортируем переменные из config.pyfrom config import SCREENSHOT_PATH, SCREENSHOT_PATTERN, MODEL_PATH# Для Luantifrom luanti.controller import LuantiController as Controller# Для Minecraft# from minecraft.controller import MinecraftController as Controllerdef get_window_size(): """Получает размер окна игры.""" # Делаем снимок, чтобы получить размер окна # Для Luanti pyautogui.press('f12') # Pyautogui.press('F2') # Для Minecraft time.sleep(1) # Находим снимок files = os.listdir(SCREENSHOT_PATH) screenshot_file = None for file in files: if re.match(SCREENSHOT_PATTERN, file): screenshot_file = file break if not screenshot_file: print(f"Файл скриншота не найден по пути: {SCREENSHOT_PATH}") return None, None full_screenshot_path = os.path.join(SCREENSHOT_PATH, screenshot_file) # Узнаем его размер screenshot = cv2.imread(full_screenshot_path) if screenshot is None: print(f"Не удалось прочитать изображение по пути: {full_screenshot_path}") return None, None window_height, window_width, _ = screenshot.shape print(f"Размер окна игры: {window_width}x{window_height}") return window_width, window_heightdef get_screenshot_path(): """Находит путь к последнему скриншоту.""" files = os.listdir(SCREENSHOT_PATH) screenshot_files = [f for f in files if re.match(SCREENSHOT_PATTERN, f)] if screenshot_files: # Сортируем по времени (последний файл первым) screenshot_files.sort(reverse=True) return os.path.join(SCREENSHOT_PATH, screenshot_files[0]) else: return Nonedef detect_cobblestone(image_path, model, confidence_threshold=0.3): """Обнаруживает булыжники на изображении с помощью YOLO.""" img = cv2.imread(image_path) if img is None: print(f"Ошибка: не удалось прочитать изображение по пути: {image_path}") # Возвращаем пустой список и None в случае ошибки return [], None # Создаем копию изображения для рисования img_copy = img.copy() detections = [] results = model(image_path) # Проверяем, что результаты есть if results is None or not results: return detections, img_copy # Итерируемся по результатам for result in results: # Получаем boxes boxes = result.boxes # Если boxes нет, то переходим к следующему результату if boxes is None or not boxes : continue for box in boxes: # Пропускаем пустой box if box is None: continue # Получаем id класса class_id = int(box.cls) # Получаем уверенность confidence = float(box.conf) # Получаем координаты bounding box x1, y1, x2, y2 = map(int, box.xyxy[0]) # Проверяем, является ли это булыжником с достаточной уверенностью if class_id == 1 and confidence >= confidence_threshold: # Добавляем в detections detections.append([x1, y1, x2, y2, confidence]) # Рисуем прямоугольник cv2.rectangle(img_copy, (x1, y1), (x2, y2), (0, 255, 0), 2) return detections, img_copydef find_largest_cobblestone(detections): """Находит наибольший булыжник из списка обнаружений.""" if not detections: return None # Площадь прямоугольника largest_cobblestone = max(detections, key=lambda x: (x[2] - x[0]) * (x[3] - x[1])) return largest_cobblestonedef turn_to_cobblestone(controller, cobblestone_bbox, img): """Поворачивает игрока к булыжнику.""" if cobblestone_bbox is None or img is None: return # Расчет поворота center_x = controller.screen_width / 2 center_y = controller.screen_height / 2 bbox_center_x = (cobblestone_bbox[0] + cobblestone_bbox[2]) / 2 bbox_center_y = (cobblestone_bbox[1] + cobblestone_bbox[3]) / 2 degrees_x = (bbox_center_x - center_x) * (controller.fov / controller.screen_width) degrees_y = (bbox_center_y - center_y) * (controller.fov / controller.screen_height) # Рисуем точку # Красная точка cv2.circle(img, (int(bbox_center_x), int(bbox_center_y)), 5, (0, 0, 255), -1) controller.turn(degrees_x, degrees_y)def show_image(image): """Сохраняет и отображает изображение.""" cv2.imshow("Detection", image) # Отображаем изображение (необходима пауза, чтобы окно успело отрисоваться) cv2.waitKey(1)if __name__ == "__main__": print("Скрипт запущен. Для завершения нажмите Ctrl+C.") # Загрузка модели YOLO model = YOLO(MODEL_PATH) # Получение размеров окна игры window_width, window_height = get_window_size() if window_width is None or window_height is None: print("Не удалось получить размеры окна. Завершение работы.") exit() # Инициализация контроллера Luanti controller = Controller(window_width, window_height) try: while True: # Делаем скриншот controller.take_screenshot() # Ожидаем сохранения скриншота time.sleep(1) screenshot_path = get_screenshot_path() if screenshot_path is None: print("Скриншот не найден.") continue try: # Обнаружение булыжников detections, img_with_mask = detect_cobblestone(screenshot_path, model) print(f"Найдено булыжников: {len(detections)}") for detection in detections: x1, y1, x2, y2, confidence = detection print(f"Булыжник: ({x1}, {y1}, {x2}, {y2}), уверенность = {confidence:.2f}") largest_cobblestone = find_largest_cobblestone(detections) if largest_cobblestone is not None: print(f"Поворот к наибольшему булыжнику: {largest_cobblestone}") turn_to_cobblestone(controller, largest_cobblestone, img_with_mask) show_image(img_with_mask) except Exception as e: print(f"Ошибка: {e}") # Пауза между итерациями time.sleep(5) except KeyboardInterrupt: print("\nСкрипт завершен пользователем.")
Немного побегав и поняв, что модель, в принципе, работает, перейдем к следующему этапу обучения.
Для обучения второй модели будем использовать те же снимки, но аннотацию сделаем не типа border box, а сегментационную. То есть объекты будут выделены не прямоугольниками, а точно по контуру. Для этого есть метод автоматической аннотации.
Создай новый каталог dataset_seg
со структурой, идентичной структуре каталога dataset
. Внутри создай каталог img
со снимками, которые мы будем размечать. Теперь можно сделать еще сотню снимков — чем больше разнообразного материала, тем лучше будет обучение модели.
Запусти в терминале скрипт auto_annotate.
для автоаннотации снимков, расположенных в каталоге ~/
.
auto_annotate.py
import osfrom ultralytics.data.annotator import auto_annotatefrom pathlib import Path# Импортируем переменные из config.pyfrom config import BASE_DIR, MODEL_PATHdef annotate_images(images_dir, output_dir): image_files = [] for filename in os.listdir(images_dir): if filename.endswith('.png'): image_files.append(os.path.join(images_dir, filename)) for image_path in image_files: auto_annotate( data = Path(image_path), output_dir = output_dir, det_model = MODEL_PATH, sam_model = 'sam_b.pt' )if __name__ == "__main__": # Директория со снимками images_dir = os.path.join(BASE_DIR, "dataset_seg", "img") output_dir = images_dir if not os.path.exists(images_dir): print(f"Ошибка: Папка '{images_dir}' не найдена.") exit() print(f"Начинаю автоматическую аннотацию изображений в '{images_dir}'.") annotate_images(images_dir, output_dir) print(f"Автоматическая аннотация завершена. Файлы меток сохранены в '{output_dir}'.")
При запуске этого скрипта скачивается и используется предобученная модель sam_b.
, которая ищет внутри border box объект и обводит его по контуру. Сам border box на снимке ищет обученная нами модель detect
.
Результатом работы будут файлы аннотации для всех изображений в том же каталоге, что и сами картинки. Снимки разложи по каталогам в такой же пропорции, что и для первой модели.
Для наглядности наложим маски аннотаций на файлы и посмотрим, что выделили наши модели. Для этого скопируй снимки и файлы с аннотациями в каталог ~/
и выполни скрипт visualize_seg_mask.
.
visualize_seg_mask.py
import cv2import osimport numpy as npimport re# Импортируем переменные из config.pyfrom config import BASE_DIRdef visualize_segmentation(image_path, labels_path, output_path): """Визуализирует маски сегментации на изображении.""" img = cv2.imread(image_path) if img is None: print(f"Ошибка: Не удалось загрузить изображение {image_path}") return height, width, _ = img.shape # Создаем пустую маску mask_img = np.zeros_like(img, dtype=np.uint8) if not os.path.exists(labels_path): print(f"Предупреждение: Файл меток {labels_path} не найден.") return with open(labels_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) < 4: print(f"Предупреждение: Неверное кол-во элементов в файле {labels_path}: {line}") continue try: # 1-е значение в строке — класс class_id = int(parts[0]) # Все остальные — координаты coords = [float(x) for x in parts[1:]] # Преобразуем координаты в пиксели coords = np.array(coords).reshape(-1, 2) * np.array([width, height]) coords = coords.astype(np.int32) # Заполняем маску цветом для отображения cv2.fillPoly(mask_img, [coords], (0, 255, 0)) except ValueError as e: print(f"Ошибка при обработке строки в файле {labels_path}: {line}, ошибка: {e}") except Exception as e: print(f"Неизвестная ошибка при обработке строки в файле {labels_path}: {line}, ошибка: {e}") # Объединяем исходное изображение и маску overlay = cv2.addWeighted(img, 0.7, mask_img, 0.3, 0) cv2.imwrite(output_path, overlay) print(f"Визуализация сохранена в {output_path}")if __name__ == "__main__": # Директория с изображениями images_dir = os.path.join(BASE_DIR, "dataset_seg", "check_mask") labels_dir = images_dir if not os.path.exists(images_dir): print(f"Ошибка: Папка '{images_dir}' не найдена.") exit() # Регулярное выражение для проверки имени файла screenshot_regex = re.compile(r"(\w+)\.png") for filename in os.listdir(images_dir): match = screenshot_regex.match(filename) if match: image_path = os.path.join(images_dir, filename) # Формируем имя файла меток labels_name = os.path.splitext(filename)[0] + ".txt" # Путь к файлу меток labels_path = os.path.join(labels_dir, labels_name) output_name = os.path.splitext(filename)[0] + "_masked.png" output_path = os.path.join(images_dir, output_name) visualize_segmentation(image_path, labels_path, output_path)
Результат должен выглядеть так, как на скриншотах ниже.


Снимки с самыми неудачными масками поместим в каталог ~/
.
Просмотри каждый файл: нет ли таких, где маска наложилась не на весь объект целиком или пометила области, в которых нет булыжника? Если такое попадется, то считай, что это плохая разметка. Такие файлы нельзя использовать для обучения модели, но они пригодятся для тестирования. Перемести такие файлы в каталог ~/
.
После обучения сегментационной модели будет интересно сравнить, как изменилось качество обнаружения. Для этого надо будет в файле автоаннотации auto_annotate.
заменить строку sam_model='sam_b.
строкой sam_model='Путь
.
Теперь напишем конфигурационный файл ~/
.
Листинг dataset.yaml
# train и val пути к изображениямtrain: /home/ksandr/minebot/dataset_seg/train_imagesval: /home/ksandr/minebot/dataset_seg/val_imagestest: /home/ksandr/minebot/dataset_seg/test_images# Количество классовnc: 1# Имена классовnames: ['cobblestone']
Все просто, поменялись только пути к каталогам.
Скрипт для запуска обучения сегментационной модели — train_segment_model.
.
train_model_seg.py
import osfrom ultralytics import YOLO# Импортируем переменные из config.pyfrom config import BASE_DIR# 1. Загрузка модели# Загрузка предобученной модели yolov8n-segmodel = YOLO("yolov8n-seg.pt")# Путь к файлу dataset.yamldata_path = os.path.join(BASE_DIR, "dataset_seg", "dataset.yaml")# 2. Обучение моделиmodel.train( data = data_path, # Количество эпох обучения epochs = 150, # Размер пакета batch = 16, # Размер изображений imgsz = 640,)

Отличие от предыдущего файла в том, что мы скачиваем сегментационную предобученную модель, используем другой тип аннотации снимков и увеличиваем количество эпох. Если первая детекционная модель нам была нужна только для разметки файлов аннотаций для обучения второй модели, то к обучению этой модели стоит отнестись со всей серьезностью, так как именно она и будет анализировать наши снимки.
Обучение этой модели займет в несколько раз больше времени, чем обучение предыдущей. Так что опять идем смотреть сериал.
После окончания обучения (хорошо, если не сериала) у нас появится каталог ~/
, содержащий две модели.
Для начала мы опять проверим, как модель «видит» булыжник. Запусти игру, запусти скрипт experiment_3.
в терминале, переключи фокус на окно с игрой. Если у тебя Minecraft, снова раскомментируй нужную и закомментируй ненужную строки в начале скрипта и в начале метода def
.
experiment_3.py
import pyautoguiimport timeimport cv2import numpy as npimport osimport reimport mathfrom ultralytics import YOLO# Импортируем переменные из config.pyfrom config import SCREENSHOT_PATH, SCREENSHOT_PATTERN, MODEL_PATH# Для Luantifrom luanti.controller import LuantiController as Controller# Для Minecraft# from minecraft.controller import MinecraftController as Controllerdef get_window_size(): """Получает размер окна игры.""" # Делаем снимок, чтобы получить размер окна # Для Luanti pyautogui.press('f12') # Pyautogui.press('F2') # Для Minecraft time.sleep(1) # Ищем файл снимка files = os.listdir(SCREENSHOT_PATH) screenshot_file = None for file in files: if re.match(SCREENSHOT_PATTERN, file): screenshot_file = file break if not screenshot_file: print(f"Предупреждение: Файл скриншота не найден по пути: {SCREENSHOT_PATH}") return None, None full_screenshot_path = os.path.join(SCREENSHOT_PATH, screenshot_file) # Получаем размер окна screenshot = cv2.imread(full_screenshot_path) if screenshot is None: print(f"Предупреждение: Не удалось прочитать изображение по пути: {full_screenshot_path}") return None, None window_height, window_width, _ = screenshot.shape print(f"Размер окна игры: {window_width}x{window_height}") return window_width, window_heightdef get_screenshot_path(): """Находит путь к последнему скриншоту.""" files = os.listdir(SCREENSHOT_PATH) screenshot_files = [f for f in files if re.match(SCREENSHOT_PATTERN, f)] if screenshot_files: # Сортируем по времени (последний файл первым) screenshot_files.sort(reverse=True) return os.path.join(SCREENSHOT_PATH, screenshot_files[0]) else: return Nonedef _prepare_mask(mask): """Подготавливает маску для обработки.""" mask = mask.cpu().numpy() # Делаем маску черно-белой mask = (mask * 255).astype(np.uint8) # Находим все ненулевые точки в маске coords = cv2.findNonZero(mask) return mask, coordsdef _process_mask(mask, width, height): """Обрабатывает маску, вычисляет центр и угол поворота.""" mask, coords = _prepare_mask(mask) # Проверяем, что точки найдены if coords is None: return None, None # Получаем координаты X точек x_coords = coords[:, :, 0] # Получаем координаты Y точек y_coords = coords[:, :, 1] # Вычисляем центр маски center_x = np.mean(x_coords) center_y = np.mean(y_coords) # Вычисляем угол поворота # Фиксированная точка у ног бота (середина внизу изображения) start_x = width // 2 # Фиксированная точка у ног бота (середина внизу изображения) start_y = height dx = center_x - start_x dy = center_y - start_y angle = math.degrees(math.atan2(dx, -dy)) return angle, (center_x, center_y)def _overlay_mask(image, mask, center): """Отрисовывает маску на изображении.""" mask, coords = _prepare_mask(mask) # Проверяем, что точки найдены if coords is None: return image # Приводим координаты в нужный формат coords = coords.reshape(-1, 2).astype(np.int32) # Создаем пустую маску mask_img = np.zeros_like(image, dtype=np.uint8) # Заполняем маску цветом для отображения cv2.fillPoly(mask_img, [coords], (0, 255, 0)) # Рисуем центр маски cv2.circle(mask_img, (int(center[0]), int(center[1])), 5, (0, 0, 255), -1) # Объединяем изображения overlay = cv2.addWeighted(image, 0.7, mask_img, 0.3, 0) return overlaydef detect_cobblestone(image_path, model): """Обнаруживает булыжники на изображении и возвращает угол поворота.""" # Проверяем существование файла if not os.path.exists(image_path): print(f"Предупреждение: Файл изображения '{image_path}' не существует.") return None, None, False # Загружаем изображение image = cv2.imread(image_path) # Проверяем, что изображение загрузилось if image is None: print(f"Предупреждение: Не удалось прочитать изображение '{image_path}'.") return None, None, False # Сохраняем высоту и ширину height, width = image.shape[:2] angles = [] processed_image = image.copy() # Получаем результаты сегментации results = model(image) # Проверяем, что объекты найдены if not results or not results[0].masks: print(f"Предупреждение: Объекты не найдены на изображении '{image_path}'.") # Возвращаем None для угла и False для обнаружения return image, None, False # Обходим все найденные маски for mask_data in results[0].masks.data: try: angle, center = _process_mask(mask_data, width, height) if angle is not None: angles.append(angle) processed_image = _overlay_mask(processed_image, mask_data, center) except Exception as e: print(f"Ошибка: {e}") if len(angles) > 0: # Возвращаем изображение, углы и True return processed_image, angles, True else: # Возвращаем изображение, None и False return processed_image, None, Falsedef turn_to_cobblestone(controller, angles): """Поворачивает игрока к ближайшему булыжнику.""" if angles is None: return # Выбираем угол с минимальным абсолютным значением для ближайшего булыжника best_angle = min(angles, key=abs) # Поворачиваем игрока controller.turn(best_angle, 0)def show_image(image): """Сохраняет и отображает изображение.""" cv2.imshow("Detection", image) # Отображаем изображение (необходима пауза, чтобы окно успело отрисоваться) cv2.waitKey(1)if __name__ == "__main__": print("Скрипт запущен. Для завершения нажмите Ctrl+C.") # Загрузка модели YOLO model = YOLO(MODEL_PATH) # Получение размеров окна игры window_width, window_height = get_window_size() if window_width is None or window_height is None: print("Не удалось получить размеры окна. Завершение работы.") exit() # Инициализация контроллера игры controller = Controller(window_width, window_height) try: while True: # Делаем скриншот controller.take_screenshot() # Ожидаем сохранения скриншота time.sleep(1) screenshot_path = get_screenshot_path() if screenshot_path is None: print("Скриншот не найден.") continue try: # Обнаружение булыжников img_with_mask, angles, is_detected = detect_cobblestone(screenshot_path, model) show_image(img_with_mask) if is_detected: print(f"Углы поворота: {[f'{angle:.2f}' for angle in angles]}") turn_to_cobblestone(controller, angles) else: print("Булыжник не обнаружен") except Exception as e: print(f"Ошибка: {e}") # Пауза между итерациями time.sleep(5) except KeyboardInterrupt: print("\nСкрипт завершен пользователем.")


Запустив скрипт и подойдя к булыжнику, мы должны увидеть новое окно, на котором маской будет выделен распознанный объект.
Использование
И вот после всех подготовок мы добрались до самой интересной части. Давай попробуем научить бота передвигаться по дорожкам из текстур распознанного нами булыжника.
Наша стратегия будет такой:
- Ищем верхнюю границу контура самого большого объекта, который нашла наша модель.
- Строим вектор из середины нижней границы нашего снимка в середину «верхней» границы контура. Это будет то направление, куда должен повернуться наш бот.
- Высчитываем угол между вектором и вертикалью, а потом переводим его из градусов в пиксели по формуле, описанной в методе
_calculate_pixels_for_degrees(
в файле) controller.
.py - Для поворота используем метод
turn
нашего контроллера, управляющего ботом.
Запусти игру, запусти скрипт detect_and_turn.
в терминале, переключи фокус на окно с игрой.
В коде снова есть строки, которые различаются для Minecraft и Luanti: в начале файла и в начале метода get_window_size(
.
detect_and_turn.py
import pyautoguiimport timeimport cv2import numpy as npimport osimport reimport mathfrom ultralytics import YOLOfrom config import SCREENSHOT_PATH, SCREENSHOT_PATTERN, MODEL_PATH# Для Luantifrom luanti.controller import LuantiController as Controller# Для Minecraft# from minecraft.controller import MinecraftController as Controllerdef get_window_size(): """Получает размер окна игры.""" # Делаем снимок, чтобы получить размер окна # Для Luanti pyautogui.press('f12') # Pyautogui.press('F2') # Для Minecraft time.sleep(1) files = os.listdir(SCREENSHOT_PATH) screenshot_file = next((f for f in files if re.match(SCREENSHOT_PATTERN, f)), None) if not screenshot_file: print(f"Предупреждение: Файл скриншота не найден по пути: {SCREENSHOT_PATH}") return None, None full_screenshot_path = os.path.join(SCREENSHOT_PATH, screenshot_file) screenshot = cv2.imread(full_screenshot_path) if screenshot is None: print(f"Предупреждение: Не удалось прочитать изображение по пути: {full_screenshot_path}") return None, None window_height, window_width, _ = screenshot.shape print(f"Размер окна игры: {window_width}x{window_height}") return window_width, window_heightdef get_screenshot_path(): """Находит путь к последнему скриншоту.""" files = os.listdir(SCREENSHOT_PATH) screenshot_files = [f for f in files if re.match(SCREENSHOT_PATTERN, f)] if screenshot_files: screenshot_files.sort(reverse=True) return os.path.join(SCREENSHOT_PATH, screenshot_files[0]) return Nonedef _delete_png_files(directory): """Удаляет все PNG-файлы из указанной директории.""" for filename in os.listdir(directory): if filename.lower().endswith(".png"): file_path = os.path.join(directory, filename) try: os.remove(file_path) except Exception as e: print(f"Предупреждение: Не удалось удалить файл '{filename}': {e}")def _find_and_draw_contours(image, mask, width, height, draw_vector=False, vector_target=None): """Находит и отрисовывает контуры на изображении.""" mask = mask.cpu().numpy() mask = (mask * 255).astype(np.uint8) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: print("Отладочная информация: Контуры не найдены.") return None, image, None largest_contour = max(contours, key=cv2.contourArea) min_y = min(point[0][1] for point in largest_contour) top_points = [(x, y) for x, y in largest_contour[:, 0] if y == min_y] if not top_points: print("Отладочная информация: Верхние точки контура не найдены.") return largest_contour, image, None mid_x = sum(x for x, _ in top_points) // len(top_points) top_point = (mid_x, min_y) start_x, start_y = width // 2, height cv2.drawContours(image, [largest_contour], -1, (255, 0, 0), 2) if top_points: cv2.polylines(image, [np.array(top_points)], isClosed=False, color=(0, 255, 0), thickness=3) if draw_vector and vector_target: if 0 <= vector_target[0] < width and 0 <= vector_target[1] < height: cv2.arrowedLine(image, (start_x, start_y), vector_target, (0, 255, 0), 2, tipLength=0.2) else: print(f"Отладочная информация: Недопустимые координаты vector_target: {vector_target}") return largest_contour, image, top_pointdef _calculate_turn_angle(target_point, width, height): """Вычисляет угол поворота к целевой точке.""" start_x, start_y = width // 2, height dx = target_point[0] - start_x dy = target_point[1] - start_y return math.degrees(math.atan2(dx, -dy))def _process_detection(image, model, width, height): """Обрабатывает обнаружение булыжника на изображении.""" processed_image = image.copy() largest_contour_area = 0 largest_contour_top_point = None results = model(image) if not results or not results[0].masks: print("Предупреждение: Объекты не найдены на изображении.") return processed_image, None, False for mask_data in results[0].masks.data: try: contour, processed_image, top_point = _find_and_draw_contours(processed_image, mask_data, width, height) if contour is None or top_point is None: print("Отладочная информация: Контур или верхняя точка не найдены.") continue contour_area = cv2.contourArea(contour) if contour_area > largest_contour_area: largest_contour_area = contour_area largest_contour_top_point = top_point except Exception as e: print(f"Ошибка при обработке контура: {e}") if largest_contour_top_point: angle = _calculate_turn_angle(largest_contour_top_point, width, height) print(f"Угол поворота: {angle:.2f}") _find_and_draw_contours(processed_image, results[0].masks.data[0], width, height, True, largest_contour_top_point) return processed_image, angle, True print("Отладочная информация: Ни один контур не был обработан.") return processed_image, None, Falsedef detect_cobblestone(image_path, model, width, height): """Обнаруживает булыжники на изображении и возвращает угол поворота.""" if not os.path.exists(image_path): print(f"Предупреждение: Файл изображения '{image_path}' не существует.") return None, None, False image = cv2.imread(image_path) if image is None: print(f"Предупреждение: Не удалось прочитать изображение '{image_path}'.") return None, None, False return _process_detection(image, model, width, height)def turn_to_cobblestone(controller, angle): """Поворачивает игрока к ближайшему булыжнику.""" if angle is not None: controller.turn(angle, 0)def show_image(image): """Сохраняет и отображает изображение.""" cv2.imshow("Detection", image) cv2.waitKey(1)if __name__ == "__main__": print("Скрипт запущен. Для завершения нажмите Ctrl+C.") model = YOLO(MODEL_PATH) window_width, window_height = get_window_size() if window_width is None or window_height is None: print("Не удалось получить размеры окна. Завершение работы.") exit() controller = Controller(window_width, window_height) turn_counter = 0 _delete_png_files(SCREENSHOT_PATH) try: while True: controller.take_screenshot() # Ожидаем сохранения скриншота time.sleep(0.1) screenshot_path = get_screenshot_path() if screenshot_path is None: print("Скриншот не найден. Окно с игрой должно быть в фокусе.") # Ожидаем сохранения скриншота time.sleep(2) continue try: img_with_mask, angle, is_detected = detect_cobblestone(screenshot_path, model, window_width, window_height) show_image(img_with_mask) if is_detected: turn_counter = 0 print(f"Угол поворота: {angle:.2f}") controller.move_forward(1) turn_to_cobblestone(controller, angle) else: turn_counter += 1 controller.turn(45, 0) print("Булыжник не обнаружен, поворот на 45 градусов.") time.sleep(2) if turn_counter > 7: controller.send_chat_message("Я потерялся, мне нужна помощь!") time.sleep(60) except Exception as e: print(f"Ошибка: {e}") finally: _delete_png_files(SCREENSHOT_PATH) except KeyboardInterrupt: print("\nСкрипт завершен пользователем.")
Построй замкнутую дорожку шириной в три блока, со стороной ~15 блоков. Поставь бота на любую ее часть так, чтобы он мог видеть дорожку. Запусти скрипт, и бот должен идти по твоей дорожке.
Выводы
Мы научились базовым принципам, которые помогут создавать алгоритмы на основе машинного зрения. Применения могут быть не только в Minecraft, да и не только в играх — используя тот же метод, можно запрограммировать и настоящего робота, который будет ориентироваться по маркерам или QR-кодам.