JPEG, который можно посмотреть в блокноте

в 13:24, , рубрики: ASCII, jpeg, open source, Алгоритмы, ненормальное программирование, обработка изображений, Программирование
JPEG, который можно посмотреть в блокноте - 1

Если открыть произвольный JPEG-файл в блокноте, то можно увидеть лишь хаотичный набор символов. Отсюда вопрос: возможно ли закодировать изображение так, чтобы его было можно просмотреть не только обычным способом, но и в обычном блокноте, в виде ASCII-графики. Ответ положительный, если использовать максимальное сжатие:

  • Grayscale (только оттенки серого).

  • Обнулить все AC-коэффициенты, то есть весь блок 8x8 пикселей сделать одноцветным.

  • Задать максимальный шаг квантования DC-коэффициента - 255. Это ограничивает цвет всего 9 оттенками серого: 0, 32, 64, 96, 128, 160, 192, 224, 255.

Вот так:

JPEG, который можно посмотреть в блокноте - 2

Да, не очень впечатляет. Но, тем не менее, это все еще JPEG, каждый блок 8x8 которого закодирован одним ASCII-символом:

JPEG, который можно посмотреть в блокноте - 3

Практическое применение отсутствует. Просто забавно.

Минимум теории

Для каждого блока 8x8 пикселей выполняется дискретное косинусное преобразование, результатом которого является один DC- и 63 AC-коэффициента. DC-коэффициент может принимать значения от -1020 до 1020 (255*4). Для grayscale-изображения минимальному значению соответствует черный цвет, максимальному - белый. В случае, если весь блок полностью одноцветен, все 63 AC-коэффициента равны 0.

Затем коэффициенты квантуются, то есть делятся на некоторое число и округляются. Наибольший возможный шаг квантования - 255. Это максимальный уровень сжатия. Тогда DC-коэффициенты принимают значения от -4 до 4. При записи в файл используется дельта-кодирование: сохраняются разности квантованных коэффициентов текущего и предыдущего блоков. То есть, при шаге квантования 255, дельта-значения могут быть от -8 до 8. Например, если изображение начинается с двух белых блоков после которых идет черный (4, 4, -4), то последовательность будет записана как: 4, 0, -8.

Коэффициенты записываются с помощью кодов (деревьев) Хаффмана. Эти коды хранятся в самом JPEG-файле. Для серого изображения нужно 2 дерева - одно для DC, другое для AC. Для решаемой задачи деревья были построены специальным образом.

JPEG, который можно посмотреть в блокноте - 4

Декодирование

Битовый поток начинается с байта 0xA, то есть 00001010 в двоичном представлении. Это символ переноса строки, который необходим в самом начале, иначе ASCII-графика окажется на одной строке вместе с заголовком JPEG. Декодер при чтении этих битов проходит по дереву Хаффмана, каждый раз выбирая ветвь в зависимости от значения бита.

Чтение 3 битов 000 приводит к листу со значением 1. Это означает, что нужно прочитать 1 следующий бит 0, чтобы получить DC-коэффициент. Если первая цифра значения в двоичном представлении — 1, то оставляем как есть: DC = <значение>. Иначе преобразуем: DC = <значение>-2^<длина значения>+1. В нашем случае DC = 0 - 2^1 + 1 = -1. Обратите внимание, что если последовательность битов начинается на 011000, то значение листа равно нулю. В этом случае DC = 0.

Далее опять осуществляется проход по дереву Хаффмана, но уже для AC-коэффициентов. Биты 1010 приводят к листу со значением 0. Как можно видеть, других значений в этом дереве нет. Ноль означает, что нужно прекратить чтение AC-коэффициентов и обнулить оставшиеся. Поэтому в нашем случае все AC-коэффициенты равны нулю и в этом блоке и во всех других.

Весь остальной поток читается аналогично, но, как уже было сказано: начиная со второго блока читаются не сами DC-коэффициенты, а разности (дельты) между коэффициентом текущего и предыдущего блоков. Используемое дерево Хаффмана для DC-коэффициентов позволяет закодировать максимум 3-битные дельты — от -7 до 7. Значит крайние значения DC должны различаться не более чем 7. Например, от -3 до 4 (то есть совсем черный цвет уже не получится). Добавить 4-битный код у меня нормально не получилось.

Все встречаемые символы приведены в следующей таблице.

HEX ASCII  DC       AC    Дельта   
60  `     [011000   00]     0     

4C  L     [0100 1  100]    +1     
44  D     [0100 0  100]    -1     

58  X     [0101 10  00]    +2     
54  T     [0101 01  00]    -2     

5C       [0101 11  00]    +3     
50  P     [0101 00  00]    -3     

30  0     [001 100  00]    +4     
2D  -     [001 011  01]    -4     

34  4     [001 101  00]    +5     
28  (     [001 010  00]    -5     

38  8     [001 110  00]    +6     
24  $     [001 001  00]    -6     

3C  <     [001 111  00]    +7     
21  !     [001 000  01]    -7     

10  LF    [000 0  1010]     1  

Было бы хорошо, например, белый блок кодировать пробелом. Но из-за дельта-кодирования мы можем лишь сопоставить символам разность цветов соседних блоков. Это тоже неплохо, так как разность цветов позволяет четко выделить границы объектов на изображении. Тогда почему бы не использовать пробел для одноцветных блоков? У него неудобное двоичное представление 00100000 из-за которого у меня не получилось использовать 3-битные дельты. Пришлось искать какой-нибудь альтернативный символ. Самый малозаметный - обратный апостроф ( ` ).

Символ переноса строки не только "рисует" мусорный столбец на изображении, но уменьшает дельту на 1. Поэтому после него добавляется символ L, восстанавливающий дельту.

Все это реализовано с помощью простейшей программы на Питоне (https://github.com/FilippVolodin/jpgtxt), позволяющей генерировать jpg-файл в описанном формате по любому исходному изображению.

Пример. Можно скачать и проверить.

JPEG, который можно посмотреть в блокноте - 5

Автор: Филипп Володин

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js