Превращает «ИИ-пиксель-арт» в высоком разрешении — картинку, на которой пиксельная
сетка только нарисована, — в настоящий пиксель-арт,
где один логический пиксель равен одному пикселю файла.
слева исходник 1024×1024, справа результат — честные 42×42
python img2pixels.py art.png --allТолько numpy и Pillow. Ничего больше ставить не нужно.
Сетка в таких картинках неравномерная. У примера выше реальные шаги блоков идут 22, 25, 24, 24, 26, 28, 21… при среднем 24 — разброс ±15%.
Ровный гребень туда не ложится в принципе: расхождение с настоящими границами блоков доходит до 5 px при шаге 24, а к дальнему краю оси сетка уезжает почти на треть ячейки. На глаз это выглядит как «поплывшие» детали — у персонажа один глаз просто исчезает.
исходник · равномерная сетка · эластичная сетка
Поэтому сетка ищется эластичная: каждая линия ставится по реальным границам блоков, а не по среднему шагу. Ячейки получаются разной ширины — как в исходнике.
1. Грубая оценка шага. По периодичности краёв: профиль границ сворачивается по модулю пробного шага, что даёт амплитуду и фазу для всех фаз сразу. Нужна только чтобы задать диапазон перебора — опираться на неё нельзя, на примере выше она ошибается вдвое.
2. Стоимость полосы. Для полосы пикселей [a, a+L) считается, насколько
она неоднородна внутри каждого среза. У правильной полосы каждая строка почти
постоянна; у съехавшей — содержит границу блока и разваливается на два цвета.
Через префиксные суммы стоимость всех положений всех ширин берётся за один проход.
3. Динамическое программирование. Ищется разбиение оси ровно на N полос с
минимальной суммарной стоимостью и мягким штрафом за отклонение от среднего шага.
Перебор N даёт кривую цены, и штраф делает её U-образной: при N меньше
истинного полосы вынужденно накрывают по два блока, при большем — режут блоки
пополам. Минимум приходится ровно на истинное число ячеек.
4. Две страховки. Покрытие краёв ловит кратное измельчение сетки — при нём половина границ стоит внутри блоков, где никакого края нет. Условие «пиксель квадратный» подтягивает ту ось, у которой минимум цены оказался пологим: узкий спрайт с большими полями иначе сбивает оценку по короткой стороне.
5. Правка съехавших пикселей. Цвет ячейки берётся не из её серединки. Из первичной выборки собирается палитра спрайта, а затем цвет каждой ячейки переголосовывается по всей её площади: каждый пиксель исходника голосует за ближайший цвет палитры. Ячейка, которой при неровной сетке в серединку попала кромка соседа, всё равно получает цвет большинства. На примере выше так переголосовывается около четверти ячеек.
Метрика — tests/evaluate.py. Генератор tests/make_synth.py растягивает
спрайт из ограниченной палитры неравномерной сеткой (jitter до 15%), затем
размывает, ресайзит нецелым коэффициентом и жмёт в JPEG. Эталон известен точно,
поэтому считается доля пикселей, восстановленных с точностью до 3.0 в Lab.
| равномерная сетка | эластичная | |
|---|---|---|
| восстановлено пикселей, настроечный набор | 76.9% | 95.3% |
| восстановлено пикселей, отложенный набор | 84.2% | 92.0% |
| размер угадан, настроечный набор | 19% | 88% |
| размер угадан, отложенный набор | 17% | 100% |
| расхождение с исходником, Lab | 5.76 | 2.63 |
| доля испорченных ячеек | 9.9% | 3.4% |
Отложенный набор порождён другим сидом и в подборе параметров не участвовал.
python tests/make_synth.py -o tests/synth -n 16 --seed 1234
python tests/evaluate.py| Файл | Что это |
|---|---|
ИМЯ_px.png |
результат 1:1 — то, ради чего всё затевалось |
ИМЯ_px_x8.png |
превью с nearest-увеличением, чтобы разглядеть |
ИМЯ.aseprite |
индексный спрайт, палитра ровно из цветов арта |
ИМЯ_indexed.png |
indexed PNG с той же палитрой |
ИМЯ.gpl |
палитра для Aseprite / GIMP / Photoshop |
ИМЯ_grid.png |
исходник с наложенной найденной сеткой — для проверки |
.aseprite пишется напрямую по спецификации формата: установленный Aseprite
для этого не нужен. Файл открывается сразу в индексном режиме, без дефолтных
256 оттенков — только те цвета, что реально есть в арте (плюс слот
прозрачности, если включён -t).
python img2pixels.py [ФАЙЛЫ_ИЛИ_ПАПКИ...] [опции]
Без аргументов обрабатывает текущую папку. На Windows можно просто перетащить
файлы или папку на img2pixels.bat.
python img2pixels.py # всё в текущей папке
python img2pixels.py art.png # одну картинку
python img2pixels.py pics -o готово # всю папку, результат в ./готово
python img2pixels.py art.png --all # все форматы + отладочная сетка
python img2pixels.py art.png -t -a # прозрачный фон + .aseprite
python img2pixels.py art.png -s 64x64 # принудительный размер
python img2pixels.py art.png -c 16 # сжать палитру до 16 цветов| Опция | Что делает |
|---|---|
-o, --outdir |
куда складывать результат (по умолчанию ./out) |
-p, --preview N |
превью с увеличением в N раз, 0 — не сохранять (по умолчанию 8) |
-a, --aseprite |
сохранить .aseprite |
-i, --indexed |
сохранить indexed PNG |
-g, --palette |
сохранить палитру .gpl |
-d, --debug |
сохранить исходник с наложенной сеткой |
--all |
всё сразу: -a -i -g -d |
-t, --transparent |
сделать связный с краями фон прозрачным |
-c, --colors N |
сжать палитру до N цветов (k-means в Lab) |
-s, --size ШxВ |
задать итоговый размер вручную |
-q, --quiet |
не печатать подробности |
-help |
полная справка со всеми параметрами |
Остальные параметры (--lam, --span, --merge, --vote-inset, --cell,
--no-square, --no-repair и прочие) трогать обычно не нужно — смотрите
-help.
Программа печатает найденный размер и разброс шага. sd в районе 1–2 px при
шаге 24 — норма для ИИ-артов; sd больше трети шага означает, что сетку не нашли.
- Запустите с
-dи посмотритеИМЯ_grid.png: линии должны совпадать с границами блоков. - Размер вдвое больше нужного — поднимите
--lam(сетка станет ровнее) или задайте-sвручную. - Сетка «гуляет» сильнее, чем в исходнике — тоже
--lamвверх. - Сетка не поспевает за неровным исходником —
--lamвниз, до 1.0. - Потерялись одиночные пиксели-детали — уменьшите
--vote-insetдо 0 или отключите правку через--no-repair.
- Картинка должна быть растянутой. Если на входе уже готовый пиксель-арт
1:1, находить нечего: минимальный шаг ограничен
--min-cell(2.5 px). - Если исходник не пиксель-арт, а обычная иллюстрация с пиксельной текстурой,
под текстурой нет структуры блоков. Результат честно отражает текстуру, но
полноценным спрайтом не станет, и размер определяется неустойчиво — задавайте
-sруками. - Предполагается, что сетка покрывает картинку целиком, без полей: границы ставятся от края до края. Обрезанный не по ячейке арт лучше подрезать.
- Побайтовое совпадение цвета с оригиналом — 6–17%: JPEG сдвигает цвета на 1–3 единицы, и палитра восстанавливается с этой погрешностью. Визуально это незаметно (порог 3.0 в Lab), но побитовой точности нет.
git clone https://github.com/AnatomikPerq/img2pixels
cd img2pixels
pip install -r requirements.txtPython 3.10+, numpy, Pillow.
In English
img2pixels converts high-resolution "AI pixel art" — images where the pixel
grid is merely painted on — into real pixel art at 1:1 resolution.
The catch is that these grids are not uniform: in the sample above the real block widths run 22, 25, 24, 24, 26, 28, 21… around a mean of 24. A regular comb drifts by a third of a cell across the image and destroys fine detail.
So the grid is recovered elastically. For every candidate strip of pixels the
tool measures how non-uniform that strip is within each scanline — a correct
strip is nearly constant per row, a misaligned one contains a block boundary and
splits into two colours. Dynamic programming then finds the partition of each
axis into exactly N strips minimising total cost, with a soft penalty for
deviating from the mean step. Sweeping N yields a U-shaped cost curve whose
minimum lands on the true cell count. Edge coverage guards against picking a
multiple of the real grid, and a square-pixel assumption fixes the axis whose
minimum is shallow.
Cell colours are decided by an area vote against the sprite palette rather than by sampling the cell centre, which repairs cells that drifted off their block.
Measured on synthetic data with known ground truth: 92.0% of pixels recovered
and 100% correct output size on a held-out set, versus 84.2% / 17% for a uniform
grid. Outputs 1:1 PNG, a nearest-neighbour preview, indexed PNG, a .gpl
palette, and a native .aseprite file written directly from the format spec —
indexed, with a palette containing exactly the colours used.
Requires only numpy and Pillow. Run python img2pixels.py -help.