
Лето, скоро отпуск — захотелось написать статью, которую просто кайф прочитать, и заодно попробовать что‑то новое. Для ИИ есть бенчмарки вроде HumanEval, где модель просят написать функцию на пару строк, есть задачи уровня «сделай мне todo‑лист на React». А что будет, если дать современным ИИ‑агентам по‑настоящему наукоёмкую задачу — реализовать алгоритм из статьи SIGGRAPH на Swift, без сторонних библиотек, — и потом честно сравнить, что получилось на выходе?
Для этого я взял алгоритм «Depixelizing Pixel Art» (Johannes Kopf, Dani Lischinski, SIGGRAPH 2011) — тот, который я когда‑то давно реализовывал на C++. Поставил одинаковую задачу реализовать на языке Swift разным агентам (Claude, Codex, Cursor, Cline, Antigravity, Kimi, Grok — на разных моделях). Условия просты — один промпт = одна реализация, без уточнений, указаний недочетов и итераций правок
Вот полный текст промпта:
Скрытый текст
## ЗаданиеТы выполняешь готовый план реализации алгоритма **«Depixelizing Pixel Art»**(J. Kopf, D. Lischinski, SIGGRAPH 2011). Статья лежит рядом: `pixel.pdf`.Следуй плану шаг за шагом, не пропускай шаги и не заменяй алгоритм другим.Если шаг невозможно выполнить точно — реализуй указанное в нём упрощение изафиксируй отклонение в `solution/NOTES.md`.### Шаг 0. Контракт- Прочитай `config.txt`: `language` — язык реализации, `os` — целевая ОС (для кроссплатформенного языка игнорируется), `upscale` — целочисленный множитель увеличения.- Весь код помести в папку `solution/`.- Обработай **каждый** файл из `input/`. Результат сохрани в `output/`, заменив в имени суффикс `_input` на `_output` (`smw_boo_input.png` → `smw_boo_output.png`), формат PNG.- Размер каждого результата: `(W*upscale) x (H*upscale)`, где `W x H` — размер оригинала.- Запуск — одной командой; команду опиши в `solution/README.md`.### Шаг 1. Подготовка проекта- Модули: конфиг, ввод/вывод изображений, граф похожести, эвристики, перестройка ячеек, сплайны, оптимизация, рендеринг, точка входа.- Зависимости — только базовые: чтение/запись PNG и математика. **Запрещены** готовые реализации апскейла/векторизации (hqx, xBR, Scale2x/ScaleNx, EPX, 2xSaI, potrace, функции масштабирования из opencv/PIL как итоговый результат).- Загрузка изображения в RGB. Если есть альфа-канал: полностью прозрачные пиксели считай отдельным самостоятельным «цветом».### Шаг 2. Граф похожести (статья, §3.2, начало)- Узел на каждый пиксель, рёбра ко всем 8 соседям (включая диагонали).- Переведи цвета в YUV. Ребро удаляется, если цвета «непохожи»: `|dY| > 48/255` **или** `|dU| > 7/255` **или** `|dV| > 6/255`.- Самопроверка: на однотонной картинке граф остаётся полным; на шахматной доске 1x1 остаются только диагональные рёбра внутри каждого цвета.### Шаг 3. Разрешение пересекающихся диагоналей (§3.2)Для каждого блока 2x2, в котором присутствуют обе диагонали:- Если блок полностью связан (все четыре пикселя взаимно похожи) — удали обе диагонали: это плоско закрашенная область.- Иначе подсчитай голоса трёх эвристик за каждую из диагоналей: - **Кривые.** Если диагональ — часть цепочки узлов валентности 2, она принадлежит «кривой». Вычисли длины двух кривых, проходящих через две диагонали; голос за более длинную, вес = разность длин. - **Разреженные пиксели.** В окне 8x8 с центром в блоке сравни размеры компонент связности, к которым подключены концы каждой диагонали. Голос за диагональ компоненты **меньшего** размера (разреженный цвет — передний план), вес = разность размеров компонент. - **Острова.** Если у одного из концов диагонали валентность 1, её удаление создаст одинокий пиксель-«остров». Голос за сохранение этой диагонали с фиксированным весом **5**.- Оставь диагональ с большей суммой весов; при равенстве удали обе.- Результат: планарный граф.- Самопроверка: визуализируй граф для `smw_boo_input.png` и сравни со схемой Figure 3(c) статьи.### Шаг 4. Перестройка пиксельных ячеек (§3.2, конец)- Построй **упрощённую обобщённую диаграмму Вороного**: проход окном 3x3 по графу, подстановка готовых шаблонов формы ячеек (формы перечислимы, потому что граф планарен).- Координаты узлов ячеек квантованы к **четвертям пикселя**.- Схлопни узлы валентности 2 для упрощения диаграммы.- Самопроверка: похожие пиксели, соседние по диагонали, теперь делят общее ребро ячейки; сравни с Figure 3(d).### Шаг 5. Извлечение сплайнов (§3.3)- Ребро ячейки **видимое**, если цвета по его сторонам различны (критерий из шага 2).- Последовательности видимых рёбер, проходящие через узлы валентности 2 (по видимым рёбрам), преврати в **квадратичные B-сплайны**; контрольные точки — узлы ячеек.- T-стыки (три видимых ребра в одном узле): классифицируй каждое ребро как «теневое» (shading), если YUV-расстояние цветов с двух сторон `<= 100/255`, иначе «контурное». Если теневое ровно одно — соедини два контурных в один сплайн. Иначе соедини пару рёбер с углом, ближайшим к 180 градусам. Конец третьей кривой спроецируй на продолжающуюся кривую (B-сплайн не интерполирует контрольные точки).- Самопроверка: отрисуй сплайны поверх сетки, сравни с Figure 3(e).### Шаг 6. Оптимизация кривых (§3.4)- Минимизируй сумму поузловых энергий `E = Es + Ep`: - `Es` — гладкость: интеграл `|kappa| ds` (модуль кривизны) по участку кривой, на который влияет узел; считай численно, сэмплированием с фиксированным шагом. - `Ep` — позиция: `||p - p_hat||^4`, где `p_hat` — исходное положение узла (четвёртая степень: свободно в малом радиусе, резкий штраф дальше).- Релаксация: случайный обход узлов; для каждого пробуй несколько случайных смещений в малом радиусе, оставляй лучшее. Несколько итераций. **Зафиксируй seed** — результат должен быть воспроизводим.- Углы: найди в перестроенном графе ячеек шаблоны углов (Figure 7, плюс их повороты и отражения) и исключи участки кривой между узлами шаблона из интеграла гладкости — намеренно острые углы сглаживать нельзя.- Допустимое упрощение: вместо гармонических отображений для узлов, не лежащих на сплайнах, — локальное усреднение соседей или пропуск перепозиционирования (выбор зафиксируй в NOTES.md).- Самопроверка: «лесенка» на длинных дугах исчезает, сравни с Figure 6(b)→(c).### Шаг 7. Рендеринг (§3.5)- Растеризуй результат в `(W*upscale) x (H*upscale)`.- Эталонный способ: цвет точки — взвешенное среднее цветов ячеек с усечёнными гауссовыми влияниями (`sigma = 1`, радиус 2 ячейки), размещёнными в центроидах ячеек; влияние **не распространяется через контурные сплайны** (edge-aware).- Допустимое упрощение: залить каждую область, ограниченную сплайнами, цветом её ячеек, с антиалиасингом по границам (суперсэмплинг не менее 4x).- **Запрещено**: nearest neighbor / bilinear / bicubic как итоговый способ масштабирования.### Шаг 8. Прогон и самопроверка- Прогони все файлы из `input/`.- Проверь: все выходные файлы существуют, размеры верны, повторный запуск даёт идентичный результат.- Сравни выходы с эталонами в статье: `smw_dolphin` — Figure 1, `smw_boo` — Figure 3(f), `invaders_03` — Figure 9, `sma_toad` — Figure 9, `win31_keyboard` — Figure 9, `smw2_yoshi_*` — Figure 9 (Yoshi).- Все отклонения от плана зафиксируй в `solution/NOTES.md`.### Критерии готовности- [ ] В `output/` лежит результат для каждого входного файла, размеры `(W,H) * upscale`.- [ ] Контуры гладкие, без «лесенки»; тонкие линии не разорваны; одиночные пиксели (глаза персонажей) не потеряны.- [ ] Язык реализации соответствует `config.txt`; запуск одной командой по `solution/README.md`.- [ ] Повторный запуск даёт байт-в-байт тот же результат.
Оценивать результаты я «пригласил» тоже три разных ИИ — Claude, Codex и Antigravity (Gemini).
Что за задача и почему она сложная
«Depixelizing Pixel Art» — это алгоритм, который восстанавливает намерение художника: где у спрайта непрерывный контур, где тонкая диагональная линия в один пиксель, а где одиночная точка-блик, которую нельзя терять.
Оценка на верность производилась по этим пяти этапам:
-
Граф похожести. 8-связность, сравнение пикселей в пространстве YUV с порогами из статьи (48/255 по яркости, 7/255 и 6/255 по цветности).
-
Разрешение диагоналей. В полносвязном блоке 2×2 надо выбрать одну из двух диагоналей по трём эвристикам (продолжение кривых, разреженные пиксели с окном 8×8, «острова» с весом 5); ничья — удалить обе.
-
Перестройка ячеек. Упрощённая обобщённая диаграмма Вороного, узлы квантуются к четвертям пикселя.
-
Сплайны. Видимые рёбра превращаются в квадратичные B-сплайны; отдельно обрабатываются T-стыки (теневые/контурные рёбра, порог 100/255, угол ближе к 180°).
-
Оптимизация кривых. Минимизация энергии «гладкость + позиция», при этом узловые шаблоны углов из сглаживания исключаются (углы должны остаться углами).
Вот как выглядит вход — 18×18 пикселей, на которые без увеличения и не взглянешь:

А вот эталон, к которому все стремились, — Figure 3(f) из статьи: гладкий контур, два глаза (правый — характерный «крючок»), намеренно «волнистый» розовый рот и мягкая тень.
А судьи кто? Главный тест всего бенчмарка
Оценка шла по 100-балльной шкале, пять категорий: контракт (25), верность алгоритму (30), визуальное качество (20), инженерное качество (15), процесс (10).
Но самое полезное в проверке оказалось почти тривиальным. Это тест на даунсэмплинг:
Взять выход 288×288, усреднить его обратно до 18×18 методом BOX, сравнить с оригиналом. Если средняя дельта около нуля — значит каждый блок 16×16 в выходе постоянен и выход практически полностью состоит из однородных блоков. А это и есть обычный nearest-neighbour, то есть депикселизации не было вообще.
import numpy as npfrom PIL import Imageup = 16src = Image.open("input/smw_boo_input.png").convert("RGB")out = Image.open("output/smw_boo_output.png").convert("RGB")ds = out.resize(src.size, Image.BOX) # свернуть выход обратно к 18×18d = np.abs(np.float32(src) - np.float32(ds)).mean()print(f"средняя дельта: {d:.1f}") # 10–25 — норма; 0.0 — это resize
Десять строк Python — и стало видно то, что не видно при беглом просмотре «красивых» исходников. Нормальное сглаживание «съедает» углы и даёт дельту в районе 10–25. Ноль означает, что модель просто растянула картинку.
|
Модель / агент |
Размер |
Δ к входу |
Пикселей ≠ NN |
Что это значит |
|---|---|---|---|---|
|
Claude Fable 5 |
288² |
24.2 |
39.98% |
норма, есть сглаживание |
|
Codex 5.5 |
288² |
40.2 → 21.3 при v-flip |
47.17% |
обработка есть, но кадр перевёрнут |
|
Cursor Auto |
288² |
27.8 |
25.77% |
обработка есть, но рендер битый |
|
Kimi Code 2.7 |
288² |
42.7 |
31.24% |
обработка есть (рендер по Вороному) сплайны в рендере не участвуют |
|
Claude Sonnet 4.6 |
288² |
19.0 |
13.55% |
норма, есть сглаживание |
|
Antigravity (Gemini 3.5 Flash) |
288² |
0.0 |
0.00% |
чистый nearest ×16 |
|
Cline + DeepSeek v4 Pro |
288² |
0.0 |
0.00% |
чистый nearest ×16 |
|
Cline + Qwen 3.7 Max |
288² |
0.0 |
0.00% |
nearest ×16 |
|
Grok |
4608² |
0.0 |
1.85% |
nearest ×256 — и неверный размер |
Все агенты Интересные особенности и впечатления:
-
Fable — идеальный результат (его разберем далее)
-
Codex честно выполнил все инструкции и получил перевернутое изображение (помню, когда я реализовывал этот алгоритм, также перевернутое изображение)
-
Kimi Code 2.7 высадил 5-часовой лимит за один промпт, застрял в петле из постоянных улучшений
-
Antigravity — отправился гуглить реализацию на питоне, открыл браузер и начал серфить
-
DeepSeek v4 Pro — прогон стоил всего $0,08 на токены
Общий зачёт (Оценка ИИ)
Ниже — сводка. Поскольку оценивали три разных ИИ-судьи (Claude, Codex, Antigravity/Gemini), я привожу баллы каждого: расхождения сами по себе любопытны (об этом — в разделе про судей).
|
|
Модель / агент |
Claude |
Codex |
Gemini |
Вердикт |
|---|---|---|---|---|---|
|
🥇 |
Claude Fable 5 |
97 |
97 |
100 |
Отлично |
|
🥈 |
Sonnet 4.6 |
81 |
85 |
88 |
Хорошо |
|
🥉 |
Codex 5.5 |
85 |
83 |
84 |
Хорошо (кадр перевёрнут) |
|
4 |
Kimi Code 2.7 |
76 |
78 |
80 |
Хорошо — зачтено (рендер по Вороному) |
|
5 |
Cursor Auto |
68 |
66 |
81.5 |
Удовлетворительно |
|
— |
Antigravity (Gemini 3.5 Flash) |
— |
44 |
47 |
Не зачтено (nearest) |
|
— |
Cline + DeepSeek v4 Pro |
— |
53 |
47 |
Не зачтено (nearest) |
|
— |
Cline + Qwen 3.7 Max |
— |
40 |
42 |
Не зачтено (nearest) |
|
— |
Grok |
— |
38 |
19 |
Не зачтено (nearest ×256) |
А вот как это выглядит глазами — та самая картинка, ради которой стоит читать статью. Восемь результатов одного и того же призрака Boo:

Разница видна невооружённым глазом
Мой разбор призёров
Claude Fable — Однозначный лидер
Единственная реализация, которая одновременно и хорошо выглядит, и почти буквально покрывает все пять этапов алгоритма. Корректные YUV-пороги, все три эвристики диагоналей, четверть-пиксельные ячейки, квадратичные B-сплайны, полная логика T-стыков, шаблоны углов из Figure 7
Отдельно порадовала инженерия: фиксированный seed (SplitMix64), собственный PNG-кодер ради байт-в-байт воспроизводимости (два запуска → идентичный SHA-256), многопоточный рендер и режим --debug, выгружающий промежуточные этапы. Эти выгрузки можно прямо сопоставить со статьёй:


2. Перестроенные ячейки Вороного, узлы квантованы к ¼ пикселя (Figure 3d).

3. Сплайны на видимых рёбрах (Figure 3e): «лесенка» становится кривыми.

4. Финальный рендер — практически эталон.
Codex — моё второе место
По исходникам это одна из самых полных реализаций: B1–B5 разнесены по модулям, есть self-test графа, T-стыки с проекцией по 32 точкам сплайна, защита тонких компонент, гауссов рендер и три диагностических картинки.
финальный PNG перевёрнут по вертикали (рассогласование «начала отсчёта Y» между чтением и записью буфера) — поэтому все «ИИ-судьи» снизили баллы
Claude Sonnet
Результат: контур получился заметно гранёным — сплайны не убрали «лесенку», а превратили её в многоугольные фасетки; глаза стали ромбовидными, рот потерял волну. Плюс из-за ошибок округления на границах полигонов появились микро-зазоры (полупрозрачные пиксели). Цветовая верность при этом хорошая.
Kimi Code 2.7 —маньяк перфекционист
Мой приз зрительских симпатий, модель действительно старалась, когда я увидел, что потрачено 20% 5-часового лимита — я улыбнулся, 50% — я уже ждал «ошеломляющий» результат, и дальше с упоением наблюдал как заполняется 100%
Результат оказался далёк от идеала, но именно Kimi показал самое интересное агентное поведение: вместо решения задачи он начал бесконечно улучшать собственное решение и в итоге сжёг весь доступный лимит.
Выводы
-
Claude Fable — чистый победитель. Единственный результат уровня эталона статьи, ещё и с диагностикой этапов и байт-в-байт воспроизводимостью. Пример когда модель понимает задачу, а не имитирует понимание
-
Главный вывод: для AI-агентов нужен численный критерий. Это вероятностные системы, и статическое ревью «красивого кода» не показывает, работает ли алгоритм на самом деле.
-
Много кода — не всегда работающий результат. Antigravity и Cline+DeepSeek написали полноценный каркас алгоритма с графами и сплайнами — и из-за одной логической ошибки (схлопывание коллинеарных вершин) всё выродилось в просто ресайз. Сквозные smoke-тесты на синтетике обязательны прямо в цикле генерации.
-
LLM-судьи воспроизводимы ровно настолько, насколько объективны критерии. По числам — консенсус, по «вкусу» — разброс в 15 баллов. Это работающий рецепт для любого, кто строит автоматическую оценку на LLM.
Если кто-то захочет прогнать этот же тест на Kotlin, Rust, C#, Zig или любом другом языке — вот репозиторий с полным промптом, исходными артефактами и структурой, которую я использовал для эксперимента.
ссылка на оригинал статьи https://habr.com/ru/articles/1051630/