- BrainTools - https://www.braintools.ru -

Как приручить AI-пиксель-арт

Как приручить AI-пиксель-арт - 1

За последние пару лет генеративные нейросети стали волшебной кисточкой для всего: концепт-артов, иконок, иллюстраций, обложек, аватаров, спрайтов… Особенно – пиксель-арта. В Midjourney, Stable Diffusion, Dall-E, Image-1 и в других моделях можно просто вбить:
“Pixel art goose with goggles in the style of SNES” — и получить шикарного пиксельного гуся за 10 секунд.

Но если ты пробовал вставить такого гуся в игру — ты уже знаешь боль [1].

Я решил вкопаться в эту тему поглубже и сделать open‑source‑инструмент, который автоматизирует превращение AI‑generated pixel art в pixel‑perfect pixel art.


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

Введение

Нейросеть не знает, что такое «пиксель». Современные модели, вроде Stable Diffusion, работают не с сеткой пикселей напрямую, а с латентным представлением [2] изображения в виде непрерывного шума. Они начинают с «тумана» и шаг за шагом приближаются к финальной картинке, добавляя детали, формы, цвета — но всё это происходит в непрерывном пространстве, где нет понятия дискретной сетки или фиксированной палитры.

Рассмотрим типичный AI Pixel Art:

С первого взгляда всё красиво. Но пиксели — липовые.

С первого взгляда всё красиво. Но пиксели — липовые.

Во-первых, у него неровная сетка

Как приручить AI-пиксель-арт - 3

Во-вторых, даже если мы выровняем сетку — мы увидим, что не все пиксели идеально ложатся в нее

Как приручить AI-пиксель-арт - 4

В-третьих, если мы визуализируем нашу палитру цветов, мы получим следующее:

Как приручить AI-пиксель-арт - 5

Как следствие, при попытке даунскейла через nearest neighbor мы получим примерно следующее

Как приручить AI-пиксель-арт - 6

Соответственно, чтобы решить нашу задачу, нам требуется автоматически:

  1. Определять размер пикселя

  2. Находить оптимальную сетку

  3. Формировать ограниченную палитру

  4. Даунскейлить без потерь

  5. Финально очищать от шума и артефактов

Этап 1: Поиск масштаба псевдо-пикселя (Scale Detection)

Здесь я использую edge‑aware detection: анализ градиентов Собеля [3] + голосование по тайлам.

Шаг 1: Выбор информативных тайлов

Разбиваю картинку на 3 × 3 и беру тайлы с высокой дисперсией.

 Пример: тайл 150×150, выбранный за высокую детализацию

Пример: тайл 150×150, выбранный за высокую детализацию

Шаг 2: Поиск границ через фильтр Собеля

Фильтр Собеля позволяет найти места с резкими переходами цветов — то есть потенциальные границы между псевдопикселями.

 Вертикальные границы (Sobel X)  и  Горизонтальные границы (Sobel Y)

Вертикальные границы (Sobel X) и Горизонтальные границы (Sobel Y)

Мы получаем двумерную карту “резкости” по X и Y, где яркие линии — это места, где картинка притворяется пиксельной.

Шаг 3: Генерация профиля

Теперь мы превращаем 2D-гистограмму границ в 1D-профиль: суммируем яркость по каждому столбцу (для X) и строке (для Y). Это даёт нам наглядную кривую, где пики указывают на предполагаемые линии сетки.

 Горизонтальный профиль с отмеченными пиками — предположительными границами «пикселей».

Горизонтальный профиль с отмеченными пиками — предположительными границами «пикселей».

Шаг 4: Выбор масштаба через голосование

Далее рассмотрим распределение расстояний между пиками. В большинстве тестовых картинок всплывает шаг 8/12/16/24 px; 43 px — просто хороший «демо‑случай».

 Анализ всей картинки: сотни измерений показывают доминирующий шаг — 43 пикселя.

Анализ всей картинки: сотни измерений показывают доминирующий шаг — 43 пикселя.

Для наглядности мы визуализируем границы между “псевдопикселями” — то есть места с наибольшими цветовыми переходами по всей картинке. Это помогает убедиться, что структура действительно регулярная, а выбранный масштаб соответствует реальному «рисунку» нейросети.

 Тёплые зоны — резкие границы, которые нейросеть создала между «пикселями». Их регулярность подтверждает корректность выбранного масштаба

Тёплые зоны — резкие границы, которые нейросеть создала между «пикселями». Их регулярность подтверждает корректность выбранного масштаба

Этап 2: Выравнивание сетки и умная обрезка

Итак, мы научились определять размер сетки, в которую нейросеть пыталась уложить «пиксели». В нашем случае — это 43×43 пикселя. Но одного масштаба недостаточно. Нам нужно понять, с какой точки сетку начинать, чтобы она совпадала с содержимым картинки. Сделаем это через следующий алгоритм:

  1. Преобразуем изображение в градации серого.

  2. Считаем границы — места, где изображение резко меняется, с помощью фильтра Собеля.

  3. Строим профили по строкам и столбцам — сколько “резкости” приходится на каждую линию.

  4. Перебираем все возможные сдвиги от 0 до scale – 1, и на каждом шаге оцениваем, насколько хорошо сетка совпадает с пиками в этих профилях.

  5. Выбираем такой сдвиг (x, y), при котором сетка максимально точно ложится на изображение.

function findOptimalCrop(grayMat, scale, cv) {
  const sobelX = new cv.Mat();
  const sobelY = new cv.Mat();
  
  try {
    cv.Sobel(grayMat, sobelX, cv.CV_32F, 1, 0, 3);
    cv.Sobel(grayMat, sobelY, cv.CV_32F, 0, 1, 3);

    const profileX = new Float32Array(grayMat.cols).fill(0);
    const profileY = new Float32Array(grayMat.rows).fill(0);
    const dataX = sobelX.data32F;
    const dataY = sobelY.data32F;
    
    for (let y = 0; y < grayMat.rows; y++) {
      for (let x = 0; x < grayMat.cols; x++) {
        const idx = y * grayMat.cols + x;
        profileX[x] += Math.abs(dataX[idx]);
        profileY[y] += Math.abs(dataY[idx]);
      }
    }

    const findBestOffset = (profile, s) => {
      let bestOffset = 0, maxScore = -1;
      for (let offset = 0; offset < s; offset++) {
        let currentScore = 0;
        for (let i = offset; i < profile.length; i += s) {
          currentScore += profile[i] || 0;
        }
        if (currentScore > maxScore) {
          maxScore = currentScore;
          bestOffset = offset;
        }
      }
      return bestOffset;
    };

    const bestDx = findBestOffset(profileX, scale);
    const bestDy = findBestOffset(profileY, scale);
    logger.log(`Optimal crop found: x=${bestDx}, y=${bestDy}`);
    return { x: bestDx, y: bestDy };

  } finally {
    sobelX.delete();
    sobelY.delete();
  }
}

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

После этого картинку можно обрезать так, чтобы её размеры делились на 43 без остатка, и каждый блок внутри сетки стал честной «ячейкой», которую можно анализировать и уменьшать без искажений.

Этап 3. Формирование палитры

Настоящий ретро‑пиксель‑арт — это всегда ещё и палитра. Например, в NES было 54 отображаемых цвета (из 64) и максимум 4 на тайл; в SNES — больше, но всё равно жёстко фиксировано.

Поэтому настоящая пиксельная графика — это всегда не только про сетку, но и про сдержанную палитру.

Для нас ограничение палитры решает сразу несколько задач:

  • Убирает шум — плавные градиенты, антиалиасинг, случайные оттенки.

  • Приближает к эстетике ретро — делает картинку «собранной» и аккуратной, как если бы её действительно рисовали для GBA или Mega Drive.

  • Также это позволяет нам проще подогнать спрайт к остальным ассетам нашей игры, если мы используем единую палитру, например, из lospec.com [4]

Для формирования палитры я использовал квантизацию через алгоритм WuQuant из библиотеки image-q [5]

Квантизация — это процесс сведения похожих цветов к ближайшему из фиксированной палитры.
Если в изображении было, скажем, 1500 зелёных оттенков, то после квантизации останется 4–8, и каждый пиксель будет перекрашен в ближайший.

Чистая квантизированная палитра из 16 цветов

Чистая квантизированная палитра из 16 цветов

Этап 4. Даунскейл по доминирующему цвету

Для каждого блока (например, 43×43 пикселя) мы проводим голосование:

  1. Выделяем блок — берём участок, соответствующий одному пикселю в финальной картинке

  2. Считаем цвета — сколько раз встречается каждый

  3. Выбираем победителя — если один цвет встречается чаще остальных (больше 5% от всех), он и становится цветом блока

  4. Если голосование не определило явного победителя, берём средний цвет (с усреднением по RGB)

Демо-блок

Демо-блок
Как приручить AI-пиксель-арт - 14

В отличие от наивного рескейла (где цвета смешиваются и рождают артефакты), такой блоковый подход делает каждый пиксель осмысленным.

Итог

В результате мы получаем чистое pixel-art изображение

Финальный спрайт 43x43 пикселя, сетка добавлена для наглядности

Финальный спрайт 43×43 пикселя, сетка добавлена для наглядности

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

Другие примеры

Здесь отключена привязка к сетке

Здесь отключена привязка к сетке
Как приручить AI-пиксель-арт - 17
Здесь вручную подобран размер палитры чтобы помочь избавиться от ненужных цветов

Здесь вручную подобран размер палитры чтобы помочь избавиться от ненужных цветов
Как приручить AI-пиксель-арт - 19

Опробовать инструмент самостоятельно можно тут [6]

Исходный код библиотеки и тула можно найти на Github [7]

Bonus: если тебе понравилась статья, возможно будет интересно подписаться на мой Telegram [8]

Автор: jenissimo

Источник [9]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/17585

URLs in this post:

[1] боль: http://www.braintools.ru/article/9901

[2] латентным представлением: https://habr.com/ru/articles/807405/

[3] анализ градиентов Собеля: https://ru.wikipedia.org/wiki/%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80_%D0%A1%D0%BE%D0%B1%D0%B5%D0%BB%D1%8F

[4] lospec.com: http://lospec.com

[5] image-q: https://github.com/ibezkrovnyi/image-quantization

[6] тут: https://jenissimo.itch.io/unfaker

[7] Github: https://github.com/jenissimo/unfake.js

[8] Telegram: https://t.me/ai_madness_diary

[9] Источник: https://habr.com/ru/articles/930462/?utm_campaign=930462&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100