Система - IBM:Об алгаритме сжатия Lempel-Ziw Welch и его реализации для формата GIF.

Odyssey Magazine #01
  Steve Blackstock
  Стиву Блэкстоку помог заговорить  по-
русски  сотрудник  Института прикладной
математики AH CCCP А.Самотохин


Мusic by Phantom Lord
        
          ОБЪЯСНЕНИЕ LZW И GIF


      Я надеюсь, что этот маленький доку-
мент  поможет  просветить  тех, кто хочет
знать  немного больше об алгоритме сжатия
Lempel-Ziv Welch и, конкретно, о его реа-
лизации для формата GIF.
     Перед  тем, как мы начнем, немного о
терминологии в свете данного документа:
  "Символ":  фундаментальный элемент дан-
ных.  В  обычных текстовых файлах это от-
дельный  байт.  В растровых изображениях,
которыми вы заинтересовались, это индекс,
который указывает цвет данного пиксела. Я
буду ссылаться на произвольный символ как
на "K".
  "Поток символов": поток символов такой,
как файл данных.
  "Цепочка":  несколько  последовательных
символов.  Длина цепочки может изменяться
от  1 до очень большого числа символов. Я
могу  указывать  произвольную цепочку как
"[...]K".
  "Префикс":  почти  то же самое, что це-
почка,  но  подразумевается,  что префикс
непосредственно  предшествует  символу, и
префикс может иметь нулевую длину. Я буду
ссылаться на произвольный префикс, как на
"[...]".
  "Корень":  односимвольная  цепочка. Для
большинства  целей  это просто символ, но
иногда  это может быть иначе. Это [...]K,
где [...] пуста.
  "Код":  число,  определяемое  известным
количеством  бит, которое кодирует цепоч-
ку.
  "Поток  кодов":  выходной  поток кодов,
таких как "растровые данные".
  "Элемент": код и его цепочка.
  "Таблица   цепочек":  список  элементов
обычно, но не обязательно, уникальных.
     Этого должно быть достаточно для по-
нимания документа.
     LZW  - это способ сжатия данных, ко-
торый извлекает преимущества при повторя-
ющихся цепочках данных. Поскольку растро-
вые данные обычно содержат довольно много
таких  повторений,  LZW  является хорошим
методом для их сжатия и раскрытия.
     В  данный  момент давайте рассмотрим
обычное кодирование и декодирование с по-
мощью  LZW-алгоритма.  В GIF используется
вариация этого алгоритма.
     При сжатии и раскрытии LZW манипули-
рует  тремя  объектами: потоком символов,
потоком  кодов  и  таблицей  цепочек. При
сжатии  поток символов является входным и
поток  кодов  -  выходным.  При раскрытии
входным  является  поток  кодов,  а поток
символов  - выходным. Таблица цепочек по-
рождается  и  при сжатии и при раскрытии,
однако  она никогда не передается от сжа-
тия к раскрытию и наоборот.
     Первой  вещью, которую мы делаем при
LZW-сжатии  является  инициализация нашей
цепочки  символов. Чтобы сделать это, нам
необходимо  выбрать  код  размера  (коли-
чество  бит)  и  знать  сколько возможных
значений  могут  принимать  наши символы.
Давайте положим код размера равным 12 би-
там, что означает возможность запоминания
0FFF, или 4096, элементов в нашей таблице
цепочек.  Давайте  также предположим, что
мы  имеем 32 возможных различных символа.
(Это  соответствует, например, картинке с
32  возможными цветами для каждого пиксе-
ла.)  Чтобы  инициализировать таблицу, мы
установим  соответствие  кода  #0 символу
#0, кода #1 to символу #1, и т.д., до ко-
да  #31  и  символа #31. На самом деле мы
указали,  что каждый код от 0 до 31 явля-
ется  корневым. Больше в таблице не будет
других кодов, обладающих этим свойством.
     Теперь  мы начнем сжатие данных. Да-
вайте сначала определим нечто, называемое
"текущим  префиксом". Этот префикс мы бу-
дем постоянно помнить и проводить сравне-
ние  с  ним  здесь и в дальнейшем. Я буду
обозначать  его  как  "[.c.]". Изначально
текущий  префикс  ничего не содержит. Да-
вайте  также определим также "текущую це-
почку",  которая  образуется текущим пре-
фиксом и следующим символом в потоке сим-
волов.  Я буду обозначать текущую цепочку
как "[.c.]K", где K - некоторый символ.
     Теперь посмотрите на первый символ в
потоке  символов.  Назовем его P. Сделаем
[.c.]P  текущей цепочкой. (В данной точке
это,  конечно, корень P.) Теперь выполним
поиск в таблице цепочек, чтобы определить
входит  ли  в нее [.c.]P. Конечно, сейчас
это  произойдет, поскольку в нашу таблицу
при  инициализации были помещены все кор-
ни.  В  этом  случае мы ничего не делаем.
Теперь делаем текущим префиксом [.c.]P.
     Берем  следующий  символ  из  потока
символом.  Назовем его Q. Добавим текущий
префикс,  чтобы сформировать [.c.]Q, т.е.
текущую цепочку. Выполняем поиск в табли-
це  цепочек, чтобы определить входит ли в
нее [.c.]Q. В данном случае этого, конеч-
но,  не  будет. Ага! Вот теперь нам нужно
кое-что  сделать. Добавим [.c.]Q (которая
в  данном случае есть PQ) в таблицу цепо-
чек  под  кодом  #32,  и  выведем код для
[.c.]  в поток кодов. Теперь начнем опять
с   текущего  префикса,  соответствующего
корню P. Продолжаем добавление символов к
[.c.],  чтобы сформировать [.c.]K, до тех
пор,  пока  мы  не  сможем найти [.c.]K в
таблице  цепочек.  Затем  выводим код для
[.c.]  и добавляем [.c.]K в таблицу цепо-
чек. На псевдо коде алгоритм будет описан
приблизительно так:
     [1] Инициализация таблицы цепочек;
     [2] [.c.] <- пусто;
     [3] K <- следующий символ в потоке
         символов;
     [4] Входит ли [.c.]K в таблицу це-
         почек? (да: [.c.] <- [.c.]K;
         go to [3];)
         (нет: добавить [.c.]K в табли-
         цу цепочек;
         вывести код для [.c.] в поток
         кодов;[.c.] <- K; go to [3];)

      Насколько это просто! Конечно, ког-
да мы выполняем шаг [3] и в входном пото-
ке  не остается больше символов, вы выво-
дите  код  для [.c.] и покидаете таблицу.
Все сделано.
      Хотите пример? Давайте предположим,
что   мы   имеем   4-символьный  алфавит:
A,B,C,D. Поток символов выглядит как ABA-
CABA. Давайте сожмем его. Сначала мы ини-
циализируем  нашу  таблицу цепочек: #0=A,
#1=B,  #2=C,  #3=D. Первый символ есть A,
который  входит в таблицу цепочек, следо-
вательно [.c.] становится равным A. Далее
мы берем AB, которая не входит в таблицу,
следовательно  мы  выводим  код  #0  (для
[.c.]),  и добавляем AB в таблицу цепочек
с  кодом  #4.  [.c.] становится равным B.
Далее  мы  берем  [.c.]A = BA, которая не
входит  в  таблицу цепочек, следовательно
выводим  код #1, и добавляем BA в таблицу
цепочек с кодом #5. [.c.] становится рав-
ным A. Далее мы берем AC, которая не вхо-
дит  в таблицу цепочек. Выводим код #0, и
добавляем  AC  в  таблицу цепочек с кодом
#6.  Теперь [.c.] равно C. Далее мы берем
[.c.]A = CA, которая не входит в таблицу.
Выводим #2 для C, и добавляем CA к табли-
це под кодом #7. Теперь [.c.]=A. Далее мы
берем  AB, которая ВХОДИТ в таблицу цепо-
чек,  следовательно [.c.] становится рав-
ным AB, и мы ищем ABA, которой нет в таб-
лице  цепочек, поэтому мы выводим код для
AB,  который  равен #4, и добавляем ABA в
таблицу цепочек под кодом #8. [.c.] равно
A. Мы не можем более взять символов, поэ-
тому мы выводим код #0 для A и заканчива-
ем.   Следовательно,  поток  кодов  равен
#0#1#0#2#4#0.
      Несколько  слов  (три) следует ска-
зать  об эффективности: используйте стра-
тегию  хеширования. Поиск в таблице цепо-
чек  может быть сопряжен со значительными
вычислениями  и  хеширование  значительно
снижает  эти  затраты. Обратите внимание,
что "прямое LZW" сжатие работает с риском
переполнения таблицы цепочек - получается
код,  который  не  может быть представлен
числом битов, ранее установленных для ко-
дов. Существует несколько путей для того,
чтобы  справиться  с этой проблемой и GIF
реализует  самый простой из них. Мы будем
делать также.
      Важным  моментом,  на который стоит
обратить внимание, является то, что в лю-
бой точке во время сжатия выполняется ус-
ловие: если [...]K входит в таблицу цепо-
чек,  то  [...]  тоже  входит  в нее. Это
обстоятельство  приводит  к  эффективному
методу  запоминания  цепочек  в  таблице.
Вместо  того,  чтобы запоминать в таблице
всю  цепочку, используйте тот факт, любая
цепочка  может быть представлена как пре-
фикс плюс символ: [...]K. Если вы вносите
[...]K  в  таблицу,  вы знаете, что [...]
уже  находится в ней, и поэтому вы можете
запомнить  код  для [...] плюс замыкающий
символ K.
      Это  все,  о чем следует заботиться
при  сжатии.  Раскрытие,  возможно  более
сложно  концептуально, однако программная
реализация его проще.
      Опишем  как  это делается. Мы опять
начинаем с инициализации таблицы цепочек.
Эта таблица образуется исходя из тех зна-
ний,  которыми мы располагаем о порождае-
мом  в конце концов потоке символов, нап-
ример,  о возможных значениях символов. В
GIF-файлах эта информация находится в за-
головке,  как  число  возможных  значений
пикселов.  Однако, прелесть LZW состоит в
том,  что  это все, что нам нужно. Сжатие
было  выполнено таким образом, что мы ни-
когда не встретим в потоке кодов код, ко-
торый  мы не могли бы преобразовать в це-
почку.
      Нам  необходимо  определить  нечто,
называемое "текущим кодом", на что мы бу-
дем ссылаться как "<code>", и "старым ко-
дом",  на  который  будем  ссылаться  как
"<old>".  Чтобы начать распаковку возьмем
первый  код. Теперь он становится <code>.
Этот  код  будет инициализировать таблицу
цепочек в качестве корневого. Выводим ко-
рень  в  поток  символов. Делаем этот код
старым кодом <old>.
     (*)  Теперь  берем  следующий  код и
присваиваем  его  <code>.  Возможно,  что
этот  код не входит в таблицу цепочек, но
давайте  пока  предположим,  что  он  там
есть.  Выводим  цепочку,  соответствующую
<code>  в  поток  символов. Теперь найдем
первый   символ  в  цепочке,  которую  вы
только что получили. Назовем его K. Доба-
вим его к префиксу [...], сгенерированно-
му  посредством <old>, чтобы получить но-
вую цепочку [...]K. Добавим эту цепочку в
таблицу  цепочек  и  установим старый код
<old> равным текущему коду <code>. Повто-
ряйте  от того места, которое я обозначил
звездочкой  и  вы  все сделаете. Прочтите
этот  абзац еще раз, если вы только "про-
бежались" по нему!!!
     Теперь давайте рассмотрим ту возмож-
ность, что <code> не входит в таблицу це-
почек. Вернемся обратно к сжатию и поста-
раемся  понять,  что  происходит, если во
входном  потоке  появляется  цепочка типа
P[...]P[...]PQ.  Предположим,  что P[...]
уже находится в таблице, а P[...]P - нет.
Кодировщик выполнит грамматический разбор
P[...],  и  обнаружит,  что  P[...]P  от-
сутствует в таблице. Это приведет к выво-
ду кода для P[...] и добавлению P[...]P в
таблицу цепочек. Затем он возьмет P[...]P
для  следующей  цепочки  и определит, что
P[...]P  есть в таблице и выдаст выходной
код   для  P[...]P,  если  окажется,  что
P[...]PQ в таблице отсутствует.
      Декодировщик  всегда  находится "на
один  шаг сзади" кодировщика. Когда деко-
дировщик  увидит  код  для P[...]P, он не
добавит  этот  код к своей таблице сразу,
поскольку   ему  нужен  начальный  символ
P[...]P для добавления к цепочке для пос-
леднего  кода  P[...], чтобы сформировать
код для P[...]P. Однако, когда декодиров-
щик  найдет код, который ему еще неизвес-
тен,  он всегда будет на 1 больше послед-
него  добавленного  к  таблице.  Следова-
тельно,  он  может догадаться что цепочка
для этого кода должна быть и, фактически,
всегда будет правильной.
     Если  я декодировщик, и я увидел код
#124, а моя таблица цепочек содержит пос-
ледний код только с #123, я могу считать,
что  код с #124 должен быть, добавить его
к моей таблице цепочек и вывести саму це-
почку.  Если код #123 генерирует цепочку,
на которую я сошлюсь здесь как на префикс
[...],  то  код #124 в этом особом случае
будет [...] плюс первый символ [...]. По-
этому  я  должен  добавить  первый символ
[...] к ней самой. Не так плохо.
     В  качестве  примера (довольно часто
встречающегося)  давайте предположим, что
мы  имеем растровое изображение в котором
первые три пиксела имеют одинаковый цвет.
Т.е.  мой  поток  символов выглядит как :
QQQ....  Для  определенности давайте ска-
жем,  что  мы  имеем  32  цвета и Q соот-
ветствует  цвету #12. Кодировщик сгенери-
рует  последовательность кодов 12,32,....
(если вы не поняли почему, возьмите мину-
ту,  чтобы понять.) Вспомним, что код #32
не  входит  в  начальную таблицу, которая
содержит  коды от #0 до #31. Декодировщик
увидит код #12 и транслирует его как цвет
Q.  Затем  он  увидит код #32, о значении
которого он пока не знает. Но если он по-
думает  о нем достаточно долго, он сможет
понять,  что QQ должно быть элементом #32
в  таблице и QQ должна быть следующей це-
почкой вывода.
     Таким  образом, псевдо код декодиро-
вания  можно  представить следующим обра-
зом:
     [1] Инициализация строки цепочек;
     [2] взять первый код: <code>;
     [3] вывести цепочку для <code> в
         поток символов;
     [4] <old> = <code>;
     [5] <code> <- следующий код в по-
         токе кодов;
     [6] существует ли <code> в табли-
         це цепочек? (да: вывод цепоч-
         ки для <code> в  поток симво-
         лов;[...]  <-  трансляция для
         <old>;K<- первый символ тран-
         сляции  для  <code>; добавить
         [...]K в таблицу цепочек;
         <old> <- <code>; )(нет: [...]
         <- трансляция для <old>; K
         <- первый символ [...]; вывод
         [...]K в поток символов и до-
         бавление  его к таблице цепо-
         чек; <old> <- <code> )
     [7] go to [5];

      Опять же, если вы обнаружите на ша-
ге  [5],  что  нет  больше  символов,  вы
должны  закончить. Вывод цепочек и нахож-
дение начальных символов в них ставят са-
ми  по  себе проблемы эффективности, но я
не  собираюсь здесь предлагать способы их
решения.  Половина  удовольствия от прог-
раммирования  состоит в разрешении подоб-
ных штук!
      А  теперь вариации GIF'а на эту те-
му. В части заголовка GIF-файла существу-
ет  поле,  называемое  в потоке растровых
данных  "кодом размера". Это весьма запу-
тывающее  название  для этого поля, но мы
должны с ним смириться. На самом деле это
"размер корня". Фактический размер (в би-
тах)  кодов сжатия в действительности из-
меняется в процессе сжатия/раскрытия, и я
буду  ссылаться  на  него  здесь,  как на
"размер сжатия".
     Начальная  таблица,  как обычно, со-
держит  коды  для  всех  корней,  но к ее
верхней части добавляются два специальных
кода.  Предположим,  мы имеем "размер ко-
да",  который обычно равен числу битов на
пиксел. Обозначим его N. Если число битов
на  пиксел равно 1, N должно равняться 2:
корни занимают ячейки #0 и #1 в начальной
таблице  и два специальных кода будут за-
нимать  ячейки #4 #5. В любом другом слу-
чае  N равно числу битов на пиксел, корни
занимают  ячейки  от  #0  до #(2**N-1), а
специальные  коды  равны (2**N) и (2**N +
1).
     Начальный  размер сжатия будет равен
N+1 биту на код. Если вы ведете кодирова-
ние,  вы  выводите  сначала  коды  длиной
(N+1)  бит  и, если вы ведете декодирова-
ние,  вы  выбираете  сначала (N+1) бит из
потока  кодов. В качестве специальных ко-
дов  используются:  <CC> или код очистки,
равный (2**N), и <EOI> или конец информа-
ции,  равный (2**N + 1). <CC> говорит ко-
дировщику,  что нужно снова инициализиро-
вать  таблицу  цепочек  и  переустановить
размер сжатия равным (N+1). <EOI> означа-
ет  что  кодов больше нет. Если вы ведете
кодирование  или декодирование, вы должны
начать добавление элементов в таблицу це-
почек  с <CC> + 2. Если вы ведете кодиро-
вание,  вам  следует  вывести  <CC> в ка-
честве самого первого кода, и затем опять
каждый раз, как только вы достигните кода
#4095  (шестнадцатиричное FFF), поскольку
GIF  не допускает размера сжатия большего
12  бит.  Если  вы  ведете раскрытие, вам
следует  реинициализировать  вашу таблицу
цепочек, как только вы обнаружите <CC>.
     Переменный  размер  сжатия  на самом
деле не доставляет особых хлопот. Если вы
ведете кодирование вы начинаете с размера
сжатия  в  (N+1)  битов, и, как только вы
выведете  код  (2**(размер сжатия)-1), вы
увеличиваете  размер  сжатия на один бит.
Следовательно, следующий код вашего выво-
да  будет  на  один бит длиннее. Помните,
что наибольший размер сжатия равен 12 би-
там, что соответствует коду 4095. Если вы
достигли этого предела, вы должны вывести
<CC>  в качестве следующего кода и начать
сначала. Если вы ведете декодирование, вы
должны  увеличить  ваш  размер сжатия КАК
ТОЛЬКО  ВЫ  запишите элемент #(2**(размер
сжатия) - 1) в таблицу цепочек. Следующий
код,  который вы ПРОЧИТАЕТЕ будет на один
бит  длиннее.  Не делайте ошибки, дожида-
ясь, пока вам будет нужно добавить к таб-
лице  код (2**размер сжатия). Вы уже про-
пустили бит из последнего кода.
     Упаковка   кодов   в  битовый  поток
растровых  данных также является потенци-
альным  камнем  преткновения для новичков
кодирования  и декодирования. Младший бит
кода должен совпадать с младшим доступным
битом  первого  доступного байта в потоке
кодов.  Например, если вы начали с 5-бит-
ного  кодов сжатия, и ваши три первых ко-
да,  скажем,  <abcde>,  <fghij>, <klmno>,
где  e,  j,  и o биты #0, ваш поток кодов
начнется как:

       byte#0: hijabcde
       byte#1: .klmnofg

      Таким  образом различие между обыч-
ным LZW и LZW для GIF заключаются в нали-
чии двух дополнительных специальных кодов
и  переменном размере сжатия. Если вы по-
няли  LZW,  и  вы поняли эти различия, вы
поняли все!
      В  качестве P.S. Вы могли заметить,
что  кодировщик  имеет  небольшую битовую
гибкость  во время сжатия. Я описал "жад-
ный" способ, выбирающий перед выводом ко-
да  настолько  много  символов, насколько
это возможно. Фактически такой способ яв-
ляется  стандартным  для LZW и дает в ре-
зультате  наилучшую степень сжатия. Одна-
ко,  нет никакого правила, которое запре-
щало  бы  вам  остановиться и вывести код
для текущего префикса, вне зависимости от
того, есть ли он уже в таблице или нет, и
добавить  эту цепочку плюс следующий сим-
вол в таблицу цепочек. Существуют различ-
ные  причины, чтобы пожелать это сделать,
особенно,  если  цепочка слишком длинна и
порождает трудности при хешировании. Если
вам это нужно, сделайте это.

       Надеюсь, это поможет вам.

__________________________________________                                                Steve Blackstock