
Лето, скоро отпуск — захотелось написать статью, которую просто кайф прочитать, и заодно попробовать что‑то новое. Для ИИ есть бенчмарки вроде 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 np from PIL import Image up = 16 src = 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×18 d = 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 или любом другом языке — вот репозиторий с полным промптом, исходными артефактами и структурой, которую я использовал для эксперимента.
