Позволить модели обнаружения объектов управлять эмулятором DS, чтобы стать экспертом в мини-игре Super Mario 64 DS "Wanted!"

Я обучил модель обнаружения объектов управлять своим компьютером, чтобы бесконечно играть в мини-игру, запущенную в эмуляторе DS.

Посмотреть первый час ее прохождения можно здесь. Весь код доступен на GitHub.

Прелюдия — подарок на мой восьмой день рождения

На мой восьмой день рождения бабушка щедро подарила мне самую важную технику в моей жизни — Nintendo DS. На это она купила мне две игры, чтобы я сразу начал играть — Pokémon Dash (в детстве я не осознавал, что это одна из самых низко оцененных спин-офф игр про покемонов во франшизе — я был просто счастлив увидеть Пикачу). на обложке) и Super Mario 64 DS, римейк невероятно успешной игры Super Mario 64, изначально созданной для Nintendo 64.

Несмотря на то, что в игру ждала полноценная настоящая игра про Марио, вместо этого меня привлекли мини-игры «Rec Room». И из всех этих игр одна привлекла внимание восьмилетнего меня больше всех остальных: «Разыскивается!»

"В розыске!" лучше всего описать как более простую версию «Где Уолдо»; игроку предлагается щелкнуть по определенному персонажу в море лиц в течение установленного срока.

Перенесемся на полтора десятилетия вперед. В настоящее время я нахожусь в перерыве между работами, и моя следующая должность откроется через две недели. Вместо того, чтобы расслабиться и перезарядиться, меня снова потянуло к DS, а именно к этому «Wanted!». Мини-игра.

Но теперь, когда я взрослый (и специалист по данным), конечно же, мой интерес к этой игре гораздо глубже, намного сложнее, гораздо более… зрелый. Неизбежно возник вопрос: могу ли я обучить модель машинного обучения, чтобы преуспеть в розыске «Разыскивается!» играть и автономно играть в игру навсегда?

Игра

Разыскивается! — это так просто, как только можно. На верхнем экране показан плакат с изображением одного из четырех персонажей — Марио, Луиджи, Варио или Йоши. Нижний экран показывает несколько лиц (иногда накладывающихся друг на друга, иногда странно обрезанных, а иногда все движущихся в разных направлениях), причем только одно из лиц принадлежит разыскиваемому персонажу.

Начальные уровни игры относительно просты…

… но быстро становятся более сложными.

… и становится еще сложнее, когда лица начинают двигаться.

Игрок начинает всего за десять секунд, чтобы найти персонажа. Правильные клики добавляют пять секунд ко времени игрока, а неправильные клики вычитают десять секунд.

Уникальные проблемы, которые следует учитывать

Некоторые объекты сильно перекрыты, что всегда является проблемой при обнаружении объектов, но это была не самая большая проблема, с которой я столкнулся в этом проекте. Скорее, самой большой проблемой была задержка, особенно когда лица персонажей двигались (как в GIF выше).

Если для обработки изображения и поиска символа требуется две секунды от начала до конца, к тому времени, когда мы нажмем на эту область, лицо может настолько сместиться за пределы своего исходного положения, что в конечном итоге мы щелкнем совершенно другой символ и потеряем время. . "В розыске!" работает со скоростью 30 кадров в секунду, а это означает, что если мы пропустим две секунды, наш персонаж сместится 60 раз. Это особенно заметно, когда персонаж исчезает с одной стороны экрана и снова появляется с другой.

Чтобы наша модель могла эффективно выполнять эти движущиеся этапы, мы должны принять все необходимые решения, чтобы сократить время, необходимое для обработки изображения, независимо от того, насколько болезненным станет этот процесс (предвидение).

План

Что касается компонента машинного обучения, то эта проблема кажется пустяком для модели обнаружения объектов. Поскольку мы хотим, чтобы это работало в реальном времени, я подумал, что лучше выбрать модель обнаружения объектов, оптимизированную для реального времени. Мой текущий вариант — YOLOv5, и он полностью реализован в PyTorch. Несмотря на противоречивость, этот репозиторий невероятно упрощает обучение, проверку и настройку модели YOLO (я понимаю, что YOLOv1–4 работает быстро, но… да ладно, его обязательно нужно писать на C + CUDA?!) .

Чтобы обучить модель обнаружения объектов, нам нужны помеченные данные, что является сутью любого побочного проекта, который предпринимает каждый специалист по данным. Поскольку здесь только я, я сначала подумал, что можно потратить пару дней на маркировку данных. Ничего страшного, верно?

Большое дело

К счастью, этот проект напомнил мне, насколько я презираю маркировку данных. Теперь я уверен, что скорее съем тарелку гвоздей, чем маркирую данные в течение часа. Серьезно.

Через 25 минут я был только на третьем скриншоте.

Когда я уже был готов сдаться, меня осенило: может быть, это было… в конце концов, очень просто? Каждое лицо происходит от одного спрайта лица этого персонажа — оно никогда не изменит цвета, формы, выражения и т. д. — это просто одно изображение для каждого персонажа. На выбор всего четыре предустановленных символа, а фон — просто черный экран.

Могу ли я воссоздать это самостоятельно? Гипотетически я мог бы использовать Matplotlib для построения целой группы лиц персонажей на черном экране, отслеживания расположения символов, а затем сохранения как этого изображения, так и списка всех нанесенных символов иих ограничивающих прямоугольников, что позволило бы мне полностью пропустить процесс маркировки данных.

Боль Matplotlib

Хорошая новость: я был прав, говоря, что обход шага маркировки данных возможен.

Плохая новость: несмотря на то, что я много лет использую его, как только я попытался сделать что-то конкретное в Matplotlib, мой мозг отделился от моего тела и забыл каждую деталь API.

Это становится еще сложнее, когда никто из StackOverflow не задал вопрос о сюжете, который вы пытаетесь построить. Документация, кажется, бесконечно зацикливается от функции к функции, не сообщая вам, что означает аргумент.

Имея в своем распоряжении все спрайты, используемые в игре, всего два мучительных дня спустя мне наконец удалось собрать воедино код, необходимый для создания этих макетов скриншотов игры. Рядом они выглядят одинаково по качеству. Хороший!

Чтобы обучить максимально надежную модель, я попытался оживить эти изображения с помощью различных небольших уловок и хаков. К наиболее примечательным из них относятся следующие:

  • Я хотел, чтобы лица персонажей располагались друг над другом, как в оригинальной игре, но не так, чтобы лицо одного персонажа было нанесено непосредственно поверх лица другого персонажа, так как было бы несправедливо требовать модель (или человека, не меньше) иметь рентгеновское зрение.
    Чтобы избежать этого, я установил «порог близости», который устанавливает ограничение в пикселях, насколько близко один персонаж может быть к другому. Я манипулировал этим значением, чтобы оно было немного более жестким, чем позволяет оригинальная игра, но не слишком жестким, чтобы я не мог сказать, какой персонаж был нарисован.
  • Matplotlib по умолчанию сделает логическую вещь и сохранит изображение таким образом, что ни одно лицо не будет обрезано. Однако в оригинальной игре персонажи иногда нанесены по краям экрана, так что игроку видна только часть их лица. Я не хотел, чтобы мой алгоритм обнаружения объектов обнаруживал только те символы, которые полностью находятся на экране, поэтому при сохранении изображения на диск я выбрал случайную часть границы изображения, чтобы полностью обрезать ее. Это привело к созданию изображений, на которых персонаж может находиться на краю границы, при этом видна только часть его лица. Аккуратный!
  • Если мы изменим размер окна эмулятора DS, чтобы он был больше или меньше, размеры лиц изменились. Если бы я сохранил все лица в обучающих данных одинакового размера, модель обнаружения объектов не смогла бы обнаружить лица, даже немного отличающиеся по размеру. Чтобы бороться с этим, я выбрал случайную величину для «увеличения» лиц персонажей при создании сюжетов. Это привело к набору обучающих данных с лицами персонажей разных размеров.

На следующем изображении показана установка «большого» лица персонажа:

Мой компьютер мог сгенерировать только около 15 000 изображений до того, как ядро ​​рухнуло, поэтому наш окончательный размер набора данных составляет 15 000 изображений. По моим приблизительным оценкам, мне потребовалось бы около 11 недель, чтобы вручную разметить набор данных такого размера, так что мои усилия Matplotlib того стоили!

Обучение модели

В репозитории YOLOv5 есть несколько предварительно обученных моделей, которые вы можете точно настроить, каждая из которых имеет разный размер и производительность. Я хотел максимально снизить задержку, сохраняя при этом достаточно приличную производительность, и после небольшого эксперимента я обнаружил, что модель YOLOv5s (small) работает.

Обучение модели YOLOv5s простое — одна строка в терминале и потом около часа ожидания. Вот эта единственная строка:

python train.py --data data.yaml --epochs 100 --weights '' --cfg yolov5s.yaml --batch-size -1

Лучшая новость заключается в том, что с последними версиями PyTorch я могу запустить модель непосредственно на моем устройстве M1 Mac, чтобы увеличить скорость работы в два раза (вместо того, чтобы просто оставить ее на процессоре). Это так же просто, как сказать:

model.to(torch.device('mps'))  # wow, so neat!

И это самый короткий раздел этой статьи! Фу!

Взлом верхнего экрана для экономии времени

После того, как наша обученная модель обнаружения объектов смогла обнаруживать символы на нижнем экране, мне нужно было сделать то же самое с верхним экраном, так как это подсказывало бы нам, какой символ мы на самом деле должны искать.

Сначала я ожидал, что это будет самая простая часть проекта — я мог бы просто использовать что-то вроде Сопоставление шаблонов OpenCV, поскольку лицо персонажа наверху всегда будет одним и тем же в разных раундах. Однако, когда я попробовал это, к моему ужасу, поиск одного символа занял почти… 280 мс?! Что?! Для контекста, моей модели обнаружения объектов YOLOv5 потребовалось около 30 мс, чтобы сделать выводы на экране с несколькими лицами, поэтому почти 9-кратное замедление для чего-то объективно более простого… странно.

Я мог бы просто обучить отдельную модель обнаружения объектов для верхнего экрана, но с четырьмя изображениями в тренировочном наборе это кажется излишним.

Вместо того, чтобы делать что-то действительно умное, я решил собрать решение, которое будет работать как можно быстрее. План состоял в том, чтобы выбрать только один статический пиксель на верхнем экране, получить значение его цвета и сравнить его с известным значением цветового пикселя для конкретного персонажа (определенный оттенок красного для Марио, желтый для Варио и т. д.).

Удивительно, но это простое решение сработало! Сопоставление шаблонов занимает 280 мс, модель обнаружения объектов — 30 мс, а это решение занимает… *проверки заметок*… 0,25 мс. Вау, теперь это ускорение стоит того, чтобы его взломать!

Программное управление моим компьютером

Перед началом этого проекта я знал, что использование Python для управления моим компьютером будет самой сложной частью. Раньше я работал с Matplotlib и YOLOv5, но мне никогда не приходилось использовать Python для управления своим компьютером. Чтобы это работало, мне нужно было закодировать два основных компонента:

  1. Возможность сделать скриншот окна на своем компьютере (в данном случае эмуляторе DS, на котором запущена игра).
  2. Возможность щелкнуть определенную область моего компьютера (щелкнуть правильное лицо).

Оказывается, в данном случае быть пользователем Mac — большая ошибка. Хотя пользователи Windows могут использовать удобную библиотеку, такую ​​как PyGetWindow, чтобы получить информацию об окне для создания снимка экрана, она не поддерживает MacOS.

После нескольких часов исследований я обнаружил, что единственный поддерживаемый способ сделать это — пройти через модуль Apple CoreGraphics, который написан на Objective-C (а не на Python). К счастью, кто-то создал Python-оболочку для этого фреймворка, pyobjc-framework-Quartz, но его документация… скудна, а это означает, что для каждой функции, которую мы хотим сделать, мы будем ссылаться на документацию по Objective-C. Уф.

Точно так же, хотя кажется, что намного проще сделать снимок экрана определенного окна (а не всего экрана) в Windows (может быть, поэтому они выбрали название операционной системы), Mac делает задачу гораздо более нетривиальной. . Некоторые функции, которые я пробовал, могли сделать снимок экрана только на основном дисплее, а не на внешнем мониторе; некоторые функции требовали, чтобы я сначала сохранял скриншот на диск, а затем загружал его обратно в память (процесс, который занимает гораздо больше времени); и некоторые функции просто… не работали.

(Не)к счастью, библиотека pyobjc-framework-Quartz снова спасла положение. Это не только позволило нам сделать скриншот окна на любом дисплее прямо в памяти, но и стало самой быстрой реализацией с почти 11-кратным ускорением по сравнению со следующей по скорости функцией. Боль того стоила!

Наконец, никого не удивишь, щелчок по определенной области экрана приводит нас к одному и тому же выводу: это проще в Windows, возможно на Mac (с более простыми в использовании библиотеками), но значительно быстрее, если мы используем собственный фреймворк CoreGraphics от Apple. . Отлично.

Интересная особенность, которую я обнаружил здесь, заключается в том, что в рамках Apple нет концепции «щелчка» — вместо этого вы должны отправить сигнал, чтобы нажать левую кнопку мыши вниз, а затем отправить другой сигнал, чтобы «нажать» левую кнопку мыши вверх (следовательно, имитация щелчка). Однако, если вы делаете это вплотную, это почему-то не щелчок. Путем проб и ошибок я обнаружил, что вам нужно ввести некоторую задержку между опусканием и подъемом мыши, чтобы она была зарегистрирована как щелчок. Около 50 мс было правильным размером буфера между этими двумя действиями, чтобы надежно работать как щелчок. Странный.

Собираем все вместе

Наконец-то пришло время играть в игру! Я играл в ПЗУ игры Super Mario 64 DS на эмуляторе DeSmuME Nintendo DS, одновременно запуская код Python в окне терминала рядом с ним. Вот как была настроена логика:

  1. Сделайте скриншот окна игры.
  2. Найдите на верхнем экране персонажа, которого мы должны искать на нижнем экране.
  3. Обрежьте изображение до нижнего экрана и пропустите это обрезанное изображение через нашу модель обнаружения объектов.
  4. Если персонаж обнаружен с достаточно высокой степенью достоверности, рассчитайте координаты этого персонажа в окне эмулятора и щелкните область.
  5. Подождите немного, пока начнется следующий раунд, и повторите!

Игра работает со скоростью 30 кадров в секунду, что означает, что каждый кадр остается на экране около 33 мс. По моим грубым подсчетам, мы должны определить в общей сложности около 200 мс на моем Macbook, что соответствует примерно шести кадрам игрового процесса. Это означает, что если персонаж движется, мы должны надеяться, что он останется в том же относительном месте на экране через шесть экранов, чтобы убедиться, что наш щелчок не промахнулся. Это будет работать?

Ответ: Да! Это полностью работает!

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

Не совсем тот удовлетворительный финал, о котором я мечтал, но, тем не менее, миссия выполнена. Время обновить таблицы лидеров!

В детстве эта игра казалась мне очень сложной. Будучи взрослым (как бы мне ни хотелось притворяться, что это не так), я все еще нахожу эту игру очень сложной. Я надеюсь, что мой восьмилетний «я» будет гордиться тем, что я наконец-то прошел эту игру (точнее, я сделал что-то, что победило эту игру). Это для тебя, Маленький Нейт! :)

Весь код, используемый в этом проекте, можно найти на странице GitHub здесь. Если у вас есть Mac, особенно с Apple Silicon, попробуйте! В любом случае, спасибо, что нашли время, чтобы прочитать эту статью! :)