Что я пробовал вначале. JPEG не помог. Шум тоже.
Кадр один: девочка у пруда, утки в траве, осенние деревья, 1536 × 1024. Я мял его обработкой и снова получал одно и то же. SynthID обнаружен. Картинка уже выглядела хуже. Проверка по-прежнему находила знак.
Сработал другой ход. Сгенерировать вторую версию сцены через ControlNet Canny, взять из неё полосу текстуры и подмешать в исходник. Ручная проверка этого файла вернула not detected. Трава изменилась. Утки остались утками.
Два разных следа
EXIF, текстовые поля PNG и C2PA хранят сведения о файле и его происхождении отдельно от пикселей. Пиксельный знак живёт в самом изображении. Файл можно пересохранить без метаданных и оставить такой знак читаемым.
У SynthID второй вариант. Google DeepMind описывает его как невидимый человеку водяной знак и говорит об устойчивости к обычным преобразованиям: обрезка, фильтры, сжатие с потерями. Меня интересовала граница этой заявленной устойчивости на конкретной картинке, не устройство знака вообще.
Проверял через OpenAI Verify. У сервиса разные результаты для поддерживаемых сигналов, включая C2PA и SynthID. Я смотрел SynthID. Пустые метаданные не заменяют отрицательный результат по пиксельному знаку.
not detected значит, что проверка не нашла поддерживаемый знак. Это не сертификат съёмки на камеру. Так отрицательный результат читает и OpenAI.
Картинку испортить легче, чем изменить ответ детектора
Первый инструмент был приземлённым. Небольшая геометрическая деформация, изменение размера, размытие, цветовые возмущения, шум, JPEG. Несколько проходов, если одного мало.
Идея казалась здравой: сигнал слабый, достаточно расшатать пиксели.
На этом кадре — недостаточно.
Дальше заходы тяжелее. GAN-реставрация с увеличением и обратным уменьшением. Растр. Имитация цепочки «напечатали — пересняли»: перспектива, муар, виньетка, зерно бумаги, повторное сжатие. Картинку так можно сделать совсем непривлекательной. Детектор от этого ещё не обязан сдаться.
Журнал короткий. Все строки — одна сцена и ручные проверки, не бенчмарк.
Обработка | Записанный результат SynthID |
|---|---|
Сильная классическая цепочка с шумом и JPEG | Обнаружен |
GAN-реставрация | Обнаружен |
Растр и имитация печати с пересъёмкой | Обнаружен |
Проверенные перерисовки с ControlNet Tile | Обнаружен |
Проверенные перерисовки с ControlNet Canny | Не обнаружен |
Полосовая замена с Canny-донором, keep 0.10 и 0.12 | Не обнаружен |
Из таблицы нельзя вывести, что JPEG или реставрация никогда не меняют результат SynthID. Можно перестать выдавать очередной фильтр за готовый способ. Для моего исходника эти попытки не сработали.
Почему донором стал Canny
В remaster.py я использовал Stable Diffusion 1.5 и ControlNet. У Tile в условие идёт изображение с его тональной структурой. В проверенных вариантах, включая сильную перерисовку, SynthID оставался обнаруживаемым.
Canny даёт другой вид условия: карту контуров. Через этот канал модель получает границы, а не цветовую картинку. Проверенные версии сцены уже возвращали not detected.
У объяснения есть предел. Исходный тайл всё ещё поступает в img2img. Canny не обнуляет информацию об исходнике, и смена условия не доказывает, каким путём знак держался в вариантах с Tile. Я поменял условие и увидел другой ответ. Внутренности детектора от этого не открылись.
Полная перерисовка вышла слишком заметной.
Композиция оставалась. Трава получала другой рисунок, мелкие детали ехали, некоторые поверхности становились живописнее, чем хотелось. Нужный ответ проверки можно купить ценой картинки, ради которой всё затевалось.
Тогда перерисованная версия стала донором. Теперь от неё нужна была только часть текстуры.
Заменить часть деталей и оставить остальное
У кадра есть детали разного масштаба. Тонкие линии и мелкое зерно. Более широкие переходы света и цвета. Между ними текстура, которую можно выделить разностью двух размытых копий.
Для изображения I Gaussian blur с параметром sigma — G_sigma(I). В bandmix.py полоса считается так:
B(I) = G_1.2(I) - G_8(I)
Та же операция выполняется над донором D. Его полоса не подмешивается как есть. match_local_energy умножает её на локальный коэффициент, чтобы сглаженная энергия донорской полосы следовала за энергией B(I). Окно этого сглаживания — --match-window. В финале оно равно 12 пикселям. Эту подогнанную полосу я обозначаю B_matched(D):
B_mix = k * B(I) + (1 - k) * B_matched(D) R = I - B(I) + B_mix
k — доля исходной полосы, которую оставляем. В финальном файле это 0.12. Остальные 88% смеси берутся из подогнанной донорской полосы. Исходник сохраняет две составляющие: I - G_1.2(I) и G_8(I). Они отвечают за более тонкие детали и широкие переходы тона.
Формула ещё не PNG. На выходе значения ограничиваются диапазоном канала и округляются. Эти операции тоже меняют изображение, поэтому обещать абсолютную неизменность деталей других масштабов нельзя.
С числами 1.2 и 8 тоже лучше не геройствовать. Это sigma двух Gaussian-фильтров в пикселях. У фильтров нет резкой частотной границы. Один кадр не рисует карту, где живёт любой SynthID. Я нашёл параметры полезной обработки для этой картинки.
Дальше я двигал долю исходной полосы.
При 10% проверка давала not detected. При 12% тоже. При 15% знак снова обнаруживался, и с подгонкой контраста, и без неё. Разница небольшая. На этом изображении она оказалась существенной.
Результат в пикселях
Оба файла лежат в репозитории целиком, не только уменьшенные превью.
Исходник | Результат |
|---|---|
Открыть исходник и результат в полном размере.
Величина | Значение |
|---|---|
Размер обоих изображений | 1536 × 1024 |
Средняя абсолютная разница значений каналов относительно исходника, шкала 0–255 | 16.91 |
PSNR относительно исходника | 20.68 dB |
Записанный ручной результат SynthID от 9 октября 2026 года | Не обнаружен |
Метрики пересчитаны по двум PNG без вызова детектора; их можно проверить по опубликованным файлам. Ответ SynthID в таблице — отдельное наблюдение от 9 октября. Наличие файлов на GitHub проверку заново не запускает.
Что видно человеку? Сцена узнаваема. Самые броские изменения сидят в густой растительности: траве, тростнике, переплетении тонких деталей. В маленьком превью это легко пропустить. В полном размере уже труднее.
Средняя разница 16.91 не становится «незаметной правкой» оттого, что она меньше ранних попыток. PSNR измеряет близость пикселей. Вкуса у него нет, и одним числом вид не гарантируется.
Результат ближе к исходной сцене, чем полная перерисовка. До невидимого изменения я не добрался.
Как устроен проект
Неудачные ветки лежат рядом с основной. Они нужны для сравнения и не притворяются взаимозаменяемыми решениями.
Файл | Роль |
|---|---|
Донор с ControlNet Canny, Tile или отключённым условием | |
Выделяет полосы, подгоняет локальную энергию и смешивает их | |
| Классическая цепочка, загрузка и сохранение, метрики |
GAN-реставрация с локальным checkpoint | |
Имитация печати и съёмки | |
Плавные тональные и цветовые возмущения | |
| Клиент API проверки происхождения, если доступ есть |
Тесты проверяют воспроизводимость классической цепочки, размеры, прозрачность, режимы изображения, метаданные, метрики и ошибки. Отдельные ML-тесты проверяют геометрию тайлов и режимы условия. Внешний детектор они не вызывают и модели не скачивают. Зелёный pytest не равен успешному результату SynthID.
Повторить обработку
Опубликованное окружение — Python 3.14. Лёгкая часть использует Pillow и NumPy. Генерация донора вынесена отдельно: PyTorch, Diffusers, ControlNet. Рабочий ML-прогон был на macOS 14 или новее, Apple Silicon, MPS. Нет MPS — код выбирает CPU. Ветки CUDA нет, и на CUDA этот проект не проверялся. Скорости того прогона от CPU ждать не стоит.
Установка из корня клонированного репозитория:
python3.14 -m venv .venv .venv/bin/python -m pip install -r requirements.txt python3.14 -m venv .venv-restore .venv-restore/bin/python -m pip install -r requirements-diffusion.txt mkdir -p candidates
Первый запуск генератора скачивает несколько гигабайт весов. Сами веса в Git не лежат.
Сначала донор. Здесь нынешний универсальный промпт проекта:
.venv-restore/bin/python remaster.py \ autumn-10x15-yellow-v2.png candidates/donor.png \ --controlnet lllyasviel/control_v11p_sd15_canny \ --control-mode canny --control-blur 0 --pre-blur 0 \ --controlnet-scale 1.0 --strength 0.9 --steps 30 \ --tile 640 --overlap 256
По умолчанию это a photograph, natural light, sharp focus, fine detail, realistic textures. Если сцене нужно более точное описание, его можно передать через --prompt.
Донор опубликованного успешного примера делался с описанием девочки в красной куртке, осеннего пруда, уток и деревьев. Команда выше создаёт новый вариант. Ему нужна собственная проверка. Нельзя заменить промпт и приписать новому файлу старый ответ детектора.
Затем полосы:
.venv/bin/python bandmix.py \ autumn-10x15-yellow-v2.png candidates/donor.png candidates/result.png \ --split-sigma 1.2 --correct 8 \ --keep-share 0.12 --match-window 12
Четыре параметра задают режим этого результата. Сдвиг полосы, порог, фаза, выравнивание и смешивание только яркости в примере не требуются. Остальное есть в --help.
Параметр | Что меняет |
|---|---|
| Sigma более узкого Gaussian blur в пикселях; резкой частотной границы нет |
| Sigma второго, более широкого blur; ноль заменяет всё, что грубее первого |
| Долю исходной полосы в смеси, от 0 до 1 |
| Sigma окна, в котором энергия донора подгоняется к исходной полосе; ноль отключает подгонку |
Для другой картинки заменяются входные пути в обеих командах. Размеры исходника и донора должны совпадать, иначе bandmix.py откажется писать результат. Один донор можно переиспользовать, пока перебираете параметры смеси. Десяти разным картинкам нужны десять доноров.
--overlap держите неотрицательным и меньше --tile. В команде выше это 256 и 640. Если у файла есть EXIF-ориентация, поверните пиксели сами до обработки: инструменты выбрасывают это поле и поворот не применяют.
Сравнивать удобнее PNG. Лишний JPEG добавит ещё один источник изменений. Потом файл проверяется отдельно. В репозитории есть API-клиент, но ему нужен доступ к соответствующему API. Сам факт наличия OpenAI-ключа такого доступа не обещает.
Чего этот кадр не доказывает
Серии независимых сцен у меня нет, долю удачных попыток считать не из чего. Нет доказательства, что похожей правки хватит для другой версии знака или другого проверяющего сервиса. Даже донор с тем же seed зависит от окружения, модели и настроек генерации. Совпадение на другой машине он не обещает.
Полосовая замена экономнее полной перерисовки, но материал картинки она всё равно меняет. Особенно там, где текстуры и так много. Следующий интересный шаг для меня — качество донорской текстуры и другие сцены. Ещё один шумовой фильтр из этого эксперимента не напрашивается.
Код под MIT: balyakin/synthid-remover. Если попробуете подход, полезнее всего набор из исходника, результата, параметров и ответа проверки.

