[Перевод] QOI: как сжимать изображения в 20 раз быстрее STBI и без потерь

У представленного месяц назад формата сжатия изображений QOI уже есть реализации на различных языках, плагины для GIMP, Xn View MP и Paint.NET, а также dll для отображения эскизов в Проводнике Windows. Можно скачать изображение и сразу посмотреть на него здесь. Подробности о qoi от автора формата читайте под катом.

Представляем QOI — Quite OK Image Format. Он без потерь сжимает RGB- и RGBA-изображения до размера PNG, обеспечивая при этом ускорение в 20–50 раз при сжатии и ускорение в 3–4 раза при распаковке. Всё однопоточно, без SIMD. И до глупости просто.

В начале я хочу сказать, что не имею ни малейшего представления о том, что делаю. Я не любитель компрессии и едва понимаю, как работает кодирование Хаффмана и DCT. К счастью, QOI не использует ни то, ни другое. Я просто прорабатывал некоторые идеи, которые, как я подумал, могли бы помочь сжимать изображения. Результат меня весьма удивил.

Ради чего? Краткая тирада

Форматы файлов ужасны. Я совершенно не люблю PNG, JPEG или, ещё хуже, MPEG, MOV, MP4. Они от сложности рвутся по швам. Абсолютно каждая мелочь в них кричит: «Дизайн консорциума!».

Некоторое время назад я немного побаловался с MPEG. Основные идеи сжатия видео в MPEG гениальны, а ещё более гениальны они для 1993 года, но получившийся формат файла — мерзость.

Я представляю себе заседание группы экспертов по движущимся изображениям, где какой-то случайный иск потребовал, чтобы у формата был способ указать, что видеопоток защищён авторским правом. И битовый флаг copyright вошёл в стандарт и успешно остановил пиратство в кино ещё до его начала.

Промышленный стандарт MPEG задуман тридцать лет назад, все патенты давно истекли, а профессиональный интерес — потерян. Но священная спецификация, названная ISO/IEC 11172–2, — это хорошо охраняемый секрет, раскрываемый только тем, кто выложит 200 долларов, чтобы спонсировать ISO.

Есть альтернативные, открытые видеокодеки, но они опять же кошмарно сложные. Они конкурируют с последними технологиями, требуют огромных библиотек, требовательных к вычислениям и сложных в работе. Альтернативы PNG конкурируют между собой по степени сжатия, при этом сложность их постоянно возрастает.

Безусловно, есть рынок кодеков для видео, аудио и изображений, которые разменяли степень сжатия на скорость и простоту, но никто не служит ему. Эти ребята могли бы, но там всё запатентовано.

Да, stb_image избавил всех нас от мучений с libpng и поэтому используется в бесчисленных играх и приложениях. Какое-то время назад я пытался сделать то же самое для видео с помощью pl_mpeg и добился некоторого успеха.

Но со всем тем, чему я научился, почему бы не отойти на шаг назад и не реализовать простую схему сжатия, способную конкурировать с PNG, но без сотрясений? Или почему бы не реализовать простую схему сжатия видео, похожую на MPEG, но в нормальном формате?

И я возился со всем этим, хотел взять элементы MPEG-1 и упростить парсинг, ускорить его на графическом процессоре, но наткнулся на решение в смысле формата изображения без потерь, иногда способного конкурировать с PNG. Степень сжатия немного хуже, но формат проще.

Технические детали

Cпецификация формата файла QOI обновлена. Пожалуйста, ознакомьтесь с ней на qoiformat.org. QOI кодирует и декодирует изображения за один проход, при этом он проходит через каждый пиксель только один раз. Пиксель кодируется разными способами.

Результирующие значения упаковываются в блоки, которые начинаются с 2–4-битного тега (указывающего один из этих четырёх методов), за которым следует ряд битов данных. Все эти блоки (биты тегов и данных) выровнены по байтам, поэтому между блоками не нужно выкручиваться с битами.

Вот четыре метода сжатия:

1. Пробег по пикселям

Если текущий пиксель точно такой же, как и предыдущий, длина пробега увеличивается на 1. При обнаружении пикселя, который отличается от предыдущего, длина пробега сохраняется в закодированных данных, а текущий пиксель упаковывается одним из трёх других методов. Фрагмент длины пробега имеет два варианта, вариант зависит от количества необходимых битов:

QOI_RUN_8 {
    u8 tag  :  3;   // b010
    u8 run  :  5;   // 5-bit run-length repeating the previous pixel: 1..32
}
QOI_RUN_16 {
u8 tag  :  3;   // b011
u16 run : 13;   // 13-bit run-length repeating the previous pixel: 33..8224
}

2. Индекс в ранее увиденном пикселе

Энкодер сохраняет массив пробега из 64 пикселей, с которыми сталкивался ранее. Обнаружив, что текущий пиксель присутствует в этом массиве, программа сохраняет индекс в этом массиве в поток. Чтобы кодирование занимало время O (n), в этом массиве выполняется только один поиск.

Позиция поиска определяется «хешем» значения rgba (на самом деле просто r^g^b^a). Линейный поиск или бухгалтерия сложнее не слишком улучшит сжатие, но немного замедлит работу.

QOI_INDEX {
u8 tag  :  2;   // b00
u8 idx  :  6;   // 6-bit index into the color index array: 0..63
}

3. Разница с предыдущим пикселем

Когда текущий цвет пикселя не слишком далёк от предыдущего, разница с предыдущим пикселем сохраняется в потоке. Здесь есть три варианта, в зависимости от того, насколько велика разница. Основное внимание уделяется значению RGB; изменения альфа-канала обходятся дороже:

QOI_DIFF_8 {
u8 tag  :  2;   // b10
u8 dr   :  2;   // 2-bit   red channel difference: -1..2
u8 dg   :  2;   // 2-bit green channel difference: -1..2
u8 db   :  2;   // 2-bit  blue channel difference: -1..2
}
QOI_DIFF_16 {
u8 tag  :  3;   // b110
u8 dr   :  5;   // 5-bit   red channel difference: -15..16
u8 dg   :  4;   // 4-bit green channel difference:  -7.. 8
u8 db   :  4;   // 4-bit  blue channel difference:  -7.. 8
}
QOI_DIFF_24 {
u8 tag  :  4;   // b1110
u8 dr   :  5;   // 5-bit   red channel difference: -15..16
u8 dg   :  5;   // 5-bit green channel difference: -15..16
u8 db   :  5;   // 5-bit  blue channel difference: -15..16
u8 da   :  5;   // 5-bit alpha channel difference: -15..16
}

4. Полные значения rgba

Если все три предыдущих метода не подходят, значения r, g, b, a (но только те, которые отличаются от предыдущего пикселя) сохраняются в потоке в виде полных байтов:

QOI_COLOR {
u8 tag  :  4;   // b1111
u8 has_r:  1;   //   red byte follows
u8 has_g:  1;   // green byte follows
u8 has_b:  1;   //  blue byte follows
u8 has_a:  1;   // alpha byte follows
u8 r;           //   red value if has_r == 1: 0..255
u8 g;           // green value if has_g == 1: 0..255
u8 b;           //  blue value if has_b == 1: 0..255
u8 a;           // alpha value if has_a == 1: 0..255
}

Вот и всё. Если у вас есть минута, пожалуйста, прочитайте исходник qoi.h.

Что дальше?

Серьёзно, я ошеломлён. BMP и TIFF имеют кодировку длины пробега, GIF работает с LZW. Но что находится между ними? Ничего. Почему? Я обнаружил, что поле для работы между RLE и LZW достаточно велико, чтобы провести над ним не один день. Работа над QOI шла очень весело. Видеть, как каждое сделанное изменение повлияло на степень сжатия, — это захватывало.

Проработанный лучше, QOI может служить основой видеокодека без потерь для скринкастов и т.п. Ускорение SIMD для QOI также было бы круто, но (исходя из моих очень ограниченных знаний о некоторых инструкциях SIMD на ARM), формат, похоже, не очень хорошо подходит для этого. Может быть, кто-то с опытом больше может пролить на это свет?

Я также готов исследовать довольно обширное поле простого формата сжатия изображений с потерями. Многие схемы сжатия текстур реализуют очень интересные идеи, но нет ничего, что могло бы конкурировать с JPEG и быть проще.

На этом автор заканчивает свой пост. А научиться решать проблемы при помощи алгоритмов вы сможете на наших курсах:

Узнайте подробности акции.

Другие профессии и курсы

Data Science и Machine Learning

Python, веб-разработка

Мобильная разработка

Java и C#

От основ — в глубину

А также

© Habrahabr.ru