Нечестный обзор ИИ-агентов. Кто действительно смог реализовать Depixelizing Pixel Art?

от автора

Лето, скоро отпуск — захотелось написать статью, которую просто кайф прочитать, и заодно попробовать что‑то новое. Для ИИ есть бенчмарки вроде 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» — это алгоритм, который восстанавливает намерение художника: где у спрайта непрерывный контур, где тонкая диагональная линия в один пиксель, а где одиночная точка-блик, которую нельзя терять.

Figure 3 из статьи Kopf & Lischinski: (a) вход, (b) граф похожести, (c) граф после разрешения диагоналей, (d) перестроенные ячейки, (e) сплайны, (f) финальный рендер.

Figure 3 из статьи Kopf & Lischinski: (a) вход, (b) граф похожести, (c) граф после разрешения диагоналей, (d) перестроенные ячейки, (e) сплайны, (f) финальный рендер.

Оценка на верность производилась по этим пяти этапам:

  1. Граф похожести. 8-связность, сравнение пикселей в пространстве YUV с порогами из статьи (48/255 по яркости, 7/255 и 6/255 по цветности).

  2. Разрешение диагоналей. В полносвязном блоке 2×2 надо выбрать одну из двух диагоналей по трём эвристикам (продолжение кривых, разреженные пиксели с окном 8×8, «острова» с весом 5); ничья — удалить обе.

  3. Перестройка ячеек. Упрощённая обобщённая диаграмма Вороного, узлы квантуются к четвертям пикселя.

  4. Сплайны. Видимые рёбра превращаются в квадратичные B-сплайны; отдельно обрабатываются T-стыки (теневые/контурные рёбра, порог 100/255, угол ближе к 180°).

  5. Оптимизация кривых. Минимизация энергии «гладкость + позиция», при этом узловые шаблоны углов из сглаживания исключаются (углы должны остаться углами).

Входной спрайт smw_boo, 18×18 пикселей.

Входной спрайт smw_boo, 18×18 пикселей.

Вот как выглядит вход — 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:

Kimi Code 2.7

Kimi Code 2.7

Разница видна невооружённым глазом

Мой разбор призёров

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 показал самое интересное агентное поведение: вместо решения задачи он начал бесконечно улучшать собственное решение и в итоге сжёг весь доступный лимит.

Выводы

  1. Claude Fable — чистый победитель. Единственный результат уровня эталона статьи, ещё и с диагностикой этапов и байт-в-байт воспроизводимостью. Пример когда модель понимает задачу, а не имитирует понимание

  2. Главный вывод: для AI-агентов нужен численный критерий. Это вероятностные системы, и статическое ревью «красивого кода» не показывает, работает ли алгоритм на самом деле.

  3. Много кода — не всегда работающий результат. Antigravity и Cline+DeepSeek написали полноценный каркас алгоритма с графами и сплайнами — и из-за одной логической ошибки (схлопывание коллинеарных вершин) всё выродилось в просто ресайз. Сквозные smoke-тесты на синтетике обязательны прямо в цикле генерации.

  4. LLM-судьи воспроизводимы ровно настолько, насколько объективны критерии. По числам — консенсус, по «вкусу» — разброс в 15 баллов. Это работающий рецепт для любого, кто строит автоматическую оценку на LLM.

Если кто-то захочет прогнать этот же тест на Kotlin, Rust, C#, Zig или любом другом языке — вот репозиторий с полным промптом, исходными артефактами и структурой, которую я использовал для эксперимента.

ссылка на оригинал статьи https://habr.com/ru/articles/1051630/