Если вы хоть раз сохраняли фото в формате JPEG, вы возможно знаете, насколько ужасно могут выглядеть джипеги. Десятилетия доминирования этого формата привели к тому, что у многих интернет-пользователей сформировалась стойкая аллергия к тому, что называется «артефакты JPEG».
![](https://habrastorage.org/getpro/habr/upload_files/d91/4a1/2ea/d914a12eaa35b5933a82e3ccf4cf7839.jpg)
Лувр. 480×245 пикселей, сильно сжатый JPEG
Без сомнения, никто бы не хотел делиться таким месивом пикселей с кем-то. Если бы вы спросили меня, что конкретно не так с этим изображением, я бы сказал, что в целом оно выглядит довольно близко к оригиналу: нет существенных искажений цветов, объекты узнаваемы. Однако я бы сказал, что тут есть две огромные проблемы:
- Мягкие градиенты выглядят как раздельные блоки или цветные полосы. Это особенно хорошо заметно на небе, но то же самое можно заметить и на земле.
- Есть паразитные цветовые градиенты вокруг четких объектов (взгляните на крышу) и мелких объектов. Такой эффект называется «звон».
Позвольте показать немного магии. Сейчас я применю эти два эффекта независимо друг от друга!
Оригинал | Звон | Цветовые полосы |
---|---|---|
![]() |
![]() |
![]() |
Итак, левая картинка — это ориентир с максимальным качеством. Центральное изображение сильно сжато, но на нем есть только артефакты звона, тогда как на правом изображении только цветовые полосы. Все три изображения — настоящие джипеги, созданные с помощью обычной библиотеки libjpeg, никакого фотошопа.
Оба изображения, со звоном и с цветовыми полосами, выглядят неприемлемо. Но тут есть один трюк, на самом деле я слегка увеличил размеры картинок на этой странице. То что вы видите — это картинка шириной 480 пикселей в сумме, растянутая до 600 CSS-пикселей на экране. Можно сказать, что это плотность пикселей 0.8x. Современные экраны часто имеют плотность пикселей 2x и даже больше. Для таких экранов привычной техникой является распространение изображений с плотностью пикселей 2x (относительно CSS-пикселей) с минимально возможным качеством.
Так что изменится, если мы попытаемся сделать то же самое с картинками с двойной плотностью пикселей на экране? Вот те же самые эффекты:
Оригинал | Звон | Цветовые полосы |
---|---|---|
![]() |
![]() |
![]() |
Признайтесь, вы подумали на секунду, что тут какая-то ошибка? Тут нет ошибки, центральное изображение действительно выглядит превосходно, почти как левое. В отличие от правого, которое выглядит так же ужасно, как это было при плотности пикселей 0.8x.
Просто потрясающе, что человеческий глаз не чувствителен к артефактам звона при маленьком масштабе. Если увеличить страницу, можно увидеть, что сильный звон всё еще присутствует, просто мы его не замечаем.
Постановка проблемы
То, что мы не видим звон — само по себе очень интересно, но какую пользу мы можем извлечь из этого факта? Ключ к пониманию выше. Для плотности пикселей 2x нам нужно выбирать «минимально возможное качество» (хотя это в принципе верно для любой плотности пикселей). Мы должны выбрать качество, которое не приведет к появлению значительных артефактов.
Проблема заключается в том, что с уменьшением качества, и звон, и цветовые полосы начинают появляться на изображении. На практике это означает, что чаще всего нам приходится выбирать качество, которое не приводит к появлению цветовых полос.
Но что если бы мы могли уменьшить качество ещё сильнее и это бы не приводило появлению цветовых полос?
Чтобы ответить на этот вопрос, нужно разобраться в том, как работает сжатие JPEG. Но не переживайте, описание будет сильно упрощенным.
Краткий экскурс в сжатие JPEG
Первое, что нужно знать — JPEG сжимает изображение небольшими блоками, размером 8x8 пикселей. Вы даже могли их заметить на любом примере сильно сжатых изображений выше. И да, границы цветовых полос совпадают с этими блоками.
Далее, кодек JPEG не сохраняет значения пикселей напрямую. Вместо этого, каждый блок сопоставляется с 64 возможными шаблонами частот, и коэффициенты для этих шаблонов сохраняются в файле. Подробнее об этом процессе вы можете прочитать в статье Потери качества JPEG (eng).
Взгляните на таблицу:
![](https://habrastorage.org/webt/fc/uj/gj/fcujgjpwlksdpnljuu5wcytl3vg.png)
Шаблоны частот для блока 8×8 пикселей
Каждая ячейка имеет до 7 переходов цвета сверху вниз и до 7 слева направо. Шаблоны первой строки не имеют перехода сверху вниз, шаблоны второй строки имеют один такой переход и так далее. То же самое для колонок и горизонтальных переходов.
Вы можете восстановить любой блок изображения, взяв каждый шаблон с определенным коэффициентом. Например, если все пиксели внутри блока имеют одинаковый цвет, вам нужен коэффициент только для первой ячейки, все остальные коэффициенты будут равны нулю. Для более сложных узоров нужны будут коэффициенты для нескольких шаблонов. Такое преобразование называется Дискретной Косинусной Трансформацией (DCT).
Вплоть до этого момента не происходит компрессии с потерями. Вы можете совершенно точно восстановить оригинальное изображение используя коэффициенты для DCT. Чтобы сократить реальное количество данных, которое хранится в файле, используются таблицы квантования. Таблица всего лишь говорит кодировщику, сколько данных нужно срезать с каждого коэффициента. Вот например базовая таблица квантования из библиотеки libjpeg:
static const unsigned int std_luminance_quant_tbl[DCTSIZE2] = {
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99
};
Первый элемент, 16, означает, что кодировщик должен поделить коэффициент для первого шаблона на 16 прежде чем сохранить в файл. Поэтому, вместо сохранения значения от 0 до 255 (8 бит), нужно будет хранить лишь значения от 0 до 15 (4 бита), что займет существенно меньше места.
Выходит, что чем больше значения в таблице квантования, тем более агрессивная компрессия применяется и тем ниже качество конечного JPEG файла.
Как работает параметр quality
Допустим, что так и есть, но откуда берется эта таблица при сохранении файла? Было бы ужасным усложнением, если бы для каждого файла нужно было бы придумать и передать кодировщику 64 независимых значения таблицы. Вместо этого большинство кодировщиков JPEG предоставляют простой интерфейс, который позволяет выставить все 64 значения одновременно. Это тот самый хорошо известный параметр quality, изменяемый от 0 до 100. Кодировщик получает от нас число и просто масштабирует значения в некой базовой таблице квантования. Чем выше заданное quality, тем меньше будут значения в конечной таблице квантования.
Теперь, когда мы знаем как работает компрессия JPEG, мы можем наконец сказать, что цветовые полосы — это всего лишь результат слишком сильного квантования низкочастотных коэффициентов, то есть слишком больших значений в верхнем левом углу таблицы квантования.
Хорошая новость заключается в том, что хотя большинство кодировщиков позволяют задать параметр quality, некоторые из них также позволяют задать произвольные таблицы квантования при сохранении. А ещё мы можем прочитать уже готовые таблицы из ранее сохраненных файлов. Это означает, что можно достать таблицу квантования кодировщика для определенного уровня quality и поменять её как нам нужно, не придумывая её с нуля:
from io import BytesIO
from PIL import Image
qtables_by_q = []
empty = Image.new('RGB', (8, 8))
for q in range(101):
with BytesIO() as buf:
empty.save(buf, format='JPEG', quality=q)
qtables = Image.open(buf).quantization
qtables_by_q.append(qtables)
Image.open('in.jpg').save('out.jpg', qtables=qtables_by_q[10])
Решение
Мы уже поняли, что нужно исправить низкочастотные значения в верхнем левом углу таблицы квантования. Но какие именно и как исправить?
После долгих экспериментов я пришел к выводу, что только самый первый элемент оказывает наибольшее влияние на цветовые полосы. Для теста я ограничил максимальное значение в таблице для канала яркости значением 10, а для цветовых каналов 16. Такие значения делают цветовые полосы едва ли различимыми для меня.
Я приведу несколько примеров, где левое изображение — минимальное приемлемое по моему мнению качество с таблицами квантованию по-умолчанию, изображение по центру — минимальное приемлемое качество с исправленными таблицами, а изображение справа — изображение с таблицами квантованию по-умолчанию, но максимально приближенные по размеру к исправленному варианту.
Сопоставимое качество | Исправление | Сопоставимый размер |
---|---|---|
![]() |
![]() |
![]() |
Q50, 23 Kb | Q15 + fix, 11 Kb | Q18, 11.1 Kb |
![]() |
![]() |
![]() |
Q50, 24.5 Kb | Q20 + fix, 14.6 Kb | Q22, 14.5 Kb |
![]() |
![]() |
![]() |
Q40, 24.5 Kb | Q20 + fix, 15.8 Kb | Q22, 15.8 Kb |
![]() |
![]() |
![]() |
Q25, 18.4 Kb | Q16 + fix, 15 Kb | Q19, 15.3 Kb |
![]() |
![]() |
![]() |
Q32, 22 Kb | Q24 + fix, 19.5 Kb | Q26, 19.4 Kb |
![]() |
![]() |
![]() |
Q30, 25.6 Kb | Q24 + fix, 24.1 Kb | Q27, 24 Kb |
![]() |
![]() |
|
Q11, 19.8 Kb | Q10 + fix, 20.3 Kb |
Очевидно, что результат отличается от картинки к картинке. В редких случаях размер с исправленной версией получается сопоставимый или даже слегка больше, чем с таблицами по-умолчанию. Тем не менее, нет сомнений, что паттерн, который страдает от излишних цветовых полос, часто встречается среди различных изображений. Без данного фикса приходится сильно задирать quality, чтобы избавиться от цветовых полос.
На практике, вы часто не можете подбирать значение quality для каждого изображения вручную. Приходится выбирать единое значение, которое бы не портило большинство обрабатываемых изображений. Без данного фикса, такое качество находится на уровне 50, в то время как с данным фиксом, quality можно уменьшить до 25. В среднем это дает 33% уменьшение размера файлов для 2x плотности пикселей, что является огромным выигрышем.
Текущий статус
С самого начала мы не давали нашим пользователям выставлять уровень quality в виде числа. На то было много причин: одно и то же число означает разное для разных форматов и даже кодеков. Число не дает выставить другие параметры компрессии, например субсэмплинг (eng) . И, возможно, самое важное, указание качества через число не совместимо с автовыбором формата, когда один и тот же URL возвращает изображения в разном формате для разных клиентов.
Вместо этого, мы предлагаем пользователям выбрать один из пяти уровней качества и ещё два «умных» уровня, когда качество для изображения подбирается индивидуально используя компьютерное зрение.
Сегодня мы ещё раз можем убедиться в правильности такого подхода.
Так как для двойной плотности пикселей мы уже отдавали quality ниже 50, сейчас мы остановились на том, что ограничили первый элемент в таблице квантования для всех JPEG файлов. Это привело к тому, что для уровней качества, рассчитанных на двойную плотность пикселей, размер файла мог увеличиться от 2% до 5%. Но зато мы полностью избавились от цветовых полос.
Открытие того, что пользователь по-разному воспринимает артефакты сжатия в зависимости от плотности пикселей очень важно. Это дает простор для дальнейшего уменьшения размера файлов без значительной визуальной потери качества. Я намерен и дальше экспериментировать c таблицами квантования, т.к. очевидно, что таблицы по-умолчанию были разработаны для одинарной плотности пикселей и не соответствуют современным требованиям.